[
  {
    "path": ".devcontainer/Dockerfile",
    "content": "# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.148.1/containers/python-3/.devcontainer/base.Dockerfile\n\n# [Choice] Python version: 3, 3.9, 3.8, 3.7, 3.6\nARG VARIANT=\"3\"\nFROM mcr.microsoft.com/devcontainers/python:0-${VARIANT}\n\n# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image.\n# COPY requirements.txt /tmp/pip-tmp/\n# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \\\n#    && rm -rf /tmp/pip-tmp\n\n# [Optional] Uncomment this section to install additional OS packages.\n# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \\\n#     && apt-get -y install --no-install-recommends <your-package-list-here>\n\n# [Optional] Uncomment this line to install global node packages.\n# RUN su vscode -c \"source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>\" 2>&1\n"
  },
  {
    "path": ".devcontainer/devcontainer.json",
    "content": "{\n\t\"name\": \"Python 3\",\n\t\"build\": {\n\t\t\"dockerfile\": \"Dockerfile\",\n\t\t\"context\": \"..\",\n\t\t\"args\": { \n\t\t\t// Update 'VARIANT' to pick a Python version: 3, 3.6, 3.7, 3.8, 3.9\n\t\t\t\"VARIANT\": \"3\"\n\t\t}\n\t},\n\n\t// To give the container access to a device serial port, you can uncomment one of the following lines.\n\t// Note: If running on Windows, you will have to do some additional steps:\n\t// https://stackoverflow.com/questions/68527888/how-can-i-use-a-usb-com-port-inside-of-a-vscode-development-container\n\t//\n\t// You can explicitly just forward the port you want to connect to. Replace `/dev/ttyACM0` with the serial port for\n\t// your device. This will only work if the device is plugged in from the start without reconnecting. Adding the\n\t// `dialout` group is needed if read/write permisions for the port are limitted to the dialout user.\n\t// \"runArgs\": [\"--device=/dev/ttyACM0\", \"--group-add\", \"dialout\"],\n\t//\n\t// Alternatively, you can give more comprehensive access to the host system. This will expose all the host devices to\n\t// the container. Adding the `dialout` group is needed if read/write permisions for the port are limitted to the\n\t// dialout user. This could allow the container to modify unrelated serial devices, which would be a similar level of\n\t// risk to running the build directly on the host.\n\t// \"runArgs\": [\"--privileged\", \"-v\", \"/dev/bus/usb:/dev/bus/usb\", \"--group-add\", \"dialout\"],\n\n\t\"customizations\": {\n\t\t\"vscode\": {\n\t\t\t\"settings\": { \n\t\t\t\t\"terminal.integrated.shell.linux\": \"/bin/bash\",\n\t\t\t\t\"python.pythonPath\": \"/usr/local/bin/python\",\n\t\t\t\t\"python.linting.enabled\": true,\n\t\t\t\t\"python.linting.pylintEnabled\": true,\n\t\t\t\t\"python.formatting.autopep8Path\": \"/usr/local/py-utils/bin/autopep8\",\n\t\t\t\t\"python.formatting.blackPath\": \"/usr/local/py-utils/bin/black\",\n\t\t\t\t\"python.formatting.yapfPath\": \"/usr/local/py-utils/bin/yapf\",\n\t\t\t\t\"python.linting.banditPath\": \"/usr/local/py-utils/bin/bandit\",\n\t\t\t\t\"python.linting.flake8Path\": \"/usr/local/py-utils/bin/flake8\",\n\t\t\t\t\"python.linting.mypyPath\": \"/usr/local/py-utils/bin/mypy\",\n\t\t\t\t\"python.linting.pycodestylePath\": \"/usr/local/py-utils/bin/pycodestyle\",\n\t\t\t\t\"python.linting.pydocstylePath\": \"/usr/local/py-utils/bin/pydocstyle\",\n\t\t\t\t\"python.linting.pylintPath\": \"/usr/local/py-utils/bin/pylint\"\n\t\t\t},\n\t\t\t\"extensions\": [\n\t\t\t\t\"ms-python.python\",\n\t\t\t\t\"platformio.platformio-ide\"\n\t\t\t]\n\t\t}\n\t},\n\n\t// Use 'forwardPorts' to make a list of ports inside the container available locally.\n\t// \"forwardPorts\": [],\n\n\t// Use 'postCreateCommand' to run commands after the container is created.\n\t\"postCreateCommand\": \"bash -i -c 'nvm install && npm ci'\",\n\n\t// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.\n\t\"remoteUser\": \"vscode\"\n}\n\n"
  },
  {
    "path": ".envrc",
    "content": "layout python-venv python3\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: [DedeHai,lost-hope,willmmiles,netmindz,softhack007]\ncustom: ['https://paypal.me/Aircoookie','https://paypal.me/blazoncek']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug.yml",
    "content": "name: Bug Report\ndescription: File a bug report\nlabels: [\"bug\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Please quickly search existing issues first before submitting a bug.\n  - type: textarea\n    id: what-happened\n    attributes:\n      label: What happened?\n      description: A clear and concise description of what the bug is.\n      placeholder: Tell us what the problem is.\n    validations:\n      required: true\n  - type: textarea\n    id: how-to-reproduce\n    attributes:\n      label: To Reproduce Bug\n      description: Steps to reproduce the behavior, if consistently possible.\n      placeholder: Tell us how to make the bug appear.\n    validations:\n      required: true\n  - type: textarea\n    id: expected-behavior\n    attributes:\n      label: Expected Behavior\n      description: A clear and concise description of what you expected to happen.\n      placeholder: Tell us what you expected to happen.\n    validations:\n      required: true\n  - type: dropdown\n    id: install_format\n    attributes:\n      label: Install Method\n      description: How did you install WLED?\n      options:\n        - Binary from WLED.me\n        - Self-Compiled\n    validations:\n      required: true\n  - type: input\n    id: version\n    attributes:\n      label: What version of WLED?\n      description: |-\n        Find this by going to <kbd><samp>⚙️ Config</samp></kbd> → <kbd><samp>Security & Updates</samp></kbd> → Scroll to Bottom.\n        Copy and paste the rest of the line that begins “<samp>Installed version: </samp>”,\n        or, for older versions, the entire line after “<samp>Server message</samp>”.\n      placeholder: \"e.g. WLED 0.13.1 (build 2203150)\"\n    validations:\n      required: true\n  - type: dropdown\n    id: Board\n    attributes:\n      label: Which microcontroller/board are you seeing the problem on?\n      multiple: true\n      options:\n        - ESP8266\n        - ESP32\n        - ESP32-S3\n        - ESP32-S2\n        - ESP32-C3\n        - Other\n        - ESP32-C6 (experimental)\n        - ESP32-C5 (experimental)\n    validations:\n      required: true\n  - type: textarea\n    id: logs\n    attributes:\n      label: Relevant log/trace output\n      description: Please copy and paste any relevant log output if you have it. This will be automatically formatted into code, so no need for backticks.\n      render: shell\n  - type: textarea\n    attributes:\n      label: Anything else?\n      description: |\n        Links? References? Anything that will give us more context about the issue you are encountering!\n        Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.\n    validations:\n      required: false\n  - type: checkboxes\n    id: terms\n    attributes:\n      label: Code of Conduct\n      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)\n      options:\n        - label: I agree to follow this project's Code of Conduct\n          required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: WLED Discord community\n    url: https://discord.gg/KuqP7NE\n    about: Please ask and answer questions and discuss setup issues here!\n  - name: WLED community forum\n    url: https://wled.discourse.group/\n    about: For issues and ideas that might need longer discussion.\n  - name: kno.wled.ge base\n    url:  https://kno.wled.ge/basics/faq/\n    about: Take a look at the frequently asked questions and documentation, perhaps your question is already answered!"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an improvement idea for WLED!\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n\nThank you for your ideas for making WLED better!\n"
  },
  {
    "path": ".github/copilot-instructions.md",
    "content": "# WLED - ESP32/ESP8266 LED Controller Firmware\n\nWLED 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.\n\nAlways reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.\n\n## Working Effectively\n\n### Initial Setup\n- Install Node.js 20+ (specified in `.nvmrc`): Check your version with `node --version`\n- Install dependencies: `npm ci` (takes ~5 seconds)\n- Install PlatformIO for hardware builds: `pip install -r requirements.txt` (takes ~60 seconds)\n\n### Build and Test Workflow\n- **ALWAYS build web UI first**: `npm run build` -- takes 3 seconds. NEVER CANCEL.\n- **Run tests**: `npm test` -- takes 40 seconds. NEVER CANCEL. Set timeout to 2+ minutes.\n- **Development mode**: `npm run dev` -- monitors file changes and auto-rebuilds web UI\n- **Hardware firmware build**: `pio run -e [environment]` -- takes 15+ minutes. NEVER CANCEL. Set timeout to 30+ minutes.\n\n### Build Process Details\nThe build has two main phases:\n1. **Web UI Generation** (`npm run build`):\n   - Processes files in `wled00/data/` (HTML, CSS, JS)\n   - Minifies and compresses web content \n   - Generates `wled00/html_*.h` files with embedded web content\n   - **CRITICAL**: Must be done before any hardware build\n\n2. **Hardware Compilation** (`pio run`):\n   - Compiles C++ firmware for various ESP32/ESP8266 targets\n   - Common environments: `nodemcuv2`, `esp32dev`, `esp8266_2m`\n   - List all targets: `pio run --list-targets`\n\n## Before Finishing Work\n\n**CRITICAL: You MUST complete ALL of these steps before marking your work as complete:**\n\n1. **Run the test suite**: `npm test` -- Set timeout to 2+ minutes. NEVER CANCEL.\n   - All tests MUST pass\n   - If tests fail, fix the issue before proceeding\n\n2. **Build at least one hardware environment**: `pio run -e esp32dev` -- Set timeout to 30+ minutes. NEVER CANCEL.\n   - Choose `esp32dev` as it's a common, representative environment\n   - See \"Hardware Compilation\" section above for the full list of common environments\n   - The build MUST complete successfully without errors\n   - If the build fails, fix the issue before proceeding\n   - **DO NOT skip this step** - it validates that firmware compiles with your changes\n\n3. **For web UI changes only**: Manually test the interface\n   - See \"Manual Testing Scenarios\" section below\n   - Verify the UI loads and functions correctly\n\n**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.**\n\n## Validation and Testing\n\n### Web UI Testing\n- **ALWAYS validate web UI changes manually**:\n  - Start local server: `cd wled00/data && python3 -m http.server 8080`\n  - Open `http://localhost:8080/index.htm` in browser\n  - Test basic functionality: color picker, effects, settings pages\n- **Check for JavaScript errors** in browser console\n\n### Code Validation\n- **No automated linting configured** - follow existing code style in files you edit\n- **Code style**: Use tabs for web files (.html/.css/.js), spaces (2 per level) for C++ files\n- **Language**: The repository language is English (british, american, canadian, or australian). If you find other languages, suggest a translation into English.\n- **C++ formatting available**: `clang-format` is installed but not in CI\n- **Always run tests before finishing**: `npm test`\n- **MANDATORY: Always run a hardware build before finishing** (see \"Before Finishing Work\" section below)\n\n### Manual Testing Scenarios\nAfter making changes to web UI, always test:\n- **Load main interface**: Verify index.htm loads without errors\n- **Navigation**: Test switching between main page and settings pages\n- **Color controls**: Verify color picker and brightness controls work\n- **Effects**: Test effect selection and parameter changes\n- **Settings**: Test form submission and validation\n\n## Common Tasks\n\n### Repository Structure\n```\nwled00/                 # Main firmware source (C++)\n  ├── data/            # Web interface files \n  │   ├── index.htm    # Main UI\n  │   ├── settings*.htm # Settings pages\n  │   └── *.js/*.css   # Frontend resources\n  ├── *.cpp/*.h        # Firmware source files\n  └── html_*.h         # Auto-generated embedded web files (DO NOT EDIT, DO NOT COMMIT)\ntools/                 # Build tools (Node.js)\n  ├── cdata.js         # Web UI build script\n  └── cdata-test.js    # Test suite\nplatformio.ini         # Hardware build configuration\npackage.json           # Node.js dependencies and scripts\n.github/workflows/     # CI/CD pipelines\n```\n\n### Key Files and Their Purpose\n- `wled00/data/index.htm` - Main web interface\n- `wled00/data/settings*.htm` - Configuration pages  \n- `tools/cdata.js` - Converts web files to C++ headers\n- `wled00/wled.h` - Main firmware configuration\n- `platformio.ini` - Hardware build targets and settings\n\n### Development Workflow (applies to agent mode only)\n1. **For web UI changes**:\n   - Edit files in `wled00/data/`\n   - Run `npm run build` to regenerate headers\n   - Test with local HTTP server\n   - Run `npm test` to validate build system\n\n2. **For firmware changes**:\n   - Edit files in `wled00/` (but NOT `html_*.h` files)\n   - Ensure web UI is built first (`npm run build`)\n   - Build firmware: `pio run -e [target]`\n   - Flash to device: `pio run -e [target] --target upload`\n\n3. **For both web and firmware**:\n   - Always build web UI first\n   - Test web interface manually\n   - Build and test firmware if making firmware changes\n\n## Build Timing and Timeouts\n\n**IMPORTANT: Use these timeout values when running builds:**\n\n- **Web UI build** (`npm run build`): 3 seconds typical - Set timeout to 30 seconds minimum\n- **Test suite** (`npm test`): 40 seconds typical - Set timeout to 120 seconds (2 minutes) minimum  \n- **Hardware builds** (`pio run -e [target]`): 15-20 minutes typical for first build - Set timeout to 1800 seconds (30 minutes) minimum\n  - Subsequent builds are faster due to caching\n  - First builds download toolchains and dependencies which takes significant time\n- **NEVER CANCEL long-running builds** - PlatformIO downloads and compilation require patience\n\n**When validating your changes before finishing, you MUST wait for the hardware build to complete successfully. Set the timeout appropriately and be patient.**\n\n## Troubleshooting\n\n### Common Issues\n- **Build fails with missing html_*.h**: Run `npm run build` first\n- **Web UI looks broken**: Check browser console for JavaScript errors\n- **PlatformIO network errors**: Try again, downloads can be flaky\n- **Node.js version issues**: Ensure Node.js 20+ is installed (check `.nvmrc`)\n\n### When Things Go Wrong\n- **Clear generated files**: `rm -f wled00/html_*.h` then rebuild\n- **Force web UI rebuild**: `npm run build -- --force` or `npm run build -- -f`\n- **Clean PlatformIO cache**: `pio run --target clean`\n- **Reinstall dependencies**: `rm -rf node_modules && npm install`\n\n## Important Notes\n\n- **Always commit source files**\n- **Web UI re-built is part of the platformio firmware compilation**\n- **do not commit generated html_*.h files**\n- **DO NOT edit `wled00/html_*.h` files** - they are auto-generated. If needed, modify Web UI files in `wled00/data/`.\n- **Test web interface manually after any web UI changes**\n- When reviewing a PR: the PR author does not need to update/commit generated html_*.h files - these files will be auto-generated when building the firmware binary.\n- If updating Web UI files in `wled00/data/`, make use of common functions availeable in `wled00/data/common.js` where possible.\n- **Use VS Code with PlatformIO extension for best development experience**\n- **Hardware builds require appropriate ESP32/ESP8266 development board**\n\n## CI/CD Pipeline\n\n**The GitHub Actions CI workflow will:**\n1. Installs Node.js and Python dependencies\n2. Runs `npm test` to validate build system (MUST pass)\n3. Builds web UI with `npm run build` (automatically run by PlatformIO)\n4. Compiles firmware for ALL hardware targets listed in `default_envs` (MUST succeed for all)\n5. Uploads build artifacts\n\n**To ensure CI success, you MUST locally:**\n- Run `npm test` and ensure it passes\n- Run `pio run -e esp32dev` (or another common environment from \"Hardware Compilation\" section) and ensure it completes successfully\n- If either fails locally, it WILL fail in CI\n\n**Match this workflow in your local development to ensure CI success. Do not mark work complete until you have validated builds locally.**\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: WLED Build\n\n# Only included into other workflows\non:\n  workflow_call:\n  \njobs:\n\n  get_default_envs:\n    name: Gather Environments\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v4\n    - uses: actions/setup-python@v5\n      with:\n        python-version: '3.12'\n        cache: 'pip'\n    - name: Install PlatformIO\n      run: pip install -r requirements.txt\n    - name: Get default environments\n      id: envs\n      run: |\n        echo \"environments=$(pio project config --json-output | jq -cr '.[0][1][0][1]')\" >> $GITHUB_OUTPUT\n    outputs:\n      environments: ${{ steps.envs.outputs.environments }}\n\n\n  build:\n    name: Build Environments\n    runs-on: ubuntu-latest\n    needs: get_default_envs\n    strategy:\n      fail-fast: false\n      matrix:\n        environment: ${{ fromJSON(needs.get_default_envs.outputs.environments) }}\n    steps:\n    - uses: actions/checkout@v4\n    - name: Set up Node.js\n      uses: actions/setup-node@v4\n      with:\n        node-version-file: '.nvmrc'\n        cache: 'npm'\n    - run: |\n        npm ci\n        VERSION=`date +%y%m%d0`\n        sed -i -r -e \"s/define VERSION .+/define VERSION $VERSION/\" wled00/wled.h\n    - name: Cache PlatformIO\n      uses: actions/cache@v4\n      with:\n        path: |\n              ~/.platformio/.cache\n              ~/.buildcache\n              build_output\n        key: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}-${{ hashFiles('wled00/**', 'usermods/**') }}\n        restore-keys: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}-\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      with:\n          python-version: '3.12'\n          cache: 'pip'\n    - name: Install PlatformIO\n      run: pip install -r requirements.txt\n\n    - name: Build firmware\n      run: pio run -e ${{ matrix.environment }}\n    - uses: actions/upload-artifact@v4\n      with:\n        name: firmware-${{ matrix.environment }}\n        path: |\n          build_output/release/*.bin\n          build_output/release/*_ESP02*.bin.gz\n\n\n  testCdata:\n    name: Test cdata.js\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Use Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: '.nvmrc'\n          cache: 'npm'\n      - run: npm ci\n      - run: npm test\n"
  },
  {
    "path": ".github/workflows/nightly.yml",
    "content": "\nname: Deploy Nightly\non:\n  # This can be used to automatically publish nightlies at UTC nighttime\n  schedule:\n    - cron: '0 2 * * *' # run at 2 AM UTC\n  # This can be used to allow manually triggering nightlies from the web interface\n  workflow_dispatch:\n\njobs:\n  wled_build:\n    uses: ./.github/workflows/build.yml\n  nightly:\n    name: Deploy nightly\n    runs-on: ubuntu-latest\n    needs: wled_build\n    steps:\n      - name: Download artifacts\n        uses: actions/download-artifact@v4\n        with:\n          merge-multiple: true\n      - name: Show Files\n        run: ls -la\n      - name: \"✏️ Generate release changelog\"\n        id: changelog\n        uses: janheinrichmerker/action-github-changelog-generator@v2.4\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }} \n          sinceTag: v0.15.0\n          output: CHANGELOG_NIGHTLY.md\n          # Exclude issues that were closed without resolution from changelog\n          excludeLabels: 'stale,wontfix,duplicate,invalid,external,question,use-as-is,not_planned'\n      - name: Update Nightly Release\n        uses: andelf/nightly-release@main\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          tag_name: nightly\n          name: 'Nightly Release $$'\n          prerelease: true\n          body_path: CHANGELOG_NIGHTLY.md\n          files: |\n            *.bin\n            *.bin.gz\n      - name: Repository Dispatch\n        uses: peter-evans/repository-dispatch@v3\n        with:\n          repository: wled/WLED-WebInstaller\n          event-type: release-nightly\n          token: ${{ secrets.PAT_PUBLIC }}\n"
  },
  {
    "path": ".github/workflows/pr-merge.yaml",
    "content": "    name: Notify Discord on PR Merge\n    on:\n      workflow_dispatch:\n      pull_request_target:\n        types: [closed]\n\n    jobs:\n      notify:\n        runs-on: ubuntu-latest\n        if: github.event.pull_request.merged == true\n        steps:\n        - name: Get User Permission\n          id: checkAccess\n          uses: actions-cool/check-user-permission@v2\n          with:\n            require: write\n            username: ${{ github.triggering_actor }}\n          env:\n            GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        - name: Check User Permission\n          if: steps.checkAccess.outputs.require-result == 'false'\n          run: |\n            echo \"${{ github.triggering_actor }} does not have permissions on this repo.\"\n            echo \"Current permission level is ${{ steps.checkAccess.outputs.user-permission }}\"\n            echo \"Job originally triggered by ${{ github.actor }}\"\n            exit 1\n        - name: Send Discord notification\n          env:\n            PR_NUMBER: ${{ github.event.pull_request.number }}\n            PR_TITLE: ${{ github.event.pull_request.title }}\n            PR_URL: ${{ github.event.pull_request.html_url }}\n            ACTOR: ${{ github.actor }}\n          run: |\n            jq -n \\\n              --arg content \"Pull Request #${PR_NUMBER} \\\"${PR_TITLE}\\\" merged by ${ACTOR}\n            ${PR_URL} . It will be included in the next nightly builds, please test\" \\\n              '{content: $content}' \\\n              | curl -H \"Content-Type: application/json\" -d @- ${{ secrets.DISCORD_WEBHOOK_BETA_TESTERS }}\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: WLED Release CI\n\non:\n  push:\n    tags:\n      - '*'\n\njobs:\n  \n  wled_build:\n    uses: ./.github/workflows/build.yml\n    \n  release:\n    name: Create Release\n    runs-on: ubuntu-latest\n    needs: wled_build\n    steps:\n    - uses: actions/download-artifact@v4\n      with:\n        merge-multiple: true\n    - name: \"✏️ Generate release changelog\"\n      id: changelog\n      uses: janheinrichmerker/action-github-changelog-generator@v2.4\n      with:\n          token: ${{ secrets.GITHUB_TOKEN }} \n          sinceTag: v0.15.0\n          maxIssues: 500\n          # Exclude issues that were closed without resolution from changelog\n          excludeLabels: 'stale,wontfix,duplicate,invalid,external,question,use-as-is,not_planned'       \n    - name: Create draft release\n      uses: softprops/action-gh-release@v1\n      with:\n        body: ${{ steps.changelog.outputs.changelog }}\n        draft: True\n        files: |\n          *.bin\n          *.bin.gz\n\n"
  },
  {
    "path": ".github/workflows/stale.yml",
    "content": "name: 'Close stale issues and PRs'\non:\n  schedule:\n    - cron: '0 12 * * *'\n  workflow_dispatch:\n\njobs:\n  stale:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/stale@v9\n        with:\n          days-before-stale: 120\n          days-before-close: 7\n          stale-issue-label: 'stale'\n          stale-pr-label: 'stale'\n          exempt-issue-labels: 'pinned,keep,enhancement,confirmed'\n          exempt-pr-labels: 'pinned,keep,enhancement,confirmed'\n          exempt-all-milestones: true\n          operations-per-run: 1000\n          stale-issue-message: >\n            Hey! This issue has been open for quite some time without any new comments now.\n            It will be closed automatically in a week if no further activity occurs.\n            \n            Thank you for using WLED! ✨\n          stale-pr-message: >\n            Hey! This pull request has been open for quite some time without any new comments now.\n            It will be closed automatically in a week if no further activity occurs.\n            \n            Thank you for contributing to WLED! ❤️\n"
  },
  {
    "path": ".github/workflows/test.yaml",
    "content": "on:\n  workflow_dispatch:\n\njobs:\n  dispatch:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Repository Dispatch\n        uses: peter-evans/repository-dispatch@v3\n        with:\n          repository: wled/WLED-WebInstaller\n          event-type: release-nightly\n          token: ${{ secrets.PAT_PUBLIC }}\n"
  },
  {
    "path": ".github/workflows/usermods.yml",
    "content": "name: Usermod CI\n\non:\n  pull_request:\n    paths:\n      - usermods/**\n    \njobs:\n\n  get_usermod_envs:\n    # Only run for pull requests from forks (not from branches within wled/WLED)\n    if: github.event.pull_request.head.repo.full_name != github.repository\n    name: Gather Usermods\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v4\n    - uses: actions/setup-python@v5\n      with:\n        python-version: '3.12'\n        cache: 'pip'\n    - name: Install PlatformIO\n      run: pip install -r requirements.txt\n    - name: Get default environments\n      id: envs\n      run: |\n        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\n    outputs:\n      usermods: ${{ steps.envs.outputs.usermods }}\n\n\n  build:\n    # Only run for pull requests from forks (not from branches within wled/WLED)\n    if: github.event.pull_request.head.repo.full_name != github.repository\n    name: Build Enviornments\n    runs-on: ubuntu-latest\n    needs: get_usermod_envs\n    strategy:\n      fail-fast: false\n      matrix:\n        usermod: ${{ fromJSON(needs.get_usermod_envs.outputs.usermods) }}\n        environment: [usermods_esp32, usermods_esp32c3, usermods_esp32s2, usermods_esp32s3]\n    steps:\n    - uses: actions/checkout@v4\n    - name: Set up Node.js\n      uses: actions/setup-node@v4\n      with:\n        node-version-file: '.nvmrc'\n        cache: 'npm'\n    - run: npm ci\n    - name: Cache PlatformIO\n      uses: actions/cache@v4\n      with:\n        path: |\n              ~/.platformio/.cache\n              ~/.buildcache\n              build_output\n        key: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}-${{ hashFiles('wled00/**', 'usermods/**') }}\n        restore-keys: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}-\n    - name: Set up Python\n      uses: actions/setup-python@v5\n      with:\n          python-version: '3.12'\n          cache: 'pip'\n    - name: Install PlatformIO\n      run: pip install -r requirements.txt\n    - name: Add usermods environment\n      run: |\n        cp -v usermods/platformio_override.usermods.ini platformio_override.ini\n        echo >> platformio_override.ini\n        echo  \"custom_usermods = ${{ matrix.usermod }}\" >> platformio_override.ini\n        cat platformio_override.ini\n\n    - name: Build firmware\n      run: pio run -e ${{ matrix.environment }}    \n"
  },
  {
    "path": ".github/workflows/wled-ci.yml",
    "content": "name: WLED CI\n\non:\n  push:\n    branches:\n      - '*'\n  pull_request:\n\njobs:\n  wled_build:\n    uses: ./.github/workflows/build.yml\n"
  },
  {
    "path": ".gitignore",
    "content": ".cache\n.clang-format\n.direnv\n.DS_Store\n.idea\n.pio\n.pioenvs\n.piolibdeps\n.vscode\ncompile_commands.json\n__pycache__/\n\n/.dummy\n/dependencies.lock\n/managed_components\n\nesp01-update.sh\nplatformio_override.ini\nreplace_fs.py\nwled-update.sh\n\n/build_output/\n/node_modules/\n/logs/\n\n/wled00/extLibs\n/wled00/LittleFS\n/wled00/my_config.h\n/wled00/Release\n/wled00/wled00.ino.cpp\n/wled00/html_*.h\n/wled00/js_*.h\n"
  },
  {
    "path": ".gitpod.Dockerfile",
    "content": "FROM gitpod/workspace-full\n\nUSER gitpod\n"
  },
  {
    "path": ".gitpod.yml",
    "content": "tasks:\n  - command: pip3 install -U platformio && platformio run\n\nimage:\n  file: .gitpod.Dockerfile\n\nvscode:\n  extensions:\n    - Atishay-Jain.All-Autocomplete\n    - esbenp.prettier-vscode\n    - shardulm94.trailing-spaces\n"
  },
  {
    "path": ".nvmrc",
    "content": "20.18\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## WLED changelog\n\n#### Build 2410270\n-   WLED 0.15.0-b7 release\n-   Re-license the WLED project from MIT to EUPL (#4194 by @Aircoookie)\n-   Fix alexa devices invisible/uncontrollable (#4214 by @Svennte)\n-   Add visual expand button on hover (#4172)\n-   Usermod: Audioreactive tuning and performance enhancements (by @softhack007)\n-   `/json/live` (JSON live data/peek) only enabled when WebSockets are disabled\n-   Various bugfixes and optimisations: #4179, #4215, #4219, #4222, #4223, #4224, #4228, #4230\n\n#### Build 2410140\n-   WLED 0.15.0-b6 release\n-   Added BRT timezone (#4188 by @LuisFadini)\n-   Fixed the positioning of the \"Download the latest binary\" button (#4184 by @maxi4329)\n-   Add WLED_AUTOSEGMENTS compile flag (#4183 by @PaoloTK)\n-   New 512kB FS parition map for 4MB devices\n-   Internal API change: Static PinManager & UsermodManager\n-   Change in Improv chip ID and version generation\n-   Various optimisations, bugfixes and enhancements (#4005, #4174 & #4175 by @Xevel, #4180, #4168, #4154, #4189 by @dosipod)\n\n#### Build 2409170\n-   UI: Introduce common.js in settings pages (size optimisation)\n-   Add the ability to toggle the reception of palette synchronizations (#4137 by @felddy)\n-   Usermod/FX: Temperature usermod added Temperature effect (example usermod effect by @blazoncek)\n-   Fix AsyncWebServer version pin\n\n#### Build 2409140\n-   Configure different kinds of busses at compile (#4107 by @PaoloTK)\n    - BREAKING: removes LEDPIN and DEFAULT_LED_TYPE compile overrides\n-   Fetch LED types from Bus classes (dynamic UI) (#4129 by @netmindz, @blazoncek, @dedehai)\n-   Temperature usermod: update OneWire to 2.3.8 (#4131 by @iammattcoleman)\n\n#### Build 2409100\n-   WLED 0.15.0-b5 release\n-   Audioreactive usermod included by default in all compatible builds (including ESP8266)\n-   Demystified some byte definitions of WiZmote ESP-NOW message (#4114 by @ChuckMash)\n-   Update usermod \"Battery\" improved MQTT support (#4110 by @itCarl)\n-   Added a usermod for interacting with BLE Pixels Dice (#4093 by @axlan)\n-   Allow lower values for touch threshold (#4081 by @RobinMeis)\n-   Added POV image effect usermod (#3539 by @Liliputech)\n-   Remove repeating code to fetch audio data (#4103 by @netmindz)\n-   Loxone JSON parser doesn't handle lx=0 correctly (#4104 by @FreakyJ, fixes #3809)\n-   Rename wled00.ino to wled_main.cpp (#4090 by @willmmiles)\n-   SM16825 chip support including WW & CW channel swap (#4092)\n-   Add stress testing scripts (#4088 by @willmmiles)\n-   Improve jsonBufferLock management (#4089 by @willmmiles)\n-   Fix incorrect PWM bit depth on Esp32 with XTAL clock (#4082 by @PaoloTK)\n-   Devcontainer args (#4073 by @axlan)\n-   Effect: Fire2012 optional blur amount (#4078 by @apanteleev)\n-   Effect: GEQ fix bands (#4077 by @adrianschroeter)\n-   Boot delay option (#4060 by @DedeHai)\n-   ESP8266 Audioreactive sync (#3962 by @gaaat98, @netmindz, @softhack007)\n-   ESP8266 PWM crash fix (#4035 by @willmmiles)\n-   Usermod: Battery fix (#4051 by @Nickbert7)\n-   Usermod: Mpu6050 usermod crash fix (#4048 by @willmmiles)\n-   Usermod: Internal Temperature V2 (#4033 by @adamsthws)\n-   Various fixes and improvements (including build environments to emulate 0.14.0 for ESP8266)\n\n#### Build 2407070\n-   Various fixes and improvements (mainly LED settings fix)\n\n#### Build 2406290\n-   WLED 0.15.0-b4 release\n-   LED settings bus management update (WARNING: only allows available outputs)\n-   Add ETH support for LILYGO-POE-Pro (#4030 by @rorosaurus)\n-   Update usermod_sn_photoresistor (#4017 by @xkvmoto)\n-   Several internal fixes and optimisations\n    - move LED_BUILTIN handling to BusManager class\n    - reduce max panels (web server limitation)\n    - edit WiFi TX power (ESP32)\n    - keep current ledmap ID in UI\n    - limit outputs in UI based on length\n    - wifi.ap addition to JSON Info (JSON API)\n    - relay pin init bugfix\n    - file editor button in UI\n    - ESP8266: update was restarting device on some occasions\n    - a bit of throttling in UI (for ESP8266)\n\n#### Build 2406120\n-   Update NeoPixelBus to v2.8.0\n-   Increased LED outputs one ESP32 using parallel I2S (up to 17)\n    - use single/mono I2S + 4x RMT for 5 outputs or less\n    - use parallel x8 I2S + 8x RMT for >5 outputs (limit of 300 LEDs per output)\n-   Fixed code of Smartnest and updated documentation (#4001 by @DevilPro1)\n-   ESP32-S3 WiFi fix (#4010 by @cstruck)\n-   TetrisAI usermod fix (#3897 by @muebau)\n-   ESP-NOW usermod hook\n-   Update wled.h regarding OTA Password (#3993 by @gsieben)\n-   Usermod BME68X Sensor Implementation (#3994 by @gsieben)\n-   Add a usermod for AHT10, AHT15 and AHT20 temperature/humidity sensors (#3977 by @LordMike)\n-   Update Battery usermod documentation (#3968 by @adamsthws)\n-   Add INA226 usermod for reading current and power over i2c (#3986 by @LordMike)\n-   Bugfixes: #3991\n-   Several internal fixes and optimisations (WARNING: some effects may be broken that rely on overflow/narrow width)\n    - replace uint8_t and uint16_t with unsigned\n    - replace in8_t and int16_t with int\n    - reduces code by 1kB\n\n#### Build 2405180\n-   WLED 0.14.4 release\n-   Fix for #3978\n-   Official 0.15.0-b3 release\n-   Merge 0.14.3 fixes into 0_15\n-   Added Pinwheel Expand 1D->2D effect mapping mode (#3961 by @Brandon502)\n-   Add changeable i2c address to BME280 usermod (#3966 by @LordMike)\n-   Effect: Firenoise - add palette selection\n-   Experimental parallel I2S support for ESP32 (compile time option)\n    - increased outputs to 17\n    - increased max possible color order overrides\n    - use WLED_USE_PARALLEL_I2S during compile\n    WARNING: Do not set up more than 256 LEDs per output when using parallel I2S with NeoPixelBus less than 2.9.0\n-   Update Usermod: Battery (#3964 by @adamsthws)\n-   Update Usermod: BME280 (#3965 by @LordMike)\n-   TM1914 chip support (#3913)\n-   Ignore brightness in Peek\n-   Antialiased line & circle drawing functions\n-   Enabled some audioreactive effects for single pixel strips/segments (#3942 by @gaaat98)\n-   Usermod Battery: Added Support for different battery types, Optimized file structure (#3003 by @itCarl)\n-   Skip playlist entry API (#3946 by @freakintoddles2)\n-   various optimisations and bugfixes (#3987, #3978)\n\n#### Build 2405030\n-   Using brightness in analog clock overlay (#3944 by @paspiz85)\n-   Add Webpage shortcuts (#3945 by @w00000dy)\n-   ArtNet Poll reply (#3892 by @askask)\n-   Improved brightness change via long button presses (#3933 by @gaaat98)\n-   Relay open drain output (#3920 by @Suxsem)\n-   NEW JSON API: release info (update page, `info.release`)\n-   update esp32 platform to arduino-esp32 v2.0.9 (#3902)\n-   various optimisations and bugfixes (#3952, #3922, #3878, #3926, #3919, #3904 @DedeHai)\n\n#### Build 2404120\n-   v0.15.0-b3\n-   fix for #3896 & WS2815 current saving\n-   conditional compile for AA setPixelColor()\n\n#### Build 2404100\n-   Internals: #3859, #3862, #3873, #3875\n-   Prefer I2S1 over RMT on ESP32\n-   usermod for Adafruit MAX17048 (#3667 by @ccruz09)\n-   Runtime detection of ESP32 PICO, general PSRAM support\n-   Extend JSON API \"info\" object\n    - add \"clock\" - CPU clock in MHz\n    - add \"flash\" - flash size in MB\n-   Fix for #3879\n-   Analog PWM fix for ESP8266 (#3887 by @gaaat98)\n-   Fix for #3870 (#3880 by @DedeHai)\n-   ESP32 S3/S2 touch fix (#3798 by @DedeHai)\n-   PIO env. PSRAM fix for S3 & S3 with 4M flash\n    - audioreactive always included for S3 & S2\n-   Fix for #3889\n-   BREAKING: Effect: modified KITT (Scanner) (#3763)\n\n#### Build 2404040\n-   WLED 0.14.3 release\n-   Fix for transition 0 (#3854, #3832, #3720)\n-   Fix for #3855 via #3873 (by @willmmiles)\n\n#### Build 2403280\n-   Individual color channel control for JSON API (fixes #3860)\n    - \"col\":[int|string|object|array, int|string|object|array, int|string|object|array]\n        int = Kelvin temperature or 0 for black\n        string = hex representation of [WW]RRGGBB\n        object = individual channel control {\"r\":0,\"g\":127,\"b\":255,\"w\":255}, each being optional (valid to send {})\n        array = direct channel values [r,g,b,w] (w element being optional)\n-   runtime selection for CCT IC (Athom 15W bulb)\n-   #3850 (by @w00000dy)\n-   Rotary encoder palette count bugfix\n-   bugfixes and optimisations\n\n#### Build 2403240\n-   v0.15.0-b2\n-   WS2805 support (RGB + WW + CW, 600kbps)\n-   Unified PSRAM use\n-   NeoPixelBus v2.7.9 (for future WS2805 support)\n-   Ubiquitous PSRAM mode for all variants of ESP32\n-   SSD1309_64 I2C Support for FLD Usermod (#3836 by @THATDONFC)\n-   Palette cycling fix (add support for `{\"seg\":[{\"pal\":\"X~Y~\"}]}` or `{\"seg\":[{\"pal\":\"X~Yr\"}]}`)\n-   FW1906 Support (#3810 by @deece and @Robert-github-com)\n-   ESPAsyncWebServer 2.2.0 (#3828 by @willmmiles)\n-   Bugfixes: #3843, #3844\n\n#### Build 2403190\n-   limit max PWM frequency (fix incorrect PWM resolution)\n-   Segment UI bugfix\n-   Updated AsyncWebServer (by @wlillmmiles)\n-   Simpler boot preset (fix for #3806)\n-   Effect: Fix for 2D Drift animation (#3816 by @BaptisteHudyma)\n-   Effect: Add twin option to 2D Drift\n-   MQTT cleanup\n-   DDP: Support sources that don't push (#3833 by @willmmiles)\n-   Usermod: Tetris AI usermod (#3711 by @muebau)\n\n#### Build 2403171\n-   merge 0.14.2 changes into 0.15\n\n#### Build 2403070\n-   Add additional segment options when controlling over e1.31 (#3616 by @demophoon)\n-   LockedJsonResponse: Release early if possible (#3760 by @willmmiles)\n-   Update setup-node and cache usermods in wled-ci.yml (#3737 by @WoodyLetsCode)\n-   Fix preset sorting (#3790 by @WoodyLetsCode)\n-   compile time button configuration #3792\n-   remove IR config if not compiled\n-   additional string optimisations\n-   Better low brightness level PWM handling (fixes #2767, #2868)\n\n#### Build 2402290\n-   Multiple analog button fix for #3549\n-   Preset caching on chips with PSRAM (credit @akaricchi)\n-   Fixing stairway usermod and adding buildflags (by @lost-hope)\n-   ESP-NOW packet modification\n-   JSON buffer lock error messages / Reduce wait time for lock to 100ms\n-   Reduce string RAM usage for ESP8266\n-   Fixing a potential array bounds violation in ESPDMX\n-   Move timezone table to PROGMEM (#3766 by @willmmiles)\n-   Reposition upload warning message. (fixes #3778)\n-   ABL display fix & optimisation\n-   Add virtual Art-Net RGBW option (#3783 by @shammy642)\n\n#### Build 2402090\n-   Added new Ethernet controller RGB2Go Tetra (duplicate of ESP3DEUXQuattro)\n-   Usermod: httpPullLightControl (#3560 by @roelbroersma)\n-   DMX: S2 & C3 support via modified ESPDMX\n-   Bugfix: prevent cleaning of JSON buffer after a failed lock attempt (BufferGuard)\n-   Product/Brand override (API & AP SSID) (#3750 by @moustachauve)\n\n#### Build 2402060\n-   WLED version 0.15.0-b1\n-   Harmonic Random Cycle palette (#3729 by @dedehai)\n-   Multi PIR sensor usermod (added support for attaching multiple PIR sensors)\n-   Removed obsolete (and nonfunctional) usermods\n\n#### Build 2309120 till build 2402010\n-   WLED version 0.15.0-a0\n-   Multi-WiFi support. Add up to 3 (or more via cusom compile) WiFis to connect to (with help from @JPZV)\n-   Temporary AP. Use your WLED in public with temporary AP.\n-   Github CI build system enhancements (#3718 by @WoodyLetsCode)\n-   Accessibility: Node list ( #3715 by @WoodyLetsCode)\n-   Analog clock overlay enhancement (#3489 by @WoodyLetsCode)\n-   ESP32-POE-WROVER from Olimex ethernet support (#3625 by @m-wachter)\n-   APA106 support (#3580 by @itstefanjanos)\n-   BREAKING: Effect: updated Palette effect to support 2D (#3683 by @TripleWhy)\n-   \"SuperSync\" from WLED MM (by @MoonModules)\n-   Effect: DNA Spiral Effect Speed Fix (#3723 by @Derek4aty1)\n-   Fix for #3693\n-   Orange flash fix (#3196) for transitions\n-   Add own background image upload (#3596 by @WoodyLetsCode)\n-   WLED time overrides (`WLED_NTP_ENABLED`, `WLED_TIMEZONE`, `WLED_UTC_OFFSET`, `WLED_LAT` and `WLED_LON`)\n-   Better sorting and naming of static palettes (by @WoodyLetsCode)\n-   ANIMartRIX usermod and effects (#3673 by @netmindz)\n-   Use canvas instead of CSS gradient for liveview (#3621 by @zanhecht)\n-   Fix for #3672\n-   ColoOrderMap W channel swap (color order overrides now have W swap)\n-   En-/disable LED maps when receiving realtime data (#3554 by @ezcGman)\n-   Added PWM frequency selection to UI (Settings)\n-   Automatically build UI before compiling (#3598, #3666 by @WoodyLetsCode)\n-   Internal: Added *suspend* API to `strip` (`WS2812FX class`)\n-   Possible fix for #3589 & partial fix for #3605\n-   MPU6050 upgrade (#3654 by @willmmiles)\n-   UI internals (#3656 by @WoodyLetsCode)\n-   ColorPicker fix (#3658 by @WoodyLetsCode)\n-   Global JSON buffer guarding (#3648 by @willmmiles, resolves #3641, #3312, #3367, #3637, #3646, #3447)\n-   Effect: Fireworks 1D (fix for matrix trailing strip)\n-   BREAKING: Reduced number of segments (12) on ESP8266 due to less available RAM\n-   Increased available effect data buffer (increases more if board has PSRAM)\n-   Custom palette editor mobile UI enhancement (by @imeszaros)\n-   Per port Auto Brightness Limiter (ABL)\n-   Use PSRAM for JSON buffer (double size, larger ledmaps, up to 2k)\n-   Reduced heap fragmentation by allocating ledmap array only once and not deallocating effect buffer\n-   HTTP retries on failed UI load\n-   UI Search: scroll to top (#3587 by @WoodyLetsCode)\n-   Return to inline iro.js and rangetouch.js (#3597 by @WoodyLetsCode)\n-   Better caching (#3591 by @WoodyLetsCode)\n-   Do not send 404 for missing `skin.css` (#3590 by @WoodyLetsCode)\n-   Simplified UI rework (#3511 by @WoodyLetsCode)\n-   Domoticz device ID for PIR and Temperature usermods\n-   Bugfix for UCS8904 `hasWhite()`\n-   Better search in UI (#3540 by @WoodyLetsCode)\n-   Seeding FastLED PRNG (#3552 by @TripleWhy)\n-   WIZ Smart Button support (#3547 by @micw)\n-   New button type (button switch, fix for #3537)\n-   Pixel Magic Tool update (#3483 by @ajotanc)\n-   Effect: 2D Matrix fix for gaps\n-   Bugfix #3526, #3533, #3561\n-   Spookier Halloween Eyes (#3501)\n-   Compile time options for Multi Relay usermod (#3498)\n-   Effect: Fix for Dissolve (#3502)\n-   Better reverse proxy support (nested paths)\n-   Implement global JSON API boolean toggle (i.e. instead of \"var\":true or \"var\":false -> \"var\":\"t\").\n-   Sort presets by ID\n-   Fix for #3641, #3312, #3367, #3637, #3646, #3447, #3632, #3496, #2922, #3593, #3514, #3522, #3578 (partial), #3606 (@WoodyLetsCode)\n-   Improved random bg image and added random bg image options (@WoodyLetsCode, #3481)\n-   Audio palettes (Audioreactive usermod, credit @netmindz)\n-   Better UI tooltips (@ajotnac, #3464)\n-   Better effect filters (filter dropdown)\n-   UDP sync fix (for #3487)\n-   Power button override (solves #3431)\n-   Additional HTTP request throttling (ESP8266)\n-   Additional UI/UX improvements\n-   Segment class optimisations (internal)\n-   ESP-NOW sync\n-   ESP-NOW Wiz remote JSON overrides (similar to IR JSON) & bugfixes\n-   Gamma correction for custom palettes (#3399).\n-   Restore presets from browser local storage\n-   Optional effect blending\n-   Restructured UDP Sync (internal)\n    -   Remove sync receive\n    -   Sync clarification\n-   Disallow 2D effects on non-2D segments\n-   Return of 2 audio simulations\n-   Bugfix in sync #3344 (internal)\n    -   remove excessive segments\n    -   ignore inactive segments if not syncing bounds\n    -   send UDP/WS on segment change\n    -   pop_back() when removing last segment\n\n#### Build 2403170\n-   WLED 0.14.2 release\n\n#### Build 2403110\n-   Beta WLED 0.14.2-b2\n-   New AsyncWebServer (improved performance and reduced memory use)\n-   New builds for ESP8266 with 160MHz CPU clock\n-   Fixing stairway usermod and adding buildflags (#3758 by @lost-hope)\n-   Fixing a potential array bounds violation in ESPDMX\n-   Reduced RAM usage (moved strings and TZ data (by @willmmiles) to PROGMEM)\n-   LockedJsonResponse: Release early if possible (by @willmmiles)\n\n#### Build 2402120\n-   Beta WLED 0.14.2-b1\n-   Possible fix for #3589 & partial fix for #3605\n-   Prevent JSON buffer clear after failed lock attempt\n-   Multiple analog button fix for #3549\n-   UM Audioreactive: add two compiler options (#3732 by @wled-install)\n-   Fix for #3693\n\n#### Build 2401141\n-   Official release of WLED 0.14.1\n-   Fix for #3566, #3665, #3672\n-   Sorting of palettes in custom palette editor (#3674 by @WoodyLetsCode)\n\n#### Build 2401060\n-   Version bump: 0.14.1-b3\n-   Global JSON buffer guarding (#3648 by @willmmiles, resolves #3641, #3312, #3367, #3637, #3646, #3447)\n-   Fix for #3632\n-   Custom palette editor mobile UI enhancement (#3617 by @imeszaros)\n-   changelog update\n\n#### Build 2312290\n-   Fix for #3622, #3613, #3609\n-   Various tweaks and fixes\n-   changelog update\n\n#### Build 2312230\n-   Version bump: 0.14.1-b2\n-   Fix for Pixel Magic button\n-   Fix for #2922 (option to force WiFi PHY mode to G on ESP8266)\n-   Fix for #3601, #3400 (incorrect sunrise/sunset, #3612 by @softhack007)\n\n#### Build 2312180\n-   Bugfixes (#3593, #3490, #3573, #3517, #3561, #3555, #3541, #3536, #3515, #3522, #3533, #3508)\n-   Various other internal cleanups and optimisations\n\n#### Build 2311160\n-   Version bump: 0.14.1-b1\n-   Bugfixes (#3526, #3502, #3496, #3484, #3487, #3445, #3466, #3296, #3382, #3312)\n-   New feature: Sort presets by ID\n-   New usermod: LDR sensor (#3490 by @JeffWDH)\n-   Effect: Twinklefox & Tinklecat metadata fix\n-   Effect: separate #HH and #MM for Scrolling Text (#3480)\n-   SSDR usermod enhancements (#3368)\n-   PWM fan usermod enhancements (#3414)\n\n#### Build 2310010, build 2310130\n-   Release of WLED version 0.14.0 \"Hoshi\"\n-   Bugfixes for #3400, #3403, #3405\n-   minor HTML optimizations\n-   audioreactive: bugfix for UDP sound sync (partly initialized packets)\n\n#### Build 2309240\n-   Release of WLED beta version 0.14.0-b6 \"Hoshi\"\n-   Effect bugfixes and improvements (Meteor, Meteor Smooth, Scrolling Text)\n-   audioreactive: bugfixes for ES8388 and ES7243 init; minor improvements for analog inputs\n\n#### Build 2309100\n-   Release of WLED beta version 0.14.0-b5 \"Hoshi\"\n-   New standard esp32 build with audioreactive\n-   Effect blending bugfixes, and minor optimizations\n\n#### Build 2309050\n-   Effect blending (#3311) (finally effect transitions!)\n    *WARNING*: May not work well with ESP8266, with plenty of segments or usermods (low RAM condition)!!!\n-   Added receive and send sync groups to JSON API (#3317) (you can change sync groups using preset)\n-   Internal temperature usermod (#3246)\n-   MQTT server and topic length overrides (#3354) (new build flags)\n-   Animated Staircase usermod enhancement (#3348) (on/off toggle/relay control)\n-   Added local time info to Info page (#3351)\n-   New effect: Rolling Balls (a.k.a. linear bounce) (#1039)\n-   Various bug fixes and enhancements.\n\n#### Build 2308110\n-   Release of WLED beta version 0.14.0-b4 \"Hoshi\"\n-   Reset effect data immediately upon mode change\n\n#### Build 2308030\n-   Improved random palette handling and blending\n-   Soap bugfix\n-   Fix ESP-NOW crash with AP mode Always\n\n#### Build 2307180\n-   Bus-level global buffering (#3280)\n-   Removed per-segment LED buffer (SEGMENT.leds)\n-   various fixes and improvements (ESP variants platform 5.3.0, effect optimizations, /json/cfg pin allocation)\n\n#### Build 2307130\n-   larger `oappend()` stack buffer (3.5k) for ESP32\n-   Preset cycle bugfix (#3262)\n-   Rotary encoder ALT fix for large LED count (#3276)\n-   effect updates (2D Plasmaball), `blur()` speedup\n-   On/Off toggle from nodes view (may show unknown device type on older versions) (#3291)\n-   various fixes and improvements (ABL, crashes when changing presets with different segments)\n\n#### Build 2306270\n-   ESP-NOW remote support (#3237)\n-   Pixel Magic tool (display pixel art) (#3249)\n-   Websocket (peek) fallback when connection cannot be established, WS retries (#3267)\n-   Add WiFi network scan RPC command to Improv Serial (#3271)\n-   Longer (custom option available) segment name for ESP32\n-   various fixes and improvements\n\n#### Build 2306210\n-   0.14.0-b3 release\n-   respect global I2C in all usermods (no local initialization of I2C bus)\n-   Multi relay usermod compile-time enabled option (-D MULTI_RELAY_ENABLED=true|false)\n\n#### Build 2306180\n-   Added client-side option for applying effect defaults from metadata\n-   Improved ESP8266 stability by reducing WebSocket response resends\n-   Updated ESP8266 core to 3.1.2\n\n#### Build 2306141\n-   Lissajous improvements\n-   Scrolling Text improvements (leading 0)\n\n#### Build 2306140\n-   Add settings PIN (un)locking to JSON post API\n\n#### Build 2306130\n-   Bumped version to 0.14-b3 (beta 3)\n-   added pin dropdowns in LED preferences (not for LED pins) and usermods\n-   introduced (unused ATM) NeoGammaWLEDMethod class\n-   Reverse proxy support\n-   PCF8754 support for Rotary encoder (requires wiring INT pin to ESP GPIO)\n-   Rely on global I2C pins for usermods (breaking change)\n-   various fixes and enhancements\n\n#### Build 2306020\n-   Support for segment sets (PR #3171)\n-   Reduce sound simulation modes to 2 to facilitate segment sets\n-   Trigger button immediately on press if all configured presets are the same (PR #3226)\n-   Changes for allowing Alexa to change light color to White when auto-calculating from RGB (PR #3211)\n\n#### Build 2305280\n-   DDP protocol update (#3193)\n-   added PCF8574 I2C port expander support for Multi relay usermod\n-   MQTT multipacket (fragmented) message fix\n-   added option to retain MQTT brightness and color messages\n-   new ethernet board: @srg74 Ethernet Shield\n-   new 2D effects: Soap (#3184) & Octopus & Waving cell (credit @St3P40 https://github.com/80Stepko08)\n-   various fixes and enhancements\n\n#### Build 2305090\n-   new ethernet board: @Wladi ABC! WLED Eth\n-   Battery usermod voltage calculation (#3116)\n-   custom palette editor (#3164)\n-   improvements in Dancing Shadows and Tartan effects\n-   UCS389x support\n-   switched to NeoPixelBus 2.7.5 (replaced NeoPixelBrightnessBus with NeoPixelBusLg)\n-   SPI bus clock selection (for LEDs) (#3173)\n-   DMX mode preset fix (#3134)\n-   iOS fix for scroll (#3182)\n-   Wordclock \"Norddeutsch\" fix (#3161)\n-   various fixes and enhancements\n\n#### Build 2304090\n-   updated Arduino ESP8266 core to 4.1.0 (newer compiler)\n-   updated NeoPixelBus to 2.7.3 (with support for UCS890x chipset)\n-   better support for ESP32-C3, ESP32-S2 and ESP32-S3 (Arduino ESP32 core 5.2.0)\n-   iPad/tablet with 1024 pixels width in landscape orientation PC mode support (#3153)\n-   fix for Pixel Art Converter (#3155)\n\n#### Build 2303240\n-   Peek scaling of large 2D matrices\n-   Added 0D (1 pixel) metadata for effects & enhance 0D (analog strip) UI handling\n-   Added ability to disable ADAlight (-D WLED_DISABLE_ADALIGHT)\n-   Fixed APA102 output on Ethernet enabled controllers\n-   Added ArtNet virtual/network output (#3121)\n-   Klipper usermod (#3106)\n-   Remove DST from CST timezone\n-   various fixes and enhancements\n\n#### Build 2302180\n\n-   Removed Blynk support (servers shut down on 31st Dec 2022)\n-   Added `ledgap.json` to complement ledmaps for 2D matrices\n-   Added support for white addressable strips (#3073)\n-   Ability to use SHT temperature usermod with PWM fan usermod\n-   Added `onStateChange()` callback to usermods (#3081)\n-   Refactored `bus_manager` [internal]\n-   Dual 1D & 2D mode (add 1D strip after the matrix)\n-   Removed 1D -> 2D mapping for individual pixel control\n-   effect tweak: Fireworks 1D\n-   various bugfixes\n\n#### Build 2301240\n\n-   Version bump to v0.14.0-b2 \"Hoshi\"\n-   PixelArt converter (convert any image to pixel art and display it on a matrix) (PR #3042)\n-   various effect updates and optimisations\n    -   added Overlay option to some effects (allows overlapping segments)\n    -   added gradient text on Scrolling Text\n    -   added #DDMM, #MMDD & #HHMM date and time options for Scrolling Text effect (PR #2990)\n    -   deprecated: Dynamic Smooth, Dissolve Rnd, Solid Glitter\n    -   optimised & enhanced loading of default values\n    -   new effect: Distortion Waves (2D)\n    -   2D support for Ripple effect\n    -   slower minimum speed for Railway effect\n-   DMX effect mode & segment controls (PR #2891)\n-   Optimisations for conditional compiles (further reduction of code size)\n-   better UX with effect sliders (PR #3012)\n-   enhanced support for ESP32 variants: C3, S2 & S3\n-   usermod enhancements (PIR, Temperature, Battery (PR #2975), Analog Clock (PR #2993))\n-   new usermod SHT (PR #2963)\n-   2D matrix set up with gaps or irregular panels (breaking change!) (PR #2892)\n-   palette blending/transitions\n-   random palette smooth changes\n-   hex color notations in custom palettes\n-   allow more virtual buses\n-   plethora of bugfixes\n\n### WLED release 0.14.0-b1\n\n#### Build 2212222\n\n-   Version bump to v0.14.0-b1 \"Hoshi\"\n-   2D matrix support (including mapping 1D effects to 2D and 2D peek)\n-   [internal] completely rewritten Segment & WS2812FX handling code\n-   [internal] ability to add custom effects via usermods\n-   [internal] set of 2D drawing functions\n-   transitions on every segment (including ESP8266)\n-   enhanced old and new 2D effects (metadata: default values)\n-   custom palettes (up to 10; upload palette0.json, palette1.json, ...)\n-   custom effect sliders and options, quick filters\n-   global I2C and SPI GPIO allocation (for usermods)\n-   usermod settings page enhancements (dropdown & info)\n-   asynchronous preset loading (and added \"pd\" JSON API call for direct preset apply)\n-   new usermod Boblight (PR #2917)\n-   new usermod PWM Outputs (PR #2912)\n-   new usermod Audioreactive\n-   new usermod Word Clock Matrix (PR #2743)\n-   new usermod Ping Pong Clock (PR #2746)\n-   new usermod ADS1115 (PR #2752)\n-   new usermod Analog Clock (PR #2736)\n-   various usermod enhancements and updates\n-   allow disabling pull-up resistors on buttons\n-   SD card support (PR #2877)\n-   enhanced HTTP API to support custom effect sliders & options (X1, X2, X3, M1, M2, M3)\n-   multiple UDP sync message retries (PR #2830)\n-   network debug printer (PR #2870)\n-   automatic UI PC mode on large displays\n-   removed support for upgrading from pre-0.10 (EEPROM)\n-   support for setting GPIO level when LEDs are off (RMT idle level, ESP32 only) (PR #2478)\n-   Pakistan time-zone (PKT)\n-   ArtPoll support\n-   TM1829 LED support\n-   experimental support for ESP32 S2, S3 and C3\n-   general improvements and bugfixes\n\n### WLED release 0.13.3\n\n-   Version bump to v0.13.3 \"Toki\"\n-   Disable ESP watchdog by default (fixes flickering and boot issues on a fresh install)\n-   Added support for LPD6803\n\n### WLED release 0.13.2\n\n#### Build 2208140\n\n-   Version bump to v0.13.2 \"Toki\"\n-   Added option to receive live data on the main segment only (PR #2601)\n-   Enable ESP watchdog by default (PR #2657)\n-   Fixed race condition when saving bus config\n-   Better potentiometer filtering (PR #2693)\n-   More suitable DMX libraries (PR #2652)\n-   Fixed outgoing serial TPM2 message length (PR #2628)\n-   Fixed next universe overflow and Art-Net DMX start address (PR #2607)\n-   Fixed relative segment brightness (PR #2665)\n\n### Builds between releases 0.13.1 and 0.13.2\n\n#### Build 2203191\n\n-   Fixed sunrise/set calculation (once again)\n\n#### Build 2203190\n\n-   Fixed `/json/cfg` unable to set busses (#2589)\n-   Fixed Peek with odd LED counts > 255 (#2586)\n\n#### Build 2203160\n\n-   Version bump to v0.13.2-a0 \"Toki\"\n-   Add ability to skip up to 255 LEDs\n-   Dependency version bumps\n\n### WLED release 0.13.1\n\n#### Build 2203150\n\n-   Version bump to v0.13.1 \"Toki\"\n-   Fix persistent preset bug, preventing save of new presets\n\n### WLED release 0.13.0\n\n#### Build 2203142\n\n-   Release of WLED v0.13.0 \"Toki\"\n-   Reduce APA102 hardware SPI frequency to 5Mhz\n-   Remove `persistent` parameter in `savePreset()`\n\n### Builds between releases 0.12.0 and 0.13.0\n\n#### Build 2203140\n\n-   Added factory reset by pressing button 0 for >10 seconds\n-   Added ability to set presets from DMX Effect mode\n-   Simplified label hiding JS in user interface\n-   Fixed JSON `{\"live\":true}` indefinite realtime mode\n\n#### Build 2203080\n\n-   Disabled auto white mode in segments with no RGB bus\n-   Fixed hostname string not 0-terminated \n-   Fixed Popcorn mode not lighting first LED on pop\n\n#### Build 2203060\n\n-   Dynamic hiding of unused color controls in UI (PR #2567)\n-   Removed native Cronixie support and added Cronixie usermod\n-   Fixed disabled timed preset expanding calendar\n-   Fixed Color Order setting shown for analog busses\n-   Fixed incorrect operator (#2566)\n\n#### Build 2203011\n\n-   IR rewrite (PR #2561), supports CCT\n-   Added locate button to Time settings\n-   CSS fixes and adjustments\n-   Consistent Tab indentation in index JS and CSS\n-   Added initial contribution style guideline\n\n#### Build 2202222\n\n-   Version bump to 0.13.0-b7 \"Toki\"\n-   Fixed HTTP API commands not applying to all selected segments in some conditions\n-   Blynk support is not compiled in by default on ESP32 builds\n\n#### Build 2202210\n\n-   Fixed HTTP API commands not applying to all selected segments if called from JSON\n-   Improved Stream effects, no longer rely on LED state and won't fade out at low brightness\n\n#### Build 2202200\n\n-   Added `info.leds.seglc` per-segment light capability info (PR #2552)\n-   Fixed `info.leds.rgbw` behavior\n-   Segment bounds sync (PR #2547)\n-   WebSockets auto reconnection and error handling\n-   Disable relay pin by default (PR #2531)\n-   Various fixes (ESP32 touch pin 33, floats, PR #2530, #2534, #2538)\n-   Deprecated `info.leds.cct`, `info.leds.wv` and `info.leds.rgbw`\n-   Deprecated `/url` endpoint\n\n#### Build 2202030\n\n-   Switched to binary format for WebSockets peek (PR #2516)\n-   Playlist bugfix\n-   Added `extractModeName()` utility function\n-   Added serial out (PR #2517)\n-   Added configurable baud rate\n\n#### Build 2201260\n\n-   Initial ESP32-C3 and ESP32-S2 support (PRs #2452, #2454, #2502)\n-   Full segment sync (PR #2427)\n-   Allow overriding of color order by ranges (PR #2463) \n-   Added white channel to Peek\n\n#### Build 2112080\n\n-   Version bump to 0.13.0-b6 \"Toki\"\n-   Added \"ESP02\" (ESP8266 with 2M of flash) to PIO/release binaries\n\n#### Build 2112070\n\n-   Added new effect \"Fairy\", replacing \"Police All\"\n-   Added new effect \"Fairytwinkle\", replacing \"Two Areas\"\n-   Static single JSON buffer (performance and stability improvement) (PR #2336)\n\n#### Build 2112030\n\n-   Fixed ESP32 crash on Colortwinkles brightness change\n-   Fixed setting picker to black resetting hue and saturation\n-   Fixed auto white mode not saved to config\n\n#### Build 2111300\n\n-   Added CCT and white balance correction support (PR #2285)\n-   Unified UI slider style\n-   Added LED settings config template upload\n\n#### Build 2111220\n\n-   Fixed preset cycle not working from preset called by UI\n-   Reintroduced permanent min. and max. cycle bounds\n\n#### Build 2111190\n\n-   Changed default ESP32 LED pin from 16 to 2\n-   Renamed \"Running 2\" to \"Chase 2\"\n-   Renamed \"Tri Chase\" to \"Chase 3\"\n\n#### Build 2111170\n\n-   Version bump to 0.13.0-b5 \"Toki\"\n-   Improv Serial support (PR #2334)\n-   Button improvements (PR #2284)\n-   Added two time zones (PR #2264, 2311)\n-   JSON in/decrementing support for brightness and presets\n-   Fixed no gamma correction for JSON individual LED control\n-   Preset cycle bugfix\n-   Removed ledCount\n-   LED settings buffer bugfix\n-   Network pin conflict bugfix\n-   Changed default ESP32 partition layout to 4M, 1M FS\n\n#### Build 2110110\n\n-   Version bump to 0.13.0-b4 \"Toki\"\n-   Added option for bus refresh if off (PR #2259)\n-   New auto segment logic\n-   Fixed current calculations for virtual or non-linear configs (PR #2262)\n\n#### Build 2110060\n\n-   Added virtual network DDP busses (PR #2245)\n-   Allow playlist as end preset in playlist\n-   Improved bus start field UX\n-   Pin reservations improvements (PR #2214)\n\n#### Build 2109220\n\n-   Version bump to 0.13.0-b3 \"Toki\"\n-   Added segment names (PR #2184)\n-   Improved Police and other effects (PR #2184)\n-   Reverted PR #1902 (Live color correction - will be implemented as usermod) (PR #2175)\n-   Added transitions for segment on/off\n-   Improved number of sparks/stars in Fireworks effect with low number of segments\n-   Fixed segment name edit pencil disappearing with request\n-   Fixed color transition active even if the segment is off\n-   Disallowed file upload with OTA lock active\n-   Fixed analog invert option missing (PR #2219)\n\n#### Build 2109100\n\n-   Added an auto create segments per bus setting\n-   Added 15 new palettes from SR branch (PR #2134)\n-   Fixed segment runtime not reset on FX change via HTTP API\n-   Changed AsyncTCP dependency to pbolduc fork v1.2.0\n\n#### Build 2108250\n\n-   Added Sync groups (PR #2150)\n-   Added JSON API over Serial support\n-   Live color correction (PR #1902)\n\n#### Build 2108180\n\n-   Fixed JSON IR remote not working with codes greater than 0xFFFFFF (fixes #2135)\n-   Fixed transition 0 edge case\n\n#### Build 2108170\n\n-   Added application level pong websockets reply (#2139)\n-   Use AsyncTCP 1.0.3 as it mitigates the flickering issue from 0.13.0-b2\n-   Fixed transition manually updated in preset overridden by field value\n\n#### Build 2108050\n\n-   Fixed undesirable color transition from Orange to boot preset color on first boot\n-   Removed misleading Delete button on new playlist with one entry\n-   Updated NeoPixelBus to 2.6.7 and AsyncTCP to 1.1.1\n\n#### Build 2107230\n\n-   Added skinning (extra custom CSS) (PR #2084)\n-   Added presets/config backup/restore (PR #2084)\n-   Added option for using length instead of Stop LED in UI (PR #2048)\n-   Added custom `holidays.json` holiday list (PR #2048)\n\n#### Build 2107100\n\n-   Version bump to 0.13.0-b2 \"Toki\"\n-   Accept hex color strings in individual LED API\n-   Fixed transition property not applying unless power/bri/color changed next\n-   Moved transition field below segments (temporarily)\n-   Reduced unneeded websockets pushes\n\n#### Build 2107091\n\n-   Fixed presets using wrong call mode (e.g. causing buttons to send UDP under direct change type)\n-   Increased hue buffer\n-   Renamed `NOTIFIER_CALL_MODE_` to `CALL_MODE_`\n\n#### Build 2107090\n\n-   Busses extend total configured LEDs if required\n-   Fixed extra button pins defaulting to 0 on first boot\n\n#### Build 2107080\n\n-   Made Peek use the main websocket connection instead of opening a second one\n-   Temperature usermod fix (from @blazoncek's dev branch)\n\n#### Build 2107070\n\n-   More robust initial resource loading in UI\n-   Added `getJsonValue()` for usermod config parsing (PR #2061)\n-   Fixed preset saving over websocket\n-   Alpha ESP32 S2 support (filesystem does not work) (PR #2067)\n\n#### Build 2107042\n\n-   Updated ArduinoJson to 6.18.1\n-   Improved Twinkleup effect\n-   Fixed preset immediately deselecting when set via HTTP API `PL=`\n\n#### Build 2107041\n\n-   Restored support for \"PL=~\" mistakenly removed in 2106300\n-   JSON IR improvements\n\n#### Build 2107040\n\n-   Playlist entries are now more compact\n-   Added the possibility to enter negative numbers for segment offset\n\n#### Build 2107021\n\n-   Added WebSockets support to UI\n\n#### Build 2107020\n\n-   Send websockets on every state change\n-   Improved Aurora effect\n\n#### Build 2107011\n\n-   Added MQTT button feedback option (PR #2011)\n\n#### Build 2107010\n\n-   Added JSON IR codes (PR #1941)\n-   Adjusted the width of WiFi and LED settings input fields\n-   Fixed a minor visual issue with slider trail not reaching thumb on low values\n\n#### Build 2106302\n\n-   Fixed settings page broken by using \"%\" in input fields\n\n#### Build 2106301\n\n-   Fixed a problem with disabled buttons reverting to pin 0 causing conflict\n\n#### Build 2106300\n\n-   Version bump to 0.13.0-b0 \"Toki\"\n-   BREAKING: Removed preset cycle (use playlists)\n-   BREAKING: Removed `nl.fade`, `leds.pin` and `ccnf` from JSON API\n-   Added playlist editor UI\n-   Reordered segment UI and added offset field\n-   Raised maximum MQTT password length to 64 (closes #1373)\n\n#### Build 2106290\n\n-   Added Offset to segments, allows shifting the LED considered first within a segment\n-   Added `of` property to seg object in JSON API to set offset\n-   Usermod settings improvements (PR #2043, PR #2045)\n\n#### Build 2106250\n\n-   Fixed preset only disabling on second effect/color change\n\n#### Build 2106241\n\n-   BREAKING: Added ability for usermods to force a config save if config incomplete. `readFromConfig()` needs to return a `bool` to indicate if the config is complete\n-   Updated usermods implementing `readFromConfig()`\n-   Auto-create segments based on configured busses\n\n#### Build 2106200\n\n-   Added 2 Ethernet boards and split Ethernet configs into separate file\n\n#### Build 2106180\n\n-   Fixed DOS on Chrome tab restore causing reboot\n\n#### Build 2106170\n\n-   Optimized JSON buffer usage (pre-serialized color arrays)\n\n#### Build 2106140\n\n-   Updated main logo\n-   Reduced flash usage by 0.8kB by using 8-bit instead of 32-bit PNGs for welcome and 404 pages\n-   Added a check to stop Alexa reporting an error if state set by macro differs from the expected state\n\n#### Build 2106100\n\n-   Added support for multiple buttons with various types (PR #1977)\n-   Fixed infinite playlists (PR #2020)\n-   Added `r` to playlist object, allows for shuffle regardless of the `repeat` value\n-   Improved accuracy of NTP time sync\n-   Added possibility for WLED UDP sync to sync system time\n-   Improved UDP sync accuracy, if both sender and receiver are NTP synced\n-   Fixed a cache issue with restored tabs\n-   Cache CORS request\n-   Disable WiFi sleep by default on ESP32\n\n#### Build 2105230\n\n-   No longer retain MQTT `/v` topic to alleviate storage loads on MQTT broker\n-   Fixed Sunrise calculation (atan_t approx. used outside of value range)\n\n#### Build 2105200\n\n-   Fixed WS281x output on ESP32\n-   Fixed potential out-of-bounds write in MQTT\n-   Fixed IR pin not changeable if IR disabled\n-   Fixed XML API <wv> containing -1 on Manual only RGBW mode (see #888, #1783)\n\n#### Build 2105171\n\n-   Always copy MQTT payloads to prevent non-0-terminated strings\n-   Updated ArduinoJson to 6.18.0\n-   Added experimental support for `{\"on\":\"t\"}` to toggle on/off state via JSON\n\n#### Build 2105120\n\n-   Fixed possibility of non-0-terminated MQTT payloads\n-   Fixed two warnings regarding integer comparison\n\n#### Build 2105112\n\n-   Usermod settings page no usermods message\n-   Lowered min speed for Drip effect\n\n#### Build 2105111\n\n-   Fixed various Codacy code style and logic issues\n\n#### Build 2105110\n\n-   Added Usermod settings page and configurable usermods (PR #1951)\n-   Added experimental `/json/cfg` endpoint for changing settings from JSON (see #1944, not part of official API)\n\n#### Build 2105070\n\n-   Fixed not turning on after pressing \"Off\" on IR remote twice (#1950)\n-   Fixed OTA update file selection from Android app (TODO: file type verification in JS, since android can't deal with accept='.bin' attribute)\n\n#### Build 2104220\n\n-   Version bump to 0.12.1-b1 \"Hikari\"\n-   Release and build script improvements (PR #1844)\n\n#### Build 2104211\n\n-   Replace default TV simulator effect with the version that saves 18k of flash and appears visually identical\n\n#### Build 2104210\n\n-   Added `tb` to JSON state, allowing setting the timebase (set tb=0 to start e.g. wipe effect from the beginning). Receive only.\n-   Slightly raised Solid mode refresh rate to work with LEDs (TM1814) that require refresh rates of at least 2fps\n-   Added sunrise and sunset calculation to the backup JSON time source\n\n#### Build 2104151\n\n-   `NUM_STRIPS` no longer required with compile-time strip defaults\n-   Further optimizations in wled_math.h\n\n#### Build 2104150\n\n-   Added ability to add multiple busses as compile time defaults using the esp32_multistrip usermod define syntax\n\n#### Build 2104141\n\n-   Reduced memory usage by 540b by switching to a different trigonometric approximation\n\n#### Build 2104140\n\n-   Added dynamic location-based Sunrise/Sunset macros (PR #1889)\n-   Improved seasonal background handling (PR #1890)\n-   Fixed instance discovery not working if MQTT not compiled in\n-   Fixed Button, IR, Relay pin not assigned by default (resolves #1891)\n\n#### Build 2104120\n\n-   Added switch support (button macro is switch closing action, long press macro switch opening)\n-   Replaced Circus effect with new Running Dual effect (Circus is Tricolor Chase with Red/White/Black)\n-   Fixed ledmap with multiple segments (PR #1864)\n\n#### Build 2104030\n\n-   Fixed ESP32 crash on Drip effect with reversed segment (#1854)\n-   Added flag `WLED_DISABLE_BROWNOUT_DET` to disable ESP32 brownout detector (off by default)\n\n### WLED release 0.12.0\n\n#### Build 2104020\n\n-   Allow clearing button/IR/relay pin on platforms that don't support negative numbers\n-   Removed AUX pin\n-   Hid some easter eggs, only to be found at easter\n\n### Development versions between 0.11.1 and 0.12.0 releases\n\n#### Build 2103310\n\n-   Version bump to 0.12.0 \"Hikari\"\n-   Fixed LED settings submission in iOS app\n\n#### Build 2103300\n\n-   Version bump to 0.12.0-b5 \"Hikari\"\n-   Update to core espressif32@3.2\n-   Fixed IR pin not configurable\n\n#### Build 2103290\n\n-   Version bump to 0.12.0-b4 \"Hikari\"\n-   Experimental use of espressif32@3.1.1\n-   Fixed RGBW mode disabled after LED settings saved\n-   Fixed infrared support not compiled in if IRPIN is not defined\n\n#### Build 2103230\n\n-   Fixed current estimation\n\n#### Build 2103220\n\n-   Version bump to 0.12.0-b2 \"Hikari\"\n-   Worked around an issue causing a critical decrease in framerate (wled.cpp l.240 block)\n-   Bump to Espalexa v2.7.0, fixing discovery\n\n#### Build 2103210\n\n-   Version bump to 0.12.0-b1 \"Hikari\"\n-   More colors visible on Palette preview\n-   Fixed chevron icon not included\n-   Fixed color order override\n-   Cleanup\n\n#### Build 2103200\n\n-   Version bump to 0.12.0-b0 \"Hikari\"\n-   Added palette preview and search (PR #1637)\n-   Added Reverse checkbox for PWM busses - reverses logic level for on\n-   Fixed various problems with the Playlist feature (PR #1724)\n-   Replaced \"Layer\" icon with \"i\" icon for Info button\n-   Chunchun effect more fitting for various segment lengths (PR #1804)\n-   Removed global reverse (in favor of individual bus reverse)\n-   Removed some unused icons from UI icon font\n\n#### Build 2103130\n\n-   Added options for Auto Node discovery\n-   Optimized strings (no string both F() and raw)\n\n#### Build 2103090\n\n-   Added Auto Node discovery (PR #1683)\n-   Added tooltips to quick color selectors for accessibility\n\n#### Build 2103060\n\n-   Auto start field population in bus config\n\n#### Build 2103050\n\n-   Fixed incorrect over-memory indication in LED settings on ESP32\n\n#### Build 2103041\n\n-   Added destructor for BusPwm (fixes #1789)\n\n#### Build 2103040\n\n-   Fixed relay mode inverted when upgrading from 0.11.0\n-   Fixed no more than 2 pins per bus configurable in UI\n-   Changed to non-linear IR brightness steps (PR #1742)\n-   Fixed various warnings (PR #1744)\n-   Added UDP DNRGBW Mode (PR #1704)\n-   Added dynamic LED mapping with ledmap.json file (PR #1738)\n-   Added support for QuinLED-ESP32-Ethernet board\n-   Added support for WESP32 ethernet board (PR #1764)\n-   Added Caching for main UI (PR #1704)\n-   Added Tetrix mode (PR #1729)\n-   Removed Merry Christmas mode (use \"Chase 2\" - called Running 2 before 0.13.0)\n-   Added memory check on Bus creation\n\n#### Build 2102050\n\n-   Version bump to 0.12.0-a0 \"Hikari\"\n-   Added FPS indication in info\n-   Bumped max outputs from 7 to 10 busses for ESP32\n\n#### Build 2101310\n\n-   First alpha configurable multipin\n\n#### Build 2101130\n\n-   Added color transitions for all segments and slots and for segment brightness\n-   Fixed bug that prevented setting a boot preset higher than 25\n\n#### Build 2101040\n\n-   Replaced Red & Blue effect with Aurora effect (PR #1589)\n-   Fixed HTTP changing segments uncommanded (#1618)\n-   Updated copyright year and contributor page link\n\n#### Build 2012311\n\n-   Fixed Countdown mode\n\n#### Build 2012310\n\n-   (Hopefully actually) fixed display of usermod values in info screen\n\n#### Build 2012240\n\n-   Fixed display of usermod values in info screen\n-   4 more effects now use FRAMETIME\n-   Remove unsupported environments from platformio.ini\n\n#### Build 2012210\n\n-   Split index.htm in separate CSS + JS files (PR #1542)\n-   Minify UI HTML, saving >1.5kB flash\n-   Fixed JShint warnings\n\n#### Build 2012180\n\n-   Boot brightness 0 will now use the brightness from preset\n-   Add iOS scrolling momentum (from PR #1528)\n\n### WLED release 0.11.1\n\n#### Build 2012180\n\n-   Release of WLED 0.11.1 \"Mirai\"\n-   Fixed AP hide not saving (fixes #1520)\n-   Fixed MQTT password re-transmitted to HTML\n-   Hide Update buttons while uploading, accept .bin\n-   Make sure AP password is at least 8 characters long\n\n### Development versions after 0.11.0 release\n\n#### Build 2012160\n\n-   Bump Espalexa to 2.5.0, fixing discovery (PR Espalexa/#152, originally PR #1497)\n\n#### Build 2012150\n\n-   Added Blends FX (PR #1491)\n-   Fixed an issue that made it impossible to deactivate timed presets\n\n#### Build 2012140\n\n-   Added Preset ID quick display option (PR #1462)\n-   Fixed LEDs not turning on when using gamma correct brightness and LEDPIN 2 (default)\n-   Fixed notifier applying main segment to selected segments on notification with FX/Col disabled \n\n#### Build 2012130\n\n-   Fixed RGBW mode not saved between reboots (fixes #1457)\n-   Added brightness scaling in palette function for default (PR #1484)\n\n#### Build 2012101\n\n-   Fixed preset cycle default duration rounded down to nearest 10sec interval (#1458)\n-   Enabled E1.31/DDP/Art-Net in AP mode\n\n#### Build 2012100\n\n-   Fixed multi-segment preset cycle\n-   Fixed EEPROM (pre-0.11 settings) not cleared on factory reset\n-   Fixed an issue with intermittent crashes on FX change (PR #1465)\n-   Added function to know if strip is updating (PR #1466)\n-   Fixed using colorwheel sliding the UI (PR #1459)\n-   Fixed analog clock settings not saving (PR #1448)\n-   Added Temperature palette (PR #1430)\n-   Added Candy cane FX (PR #1445)\n\n#### Build 2012020\n\n-   UDP `parsePacket()` with sync disabled (#1390)\n-   Added Multi RGBW DMX mode (PR #1383)\n\n#### Build 2012010\n\n-   Fixed compilation for analog (PWM) LEDs\n\n### WLED version 0.11.0\n\n#### Build 2011290\n\n-   Release of WLED 0.11.0 \"Mirai\"\n-   Workaround for weird empty %f Espalexa issue\n-   Fixed crash on saving preset with HTTP API `PS`\n-   Improved performance for color changes in non-main segment\n\n#### Build 2011270\n\n-   Added tooltips for speed and intensity sliders (PR #1378)\n-   Moved color order to NpbWrapper.h\n-   Added compile time define to override the color order for a specific range\n\n#### Build 2011260\n\n-   Add `live` property to state, allowing toggling of realtime (not incl. in state resp.)\n-   PIO environment changes\n\n#### Build 2011230\n\n-   Version bump to 0.11.0 \"Mirai\"\n-   Improved preset name sorting\n-   Fixed Preset cycle not working beyond preset 16\n\n### Development versions between 0.10.2 and 0.11.0 releases\n\n#### Build 2011220\n\n-   Fixed invalid save when modifying preset before refresh (might be related to #1361)\n-   Fixed brightness factor ignored on realtime timeout (fixes #1363)\n-   Fixed Phase and Chase effects with LED counts >256 (PR #1366)\n\n#### Build 2011210\n\n-   Fixed Brightness slider beneath color wheel not working (fixes #1360)\n-   Fixed invalid UI state after saving modified preset\n\n#### Build 2011200\n\n-   Added HEX color receiving to JSON API with `\"col\":[\"RRGGBBWW\"]` format\n-   Moved Kelvin color receiving in JSON API from `\"col\":[[val]]` to `\"col\":[val]` format\n    _Notice:_ This is technically a breaking change. Since no release was made since the introduction and the Kelvin property was not previously documented in the wiki,\n    impact should be minimal. \n-   BTNPIN can now be disabled by setting to -1 (fixes #1237)\n\n#### Build 2011180\n\n-   Platformio.ini updates and streamlining (PR #1266)\n-   my_config.h custom compile settings system (not yet used for much, adapted from PR #1266)\n-   Added Hawaii timezone (HST)\n-   Linebreak after 5 quick select buttons\n\n#### Build 2011154\n\n-   Fixed RGBW saved incorrectly\n-   Fixed pmt caching requesting /presets.json too often\n-   Fixed deEEP not copying the first segment of EEPROM preset 16\n\n#### Build 2011153\n\n-   Fixed an ESP32 end-of-file issue\n-   Fixed strip.isRgbw not read from cfg.json\n\n#### Build 2011152\n\n-   Version bump to 0.11.0p \"Mirai\"\n-   Increased max. num of segments to 12 (ESP8266) / 16 (ESP32)\n-   Up to 250 presets stored in the `presets.json` file in filesystem\n-   Complete overhaul of the Presets UI tab\n-   Updated iro.js to v5 (fixes black color wheel)\n-   Added white temperature slider to color wheel\n-   Add JSON settings serialization/deserialization to cfg.json and wsec.json\n-   Added deEEP to convert the EEPROM settings and presets to files\n-   Playlist support - JSON only for now\n-   New v2 usermod methods `addToConfig()` and `readFromConfig()` (see EXAMPLE_v2 for doc)\n-   Added Ethernet support for ESP32 (PR #1316)\n-   IP addresses are now handled by the `Network` class\n-   New `esp32_poe` PIO environment\n-   Use EspAsyncWebserver Aircoookie fork v.2.0.0 (hiding wsec.json)\n-   Removed `WLED_DISABLE_FILESYSTEM` and `WLED_ENABLE_FS_SERVING` defines as they are now required\n-   Added pin manager\n-   UI performance improvements (no drop shadows)\n-   More explanatory error messages in UI\n-   Improved candle brightness\n-   Return remaining nightlight time `nl.rem` in JSON API (PR #1302)\n-   UI sends timestamp with every command, allowing for timed presets without using NTP\n-   Added gamma calculation (yet unused)\n-   Added LED type definitions to const.h (yet unused)\n-   Added nicer 404 page\n-   Removed `NP` and `MS=` macro HTTP API commands\n-   Removed macros from Time settings\n\n#### Build 2011120\n\n-   Added the ability for the /api MQTT topic to receive JSON API payloads\n\n#### Build 2011040\n\n-   Inverted Rain direction (fixes #1147)\n\n#### Build 2011010\n\n-   Re-added previous C9 palette\n-   Renamed new C9 palette\n\n#### Build 2010290\n\n-   Colorful effect now supports palettes\n-   Added C9 2 palette (#1291)\n-   Improved C9 palette brightness by 12%\n-   Disable onboard LED if LEDs are off (PR #1245)\n-   Added optional status LED (PR #1264)\n-   Realtime max. brightness now honors brightness factor (fixes #1271)\n-   Updated ArduinoJSON to 6.17.0\n\n#### Build 2010020\n\n-   Fixed interaction of `T` and `NL` HTTP API commands (#1214)\n-   Fixed an issue where Sunrise mode nightlight does not activate if toggled on simultaneously \n\n#### Build 2009291\n\n-   Fixed MQTT bootloop (no F() macro, #1199)\n\n#### Build 2009290\n\n-   Added basic DDP protocol support\n-   Added Washing Machine effect (PR #1208)\n\n#### Build 2009260\n\n-   Added Loxone parser (PR #1185)\n-   Added support for kelvin input via `K=` HTTP and `\"col\":[[val]]` JSON API calls\n    _Notice:_ `\"col\":[[val]]` removed in build 2011200, use `\"col\":[val]`\n-   Added supplementary UDP socket (#1205)\n-   TMP2.net receivable by default\n-   UDP sockets accept HTTP and JSON API commands\n-   Fixed missing timezones (#1201)\n\n#### Build 2009202\n\n-   Fixed LPD8806 compilation\n\n#### Build 2009201\n\n-   Added support for preset cycle toggling using CY=2\n-   Added ESP32 touch pin support (#1190)\n-   Fixed modem sleep on ESP8266 (#1184)\n\n#### Build 2009200\n\n-   Increased available heap memory by 4kB\n-   Use F() macro for the majority of strings\n-   Restructure timezone code\n-   Restructured settings saved code\n-   Updated ArduinoJSON to 6.16.1\n\n#### Build 2009170\n\n-   New WLED logo on Welcome screen (#1164)\n-   Fixed 170th pixel dark in E1.31\n\n#### Build 2009100\n\n-   Fixed sunrise mode not reinitializing\n-   Fixed passwords not clearable\n\n#### Build 2009070\n\n-   New Segments are now initialized with default speed and intensity\n\n#### Build 2009030\n\n-   Fixed bootloop if mDNS is used on builds without OTA support\n\n### WLED version 0.10.2\n\n#### Build 2008310\n\n-   Added new logo\n-   Maximum GZIP compression (#1126)\n-   Enable WebSockets by default\n\n### Development versions between 0.10.0 and 0.10.2 releases\n\n#### Build 2008300\n\n-   Added new UI customization options to UI settings\n-   Added Dancing Shadows effect (#1108)\n-   Preset cycle is now paused if lights turned off or nightlight active\n-   Removed `esp01` and `esp01_ota` envs from travis build (need too much flash)\n\n#### Build 2008290\n\n-   Added individual LED control support to JSON API\n-   Added internal Segment Freeze/Pause option\n\n#### Build 2008250\n\n-   Made `platformio_override.ini` example easier to use by including the `default_envs` property\n-   FastLED uses `now` as timer, so effects using e.g. `beatsin88()` will sync correctly\n-   Extended the speed range of Pacifica effect\n-   Improved TPM2.net receiving (#1100)\n-   Fixed exception on empty MQTT payload (#1101)\n\n#### Build 2008200\n\n-   Added segment mirroring to web UI\n-   Fixed segment mirroring when in reverse mode\n\n#### Build 2008140\n\n-   Removed verbose live mode info from `<ds>` in HTTP API response\n\n#### Build 2008100\n\n-   Fixed Auto White mode setting (fixes #1088)\n\n#### Build 2008070\n\n-   Added segment mirroring (`mi` property) (#1017)\n-   Fixed DMX settings page not displayed (#1070)\n-   Fixed ArtNet multi universe and improve code style (#1076)\n-   Renamed global var `local` to `localTime` (#1078)\n\n#### Build 2007190\n\n-   Fixed hostname containing illegal characters (#1035)\n\n#### Build 2006251\n\n-   Added `SV=2` to HTTP API, allow selecting single segment only\n\n#### Build 2006250\n\n-   Fix Alexa not turning off white channel (fixes #1012)\n\n#### Build 2006220\n\n-   Added Sunrise nightlight mode\n-   Added Chunchun effect\n-   Added `LO` (live override) command to HTTP API\n-   Added `mode` to `nl` object of JSON state API, deprecating `fade`\n-   Added light color scheme support to web UI (click sun next to brightness slider)\n-   Added option to hide labels in web UI (click flame icon next to intensity slider)\n-   Added hex color input (click palette icon next to palette select) (resolves #506)\n-   Added support for RGB sliders (need to set in localstorage)\n-   Added support for custom background color or image (need to set in localstorage)\n-   Added option to hide bottom tab bar in PC mode (need to set in localstorage)\n-   Fixed transition lag with multiple segments (fixes #985)\n-   Changed Nightlight wording (resolves #940)\n\n#### Build 2006060\n\n-   Added five effects by Andrew Tuline (Phased, Phased Noise, Sine, Noise Pal and Twinkleup)\n-   Added two new effects by Aircoookie (Sunrise and Flow)\n-   Added US-style sequence to traffic light effect\n-   Merged pull request #964 adding 9 key IR remote\n\n#### Build 2005280\n\n-   Added v2 usermod API\n-   Added v2 example usermod `usermod_v2_example` in the usermods folder as prelimary documentation\n-   Added DS18B20 Temperature usermod with Info page support\n-   Disabled MQTT on ESP01 build to make room in flash\n\n#### Build 2005230\n\n-   Fixed TPM2\n\n#### Build 2005220\n\n-   Added TPM2.NET protocol support (need to set WLED broadcast UDP port to 65506)\n-   Added TPM2 protocol support via Serial\n-   Support up to 6553 seconds preset cycle durations (backend, NOT yet in UI)\n-   Merged pull request #591 fixing WS2801 color order\n-   Merged pull request #858 adding fully featured travis builds\n-   Merged pull request #862 adding DMX proxy feature\n\n#### Build 2005100\n\n-   Update to Espalexa v2.4.6 (+1.6kB free heap memory)\n-   Added `m5atom` PlatformIO environment\n\n#### Build 2005090\n\n-   Default to ESP8266 Arduino core v2.7.1 in PlatformIO\n-   Fixed Preset Slot 16 always indicating as empty (#891)\n-   Disabled Alexa emulation by default (causes bootloop for some users)\n-   Added BWLT11 and SHOJO_PCB defines to NpbWrapper\n-   Merged pull request #898 adding Solid Glitter effect\n\n### WLED version 0.10.0\n\n#### Build 2005030\n\n-   DMX Single RGW and Single DRGB modes now support an additional white channel\n-   Improved palettes derived from set colors and changed their names\n\n### Development versions between 0.9.1 and 0.10.0 release\n\n#### Build 2005020\n\n-   Added ACST and ACST/ACDT timezones\n\n#### Build 2005010\n\n-   Added module info page to web UI\n-   Added realtime override functionality to web UI\n-   Added individual segment power and brightness to web UI\n-   Added feature to one-click select single segment only by tapping segment name\n-   Removed palette jumping to default if color is changed\n\n#### Build 2004300\n\n-   Added realtime override option and `lor` JSON property\n-   Added `lm` (live mode) and `lip` (live IP) properties to info in JSON API\n-   Added reset commands to APIs\n-   Added `json/si`, returning state and info, but no FX or Palette lists\n-   Added rollover detection to millis(). Can track uptimes longer than 49 days\n-   Attempted to fix Wifi issues with Unifi brand APs\n\n#### Build 2004230\n\n-   Added brightness and power for individual segments\n-   Added `on` and `bri` properties to Segment object in JSON API\n-   Added `C3` an `SB` commands to HTTP get API\n-   Merged pull request #865 for 5CH_Shojo_PCB environment\n\n#### Build 2004220\n\n-   Added Candle Multi effect\n-   Added Palette capability to Pacifica effect\n\n#### Build 2004190\n\n-   Added TM1814 type LED defines\n\n#### Build 2004120\n\n-   Added Art-Net support\n-   Added OTA platform to platformio.ini\n\n#### Build 2004100\n\n-   Fixed DMX output compilation\n-   Added DMX start LED setting\n\n#### Build 2004061\n\n-   Fixed RBG and BGR getPixelColor (#825)\n-   Improved formatting\n\n#### Build 2004060\n\n-   Consolidated global variables in wled.h\n\n#### Build 2003300\n\n-   Major change of project structure from .ino to .cpp and func_declare.h\n\n#### Build 2003262\n\n-   Fixed compilation for Analog LEDs\n-   Fixed sync settings network port fields too small\n\n#### Build 2003261\n\n-   Fixed live preview not displaying whole light if over 255 LEDs\n\n#### Build 2003251\n\n-   Added Pacifica effect (tentative, doesn't yet support other colors)\n-   Added Atlantica palette\n-   Fixed ESP32 build of Espalexa\n\n#### Build 2003222\n\n-   Fixed Alexa Whites on non-RGBW lights (bump Espalexa to 2.4.5)\n\n#### Build 2003221\n\n-   Moved Cronixie driver from FX library to drawOverlay handler\n\n#### Build 2003211\n\n-   Added custom mapping compile define to FX_fcn.h\n-   Merged pull request #784 by @TravisDean: Fixed initialization bug when toggling skip first\n-   Added link to youtube videos by Room31 to readme\n\n#### Build 2003141\n\n-   Fixed color of main segment returned in JSON API during transition not being target color (closes #765)\n-   Fixed arlsLock() being called after pixels set in E1.31 (closes #772)\n-   Fixed HTTP API calls not having an effect if no segment selected (now applies to main segment)\n\n#### Build 2003121\n\n-   Created changelog.md - make tracking changes to code easier\n-   Merged pull request #766 by @pille: Fix E1.31 out-of sequence detection\n\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at dev.aircoookie@gmail.com. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Thank you for making WLED better!\n\nWLED is a community-driven project, and every contribution matters! We appreciate your time and effort.\n\nOur maintainers are here for two things: **helping you** improve your code, and **keeping WLED** lean, efficient, and maintainable. \nWe'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!\n\n## Getting Started\n\nHere are a few suggestions to make it easier for you to contribute:\n\n### PR from a branch in your own fork\nStart your pull request (PR) in a branch of your own fork. Don't make a PR directly from your main branch.\nThis lets you update your PR if needed, while you can work on other tasks in 'main' or in other branches.\n\n> [!TIP]\n>   **The easiest way to start your first PR**\n>   When viewing a file in `wled/WLED`, click on the \"pen\" icon and start making changes.\n>   When you choose to 'Commit changes', GitHub will automatically create a PR from your fork.\n>   \n>   <img width=\"295\" height=\"134\" alt=\"image: fork and edit\" src=\"https://github.com/user-attachments/assets/f0dc7567-edcb-4409-a530-cd621ae9661f\" />\n\n\n### Target branch for pull requests\n\nPlease make all PRs against the `main` branch.\n\n### Describing your PR\n\nPlease add a description of your proposed code changes. \nA PR with no description or just a few words might not get accepted, simply because very basic information is missing.\nNo need to write an essay!\n\nA good description helps us to review and understand your proposed changes. For example, you could say a few words about\n* What you try to achieve (new feature, fixing a bug, refactoring, security enhancements, etc.)\n* How your code works (short technical summary - focus on important aspects that might not be obvious when reading the code)\n* Testing you performed, known limitations, anything you couldn't quite solve.\n* Let us know if you'd like guidance from a maintainer (WLED is a big project 😉)\n\n### Testing Your Changes\n\nBefore submitting:\n\n- ✅ Does it compile?\n- ✅ Does your feature/fix actually work?\n- ✅ Did you break anything else?\n- ✅ Tested on actual hardware if possible?\n\nMention your testing in the PR description (e.g., \"Tested on ESP32 + WS2812B\").\n\n## During Review\n\nWe're all volunteers, so reviews can take some time (longer during busy times). \nDon't worry - we haven't forgotten you! Feel free to ping after a week if there's no activity.\n\n### Updating your code\nWhile the PR is open, you can keep updating your branch - just push more commits! GitHub will automatically update your PR. \n\nYou don't need to squash commits or clean up history - we'll handle that when merging.\n\n> [!CAUTION] \n> Do not use \"force-push\" while your PR is open!\n> It has many subtle and unexpected consequences on our GitHub repository.\n> 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.\n> So, pretty please, do not force-push.\n\n> [!TIP]\n> 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.\n\n\n### Responding to Reviews\n\nWhen we ask for changes:\n\n- **Add new commits** - please don't amend or force-push\n- **Reply in the PR** - let us know when you've addressed comments\n- **Ask questions** - if something's unclear, just ask!\n- **Be patient** - we're all volunteers here 😊\n\nYou can reference feedback in commit messages:\n> ```text\n> Fix naming per @Aircoookie's suggestion\n> ```\n\n### Dealing with Merge Conflicts\n\nGot conflicts with `main`? No worries - here's how to fix them:\n\n**Using GitHub Desktop** (easier for beginners):\n\n1. Click **Fetch origin**, then **Pull origin**\n2. If conflicts exist, GitHub Desktop will warn you - click **View conflicts**\n3. Open the conflicted files in your editor (VS Code, etc.)\n4. Remove the conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) and keep the correct code\n5. Save the files\n6. Back in GitHub Desktop, commit the merge (it'll suggest a message)\n7. Click **Push origin**\n\n**Using command line**:\n\n   ```bash\n   git fetch origin\n   git merge origin/main\n   # Fix conflicts in your editor\n   git add .\n   git commit\n   git push\n   ```\n\nEither way works fine - pick what you're comfortable with! Merging is simpler than rebasing and keeps everything connected.\n\n#### When you MUST rebase (really rare!)\n\nSometimes you might hit merge conflicts with `main` that are harder to solve. Here's what to try:\n\n1. **Merge instead of rebase** (safest option):\n   ```bash\n   git fetch origin\n   git merge origin/main\n   git push\n   ```\n   Keeps review comments attached and CI results visible!\n\n2. **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).\n\n3. **If all else fails, use `--force-with-lease`** (not plain `--force`):\n   ```bash\n   git rebase origin/main\n   git push --force-with-lease\n   ```\n   Then **leave a comment** explaining why you had to force-push, and be ready to re-address some feedback.\n\n### Additional Resources\nWant to know more? Check out:\n- 📚 [GitHub Desktop documentation](https://docs.github.com/en/desktop) - if you prefer GUI tools\n- 🎓 [How to properly submit a PR](https://github.com/wled-dev/WLED/wiki/How-to-properly-submit-a-PR) - detailed tips and tricks\n\n\n## After Approval\nOnce approved, a maintainer will merge your PR (possibly squashing commits). \nYour contribution will be in the next WLED release - thank you! 🎉\n\n\n## Coding Guidelines\n\n### Source Code from an AI agent or bot\n> [!IMPORTANT]\n> It's OK if you took help from an AI for writing your source code. \n>\n> AI tools can be very helpful, but as the contributor, **you're responsible for the code**.\n\n* Make sure you really understand the AI-generated code, don't just accept it because it \"seems to work\".\n* 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.\n* Remember that AI is still \"Often-Wrong\" ;-)\n* 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.\n\n#### Best Practice with AI\n\nAI tools are powerful but \"often wrong\" - your judgment is essential! 😊\n\n- ✅ **Understand the code** - As the person contributing to WLED, make sure you understand exactly what the AI-generated source code does\n- ✅ **Review carefully** - AI can lose comments, introduce bugs, or make unnecessary changes\n- ✅ **Be transparent** - Add a comment like `// This section was AI-generated` for larger chunks\n- ✅ **Use AI for translation** - AI is great for translating comments to English (but verify technical terms!)\n\n### Code style\n\nDon't stress too much about style! When in doubt, just match the style in the files you're editing. 😊\n\nHere are our main guidelines:\n\n#### Indentation\n\nWe use tabs for indentation in Web files (.html/.css/.js) and spaces (2 per indentation level) for all other files.  \nYou are all set if you have enabled `Editor: Detect Indentation` in VS Code.\n\n#### Blocks\n\nWhether the opening bracket of e.g. an `if` block is in the same line as the condition or in a separate line is up to your discretion. If there is only one statement, leaving out block brackets is acceptable.\n\nGood:  \n```cpp\nif (a == b) {\n  doStuff(a);\n}\n```\n\n```cpp\nif (a == b) doStuff(a);\n```\n\nAlso acceptable (though the first style is usually easier to read):\n```cpp\nif (a == b)\n{\n  doStuff(a);\n}\n```\n\n\nThere should always be a space between a keyword and its condition and between the condition and brace.  \nWithin the condition, no space should be between the parenthesis and variables.  \nSpaces between variables and operators are up to the authors discretion.\nThere should be no space between function names and their argument parenthesis.\n\nGood:  \n```cpp\nif (a == b) {\n  doStuff(a);\n}\n```\n\nNot good:  \n```cpp\nif( a==b ){\n  doStuff ( a);\n}\n```\n\n#### Comments\n\nComments should have a space between the delimiting characters (e.g. `//`) and the comment text.\nWe're gradually adopting this style - don't worry if you see older code without spaces!\n\nGood:  \n```cpp\n// This is a short inline comment.\n\n/* \n * This is a longer comment\n * wrapping over multiple lines,\n * used in WLED for file headers and function explanations\n */\n```\n```css\n/* This is a CSS inline comment */\n```\n```html\n<!-- This is an HTML comment -->\n```\n\nThere is no hard character limit for a comment within a line,\nthough as a rule of thumb consider wrapping after 120 characters.\nInline comments are OK if they describe that line only and are not exceedingly wide.\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2016-present Christian Schwinne and individual WLED contributors\nLicensed under the EUPL v. 1.2 or later\n\n                      EUROPEAN UNION PUBLIC LICENCE v. 1.2\n                      EUPL © the European Union 2007, 2016\n\nThis European Union Public Licence (the ‘EUPL’) applies to the Work (as\ndefined below) which is provided under the terms of this Licence. Any use of\nthe Work, other than as authorised under this Licence is prohibited (to the\nextent such use is covered by a right of the copyright holder of the Work).\n\nThe Work is provided under the terms of this Licence when the Licensor (as\ndefined below) has placed the following notice immediately following the\ncopyright notice for the Work:\n\n        Licensed under the EUPL\n\nor has expressed by any other means his willingness to license under the EUPL.\n\n1. Definitions\n\nIn this Licence, the following terms have the following meaning:\n\n- ‘The Licence’: this Licence.\n\n- ‘The Original Work’: the work or software distributed or communicated by the\n  Licensor under this Licence, available as Source Code and also as Executable\n  Code as the case may be.\n\n- ‘Derivative Works’: the works or software that could be created by the\n  Licensee, based upon the Original Work or modifications thereof. This\n  Licence does not define the extent of modification or dependence on the\n  Original Work required in order to classify a work as a Derivative Work;\n  this extent is determined by copyright law applicable in the country\n  mentioned in Article 15.\n\n- ‘The Work’: the Original Work or its Derivative Works.\n\n- ‘The Source Code’: the human-readable form of the Work which is the most\n  convenient for people to study and modify.\n\n- ‘The Executable Code’: any code which has generally been compiled and which\n  is meant to be interpreted by a computer as a program.\n\n- ‘The Licensor’: the natural or legal person that distributes or communicates\n  the Work under the Licence.\n\n- ‘Contributor(s)’: any natural or legal person who modifies the Work under\n  the Licence, or otherwise contributes to the creation of a Derivative Work.\n\n- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of\n  the Work under the terms of the Licence.\n\n- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending,\n  renting, distributing, communicating, transmitting, or otherwise making\n  available, online or offline, copies of the Work or providing access to its\n  essential functionalities at the disposal of any other natural or legal\n  person.\n\n2. Scope of the rights granted by the Licence\n\nThe Licensor hereby grants You a worldwide, royalty-free, non-exclusive,\nsublicensable licence to do the following, for the duration of copyright\nvested in the Original Work:\n\n- use the Work in any circumstance and for all usage,\n- reproduce the Work,\n- modify the Work, and make Derivative Works based upon the Work,\n- communicate to the public, including the right to make available or display\n  the Work or copies thereof to the public and perform publicly, as the case\n  may be, the Work,\n- distribute the Work or copies thereof,\n- lend and rent the Work or copies thereof,\n- sublicense rights in the Work or copies thereof.\n\nThose rights can be exercised on any media, supports and formats, whether now\nknown or later invented, as far as the applicable law permits so.\n\nIn the countries where moral rights apply, the Licensor waives his right to\nexercise his moral right to the extent allowed by law in order to make\neffective the licence of the economic rights here above listed.\n\nThe Licensor grants to the Licensee royalty-free, non-exclusive usage rights\nto any patents held by the Licensor, to the extent necessary to make use of\nthe rights granted on the Work under this Licence.\n\n3. Communication of the Source Code\n\nThe Licensor may provide the Work either in its Source Code form, or as\nExecutable Code. If the Work is provided as Executable Code, the Licensor\nprovides in addition a machine-readable copy of the Source Code of the Work\nalong with each copy of the Work that the Licensor distributes or indicates,\nin a notice following the copyright notice attached to the Work, a repository\nwhere the Source Code is easily and freely accessible for as long as the\nLicensor continues to distribute or communicate the Work.\n\n4. Limitations on copyright\n\nNothing in this Licence is intended to deprive the Licensee of the benefits\nfrom any exception or limitation to the exclusive rights of the rights owners\nin the Work, of the exhaustion of those rights or of other applicable\nlimitations thereto.\n\n5. Obligations of the Licensee\n\nThe grant of the rights mentioned above is subject to some restrictions and\nobligations imposed on the Licensee. Those obligations are the following:\n\nAttribution right: The Licensee shall keep intact all copyright, patent or\ntrademarks notices and all notices that refer to the Licence and to the\ndisclaimer of warranties. The Licensee must include a copy of such notices and\na copy of the Licence with every copy of the Work he/she distributes or\ncommunicates. The Licensee must cause any Derivative Work to carry prominent\nnotices stating that the Work has been modified and the date of modification.\n\nCopyleft clause: If the Licensee distributes or communicates copies of the\nOriginal Works or Derivative Works, this Distribution or Communication will be\ndone under the terms of this Licence or of a later version of this Licence\nunless the Original Work is expressly distributed only under this version of\nthe Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee\n(becoming Licensor) cannot offer or impose any additional terms or conditions\non the Work or Derivative Work that alter or restrict the terms of the\nLicence.\n\nCompatibility clause: If the Licensee Distributes or Communicates Derivative\nWorks or copies thereof based upon both the Work and another work licensed\nunder a Compatible Licence, this Distribution or Communication can be done\nunder the terms of this Compatible Licence. For the sake of this clause,\n‘Compatible Licence’ refers to the licences listed in the appendix attached to\nthis Licence. Should the Licensee's obligations under the Compatible Licence\nconflict with his/her obligations under this Licence, the obligations of the\nCompatible Licence shall prevail.\n\nProvision of Source Code: When distributing or communicating copies of the\nWork, the Licensee will provide a machine-readable copy of the Source Code or\nindicate a repository where this Source will be easily and freely available\nfor as long as the Licensee continues to distribute or communicate the Work.\n\nLegal Protection: This Licence does not grant permission to use the trade\nnames, trademarks, service marks, or names of the Licensor, except as required\nfor reasonable and customary use in describing the origin of the Work and\nreproducing the content of the copyright notice.\n\n6. Chain of Authorship\n\nThe original Licensor warrants that the copyright in the Original Work granted\nhereunder is owned by him/her or licensed to him/her and that he/she has the\npower and authority to grant the Licence.\n\nEach Contributor warrants that the copyright in the modifications he/she\nbrings to the Work are owned by him/her or licensed to him/her and that he/she\nhas the power and authority to grant the Licence.\n\nEach time You accept the Licence, the original Licensor and subsequent\nContributors grant You a licence to their contributions to the Work, under the\nterms of this Licence.\n\n7. Disclaimer of Warranty\n\nThe Work is a work in progress, which is continuously improved by numerous\nContributors. It is not a finished work and may therefore contain defects or\n‘bugs’ inherent to this type of development.\n\nFor the above reason, the Work is provided under the Licence on an ‘as is’\nbasis and without warranties of any kind concerning the Work, including\nwithout limitation merchantability, fitness for a particular purpose, absence\nof defects or errors, accuracy, non-infringement of intellectual property\nrights other than copyright as stated in Article 6 of this Licence.\n\nThis disclaimer of warranty is an essential part of the Licence and a\ncondition for the grant of any rights to the Work.\n\n8. Disclaimer of Liability\n\nExcept in the cases of wilful misconduct or damages directly caused to natural\npersons, the Licensor will in no event be liable for any direct or indirect,\nmaterial or moral, damages of any kind, arising out of the Licence or of the\nuse of the Work, including without limitation, damages for loss of goodwill,\nwork stoppage, computer failure or malfunction, loss of data or any commercial\ndamage, even if the Licensor has been advised of the possibility of such\ndamage. However, the Licensor will be liable under statutory product liability\nlaws as far such laws apply to the Work.\n\n9. Additional agreements\n\nWhile distributing the Work, You may choose to conclude an additional\nagreement, defining obligations or services consistent with this Licence.\nHowever, if accepting obligations, You may act only on your own behalf and on\nyour sole responsibility, not on behalf of the original Licensor or any other\nContributor, and only if You agree to indemnify, defend, and hold each\nContributor harmless for any liability incurred by, or claims asserted against\nsuch Contributor by the fact You have accepted any warranty or additional\nliability.\n\n10. Acceptance of the Licence\n\nThe provisions of this Licence can be accepted by clicking on an icon ‘I\nagree’ placed under the bottom of a window displaying the text of this Licence\nor by affirming consent in any other similar way, in accordance with the rules\nof applicable law. Clicking on that icon indicates your clear and irrevocable\nacceptance of this Licence and all of its terms and conditions.\n\nSimilarly, you irrevocably accept this Licence and all of its terms and\nconditions by exercising any rights granted to You by Article 2 of this\nLicence, such as the use of the Work, the creation by You of a Derivative Work\nor the Distribution or Communication by You of the Work or copies thereof.\n\n11. Information to the public\n\nIn case of any Distribution or Communication of the Work by means of\nelectronic communication by You (for example, by offering to download the Work\nfrom a remote location) the distribution channel or media (for example, a\nwebsite) must at least provide to the public the information requested by the\napplicable law regarding the Licensor, the Licence and the way it may be\naccessible, concluded, stored and reproduced by the Licensee.\n\n12. Termination of the Licence\n\nThe Licence and the rights granted hereunder will terminate automatically upon\nany breach by the Licensee of the terms of the Licence.\n\nSuch a termination will not terminate the licences of any person who has\nreceived the Work from the Licensee under the Licence, provided such persons\nremain in full compliance with the Licence.\n\n13. Miscellaneous\n\nWithout prejudice of Article 9 above, the Licence represents the complete\nagreement between the Parties as to the Work.\n\nIf any provision of the Licence is invalid or unenforceable under applicable\nlaw, this will not affect the validity or enforceability of the Licence as a\nwhole. Such provision will be construed or reformed so as necessary to make it\nvalid and enforceable.\n\nThe European Commission may publish other linguistic versions or new versions\nof this Licence or updated versions of the Appendix, so far this is required\nand reasonable, without reducing the scope of the rights granted by the\nLicence. New versions of the Licence will be published with a unique version\nnumber.\n\nAll linguistic versions of this Licence, approved by the European Commission,\nhave identical value. Parties can take advantage of the linguistic version of\ntheir choice.\n\n14. Jurisdiction\n\nWithout prejudice to specific agreement between parties,\n\n- any litigation resulting from the interpretation of this License, arising\n  between the European Union institutions, bodies, offices or agencies, as a\n  Licensor, and any Licensee, will be subject to the jurisdiction of the Court\n  of Justice of the European Union, as laid down in article 272 of the Treaty\n  on the Functioning of the European Union,\n\n- any litigation arising between other parties and resulting from the\n  interpretation of this License, will be subject to the exclusive\n  jurisdiction of the competent court where the Licensor resides or conducts\n  its primary business.\n\n15. Applicable Law\n\nWithout prejudice to specific agreement between parties,\n\n- this Licence shall be governed by the law of the European Union Member State\n  where the Licensor has his seat, resides or has his registered office,\n\n- this licence shall be governed by Belgian law if the Licensor has no seat,\n  residence or registered office inside a European Union Member State.\n\nAppendix\n\n‘Compatible Licences’ according to Article 5 EUPL are:\n\n- GNU General Public License (GPL) v. 2, v. 3\n- GNU Affero General Public License (AGPL) v. 3\n- Open Software License (OSL) v. 2.1, v. 3.0\n- Eclipse Public License (EPL) v. 1.0\n- CeCILL v. 2.0, v. 2.1\n- Mozilla Public Licence (MPL) v. 2\n- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3\n- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for\n  works other than software\n- European Union Public Licence (EUPL) v. 1.1, v. 1.2\n- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong\n  Reciprocity (LiLiQ-R+).\n\nThe European Commission may update this Appendix to later versions of the\nabove licences without producing a new version of the EUPL, as long as they\nprovide the rights granted in Article 2 of this Licence and protect the\ncovered Source Code from exclusive appropriation.\n\nAll other changes or additions to this Appendix require the production of a\nnew EUPL version."
  },
  {
    "path": "boards/adafruit_matrixportal_esp32s3_wled.json",
    "content": "{\n  \"build\": {\n    \"arduino\":{\n      \"ldscript\": \"esp32s3_out.ld\",\n      \"partitions\": \"default_8MB.csv\"\n    },\n    \"core\": \"esp32\",\n    \"extra_flags\": [\n      \"-DARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3\",\n      \"-DARDUINO_USB_CDC_ON_BOOT=1\",\n      \"-DARDUINO_RUNNING_CORE=1\",\n      \"-DARDUINO_EVENT_RUNNING_CORE=1\",\n      \"-DBOARD_HAS_PSRAM\"\n    ],\n    \"f_cpu\": \"240000000L\",\n    \"f_flash\": \"80000000L\",\n    \"flash_mode\": \"qio\",\n    \"hwids\": [\n      [\n        \"0x239A\",\n        \"0x8125\"\n      ],\n      [\n        \"0x239A\",\n        \"0x0125\"\n      ],\n      [\n        \"0x239A\",\n        \"0x8126\"\n      ]\n    ],\n    \"mcu\": \"esp32s3\",\n    \"variant\": \"adafruit_matrixportal_esp32s3\"\n  },\n  \"connectivity\": [\n    \"bluetooth\",\n    \"wifi\"\n  ],\n  \"debug\": {\n    \"openocd_target\": \"esp32s3.cfg\"\n  },\n  \"frameworks\": [\n    \"arduino\",\n    \"espidf\"\n  ],\n  \"name\": \"Adafruit MatrixPortal ESP32-S3 for WLED\",\n  \"upload\": {\n    \"flash_size\": \"8MB\",\n    \"maximum_ram_size\": 327680,\n    \"maximum_size\": 8388608,\n    \"use_1200bps_touch\": true,\n    \"wait_for_upload_port\": true,\n    \"require_upload_port\": true,\n    \"speed\": 460800\n  },\n  \"url\": \"https://www.adafruit.com/product/5778\",\n  \"vendor\": \"Adafruit\"\n}\n"
  },
  {
    "path": "boards/lilygo-t7-s3.json",
    "content": "{\n    \"build\": {\n        \"arduino\":{\n            \"ldscript\": \"esp32s3_out.ld\",\n            \"memory_type\": \"qio_opi\",\n            \"partitions\": \"default_16MB.csv\"\n        },\n        \"core\": \"esp32\",\n        \"extra_flags\": [\n\t\t\t\"-DARDUINO_TTGO_T7_S3\",\n            \"-DBOARD_HAS_PSRAM\",\n            \"-DARDUINO_USB_MODE=1\"\n        ],\n        \"f_cpu\": \"240000000L\",\n        \"f_flash\": \"80000000L\",\n        \"flash_mode\": \"qio\",\n        \"hwids\": [\n            [\n                \"0X303A\",\n                \"0x1001\"\n            ]\n        ],\n        \"mcu\": \"esp32s3\",\n        \"variant\": \"esp32s3\"\n    },\n    \"connectivity\": [\n        \"wifi\",\n        \"bluetooth\"\n    ],\n    \"debug\": {\n        \"openocd_target\": \"esp32s3.cfg\"\n    },\n    \"frameworks\": [\n        \"arduino\",\n        \"espidf\"\n    ],\n    \"name\": \"LILYGO T3-S3\",\n    \"upload\": {\n        \"flash_size\": \"16MB\",\n        \"maximum_ram_size\": 327680,\n        \"maximum_size\": 16777216,\n        \"require_upload_port\": true,\n        \"speed\": 921600\n    },\n    \"url\": \"https://www.aliexpress.us/item/3256804591247074.html\",\n    \"vendor\": \"LILYGO\"\n}"
  },
  {
    "path": "boards/lolin_s3_mini.json",
    "content": "{\r\n    \"build\": {\r\n      \"arduino\": {\r\n        \"ldscript\": \"esp32s3_out.ld\",\r\n        \"memory_type\": \"qio_qspi\"\r\n      },\r\n      \"core\": \"esp32\",\r\n      \"extra_flags\": [\r\n        \"-DBOARD_HAS_PSRAM\",\r\n        \"-DARDUINO_LOLIN_S3_MINI\",\r\n        \"-DARDUINO_USB_MODE=1\"\r\n      ],\r\n      \"f_cpu\": \"240000000L\",\r\n      \"f_flash\": \"80000000L\",\r\n      \"flash_mode\": \"qio\",\r\n      \"hwids\": [\r\n        [\r\n          \"0x303A\",\r\n          \"0x8167\"\r\n        ]\r\n      ],\r\n      \"mcu\": \"esp32s3\",\r\n      \"variant\": \"lolin_s3_mini\"\r\n    },\r\n    \"connectivity\": [\r\n      \"bluetooth\",\r\n      \"wifi\"\r\n    ],\r\n    \"debug\": {\r\n      \"openocd_target\": \"esp32s3.cfg\"\r\n    },\r\n    \"frameworks\": [\r\n      \"arduino\",\r\n      \"espidf\"\r\n    ],\r\n    \"name\": \"WEMOS LOLIN S3 Mini\",\r\n    \"upload\": {\r\n      \"flash_size\": \"4MB\",\r\n      \"maximum_ram_size\": 327680,\r\n      \"maximum_size\": 4194304,\r\n      \"require_upload_port\": true,\r\n      \"speed\": 460800\r\n    },\r\n    \"url\": \"https://www.wemos.cc/en/latest/s3/index.html\",\r\n    \"vendor\": \"WEMOS\"\r\n}\r\n  "
  },
  {
    "path": "images/Readme.md",
    "content": "### Additional Logos\n\nAdditional awesome logos for WLED can be found here [Aircoookie/Akemi](https://github.com/Aircoookie/Akemi).\n\n<img src=\"https://github.com/Aircoookie/Akemi/blob/master/akemi/001_cheerful.png\">\n"
  },
  {
    "path": "include/README",
    "content": "\nThis directory is intended for project header files.\n\nA header file is a file containing C declarations and macro definitions\nto be shared between several project source files. You request the use of a\nheader file in your project source file (C, C++, etc) located in `src` folder\nby including it, with the C preprocessing directive `#include'.\n\n```src/main.c\n\n#include \"header.h\"\n\nint main (void)\n{\n ...\n}\n```\n\nIncluding a header file produces the same results as copying the header file\ninto each source file that needs it. Such copying would be time-consuming\nand error-prone. With a header file, the related declarations appear\nin only one place. If they need to be changed, they can be changed in one\nplace, and programs that include the header file will automatically use the\nnew version when next recompiled. The header file eliminates the labor of\nfinding and changing all the copies as well as the risk that a failure to\nfind one copy will result in inconsistencies within a program.\n\nIn C, the usual convention is to give header files names that end with `.h'.\nIt is most portable to use only letters, digits, dashes, and underscores in\nheader file names, and at most one dot.\n\nRead more about using header files in official GCC documentation:\n\n* Include Syntax\n* Include Operation\n* Once-Only Headers\n* Computed Includes\n\nhttps://gcc.gnu.org/onlinedocs/cpp/Header-Files.html\n"
  },
  {
    "path": "lib/ESP8266PWM/src/core_esp8266_waveform_phase.cpp",
    "content": "/* esp8266_waveform imported from platform source code\n   Modified for WLED to work around a fault in the NMI handling,\n   which can result in the system locking up and hard WDT crashes.\n\n   Imported from https://github.com/esp8266/Arduino/blob/7e0d20e2b9034994f573a236364e0aef17fd66de/cores/esp8266/core_esp8266_waveform_phase.cpp\n*/\n\n\n/*\n  esp8266_waveform - General purpose waveform generation and control,\n                     supporting outputs on all pins in parallel.\n\n  Copyright (c) 2018 Earle F. Philhower, III.  All rights reserved.\n  Copyright (c) 2020 Dirk O. Kaar.\n\n  The core idea is to have a programmable waveform generator with a unique\n  high and low period (defined in microseconds or CPU clock cycles).  TIMER1 is\n  set to 1-shot mode and is always loaded with the time until the next edge\n  of any live waveforms.\n\n  Up to one waveform generator per pin supported.\n\n  Each waveform generator is synchronized to the ESP clock cycle counter, not the\n  timer.  This allows for removing interrupt jitter and delay as the counter\n  always increments once per 80MHz clock.  Changes to a waveform are\n  contiguous and only take effect on the next waveform transition,\n  allowing for smooth transitions.\n\n  This replaces older tone(), analogWrite(), and the Servo classes.\n\n  Everywhere in the code where \"ccy\" or \"ccys\" is used, it means ESP.getCycleCount()\n  clock cycle time, or an interval measured in clock cycles, but not TIMER1\n  cycles (which may be 2 CPU clock cycles @ 160MHz).\n\n  This library is free software; you can redistribute it and/or\n  modify it under the terms of the GNU Lesser General Public\n  License as published by the Free Software Foundation; either\n  version 2.1 of the License, or (at your option) any later version.\n\n  This library is distributed in the hope that it will be useful,\n  but WITHOUT ANY WARRANTY; without even the implied warranty of\n  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n  Lesser General Public License for more details.\n\n  You should have received a copy of the GNU Lesser General Public\n  License along with this library; if not, write to the Free Software\n  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA\n*/\n\n#include \"core_esp8266_waveform.h\"\n#include <Arduino.h>\n#include \"debug.h\"\n#include \"ets_sys.h\"\n#include <atomic>\n\n\n// ----- @willmmiles begin patch -----\n// Linker magic\nextern \"C\" void usePWMFixedNMI(void) {};\n\n// NMI crash workaround\n// Sometimes the NMI fails to return, stalling the CPU.  When this happens,\n// the next NMI gets a return address /inside the NMI handler function/.\n// We work around this by caching the last NMI return address, and restoring\n// the epc3 and eps3 registers to the previous values if the observed epc3\n// happens to be pointing to the _NMILevelVector function.\nextern \"C\" void _NMILevelVector();\nextern \"C\" void _UserExceptionVector_1(); // the next function after _NMILevelVector\nstatic inline IRAM_ATTR void nmiCrashWorkaround() {\n  static uintptr_t epc3_backup, eps3_backup;\n\n  uintptr_t epc3, eps3;\n  __asm__ __volatile__(\"rsr %0,epc3; rsr %1,eps3\":\"=a\"(epc3),\"=a\" (eps3));\n  if ((epc3 < (uintptr_t) &_NMILevelVector) || (epc3 >= (uintptr_t) &_UserExceptionVector_1)) {\n    // Address is good; save backup\n    epc3_backup = epc3;\n    eps3_backup = eps3;\n  } else {\n    // Address is inside the NMI handler -- restore from backup\n    __asm__ __volatile__(\"wsr %0,epc3; wsr %1,eps3\"::\"a\"(epc3_backup),\"a\"(eps3_backup));\n  }\n}\n// ----- @willmmiles end patch -----\n\n\n// No-op calls to override the PWM implementation\nextern \"C\" void _setPWMFreq_weak(uint32_t freq) { (void) freq; }\nextern \"C\" IRAM_ATTR bool _stopPWM_weak(int pin) { (void) pin; return false; }\nextern \"C\" bool _setPWM_weak(int pin, uint32_t val, uint32_t range) { (void) pin; (void) val; (void) range; return false; }\n\n\n// Timer is 80MHz fixed. 160MHz CPU frequency need scaling.\nconstexpr bool ISCPUFREQ160MHZ = clockCyclesPerMicrosecond() == 160;\n// Maximum delay between IRQs, Timer1, <= 2^23 / 80MHz\nconstexpr int32_t MAXIRQTICKSCCYS = microsecondsToClockCycles(10000);\n// Maximum servicing time for any single IRQ\nconstexpr uint32_t ISRTIMEOUTCCYS = microsecondsToClockCycles(18);\n// The latency between in-ISR rearming of the timer and the earliest firing\nconstexpr int32_t IRQLATENCYCCYS = microsecondsToClockCycles(2);\n// The SDK and hardware take some time to actually get to our NMI code\nconstexpr int32_t DELTAIRQCCYS = ISCPUFREQ160MHZ ?\n  microsecondsToClockCycles(2) >> 1 : microsecondsToClockCycles(2);\n\n// for INFINITE, the NMI proceeds on the waveform without expiry deadline.\n// for EXPIRES, the NMI expires the waveform automatically on the expiry ccy.\n// for UPDATEEXPIRY, the NMI recomputes the exact expiry ccy and transitions to EXPIRES.\n// for UPDATEPHASE, the NMI recomputes the target timings\n// for INIT, the NMI initializes nextPeriodCcy, and if expiryCcy != 0 includes UPDATEEXPIRY.\nenum class WaveformMode : uint8_t {INFINITE = 0, EXPIRES = 1, UPDATEEXPIRY = 2, UPDATEPHASE = 3, INIT = 4};\n\n// Waveform generator can create tones, PWM, and servos\ntypedef struct {\n  uint32_t nextPeriodCcy; // ESP clock cycle when a period begins.\n  uint32_t endDutyCcy;    // ESP clock cycle when going from duty to off\n  int32_t dutyCcys;       // Set next off cycle at low->high to maintain phase\n  int32_t adjDutyCcys;    // Temporary correction for next period\n  int32_t periodCcys;     // Set next phase cycle at low->high to maintain phase\n  uint32_t expiryCcy;     // For time-limited waveform, the CPU clock cycle when this waveform must stop. If WaveformMode::UPDATE, temporarily holds relative ccy count\n  WaveformMode mode;\n  bool autoPwm;           // perform PWM duty to idle cycle ratio correction under high load at the expense of precise timings\n} Waveform;\n\nnamespace {\n\n  static struct {\n    Waveform pins[17];             // State of all possible pins\n    uint32_t states = 0;           // Is the pin high or low, updated in NMI so no access outside the NMI code\n    uint32_t enabled = 0; // Is it actively running, updated in NMI so no access outside the NMI code\n\n    // Enable lock-free by only allowing updates to waveform.states and waveform.enabled from IRQ service routine\n    int32_t toSetBits = 0;     // Message to the NMI handler to start/modify exactly one waveform\n    int32_t toDisableBits = 0; // Message to the NMI handler to disable exactly one pin from waveform generation\n\n    // toSetBits temporaries\n    // cheaper than packing them in every Waveform, since we permit only one use at a time\n    uint32_t phaseCcy;      // positive phase offset ccy count  \n    int8_t alignPhase;      // < 0 no phase alignment, otherwise starts waveform in relative phase offset to given pin\n\n    uint32_t(*timer1CB)() = nullptr;\n\n    bool timer1Running = false;\n\n    uint32_t nextEventCcy;\n  } waveform;\n\n}\n\n// Interrupt on/off control\nstatic IRAM_ATTR void timer1Interrupt();\n\n// Non-speed critical bits\n#pragma GCC optimize (\"Os\")\n\nstatic void initTimer() {\n  timer1_disable();\n  ETS_FRC_TIMER1_INTR_ATTACH(NULL, NULL);\n  ETS_FRC_TIMER1_NMI_INTR_ATTACH(timer1Interrupt);\n  timer1_enable(TIM_DIV1, TIM_EDGE, TIM_SINGLE);\n  waveform.timer1Running = true;\n  timer1_write(IRQLATENCYCCYS); // Cause an interrupt post-haste\n}\n\nstatic void IRAM_ATTR deinitTimer() {\n  ETS_FRC_TIMER1_NMI_INTR_ATTACH(NULL);\n  timer1_disable();\n  timer1_isr_init();\n  waveform.timer1Running = false;\n}\n\nextern \"C\" {\n\n// Set a callback.  Pass in NULL to stop it\nvoid setTimer1Callback_weak(uint32_t (*fn)()) {\n  waveform.timer1CB = fn;\n  std::atomic_thread_fence(std::memory_order_acq_rel);\n  if (!waveform.timer1Running && fn) {\n    initTimer();\n  } else if (waveform.timer1Running && !fn && !waveform.enabled) {\n    deinitTimer();\n  }\n}\n\n// Start up a waveform on a pin, or change the current one.  Will change to the new\n// waveform smoothly on next low->high transition.  For immediate change, stopWaveform()\n// first, then it will immediately begin.\nint startWaveformClockCycles_weak(uint8_t pin, uint32_t highCcys, uint32_t lowCcys,\n  uint32_t runTimeCcys, int8_t alignPhase, uint32_t phaseOffsetCcys, bool autoPwm) {\n  uint32_t periodCcys = highCcys + lowCcys;\n  if (periodCcys < MAXIRQTICKSCCYS) {\n    if (!highCcys) {\n      periodCcys = (MAXIRQTICKSCCYS / periodCcys) * periodCcys;\n    }\n    else if (!lowCcys) {\n      highCcys = periodCcys = (MAXIRQTICKSCCYS / periodCcys) * periodCcys;\n    }\n  }\n  // sanity checks, including mixed signed/unsigned arithmetic safety\n  if ((pin > 16) || isFlashInterfacePin(pin) || (alignPhase > 16) ||\n    static_cast<int32_t>(periodCcys) <= 0 ||\n    static_cast<int32_t>(highCcys) < 0 || static_cast<int32_t>(lowCcys) < 0) {\n    return false;\n  }\n  Waveform& wave = waveform.pins[pin];\n  wave.dutyCcys = highCcys;\n  wave.adjDutyCcys = 0;\n  wave.periodCcys = periodCcys;\n  wave.autoPwm = autoPwm;\n  waveform.alignPhase = (alignPhase < 0) ? -1 : alignPhase;\n  waveform.phaseCcy = phaseOffsetCcys;\n\n  std::atomic_thread_fence(std::memory_order_acquire);\n  const uint32_t pinBit = 1UL << pin;\n  if (!(waveform.enabled & pinBit)) {\n    // wave.nextPeriodCcy and wave.endDutyCcy are initialized by the ISR\n    wave.expiryCcy = runTimeCcys; // in WaveformMode::INIT, temporarily hold relative cycle count\n    wave.mode = WaveformMode::INIT;\n    if (!wave.dutyCcys) {\n      // If initially at zero duty cycle, force GPIO off\n      if (pin == 16) {\n        GP16O = 0;\n      }\n      else {\n        GPOC = pinBit;\n      }\n    }\n    std::atomic_thread_fence(std::memory_order_release);\n    waveform.toSetBits = 1UL << pin;\n    std::atomic_thread_fence(std::memory_order_release);\n    if (!waveform.timer1Running) {\n      initTimer();\n    }\n    else if (T1V > IRQLATENCYCCYS) {\n      // Must not interfere if Timer is due shortly\n      timer1_write(IRQLATENCYCCYS);\n    }\n  }\n  else {\n    wave.mode = WaveformMode::INFINITE; // turn off possible expiry to make update atomic from NMI\n    std::atomic_thread_fence(std::memory_order_release);\n    if (runTimeCcys) {\n      wave.expiryCcy = runTimeCcys; // in WaveformMode::UPDATEEXPIRY, temporarily hold relative cycle count\n      wave.mode = WaveformMode::UPDATEEXPIRY;\n      std::atomic_thread_fence(std::memory_order_release);\n      waveform.toSetBits = 1UL << pin;\n    } else if (alignPhase >= 0) {\n      // @willmmiles new feature\n      wave.mode = WaveformMode::UPDATEPHASE; // recalculate start\n      std::atomic_thread_fence(std::memory_order_release);\n      waveform.toSetBits = 1UL << pin;\n    }\n  }\n  std::atomic_thread_fence(std::memory_order_acq_rel);\n  while (waveform.toSetBits) {\n    esp_yield(); // Wait for waveform to update\n    std::atomic_thread_fence(std::memory_order_acquire);\n  }\n  return true;\n}\n\n// Stops a waveform on a pin\nIRAM_ATTR int stopWaveform_weak(uint8_t pin) {\n  // Can't possibly need to stop anything if there is no timer active\n  if (!waveform.timer1Running) {\n    return false;\n  }\n  // If user sends in a pin >16 but <32, this will always point to a 0 bit\n  // If they send >=32, then the shift will result in 0 and it will also return false\n  std::atomic_thread_fence(std::memory_order_acquire);\n  const uint32_t pinBit = 1UL << pin;\n  if (waveform.enabled & pinBit) {\n    waveform.toDisableBits = 1UL << pin;\n    std::atomic_thread_fence(std::memory_order_release);\n    // Must not interfere if Timer is due shortly\n    if (T1V > IRQLATENCYCCYS) {\n      timer1_write(IRQLATENCYCCYS);\n    }\n    while (waveform.toDisableBits) {\n      /* no-op */ // Can't delay() since stopWaveform may be called from an IRQ\n      std::atomic_thread_fence(std::memory_order_acquire);\n    }\n  }\n  if (!waveform.enabled && !waveform.timer1CB) {\n    deinitTimer();\n  }\n  return true;\n}\n\n};\n\n// Speed critical bits\n#pragma GCC optimize (\"O2\")\n\n// For dynamic CPU clock frequency switch in loop the scaling logic would have to be adapted.\n// Using constexpr makes sure that the CPU clock frequency is compile-time fixed.\nstatic inline IRAM_ATTR int32_t scaleCcys(const int32_t ccys, const bool isCPU2X) {\n  if (ISCPUFREQ160MHZ) {\n    return isCPU2X ? ccys : (ccys >> 1);\n  }\n  else {\n    return isCPU2X ? (ccys << 1) : ccys;\n  }\n}\n\nstatic IRAM_ATTR void timer1Interrupt() {\n  const uint32_t isrStartCcy = ESP.getCycleCount();\n  //int32_t clockDrift = isrStartCcy - waveform.nextEventCcy;\n\n  // ----- @willmmiles begin patch -----\n  nmiCrashWorkaround();\n  // ----- @willmmiles end patch -----\n\n  const bool isCPU2X = CPU2X & 1;\n  if ((waveform.toSetBits && !(waveform.enabled & waveform.toSetBits)) || waveform.toDisableBits) {\n    // Handle enable/disable requests from main app.\n    waveform.enabled = (waveform.enabled & ~waveform.toDisableBits) | waveform.toSetBits; // Set the requested waveforms on/off\n    // 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)\n    waveform.toDisableBits = 0;\n  }\n\n  if (waveform.toSetBits) {\n    const int toSetPin = __builtin_ffs(waveform.toSetBits) - 1;\n    Waveform& wave = waveform.pins[toSetPin];\n    switch (wave.mode) {\n    case WaveformMode::INIT:\n      waveform.states &= ~waveform.toSetBits; // Clear the state of any just started\n      if (waveform.alignPhase >= 0 && waveform.enabled & (1UL << waveform.alignPhase)) {\n        wave.nextPeriodCcy = waveform.pins[waveform.alignPhase].nextPeriodCcy + scaleCcys(waveform.phaseCcy, isCPU2X);\n      }\n      else {\n        wave.nextPeriodCcy = waveform.nextEventCcy;\n      }\n      if (!wave.expiryCcy) {\n        wave.mode = WaveformMode::INFINITE;\n        break;\n      }\n      // fall through\n    case WaveformMode::UPDATEEXPIRY:\n      // in WaveformMode::UPDATEEXPIRY, expiryCcy temporarily holds relative CPU cycle count\n      wave.expiryCcy = wave.nextPeriodCcy + scaleCcys(wave.expiryCcy, isCPU2X);\n      wave.mode = WaveformMode::EXPIRES;\n      break;\n    // @willmmiles new feature\n    case WaveformMode::UPDATEPHASE:\n      // in WaveformMode::UPDATEPHASE, we recalculate the targets\n      if ((waveform.alignPhase >= 0) && (waveform.enabled & (1UL << waveform.alignPhase))) {\n        // Compute phase shift to realign with target\n        auto const newPeriodCcy = waveform.pins[waveform.alignPhase].nextPeriodCcy + scaleCcys(waveform.phaseCcy, isCPU2X);\n        auto const period = scaleCcys(wave.periodCcys, isCPU2X);\n        auto shift = ((static_cast<int32_t> (newPeriodCcy - wave.nextPeriodCcy) + period/2) % period) - (period/2);\n        wave.nextPeriodCcy += static_cast<uint32_t>(shift);\n        if (static_cast<int32_t>(wave.endDutyCcy - wave.nextPeriodCcy) > 0) {\n          wave.endDutyCcy = wave.nextPeriodCcy;\n        }\n      }\n    default:\n      break;\n    }\n    waveform.toSetBits = 0;\n  }\n\n  // Exit the loop if the next event, if any, is sufficiently distant.\n  const uint32_t isrTimeoutCcy = isrStartCcy + ISRTIMEOUTCCYS;\n  uint32_t busyPins = waveform.enabled;\n  waveform.nextEventCcy = isrStartCcy + MAXIRQTICKSCCYS;\n\n  uint32_t now = ESP.getCycleCount();\n  uint32_t isrNextEventCcy = now;\n  while (busyPins) {\n    if (static_cast<int32_t>(isrNextEventCcy - now) > IRQLATENCYCCYS) {\n      waveform.nextEventCcy = isrNextEventCcy;\n      break;\n    }\n    isrNextEventCcy = waveform.nextEventCcy;\n    uint32_t loopPins = busyPins;\n    while (loopPins) {\n      const int pin = __builtin_ffsl(loopPins) - 1;\n      const uint32_t pinBit = 1UL << pin;\n      loopPins ^= pinBit;\n\n      Waveform& wave = waveform.pins[pin];\n\n/* @willmmiles - wtf?  We don't want to accumulate drift\n      if (clockDrift) {\n        wave.endDutyCcy += clockDrift;\n        wave.nextPeriodCcy += clockDrift;\n        wave.expiryCcy += clockDrift;\n      }\n*/          \n\n      uint32_t waveNextEventCcy = (waveform.states & pinBit) ? wave.endDutyCcy : wave.nextPeriodCcy;\n      if (WaveformMode::EXPIRES == wave.mode &&\n        static_cast<int32_t>(waveNextEventCcy - wave.expiryCcy) >= 0 &&\n        static_cast<int32_t>(now - wave.expiryCcy) >= 0) {\n        // Disable any waveforms that are done\n        waveform.enabled ^= pinBit;\n        busyPins ^= pinBit;\n      }\n      else {\n        const int32_t overshootCcys = now - waveNextEventCcy;\n        if (overshootCcys >= 0) {\n          const int32_t periodCcys = scaleCcys(wave.periodCcys, isCPU2X);\n          if (waveform.states & pinBit) {\n            // active configuration and forward are 100% duty\n            if (wave.periodCcys == wave.dutyCcys) {\n              wave.nextPeriodCcy += periodCcys;\n              wave.endDutyCcy = wave.nextPeriodCcy;\n            }\n            else {\n              if (wave.autoPwm) {\n                wave.adjDutyCcys += overshootCcys;\n              }\n              waveform.states ^= pinBit;\n              if (16 == pin) {\n                GP16O = 0;\n              }\n              else {\n                GPOC = pinBit;\n              }\n            }\n            waveNextEventCcy = wave.nextPeriodCcy;\n          }\n          else {\n            wave.nextPeriodCcy += periodCcys;\n            if (!wave.dutyCcys) {\n              wave.endDutyCcy = wave.nextPeriodCcy;\n            }\n            else {\n              int32_t dutyCcys = scaleCcys(wave.dutyCcys, isCPU2X);\n              if (dutyCcys <= wave.adjDutyCcys) {\n                dutyCcys >>= 1;\n                wave.adjDutyCcys -= dutyCcys;\n              }\n              else if (wave.adjDutyCcys) {\n                dutyCcys -= wave.adjDutyCcys;\n                wave.adjDutyCcys = 0;\n              }\n              wave.endDutyCcy = now + dutyCcys;\n              if (static_cast<int32_t>(wave.endDutyCcy - wave.nextPeriodCcy) > 0) {\n                wave.endDutyCcy = wave.nextPeriodCcy;\n              }\n              waveform.states |= pinBit;\n              if (16 == pin) {\n                GP16O = 1;\n              }\n              else {\n                GPOS = pinBit;\n              }\n            }\n            waveNextEventCcy = wave.endDutyCcy;\n          }\n\n          if (WaveformMode::EXPIRES == wave.mode && static_cast<int32_t>(waveNextEventCcy - wave.expiryCcy) > 0) {\n            waveNextEventCcy = wave.expiryCcy;\n          }\n        }\n\n        if (static_cast<int32_t>(waveNextEventCcy - isrTimeoutCcy) >= 0) {\n          busyPins ^= pinBit;\n          if (static_cast<int32_t>(waveform.nextEventCcy - waveNextEventCcy) > 0) {\n            waveform.nextEventCcy = waveNextEventCcy;\n          }\n        }\n        else if (static_cast<int32_t>(isrNextEventCcy - waveNextEventCcy) > 0) {\n          isrNextEventCcy = waveNextEventCcy;\n        }\n      }\n      now = ESP.getCycleCount();\n    }\n    //clockDrift = 0;\n  }\n\n  int32_t callbackCcys = 0;\n  if (waveform.timer1CB) {\n    callbackCcys = scaleCcys(waveform.timer1CB(), isCPU2X);\n  }\n  now = ESP.getCycleCount();\n  int32_t nextEventCcys = waveform.nextEventCcy - now;\n  // Account for unknown duration of timer1CB().\n  if (waveform.timer1CB && nextEventCcys > callbackCcys) {\n    waveform.nextEventCcy = now + callbackCcys;\n    nextEventCcys = callbackCcys;\n  }\n\n  // Timer is 80MHz fixed. 160MHz CPU frequency need scaling.\n  int32_t deltaIrqCcys = DELTAIRQCCYS;\n  int32_t irqLatencyCcys = IRQLATENCYCCYS;\n  if (isCPU2X) {\n    nextEventCcys >>= 1;\n    deltaIrqCcys >>= 1;\n    irqLatencyCcys >>= 1;\n  }\n\n  // Firing timer too soon, the NMI occurs before ISR has returned.\n  if (nextEventCcys < irqLatencyCcys + deltaIrqCcys) {\n    waveform.nextEventCcy = now + IRQLATENCYCCYS + DELTAIRQCCYS;\n    nextEventCcys = irqLatencyCcys;\n  }\n  else {\n    nextEventCcys -= deltaIrqCcys;\n  }\n\n  // Register access is fast and edge IRQ was configured before.\n  T1L = nextEventCcys;\n}\n"
  },
  {
    "path": "lib/NeoESP32RmtHI/include/NeoEsp32RmtHIMethod.h",
    "content": "/*-------------------------------------------------------------------------\r\nNeoPixel driver for ESP32 RMTs using High-priority Interrupt\r\n\r\n(NB. This cannot be mixed with the non-HI driver.)\r\n\r\nWritten by Will M. Miles.\r\n\r\nI invest time and resources providing this open source code,\r\nplease support me by donating (see https://github.com/Makuna/NeoPixelBus)\r\n\r\n-------------------------------------------------------------------------\r\nThis file is part of the Makuna/NeoPixelBus library.\r\n\r\nNeoPixelBus is free software: you can redistribute it and/or modify\r\nit under the terms of the GNU Lesser General Public License as\r\npublished by the Free Software Foundation, either version 3 of\r\nthe License, or (at your option) any later version.\r\n\r\nNeoPixelBus is distributed in the hope that it will be useful,\r\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\r\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r\nGNU Lesser General Public License for more details.\r\n\r\nYou should have received a copy of the GNU Lesser General Public\r\nLicense along with NeoPixel.  If not, see\r\n<http://www.gnu.org/licenses/>.\r\n-------------------------------------------------------------------------*/\r\n\r\n#pragma once\r\n\r\n#if defined(ARDUINO_ARCH_ESP32)\r\n\r\n// Use the NeoEspRmtSpeed types from the driver-based implementation\r\n#include <NeoPixelBus.h>\r\n\r\n\r\nnamespace NeoEsp32RmtHiMethodDriver {\r\n    // Install the driver for a specific channel, specifying timing properties\r\n    esp_err_t Install(rmt_channel_t channel, uint32_t rmtBit0, uint32_t rmtBit1, uint32_t resetDuration);\r\n\r\n    // Remove the driver on a specific channel\r\n    esp_err_t Uninstall(rmt_channel_t channel);\r\n\r\n    // Write a buffer of data to a specific channel.\r\n    // Buffer reference is held until write completes.\r\n    esp_err_t Write(rmt_channel_t channel, const uint8_t *src, size_t src_size);\r\n\r\n    // Wait until transaction is complete.\r\n    esp_err_t WaitForTxDone(rmt_channel_t channel, TickType_t wait_time);\r\n};\r\n\r\ntemplate<typename T_SPEED, typename T_CHANNEL> class NeoEsp32RmtHIMethodBase\r\n{\r\npublic:\r\n    typedef NeoNoSettings SettingsObject;\r\n\r\n    NeoEsp32RmtHIMethodBase(uint8_t pin, uint16_t pixelCount, size_t elementSize, size_t settingsSize)  :\r\n        _sizeData(pixelCount * elementSize + settingsSize),\r\n        _pin(pin)\r\n    {\r\n        construct();\r\n    }\r\n\r\n    NeoEsp32RmtHIMethodBase(uint8_t pin, uint16_t pixelCount, size_t elementSize, size_t settingsSize, NeoBusChannel channel) :\r\n        _sizeData(pixelCount* elementSize + settingsSize),\r\n        _pin(pin),\r\n        _channel(channel)\r\n    {\r\n        construct();\r\n    }\r\n\r\n    ~NeoEsp32RmtHIMethodBase()\r\n    {\r\n        // wait until the last send finishes before destructing everything\r\n        // arbitrary time out of 10 seconds\r\n        ESP_ERROR_CHECK_WITHOUT_ABORT(NeoEsp32RmtHiMethodDriver::WaitForTxDone(_channel.RmtChannelNumber, 10000 / portTICK_PERIOD_MS));\r\n\r\n        ESP_ERROR_CHECK(NeoEsp32RmtHiMethodDriver::Uninstall(_channel.RmtChannelNumber));\r\n\r\n        gpio_matrix_out(_pin, SIG_GPIO_OUT_IDX, false, false);\r\n        pinMode(_pin, INPUT);\r\n\r\n        free(_dataEditing);\r\n        free(_dataSending);\r\n    }\r\n\r\n    bool IsReadyToUpdate() const\r\n    {\r\n        return (ESP_OK == ESP_ERROR_CHECK_WITHOUT_ABORT_SILENT_TIMEOUT(NeoEsp32RmtHiMethodDriver::WaitForTxDone(_channel.RmtChannelNumber, 0)));\r\n    }\r\n\r\n    void Initialize()\r\n    {\r\n        rmt_config_t config = {};\r\n\r\n        config.rmt_mode = RMT_MODE_TX;\r\n        config.channel = _channel.RmtChannelNumber;\r\n        config.gpio_num = static_cast<gpio_num_t>(_pin);\r\n        config.mem_block_num = 1;\r\n        config.tx_config.loop_en = false;\r\n\r\n        config.tx_config.idle_output_en = true;\r\n        config.tx_config.idle_level = T_SPEED::IdleLevel;\r\n\r\n        config.tx_config.carrier_en = false;\r\n        config.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW;\r\n\r\n        config.clk_div = T_SPEED::RmtClockDivider;\r\n\r\n        ESP_ERROR_CHECK(rmt_config(&config));   // Uses ESP library\r\n        ESP_ERROR_CHECK(NeoEsp32RmtHiMethodDriver::Install(_channel.RmtChannelNumber, T_SPEED::RmtBit0, T_SPEED::RmtBit1, T_SPEED::RmtDurationReset));\r\n    }\r\n\r\n    void Update(bool maintainBufferConsistency)\r\n    {\r\n        // wait for not actively sending data\r\n        // this will time out at 10 seconds, an arbitrarily long period of time\r\n        // and do nothing if this happens\r\n        if (ESP_OK == ESP_ERROR_CHECK_WITHOUT_ABORT(NeoEsp32RmtHiMethodDriver::WaitForTxDone(_channel.RmtChannelNumber, 10000 / portTICK_PERIOD_MS)))\r\n        {\r\n            // now start the RMT transmit with the editing buffer before we swap\r\n            ESP_ERROR_CHECK_WITHOUT_ABORT(NeoEsp32RmtHiMethodDriver::Write(_channel.RmtChannelNumber, _dataEditing, _sizeData));\r\n\r\n            if (maintainBufferConsistency)\r\n            {\r\n                // copy editing to sending,\r\n                // this maintains the contract that \"colors present before will\r\n                // be the same after\", otherwise GetPixelColor will be inconsistent\r\n                memcpy(_dataSending, _dataEditing, _sizeData);\r\n            }\r\n\r\n            // swap so the user can modify without affecting the async operation\r\n            std::swap(_dataSending, _dataEditing);\r\n        }\r\n    }\r\n\r\n    bool AlwaysUpdate()\r\n    {\r\n        // this method requires update to be called only if changes to buffer\r\n        return false;\r\n    }\r\n\r\n    bool SwapBuffers()\r\n    {\r\n        std::swap(_dataSending, _dataEditing);\r\n        return true;\r\n    }\r\n\r\n    uint8_t* getData() const\r\n    {\r\n        return _dataEditing;\r\n    };\r\n\r\n    size_t getDataSize() const\r\n    {\r\n        return _sizeData;\r\n    }\r\n\r\n    void applySettings([[maybe_unused]] const SettingsObject& settings)\r\n    {\r\n    }\r\n\r\nprivate:\r\n    const size_t  _sizeData;      // Size of '_data*' buffers\r\n    const uint8_t _pin;            // output pin number\r\n    const T_CHANNEL _channel; // holds instance for multi channel support\r\n\r\n    // Holds data stream which include LED color values and other settings as needed\r\n    uint8_t*  _dataEditing;   // exposed for get and set\r\n    uint8_t*  _dataSending;   // used for async send using RMT\r\n\r\n\r\n    void construct()\r\n    {\r\n        _dataEditing = static_cast<uint8_t*>(malloc(_sizeData));\r\n        // data cleared later in Begin()\r\n\r\n        _dataSending = static_cast<uint8_t*>(malloc(_sizeData));\r\n        // no need to initialize it, it gets overwritten on every send\r\n    }\r\n};\r\n\r\n// normal\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2811, NeoEsp32RmtChannelN> NeoEsp32RmtHINWs2811Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2812x, NeoEsp32RmtChannelN> NeoEsp32RmtHINWs2812xMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2812x, NeoEsp32RmtChannelN> NeoEsp32RmtHINWs2816Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2805, NeoEsp32RmtChannelN> NeoEsp32RmtHINWs2805Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedSk6812, NeoEsp32RmtChannelN> NeoEsp32RmtHINSk6812Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1814, NeoEsp32RmtChannelN> NeoEsp32RmtHINTm1814Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1829, NeoEsp32RmtChannelN> NeoEsp32RmtHINTm1829Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1914, NeoEsp32RmtChannelN> NeoEsp32RmtHINTm1914Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedApa106, NeoEsp32RmtChannelN> NeoEsp32RmtHINApa106Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTx1812, NeoEsp32RmtChannelN> NeoEsp32RmtHINTx1812Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedGs1903, NeoEsp32RmtChannelN> NeoEsp32RmtHINGs1903Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeed800Kbps, NeoEsp32RmtChannelN> NeoEsp32RmtHIN800KbpsMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeed400Kbps, NeoEsp32RmtChannelN> NeoEsp32RmtHIN400KbpsMethod;\r\ntypedef NeoEsp32RmtHINWs2805Method NeoEsp32RmtHINWs2814Method;\r\n\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2811, NeoEsp32RmtChannel0> NeoEsp32RmtHI0Ws2811Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2812x, NeoEsp32RmtChannel0> NeoEsp32RmtHI0Ws2812xMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2812x, NeoEsp32RmtChannel0> NeoEsp32RmtHI0Ws2816Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2805, NeoEsp32RmtChannel0> NeoEsp32RmtHI0Ws2805Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedSk6812, NeoEsp32RmtChannel0> NeoEsp32RmtHI0Sk6812Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1814, NeoEsp32RmtChannel0> NeoEsp32RmtHI0Tm1814Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1829, NeoEsp32RmtChannel0> NeoEsp32RmtHI0Tm1829Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1914, NeoEsp32RmtChannel0> NeoEsp32RmtHI0Tm1914Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedApa106, NeoEsp32RmtChannel0> NeoEsp32RmtHI0Apa106Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTx1812, NeoEsp32RmtChannel0> NeoEsp32RmtHI0Tx1812Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedGs1903, NeoEsp32RmtChannel0> NeoEsp32RmtHI0Gs1903Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeed800Kbps, NeoEsp32RmtChannel0> NeoEsp32RmtHI0800KbpsMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeed400Kbps, NeoEsp32RmtChannel0> NeoEsp32RmtHI0400KbpsMethod;\r\ntypedef NeoEsp32RmtHI0Ws2805Method NeoEsp32RmtHI0Ws2814Method;\r\n\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2811, NeoEsp32RmtChannel1> NeoEsp32RmtHI1Ws2811Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2812x, NeoEsp32RmtChannel1> NeoEsp32RmtHI1Ws2812xMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2812x, NeoEsp32RmtChannel1> NeoEsp32RmtHI1Ws2816Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2805, NeoEsp32RmtChannel1> NeoEsp32RmtHI1Ws2805Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedSk6812, NeoEsp32RmtChannel1> NeoEsp32RmtHI1Sk6812Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1814, NeoEsp32RmtChannel1> NeoEsp32RmtHI1Tm1814Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1829, NeoEsp32RmtChannel1> NeoEsp32RmtHI1Tm1829Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1914, NeoEsp32RmtChannel1> NeoEsp32RmtHI1Tm1914Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedApa106, NeoEsp32RmtChannel1> NeoEsp32RmtHI1Apa106Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTx1812, NeoEsp32RmtChannel1> NeoEsp32RmtHI1Tx1812Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedGs1903, NeoEsp32RmtChannel1> NeoEsp32RmtHI1Gs1903Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeed800Kbps, NeoEsp32RmtChannel1> NeoEsp32RmtHI1800KbpsMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeed400Kbps, NeoEsp32RmtChannel1> NeoEsp32RmtHI1400KbpsMethod;\r\ntypedef NeoEsp32RmtHI1Ws2805Method NeoEsp32RmtHI1Ws2814Method;\r\n\r\n#if !defined(CONFIG_IDF_TARGET_ESP32C3)\r\n\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2811, NeoEsp32RmtChannel2> NeoEsp32RmtHI2Ws2811Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2812x, NeoEsp32RmtChannel2> NeoEsp32RmtHI2Ws2812xMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2812x, NeoEsp32RmtChannel2> NeoEsp32RmtHI2Ws2816Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2805, NeoEsp32RmtChannel2> NeoEsp32RmtHI2Ws2805Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedSk6812, NeoEsp32RmtChannel2>  NeoEsp32RmtHI2Sk6812Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1814, NeoEsp32RmtChannel2> NeoEsp32RmtHI2Tm1814Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1829, NeoEsp32RmtChannel2> NeoEsp32RmtHI2Tm1829Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1914, NeoEsp32RmtChannel2> NeoEsp32RmtHI2Tm1914Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedApa106, NeoEsp32RmtChannel2> NeoEsp32RmtHI2Apa106Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTx1812, NeoEsp32RmtChannel2> NeoEsp32RmtHI2Tx1812Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedGs1903, NeoEsp32RmtChannel2> NeoEsp32RmtHI2Gs1903Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeed800Kbps, NeoEsp32RmtChannel2> NeoEsp32RmtHI2800KbpsMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeed400Kbps, NeoEsp32RmtChannel2> NeoEsp32RmtHI2400KbpsMethod;\r\ntypedef NeoEsp32RmtHI2Ws2805Method NeoEsp32RmtHI2Ws2814Method;\r\n\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2811, NeoEsp32RmtChannel3> NeoEsp32RmtHI3Ws2811Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2812x, NeoEsp32RmtChannel3> NeoEsp32RmtHI3Ws2812xMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2812x, NeoEsp32RmtChannel3> NeoEsp32RmtHI3Ws2816Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2805, NeoEsp32RmtChannel3> NeoEsp32RmtHI3Ws2805Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedSk6812, NeoEsp32RmtChannel3>  NeoEsp32RmtHI3Sk6812Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1814, NeoEsp32RmtChannel3> NeoEsp32RmtHI3Tm1814Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1829, NeoEsp32RmtChannel3> NeoEsp32RmtHI3Tm1829Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1914, NeoEsp32RmtChannel3> NeoEsp32RmtHI3Tm1914Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedApa106, NeoEsp32RmtChannel3> NeoEsp32RmtHI3Apa106Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTx1812, NeoEsp32RmtChannel3> NeoEsp32RmtHI3Tx1812Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedGs1903, NeoEsp32RmtChannel3> NeoEsp32RmtHI3Gs1903Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeed800Kbps, NeoEsp32RmtChannel3> NeoEsp32RmtHI3800KbpsMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeed400Kbps, NeoEsp32RmtChannel3> NeoEsp32RmtHI3400KbpsMethod;\r\ntypedef NeoEsp32RmtHI3Ws2805Method NeoEsp32RmtHI3Ws2814Method;\r\n\r\n#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3)\r\n\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2811, NeoEsp32RmtChannel4> NeoEsp32RmtHI4Ws2811Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2812x, NeoEsp32RmtChannel4> NeoEsp32RmtHI4Ws2812xMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2812x, NeoEsp32RmtChannel4> NeoEsp32RmtHI4Ws2816Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2805, NeoEsp32RmtChannel4> NeoEsp32RmtHI4Ws2805Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedSk6812, NeoEsp32RmtChannel4>  NeoEsp32RmtHI4Sk6812Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1814, NeoEsp32RmtChannel4> NeoEsp32RmtHI4Tm1814Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1829, NeoEsp32RmtChannel4> NeoEsp32RmtHI4Tm1829Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1914, NeoEsp32RmtChannel4> NeoEsp32RmtHI4Tm1914Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedApa106, NeoEsp32RmtChannel4> NeoEsp32RmtHI4Apa106Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTx1812, NeoEsp32RmtChannel4> NeoEsp32RmtHI4Tx1812Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedGs1903, NeoEsp32RmtChannel4> NeoEsp32RmtHI4Gs1903Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeed800Kbps, NeoEsp32RmtChannel4> NeoEsp32RmtHI4800KbpsMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeed400Kbps, NeoEsp32RmtChannel4> NeoEsp32RmtHI4400KbpsMethod;\r\ntypedef NeoEsp32RmtHI4Ws2805Method NeoEsp32RmtHI4Ws2814Method;\r\n\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2811, NeoEsp32RmtChannel5> NeoEsp32RmtHI5Ws2811Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2812x, NeoEsp32RmtChannel5> NeoEsp32RmtHI5Ws2812xMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2812x, NeoEsp32RmtChannel5> NeoEsp32RmtHI5Ws2816Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2805, NeoEsp32RmtChannel5> NeoEsp32RmtHI5Ws2805Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedSk6812, NeoEsp32RmtChannel5>  NeoEsp32RmtHI5Sk6812Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1814, NeoEsp32RmtChannel5> NeoEsp32RmtHI5Tm1814Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1829, NeoEsp32RmtChannel5> NeoEsp32RmtHI5Tm1829Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1914, NeoEsp32RmtChannel5> NeoEsp32RmtHI5Tm1914Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedApa106, NeoEsp32RmtChannel5> NeoEsp32RmtHI5Apa106Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTx1812, NeoEsp32RmtChannel5> NeoEsp32RmtHI5Tx1812Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedGs1903, NeoEsp32RmtChannel5> NeoEsp32RmtHI5Gs1903Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeed800Kbps, NeoEsp32RmtChannel5> NeoEsp32RmtHI5800KbpsMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeed400Kbps, NeoEsp32RmtChannel5> NeoEsp32RmtHI5400KbpsMethod;\r\ntypedef NeoEsp32RmtHI5Ws2805Method NeoEsp32RmtHI5Ws2814Method;\r\n\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2811, NeoEsp32RmtChannel6> NeoEsp32RmtHI6Ws2811Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2812x, NeoEsp32RmtChannel6> NeoEsp32RmtHI6Ws2812xMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2812x, NeoEsp32RmtChannel6> NeoEsp32RmtHI6Ws2816Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2805, NeoEsp32RmtChannel6> NeoEsp32RmtHI6Ws2805Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedSk6812, NeoEsp32RmtChannel6>  NeoEsp32RmtHI6Sk6812Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1814, NeoEsp32RmtChannel6> NeoEsp32RmtHI6Tm1814Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1829, NeoEsp32RmtChannel6> NeoEsp32RmtHI6Tm1829Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1914, NeoEsp32RmtChannel6> NeoEsp32RmtHI6Tm1914Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedApa106, NeoEsp32RmtChannel6> NeoEsp32RmtHI6Apa106Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTx1812, NeoEsp32RmtChannel6> NeoEsp32RmtHI6Tx1812Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedGs1903, NeoEsp32RmtChannel6> NeoEsp32RmtHI6Gs1903Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeed800Kbps, NeoEsp32RmtChannel6> NeoEsp32RmtHI6800KbpsMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeed400Kbps, NeoEsp32RmtChannel6> NeoEsp32RmtHI6400KbpsMethod;\r\ntypedef NeoEsp32RmtHI6Ws2805Method NeoEsp32RmtHI6Ws2814Method;\r\n\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2811, NeoEsp32RmtChannel7> NeoEsp32RmtHI7Ws2811Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2812x, NeoEsp32RmtChannel7> NeoEsp32RmtHI7Ws2812xMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2812x, NeoEsp32RmtChannel7> NeoEsp32RmtHI7Ws2816Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedWs2805, NeoEsp32RmtChannel7> NeoEsp32RmtHI7Ws2805Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedSk6812, NeoEsp32RmtChannel7>  NeoEsp32RmtHI7Sk6812Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1814, NeoEsp32RmtChannel7> NeoEsp32RmtHI7Tm1814Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1829, NeoEsp32RmtChannel7> NeoEsp32RmtHI7Tm1829Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTm1914, NeoEsp32RmtChannel7> NeoEsp32RmtHI7Tm1914Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedApa106, NeoEsp32RmtChannel7> NeoEsp32RmtHI7Apa106Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedTx1812, NeoEsp32RmtChannel7> NeoEsp32RmtHI7Tx1812Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeedGs1903, NeoEsp32RmtChannel7> NeoEsp32RmtHI7Gs1903Method;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeed800Kbps, NeoEsp32RmtChannel7> NeoEsp32RmtHI7800KbpsMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtSpeed400Kbps, NeoEsp32RmtChannel7> NeoEsp32RmtHI7400KbpsMethod;\r\ntypedef NeoEsp32RmtHI7Ws2805Method NeoEsp32RmtHI7Ws2814Method;\r\n\r\n#endif // !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3)\r\n#endif // !defined(CONFIG_IDF_TARGET_ESP32C3)\r\n\r\n// inverted\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2811, NeoEsp32RmtChannelN> NeoEsp32RmtHINWs2811InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2812x, NeoEsp32RmtChannelN> NeoEsp32RmtHINWs2812xInvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2812x, NeoEsp32RmtChannelN> NeoEsp32RmtHINWs2816InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2805, NeoEsp32RmtChannelN> NeoEsp32RmtHINWs2805InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedSk6812, NeoEsp32RmtChannelN> NeoEsp32RmtHINSk6812InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1814, NeoEsp32RmtChannelN> NeoEsp32RmtHINTm1814InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1829, NeoEsp32RmtChannelN> NeoEsp32RmtHINTm1829InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1914, NeoEsp32RmtChannelN> NeoEsp32RmtHINTm1914InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedApa106, NeoEsp32RmtChannelN> NeoEsp32RmtHINApa106InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTx1812, NeoEsp32RmtChannelN> NeoEsp32RmtHINTx1812InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedGs1903, NeoEsp32RmtChannelN> NeoEsp32RmtHINGs1903InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeed800Kbps, NeoEsp32RmtChannelN> NeoEsp32RmtHIN800KbpsInvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeed400Kbps, NeoEsp32RmtChannelN> NeoEsp32RmtHIN400KbpsInvertedMethod;\r\ntypedef NeoEsp32RmtHINWs2805InvertedMethod NeoEsp32RmtHINWs2814InvertedMethod;\r\n\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2811, NeoEsp32RmtChannel0> NeoEsp32RmtHI0Ws2811InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2812x, NeoEsp32RmtChannel0> NeoEsp32RmtHI0Ws2812xInvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2812x, NeoEsp32RmtChannel0> NeoEsp32RmtHI0Ws2816InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2805, NeoEsp32RmtChannel0> NeoEsp32RmtHI0Ws2805InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedSk6812, NeoEsp32RmtChannel0> NeoEsp32RmtHI0Sk6812InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1814, NeoEsp32RmtChannel0> NeoEsp32RmtHI0Tm1814InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1829, NeoEsp32RmtChannel0> NeoEsp32RmtHI0Tm1829InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1914, NeoEsp32RmtChannel0> NeoEsp32RmtHI0Tm1914InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedApa106, NeoEsp32RmtChannel0> NeoEsp32RmtHI0Apa106InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTx1812, NeoEsp32RmtChannel0> NeoEsp32RmtHI0Tx1812InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedGs1903, NeoEsp32RmtChannel0> NeoEsp32RmtHI0Gs1903InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeed800Kbps, NeoEsp32RmtChannel0> NeoEsp32RmtHI0800KbpsInvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeed400Kbps, NeoEsp32RmtChannel0> NeoEsp32RmtHI0400KbpsInvertedMethod;\r\ntypedef NeoEsp32RmtHI0Ws2805InvertedMethod NeoEsp32RmtHI0Ws2814InvertedMethod;\r\n\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2811, NeoEsp32RmtChannel1> NeoEsp32RmtHI1Ws2811InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2812x, NeoEsp32RmtChannel1> NeoEsp32RmtHI1Ws2812xInvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2812x, NeoEsp32RmtChannel1> NeoEsp32RmtHI1Ws2816InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2805, NeoEsp32RmtChannel1> NeoEsp32RmtHI1Ws2805InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedSk6812, NeoEsp32RmtChannel1> NeoEsp32RmtHI1Sk6812InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1814, NeoEsp32RmtChannel1> NeoEsp32RmtHI1Tm1814InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1829, NeoEsp32RmtChannel1> NeoEsp32RmtHI1Tm1829InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1914, NeoEsp32RmtChannel1> NeoEsp32RmtHI1Tm1914InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedApa106, NeoEsp32RmtChannel1> NeoEsp32RmtHI1Apa106InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTx1812, NeoEsp32RmtChannel1> NeoEsp32RmtHI1Tx1812InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedGs1903, NeoEsp32RmtChannel1> NeoEsp32RmtHI1Gs1903InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeed800Kbps, NeoEsp32RmtChannel1> NeoEsp32RmtHI1800KbpsInvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeed400Kbps, NeoEsp32RmtChannel1> NeoEsp32RmtHI1400KbpsInvertedMethod;\r\ntypedef NeoEsp32RmtHI1Ws2805InvertedMethod NeoEsp32RmtHI1Ws2814InvertedMethod;\r\n\r\n#if !defined(CONFIG_IDF_TARGET_ESP32C3)\r\n\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2811, NeoEsp32RmtChannel2> NeoEsp32RmtHI2Ws2811InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2812x, NeoEsp32RmtChannel2> NeoEsp32RmtHI2Ws2812xInvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2812x, NeoEsp32RmtChannel2> NeoEsp32RmtHI2Ws2816InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2805, NeoEsp32RmtChannel2> NeoEsp32RmtHI2Ws2805InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedSk6812, NeoEsp32RmtChannel2>  NeoEsp32RmtHI2Sk6812InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1814, NeoEsp32RmtChannel2> NeoEsp32RmtHI2Tm1814InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1829, NeoEsp32RmtChannel2> NeoEsp32RmtHI2Tm1829InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1914, NeoEsp32RmtChannel2> NeoEsp32RmtHI2Tm1914InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedApa106, NeoEsp32RmtChannel2> NeoEsp32RmtHI2Apa106InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTx1812, NeoEsp32RmtChannel2> NeoEsp32RmtHI2Tx1812InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedGs1903, NeoEsp32RmtChannel2> NeoEsp32RmtHI2Gs1903InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeed800Kbps, NeoEsp32RmtChannel2> NeoEsp32RmtHI2800KbpsInvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeed400Kbps, NeoEsp32RmtChannel2> NeoEsp32RmtHI2400KbpsInvertedMethod;\r\ntypedef NeoEsp32RmtHI2Ws2805InvertedMethod NeoEsp32RmtHI2Ws2814InvertedMethod;\r\n\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2811, NeoEsp32RmtChannel3> NeoEsp32RmtHI3Ws2811InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2812x, NeoEsp32RmtChannel3> NeoEsp32RmtHI3Ws2812xInvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2805, NeoEsp32RmtChannel3> NeoEsp32RmtHI3Ws2805InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2812x, NeoEsp32RmtChannel3> NeoEsp32RmtHI3Ws2816InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedSk6812, NeoEsp32RmtChannel3>  NeoEsp32RmtHI3Sk6812InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1814, NeoEsp32RmtChannel3> NeoEsp32RmtHI3Tm1814InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1829, NeoEsp32RmtChannel3> NeoEsp32RmtHI3Tm1829InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1914, NeoEsp32RmtChannel3> NeoEsp32RmtHI3Tm1914InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedApa106, NeoEsp32RmtChannel3> NeoEsp32RmtHI3Apa106InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTx1812, NeoEsp32RmtChannel3> NeoEsp32RmtHI3Tx1812InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedGs1903, NeoEsp32RmtChannel3> NeoEsp32RmtHI3Gs1903InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeed800Kbps, NeoEsp32RmtChannel3> NeoEsp32RmtHI3800KbpsInvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeed400Kbps, NeoEsp32RmtChannel3> NeoEsp32RmtHI3400KbpsInvertedMethod;\r\ntypedef NeoEsp32RmtHI3Ws2805InvertedMethod NeoEsp32RmtHI3Ws2814InvertedMethod;\r\n\r\n#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3)\r\n\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2811, NeoEsp32RmtChannel4> NeoEsp32RmtHI4Ws2811InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2812x, NeoEsp32RmtChannel4> NeoEsp32RmtHI4Ws2812xInvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2812x, NeoEsp32RmtChannel4> NeoEsp32RmtHI4Ws2816InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2805, NeoEsp32RmtChannel4> NeoEsp32RmtHI4Ws2805InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedSk6812, NeoEsp32RmtChannel4>  NeoEsp32RmtHI4Sk6812InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1814, NeoEsp32RmtChannel4> NeoEsp32RmtHI4Tm1814InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1829, NeoEsp32RmtChannel4> NeoEsp32RmtHI4Tm1829InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1914, NeoEsp32RmtChannel4> NeoEsp32RmtHI4Tm1914InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedApa106, NeoEsp32RmtChannel4> NeoEsp32RmtHI4Apa106InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTx1812, NeoEsp32RmtChannel4> NeoEsp32RmtHI4Tx1812InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedGs1903, NeoEsp32RmtChannel4> NeoEsp32RmtHI4Gs1903InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeed800Kbps, NeoEsp32RmtChannel4> NeoEsp32RmtHI4800KbpsInvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeed400Kbps, NeoEsp32RmtChannel4> NeoEsp32RmtHI4400KbpsInvertedMethod;\r\ntypedef NeoEsp32RmtHI4Ws2805InvertedMethod NeoEsp32RmtHI4Ws2814InvertedMethod;\r\n\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2811, NeoEsp32RmtChannel5> NeoEsp32RmtHI5Ws2811InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2812x, NeoEsp32RmtChannel5> NeoEsp32RmtHI5Ws2812xInvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2812x, NeoEsp32RmtChannel5> NeoEsp32RmtHI5Ws2816InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2805, NeoEsp32RmtChannel5> NeoEsp32RmtHI5Ws2805InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedSk6812, NeoEsp32RmtChannel5>  NeoEsp32RmtHI5Sk6812InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1814, NeoEsp32RmtChannel5> NeoEsp32RmtHI5Tm1814InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1829, NeoEsp32RmtChannel5> NeoEsp32RmtHI5Tm1829InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1914, NeoEsp32RmtChannel5> NeoEsp32RmtHI5Tm1914InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedApa106, NeoEsp32RmtChannel5> NeoEsp32RmtHI5Apa106InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTx1812, NeoEsp32RmtChannel5> NeoEsp32RmtHI5Tx1812InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedGs1903, NeoEsp32RmtChannel5> NeoEsp32RmtHI5Gs1903InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeed800Kbps, NeoEsp32RmtChannel5> NeoEsp32RmtHI5800KbpsInvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeed400Kbps, NeoEsp32RmtChannel5> NeoEsp32RmtHI5400KbpsInvertedMethod;\r\ntypedef NeoEsp32RmtHI5Ws2805InvertedMethod NeoEsp32RmtHI5Ws2814InvertedMethod;\r\n\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2811, NeoEsp32RmtChannel6> NeoEsp32RmtHI6Ws2811InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2812x, NeoEsp32RmtChannel6> NeoEsp32RmtHI6Ws2812xInvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2812x, NeoEsp32RmtChannel6> NeoEsp32RmtHI6Ws2816InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2805, NeoEsp32RmtChannel6> NeoEsp32RmtHI6Ws2805InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedSk6812, NeoEsp32RmtChannel6>  NeoEsp32RmtHI6Sk6812InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1814, NeoEsp32RmtChannel6> NeoEsp32RmtHI6Tm1814InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1829, NeoEsp32RmtChannel6> NeoEsp32RmtHI6Tm1829InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1914, NeoEsp32RmtChannel6> NeoEsp32RmtHI6Tm1914InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedApa106, NeoEsp32RmtChannel6> NeoEsp32RmtHI6Apa106InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTx1812, NeoEsp32RmtChannel6> NeoEsp32RmtHI6Tx1812InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedGs1903, NeoEsp32RmtChannel6> NeoEsp32RmtHI6Gs1903InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeed800Kbps, NeoEsp32RmtChannel6> NeoEsp32RmtHI6800KbpsInvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeed400Kbps, NeoEsp32RmtChannel6> NeoEsp32RmtHI6400KbpsInvertedMethod;\r\ntypedef NeoEsp32RmtHI6Ws2805InvertedMethod NeoEsp32RmtHI6Ws2814InvertedMethod;\r\n\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2811, NeoEsp32RmtChannel7> NeoEsp32RmtHI7Ws2811InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2812x, NeoEsp32RmtChannel7> NeoEsp32RmtHI7Ws2812xInvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2812x, NeoEsp32RmtChannel7> NeoEsp32RmtHI7Ws2816InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedWs2805, NeoEsp32RmtChannel7> NeoEsp32RmtHI7Ws2805InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedSk6812, NeoEsp32RmtChannel7>  NeoEsp32RmtHI7Sk6812InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1814, NeoEsp32RmtChannel7> NeoEsp32RmtHI7Tm1814InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1829, NeoEsp32RmtChannel7> NeoEsp32RmtHI7Tm1829InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTm1914, NeoEsp32RmtChannel7> NeoEsp32RmtHI7Tm1914InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedApa106, NeoEsp32RmtChannel7> NeoEsp32RmtHI7Apa106InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedTx1812, NeoEsp32RmtChannel7> NeoEsp32RmtHI7Tx1812InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeedGs1903, NeoEsp32RmtChannel7> NeoEsp32RmtHI7Gs1903InvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeed800Kbps, NeoEsp32RmtChannel7> NeoEsp32RmtHI7800KbpsInvertedMethod;\r\ntypedef NeoEsp32RmtHIMethodBase<NeoEsp32RmtInvertedSpeed400Kbps, NeoEsp32RmtChannel7> NeoEsp32RmtHI7400KbpsInvertedMethod;\r\ntypedef NeoEsp32RmtHI7Ws2805InvertedMethod NeoEsp32RmtHI7Ws2814InvertedMethod;\r\n\r\n#endif // !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3)\r\n#endif // !defined(CONFIG_IDF_TARGET_ESP32C3)\r\n\r\n#endif\r\n"
  },
  {
    "path": "lib/NeoESP32RmtHI/library.json",
    "content": "{\n  \"name\": \"NeoESP32RmtHI\",\n  \"build\": { \"libArchive\": false },\n  \"platforms\": [\"espressif32\"],\n  \"dependencies\": [\n    {\n      \"owner\": \"makuna\",      \n      \"name\": \"NeoPixelBus\",\n      \"version\": \"^2.8.3\"\n    }\n  ]\n}  \n"
  },
  {
    "path": "lib/NeoESP32RmtHI/src/NeoEsp32RmtHI.S",
    "content": "/* RMT ISR shim\n * Bridges from a high-level interrupt to the C++ code.\n *\n * This code is largely derived from Espressif's 'hli_vector.S' Bluetooth ISR.\n *\n */\n\n#if defined(__XTENSA__) && defined(ESP32) && !defined(CONFIG_BTDM_CTRL_HLI)\n\n#include <freertos/xtensa_context.h>\n#include \"sdkconfig.h\"\n#include \"soc/soc.h\"\n\n/* If the Bluetooth driver has hooked the high-priority interrupt, we piggyback on it and don't need this. */\n#ifndef CONFIG_BTDM_CTRL_HLI\n\n/*\n   Select interrupt based on system check level\n   - Base ESP32: could be 4 or 5, depends on platform config\n   - S2: 5\n   - S3: 5\n*/\n\n#if CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_5\n/* Use level 4 */\n#define RFI_X               4\n#define xt_highintx         xt_highint4\n#else /* !CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_5 */\n/* Use level 5 */\n#define RFI_X               5\n#define xt_highintx         xt_highint5\n#endif /* CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_5 */\n\n// Register map, based on interrupt level\n#define EPC_X               (EPC + RFI_X)\n#define EXCSAVE_X           (EXCSAVE + RFI_X)\n\n// The sp mnemonic is used all over in ESP's assembly, though I'm not sure where it's expected to be defined?\n#define sp a1\n\n/* Interrupt stack size, for C code. */\n#define RMT_INTR_STACK_SIZE  512\n\n/* Save area for the CPU state:\n * - 64 words for the general purpose registers\n * - 7 words for some of the special registers:\n *   - WINDOWBASE, WINDOWSTART — only WINDOWSTART is truly needed\n *   - SAR, LBEG, LEND, LCOUNT — since the C code might use these\n *   - EPC1 — since the C code might cause window overflow exceptions\n * This is not laid out as standard exception frame structure\n * for simplicity of the save/restore code.\n */\n#define REG_FILE_SIZE         (64 * 4)\n#define SPECREG_OFFSET        REG_FILE_SIZE\n#define SPECREG_SIZE          (7 * 4)\n#define REG_SAVE_AREA_SIZE    (SPECREG_OFFSET + SPECREG_SIZE)\n\n    .data\n_rmt_intr_stack:\n    .space      RMT_INTR_STACK_SIZE\n_rmt_save_ctx:\n    .space      REG_SAVE_AREA_SIZE\n\n    .section .iram1,\"ax\"\n    .global     xt_highintx\n    .type       xt_highintx,@function\n    .align      4\n\nxt_highintx:\n\n    movi    a0, _rmt_save_ctx\n    /* save 4 lower registers */\n    s32i    a1, a0, 4\n    s32i    a2, a0, 8\n    s32i    a3, a0, 12\n    rsr     a2, EXCSAVE_X  /* holds the value of a0 */\n    s32i    a2, a0, 0\n\n    /* Save special registers */\n    addi    a0, a0, SPECREG_OFFSET\n    rsr     a2, WINDOWBASE\n    s32i    a2, a0, 0\n    rsr     a2, WINDOWSTART\n    s32i    a2, a0, 4\n    rsr     a2, SAR\n    s32i    a2, a0, 8\n    #if XCHAL_HAVE_LOOPS\n    rsr     a2, LBEG\n    s32i    a2, a0, 12\n    rsr     a2, LEND\n    s32i    a2, a0, 16\n    rsr     a2, LCOUNT\n    s32i    a2, a0, 20\n    #endif\n    rsr     a2, EPC1\n    s32i    a2, a0, 24\n\n    /* disable exception mode, window overflow */\n    movi    a0, PS_INTLEVEL(RFI_X+1) | PS_EXCM\n    wsr     a0, PS\n    rsync\n\n    /* Save the remaining physical registers.\n     * 4 registers are already saved, which leaves 60 registers to save.\n     * (FIXME: consider the case when the CPU is configured with physical 32 registers)\n     * These 60 registers are saved in 5 iterations, 12 registers at a time.\n     */\n    movi    a1, 5\n    movi    a3, _rmt_save_ctx + 4 * 4\n\n    /* This is repeated 5 times, each time the window is shifted by 12 registers.\n     * We come here with a1 = downcounter, a3 = save pointer, a2 and a0 unused.\n     */\n1:\n    s32i    a4, a3, 0\n    s32i    a5, a3, 4\n    s32i    a6, a3, 8\n    s32i    a7, a3, 12\n    s32i    a8, a3, 16\n    s32i    a9, a3, 20\n    s32i    a10, a3, 24\n    s32i    a11, a3, 28\n    s32i    a12, a3, 32\n    s32i    a13, a3, 36\n    s32i    a14, a3, 40\n    s32i    a15, a3, 44\n\n    /* We are about to rotate the window, so that a12-a15 will become the new a0-a3.\n     * Copy a0-a3 to a12-15 to still have access to these values.\n     * At the same time we can decrement the counter and adjust the save area pointer\n     */\n\n    /* a0 is constant (_rmt_save_ctx), no need to copy */\n    addi    a13, a1, -1  /* copy and decrement the downcounter */\n    /* a2 is scratch so no need to copy */\n    addi    a15, a3, 48  /* copy and adjust the save area pointer */\n    beqz    a13, 2f      /* have saved all registers ? */\n    rotw    3            /* rotate the window and go back */\n    j       1b\n\n    /* the loop is complete */\n2:\n    rotw 4      /* this brings us back to the original window */\n    /* a0 still points to _rmt_save_ctx */\n\n    /* Can clear WINDOWSTART now, all registers are saved */\n    rsr     a2, WINDOWBASE\n    /* WINDOWSTART = (1 << WINDOWBASE) */\n    movi    a3, 1\n    ssl     a2\n    sll     a3, a3\n    wsr     a3, WINDOWSTART\n\n_highint_stack_switch:\n    movi    a0, 0\n    movi    sp, _rmt_intr_stack + RMT_INTR_STACK_SIZE - 16\n    s32e    a0, sp, -12         /* For GDB: set null SP */\n    s32e    a0, sp, -16         /* For GDB: set null PC */\n    movi    a0, _highint_stack_switch     /* For GDB: cosmetics, for the frame where stack switch happened */\n\n    /* Set up PS for C, disable all interrupts except NMI and debug, and clear EXCM. */\n    movi    a6, PS_INTLEVEL(RFI_X) | PS_UM | PS_WOE\n    wsr     a6, PS\n    rsync\n\n    /* Call C handler */\n    mov     a6, sp\n    call4   NeoEsp32RmtMethodIsr\n\n    l32e    sp, sp, -12                     /* switch back to the original stack */\n\n    /* Done with C handler; re-enable exception mode, disabling window overflow */\n    movi    a2, PS_INTLEVEL(RFI_X+1) | PS_EXCM    /* TOCHECK */\n    wsr     a2, PS\n    rsync\n\n    /* Restore the special registers.\n     * WINDOWSTART will be restored near the end.\n     */\n    movi    a0, _rmt_save_ctx + SPECREG_OFFSET\n    l32i    a2, a0, 8\n    wsr     a2, SAR\n    #if XCHAL_HAVE_LOOPS\n    l32i    a2, a0, 12\n    wsr     a2, LBEG\n    l32i    a2, a0, 16\n    wsr     a2, LEND\n    l32i    a2, a0, 20\n    wsr     a2, LCOUNT\n    #endif\n    l32i    a2, a0, 24\n    wsr     a2, EPC1\n\n    /* Restoring the physical registers.\n     * This is the reverse to the saving process above.\n     */\n\n    /* Rotate back to the final window, then start loading 12 registers at a time,\n     * in 5 iterations.\n     * Again, a1 is the downcounter and a3 is the save area pointer.\n     * After each rotation, a1 and a3 are copied from a13 and a15.\n     * To simplify the loop, we put the initial values into a13 and a15.\n     */\n    rotw     -4\n    movi    a15, _rmt_save_ctx + 64 * 4  /* point to the end of the save area */\n    movi    a13, 5\n\n1:\n    /* Copy a1 and a3 from their previous location,\n     * at the same time decrementing and adjusting the save area pointer.\n     */\n    addi    a1, a13, -1\n    addi    a3, a15, -48\n\n    /* Load 12 registers */\n    l32i    a4, a3, 0\n    l32i    a5, a3, 4\n    l32i    a6, a3, 8\n    l32i    a7, a3, 12\n    l32i    a8, a3, 16\n    l32i    a9, a3, 20\n    l32i    a10, a3, 24\n    l32i    a11, a3, 28                                /* ensure PS and EPC written */\n    l32i    a12, a3, 32\n    l32i    a13, a3, 36\n    l32i    a14, a3, 40\n    l32i    a15, a3, 44\n\n    /* Done with the loop? */\n    beqz    a1, 2f\n    /* If no, rotate the window and repeat */\n    rotw    -3\n    j       1b\n\n2:\n    /* Done with the loop. Only 4 registers (a0-a3 in the original window) remain\n     * to be restored. Also need to restore WINDOWSTART, since all the general\n     * registers are now in place.\n     */\n    movi    a0, _rmt_save_ctx\n\n    l32i    a2, a0, SPECREG_OFFSET + 4\n    wsr     a2, WINDOWSTART\n\n    l32i    a1, a0, 4\n    l32i    a2, a0, 8\n    l32i    a3, a0, 12\n    rsr     a0, EXCSAVE_X  /* holds the value of a0 before the interrupt handler */\n\n    /* Return from the interrupt, restoring PS from EPS_X */\n    rfi     RFI_X\n\n\n/* The linker has no reason to link in this file; all symbols it exports are already defined\n   (weakly!) in the default int handler. Define a symbol here so we can use it to have the\n   linker inspect this anyway. */\n\n    .global ld_include_hli_vectors_rmt\nld_include_hli_vectors_rmt:\n\n\n#endif // CONFIG_BTDM_CTRL_HLI\n#endif // XTensa"
  },
  {
    "path": "lib/NeoESP32RmtHI/src/NeoEsp32RmtHIMethod.cpp",
    "content": "/*-------------------------------------------------------------------------\nNeoPixel library helper functions for Esp32.\n\nA BIG thanks to Andreas Merkle for the investigation and implementation of\na workaround to the GCC bug that drops method attributes from template methods\n\nWritten by Michael C. Miller.\n\nI invest time and resources providing this open source code,\nplease support me by donating (see https://github.com/Makuna/NeoPixelBus)\n\n-------------------------------------------------------------------------\nThis file is part of the Makuna/NeoPixelBus library.\n\nNeoPixelBus is free software: you can redistribute it and/or modify\nit under the terms of the GNU Lesser General Public License as\npublished by the Free Software Foundation, either version 3 of\nthe License, or (at your option) any later version.\n\nNeoPixelBus is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU Lesser General Public License for more details.\n\nYou should have received a copy of the GNU Lesser General Public\nLicense along with NeoPixel.  If not, see\n<http://www.gnu.org/licenses/>.\n-------------------------------------------------------------------------*/\n\n#include <Arduino.h>\n\n#if defined(ARDUINO_ARCH_ESP32)\n\n#include <algorithm>  \n#include \"esp_idf_version.h\"\n#include \"NeoEsp32RmtHIMethod.h\"\n#include \"soc/soc.h\"\n#include \"soc/rmt_reg.h\"\n\n#ifdef __riscv\n#include \"riscv/interrupt.h\"\n#endif\n\n\n#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0)\n#include \"hal/rmt_ll.h\"\n#else\n/* Shims for older ESP-IDF v3; we can safely assume original ESP32 */\n#include \"soc/rmt_struct.h\"\n\n// Selected RMT API functions borrowed from ESP-IDF v4.4.8\n// components/hal/esp32/include/hal/rmt_ll.h\n// Copyright 2019 Espressif Systems (Shanghai) PTE LTD\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n__attribute__((always_inline))\nstatic inline void rmt_ll_tx_reset_pointer(rmt_dev_t *dev, uint32_t channel)\n{\n    dev->conf_ch[channel].conf1.mem_rd_rst = 1;\n    dev->conf_ch[channel].conf1.mem_rd_rst = 0;\n}\n\n__attribute__((always_inline))\nstatic inline void rmt_ll_tx_start(rmt_dev_t *dev, uint32_t channel)\n{\n    dev->conf_ch[channel].conf1.tx_start = 1;\n}\n\n__attribute__((always_inline))\nstatic inline void rmt_ll_tx_stop(rmt_dev_t *dev, uint32_t channel)\n{\n    RMTMEM.chan[channel].data32[0].val = 0;\n    dev->conf_ch[channel].conf1.tx_start = 0;\n    dev->conf_ch[channel].conf1.mem_rd_rst = 1;\n    dev->conf_ch[channel].conf1.mem_rd_rst = 0;\n}\n\n__attribute__((always_inline))\nstatic inline void rmt_ll_tx_enable_pingpong(rmt_dev_t *dev, uint32_t channel, bool enable)\n{\n    dev->apb_conf.mem_tx_wrap_en = enable;\n}\n\n__attribute__((always_inline))\nstatic inline void rmt_ll_tx_enable_loop(rmt_dev_t *dev, uint32_t channel, bool enable)\n{\n    dev->conf_ch[channel].conf1.tx_conti_mode = enable;\n}\n\n__attribute__((always_inline))\nstatic inline uint32_t rmt_ll_tx_get_channel_status(rmt_dev_t *dev, uint32_t channel)\n{\n    return dev->status_ch[channel];\n}\n\n__attribute__((always_inline))\nstatic inline void rmt_ll_tx_set_limit(rmt_dev_t *dev, uint32_t channel, uint32_t limit)\n{\n    dev->tx_lim_ch[channel].limit = limit;\n}\n\n__attribute__((always_inline))\nstatic inline void rmt_ll_enable_interrupt(rmt_dev_t *dev, uint32_t mask, bool enable)\n{\n    if (enable) {\n        dev->int_ena.val |= mask;\n    } else {\n        dev->int_ena.val &= ~mask;\n    }\n}\n\n__attribute__((always_inline))\nstatic inline void rmt_ll_enable_tx_end_interrupt(rmt_dev_t *dev, uint32_t channel, bool enable)\n{\n    dev->int_ena.val &= ~(1 << (channel * 3));\n    dev->int_ena.val |= (enable << (channel * 3));\n}\n\n__attribute__((always_inline))\nstatic inline void rmt_ll_enable_tx_err_interrupt(rmt_dev_t *dev, uint32_t channel, bool enable)\n{\n    dev->int_ena.val &= ~(1 << (channel * 3 + 2));\n    dev->int_ena.val |= (enable << (channel * 3 + 2));\n}\n\n__attribute__((always_inline))\nstatic inline void rmt_ll_enable_tx_thres_interrupt(rmt_dev_t *dev, uint32_t channel, bool enable)\n{\n    dev->int_ena.val &= ~(1 << (channel + 24));\n    dev->int_ena.val |= (enable << (channel + 24));\n}\n\n__attribute__((always_inline))\nstatic inline void rmt_ll_clear_tx_end_interrupt(rmt_dev_t *dev, uint32_t channel)\n{\n    dev->int_clr.val = (1 << (channel * 3));\n}\n\n__attribute__((always_inline))\nstatic inline void rmt_ll_clear_tx_err_interrupt(rmt_dev_t *dev, uint32_t channel)\n{\n    dev->int_clr.val = (1 << (channel * 3 + 2));\n}\n\n__attribute__((always_inline))\nstatic inline void rmt_ll_clear_tx_thres_interrupt(rmt_dev_t *dev, uint32_t channel)\n{\n    dev->int_clr.val = (1 << (channel + 24));\n}\n\n\n__attribute__((always_inline))\nstatic inline uint32_t rmt_ll_get_tx_thres_interrupt_status(rmt_dev_t *dev)\n{\n    uint32_t status =  dev->int_st.val;\n    return (status & 0xFF000000) >> 24;\n}\n#endif\n\n\n// *********************************\n// Select method for binding interrupt\n//\n// - If the Bluetooth driver has registered a high-level interrupt, piggyback on that API\n// - If we're on a modern core, allocate the interrupt with the API (old cores are bugged)\n// - Otherwise use the low-level hardware API to manually bind the interrupt\n\n\n#if defined(CONFIG_BTDM_CTRL_HLI)\n// Espressif's bluetooth driver offers a helpful sharing layer; bring in the interrupt management calls\n#include \"hal/interrupt_controller_hal.h\"\nextern \"C\" esp_err_t hli_intr_register(intr_handler_t handler, void* arg, uint32_t intr_reg, uint32_t intr_mask);\n\n#else /* !CONFIG_BTDM_CTRL_HLI*/\n\n// Declare the our high-priority ISR handler\nextern \"C\" void ld_include_hli_vectors_rmt();   // an object with an address, but no space\n\n#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3)\n#include \"soc/periph_defs.h\"\n#endif\n\n// Select level flag\n#if defined(__riscv)\n// RISCV chips don't block interrupts while scheduling; all we need to do is be higher than the WiFi ISR\n#define INT_LEVEL_FLAG ESP_INTR_FLAG_LEVEL3\n#elif defined(CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_5)\n#define INT_LEVEL_FLAG ESP_INTR_FLAG_LEVEL4\n#else\n#define INT_LEVEL_FLAG ESP_INTR_FLAG_LEVEL5\n#endif\n\n// ESP-IDF v3 cannot enable high priority interrupts through the API at all;\n// and ESP-IDF v4 on XTensa cannot enable Level 5 due to incorrect interrupt descriptor tables\n#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))\n#define NEOESP32_RMT_CAN_USE_INTR_ALLOC\n\n// XTensa cores require the assembly bridge\n#ifdef __XTENSA__\n#define HI_IRQ_HANDLER nullptr\n#define HI_IRQ_HANDLER_ARG ld_include_hli_vectors_rmt\n#else\n#define HI_IRQ_HANDLER NeoEsp32RmtMethodIsr\n#define HI_IRQ_HANDLER_ARG nullptr\n#endif\n\n#else\n/* !CONFIG_BTDM_CTRL_HLI && !NEOESP32_RMT_CAN_USE_INTR_ALLOC */\n// This is the index of the LV5 interrupt vector - see interrupt descriptor table in idf components/hal/esp32/interrupt_descriptor_table.c\n#define ESP32_LV5_IRQ_INDEX 26\n\n#endif  /* NEOESP32_RMT_CAN_USE_INTR_ALLOC */\n#endif  /* CONFIG_BTDM_CTRL_HLI */\n\n\n// RMT driver implementation\nstruct NeoEsp32RmtHIChannelState {\n    uint32_t rmtBit0, rmtBit1;\n    uint32_t resetDuration;\n\n    const byte* txDataStart;    // data array\n    const byte* txDataEnd;      // one past end\n    const byte* txDataCurrent;      // current location\n    size_t rmtOffset;\n};\n\n// Global variables\n#if defined(NEOESP32_RMT_CAN_USE_INTR_ALLOC)\nstatic intr_handle_t isrHandle = nullptr;\n#endif\n\nstatic NeoEsp32RmtHIChannelState** driverState = nullptr;\nconstexpr size_t rmtBatchSize =  RMT_MEM_ITEM_NUM / 2;\n\n// Fill the RMT buffer memory\n// This is implemented using many arguments instead of passing the structure object to ensure we do only one lookup\n// All the arguments are passed in registers, so they don't need to be looked up again\nstatic 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) {\n    // We assume that (rmtToWrite % 8) == 0\n    size_t rmtToWrite = rmtBatchSize - reserve;\n    rmt_item32_t* dest =(rmt_item32_t*)  &RMTMEM.chan[channel].data32[*offset_ptr + reserve]; // write directly in to RMT memory\n    const byte* psrc = *src_ptr;\n\n    *offset_ptr ^= rmtBatchSize;\n\n    if (psrc != end) {\n        while (rmtToWrite > 0) {\n            uint8_t data = *psrc;\n            for (uint8_t bit = 0; bit < 8; bit++)\n            {\n                dest->val = (data & 0x80) ? bit1 : bit0;\n                dest++;\n                data <<= 1;\n            }\n            rmtToWrite -= 8;\n            psrc++;\n\n            if (psrc == end) {\n                break;\n            }\n        }\n\n        *src_ptr = psrc;\n    }\n\n    if (rmtToWrite > 0) {\n        // Add end event\n        rmt_item32_t bit0_val = {{.val = bit0 }};\n        *dest = rmt_item32_t {{{ .duration0 = 0, .level0 = bit0_val.level1, .duration1 = 0, .level1 = bit0_val.level1 }}};\n    }\n}\n\nstatic void IRAM_ATTR RmtStartWrite(uint8_t channel, NeoEsp32RmtHIChannelState& state) {\n    // Reset context state\n    state.rmtOffset = 0;\n\n    // Fill the first part of the buffer with a reset event\n    // FUTURE: we could do timing analysis with the last interrupt on this channel\n    // Use 8 words to stay aligned with the buffer fill logic\n    rmt_item32_t bit0_val = {{.val = state.rmtBit0 }};\n    rmt_item32_t fill = {{{ .duration0 = 100, .level0 = bit0_val.level1, .duration1 = 100, .level1 = bit0_val.level1 }}};\n    rmt_item32_t* dest = (rmt_item32_t*) &RMTMEM.chan[channel].data32[0];\n    for (auto i = 0; i < 7; ++i) dest[i] = fill;\n    fill.duration1 = state.resetDuration > 1400 ? (state.resetDuration - 1400) : 100;\n    dest[7] = fill;\n\n    // Fill the remaining buffer with real data\n    RmtFillBuffer(channel, &state.txDataCurrent, state.txDataEnd, state.rmtBit0, state.rmtBit1, &state.rmtOffset, 8);\n    RmtFillBuffer(channel, &state.txDataCurrent, state.txDataEnd, state.rmtBit0, state.rmtBit1, &state.rmtOffset, 0);\n\n    // Start operation\n    rmt_ll_clear_tx_thres_interrupt(&RMT, channel);\n    rmt_ll_tx_reset_pointer(&RMT, channel);\n    rmt_ll_tx_start(&RMT, channel);\n}\n\nextern \"C\" void IRAM_ATTR NeoEsp32RmtMethodIsr(void *arg) {\n    // Tx threshold interrupt\n    uint32_t status = rmt_ll_get_tx_thres_interrupt_status(&RMT);\n    while (status) {\n        uint8_t channel = __builtin_ffs(status) - 1;                \n        if (driverState[channel]) {            \n            // Normal case\n            NeoEsp32RmtHIChannelState& state = *driverState[channel];\n            RmtFillBuffer(channel, &state.txDataCurrent, state.txDataEnd, state.rmtBit0, state.rmtBit1, &state.rmtOffset, 0);\n        } else {\n            // Danger - another driver got invoked?\n            rmt_ll_tx_stop(&RMT, channel);\n        }\n        rmt_ll_clear_tx_thres_interrupt(&RMT, channel);\n        status = rmt_ll_get_tx_thres_interrupt_status(&RMT);\n    }\n};\n\n// Wrapper around the register analysis defines\n// For all currently supported chips, this is constant for all channels; but this is not true of *all* ESP32\nstatic inline bool _RmtStatusIsTransmitting(rmt_channel_t channel, uint32_t status) {\n    uint32_t v;\n    switch(channel) {\n#ifdef RMT_STATE_CH0        \n        case 0: v = (status >> RMT_STATE_CH0_S) & RMT_STATE_CH0_V; break;\n#endif\n#ifdef RMT_STATE_CH1\n        case 1: v = (status >> RMT_STATE_CH1_S) & RMT_STATE_CH1_V; break;\n#endif\n#ifdef RMT_STATE_CH2\n        case 2: v = (status >> RMT_STATE_CH2_S) & RMT_STATE_CH2_V; break;\n#endif\n#ifdef RMT_STATE_CH3        \n        case 3: v = (status >> RMT_STATE_CH3_S) & RMT_STATE_CH3_V; break;\n#endif        \n#ifdef RMT_STATE_CH4\n        case 4: v = (status >> RMT_STATE_CH4_S) & RMT_STATE_CH4_V; break;\n#endif\n#ifdef RMT_STATE_CH5\n        case 5: v = (status >> RMT_STATE_CH5_S) & RMT_STATE_CH5_V; break;\n#endif\n#ifdef RMT_STATE_CH6\n        case 6: v = (status >> RMT_STATE_CH6_S) & RMT_STATE_CH6_V; break;\n#endif\n#ifdef RMT_STATE_CH7\n        case 7: v = (status >> RMT_STATE_CH7_S) & RMT_STATE_CH7_V; break;\n#endif\n        default: v = 0;\n    }\n\n    return v != 0;\n}\n\n\nesp_err_t NeoEsp32RmtHiMethodDriver::Install(rmt_channel_t channel, uint32_t rmtBit0, uint32_t rmtBit1, uint32_t reset) {\n    // Validate channel number\n    if (channel >= RMT_CHANNEL_MAX) {\n        return ESP_ERR_INVALID_ARG;\n    }\n\n    esp_err_t err = ESP_OK;\n    if (!driverState) {\n        // First time init\n        driverState = reinterpret_cast<NeoEsp32RmtHIChannelState**>(heap_caps_calloc(RMT_CHANNEL_MAX, sizeof(NeoEsp32RmtHIChannelState*), MALLOC_CAP_INTERNAL));\n        if (!driverState) return ESP_ERR_NO_MEM;\n        \n        // Ensure all interrupts are cleared before binding\n        RMT.int_ena.val = 0;\n        RMT.int_clr.val = 0xFFFFFFFF;\n\n        // Bind interrupt handler\n#if defined(CONFIG_BTDM_CTRL_HLI)\n        // Bluetooth driver has taken the empty high-priority interrupt.  Fortunately, it allows us to\n        // hook up another handler.\n        err = hli_intr_register(NeoEsp32RmtMethodIsr, nullptr, (uintptr_t) &RMT.int_st, 0xFF000000);\n        // 25 is the magic number of the bluetooth ISR on ESP32 - see soc/soc.h.\n        intr_matrix_set(cpu_hal_get_core_id(), ETS_RMT_INTR_SOURCE, 25);\n        intr_cntrl_ll_enable_interrupts(1<<25);\n#elif defined(NEOESP32_RMT_CAN_USE_INTR_ALLOC)\n        // Use the platform code to allocate the interrupt\n        // If we need the additional assembly bridge, we pass it as the \"arg\" to the IDF so it gets linked in       \n        err = esp_intr_alloc(ETS_RMT_INTR_SOURCE, INT_LEVEL_FLAG | ESP_INTR_FLAG_IRAM, HI_IRQ_HANDLER, (void*) HI_IRQ_HANDLER_ARG, &isrHandle);\n        //err = ESP_ERR_NOT_FINISHED;\n#else\n        // Broken IDF API does not allow us to reserve the interrupt; do it manually\n        static volatile const void*  __attribute__((used)) pleaseLinkAssembly = (void*) ld_include_hli_vectors_rmt;\n        intr_matrix_set(xPortGetCoreID(), ETS_RMT_INTR_SOURCE, ESP32_LV5_IRQ_INDEX);\n        ESP_INTR_ENABLE(ESP32_LV5_IRQ_INDEX);\n#endif\n\n        if (err != ESP_OK) {\n            heap_caps_free(driverState);\n            driverState = nullptr;\n            return err;\n        }\n    }\n\n    if (driverState[channel] != nullptr) {\n        return ESP_ERR_INVALID_STATE;   // already in use\n    }\n\n    NeoEsp32RmtHIChannelState* state = reinterpret_cast<NeoEsp32RmtHIChannelState*>(heap_caps_calloc(1, sizeof(NeoEsp32RmtHIChannelState), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT));\n    if (state == nullptr) {\n        return ESP_ERR_NO_MEM;\n    }\n\n    // Store timing information\n    state->rmtBit0 = rmtBit0;\n    state->rmtBit1 = rmtBit1;\n    state->resetDuration = reset;\n\n    // Initialize hardware\n    rmt_ll_tx_stop(&RMT, channel);\n    rmt_ll_tx_reset_pointer(&RMT, channel);\n    rmt_ll_enable_tx_err_interrupt(&RMT, channel, false);\n    rmt_ll_enable_tx_end_interrupt(&RMT, channel, false);\n    rmt_ll_enable_tx_thres_interrupt(&RMT, channel, false);\n    rmt_ll_clear_tx_err_interrupt(&RMT, channel);\n    rmt_ll_clear_tx_end_interrupt(&RMT, channel);\n    rmt_ll_clear_tx_thres_interrupt(&RMT, channel);\n    \n    rmt_ll_tx_enable_loop(&RMT, channel, false);\n    rmt_ll_tx_enable_pingpong(&RMT, channel, true);\n    rmt_ll_tx_set_limit(&RMT, channel, rmtBatchSize);\n\n    driverState[channel] = state;\n\n    rmt_ll_enable_tx_thres_interrupt(&RMT, channel, true);\n\n    return err;\n}\n\nesp_err_t  NeoEsp32RmtHiMethodDriver::Uninstall(rmt_channel_t channel) {\n    if ((channel >= RMT_CHANNEL_MAX) || !driverState || !driverState[channel]) return ESP_ERR_INVALID_ARG;\n\n    NeoEsp32RmtHIChannelState* state = driverState[channel];\n\n    WaitForTxDone(channel, 10000 / portTICK_PERIOD_MS);\n\n    // Done or not, we're out of here\n    rmt_ll_tx_stop(&RMT, channel);\n    rmt_ll_enable_tx_thres_interrupt(&RMT, channel, false);\n    driverState[channel] = nullptr;\n    heap_caps_free(state);\n\n#if !defined(CONFIG_BTDM_CTRL_HLI)  /* Cannot unbind from bluetooth ISR */\n    // Turn off the driver ISR and release global state if none are left\n    for (uint8_t channelIndex = 0; channelIndex < RMT_CHANNEL_MAX; ++channelIndex) {\n        if (driverState[channelIndex]) return ESP_OK; // done\n    }\n\n#if defined(NEOESP32_RMT_CAN_USE_INTR_ALLOC)\n    esp_intr_free(isrHandle);\n#else\n    ESP_INTR_DISABLE(ESP32_LV5_IRQ_INDEX);\n#endif\n\n    heap_caps_free(driverState);\n    driverState = nullptr;\n#endif /* !defined(CONFIG_BTDM_CTRL_HLI) */\n\n    return ESP_OK;\n}\n\nesp_err_t  NeoEsp32RmtHiMethodDriver::Write(rmt_channel_t channel, const uint8_t *src, size_t src_size) {\n    if ((channel >= RMT_CHANNEL_MAX) || !driverState || !driverState[channel]) return ESP_ERR_INVALID_ARG;\n\n    NeoEsp32RmtHIChannelState& state = *driverState[channel];\n    esp_err_t result = WaitForTxDone(channel, 10000 / portTICK_PERIOD_MS);\n\n    if (result == ESP_OK) {\n        state.txDataStart = src;\n        state.txDataCurrent = src;\n        state.txDataEnd = src + src_size;\n        RmtStartWrite(channel, state);\n    }\n    return result;\n}\n\nesp_err_t NeoEsp32RmtHiMethodDriver::WaitForTxDone(rmt_channel_t channel, TickType_t wait_time) {\n    if ((channel >= RMT_CHANNEL_MAX) || !driverState || !driverState[channel]) return ESP_ERR_INVALID_ARG;\n\n    NeoEsp32RmtHIChannelState& state = *driverState[channel];\n    // yield-wait until wait_time\n    esp_err_t rv = ESP_OK;\n    uint32_t status;\n    while(1) {\n        status = rmt_ll_tx_get_channel_status(&RMT, channel);\n        if (!_RmtStatusIsTransmitting(channel, status)) break;\n        if (wait_time == 0) { rv = ESP_ERR_TIMEOUT; break; };\n\n        TickType_t sleep = std::min(wait_time, (TickType_t) 5);\n        vTaskDelay(sleep);\n        wait_time -= sleep;\n    };\n\n    return rv;\n}\n\n#endif"
  },
  {
    "path": "lib/README",
    "content": "\nThis directory is intended for project specific (private) libraries.\nPlatformIO will compile them to static libraries and link into executable file.\n\nThe source code of each library should be placed in a an own separate directory\n(\"lib/your_library_name/[here are source files]\").\n\nFor example, see a structure of the following two libraries `Foo` and `Bar`:\n\n|--lib\n|  |\n|  |--Bar\n|  |  |--docs\n|  |  |--examples\n|  |  |--src\n|  |     |- Bar.c\n|  |     |- Bar.h\n|  |  |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html\n|  |\n|  |--Foo\n|  |  |- Foo.c\n|  |  |- Foo.h\n|  |\n|  |- README --> THIS FILE\n|\n|- platformio.ini\n|--src\n   |- main.c\n\nand a contents of `src/main.c`:\n```\n#include <Foo.h>\n#include <Bar.h>\n\nint main (void)\n{\n  ...\n}\n\n```\n\nPlatformIO Library Dependency Finder will find automatically dependent\nlibraries scanning project source files.\n\nMore information about PlatformIO Library Dependency Finder\n- https://docs.platformio.org/page/librarymanager/ldf.html\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"wled\",\n  \"version\": \"16.0.0-alpha\",\n  \"description\": \"Tools for WLED project\",\n  \"main\": \"tools/cdata.js\",\n  \"directories\": {\n    \"lib\": \"lib\",\n    \"test\": \"test\"\n  },\n  \"scripts\": {\n    \"build\": \"node tools/cdata.js\",\n    \"test\": \"node --test\",\n    \"dev\": \"nodemon -e js,html,htm,css,png,jpg,gif,ico,js -w tools/ -w wled00/data/ -x node tools/cdata.js\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/wled/WLED.git\"\n  },\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"bugs\": {\n    \"url\": \"https://github.com/wled/WLED/issues\"\n  },\n  \"homepage\": \"https://github.com/wled/WLED#readme\",\n  \"dependencies\": {\n    \"clean-css\": \"^5.3.3\",\n    \"html-minifier-terser\": \"^7.2.0\",\n    \"web-resource-inliner\": \"^7.0.0\",\n    \"nodemon\": \"^3.1.14\"\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  }\n}\n"
  },
  {
    "path": "pio-scripts/build_ui.py",
    "content": "Import(\"env\")\nimport shutil\n\nnode_ex = shutil.which(\"node\")\n# Check if Node.js is installed and present in PATH if it failed, abort the build\nif node_ex is None:\n    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')\n    exitCode = env.Execute(\"null\")\n    exit(exitCode)\nelse:\n    # Install the necessary node packages for the pre-build asset bundling script\n    print('\\x1b[6;33;42m' + 'Installing node packages' + '\\x1b[0m')\n    env.Execute(\"npm ci\")\n\n    # Call the bundling script\n    exitCode = env.Execute(\"npm run build\")\n\n    # If it failed, abort the build\n    if (exitCode):\n      print('\\x1b[0;31;43m' + 'npm run build fails check https://kno.wled.ge/advanced/compiling-wled/' + '\\x1b[0m')\n      exit(exitCode)\n"
  },
  {
    "path": "pio-scripts/dynarray.py",
    "content": "# Add a section to the linker script to store our dynamic arrays\n# This is implemented as a pio post-script to ensure that we can\n# place our linker script at the correct point in the command arguments.\nImport(\"env\")\nfrom pathlib import Path\n\nplatform = env.get(\"PIOPLATFORM\")\nscript_file = Path(f\"tools/dynarray_{platform}.ld\")\nif script_file.is_file():\n    linker_script = f\"-T{script_file}\"\n    if platform == \"espressif32\":\n        # For ESP32, the script must be added at the right point in the list\n        linkflags = env.get(\"LINKFLAGS\", [])\n        idx = linkflags.index(\"memory.ld\")    \n        linkflags.insert(idx+1, linker_script)    \n        env.Replace(LINKFLAGS=linkflags)\n    else:\n        # For other platforms, put it in last\n        env.Append(LINKFLAGS=[linker_script])\n"
  },
  {
    "path": "pio-scripts/load_usermods.py",
    "content": "Import('env')\nfrom collections import deque\nfrom pathlib import Path   # For OS-agnostic path manipulation\nimport re\nfrom urllib.parse import urlparse\nfrom click import secho\nfrom SCons.Script import Exit\nfrom platformio.builder.tools.piolib import LibBuilderBase\n\nusermod_dir = Path(env[\"PROJECT_DIR\"]).resolve() / \"usermods\"\n\n# Utility functions\ndef find_usermod(mod: str) -> Path:\n  \"\"\"Locate this library in the usermods folder.\n     We do this to avoid needing to rename a bunch of folders;\n     this could be removed later\n  \"\"\"\n  # Check name match\n  mp = usermod_dir / mod\n  if mp.exists():\n    return mp\n  mp = usermod_dir / f\"{mod}_v2\"\n  if mp.exists():\n    return mp\n  mp = usermod_dir / f\"usermod_v2_{mod}\"\n  if mp.exists():\n    return mp\n  raise RuntimeError(f\"Couldn't locate module {mod} in usermods directory!\")\n\n# Names of external/registry deps listed in custom_usermods.\n# Populated during parsing below; read by is_wled_module() at configure time.\n_custom_usermod_names: set[str] = set()\n\n# Matches any RFC-valid URL scheme (http, https, git, git+https, symlink, file, hg+ssh, etc.)\n_URL_SCHEME_RE = re.compile(r'^[a-zA-Z][a-zA-Z0-9+.-]*://')\n# SSH git URL: user@host:path  (e.g. git@github.com:user/repo.git#tag)\n_SSH_URL_RE = re.compile(r'^[^@\\s]+@[^@:\\s]+:[^:\\s]')\n# Explicit custom name: \"LibName = <spec>\"  (PlatformIO [<name>=]<spec> form)\n_NAME_EQ_RE = re.compile(r'^([A-Za-z0-9_.-]+)\\s*=\\s*(\\S.*)')\n\n\ndef _is_external_entry(line: str) -> bool:\n  \"\"\"Return True if line is a lib_deps-style external/registry entry.\"\"\"\n  if _NAME_EQ_RE.match(line):              # \"LibName = <spec>\"\n    return True\n  if _URL_SCHEME_RE.match(line):           # https://, git://, symlink://, etc.\n    return True\n  if _SSH_URL_RE.match(line):              # git@github.com:user/repo.git\n    return True\n  if '@' in line:                          # \"owner/Name @ ^1.0.0\"\n    return True\n  if re.match(r'^[^/\\s]+/[^/\\s]+$', line):  # \"owner/Name\"\n    return True\n  return False\n\n\ndef _predict_dep_name(entry: str) -> str | None:\n  \"\"\"Predict the library name PlatformIO will assign to this dep (best-effort).\n\n  Accuracy relies on the library's manifest \"name\" matching the repo/package\n  name in the spec. This holds for well-authored libraries; the libArchive\n  check (which requires library.json) provides an early-failure safety net.\n  \"\"\"\n  entry = entry.strip()\n  # \"LibName = <spec>\" — name is given explicitly; always use it\n  m = _NAME_EQ_RE.match(entry)\n  if m:\n    return m.group(1).strip()\n  # URL scheme: extract name from path\n  if _URL_SCHEME_RE.match(entry):\n    parsed = urlparse(entry)\n    if parsed.netloc in ('github.com', 'gitlab.com', 'bitbucket.com'):\n      parts = [p for p in parsed.path.split('/') if p]\n      if len(parts) >= 2:\n        name = parts[1]\n      else:\n        name = Path(parsed.path.rstrip('/')).name.strip()\n      if name.endswith('.git'):\n        name = name[:-4]\n      return name or None\n  # SSH git URL: git@github.com:user/repo.git#tag → repo\n  if _SSH_URL_RE.match(entry):\n    path_part = entry.split(':', 1)[1].split('#')[0].rstrip('/')\n    name = Path(path_part).name\n    return (name[:-4] if name.endswith('.git') else name) or None\n  # Versioned registry: \"owner/Name @ version\" → Name\n  if '@' in entry:\n    name_part = entry.split('@')[0].strip()\n    return name_part.split('/')[-1].strip() if '/' in name_part else name_part\n  # Plain registry: \"owner/Name\" → Name\n  if re.match(r'^[^/\\s]+/[^/\\s]+$', entry):\n    return entry.split('/')[-1].strip()\n  return None\n\n\ndef is_wled_module(dep: LibBuilderBase) -> bool:\n  \"\"\"Returns true if the specified library is a wled module.\"\"\"\n  return (\n    usermod_dir in Path(dep.src_dir).parents\n    or str(dep.name).startswith(\"wled-\")\n    or dep.name in _custom_usermod_names\n  )\n\n\n## Script starts here — parse custom_usermods\nraw_usermods = env.GetProjectOption(\"custom_usermods\", \"\")\nusermods_libdeps: list[str] = []\n\nfor line in raw_usermods.splitlines():\n  line = line.strip()\n  if not line or line.startswith('#') or line.startswith(';'):\n    continue\n\n  if _is_external_entry(line):\n    # External URL or registry entry: pass through to lib_deps unchanged.\n    predicted = _predict_dep_name(line)\n    if predicted:\n      _custom_usermod_names.add(predicted)\n    else:\n      secho(\n        f\"WARNING: Cannot determine library name for custom_usermods entry \"\n        f\"{line!r}. If it is not recognised as a WLED module at build time, \"\n        f\"ensure its library.json 'name' matches the repo name.\",\n        fg=\"yellow\", err=True)\n    usermods_libdeps.append(line)\n  else:\n    # Bare name(s): split on whitespace for backwards compatibility.\n    for token in line.split():\n      if token == '*':\n        for mod_path in sorted(usermod_dir.iterdir()):\n          if mod_path.is_dir() and (mod_path / 'library.json').exists():\n            _custom_usermod_names.add(mod_path.name)\n            usermods_libdeps.append(f\"symlink://{mod_path.resolve()}\")\n      else:\n        resolved = find_usermod(token)\n        _custom_usermod_names.add(resolved.name)\n        usermods_libdeps.append(f\"symlink://{resolved.resolve()}\")\n\nif usermods_libdeps:\n  env.GetProjectConfig().set(\"env:\" + env['PIOENV'], 'lib_deps', env.GetProjectOption('lib_deps') + usermods_libdeps)\n\n# Utility function for assembling usermod include paths\ndef cached_add_includes(dep, dep_cache: set, includes: deque):\n  \"\"\" Add dep's include paths to includes if it's not in the cache \"\"\"\n  if dep not in dep_cache:\n    dep_cache.add(dep)\n    for include in dep.get_include_dirs():\n      if include not in includes:\n        includes.appendleft(include)\n      if usermod_dir not in Path(dep.src_dir).parents:\n        # Recurse, but only for NON-usermods\n        for subdep in dep.depbuilders:\n          cached_add_includes(subdep, dep_cache, includes)\n\n# Monkey-patch ConfigureProjectLibBuilder to mark up the dependencies\n# Save the old value\nold_ConfigureProjectLibBuilder = env.ConfigureProjectLibBuilder\n\n# Our new wrapper\ndef wrapped_ConfigureProjectLibBuilder(xenv):\n  # Call the wrapped function\n  result = old_ConfigureProjectLibBuilder.clone(xenv)()\n\n  # Fix up include paths\n  # In PlatformIO >=6.1.17, this could be done prior to ConfigureProjectLibBuilder\n  wled_dir = xenv[\"PROJECT_SRC_DIR\"]\n  # Build a list of dependency include dirs\n  # TODO: Find out if this is the order that PlatformIO/SCons puts them in??\n  processed_deps = set()\n  extra_include_dirs = deque()  # Deque used for fast prepend\n  for dep in result.depbuilders:\n     cached_add_includes(dep, processed_deps, extra_include_dirs)\n\n  wled_deps = [dep for dep in result.depbuilders if is_wled_module(dep)]\n\n  broken_usermods = []\n  for dep in wled_deps:\n    # Add the wled folder to the include path\n    dep.env.PrependUnique(CPPPATH=str(wled_dir))\n    # Add WLED's own dependencies\n    for dir in extra_include_dirs:\n      dep.env.PrependUnique(CPPPATH=str(dir))\n    # Ensure debug info is emitted for this module's source files.\n    # validate_modules.py uses `nm --defined-only -l` on the final ELF to check\n    # that each module has at least one symbol placed in the binary.  The -l flag\n    # reads DWARF debug sections to map placed symbols back to their original source\n    # files; without -g those sections are absent and the check cannot attribute any\n    # symbol to a specific module.  We scope this to usermods only — the main WLED\n    # build and other libraries are unaffected.\n    dep.env.AppendUnique(CCFLAGS=[\"-g\"])\n    # Enforce that libArchive is not set; we must link them directly to the executable\n    if dep.lib_archive:\n      broken_usermods.append(dep)\n\n  if broken_usermods:\n    broken_usermods = [usermod.name for usermod in broken_usermods]\n    secho(\n      f\"ERROR: libArchive=false is missing on usermod(s) {' '.join(broken_usermods)} -- \"\n      f\"modules will not compile in correctly. Add '\\\"build\\\": {{\\\"libArchive\\\": false}}' \"\n      f\"to their library.json.\",\n      fg=\"red\", err=True)\n    Exit(1)\n\n  # Save the depbuilders list for later validation\n  xenv.Replace(WLED_MODULES=wled_deps)\n\n  return result\n\n# Apply the wrapper\nenv.AddMethod(wrapped_ConfigureProjectLibBuilder, \"ConfigureProjectLibBuilder\")\n"
  },
  {
    "path": "pio-scripts/obj-dump.py",
    "content": "# Little convenience script to get an object dump\n#   You may add \"-S\" to the objdump commandline (i.e. replace \"-D -C \" with \"-d -S -C \") \n#   to get source code intermixed with disassembly (SLOW !)\n\nImport('env')\n\ndef obj_dump_after_elf(source, target, env):\n    platform = env.PioPlatform()\n    board = env.BoardConfig()\n    mcu = board.get(\"build.mcu\", \"esp32\")\n\n    print(\"Create firmware.asm\")\n    if mcu == \"esp8266\":\n        env.Execute(\"xtensa-lx106-elf-objdump \"+ \"-D -C \" + str(target[0]) + \" > \"+ \"$BUILD_DIR/${PROGNAME}.asm\")\n    if mcu == \"esp32\":\n        env.Execute(\"xtensa-esp32-elf-objdump \"+ \"-D -C \" + str(target[0]) + \" > \"+ \"$BUILD_DIR/${PROGNAME}.asm\")\n    if mcu == \"esp32s2\":\n        env.Execute(\"xtensa-esp32s2-elf-objdump \"+ \"-D -C \" + str(target[0]) + \" > \"+ \"$BUILD_DIR/${PROGNAME}.asm\")\n    if mcu == \"esp32s3\":\n        env.Execute(\"xtensa-esp32s3-elf-objdump \"+ \"-D -C \" + str(target[0]) + \" > \"+ \"$BUILD_DIR/${PROGNAME}.asm\")\n    if mcu == \"esp32c3\":\n        env.Execute(\"riscv32-esp-elf-objdump \"+ \"-D -C \" + str(target[0]) + \" > \"+ \"$BUILD_DIR/${PROGNAME}.asm\")\n\nenv.AddPostAction(\"$BUILD_DIR/${PROGNAME}.elf\", [obj_dump_after_elf])\n"
  },
  {
    "path": "pio-scripts/output_bins.py",
    "content": "Import('env')\nimport os\nimport shutil\nimport gzip\nimport json\n\nOUTPUT_DIR = \"build_output{}\".format(os.path.sep)\n#OUTPUT_DIR = os.path.join(\"build_output\")\n\ndef _get_cpp_define_value(env, define):\n    define_list = [item[-1] for item in env[\"CPPDEFINES\"] if item[0] == define]\n\n    if define_list:\n        return define_list[0]\n\n    return None\n\ndef _create_dirs(dirs=[\"map\", \"release\", \"firmware\"]):\n    for d in dirs:\n        os.makedirs(os.path.join(OUTPUT_DIR, d), exist_ok=True)\n\ndef create_release(source):\n    release_name_def = _get_cpp_define_value(env, \"WLED_RELEASE_NAME\")\n    if release_name_def:\n        release_name = release_name_def.replace(\"\\\\\\\"\", \"\")\n        with open(\"package.json\", \"r\") as package:\n            version = json.load(package)[\"version\"]        \n        release_file = os.path.join(OUTPUT_DIR, \"release\", f\"WLED_{version}_{release_name}.bin\")\n        release_gz_file = release_file + \".gz\"\n        print(f\"Copying {source} to {release_file}\")\n        shutil.copy(source, release_file)\n        bin_gzip(release_file, release_gz_file)\n    else:\n        variant = env[\"PIOENV\"]\n        bin_file = \"{}firmware{}{}.bin\".format(OUTPUT_DIR, os.path.sep, variant)\n        print(f\"Copying {source} to {bin_file}\")\n        shutil.copy(source, bin_file)\n\ndef bin_rename_copy(source, target, env):\n    _create_dirs()\n    variant = env[\"PIOENV\"]\n    builddir = os.path.join(env[\"PROJECT_BUILD_DIR\"],  variant)\n    source_map = os.path.join(builddir, env[\"PROGNAME\"] + \".map\")\n\n    # create string with location and file names based on variant\n    map_file = \"{}map{}{}.map\".format(OUTPUT_DIR, os.path.sep, variant)\n\n    create_release(str(target[0]))\n\n    # copy firmware.map to map/<variant>.map\n    if os.path.isfile(\"firmware.map\"):\n        print(\"Found linker mapfile firmware.map\")\n        shutil.copy(\"firmware.map\", map_file)\n    if os.path.isfile(source_map):\n        print(f\"Found linker mapfile {source_map}\")\n        shutil.copy(source_map, map_file)\n\ndef bin_gzip(source, target):\n    # only create gzip for esp8266\n    if not env[\"PIOPLATFORM\"] == \"espressif8266\":\n        return\n    \n    print(f\"Creating gzip file {target} from {source}\")\n    with open(source,\"rb\") as fp:\n        with gzip.open(target, \"wb\", compresslevel = 9) as f:\n            shutil.copyfileobj(fp, f)\n\nenv.AddPostAction(\"$BUILD_DIR/${PROGNAME}.bin\", bin_rename_copy)\n"
  },
  {
    "path": "pio-scripts/set_metadata.py",
    "content": "Import('env')\nimport subprocess\nimport json\nimport re\n\ndef get_github_repo():\n    \"\"\"Extract GitHub repository name from git remote URL.\n    \n    Uses the remote that the current branch tracks, falling back to 'origin'.\n    This handles cases where repositories have multiple remotes or where the\n    main remote is not named 'origin'.\n    \n    Returns:\n        str: Repository name in 'owner/repo' format for GitHub repos,\n             'unknown' for non-GitHub repos, missing git CLI, or any errors.\n    \"\"\"\n    try:\n        remote_name = 'origin'  # Default fallback\n        \n        # Try to get the remote for the current branch\n        try:\n            # Get current branch name\n            branch_result = subprocess.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], \n                                         capture_output=True, text=True, check=True)\n            current_branch = branch_result.stdout.strip()\n            \n            # Get the remote for the current branch\n            remote_result = subprocess.run(['git', 'config', f'branch.{current_branch}.remote'], \n                                         capture_output=True, text=True, check=True)\n            tracked_remote = remote_result.stdout.strip()\n            \n            # Use the tracked remote if we found one\n            if tracked_remote:\n                remote_name = tracked_remote\n        except subprocess.CalledProcessError:\n            # If branch config lookup fails, continue with 'origin' as fallback\n            pass\n        \n        # Get the remote URL for the determined remote\n        result = subprocess.run(['git', 'remote', 'get-url', remote_name], \n                              capture_output=True, text=True, check=True)\n        remote_url = result.stdout.strip()\n        \n        # Check if it's a GitHub URL\n        if 'github.com' not in remote_url.lower():\n            return None\n        \n        # Parse GitHub URL patterns:\n        # https://github.com/owner/repo.git\n        # git@github.com:owner/repo.git\n        # https://github.com/owner/repo\n        \n        # Remove .git suffix if present\n        if remote_url.endswith('.git'):\n            remote_url = remote_url[:-4]\n        \n        # Handle HTTPS URLs\n        https_match = re.search(r'github\\.com/([^/]+/[^/]+)', remote_url, re.IGNORECASE)\n        if https_match:\n            return https_match.group(1)\n        \n        # Handle SSH URLs\n        ssh_match = re.search(r'github\\.com:([^/]+/[^/]+)', remote_url, re.IGNORECASE)\n        if ssh_match:\n            return ssh_match.group(1)\n        \n        return None\n        \n    except FileNotFoundError:\n        # Git CLI is not installed or not in PATH\n        return None\n    except subprocess.CalledProcessError:\n        # Git command failed (e.g., not a git repo, no remote, etc.)\n        return None\n    except Exception:\n        # Any other unexpected error\n        return None\n\n# WLED version is managed by package.json; this is picked up in several places\n# - It's integrated in to the UI code\n# - Here, for wled_metadata.cpp\n# - The output_bins script\n# We always take it from package.json to ensure consistency\nwith open(\"package.json\", \"r\") as package:\n    WLED_VERSION = json.load(package)[\"version\"]\n\ndef has_def(cppdefs, name):\n    \"\"\" Returns true if a given name is set in a CPPDEFINES collection \"\"\"\n    for f in cppdefs:\n        if isinstance(f, tuple):\n            f = f[0]\n        if f == name:\n            return True\n    return False\n\n\ndef add_wled_metadata_flags(env, node):    \n    cdefs = env[\"CPPDEFINES\"].copy()\n\n    if not has_def(cdefs, \"WLED_REPO\"):\n        repo = get_github_repo()\n        if repo:\n            cdefs.append((\"WLED_REPO\", f\"\\\\\\\"{repo}\\\\\\\"\"))\n\n    cdefs.append((\"WLED_VERSION\", WLED_VERSION))\n\n    # This transforms the node in to a Builder; it cannot be modified again\n    return env.Object(\n        node,\n        CPPDEFINES=cdefs\n    )\n   \nenv.AddBuildMiddleware(\n    add_wled_metadata_flags,\n    \"*/wled_metadata.cpp\"\n)\n"
  },
  {
    "path": "pio-scripts/strip-floats.py",
    "content": "Import('env')\n\n#\n# Dump build environment (for debug)\n#print env.Dump()\n#\n\nflags = \" \".join(env['LINKFLAGS'])\nflags = flags.replace(\"-u _printf_float\", \"\")\nflags = flags.replace(\"-u _scanf_float\", \"\")\nnewflags = flags.split()\n\nenv.Replace(\n  LINKFLAGS=newflags\n)"
  },
  {
    "path": "pio-scripts/user_config_copy.py",
    "content": "Import('env')\nimport os\nimport shutil\n\n# copy WLED00/my_config_sample.h to WLED00/my_config.h\nif os.path.isfile(\"wled00/my_config.h\"):\n    print (\"*** use existing my_config.h ***\")\nelse: \n    shutil.copy(\"wled00/my_config_sample.h\", \"wled00/my_config.h\")\n"
  },
  {
    "path": "pio-scripts/validate_modules.py",
    "content": "import os\nimport re\nimport subprocess\nfrom pathlib import Path   # For OS-agnostic path manipulation\nfrom click import secho\nfrom SCons.Script import Action, Exit\nImport(\"env\")\n\n\ndef read_lines(p: Path):\n    \"\"\" Read in the contents of a file for analysis \"\"\"\n    with p.open(\"r\", encoding=\"utf-8\", errors=\"ignore\") as f:\n        return f.readlines()\n\n\ndef _get_nm_path(env) -> str:\n    \"\"\" Derive the nm tool path from the build environment \"\"\"\n    if \"NM\" in env:\n        return env.subst(\"$NM\")\n    # Derive from the C compiler: xtensa-esp32-elf-gcc → xtensa-esp32-elf-nm\n    cc = env.subst(\"$CC\")\n    nm = re.sub(r'(gcc|g\\+\\+)$', 'nm', os.path.basename(cc))\n    return os.path.join(os.path.dirname(cc), nm)\n\n\ndef check_elf_modules(elf_path: Path, env, module_lib_builders) -> set[str]:\n    \"\"\" Check which modules have at least one defined symbol placed in the ELF.\n\n        The map file is not a reliable source for this: with LTO, original object\n        file paths are replaced by temporary ltrans.o partitions in all output\n        sections, making per-module attribution impossible from the map alone.\n        Instead we invoke nm --defined-only -l on the ELF, which uses DWARF debug\n        info to attribute each placed symbol to its original source file.\n\n        Requires usermod libraries to be compiled with -g so that DWARF sections\n        are present in the ELF.  load_usermods.py injects -g for all WLED modules\n        via dep.env.AppendUnique(CCFLAGS=[\"-g\"]).\n\n        Returns the set of build_dir basenames for confirmed modules.\n    \"\"\"\n    nm_path = _get_nm_path(env)\n    try:\n        result = subprocess.run(\n            [nm_path, \"--defined-only\", \"-l\", str(elf_path)],\n            capture_output=True, text=True, errors=\"ignore\", timeout=120,\n        )\n        nm_output = result.stdout\n    except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:\n        secho(f\"WARNING: nm failed ({e}); skipping per-module validation\", fg=\"yellow\", err=True)\n        return {Path(b.build_dir).name for b in module_lib_builders}  # conservative pass\n\n    # Match placed symbols against builders as we parse nm output, exiting early\n    # once all builders are accounted for.\n    # nm --defined-only still includes debugging symbols (type 'N') such as the\n    # per-CU markers GCC emits in .debug_info (e.g. \"usermod_example_cpp_6734d48d\").\n    # These live at address 0x00000000 in their debug section — not in any load\n    # segment — so filtering them out leaves only genuinely placed symbols.\n    # nm -l appends a tab-separated \"file:lineno\" location to each symbol line.\n    remaining = {Path(str(b.src_dir)): Path(b.build_dir).name for b in module_lib_builders}\n    found = set()\n\n    for line in nm_output.splitlines():\n        if not remaining:\n            break  # all builders matched\n        addr, _, _ = line.partition(' ')\n        if not addr.lstrip('0'):\n            continue  # zero address — skip debug-section marker\n        if '\\t' not in line:\n            continue\n        loc = line.rsplit('\\t', 1)[1]\n        # Strip trailing :lineno  (e.g. \"/path/to/foo.cpp:42\" → \"/path/to/foo.cpp\")\n        src_path = Path(loc.rsplit(':', 1)[0])\n        # Path.is_relative_to() handles OS-specific separators correctly without\n        # any regex, avoiding Windows path escaping issues.\n        for src_dir in list(remaining):\n            if src_path.is_relative_to(src_dir):\n                found.add(remaining.pop(src_dir))\n                break\n\n    return found\n\n\nDYNARRAY_SECTION = \".dtors\" if env.get(\"PIOPLATFORM\") == \"espressif8266\" else \".dynarray\"\nUSERMODS_SECTION = f\"{DYNARRAY_SECTION}.usermods.1\"\n\ndef count_usermod_objects(map_file: list[str]) -> int:\n    \"\"\" Returns the number of usermod objects in the usermod list \"\"\"\n    # Count the number of entries in the usermods table section\n    return len([x for x in map_file if USERMODS_SECTION in x])\n\n\ndef validate_map_file(source, target, env):\n    \"\"\" Validate that all modules appear in the output build \"\"\"\n    build_dir = Path(env.subst(\"$BUILD_DIR\"))\n    map_file_path = build_dir /  env.subst(\"${PROGNAME}.map\")\n\n    if not map_file_path.exists():\n        secho(f\"ERROR: Map file not found: {map_file_path}\", fg=\"red\", err=True)\n        Exit(1)\n\n    # Identify the WLED module builders, set by load_usermods.py\n    module_lib_builders = env['WLED_MODULES']\n\n    # Extract the values we care about\n    modules = {Path(builder.build_dir).name: builder.name for builder in module_lib_builders}\n    secho(f\"INFO: {len(modules)} libraries linked as WLED optional/user modules\")\n\n    # Now parse the map file\n    map_file_contents = read_lines(map_file_path)\n    usermod_object_count = count_usermod_objects(map_file_contents)\n    secho(f\"INFO: {usermod_object_count} usermod object entries\")\n\n    elf_path = build_dir / env.subst(\"${PROGNAME}.elf\")\n    confirmed_modules = check_elf_modules(elf_path, env, module_lib_builders)\n\n    missing_modules = [modname for mdir, modname in modules.items() if mdir not in confirmed_modules]\n    if missing_modules:\n        secho(\n            f\"ERROR: No symbols from {missing_modules} found in linked output!\",\n            fg=\"red\",\n            err=True)\n        Exit(1)\n    return None\n\nenv.Append(LINKFLAGS=[env.subst(\"-Wl,--Map=${BUILD_DIR}/${PROGNAME}.map\")])\nenv.AddPostAction(\"$BUILD_DIR/${PROGNAME}.elf\", Action(validate_map_file, cmdstr='Checking linked optional modules (usermods) in map file'))\n"
  },
  {
    "path": "platformio.ini",
    "content": "; PlatformIO Project Configuration File\n; Please visit documentation: https://docs.platformio.org/page/projectconf.html\n\n[platformio]\n# ------------------------------------------------------------------------------\n# ENVIRONMENTS\n#\n# Please uncomment one of the lines below to select your board(s)\n# (use `platformio_override.ini` when building for your own board; see `platformio_override.ini.sample` for an example)\n# ------------------------------------------------------------------------------\n\n# CI/release binaries\ndefault_envs = nodemcuv2\n               esp8266_2m\n               esp01_1m_full\n               nodemcuv2_160\n               esp8266_2m_160\n               esp01_1m_full_160\n               nodemcuv2_compat\n               esp8266_2m_compat\n               esp01_1m_full_compat\n               esp32dev\n               esp32dev_debug\n               esp32_eth\n               esp32_wrover\n               lolin_s2_mini\n               esp32c3dev\n               esp32c3dev_qio\n               esp32S3_wroom2\n               esp32s3dev_16MB_opi\n               esp32s3dev_8MB_opi\n               esp32s3dev_8MB_qspi\n               esp32s3_4M_qspi\n               usermods\n\nsrc_dir  = ./wled00\ndata_dir = ./wled00/data\nbuild_cache_dir = ~/.buildcache\nextra_configs =\n  platformio_override.ini\n\n[common]\n# ------------------------------------------------------------------------------\n# PLATFORM:\n#   !! DO NOT confuse platformio's ESP8266 development platform with Arduino core for ESP8266\n#\n#   arduino core 2.6.3 = platformIO 2.3.2\n#   arduino core 2.7.0 = platformIO 2.5.0\n# ------------------------------------------------------------------------------\narduino_core_2_6_3 = espressif8266@2.3.3\narduino_core_2_7_4 = espressif8266@2.6.2\narduino_core_3_0_0 = espressif8266@3.0.0\narduino_core_3_0_2 = espressif8266@3.2.0\narduino_core_3_1_0 = espressif8266@4.1.0\narduino_core_3_1_2 = espressif8266@4.2.1\n\n# Development platforms\narduino_core_develop = https://github.com/platformio/platform-espressif8266#develop\narduino_core_git = https://github.com/platformio/platform-espressif8266#feature/stage\n\n# Platform to use for ESP8266\nplatform_wled_default = ${common.arduino_core_3_1_2}\n# We use 2.7.4.7 for all, includes PWM flicker fix and Wstring optimization\n#platform_packages = tasmota/framework-arduinoespressif8266 @ 3.20704.7\nplatform_packages = platformio/toolchain-xtensa @ ~2.100300.220621 #2.40802.200502\n                    platformio/tool-esptool #@ ~1.413.0\n                    platformio/tool-esptoolpy #@ ~1.30000.0\n\n## previous platform for 8266, in case of problems with the new one\n## you'll need  makuna/NeoPixelBus@ 2.6.9 for arduino_core_3_0_2, which does not support Ucs890x\n;; platform_wled_default = ${common.arduino_core_3_0_2}\n;; platform_packages = tasmota/framework-arduinoespressif8266 @ 3.20704.7\n;;                    platformio/toolchain-xtensa @ ~2.40802.200502\n;;                    platformio/tool-esptool @ ~1.413.0\n;;                    platformio/tool-esptoolpy @ ~1.30000.0\n\n# ------------------------------------------------------------------------------\n# FLAGS: DEBUG\n# esp8266 : see https://docs.platformio.org/en/latest/platforms/espressif8266.html#debug-level\n# esp32   : see https://docs.platformio.org/en/latest/platforms/espressif32.html#debug-level\n# ------------------------------------------------------------------------------\ndebug_flags = -D DEBUG=1 -D WLED_DEBUG\n  -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_UPDATER -DDEBUG_ESP_OTA -DDEBUG_TLS_MEM ;; for esp8266\n  # if needed (for memleaks etc) also add; -DDEBUG_ESP_OOM -include \"umm_malloc/umm_malloc_cfg.h\"\n  # -DDEBUG_ESP_CORE is not working right now\n\n# ------------------------------------------------------------------------------\n# FLAGS: ldscript (available ldscripts at https://github.com/esp8266/Arduino/tree/master/tools/sdk/ld)\n#    ldscript_2m1m (2048 KB) = 1019 KB sketch, 4 KB eeprom, 1004 KB spiffs, 16 KB reserved\n#    ldscript_4m1m (4096 KB) = 1019 KB sketch, 4 KB eeprom, 1002 KB spiffs, 16 KB reserved, 2048 KB empty/ota?\n#\n# Available lwIP variants (macros):\n#    -DPIO_FRAMEWORK_ARDUINO_LWIP_HIGHER_BANDWIDTH  = v1.4 Higher Bandwidth (default)\n#    -DPIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY       = v2 Lower Memory\n#    -DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH = v2 Higher Bandwidth\n#    -DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH\n#\n# BearSSL performance:\n#  When building with -DSECURE_CLIENT=SECURE_CLIENT_BEARSSL, please add `board_build.f_cpu = 160000000` to the environment configuration\n#\n# BearSSL ciphers:\n#   When building on core >= 2.5, you can add the build flag -DBEARSSL_SSL_BASIC in order to build BearSSL with a limited set of ciphers:\n#     TLS_RSA_WITH_AES_128_CBC_SHA256 / AES128-SHA256\n#     TLS_RSA_WITH_AES_256_CBC_SHA256 / AES256-SHA256\n#     TLS_RSA_WITH_AES_128_CBC_SHA / AES128-SHA\n#     TLS_RSA_WITH_AES_256_CBC_SHA / AES256-SHA\n#  This reduces the OTA size with ~45KB, so it's especially useful on low memory boards (512k/1m).\n# ------------------------------------------------------------------------------\nbuild_flags =\n  -DMQTT_MAX_PACKET_SIZE=1024\n  -DSECURE_CLIENT=SECURE_CLIENT_BEARSSL\n  -DBEARSSL_SSL_BASIC\n  -D CORE_DEBUG_LEVEL=0\n  -D NDEBUG\n  -Wno-attributes ;; silence warnings about unknown attribute 'maybe_unused' in NeoPixelBus\n  #build_flags for the IRremoteESP8266 library (enabled decoders have to appear here)\n  -D _IR_ENABLE_DEFAULT_=false\n  -D DECODE_HASH=true\n  -D DECODE_NEC=true\n  -D DECODE_SONY=true\n  -D DECODE_SAMSUNG=true\n  -D DECODE_LG=true\n  -DWLED_USE_MY_CONFIG\n  -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\n\nbuild_unflags =\n\nldscript_1m128k = eagle.flash.1m128.ld\nldscript_2m512k = eagle.flash.2m512.ld\nldscript_2m1m = eagle.flash.2m1m.ld\nldscript_4m1m = eagle.flash.4m1m.ld\n\n[scripts_defaults]\nextra_scripts =\n  pre:pio-scripts/set_metadata.py\n  post:pio-scripts/output_bins.py\n  post:pio-scripts/strip-floats.py\n  post:pio-scripts/dynarray.py\n  pre:pio-scripts/user_config_copy.py\n  pre:pio-scripts/load_usermods.py\n  pre:pio-scripts/build_ui.py\n  post:pio-scripts/validate_modules.py  ;; double-check the build output usermods\n  ; post:pio-scripts/obj-dump.py  ;; convenience script to create a disassembly dump of the firmware (hardcore debugging)\n\n# ------------------------------------------------------------------------------\n# COMMON SETTINGS:\n# ------------------------------------------------------------------------------\n[env]\nframework = arduino\nboard_build.flash_mode = dout\nmonitor_speed = 115200\n# slow upload speed but most compatible (use platformio_override.ini to use faster speed)\nupload_speed = 115200\n\n# ------------------------------------------------------------------------------\n# LIBRARIES: required dependencies\n#   Please note that we don't always use the latest version of a library.\n#\n#   The following libraries have been included (and some of them changed) in the source:\n#     ArduinoJson@5.13.5, E131@1.0.0(changed), Time@1.5, Timezone@1.2.1\n# ------------------------------------------------------------------------------\nlib_compat_mode = strict\nlib_deps =\n    fastled/FastLED @ 3.6.0\n    IRremoteESP8266 @ 2.8.2\n    https://github.com/Makuna/NeoPixelBus.git#a0919d1c10696614625978dd6fb750a1317a14ce\n    https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.2\n    marvinroger/AsyncMqttClient @ 0.9.0\n  # for I2C interface\n    ;Wire\n  # ESP-NOW library\n    ;gmag11/QuickESPNow @ ~0.7.0\n    https://github.com/blazoncek/QuickESPNow.git#optional-debug\n  #For use of the TTGO T-Display ESP32 Module with integrated TFT display uncomment the following line\n    #TFT_eSPI\n  #For compatible OLED display uncomment following\n    #olikraus/U8g2 #@ ~2.33.15\n  #For Dallas sensor uncomment following\n    #paulstoffregen/OneWire @ ~2.3.8\n  #For BME280 sensor uncomment following\n    #BME280 @ ~3.0.0\n    ;adafruit/Adafruit BMP280 Library @ 2.1.0\n    ;adafruit/Adafruit CCS811 Library @ 1.0.4\n    ;adafruit/Adafruit Si7021 Library @ 1.4.0\n  #For MAX1704x Lipo Monitor / Fuel Gauge uncomment following\n    ; https://github.com/adafruit/Adafruit_BusIO @ 1.14.5\n    ; https://github.com/adafruit/Adafruit_MAX1704X @ 1.0.2\n  #For MPU6050 IMU uncomment follwoing\n    ;electroniccats/MPU6050 @1.0.1\n  # SHT85\n    ;robtillaart/SHT85@~0.3.3\n\nextra_scripts = ${scripts_defaults.extra_scripts}\n\n[esp8266]\nbuild_unflags = ${common.build_unflags}\nbuild_flags =\n  -DESP8266\n  -DFP_IN_IROM\n  ;-Wno-deprecated-declarations\n  ;-Wno-register  ;; leaves some warnings when compiling C files: command-line option '-Wno-register' is valid for C++/ObjC++ but not for C\n  ;-Dregister= # remove warnings in C++17 due to use of deprecated register keyword by the FastLED library ;; warning: this can be dangerous\n  -Wno-misleading-indentation\n  ; NONOSDK22x_190703 = 2.2.2-dev(38a443e)\n  -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x_190703\n  ; lwIP 2 - Higher Bandwidth no Features\n  ;  -DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH\n  ; lwIP 1.4 - Higher Bandwidth (Aircoookie has)\n  -DPIO_FRAMEWORK_ARDUINO_LWIP_HIGHER_BANDWIDTH\n  ; VTABLES in Flash\n  -DVTABLES_IN_FLASH\n  ; restrict to minimal mime-types\n  -DMIMETYPE_MINIMAL\n  ; other special-purpose framework flags (see https://docs.platformio.org/en/latest/platforms/espressif8266.html)\n  ; decrease code cache size and increase IRAM to fit all pixel functions\n  -D PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48 ;; in case of linker errors like \"section `.text1' will not fit in region `iram1_0_seg'\"\n  ; -D PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48_SECHEAP_SHARED ;; (experimental) adds some extra heap, but may cause slowdown\n  -D NON32XFER_HANDLER ;; ask forgiveness for PROGMEM misuse\n\nlib_deps =\n  #https://github.com/lorol/LITTLEFS.git\n  ESPAsyncTCP @ 1.2.2\n  ESPAsyncUDP\n  ESP8266PWM\n  ${env.lib_deps}\n\n;; compatibilty flags - same as 0.14.0 which seems to work better on some 8266 boards. Not using PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48\nbuild_flags_compat =\n  -DESP8266\n  -DFP_IN_IROM\n  ;;-Wno-deprecated-declarations\n  -Wno-misleading-indentation\n  ;;-Wno-attributes ;; silence warnings about unknown attribute 'maybe_unused' in NeoPixelBus\n  -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x_190703\n  -DPIO_FRAMEWORK_ARDUINO_LWIP_HIGHER_BANDWIDTH\n  -DVTABLES_IN_FLASH\n  -DMIMETYPE_MINIMAL\n  -DWLED_SAVE_IRAM ;; needed to prevent linker error\n\n;; this platform version was used for WLED 0.14.0\nplatform_compat = espressif8266@4.2.0\nplatform_packages_compat =\n                    platformio/toolchain-xtensa @ ~2.100300.220621 #2.40802.200502\n                    platformio/tool-esptool #@ ~1.413.0\n                    platformio/tool-esptoolpy #@ ~1.30000.0\n\n;; experimental - for using older NeoPixelBus 2.7.9\nlib_deps_compat =\n  ESPAsyncTCP @ 1.2.2\n  ESPAsyncUDP\n  ESP8266PWM\n  fastled/FastLED @ 3.6.0\n  IRremoteESP8266 @ 2.8.2\n  makuna/NeoPixelBus @ 2.7.9\n  https://github.com/blazoncek/QuickESPNow.git#optional-debug\n  https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.0\n\n[esp32_all_variants]\nlib_deps =\n  esp32async/AsyncTCP @ 3.4.7\n  bitbank2/AnimatedGIF@^1.4.7\n  https://github.com/Aircoookie/GifDecoder.git#bc3af189b6b1e06946569f6b4287f0b79a860f8e\nbuild_flags =\n  -D CONFIG_ASYNC_TCP_USE_WDT=0\n  -D CONFIG_ASYNC_TCP_STACK_SIZE=8192\n  -D WLED_ENABLE_GIF\n\n[esp32]\nplatform = ${esp32_idf_V4.platform}\nplatform_packages =\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${esp32_idf_V4.build_flags}\nlib_deps = ${esp32_idf_V4.lib_deps}\n\ntiny_partitions = tools/WLED_ESP32_2MB_noOTA.csv\ndefault_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv\nextended_partitions = tools/WLED_ESP32_4MB_700k_FS.csv\nbig_partitions = tools/WLED_ESP32_4MB_256KB_FS.csv     ;; 1.8MB firmware, 256KB filesystem, coredump support\nlarge_partitions = tools/WLED_ESP32_8MB.csv\nextreme_partitions = tools/WLED_ESP32_16MB_9MB_FS.csv\n\nboard_build.partitions = ${esp32.default_partitions}   ;; default partioning for 4MB Flash - can be overridden in build envs\n# additional build flags for audioreactive - must be applied globally\nAR_build_flags = ;; -fsingle-precision-constant ;; forces ArduinoFFT to use float math (2x faster)\nAR_lib_deps =  ;; for pre-usermod-library platformio_override compatibility\n\n\n[esp32_idf_V4]\n;; build environment for ESP32 using ESP-IDF 4.4.x / arduino-esp32 v2.0.5\n;; *** important: build flags from esp32_idf_V4 are inherited by _all_ esp32-based MCUs: esp32, esp32s2, esp32s3, esp32c3\n;;\n;; please note that you can NOT update existing ESP32 installs with a \"V4\" build. Also updating by OTA will not work properly.\n;; You need to completely erase your device (esptool erase_flash) first, then install the \"V4\" build from VSCode+platformio.\n\nplatform = 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\nplatform_packages =\nbuild_unflags = ${common.build_unflags}\nbuild_flags = -g\n  -Wshadow=compatible-local ;; emit warning in case a local variable \"shadows\" another local one\n  -DARDUINO_ARCH_ESP32 -DESP32\n  ${esp32_all_variants.build_flags}\n  -D WLED_ENABLE_DMX_INPUT\nlib_deps =\n  ${esp32_all_variants.lib_deps}\n  https://github.com/someweisguy/esp_dmx.git#47db25d8c515e76fabcf5fc5ab0b786f98eeade0\n  ${env.lib_deps}\n\n[esp32s2]\n;; generic definitions for all ESP32-S2 boards\nplatform = ${esp32_idf_V4.platform}\nplatform_packages = ${esp32_idf_V4.platform_packages}\nbuild_unflags = ${common.build_unflags}\nbuild_flags = -g\n  -DARDUINO_ARCH_ESP32\n  -DARDUINO_ARCH_ESP32S2\n  -DCONFIG_IDF_TARGET_ESP32S2=1\n  -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=0\n  -DCO\n  -DARDUINO_USB_MODE=0 ;; this flag is mandatory for ESP32-S2 !\n  ;; 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:\n  ;; ARDUINO_USB_CDC_ON_BOOT\n  ${esp32_idf_V4.build_flags}\nlib_deps =\n  ${esp32_idf_V4.lib_deps}\nboard_build.partitions = ${esp32.default_partitions}   ;; default partioning for 4MB Flash - can be overridden in build envs\n\n[esp32c3]\n;; generic definitions for all ESP32-C3 boards\nplatform = ${esp32_idf_V4.platform}\nplatform_packages = ${esp32_idf_V4.platform_packages}\nbuild_unflags = ${common.build_unflags}\nbuild_flags = -g\n  -DARDUINO_ARCH_ESP32\n  -DARDUINO_ARCH_ESP32C3\n  -DCONFIG_IDF_TARGET_ESP32C3=1\n  -DCO\n  -DARDUINO_USB_MODE=1 ;; this flag is mandatory for ESP32-C3\n  ;; 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:\n  ;; ARDUINO_USB_CDC_ON_BOOT\n  ${esp32_idf_V4.build_flags}\nlib_deps =\n  ${esp32_idf_V4.lib_deps}\nboard_build.partitions = ${esp32.default_partitions}   ;; default partioning for 4MB Flash - can be overridden in build envs\nboard_build.flash_mode = qio\n\n[esp32s3]\n;; generic definitions for all ESP32-S3 boards\nplatform = ${esp32_idf_V4.platform}\nplatform_packages = ${esp32_idf_V4.platform_packages}\nbuild_unflags = ${common.build_unflags}\nbuild_flags = -g\n  -DESP32\n  -DARDUINO_ARCH_ESP32\n  -DARDUINO_ARCH_ESP32S3\n  -DCONFIG_IDF_TARGET_ESP32S3=1\n  -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_DFU_ON_BOOT=0\n  -DCO\n  ;; 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:\n  ;; ARDUINO_USB_MODE, ARDUINO_USB_CDC_ON_BOOT\n  ${esp32_idf_V4.build_flags}\nlib_deps =\n  ${esp32_idf_V4.lib_deps}\nboard_build.partitions = ${esp32.large_partitions}   ;; default partioning for 8MB flash - can be overridden in build envs\n\n\n# ------------------------------------------------------------------------------\n# WLED BUILDS\n# ------------------------------------------------------------------------------\n\n[env:nodemcuv2]\nboard = nodemcuv2\nplatform = ${common.platform_wled_default}\nplatform_packages = ${common.platform_packages}\nboard_build.ldscript = ${common.ldscript_4m1m}\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\\\"ESP8266\\\" #-DWLED_DISABLE_2D\n  -D WLED_DISABLE_PARTICLESYSTEM2D\nlib_deps = ${esp8266.lib_deps}\nmonitor_filters = esp8266_exception_decoder\n\n[env:nodemcuv2_compat]\nextends = env:nodemcuv2\n;; using platform version and build options from WLED 0.14.0\nplatform = ${esp8266.platform_compat}\nplatform_packages = ${esp8266.platform_packages_compat}\nbuild_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\\\"ESP8266_compat\\\" #-DWLED_DISABLE_2D\n  -D WLED_DISABLE_PARTICLESYSTEM2D\n;; lib_deps = ${esp8266.lib_deps_compat} ;; experimental - use older NeoPixelBus 2.7.9\n\n[env:nodemcuv2_160]\nextends = env:nodemcuv2\nboard_build.f_cpu = 160000000L\nbuild_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\\\"ESP8266_160\\\" #-DWLED_DISABLE_2D\n  -D WLED_DISABLE_PARTICLESYSTEM2D\ncustom_usermods = audioreactive\n\n[env:esp8266_2m]\nboard = esp_wroom_02\nplatform = ${common.platform_wled_default}\nplatform_packages = ${common.platform_packages}\nboard_build.ldscript = ${common.ldscript_2m512k}\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\\\"ESP02\\\"\n  -D WLED_DISABLE_PARTICLESYSTEM2D\n  -D WLED_DISABLE_PARTICLESYSTEM1D\nlib_deps = ${esp8266.lib_deps}\n\n[env:esp8266_2m_compat]\nextends = env:esp8266_2m\n;; using platform version and build options from WLED 0.14.0\nplatform = ${esp8266.platform_compat}\nplatform_packages = ${esp8266.platform_packages_compat}\nbuild_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\\\"ESP02_compat\\\" #-DWLED_DISABLE_2D\n  -D WLED_DISABLE_PARTICLESYSTEM1D\n  -D WLED_DISABLE_PARTICLESYSTEM2D\n\n[env:esp8266_2m_160]\nextends = env:esp8266_2m\nboard_build.f_cpu = 160000000L\nbuild_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\\\"ESP02_160\\\"\n  -D WLED_DISABLE_PARTICLESYSTEM1D\n  -D WLED_DISABLE_PARTICLESYSTEM2D\ncustom_usermods = audioreactive\n\n[env:esp01_1m_full]\nboard = esp01_1m\nplatform = ${common.platform_wled_default}\nplatform_packages = ${common.platform_packages}\nboard_build.ldscript = ${common.ldscript_1m128k}\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\\\"ESP01\\\" -D WLED_DISABLE_OTA\n  ; -D WLED_USE_REAL_MATH ;; may fix wrong sunset/sunrise times, at the cost of 7064 bytes FLASH and 975 bytes RAM\n  -D WLED_DISABLE_PARTICLESYSTEM1D\n  -D WLED_DISABLE_PARTICLESYSTEM2D\nlib_deps = ${esp8266.lib_deps}\n\n[env:esp01_1m_full_compat]\nextends = env:esp01_1m_full\n;; using platform version and build options from WLED 0.14.0\nplatform = ${esp8266.platform_compat}\nplatform_packages = ${esp8266.platform_packages_compat}\nbuild_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\\\"ESP01_compat\\\" -D WLED_DISABLE_OTA #-DWLED_DISABLE_2D\n  -D WLED_DISABLE_PARTICLESYSTEM1D\n  -D WLED_DISABLE_PARTICLESYSTEM2D\n\n[env:esp01_1m_full_160]\nextends = env:esp01_1m_full\nboard_build.f_cpu = 160000000L\nbuild_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\\\"ESP01_160\\\" -D WLED_DISABLE_OTA\n  ; -D WLED_USE_REAL_MATH ;; may fix wrong sunset/sunrise times, at the cost of 7064 bytes FLASH and 975 bytes RAM\n  -D WLED_DISABLE_PARTICLESYSTEM1D\n  -D WLED_DISABLE_PARTICLESYSTEM2D\ncustom_usermods = audioreactive\n\n[env:esp32dev]\nboard = esp32dev\nplatform = ${esp32_idf_V4.platform}\nplatform_packages = ${esp32_idf_V4.platform_packages}\nbuild_unflags = ${common.build_unflags}\ncustom_usermods = audioreactive\nbuild_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\\\"ESP32\\\" #-D WLED_DISABLE_BROWNOUT_DET\n              -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for \"classic ESP32\" when building with arduino-esp32 >=2.0.3\nlib_deps = ${esp32_idf_V4.lib_deps}\nmonitor_filters = esp32_exception_decoder\nboard_build.partitions = ${esp32.default_partitions}\nboard_build.flash_mode = dio\n\n[env:esp32dev_debug]\nextends = env:esp32dev\nupload_speed = 921600\nbuild_flags = ${common.build_flags} ${esp32_idf_V4.build_flags}\n              -D WLED_DEBUG\n              -D WLED_RELEASE_NAME=\\\"ESP32_DEBUG\\\"\n\n[env:esp32dev_8M]\nboard = esp32dev\nplatform = ${esp32_idf_V4.platform}\nplatform_packages = ${esp32_idf_V4.platform_packages}\ncustom_usermods = audioreactive\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\\\"ESP32_8M\\\" #-D WLED_DISABLE_BROWNOUT_DET\n  -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for \"classic ESP32\" when building with arduino-esp32 >=2.0.3\nlib_deps = ${esp32_idf_V4.lib_deps}\nmonitor_filters = esp32_exception_decoder\nboard_build.partitions = ${esp32.large_partitions}\nboard_upload.flash_size = 8MB\nboard_upload.maximum_size = 8388608\n; board_build.f_flash = 80000000L\nboard_build.flash_mode = dio\n\n[env:esp32dev_16M]\nboard = esp32dev\nplatform = ${esp32_idf_V4.platform}\nplatform_packages = ${esp32_idf_V4.platform_packages}\ncustom_usermods = audioreactive\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\\\"ESP32_16M\\\" #-D WLED_DISABLE_BROWNOUT_DET\n              -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for \"classic ESP32\" when building with arduino-esp32 >=2.0.3\nlib_deps = ${esp32_idf_V4.lib_deps}\nmonitor_filters = esp32_exception_decoder\nboard_build.partitions = ${esp32.extreme_partitions}\nboard_upload.flash_size = 16MB\nboard_upload.maximum_size = 16777216\nboard_build.f_flash = 80000000L\nboard_build.flash_mode = dio\n\n[env:esp32_eth]\nboard = esp32-poe\nplatform = ${esp32_idf_V4.platform}\nplatform_packages = ${esp32_idf_V4.platform_packages}\nupload_speed = 921600\ncustom_usermods = audioreactive\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\\\"ESP32_Ethernet\\\" -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1\n  -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\n  -D DATA_PINS=4 ;; default led pin = 16 conflicts with pins used for ethernet\n  ; -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\n  -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for \"classic ESP32\" when building with arduino-esp32 >=2.0.3\nlib_deps = ${esp32.lib_deps}\nboard_build.partitions = ${esp32.default_partitions}\nboard_build.flash_mode = dio\n\n[env:esp32_wrover]\nextends = esp32_idf_V4\nboard = ttgo-t7-v14-mini32\nboard_build.f_flash = 80000000L\nboard_build.flash_mode = qio\nboard_build.partitions = ${esp32.extended_partitions}\ncustom_usermods = audioreactive\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\\\"ESP32_WROVER\\\"\n  -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for \"classic ESP32\" when building with arduino-esp32 >=2.0.3\n  -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\n  -D DATA_PINS=25\nlib_deps = ${esp32_idf_V4.lib_deps}\n  \n[env:esp32c3dev]\nextends = esp32c3\nplatform = ${esp32c3.platform}\nplatform_packages = ${esp32c3.platform_packages}\nframework = arduino\nboard = esp32-c3-devkitm-1\nboard_build.partitions = ${esp32.default_partitions}\nbuild_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME=\\\"ESP32-C3\\\"\n  -D WLED_WATCHDOG_TIMEOUT=0\n  -DLOLIN_WIFI_FIX ; seems to work much better with this\n  -DARDUINO_USB_CDC_ON_BOOT=1 ;; for virtual CDC USB\n  ;-DARDUINO_USB_CDC_ON_BOOT=0   ;; for serial-to-USB chip\nupload_speed = 460800\nbuild_unflags = ${common.build_unflags}\nlib_deps = ${esp32c3.lib_deps}\nboard_build.flash_mode = dio ; safe default, required for OTA updates to 0.16 from older version which used dio (must match the bootloader!)\n\n[env:esp32c3dev_qio]\nextends = env:esp32c3dev\nbuild_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME=\\\"ESP32-C3-QIO\\\"\nboard_build.flash_mode = qio ; qio is faster and works on almost all boards (some boards may use dio to get 2 extra pins)\n\n[env:esp32s3dev_16MB_opi]\n;; ESP32-S3 development board, with 16MB FLASH and >= 8MB PSRAM (memory_type: qio_opi)\nboard = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support\nboard_build.arduino.memory_type = qio_opi     ;; use with PSRAM: 8MB or 16MB\nplatform = ${esp32s3.platform}\nplatform_packages = ${esp32s3.platform_packages}\nupload_speed = 921600\ncustom_usermods = audioreactive\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\\\"ESP32-S3_16MB_opi\\\"\n  -D WLED_WATCHDOG_TIMEOUT=0\n  ;-D ARDUINO_USB_CDC_ON_BOOT=0  ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip\n  -D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1      ;; for boards with USB-OTG connector only (USBCDC or \"TinyUSB\")\n  -DBOARD_HAS_PSRAM\nlib_deps = ${esp32s3.lib_deps}\nboard_build.partitions = ${esp32.extreme_partitions}\nboard_upload.flash_size = 16MB\nboard_upload.maximum_size = 16777216\nboard_build.f_flash = 80000000L\nboard_build.flash_mode = qio\nmonitor_filters = esp32_exception_decoder\n\n[env:esp32s3dev_8MB_opi]\n;; ESP32-S3 development board, with 8MB FLASH and >= 8MB PSRAM (memory_type: qio_opi)\nboard = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support\nboard_build.arduino.memory_type = qio_opi     ;; use with PSRAM: 8MB or 16MB\nplatform = ${esp32s3.platform}\nplatform_packages = ${esp32s3.platform_packages}\nupload_speed = 921600\ncustom_usermods = audioreactive\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\\\"ESP32-S3_8MB_opi\\\"\n  -D WLED_WATCHDOG_TIMEOUT=0\n  ;-D ARDUINO_USB_CDC_ON_BOOT=0  ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip\n  -D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1      ;; for boards with USB-OTG connector only (USBCDC or \"TinyUSB\")\n  -DBOARD_HAS_PSRAM\nlib_deps = ${esp32s3.lib_deps}\nboard_build.partitions = ${esp32.large_partitions}\nboard_build.f_flash = 80000000L\nboard_build.flash_mode = qio\nmonitor_filters = esp32_exception_decoder\n\n[env:esp32s3dev_8MB_qspi]\n;; 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\nextends = env:esp32s3dev_8MB_opi\nboard_build.arduino.memory_type = qio_qspi\nboard_build.flash_mode = qio\nbuild_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\\\"ESP32-S3_8MB_qspi\\\"\n  -D WLED_WATCHDOG_TIMEOUT=0\n  ;-D ARDUINO_USB_CDC_ON_BOOT=0  ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip\n  -D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1      ;; for boards with USB-OTG connector only (USBCDC or \"TinyUSB\")\n  -DBOARD_HAS_PSRAM\n  ;; -DLOLIN_WIFI_FIX ;; uncomment if you have WiFi connectivity problems\nmonitor_filters = esp32_exception_decoder\n\n[env:esp32S3_wroom2]\n;; For ESP32-S3 WROOM-2, a.k.a. ESP32-S3 DevKitC-1 v1.1\n;; with >= 16MB FLASH and >= 8MB PSRAM (memory_type: opi_opi)\nplatform = ${esp32s3.platform}\nplatform_packages = ${esp32s3.platform_packages}\nboard = esp32s3camlcd ;; this is the only standard board with \"opi_opi\"\nboard_build.arduino.memory_type = opi_opi\nupload_speed = 921600\ncustom_usermods = audioreactive\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\\\"ESP32-S3_WROOM-2\\\"\n  -D WLED_WATCHDOG_TIMEOUT=0\n  -D ARDUINO_USB_CDC_ON_BOOT=0  ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip\n  ;; -D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1      ;; for boards with USB-OTG connector only (USBCDC or \"TinyUSB\")\n  -DBOARD_HAS_PSRAM\n  -D LEDPIN=38 -D DATA_PINS=38 ;; buildin WS2812b LED\n  -D BTNPIN=0 -D RLYPIN=16 -D IRPIN=17 -D AUDIOPIN=-1\n  ;;-D WLED_DEBUG\n  -D SR_DMTYPE=1 -D I2S_SDPIN=13 -D I2S_CKPIN=14 -D I2S_WSPIN=15 -D MCLK_PIN=4  ;; I2S mic\nlib_deps = ${esp32s3.lib_deps}\n\nboard_build.partitions = ${esp32.extreme_partitions}\nboard_upload.flash_size = 16MB\nboard_upload.maximum_size = 16777216\nmonitor_filters = esp32_exception_decoder\n\n[env:esp32S3_wroom2_32MB]\n;; For ESP32-S3 WROOM-2 with 32MB Flash, and >= 8MB PSRAM (memory_type: opi_opi)\nextends = env:esp32S3_wroom2\nbuild_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\\\"ESP32-S3_WROOM-2_32MB\\\"\n  -D WLED_WATCHDOG_TIMEOUT=0\n  -D ARDUINO_USB_CDC_ON_BOOT=0  ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip\n  ;; -D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1      ;; for boards with USB-OTG connector only (USBCDC or \"TinyUSB\")\n  -DBOARD_HAS_PSRAM\n  -D LEDPIN=38 -D DATA_PINS=38 ;; buildin WS2812b LED\n  -D BTNPIN=0 -D RLYPIN=16 -D IRPIN=17 -D AUDIOPIN=-1\n  ;;-D WLED_DEBUG\n  -D SR_DMTYPE=1 -D I2S_SDPIN=13 -D I2S_CKPIN=14 -D I2S_WSPIN=15 -D MCLK_PIN=4  ;; I2S mic\nboard_build.partitions = tools/WLED_ESP32_32MB.csv\nboard_upload.flash_size = 32MB\nboard_upload.maximum_size = 33554432\nmonitor_filters = esp32_exception_decoder\n\n[env:esp32s3_4M_qspi]\n;; ESP32-S3, with 4MB FLASH and <= 4MB PSRAM (memory_type: qio_qspi)\nboard = lolin_s3_mini ;; -S3 mini, 4MB flash 2MB PSRAM \nplatform = ${esp32s3.platform}\nplatform_packages = ${esp32s3.platform_packages}\nupload_speed = 921600\ncustom_usermods = audioreactive\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\\\"ESP32-S3_4M_qspi\\\"\n  -DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1      ;; for boards with USB-OTG connector only (USBCDC or \"TinyUSB\")\n  -DBOARD_HAS_PSRAM\n  -DLOLIN_WIFI_FIX ; seems to work much better with this\n  -D WLED_WATCHDOG_TIMEOUT=0\nlib_deps = ${esp32s3.lib_deps}\nboard_build.partitions = ${esp32.default_partitions}\nboard_build.f_flash = 80000000L\nboard_build.flash_mode = qio\nmonitor_filters = esp32_exception_decoder\n\n[env:lolin_s2_mini]\nplatform = ${esp32s2.platform}\nplatform_packages = ${esp32s2.platform_packages}\nboard = lolin_s2_mini\nboard_build.partitions = ${esp32.default_partitions}\nboard_build.flash_mode = qio\nboard_build.f_flash = 80000000L\ncustom_usermods = audioreactive\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp32s2.build_flags} -D WLED_RELEASE_NAME=\\\"ESP32-S2\\\"\n  -DARDUINO_USB_CDC_ON_BOOT=1\n  -DARDUINO_USB_MSC_ON_BOOT=0\n  -DARDUINO_USB_DFU_ON_BOOT=0\n  -DBOARD_HAS_PSRAM\n  -DLOLIN_WIFI_FIX ; seems to work much better with this\n  -D WLED_WATCHDOG_TIMEOUT=0\n  -D DATA_PINS=16\n  -D HW_PIN_SCL=35\n  -D HW_PIN_SDA=33\n  -D HW_PIN_CLOCKSPI=7\n  -D HW_PIN_DATASPI=11\n  -D HW_PIN_MISOSPI=9\n;  -D STATUSLED=15\nlib_deps = ${esp32s2.lib_deps}\n\n\n[env:usermods]\nboard = esp32dev\nplatform = ${esp32_idf_V4.platform}\nplatform_packages = ${esp32_idf_V4.platform_packages}\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\\\"ESP32_USERMODS\\\"\n  -DTOUCH_CS=9\nlib_deps = ${esp32_idf_V4.lib_deps}\nmonitor_filters = esp32_exception_decoder\nboard_build.flash_mode = dio\ncustom_usermods = *   ; Expands to all usermods in usermods folder\nboard_build.partitions = ${esp32.extreme_partitions}  ; We're gonna need a bigger boat\n"
  },
  {
    "path": "platformio_override.sample.ini",
    "content": "# Example PlatformIO Project Configuration Override\n# ------------------------------------------------------------------------------\n# Copy to platformio_override.ini to activate overrides\n# ------------------------------------------------------------------------------\n# Please visit documentation: https://docs.platformio.org/page/projectconf.html\n\n[platformio]\ndefault_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\n\n#----------\n# SAMPLE\n#----------\n[env:WLED_generic8266_1M]\nextends = env:esp01_1m_full  # when you want to extend the existing environment (define only updated options)\n; board = esp01_1m  # uncomment when ou need different board\n; platform = ${common.platform_wled_default}  # uncomment and change when you want particular platform\n; platform_packages = ${common.platform_packages}\n; board_build.ldscript = ${common.ldscript_1m128k}\n; upload_speed = 921600 # fast upload speed (remove ';' if your board supports fast upload speed)\n# Sample libraries used for various usermods. Uncomment when using particular usermod.\nlib_deps = ${esp8266.lib_deps}\n;  olikraus/U8g2 # @~2.33.15\n;  paulstoffregen/OneWire@~2.3.8\n;  adafruit/Adafruit Unified Sensor@^1.1.4\n;  adafruit/DHT sensor library@^1.4.1\n;  adafruit/Adafruit BME280 Library@^2.2.2\n;  Wire\n;  robtillaart/SHT85@~0.3.3\n;  ;gmag11/QuickESPNow @ ~0.7.0 # will also load QuickDebug\n;  https://github.com/blazoncek/QuickESPNow.git#optional-debug  ;; exludes debug library\n\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp8266.build_flags}\n;\n; *** To use the below defines/overrides, copy and paste each onto its own line just below build_flags in the section above.\n; \n; Set a release name that may be used to distinguish required binary for flashing\n;   -D WLED_RELEASE_NAME=\\\"ESP32_MULTI_USREMODS\\\"\n;\n; disable specific features\n;   -D WLED_DISABLE_OTA\n;   -D WLED_DISABLE_ALEXA\n;   -D WLED_DISABLE_HUESYNC\n;   -D WLED_DISABLE_LOXONE\n;   -D WLED_DISABLE_INFRARED\n;   -D WLED_DISABLE_WEBSOCKETS\n;   -D WLED_DISABLE_MQTT\n;   -D WLED_DISABLE_ADALIGHT\n;   -D WLED_DISABLE_2D\n;   -D WLED_DISABLE_PXMAGIC\n;   -D WLED_DISABLE_ESPNOW\n;   -D WLED_DISABLE_BROWNOUT_DET\n;\n; enable optional built-in features\n;   -D WLED_ENABLE_PIXART\n;   -D WLED_ENABLE_USERMOD_PAGE # if created\n;   -D WLED_ENABLE_DMX\n;\n; PIN defines - uncomment and change, if needed:\n;   -D DATA_PINS=2\n; or use this for multiple outputs\n;   -D DATA_PINS=1,3\n;   -D BTNPIN=0\n;   -D IRPIN=4\n;   -D RLYPIN=12\n;   -D RLYMDE=1\n;   -D RLYODRAIN=0\n;   -D LED_BUILTIN=2 # GPIO of built-in LED\n;\n; Limit max buses\n;   -D WLED_MAX_BUSSES=2\n;   -D WLED_MAX_ANALOG_CHANNELS=3   # only 3 PWM HW pins available\n;   -D WLED_MAX_DIGITAL_CHANNELS=2  # only 2 HW accelerated pins available\n;\n; Configure default WiFi\n;   -D CLIENT_SSID='\"MyNetwork\"'\n;   -D CLIENT_PASS='\"Netw0rkPassw0rd\"'\n;\n; Configure and use Ethernet\n;   -D WLED_USE_ETHERNET\n;   -D WLED_ETH_DEFAULT=5\n; do not use pins 5, (16,) 17, 18, 19, 21, 22, 23, 25, 26, 27 for anything but ethernet\n;   -D PHY_ADDR=0 -D ETH_PHY_POWER=5 -D ETH_PHY_MDC=23 -D ETH_PHY_MDIO=18\n;   -D ETH_CLK_MODE=ETH_CLOCK_GPIO17_OUT\n;\n; NTP time configuration\n;   -D WLED_NTP_ENABLED=true\n;   -D WLED_TIMEZONE=2\n;   -D WLED_LAT=48.86\n;   -D WLED_LON=2.33\n;\n; Use Watchdog timer with 10s guard\n;   -D WLED_WATCHDOG_TIMEOUT=10\n;\n; Create debug build (with remote debug)\n;   -D WLED_DEBUG\n;   -D WLED_DEBUG_HOST='\"192.168.0.100\"'\n;   -D WLED_DEBUG_PORT=7868\n;\n; Use Autosave usermod and set it to do save after 90s\n;   -D USERMOD_AUTO_SAVE\n;   -D AUTOSAVE_AFTER_SEC=90\n;\n; Use AHT10/AHT15/AHT20 usermod\n;   -D USERMOD_AHT10\n;\n; Use INA226 usermod\n;   -D USERMOD_INA226\n;\n; Use 4 Line Display usermod with SPI display\n;   -D USERMOD_FOUR_LINE_DISPLAY\n;   -DFLD_SPI_DEFAULT\n;   -D FLD_TYPE=SSD1306_SPI64\n;   -D FLD_PIN_CLOCKSPI=14\n;   -D FLD_PIN_DATASPI=13\n;   -D FLD_PIN_DC=26\n;   -D FLD_PIN_CS=15\n;   -D FLD_PIN_RESET=27\n;\n; Use Rotary encoder usermod (in conjunction with 4LD)\n;   -D USERMOD_ROTARY_ENCODER_UI\n;   -D ENCODER_DT_PIN=5\n;   -D ENCODER_CLK_PIN=18\n;   -D ENCODER_SW_PIN=19\n;\n; Use Dallas DS18B20 temperature sensor usermod and configure it to use GPIO13\n;   -D USERMOD_DALLASTEMPERATURE\n;   -D TEMPERATURE_PIN=13\n;\n; Use Multi Relay usermod and configure it to use 6 relays and appropriate GPIO\n;   -D USERMOD_MULTI_RELAY\n;   -D MULTI_RELAY_MAX_RELAYS=6\n;   -D MULTI_RELAY_PINS=12,23,22,21,24,25\n;\n; Use PIR sensor usermod and configure it to use GPIO4 and timer of 60s\n;   -D USERMOD_PIRSWITCH\n;   -D PIR_SENSOR_PIN=4   # use -1 to disable usermod\n;   -D PIR_SENSOR_OFF_SEC=60\n;   -D PIR_SENSOR_MAX_SENSORS=2 # max allowable sensors (uses OR logic for triggering)\n;\n; Use Audioreactive usermod and configure I2S microphone\n;   -D AUDIOPIN=-1\n;   -D DMTYPE=1     # 0-analog/disabled, 1-I2S generic, 2-ES7243, 3-SPH0645, 4-I2S+mclk, 5-I2S PDM\n;   -D I2S_SDPIN=36\n;   -D I2S_WSPIN=23\n;   -D I2S_CKPIN=19\n;\n; Use PWM fan usermod\n;   -D USERMOD_PWM_FAN\n;   -D TACHO_PIN=33\n;   -D PWM_PIN=32\n;\n;  Use POV Display usermod\n;   -D USERMOD_POV_DISPLAY\n; Use built-in or custom LED as a status indicator (assumes LED is connected to GPIO16)\n;   -D STATUSLED=16\n;\n; 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\n;   -D SERVERNAME=\"\\\"WLED\\\"\"\n;\n; set the number of LEDs\n;   -D PIXEL_COUNTS=30\n; or this for multiple outputs\n;   -D PIXEL_COUNTS=30,30\n;\n; set the default LED type\n;   -D LED_TYPES=22    # see const.h (TYPE_xxxx)\n; or this for multiple outputs\n;   -D LED_TYPES=TYPE_SK6812_RGBW,TYPE_WS2812_RGB\n;\n; set default color order of your led strip\n;   -D DEFAULT_LED_COLOR_ORDER=COL_ORDER_GRB\n;\n; set milliampere limit when using ESP power pin (or inadequate PSU) to power LEDs\n;   -D ABL_MILLIAMPS_DEFAULT=850\n;   -D LED_MILLIAMPS_DEFAULT=55\n;\n; enable IR by setting remote type\n;   -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\n;\n; use PSRAM on classic ESP32 rev.1 (rev.3 or above has no issues)\n;   -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue   # needed only for classic ESP32 rev.1\n;\n; configure I2C and SPI interface (for various hardware)\n;   -D I2CSDAPIN=33 # initialise interface\n;   -D I2CSCLPIN=35 # initialise interface\n;   -D HW_PIN_SCL=35\n;   -D HW_PIN_SDA=33\n;   -D HW_PIN_CLOCKSPI=7\n;   -D HW_PIN_DATASPI=11\n;   -D HW_PIN_MISOSPI=9\n\n\n# ------------------------------------------------------------------------------\n# Optional: build flags for speed, instead of optimising for size.\n# Example of usage: see [env:esp32S3_PSRAM_HUB75]\n# ------------------------------------------------------------------------------\n\n[Speed_Flags]\nbuild_unflags = -Os ;; to disable standard optimization for small size\nbuild_flags =\n  -O2 ;; optimize for speed\n  -free -fipa-pta ;; very useful, too\n  ;;-fsingle-precision-constant ;; makes all floating point literals \"float\" (default is \"double\")\n  ;;-funsafe-math-optimizations ;; less dangerous than -ffast-math; still allows the compiler to exploit FMA and reciprocals (up to 10% faster on -S3)\n  # Important: we need to explicitly switch off some \"-O2\" optimizations\n  -fno-jump-tables -fno-tree-switch-conversion                 ;; needed - firmware may crash otherwise\n  -freorder-blocks -Wwrite-strings -fstrict-volatile-bitfields ;; needed - recommended by espressif\n\n\n# ------------------------------------------------------------------------------\n# PRE-CONFIGURED DEVELOPMENT BOARDS AND CONTROLLERS\n# ------------------------------------------------------------------------------\n\n[env:esp07]\nboard = esp07\nplatform = ${common.platform_wled_default}\nplatform_packages = ${common.platform_packages}\nboard_build.ldscript = ${common.ldscript_4m1m}\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp8266.build_flags}\nlib_deps = ${esp8266.lib_deps}\n\n[env:d1_mini]\nboard = d1_mini\nplatform = ${common.platform_wled_default}\nplatform_packages = ${common.platform_packages}\nupload_speed = 921600\nboard_build.ldscript = ${common.ldscript_4m1m}\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp8266.build_flags}\nlib_deps = ${esp8266.lib_deps}\nmonitor_filters = esp8266_exception_decoder\n\n[env:heltec_wifi_kit_8]\nboard = d1_mini\nplatform = ${common.platform_wled_default}\nplatform_packages = ${common.platform_packages}\nboard_build.ldscript = ${common.ldscript_4m1m}\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp8266.build_flags}\nlib_deps = ${esp8266.lib_deps}\n\n[env:h803wf]\nboard = d1_mini\nplatform = ${common.platform_wled_default}\nplatform_packages = ${common.platform_packages}\nboard_build.ldscript = ${common.ldscript_4m1m}\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp8266.build_flags} -D DATA_PINS=1 -D WLED_DISABLE_INFRARED\nlib_deps = ${esp8266.lib_deps}\n\n[env:esp32dev_qio80]\nextends = env:esp32dev  # we want to extend the existing esp32dev environment (and define only updated options)\nboard = esp32dev\nbuild_flags = ${common.build_flags} ${esp32.build_flags} #-D WLED_DISABLE_BROWNOUT_DET\nlib_deps = ${esp32.lib_deps}\nmonitor_filters = esp32_exception_decoder\nboard_build.f_flash = 80000000L\nboard_build.flash_mode = qio\n\n[env:esp32dev_V4_dio80]\n;; experimental ESP32 env using ESP-IDF V4.4.x\n;; Warning: this build environment is not stable!!\n;; please erase your device before installing.\nextends = esp32_idf_V4  # based on newer \"esp-idf V4\" platform environment\nboard = esp32dev\nbuild_flags = ${common.build_flags}  ${esp32_idf_V4.build_flags} #-D WLED_DISABLE_BROWNOUT_DET\nlib_deps = ${esp32_idf_V4.lib_deps}\nmonitor_filters = esp32_exception_decoder\nboard_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}\nboard_build.f_flash = 80000000L\nboard_build.flash_mode = dio\n\n[env:esp32s2_saola]\nextends = esp32s2\nboard = esp32-s2-saola-1\nplatform = ${esp32s2.platform}\nplatform_packages = ${esp32s2.platform_packages}\nframework = arduino\nboard_build.flash_mode = qio\nupload_speed = 460800\nbuild_flags = ${common.build_flags} ${esp32s2.build_flags}\n  ;-DLOLIN_WIFI_FIX ;; try this in case Wifi does not work\n  -DARDUINO_USB_CDC_ON_BOOT=1\nlib_deps = ${esp32s2.lib_deps}\n\n[env:esp32s3dev_8MB_PSRAM_qspi]\n;; ESP32-TinyS3 development board, with 8MB FLASH and PSRAM (memory_type: qio_qspi)\nextends = env:esp32s3dev_8MB_PSRAM_opi\n;board = um_tinys3 ;    -> needs workaround from https://github.com/wled-dev/WLED/pull/2905#issuecomment-1328049860\nboard = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support\nboard_build.arduino.memory_type = qio_qspi ;; use with PSRAM: 2MB or  4MB\n\n[env:esp8285_4CH_MagicHome]\nboard = esp8285\nplatform = ${common.platform_wled_default}\nplatform_packages = ${common.platform_packages}\nboard_build.ldscript = ${common.ldscript_1m128k}\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA\nlib_deps = ${esp8266.lib_deps}\n\n[env:esp8285_H801]\nboard = esp8285\nplatform = ${common.platform_wled_default}\nplatform_packages = ${common.platform_packages}\nboard_build.ldscript = ${common.ldscript_1m128k}\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA\nlib_deps = ${esp8266.lib_deps}\n\n[env:d1_mini_5CH_Shojo_PCB]\nboard = d1_mini\nplatform = ${common.platform_wled_default}\nplatform_packages = ${common.platform_packages}\nboard_build.ldscript = ${common.ldscript_4m1m}\nbuild_unflags = ${common.build_unflags}\nbuild_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.\nlib_deps = ${esp8266.lib_deps}\n\n[env:d1_mini_debug]\nboard = d1_mini\nbuild_type = debug\nplatform = ${common.platform_wled_default}\nplatform_packages = ${common.platform_packages}\nboard_build.ldscript = ${common.ldscript_4m1m}\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp8266.build_flags} ${common.debug_flags}\nlib_deps = ${esp8266.lib_deps}\n\n[env:d1_mini_ota]\nboard = d1_mini\nupload_protocol = espota\n# exchange for your WLED IP\nupload_port = \"10.10.1.27\"\nplatform = ${common.platform_wled_default}\nplatform_packages = ${common.platform_packages}\nboard_build.ldscript = ${common.ldscript_4m1m}\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp8266.build_flags}\nlib_deps = ${esp8266.lib_deps}\n\n[env:anavi_miracle_controller]\nboard = d1_mini\nplatform = ${common.platform_wled_default}\nplatform_packages = ${common.platform_packages}\nboard_build.ldscript = ${common.ldscript_4m1m}\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp8266.build_flags} -D DATA_PINS=12 -D IRPIN=-1 -D RLYPIN=2\nlib_deps = ${esp8266.lib_deps}\n\n[env:esp32c3dev_2MB]\n;; for ESP32-C3 boards with 2MB flash (instead of 4MB).\n;; this board need a specific partition file. OTA not possible.\nextends = esp32c3\nplatform = ${esp32c3.platform}\nplatform_packages = ${esp32c3.platform_packages}\nboard = esp32-c3-devkitm-1\nbuild_flags = ${common.build_flags} ${esp32c3.build_flags}\n  -D WLED_WATCHDOG_TIMEOUT=0\n  -D WLED_DISABLE_OTA\n  ; -DARDUINO_USB_CDC_ON_BOOT=1 ;; for virtual CDC USB\n  -DARDUINO_USB_CDC_ON_BOOT=0   ;; for serial-to-USB chip\nbuild_unflags = ${common.build_unflags}\nupload_speed = 115200\nlib_deps = ${esp32c3.lib_deps}\nboard_build.partitions = tools/WLED_ESP32_2MB_noOTA.csv\nboard_build.flash_mode = dio\nboard_upload.flash_size = 2MB\nboard_upload.maximum_size = 2097152\n\n[env:wemos_shield_esp32]\nextends = esp32              ;; use default esp32 platform\nboard = esp32dev\nupload_speed = 460800\nbuild_flags = ${common.build_flags} ${esp32.build_flags}\n  -D WLED_RELEASE_NAME=\\\"ESP32_wemos_shield\\\"\n  -D DATA_PINS=16\n  -D RLYPIN=19\n  -D BTNPIN=17\n  -D IRPIN=18\n  -UWLED_USE_MY_CONFIG\n  -D USERMOD_DALLASTEMPERATURE\n  -D USERMOD_FOUR_LINE_DISPLAY\n  -D TEMPERATURE_PIN=23\nlib_deps = ${esp32.lib_deps}\n  OneWire@~2.3.5          ;; needed for USERMOD_DALLASTEMPERATURE\n  olikraus/U8g2 @ ^2.28.8 ;; needed for USERMOD_FOUR_LINE_DISPLAY\nboard_build.partitions = ${esp32.default_partitions}\n\n[env:esp32_pico-D4]\nextends = esp32              ;; use default esp32 platform\nboard = pico32               ;; pico32-D4 is different from the standard esp32dev\n                             ;; hardware details from https://github.com/srg74/WLED-ESP32-pico\nbuild_flags = ${common.build_flags} ${esp32.build_flags}\n  -D WLED_RELEASE_NAME=\\\"pico32-D4\\\" -D SERVERNAME='\"WLED-pico32\"'\n  -D WLED_DISABLE_ADALIGHT   ;; no serial-to-USB chip on this board - better to disable serial protocols\n  -D DATA_PINS=2,18          ;; LED pins\n  -D RLYPIN=19 -D BTNPIN=0 -D IRPIN=-1 ;; no default pin for IR\n  -D UM_AUDIOREACTIVE_ENABLE ;; enable AR by default\n  ;; Audioreactive settings for on-board microphone (ICS-43432)\n  -D SR_DMTYPE=1 -D I2S_SDPIN=25 -D I2S_WSPIN=15 -D I2S_CKPIN=14\n  -D SR_SQUELCH=5 -D SR_GAIN=30\nlib_deps = ${esp32.lib_deps}\nboard_build.partitions = ${esp32.default_partitions}\nboard_build.f_flash = 80000000L\n\n[env:m5atom]\nextends = env:esp32dev  # we want to extend the existing esp32dev environment (and define only updated options)\nbuild_flags = ${common.build_flags} ${esp32.build_flags} -D DATA_PINS=27 -D BTNPIN=39\n\n[env:sp501e]\nboard = esp_wroom_02\nplatform = ${common.platform_wled_default}\nboard_build.ldscript = ${common.ldscript_2m512k}\nbuild_flags = ${common.build_flags} ${esp8266.build_flags} -D DATA_PINS=3 -D BTNPIN=1\nlib_deps = ${esp8266.lib_deps}\n\n[env:sp511e]\nboard = esp_wroom_02\nplatform = ${common.platform_wled_default}\nboard_build.ldscript = ${common.ldscript_2m512k}\nbuild_flags = ${common.build_flags} ${esp8266.build_flags} -D DATA_PINS=3 -D BTNPIN=2 -D IRPIN=5 -D WLED_MAX_BUTTONS=3\nlib_deps = ${esp8266.lib_deps}\n\n[env:Athom_RGBCW]        ;7w and 5w(GU10) bulbs\nboard = esp8285\nplatform = ${common.platform_wled_default}\nplatform_packages = ${common.platform_packages}\nboard_build.ldscript = ${common.ldscript_2m512k}\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=-1 -D RLYPIN=-1 -D DATA_PINS=4,12,14,13,5\n                                            -D LED_TYPES=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0\nlib_deps = ${esp8266.lib_deps}\n\n[env:Athom_15w_RGBCW]        ;15w bulb\nboard = esp8285\nplatform = ${common.platform_wled_default}\nplatform_packages = ${common.platform_packages}\nboard_build.ldscript = ${common.ldscript_2m512k}\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=-1 -D RLYPIN=-1 -D DATA_PINS=4,12,14,5,13\n                                            -D LED_TYPES=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0 -D WLED_USE_IC_CCT\nlib_deps = ${esp8266.lib_deps}\n\n[env:Athom_3Pin_Controller]        ;small controller with only data\nboard = esp8285\nplatform = ${common.platform_wled_default}\nplatform_packages = ${common.platform_packages}\nboard_build.ldscript = ${common.ldscript_2m512k}\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=0 -D RLYPIN=-1 -D DATA_PINS=1 -D WLED_DISABLE_INFRARED\nlib_deps = ${esp8266.lib_deps}\n\n[env:Athom_4Pin_Controller]       ; With clock and data interface\nboard = esp8285\nplatform = ${common.platform_wled_default}\nplatform_packages = ${common.platform_packages}\nboard_build.ldscript = ${common.ldscript_2m512k}\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=0 -D RLYPIN=12 -D DATA_PINS=1 -D WLED_DISABLE_INFRARED\nlib_deps = ${esp8266.lib_deps}\n\n[env:Athom_5Pin_Controller]      ;Analog light strip controller\nboard = esp8285\nplatform = ${common.platform_wled_default}\nplatform_packages = ${common.platform_packages}\nboard_build.ldscript = ${common.ldscript_2m512k}\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=0 -D RLYPIN=-1 DATA_PINS=4,12,14,13 -D WLED_DISABLE_INFRARED\nlib_deps = ${esp8266.lib_deps}\n\n[env:MY9291]\nboard = esp01_1m\nplatform = ${common.platform_wled_default}\nplatform_packages = ${common.platform_packages}\nboard_build.ldscript = ${common.ldscript_1m128k}\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA -D USERMOD_MY9291\nlib_deps = ${esp8266.lib_deps}\n\n# ------------------------------------------------------------------------------\n# codm pixel controller board configurations\n# codm-controller-0_6 can also be used for the TYWE3S controller\n# ------------------------------------------------------------------------------\n\n[env:codm-controller-0_6]\nboard = esp_wroom_02\nplatform = ${common.platform_wled_default}\nplatform_packages = ${common.platform_packages}\nboard_build.ldscript = ${common.ldscript_2m512k}\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp8266.build_flags}\nlib_deps = ${esp8266.lib_deps}\n\n[env:codm-controller-0_6-rev2]\nboard = esp_wroom_02\nplatform = ${common.platform_wled_default}\nplatform_packages = ${common.platform_packages}\nboard_build.ldscript = ${common.ldscript_4m1m}\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp8266.build_flags}\nlib_deps = ${esp8266.lib_deps}\n\n# ------------------------------------------------------------------------------\n# EleksTube-IPS\n# ------------------------------------------------------------------------------\n[env:elekstube_ips]\nextends = esp32              ;; use default esp32 platform\nboard = esp32dev\nupload_speed = 921600\ncustom_usermods = ${env:esp32dev.custom_usermods} RTC EleksTube_IPS\nbuild_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_DISABLE_BROWNOUT_DET -D WLED_DISABLE_INFRARED\n  -D DATA_PINS=12\n  -D RLYPIN=27\n  -D BTNPIN=34\n  -D PIXEL_COUNTS=6\n  # Display config\n  -D ST7789_DRIVER\n  -D TFT_WIDTH=135\n  -D TFT_HEIGHT=240\n  -D CGRAM_OFFSET\n  -D TFT_SDA_READ\n  -D TFT_MOSI=23\n  -D TFT_SCLK=18\n  -D TFT_DC=25\n  -D TFT_RST=26\n  -D SPI_FREQUENCY=40000000\n  -D USER_SETUP_LOADED\nmonitor_filters = esp32_exception_decoder\n\n\n# ------------------------------------------------------------------------------\n# Usermod examples\n# ------------------------------------------------------------------------------\n\n# 433MHz RF remote example for esp32dev\n[env:esp32dev_usermod_RF433]\nextends = env:esp32dev\ncustom_usermods =\n  ${env:esp32dev.custom_usermods}\n  RF433\n\n# External usermod from a git repository.\n# The library's `library.json` must include `\"build\": {\"libArchive\": false}`.\n# The name PlatformIO assigns is taken from the library's `library.json` \"name\" field.\n# If that name doesn't match the repo name in the URL, use the \"LibName = URL\" form\n# shown in the commented-out line below to supply the name explicitly.\n[env:esp32dev_external_usermod]\nextends = env:esp32dev\ncustom_usermods =\n  ${env:esp32dev.custom_usermods}\n  https://github.com/wled/wled-usermod-example.git#main\n\n\n# ------------------------------------------------------------------------------\n# Hub75 examples\n# ------------------------------------------------------------------------------\n# Note: some panels may experience ghosting with default full brightness. use -D WLED_HUB75_MAX_BRIGHTNESS=239 or lower to fix it.\n\n[env:esp32dev_hub75]\nboard = esp32dev\nupload_speed = 921600\nplatform = ${esp32_idf_V4.platform}\nplatform_packages =\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags}\n  -D WLED_RELEASE_NAME=\\\"ESP32_hub75\\\"\n  -D WLED_ENABLE_HUB75MATRIX -D NO_GFX\n  -D WLED_DEBUG_BUS\n  ; -D WLED_DEBUG\n  -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\n\nlib_deps = ${esp32_idf_V4.lib_deps}\n  https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#3.0.11\n\nmonitor_filters = esp32_exception_decoder\nboard_build.partitions = ${esp32.default_partitions}\nboard_build.flash_mode = dio\ncustom_usermods = audioreactive\n\n[env:esp32dev_hub75_forum_pinout]\nextends = env:esp32dev_hub75\nbuild_flags = ${common.build_flags}\n              -D WLED_RELEASE_NAME=\\\"ESP32_hub75_forum_pinout\\\"\n              -D WLED_ENABLE_HUB75MATRIX -D NO_GFX \n              -D ESP32_FORUM_PINOUT ;; enable for SmartMatrix default pins\n              -D WLED_DEBUG_BUS\n              -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\n; -D WLED_DEBUG\n\n\n[env:adafruit_matrixportal_esp32s3]\n; ESP32-S3 processor, 8 MB flash, 2 MB of PSRAM, dedicated driver pins for HUB75\nboard = adafruit_matrixportal_esp32s3_wled ; modified board definition: removed flash section that causes FS erase on upload\n;; adafruit recommends to use arduino-esp32 2.0.14\n;;platform = espressif32@ ~6.5.0\n;;platform_packages = platformio/framework-arduinoespressif32 @ 3.20014.231204 ;; arduino-esp32 2.0.14\nplatform = ${esp32s3.platform}\nplatform_packages =\nupload_speed = 921600\nbuild_unflags = ${common.build_unflags}\nbuild_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\\\"ESP32-S3_8M_qspi\\\"\n  -DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1      ;; for boards with USB-OTG connector only (USBCDC or \"TinyUSB\")\n  -DBOARD_HAS_PSRAM\n  -DLOLIN_WIFI_FIX ; seems to work much better with this (sets lower TX power)\n  -D WLED_WATCHDOG_TIMEOUT=0\n  -D WLED_ENABLE_HUB75MATRIX -D NO_GFX\n  -D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips\n  -D ARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3\n  -D WLED_DEBUG_BUS\n  -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\n\n\nlib_deps = ${esp32s3.lib_deps}\n  https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#aa28e2a ;; S3_LCD_DIV_NUM fix\n\nboard_build.partitions = ${esp32.large_partitions}                  ;; standard bootloader and 8MB Flash partitions\n;; board_build.partitions = tools/partitions-8MB_spiffs-tinyuf2.csv ;; supports adafruit UF2 bootloader\nboard_build.f_flash = 80000000L\nboard_build.flash_mode = qio\nmonitor_filters = esp32_exception_decoder\ncustom_usermods = audioreactive\n\n[env:esp32S3_PSRAM_HUB75]\n;; MOONHUB HUB75 adapter board (lilygo T7-S3 with 16MB flash and PSRAM)\nboard = lilygo-t7-s3\nplatform = ${esp32s3.platform}\nplatform_packages =\nupload_speed = 921600\nbuild_unflags = ${common.build_unflags}\n              ${Speed_Flags.build_unflags}    ;; optional: removes \"-Os\" so we can override with \"-O2\" in build_flags\nbuild_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\\\"esp32S3_16MB_PSRAM_HUB75\\\"\n              ${Speed_Flags.build_flags}      ;; optional: -O2 -> optimize for speed instead of size\n              -DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1      ;; for boards with USB-OTG connector only (USBCDC or \"TinyUSB\")\n              -DBOARD_HAS_PSRAM\n              -DLOLIN_WIFI_FIX ; seems to work much better with this (sets lower TX power)\n              -D WLED_WATCHDOG_TIMEOUT=0\n              -D WLED_ENABLE_HUB75MATRIX -D NO_GFX \n              -D S3_LCD_DIV_NUM=20 ;; Attempt to fix wifi performance issue when panel active with S3 chips\n              -D MOONHUB_S3_PINOUT ;; HUB75 pinout\n              -D WLED_DEBUG_BUS\n              -D LEDPIN=14 -D BTNPIN=0 -D RLYPIN=15 -D IRPIN=-1 -D AUDIOPIN=-1 ;; defaults that avoid pin conflicts with HUB75\n              -D SR_DMTYPE=1 -D I2S_SDPIN=10 -D I2S_CKPIN=11 -D I2S_WSPIN=12 -D MCLK_PIN=-1  ;; I2S mic\n\nlib_deps = ${esp32s3.lib_deps}\n           https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#aa28e2a ;; S3_LCD_DIV_NUM fix\n\n;;board_build.partitions = ${esp32.large_partitions}               ;; for 8MB flash\nboard_build.partitions = ${esp32.extreme_partitions}               ;; for 16MB flash\nboard_build.f_flash = 80000000L\nboard_build.flash_mode = qio\nmonitor_filters = esp32_exception_decoder\ncustom_usermods = audioreactive"
  },
  {
    "path": "readme.md",
    "content": "<p align=\"center\">\n  <img src=\"/images/wled_logo_akemi.png\">\n  <a href=\"https://github.com/wled-dev/WLED/releases\"><img src=\"https://img.shields.io/github/release/wled-dev/WLED.svg?style=flat-square\"></a>\n  <a href=\"https://raw.githubusercontent.com/wled-dev/WLED/main/LICENSE\"><img src=\"https://img.shields.io/github/license/wled-dev/wled?color=blue&style=flat-square\"></a>\n  <a href=\"https://wled.discourse.group\"><img src=\"https://img.shields.io/discourse/topics?colorB=blue&label=forum&server=https%3A%2F%2Fwled.discourse.group%2F&style=flat-square\"></a>\n  <a href=\"https://discord.gg/QAh7wJHrRM\"><img src=\"https://img.shields.io/discord/473448917040758787.svg?colorB=blue&label=discord&style=flat-square\"></a>\n  <a href=\"https://kno.wled.ge\"><img src=\"https://img.shields.io/badge/quick_start-wiki-blue.svg?style=flat-square\"></a>\n  <a href=\"https://github.com/Aircoookie/WLED-App\"><img src=\"https://img.shields.io/badge/app-wled-blue.svg?style=flat-square\"></a>\n  <a href=\"https://gitpod.io/#https://github.com/wled-dev/WLED\"><img src=\"https://img.shields.io/badge/Gitpod-ready--to--code-blue?style=flat-square&logo=gitpod\"></a>\n\n  </p>\n\n# Welcome to WLED! ✨\n\nA 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!\n\nOriginally created by [Aircoookie](https://github.com/Aircoookie)\n\n## ⚙️ Features\n- WS2812FX library with more than 100 special effects  \n- FastLED noise effects and 50 palettes  \n- Modern UI with color, effect and segment controls  \n- Segments to set different effects and colors to user defined parts of the LED string  \n- Settings page - configuration via the network  \n- Access Point and station mode - automatic failsafe AP  \n- [Up to 10 LED outputs](https://kno.wled.ge/features/multi-strip/#esp32) per instance\n- Support for RGBW strips  \n- Up to 250 user presets to save and load colors/effects easily, supports cycling through them.  \n- Presets can be used to automatically execute API calls  \n- Nightlight function (gradually dims down)  \n- Full OTA software updateability (HTTP + ArduinoOTA), password protectable  \n- Configurable analog clock (Cronixie, 7-segment and EleksTube IPS clock support via usermods) \n- Configurable Auto Brightness limit for safe operation  \n- Filesystem-based config for easier backup of presets and settings  \n\n## 💡 Supported light control interfaces\n- 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)\n- JSON and HTTP request APIs  \n- MQTT   \n- E1.31, Art-Net, DDP and TPM2.net\n- [diyHue](https://github.com/diyhue/diyHue) (Wled is supported by diyHue, including Hue Sync Entertainment under udp. Thanks to [Gregory Mallios](https://github.com/gmallios))\n- [Hyperion](https://github.com/hyperion-project/hyperion.ng)\n- UDP realtime  \n- Alexa voice control (including dimming and color)  \n- Sync to Philips hue lights  \n- Adalight (PC ambilight via serial) and TPM2  \n- Sync color of multiple WLED devices (UDP notifier)  \n- Infrared remotes (24-key RGB, receiver required)  \n- Simple timers/schedules (time from NTP, timezones/DST supported)  \n\n## 📲 Quick start guide and documentation\n\nSee the [documentation on our official site](https://kno.wled.ge)!\n\n[On this page](https://kno.wled.ge/basics/tutorials/) you can find excellent tutorials and tools to help you get your new project up and running!\n\n## 🖼️ User interface\n<img src=\"/images/macbook-pro-space-gray-on-the-wooden-table.jpg\" width=\"50%\"><img src=\"/images/walking-with-iphone-x.jpg\" width=\"50%\">\n\n## 💾 Compatible hardware\n\nSee [here](https://kno.wled.ge/basics/compatible-hardware)!\n\n## ✌️ Other\n\nLicensed under the EUPL v1.2 license  \nCredits [here](https://kno.wled.ge/about/contributors/)!\nCORS proxy by [Corsfix](https://corsfix.com/)\n\nJoin the Discord server to discuss everything about WLED!\n\n<a href=\"https://discord.gg/QAh7wJHrRM\"><img src=\"https://discordapp.com/api/guilds/473448917040758787/widget.png?style=banner2\" width=\"25%\"></a>\n\nCheck out the WLED [Discourse forum](https://wled.discourse.group)!  \n\nYou can also send me mails to [dev.aircoookie@gmail.com](mailto:dev.aircoookie@gmail.com), but please, only do so if you want to talk to me privately.  \n\nIf WLED really brightens up your day, you can [![](https://img.shields.io/badge/send%20me%20a%20small%20gift-paypal-blue.svg?style=flat-square)](https://paypal.me/aircoookie)\n\n\n*Disclaimer:*   \n\nIf you are prone to photosensitive epilepsy, we recommended you do **not** use this software.  \nIf you still want to try, don't use strobe, lighting or noise modes or high effect speed settings.\n\nAs per the EUPL license, I assume no liability for any damage to you or any other person or equipment.  \n\n"
  },
  {
    "path": "requirements.in",
    "content": "platformio>=6.1.17\n"
  },
  {
    "path": "requirements.txt",
    "content": "#\n# This file is autogenerated by pip-compile with Python 3.11\n# by the following command:\n#\n#    pip-compile requirements.in\n#\najsonrpc==1.2.0\n    # via platformio\nanyio==4.8.0\n    # via starlette\nbottle==0.13.2\n    # via platformio\ncertifi==2025.1.31\n    # via requests\ncharset-normalizer==3.4.1\n    # via requests\nclick==8.1.8\n    # via\n    #   platformio\n    #   uvicorn\ncolorama==0.4.6\n    # via platformio\nh11==0.16.0\n    # via\n    #   uvicorn\n    #   wsproto\nidna==3.10\n    # via\n    #   anyio\n    #   requests\nmarshmallow==3.26.1\n    # via platformio\npackaging==24.2\n    # via marshmallow\nplatformio==6.1.17\n    # via -r requirements.in\npyelftools==0.32\n    # via platformio\npyserial==3.5\n    # via platformio\nrequests==2.32.4\n    # via platformio\nsemantic-version==2.10.0\n    # via platformio\nsniffio==1.3.1\n    # via anyio\nstarlette==0.45.3\n    # via platformio\ntabulate==0.9.0\n    # via platformio\ntyping-extensions==4.12.2\n    # via anyio\nurllib3==2.5.0\n    # via requests\nuvicorn==0.34.0\n    # via platformio\nwsproto==1.2.0\n    # via platformio\n"
  },
  {
    "path": "test/README",
    "content": "\nThis directory is intended for PIO Unit Testing and project tests.\n\nUnit Testing is a software testing method by which individual units of\nsource code, sets of one or more MCU program modules together with associated\ncontrol data, usage procedures, and operating procedures, are tested to\ndetermine whether they are fit for use. Unit testing finds problems early\nin the development cycle.\n\nMore information about PIO Unit Testing:\n- https://docs.platformio.org/page/plus/unit-testing.html\n"
  },
  {
    "path": "tools/WLED_ESP32-wrover_4MB.csv",
    "content": "# Name,   Type, SubType, Offset,  Size, Flags\nnvs,      data, nvs,     0x9000,  0x5000,\notadata,  data, ota,     0xe000,  0x2000,\napp0,     app,  ota_0,   0x10000, 0x1A0000,\napp1,     app,  ota_1,   0x1B0000,0x1A0000,\nspiffs,   data, spiffs,  0x350000,0xB0000,\n"
  },
  {
    "path": "tools/WLED_ESP32_16MB.csv",
    "content": "# Name,   Type, SubType, Offset,  Size, Flags\nnvs,      data, nvs,     0x9000,  0x5000,\notadata,  data, ota,     0xe000,  0x2000,\napp0,     app,  ota_0,   0x10000, 0x200000,\napp1,     app,  ota_1,   0x210000,0x200000,\nspiffs,   data, spiffs,  0x410000,0xBE0000,"
  },
  {
    "path": "tools/WLED_ESP32_16MB_9MB_FS.csv",
    "content": "# Name,   Type, SubType, Offset,  Size, Flags\nnvs,      data, nvs,     0x9000,  0x5000,\notadata,  data, ota,     0xe000,  0x2000,\napp0,     app,  ota_0,   0x10000, 0x300000,\napp1,     app,  ota_1,   0x310000,0x300000,\nspiffs,   data, spiffs,  0x610000,0x9E0000,\ncoredump, data, coredump,,64K\n# to create/use ffat, see https://github.com/marcmerlin/esp32_fatfsimage"
  },
  {
    "path": "tools/WLED_ESP32_2MB_noOTA.csv",
    "content": "# Name,   Type, SubType, Offset,  Size, Flags\nnvs,      data, nvs,     0x9000,    20K,\notadata,  data, ota,     0xe000,    8K,\napp0,     app,  ota_0,   0x10000,   1536K,\nspiffs,   data, spiffs,  0x190000,  384K,\n"
  },
  {
    "path": "tools/WLED_ESP32_32MB.csv",
    "content": "# Name,   Type, SubType, Offset,  Size, Flags\nnvs,      data, nvs,     0x9000,  0x5000,\notadata,  data, ota,     0xe000,  0x2000,\napp0,     app,  ota_0,   0x10000, 0x300000,\napp1,     app,  ota_1,   0x310000,0x300000,\nspiffs,   data, spiffs,  0x610000,0x19E0000,\ncoredump, data, coredump,,64K\n"
  },
  {
    "path": "tools/WLED_ESP32_4MB_1MB_FS.csv",
    "content": "# Name,   Type, SubType, Offset,  Size, Flags\nnvs,      data, nvs,     0x9000,  0x5000,\notadata,  data, ota,     0xe000,  0x2000,\napp0,     app,  ota_0,   0x10000, 0x180000,\napp1,     app,  ota_1,   0x190000,0x180000,\nspiffs,   data, spiffs,  0x310000,0xF0000,"
  },
  {
    "path": "tools/WLED_ESP32_4MB_256KB_FS.csv",
    "content": "# Name,   Type, SubType, Offset,  Size, Flags\nnvs,      data, nvs,     0x9000,  0x5000,\notadata,  data, ota,     0xe000,  0x2000,\napp0,     app,  ota_0,   0x10000, 0x1D0000,\napp1,     app,  ota_1,   0x1E0000,0x1D0000,\nspiffs,   data, spiffs,  0x3B0000,0x40000,\ncoredump, data, coredump,,64K"
  },
  {
    "path": "tools/WLED_ESP32_4MB_512KB_FS.csv",
    "content": "# Name,   Type, SubType, Offset,  Size, Flags\nnvs,      data, nvs,     0x9000,  0x5000,\notadata,  data, ota,     0xe000,  0x2000,\napp0,     app,  ota_0,   0x10000, 0x1B0000,\napp1,     app,  ota_1,   0x1C0000,0x1B0000,\nspiffs,   data, spiffs,  0x370000,0x80000,\ncoredump, data, coredump,,64K"
  },
  {
    "path": "tools/WLED_ESP32_4MB_700k_FS.csv",
    "content": "# Name,   Type, SubType, Offset,  Size, Flags\nnvs,      data, nvs,     0x9000,  0x5000,\notadata,  data, ota,     0xe000,  0x2000,\napp0,     app,  ota_0,   0x10000, 0x1A0000,\napp1,     app,  ota_1,   0x1B0000,0x1A0000,\nspiffs,   data, spiffs,  0x350000,0xB0000,\n"
  },
  {
    "path": "tools/WLED_ESP32_8MB.csv",
    "content": "# Name,   Type, SubType, Offset,  Size, Flags\nnvs,      data, nvs,     0x9000,  0x5000,\notadata,  data, ota,     0xe000,  0x2000,\napp0,     app,  ota_0,   0x10000, 0x200000,\napp1,     app,  ota_1,   0x210000,0x200000,\nspiffs,   data, spiffs,  0x410000,0x3E0000,\ncoredump, data, coredump,,64K\n"
  },
  {
    "path": "tools/all_xml.sh",
    "content": "#!/bin/bash\n# Pull all settings pages for comparison\nHOST=$1\nTGT_PATH=$2\nCURL_ARGS=\"--compressed\"\n\n# Replicate one target many times\nfunction replicate() {\n  for i in {0..10}\n  do  \n    echo -n \" http://${HOST}/settings.js?p=$i -o ${TGT_PATH}/$i.xml\"\n  done\n}\nread -a TARGETS <<< $(replicate)\n\nmkdir -p ${TGT_PATH}\ncurl ${CURL_ARGS} ${TARGETS[@]}\n"
  },
  {
    "path": "tools/cdata-test.js",
    "content": "'use strict';\n\nconst assert = require('node:assert');\nconst { describe, it, before, after } = require('node:test');\nconst fs = require('fs');\nconst path = require('path');\nconst child_process = require('child_process');\nconst util = require('util');\nconst execPromise = util.promisify(child_process.exec);\n\nprocess.env.NODE_ENV = 'test'; // Set the environment to testing\nconst cdata = require('./cdata.js');\n\ndescribe('Function', () => {\n  const testFolderPath = path.join(__dirname, 'testFolder');\n  const oldFilePath = path.join(testFolderPath, 'oldFile.txt');\n  const newFilePath = path.join(testFolderPath, 'newFile.txt');\n\n  // Create a temporary file before the test\n  before(() => {\n    // Create test folder\n    if (!fs.existsSync(testFolderPath)) {\n      fs.mkdirSync(testFolderPath);\n    }\n\n    // Create an old file\n    fs.writeFileSync(oldFilePath, 'This is an old file.');\n    // Modify the 'mtime' to simulate an old file\n    const oldTime = new Date();\n    oldTime.setFullYear(oldTime.getFullYear() - 1);\n    fs.utimesSync(oldFilePath, oldTime, oldTime);\n\n    // Create a new file\n    fs.writeFileSync(newFilePath, 'This is a new file.');\n  });\n\n  // delete the temporary files after the test\n  after(() => {\n    fs.rmSync(testFolderPath, { recursive: true });\n  });\n\n  describe('isFileNewerThan', async () => {\n    it('should return true if the file is newer than the provided time', async () => {\n      const pastTime = Date.now() - 10000; // 10 seconds ago\n      assert.strictEqual(cdata.isFileNewerThan(newFilePath, pastTime), true);\n    });\n\n    it('should return false if the file is older than the provided time', async () => {\n      assert.strictEqual(cdata.isFileNewerThan(oldFilePath, Date.now()), false);\n    });\n\n    it('should throw an exception if the file does not exist', async () => {\n      assert.throws(() => {\n        cdata.isFileNewerThan('nonexistent.txt', Date.now());\n      });\n    });\n  });\n\n  describe('isAnyFileInFolderNewerThan', async () => {\n    it('should return true if a file in the folder is newer than the given time', async () => {\n      const time = fs.statSync(path.join(testFolderPath, 'oldFile.txt')).mtime;\n      assert.strictEqual(cdata.isAnyFileInFolderNewerThan(testFolderPath, time), true);\n    });\n\n    it('should return false if no files in the folder are newer than the given time', async () => {\n      assert.strictEqual(cdata.isAnyFileInFolderNewerThan(testFolderPath, new Date()), false);\n    });\n\n    it('should throw an exception if the folder does not exist', async () => {\n      assert.throws(() => {\n        cdata.isAnyFileInFolderNewerThan('nonexistent', new Date());\n      });\n    });\n  });\n});\n\ndescribe('Script', () => {\n  const folderPath = 'wled00';\n  const dataPath = path.join(folderPath, 'data');\n\n  before(() => {\n    process.env.NODE_ENV = 'production';\n    // Backup files\n    fs.cpSync(\"wled00/data\", \"wled00Backup\", { recursive: true });\n    fs.cpSync(\"tools/cdata.js\", \"cdata.bak.js\");\n    fs.cpSync(\"package.json\", \"package.bak.json\");\n  });\n  after(() => {\n    // Restore backup\n    fs.rmSync(\"wled00/data\", { recursive: true });\n    fs.renameSync(\"wled00Backup\", \"wled00/data\");\n    fs.rmSync(\"tools/cdata.js\");\n    fs.renameSync(\"cdata.bak.js\", \"tools/cdata.js\");\n    fs.rmSync(\"package.json\");\n    fs.renameSync(\"package.bak.json\", \"package.json\");\n  });\n\n  // delete all html_*.h files\n  async function deleteBuiltFiles() {\n    const files = await fs.promises.readdir(folderPath);\n    await Promise.all(files.map(file => {\n      if (file.startsWith('html_') && path.extname(file) === '.h') {\n        return fs.promises.unlink(path.join(folderPath, file));\n      }\n    }));\n  }\n\n  // check if html_*.h files were created\n  async function checkIfBuiltFilesExist() {\n    const files = await fs.promises.readdir(folderPath);\n    const htmlFiles = files.filter(file => file.startsWith('html_') && path.extname(file) === '.h');\n    assert(htmlFiles.length > 0, 'html_*.h files were not created');\n  }\n\n  async function runAndCheckIfBuiltFilesExist() {\n    await execPromise('node tools/cdata.js');\n    await checkIfBuiltFilesExist();\n  }\n\n  async function checkIfFileWasNewlyCreated(file) {\n    const modifiedTime = fs.statSync(file).mtimeMs;\n    assert(Date.now() - modifiedTime < 500, file + ' was not modified');\n  }\n\n  async function testFileModification(sourceFilePath, resultFile) {\n    // run cdata.js to ensure html_*.h files are created\n    await execPromise('node tools/cdata.js');\n\n    // modify file\n    fs.appendFileSync(sourceFilePath, ' ');\n    // delay for 1 second to ensure the modified time is different\n    await new Promise(resolve => setTimeout(resolve, 1000));\n\n    // run script cdata.js again and wait for it to finish\n    await execPromise('node tools/cdata.js');\n\n    await checkIfFileWasNewlyCreated(path.join(folderPath, resultFile));\n  }\n\n  describe('should build if', () => {\n    it('html_*.h files are missing', async () => {\n      await deleteBuiltFiles();\n      await runAndCheckIfBuiltFilesExist();\n    });\n\n    it('only one html_*.h file is missing', async () => {\n      // run script cdata.js and wait for it to finish\n      await execPromise('node tools/cdata.js');\n\n      // delete a random html_*.h file\n      let files = await fs.promises.readdir(folderPath);\n      let htmlFiles = files.filter(file => file.startsWith('html_') && path.extname(file) === '.h');\n      const randomFile = htmlFiles[Math.floor(Math.random() * htmlFiles.length)];\n      await fs.promises.unlink(path.join(folderPath, randomFile));\n\n      await runAndCheckIfBuiltFilesExist();\n    });\n\n    it('script was executed with -f or --force', async () => {\n      await execPromise('node tools/cdata.js');\n      await new Promise(resolve => setTimeout(resolve, 1000));\n      await execPromise('node tools/cdata.js --force');\n      await checkIfFileWasNewlyCreated(path.join(folderPath, 'html_ui.h'));\n      await new Promise(resolve => setTimeout(resolve, 1000));\n      await execPromise('node tools/cdata.js -f');\n      await checkIfFileWasNewlyCreated(path.join(folderPath, 'html_ui.h'));\n    });\n\n    it('a file changes', async () => {\n      await testFileModification(path.join(dataPath, 'index.htm'), 'html_ui.h');\n    });\n\n    it('a inlined file changes', async () => {\n      await testFileModification(path.join(dataPath, 'index.js'), 'html_ui.h');\n    });\n\n    it('a settings file changes', async () => {\n      await testFileModification(path.join(dataPath, 'settings_leds.htm'), 'html_ui.h');\n    });\n\n    it('the favicon changes', async () => {\n      await testFileModification(path.join(dataPath, 'favicon.ico'), 'html_ui.h');\n    });\n\n    it('cdata.js changes', async () => {\n      await testFileModification('tools/cdata.js', 'html_ui.h');\n    });\n\n    it('package.json changes', async () => {\n      await testFileModification('package.json', 'html_ui.h');\n    });\n  });\n\n  describe('should not build if', () => {\n    it('the files are already built', async () => {\n      await deleteBuiltFiles();\n\n      // run script cdata.js and wait for it to finish\n      let startTime = Date.now();\n      await execPromise('node tools/cdata.js');\n      const firstRunTime = Date.now() - startTime;\n\n      // run script cdata.js and wait for it to finish\n      startTime = Date.now();\n      await execPromise('node tools/cdata.js');\n      const secondRunTime = Date.now() - startTime;\n\n      // check if second run was faster than the first (must be at least 2x faster)\n      assert(secondRunTime < firstRunTime / 2, 'html_*.h files were rebuilt');\n    });\n  });\n});"
  },
  {
    "path": "tools/cdata.js",
    "content": "/**\n * Writes compressed C arrays of data files (web interface)\n * How to use it?\n *\n * 1) Install Node 20+ and npm\n * 2) npm install\n * 3) npm run build\n *\n * If you change data folder often, you can run it in monitoring mode (it will recompile and update *.h on every file change)\n *\n * > npm run dev\n *\n * How it works?\n *\n * It uses NodeJS packages to inline, minify and GZIP files. See writeHtmlGzipped and writeChunks invocations at the bottom of the page.\n */\n\nconst fs = require(\"node:fs\");\nconst path = require(\"path\");\nconst inline = require(\"web-resource-inliner\");\nconst zlib = require(\"node:zlib\");\nconst CleanCSS = require(\"clean-css\");\nconst minifyHtml = require(\"html-minifier-terser\").minify;\nconst packageJson = require(\"../package.json\");\n\n// Export functions for testing\nmodule.exports = { isFileNewerThan, isAnyFileInFolderNewerThan };\n\nconst 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\"]\n\n// \\x1b[34m is blue, \\x1b[36m is cyan, \\x1b[0m is reset\nconst wledBanner = `\n\\t\\x1b[34m  ##  ##      ##        ######    ######\n\\t\\x1b[34m##      ##    ##      ##        ##  ##  ##\n\\t\\x1b[34m##  ##  ##  ##        ######        ##  ##\n\\t\\x1b[34m##  ##  ##  ##        ##            ##  ##\n\\t\\x1b[34m  ##  ##      ######    ######    ######\n\\t\\t\\x1b[36m build script for web UI\n\\x1b[0m`;\n\n// Generate build timestamp as UNIX timestamp (seconds since epoch)\nfunction generateBuildTime() {\n  return Math.floor(Date.now() / 1000);\n}\n\nconst singleHeader = `/*\n * Binary array for the Web UI.\n * gzip is used for smaller size and improved speeds.\n * \n * Please see https://kno.wled.ge/advanced/custom-features/#changing-web-ui\n * to find out how to easily modify the web UI source!\n */\n\n// Automatically generated build time for cache busting (UNIX timestamp)\n#define WEB_BUILD_TIME ${generateBuildTime()}\n \n`;\n\nconst multiHeader = `/*\n * More web UI HTML source arrays.\n * This file is auto generated, please don't make any changes manually.\n *\n * Instead, see https://kno.wled.ge/advanced/custom-features/#changing-web-ui\n * to find out how to easily modify the web UI source!\n */\n`;\n\nfunction hexdump(buffer, isHex = false) {\n  let lines = [];\n\n  for (let i = 0; i < buffer.length; i += (isHex ? 32 : 16)) {\n    var block;\n    let hexArray = [];\n    if (isHex) {\n      block = buffer.slice(i, i + 32)\n      for (let j = 0; j < block.length; j += 2) {\n        hexArray.push(\"0x\" + block.slice(j, j + 2))\n      }\n    } else {\n      block = buffer.slice(i, i + 16); // cut buffer into blocks of 16\n      for (let value of block) {\n        hexArray.push(\"0x\" + value.toString(16).padStart(2, \"0\"));\n      }\n    }\n\n    let hexString = hexArray.join(\", \");\n    let line = `  ${hexString}`;\n    lines.push(line);\n  }\n\n  return lines.join(\",\\n\");\n}\n\nfunction adoptVersionAndRepo(html) {\n  let repoUrl = packageJson.repository ? packageJson.repository.url : undefined;\n  if (repoUrl) {\n    repoUrl = repoUrl.replace(/^git\\+/, \"\");\n    repoUrl = repoUrl.replace(/\\.git$/, \"\");\n    html = html.replaceAll(\"https://github.com/atuline/WLED\", repoUrl);\n    html = html.replaceAll(\"https://github.com/wled-dev/WLED\", repoUrl);\n  }\n  let version = packageJson.version;\n  if (version) {\n    html = html.replaceAll(\"##VERSION##\", version);\n  }\n  return html;\n}\n\nasync function minify(str, type = \"plain\") {\n  const options = {\n    collapseWhitespace: true,\n    conservativeCollapse: true, // preserve spaces in text\n    collapseBooleanAttributes: true,\n    collapseInlineTagWhitespace: true,\n    minifyCSS: true,\n    minifyJS: true,\n    removeAttributeQuotes: true,\n    removeComments: true,\n    sortAttributes: true,\n    sortClassName: true,\n  };\n\n  if (type == \"plain\") {\n    return str;\n  } else if (type == \"css-minify\") {\n    return new CleanCSS({}).minify(str).styles;\n  } else if (type == \"js-minify\") {\n    let js = await minifyHtml('<script>' + str + '</script>', options);\n    return js.replace(/<[\\/]*script>/g, '');\n  } else if (type == \"html-minify\") {\n    return await minifyHtml(str, options);\n  }\n\n  throw new Error(\"Unknown filter: \" + type);\n}\n\nasync function writeHtmlGzipped(sourceFile, resultFile, page, inlineCss = true) {\n  console.info(\"Reading \" + sourceFile);\n  inline.html({\n    fileContent: fs.readFileSync(sourceFile, \"utf8\"),\n    relativeTo: path.dirname(sourceFile),\n    strict: inlineCss,     // when not inlining css, ignore errors (enables linking style.css from subfolder htm files)\n    stylesheets: inlineCss // when true (default), css is inlined\n  },\n    async function (error, html) {\n      if (error) throw error;\n\n      html = adoptVersionAndRepo(html);\n      const originalLength = html.length;\n      html = await minify(html, \"html-minify\");\n      const result = zlib.gzipSync(html, { level: zlib.constants.Z_BEST_COMPRESSION });\n      console.info(\"Minified and compressed \" + sourceFile + \" from \" + originalLength + \" to \" + result.length + \" bytes\");\n      const array = hexdump(result);\n      let src = singleHeader;\n      src += `const uint16_t PAGE_${page}_length = ${result.length};\\n`;\n      src += `const uint8_t PAGE_${page}[] PROGMEM = {\\n${array}\\n};\\n\\n`;\n      console.info(\"Writing \" + resultFile);\n      fs.writeFileSync(resultFile, src);\n    });\n}\n\nasync function specToChunk(srcDir, s) {\n  const buf = fs.readFileSync(srcDir + \"/\" + s.file);\n  let chunk = `\\n// Autogenerated from ${srcDir}/${s.file}, do not edit!!\\n`\n\n  if (s.method == \"plaintext\" || s.method == \"gzip\") {\n    let str = buf.toString(\"utf-8\");\n    str = adoptVersionAndRepo(str);\n    const originalLength = str.length;\n    if (s.method == \"gzip\") {\n      if (s.mangle) str = s.mangle(str);\n      const zip = zlib.gzipSync(await minify(str, s.filter), { level: zlib.constants.Z_BEST_COMPRESSION });\n      console.info(\"Minified and compressed \" + s.file + \" from \" + originalLength + \" to \" + zip.length + \" bytes\");\n      const result = hexdump(zip);\n      chunk += `const uint16_t ${s.name}_length = ${zip.length};\\n`;\n      chunk += `const uint8_t ${s.name}[] PROGMEM = {\\n${result}\\n};\\n\\n`;\n      return chunk;\n    } else {\n      const minified = await minify(str, s.filter);\n      console.info(\"Minified \" + s.file + \" from \" + originalLength + \" to \" + minified.length + \" bytes\");\n      chunk += `const char ${s.name}[] PROGMEM = R\"${s.prepend || \"\"}${minified}${s.append || \"\"}\";\\n\\n`;\n      return s.mangle ? s.mangle(chunk) : chunk;\n    }\n  } else if (s.method == \"binary\") {\n    const result = hexdump(buf);\n    chunk += `const uint16_t ${s.name}_length = ${buf.length};\\n`;\n    chunk += `const uint8_t ${s.name}[] PROGMEM = {\\n${result}\\n};\\n\\n`;\n    return chunk;\n  }\n\n  throw new Error(\"Unknown method: \" + s.method);\n}\n\nasync function writeChunks(srcDir, specs, resultFile) {\n  let src = multiHeader;\n  for (const s of specs) {\n    console.info(\"Reading \" + srcDir + \"/\" + s.file + \" as \" + s.name);\n    src += await specToChunk(srcDir, s);\n  }\n  console.info(\"Writing \" + src.length + \" characters into \" + resultFile);\n  fs.writeFileSync(resultFile, src);\n}\n\n// Check if a file is newer than a given time\nfunction isFileNewerThan(filePath, time) {\n  const stats = fs.statSync(filePath);\n  return stats.mtimeMs > time;\n}\n\n// Check if any file in a folder (or its subfolders) is newer than a given time\nfunction isAnyFileInFolderNewerThan(folderPath, time) {\n  const files = fs.readdirSync(folderPath, { withFileTypes: true });\n  for (const file of files) {\n    const filePath = path.join(folderPath, file.name);\n    if (isFileNewerThan(filePath, time)) {\n      return true;\n    }\n    if (file.isDirectory() && isAnyFileInFolderNewerThan(filePath, time)) {\n      return true;\n    }\n  }\n  return false;\n}\n\n// Check if the web UI is already built\nfunction isAlreadyBuilt(webUIPath, packageJsonPath = \"package.json\") {\n  let lastBuildTime = Infinity;\n\n  for (const file of output) {\n    try {\n      lastBuildTime = Math.min(lastBuildTime, fs.statSync(file).mtimeMs);\n    } catch (e) {\n      if (e.code !== 'ENOENT') throw e;\n      console.info(\"File \" + file + \" does not exist. Rebuilding...\");\n      return false;\n    }\n  }\n\n  return !isAnyFileInFolderNewerThan(webUIPath, lastBuildTime) && !isFileNewerThan(packageJsonPath, lastBuildTime) && !isFileNewerThan(__filename, lastBuildTime);\n}\n\n// Don't run this script if we're in a test environment\nif (process.env.NODE_ENV === 'test') {\n  return;\n}\n\nconsole.info(wledBanner);\n\nif (isAlreadyBuilt(\"wled00/data\") && process.argv[2] !== '--force' && process.argv[2] !== '-f') {\n  console.info(\"Web UI is already built\");\n  return;\n}\n\nwriteHtmlGzipped(\"wled00/data/index.htm\", \"wled00/html_ui.h\", 'index');\nwriteHtmlGzipped(\"wled00/data/pixart/pixart.htm\", \"wled00/html_pixart.h\", 'pixart');\nwriteHtmlGzipped(\"wled00/data/pxmagic/pxmagic.htm\", \"wled00/html_pxmagic.h\", 'pxmagic');\nwriteHtmlGzipped(\"wled00/data/pixelforge/pixelforge.htm\", \"wled00/html_pixelforge.h\", 'pixelforge', false); // do not inline css\n//writeHtmlGzipped(\"wled00/data/edit.htm\", \"wled00/html_edit.h\", 'edit');\n\nwriteChunks(\n  \"wled00/data/\",\n  [\n    {\n      file: \"iro.js\",\n      name: \"JS_iro\",\n      method: \"gzip\",\n      filter: \"plain\", // no minification, it is already minified\n      mangle: (s) => s.replace(/^\\/\\*![\\s\\S]*?\\*\\//, '') // remove license comment at the top\n    }\n  ],\n  \"wled00/js_iro.h\"\n);\n\nwriteChunks(\n  \"wled00/data/pixelforge\",\n  [\n    {\n      file: \"omggif.js\",\n      name: \"JS_omggif\",\n      method: \"gzip\",\n      filter: \"js-minify\",\n      mangle: (s) => s.replace(/^\\/\\*![\\s\\S]*?\\*\\//, '') // remove license comment at the top\n    }\n  ],\n  \"wled00/js_omggif.h\"\n);\n\nwriteChunks(\n  \"wled00/data\",\n  [\n    {\n      file: \"edit.htm\",\n      name: \"PAGE_edit\",\n      method: \"gzip\",\n      filter: \"html-minify\"\n    }\n  ],\n  \"wled00/html_edit.h\"\n);\n\nwriteChunks(\n  \"wled00/data/cpal\",\n  [\n    {\n      file: \"cpal.htm\",\n      name: \"PAGE_cpal\",\n      method: \"gzip\",\n      filter: \"html-minify\"\n    }\n  ],\n  \"wled00/html_cpal.h\"\n);\n\nwriteChunks(\n  \"wled00/data\",\n  [\n    {\n      file: \"style.css\",\n      name: \"PAGE_settingsCss\",\n      method: \"gzip\",\n      filter: \"css-minify\",\n      mangle: (str) =>\n        str\n          .replace(\"%%\", \"%\")\n    },\n    {\n      file: \"common.js\",\n      name: \"JS_common\",\n      method: \"gzip\",\n      filter: \"js-minify\",\n    },\n    {\n      file: \"settings.htm\",\n      name: \"PAGE_settings\",\n      method: \"gzip\",\n      filter: \"html-minify\",\n    },\n    {\n      file: \"settings_wifi.htm\",\n      name: \"PAGE_settings_wifi\",\n      method: \"gzip\",\n      filter: \"html-minify\",\n    },\n    {\n      file: \"settings_leds.htm\",\n      name: \"PAGE_settings_leds\",\n      method: \"gzip\",\n      filter: \"html-minify\",\n    },\n    {\n      file: \"settings_dmx.htm\",\n      name: \"PAGE_settings_dmx\",\n      method: \"gzip\",\n      filter: \"html-minify\",\n    },\n    {\n      file: \"settings_ui.htm\",\n      name: \"PAGE_settings_ui\",\n      method: \"gzip\",\n      filter: \"html-minify\",\n    },\n    {\n      file: \"settings_sync.htm\",\n      name: \"PAGE_settings_sync\",\n      method: \"gzip\",\n      filter: \"html-minify\",\n    },\n    {\n      file: \"settings_time.htm\",\n      name: \"PAGE_settings_time\",\n      method: \"gzip\",\n      filter: \"html-minify\",\n    },\n    {\n      file: \"settings_sec.htm\",\n      name: \"PAGE_settings_sec\",\n      method: \"gzip\",\n      filter: \"html-minify\",\n    },\n    {\n      file: \"settings_um.htm\",\n      name: \"PAGE_settings_um\",\n      method: \"gzip\",\n      filter: \"html-minify\",\n    },\n    {\n      file: \"settings_2D.htm\",\n      name: \"PAGE_settings_2D\",\n      method: \"gzip\",\n      filter: \"html-minify\",\n    },\n    {\n      file: \"settings_pin.htm\",\n      name: \"PAGE_settings_pin\",\n      method: \"gzip\",\n      filter: \"html-minify\"\n    },\n    {\n      file: \"settings_pininfo.htm\",\n      name: \"PAGE_settings_pininfo\",\n      method: \"gzip\",\n      filter: \"html-minify\"\n    }\n  ],\n  \"wled00/html_settings.h\"\n);\n\nwriteChunks(\n  \"wled00/data\",\n  [\n    {\n      file: \"usermod.htm\",\n      name: \"PAGE_usermod\",\n      method: \"gzip\",\n      filter: \"html-minify\",\n      mangle: (str) =>\n        str.replace(/fetch\\(\"http\\:\\/\\/.*\\/win/gms, 'fetch(\"/win'),\n    },\n    {\n      file: \"msg.htm\",\n      name: \"PAGE_msg\",\n      prepend: \"=====(\",\n      append: \")=====\",\n      method: \"plaintext\",\n      filter: \"html-minify\",\n      mangle: (str) => str.replace(/\\<h2\\>.*\\<\\/body\\>/gms, \"<h2>%MSG%</body>\"),\n    },\n    {\n      file: \"dmxmap.htm\",\n      name: \"PAGE_dmxmap\",\n      prepend: \"=====(\",\n      append: \")=====\",\n      method: \"plaintext\",\n      filter: \"html-minify\",\n      mangle: (str) => `\n#ifdef WLED_ENABLE_DMX\n${str.replace(/function FM\\(\\)[ ]?\\{/gms, \"function FM() {%DMXVARS%\\n\")}\n#else\nconst char PAGE_dmxmap[] PROGMEM = R\"=====()=====\";\n#endif\n`,\n    },\n    {\n      file: \"update.htm\",\n      name: \"PAGE_update\",\n      method: \"gzip\",\n      filter: \"html-minify\",\n    },\n    {\n      file: \"welcome.htm\",\n      name: \"PAGE_welcome\",\n      method: \"gzip\",\n      filter: \"html-minify\",\n    },\n    {\n      file: \"liveview.htm\",\n      name: \"PAGE_liveview\",\n      method: \"gzip\",\n      filter: \"html-minify\",\n    },\n    {\n      file: \"liveviewws2D.htm\",\n      name: \"PAGE_liveviewws2D\",\n      method: \"gzip\",\n      filter: \"html-minify\",\n    },\n    {\n      file: \"404.htm\",\n      name: \"PAGE_404\",\n      method: \"gzip\",\n      filter: \"html-minify\",\n    },\n    {\n      file: \"favicon.ico\",\n      name: \"favicon\",\n      method: \"binary\",\n    }\n  ],\n  \"wled00/html_other.h\"\n);\n"
  },
  {
    "path": "tools/dynarray_espressif32.ld",
    "content": "/* ESP32 linker script fragment to add dynamic array section to binary */\nSECTIONS\n{\n  .dynarray :\n  {\n    . = ALIGN(0x10);\n    KEEP(*(SORT_BY_INIT_PRIORITY(.dynarray.*)))\n  } > default_rodata_seg\n}\nINSERT AFTER .flash.rodata;\n"
  },
  {
    "path": "tools/fps_test.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <title>WLED frame rate test tool</title>\n    <style>\n        body {\n            background-color: #222;\n            color: #fff;\n            font-family: Helvetica, Verdana, sans-serif;\n        }\n        input {\n            background-color: #333;\n            color: #fff;\n        }\n        #ip {\n            width: 100px;\n        }\n        #secs {\n            width: 36px;\n        }\n        #csva {\n            position: absolute;\n            top: -100px; /*gtfo*/\n        }\n        button {\n            background-color: #333;\n            color: #fff;\n        }\n        table, th, td {\n            border: 1px solid #aaa;\n            border-collapse: collapse;\n            text-align: center;\n        }\n        .red {\n            color: #d20;\n        }\n    </style>\n    <script>\n        var gotfx = false, running = false;\n        var pos = 0, prev = 0, min = 999, max = 0, fpslist = [], names = [], names_checked = [];\n        var to;\n        function S() {\n            document.getElementById('ip').value = localStorage.getItem('locIpFps');\n            if (document.getElementById('ip').value) req(false);\n        }\n        function loadC() {\n            hide(false);\n            var list = localStorage.getItem('fpsFxSelection');\n            if (!list) return;\n            list = JSON.parse(list);\n            var chks = document.querySelectorAll('.fxcheck');\n            for (let i = 0; i < chks.length; i++) {\n               if (i < list.length) chks[i].checked = list[i];\n            }\n        }\n        function saveC() {\n            var list = [];\n            var chks = document.querySelectorAll('.fxcheck');\n            for (let i = 0; i < chks.length; i++) {\n                list.push(chks[i].checked);\n            }\n            localStorage.setItem('fpsFxSelection', JSON.stringify(list));\n        }\n        function setC(c) {\n            hide(false);\n            var chks = document.querySelectorAll('.fxcheck');\n            for (let i = 0; i < chks.length; i++) {\n                chks[i].checked = (c == 255);\n            }\n            if (c == 1 && chks.length > 100) {\n                chks[1].checked = true;  //Blink\n                chks[15].checked = true; //Running\n                chks[16].checked = true; //Saw\n                chks[37].checked = true; //Running 2\n                chks[44].checked = true; //Tetrix\n                chks[63].checked = true; //Pride 2015\n                chks[74].checked = true; //Colortwinkles\n                chks[101].checked = true;//Pacifica\n            }\n        }\n        function hide(h) {\n            var trs = document.querySelectorAll('.trs');\n            var chks = document.querySelectorAll('.fxcheck');\n            for (let i = 0; i < trs.length; i++) {\n                trs[i].style.display = (h && !chks[i].checked) ? \"none\":\"table-row\";\n            }\n        }\n        function run(init) {\n            if (init) {\n                running = !running;\n                document.getElementById('runbtn').innerText = running ? 'Stop':'Run';\n                if (running) {pos = 0; prev = -1; min = 999; max = 0; fpslist = []; names_checked = []; hide(true);}\n                clearTimeout(to);\n                if (!running) {req({seg:{fx:0},v:true,stop:true}); return;}\n            }\n            if (!gotfx) {req(false); return;}\n            var chks = document.querySelectorAll('.fxcheck');\n            var fpsb = document.querySelectorAll('.fps');\n            if (prev >= 0) {pos++};\n            if (pos >= chks.length) {run(true); return;} //end\n            while (!chks[pos].checked) {\n                fpsb[pos].innerText = \"-\";\n                pos++;\n                if (pos >= chks.length) {run(true); return;} //end\n            }\n            names_checked.push(names[pos]);\n            var extra = {};\n            try {\n                extra = JSON.parse(document.getElementById('ej').value);\n            } catch (e) {\n\n            }\n            var cmd = {seg:{fx:pos},v:true};\n            Object.assign(cmd, extra);\n            req(cmd);\n        }\n        function req(command) {\n            var ip = document.getElementById('ip').value;\n            if (!ip) {alert(\"Please enter WLED IP\"); return;}\n            if (ip != localStorage.getItem('locIpFps')) localStorage.setItem('locIpFps', document.getElementById('ip').value);\n            var url = command ? `http://${ip}/json/si` : `http://${ip}/json/effects`;\n            var type = command ? 'post':'get';\n            var req = undefined;\n            if (command)\n            {\n                req = JSON.stringify(command);\n            }\n            fetch\n            (url, {\n                method: type,\n                headers: {\n                    \"Content-type\": \"application/json; charset=UTF-8\"\n                },\n                body: req\n            })\n            .then(res => {\n                if (!res.ok) {\n                    alert('Data malfunction');\n                }\n                return res.json();\n            })\n            .then(json => {\n                if (!json) {\n                    alert('Empty response'); return;\n                }\n                if (!command) {\n                    names = json;\n                    var tblc = '';\n                    for (let i = 0; i < json.length; i++) {\n\t\t                tblc += `<tr class=\"trs\"><td><input type=\"checkbox\" class=\"fxcheck\" /></td><td>${i}</td><td>${json[i]}</td><td class=\"fps\"></td></tr>`\n\t                }\n                    var tbl = `<table>\n                        <tr>\n                            <th>Test?</th><th>ID</th><th>Effect Name</th><th>FPS</th>\n                        </tr>\n                        ${tblc}\n                    </table>`;\n                    document.getElementById('tablecon').innerHTML = tbl;\n                    setC(1);\n                    loadC();\n                    gotfx = true;\n                    document.getElementById('runbtn').innerText = \"Run\";\n                } else {\n                    if (!json.info) return;\n                    document.getElementById('leds').innerText = json.info.leds.count;\n                    document.getElementById('seg').innerText = json.state.seg[0].len;\n                    document.getElementById('bri').innerText = json.state.bri;\n                    if (prev >= 0) {\n                        var lastfps = parseInt(json.info.leds.fps); //previous FX\n                        if (lastfps < min) min = lastfps;\n                        if (lastfps > max) max = lastfps;\n                        fpslist.push(lastfps);\n                        var sum = 0;\n                        for (let i = 0; i < fpslist.length; i++) {\n                            sum += fpslist[i];\n                        }\n                        sum /= fpslist.length;\n                        document.getElementById('fps_min').innerText = min;\n                        document.getElementById('fps_max').innerText = max;\n                        document.getElementById('fps_avg').innerText = Math.round(sum*10)/10;\n                        var fpsb = document.querySelectorAll('.fps');\n                        fpsb[prev].innerHTML = lastfps;\n                    }\n                    prev = pos;\n                    var delay = parseInt(document.getElementById('secs').value)*1000;\n                    delay = Math.min(Math.max(delay, 2000), 15000)\n                    if (!command.stop) to = setTimeout(run,delay);\n                }\n            })\n            .catch(function (error) {\n\t\t        alert('Comms malfunction');\n\t\t        console.log(error);\n\t        });\n        }\n        function csv(n) {\n            var txt = \"\";\n            for (let i = 0; i < fpslist.length; i++) {\n                if (!n) txt += names_checked[i] + ',';\n                txt += fpslist[i]; txt += \"\\n\";\n            }\n            document.getElementById('csva').value = txt;\n            var copyText = document.getElementById('csva');\n\n            copyText.select();\n            copyText.setSelectionRange(0, 999999);\n            document.execCommand(\"copy\");\n        }\n    </script>\n</head>\n<body onload=\"S()\">\n    <h2>Starship monitoring dashboard</h2>\n    (or rather just a WLED frame rate tester lol)<br><br>\n    IP: <input id=\"ip\" /><br>\n    Time per effect: <input type=number id=secs value=5 max=15 min=2 />s<br>\n    Effects to test:\n    <button type=\"button\" onclick=\"setC(255)\">All</button>\n    <button type=\"button\" onclick=\"setC(1)\">Selection 1</button>\n    <button type=\"button\" onclick=\"setC(0)\">None</button>\n    <button type=\"button\" onclick=\"loadC()\">Get LS</button>\n    <button type=\"button\" class=\"red\" onclick=\"saveC()\">Save to LS</button><br>\n    Extra JSON: <input id=\"ej\" /><br>\n\n    <button type=\"button\" onclick=\"run(true)\" id=\"runbtn\">Fetch FX list</button><br>\n    LEDs: <span id=\"leds\">-</span>, Seg: <span id=\"seg\">-</span>, Bri: <span id=\"bri\">-</span><br>\n    FPS min: <span id=\"fps_min\">-</span>, max: <span id=\"fps_max\">-</span>, avg: <span id=\"fps_avg\">-</span><br><br>\n    <div id=\"tablecon\">\n    </div><br>\n    <button type=\"button\" onclick=\"csv(false)\">Copy csv to clipboard</button>\n    <button type=\"button\" onclick=\"csv(true)\">Copy csv (FPS only)</button>\n    <textarea id=csva></textarea>\n</body>\n</html>"
  },
  {
    "path": "tools/json_test.htm",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>JSON client</title>\n    <style>\n      :root {\n        --bCol:#333;--cCol:#222;--dCol:#666;--tCol:#fff;\n      }\n      body {\n        font-family: Verdana, sans-serif;\n        text-align: center;\n        background: var(--cCol);\n        color: var(--tCol);\n        margin: 20px;\n        background-attachment: fixed;\n      }\n      button {\n        background: var(--cCol);\n        color: var(--tCol);\n        border: 0.3ch solid var(--cCol);\n        display: inline-block;\n        font-size: 20px;\n        margin: 8px;\n        margin-top: 12px;\n      }\n      input {\n        background: var(--cCol);\n        color: var(--tCol);\n        border: 0.5ch solid var(--cCol);\n        width: 100%;\n      }\n      h1{\n        margin: 0px;\n        font-size: 20px;\n      }\n      h2{\n        font-size: 16px;\n        margin-top: 20px;\n      }\n      form{\n        background: var(--bCol);\n        width: 500px;\n        padding: 20px;\n        -webkit-border-radius: 10px;\n        -moz-border-radius: 10px;\n        display: inline-block;\n      }\n      textarea{\n        background: var(--cCol);\n        color: var(--tCol);\n        padding-top: 10px;\n        width: 100%;\n        font-family: monaco,monospace;\n        font-size: 12px;\n        -webkit-border-radius: 10px;\n        -moz-border-radius: 10px;\n      }\n    </style>\n  </head>\n\n  <body>\n      <form name=\"cf\">\n          <h1>JSON API test tool</h1>\n          <h2>URL:</h2>\n          <input name=\"cu\" type=\"text\" size=\"60\" value=\"http://192.168.4.1/json\">\n          <div id=\"buttons\">\n              <button type=\"button\" onclick=\"rq('GET')\">GET</button>\n              <button type=\"button\" onclick=\"rq('POST')\">POST</button>\n          </div>\n          <h2>Body:</h2>\n          <textarea name=\"bd\" rows=\"8\" cols=\"100\"></textarea>\n          <h2>Response:</h2>\n          <textarea name=\"rsp\" rows=\"25\" cols=\"100\"></textarea>\n      </form>\n  </body>\n</html>\n\n<script>\nfunction rq(cm)\n{\n  var h = new XMLHttpRequest();\n  h.open(cm, document.cf.cu.value, true);\n  h.onreadystatechange = function()\n  {\n    if(h.readyState == 4)\n    {\n      if(h.status==200)\n      {\n        document.cf.rsp.value=\"Bad JSON: \"+h.responseText\n        document.cf.rsp.value=JSON.stringify(JSON.parse(h.responseText), null, '\\t');\n      }\n      else\n      {\n        document.cf.rsp.value=\"Error \"+h.status+\"\\r\\n\\n\"+h.responseText;\n      }\n    }\n  }\n  h.send(document.cf.bd.value);\n}\n</script>"
  },
  {
    "path": "tools/multi-update.cmd",
    "content": "@echo off\nSETLOCAL\nSET FWPATH=c:\\path\\to\\your\\WLED\\build_output\\firmware\nGOTO ESPS\n\n:UPDATEONE\nIF NOT EXIST %FWPATH%\\%2 GOTO SKIP\n\tping -w 1000 -n 1 %1 | find \"TTL=\" || GOTO SKIP\n\tECHO Updating %1\n\tcurl -s -F \"update=@%FWPATH%/%2\" %1/update >nul\n:SKIP\nGOTO:EOF\n\n:ESPS\ncall :UPDATEONE 192.168.x.x firmware.bin\ncall :UPDATEONE ....\n"
  },
  {
    "path": "tools/multi-update.sh",
    "content": "#!/bin/bash\nFWPATH=/path/to/your/WLED/build_output/firmware\n\nupdate_one() {\nif [ -f $FWPATH/$2 ]; then\n\tping -c 1 $1 >/dev/null\n\tPINGRESULT=$?\n\tif [ $PINGRESULT -eq 0 ]; then\n\t\techo Updating $1\n\t\tcurl -s -F \"update=@${FWPATH}/$2\" $1/update >/dev/null\n\t\treturn 0\n\tfi\n\treturn 1\nfi\n}\n\nupdate_one 192.168.x.x firmware.bin\nupdate_one 192.168.x.x firmware.bin\n# ..."
  },
  {
    "path": "tools/partitions-16MB_spiffs-tinyuf2.csv",
    "content": "# ESP-IDF Partition Table\n# Name,   Type, SubType, Offset,  Size, Flags\n# bootloader.bin,,          0x1000, 32K\n# partition table,,         0x8000, 4K\nnvs,      data, nvs,      0x9000,  20K,\notadata,  data, ota,      0xe000,  8K,\nota_0,    app,  ota_0,   0x10000,  2048K,\nota_1,    app,  ota_1,  0x210000,  2048K,\nuf2,      app,  factory,0x410000,  256K,\nspiffs,     data, spiffs,    0x450000,  11968K,\n"
  },
  {
    "path": "tools/partitions-4MB_spiffs-tinyuf2.csv",
    "content": "# ESP-IDF Partition Table\n# Name,   Type, SubType, Offset,  Size, Flags\n# bootloader.bin,,          0x1000, 32K\n# partition table,          0x8000, 4K\n\nnvs,      data, nvs,      0x9000,  20K,\notadata,  data, ota,      0xe000,  8K,\nota_0,    0,    ota_0,   0x10000,  1408K,\nota_1,    0,    ota_1,  0x170000,  1408K,\nuf2,      app,  factory,0x2d0000,  256K,\nspiffs,   data, spiffs, 0x310000,  960K,\n"
  },
  {
    "path": "tools/partitions-8MB_spiffs-tinyuf2.csv",
    "content": "# ESP-IDF Partition Table\n# Name,   Type, SubType, Offset,  Size, Flags\n# bootloader.bin,,          0x1000, 32K\n# partition table,,         0x8000, 4K\nnvs,      data, nvs,      0x9000,  20K,\notadata,  data, ota,      0xe000,  8K,\nota_0,    app,    ota_0,   0x10000,  2048K,\nota_1,    app,    ota_1,  0x210000,  2048K,\nuf2,      app,  factory,0x410000,  256K,\nspiffs,   data, spiffs,   0x450000,  3776K,\n"
  },
  {
    "path": "tools/stress_test.sh",
    "content": "#!/bin/bash\n# Some web server stress tests\n#\n# Perform a large number of parallel requests, stress testing the web server\n# TODO: some kind of performance metrics\n\n# Accepts three command line arguments:\n# - first argument - mandatory - IP or hostname of target server\n# - second argument - target type (optional)\n# - third argument - xfer count (for replicated targets) (optional)\nHOST=$1\ndeclare -n TARGET_STR=\"${2:-JSON_LARGER}_TARGETS\"\nREPLICATE_COUNT=$((\"${3:-10}\"))\n\nPARALLEL_MAX=${PARALLEL_MAX:-50}\n\nCURL_ARGS=\"--compressed --parallel --parallel-immediate --parallel-max ${PARALLEL_MAX}\"\nCURL_PRINT_RESPONSE_ARGS=\"-w %{http_code}\\n\"\n\nJSON_TARGETS=('json/state' 'json/info' 'json/si', 'json/palettes' 'json/fxdata' 'settings/s.js?p=2')\nFILE_TARGETS=('' 'iro.js' 'rangetouch.js' 'settings' 'settings/wifi')\n# Replicate one target many times\nfunction replicate() {\n  printf \"${1}?%d \" $(seq 1 ${REPLICATE_COUNT})\n}\nread -a JSON_TINY_TARGETS <<< $(replicate \"json/nodes\")\nread -a JSON_SMALL_TARGETS <<< $(replicate \"json/info\")\nread -a JSON_LARGE_TARGETS <<< $(replicate \"json/si\")\nread -a JSON_LARGER_TARGETS <<< $(replicate \"json/fxdata\")\nread -a INDEX_TARGETS <<< $(replicate \"\")\n\n# Expand target URLS to full arguments for curl\nTARGETS=(${TARGET_STR[@]})\n#echo \"${TARGETS[@]}\"\nFULL_TGT_OPTIONS=$(printf \"http://${HOST}/%s -o /dev/null \" \"${TARGETS[@]}\")\n#echo ${FULL_TGT_OPTIONS}\n\ntime curl ${CURL_ARGS} ${FULL_TGT_OPTIONS}\n"
  },
  {
    "path": "tools/udp_test.py",
    "content": "import numpy as np\r\nimport socket\r\n\r\nclass WledRealtimeClient:\r\n    def __init__(self, wled_controller_ip, num_pixels, udp_port=21324, max_pixels_per_packet=126):\r\n        self.wled_controller_ip = wled_controller_ip\r\n        self.num_pixels = num_pixels\r\n        self.udp_port = udp_port\r\n        self.max_pixels_per_packet = max_pixels_per_packet\r\n        self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)\r\n        self._prev_pixels = np.full((3, self.num_pixels), 253, dtype=np.uint8)\r\n        self.pixels = np.full((3, self.num_pixels), 1, dtype=np.uint8)\r\n    \r\n    def update(self):\r\n        # Truncate values and cast to integer\r\n        self.pixels = np.clip(self.pixels, 0, 255).astype(np.uint8)\r\n        p = np.copy(self.pixels)\r\n        \r\n        idx = np.where(~np.all(p == self._prev_pixels, axis=0))[0]\r\n        num_pixels = len(idx)\r\n        n_packets = (num_pixels + self.max_pixels_per_packet - 1) // self.max_pixels_per_packet\r\n        idx_split = np.array_split(idx, n_packets)\r\n        \r\n        header = bytes([1, 2])  # WARLS protocol header\r\n        for packet_indices in idx_split:\r\n            data = bytearray(header)\r\n            for i in packet_indices:\r\n                data.extend([i, *p[:, i]])  # Index and RGB values\r\n            self._sock.sendto(bytes(data), (self.wled_controller_ip, self.udp_port))\r\n        \r\n        self._prev_pixels = np.copy(p)\r\n\r\n\r\n\r\n################################## LED blink test ##################################\r\nif __name__ == \"__main__\":\r\n    WLED_CONTROLLER_IP = \"192.168.1.153\"\r\n    NUM_PIXELS = 255 # Amount of LEDs on your strip\r\n    import time\r\n    wled = WledRealtimeClient(WLED_CONTROLLER_IP, NUM_PIXELS)\r\n    print('Starting LED blink test')\r\n    while True:\r\n        for i in range(NUM_PIXELS):\r\n            wled.pixels[1, i] = 255 if wled.pixels[1, i] == 0 else 0\r\n        wled.update()\r\n        time.sleep(.01)\r\n"
  },
  {
    "path": "tools/wled-tools",
    "content": "#!/bin/bash\n\n# WLED Tools\n# A utility for managing WLED devices in a local network\n# https://github.com/wled/WLED\n\n# Color Definitions\nGREEN=\"\\e[32m\"\nRED=\"\\e[31m\"\nBLUE=\"\\e[34m\"\nYELLOW=\"\\e[33m\"\nRESET=\"\\e[0m\"\n\n# Logging function\nlog() {\n    local category=\"$1\"\n    local color=\"$2\"\n    local text=\"$3\"\n\n    if [ \"$quiet\" = true ]; then\n        return\n    fi\n\n    if [ -t 1 ]; then  # Check if output is a terminal\n        echo -e \"${color}[${category}]${RESET} ${text}\"\n    else\n        echo \"[${category}] ${text}\"\n    fi\n}\n\n# Fetch a URL to a destination file, validating status codes.\n# Usage: fetch \"<url>\" \"<dest or empty>\" \"200 404\"\nfetch() {\n    local url=\"$1\"\n    local dest=\"$2\"\n    local accepted=\"${3:-200}\"\n\n    # If no dest given, just discard body\n    local out\n    if [ -n \"$dest\" ]; then\n        # Write to \".tmp\" files first, then move when success, to ensure we don't write partial files\n        out=\"${dest}.tmp\"\n    else\n        out=\"/dev/null\"\n    fi\n\n    response=$(curl --connect-timeout 5 --max-time 30 -s -w \"%{http_code}\" -o \"$out\" \"$url\")\n    local curl_exit_code=$?\n\n    if [ $curl_exit_code -ne 0 ]; then\n        [ -n \"$dest\" ] && rm -f \"$out\"\n        log \"ERROR\" \"$RED\" \"Connection error during request to $url (curl exit code: $curl_exit_code).\"\n        return 1\n    fi\n\n    for code in $accepted; do\n        if [ \"$response\" = \"$code\" ]; then\n            # Accepted; only persist body for 2xx responses\n            if [ -n \"$dest\" ]; then\n                if [[ \"$response\" =~ ^2 ]]; then\n                    mv \"$out\" \"$dest\"\n                else\n                    rm -f \"$out\"\n                fi\n            fi\n            return 0\n        fi\n    done\n\n    # not accepted\n    [ -n \"$dest\" ] && rm -f \"$out\"\n    log \"ERROR\" \"$RED\" \"Unexpected response from $url (HTTP $response).\"\n    return 2\n}\n\n\n# POST a file to a URL, validating status codes.\n# Usage: post_file \"<url>\" \"<file>\" \"200\"\npost_file() {\n    local url=\"$1\"\n    local file=\"$2\"\n    local accepted=\"${3:-200}\"\n\n    response=$(curl --connect-timeout 5 --max-time 300 -s -w \"%{http_code}\" -o /dev/null -X POST -F \"file=@$file\" \"$url\")\n    local curl_exit_code=$?\n\n    if [ $curl_exit_code -ne 0 ]; then\n        log \"ERROR\" \"$RED\" \"Connection error during POST to $url (curl exit code: $curl_exit_code).\"\n        return 1\n    fi\n\n    for code in $accepted; do\n        if [ \"$response\" -eq \"$code\" ]; then\n            return 0\n        fi\n    done\n\n    log \"ERROR\" \"$RED\" \"Unexpected response from $url (HTTP $response).\"\n    return 2\n}\n\n\n# Print help message\nshow_help() {\n    cat << EOF\nUsage: wled-tools.sh [OPTIONS] COMMAND [ARGS...]\n\nOptions:\n  -h, --help              Show this help message and exit.\n  -t, --target <IP/Host>  Specify a single WLED device by IP address or hostname.\n  -D, --discover          Discover multiple WLED devices using mDNS.\n  -d, --directory <Path>  Specify a directory for saving backups (default: working directory).\n  -f, --firmware <File>   Specify the firmware file for updating devices.\n  -q, --quiet             Suppress logging output (also makes discover output hostnames only).\n\nCommands:\n  backup      Backup the current state of a WLED device or multiple discovered devices.\n  update      Update the firmware of a WLED device or multiple discovered devices.\n  discover    Discover WLED devices using mDNS and list their IP addresses and names.\n\nExamples:\n  # Discover all WLED devices on the network\n  ./wled-tools discover\n\n  # Backup a specific WLED device\n  ./wled-tools -t 192.168.1.100 backup\n\n  # Backup all discovered WLED devices to a specific directory\n  ./wled-tools -D -d /path/to/backups backup\n\n  # Update firmware on all discovered WLED devices\n  ./wled-tools -D -f /path/to/firmware.bin update\n\nEOF\n}\n\n# Discover devices using mDNS\ndiscover_devices() {  \n    if ! command -v avahi-browse &> /dev/null; then  \n        log \"ERROR\" \"$RED\" \"'avahi-browse' is required but not installed, please install avahi-utils using your preferred package manager.\"\n        exit 1  \n    fi  \n\n    # Map avahi responses to strings seperated by 0x1F (unit separator)\n    mapfile -t raw_devices < <(avahi-browse _wled._tcp --terminate -r -p | awk -F';' '/^=/ {print $7\"\\x1F\"$8\"\\x1F\"$9}')  \n\n    local devices_array=()  \n    for device in \"${raw_devices[@]}\"; do  \n        IFS=$'\\x1F' read -r hostname address port <<< \"$device\"  \n        devices_array+=(\"$hostname\" \"$address\" \"$port\")  \n    done  \n\n    echo \"${devices_array[@]}\"  \n}  \n\n# Backup one device\nbackup_one() {\n    local hostname=\"$1\"\n    local address=\"$2\"\n    local port=\"$3\"\n\n    log \"INFO\" \"$YELLOW\" \"Backing up device config/presets/ir: $hostname ($address:$port)\"\n\n    mkdir -p \"$backup_dir\"\n\n    local file_prefix=\"${backup_dir}/${hostname}\"\n\n    if ! fetch \"http://$address:$port/cfg.json\" \"${file_prefix}.cfg.json\"; then  \n        log \"ERROR\" \"$RED\" \"Failed to backup configuration for $hostname\"  \n        return 1  \n    fi  \n    \n    if ! fetch \"http://$address:$port/presets.json\" \"${file_prefix}.presets.json\"; then  \n        log \"ERROR\" \"$RED\" \"Failed to backup presets for $hostname\"  \n        return 1  \n    fi\n\n    # ir.json is optional\n    if ! fetch \"http://$address:$port/ir.json\" \"${file_prefix}.ir.json\" \"200 404\"; then\n        log \"ERROR\" \"$RED\" \"Failed to backup ir configs for $hostname\" \n    fi\n\n    log \"INFO\" \"$GREEN\" \"Successfully backed up config and presets for $hostname\"\n    return 0\n}\n\n# Update one device\nupdate_one() {\n    local hostname=\"$1\"\n    local address=\"$2\"\n    local port=\"$3\"\n    local firmware=\"$4\"\n\n    log \"INFO\" \"$YELLOW\" \"Starting firmware update for device: $hostname ($address:$port)\"\n\n    local url=\"http://$address:$port/update\"\n\n    if ! post_file \"$url\" \"$firmware\" \"200\"; then\n        log \"ERROR\" \"$RED\" \"Failed to update firmware for $hostname\"\n        return 1\n    fi\n    \n    log \"INFO\" \"$GREEN\" \"Successfully initiated firmware update for $hostname\"\n    return 0\n}\n\n# Command-line arguments processing\ncommand=\"\"\ntarget=\"\"\ndiscover=false\nquiet=false\nbackup_dir=\"./\"\nfirmware_file=\"\"\n\nif [ $# -eq 0 ]; then\n    show_help\n    exit 0\nfi\n\nwhile [[ $# -gt 0 ]]; do\n    case \"$1\" in\n        -h|--help)\n            show_help\n            exit 0\n            ;;\n        -t|--target)\n            if [ -z \"$2\" ] || [[ \"$2\" == -* ]]; then\n                log \"ERROR\" \"$RED\" \"The --target option requires an argument.\"\n                exit 1\n            fi\n            target=\"$2\"\n            shift 2\n            ;;\n        -D|--discover)\n            discover=true\n            shift\n            ;;\n        -d|--directory)\n            if [ -z \"$2\" ] || [[ \"$2\" == -* ]]; then\n                log \"ERROR\" \"$RED\" \"The --directory option requires an argument.\"\n                exit 1\n            fi\n            backup_dir=\"$2\"\n            shift 2\n            ;;\n        -f|--firmware)\n            if [ -z \"$2\" ] || [[ \"$2\" == -* ]]; then\n                log \"ERROR\" \"$RED\" \"The --firmware option requires an argument.\"\n                exit 1\n            fi\n            firmware_file=\"$2\"\n            shift 2\n            ;;\n        -q|--quiet)\n            quiet=true\n            shift\n            ;;\n        backup|update|discover)\n            command=\"$1\"\n            shift\n            ;;\n        *)\n            log \"ERROR\" \"$RED\" \"Unknown argument: $1\"\n            exit 1\n            ;;\n    esac\ndone\n\n# Execute the appropriate command\ncase \"$command\" in\n    discover)\n        read -ra devices <<< \"$(discover_devices)\"\n        for ((i=0; i<${#devices[@]}; i+=3)); do\n            hostname=\"${devices[$i]}\"\n            address=\"${devices[$i+1]}\"\n            port=\"${devices[$i+2]}\"\n\n            if [ \"$quiet\" = true ]; then\n                echo \"$hostname\"\n            else\n                log \"INFO\" \"$BLUE\" \"Discovered device: Hostname=$hostname, Address=$address, Port=$port\"\n            fi\n        done\n        ;;\n    backup)\n        if [ -n \"$target\" ]; then\n            # Assume target is both the hostname and address, with port 80\n            backup_one \"$target\" \"$target\" \"80\"\n        elif [ \"$discover\" = true ]; then\n            read -ra devices <<< \"$(discover_devices)\"\n            for ((i=0; i<${#devices[@]}; i+=3)); do\n                hostname=\"${devices[$i]}\"\n                address=\"${devices[$i+1]}\"\n                port=\"${devices[$i+2]}\"\n                backup_one \"$hostname\" \"$address\" \"$port\"\n            done\n        else\n            log \"ERROR\" \"$RED\" \"No target specified. Use --target or --discover.\"\n            exit 1\n        fi\n        ;;\n    update)\n        # Validate firmware before proceeding\n        if [ -z \"$firmware_file\" ] || [ ! -f \"$firmware_file\" ]; then\n            log \"ERROR\" \"$RED\" \"Please provide a file in --firmware that exists\"\n            exit 1\n        fi\n        \n        if [ -n \"$target\" ]; then\n            # Assume target is both the hostname and address, with port 80\n            update_one \"$target\" \"$target\" \"80\" \"$firmware_file\"\n        elif [ \"$discover\" = true ]; then\n            read -ra devices <<< \"$(discover_devices)\"\n            for ((i=0; i<${#devices[@]}; i+=3)); do\n                hostname=\"${devices[$i]}\"\n                address=\"${devices[$i+1]}\"\n                port=\"${devices[$i+2]}\"\n                update_one \"$hostname\" \"$address\" \"$port\" \"$firmware_file\"\n            done\n        else\n            log \"ERROR\" \"$RED\" \"No target specified. Use --target or --discover.\"\n            exit 1\n        fi\n        ;;\n    *)\n        show_help\n        exit 1\n        ;;\nesac\n"
  },
  {
    "path": "usermods/ADS1115_v2/ADS1115_v2.cpp",
    "content": "#include \"wled.h\"\n#include <Adafruit_ADS1X15.h>\n#include <math.h>\n\n#include \"ChannelSettings.h\"\n\nusing namespace ADS1115;\n\nclass ADS1115Usermod : public Usermod {\n  public:\n    void setup() {\n      ads.setGain(GAIN_ONE);        // 1x gain   +/- 4.096V\n\n      if (!ads.begin()) {\n        Serial.println(\"Failed to initialize ADS\");\n        return;\n      }\n\n      if (!initChannel()) {\n        isInitialized = true;\n        return;\n      }\n\n      startReading();\n\n      isEnabled = true;\n      isInitialized = true;\n    }\n\n    void loop() {\n      if (isEnabled && millis() - lastTime > loopInterval) {\n        lastTime = millis();\n\n        // If we don't have new data, skip this iteration.\n        if (!ads.conversionComplete()) {\n          return;\n        }\n\n        updateResult();\n        moveToNextChannel();\n        startReading();\n      }\n    }\n\n    void addToJsonInfo(JsonObject& root)\n    {\n      if (!isEnabled) {\n        return;\n      }\n\n      JsonObject user = root[F(\"u\")];\n      if (user.isNull()) user = root.createNestedObject(F(\"u\"));\n\n      for (uint8_t i = 0; i < channelsCount; i++) {\n        ChannelSettings* settingsPtr = &(channelSettings[i]);\n\n        if (!settingsPtr->isEnabled) {\n          continue;\n        }\n\n        JsonArray lightArr = user.createNestedArray(settingsPtr->name); //name\n        float value = round((readings[i] + settingsPtr->offset) * settingsPtr->multiplier, settingsPtr->decimals);\n        lightArr.add(value); //value\n        lightArr.add(\" \" + settingsPtr->units); //unit\n      }\n    }\n\n    void addToConfig(JsonObject& root)\n    {\n      JsonObject top = root.createNestedObject(F(\"ADC ADS1115\"));\n      \n      for (uint8_t i = 0; i < channelsCount; i++) {\n        ChannelSettings* settingsPtr = &(channelSettings[i]);\n        JsonObject channel = top.createNestedObject(settingsPtr->settingName);\n        channel[F(\"Enabled\")] = settingsPtr->isEnabled;\n        channel[F(\"Name\")] = settingsPtr->name;\n        channel[F(\"Units\")] = settingsPtr->units;\n        channel[F(\"Multiplier\")] = settingsPtr->multiplier;\n        channel[F(\"Offset\")] = settingsPtr->offset;\n        channel[F(\"Decimals\")] = settingsPtr->decimals;\n      }\n\n      top[F(\"Loop Interval\")] = loopInterval;\n    }\n\n    bool readFromConfig(JsonObject& root)\n    {\n      JsonObject top = root[F(\"ADC ADS1115\")];\n\n      bool configComplete = !top.isNull();\n      bool hasEnabledChannels = false;\n\n      for (uint8_t i = 0; i < channelsCount && configComplete; i++) {\n        ChannelSettings* settingsPtr = &(channelSettings[i]);\n        JsonObject channel = top[settingsPtr->settingName];\n\n        configComplete &= !channel.isNull();\n\n        configComplete &= getJsonValue(channel[F(\"Enabled\")], settingsPtr->isEnabled);\n        configComplete &= getJsonValue(channel[F(\"Name\")], settingsPtr->name);\n        configComplete &= getJsonValue(channel[F(\"Units\")], settingsPtr->units);\n        configComplete &= getJsonValue(channel[F(\"Multiplier\")], settingsPtr->multiplier);\n        configComplete &= getJsonValue(channel[F(\"Offset\")], settingsPtr->offset);\n        configComplete &= getJsonValue(channel[F(\"Decimals\")], settingsPtr->decimals);\n\n        hasEnabledChannels |= settingsPtr->isEnabled;\n      }\n\n      configComplete &= getJsonValue(top[F(\"Loop Interval\")], loopInterval);\n\n      isEnabled = isInitialized && configComplete && hasEnabledChannels;\n\n      return configComplete;\n    }\n\n    uint16_t getId()\n    {\n      return USERMOD_ID_ADS1115;\n    }\n\n  private:\n    static const uint8_t channelsCount = 8;\n\n    ChannelSettings channelSettings[channelsCount] = {\n      {\n        \"Differential reading from AIN0 (P) and AIN1 (N)\",\n        false,\n        \"Differential AIN0 AIN1\",\n        \"V\",\n        ADS1X15_REG_CONFIG_MUX_DIFF_0_1,\n        1,\n        0,\n        3\n      },\n      {\n        \"Differential reading from AIN0 (P) and AIN3 (N)\",\n        false,\n        \"Differential AIN0 AIN3\",\n        \"V\",\n        ADS1X15_REG_CONFIG_MUX_DIFF_0_3,\n        1,\n        0,\n        3\n      },\n      {\n        \"Differential reading from AIN1 (P) and AIN3 (N)\",\n        false,\n        \"Differential AIN1 AIN3\",\n        \"V\",\n        ADS1X15_REG_CONFIG_MUX_DIFF_1_3,\n        1,\n        0,\n        3\n      },\n      {\n        \"Differential reading from AIN2 (P) and AIN3 (N)\",\n        false,\n        \"Differential AIN2 AIN3\",\n        \"V\",\n        ADS1X15_REG_CONFIG_MUX_DIFF_2_3,\n        1,\n        0,\n        3\n      },\n      {\n        \"Single-ended reading from AIN0\",\n        false,\n        \"Single-ended AIN0\",\n        \"V\",\n        ADS1X15_REG_CONFIG_MUX_SINGLE_0,\n        1,\n        0,\n        3\n      },\n      {\n        \"Single-ended reading from AIN1\",\n        false,\n        \"Single-ended AIN1\",\n        \"V\",\n        ADS1X15_REG_CONFIG_MUX_SINGLE_1,\n        1,\n        0,\n        3\n      },\n      {\n        \"Single-ended reading from AIN2\",\n        false,\n        \"Single-ended AIN2\",\n        \"V\",\n        ADS1X15_REG_CONFIG_MUX_SINGLE_2,\n        1,\n        0,\n        3\n      },\n      {\n        \"Single-ended reading from AIN3\",\n        false,\n        \"Single-ended AIN3\",\n        \"V\",\n        ADS1X15_REG_CONFIG_MUX_SINGLE_3,\n        1,\n        0,\n        3\n      },\n    };\n    float readings[channelsCount] = {0, 0, 0, 0, 0, 0, 0, 0};\n\n    unsigned long loopInterval = 1000;\n    unsigned long lastTime = 0;\n\n    Adafruit_ADS1115 ads;\n    uint8_t activeChannel;\n\n    bool isEnabled = false;\n    bool isInitialized = false;\n\n    static float round(float value, uint8_t decimals) {\n      return roundf(value * powf(10, decimals)) / powf(10, decimals);\n    }\n\n    bool initChannel() {\n      for (uint8_t i = 0; i < channelsCount; i++) {\n        if (channelSettings[i].isEnabled) {\n          activeChannel = i;\n          return true;\n        }\n      }\n\n      activeChannel = 0;\n      return false;\n    }\n\n    void moveToNextChannel() {\n      uint8_t oldActiveChannel = activeChannel;\n\n      do\n      {\n        if (++activeChannel >= channelsCount){\n          activeChannel = 0;\n        }\n      }\n      while (!channelSettings[activeChannel].isEnabled && oldActiveChannel != activeChannel);\n    }\n\n    void startReading() {\n      ads.startADCReading(channelSettings[activeChannel].mux, /*continuous=*/false);\n    }\n\n    void updateResult() {\n        int16_t results = ads.getLastConversionResults();\n        readings[activeChannel] = ads.computeVolts(results);\n    }\n};\n\nstatic ADS1115Usermod ads1115_v2;\nREGISTER_USERMOD(ads1115_v2);"
  },
  {
    "path": "usermods/ADS1115_v2/ChannelSettings.h",
    "content": "#include \"wled.h\"\n\nnamespace ADS1115\n{\n  struct ChannelSettings {\n    const String settingName;\n    bool isEnabled;\n    String name;\n    String units;\n    const uint16_t mux;\n    float multiplier;\n    float offset;\n    uint8_t decimals;\n  };\n}"
  },
  {
    "path": "usermods/ADS1115_v2/library.json",
    "content": "{\n  \"name\": \"ADS1115_v2\",\n  \"build\": { \"libArchive\": false },\n  \"dependencies\": {\n    \"Adafruit BusIO\": \"https://github.com/adafruit/Adafruit_BusIO#1.13.2\",\n    \"Adafruit ADS1X15\": \"https://github.com/adafruit/Adafruit_ADS1X15#2.4.0\"\n  }\n}\n"
  },
  {
    "path": "usermods/ADS1115_v2/readme.md",
    "content": "# ADS1115 16-Bit ADC with four inputs\n\nThis usermod will read from an ADS1115 ADC. The voltages are displayed in the Info section of the web UI.\n\nConfiguration is performed via the Usermod menu. There are no parameters to set in code!\n\n## Installation \n\nAdd 'ADS1115' to `custom_usermods` in your platformio environment.\n\n"
  },
  {
    "path": "usermods/AHT10_v2/AHT10_v2.cpp",
    "content": "#include \"wled.h\"\n#include <AHT10.h>\n\n#define AHT10_SUCCESS 1\n\nclass UsermodAHT10 : public Usermod\n{\nprivate:\n  static const char _name[];\n\n  unsigned long _lastLoopCheck = 0;\n\n  bool _settingEnabled : 1;    // Enable the usermod\n  bool _mqttPublish : 1;       // Publish mqtt values\n  bool _mqttPublishAlways : 1; // Publish always, regardless if there is a change\n  bool _mqttHomeAssistant : 1; // Enable Home Assistant docs\n  bool _initDone : 1;          // Initialization is done\n\n  // Settings. Some of these are stored in a different format than they're user settings - so we don't have to convert at runtime\n  uint8_t _i2cAddress = AHT10_ADDRESS_0X38;\n  ASAIR_I2C_SENSOR _ahtType = AHT10_SENSOR;\n  uint16_t _checkInterval = 60000; // milliseconds, user settings is in seconds\n  float _decimalFactor = 100;      // a power of 10 factor. 1 would be no change, 10 is one decimal, 100 is two etc. User sees a power of 10 (0, 1, 2, ..)\n\n  uint8_t _lastStatus = 0;\n  float _lastHumidity = 0;\n  float _lastTemperature = 0;\n\n#ifndef WLED_MQTT_DISABLE\n  float _lastHumiditySent = 0;\n  float _lastTemperatureSent = 0;\n#endif\n\n  AHT10 *_aht = nullptr;\n\n  float truncateDecimals(float val)\n  {\n    return roundf(val * _decimalFactor) / _decimalFactor;\n  }\n\n  void initializeAht()\n  {\n    if (_aht != nullptr)\n    {\n      delete _aht;\n    }\n\n    _aht = new AHT10(_i2cAddress, _ahtType);\n\n    _lastStatus = 0;\n    _lastHumidity = 0;\n    _lastTemperature = 0;\n  }\n\n#ifndef WLED_DISABLE_MQTT\n  void mqttInitialize()\n  {\n    // This is a generic \"setup mqtt\" function, So we must abort if we're not to do mqtt\n    if (!WLED_MQTT_CONNECTED || !_mqttPublish || !_mqttHomeAssistant)\n      return;\n\n    char topic[128];\n    snprintf_P(topic, 127, \"%s/temperature\", mqttDeviceTopic);\n    mqttCreateHassSensor(F(\"Temperature\"), topic, F(\"temperature\"), F(\"°C\"));\n\n    snprintf_P(topic, 127, \"%s/humidity\", mqttDeviceTopic);\n    mqttCreateHassSensor(F(\"Humidity\"), topic, F(\"humidity\"), F(\"%\"));\n  }\n\n  void mqttPublishIfChanged(const __FlashStringHelper *topic, float &lastState, float state, float minChange)\n  {\n    // Check if MQTT Connected, otherwise it will crash the 8266\n    // Only report if the change is larger than the required diff\n    if (WLED_MQTT_CONNECTED && _mqttPublish && (_mqttPublishAlways || fabsf(lastState - state) > minChange))\n    {\n      char subuf[128];\n      snprintf_P(subuf, 127, PSTR(\"%s/%s\"), mqttDeviceTopic, (const char *)topic);\n      mqtt->publish(subuf, 0, false, String(state).c_str());\n\n      lastState = state;\n    }\n  }\n\n  // Create an MQTT Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop.\n  void mqttCreateHassSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement)\n  {\n    String t = String(F(\"homeassistant/sensor/\")) + mqttClientID + \"/\" + name + F(\"/config\");\n\n    StaticJsonDocument<600> doc;\n\n    doc[F(\"name\")] = name;\n    doc[F(\"state_topic\")] = topic;\n    doc[F(\"unique_id\")] = String(mqttClientID) + name;\n    if (unitOfMeasurement != \"\")\n      doc[F(\"unit_of_measurement\")] = unitOfMeasurement;\n    if (deviceClass != \"\")\n      doc[F(\"device_class\")] = deviceClass;\n    doc[F(\"expire_after\")] = 1800;\n\n    JsonObject device = doc.createNestedObject(F(\"device\")); // attach the sensor to the same device\n    device[F(\"name\")] = serverDescription;\n    device[F(\"identifiers\")] = \"wled-sensor-\" + String(mqttClientID);\n    device[F(\"manufacturer\")] = F(WLED_BRAND);\n    device[F(\"model\")] = F(WLED_PRODUCT_NAME);\n    device[F(\"sw_version\")] = versionString;\n\n    String temp;\n    serializeJson(doc, temp);\n    DEBUG_PRINTLN(t);\n    DEBUG_PRINTLN(temp);\n\n    mqtt->publish(t.c_str(), 0, true, temp.c_str());\n  }\n#endif\n\npublic:\n  void setup()\n  {\n    initializeAht();\n  }\n\n  void loop()\n  {\n    // if usermod is disabled or called during strip updating just exit\n    // NOTE: on very long strips strip.isUpdating() may always return true so update accordingly\n    if (!_settingEnabled || strip.isUpdating())\n      return;\n\n    // do your magic here\n    unsigned long currentTime = millis();\n\n    if (currentTime - _lastLoopCheck < _checkInterval)\n      return;\n    _lastLoopCheck = currentTime;\n\n    _lastStatus = _aht->readRawData();\n\n    if (_lastStatus == AHT10_ERROR)\n    {\n      // Perform softReset and retry\n      DEBUG_PRINTLN(F(\"AHTxx returned error, doing softReset\"));\n      if (!_aht->softReset())\n      {\n        DEBUG_PRINTLN(F(\"softReset failed\"));\n        return;\n      }\n\n      _lastStatus = _aht->readRawData();\n    }\n\n    if (_lastStatus == AHT10_SUCCESS)\n    {\n      float temperature = truncateDecimals(_aht->readTemperature(AHT10_USE_READ_DATA));\n      float humidity = truncateDecimals(_aht->readHumidity(AHT10_USE_READ_DATA));\n\n#ifndef WLED_DISABLE_MQTT\n      // Push to MQTT\n\n      // We can avoid reporting if the change is insignificant. The threshold chosen is below the level of accuracy, but way above 0.01 which is the precision of the value provided.\n      // The AHT10/15/20 has an accuracy of 0.3C in the temperature readings\n      mqttPublishIfChanged(F(\"temperature\"), _lastTemperatureSent, temperature, 0.1f);\n\n      // The AHT10/15/20 has an accuracy in the humidity sensor of 2%\n      mqttPublishIfChanged(F(\"humidity\"), _lastHumiditySent, humidity, 0.5f);\n#endif\n\n      // Store\n      _lastTemperature = temperature;\n      _lastHumidity = humidity;\n    }\n  }\n\n#ifndef WLED_DISABLE_MQTT\n  void onMqttConnect(bool sessionPresent)\n  {\n    mqttInitialize();\n  }\n#endif\n\n  uint16_t getId()\n  {\n    return USERMOD_ID_AHT10;\n  }\n\n  void addToJsonInfo(JsonObject &root) override\n  {\n    // if \"u\" object does not exist yet wee need to create it\n    JsonObject user = root[\"u\"];\n    if (user.isNull())\n      user = root.createNestedObject(\"u\");\n\n#ifdef USERMOD_AHT10_DEBUG\n    JsonArray temp = user.createNestedArray(F(\"AHT last loop\"));\n    temp.add(_lastLoopCheck);\n\n    temp = user.createNestedArray(F(\"AHT last status\"));\n    temp.add(_lastStatus);\n#endif\n\n    JsonArray jsonTemp = user.createNestedArray(F(\"Temperature\"));\n    JsonArray jsonHumidity = user.createNestedArray(F(\"Humidity\"));\n\n    if (_lastLoopCheck == 0)\n    {\n      // Before first run\n      jsonTemp.add(F(\"Not read yet\"));\n      jsonHumidity.add(F(\"Not read yet\"));\n      return;\n    }\n\n    if (_lastStatus != AHT10_SUCCESS)\n    {\n      jsonTemp.add(F(\"An error occurred\"));\n      jsonHumidity.add(F(\"An error occurred\"));\n      return;\n    }\n\n    jsonTemp.add(_lastTemperature);\n    jsonTemp.add(F(\"°C\"));\n\n    jsonHumidity.add(_lastHumidity);\n    jsonHumidity.add(F(\"%\"));\n  }\n\n  void addToConfig(JsonObject &root)\n  {\n    JsonObject top = root.createNestedObject(FPSTR(_name));\n    top[F(\"Enabled\")] = _settingEnabled;\n    top[F(\"I2CAddress\")] = static_cast<uint8_t>(_i2cAddress);\n    top[F(\"SensorType\")] = _ahtType;\n    top[F(\"CheckInterval\")] = _checkInterval / 1000;\n    top[F(\"Decimals\")] = log10f(_decimalFactor);\n#ifndef WLED_DISABLE_MQTT\n    top[F(\"MqttPublish\")] = _mqttPublish;\n    top[F(\"MqttPublishAlways\")] = _mqttPublishAlways;\n    top[F(\"MqttHomeAssistantDiscovery\")] = _mqttHomeAssistant;\n#endif\n\n    DEBUG_PRINTLN(F(\"AHT10 config saved.\"));\n  }\n\n  bool readFromConfig(JsonObject &root) override\n  {\n    // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor\n    // 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)\n\n    JsonObject top = root[FPSTR(_name)];\n\n    bool configComplete = !top.isNull();\n    if (!configComplete)\n      return false;\n\n    bool tmpBool = false;\n    configComplete &= getJsonValue(top[F(\"Enabled\")], tmpBool);\n    if (configComplete)\n      _settingEnabled = tmpBool;\n\n    configComplete &= getJsonValue(top[F(\"I2CAddress\")], _i2cAddress);\n    configComplete &= getJsonValue(top[F(\"CheckInterval\")], _checkInterval);\n    if (configComplete)\n    {\n      if (1 <= _checkInterval && _checkInterval <= 600)\n        _checkInterval *= 1000;\n      else\n        // Invalid input\n        _checkInterval = 60000;\n    }\n\n    configComplete &= getJsonValue(top[F(\"Decimals\")], _decimalFactor);\n    if (configComplete)\n    {\n      if (0 <= _decimalFactor && _decimalFactor <= 5)\n        _decimalFactor = pow10f(_decimalFactor);\n      else\n        // Invalid input\n        _decimalFactor = 100;\n    }\n\n    uint8_t tmpAhtType;\n    configComplete &= getJsonValue(top[F(\"SensorType\")], tmpAhtType);\n    if (configComplete)\n    {\n      if (0 <= tmpAhtType && tmpAhtType <= 2)\n        _ahtType = static_cast<ASAIR_I2C_SENSOR>(tmpAhtType);\n      else\n        // Invalid input\n        _ahtType = ASAIR_I2C_SENSOR::AHT10_SENSOR;\n    }\n\n#ifndef WLED_DISABLE_MQTT\n    configComplete &= getJsonValue(top[F(\"MqttPublish\")], tmpBool);\n    if (configComplete)\n      _mqttPublish = tmpBool;\n\n    configComplete &= getJsonValue(top[F(\"MqttPublishAlways\")], tmpBool);\n    if (configComplete)\n      _mqttPublishAlways = tmpBool;\n\n    configComplete &= getJsonValue(top[F(\"MqttHomeAssistantDiscovery\")], tmpBool);\n    if (configComplete)\n      _mqttHomeAssistant = tmpBool;\n#endif\n\n    if (_initDone)\n    {\n      // Reloading config\n      initializeAht();\n\n#ifndef WLED_DISABLE_MQTT\n      mqttInitialize();\n#endif\n    }\n\n    _initDone = true;\n    return configComplete;\n  }\n\n  ~UsermodAHT10()\n  {\n    delete _aht;\n    _aht = nullptr;\n  }\n};\n\nconst char UsermodAHT10::_name[] PROGMEM = \"AHTxx\";\n\nstatic UsermodAHT10 aht10_v2;\nREGISTER_USERMOD(aht10_v2);"
  },
  {
    "path": "usermods/AHT10_v2/README.md",
    "content": "# Usermod AHT10\nThis Usermod is designed to read a `AHT10`, `AHT15` or `AHT20` sensor and output the following:\n- Temperature\n- Humidity\n\nConfiguration is performed via the Usermod menu. The following settings can be configured in the Usermod Menu:\n- I2CAddress: The i2c address in decimal. Set it to either 56 (0x38, the default) or 57 (0x39).\n- SensorType, one of:\n  - 0 - AHT10\n  - 1 - AHT15\n  - 2 - AHT20\n- CheckInterval: Number of seconds between readings\n- Decimals: Number of decimals to put in the output\n\nDependencies, These must be added under `lib_deps` in your `platform.ini` (or `platform_override.ini`).\n- Libraries\n  - `enjoyneering/AHT10@~1.1.0` (by [enjoyneering](https://registry.platformio.org/libraries/enjoyneering/AHT10))\n  - `Wire`\n\n## Author\n[@LordMike](https://github.com/LordMike)\n\n# Compiling\n\nTo enable, add 'AHT10' to `custom_usermods` in your platformio encrionment  (e.g. in `platformio_override.ini`)\n```ini\n[env:aht10_example]\nextends = env:esp32dev\ncustom_usermods = ${env:esp32dev.custom_usermods} AHT10\n```\n"
  },
  {
    "path": "usermods/AHT10_v2/library.json",
    "content": "{\n  \"name\": \"AHT10_v2\",\n  \"build\": { \"libArchive\": false },  \n  \"dependencies\": {\n    \"enjoyneering/AHT10\":\"~1.1.0\"\n  }\n}\n"
  },
  {
    "path": "usermods/Analog_Clock/Analog_Clock.cpp",
    "content": "#include \"wled.h\"\n\n/*\n * Usermod for analog clock\n */\n\nclass AnalogClockUsermod : public Usermod {\nprivate:\n    static constexpr uint32_t refreshRate = 50; // per second\n    static constexpr uint32_t refreshDelay = 1000 / refreshRate;\n\n    struct Segment {\n        // config\n        int16_t firstLed  = 0;\n        int16_t lastLed   = 59;\n        int16_t centerLed = 0;\n\n        // runtime\n        int16_t size;\n\n        Segment() {\n            update();\n        }\n\n        void validateAndUpdate() {\n            if (firstLed < 0 || firstLed >= strip.getLengthTotal() ||\n                    lastLed < firstLed || lastLed >= strip.getLengthTotal()) {\n                *this = {};\n                return;\n            }\n            if (centerLed < firstLed || centerLed > lastLed) {\n                centerLed = firstLed;\n            }\n            update();\n        }\n\n        void update() {\n            size = lastLed - firstLed + 1;\n        }\n    };\n\n    // configuration (available in API and stored in flash)\n    bool     enabled          = false;\n    Segment  mainSegment;\n    bool     hourMarksEnabled = true;\n    uint32_t hourMarkColor    = 0xFF0000;\n    uint32_t hourColor        = 0x0000FF;\n    uint32_t minuteColor      = 0x00FF00;\n    bool     secondsEnabled   = true;\n    Segment  secondsSegment;\n    uint32_t secondColor      = 0xFF0000;\n    bool     blendColors      = true;\n    uint16_t secondsEffect    = 0;\n\n    // runtime\n    bool     initDone         = false;\n    uint32_t lastOverlayDraw  = 0;\n\n    void validateAndUpdate() {\n        mainSegment.validateAndUpdate();\n        secondsSegment.validateAndUpdate();\n        if (secondsEffect < 0 || secondsEffect > 1) {\n            secondsEffect = 0;\n        }\n    }\n\n    int16_t adjustToSegment(double progress, Segment const& segment) {\n        int16_t led = segment.centerLed + progress * segment.size;\n        return led > segment.lastLed\n                ? segment.firstLed + led - segment.lastLed - 1\n                : led;\n    }\n\n    void setPixelColor(uint16_t n, uint32_t c) {\n        if (!blendColors) {\n            strip.setPixelColor(n, c);\n        } else {\n            uint32_t oldC = strip.getPixelColor(n);\n            strip.setPixelColor(n, qadd32(oldC, c));\n        }\n    }\n\n    String colorToHexString(uint32_t c) {\n        char buffer[9];\n        sprintf(buffer, \"%06X\", c);\n        return buffer;\n    }\n\n    bool hexStringToColor(String const& s, uint32_t& c, uint32_t def) {\n        char *ep;\n        unsigned long long r = strtoull(s.c_str(), &ep, 16);\n        if (*ep == 0) {\n            c = r;\n            return true;\n        } else {\n            c = def;\n            return false;\n        }\n    }\n\n    void secondsEffectSineFade(int16_t secondLed, Toki::Time const& time) {\n        uint32_t ms = time.ms % 1000;\n        uint8_t b0 = (cos8_t(ms * 64 / 1000) - 128) * 2;\n        setPixelColor(secondLed, scale32(secondColor, b0));\n        uint8_t b1 = (sin8_t(ms * 64 / 1000) - 128) * 2;\n        setPixelColor(inc(secondLed, 1, secondsSegment), scale32(secondColor, b1));\n    }\n\n    static inline uint32_t qadd32(uint32_t c1, uint32_t c2) {\n        return RGBW32(\n            qadd8(R(c1), R(c2)),\n            qadd8(G(c1), G(c2)),\n            qadd8(B(c1), B(c2)),\n            qadd8(W(c1), W(c2))\n        );\n    }\n\n    static inline uint32_t scale32(uint32_t c, fract8 scale) {\n        return RGBW32(\n            scale8(R(c), scale),\n            scale8(G(c), scale),\n            scale8(B(c), scale),\n            scale8(W(c), scale)\n        );\n    }\n\n    static inline int16_t dec(int16_t n, int16_t i, Segment const& seg) {\n        return n - seg.firstLed >= i\n                ? n - i\n                : seg.lastLed - seg.firstLed - i + n + 1;\n    }\n\n    static inline int16_t inc(int16_t n, int16_t i, Segment const& seg) {\n        int16_t r = n + i;\n        if (r > seg.lastLed) {\n            return seg.firstLed + n - seg.lastLed;\n        }\n        return r;\n    }\n\npublic:\n    AnalogClockUsermod() {\n    }\n\n    void setup() override {\n        initDone = true;\n        validateAndUpdate();\n    }\n\n    void loop() override {\n        if (millis() - lastOverlayDraw > refreshDelay) {\n            strip.trigger();\n        }\n    }\n\n    void handleOverlayDraw() override {\n        if (!enabled) {\n            return;\n        }\n\n        lastOverlayDraw = millis();\n\n        auto time = toki.getTime();\n        double secondP = second(localTime) / 60.0;\n        double minuteP = minute(localTime) / 60.0;\n        double hourP = (hour(localTime) % 12) / 12.0 + minuteP / 12.0;\n\n        if (hourMarksEnabled)         {\n            for (int Led = 0; Led <= 55; Led = Led + 5)\n            {\n                int16_t hourmarkled = adjustToSegment(Led / 60.0, mainSegment);\n                setPixelColor(hourmarkled, hourMarkColor);\n            }\n        }\n\n        if (secondsEnabled) {\n            int16_t secondLed = adjustToSegment(secondP, secondsSegment);\n\n            switch (secondsEffect) {\n                case 0: // no effect\n                    setPixelColor(secondLed, secondColor);\n                    break;\n\n                case 1: // fading seconds\n                    secondsEffectSineFade(secondLed, time);\n                    break;\n            }\n\n            // TODO: move to secondsTrailEffect\n            // for (uint16_t i = 1; i < secondsTrail + 1; ++i) {\n            //     uint16_t trailLed = dec(secondLed, i, secondsSegment);\n            //     uint8_t trailBright = 255 / (secondsTrail + 1) * (secondsTrail - i + 1);\n            //     setPixelColor(trailLed, scale32(secondColor, trailBright));\n            // }\n        }\n\n        setPixelColor(adjustToSegment(minuteP, mainSegment), minuteColor);\n        setPixelColor(adjustToSegment(hourP, mainSegment), hourColor);\n    }\n\n    void addToConfig(JsonObject& root) override {\n        validateAndUpdate();\n\n        JsonObject top = root.createNestedObject(F(\"Analog Clock\"));\n        top[F(\"Overlay Enabled\")]               = enabled;\n        top[F(\"First LED (Main Ring)\")]         = mainSegment.firstLed;\n        top[F(\"Last LED (Main Ring)\")]          = mainSegment.lastLed;\n        top[F(\"Center/12h LED (Main Ring)\")]    = mainSegment.centerLed;\n        top[F(\"Hour Marks Enabled\")]            = hourMarksEnabled;\n        top[F(\"Hour Mark Color (RRGGBB)\")]      = colorToHexString(hourMarkColor);\n        top[F(\"Hour Color (RRGGBB)\")]           = colorToHexString(hourColor);\n        top[F(\"Minute Color (RRGGBB)\")]         = colorToHexString(minuteColor);\n        top[F(\"Show Seconds\")]                  = secondsEnabled;\n        top[F(\"First LED (Seconds Ring)\")]      = secondsSegment.firstLed;\n        top[F(\"Last LED (Seconds Ring)\")]       = secondsSegment.lastLed;\n        top[F(\"Center/12h LED (Seconds Ring)\")] = secondsSegment.centerLed;\n        top[F(\"Second Color (RRGGBB)\")]         = colorToHexString(secondColor);\n        top[F(\"Seconds Effect (0-1)\")]          = secondsEffect;\n        top[F(\"Blend Colors\")]                  = blendColors;\n    }\n\n    bool readFromConfig(JsonObject& root) override {\n        JsonObject top = root[F(\"Analog Clock\")];\n\n        bool configComplete = !top.isNull();\n\n        String color;\n        configComplete &= getJsonValue(top[F(\"Overlay Enabled\")], enabled, false);\n        configComplete &= getJsonValue(top[F(\"First LED (Main Ring)\")], mainSegment.firstLed, 0);\n        configComplete &= getJsonValue(top[F(\"Last LED (Main Ring)\")], mainSegment.lastLed, 59);\n        configComplete &= getJsonValue(top[F(\"Center/12h LED (Main Ring)\")], mainSegment.centerLed, 0);\n        configComplete &= getJsonValue(top[F(\"Hour Marks Enabled\")], hourMarksEnabled, false);\n        configComplete &= getJsonValue(top[F(\"Hour Mark Color (RRGGBB)\")], color, F(\"161616\")) && hexStringToColor(color, hourMarkColor, 0x161616);\n        configComplete &= getJsonValue(top[F(\"Hour Color (RRGGBB)\")], color, F(\"0000FF\")) && hexStringToColor(color, hourColor, 0x0000FF);\n        configComplete &= getJsonValue(top[F(\"Minute Color (RRGGBB)\")], color, F(\"00FF00\")) && hexStringToColor(color, minuteColor, 0x00FF00);\n        configComplete &= getJsonValue(top[F(\"Show Seconds\")], secondsEnabled, true);\n        configComplete &= getJsonValue(top[F(\"First LED (Seconds Ring)\")], secondsSegment.firstLed, 0);\n        configComplete &= getJsonValue(top[F(\"Last LED (Seconds Ring)\")], secondsSegment.lastLed, 59);\n        configComplete &= getJsonValue(top[F(\"Center/12h LED (Seconds Ring)\")], secondsSegment.centerLed, 0);\n        configComplete &= getJsonValue(top[F(\"Second Color (RRGGBB)\")], color, F(\"FF0000\")) && hexStringToColor(color, secondColor, 0xFF0000);\n        configComplete &= getJsonValue(top[F(\"Seconds Effect (0-1)\")], secondsEffect, 0);\n        configComplete &= getJsonValue(top[F(\"Blend Colors\")], blendColors, true);\n\n        if (initDone) {\n            validateAndUpdate();\n        }\n\n        return configComplete;\n    }\n\n    uint16_t getId() override {\n        return USERMOD_ID_ANALOG_CLOCK;\n    }\n};\n\n\nstatic AnalogClockUsermod analog_clock;\nREGISTER_USERMOD(analog_clock);"
  },
  {
    "path": "usermods/Analog_Clock/library.json",
    "content": "{\n  \"name\": \"Analog_Clock\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/Animated_Staircase/Animated_Staircase.cpp",
    "content": "/*\n * Usermod for detecting people entering/leaving a staircase and switching the\n * staircase on/off.\n *\n * Edit the Animated_Staircase_config.h file to compile this usermod for your\n * specific configuration.\n * \n * See the accompanying README.md file for more info.\n */\n#include \"wled.h\"\n\nclass Animated_Staircase : public Usermod {\n  private:\n\n    /* configuration (available in API and stored in flash) */\n    bool enabled = false;                   // Enable this usermod\n    unsigned long segment_delay_ms = 150;   // Time between switching each segment\n    unsigned long on_time_ms       = 30000; // The time for the light to stay on\n    int8_t topPIRorTriggerPin      = -1;    // disabled\n    int8_t bottomPIRorTriggerPin   = -1;    // disabled\n    int8_t topEchoPin              = -1;    // disabled\n    int8_t bottomEchoPin           = -1;    // disabled\n    bool useUSSensorTop            = false; // using PIR or UltraSound sensor?\n    bool useUSSensorBottom         = false; // using PIR or UltraSound sensor?\n    unsigned int topMaxDist        = 50;    // default maximum measured distance in cm, top\n    unsigned int bottomMaxDist     = 50;    // default maximum measured distance in cm, bottom\n    bool togglePower               = false; // toggle power on/off with staircase on/off\n\n    /* runtime variables */\n    bool initDone = false;\n\n    // Time between checking of the sensors\n    const unsigned int scanDelay = 100;\n\n    // Lights on or off.\n    // Flipping this will start a transition.\n    bool on = false;\n\n    // Swipe direction for current transition\n  #define SWIPE_UP true\n  #define SWIPE_DOWN false\n    bool swipe = SWIPE_UP;\n\n    // Indicates which Sensor was seen last (to determine\n    // the direction when swiping off)\n  #define LOWER false\n  #define UPPER true\n    bool lastSensor = LOWER;\n\n    // Time of the last transition action\n    unsigned long lastTime = 0;\n\n    // Time of the last sensor check\n    unsigned long lastScanTime = 0;\n\n    // Last time the lights were switched on or off\n    unsigned long lastSwitchTime = 0;\n\n    // segment id between onIndex and offIndex are on.\n    // controll the swipe by setting/moving these indices around.\n    // onIndex must be less than or equal to offIndex\n    byte onIndex = 0;\n    byte offIndex = 0;\n\n    // The maximum number of configured segments.\n    // Dynamically updated based on user configuration.\n    byte maxSegmentId = 1;\n    byte minSegmentId = 0;\n\n    // These values are used by the API to read the\n    // last sensor state, or trigger a sensor\n    // through the API\n    bool topSensorRead     = false;\n    bool topSensorWrite    = false;\n    bool bottomSensorRead  = false;\n    bool bottomSensorWrite = false;\n    bool topSensorState    = false;\n    bool bottomSensorState = false;\n\n    // strings to reduce flash memory usage (used more than twice)\n    static const char _name[];\n    static const char _enabled[];\n    static const char _segmentDelay[];\n    static const char _onTime[];\n    static const char _useTopUltrasoundSensor[];\n    static const char _topPIRorTrigger_pin[];\n    static const char _topEcho_pin[];\n    static const char _useBottomUltrasoundSensor[];\n    static const char _bottomPIRorTrigger_pin[];\n    static const char _bottomEcho_pin[];\n    static const char _topEchoCm[];\n    static const char _bottomEchoCm[];\n    static const char _togglePower[];\n\n    void publishMqtt(bool bottom, const char* state) {\n#ifndef WLED_DISABLE_MQTT\n      //Check if MQTT Connected, otherwise it will crash the 8266\n      if (WLED_MQTT_CONNECTED){\n        char subuf[64];\n        sprintf_P(subuf, PSTR(\"%s/motion/%d\"), mqttDeviceTopic, (int)bottom);\n        mqtt->publish(subuf, 0, false, state);\n      }\n#endif\n    }\n\n    void updateSegments() {\n      for (int i = minSegmentId; i < maxSegmentId; i++) {\n        Segment &seg = strip.getSegment(i);\n        if (!seg.isActive()) continue; // skip gaps\n        if (i >= onIndex && i < offIndex) {\n          seg.setOption(SEG_OPTION_ON, true);\n          // We may need to copy mode and colors from segment 0 to make sure\n          // changes are propagated even when the config is changed during a wipe\n          // seg.setMode(mainsegment.mode);\n          // seg.setColor(0, mainsegment.colors[0]);\n        } else {\n          seg.setOption(SEG_OPTION_ON, false);\n        }\n        // Always mark segments as \"transitional\", we are animating the staircase\n        //seg.setOption(SEG_OPTION_TRANSITIONAL, true); // not needed anymore as setOption() does it\n      }\n      strip.trigger();  // force strip refresh\n      stateChanged = true;  // inform external devices/UI of change\n      colorUpdated(CALL_MODE_DIRECT_CHANGE);\n    }\n\n    /*\n    * Detects if an object is within ultrasound range.\n    * signalPin: The pin where the pulse is sent\n    * echoPin:   The pin where the echo is received\n    * maxTimeUs: Detection timeout in microseconds. If an echo is\n    *            received within this time, an object is detected\n    *            and the function will return true.\n    *\n    * The speed of sound is 343 meters per second at 20 degrees Celsius.\n    * Since the sound has to travel back and forth, the detection\n    * distance for the sensor in cm is (0.0343 * maxTimeUs) / 2.\n    *\n    * For practical reasons, here are some useful distances:\n    *\n    * Distance =\tmaxtime\n    *     5 cm =  292 uS\n    *    10 cm =  583 uS\n    *    20 cm = 1166 uS\n    *    30 cm = 1749 uS\n    *    50 cm = 2915 uS\n    *   100 cm = 5831 uS\n    */\n    bool ultrasoundRead(int8_t signalPin, int8_t echoPin, unsigned int maxTimeUs) {\n      if (signalPin<0 || echoPin<0) return false;\n      digitalWrite(signalPin, LOW);\n      delayMicroseconds(2);\n      digitalWrite(signalPin, HIGH);\n      delayMicroseconds(10);\n      digitalWrite(signalPin, LOW);\n      return pulseIn(echoPin, HIGH, maxTimeUs) > 0;\n    }\n\n    bool checkSensors() {\n      bool sensorChanged = false;\n\n      if ((millis() - lastScanTime) > scanDelay) {\n        lastScanTime = millis();\n\n        bottomSensorRead = bottomSensorWrite ||\n          (!useUSSensorBottom ?\n            (bottomPIRorTriggerPin<0 ? false : digitalRead(bottomPIRorTriggerPin)) :\n            ultrasoundRead(bottomPIRorTriggerPin, bottomEchoPin, bottomMaxDist*59)  // cm to us\n          );\n        topSensorRead = topSensorWrite ||\n          (!useUSSensorTop ?\n            (topPIRorTriggerPin<0 ? false : digitalRead(topPIRorTriggerPin)) :\n            ultrasoundRead(topPIRorTriggerPin, topEchoPin, topMaxDist*59)   // cm to us\n          );\n\n        if (bottomSensorRead != bottomSensorState) {\n          bottomSensorState = bottomSensorRead; // change previous state\n          sensorChanged = true;\n          publishMqtt(true, bottomSensorState ? \"on\" : \"off\");\n          DEBUG_PRINTLN(F(\"Bottom sensor changed.\"));\n        }\n\n        if (topSensorRead != topSensorState) {\n          topSensorState = topSensorRead; // change previous state\n          sensorChanged = true;\n          publishMqtt(false, topSensorState ? \"on\" : \"off\");\n          DEBUG_PRINTLN(F(\"Top sensor changed.\"));\n        }\n\n        // Values read, reset the flags for next API call\n        topSensorWrite = false;\n        bottomSensorWrite = false;\n\n        if (topSensorRead != bottomSensorRead) {\n          lastSwitchTime = millis();\n\n          if (on) {\n            lastSensor = topSensorRead;\n          } else {\n            if (togglePower && onIndex == offIndex && offMode) toggleOnOff(); // toggle power on if off\n            // If the bottom sensor triggered, we need to swipe up, ON\n            swipe = bottomSensorRead;\n\n            DEBUG_PRINT(F(\"ON -> Swipe \"));\n            DEBUG_PRINTLN(swipe ? F(\"up.\") : F(\"down.\"));\n\n            if (onIndex == offIndex) {\n              // Position the indices for a correct on-swipe\n              if (swipe == SWIPE_UP) {\n                onIndex = minSegmentId;\n              } else {\n                onIndex = maxSegmentId;\n              }\n              offIndex = onIndex;\n            }\n            on = true;\n          }\n        }\n      }\n      return sensorChanged;\n    }\n\n    void autoPowerOff() {\n      if ((millis() - lastSwitchTime) > on_time_ms) {\n        // if sensors are still on, do nothing\n        if (bottomSensorState || topSensorState) return;\n\n        // Swipe OFF in the direction of the last sensor detection\n        swipe = lastSensor;\n        on = false;\n\n        DEBUG_PRINT(F(\"OFF -> Swipe \"));\n        DEBUG_PRINTLN(swipe ? F(\"up.\") : F(\"down.\"));\n      }\n    }\n\n    void updateSwipe() {\n      if ((millis() - lastTime) > segment_delay_ms) {\n        lastTime = millis();\n\n        byte oldOn  = onIndex;\n        byte oldOff = offIndex;\n        if (on) {\n          // Turn on all segments\n          onIndex  = MAX(minSegmentId, onIndex - 1);\n          offIndex = MIN(maxSegmentId, offIndex + 1);\n        } else {\n          if (swipe == SWIPE_UP) {\n            onIndex = MIN(offIndex, onIndex + 1);\n          } else {\n            offIndex = MAX(onIndex, offIndex - 1);\n          }\n        }\n        if (oldOn != onIndex || oldOff != offIndex) {\n          updateSegments(); // reduce the number of updates to necessary ones\n          if (togglePower && onIndex == offIndex && !offMode && !on) toggleOnOff();  // toggle power off for all segments off\n        }\n      }\n    }\n\n    // send sensor values to JSON API\n    void writeSensorsToJson(JsonObject& staircase) {\n      staircase[F(\"top-sensor\")]    = topSensorRead;\n      staircase[F(\"bottom-sensor\")] = bottomSensorRead;\n    }\n\n    // allow overrides from JSON API\n    void readSensorsFromJson(JsonObject& staircase) {\n      bottomSensorWrite = bottomSensorState || (staircase[F(\"bottom-sensor\")].as<bool>());\n      topSensorWrite    = topSensorState    || (staircase[F(\"top-sensor\")].as<bool>());\n    }\n\n    void enable(bool enable) {\n      if (enable) {\n        DEBUG_PRINTLN(F(\"Animated Staircase enabled.\"));\n        DEBUG_PRINT(F(\"Delay between steps: \"));\n        DEBUG_PRINT(segment_delay_ms);\n        DEBUG_PRINT(F(\" milliseconds.\\nStairs switch off after: \"));\n        DEBUG_PRINT(on_time_ms / 1000);\n        DEBUG_PRINTLN(F(\" seconds.\"));\n\n        if (!useUSSensorBottom)\n          pinMode(bottomPIRorTriggerPin, INPUT_PULLUP);\n        else {\n          pinMode(bottomPIRorTriggerPin, OUTPUT);\n          pinMode(bottomEchoPin, INPUT);\n        }\n\n        if (!useUSSensorTop)\n          pinMode(topPIRorTriggerPin, INPUT_PULLUP);\n        else {\n          pinMode(topPIRorTriggerPin, OUTPUT);\n          pinMode(topEchoPin, INPUT);\n        }\n        onIndex  = minSegmentId = strip.getMainSegmentId(); // it may not be the best idea to start with main segment as it may not be the first one\n        offIndex = maxSegmentId = strip.getLastActiveSegmentId() + 1;\n\n        // shorten the strip transition time to be equal or shorter than segment delay\n        transitionDelay = segment_delay_ms;\n        strip.setTransition(segment_delay_ms);\n        strip.trigger();\n      } else {\n        if (togglePower && !on && offMode) toggleOnOff(); // toggle power on if off\n        // Restore segment options\n        for (int i = 0; i <= strip.getLastActiveSegmentId(); i++) {\n          Segment &seg = strip.getSegment(i);\n          if (!seg.isActive()) continue; // skip vector gaps\n          seg.setOption(SEG_OPTION_ON, true);\n        }\n        strip.trigger();  // force strip update\n        stateChanged = true;  // inform external devices/UI of change\n        colorUpdated(CALL_MODE_DIRECT_CHANGE);\n        DEBUG_PRINTLN(F(\"Animated Staircase disabled.\"));\n      }\n      enabled = enable;\n    }\n\n  public:\n    void setup() {\n      // standardize invalid pin numbers to -1\n      if (topPIRorTriggerPin    < 0) topPIRorTriggerPin    = -1;\n      if (topEchoPin            < 0) topEchoPin            = -1;\n      if (bottomPIRorTriggerPin < 0) bottomPIRorTriggerPin = -1;\n      if (bottomEchoPin         < 0) bottomEchoPin         = -1;\n      // allocate pins\n      PinManagerPinType pins[4] = {\n        { topPIRorTriggerPin, useUSSensorTop },\n        { topEchoPin, false },\n        { bottomPIRorTriggerPin, useUSSensorBottom },\n        { bottomEchoPin, false },\n      };\n      // NOTE: this *WILL* return TRUE if all the pins are set to -1.\n      //       this is *BY DESIGN*.\n      if (!PinManager::allocateMultiplePins(pins, 4, PinOwner::UM_AnimatedStaircase)) {\n        topPIRorTriggerPin = -1;\n        topEchoPin = -1;\n        bottomPIRorTriggerPin = -1;\n        bottomEchoPin = -1;\n        enabled = false;\n      }\n      enable(enabled);\n      initDone = true;\n    }\n\n    void loop() {\n      if (!enabled || strip.isUpdating()) return;\n      minSegmentId = strip.getMainSegmentId();  // it may not be the best idea to start with main segment as it may not be the first one\n      maxSegmentId = strip.getLastActiveSegmentId() + 1;\n      checkSensors();\n      if (on) autoPowerOff();\n      updateSwipe();\n    }\n\n    uint16_t getId() { return USERMOD_ID_ANIMATED_STAIRCASE; }\n\n#ifndef WLED_DISABLE_MQTT\n    /**\n     * handling of MQTT message\n     * topic only contains stripped topic (part after /wled/MAC)\n     * topic should look like: /swipe with amessage of [up|down]\n     */\n    bool onMqttMessage(char* topic, char* payload) {\n      if (strlen(topic) == 6 && strncmp_P(topic, PSTR(\"/swipe\"), 6) == 0) {\n        String action = payload;\n        if (action == \"up\") {\n          bottomSensorWrite = true;\n          return true;\n        } else if (action == \"down\") {\n          topSensorWrite = true;\n          return true;\n        } else if (action == \"on\") {\n          enable(true);\n          return true;\n        } else if (action == \"off\") {\n          enable(false);\n          return true;\n        }\n      }\n      return false;\n    }\n\n    /**\n     * subscribe to MQTT topic for controlling usermod\n     */\n    void onMqttConnect(bool sessionPresent) {\n      //(re)subscribe to required topics\n      char subuf[64];\n      if (mqttDeviceTopic[0] != 0) {\n        strcpy(subuf, mqttDeviceTopic);\n        strcat_P(subuf, PSTR(\"/swipe\"));\n        mqtt->subscribe(subuf, 0);\n      }\n    }\n#endif\n\n    void addToJsonState(JsonObject& root) {\n      JsonObject staircase = root[FPSTR(_name)];\n      if (staircase.isNull()) {\n        staircase = root.createNestedObject(FPSTR(_name));\n      }\n      writeSensorsToJson(staircase);\n      DEBUG_PRINTLN(F(\"Staircase sensor state exposed in API.\"));\n    }\n\n    /*\n    * Reads configuration settings from the json API.\n    * See void addToJsonState(JsonObject& root)\n    */\n    void readFromJsonState(JsonObject& root) {\n      if (!initDone) return;  // prevent crash on boot applyPreset()\n      bool en = enabled;\n      JsonObject staircase = root[FPSTR(_name)];\n      if (!staircase.isNull()) {\n        if (staircase[FPSTR(_enabled)].is<bool>()) {\n          en = staircase[FPSTR(_enabled)].as<bool>();\n        } else {\n          String str = staircase[FPSTR(_enabled)]; // checkbox -> off or on\n          en = (bool)(str!=\"off\"); // off is guaranteed to be present\n        }\n        if (en != enabled) enable(en);\n        readSensorsFromJson(staircase);\n        DEBUG_PRINTLN(F(\"Staircase sensor state read from API.\"));\n      }\n    }\n\n    void appendConfigData() {\n      //oappend(F(\"dd=addDropdown('staircase','selectfield');\"));\n      //oappend(F(\"addOption(dd,'1st value',0);\"));\n      //oappend(F(\"addOption(dd,'2nd value',1);\"));\n      //oappend(F(\"addInfo('staircase:selectfield',1,'additional info');\"));  // 0 is field type, 1 is actual field\n    }\n\n\n    /*\n    * Writes the configuration to internal flash memory.\n    */\n    void addToConfig(JsonObject& root) {\n      JsonObject staircase = root[FPSTR(_name)];\n      if (staircase.isNull()) {\n        staircase = root.createNestedObject(FPSTR(_name));\n      }\n      staircase[FPSTR(_enabled)]                   = enabled;\n      staircase[FPSTR(_segmentDelay)]              = segment_delay_ms;\n      staircase[FPSTR(_onTime)]                    = on_time_ms / 1000;\n      staircase[FPSTR(_useTopUltrasoundSensor)]    = useUSSensorTop;\n      staircase[FPSTR(_topPIRorTrigger_pin)]       = topPIRorTriggerPin;\n      staircase[FPSTR(_topEcho_pin)]               = useUSSensorTop ? topEchoPin : -1;\n      staircase[FPSTR(_useBottomUltrasoundSensor)] = useUSSensorBottom;\n      staircase[FPSTR(_bottomPIRorTrigger_pin)]    = bottomPIRorTriggerPin;\n      staircase[FPSTR(_bottomEcho_pin)]            = useUSSensorBottom ? bottomEchoPin : -1;\n      staircase[FPSTR(_topEchoCm)]                 = topMaxDist;\n      staircase[FPSTR(_bottomEchoCm)]              = bottomMaxDist;\n      staircase[FPSTR(_togglePower)]               = togglePower;\n      DEBUG_PRINTLN(F(\"Staircase config saved.\"));\n    }\n\n    /*\n    * Reads the configuration to internal flash memory before setup() is called.\n    * \n    * The function should return true if configuration was successfully loaded or false if there was no configuration.\n    */\n    bool readFromConfig(JsonObject& root) {\n      bool oldUseUSSensorTop = useUSSensorTop;\n      bool oldUseUSSensorBottom = useUSSensorBottom;\n      int8_t oldTopAPin = topPIRorTriggerPin;\n      int8_t oldTopBPin = topEchoPin;\n      int8_t oldBottomAPin = bottomPIRorTriggerPin;\n      int8_t oldBottomBPin = bottomEchoPin;\n\n      JsonObject top = root[FPSTR(_name)];\n      if (top.isNull()) {\n        DEBUG_PRINT(FPSTR(_name));\n        DEBUG_PRINTLN(F(\": No config found. (Using defaults.)\"));\n        return false;\n      }\n\n      enabled   = top[FPSTR(_enabled)] | enabled;\n\n      segment_delay_ms = top[FPSTR(_segmentDelay)] | segment_delay_ms;\n      segment_delay_ms = (unsigned long) min((unsigned long)10000,max((unsigned long)10,(unsigned long)segment_delay_ms));  // max delay 10s\n\n      on_time_ms = top[FPSTR(_onTime)] | on_time_ms/1000;\n      on_time_ms = min(900,max(10,(int)on_time_ms)) * 1000; // min 10s, max 15min\n\n      useUSSensorTop     = top[FPSTR(_useTopUltrasoundSensor)] | useUSSensorTop;\n      topPIRorTriggerPin = top[FPSTR(_topPIRorTrigger_pin)] | topPIRorTriggerPin;\n      topEchoPin         = top[FPSTR(_topEcho_pin)] | topEchoPin;\n\n      useUSSensorBottom     = top[FPSTR(_useBottomUltrasoundSensor)] | useUSSensorBottom;\n      bottomPIRorTriggerPin = top[FPSTR(_bottomPIRorTrigger_pin)] | bottomPIRorTriggerPin;\n      bottomEchoPin         = top[FPSTR(_bottomEcho_pin)] | bottomEchoPin;\n\n      topMaxDist    = top[FPSTR(_topEchoCm)] | topMaxDist;\n      topMaxDist    = min(150,max(30,(int)topMaxDist));     // max distance ~1.5m (a lag of 9ms may be expected)\n      bottomMaxDist = top[FPSTR(_bottomEchoCm)] | bottomMaxDist;\n      bottomMaxDist = min(150,max(30,(int)bottomMaxDist));  // max distance ~1.5m (a lag of 9ms may be expected)\n\n      togglePower = top[FPSTR(_togglePower)] | togglePower;  // staircase toggles power on/off\n\n      DEBUG_PRINT(FPSTR(_name));\n      if (!initDone) {\n        // first run: reading from cfg.json\n        DEBUG_PRINTLN(F(\" config loaded.\"));\n      } else {\n        // changing parameters from settings page\n        DEBUG_PRINTLN(F(\" config (re)loaded.\"));\n        bool changed = false;\n        if ((oldUseUSSensorTop != useUSSensorTop) ||\n            (oldUseUSSensorBottom != useUSSensorBottom) ||\n            (oldTopAPin != topPIRorTriggerPin) ||\n            (oldTopBPin != topEchoPin) ||\n            (oldBottomAPin != bottomPIRorTriggerPin) ||\n            (oldBottomBPin != bottomEchoPin)) {\n          changed = true;\n          PinManager::deallocatePin(oldTopAPin, PinOwner::UM_AnimatedStaircase);\n          PinManager::deallocatePin(oldTopBPin, PinOwner::UM_AnimatedStaircase);\n          PinManager::deallocatePin(oldBottomAPin, PinOwner::UM_AnimatedStaircase);\n          PinManager::deallocatePin(oldBottomBPin, PinOwner::UM_AnimatedStaircase);\n        }\n        if (changed) setup();\n      }\n      // use \"return !top[\"newestParameter\"].isNull();\" when updating Usermod with new features\n      return !top[FPSTR(_togglePower)].isNull();\n    }\n\n    /*\n    * Shows the delay between steps and power-off time in the \"info\"\n    * tab of the web-UI.\n    */\n    void addToJsonInfo(JsonObject& root) {\n      JsonObject user = root[\"u\"];\n      if (user.isNull()) {\n        user = root.createNestedObject(\"u\");\n      }\n\n      JsonArray infoArr = user.createNestedArray(FPSTR(_name));  // name\n\n      String uiDomString = F(\"<button class=\\\"btn btn-xs\\\" onclick=\\\"requestJson({\");\n      uiDomString += FPSTR(_name);\n      uiDomString += F(\":{\");\n      uiDomString += FPSTR(_enabled);\n      uiDomString += enabled ? F(\":false}});\\\">\") : F(\":true}});\\\">\");\n      uiDomString += F(\"<i class=\\\"icons \");\n      uiDomString += enabled ? \"on\" : \"off\";\n      uiDomString += F(\"\\\">&#xe08f;</i></button>\");\n      infoArr.add(uiDomString);\n    }\n};\n\n// strings to reduce flash memory usage (used more than twice)\nconst char Animated_Staircase::_name[]                      PROGMEM = \"staircase\";\nconst char Animated_Staircase::_enabled[]                   PROGMEM = \"enabled\";\nconst char Animated_Staircase::_segmentDelay[]              PROGMEM = \"segment-delay-ms\";\nconst char Animated_Staircase::_onTime[]                    PROGMEM = \"on-time-s\";\nconst char Animated_Staircase::_useTopUltrasoundSensor[]    PROGMEM = \"useTopUltrasoundSensor\";\nconst char Animated_Staircase::_topPIRorTrigger_pin[]       PROGMEM = \"topPIRorTrigger_pin\";\nconst char Animated_Staircase::_topEcho_pin[]               PROGMEM = \"topEcho_pin\";\nconst char Animated_Staircase::_useBottomUltrasoundSensor[] PROGMEM = \"useBottomUltrasoundSensor\";\nconst char Animated_Staircase::_bottomPIRorTrigger_pin[]    PROGMEM = \"bottomPIRorTrigger_pin\";\nconst char Animated_Staircase::_bottomEcho_pin[]            PROGMEM = \"bottomEcho_pin\";\nconst char Animated_Staircase::_topEchoCm[]                 PROGMEM = \"top-dist-cm\";\nconst char Animated_Staircase::_bottomEchoCm[]              PROGMEM = \"bottom-dist-cm\";\nconst char Animated_Staircase::_togglePower[]               PROGMEM = \"toggle-on-off\";\n\n\nstatic Animated_Staircase animated_staircase;\nREGISTER_USERMOD(animated_staircase);"
  },
  {
    "path": "usermods/Animated_Staircase/README.md",
    "content": "# Usermod Animated Staircase\n\nThis usermod makes your staircase look cool by illuminating it with an animation. It uses\nPIR or ultrasonic sensors at the top and bottom of your stairs to:\n\n- Light up the steps in the direction you're walking.\n- Switch off the steps after you, in the direction of the last detected movement.\n- Always switch on when one of the sensors detects movement, even if an effect\n  is still running. It can gracefully handle multiple people on the stairs.\n\nThe Animated Staircase can be controlled by the WLED API. Change settings such as\nspeed, on/off time and distance by sending an HTTP request, see below.\n\n## WLED integration\n\nTo include this usermod in your WLED setup, you have to be able to [compile WLED from source](https://kno.wled.ge/advanced/compiling-wled/).\n\nBefore compiling, you have to make the following modifications:\n\nEdit your environment in `platformio_override.ini`\n\n1. Open `platformio_override.ini`\n2. add `Animated_Staircase` to the `custom_usermods` line for your environment\n\nYou can configure usermod using the Usermods settings page.\nPlease enter GPIO pins for PIR or ultrasonic sensors (trigger and echo).\nIf you use PIR sensor enter -1 for echo pin.\nMaximum distance for ultrasonic sensor can be configured as the time needed for an echo (see below).\n\n## Hardware installation\n\n1. Attach the LED strip to each step of the stairs.\n2. Connect the ESP8266 pin D4 or ESP32 pin D2 to the first LED data pin at the bottom step.\n3. 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.\n4. Mount sensors of choice at the bottom and top of the stairs and connect them to the ESP.\n5. To make sure all LEDs get enough power and have your staircase lighted evenly, power each\n   step from one side, using at least AWG14 or 2.5mm^2 cable. Don't connect them serial as you\n   do for the datacable!\n\nYou _may_ need to use 10k pull-down resistors on the selected PIR pins, depending on the sensor.\n\n## WLED configuration\n\n1. In the WLED UI, configure a segment for each step. The lowest step of the stairs is the lowest segment id.\n2. Save your segments into a preset.\n3. Ideally, add the preset in the config > LED setup menu to the \"apply preset **n** at boot\" setting.\n\n## Changing behavior through API\n\nThe Staircase settings can be changed through the WLED JSON api.\n\n**NOTE:** We are using [curl](https://curl.se/) to send HTTP POSTs to the WLED API.\nIf you're using Windows and want to use the curl commands, replace the `\\` with a `^`\nor remove them and put everything on one line.\n\n| Setting          | Description                                                   | Default |\n|------------------|---------------------------------------------------------------|---------|\n| enabled          | Enable or disable the usermod                                 | true    |\n| bottom-sensor    | Manually trigger a down to up animation via API               | false   |\n| top-sensor       | Manually trigger an up to down animation via API              | false   |\n\n\nTo read the current settings, open a browser to `http://xxx.xxx.xxx.xxx/json/state` (use your WLED \ndevice IP address). The device will respond with a json object containing all WLED settings. \nThe staircase settings and sensor states are inside the WLED \"state\" element:\n\n```json\n{\n    \"state\": {\n        \"staircase\": {\n            \"enabled\": true,\n            \"bottom-sensor\": false,\n            \"top-sensor\": false\n        },\n}\n```\n\n### Enable/disable the usermod\n\nBy disabling the usermod you will be able to keep the LED's on, independent from the sensor\nactivity. This enables you to play with the lights without the usermod switching them on or off.\n\nTo disable the usermod:\n\n```bash\ncurl -X POST -H \"Content-Type: application/json\" \\\n     -d {\"staircase\":{\"enabled\":false}} \\\n     xxx.xxx.xxx.xxx/json/state\n```\n\nTo enable the usermod again, use `\"enabled\":true`.\n\nAlternatively you can use _Usermod_ Settings page where you can change other parameters as well.\n\n### Changing animation parameters and detection range of the ultrasonic HC-SR04 sensor\n\nUsing _Usermod_ Settings page you can define different usermod parameters, including sensor pins, delay between segment activation etc.\n\nWhen an ultrasonic sensor is enabled you can enter maximum detection distance in centimeters separately for top and bottom sensors.\n\n**Please note:** using an HC-SR04 sensor, particularly when detecting echos at longer\ndistances creates delays in the WLED software, _might_ introduce timing hiccups in your animation or\na less responsive web interface. It is therefore advised to keep the detection distance as short as possible.\n\n### Animation triggering through the API\n\nIn addition to activation by one of the stair sensors, you can also trigger the animation manually\nvia the API. To simulate triggering the bottom sensor, use:\n\n```bash\ncurl -X POST -H \"Content-Type: application/json\" \\\n     -d '{\"staircase\":{\"bottom-sensor\":true}}' \\\n     xxx.xxx.xxx.xxx/json/state\n```\n\nLikewise, to trigger the top sensor:\n\n```bash\ncurl -X POST -H \"Content-Type: application/json\" \\\n     -d '{\"staircase\":{\"top-sensor\":true}}' \\\n     xxx.xxx.xxx.xxx/json/state\n```\n\n**MQTT**\nYou can publish a message with either `up` or `down` on topic `/swipe` to trigger animation.\nYou can also use `on` or `off` for enabling or disabling the usermod.\n\nHave fun with this usermod\n\n`www.rolfje.com`\n\nModifications @blazoncek\n\n## Change log\n\n2021-04\n\n- Adaptation for runtime configuration.\n"
  },
  {
    "path": "usermods/Animated_Staircase/library.json",
    "content": "{\n  \"name\": \"Animated_Staircase\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/Artemis_reciever/readme.md",
    "content": "Usermod to allow WLED to receive via UDP port from RGB.NET (and therefore add as a device to be controlled within artemis on PC)\n\nThis is only a very simple code to support a single led strip, it does not support the full function of the RGB.NET sketch for esp8266 only what is needed to be used with Artemis. It will show as a ws281x device in artemis when you provide the correct hostname or ip. Artemis queries the number of LEDs via the web interface (/config) but communication to set the LEDs is all done via the UDP interface.\n\nTo install, copy the usermod.cpp file to wled00 folder and recompile"
  },
  {
    "path": "usermods/Artemis_reciever/usermod.cpp",
    "content": "/*\n *          RGB.NET (artemis) receiver\n *          \n * This works via the UDP, http is not supported apart from reporting LED count\n * \n * \n */\n#include \"wled.h\"\n#include <WiFiUdp.h>\n\nWiFiUDP UDP;\nconst unsigned int RGBNET_localUdpPort = 1872; // local port to listen on\nunsigned char RGBNET_packet[770];\nlong lastTime = 0;\nint delayMs = 10;\nbool isRGBNETUDPEnabled;\n\nvoid RGBNET_readValues() {\n  \n  int RGBNET_packetSize = UDP.parsePacket();\n  if (RGBNET_packetSize) {\n    // receive incoming UDP packets\n    int sequenceNumber = UDP.read();\n    int channel = UDP.read();\n\n    //channel data is not used we only supports one channel\n    int len = UDP.read(RGBNET_packet, strip.getLengthTotal()*3);\n    if(len==0){\n      return;\n    }\n    \n    for (int i = 0; i < len; i=i+3) {\n      strip.setPixelColor(i/3, RGBNET_packet[i], RGBNET_packet[i+1], RGBNET_packet[i+2], 0);\n    } \n    //strip.show();  \n  }\n}\n\n//update LED strip\nvoid RGBNET_show() {\n  strip.show();\n  lastTime = millis();\n}\n\n//This function provides a json with info on the number of LEDs connected\n// it is needed by artemis to know how many LEDs to display on the surface\nvoid handleConfig(AsyncWebServerRequest *request)\n{\n  String config = (String)\"{\\\n  \\\"channels\\\": [\\\n    {\\\n      \\\"channel\\\": 1,\\\n      \\\"leds\\\": \" + strip.getLengthTotal() + \"\\\n    },\\\n    {\\\n      \\\"channel\\\": 2,\\\n      \\\"leds\\\": \" + \"0\" + \"\\\n    },\\\n    {\\\n      \\\"channel\\\": 3,\\\n      \\\"leds\\\": \" + \"0\" + \"\\\n    },\\\n    {\\\n      \\\"channel\\\": 4,\\\n      \\\"leds\\\": \" + \"0\" + \"\\\n    }\\\n  ]\\\n}\";\n  request->send(200, \"application/json\", config);\n}\n\n\nvoid userSetup()\n{\n  server.on(\"/config\", HTTP_GET, [](AsyncWebServerRequest *request){ \n    handleConfig(request);\n  });\n}\n\nvoid userConnected()\n{\n  // new wifi, who dis?\n  UDP.begin(RGBNET_localUdpPort);\n  isRGBNETUDPEnabled = true;\n}\n\nvoid userLoop()\n{\n  RGBNET_readValues();\n    if (millis()-lastTime > delayMs) {\n      RGBNET_show();\n    }\n}"
  },
  {
    "path": "usermods/BH1750_v2/BH1750_v2.cpp",
    "content": "// force the compiler to show a warning to confirm that this file is included\n#warning **** Included USERMOD_BH1750 ****\n\n#include \"wled.h\"\n#include \"BH1750_v2.h\"\n\n#ifdef WLED_DISABLE_MQTT\n#error \"This user mod requires MQTT to be enabled.\"\n#endif\n\nstatic bool checkBoundSensor(float newValue, float prevValue, float maxDiff)\n{\n  return isnan(prevValue) || newValue <= prevValue - maxDiff || newValue >= prevValue + maxDiff || (newValue == 0.0 && prevValue > 0.0);\n}\n\nvoid Usermod_BH1750::_mqttInitialize()\n{\n  mqttLuminanceTopic = String(mqttDeviceTopic) + F(\"/brightness\");\n\n  if (HomeAssistantDiscovery) _createMqttSensor(F(\"Brightness\"), mqttLuminanceTopic, F(\"Illuminance\"), F(\" lx\"));\n}\n\n// Create an MQTT Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop.\nvoid Usermod_BH1750::_createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement)\n{\n  String t = String(F(\"homeassistant/sensor/\")) + mqttClientID + F(\"/\") + name + F(\"/config\");\n  \n  StaticJsonDocument<600> doc;\n  \n  doc[F(\"name\")] = String(serverDescription) + \" \" + name;\n  doc[F(\"state_topic\")] = topic;\n  doc[F(\"unique_id\")] = String(mqttClientID) + name;\n  if (unitOfMeasurement != \"\")\n    doc[F(\"unit_of_measurement\")] = unitOfMeasurement;\n  if (deviceClass != \"\")\n    doc[F(\"device_class\")] = deviceClass;\n  doc[F(\"expire_after\")] = 1800;\n\n  JsonObject device = doc.createNestedObject(F(\"device\")); // attach the sensor to the same device\n  device[F(\"name\")] = serverDescription;\n  device[F(\"identifiers\")] = \"wled-sensor-\" + String(mqttClientID);\n  device[F(\"manufacturer\")] = F(WLED_BRAND);\n  device[F(\"model\")] = F(WLED_PRODUCT_NAME);\n  device[F(\"sw_version\")] = versionString;\n\n  String temp;\n  serializeJson(doc, temp);\n  DEBUG_PRINTLN(t);\n  DEBUG_PRINTLN(temp);\n\n  mqtt->publish(t.c_str(), 0, true, temp.c_str());\n}\n\nvoid Usermod_BH1750::setup()\n{\n  if (i2c_scl<0 || i2c_sda<0) { enabled = false; return; }\n  sensorFound = lightMeter.begin();\n  initDone = true;\n}\n\nvoid Usermod_BH1750::loop()\n{\n  if ((!enabled) || strip.isUpdating())\n    return;\n\n  unsigned long now = millis();\n\n  // check to see if we are due for taking a measurement\n  // lastMeasurement will not be updated until the conversion\n  // is complete the the reading is finished\n  if (now - lastMeasurement < minReadingInterval)\n  {\n    return;\n  }\n\n  bool shouldUpdate = now - lastSend > maxReadingInterval;\n\n  float lux = lightMeter.readLightLevel();\n  lastMeasurement = millis();\n  getLuminanceComplete = true;\n\n  if (shouldUpdate || checkBoundSensor(lux, lastLux, offset))\n  {\n    lastLux = lux;\n    lastSend = millis();\n\n    if (WLED_MQTT_CONNECTED)\n    {\n      if (!mqttInitialized)\n        {\n          _mqttInitialize();\n          mqttInitialized = true;\n        }\n      mqtt->publish(mqttLuminanceTopic.c_str(), 0, true, String(lux).c_str());\n      DEBUG_PRINTLN(F(\"Brightness: \") + String(lux) + F(\"lx\"));\n    }\n    else\n    {\n      DEBUG_PRINTLN(F(\"Missing MQTT connection. Not publishing data\"));\n    }\n  }\n}\n\n\nvoid Usermod_BH1750::addToJsonInfo(JsonObject &root)\n{\n  JsonObject user = root[F(\"u\")];\n  if (user.isNull())\n    user = root.createNestedObject(F(\"u\"));\n\n  JsonArray lux_json = user.createNestedArray(F(\"Luminance\"));\n  if (!enabled) {\n    lux_json.add(F(\"disabled\"));\n  } else if (!sensorFound) {\n      // if no sensor \n      lux_json.add(F(\"BH1750 \"));\n      lux_json.add(F(\"Not Found\"));\n  } else if (!getLuminanceComplete) {\n    // if we haven't read the sensor yet, let the user know\n      // that we are still waiting for the first measurement\n      lux_json.add((USERMOD_BH1750_FIRST_MEASUREMENT_AT - millis()) / 1000);\n      lux_json.add(F(\" sec until read\"));\n      return;\n  } else {\n    lux_json.add(lastLux);\n    lux_json.add(F(\" lx\"));\n  }\n}\n\n// (called from set.cpp) stores persistent properties to cfg.json\nvoid Usermod_BH1750::addToConfig(JsonObject &root)\n{\n  // we add JSON object.\n  JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname\n  top[FPSTR(_enabled)] = enabled;\n  top[FPSTR(_maxReadInterval)] = maxReadingInterval;\n  top[FPSTR(_minReadInterval)] = minReadingInterval;\n  top[FPSTR(_HomeAssistantDiscovery)] = HomeAssistantDiscovery;\n  top[FPSTR(_offset)] = offset;\n\n  DEBUG_PRINTLN(F(\"BH1750 config saved.\"));\n}\n\n// called before setup() to populate properties from values stored in cfg.json\nbool Usermod_BH1750::readFromConfig(JsonObject &root)\n{\n  // we look for JSON object.\n  JsonObject top = root[FPSTR(_name)];\n  if (top.isNull())\n  {\n    DEBUG_PRINT(FPSTR(_name));\n    DEBUG_PRINT(F(\"BH1750\"));\n    DEBUG_PRINTLN(F(\": No config found. (Using defaults.)\"));\n    return false;\n  }\n  bool configComplete = !top.isNull();\n\n  configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled, false);\n  configComplete &= getJsonValue(top[FPSTR(_maxReadInterval)], maxReadingInterval, 10000); //ms\n  configComplete &= getJsonValue(top[FPSTR(_minReadInterval)], minReadingInterval, 500); //ms\n  configComplete &= getJsonValue(top[FPSTR(_HomeAssistantDiscovery)], HomeAssistantDiscovery, false);\n  configComplete &= getJsonValue(top[FPSTR(_offset)], offset, 1);\n\n  DEBUG_PRINT(FPSTR(_name));\n  if (!initDone) {\n    DEBUG_PRINTLN(F(\" config loaded.\"));\n  } else {\n    DEBUG_PRINTLN(F(\" config (re)loaded.\"));\n  }\n\n  return configComplete;\n  \n}\n\n\n// strings to reduce flash memory usage (used more than twice)\nconst char Usermod_BH1750::_name[] PROGMEM = \"BH1750\";\nconst char Usermod_BH1750::_enabled[] PROGMEM = \"enabled\";\nconst char Usermod_BH1750::_maxReadInterval[] PROGMEM = \"max-read-interval-ms\";\nconst char Usermod_BH1750::_minReadInterval[] PROGMEM = \"min-read-interval-ms\";\nconst char Usermod_BH1750::_HomeAssistantDiscovery[] PROGMEM = \"HomeAssistantDiscoveryLux\";\nconst char Usermod_BH1750::_offset[] PROGMEM = \"offset-lx\";\n\n\nstatic Usermod_BH1750 bh1750_v2;\nREGISTER_USERMOD(bh1750_v2);"
  },
  {
    "path": "usermods/BH1750_v2/BH1750_v2.h",
    "content": "\r\n#pragma once\r\n#include \"wled.h\"\r\n#include <BH1750.h>\r\n\r\n#ifdef WLED_DISABLE_MQTT\r\n#error \"This user mod requires MQTT to be enabled.\"\r\n#endif\r\n\r\n// the max frequency to check photoresistor, 10 seconds\r\n#ifndef USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL\r\n#define USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL 10000\r\n#endif\r\n\r\n// the min frequency to check photoresistor, 500 ms\r\n#ifndef USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL\r\n#define USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL 500\r\n#endif\r\n\r\n// how many seconds after boot to take first measurement, 10 seconds\r\n#ifndef USERMOD_BH1750_FIRST_MEASUREMENT_AT\r\n#define USERMOD_BH1750_FIRST_MEASUREMENT_AT 10000\r\n#endif\r\n\r\n// only report if difference grater than offset value\r\n#ifndef USERMOD_BH1750_OFFSET_VALUE\r\n#define USERMOD_BH1750_OFFSET_VALUE 1\r\n#endif\r\n\r\nclass Usermod_BH1750 : public Usermod\r\n{\r\nprivate:\r\n  int8_t offset = USERMOD_BH1750_OFFSET_VALUE;\r\n\r\n  unsigned long maxReadingInterval = USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL;\r\n  unsigned long minReadingInterval = USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL;\r\n  unsigned long lastMeasurement = UINT32_MAX - (USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL - USERMOD_BH1750_FIRST_MEASUREMENT_AT);\r\n  unsigned long lastSend = UINT32_MAX - (USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL - USERMOD_BH1750_FIRST_MEASUREMENT_AT);\r\n  // flag to indicate we have finished the first readLightLevel call\r\n  // allows this library to report to the user how long until the first\r\n  // measurement\r\n  bool getLuminanceComplete = false;\r\n\r\n  // flag set at startup\r\n  bool enabled = true;\r\n\r\n  // strings to reduce flash memory usage (used more than twice)\r\n  static const char _name[];\r\n  static const char _enabled[];\r\n  static const char _maxReadInterval[];\r\n  static const char _minReadInterval[];\r\n  static const char _offset[];\r\n  static const char _HomeAssistantDiscovery[];\r\n\r\n  bool initDone = false;\r\n  bool sensorFound = false;\r\n\r\n  // Home Assistant and MQTT  \r\n  String mqttLuminanceTopic;\r\n  bool mqttInitialized = false;\r\n  bool HomeAssistantDiscovery = true; // Publish Home Assistant Discovery messages\r\n\r\n  BH1750 lightMeter;\r\n  float lastLux = -1000;\r\n\r\n  // set up Home Assistant discovery entries\r\n  void _mqttInitialize();\r\n\r\n  // Create an MQTT Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop.\r\n  void _createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement);\r\n\r\npublic:\r\n  void setup();\r\n  void loop();\r\n  inline float getIlluminance()  {\r\n    return (float)lastLux;\r\n  }\r\n\r\n  void addToJsonInfo(JsonObject &root);\r\n\r\n  // (called from set.cpp) stores persistent properties to cfg.json\r\n  void addToConfig(JsonObject &root);\r\n\r\n  // called before setup() to populate properties from values stored in cfg.json\r\n  bool readFromConfig(JsonObject &root);\r\n\r\n  inline uint16_t getId()\r\n  {\r\n    return USERMOD_ID_BH1750;\r\n  }\r\n\r\n};\r\n"
  },
  {
    "path": "usermods/BH1750_v2/library.json",
    "content": "{\n  \"name\": \"BH1750_v2\",\n  \"build\": { \"libArchive\": false },\n  \"dependencies\": {\n    \"claws/BH1750\":\"^1.2.0\"\n  }\n}\n"
  },
  {
    "path": "usermods/BH1750_v2/readme.md",
    "content": "# BH1750 usermod\n\nThis usermod will read from an ambient light sensor like the BH1750.\nThe luminance is displayed in both the Info section of the web UI, as well as published to the `/luminance` MQTT topic if enabled.\n\n## Dependencies\n\n- Libraries\n  - `claws/BH1750 @^1.2.0`\n- Data is published over MQTT - make sure you've enabled the MQTT sync interface.\n\n## Compilation\n\nTo enable, compile with `BH1750` in `custom_usermods` (e.g. in `platformio_override.ini`)\n\n### Configuration Options\n\nThe following settings can be set at compile-time but are configurable on the usermod menu (except First Measurement time):\n\n- `USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL` - the max number of milliseconds between measurements, defaults to 10000ms\n- `USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL` - the min number of milliseconds between measurements, defaults to 500ms\n- `USERMOD_BH1750_OFFSET_VALUE` - the offset value to report on, defaults to 1\n- `USERMOD_BH1750_FIRST_MEASUREMENT_AT` - the number of milliseconds after boot to take first measurement, defaults to 10000 ms\n\nIn addition, the Usermod screen allows you to:\n\n- enable/disable the usermod\n- Enable Home Assistant Discovery of usermod\n- Configure the SCL/SDA pins\n\n## API\n\nThe following method is available to interact with the usermod from other code modules:\n\n- `getIlluminance` read the brightness from the sensor\n\n## Change Log\n\nJul 2022\n\n- Added Home Assistant Discovery\n- Implemented PinManager to register pins\n- Made pins configurable in usermod menu\n- Added API call to read luminance from other modules\n- Enhanced info-screen outputs\n- Updated `readme.md`\n"
  },
  {
    "path": "usermods/BME280_v2/BME280_v2.cpp",
    "content": "// force the compiler to show a warning to confirm that this file is included\n#warning **** Included USERMOD_BME280 version 2.0 ****\n\n#include \"wled.h\"\n#include <Arduino.h>\n#include <BME280I2C.h>               // BME280 sensor\n#include <EnvironmentCalculations.h> // BME280 extended measurements\n\n#ifdef WLED_DISABLE_MQTT\n#error \"This user mod requires MQTT to be enabled.\"\n#endif\n\nclass UsermodBME280 : public Usermod\n{\nprivate:\n  \n  // NOTE: Do not implement any compile-time variables, anything the user needs to configure\n  // should be configurable from the Usermod menu using the methods below\n  // key settings set via usermod menu\n  uint8_t  TemperatureDecimals = 0;  // Number of decimal places in published temperaure values\n  uint8_t  HumidityDecimals = 0;    // Number of decimal places in published humidity values\n  uint8_t  PressureDecimals = 0;    // Number of decimal places in published pressure values\n  uint16_t TemperatureInterval = 5; // Interval to measure temperature (and humidity, dew point if available) in seconds\n  uint16_t PressureInterval = 300;  // Interval to measure pressure in seconds\n  BME280I2C::I2CAddr I2CAddress = BME280I2C::I2CAddr_0x76;  // i2c address, defaults to 0x76\n  bool PublishAlways = false;             // Publish values even when they have not changed\n  bool UseCelsius = true;                 // Use Celsius for Reporting\n  bool HomeAssistantDiscovery = false;    // Publish Home Assistant Device Information\n  bool enabled = true;\n\n  // set the default pins based on the architecture, these get overridden by Usermod menu settings\n  #ifdef ESP8266\n    //uint8_t RST_PIN = 16; // Un-comment for Heltec WiFi-Kit-8\n  #endif\n  bool initDone = false;\n\n  BME280I2C bme;\n\n  uint8_t sensorType;\n\n  // Measurement timers\n  long timer;\n  long lastTemperatureMeasure = 0;\n  long lastPressureMeasure = 0;\n\n  // Current sensor values\n  float sensorTemperature;\n  float sensorHumidity;\n  float sensorHeatIndex;\n  float sensorDewPoint;\n  float sensorPressure;\n  String tempScale;\n  // Track previous sensor values\n  float lastTemperature;\n  float lastHumidity;\n  float lastHeatIndex;\n  float lastDewPoint;\n  float lastPressure;\n\n  // MQTT topic strings for publishing Home Assistant discovery topics\n  bool mqttInitialized = false;\n\n  // strings to reduce flash memory usage (used more than twice)\n  static const char _name[];\n  static const char _enabled[];\n\n  // Read the BME280/BMP280 Sensor (which one runs depends on whether Celsius or Fahrenheit being set in Usermod Menu)\n  void UpdateBME280Data(int SensorType)\n  {\n    float _temperature, _humidity, _pressure;\n\n    if (UseCelsius) {\n      BME280::TempUnit tempUnit(BME280::TempUnit_Celsius);\n      EnvironmentCalculations::TempUnit envTempUnit(EnvironmentCalculations::TempUnit_Celsius);\n      BME280::PresUnit presUnit(BME280::PresUnit_hPa);\n\n      bme.read(_pressure, _temperature, _humidity, tempUnit, presUnit);\n\n      sensorTemperature = _temperature;\n      sensorHumidity = _humidity;\n      sensorPressure = _pressure;\n      tempScale = F(\"°C\");\n      if (sensorType == 1)\n      {\n        sensorHeatIndex = EnvironmentCalculations::HeatIndex(_temperature, _humidity, envTempUnit);\n        sensorDewPoint = EnvironmentCalculations::DewPoint(_temperature, _humidity, envTempUnit);\n      }\n    } else {\n      BME280::TempUnit tempUnit(BME280::TempUnit_Fahrenheit);\n      EnvironmentCalculations::TempUnit envTempUnit(EnvironmentCalculations::TempUnit_Fahrenheit);\n      BME280::PresUnit presUnit(BME280::PresUnit_hPa);\n\n      bme.read(_pressure, _temperature, _humidity, tempUnit, presUnit);\n\n      sensorTemperature = _temperature;\n      sensorHumidity = _humidity;\n      sensorPressure = _pressure;\n      tempScale = F(\"°F\");\n      if (sensorType == 1)\n      {\n        sensorHeatIndex = EnvironmentCalculations::HeatIndex(_temperature, _humidity, envTempUnit);\n        sensorDewPoint = EnvironmentCalculations::DewPoint(_temperature, _humidity, envTempUnit);\n      }\n    }\n  }\n\n  // Procedure to define all MQTT discovery Topics \n  void _mqttInitialize()\n  {\n    char mqttTemperatureTopic[128];\n    char mqttHumidityTopic[128];\n    char mqttPressureTopic[128];\n    char mqttHeatIndexTopic[128];\n    char mqttDewPointTopic[128];\n    snprintf_P(mqttTemperatureTopic, 127, PSTR(\"%s/temperature\"), mqttDeviceTopic);\n    snprintf_P(mqttPressureTopic, 127, PSTR(\"%s/pressure\"), mqttDeviceTopic);\n    snprintf_P(mqttHumidityTopic, 127, PSTR(\"%s/humidity\"), mqttDeviceTopic);\n    snprintf_P(mqttHeatIndexTopic, 127, PSTR(\"%s/heat_index\"), mqttDeviceTopic);\n    snprintf_P(mqttDewPointTopic, 127, PSTR(\"%s/dew_point\"), mqttDeviceTopic);\n\n    if (HomeAssistantDiscovery) {\n      _createMqttSensor(F(\"Temperature\"), mqttTemperatureTopic, \"temperature\", tempScale);\n      _createMqttSensor(F(\"Pressure\"), mqttPressureTopic, \"pressure\", F(\"hPa\"));\n      _createMqttSensor(F(\"Humidity\"), mqttHumidityTopic, \"humidity\", F(\"%\"));\n      _createMqttSensor(F(\"HeatIndex\"), mqttHeatIndexTopic, \"temperature\", tempScale);\n      _createMqttSensor(F(\"DewPoint\"), mqttDewPointTopic, \"temperature\", tempScale);\n    }\n  }\n\n  // Create an MQTT Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop.\n  void _createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement)\n  {\n    String t = String(F(\"homeassistant/sensor/\")) + mqttClientID + F(\"/\") + name + F(\"/config\");\n    \n    StaticJsonDocument<600> doc;\n    \n    doc[F(\"name\")] = String(serverDescription) + \" \" + name;\n    doc[F(\"state_topic\")] = topic;\n    doc[F(\"unique_id\")] = String(mqttClientID) + name;\n    if (unitOfMeasurement != \"\")\n      doc[F(\"unit_of_measurement\")] = unitOfMeasurement;\n    if (deviceClass != \"\")\n      doc[F(\"device_class\")] = deviceClass;\n    doc[F(\"expire_after\")] = 1800;\n\n    JsonObject device = doc.createNestedObject(F(\"device\")); // attach the sensor to the same device\n    device[F(\"name\")] = serverDescription;\n    device[F(\"identifiers\")] = \"wled-sensor-\" + String(mqttClientID);\n    device[F(\"manufacturer\")] = F(WLED_BRAND);\n    device[F(\"model\")] = F(WLED_PRODUCT_NAME);\n    device[F(\"sw_version\")] = versionString;\n\n    String temp;\n    serializeJson(doc, temp);\n    DEBUG_PRINTLN(t);\n    DEBUG_PRINTLN(temp);\n\n    mqtt->publish(t.c_str(), 0, true, temp.c_str());\n  }\n\n    void publishMqtt(const char *topic, const char* state) {\n      //Check if MQTT Connected, otherwise it will crash the 8266\n      if (WLED_MQTT_CONNECTED){\n        char subuf[128];\n        snprintf_P(subuf, 127, PSTR(\"%s/%s\"), mqttDeviceTopic, topic);\n        mqtt->publish(subuf, 0, false, state);\n      }\n    }\n\n    void initializeBmeComms()\n    {\n      BME280I2C::Settings settings{\n            BME280::OSR_X16,    // Temperature oversampling x16\n            BME280::OSR_X16,    // Humidity oversampling x16\n            BME280::OSR_X16,    // Pressure oversampling x16\n            BME280::Mode_Forced,\n            BME280::StandbyTime_1000ms,\n            BME280::Filter_Off,\n            BME280::SpiEnable_False,\n            I2CAddress\n        };\n\n      bme.setSettings(settings);\n      \n      if (!bme.begin())\n      {\n        sensorType = 0;\n        DEBUG_PRINTLN(F(\"Could not find BME280 I2C sensor!\"));\n      }\n      else\n      {\n        switch (bme.chipModel())\n        {\n        case BME280::ChipModel_BME280:\n          sensorType = 1;\n          DEBUG_PRINTLN(F(\"Found BME280 sensor! Success.\"));\n          break;\n        case BME280::ChipModel_BMP280:\n          sensorType = 2;\n          DEBUG_PRINTLN(F(\"Found BMP280 sensor! No Humidity available.\"));\n          break;\n        default:\n          sensorType = 0;\n          DEBUG_PRINTLN(F(\"Found UNKNOWN sensor! Error!\"));\n        }\n      }\n    }\n\npublic:\n  void setup()\n  {\n    if (i2c_scl<0 || i2c_sda<0) { enabled = false; sensorType = 0; return; }\n    \n    initializeBmeComms();\n    initDone = true;\n  }\n\n  void loop()\n  {\n    if (!enabled || strip.isUpdating()) return;\n\n    // BME280 sensor MQTT publishing\n    // Check if sensor present and Connected, otherwise it will crash the MCU\n    if (sensorType != 0)\n    {\n      // Timer to fetch new temperature, humidity and pressure data at intervals\n      timer = millis();\n\n      if (timer - lastTemperatureMeasure >= TemperatureInterval * 1000)\n      {\n        lastTemperatureMeasure = timer;\n\n        UpdateBME280Data(sensorType);\n\n        float temperature = roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);\n        float humidity, heatIndex, dewPoint;\n\n        // If temperature has changed since last measure, create string populated with device topic\n        // from the UI and values read from sensor, then publish to broker\n        if (temperature != lastTemperature || PublishAlways)\n        {\n          publishMqtt(\"temperature\", String(temperature, (unsigned) TemperatureDecimals).c_str());\n        }\n\n        lastTemperature = temperature; // Update last sensor temperature for next loop\n\n        if (sensorType == 1) // Only if sensor is a BME280\n        {\n          humidity = roundf(sensorHumidity * powf(10, HumidityDecimals)) / powf(10, HumidityDecimals);\n          heatIndex = roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);\n          dewPoint = roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);\n\n          if (humidity != lastHumidity || PublishAlways)\n          {\n            publishMqtt(\"humidity\", String(humidity, (unsigned) HumidityDecimals).c_str());\n          }\n\n          if (heatIndex != lastHeatIndex || PublishAlways)\n          {\n            publishMqtt(\"heat_index\", String(heatIndex, (unsigned) TemperatureDecimals).c_str());\n          }\n\n          if (dewPoint != lastDewPoint || PublishAlways)\n          {\n            publishMqtt(\"dew_point\", String(dewPoint, (unsigned) TemperatureDecimals).c_str());\n          }\n\n          lastHumidity = humidity;\n          lastHeatIndex = heatIndex;\n          lastDewPoint = dewPoint;\n        }\n      }\n\n      if (timer - lastPressureMeasure >= PressureInterval * 1000)\n      {\n        lastPressureMeasure = timer;\n\n        float pressure = roundf(sensorPressure * powf(10, PressureDecimals)) / powf(10, PressureDecimals);\n\n        if (pressure != lastPressure || PublishAlways)\n        {\n          publishMqtt(\"pressure\", String(pressure, (unsigned) PressureDecimals).c_str());\n        }\n\n        lastPressure = pressure;\n      }\n    }\n  }\n\n  void onMqttConnect(bool sessionPresent)\n  {\n    if (WLED_MQTT_CONNECTED && !mqttInitialized)\n    {\n      _mqttInitialize();\n      mqttInitialized = true;\n    }\n  }\n\n    /*\n     * API calls te enable data exchange between WLED modules\n     */\n    inline float getTemperatureC() {\n      if (UseCelsius) {\n        return (float)roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);\n      } else {\n        return (float)roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) * 1.8f + 32;\n      }      \n    }\n\n    inline float getTemperatureF() {\n      if (UseCelsius) {\n        return ((float)roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) -32) * 0.56f;\n      } else {\n        return (float)roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);\n      }\n    }\n\n    inline float getHumidity() {\n      return (float)roundf(sensorHumidity * powf(10, HumidityDecimals));\n    }\n\n    inline float getPressure() {\n      return (float)roundf(sensorPressure * powf(10, PressureDecimals));\n    }\n\n    inline float getDewPointC() {\n      if (UseCelsius) {\n        return (float)roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);\n      } else {\n        return (float)roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) * 1.8f + 32;\n      }\n    }\n\n    inline float getDewPointF() {\n      if (UseCelsius) {\n        return ((float)roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) -32) * 0.56f;\n      } else {\n        return (float)roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);\n      }\n    }\n\n    inline float getHeatIndexC() {\n      if (UseCelsius) {\n        return (float)roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);\n      } else {\n        return (float)roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) * 1.8f + 32;\n      }\n    }\n\n    inline float getHeatIndexF() {\n      if (UseCelsius) {\n        return ((float)roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) -32) * 0.56f;\n      } else {\n        return (float)roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);\n      }\n    }\n\n  // Publish Sensor Information to Info Page\n  void addToJsonInfo(JsonObject &root)\n  {\n    JsonObject user = root[F(\"u\")];\n    if (user.isNull()) user = root.createNestedObject(F(\"u\"));\n    \n    if (sensorType==0) //No Sensor\n    {\n      // if we sensor not detected, let the user know\n      JsonArray temperature_json = user.createNestedArray(F(\"BME/BMP280 Sensor\"));\n      temperature_json.add(F(\"Not Found\"));\n    }\n    else if (sensorType==2) //BMP280\n    {\n      JsonArray temperature_json = user.createNestedArray(F(\"Temperature\"));\n      JsonArray pressure_json = user.createNestedArray(F(\"Pressure\"));\n      temperature_json.add(roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals));\n      temperature_json.add(tempScale);\n      pressure_json.add(roundf(sensorPressure * powf(10, PressureDecimals)) / powf(10, PressureDecimals));\n      pressure_json.add(F(\"hPa\"));\n    }\n    else if (sensorType==1) //BME280\n    {\n      JsonArray temperature_json = user.createNestedArray(F(\"Temperature\"));\n      JsonArray humidity_json = user.createNestedArray(F(\"Humidity\"));\n      JsonArray pressure_json = user.createNestedArray(F(\"Pressure\"));\n      JsonArray heatindex_json = user.createNestedArray(F(\"Heat Index\"));\n      JsonArray dewpoint_json = user.createNestedArray(F(\"Dew Point\"));\n      temperature_json.add(roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals));\n      temperature_json.add(tempScale);\n      humidity_json.add(roundf(sensorHumidity * powf(10, HumidityDecimals)) / powf(10, HumidityDecimals));\n      humidity_json.add(F(\"%\"));\n      pressure_json.add(roundf(sensorPressure * powf(10, PressureDecimals)) / powf(10, PressureDecimals));\n      pressure_json.add(F(\"hPa\"));\n      heatindex_json.add(roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals));\n      heatindex_json.add(tempScale);\n      dewpoint_json.add(roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals));\n      dewpoint_json.add(tempScale);\n    }\n      return;\n  }\n\n  // Save Usermod Config Settings\n  void addToConfig(JsonObject& root)\n  {\n    JsonObject top = root.createNestedObject(FPSTR(_name));\n    top[FPSTR(_enabled)] = enabled;\n    top[F(\"I2CAddress\")] = static_cast<uint8_t>(I2CAddress);\n    top[F(\"TemperatureDecimals\")] = TemperatureDecimals;\n    top[F(\"HumidityDecimals\")] = HumidityDecimals;\n    top[F(\"PressureDecimals\")] = PressureDecimals;\n    top[F(\"TemperatureInterval\")] = TemperatureInterval;\n    top[F(\"PressureInterval\")] = PressureInterval;\n    top[F(\"PublishAlways\")] = PublishAlways;\n    top[F(\"UseCelsius\")] = UseCelsius;\n    top[F(\"HomeAssistantDiscovery\")] = HomeAssistantDiscovery;\n    DEBUG_PRINTLN(F(\"BME280 config saved.\"));\n  }\n\n  // Read Usermod Config Settings\n  bool readFromConfig(JsonObject& root)\n  {\n    // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor\n    // 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)\n\n    JsonObject top = root[FPSTR(_name)];\n    if (top.isNull()) {\n      DEBUG_PRINT(F(_name));\n      DEBUG_PRINTLN(F(\": No config found. (Using defaults.)\"));\n      return false;\n    }\n    bool configComplete = !top.isNull();\n\n    configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled);\n    // A 3-argument getJsonValue() assigns the 3rd argument as a default value if the Json value is missing\n    uint8_t tmpI2cAddress;\n    configComplete &= getJsonValue(top[F(\"I2CAddress\")], tmpI2cAddress, 0x76);\n    I2CAddress = static_cast<BME280I2C::I2CAddr>(tmpI2cAddress);\n\n    configComplete &= getJsonValue(top[F(\"TemperatureDecimals\")], TemperatureDecimals, 1);\n    configComplete &= getJsonValue(top[F(\"HumidityDecimals\")], HumidityDecimals, 0);\n    configComplete &= getJsonValue(top[F(\"PressureDecimals\")], PressureDecimals, 0);\n    configComplete &= getJsonValue(top[F(\"TemperatureInterval\")], TemperatureInterval, 30);\n    configComplete &= getJsonValue(top[F(\"PressureInterval\")], PressureInterval, 30);\n    configComplete &= getJsonValue(top[F(\"PublishAlways\")], PublishAlways, false);\n    configComplete &= getJsonValue(top[F(\"UseCelsius\")], UseCelsius, true);\n    configComplete &= getJsonValue(top[F(\"HomeAssistantDiscovery\")], HomeAssistantDiscovery, false);\n\n    DEBUG_PRINT(FPSTR(_name));\n    if (!initDone) {\n      // first run: reading from cfg.json\n      DEBUG_PRINTLN(F(\" config loaded.\"));\n    } else {\n      // changing parameters from settings page\n      DEBUG_PRINTLN(F(\" config (re)loaded.\"));\n\n      // Reset all known values\n      sensorType = 0;\n      sensorTemperature = 0;\n      sensorHumidity = 0;\n      sensorHeatIndex = 0;\n      sensorDewPoint = 0;\n      sensorPressure = 0;\n      lastTemperature = 0;\n      lastHumidity = 0;\n      lastHeatIndex = 0;\n      lastDewPoint = 0;\n      lastPressure = 0;\n      \n      initializeBmeComms();\n    }\n\n    return configComplete;\n  }\n\n  uint16_t getId() {\n    return USERMOD_ID_BME280;\n  }\n};\n\nconst char UsermodBME280::_name[]                      PROGMEM = \"BME280/BMP280\";\nconst char UsermodBME280::_enabled[]                   PROGMEM = \"enabled\";\n\n\nstatic UsermodBME280 bme280_v2;\nREGISTER_USERMOD(bme280_v2);"
  },
  {
    "path": "usermods/BME280_v2/README.md",
    "content": "# Usermod BME280\nThis Usermod is designed to read a `BME280` or `BMP280` sensor and output the following:\n- Temperature\n- Humidity (`BME280` only)\n- Pressure\n- Heat Index (`BME280` only)\n- Dew Point (`BME280` only)\n\nConfiguration is performed via the Usermod menu.  There are no parameters to set in code!  The following settings can be configured in the Usermod Menu:\n- The i2c address in decimal. Set it to either 118 (0x76, the default) or 119 (0x77).\n- Temperature Decimals (number of decimal places to output)\n- Humidity Decimals\n- Pressure Decimals\n- Temperature Interval (how many seconds between temperature and humidity measurements)\n- Pressure Interval\n- Publish Always (turn off to only publish changes, on to publish whether or not value changed)\n- Use Celsius (turn off to use Fahrenheit)\n- Home Assistant Discovery (turn on to sent MQTT Discovery entries for Home Assistant)\n- SCL/SDA GPIO Pins\n\nDependencies\n- Libraries\n  - `BME280@~3.0.0` (by [finitespace](https://github.com/finitespace/BME280))\n  - `Wire`\n- Data is published over MQTT - make sure you've enabled the MQTT sync interface.\n- 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!\n\nIn addition to outputting via MQTT, you can read the values from the Info Screen on the dashboard page of the device's web interface.\n\nMethods also exist to read the read/calculated values from other WLED modules through code.\n- `getTemperatureC()`\n- `getTemperatureF()`\n- `getHumidity()`\n- `getPressure()`\n- `getDewPointC()`\n- `getDewPointF()`\n- `getHeatIndexC()`\n- `getHeatIndexF()`\n\n# Compiling\n\nTo enable, add `BME280_v2` to your `custom_usermods`  (e.g. in `platformio_override.ini`)\n```ini\n[env:usermod_bme280_d1_mini]\nextends = env:d1_mini\ncustom_usermods = ${env:d1_mini.custom_usermods} BME280_v2\n```\n\n\n# MQTT\nMQTT topics are as follows (`<deviceTopic>` is set in MQTT section of Sync Setup menu):\nMeasurement type | MQTT topic\n--- | ---\nTemperature | `<deviceTopic>/temperature`\nHumidity | `<deviceTopic>/humidity`\nPressure | `<deviceTopic>/pressure`\nHeat index | `<deviceTopic>/heat_index`\nDew point | `<deviceTopic>/dew_point`\n\nIf you are using Home Assistant, and `Home Assistant Discovery` is turned on, Home Assistant should automatically detect a new device, provided you have the MQTT integration installed.  The device is separate from the main WLED device and will contain sensors for Pressure, Humidity, Temperature, Dew Point and Heat Index.\n\n# Revision History\nJul 2022\n- Added Home Assistant Discovery\n- Added API interface to output data\n- Removed compile-time variables\n- Added usermod menu interface\n- Added value outputs to info screen\n- Updated `readme.md`\n- Registered usermod\n- Implemented PinManager for usermod\n- Implemented reallocation of pins without reboot\n\nApr 2021\n- Added `Publish Always` option\n\nDec 2020\n- Ported to V2 Usermod format\n- Customizable `measure intervals`\n- Customizable number of `decimal places` in published sensor values\n- Pressure measured in units of hPa instead of Pa\n- Calculation of heat index (apparent temperature) and dew point\n- `16x oversampling` of sensor during measurement\n- Values only published if they are different from the previous value\n"
  },
  {
    "path": "usermods/BME280_v2/library.json",
    "content": "{\n  \"name\": \"BME280_v2\",\n  \"build\": { \"libArchive\": false },\n  \"dependencies\": {\n    \"finitespace/BME280\":\"~3.0.0\"\n  }\n}"
  },
  {
    "path": "usermods/BME68X_v2/BME68X_v2.cpp",
    "content": "/**\n * @file usermod_BMW68X.cpp\n * @author Gabriel A. Sieben  (GeoGab)\n * @brief Usermod for WLED to implement the BME680/BME688 sensor\n * @version 1.0.2\n * @date 28 March 2025\n */\n\n #define UMOD_DEVICE \"ESP32\"                 \t\t\t // NOTE - Set your hardware here\n #define HARDWARE_VERSION \"1.0\"              \t\t\t // NOTE - Set your hardware version here\n #define UMOD_BME680X_SW_VERSION \"1.0.2\"     \t\t\t // NOTE - Version of the User Mod\n #define CALIB_FILE_NAME \"/BME680X-Calib.hex\"\t\t\t // NOTE - Calibration file name\n #define UMOD_NAME \"BME680X\"                 \t\t\t // NOTE - User module name\n #define UMOD_DEBUG_NAME \"UM-BME680X: \"      \t\t\t // NOTE - Debug print module name addon\n \n #define ESC \"\\033\"\n #define ESC_CSI ESC \"[\"\n #define ESC_STYLE_RESET ESC_CSI \"0m\"\n #define ESC_CURSOR_COLUMN(n) ESC_CSI #n \"G\"\n \n #define ESC_FGCOLOR_BLACK ESC_CSI \"30m\"\n #define ESC_FGCOLOR_RED ESC_CSI \"31m\"\n #define ESC_FGCOLOR_GREEN ESC_CSI \"32m\"\n #define ESC_FGCOLOR_YELLOW ESC_CSI \"33m\"\n #define ESC_FGCOLOR_BLUE ESC_CSI \"34m\"\n #define ESC_FGCOLOR_MAGENTA ESC_CSI \"35m\"\n #define ESC_FGCOLOR_CYAN ESC_CSI \"36m\"\n #define ESC_FGCOLOR_WHITE ESC_CSI \"37m\"\n #define ESC_FGCOLOR_DEFAULT ESC_CSI \"39m\"\n \n /* Debug Print Special Text */\n #define INFO_COLUMN ESC_CURSOR_COLUMN(60)\n #define GOGAB_OK INFO_COLUMN \"[\" ESC_FGCOLOR_GREEN \"OK\" ESC_STYLE_RESET \"]\"\n #define GOGAB_FAIL INFO_COLUMN \"[\" ESC_FGCOLOR_RED \"FAIL\" ESC_STYLE_RESET \"]\"\n #define GOGAB_WARN INFO_COLUMN \"[\" ESC_FGCOLOR_YELLOW \"WARN\" ESC_STYLE_RESET \"]\"\n #define GOGAB_DONE INFO_COLUMN \"[\" ESC_FGCOLOR_CYAN \"DONE\" ESC_STYLE_RESET \"]\"\n \n #include \"bsec.h\" // Bosch sensor library\n #include \"wled.h\"\n #include <Arduino.h>\n \n /* UsermodBME68X class definition */\n class UsermodBME68X : public Usermod {\n \n\t public:\n\t /* Public: Functions */\n\t uint16_t getId();\n\t void loop();\t\t\t\t\t\t\t\t// Loop of the user module called by wled main in loop\n\t void setup();\t\t\t\t\t\t\t\t// Setup of the user module called by wled main\n\t void addToConfig(JsonObject& root);\t\t\t// 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.\n\t void appendConfigData();\t\t\t\t\t// Adds extra info to the config page of weld\n\t bool readFromConfig(JsonObject& root);\t\t// Reads config values\n\t void addToJsonInfo(JsonObject& root);\t\t// Adds user module info to the weld info page\n \n\t /* Wled internal functions which can be used by the core or other user mods */\n\t inline float getTemperature();\t\t\t\t// Get Temperature in the selected scale of °C or °F\n\t inline float getHumidity();\t\t\t\t\t// ...\n\t inline float getPressure();\n\t inline float getGasResistance();\n\t inline float getAbsoluteHumidity();\n\t inline float getDewPoint();\n\t inline float getIaq();\n\t inline float getStaticIaq();\n\t inline float getCo2();\n\t inline float getVoc();\n\t inline float getGasPerc();\n\t inline uint8_t getIaqAccuracy();\n\t inline uint8_t getStaticIaqAccuracy();\n\t inline uint8_t getCo2Accuracy();\n\t inline uint8_t getVocAccuracy();\n\t inline uint8_t getGasPercAccuracy();\n\t inline bool getStabStatus();\n\t inline bool getRunInStatus();\n \n\t private:\n\t /* Private: Functions */\n\t void HomeAssistantDiscovery();\n\t void MQTT_PublishHASensor(const String& name, const String& deviceClass, const String& unitOfMeasurement, const int8_t& digs, const uint8_t& option = 0);\n\t void MQTT_publish(const char* topic, const float& value, const int8_t& dig);\n\t void onMqttConnect(bool sessionPresent);\n\t void checkIaqSensorStatus();\n\t void InfoHelper(JsonObject& root, const char* name, const float& sensorvalue, const int8_t& decimals, const char* unit);\n\t void InfoHelper(JsonObject& root, const char* name, const String& sensorvalue, const bool& status);\n\t void loadState();\n\t void saveState();\n\t void getValues();\n \n\t /*** V A R I A B L E s  &  C O N S T A N T s ***/\n\t /* Private: Settings of Usermod BME68X */\n\t struct settings_t {\n\t\t bool enabled;               \t\t\t\t\t// true if user module is active\n\t\t byte I2cadress; \t            \t\t\t\t// Depending on the manufacturer, the BME680 has the address 0x76 or 0x77\n\t\t uint8_t Interval; \t             \t\t\t\t// Interval of reading sensor data in seconds\n\t\t uint16_t MaxAge;\t            \t\t\t\t// Force the publication of the value of a sensor after these defined seconds at the latest\n\t\t bool pubAcc; \t        \t\t\t\t\t\t// Publish the accuracy values\n\t\t bool publishSensorState;  \t     \t\t\t\t// Publisch the sensor calibration state\n\t\t bool publishAfterCalibration ;  \t\t\t\t// 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\n\t\t bool PublischChange;\t\t       \t\t\t\t// Publish values even when they have not changed\n\t\t bool PublishIAQVerbal; \t\t     \t\t\t\t// Publish Index of Air Quality (IAQ) classification Verbal\n\t\t bool PublishStaticIAQVerbal;\t\t\t\t\t// Publish Static Index of Air Quality (Static IAQ) Verbal\n\t\t byte tempScale; \t               \t\t\t\t// 0 -> Use Celsius, 1-> Use Fahrenheit\n\t\t float tempOffset; \t             \t\t\t\t// Temperature Offset\n\t\t bool HomeAssistantDiscovery; \t\t\t\t\t// Publish Home Assistant Device Information\n\t\t bool pauseOnActiveWled ;\t\t\t\t\t\t// If this is set to true, the user mod ist not executed while wled is active\n\t\t \n\t\t /* Decimal Places (-1 means inactive) */\n\t\t struct decimals_t {\n\t\t\t int8_t temperature;\n\t\t\t int8_t humidity;\n\t\t\t int8_t pressure;\n\t\t\t int8_t gasResistance;\n\t\t\t int8_t absHumidity;\n\t\t\t int8_t drewPoint;\n\t\t\t int8_t iaq;\n\t\t\t int8_t staticIaq;\n\t\t\t int8_t co2;\n\t\t\t int8_t Voc;\n\t\t\t int8_t gasPerc;\n\t\t } decimals;\n\t } settings;\n \n\t /* Private: Flags */\n\t struct flags_t {\n\t\t bool InitSuccessful = false;  \t\t\t\t\t// Initialation was un-/successful\n\t\t bool MqttInitialized = false; \t\t\t\t\t// MQTT Initialation done flag (first MQTT Connect)\n\t\t bool SaveState = false;       \t\t\t\t\t// Save the calibration data flag\n\t\t bool DeleteCaibration = false;\t\t\t\t\t// If set the calib file will be deleted on the next round\n\t } flags;\n \n\t /* Private: Measurement timers */\n\t struct timer_t {\n\t\t long actual;  \t\t\t\t\t\t\t\t\t// Actual time stamp\n\t\t long lastRun; \t\t\t\t\t\t\t\t\t// Last measurement time stamp\n\t } timer;\n \n\t /* Private: Various variables */\n\t String stringbuff;                           \t\t// General string stringbuff buffer\n\t char charbuffer[128];                        \t\t// General char stringbuff buffer\n\t String InfoPageStatusLine;                   \t\t// Shown on the info page of WLED\n\t String tempScale;                            \t\t// °C or °F\n\t uint8_t bsecState[BSEC_MAX_STATE_BLOB_SIZE]; \t\t// Calibration data array\n\t uint16_t stateUpdateCounter; \t            \t\t// Save state couter\n\t static const uint8_t bsec_config_iaq[];\t\t\t\t// Calibration Buffer\n\t Bsec iaqSensor;\t\t\t\t\t\t\t\t\t\t// Sensor variable\n \n\t /* Private: Sensor values */\n\t struct values_t {\n\t\t float temperature;   \t\t\t\t\t\t\t// Temp [°C] (Sensor-compensated)\n\t\t float humidity;     \t\t\t\t\t\t\t// Relative humidity [%] (Sensor-compensated)\n\t\t float pressure;      \t\t\t\t\t\t\t// raw pressure [hPa]\n\t\t float gasResistance; \t\t\t\t\t\t\t// raw gas restistance [Ohm]\n\t\t float absHumidity; \t\t\t\t\t\t\t\t// UserMod calculated: Absolute Humidity [g/m³]\n\t\t float drewPoint;        \t\t\t\t\t\t// UserMod calculated: drew point [°C/°F]\n\t\t float iaq;           \t\t\t\t\t\t\t// IAQ (Indoor Air Quallity)\n\t\t float staticIaq;\t\t\t\t\t\t\t\t// Satic IAQ\n\t\t float co2;\t\t\t\t\t\t\t\t\t\t// CO2 [PPM]\n\t\t float Voc;\t\t\t\t\t\t\t\t\t\t// VOC in [PPM]\n\t\t float gasPerc;\t\t\t\t\t\t\t\t\t// Gas Percentage in [%]\n\t\t uint8_t iaqAccuracy; \t\t\t\t\t\t\t// IAQ accuracy -  IAQ Accuracy = 1 means value is inaccurate, IAQ Accuracy = 2 means sensor is being calibrated, IAQ Accuracy = 3 means sensor successfully calibrated.\n\t\t uint8_t staticIaqAccuracy;\t\t\t\t\t\t// Static IAQ accuracy\n\t\t uint8_t co2Accuracy;\t\t\t\t\t\t\t// co2 accuracy\n\t\t uint8_t VocAccuracy;\t\t\t\t\t\t\t// voc accuracy\n\t\t uint8_t gasPercAccuracy;\t\t\t\t\t\t// Gas percentage accuracy\n\t\t bool stabStatus;  \t\t\t\t\t\t\t\t// Indicates if the sensor is undergoing initial stabilization during its first use after production\n\t\t bool runInStatus; \t\t\t\t\t\t\t\t// Indicates when the sensor is ready after after switch-on\n\t } valuesA, valuesB, *ValuesPtr, *PrevValuesPtr, *swap; \t// Data Scructur A, Data Structur B, Pointers to switch between data channel A & B\n \n\t struct cvalues_t {\n\t\t String iaqVerbal;    \t\t\t\t\t\t\t// IAQ verbal\n\t\t String staticIaqVerbal; \t\t\t\t\t\t// Static IAQ verbal\n \n\t } cvalues;\n\t \n\t /* Private: Sensor settings */\n\t bsec_virtual_sensor_t sensorList[13] = {\n\t\t 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.\n\t\t BSEC_OUTPUT_STATIC_IAQ,                          // Unscaled Index for Air Quality estimate\n\t\t BSEC_OUTPUT_CO2_EQUIVALENT,                      // CO2 equivalent estimate [ppm]\n\t\t BSEC_OUTPUT_BREATH_VOC_EQUIVALENT,               // Breath VOC concentration estimate [ppm]\n\t\t 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.\n\t\t BSEC_OUTPUT_RAW_PRESSURE,                        // Pressure sensor signal [Pa] Pressure directly measured by the BME680 in Pa.\n\t\t 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.\n\t\t 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).\n\t\t 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).\n\t\t 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)\n\t\t 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.\n\t\t 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.\n\t\t BSEC_OUTPUT_GAS_PERCENTAGE                       // Percentage of min and max filtered gas value [%]\n\t };\n \n\t /*** V A R I A B L E s  &  C O N S T A N T s ***/\n\t /* Public: strings to reduce flash memory usage (used more than twice) */\n\t static const char _enabled[];\n\t static const char _hadtopic[];\n \n\t /* Public: Settings Strings*/\n\t static const char _nameI2CAdr[];\n\t static const char _nameInterval[];\n\t static const char _nameMaxAge[];\n\t static const char _namePubAc[];\n\t static const char _namePubSenState[];\n\t static const char _namePubAfterCalib[];\n\t static const char _namePublishChange[];\n\t static const char _nameTempScale[];\n\t static const char _nameTempOffset[];\n\t static const char _nameHADisc[];\n\t static const char _nameDelCalib[];\n \n\t /* Public: Sensor names / Sensor short names */\n\t static const char _nameTemp[];\n\t static const char _nameHum[];\n\t static const char _namePress[];\n\t static const char _nameGasRes[];\n\t static const char _nameAHum[];\n\t static const char _nameDrewP[];\n\t static const char _nameIaq[];\n\t static const char _nameIaqAc[];\n\t static const char _nameIaqVerb[];\n\t static const char _nameStaticIaq[];\n\t static const char _nameStaticIaqVerb[];\n\t static const char _nameStaticIaqAc[];\n\t static const char _nameCo2[];\n\t static const char _nameCo2Ac[];\n\t static const char _nameVoc[];\n\t static const char _nameVocAc[];\n\t static const char _nameComGasAc[];\n\t static const char _nameGasPer[];\n\t static const char _nameGasPerAc[];\n\t static const char _namePauseOnActWL[];\n \n\t static const char _nameStabStatus[];\n\t static const char _nameRunInStatus[];\n \n\t /* Public: Sensor Units */\n\t static const char _unitTemp[];\n\t static const char _unitHum[];\n\t static const char _unitPress[];\n\t static const char _unitGasres[];\n\t static const char _unitAHum[];\n\t static const char _unitDrewp[];\n\t static const char _unitIaq[];\n\t static const char _unitStaticIaq[];\n\t static const char _unitCo2[];\n\t static const char _unitVoc[];\n\t static const char _unitGasPer[];\n\t static const char _unitNone[];\n \n\t static const char _unitCelsius[];\n\t static const char _unitFahrenheit[];\n }; // UsermodBME68X class definition End\n \n /*** Setting C O N S T A N T S ***/\n /* Private: Settings Strings*/\n const char UsermodBME68X::_enabled[] \t\t\tPROGMEM = \"Enabled\";\n const char UsermodBME68X::_hadtopic[] \t\t\tPROGMEM = \"homeassistant/sensor/\";\n \n const char UsermodBME68X::_nameI2CAdr[] \t\tPROGMEM = \"i2C Address\";\n const char UsermodBME68X::_nameInterval[] \t\tPROGMEM = \"Interval\";\n const char UsermodBME68X::_nameMaxAge[] \t\tPROGMEM = \"Max Age\";\n const char UsermodBME68X::_namePublishChange[] \tPROGMEM = \"Pub changes only\";\n const char UsermodBME68X::_namePubAc[] \t\t\tPROGMEM = \"Pub Accuracy\";\n const char UsermodBME68X::_namePubSenState[] \tPROGMEM = \"Pub Calib State\";\n const char UsermodBME68X::_namePubAfterCalib[] \tPROGMEM = \"Pub After Calib\";\n const char UsermodBME68X::_nameTempScale[] \t\tPROGMEM = \"Temp Scale\";\n const char UsermodBME68X::_nameTempOffset[] \tPROGMEM = \"Temp Offset\";\n const char UsermodBME68X::_nameHADisc[] \t\tPROGMEM = \"HA Discovery\";\n const char UsermodBME68X::_nameDelCalib[] \t\tPROGMEM = \"Del Calibration Hist\";\n const char UsermodBME68X::_namePauseOnActWL[] \tPROGMEM = \"Pause while WLED active\";\n \n /* Private: Sensor names / Sensor short name */\n const char UsermodBME68X::_nameTemp[] \t\t\tPROGMEM = \"Temperature\";\n const char UsermodBME68X::_nameHum[] \t\t\tPROGMEM = \"Humidity\";\n const char UsermodBME68X::_namePress[] \t\t\tPROGMEM = \"Pressure\";\n const char UsermodBME68X::_nameGasRes[] \t\tPROGMEM = \"Gas-Resistance\";\n const char UsermodBME68X::_nameAHum[] \t\t\tPROGMEM = \"Absolute-Humidity\";\n const char UsermodBME68X::_nameDrewP[] \t\t\tPROGMEM = \"Drew-Point\";\n const char UsermodBME68X::_nameIaq[] \t\t\tPROGMEM = \"IAQ\";\n const char UsermodBME68X::_nameIaqVerb[] \t\tPROGMEM = \"IAQ-Verbal\";\n const char UsermodBME68X::_nameStaticIaq[] \t\tPROGMEM = \"Static-IAQ\";\n const char UsermodBME68X::_nameStaticIaqVerb[] \tPROGMEM = \"Static-IAQ-Verbal\";\n const char UsermodBME68X::_nameCo2[] \t\t\tPROGMEM = \"CO2\";\n const char UsermodBME68X::_nameVoc[] \t\t\tPROGMEM = \"VOC\";\n const char UsermodBME68X::_nameGasPer[] \t\tPROGMEM = \"Gas-Percentage\";\n const char UsermodBME68X::_nameIaqAc[] \t\t\tPROGMEM = \"IAQ-Accuracy\";\n const char UsermodBME68X::_nameStaticIaqAc[] \tPROGMEM = \"Static-IAQ-Accuracy\";\n const char UsermodBME68X::_nameCo2Ac[] \t\t\tPROGMEM = \"CO2-Accuracy\";\n const char UsermodBME68X::_nameVocAc[] \t\t\tPROGMEM = \"VOC-Accuracy\";\n const char UsermodBME68X::_nameGasPerAc[] \t\tPROGMEM = \"Gas-Percentage-Accuracy\";\n const char UsermodBME68X::_nameStabStatus[] \tPROGMEM = \"Stab-Status\";\n const char UsermodBME68X::_nameRunInStatus[] \tPROGMEM = \"Run-In-Status\";\n \n /* Private Units */\n const char UsermodBME68X::_unitTemp[] \t\t\tPROGMEM = \" \"; \t\t\t\t// NOTE - Is set with the selectable temperature unit\n const char UsermodBME68X::_unitHum[] \t\t\tPROGMEM = \"%\";\n const char UsermodBME68X::_unitPress[] \t\t\tPROGMEM = \"hPa\";\n const char UsermodBME68X::_unitGasres[] \t\tPROGMEM = \"kΩ\";\n const char UsermodBME68X::_unitAHum[] \t\t\tPROGMEM = \"g/m³\";\n const char UsermodBME68X::_unitDrewp[] \t\t\tPROGMEM = \" \"; \t\t\t\t// NOTE - Is set with the selectable temperature unit\n const char UsermodBME68X::_unitIaq[] \t\t\tPROGMEM = \" \";   \t\t\t// No unit\n const char UsermodBME68X::_unitStaticIaq[] \t\tPROGMEM = \" \";\t\t\t\t// No unit\n const char UsermodBME68X::_unitCo2[] \t\t\tPROGMEM = \"ppm\";\n const char UsermodBME68X::_unitVoc[] \t\t\tPROGMEM = \"ppm\"; \t\t\t\n const char UsermodBME68X::_unitGasPer[] \t\tPROGMEM = \"%\";\n const char UsermodBME68X::_unitNone[] \t\t\tPROGMEM = \"\";\n \n const char UsermodBME68X::_unitCelsius[] \t\tPROGMEM = \"°C\";\t\t\t\t// Symbol for Celsius\n const char UsermodBME68X::_unitFahrenheit[] \tPROGMEM = \"°F\";\t\t\t\t// Symbol for Fahrenheit\n \n /* Load Sensor Settings */\n const uint8_t UsermodBME68X::bsec_config_iaq[] = { \n\t #include \"config/generic_33v_3s_28d/bsec_iaq.txt\"\t\t// Allow 28 days for calibration because the WLED module normally stays in the same place anyway\n }; \t\n \n \n /************************************************************************************************************/\n /********************************************* M A I N  C O D E *********************************************/\n /************************************************************************************************************/\n \n /**\n  * @brief Called by WLED: Setup of the usermod\n  */\n void UsermodBME68X::setup() {\n\t DEBUG_PRINTLN(F(UMOD_DEBUG_NAME ESC_FGCOLOR_CYAN \"Initialize\" ESC_STYLE_RESET));\n \n\t /* Check, if i2c is activated */\n\t if (i2c_scl < 0 || i2c_sda < 0) {\n\t\t settings.enabled = false;\t\t\t\t\t\t\t\t\t\t\t\t// Disable usermod once i2c is not running\n\t\t DEBUG_PRINTLN(F(UMOD_DEBUG_NAME \"I2C is not activated. Please activate I2C first.\" GOGAB_FAIL));\n\t\t return;\n\t }\n \n\t flags.InitSuccessful = true; \t\t\t\t\t\t\t\t\t\t\t\t// Will be set to false on need\n \n\t /* Set data structure pointers */\n\t ValuesPtr = &valuesA;   \n\t PrevValuesPtr = &valuesB;\n \n\t /* Init Library*/\n\t iaqSensor.begin(settings.I2cadress, Wire); \t\t\t\t\t\t\t\t\t// BME68X_I2C_ADDR_LOW\n\t stringbuff = \"BSEC library version \" + String(iaqSensor.version.major) + \".\" + String(iaqSensor.version.minor) + \".\" + String(iaqSensor.version.major_bugfix) + \".\" + String(iaqSensor.version.minor_bugfix);\n\t DEBUG_PRINT(F(UMOD_NAME));\n\t DEBUG_PRINTLN(F(stringbuff.c_str()));\n \n\t /* Init Sensor*/\n\t iaqSensor.setConfig(bsec_config_iaq);\n\t iaqSensor.updateSubscription(sensorList, 13, BSEC_SAMPLE_RATE_LP);\n\t iaqSensor.setTPH(BME68X_OS_2X, BME68X_OS_16X, BME68X_OS_1X); \t\t\t\t// Set the temperature, Pressure and Humidity over-sampling\n\t iaqSensor.setTemperatureOffset(settings.tempOffset);         \t\t\t\t// set the temperature offset in degree Celsius\n\t loadState();                                                 \t\t\t\t// Load the old calibration data\n\t checkIaqSensorStatus();                                      \t\t\t\t// Check the sensor status\n\t // HomeAssistantDiscovery();\n\t DEBUG_PRINTLN(F(INFO_COLUMN GOGAB_DONE));\n }\n \n /**\n  * @brief Called by WLED: Main loop called by WLED\n  *\n  */\n void UsermodBME68X::loop() {\n\t if (!settings.enabled || strip.isUpdating() || !flags.InitSuccessful) return;\t\t\t\t\t\t// Leave if not enabled or string is updating or init failed\n \n\t if (settings.pauseOnActiveWled && strip.getBrightness()) return;\t\t\t\t\t\t\t\t\t// Workarround Known Issue: handing led update - Leave once pause on activ wled is active and wled is active\n \n\t timer.actual = millis(); \t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// Timer to fetch new temperature, humidity and pressure data at intervals\n \n\t if (timer.actual - timer.lastRun >= settings.Interval * 1000) {\n\t\t timer.lastRun = timer.actual;\n \n\t\t /* Get the sonsor measurments and publish them */\n\t\t if (iaqSensor.run()) {\t\t\t\t// iaqSensor.run()\n\t\t\t getValues();\t// Get the new values\n \n\t\t\t if (ValuesPtr->temperature   != PrevValuesPtr->temperature   || !settings.PublischChange) {\t\t\t\t// NOTE - negative dig means inactive\n\t\t\t\t MQTT_publish(_nameTemp,   ValuesPtr->temperature, \t  settings.decimals.temperature);\t\t\t\t\t\n\t\t\t }\n\t\t\t if (ValuesPtr->humidity      != PrevValuesPtr->humidity      || !settings.PublischChange) {\n\t\t\t\t MQTT_publish(_nameHum,    ValuesPtr->humidity, \t\t  settings.decimals.humidity);\n\t\t\t }\n\t\t\t if (ValuesPtr->pressure      != PrevValuesPtr->pressure      || !settings.PublischChange) {\n\t\t\t\t MQTT_publish(_namePress,  ValuesPtr->pressure, \t\t  settings.decimals.humidity);\n\t\t\t }\n\t\t\t if (ValuesPtr->gasResistance != PrevValuesPtr->gasResistance || !settings.PublischChange) {\n\t\t\t\t  MQTT_publish(_nameGasRes, ValuesPtr->gasResistance,   settings.decimals.gasResistance);\n\t\t\t }\n\t\t\t if (ValuesPtr->absHumidity \t != PrevValuesPtr->absHumidity || !settings.PublischChange) {\n\t\t\t\t  MQTT_publish(_nameAHum,   PrevValuesPtr->absHumidity, settings.decimals.absHumidity);\n\t\t\t }\n\t\t\t if (ValuesPtr->drewPoint \t !=  PrevValuesPtr->drewPoint || !settings.PublischChange) {\n\t\t\t\t MQTT_publish(_nameDrewP,  PrevValuesPtr->drewPoint,   settings.decimals.drewPoint);\n\t\t\t }\n\t\t\t if (ValuesPtr->iaq != PrevValuesPtr->iaq || !settings.PublischChange) {\n\t\t\t\t MQTT_publish(_nameIaq, ValuesPtr->iaq, settings.decimals.iaq);\n\t\t\t\t if (settings.pubAcc) MQTT_publish(_nameIaqAc, \t\tValuesPtr->iaqAccuracy, \t\t\t0);\n\t\t\t\t if (settings.decimals.iaq>-1) {\n\t\t\t\t\t if (settings.PublishIAQVerbal) {\n\t\t\t\t\t\t if \t\t(ValuesPtr->iaq <= 50) \tcvalues.iaqVerbal = F(\"Excellent\");\n\t\t\t\t\t\t else if (ValuesPtr->iaq <= 100) cvalues.iaqVerbal = F(\"Good\");\n\t\t\t\t\t\t else if (ValuesPtr->iaq <= 150) cvalues.iaqVerbal = F(\"Lightly polluted\");\n\t\t\t\t\t\t else if (ValuesPtr->iaq <= 200) cvalues.iaqVerbal = F(\"Moderately polluted\");\n\t\t\t\t\t\t else if (ValuesPtr->iaq <= 250) cvalues.iaqVerbal = F(\"Heavily polluted\");\n\t\t\t\t\t\t else if (ValuesPtr->iaq <= 350)\tcvalues.iaqVerbal = F(\"Severely polluted\");\n\t\t\t\t\t\t else \t\t\t\t\t\t\tcvalues.iaqVerbal = F(\"Extremely polluted\");\n\t\t\t\t\t\t snprintf_P(charbuffer, 127, PSTR(\"%s/%s\"), mqttDeviceTopic, _nameIaqVerb);\n\t\t\t\t\t\t if (WLED_MQTT_CONNECTED) mqtt->publish(charbuffer, 0, false, cvalues.iaqVerbal.c_str());\n\t\t\t\t\t }\n\t\t\t\t }\n\t\t\t }\t\t\t\n\t\t\t if (ValuesPtr->staticIaq != PrevValuesPtr->staticIaq || !settings.PublischChange) {\n\t\t\t\t MQTT_publish(_nameStaticIaq, ValuesPtr->staticIaq, settings.decimals.staticIaq);\n\t\t\t\t if (settings.pubAcc) MQTT_publish(_nameStaticIaqAc, \tValuesPtr->staticIaqAccuracy, \t0);\t\t\t\n\t\t\t\t if (settings.decimals.staticIaq>-1) {\n\t\t\t\t\t if (settings.PublishIAQVerbal) {\n\t\t\t\t\t\t if \t\t(ValuesPtr->staticIaq <= 50)  cvalues.staticIaqVerbal = F(\"Excellent\");\n\t\t\t\t\t\t else if (ValuesPtr->staticIaq <= 100) cvalues.staticIaqVerbal = F(\"Good\");\n\t\t\t\t\t\t else if (ValuesPtr->staticIaq <= 150) cvalues.staticIaqVerbal = F(\"Lightly polluted\");\n\t\t\t\t\t\t else if (ValuesPtr->staticIaq <= 200) cvalues.staticIaqVerbal = F(\"Moderately polluted\");\n\t\t\t\t\t\t else if (ValuesPtr->staticIaq <= 250) cvalues.staticIaqVerbal = F(\"Heavily polluted\");\n\t\t\t\t\t\t else if (ValuesPtr->staticIaq <= 350) cvalues.staticIaqVerbal = F(\"Severely polluted\");\n\t\t\t\t\t\t else \t\t\t\t\t\t\t\t  cvalues.staticIaqVerbal = F(\"Extremely polluted\");\n\t\t\t\t\t\t snprintf_P(charbuffer, 127, PSTR(\"%s/%s\"), mqttDeviceTopic, _nameStaticIaqVerb);\n\t\t\t\t\t\t if (WLED_MQTT_CONNECTED) mqtt->publish(charbuffer, 0, false, cvalues.staticIaqVerbal.c_str());\n\t\t\t\t\t }\n\t\t\t\t }\t\t\t\n\t\t\t }\n\t\t\t if (ValuesPtr->co2 != PrevValuesPtr->co2 || !settings.PublischChange) {\n\t\t\t\t MQTT_publish(_nameCo2, \t\t\tValuesPtr->co2, \t\t\t\tsettings.decimals.co2);\n\t\t\t\t if (settings.pubAcc) MQTT_publish(_nameCo2Ac, \t\tValuesPtr->co2Accuracy, \t\t\t0);\n\t\t\t }\n\t\t\t if (ValuesPtr->Voc != PrevValuesPtr->Voc || !settings.PublischChange) {\n\t\t\t\t MQTT_publish(_nameVoc, \t\t\tValuesPtr->Voc, \t\t\t\tsettings.decimals.Voc);\n\t\t\t\t if (settings.pubAcc) MQTT_publish(_nameVocAc, \t\tValuesPtr->VocAccuracy, \t\t\t0);\n\t\t\t }\n\t\t\t if (ValuesPtr->gasPerc != PrevValuesPtr->gasPerc || !settings.PublischChange) {\n\t\t\t\t MQTT_publish(_nameGasPer, \t\tValuesPtr->gasPerc, \t\t\tsettings.decimals.gasPerc);\n\t\t\t\t if (settings.pubAcc) MQTT_publish(_nameGasPerAc, \tValuesPtr->gasPercAccuracy, \t\t0);\n\t\t\t }\n \n\t\t\t /**** Publish Sensor State Entrys *****/\n\t\t\t if ((ValuesPtr->stabStatus != PrevValuesPtr->stabStatus || !settings.PublischChange) && settings.publishSensorState) \t\t\tMQTT_publish(_nameStabStatus, \tValuesPtr->stabStatus, 0);\n\t\t\t if ((ValuesPtr->runInStatus != PrevValuesPtr->runInStatus || !settings.PublischChange) && settings.publishSensorState) \t\t\tMQTT_publish(_nameRunInStatus, \tValuesPtr->runInStatus, 0);\n \n\t\t\t /* Check accuracies - if accurasy level 3 is reached -> save calibration data */\n\t\t\t if ((ValuesPtr->iaqAccuracy != PrevValuesPtr->iaqAccuracy) \t\t\t\t&& ValuesPtr->iaqAccuracy == 3) \t\tflags.SaveState = true; \t\t\t\t\t\t// Save after calibration / recalibration\n\t\t\t if ((ValuesPtr->staticIaqAccuracy != PrevValuesPtr->staticIaqAccuracy) \t&& ValuesPtr->staticIaqAccuracy == 3) \tflags.SaveState = true;\n\t\t\t if ((ValuesPtr->co2Accuracy != PrevValuesPtr->co2Accuracy) \t\t\t\t&& ValuesPtr->co2Accuracy == 3) \t\tflags.SaveState = true;\n\t\t\t if ((ValuesPtr->VocAccuracy != PrevValuesPtr->VocAccuracy) \t\t\t\t&& ValuesPtr->VocAccuracy == 3) \t\tflags.SaveState = true;\n\t\t\t if ((ValuesPtr->gasPercAccuracy != PrevValuesPtr->gasPercAccuracy) \t\t&& ValuesPtr->gasPercAccuracy == 3) \tflags.SaveState = true;\n \n\t\t\t if (flags.SaveState) saveState(); \t\t\t\t\t\t\t\t\t\t\t\t\t// Save if the save state flag is set\n\t\t }\n\t }\n }\n \n /**\n  * @brief Retrieves the sensor data and truncates it to the requested decimal places\n  * \n  */\n void UsermodBME68X::getValues() {\n\t /* Swap the point to the data structures */\n\t swap = PrevValuesPtr;\n\t PrevValuesPtr = ValuesPtr;\n\t ValuesPtr = swap;\n \n\t /* Float Values */\n\t ValuesPtr->temperature = \troundf(iaqSensor.temperature * \t\t\tpowf(10, settings.decimals.temperature)) /\tpowf(10, settings.decimals.temperature);\t\n\t ValuesPtr->humidity = \t\troundf(iaqSensor.humidity * \t\t\tpowf(10, settings.decimals.humidity)) / \tpowf(10, settings.decimals.humidity);\n\t ValuesPtr->pressure = \t\troundf(iaqSensor.pressure * \t\t\tpowf(10, settings.decimals.pressure)) / \tpowf(10, settings.decimals.pressure)\t\t/100;      \t// Pa 2 hPa\n\t ValuesPtr->gasResistance = \troundf(iaqSensor.gasResistance * \t\tpowf(10, settings.decimals.gasResistance)) /powf(10, settings.decimals.gasResistance)\t/1000;\t\t// Ohm 2 KOhm\n\t ValuesPtr->iaq = \t\t\troundf(iaqSensor.iaq * \t\t\t\t\tpowf(10, settings.decimals.iaq)) / \t\t\tpowf(10, settings.decimals.iaq);   \t\t\t\n\t ValuesPtr->staticIaq = \t\troundf(iaqSensor.staticIaq * \t\t\tpowf(10, settings.decimals.staticIaq)) / \tpowf(10, settings.decimals.staticIaq);\t\t\t\t\t\n\t ValuesPtr->co2 = \t\t\troundf(iaqSensor.co2Equivalent * \t\tpowf(10, settings.decimals.co2)) / \t\t\tpowf(10, settings.decimals.co2);\n\t ValuesPtr->Voc = \t\t\troundf(iaqSensor.breathVocEquivalent * \tpowf(10, settings.decimals.Voc)) / \t\t\tpowf(10, settings.decimals.Voc);\n\t ValuesPtr->gasPerc = \t\troundf(iaqSensor.gasPercentage * \t\tpowf(10, settings.decimals.gasPerc)) /\t\tpowf(10, settings.decimals.gasPerc);\n \n\t /* Calculate Absolute Humidity [g/m³] */\n\t if (settings.decimals.absHumidity>-1) {\n\t\t const float mw = 18.01534;                                                                                                                                                                   \t\t\t\t// molar mass of water g/mol\n\t\t const float r = 8.31447215;                                                                                                                                                                  \t\t\t\t// Universal gas constant J/mol/K\n\t\t ValuesPtr->absHumidity = (6.112 * powf(2.718281828, (17.67 * ValuesPtr->temperature) / (ValuesPtr->temperature + 243.5)) * ValuesPtr->humidity * mw) / ((273.15 + ValuesPtr->temperature) * r); \t\t// in ppm\n\t }\n\t /* Calculate Drew Point (C°) */\n\t if (settings.decimals.drewPoint>-1) {\n\t\t 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))));\n\t }\n \n\t /* Convert to Fahrenheit when selected */\n\t if (settings.tempScale) {\t\t\t\t\t\t\t\t\t\t\t\t// settings.tempScale = 0 => Celsius, = 1 => Fahrenheit\n\t\t ValuesPtr->temperature =  ValuesPtr->temperature * 1.8 + 32;\t\t// Value stored in Fahrenheit\n\t\t ValuesPtr->drewPoint = ValuesPtr->drewPoint * 1.8 + 32;\t\n\t } \n \n\t /* Integer Values */\n\t ValuesPtr->iaqAccuracy = \t\tiaqSensor.iaqAccuracy; \t\n\t ValuesPtr->staticIaqAccuracy = \tiaqSensor.staticIaqAccuracy;\n\t ValuesPtr->co2Accuracy = \t\tiaqSensor.co2Accuracy;\n\t ValuesPtr->VocAccuracy = \t\tiaqSensor.breathVocAccuracy;\n\t ValuesPtr->gasPercAccuracy = \tiaqSensor.gasPercentageAccuracy;\n\t ValuesPtr->stabStatus = \t\tiaqSensor.stabStatus;\t\t\n\t ValuesPtr->runInStatus = \t\tiaqSensor.runInStatus;\n }\n \n \n /**\n  * @brief Sends the current sensor data via MQTT\n  * @param topic Suptopic of the sensor as const char\n  * @param value Current sensor value as float\n  */\n void UsermodBME68X::MQTT_publish(const char* topic, const float& value, const int8_t& dig) {\n\t if (dig<0) return;\n\t if (WLED_MQTT_CONNECTED) {\n\t\t snprintf_P(charbuffer, 127, PSTR(\"%s/%s\"), mqttDeviceTopic, topic);\n\t\t mqtt->publish(charbuffer, 0, false, String(value, dig).c_str());\n\t }\n }\n \n /**\n  * @brief Called by WLED: Initialize the MQTT parts when the connection to the MQTT server is established.\n  * @param bool Session Present\n  */\n void UsermodBME68X::onMqttConnect(bool sessionPresent) {\n\t DEBUG_PRINTLN(UMOD_DEBUG_NAME \"OnMQTTConnect event fired\");\n\t HomeAssistantDiscovery();\n \n\t if (!flags.MqttInitialized) {\n\t\t flags.MqttInitialized=true;\n\t\t DEBUG_PRINTLN(UMOD_DEBUG_NAME \"MQTT first connect\");\n\t }\n }\n \n \n /**\n  * @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.\n  */\n void UsermodBME68X::HomeAssistantDiscovery() {\n\t if (!settings.HomeAssistantDiscovery || !flags.InitSuccessful || !settings.enabled) return; \t\t\t\t\t\t\t\t\t// Leave once HomeAssistant Discovery is inactive\n \n\t DEBUG_PRINTLN(UMOD_DEBUG_NAME ESC_FGCOLOR_CYAN \"Creating HomeAssistant Discovery Mqtt-Entrys\" ESC_STYLE_RESET);\n \n\t /* Sensor Values */\n\t MQTT_PublishHASensor(_nameTemp,  \t\t\"TEMPERATURE\", \t\t\t\ttempScale.c_str(), \tsettings.decimals.temperature\t\t); \t\t// Temperature\n\t MQTT_PublishHASensor(_namePress, \t\t\"ATMOSPHERIC_PRESSURE\", \t_unitPress, \t\tsettings.decimals.pressure\t\t\t); \t\t// Pressure\n\t MQTT_PublishHASensor(_nameHum, \t\t\t\"HUMIDITY\", \t\t\t\t_unitHum, \t\t\tsettings.decimals.humidity\t\t\t); \t\t// Humidity\n\t MQTT_PublishHASensor(_nameGasRes,\t\t\"GAS\", \t\t\t\t\t\t_unitGasres, \t\tsettings.decimals.gasResistance\t\t); \t\t// There is no device class for resistance in HA yet: https://developers.home-assistant.io/docs/core/entity/sensor/\n\t MQTT_PublishHASensor(_nameAHum,\t\t\t\"HUMIDITY\", \t\t\t\t_unitAHum, \t\t\tsettings.decimals.absHumidity\t\t); \t\t// Absolute Humidity\n\t MQTT_PublishHASensor(_nameDrewP,\t\t\"TEMPERATURE\", \t\t\t\ttempScale.c_str(), \tsettings.decimals.drewPoint\t\t\t); \t\t// Drew Point\n\t MQTT_PublishHASensor(_nameIaq,\t\t\t\"AQI\", \t\t\t\t\t\t_unitIaq, \t\t\tsettings.decimals.iaq\t\t\t\t); \t\t// IAQ\n\t MQTT_PublishHASensor(_nameIaqVerb,\t\t\"\", \t\t\t\t\t\t_unitNone,\t\t\tsettings.PublishIAQVerbal, \t\t\t2); \t// IAQ Verbal / Set Option 2 (text sensor)\n\t MQTT_PublishHASensor(_nameStaticIaq,\t\"AQI\",\t\t\t\t\t\t_unitNone, \t\t\tsettings.decimals.staticIaq\t\t\t); \t\t// Static IAQ\n\t MQTT_PublishHASensor(_nameStaticIaqVerb, \"\", \t\t\t\t\t\t_unitNone, \t\t\tsettings.PublishStaticIAQVerbal, \t2); \t// IAQ Verbal / Set Option 2 (text sensor\n\t MQTT_PublishHASensor(_nameCo2,\t\t\t\"CO2\", \t\t\t\t\t\t_unitCo2, \t\t\tsettings.decimals.co2\t\t\t\t); \t\t// CO2\n\t MQTT_PublishHASensor(_nameVoc,\t\t\t\"VOLATILE_ORGANIC_COMPOUNDS\", _unitVoc, \t\tsettings.decimals.Voc\t\t\t\t); \t\t// VOC\n\t MQTT_PublishHASensor(_nameGasPer,\t\t\"AQI\", \t\t\t\t\t\t_unitGasPer, \t\tsettings.decimals.gasPerc\t\t\t); \t\t// Gas %\n \n\t /* Accuracys - switched off once publishAccuracy=0 or the main value is switched of by digs set to a negative number */\n\t MQTT_PublishHASensor(_nameIaqAc,\t\t\"AQI\", \t\t\t\t\t\t_unitNone, \t\t\tsettings.pubAcc - 1 + settings.decimals.iaq * settings.pubAcc, \t\t\t1); \t// Option 1: Diagnostics Sektion\n\t MQTT_PublishHASensor(_nameStaticIaqAc,\t\"\", \t\t\t\t\t\t_unitNone, \t\t\tsettings.pubAcc - 1 + settings.decimals.staticIaq * settings.pubAcc, \t1);\n\t MQTT_PublishHASensor(_nameCo2Ac,\t\t\"\", \t\t\t\t\t\t_unitNone, \t\t\tsettings.pubAcc - 1 + settings.decimals.co2 * settings.pubAcc, \t\t\t1);\n\t MQTT_PublishHASensor(_nameVocAc,\t\t\"\", \t\t\t\t\t\t_unitNone, \t\t\tsettings.pubAcc - 1 + settings.decimals.Voc * settings.pubAcc, \t\t\t1);\n\t MQTT_PublishHASensor(_nameGasPerAc,\t\t\"\", \t\t\t\t\t\t_unitNone, \t\t\tsettings.pubAcc - 1 + settings.decimals.gasPerc * settings.pubAcc, \t\t1);\n\t \n\t MQTT_PublishHASensor(_nameStabStatus,\t\"\", \t\t\t\t\t\t_unitNone, \t\t\tsettings.publishSensorState - 1, 1);\n\t MQTT_PublishHASensor(_nameRunInStatus,\t\"\", \t\t\t\t\t\t_unitNone, \t\t\tsettings.publishSensorState - 1, 1);\n \n\t DEBUG_PRINTLN(UMOD_DEBUG_NAME GOGAB_DONE);\n }\n \n /**\n  * @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.\n  *        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.\n  *        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,\n  *        even though it is one device. I therefore only use the MQTT client name set in WLED here.\n  * @param name Name of the sensor\n  * @param topic Topic of the live sensor data\n  * @param unitOfMeasurement Unit of the measurment\n  * @param digs Number of decimal places\n  * @param option Set to true if the sensor is part of diagnostics (dafault 0)\n  */\n void UsermodBME68X::MQTT_PublishHASensor(const String& name, const String& deviceClass, const String& unitOfMeasurement, const int8_t& digs, const uint8_t& option) {\n\t DEBUG_PRINT(UMOD_DEBUG_NAME \"\\t\" + name);\n\t \n\t snprintf_P(charbuffer, 127, PSTR(\"%s/%s\"), mqttDeviceTopic, name.c_str());\t\t\t\t// Current values will be posted here\n\t String basetopic = String(_hadtopic) + mqttClientID + F(\"/\") + name + F(\"/config\");   \t// This is the place where Home Assinstant Discovery will check for new devices\n \n\t if (digs < 0) { // if digs are set to -1 -> entry deactivated\n\t\t /* Delete MQTT Entry */\n\t\t if (WLED_MQTT_CONNECTED) {\n\t\t\t mqtt->publish(basetopic.c_str(), 0, true, \"\");\t\t\t\t\t\t\t// Send emty entry to delete\n\t\t\t DEBUG_PRINTLN(INFO_COLUMN \"deleted\");\n\t\t }\n\t } else {\n\t\t /* Create all the necessary HAD MQTT entrys - see: https://www.home-assistant.io/integrations/sensor.mqtt/#configuration-variables */\n\t\t DynamicJsonDocument jdoc(700); // json document\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t // See: https://www.home-assistant.io/integrations/mqtt/\n\t\t JsonObject avail = jdoc.createNestedObject(F(\"avty\"));\t\t\t\t\t\t// 'avty': 'availability'\n\t\t avail[F(\"topic\")] = mqttDeviceTopic + String(\"/status\"); // An MQTT topic subscribed to receive availability (online/offline) updates.\n\t\t avail[F(\"payload_available\")] = \"online\";\n\t\t avail[F(\"payload_not_available\")] = \"offline\";\n\t\t 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.\n\t\t device[F(\"name\")] = serverDescription;\n\t\t device[F(\"identifiers\")] = String(mqttClientID);\n\t\t device[F(\"manufacturer\")] = F(\"WLED\");\n\t\t device[F(\"model\")] = UMOD_DEVICE;\n\t\t device[F(\"sw_version\")] = versionString;\n\t\t device[F(\"hw_version\")] = F(HARDWARE_VERSION);\n \n\t\t if (deviceClass != \"\") jdoc[F(\"device_class\")] = deviceClass; \t\t\t\t\t\t// The type/class of the sensor to set the icon in the frontend. The device_class can be null\n\t\t if (option == 1) jdoc[F(\"entity_category\")] = \"diagnostic\"; \t\t\t\t\t\t// Option 1: The category of the entity | When set, the entity category must be diagnostic for sensors.\n\t\t if (option == 2) jdoc[F(\"mode\")] = \"text\";                             \t\t\t\t// Option 2: Set text mode |\n\t\t jdoc[F(\"expire_after\")] = 1800;           \t\t\t\t\t\t\t\t\t\t\t// 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.\n\t\t jdoc[F(\"name\")] = name; \t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// The name of the MQTT sensor. Without server/module/device name. The device name will be added by HomeAssinstant anyhow\n\t\t if (unitOfMeasurement != \"\") jdoc[F(\"state_class\")] = \"measurement\";        \t\t// 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).\n\t\t jdoc[F(\"state_topic\")] = charbuffer;                      \t\t\t\t\t\t\t// 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.\n\t\t jdoc[F(\"unique_id\")] = String(mqttClientID) + \"-\" + name; \t\t\t\t\t\t\t// An ID that uniquely identifies this sensor. If two sensors have the same unique ID, Home Assistant will raise an exception.\n\t\t if (unitOfMeasurement != \"\") jdoc[F(\"unit_of_measurement\")] = unitOfMeasurement; \t// Defines the units of measurement of the sensor, if any. The unit_of_measurement can be null.\n \n\t\t DEBUG_PRINTF(\" (%d bytes)\", jdoc.memoryUsage());\n \n\t\t stringbuff = \"\";                                                            \t\t// clear string buffer\n\t\t serializeJson(jdoc, stringbuff); \t\t\t\t\t\t\t\t\t\t\t\t\t// JSON to String\n \n\t\t if (WLED_MQTT_CONNECTED) {                                         \t\t\t\t\t// Check if MQTT Connected, otherwise it will crash the 8266\n\t\t\t mqtt->publish(basetopic.c_str(), 0, true, stringbuff.c_str()); \t\t\t\t\t// Publish the HA discovery sensor entry\n\t\t\t DEBUG_PRINTLN(INFO_COLUMN \"published\");\n\t\t }\n\t }\n }\n \n /**\n  * @brief Called by WLED: Publish Sensor Information to Info Page\n  * @param JsonObject Pointer\n  */\n void UsermodBME68X::addToJsonInfo(JsonObject& root) {\n\t //DEBUG_PRINTLN(F(UMOD_DEBUG_NAME \"Add to info event\"));\n\t JsonObject user = root[F(\"u\")];\n \n\t if (user.isNull())\n\t\t user = root.createNestedObject(F(\"u\"));\n \n\t if (!flags.InitSuccessful) {\n\t\t // Init was not seccessful - let the user know\n\t\t JsonArray temperature_json = user.createNestedArray(F(\"BME68X Sensor\"));\n\t\t temperature_json.add(F(\"not found\"));\n\t\t JsonArray humidity_json = user.createNestedArray(F(\"BMW68x Reason\"));\n\t\t humidity_json.add(InfoPageStatusLine);\n\t }\n\t else if (!settings.enabled) {\n\t\t JsonArray temperature_json = user.createNestedArray(F(\"BME68X Sensor\"));\n\t\t temperature_json.add(F(\"disabled\"));\n\t }\n\t else {\n\t\t InfoHelper(user, _nameTemp, \t\tValuesPtr->temperature, \t\tsettings.decimals.temperature, \t\ttempScale.c_str());\n\t\t InfoHelper(user, _nameHum, \t\t\tValuesPtr->humidity, \t\t\tsettings.decimals.humidity, \t\t_unitHum);\n\t\t InfoHelper(user, _namePress, \t\tValuesPtr->pressure, \t\t\tsettings.decimals.pressure, \t\t_unitPress);\n\t\t InfoHelper(user, _nameGasRes,\t\tValuesPtr->gasResistance, \t\tsettings.decimals.gasResistance,\t_unitGasres);\n\t\t InfoHelper(user, _nameAHum, \t\tValuesPtr->absHumidity, \t\tsettings.decimals.absHumidity, \t\t_unitAHum);\n\t\t InfoHelper(user, _nameDrewP, \t\tValuesPtr->drewPoint, \t\t\tsettings.decimals.drewPoint, \t\ttempScale.c_str());\n\t\t InfoHelper(user, _nameIaq, \t\t\tValuesPtr->iaq, \t\t\t\tsettings.decimals.iaq, \t\t\t\t_unitIaq);\n\t\t InfoHelper(user, _nameIaqVerb, \t\tcvalues.iaqVerbal, \t\t\t\tsettings.PublishIAQVerbal);\n\t\t InfoHelper(user, _nameStaticIaq,\tValuesPtr->staticIaq, \t\t\tsettings.decimals.staticIaq, \t\t_unitStaticIaq);\n\t\t InfoHelper(user, _nameStaticIaqVerb,cvalues.staticIaqVerbal, \t\tsettings.PublishStaticIAQVerbal);\n\t\t InfoHelper(user, _nameCo2, \t\t\tValuesPtr->co2, \t\t\t\tsettings.decimals.co2, \t\t\t\t_unitCo2);\n\t\t InfoHelper(user, _nameVoc, \t\t\tValuesPtr->Voc, \t\t\t\tsettings.decimals.Voc, \t\t\t\t_unitVoc);\n\t\t InfoHelper(user, _nameGasPer, \t\tValuesPtr->gasPerc, \t\t\tsettings.decimals.gasPerc, \t\t\t_unitGasPer);\n \n\t\t if (settings.pubAcc) {\n\t\t\t if (settings.decimals.iaq >= 0) \t\tInfoHelper(user, _nameIaqAc, \t\tValuesPtr->iaqAccuracy, \t\t0, \" \");\n\t\t\t if (settings.decimals.staticIaq >= 0) \tInfoHelper(user, _nameStaticIaqAc, \tValuesPtr->staticIaqAccuracy, \t0, \" \");\n\t\t\t if (settings.decimals.co2 >= 0)\t\t\tInfoHelper(user, _nameCo2Ac, \t\tValuesPtr->co2Accuracy, \t\t0, \" \");\n\t\t\t if (settings.decimals.Voc >= 0)\t\t\tInfoHelper(user, _nameVocAc, \t\tValuesPtr->VocAccuracy, \t\t0, \" \");\n\t\t\t if (settings.decimals.gasPerc >= 0)\t\tInfoHelper(user, _nameGasPerAc, \tValuesPtr->gasPercAccuracy, \t0, \" \");\n\t\t }\n \n\t\t if (settings.publishSensorState) {\n\t\t\t InfoHelper(user, _nameStabStatus, \tValuesPtr->stabStatus, \t0, \" \");\n\t\t\t InfoHelper(user, _nameRunInStatus, \tValuesPtr->runInStatus, 0, \" \");\n\t\t }\n\t }\n }\n \n /**\n  * @brief Info Page helper function\n  * @param root JSON object\n  * @param name Name of the sensor as char\n  * @param sensorvalue Value of the sensor as float\n  * @param decimals Decimal places of the value\n  * @param unit Unit of the sensor\n  */\n void UsermodBME68X::InfoHelper(JsonObject& root, const char* name, const float& sensorvalue, const int8_t& decimals, const char* unit) {\n\t if (decimals > -1) {\n\t\t JsonArray sub_json = root.createNestedArray(name);\n\t\t sub_json.add(roundf(sensorvalue * powf(10, decimals)) / powf(10, decimals));\n\t\t sub_json.add(unit);\n\t }\n }\n \n /**\n  * @brief Info Page helper function (overload)\n  * @param root JSON object\n  * @param name Name of the sensor\n  * @param sensorvalue Value of the sensor as string\n  * @param status Status of the value (active/inactive)\n  */\n void UsermodBME68X::InfoHelper(JsonObject& root, const char* name, const String& sensorvalue, const bool& status) {\n\t if (status) {\n\t\t JsonArray sub_json = root.createNestedArray(name);\n\t\t sub_json.add(sensorvalue);\n\t }\n }\n \n /**\n  * @brief Called by WLED: Adds the usermodul neends on the config page for user modules\n  * @param JsonObject Pointer\n  *\n  * @see Usermod::addToConfig()\n  * @see UsermodManager::addToConfig()\n  */\n void UsermodBME68X::addToConfig(JsonObject& root) {\n\t DEBUG_PRINT(F(UMOD_DEBUG_NAME \"Creating configuration pages content: \"));\n \n\t JsonObject top = root.createNestedObject(FPSTR(UMOD_NAME));\n\t /* general settings */\n\t top[FPSTR(_enabled)] = \t\t\t\t\t\tsettings.enabled;\n\t top[FPSTR(_nameI2CAdr)] = \t\t\t\t\tsettings.I2cadress;\n\t top[FPSTR(_nameInterval)] = \t\t\t\tsettings.Interval;\n\t top[FPSTR(_namePublishChange)] =\t\t \tsettings.PublischChange;\n\t top[FPSTR(_namePubAc)] = \t\t\t\t\tsettings.pubAcc;\n\t top[FPSTR(_namePubSenState)] = \t\t\t\tsettings.publishSensorState;\n\t top[FPSTR(_nameTempScale)] = \t\t\t\tsettings.tempScale;\n\t top[FPSTR(_nameTempOffset)] = \t\t\t\tsettings.tempOffset;\n\t top[FPSTR(_nameHADisc)] = \t\t\t\t\tsettings.HomeAssistantDiscovery;\n\t top[FPSTR(_namePauseOnActWL)] = \t\t\tsettings.pauseOnActiveWled;\n\t top[FPSTR(_nameDelCalib)] = \t\t\t\tflags.DeleteCaibration;\n \n\t /* Digs */\n\t JsonObject sensors_json = top.createNestedObject(\"Sensors\");\n\t sensors_json[FPSTR(_nameTemp)] = \t\t\tsettings.decimals.temperature;\n\t sensors_json[FPSTR(_nameHum)] = \t\t\tsettings.decimals.humidity;\n\t sensors_json[FPSTR(_namePress)] = \t\t\tsettings.decimals.pressure;\n\t sensors_json[FPSTR(_nameGasRes)] = \t\t\tsettings.decimals.gasResistance;\n\t sensors_json[FPSTR(_nameAHum)] = \t\t\tsettings.decimals.absHumidity;\n\t sensors_json[FPSTR(_nameDrewP)] = \t\t\tsettings.decimals.drewPoint;\n\t sensors_json[FPSTR(_nameIaq)] = \t\t\tsettings.decimals.iaq;\n\t sensors_json[FPSTR(_nameIaqVerb)] = \t\tsettings.PublishIAQVerbal;\n\t sensors_json[FPSTR(_nameStaticIaq)] = \t\tsettings.decimals.staticIaq;\n\t sensors_json[FPSTR(_nameStaticIaqVerb)] = \tsettings.PublishStaticIAQVerbal;\n\t sensors_json[FPSTR(_nameCo2)] = \t\t\tsettings.decimals.co2;\n\t sensors_json[FPSTR(_nameVoc)] = \t\t\tsettings.decimals.Voc;\n\t sensors_json[FPSTR(_nameGasPer)] =\t\t\tsettings.decimals.gasPerc;\n \n\t DEBUG_PRINTLN(F(GOGAB_OK));\n }\n \n /**\n  * @brief Called by WLED: Add dropdown and additional infos / structure\n  * @see Usermod::appendConfigData()\n  * @see UsermodManager::appendConfigData()\n  */\n void UsermodBME68X::appendConfigData() {\n\t // snprintf_P(charbuffer, 127, PSTR(\"addInfo('%s:%s',1,'read interval [seconds]');\"), UMOD_NAME, _nameInterval); oappend(charbuffer);\n\t // snprintf_P(charbuffer, 127, PSTR(\"addInfo('%s:%s',1,'only if value changes');\"), UMOD_NAME, _namePublishChange); oappend(charbuffer);\n\t // snprintf_P(charbuffer, 127, PSTR(\"addInfo('%s:%s',1,'maximum age of a message in seconds');\"), UMOD_NAME, _nameMaxAge); oappend(charbuffer);\n\t // 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);\n\t // snprintf_P(charbuffer, 127, PSTR(\"addInfo('%s:%s',1,'*) Set to minus to deactivate (all sensors)');\"), UMOD_NAME, _nameTemp); oappend(charbuffer);\n \n\t /* Dropdown for Celsius/Fahrenheit*/\n\t oappend(F(\"dd=addDropdown('\"));\n\t oappend(UMOD_NAME);\n\t oappend(F(\"','\"));\n\t oappend(_nameTempScale);\n\t oappend(F(\"');\"));\n\t oappend(F(\"addOption(dd,'Celsius',0);\"));\n\t oappend(F(\"addOption(dd,'Fahrenheit',1);\"));\n \n\t /* i²C Address*/\n\t oappend(F(\"dd=addDropdown('\"));\n\t oappend(UMOD_NAME);\n\t oappend(F(\"','\"));\n\t oappend(_nameI2CAdr);\n\t oappend(F(\"');\"));\n\t oappend(F(\"addOption(dd,'0x76',0x76);\"));\n\t oappend(F(\"addOption(dd,'0x77',0x77);\"));\n }\n \n /**\n  * @brief Called by WLED: Read Usermod Config Settings default settings values could be set here (or below using the 3-argument getJsonValue()) \n  * \t\t  instead of in the class definition or constructor setting them inside readFromConfig() is slightly more robust, handling the rare but \n  * \t\t  plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed)\n  *        This is called whenever WLED boots and loads cfg.json, or when the UM config\n  *        page is saved. Will properly re-instantiate the SHT class upon type change and\n  *        publish HA discovery after enabling.\n  * \t\t  NOTE: Here are the default settings of the user module \n  * @param JsonObject Pointer\n  * @return bool\n  * @see Usermod::readFromConfig()\n  * @see UsermodManager::readFromConfig()\n  */\n bool UsermodBME68X::readFromConfig(JsonObject& root) {\n\t DEBUG_PRINT(F(UMOD_DEBUG_NAME \"Reading configuration: \"));\n \n\t JsonObject top = root[FPSTR(UMOD_NAME)];\n\t bool configComplete = !top.isNull();\n \n\t /* general settings */ \t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/* DEFAULTS */\n\t configComplete &= getJsonValue(top[FPSTR(_enabled)], \t\t\t\t\t\tsettings.enabled, \t\t\t\t\t\t\t1\t\t);\t\t// Usermod enabled per default\n\t configComplete &= getJsonValue(top[FPSTR(_nameI2CAdr)], \t\t\t\t\tsettings.I2cadress, \t\t\t\t\t\t0x77\t);\t\t// Defalut IC2 adress set to 0x77 (some modules are set to 0x76)\n\t configComplete &= getJsonValue(top[FPSTR(_nameInterval)], \t\t\t\t\tsettings.Interval, \t\t\t\t\t\t\t1\t\t);\t\t// Executed every second\n\t configComplete &= getJsonValue(top[FPSTR(_namePublishChange)], \t\t\t\tsettings.PublischChange, \t\t\t\t\tfalse\t);\t\t// Publish changed values only\n\t configComplete &= getJsonValue(top[FPSTR(_nameTempScale)], \t\t\t\t\tsettings.tempScale, \t\t\t\t\t\t0\t\t);\t\t// Temp sale set to Celsius (1=Fahrenheit)\n\t configComplete &= getJsonValue(top[FPSTR(_nameTempOffset)], \t\t\t\tsettings.tempOffset, \t\t\t\t\t\t0\t\t);\t\t// Temp offset is set to 0 (Celsius)\n\t configComplete &= getJsonValue(top[FPSTR(_namePubSenState)], \t\t\t\tsettings.publishSensorState, \t\t\t\t1\t\t);\t\t// Publish the sensor states\n\t configComplete &= getJsonValue(top[FPSTR(_namePubAc)], \t\t\t\t\t\tsettings.pubAcc, \t\t\t\t\t\t\t1\t\t);\t\t// Publish accuracy values \n\t configComplete &= getJsonValue(top[FPSTR(_nameHADisc)], \t\t\t\t\tsettings.HomeAssistantDiscovery, \t\t\ttrue\t);\t\t// Activate HomeAssistant Discovery (this Module will be shown as MQTT device in HA)\n\t configComplete &= getJsonValue(top[FPSTR(_namePauseOnActWL)],\t\t\t\tsettings.pauseOnActiveWled,\t\t\t\t\tfalse\t);\t\t// Pause on active WLED not activated per default\n\t configComplete &= getJsonValue(top[FPSTR(_nameDelCalib)], \t\t\t\t\tflags.DeleteCaibration, \t\t\t\t\tfalse\t);\t\t// IF checked the calibration file will be delete when the save button is pressed\n \n\t /* Decimal places */\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/* no of digs / -1 means deactivated */\n\t configComplete &= getJsonValue(top[\"Sensors\"][FPSTR(_nameTemp)], \t\t\tsettings.decimals.temperature, \t\t\t\t1\t\t);\t\t// One decimal places\n\t configComplete &= getJsonValue(top[\"Sensors\"][FPSTR(_nameHum)], \t\t\tsettings.decimals.humidity, \t\t\t\t1\t\t);\n\t configComplete &= getJsonValue(top[\"Sensors\"][FPSTR(_namePress)], \t\t\tsettings.decimals.pressure, \t\t\t\t0\t\t);\t\t// Zero decimal places\n\t configComplete &= getJsonValue(top[\"Sensors\"][FPSTR(_nameGasRes)], \t\t\tsettings.decimals.gasResistance,\t\t\t-1\t\t);\t\t// deavtivated\n\t configComplete &= getJsonValue(top[\"Sensors\"][FPSTR(_nameDrewP)], \t\t\tsettings.decimals.drewPoint, \t\t\t\t1\t\t);\n\t configComplete &= getJsonValue(top[\"Sensors\"][FPSTR(_nameAHum)], \t\t\tsettings.decimals.absHumidity, \t\t\t\t1\t\t);\n\t configComplete &= getJsonValue(top[\"Sensors\"][FPSTR(_nameIaq)], \t\t\tsettings.decimals.iaq, \t\t\t\t\t\t0\t\t);\t\t// Index for Air Quality Number is active\n\t configComplete &= getJsonValue(top[\"Sensors\"][FPSTR(_nameIaqVerb)], \t\tsettings.PublishIAQVerbal, \t\t\t\t\t-1\t\t); \t\t// deactivated - Index for Air Quality (IAQ) verbal classification\n\t configComplete &= getJsonValue(top[\"Sensors\"][FPSTR(_nameStaticIaq)], \t\tsettings.decimals.staticIaq, \t\t\t\t0\t\t);\t\t// activated - Static IAQ is better than IAQ for devices that are not moved\n\t configComplete &= getJsonValue(top[\"Sensors\"][FPSTR(_nameStaticIaqVerb)], \tsettings.PublishStaticIAQVerbal,\t\t\t0\t\t);\t\t// activated\n\t configComplete &= getJsonValue(top[\"Sensors\"][FPSTR(_nameCo2)], \t\t\tsettings.decimals.co2, \t\t\t\t\t\t0\t\t);\n\t configComplete &= getJsonValue(top[\"Sensors\"][FPSTR(_nameVoc)], \t\t\tsettings.decimals.Voc, \t\t\t\t\t\t0\t\t);\n\t configComplete &= getJsonValue(top[\"Sensors\"][FPSTR(_nameGasPer)], \t\t\tsettings.decimals.gasPerc, \t\t\t\t\t0\t\t);\n \n\t DEBUG_PRINTLN(F(GOGAB_OK));\t\n \n\t /* Set the selected temperature unit */\n\t if (settings.tempScale) {\n\t\t tempScale = F(_unitFahrenheit);\n\t }\n\t else {\n\t\t tempScale = F(_unitCelsius);\n\t }\n \n\t if (flags.DeleteCaibration) {\n\t\t DEBUG_PRINT(F(UMOD_DEBUG_NAME \"Deleting Calibration File\"));\n\t\t flags.DeleteCaibration = false;\n\t\t if (WLED_FS.remove(CALIB_FILE_NAME)) {\n\t\t\t DEBUG_PRINTLN(F(GOGAB_OK));\n\t\t }\n\t\t else {\n\t\t\t DEBUG_PRINTLN(F(GOGAB_FAIL));\n\t\t }\n\t }\n \n\t if (settings.Interval < 1) settings.Interval = 1;                           // Correct interval on need (A number less than 1 is not permitted)\n\t iaqSensor.setTemperatureOffset(settings.tempOffset); \t\t\t\t\t\t// Set Temp Offset\n \n\t return configComplete;\n }\n \n /**\n  * @brief Called by WLED: Retunrs the user modul id number\n  * \n  * @return uint16_t User module number\n  */\n uint16_t UsermodBME68X::getId() {\n\t return USERMOD_ID_BME68X;\n }\n \n \n /**\n  * @brief Returns the current temperature in the scale which is choosen in settings\n  * @return Temperature value (°C or °F as choosen in settings)\n */\n inline float UsermodBME68X::getTemperature() {\n\t return ValuesPtr->temperature;\n }\n \n /**\n  * @brief Returns the current humidity\n  * @return Humididty value (%)\n */\n inline float UsermodBME68X::getHumidity() {\n   return ValuesPtr->humidity;\n }\n \n /**\n  * @brief Returns the current pressure\n  * @return Pressure value (hPa)\n */\n inline float UsermodBME68X::getPressure() {\n   return ValuesPtr->pressure;\n }\n \n /**\n  * @brief Returns the current gas resistance\n  * @return Gas resistance value (kΩ)\n */\n inline float UsermodBME68X::getGasResistance() {\n   return ValuesPtr->gasResistance;\n }\n \n /**\n  * @brief Returns the current absolute humidity\n  * @return Absolute humidity value (g/m³)\n */\n inline float UsermodBME68X::getAbsoluteHumidity() {\n   return ValuesPtr->absHumidity;\n }\n \n /**\n  * @brief Returns the current dew point\n  * @return Dew point (°C or °F as choosen in settings)\n */\n inline float UsermodBME68X::getDewPoint() {\n   return ValuesPtr->drewPoint;\n }\n \n /**\n  * @brief Returns the current iaq (Indoor Air Quallity)\n  * @return Iaq value (0-500)\n */\n inline float UsermodBME68X::getIaq() {\n   return ValuesPtr->iaq;\n }\n \n /**\n  * @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)\n  * @return Static iaq value (float)\n */\n inline float UsermodBME68X::getStaticIaq() {\n   return ValuesPtr->staticIaq;\n }\n \n /**\n  * @brief Returns the current co2\n  * @return Co2 value (ppm)\n */\n inline float UsermodBME68X::getCo2() {\n   return ValuesPtr->co2;\n }\n \n /**\n  * @brief Returns the current voc (Breath VOC concentration estimate [ppm])\n  * @return Voc value (ppm)\n */\n inline float UsermodBME68X::getVoc() {\n   return ValuesPtr->Voc;\n }\n \n /**\n  * @brief Returns the current gas percentage\n  * @return Gas percentage value (%)\n */\n inline float UsermodBME68X::getGasPerc() {\n   return ValuesPtr->gasPerc;\n }\n \n /**\n  * @brief Returns the current iaq accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated)\n  * @return Iaq accuracy value (0-3)\n */\n inline uint8_t UsermodBME68X::getIaqAccuracy() {\n   return ValuesPtr->iaqAccuracy ;\n }\n \n /**\n  * @brief Returns the current static iaq accuracy accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated)\n  * @return Static iaq accuracy value (0-3)\n */\n inline uint8_t UsermodBME68X::getStaticIaqAccuracy() {\n   return ValuesPtr->staticIaqAccuracy;\n }\n \n /**\n  * @brief Returns the current co2 accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated)\n  * @return Co2 accuracy  value (0-3)\n */\n inline uint8_t UsermodBME68X::getCo2Accuracy() {\n   return ValuesPtr->co2Accuracy;\n }\n \n /**\n  * @brief Returns the current voc accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated)\n  * @return Voc accuracy  value (0-3)\n */\n inline uint8_t UsermodBME68X::getVocAccuracy() {\n   return ValuesPtr->VocAccuracy;\n } \n \n /**\n  * @brief Returns the current gas percentage accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated)\n  * @return Gas percentage accuracy value (0-3)\n */\n inline uint8_t UsermodBME68X::getGasPercAccuracy() {\n   return ValuesPtr->gasPercAccuracy;\n }\n \n /**\n  * @brief Returns the current stab status.\n  * \t\t  Indicates when the sensor is ready after after switch-on\n  * @return stab status value (0 = switched on / 1 = stabilized)\n */\n inline bool  UsermodBME68X::getStabStatus() {\n   return ValuesPtr->stabStatus;\n }\n \n /**\n  * @brief Returns the current run in status. \n  * \t\t  Indicates if the sensor is undergoing initial stabilization during its first use after production\n  * @return Tun status accuracy value (0 = switched on first time / 1 = stabilized)\n */\n inline bool UsermodBME68X::getRunInStatus() {\n   return ValuesPtr->runInStatus;\n }\n \n \n /**\n  * @brief Checks whether the library and the sensor are running.\n  */\n void UsermodBME68X::checkIaqSensorStatus() {\n \n\t if (iaqSensor.bsecStatus != BSEC_OK) {\n\t\t InfoPageStatusLine = \"BSEC Library \";\n\t\t DEBUG_PRINT(UMOD_DEBUG_NAME + InfoPageStatusLine);\n\t\t flags.InitSuccessful = false;\n\t\t if (iaqSensor.bsecStatus < BSEC_OK) {\n\t\t\t InfoPageStatusLine += \" Error Code : \" + String(iaqSensor.bsecStatus);\n\t\t\t DEBUG_PRINTLN(GOGAB_FAIL);\n\t\t }\n\t\t else {\n\t\t\t InfoPageStatusLine += \" Warning Code : \" + String(iaqSensor.bsecStatus);\n\t\t\t DEBUG_PRINTLN(GOGAB_WARN);\n\t\t }\n\t }\n\t else {\n\t\t InfoPageStatusLine = \"Sensor BME68X \";\n\t\t DEBUG_PRINT(UMOD_DEBUG_NAME + InfoPageStatusLine);\n \n\t\t if (iaqSensor.bme68xStatus != BME68X_OK) {\n\t\t\t flags.InitSuccessful = false;\n\t\t\t if (iaqSensor.bme68xStatus < BME68X_OK) {\n\t\t\t\t InfoPageStatusLine += \"error code: \" + String(iaqSensor.bme68xStatus);\n\t\t\t\t DEBUG_PRINTLN(GOGAB_FAIL);\n\t\t\t }\n\t\t\t else {\n\t\t\t\t InfoPageStatusLine += \"warning code: \" + String(iaqSensor.bme68xStatus);\n\t\t\t\t DEBUG_PRINTLN(GOGAB_WARN);\n\t\t\t }\n\t\t }\n\t\t else {\n\t\t\t InfoPageStatusLine += F(\"OK\");\n\t\t\t DEBUG_PRINTLN(GOGAB_OK);\n\t\t }\n\t }\n }\n \n /**\n  * @brief Loads the calibration data from the file system of the device\n  */\n void UsermodBME68X::loadState() {\n\t if (WLED_FS.exists(CALIB_FILE_NAME)) {\n\t\t DEBUG_PRINT(F(UMOD_DEBUG_NAME \"Read the calibration file: \"));\n\t\t File file = WLED_FS.open(CALIB_FILE_NAME, FILE_READ);\n\t\t if (!file) {\n\t\t\t DEBUG_PRINTLN(GOGAB_FAIL);\n\t\t }\n\t\t else {\n\t\t\t file.read(bsecState, BSEC_MAX_STATE_BLOB_SIZE);\n\t\t\t file.close();\n\t\t\t DEBUG_PRINTLN(GOGAB_OK);\n\t\t\t iaqSensor.setState(bsecState);\n\t\t }\n\t }\n\t else {\n\t\t DEBUG_PRINTLN(F(UMOD_DEBUG_NAME \"Calibration file not found.\"));\n\t }\n }\n \n /**\n  * @brief Saves the calibration data from the file system of the device\n  */\n void UsermodBME68X::saveState() {\n\t DEBUG_PRINT(F(UMOD_DEBUG_NAME \"Write the calibration file  \"));\n\t File file = WLED_FS.open(CALIB_FILE_NAME, FILE_WRITE);\n\t if (!file) {\n\t\t DEBUG_PRINTLN(GOGAB_FAIL);\n\t }\n\t else {\n\t\t iaqSensor.getState(bsecState);\n\t\t file.write(bsecState, BSEC_MAX_STATE_BLOB_SIZE);\n\t\t file.close();\n\t\t stateUpdateCounter++;\n\t\t DEBUG_PRINTF(\"(saved %d times)\" GOGAB_OK \"\\n\", stateUpdateCounter);\n\t\t flags.SaveState = false; // Clear save state flag\n \n\t\t char contbuffer[30];\n \n\t\t /* Timestamp */\n\t\t time_t curr_time;\n\t\t tm* curr_tm;\n\t\t time(&curr_time);\n\t\t curr_tm = localtime(&curr_time);\n \n\t\t snprintf_P(charbuffer, 127, PSTR(\"%s/%s\"), mqttDeviceTopic, UMOD_NAME \"/Calib Last Run\");\n\t\t strftime(contbuffer, 30, \"%d %B %Y - %T\", curr_tm);\n\t\t if (WLED_MQTT_CONNECTED) mqtt->publish(charbuffer, 0, false, contbuffer);\n \n\t\t snprintf(contbuffer, 30, \"%d\", stateUpdateCounter);\n\t\t snprintf_P(charbuffer, 127, PSTR(\"%s/%s\"), mqttDeviceTopic, UMOD_NAME \"/Calib Count\");\n\t\t if (WLED_MQTT_CONNECTED) mqtt->publish(charbuffer, 0, false, contbuffer);\n\t }\n }\n \n \n static UsermodBME68X bme68x_v2;\n REGISTER_USERMOD(bme68x_v2);"
  },
  {
    "path": "usermods/BME68X_v2/README.md",
    "content": "# Usermod BME68X\n\nThis 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.\n\n<p align=\"center\"><img src=\"pics/pic1.png\" style=\"width:60%;\"></p>\n\nIn 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.\n\n<p align=\"center\"><img src=\"pics/pic2.png\"></p>\n\nIf you use HomeAssistance discovery, the device tree for HomeAssistance is created.  This is published under the topic 'homeassistant/sensor/[MQTT Client ID]' via MQTT.\n\n<p align=\"center\"><img src=\"pics/pic3.png\"></p>\n\nA device with the following sensors appears in HomeAssistant. Please note that MQTT must be activated in HomeAssistant.\n\n<p align=\"center\"><img src=\"pics/pic4.png\" style=\"width:60%;\"></p>\n\n## Features\n\nRaw sensor types\n\nSensor\t\tAccuracy\tScale\t\tRange\n-----------------------------\n\nTemperature\t+/- 1.0\t\t°C/°F\t\t-40 to 85 °C\nHumidity\t+/- 3 \t\t%\t\t0 to 100 %\nPressure\t+/- 1 \t\thPa\t\t300 to 1100 hPa\nGas Resistance\t\t\tOhm\nThe BSEC Library calculates the following values via the gas resistance\n\nSensor\t\tAccuracy\tScale\t\tRange\n-----------------------------\n\nIAQ \t\t\t\t\t\tvalue between 0 and 500\nStatic IAQ \t\t\t\t\tsame as IAQ but for permanently installed devices\nCO2 \t\t\t\tPPM\nVOC \t\t\t\tPPM\nGas-Percentage \t\t\t%\nIn addition the usermod calculates\n\nSensor\t\tAccuracy\tScale\t\tRange\n-----------------------------\n\nAbsolute humidity\t \tg/m³\nDew point \t\t\t°C/°F\n\n### IAQ (Indoor Air Quality)\n\nThe IAQ is divided into the following value groups.\n\n<p align=\"center\"><img src=\"pics/pic5.png\"></p>\n\nFor more detailed information, please consult the enclosed Bosch product description (BME680.pdf).\n\n## Calibration of the device\n\nThe gas sensor of the BME68X must be calibrated. This differs from the BME280, which does not require any calibration.\nThere is a range of additional information for this, which the driver also provides. These values can be found in HomeAssistant under Diagnostics.\n\n- **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).\n- **RUN_IN_STATUS**: \tGas sensor run-in status [boolean] Indicates power-on stabilization status of the gas sensor element: stabilization is ongoing (0) or stabilization is finished (1)\n\nFurthermore, all GAS based values have their own accuracy value. These have the following meaning:\n\n- **Accuracy = 0** \tmeans the sensor is being stabilized (this can take a while on the first run)\n- **Accuracy = 1**\tmeans 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.\n- **Accuracy = 2**\tmeans the sensor is currently calibrating.\n- **Accuracy = 3**\tmeans 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.\n\nThe IAQ index is therefore only meaningful if IAQ Accuracy = 3. In addition to the value for IAQ, BSEC also provides us with CO2 and VOC equivalent values. When using the sensor, the calibration value should also always be read out and displayed or transmitted.\n\nReasonably reliable values are therefore only achieved when accuracy displays the value 3.\n\n## Settings\n\nThe settings of the usermods are set in the usermod section of wled.\n\n<p align=\"center\"><img src=\"pics/pic6.png\"></p>\n\nThe possible settings are\n\n- **Enable:**\t\t\tEnables / disables the usermod\n- **I2C address:**\t\tI2C address of the sensor. You can choose between 0X77 & 0X76. The default is 0x77.\n- **Interval:**\t\t\tSpecifies the interval of seconds at which the usermod should be executed. The default is every second.\n- **Pub Chages Only:**\t\tIf this item is active, the values are only published if they have changed since the last publication.\n- **Pub Accuracy:**\t\tThe Accuracy values associated with the gas values are also published.\n- **Pub Calib State:**\t\tIf this item is active, STABILIZATION_STATUS& RUN_IN_STATUS are also published.\n- **Temp Scale:**\t\tHere you can choose between °C and °F.\n- **Temp Offset:**\t\tThe temperature offset is always set in °C. It must be converted for Fahrenheit.\n- **HA Discovery:**\t\tIf this item is active, the HomeAssistant sensor tree is created.\n- **Pause While WLED Active:**\tIf 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.\n- **Del Calibration Hist:**\tIf a check mark is set here, the calibration file saved in the file system is deleted when the settings are saved.\n\n### Sensors\n\nApplies 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.\n\nIt is recommended to use the Static IAQ for the IAQ values. This is recommended by Bosch for statically placed devices.\n\n## Output\n\nData is published over MQTT - make sure you've enabled the MQTT sync interface.\n\nIn addition to outputting via MQTT, you can read the values from the Info Screen on the dashboard page of the device's web interface.\n\nMethods also exist to read the read/calculated values from other WLED modules through code.\n\n- getTemperature();\tThe scale °C/°F is depended to the settings\n- getHumidity();\n- getPressure();\n- getGasResistance();\n- getAbsoluteHumidity();\n- getDewPoint();\t\tThe scale °C/°F is depended to the settings\n- getIaq();\n- getStaticIaq();\n- getCo2();\n- getVoc();\n- getGasPerc();\n- getIaqAccuracy();\n- getStaticIaqAccuracy();\n- getCo2Accuracy();\n- getVocAccuracy();\n- getGasPercAccuracy();\n- getStabStatus();\n- getRunInStatus();\n\n## Compilation\n\nTo enable, compile with `BME68X` in `custom_usermods` (e.g. in `platformio_override.ini`)\n\nExample:\n\n```[env:esp32_mySpecial]\nextends = env:esp32dev\ncustom_usermods = ${env:esp32dev.custom_usermods} BME68X\n```\n\n## Revision History\n\n### Version 1.0.0\n\n- First version of the BME68X_v user module\n\n### Version 1.0.1\n\n- Rebased to WELD Version 0.15\n- Reworked some default settings\n- A problem with the default settings has been fixed\n\n### Version 1.0.2\n\n* Rebased to WELD Version 0.16\n* Fixed: Solved compilation problems related to some macro naming interferences.\n\n## Known problems\n\n- 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.\n- If you save the settings often, WLED can get stuck.\n- 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.\n\n<div><img src=\"pics/GeoGab.svg\" width=\"20%\"/> </div>\nGabriel Sieben (gsieben@geogab.net)\n"
  },
  {
    "path": "usermods/BME68X_v2/library.json",
    "content": "{\n  \"name\": \"BME68X\",\n  \"build\": { \"libArchive\": false },\n  \"dependencies\": {\n    \"boschsensortec/BSEC Software Library\":\"^1.8.1492\"\n  }\n}\n"
  },
  {
    "path": "usermods/Battery/Battery.cpp",
    "content": "#include \"wled.h\"\n#include \"battery_defaults.h\"\n#include \"UMBattery.h\"\n#include \"types/UnkownUMBattery.h\"\n#include \"types/LionUMBattery.h\"\n#include \"types/LipoUMBattery.h\"\n\n/*\n * Usermod by Maximilian Mewes\n * E-mail: mewes.maximilian@gmx.de\n * Created at: 25.12.2022\n * If you have any questions, please feel free to contact me.\n */\nclass UsermodBattery : public Usermod \n{\n  private:\n    // battery pin can be defined in my_config.h\n    int8_t batteryPin = USERMOD_BATTERY_MEASUREMENT_PIN;\n    \n    UMBattery* bat = new UnkownUMBattery();\n    batteryConfig cfg;\n\n    // Initial delay before first reading to allow voltage stabilization\n    unsigned long initialDelay = USERMOD_BATTERY_INITIAL_DELAY;\n    bool initialDelayComplete = false;\n    bool isFirstVoltageReading = true;\n    // how often to read the battery voltage\n    unsigned long readingInterval = USERMOD_BATTERY_MEASUREMENT_INTERVAL;\n    unsigned long nextReadTime = 0;\n    unsigned long lastReadTime = 0;\n    // between 0 and 1, to control strength of voltage smoothing filter\n    float alpha = USERMOD_BATTERY_AVERAGING_ALPHA;\n\n    // auto shutdown/shutoff/master off feature\n    bool autoOffEnabled = USERMOD_BATTERY_AUTO_OFF_ENABLED;\n    uint8_t autoOffThreshold = USERMOD_BATTERY_AUTO_OFF_THRESHOLD;\n\n    // low power indicator feature\n    bool lowPowerIndicatorEnabled = USERMOD_BATTERY_LOW_POWER_INDICATOR_ENABLED;\n    uint8_t lowPowerIndicatorPreset = USERMOD_BATTERY_LOW_POWER_INDICATOR_PRESET;\n    uint8_t lowPowerIndicatorThreshold = USERMOD_BATTERY_LOW_POWER_INDICATOR_THRESHOLD;\n    uint8_t lowPowerIndicatorReactivationThreshold = lowPowerIndicatorThreshold+10;\n    uint8_t lowPowerIndicatorDuration = USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION;\n    bool lowPowerIndicationDone = false;\n    unsigned long lowPowerActivationTime = 0; // used temporary during active time\n    uint8_t lastPreset = 0;\n\n    //\n    bool initDone = false;\n    bool initializing = true;\n    bool HomeAssistantDiscovery = false;\n\n    // strings to reduce flash memory usage (used more than twice)\n    static const char _name[];\n    static const char _readInterval[];\n    static const char _enabled[];\n    static const char _threshold[];\n    static const char _preset[];\n    static const char _duration[];\n    static const char _init[];\n    static const char _haDiscovery[];\n\n    /**\n     * Helper for rounding floating point values \n     */\n    float dot2round(float x) \n    {\n      float nx = (int)(x * 100 + .5);\n      return (float)(nx / 100);\n    }\n\n    /**\n     * Helper for converting a string to lowercase\n     */\n    String stringToLower(String str)\n    {\n      for(int i = 0; i < str.length(); i++)\n        if(str[i] >= 'A' && str[i] <= 'Z')\n            str[i] += 32;\n      return str;\n    }\n\n    /**\n     * Turn off all leds\n     */\n    void turnOff()\n    {\n      bri = 0;\n      stateUpdated(CALL_MODE_DIRECT_CHANGE);\n    }\n\n    /**\n     * Indicate low power by activating a configured preset for a given time and then switching back to the preset that was selected previously\n     */\n    void lowPowerIndicator()\n    {\n      if (!lowPowerIndicatorEnabled) return;\n      if (batteryPin < 0) return;  // no measurement\n      if (lowPowerIndicationDone && lowPowerIndicatorReactivationThreshold <= bat->getLevel()) lowPowerIndicationDone = false;\n      if (lowPowerIndicatorThreshold <= bat->getLevel()) return;\n      if (lowPowerIndicationDone) return;\n      if (lowPowerActivationTime <= 1) {\n        lowPowerActivationTime = millis();\n        lastPreset = currentPreset;\n        applyPreset(lowPowerIndicatorPreset);\n      }\n\n      if (lowPowerActivationTime+(lowPowerIndicatorDuration*1000) <= millis()) {\n        lowPowerIndicationDone = true;\n        lowPowerActivationTime = 0;\n        applyPreset(lastPreset);\n      }      \n    }\n\n    /**\n     * read the battery voltage in different ways depending on the architecture \n     */\n    float readVoltage()\n    {\n      #ifdef ARDUINO_ARCH_ESP32\n        // use calibrated millivolts analogread on esp32 (150 mV ~ 2450 mV default attentuation) and divide by 1000 to get from milivolts to volts and multiply by voltage multiplier and apply calibration value\n        return (analogReadMilliVolts(batteryPin) / 1000.0f) * bat->getVoltageMultiplier()  + bat->getCalibration();\n      #else\n        // use analog read on esp8266 ( 0V ~ 1V no attenuation options) and divide by ADC precision 1023 and multiply by voltage multiplier and apply calibration value\n        return (analogRead(batteryPin) / 1023.0f) * bat->getVoltageMultiplier() + bat->getCalibration();\n      #endif\n    }\n\n#ifndef WLED_DISABLE_MQTT\n    void addMqttSensor(const String &name, const String &type, const String &topic, const String &deviceClass, const String &unitOfMeasurement = \"\", const bool &isDiagnostic = false)\n    {      \n      StaticJsonDocument<600> doc;\n      char uid[128], json_str[1024], buf[128];\n\n      doc[F(\"name\")] = name;\n      doc[F(\"stat_t\")] = topic;\n      sprintf_P(uid, PSTR(\"%s_%s_%s\"), escapedMac.c_str(), stringToLower(name).c_str(), type);\n      doc[F(\"uniq_id\")] = uid;\n      doc[F(\"dev_cla\")] = deviceClass;\n      doc[F(\"exp_aft\")] = 1800;\n\n      if(type == \"binary_sensor\") {\n        doc[F(\"pl_on\")]  = \"on\";\n        doc[F(\"pl_off\")] = \"off\";\n      }\n\n      if(unitOfMeasurement != \"\")\n        doc[F(\"unit_of_measurement\")] = unitOfMeasurement;\n\n      if(isDiagnostic)\n        doc[F(\"entity_category\")] = \"diagnostic\";\n\n      JsonObject device = doc.createNestedObject(F(\"device\")); // attach the sensor to the same device\n      device[F(\"name\")] = serverDescription;\n      device[F(\"ids\")]  = String(F(\"wled-sensor-\")) + mqttClientID;\n      device[F(\"mf\")]   = F(WLED_BRAND);\n      device[F(\"mdl\")]  = F(WLED_PRODUCT_NAME);\n      device[F(\"sw\")]   = versionString;\n\n      sprintf_P(buf, PSTR(\"homeassistant/%s/%s/%s/config\"), type, mqttClientID, uid);\n      DEBUG_PRINTLN(buf);\n      size_t payload_size = serializeJson(doc, json_str);\n      DEBUG_PRINTLN(json_str);\n\n      mqtt->publish(buf, 0, true, json_str, payload_size);\n    }\n\n    void publishMqtt(const char* topic, const char* state)\n    {\n      if (WLED_MQTT_CONNECTED) {\n        char buf[128];\n        snprintf_P(buf, 127, PSTR(\"%s/%s\"), mqttDeviceTopic, topic);\n        mqtt->publish(buf, 0, false, state);\n      }\n    }\n#endif\n\n  public:\n    //Functions called by WLED\n\n    /**\n     * setup() is called once at boot. WiFi is not yet connected at this point.\n     * You can use it to initialize variables, sensors or similar.\n     */\n    void setup() \n    {\n      // plug in the right battery type\n      if(cfg.type == (batteryType)lipo) {\n        bat = new LipoUMBattery();\n      } else if(cfg.type == (batteryType)lion) {\n        bat = new LionUMBattery();\n      }\n\n      // update the choosen battery type with configured values\n      bat->update(cfg);\n\n      #ifdef ARDUINO_ARCH_ESP32\n        bool success = false;\n        DEBUG_PRINTLN(F(\"Allocating battery pin...\"));\n        if (batteryPin >= 0 && digitalPinToAnalogChannel(batteryPin) >= 0) \n          if (PinManager::allocatePin(batteryPin, false, PinOwner::UM_Battery)) {\n            DEBUG_PRINTLN(F(\"Battery pin allocation succeeded.\"));\n            success = true;\n          }\n\n        if (!success) {\n          DEBUG_PRINTLN(F(\"Battery pin allocation failed.\"));\n          batteryPin = -1;  // allocation failed\n        } else {\n          pinMode(batteryPin, INPUT);\n        }\n      #else //ESP8266 boards have only one analog input pin A0\n        pinMode(batteryPin, INPUT);\n      #endif\n\n      // First voltage reading is delayed to allow voltage stabilization after powering up\n      nextReadTime = millis() + initialDelay;\n      lastReadTime = millis();\n\n      initDone = true;\n    }\n\n\n    /**\n     * connected() is called every time the WiFi is (re)connected\n     * Use it to initialize network interfaces\n     */\n    void connected() \n    {\n      //Serial.println(\"Connected to WiFi!\");\n    }\n\n\n    /*\n     * loop() is called continuously. Here you can check for events, read sensors, etc.\n     * \n     */\n    void loop() \n    {\n      if(strip.isUpdating()) return;\n\n      lowPowerIndicator();\n\n      // Handling the initial delay\n      if (!initialDelayComplete && millis() < nextReadTime)\n        return; // Continue to return until the initial delay is over\n\n      // Once the initial delay is over, set it as complete\n      if (!initialDelayComplete)\n        {\n          initialDelayComplete = true;\n          // Set the regular interval after initial delay\n          nextReadTime = millis() + readingInterval;\n        }\n\n      // Make the first voltage reading after the initial delay has elapsed\n      if (isFirstVoltageReading)\n        {\n          bat->setVoltage(readVoltage());\n          isFirstVoltageReading = false;\n        }\n\n      // check the battery level every USERMOD_BATTERY_MEASUREMENT_INTERVAL (ms)\n      if (millis() < nextReadTime) return;\n\n      nextReadTime = millis() + readingInterval;\n      lastReadTime = millis();\n\n      if (batteryPin < 0) return;  // nothing to read\n\n      initializing = false;\n      float rawValue = readVoltage();\n\n      // filter with exponential smoothing because ADC in esp32 is fluctuating too much for a good single readout\n      float filteredVoltage = bat->getVoltage() + alpha * (rawValue - bat->getVoltage());\n\n      bat->setVoltage(filteredVoltage);\n      // translate battery voltage into percentage\n      bat->calculateAndSetLevel(filteredVoltage);\n\n      // Auto off -- Master power off\n      if (autoOffEnabled && (autoOffThreshold >= bat->getLevel()))\n        turnOff();\n\n#ifndef WLED_DISABLE_MQTT\n      publishMqtt(\"battery\", String(bat->getLevel(), 0).c_str());\n      publishMqtt(\"voltage\", String(bat->getVoltage()).c_str());\n#endif\n\n    }\n\n    /**\n     * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.\n     * Creating an \"u\" object allows you to add custom key/value pairs to the Info section of the WLED web UI.\n     * Below it is shown how this could be used for e.g. a light sensor\n     */\n    void addToJsonInfo(JsonObject& root)\n    {\n      JsonObject user = root[\"u\"];\n      if (user.isNull()) user = root.createNestedObject(\"u\");\n\n      if (batteryPin < 0) {\n        JsonArray infoVoltage = user.createNestedArray(F(\"Battery voltage\"));\n        infoVoltage.add(F(\"n/a\"));\n        infoVoltage.add(F(\" invalid GPIO\"));\n        return;  // no GPIO - nothing to report\n      }\n\n      // info modal display names\n      JsonArray infoPercentage = user.createNestedArray(F(\"Battery level\"));\n      JsonArray infoVoltage = user.createNestedArray(F(\"Battery voltage\"));\n      JsonArray infoNextUpdate = user.createNestedArray(F(\"Next update\"));\n\n      infoNextUpdate.add((nextReadTime - millis()) / 1000);\n      infoNextUpdate.add(F(\" sec\"));\n      \n      if (initializing) {\n        infoPercentage.add(FPSTR(_init));\n        infoVoltage.add(FPSTR(_init));\n        return;\n      }\n\n      if (bat->getLevel() < 0) {\n        infoPercentage.add(F(\"invalid\"));\n      } else {\n        infoPercentage.add(bat->getLevel());\n      }\n      infoPercentage.add(F(\" %\"));\n\n      if (bat->getVoltage() < 0) {\n        infoVoltage.add(F(\"invalid\"));\n      } else {\n        infoVoltage.add(dot2round(bat->getVoltage()));\n      }\n      infoVoltage.add(F(\" V\"));\n    }\n\n    void addBatteryToJsonObject(JsonObject& battery, bool forJsonState)\n    {\n      if(forJsonState) { battery[F(\"type\")] = cfg.type; } else {battery[F(\"type\")] = (String)cfg.type; }  // has to be a String otherwise it won't get converted to a Dropdown\n      battery[F(\"min-voltage\")] = bat->getMinVoltage();\n      battery[F(\"max-voltage\")] = bat->getMaxVoltage();\n      battery[F(\"calibration\")] = bat->getCalibration();\n      battery[F(\"voltage-multiplier\")] = bat->getVoltageMultiplier();\n      battery[FPSTR(_readInterval)] = readingInterval;\n      battery[FPSTR(_haDiscovery)] = HomeAssistantDiscovery;\n\n      JsonObject ao = battery.createNestedObject(F(\"auto-off\"));  // auto off section\n      ao[FPSTR(_enabled)] = autoOffEnabled;\n      ao[FPSTR(_threshold)] = autoOffThreshold;\n\n      JsonObject lp = battery.createNestedObject(F(\"indicator\")); // low power section\n      lp[FPSTR(_enabled)] = lowPowerIndicatorEnabled;\n      lp[FPSTR(_preset)] = lowPowerIndicatorPreset; // dropdown trickery (String)lowPowerIndicatorPreset; \n      lp[FPSTR(_threshold)] = lowPowerIndicatorThreshold;\n      lp[FPSTR(_duration)] = lowPowerIndicatorDuration;\n    }\n\n    void getUsermodConfigFromJsonObject(JsonObject& battery)\n    {\n      getJsonValue(battery[F(\"type\")], cfg.type);\n      getJsonValue(battery[F(\"min-voltage\")], cfg.minVoltage);\n      getJsonValue(battery[F(\"max-voltage\")], cfg.maxVoltage);\n      getJsonValue(battery[F(\"calibration\")], cfg.calibration);\n      getJsonValue(battery[F(\"voltage-multiplier\")], cfg.voltageMultiplier);\n      setReadingInterval(battery[FPSTR(_readInterval)] | readingInterval);\n      setHomeAssistantDiscovery(battery[FPSTR(_haDiscovery)] | HomeAssistantDiscovery);\n\n      JsonObject ao = battery[F(\"auto-off\")];\n      setAutoOffEnabled(ao[FPSTR(_enabled)] | autoOffEnabled);\n      setAutoOffThreshold(ao[FPSTR(_threshold)] | autoOffThreshold);\n\n      JsonObject lp = battery[F(\"indicator\")];\n      setLowPowerIndicatorEnabled(lp[FPSTR(_enabled)] | lowPowerIndicatorEnabled);\n      setLowPowerIndicatorPreset(lp[FPSTR(_preset)] | lowPowerIndicatorPreset);\n      setLowPowerIndicatorThreshold(lp[FPSTR(_threshold)] | lowPowerIndicatorThreshold);\n      lowPowerIndicatorReactivationThreshold = lowPowerIndicatorThreshold+10;\n      setLowPowerIndicatorDuration(lp[FPSTR(_duration)] | lowPowerIndicatorDuration);\n      \n      if(initDone) \n        bat->update(cfg);\n    }\n\n    /**\n     * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).\n     * Values in the state object may be modified by connected clients\n     */\n    void addToJsonState(JsonObject& root)\n    {\n      JsonObject battery = root.createNestedObject(FPSTR(_name));\n\n      if (battery.isNull())\n        battery = root.createNestedObject(FPSTR(_name));\n      \n      addBatteryToJsonObject(battery, true);\n      \n      DEBUG_PRINTLN(F(\"Battery state exposed in JSON API.\"));\n    }\n\n\n    /**\n     * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).\n     * Values in the state object may be modified by connected clients\n     */\n    /*\n    void readFromJsonState(JsonObject& root)\n    {\n      if (!initDone) return;  // prevent crash on boot applyPreset()\n\n      JsonObject battery = root[FPSTR(_name)];\n\n      if (!battery.isNull()) {\n        getUsermodConfigFromJsonObject(battery);\n      \n        DEBUG_PRINTLN(F(\"Battery state read from JSON API.\"));\n      }\n    }\n    */\n\n\n    /**\n     * addToConfig() can be used to add custom persistent settings to the cfg.json file in the \"um\" (usermod) object.\n     * It will be called by WLED when settings are actually saved (for example, LED settings are saved)\n     * If you want to force saving the current state, use serializeConfig() in your loop().\n     * \n     * CAUTION: serializeConfig() will initiate a filesystem write operation.\n     * It might cause the LEDs to stutter and will cause flash wear if called too often.\n     * Use it sparingly and always in the loop, never in network callbacks!\n     * \n     * addToConfig() will make your settings editable through the Usermod Settings page automatically.\n     *\n     * Usermod Settings Overview:\n     * - Numeric values are treated as floats in the browser.\n     *   - If the numeric value entered into the browser contains a decimal point, it will be parsed as a C float\n     *     before being returned to the Usermod.  The float data type has only 6-7 decimal digits of precision, and\n     *     doubles are not supported, numbers will be rounded to the nearest float value when being parsed.\n     *     The range accepted by the input field is +/- 1.175494351e-38 to +/- 3.402823466e+38.\n     *   - If the numeric value entered into the browser doesn't contain a decimal point, it will be parsed as a\n     *     C int32_t (range: -2147483648 to 2147483647) before being returned to the usermod.\n     *     Overflows or underflows are truncated to the max/min value for an int32_t, and again truncated to the type\n     *     used in the Usermod when reading the value from ArduinoJson.\n     * - Pin values can be treated differently from an integer value by using the key name \"pin\"\n     *   - \"pin\" can contain a single or array of integer values\n     *   - On the Usermod Settings page there is simple checking for pin conflicts and warnings for special pins\n     *     - Red color indicates a conflict.  Yellow color indicates a pin with a warning (e.g. an input-only pin)\n     *   - Tip: use int8_t to store the pin value in the Usermod, so a -1 value (pin not set) can be used\n     *\n     * See usermod_v2_auto_save.h for an example that saves Flash space by reusing ArduinoJson key name strings\n     * \n     * If you need a dedicated settings page with custom layout for your Usermod, that takes a lot more work.  \n     * You will have to add the setting to the HTML, xml.cpp and set.cpp manually.\n     * See the WLED Soundreactive fork (code and wiki) for reference.  https://github.com/atuline/WLED\n     * \n     * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings!\n     */\n    void addToConfig(JsonObject& root)\n    {\n      JsonObject battery = root.createNestedObject(FPSTR(_name));\n      \n      if (battery.isNull()) {\n        battery = root.createNestedObject(FPSTR(_name));\n      }\n\n      #ifdef ARDUINO_ARCH_ESP32\n        battery[F(\"pin\")] = batteryPin;\n      #endif\n      \n      addBatteryToJsonObject(battery, false);\n\n      // read voltage in case calibration or voltage multiplier changed to see immediate effect\n      bat->setVoltage(readVoltage());\n\n      DEBUG_PRINTLN(F(\"Battery config saved.\"));\n    }\n\n    void appendConfigData()\n    {\n      // Total: 462 Bytes\n      oappend(F(\"td=addDropdown('Battery','type');\"));              // 34 Bytes\n      oappend(F(\"addOption(td,'Unkown','0');\"));                    // 28 Bytes\n      oappend(F(\"addOption(td,'LiPo','1');\"));                      // 26 Bytes\n      oappend(F(\"addOption(td,'LiOn','2');\"));                      // 26 Bytes\n      oappend(F(\"addInfo('Battery:type',1,'<small style=\\\"color:orange\\\">requires reboot</small>');\")); // 81 Bytes\n      oappend(F(\"addInfo('Battery:min-voltage',1,'v');\"));          // 38 Bytes\n      oappend(F(\"addInfo('Battery:max-voltage',1,'v');\"));          // 38 Bytes\n      oappend(F(\"addInfo('Battery:interval',1,'ms');\"));            // 36 Bytes\n      oappend(F(\"addInfo('Battery:HA-discovery',1,'');\"));          // 38 Bytes\n      oappend(F(\"addInfo('Battery:auto-off:threshold',1,'%');\"));   // 45 Bytes\n      oappend(F(\"addInfo('Battery:indicator:threshold',1,'%');\"));  // 46 Bytes\n      oappend(F(\"addInfo('Battery:indicator:duration',1,'s');\"));   // 45 Bytes\n      \n      // this option list would exeed the oappend() buffer\n      // a list of all presets to select one from\n      // oappend(F(\"bd=addDropdown('Battery:low-power-indicator', 'preset');\"));\n      // the loop generates: oappend(F(\"addOption(bd, 'preset name', preset id);\"));\n      // for(int8_t i=1; i < 42; i++) {\n      //   oappend(F(\"addOption(bd, 'Preset#\"));\n      //   oappendi(i);\n      //   oappend(F(\"',\"));\n      //   oappendi(i);\n      //   oappend(F(\");\"));\n      // }\n    }\n\n\n    /**\n     * readFromConfig() can be used to read back the custom settings you added with addToConfig().\n     * This is called by WLED when settings are loaded (currently this only happens immediately after boot, or after saving on the Usermod Settings page)\n     * \n     * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes),\n     * 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.\n     * If you don't know what that is, don't fret. It most likely doesn't affect your use case :)\n     * \n     * Return true in case the config values returned from Usermod Settings were complete, or false if you'd like WLED to save your defaults to disk (so any missing values are editable in Usermod Settings)\n     * \n     * getJsonValue() returns false if the value is missing, or copies the value into the variable provided and returns true if the value is present\n     * The configComplete variable is true only if the \"exampleUsermod\" object and all values are present.  If any values are missing, WLED will know to call addToConfig() to save them\n     * \n     * This function is guaranteed to be called on boot, but could also be called every time settings are updated\n     */\n    bool readFromConfig(JsonObject& root)\n    {\n      #ifdef ARDUINO_ARCH_ESP32\n        int8_t newBatteryPin = batteryPin;\n      #endif\n      \n      JsonObject battery = root[FPSTR(_name)];\n      if (battery.isNull()) \n      {\n        DEBUG_PRINT(FPSTR(_name));\n        DEBUG_PRINTLN(F(\": No config found. (Using defaults.)\"));\n        return false;\n      }\n\n      #ifdef ARDUINO_ARCH_ESP32\n        newBatteryPin     = battery[F(\"pin\")] | newBatteryPin;\n      #endif\n      setMinBatteryVoltage(battery[F(\"min-voltage\")] | bat->getMinVoltage());\n      setMaxBatteryVoltage(battery[F(\"max-voltage\")] | bat->getMaxVoltage());\n      setCalibration(battery[F(\"calibration\")] | bat->getCalibration());\n      setVoltageMultiplier(battery[F(\"voltage-multiplier\")] | bat->getVoltageMultiplier());\n      setReadingInterval(battery[FPSTR(_readInterval)] | readingInterval);\n      setHomeAssistantDiscovery(battery[FPSTR(_haDiscovery)] | HomeAssistantDiscovery);\n\n      getUsermodConfigFromJsonObject(battery);\n\n      #ifdef ARDUINO_ARCH_ESP32\n        if (!initDone) \n        {\n          // first run: reading from cfg.json\n          batteryPin = newBatteryPin;\n          DEBUG_PRINTLN(F(\" config loaded.\"));\n        } \n        else \n        {\n          DEBUG_PRINTLN(F(\" config (re)loaded.\"));\n\n          // changing parameters from settings page\n          if (newBatteryPin != batteryPin) \n          {\n            // deallocate pin\n            PinManager::deallocatePin(batteryPin, PinOwner::UM_Battery);\n            batteryPin = newBatteryPin;\n            // initialise\n            setup();\n          }\n        }\n      #endif\n\n      return !battery[FPSTR(_readInterval)].isNull();\n    }\n\n#ifndef WLED_DISABLE_MQTT\n    void onMqttConnect(bool sessionPresent)\n    {\n      // Home Assistant Autodiscovery\n      if (!HomeAssistantDiscovery)\n        return;\n\n      // battery percentage\n      char mqttBatteryTopic[128];\n      snprintf_P(mqttBatteryTopic, 127, PSTR(\"%s/battery\"), mqttDeviceTopic);\n      this->addMqttSensor(F(\"Battery\"), \"sensor\", mqttBatteryTopic, \"battery\", \"%\", true);\n\n      // voltage\n      char mqttVoltageTopic[128];\n      snprintf_P(mqttVoltageTopic, 127, PSTR(\"%s/voltage\"), mqttDeviceTopic);\n      this->addMqttSensor(F(\"Voltage\"), \"sensor\", mqttVoltageTopic, \"voltage\", \"V\", true);\n    }\n#endif   \n\n    /*\n     *\n     * Getter and Setter. Just in case some other usermod wants to interact with this in the future\n     *\n     */\n\n    /**\n     * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).\n     * This could be used in the future for the system to determine whether your usermod is installed.\n     */\n    uint16_t getId()\n    {\n      return USERMOD_ID_BATTERY;\n    }\n\n    /**\n     * get currently active battery type\n     */\n    batteryType getBatteryType()\n    {\n      return cfg.type;\n    }\n\n    /**\n     * \n     */\n    unsigned long getReadingInterval()\n    {\n      return readingInterval;\n    }\n\n    /**\n     * minimum repetition is 3000ms (3s) \n     */\n    void setReadingInterval(unsigned long newReadingInterval)\n    {\n      readingInterval = max((unsigned long)3000, newReadingInterval);\n    }\n\n    /**\n     * Get lowest configured battery voltage\n     */\n    float getMinBatteryVoltage()\n    {\n      return bat->getMinVoltage();\n    }\n\n    /**\n     * Set lowest battery voltage\n     * can't be below 0 volt\n     */\n    void setMinBatteryVoltage(float voltage)\n    {\n      bat->setMinVoltage(voltage);\n    }\n\n    /**\n     * Get highest configured battery voltage\n     */\n    float getMaxBatteryVoltage()\n    {\n      return bat->getMaxVoltage();\n    }\n    \n    /**\n     * Set highest battery voltage\n     * can't be below minBatteryVoltage\n     */\n    void setMaxBatteryVoltage(float voltage)\n    {\n      bat->setMaxVoltage(voltage);\n    }\n\n\n    /**\n     * Get the calculated voltage\n     * formula: (adc pin value / adc precision * max voltage) + calibration\n     */\n    float getVoltage()\n    {\n      return bat->getVoltage();\n    }\n\n    /**\n     * Get the mapped battery level (0 - 100) based on voltage\n     * important: voltage can drop when a load is applied, so its only an estimate\n     */\n    int8_t getBatteryLevel()\n    {\n      return bat->getLevel();\n    }\n\n    /**\n     * Get the configured calibration value\n     * a offset value to fine-tune the calculated voltage.\n     */\n    float getCalibration()\n    {\n      return bat->getCalibration();\n    }\n\n    /**\n     * Set the voltage calibration offset value\n     * a offset value to fine-tune the calculated voltage.\n     */\n    void setCalibration(float offset)\n    {\n      bat->setCalibration(offset);\n    }\n\n    /**\n     * Set the voltage multiplier value\n     * A multiplier that may need adjusting for different voltage divider setups\n     */\n    void setVoltageMultiplier(float multiplier)\n    {\n      bat->setVoltageMultiplier(multiplier);\n    }\n\n    /*\n     * Get the voltage multiplier value\n     * A multiplier that may need adjusting for different voltage divider setups\n     */\n    float getVoltageMultiplier()\n    {\n      return bat->getVoltageMultiplier();\n    }\n\n    /**\n     * Get auto-off feature enabled status\n     * is auto-off enabled, true/false\n     */\n    bool getAutoOffEnabled()\n    {\n      return autoOffEnabled;\n    }\n\n    /**\n     * Set auto-off feature status \n     */\n    void setAutoOffEnabled(bool enabled)\n    {\n      autoOffEnabled = enabled;\n    }\n    \n    /**\n     * Get auto-off threshold in percent (0-100)\n     */\n    int8_t getAutoOffThreshold()\n    {\n      return autoOffThreshold;\n    }\n\n    /**\n     * Set auto-off threshold in percent (0-100) \n     */\n    void setAutoOffThreshold(int8_t threshold)\n    {\n      autoOffThreshold = min((int8_t)100, max((int8_t)0, threshold));\n      // when low power indicator is enabled the auto-off threshold cannot be above indicator threshold\n      autoOffThreshold  = lowPowerIndicatorEnabled /*&& autoOffEnabled*/ ? min(lowPowerIndicatorThreshold-1, (int)autoOffThreshold) : autoOffThreshold;\n    }\n\n    /**\n     * Get low-power-indicator feature enabled status\n     * is the low-power-indicator enabled, true/false\n     */\n    bool getLowPowerIndicatorEnabled()\n    {\n      return lowPowerIndicatorEnabled;\n    }\n\n    /**\n     * Set low-power-indicator feature status \n     */\n    void setLowPowerIndicatorEnabled(bool enabled)\n    {\n      lowPowerIndicatorEnabled = enabled;\n    }\n\n    /**\n     * Get low-power-indicator preset to activate when low power is detected\n     */\n    int8_t getLowPowerIndicatorPreset()\n    {\n      return lowPowerIndicatorPreset;\n    }\n\n    /** \n     * Set low-power-indicator preset to activate when low power is detected\n     */\n    void setLowPowerIndicatorPreset(int8_t presetId)\n    {\n      // String tmp = \"\"; For what ever reason this doesn't work :(\n      // lowPowerIndicatorPreset = getPresetName(presetId, tmp) ? presetId : lowPowerIndicatorPreset;\n      lowPowerIndicatorPreset = presetId;\n    }\n\n    /*\n     * Get low-power-indicator threshold in percent (0-100)\n     */\n    int8_t getLowPowerIndicatorThreshold()\n    {\n      return lowPowerIndicatorThreshold;\n    }\n\n    /**\n     * Set low-power-indicator threshold in percent (0-100)\n     */\n    void setLowPowerIndicatorThreshold(int8_t threshold)\n    {\n      lowPowerIndicatorThreshold = threshold;\n      // when auto-off is enabled the indicator threshold cannot be below auto-off threshold\n      lowPowerIndicatorThreshold  = autoOffEnabled /*&& lowPowerIndicatorEnabled*/ ? max(autoOffThreshold+1, (int)lowPowerIndicatorThreshold) : max(5, (int)lowPowerIndicatorThreshold);\n    }\n\n    /**\n     * Get low-power-indicator duration in seconds\n     */\n    int8_t getLowPowerIndicatorDuration()\n    {\n      return lowPowerIndicatorDuration;\n    }\n\n    /**\n     * Set low-power-indicator duration in seconds\n     */\n    void setLowPowerIndicatorDuration(int8_t duration)\n    {\n      lowPowerIndicatorDuration = duration;\n    }\n\n    /**\n     * Get low-power-indicator status when the indication is done this returns true\n     */\n    bool getLowPowerIndicatorDone()\n    {\n      return lowPowerIndicationDone;\n    }\n\n    /**\n     * Set Home Assistant auto discovery\n     */\n    void setHomeAssistantDiscovery(bool enable)\n    {\n      HomeAssistantDiscovery = enable;\n    }\n\n    /**\n     * Get Home Assistant auto discovery\n     */\n    bool getHomeAssistantDiscovery()\n    {\n      return HomeAssistantDiscovery;\n    }\n};\n\n// strings to reduce flash memory usage (used more than twice)\nconst char UsermodBattery::_name[]          PROGMEM = \"Battery\";\nconst char UsermodBattery::_readInterval[]  PROGMEM = \"interval\";\nconst char UsermodBattery::_enabled[]       PROGMEM = \"enabled\";\nconst char UsermodBattery::_threshold[]     PROGMEM = \"threshold\";\nconst char UsermodBattery::_preset[]        PROGMEM = \"preset\";\nconst char UsermodBattery::_duration[]      PROGMEM = \"duration\";\nconst char UsermodBattery::_init[]          PROGMEM = \"init\";\nconst char UsermodBattery::_haDiscovery[]   PROGMEM = \"HA-discovery\";\n\n\nstatic UsermodBattery battery;\nREGISTER_USERMOD(battery);"
  },
  {
    "path": "usermods/Battery/UMBattery.h",
    "content": "#ifndef UMBBattery_h\n#define UMBBattery_h\n\n#include \"battery_defaults.h\"\n\n/**\n *  Battery base class\n *  all other battery classes should inherit from this\n */\nclass UMBattery\n{\n    private:\n\n    protected:\n        float minVoltage;\n        float maxVoltage;\n        float voltage;\n        int8_t level = 100;\n        float calibration; // offset or calibration value to fine tune the calculated voltage\n        float voltageMultiplier; // ratio for the voltage divider\n        \n        float linearMapping(float v, float min, float max, float oMin = 0.0f, float oMax = 100.0f)\n        {\n            return (v-min) * (oMax-oMin) / (max-min) + oMin;\n        }\n\n    public:\n        UMBattery()\n        {\n            this->setVoltageMultiplier(USERMOD_BATTERY_VOLTAGE_MULTIPLIER);\n            this->setCalibration(USERMOD_BATTERY_CALIBRATION);\n        }\n\n        virtual void update(batteryConfig cfg)\n        {\n            if(cfg.minVoltage) this->setMinVoltage(cfg.minVoltage);\n            if(cfg.maxVoltage) this->setMaxVoltage(cfg.maxVoltage);\n            if(cfg.level) this->setLevel(cfg.level);\n            if(cfg.calibration) this->setCalibration(cfg.calibration);\n            if(cfg.voltageMultiplier) this->setVoltageMultiplier(cfg.voltageMultiplier);\n        }\n\n        /**\n         * Corresponding battery curves\n         * calculates the level in % (0-100) with given voltage and possible voltage range\n         */\n        virtual float mapVoltage(float v, float min, float max) = 0;\n        // { \n        //     example implementation, linear mapping\n        //     return (v-min) * 100 / (max-min);\n        // };\n\n        virtual void calculateAndSetLevel(float voltage) = 0;\n\n\n\n        /*\n         *\n         * Getter and Setter\n         *\n         */\n\n        /*\n        * Get lowest configured battery voltage\n        */\n        virtual float getMinVoltage()\n        {\n            return this->minVoltage;\n        }\n\n        /*\n         * Set lowest battery voltage\n         * can't be below 0 volt\n         */\n        virtual void setMinVoltage(float voltage)\n        {\n            this->minVoltage = max(0.0f, voltage);\n        }\n\n        /*\n         * Get highest configured battery voltage\n         */\n        virtual float getMaxVoltage()\n        {\n            return this->maxVoltage;\n        }\n\n        /*\n         * Set highest battery voltage\n         * can't be below minVoltage\n         */\n        virtual void setMaxVoltage(float voltage)\n        {\n            this->maxVoltage = max(getMinVoltage()+.5f, voltage);\n        }\n\n        float getVoltage()\n        {\n            return this->voltage;\n        }\n\n        /**\n         * check if voltage is within specified voltage range, allow 10% over/under voltage\n         */\n        void setVoltage(float voltage)\n        {\n            // this->voltage = ( (voltage < this->getMinVoltage() * 0.85f) || (voltage > this->getMaxVoltage() * 1.1f) ) \n            //     ? -1.0f \n            //     : voltage;\n            this->voltage = voltage;\n        }\n\n        float getLevel()\n        {\n            return this->level;\n        }\n\n        void setLevel(float level)\n        {\n            this->level = constrain(level, 0.0f, 110.0f);\n        }\n\n        /*\n         * Get the configured calibration value\n         * a offset value to fine-tune the calculated voltage.\n         */\n        virtual float getCalibration()\n        {\n            return calibration;\n        }\n\n        /*\n         * Set the voltage calibration offset value\n         * a offset value to fine-tune the calculated voltage.\n         */\n        virtual void setCalibration(float offset)\n        {\n            calibration = offset;\n        }\n\n        /*\n         * Get the configured calibration value\n         * a value to set the voltage divider ratio\n         */\n        virtual float getVoltageMultiplier()\n        {\n            return voltageMultiplier;\n        }\n\n        /*\n         * Set the voltage multiplier value\n         * a value to set the voltage divider ratio.\n         */\n        virtual void setVoltageMultiplier(float multiplier)\n        {\n            voltageMultiplier = multiplier;\n        }\n};\n\n#endif"
  },
  {
    "path": "usermods/Battery/battery_defaults.h",
    "content": "#ifndef UMBDefaults_h\n#define UMBDefaults_h\n\n#include \"wled.h\"\n\n// pin defaults\n// for the esp32 it is best to use the ADC1: GPIO32 - GPIO39\n// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/adc.html\n#ifndef USERMOD_BATTERY_MEASUREMENT_PIN\n  #ifdef ARDUINO_ARCH_ESP32\n    #define USERMOD_BATTERY_MEASUREMENT_PIN 35\n  #else //ESP8266 boards\n    #define USERMOD_BATTERY_MEASUREMENT_PIN A0\n  #endif\n#endif\n\n// The initial delay before the first battery voltage reading after power-on.\n// This allows the voltage to stabilize before readings are taken, improving accuracy of initial reading.\n#ifndef USERMOD_BATTERY_INITIAL_DELAY\n  #define USERMOD_BATTERY_INITIAL_DELAY 10000 // (milliseconds)\n#endif\n\n// the frequency to check the battery, 30 sec\n#ifndef USERMOD_BATTERY_MEASUREMENT_INTERVAL\n  #define USERMOD_BATTERY_MEASUREMENT_INTERVAL 30000\n#endif\n\n\n/* Default Battery Type\n * 0 = unkown\n * 1 = Lipo\n * 2 = Lion\n */\n#ifndef USERMOD_BATTERY_DEFAULT_TYPE\n  #define USERMOD_BATTERY_DEFAULT_TYPE 0\n#endif\n/*\n *\n *  Unkown 'Battery' defaults\n *\n */\n#ifndef USERMOD_BATTERY_UNKOWN_MIN_VOLTAGE\n  // Extra save defaults\n  #define USERMOD_BATTERY_UNKOWN_MIN_VOLTAGE 3.3f\n#endif\n#ifndef USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE\n  #define USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE 4.2f\n#endif\n\n/*\n *\n *  Lithium polymer (Li-Po) defaults\n *\n */\n#ifndef USERMOD_BATTERY_LIPO_MIN_VOLTAGE\n  // LiPo \"1S\" Batteries should not be dischared below 3V !!\n  #define USERMOD_BATTERY_LIPO_MIN_VOLTAGE 3.2f\n#endif\n#ifndef USERMOD_BATTERY_LIPO_MAX_VOLTAGE\n  #define USERMOD_BATTERY_LIPO_MAX_VOLTAGE 4.2f\n#endif\n\n/*\n *\n *  Lithium-ion (Li-Ion) defaults\n *\n */\n#ifndef USERMOD_BATTERY_LION_MIN_VOLTAGE\n  // default for 18650 battery\n  #define USERMOD_BATTERY_LION_MIN_VOLTAGE 2.6f\n#endif\n#ifndef USERMOD_BATTERY_LION_MAX_VOLTAGE\n  #define USERMOD_BATTERY_LION_MAX_VOLTAGE 4.2f\n#endif\n\n// the default ratio for the voltage divider\n#ifndef USERMOD_BATTERY_VOLTAGE_MULTIPLIER\n  #ifdef ARDUINO_ARCH_ESP32\n    #define USERMOD_BATTERY_VOLTAGE_MULTIPLIER 2.0f\n  #else //ESP8266 boards\n    #define USERMOD_BATTERY_VOLTAGE_MULTIPLIER 4.2f\n  #endif\n#endif\n\n#ifndef USERMOD_BATTERY_AVERAGING_ALPHA\n  #define USERMOD_BATTERY_AVERAGING_ALPHA 0.1f\n#endif\n\n// offset or calibration value to fine tune the calculated voltage\n#ifndef USERMOD_BATTERY_CALIBRATION\n  #define USERMOD_BATTERY_CALIBRATION 0\n#endif\n\n// auto-off feature\n#ifndef USERMOD_BATTERY_AUTO_OFF_ENABLED\n  #define USERMOD_BATTERY_AUTO_OFF_ENABLED true\n#endif\n\n#ifndef USERMOD_BATTERY_AUTO_OFF_THRESHOLD\n  #define USERMOD_BATTERY_AUTO_OFF_THRESHOLD 10\n#endif\n\n// low power indication feature\n#ifndef USERMOD_BATTERY_LOW_POWER_INDICATOR_ENABLED\n  #define USERMOD_BATTERY_LOW_POWER_INDICATOR_ENABLED true\n#endif\n\n#ifndef USERMOD_BATTERY_LOW_POWER_INDICATOR_PRESET\n  #define USERMOD_BATTERY_LOW_POWER_INDICATOR_PRESET 0\n#endif\n\n#ifndef USERMOD_BATTERY_LOW_POWER_INDICATOR_THRESHOLD\n  #define USERMOD_BATTERY_LOW_POWER_INDICATOR_THRESHOLD 20\n#endif\n\n#ifndef USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION\n  #define USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION 5\n#endif\n\n// battery types\ntypedef enum\n{\n  unknown=0,\n  lipo=1,\n  lion=2\n} batteryType;\n\n// used for initial configuration after boot \ntypedef struct bconfig_t \n{\n  batteryType type;\n  float minVoltage;\n  float maxVoltage;\n  float voltage;          // current voltage\n  int8_t level;           // current level\n  float calibration;      // offset or calibration value to fine tune the calculated voltage\n  float voltageMultiplier;\n} batteryConfig;\n\n#endif"
  },
  {
    "path": "usermods/Battery/library.json",
    "content": "{\n  \"name\": \"Battery\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/Battery/readme.md",
    "content": "<p align=\"center\">\n  <img width=\"700\" src=\"assets/battery_usermod_logo.png\">\n</p>\n\n# Welcome to the battery usermod! 🔋\n\nEnables battery level monitoring of your project.\n\n<p align=\"left\">\n  <img width=\"700\" src=\"assets/battery_info_screen.png\">\n</p>\n\n<br>\n\n## ⚙️ Features\n\n- 💯 Displays current battery voltage\n- 🚥 Displays battery level\n- 🚫 Auto-off with configurable threshold\n- 🚨 Low power indicator with many configuration possibilities\n\n<br><br>\n\n## 🎈 Installation\n\nIn `platformio_override.ini` (or `platformio.ini`)<br>Under: `custom_usermods =`, add the line: `Battery`<br><br>[Example: platformio_override.ini](assets/installation_platformio_override_ini.png) |\n\n<br><br>\n\n## 🔌 Example wiring\n\n- (see [Useful Links](#useful-links)).\n\n<table style=\"width: 100%; table-layout: fixed;\">\n<tr>\n  <!-- Column for the first image -->\n  <td style=\"width: 50%; vertical-align: bottom;\">\n    <img width=\"300\" src=\"assets/battery_connection_schematic_01.png\" style=\"display: block;\">\n    <p><strong>ESP8266</strong><br>\n      With a 100k Ohm resistor, connect the positive<br>\n      side of the battery to pin `A0`.</p>\n  </td>\n  <!-- Column for the second image -->\n  <td style=\"width: 50%; vertical-align: bottom;\">\n    <img width=\"250\" src=\"assets/battery_connection_schematic_esp32.png\" style=\"display: block;\">\n    <p><strong>ESP32</strong> (+S2, S3, C3 etc...)<br>\n      Use a voltage divider (two resistors of equal value).<br>\n      Connect to ADC1 (GPIO32 - GPIO39). GPIO35 is Default.</p>\n  </td>\n</tr>\n</table>\n\n<br><br>\n\n## Define Your Options\n\n| Name                                            | Unit        | Description                                                                           |\n| ----------------------------------------------- | ----------- |-------------------------------------------------------------------------------------- |\n| `USERMOD_BATTERY`                               |             | Define this (in `my_config.h`) to have this usermod included wled00\\usermods_list.cpp |\n| `USERMOD_BATTERY_MEASUREMENT_PIN`               |             | Defaults to A0 on ESP8266 and GPIO35 on ESP32                                         |\n| `USERMOD_BATTERY_MEASUREMENT_INTERVAL`          | ms          | Battery check interval. defaults to 30 seconds                                        |\n| `USERMOD_BATTERY_INITIAL_DELAY`                 | ms          | Delay before initial reading. defaults to 10 seconds to allow voltage stabilization   |\n| `USERMOD_BATTERY_{TYPE}_MIN_VOLTAGE`            | v           | Minimum battery voltage. default is 2.6 (18650 battery standard)                      |\n| `USERMOD_BATTERY_{TYPE}_MAX_VOLTAGE`            | v           | Maximum battery voltage. default is 4.2 (18650 battery standard)                      |\n| `USERMOD_BATTERY_{TYPE}_TOTAL_CAPACITY`         | mAh         | The capacity of all cells in parallel summed up                                       |\n| `USERMOD_BATTERY_{TYPE}_CALIBRATION`            |             | Offset / calibration number, fine tune the measured voltage by the microcontroller    |\n| Auto-Off                                        | ---         | ---                                                                                   |\n| `USERMOD_BATTERY_AUTO_OFF_ENABLED`              | true/false  | Enables auto-off                                                                      |\n| `USERMOD_BATTERY_AUTO_OFF_THRESHOLD`            | % (0-100)   | When this threshold is reached master power turns off                                 |\n| Low-Power-Indicator                             | ---         | ---                                                                                   |\n| `USERMOD_BATTERY_LOW_POWER_INDICATOR_ENABLED`   | true/false  | Enables low power indication                                                          |\n| `USERMOD_BATTERY_LOW_POWER_INDICATOR_PRESET`    | preset id   | When low power is detected then use this preset to indicate low power                 |\n| `USERMOD_BATTERY_LOW_POWER_INDICATOR_THRESHOLD` | % (0-100)   | When this threshold is reached low power gets indicated                               |\n| `USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION`  | seconds     | For this long the configured preset is played                                         |\n\nAll parameters can be configured at runtime via the Usermods settings page.\n\n<br>\n\n**NOTICE:** Each Battery type can be pre-configured individualy (in `my_config.h`)\n\n| Name            | Alias         | `my_config.h` example                 |\n| --------------- | ------------- | ------------------------------------- |\n| Lithium Polymer | lipo (Li-Po)  | `USERMOD_BATTERY_lipo_MIN_VOLTAGE`    |\n| Lithium Ionen   | lion (Li-Ion) | `USERMOD_BATTERY_lion_TOTAL_CAPACITY` |\n\n<br><br>\n\n## 🔧 Calibration\n\nThe calibration number is a value that is added to the final computed voltage after it has been scaled by the voltage multiplier. \n\nIt fine-tunes the voltage reading so that it more closely matches the actual battery voltage, compensating for inaccuracies inherent in the voltage divider resistors or the ESP's ADC measurements.\n\nSet calibration either in the Usermods settings page or at compile time in `my_config.h` or `platformio_override.ini`.\n\nIt can be either a positive or negative number.\n\n<br><br>\n\n## ⚠️ Important\n\nMake sure you know your battery specifications! All batteries are **NOT** the same!\n\nExample:\n\n| Your battery specification table  |                 | Options you can define        |\n| --------------------------------- | --------------- | ----------------------------- |\n| Capacity                          | 3500mAh 12.5Wh  |                               |\n| Minimum capacity                  | 3350mAh 11.9Wh  |                               |\n| Rated voltage                     | 3.6V - 3.7V     |                               |\n| **Charging end voltage**          | **4.2V ± 0.05** | `USERMOD_BATTERY_MAX_VOLTAGE` |\n| **Discharge voltage**             | **2.5V**        | `USERMOD_BATTERY_MIN_VOLTAGE` |\n| Max. discharge current (constant) | 10A (10000mA)   |                               |\n| max. charging current             | 1.7A (1700mA)   |                               |\n| ...                               | ...             | ...                           |\n| ..                                | ..              | ..                            |\n\nSpecification from: [Molicel INR18650-M35A, 3500mAh 10A Lithium-ion battery, 3.6V - 3.7V](https://www.akkuteile.de/lithium-ionen-akkus/18650/molicel/molicel-inr18650-m35a-3500mah-10a-lithium-ionen-akku-3-6v-3-7v_100833)\n\n<br><br>\n\n## 🌐 Useful Links\n\n- https://lazyzero.de/elektronik/esp8266/wemos_d1_mini_a0/start\n- https://arduinodiy.wordpress.com/2016/12/25/monitoring-lipo-battery-voltage-with-wemos-d1-minibattery-shield-and-thingspeak/\n\n<br><br>\n\n## 📝 Change Log\n\n2024-08-19\n\n- Improved MQTT support\n- Added battery percentage & battery voltage as MQTT topic\n\n2024-05-11\n\n- Documentation updated\n\n2024-04-30\n\n- Integrate factory pattern to make it easier to add other / custom battery types\n- Update readme\n- Improved initial reading accuracy by delaying initial measurement to allow voltage to stabilize at power-on\n\n2023-01-04\n\n- Basic support for LiPo rechargeable batteries (`-D USERMOD_BATTERY_USE_LIPO`)\n- Improved support for ESP32 (read calibrated voltage)\n- Corrected config saving (measurement pin, and battery min/max were lost)\n- Various bugfixes\n\n2022-12-25\n\n- Added \"auto-off\" feature\n- Added \"low-power-indication\" feature\n- Added \"calibration/offset\" field to configuration page\n- Added getter and setter, so that user usermods could interact with this one\n- Update readme (added new options, made it markdownlint compliant)\n\n2021-09-02\n\n- Added \"Battery voltage\" to info\n- Added circuit diagram to readme\n- Added MQTT support, sending battery voltage\n- Minor fixes\n\n2021-08-15\n\n- Changed `USERMOD_BATTERY_MIN_VOLTAGE` to 2.6 volt as default for 18650 batteries\n- Updated readme, added specification table\n\n2021-08-10\n\n- Created\n"
  },
  {
    "path": "usermods/Battery/types/LionUMBattery.h",
    "content": "#ifndef UMBLion_h\n#define UMBLion_h\n\n#include \"../battery_defaults.h\"\n#include \"../UMBattery.h\"\n\n/**\n *  LiOn Battery\n * \n */\nclass LionUMBattery : public UMBattery\n{\n    private:\n\n    public:\n        LionUMBattery() : UMBattery()\n        {\n            this->setMinVoltage(USERMOD_BATTERY_LION_MIN_VOLTAGE);\n            this->setMaxVoltage(USERMOD_BATTERY_LION_MAX_VOLTAGE);\n        }\n\n        float mapVoltage(float v, float min, float max) override\n        {\n            return this->linearMapping(v, min, max); // basic mapping\n        };\n\n        void calculateAndSetLevel(float voltage) override\n        {\n            this->setLevel(this->mapVoltage(voltage, this->getMinVoltage(), this->getMaxVoltage()));\n        };\n\n        virtual void setMaxVoltage(float voltage) override\n        {\n            this->maxVoltage = max(getMinVoltage()+1.0f, voltage);\n        }\n};\n\n#endif"
  },
  {
    "path": "usermods/Battery/types/LipoUMBattery.h",
    "content": "#ifndef UMBLipo_h\n#define UMBLipo_h\n\n#include \"../battery_defaults.h\"\n#include \"../UMBattery.h\"\n\n/**\n *  LiPo Battery\n * \n */\nclass LipoUMBattery : public UMBattery\n{\n    private:\n\n    public:\n        LipoUMBattery() : UMBattery()\n        {\n            this->setMinVoltage(USERMOD_BATTERY_LIPO_MIN_VOLTAGE);\n            this->setMaxVoltage(USERMOD_BATTERY_LIPO_MAX_VOLTAGE);\n        }\n\n        /**\n         * LiPo batteries have a differnt discharge curve, see \n         * https://blog.ampow.com/lipo-voltage-chart/\n         */\n        float mapVoltage(float v, float min, float max) override \n        {\n            float lvl = 0.0f;\n            lvl = this->linearMapping(v, min, max); // basic mapping\n\n            if (lvl < 40.0f) \n                lvl = this->linearMapping(lvl, 0, 40, 0, 12);       // last 45% -> drops very quickly\n            else {\n            if (lvl < 90.0f)\n                lvl = this->linearMapping(lvl, 40, 90, 12, 95);     // 90% ... 40% -> almost linear drop\n            else // level >  90%\n                lvl = this->linearMapping(lvl, 90, 105, 95, 100);   // highest 15% -> drop slowly\n            }\n\n            return lvl;\n        };\n\n        void calculateAndSetLevel(float voltage) override\n        {\n            this->setLevel(this->mapVoltage(voltage, this->getMinVoltage(), this->getMaxVoltage()));\n        };\n\n        virtual void setMaxVoltage(float voltage) override\n        {\n            this->maxVoltage = max(getMinVoltage()+0.7f, voltage);\n        }\n};\n\n#endif"
  },
  {
    "path": "usermods/Battery/types/UnkownUMBattery.h",
    "content": "#ifndef UMBUnkown_h\n#define UMBUnkown_h\n\n#include \"../battery_defaults.h\"\n#include \"../UMBattery.h\"\n\n/**\n *  Unkown / Default Battery\n * \n */\nclass UnkownUMBattery : public UMBattery\n{\n    private:\n\n    public:\n        UnkownUMBattery() : UMBattery()\n        {\n            this->setMinVoltage(USERMOD_BATTERY_UNKOWN_MIN_VOLTAGE);\n            this->setMaxVoltage(USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE);\n        }\n\n        void update(batteryConfig cfg)\n        {\n            if(cfg.minVoltage) this->setMinVoltage(cfg.minVoltage); else this->setMinVoltage(USERMOD_BATTERY_UNKOWN_MIN_VOLTAGE);\n            if(cfg.maxVoltage) this->setMaxVoltage(cfg.maxVoltage); else this->setMaxVoltage(USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE);\n        }\n\n        float mapVoltage(float v, float min, float max) override\n        {\n            return this->linearMapping(v, min, max); // basic mapping\n        };\n\n        void calculateAndSetLevel(float voltage) override\n        {\n            this->setLevel(this->mapVoltage(voltage, this->getMinVoltage(), this->getMaxVoltage()));\n        };\n};\n\n#endif"
  },
  {
    "path": "usermods/Cronixie/Cronixie.cpp",
    "content": "#include \"wled.h\"\n\nclass UsermodCronixie : public Usermod {\n  private:\n    unsigned long lastTime = 0;\n    char cronixieDisplay[7] = \"HHMMSS\";\n    byte _digitOut[6] = {10,10,10,10,10,10};\n    byte dP[6] = {255, 255, 255, 255, 255, 255};\n\n    // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer)\n    bool backlight = true;\n\n  public:\n    void initCronixie()\n    {\n      if (dP[0] == 255) // if dP[0] is 255, cronixie is not yet init'ed\n      {\n        setCronixie();\n        strip.getSegment(0).grouping = 10; // 10 LEDs per digit\n      }\n    }\n\n    void setup() {\n\n    }\n\n    void loop() {\n      if (!toki.isTick()) return;\n      initCronixie();\n      _overlayCronixie();\n      strip.trigger();\n    }\n\n    byte getSameCodeLength(char code, int index, char const cronixieDisplay[])\n    {\n      byte counter = 0;\n      \n      for (int i = index+1; i < 6; i++)\n      {\n        if (cronixieDisplay[i] == code)\n        {\n          counter++;\n        } else {\n          return counter;\n        }\n      }\n      return counter;\n    }\n\n    void setCronixie()\n    {\n      /*\n      * digit purpose index\n      * 0-9 | 0-9 (incl. random)\n      * 10 | blank\n      * 11 | blank, bg off\n      * 12 | test upw.\n      * 13 | test dnw.\n      * 14 | binary AM/PM\n      * 15 | BB upper +50 for no trailing 0\n      * 16 | BBB\n      * 17 | BBBB\n      * 18 | BBBBB\n      * 19 | BBBBBB\n      * 20 | H\n      * 21 | HH\n      * 22 | HHH\n      * 23 | HHHH\n      * 24 | M\n      * 25 | MM\n      * 26 | MMM\n      * 27 | MMMM\n      * 28 | MMMMM\n      * 29 | MMMMMM\n      * 30 | S\n      * 31 | SS\n      * 32 | SSS\n      * 33 | SSSS\n      * 34 | SSSSS\n      * 35 | SSSSSS\n      * 36 | Y\n      * 37 | YY\n      * 38 | YYYY\n      * 39 | I\n      * 40 | II\n      * 41 | W\n      * 42 | WW\n      * 43 | D\n      * 44 | DD\n      * 45 | DDD\n      * 46 | V\n      * 47 | VV\n      * 48 | VVV\n      * 49 | VVVV\n      * 50 | VVVVV\n      * 51 | VVVVVV\n      * 52 | v\n      * 53 | vv\n      * 54 | vvv\n      * 55 | vvvv\n      * 56 | vvvvv\n      * 57 | vvvvvv\n      */\n\n      //H HourLower | HH - Hour 24. | AH - Hour 12. | HHH Hour of Month | HHHH Hour of Year\n      //M MinuteUpper | MM Minute of Hour | MMM Minute of 12h | MMMM Minute of Day | MMMMM Minute of Month | MMMMMM Minute of Year\n      //S SecondUpper | SS Second of Minute | SSS Second of 10 Minute | SSSS Second of Hour | SSSSS Second of Day | SSSSSS Second of Week\n      //B AM/PM | BB 0-6/6-12/12-18/18-24 | BBB 0-3... | BBBB 0-1.5... | BBBBB 0-1 | BBBBBB 0-0.5\n      \n      //Y YearLower | YY - Year LU | YYYY - Std.\n      //I MonthLower | II - Month of Year \n      //W Week of Month | WW Week of Year\n      //D Day of Week | DD Day Of Month | DDD Day Of Year\n\n      DEBUG_PRINT(F(\"cset \"));\n      DEBUG_PRINTLN(cronixieDisplay);\n      \n      for (int i = 0; i < 6; i++)\n      {\n        dP[i] = 10;\n        switch (cronixieDisplay[i])\n        {\n          case '_': dP[i] = 10; break; \n          case '-': dP[i] = 11; break; \n          case 'r': dP[i] = random(1,7); break; //random btw. 1-6\n          case 'R': dP[i] = random(0,10); break; //random btw. 0-9\n          //case 't': break; //Test upw.\n          //case 'T': break; //Test dnw.\n          case 'b': dP[i] = 14 + getSameCodeLength('b',i,cronixieDisplay); i = i+dP[i]-14; break; \n          case 'B': dP[i] = 14 + getSameCodeLength('B',i,cronixieDisplay); i = i+dP[i]-14; break;\n          case 'h': dP[i] = 70 + getSameCodeLength('h',i,cronixieDisplay); i = i+dP[i]-70; break;\n          case 'H': dP[i] = 20 + getSameCodeLength('H',i,cronixieDisplay); i = i+dP[i]-20; break;\n          case 'A': dP[i] = 108; i++; break;\n          case 'a': dP[i] = 58; i++; break;\n          case 'm': dP[i] = 74 + getSameCodeLength('m',i,cronixieDisplay); i = i+dP[i]-74; break;\n          case 'M': dP[i] = 24 + getSameCodeLength('M',i,cronixieDisplay); i = i+dP[i]-24; break;\n          case 's': dP[i] = 80 + getSameCodeLength('s',i,cronixieDisplay); i = i+dP[i]-80; break; //refresh more often bc. of secs\n          case 'S': dP[i] = 30 + getSameCodeLength('S',i,cronixieDisplay); i = i+dP[i]-30; break;\n          case 'Y': dP[i] = 36 + getSameCodeLength('Y',i,cronixieDisplay); i = i+dP[i]-36; break; \n          case 'y': dP[i] = 86 + getSameCodeLength('y',i,cronixieDisplay); i = i+dP[i]-86; break; \n          case 'I': dP[i] = 39 + getSameCodeLength('I',i,cronixieDisplay); i = i+dP[i]-39; break;  //Month. Don't ask me why month and minute both start with M.\n          case 'i': dP[i] = 89 + getSameCodeLength('i',i,cronixieDisplay); i = i+dP[i]-89; break; \n          //case 'W': break;\n          //case 'w': break;\n          case 'D': dP[i] = 43 + getSameCodeLength('D',i,cronixieDisplay); i = i+dP[i]-43; break;\n          case 'd': dP[i] = 93 + getSameCodeLength('d',i,cronixieDisplay); i = i+dP[i]-93; break;\n          case '0': dP[i] = 0; break;\n          case '1': dP[i] = 1; break;\n          case '2': dP[i] = 2; break;\n          case '3': dP[i] = 3; break;\n          case '4': dP[i] = 4; break;\n          case '5': dP[i] = 5; break;\n          case '6': dP[i] = 6; break;\n          case '7': dP[i] = 7; break;\n          case '8': dP[i] = 8; break;\n          case '9': dP[i] = 9; break;\n          //case 'V': break; //user var0\n          //case 'v': break; //user var1\n        }\n      }\n      DEBUG_PRINT(F(\"result \"));\n      for (int i = 0; i < 5; i++)\n      {\n        DEBUG_PRINT((int)dP[i]);\n        DEBUG_PRINT(\" \");\n      }\n      DEBUG_PRINTLN((int)dP[5]);\n\n      _overlayCronixie(); // refresh\n    }\n\n    void _overlayCronixie()\n    {\n      byte h = hour(localTime);\n      byte h0 = h;\n      byte m = minute(localTime);\n      byte s = second(localTime);\n      byte d = day(localTime);\n      byte mi = month(localTime);\n      int y = year(localTime);\n      //this has to be changed in time for 22nd century\n      y -= 2000; if (y<0) y += 30; //makes countdown work\n\n      if (useAMPM && !countdownMode)\n      {\n        if (h>12) h-=12;\n        else if (h==0) h+=12;\n      }\n      for (int i = 0; i < 6; i++)\n      {\n        if (dP[i] < 12) _digitOut[i] = dP[i];\n        else {\n          if (dP[i] < 65)\n          {\n            switch(dP[i])\n            {\n              case 21: _digitOut[i] = h/10; _digitOut[i+1] = h- _digitOut[i]*10; i++; break; //HH\n              case 25: _digitOut[i] = m/10; _digitOut[i+1] = m- _digitOut[i]*10; i++; break; //MM\n              case 31: _digitOut[i] = s/10; _digitOut[i+1] = s- _digitOut[i]*10; i++; break; //SS\n\n              case 20: _digitOut[i] = h- (h/10)*10; break; //H\n              case 24: _digitOut[i] = m/10; break; //M\n              case 30: _digitOut[i] = s/10; break; //S\n              \n              case 43: _digitOut[i] = weekday(localTime); _digitOut[i]--; if (_digitOut[i]<1) _digitOut[i]= 7; break; //D\n              case 44: _digitOut[i] = d/10; _digitOut[i+1] = d- _digitOut[i]*10; i++; break; //DD\n              case 40: _digitOut[i] = mi/10; _digitOut[i+1] = mi- _digitOut[i]*10; i++; break; //II\n              case 37: _digitOut[i] = y/10; _digitOut[i+1] = y- _digitOut[i]*10; i++; break; //YY\n              case 39: _digitOut[i] = 2; _digitOut[i+1] = 0; _digitOut[i+2] = y/10; _digitOut[i+3] = y- _digitOut[i+2]*10; i+=3; break; //YYYY\n              \n              //case 16: _digitOut[i+2] = ((h0/3)&1)?1:0; i++; //BBB (BBBB NI)\n              //case 15: _digitOut[i+1] = (h0>17 || (h0>5 && h0<12))?1:0; i++; //BB\n              case 14: _digitOut[i] = (h0>11)?1:0; break; //B\n            }\n          } else\n          {\n            switch(dP[i])\n            {\n              case 71: _digitOut[i] = h/10; _digitOut[i+1] = h- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //hh\n              case 75: _digitOut[i] = m/10; _digitOut[i+1] = m- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //mm\n              case 81: _digitOut[i] = s/10; _digitOut[i+1] = s- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //ss\n              //case 66: _digitOut[i+2] = ((h0/3)&1)?1:10; i++; //bbb (bbbb NI)\n              //case 65: _digitOut[i+1] = (h0>17 || (h0>5 && h0<12))?1:10; i++; //bb\n              case 64: _digitOut[i] = (h0>11)?1:10; break; //b\n\n              case 93: _digitOut[i] = weekday(localTime); _digitOut[i]--; if (_digitOut[i]<1) _digitOut[i]= 7; break; //d\n              case 94: _digitOut[i] = d/10; _digitOut[i+1] = d- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //dd\n              case 90: _digitOut[i] = mi/10; _digitOut[i+1] = mi- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //ii\n              case 87: _digitOut[i] = y/10; _digitOut[i+1] = y- _digitOut[i]*10; i++; break; //yy\n              case 89: _digitOut[i] = 2; _digitOut[i+1] = 0; _digitOut[i+2] = y/10; _digitOut[i+3] = y- _digitOut[i+2]*10; i+=3; break; //yyyy\n            }\n          }\n        }\n      }\n    }\n\n    void handleOverlayDraw()\n    {\n      byte offsets[] = {5, 0, 6, 1, 7, 2, 8, 3, 9, 4};\n      \n      for (uint16_t i = 0; i < 6; i++)\n      {\n        byte o = 10*i;\n        byte excl = 10;\n        if(_digitOut[i] < 10) excl = offsets[_digitOut[i]];\n        excl += o;\n        \n        if (backlight && _digitOut[i] <11)\n        {\n          uint32_t col = strip.getSegment(0).colors[1];\n          for (uint16_t j=o; j< o+10; j++) {\n            if (j != excl) strip.setPixelColor(j, col);\n          }\n        } else\n        {\n          for (uint16_t j=o; j< o+10; j++) {\n            if (j != excl) strip.setPixelColor(j, 0);\n          }\n        }\n      }\n    }\n\n    void addToJsonState(JsonObject& root)\n    {\n      root[\"nx\"] = cronixieDisplay;\n    }\n\n    void readFromJsonState(JsonObject& root)\n    {\n      if (root[\"nx\"].is<const char*>()) {\n        strncpy(cronixieDisplay, root[\"nx\"], 6);\n        setCronixie();\n      }\n    }\n\n    void addToConfig(JsonObject& root)\n    {\n      JsonObject top = root.createNestedObject(F(\"Cronixie\"));\n      top[\"backlight\"] = backlight;\n    }\n\n    bool readFromConfig(JsonObject& root)\n    {\n      // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor\n      // 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)\n\n      JsonObject top = root[F(\"Cronixie\")];\n\n      bool configComplete = !top.isNull();\n\n      configComplete &= getJsonValue(top[\"backlight\"], backlight);\n\n      return configComplete;\n    }\n\n    uint16_t getId()\n    {\n      return USERMOD_ID_CRONIXIE;\n    }\n};\n\nstatic UsermodCronixie cronixie;\nREGISTER_USERMOD(cronixie);"
  },
  {
    "path": "usermods/Cronixie/library.json",
    "content": "{\n  \"name\": \"Cronixie\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/Cronixie/readme.md",
    "content": "# Cronixie clock usermod\n\nThis usermod supports driving the Cronixie M and L clock kits by Diamex.\n\n## Installation \n\nCompile and upload after adding `Cronixie` to `custom_usermods` of your PlatformIO environment.  \nMake sure the Auto Brightness Limiter is enabled at 420mA (!) and configure 60 WS281x LEDs."
  },
  {
    "path": "usermods/DHT/DHT.cpp",
    "content": "#include \"wled.h\"\n#ifdef WLED_DISABLE_MQTT\n#error \"This user mod requires MQTT to be enabled.\"\n#endif\n\n\n#include <dht_nonblocking.h>\n\n// USERMOD_DHT_DHTTYPE:\n//   11   // DHT 11\n//   21   // DHT 21\n//   22   // DHT 22  (AM2302), AM2321 *** default\n#ifndef USERMOD_DHT_DHTTYPE\n#define USERMOD_DHT_DHTTYPE 22\n#endif\n\n#if USERMOD_DHT_DHTTYPE == 11\n#define DHTTYPE DHT_TYPE_11\n#elif USERMOD_DHT_DHTTYPE == 21\n#define DHTTYPE DHT_TYPE_21\n#elif USERMOD_DHT_DHTTYPE == 22\n#define DHTTYPE DHT_TYPE_22\n#endif\n\n// Connect pin 1 (on the left) of the sensor to +5V\n//   NOTE: If using a board with 3.3V logic like an Arduino Due connect pin 1\n//   to 3.3V instead of 5V!\n// Connect pin 2 of the sensor to whatever your DHTPIN is\n//   NOTE: Pin defaults below are for QuinLed Dig-Uno's Q2 on the board\n// Connect pin 4 (on the right) of the sensor to GROUND\n//   NOTE: If using a bare sensor (AM*), Connect a 10K resistor from pin 2\n//   (data) to pin 1 (power) of the sensor. DHT* boards have the pullup already\n\n#ifdef USERMOD_DHT_PIN\n#define DHTPIN USERMOD_DHT_PIN\n#else\n#ifdef ARDUINO_ARCH_ESP32\n#define DHTPIN 21\n#else //ESP8266 boards\n#define DHTPIN 4\n#endif\n#endif\n\n// the frequency to check sensor, 1 minute\n#ifndef USERMOD_DHT_MEASUREMENT_INTERVAL\n#define USERMOD_DHT_MEASUREMENT_INTERVAL 60000\n#endif\n\n// how many seconds after boot to take first measurement, 90 seconds\n// 90 gives enough time to OTA update firmware if this crashes\n#ifndef USERMOD_DHT_FIRST_MEASUREMENT_AT\n#define USERMOD_DHT_FIRST_MEASUREMENT_AT 90000\n#endif\n\n// from COOLDOWN_TIME in dht_nonblocking.cpp\n#define DHT_TIMEOUT_TIME  10000\n\nDHT_nonblocking dht_sensor(DHTPIN, DHTTYPE);\n\nclass UsermodDHT : public Usermod {\n  private:\n    unsigned long nextReadTime = 0;\n    unsigned long lastReadTime = 0;\n    float humidity, temperature = 0;\n    bool initializing = true;\n    bool disabled = false;\n    #ifdef USERMOD_DHT_MQTT\n    char dhtMqttTopic[64];\n    size_t dhtMqttTopicLen;\n    #endif\n    #ifdef USERMOD_DHT_STATS\n    unsigned long nextResetStatsTime = 0;\n    uint16_t updates = 0;\n    uint16_t clean_updates = 0;\n    uint16_t errors = 0;\n    unsigned long maxDelay = 0;\n    unsigned long currentIteration = 0;\n    unsigned long maxIteration = 0;\n    #endif\n\n  public:\n    void setup() {\n      nextReadTime = millis() + USERMOD_DHT_FIRST_MEASUREMENT_AT;\n      lastReadTime = millis();\n      #ifdef USERMOD_DHT_MQTT\n      sprintf(dhtMqttTopic, \"%s/dht\", mqttDeviceTopic);\n      dhtMqttTopicLen = strlen(dhtMqttTopic);\n      #endif\n      #ifdef USERMOD_DHT_STATS\n      nextResetStatsTime = millis() + 60*60*1000;\n      #endif\n    }\n\n    void loop() {\n      if (disabled) {\n        return;\n      }\n      if (millis() < nextReadTime) {\n        return;\n      }\n\n      #ifdef USERMOD_DHT_STATS\n      if (millis() >= nextResetStatsTime) {\n        nextResetStatsTime += 60*60*1000;\n        errors = 0;\n        updates = 0;\n        clean_updates = 0;\n      }\n      unsigned long dcalc = millis();\n      if (currentIteration == 0) {\n        currentIteration = millis();\n      }\n      #endif\n\n      float tempC;\n      if (dht_sensor.measure(&tempC, &humidity)) {\n        #ifdef USERMOD_DHT_CELSIUS\n        temperature = tempC;\n        #else\n        temperature = tempC * 9 / 5 + 32;\n        #endif\n\n        #ifdef USERMOD_DHT_MQTT\n        // 10^n where n is number of decimal places to display in mqtt message. Please adjust buff size together with this constant\n        #define FLOAT_PREC 100\n        if (WLED_MQTT_CONNECTED) {\n          char buff[10];\n\n          strcpy(dhtMqttTopic + dhtMqttTopicLen, \"/temperature\");\n          sprintf(buff, \"%d.%d\", (int)temperature, ((int)(temperature * FLOAT_PREC)) % FLOAT_PREC);\n          mqtt->publish(dhtMqttTopic, 0, false, buff);\n\n          sprintf(buff, \"%d.%d\", (int)humidity, ((int)(humidity * FLOAT_PREC)) % FLOAT_PREC);\n          strcpy(dhtMqttTopic + dhtMqttTopicLen, \"/humidity\");\n          mqtt->publish(dhtMqttTopic, 0, false, buff);\n\n          dhtMqttTopic[dhtMqttTopicLen] = '\\0';\n        }\n        #undef FLOAT_PREC\n        #endif\n\n        nextReadTime = millis() + USERMOD_DHT_MEASUREMENT_INTERVAL;\n        lastReadTime = millis();\n        initializing = false;\n\n        #ifdef USERMOD_DHT_STATS\n        unsigned long icalc = millis() - currentIteration;\n        if (icalc > maxIteration) {\n          maxIteration = icalc;\n        }\n        if (icalc > DHT_TIMEOUT_TIME) {\n          errors += icalc/DHT_TIMEOUT_TIME;\n        } else {\n          clean_updates += 1;\n        }\n        updates += 1;\n        currentIteration = 0;\n\n        #endif\n      }\n\n      #ifdef USERMOD_DHT_STATS\n      dcalc = millis() - dcalc;\n      if (dcalc > maxDelay) {\n        maxDelay = dcalc;\n      }\n      #endif\n\n      if (((millis() - lastReadTime) > 10*USERMOD_DHT_MEASUREMENT_INTERVAL)) {\n        disabled = true;\n      }\n    }\n\n    void addToJsonInfo(JsonObject& root) {\n      if (disabled) {\n        return;\n      }\n      JsonObject user = root[\"u\"];\n      if (user.isNull()) user = root.createNestedObject(\"u\");\n\n      JsonArray temp = user.createNestedArray(\"Temperature\");\n      JsonArray hum = user.createNestedArray(\"Humidity\");\n\n      #ifdef USERMOD_DHT_STATS\n      JsonArray next = user.createNestedArray(\"next\");\n      if (nextReadTime >= millis()) {\n        next.add((nextReadTime - millis()) / 1000);\n        next.add(\" sec until read\");\n      } else {\n        next.add((millis() - nextReadTime) / 1000);\n        next.add(\" sec active reading\");\n      }\n\n      JsonArray last = user.createNestedArray(\"last\");\n      last.add((millis() - lastReadTime) / 60000);\n      last.add(\" min since read\");\n\n      JsonArray err = user.createNestedArray(\"errors\");\n      err.add(errors);\n      err.add(\" Errors\");\n\n      JsonArray upd = user.createNestedArray(\"updates\");\n      upd.add(updates);\n      upd.add(\" Updates\");\n\n      JsonArray cupd = user.createNestedArray(\"cleanUpdates\");\n      cupd.add(clean_updates);\n      cupd.add(\" Updates\");\n\n      JsonArray iter = user.createNestedArray(\"maxIter\");\n      iter.add(maxIteration);\n      iter.add(\" ms\");\n\n      JsonArray delay = user.createNestedArray(\"maxDelay\");\n      delay.add(maxDelay);\n      delay.add(\" ms\");\n      #endif\n\n      if (initializing) {\n        // if we haven't read the sensor yet, let the user know\n        // that we are still waiting for the first measurement\n        temp.add((nextReadTime - millis()) / 1000);\n        temp.add(\" sec until read\");\n        hum.add((nextReadTime - millis()) / 1000);\n        hum.add(\" sec until read\");\n        return;\n      }\n\n      hum.add(humidity);\n      hum.add(\"%\");\n\n      temp.add(temperature);\n      #ifdef USERMOD_DHT_CELSIUS\n      temp.add(\"°C\");\n      #else\n      temp.add(\"°F\");\n      #endif\n    }\n\n    uint16_t getId()\n    {\n      return USERMOD_ID_DHT;\n    }\n\n};\n\n\nstatic UsermodDHT dht;\nREGISTER_USERMOD(dht);"
  },
  {
    "path": "usermods/DHT/library.json",
    "content": "{\n  \"name\": \"DHT\",\n  \"build\": { \"libArchive\": false},\n  \"dependencies\": {\n    \"DHT_nonblocking\":\"https://github.com/alwynallan/DHT_nonblocking\"\n  }\n}\n"
  },
  {
    "path": "usermods/DHT/readme.md",
    "content": "# DHT Temperature/Humidity sensor usermod\n\nThis usermod will read from an attached DHT22 or DHT11 humidity and temperature sensor.\nThe sensor readings are displayed in the Info section of the web UI (and optionally sent to an MQTT broker).\n\nIf sensor is not detected after 10 update intervals, the usermod will be disabled.\n\nIf enabled, measured temperature and humidity will be published to the following MQTT topics\n* `{devceTopic}/dht/temperature`\n* `{devceTopic}/dht/humidity`\n\n## Installation\n\nCopy the example `platformio_override.ini` to the root directory.  This file should be placed in the same directory as `platformio.ini`.\n\n### Define Your Options\n\n* `USERMOD_DHT_DHTTYPE`              - DHT model: 11, 21, 22 for DHT11, DHT21, or DHT22, defaults to 22/DHT22\n* `USERMOD_DHT_PIN`                  - pin to which DTH is connected, defaults to Q2 pin on QuinLed Dig-Uno's board\n* `USERMOD_DHT_CELSIUS`              - define this to report temperatures in degrees Celsius, otherwise Fahrenheit will be reported\n* `USERMOD_DHT_MEASUREMENT_INTERVAL` - the number of milliseconds between measurements, defaults to 60000 ms\n* `USERMOD_DHT_FIRST_MEASUREMENT_AT` - the number of milliseconds after boot to take first measurement, defaults to 90000 ms\n* `USERMOD_DHT_MQTT`                 - publish measurements to an MQTT broker\n* `USERMOD_DHT_STATS`                - For debug, report delay stats\n\n## Project link\n\n* [QuinLED-Dig-Uno](https://quinled.info/2018/09/15/quinled-dig-uno/) - Project link\n\n### PlatformIO requirements\n\nIf you are using `platformio_override.ini`, you should be able to refresh the task list and see your custom task, for example `env:d1_mini_usermod_dht_C`. If not, you can add the libraries and dependencies into `platformio.ini` as you see fit.\n\n\n## Change Log\n2022-10-15\n* Add ability to publish sensor readings to an MQTT broker\n* fix compilation error for sample [env:d1_mini_usermod_dht_C] task\n2020-02-04\n* Change default QuinLed pin to Q2\n* Instead of trying to keep updates at constant cadence, space out readings by measurement interval. Hopefully, this helps eliminate occasional bursts of readings with errors\n* Add some more (optional) stats\n2020-02-03\n* Due to poor readouts on ESP32 with previous DHT library, rewrote to use https://github.com/alwynallan/DHT_nonblocking\n* The new library serializes/delays up to 5ms for the sensor readout\n2020-02-02\n* Created\n"
  },
  {
    "path": "usermods/EXAMPLE/library.json",
    "content": "{\n  \"name\": \"EXAMPLE\",\n  \"build\": { \"libArchive\": false },\n  \"dependencies\": {}\n}\n"
  },
  {
    "path": "usermods/EXAMPLE/readme.md",
    "content": "# Usermods API v2 example usermod\n\nIn this usermod file you can find the documentation on how to take advantage of the new version 2 usermods!\n\n## Installation \n\nAdd `EXAMPLE` to `custom_usermods` in your PlatformIO environment and compile!\n_(You shouldn't need to actually install this, it does nothing useful)_\n\n"
  },
  {
    "path": "usermods/EXAMPLE/usermod_v2_example.cpp",
    "content": "#include \"wled.h\"\n\n/*\n * Usermods allow you to add own functionality to WLED more easily\n * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality\n * \n * This is an example for a v2 usermod.\n * v2 usermods are class inheritance based and can (but don't have to) implement more functions, each of them is shown in this example.\n * Multiple v2 usermods can be added to one compilation easily.\n * \n * Creating a usermod:\n * This file serves as an example. If you want to create a usermod, it is recommended to use usermod_v2_empty.h from the usermods folder as a template.\n * Please remember to rename the class and file to a descriptive name.\n * You may also use multiple .h and .cpp files.\n * \n * Using a usermod:\n * 1. Copy the usermod into the sketch folder (same folder as wled00.ino)\n * 2. Register the usermod by adding #include \"usermod_filename.h\" in the top and registerUsermod(new MyUsermodClass()) in the bottom of usermods_list.cpp\n */\n\n//class name. Use something descriptive and leave the \": public Usermod\" part :)\nclass MyExampleUsermod : public Usermod {\n\n  private:\n\n    // Private class members. You can declare variables and functions only accessible to your usermod here\n    bool enabled = false;\n    bool initDone = false;\n    unsigned long lastTime = 0;\n\n    // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer)\n    bool testBool = false;\n    unsigned long testULong = 42424242;\n    float testFloat = 42.42;\n    String testString = \"Forty-Two\";\n\n    // These config variables have defaults set inside readFromConfig()\n    int testInt;\n    long testLong;\n    int8_t testPins[2];\n\n    // string that are used multiple time (this will save some flash memory)\n    static const char _name[];\n    static const char _enabled[];\n\n\n    // any private methods should go here (non-inline method should be defined out of class)\n    void publishMqtt(const char* state, bool retain = false); // example for publishing MQTT message\n\n\n  public:\n\n    // non WLED related methods, may be used for data exchange between usermods (non-inline methods should be defined out of class)\n\n    /**\n     * Enable/Disable the usermod\n     */\n    inline void enable(bool enable) { enabled = enable; }\n\n    /**\n     * Get usermod enabled/disabled state\n     */\n    inline bool isEnabled() { return enabled; }\n\n    // in such case add the following to another usermod:\n    //  in private vars:\n    //   #ifdef USERMOD_EXAMPLE\n    //   MyExampleUsermod* UM;\n    //   #endif\n    //  in setup()\n    //   #ifdef USERMOD_EXAMPLE\n    //   UM = (MyExampleUsermod*) UsermodManager::lookup(USERMOD_ID_EXAMPLE);\n    //   #endif\n    //  somewhere in loop() or other member method\n    //   #ifdef USERMOD_EXAMPLE\n    //   if (UM != nullptr) isExampleEnabled = UM->isEnabled();\n    //   if (!isExampleEnabled) UM->enable(true);\n    //   #endif\n\n\n    // methods called by WLED (can be inlined as they are called only once but if you call them explicitly define them out of class)\n\n    /*\n     * setup() is called once at boot. WiFi is not yet connected at this point.\n     * readFromConfig() is called prior to setup()\n     * You can use it to initialize variables, sensors or similar.\n     */\n    void setup() override {\n      // do your set-up here\n      //Serial.println(\"Hello from my usermod!\");\n      initDone = true;\n    }\n\n\n    /*\n     * connected() is called every time the WiFi is (re)connected\n     * Use it to initialize network interfaces\n     */\n    void connected() override {\n      //Serial.println(\"Connected to WiFi!\");\n    }\n\n\n    /*\n     * loop() is called continuously. Here you can check for events, read sensors, etc.\n     * \n     * Tips:\n     * 1. You can use \"if (WLED_CONNECTED)\" to check for a successful network connection.\n     *    Additionally, \"if (WLED_MQTT_CONNECTED)\" is available to check for a connection to an MQTT broker.\n     * \n     * 2. Try to avoid using the delay() function. NEVER use delays longer than 10 milliseconds.\n     *    Instead, use a timer check as shown here.\n     */\n    void loop() override {\n      // if usermod is disabled or called during strip updating just exit\n      // NOTE: on very long strips strip.isUpdating() may always return true so update accordingly\n      if (!enabled || strip.isUpdating()) return;\n\n      // do your magic here\n      if (millis() - lastTime > 1000) {\n        //Serial.println(\"I'm alive!\");\n        lastTime = millis();\n      }\n    }\n\n\n    /*\n     * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.\n     * Creating an \"u\" object allows you to add custom key/value pairs to the Info section of the WLED web UI.\n     * Below it is shown how this could be used for e.g. a light sensor\n     */\n    void addToJsonInfo(JsonObject& root) override\n    {\n      // if \"u\" object does not exist yet wee need to create it\n      JsonObject user = root[\"u\"];\n      if (user.isNull()) user = root.createNestedObject(\"u\");\n\n      //this code adds \"u\":{\"ExampleUsermod\":[20,\" lux\"]} to the info object\n      //int reading = 20;\n      //JsonArray lightArr = user.createNestedArray(FPSTR(_name))); //name\n      //lightArr.add(reading); //value\n      //lightArr.add(F(\" lux\")); //unit\n\n      // if you are implementing a sensor usermod, you may publish sensor data\n      //JsonObject sensor = root[F(\"sensor\")];\n      //if (sensor.isNull()) sensor = root.createNestedObject(F(\"sensor\"));\n      //temp = sensor.createNestedArray(F(\"light\"));\n      //temp.add(reading);\n      //temp.add(F(\"lux\"));\n    }\n\n\n    /*\n     * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).\n     * Values in the state object may be modified by connected clients\n     */\n    void addToJsonState(JsonObject& root) override\n    {\n      if (!initDone || !enabled) return;  // prevent crash on boot applyPreset()\n\n      JsonObject usermod = root[FPSTR(_name)];\n      if (usermod.isNull()) usermod = root.createNestedObject(FPSTR(_name));\n\n      //usermod[\"user0\"] = userVar0;\n    }\n\n\n    /*\n     * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).\n     * Values in the state object may be modified by connected clients\n     */\n    void readFromJsonState(JsonObject& root) override\n    {\n      if (!initDone) return;  // prevent crash on boot applyPreset()\n\n      JsonObject usermod = root[FPSTR(_name)];\n      if (!usermod.isNull()) {\n        // expect JSON usermod data in usermod name object: {\"ExampleUsermod:{\"user0\":10}\"}\n        userVar0 = usermod[\"user0\"] | userVar0; //if \"user0\" key exists in JSON, update, else keep old value\n      }\n      // you can as well check WLED state JSON keys\n      //if (root[\"bri\"] == 255) Serial.println(F(\"Don't burn down your garage!\"));\n    }\n\n\n    /*\n     * addToConfig() can be used to add custom persistent settings to the cfg.json file in the \"um\" (usermod) object.\n     * It will be called by WLED when settings are actually saved (for example, LED settings are saved)\n     * If you want to force saving the current state, use serializeConfig() in your loop().\n     * \n     * CAUTION: serializeConfig() will initiate a filesystem write operation.\n     * It might cause the LEDs to stutter and will cause flash wear if called too often.\n     * Use it sparingly and always in the loop, never in network callbacks!\n     * \n     * addToConfig() will make your settings editable through the Usermod Settings page automatically.\n     *\n     * Usermod Settings Overview:\n     * - Numeric values are treated as floats in the browser.\n     *   - If the numeric value entered into the browser contains a decimal point, it will be parsed as a C float\n     *     before being returned to the Usermod.  The float data type has only 6-7 decimal digits of precision, and\n     *     doubles are not supported, numbers will be rounded to the nearest float value when being parsed.\n     *     The range accepted by the input field is +/- 1.175494351e-38 to +/- 3.402823466e+38.\n     *   - If the numeric value entered into the browser doesn't contain a decimal point, it will be parsed as a\n     *     C int32_t (range: -2147483648 to 2147483647) before being returned to the usermod.\n     *     Overflows or underflows are truncated to the max/min value for an int32_t, and again truncated to the type\n     *     used in the Usermod when reading the value from ArduinoJson.\n     * - Pin values can be treated differently from an integer value by using the key name \"pin\"\n     *   - \"pin\" can contain a single or array of integer values\n     *   - On the Usermod Settings page there is simple checking for pin conflicts and warnings for special pins\n     *     - Red color indicates a conflict.  Yellow color indicates a pin with a warning (e.g. an input-only pin)\n     *   - Tip: use int8_t to store the pin value in the Usermod, so a -1 value (pin not set) can be used\n     *\n     * See usermod_v2_auto_save.h for an example that saves Flash space by reusing ArduinoJson key name strings\n     * \n     * If you need a dedicated settings page with custom layout for your Usermod, that takes a lot more work.  \n     * You will have to add the setting to the HTML, xml.cpp and set.cpp manually.\n     * See the WLED Soundreactive fork (code and wiki) for reference.  https://github.com/atuline/WLED\n     * \n     * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings!\n     */\n    void addToConfig(JsonObject& root) override\n    {\n      JsonObject top = root.createNestedObject(FPSTR(_name));\n      top[FPSTR(_enabled)] = enabled;\n      //save these vars persistently whenever settings are saved\n      top[\"great\"] = userVar0;\n      top[\"testBool\"] = testBool;\n      top[\"testInt\"] = testInt;\n      top[\"testLong\"] = testLong;\n      top[\"testULong\"] = testULong;\n      top[\"testFloat\"] = testFloat;\n      top[\"testString\"] = testString;\n      JsonArray pinArray = top.createNestedArray(\"pin\");\n      pinArray.add(testPins[0]);\n      pinArray.add(testPins[1]); \n    }\n\n\n    /*\n     * readFromConfig() can be used to read back the custom settings you added with addToConfig().\n     * This is called by WLED when settings are loaded (currently this only happens immediately after boot, or after saving on the Usermod Settings page)\n     * \n     * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes),\n     * 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.\n     * If you don't know what that is, don't fret. It most likely doesn't affect your use case :)\n     * \n     * Return true in case the config values returned from Usermod Settings were complete, or false if you'd like WLED to save your defaults to disk (so any missing values are editable in Usermod Settings)\n     * \n     * getJsonValue() returns false if the value is missing, or copies the value into the variable provided and returns true if the value is present\n     * The configComplete variable is true only if the \"exampleUsermod\" object and all values are present.  If any values are missing, WLED will know to call addToConfig() to save them\n     * \n     * This function is guaranteed to be called on boot, but could also be called every time settings are updated\n     */\n    bool readFromConfig(JsonObject& root) override\n    {\n      // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor\n      // 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)\n\n      JsonObject top = root[FPSTR(_name)];\n\n      bool configComplete = !top.isNull();\n\n      configComplete &= getJsonValue(top[\"great\"], userVar0);\n      configComplete &= getJsonValue(top[\"testBool\"], testBool);\n      configComplete &= getJsonValue(top[\"testULong\"], testULong);\n      configComplete &= getJsonValue(top[\"testFloat\"], testFloat);\n      configComplete &= getJsonValue(top[\"testString\"], testString);\n\n      // A 3-argument getJsonValue() assigns the 3rd argument as a default value if the Json value is missing\n      configComplete &= getJsonValue(top[\"testInt\"], testInt, 42);  \n      configComplete &= getJsonValue(top[\"testLong\"], testLong, -42424242);\n\n      // \"pin\" fields have special handling in settings page (or some_pin as well)\n      configComplete &= getJsonValue(top[\"pin\"][0], testPins[0], -1);\n      configComplete &= getJsonValue(top[\"pin\"][1], testPins[1], -1);\n\n      return configComplete;\n    }\n\n\n    /*\n     * appendConfigData() is called when user enters usermod settings page\n     * it may add additional metadata for certain entry fields (adding drop down is possible)\n     * be careful not to add too much as oappend() buffer is limited to 3k\n     */\n    void appendConfigData() override\n    {\n      oappend(F(\"addInfo('\")); oappend(String(FPSTR(_name)).c_str()); oappend(F(\":great\")); oappend(F(\"',1,'<i>(this is a great config value)</i>');\"));\n      oappend(F(\"addInfo('\")); oappend(String(FPSTR(_name)).c_str()); oappend(F(\":testString\")); oappend(F(\"',1,'enter any string you want');\"));\n      oappend(F(\"dd=addDropdown('\")); oappend(String(FPSTR(_name)).c_str()); oappend(F(\"','testInt');\"));\n      oappend(F(\"addOption(dd,'Nothing',0);\"));\n      oappend(F(\"addOption(dd,'Everything',42);\"));\n    }\n\n\n    /*\n     * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors.\n     * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode.\n     * Commonly used for custom clocks (Cronixie, 7 segment)\n     */\n    void handleOverlayDraw() override\n    {\n      //strip.setPixelColor(0, RGBW32(0,0,0,0)) // set the first pixel to black\n    }\n\n\n    /**\n     * handleButton() can be used to override default button behaviour. Returning true\n     * will prevent button working in a default way.\n     * Replicating button.cpp\n     */\n    bool handleButton(uint8_t b) override {\n      yield();\n      // ignore certain button types as they may have other consequences\n      if (!enabled\n       || buttons[b].type == BTN_TYPE_NONE\n       || buttons[b].type == BTN_TYPE_RESERVED\n       || buttons[b].type == BTN_TYPE_PIR_SENSOR\n       || buttons[b].type == BTN_TYPE_ANALOG\n       || buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {\n        return false;\n      }\n\n      bool handled = false;\n      // do your button handling here\n      return handled;\n    }\n  \n\n#ifndef WLED_DISABLE_MQTT\n    /**\n     * handling of MQTT message\n     * topic only contains stripped topic (part after /wled/MAC)\n     */\n    bool onMqttMessage(char* topic, char* payload) override {\n      // check if we received a command\n      //if (strlen(topic) == 8 && strncmp_P(topic, PSTR(\"/command\"), 8) == 0) {\n      //  String action = payload;\n      //  if (action == \"on\") {\n      //    enabled = true;\n      //    return true;\n      //  } else if (action == \"off\") {\n      //    enabled = false;\n      //    return true;\n      //  } else if (action == \"toggle\") {\n      //    enabled = !enabled;\n      //    return true;\n      //  }\n      //}\n      return false;\n    }\n\n    /**\n     * onMqttConnect() is called when MQTT connection is established\n     */\n    void onMqttConnect(bool sessionPresent) override {\n      // do any MQTT related initialisation here\n      //publishMqtt(\"I am alive!\");\n    }\n#endif\n\n\n    /**\n     * onStateChanged() is used to detect WLED state change\n     * @mode parameter is CALL_MODE_... parameter used for notifications\n     */\n    void onStateChange(uint8_t mode) override {\n      // do something if WLED state changed (color, brightness, effect, preset, etc)\n    }\n\n\n    /*\n     * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).\n     * This could be used in the future for the system to determine whether your usermod is installed.\n     */\n    uint16_t getId() override\n    {\n      return USERMOD_ID_EXAMPLE;\n    }\n\n   //More methods can be added in the future, this example will then be extended.\n   //Your usermod will remain compatible as it does not need to implement all methods from the Usermod base class!\n};\n\n\n// add more strings here to reduce flash memory usage\nconst char MyExampleUsermod::_name[]    PROGMEM = \"ExampleUsermod\";\nconst char MyExampleUsermod::_enabled[] PROGMEM = \"enabled\";\n\n\n// implementation of non-inline member methods\n\nvoid MyExampleUsermod::publishMqtt(const char* state, bool retain)\n{\n#ifndef WLED_DISABLE_MQTT\n  //Check if MQTT Connected, otherwise it will crash the 8266\n  if (WLED_MQTT_CONNECTED) {\n    char subuf[64];\n    strcpy(subuf, mqttDeviceTopic);\n    strcat_P(subuf, PSTR(\"/example\"));\n    mqtt->publish(subuf, 0, retain, state);\n  }\n#endif\n}\n\nstatic MyExampleUsermod example_usermod;\nREGISTER_USERMOD(example_usermod);\n"
  },
  {
    "path": "usermods/EleksTube_IPS/ChipSelect.h",
    "content": "#ifndef CHIP_SELECT_H\n#define CHIP_SELECT_H\n\n#include \"Hardware.h\"\n\n/*\n * `digit`s are as defined in Hardware.h, 0 == seconds ones, 5 == hours tens.\n */\n\nclass ChipSelect {\nprivate:\n  uint8_t digits_map;\n  const uint8_t all_on = 0x3F;\n  const uint8_t all_off = 0x00;\npublic:\n  ChipSelect() : digits_map(all_off) {}\n  \n  void update() {\n    // Documented in README.md.  Q7 and Q6 are unused. Q5 is Seconds Ones, Q0 is Hours Tens.\n    // Q7 is the first bit written, Q0 is the last.  So we push two dummy bits, then start with\n    // Seconds Ones and end with Hours Tens.\n    // CS is Active Low, but digits_map is 1 for enable, 0 for disable.  So we bit-wise NOT first.\n\n    uint8_t to_shift = (~digits_map) << 2;\n\n    digitalWrite(CSSR_LATCH_PIN, LOW);\n    shiftOut(CSSR_DATA_PIN, CSSR_CLOCK_PIN, LSBFIRST, to_shift);\n    digitalWrite(CSSR_LATCH_PIN, HIGH);\n  }\n\n    void begin() \n  {\n    pinMode(CSSR_LATCH_PIN, OUTPUT);\n    pinMode(CSSR_DATA_PIN, OUTPUT);\n    pinMode(CSSR_CLOCK_PIN, OUTPUT);\n\n    digitalWrite(CSSR_DATA_PIN, LOW);\n    digitalWrite(CSSR_CLOCK_PIN, LOW);\n    digitalWrite(CSSR_LATCH_PIN, LOW);\n    update();\n  }\n\n  // These speak the indexes defined in Hardware.h.\n  // So 0 is disabled, 1 is enabled (even though CS is active low, this gets mapped.)\n  // So bit 0 (LSB), is index 0, is SECONDS_ONES\n  // Translation to what the 74HC595 uses is done in update()\n  void setDigitMap(uint8_t map, bool update_=true)   { digits_map = map; if (update_) update(); }\n  uint8_t getDigitMap()                        { return digits_map; }\n\n  // Helper functions\n  // Sets just the one digit by digit number\n  void setDigit(uint8_t digit, bool update_=true) { setDigitMap(0x01 << digit, update_); }\n  void setAll(bool update_=true)                  { setDigitMap(all_on,  update_); }\n  void clear(bool update_=true)                   { setDigitMap(all_off, update_); }\n  void setSecondsOnes()                           { setDigit(SECONDS_ONES); }\n  void setSecondsTens()                           { setDigit(SECONDS_TENS); }\n  void setMinutesOnes()                           { setDigit(MINUTES_ONES); }\n  void setMinutesTens()                           { setDigit(MINUTES_TENS); }\n  void setHoursOnes()                             { setDigit(HOURS_ONES); }\n  void setHoursTens()                             { setDigit(HOURS_TENS); }\n  bool isSecondsOnes()                            { return ((digits_map & SECONDS_ONES_MAP) > 0); }\n  bool isSecondsTens()                            { return ((digits_map & SECONDS_TENS_MAP) > 0); }\n  bool isMinutesOnes()                            { return ((digits_map & MINUTES_ONES_MAP) > 0); }\n  bool isMinutesTens()                            { return ((digits_map & MINUTES_TENS_MAP) > 0); }\n  bool isHoursOnes()                              { return ((digits_map & HOURS_ONES_MAP) > 0); }\n  bool isHoursTens()                              { return ((digits_map & HOURS_TENS_MAP) > 0); }\n};\n\n\n#endif // CHIP_SELECT_H\n"
  },
  {
    "path": "usermods/EleksTube_IPS/EleksTube_IPS.cpp",
    "content": "#include \"TFTs.h\"\n#include \"wled.h\"\n\n//Large parts of the code are from https://github.com/SmittyHalibut/EleksTubeHAX\n\nclass ElekstubeIPSUsermod : public Usermod {\n  private:\n    // strings to reduce flash memory usage (used more than twice)\n    static const char _name[];\n    static const char _tubeSeg[];\n    static const char _digitOffset[];\n\n    char cronixieDisplay[7] = \"HHMMSS\";\n\n    TFTs tfts;\n    void updateClockDisplay(TFTs::show_t show=TFTs::yes) {\n      bool set[6] = {false}; \n      for (uint8_t i = 0; i<6; i++) {\n        char c = cronixieDisplay[i];\n        if (c >= '0' && c <= '9') {\n          tfts.setDigit(5-i, c - '0', show); set[i] = true;\n        } else if (c >= 'A' && c <= 'G') {\n          tfts.setDigit(5-i, c - 'A' + 10, show); set[i] = true; //10.bmp to 16.bmp static display\n        } else if (c == '-' || c == '_' || c == ' ') {\n          tfts.setDigit(5-i, 255, show); set[i] = true; //blank\n        } else {\n          set[i] = false; //display HHMMSS time\n        }\n      }\n\n      \n      uint8_t hr = hour(localTime);\n      uint8_t hrTens = hr/10;\n      uint8_t mi = minute(localTime);\n      uint8_t mittens = mi/10;\n      uint8_t s = second(localTime);\n      uint8_t sTens = s/10;\n      if (!set[0]) tfts.setDigit(HOURS_TENS, hrTens, show);\n      if (!set[1]) tfts.setDigit(HOURS_ONES, hr - hrTens*10, show);\n      if (!set[2]) tfts.setDigit(MINUTES_TENS, mittens, show);\n      if (!set[3]) tfts.setDigit(MINUTES_ONES, mi - mittens*10, show);\n      if (!set[4]) tfts.setDigit(SECONDS_TENS, sTens, show);\n      if (!set[5]) tfts.setDigit(SECONDS_ONES, s - sTens*10, show);\n    }\n    unsigned long lastTime = 0;\n  public:\n\n    uint8_t lastBri;\n    uint32_t lastCols[6];\n    TFTs::show_t fshow=TFTs::yes;\n\n    void setup() {\n      tfts.begin();\n      tfts.fillScreen(TFT_BLACK);\n\n      for (int8_t i = 5; i >= 0; i--) {\n        tfts.setDigit(i, 255, TFTs::force); //turn all off\n      }\n    }\n\n    void loop() {\n      if (!toki.isTick()) return;\n      updateLocalTime();\n\n      Segment& seg1 = strip.getSegment(tfts.tubeSegment);\n      if (seg1.isActive()) {\n        bool update = false;\n        if (seg1.opacity != lastBri) update = true;\n        lastBri = seg1.opacity;\n        for (uint8_t i = 0; i < 6; i++) {\n          uint32_t c = strip.getPixelColor(seg1.start + i);\n          if (c != lastCols[i]) update = true;\n          lastCols[i] = c;\n        }\n        if (update) fshow=TFTs::force;\n      } else if (lastCols[0] != 0) { // Segment 1 deleted\n        fshow=TFTs::force;\n        lastCols[0] = 0;\n      }\n      \n      updateClockDisplay(fshow);\n      fshow=TFTs::yes;\n    }\n\n    /**\n     * addToConfig() (called from set.cpp) stores persistent properties to cfg.json\n     */\n    void addToConfig(JsonObject &root) {\n      // we add JSON object: {\"EleksTubeIPS\": {\"tubeSegment\": 1, \"digitOffset\": 0}}\n      JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname\n      top[FPSTR(_tubeSeg)] = tfts.tubeSegment;\n      top[FPSTR(_digitOffset)] = tfts.digitOffset;\n      DEBUG_PRINTLN(F(\"EleksTube config saved.\"));\n    }\n\n    /**\n     * readFromConfig() is called before setup() to populate properties from values stored in cfg.json\n     *\n     * The function should return true if configuration was successfully loaded or false if there was no configuration.\n     */\n    bool readFromConfig(JsonObject &root) {\n      // we look for JSON object: {\"EleksTubeIPS\": {\"tubeSegment\": 1, \"digitOffset\": 0}}\n      DEBUG_PRINT(FPSTR(_name));\n\n      JsonObject top = root[FPSTR(_name)];\n      if (top.isNull()) {\n        DEBUG_PRINTLN(F(\": No config found. (Using defaults.)\"));\n        return false;\n      }\n\n      tfts.tubeSegment = top[FPSTR(_tubeSeg)] | tfts.tubeSegment;\n      uint8_t digitOffsetPrev = tfts.digitOffset;\n      tfts.digitOffset = top[FPSTR(_digitOffset)] | tfts.digitOffset;\n      if (tfts.digitOffset > 240) tfts.digitOffset = 240;\n      if (tfts.digitOffset != digitOffsetPrev) fshow=TFTs::force;\n\n      // use \"return !top[\"newestParameter\"].isNull();\" when updating Usermod with new features\n      return !top[FPSTR(_digitOffset)].isNull();\n    }\n\n    /*\n     * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).\n     * Values in the state object may be modified by connected clients\n     */\n    void addToJsonState(JsonObject& root)\n    {\n      root[\"nx\"] = cronixieDisplay;\n      root[FPSTR(_digitOffset)] = tfts.digitOffset;\n    }\n\n\n    /*\n     * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).\n     * Values in the state object may be modified by connected clients\n     */\n    void readFromJsonState(JsonObject& root)\n    {\n      if (root[\"nx\"].is<const char*>()) {\n        strncpy(cronixieDisplay, root[\"nx\"], 6);\n      }\n\n      uint8_t digitOffsetPrev = tfts.digitOffset;\n      tfts.digitOffset = root[FPSTR(_digitOffset)] | tfts.digitOffset;\n      if (tfts.digitOffset > 240) tfts.digitOffset = 240;\n      if (tfts.digitOffset != digitOffsetPrev) fshow=TFTs::force;\n    }\n\n    uint16_t getId()\n    {\n      return USERMOD_ID_ELEKSTUBE_IPS;\n    }\n};\n\n// strings to reduce flash memory usage (used more than twice)\nconst char ElekstubeIPSUsermod::_name[]         PROGMEM = \"EleksTubeIPS\";\nconst char ElekstubeIPSUsermod::_tubeSeg[]      PROGMEM = \"tubeSegment\";\nconst char ElekstubeIPSUsermod::_digitOffset[]  PROGMEM = \"digitOffset\";\n\n\nstatic ElekstubeIPSUsermod elekstube_ips;\nREGISTER_USERMOD(elekstube_ips);"
  },
  {
    "path": "usermods/EleksTube_IPS/Hardware.h",
    "content": "/*\n * Define the hardware for the EleksTube IPS clock.  Mostly pin definitions\n */\n#ifndef ELEKSTUBEHAX_HARDWARE_H\n#define ELEKSTUBEHAX_HARDWARE_H\n\n#include <stdint.h> \n#include <Arduino.h> // for HIGH and LOW\n\n// Common indexing scheme, used to identify the digit\n#define SECONDS_ONES (0)\n#define SECONDS_TENS (1)\n#define MINUTES_ONES (2)\n#define MINUTES_TENS (3)\n#define HOURS_ONES   (4)\n#define HOURS_TENS   (5)\n#define NUM_DIGITS   (6)\n\n#define SECONDS_ONES_MAP (0x01 << SECONDS_ONES)\n#define SECONDS_TENS_MAP (0x01 << SECONDS_TENS)\n#define MINUTES_ONES_MAP (0x01 << MINUTES_ONES)\n#define MINUTES_TENS_MAP (0x01 << MINUTES_TENS)\n#define HOURS_ONES_MAP   (0x01 << HOURS_ONES)\n#define HOURS_TENS_MAP   (0x01 << HOURS_TENS)\n\n// WS2812 (or compatible) LEDs on the back of the display modules.\n#define BACKLIGHTS_PIN (12)\n\n// Buttons, active low, externally pulled up (with actual resistors!)\n#define BUTTON_LEFT_PIN (33)\n#define BUTTON_MODE_PIN (32)\n#define BUTTON_RIGHT_PIN (35)\n#define BUTTON_POWER_PIN (34)\n\n// I2C to DS3231 RTC.\n#define RTC_SCL_PIN (22)\n#define RTC_SDA_PIN (21)\n\n// Chip Select shift register, to select the display\n#define CSSR_DATA_PIN (14)\n#define CSSR_CLOCK_PIN (16)\n#define CSSR_LATCH_PIN (17)\n\n// SPI to displays\n// DEFINED IN User_Setup.h\n// Look for: TFT_MOSI, TFT_SCLK, TFT_CS, TFT_DC, and TFT_RST\n\n// Power for all TFT displays are grounded through a MOSFET so they can all be turned off.\n// Active HIGH.\n#define TFT_ENABLE_PIN (27)\n\n#endif // ELEKSTUBEHAX_HARDWARE_H\n"
  },
  {
    "path": "usermods/EleksTube_IPS/TFTs.h",
    "content": "#ifndef TFTS_H\n#define TFTS_H\n\n#include \"wled.h\"\n#include <FS.h>\n\n#include <TFT_eSPI.h>\n#include \"Hardware.h\"\n#include \"ChipSelect.h\"\n\nclass TFTs : public TFT_eSPI {\nprivate:\n  uint8_t digits[NUM_DIGITS];\n\n\n  // These read 16- and 32-bit types from the SD card file.\n  // BMP data is stored little-endian, Arduino is little-endian too.\n  // May need to reverse subscript order if porting elsewhere.\n\n  uint16_t read16(fs::File &f) {\n    uint16_t result;\n    ((uint8_t *)&result)[0] = f.read(); // LSB\n    ((uint8_t *)&result)[1] = f.read(); // MSB\n    return result;\n  }\n\n  uint32_t read32(fs::File &f) {\n    uint32_t result;\n    ((uint8_t *)&result)[0] = f.read(); // LSB\n    ((uint8_t *)&result)[1] = f.read();\n    ((uint8_t *)&result)[2] = f.read();\n    ((uint8_t *)&result)[3] = f.read(); // MSB\n    return result;\n  }\n\n  uint16_t output_buffer[TFT_HEIGHT][TFT_WIDTH];\n  int16_t w = 135, h = 240, x = 0, y = 0, bufferedDigit = 255;\n  uint16_t digitR, digitG, digitB, dimming = 255;\n  uint32_t digitColor = 0;\n\n  void drawBuffer() {\n    bool oldSwapBytes = getSwapBytes();\n    setSwapBytes(true);\n    pushImage(x, y, w, h, (uint16_t *)output_buffer);\n    setSwapBytes(oldSwapBytes);\n  }\n\n  // These BMP functions are stolen directly from the TFT_SPIFFS_BMP example in the TFT_eSPI library.\n  // Unfortunately, they aren't part of the library itself, so I had to copy them.\n  // I've modified drawBmp to buffer the whole image at once instead of doing it line-by-line.\n\n  //// BEGIN STOLEN CODE\n\n  // Draw directly from file stored in RGB565 format. Fastest\n  bool drawBin(const char *filename) {\n    fs::File bmpFS;\n\n    // Open requested file on SD card\n    bmpFS = WLED_FS.open(filename, \"r\");\n\n    size_t sz = bmpFS.size();\n    if (sz > 64800) {\n      bmpFS.close();\n      return false;\n    }\n\n    uint16_t r, g, b, dimming = 255;\n    int16_t row, col;\n\n    //draw img that is shorter than 240pix into the center\n    w = 135;\n    h = sz / (w * 2);\n    x = 0;\n    y = (height() - h) /2;\n    \n    uint8_t lineBuffer[w * 2];\n\n    if (!realtimeMode || realtimeOverride || (realtimeMode && useMainSegmentOnly)) strip.service();\n\n    // 0,0 coordinates are top left\n    for (row = 0; row < h; row++) {\n\n      bmpFS.read(lineBuffer, sizeof(lineBuffer));\n      uint8_t PixM, PixL;\n      \n      // Colors are already in 16-bit R5, G6, B5 format\n      for (col = 0; col < w; col++)\n      {\n        if (dimming == 255 && !digitColor) { // not needed, copy directly\n          output_buffer[row][col] = (lineBuffer[col*2+1] << 8) | (lineBuffer[col*2]);\n        } else {\n          // 16 BPP pixel format: R5, G6, B5 ; bin: RRRR RGGG GGGB BBBB\n          PixM = lineBuffer[col*2+1];\n          PixL = lineBuffer[col*2];\n          // align to 8-bit value (MSB left aligned)\n          r = (PixM) & 0xF8;\n          g = ((PixM << 5) | (PixL >> 3)) & 0xFC;\n          b = (PixL << 3) & 0xF8;\n          r *= dimming; g *= dimming; b *= dimming;\n          r  = r  >> 8; g  = g  >> 8; b  = b  >> 8;\n          if (digitColor) { // grayscale pixel coloring\n            uint8_t l = (r > g) ? ((r > b) ? r:b) : ((g > b) ? g:b);\n            r = g = b = l;\n            r *= digitR; g *= digitG; b *= digitB;\n            r  = r >> 8; g  = g >> 8; b  = b >> 8;\n          }\n          output_buffer[row][col] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);\n        }\n      }\n    }\n\n    drawBuffer();\n\n    bmpFS.close();\n\n    return true;\n  }\n\n  bool drawBmp(const char *filename) {\n    fs::File bmpFS;\n\n    // Open requested file on SD card\n    bmpFS = WLED_FS.open(filename, \"r\");\n\n    uint32_t seekOffset, headerSize, paletteSize = 0;\n    int16_t row;\n    uint16_t r, g, b, dimming = 255, bitDepth;\n\n    uint16_t magic = read16(bmpFS);\n    if (magic != ('B' | ('M' << 8))) { // File not found or not a BMP\n      Serial.println(F(\"BMP not found!\"));\n      bmpFS.close();\n      return false;\n    }\n\n    (void) read32(bmpFS); // filesize in bytes\n    (void) read32(bmpFS); // reserved\n    seekOffset = read32(bmpFS); // start of bitmap\n    headerSize = read32(bmpFS); // header size\n    w = read32(bmpFS); // width\n    h = read32(bmpFS); // height\n    (void) read16(bmpFS); // color planes (must be 1)\n    bitDepth = read16(bmpFS);\n\n    if (read32(bmpFS) != 0 || (bitDepth != 24 && bitDepth != 1 && bitDepth != 4 && bitDepth != 8)) {\n      Serial.println(F(\"BMP format not recognized.\"));\n      bmpFS.close();\n      return false;\n    }\n\n    uint32_t palette[256];\n    if (bitDepth <= 8) // 1,4,8 bit bitmap: read color palette\n    {\n      (void) read32(bmpFS); (void) read32(bmpFS); (void) read32(bmpFS); // size, w resolution, h resolution\n      paletteSize = read32(bmpFS);\n      if (paletteSize == 0) paletteSize = 1 << bitDepth; //if 0, size is 2^bitDepth\n      bmpFS.seek(14 + headerSize); // start of color palette\n      for (uint16_t i = 0; i < paletteSize; i++) {\n        palette[i] = read32(bmpFS);\n      }\n    }\n\n    // draw img that is shorter than 240pix into the center\n    x = (width() - w) /2;\n    y = (height() - h) /2;\n\n    bmpFS.seek(seekOffset);\n\n    uint32_t lineSize = ((bitDepth * w +31) >> 5) * 4;\n    uint8_t lineBuffer[lineSize];\n    \n    uint8_t serviceStrip = (!realtimeMode || realtimeOverride || (realtimeMode && useMainSegmentOnly)) ? 7 : 0;\n    // row is decremented as the BMP image is drawn bottom up\n    for (row = h-1; row >= 0; row--) {\n      if ((row & 0b00000111) == serviceStrip) strip.service(); //still refresh backlight to mitigate stutter every few rows\n      bmpFS.read(lineBuffer, sizeof(lineBuffer));\n      uint8_t*  bptr = lineBuffer;\n      \n      // Convert 24 to 16 bit colors while copying to output buffer.\n      for (uint16_t col = 0; col < w; col++)\n      {\n        if (bitDepth == 24) {\n          b = *bptr++;\n          g = *bptr++;\n          r = *bptr++;\n        } else {\n          uint32_t c = 0;\n          if (bitDepth == 8) {\n            c = palette[*bptr++];\n          }\n          else if (bitDepth == 4) {\n            c = palette[(*bptr >> ((col & 0x01)?0:4)) & 0x0F];\n            if (col & 0x01) bptr++;\n          }\n          else { // bitDepth == 1\n            c = palette[(*bptr >> (7 - (col & 0x07))) & 0x01];\n            if ((col & 0x07) == 0x07) bptr++;\n          }\n          b = c; g = c >> 8; r = c >> 16;\n        }\n        if (dimming != 255) { // only dim when needed\n          r *= dimming; g *= dimming; b *= dimming;\n          r  = r  >> 8; g  = g  >> 8; b  = b  >> 8;\n        }\n        if (digitColor) { // grayscale pixel coloring\n          uint8_t l = (r > g) ? ((r > b) ? r:b) : ((g > b) ? g:b);\n          r = g = b = l;\n\n          r *= digitR; g *= digitG; b *= digitB;\n          r  = r >> 8; g  = g >> 8; b  = b >> 8;\n        }\n        output_buffer[row][col] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xFF) >> 3);\n      }\n    }\n    \n    drawBuffer();\n\n    bmpFS.close();\n    return true;\n  }\n\n  bool drawClk(const char *filename) {\n    fs::File bmpFS;\n\n    // Open requested file on SD card\n    bmpFS = WLED_FS.open(filename, \"r\");\n\n    if (!bmpFS)\n    {\n      Serial.print(\"File not found: \");\n      Serial.println(filename);\n      return false;\n    }\n\n    uint16_t r, g, b, dimming = 255, magic;\n    int16_t row, col;\n    \n    magic = read16(bmpFS);\n    if (magic != 0x4B43) { // look for \"CK\" header\n      Serial.print(F(\"File not a CLK. Magic: \"));\n      Serial.println(magic);\n      bmpFS.close();\n      return false;\n    }\n\n    w = read16(bmpFS);\n    h = read16(bmpFS);\n    x = (width() - w) / 2;\n    y = (height() - h) / 2;\n    \n    uint8_t lineBuffer[w * 2];\n    \n    if (!realtimeMode || realtimeOverride || (realtimeMode && useMainSegmentOnly)) strip.service();\n\n    // 0,0 coordinates are top left\n    for (row = 0; row < h; row++) {\n\n      bmpFS.read(lineBuffer, sizeof(lineBuffer));\n      uint8_t PixM, PixL;\n      \n      // Colors are already in 16-bit R5, G6, B5 format\n      for (col = 0; col < w; col++)\n      {\n        if (dimming == 255 && !digitColor) { // not needed, copy directly\n          output_buffer[row][col+x] = (lineBuffer[col*2+1] << 8) | (lineBuffer[col*2]);\n        } else {\n          // 16 BPP pixel format: R5, G6, B5 ; bin: RRRR RGGG GGGB BBBB\n          PixM = lineBuffer[col*2+1];\n          PixL = lineBuffer[col*2];\n          // align to 8-bit value (MSB left aligned)\n          r = (PixM) & 0xF8;\n          g = ((PixM << 5) | (PixL >> 3)) & 0xFC;\n          b = (PixL << 3) & 0xF8;\n          r *= dimming; g *= dimming; b *= dimming;\n          r  = r  >> 8; g  = g  >> 8; b  = b  >> 8;\n          if (digitColor) { // grayscale pixel coloring\n            uint8_t l = (r > g) ? ((r > b) ? r:b) : ((g > b) ? g:b);\n            r = g = b = l;\n            r *= digitR; g *= digitG; b *= digitB;\n            r  = r >> 8; g  = g >> 8; b  = b >> 8;\n          }\n          output_buffer[row][col+x] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);\n        }\n      }\n    }\n\n    drawBuffer();\n\n    bmpFS.close();\n    return true;\n  }\n\n\npublic:\n  TFTs() : TFT_eSPI(), chip_select()\n    { for (uint8_t digit=0; digit < NUM_DIGITS; digit++) digits[digit] = 0; }\n\n  // no == Do not send to TFT. yes == Send to TFT if changed. force == Send to TFT.\n  enum show_t { no, yes, force };\n  // A digit of 0xFF means blank the screen.\n  const static uint8_t blanked = 255;\n\n  uint8_t tubeSegment = 1;\n  uint8_t digitOffset = 0;\n  \n  void begin() {\n    pinMode(TFT_ENABLE_PIN, OUTPUT);\n    digitalWrite(TFT_ENABLE_PIN, HIGH); //enable displays on boot\n\n    // Start with all displays selected.\n    chip_select.begin();\n    chip_select.setAll();\n\n    // Initialize the super class.\n    init();\n  }\n\n  void showDigit(uint8_t digit) {\n    chip_select.setDigit(digit);\n    uint8_t digitToDraw = digits[digit];\n    if (digitToDraw < 10) digitToDraw += digitOffset;\n\n    if (digitToDraw == blanked) {\n      fillScreen(TFT_BLACK); return;\n    }\n\n    // if last digit was the same, skip loading from FS to buffer\n    if (!digitColor && digitToDraw == bufferedDigit) drawBuffer();\n    digitR = R(digitColor); digitG = G(digitColor); digitB = B(digitColor);\n\n    // Filenames are no bigger than \"254.bmp\\0\"\n    char file_name[10];\n    // Fastest, raw RGB565\n    sprintf(file_name, \"/%d.bin\", digitToDraw);\n    if (WLED_FS.exists(file_name)) {\n      if (drawBin(file_name)) bufferedDigit = digitToDraw;\n      return;\n    }\n    // Fast, raw RGB565, see https://github.com/aly-fly/EleksTubeHAX on how to create this clk format\n    sprintf(file_name, \"/%d.clk\", digitToDraw);\n    if (WLED_FS.exists(file_name)) {\n      if (drawClk(file_name)) bufferedDigit = digitToDraw;\n      return;\n    }\n    // Slow, regular RGB888 or 1,4,8 bit palette BMP\n    sprintf(file_name, \"/%d.bmp\", digitToDraw);\n    if (drawBmp(file_name)) bufferedDigit = digitToDraw;\n    return;\n  } \n\n  void setDigit(uint8_t digit, uint8_t value, show_t show=yes) {\n    uint8_t old_value = digits[digit];\n    digits[digit] = value;\n\n    // Color in grayscale bitmaps if Segment 1 exists\n    // TODO If secondary and tertiary are black, color all in primary,\n    // else color first three from Seg 1 color slots and last three from Seg 2 color slots\n    Segment& seg1 = strip.getSegment(tubeSegment);\n    if (seg1.isActive()) {\n      digitColor = strip.getPixelColor(seg1.start + digit);\n      dimming = seg1.opacity;\n    } else {\n      digitColor = 0;\n      dimming = 255;\n    }\n\n    if (show != no && (old_value != value || show == force)) {\n      showDigit(digit);\n    }\n  }\n  uint8_t getDigit(uint8_t digit) {return digits[digit];}\n\n  void showAllDigits()            {for (uint8_t digit=0; digit < NUM_DIGITS; digit++) showDigit(digit);}\n\n  // Making chip_select public so we don't have to proxy all methods, and the caller can just use it directly.\n  ChipSelect chip_select;\n};\n\n#endif // TFTS_H\n"
  },
  {
    "path": "usermods/EleksTube_IPS/User_Setup.h",
    "content": "/*\n * This is intended to over-ride `User_Setup.h` that comes with the TFT_eSPI library.\n * I hate having to modify the library code.\n */\n\n// ST7789 135 x 240 display with no chip select line\n\n#define ST7789_DRIVER     // Configure all registers\n\n#define TFT_WIDTH  135\n#define TFT_HEIGHT 240\n\n#define CGRAM_OFFSET      // Library will add offsets required\n\n//#define TFT_RGB_ORDER TFT_RGB  // Colour order Red-Green-Blue\n//#define TFT_RGB_ORDER TFT_BGR  // Colour order Blue-Green-Red\n\n//#define TFT_INVERSION_ON\n//#define TFT_INVERSION_OFF\n\n// EleksTube IPS\n#define TFT_SDA_READ      // Read and write on the MOSI/SDA pin, no separate MISO pin\n#define TFT_MOSI 23\n#define TFT_SCLK 18\n//#define TFT_CS    -1 // Not connected\n#define TFT_DC   25  // Data Command, aka Register Select or RS\n#define TFT_RST  26  // Connect reset to ensure display initialises\n\n#define LOAD_GLCD   // Font 1. Original Adafruit 8 pixel font needs ~1820 bytes in FLASH\n//#define LOAD_FONT2  // Font 2. Small 16 pixel high font, needs ~3534 bytes in FLASH, 96 characters\n//#define LOAD_FONT4  // Font 4. Medium 26 pixel high font, needs ~5848 bytes in FLASH, 96 characters\n//#define LOAD_FONT6  // Font 6. Large 48 pixel font, needs ~2666 bytes in FLASH, only characters 1234567890:-.apm\n//#define LOAD_FONT7  // Font 7. 7 segment 48 pixel font, needs ~2438 bytes in FLASH, only characters 1234567890:.\n//#define LOAD_FONT8  // Font 8. Large 75 pixel font needs ~3256 bytes in FLASH, only characters 1234567890:-.\n//#define LOAD_FONT8N // Font 8. Alternative to Font 8 above, slightly narrower, so 3 digits fit a 160 pixel TFT\n//#define LOAD_GFXFF  // FreeFonts. Include access to the 48 Adafruit_GFX free fonts FF1 to FF48 and custom fonts\n\n//#define SMOOTH_FONT\n\n\n//#define SPI_FREQUENCY  27000000\n#define SPI_FREQUENCY  40000000\n\n/*\n * To make the Library not over-write all this:\n */\n#define USER_SETUP_LOADED\n"
  },
  {
    "path": "usermods/EleksTube_IPS/library.json.disabled",
    "content": "{\n  \"name:\": \"EleksTube_IPS\",\n  \"build\": { \"libArchive\": false },\n  \"dependencies\": {\n    \"TFT_eSPI\" : \"2.5.33\"\n  } \n}\n# Seems to add 300kb to the RAM requirement???\n"
  },
  {
    "path": "usermods/EleksTube_IPS/readme.md",
    "content": "# EleksTube IPS Clock usermod\n\nThis usermod allows WLED to run on the EleksTube IPS clock.\nIt enables running all WLED effects on the background SK6812 lighting, while displaying digit bitmaps on the 6 IPS screens.\nCode is largely based on https://github.com/SmittyHalibut/EleksTubeHAX by Mark Smith!\n\nSupported:\n- Display with custom bitmaps (.bmp) or raw RGB565 images (.bin) from filesystem\n- Background lighting\n- All 4 hardware buttons\n- RTC (with RTC usermod)\n- Standard WLED time features (NTP, DST, timezones)\n\nNot supported:\n- On-device setup with buttons (WiFi setup only)\n\nYour images must be 1-135 pixels wide and 1-240 pixels high.\nBMP 1, 4, 8, and 24 bits per pixel formats are supported.\n\n## Installation \n\nCompile and upload to clock using the `elekstube_ips` PlatformIO environment\nOnce uploaded (the clock can be flashed like any ESP32 module), go to `[WLED-IP]/edit` and upload the 0-9.bin files from [here](https://github.com/Aircoookie/NixieThemes/tree/master/themes/RealisticNixie/bin).\nYou can find more clockfaces in the [NixieThemes](https://github.com/Aircoookie/NixieThemes/) repo.\nUse LED pin 12, relay pin 27 and button pin 34.\n\n## Use of RGB565 images\n\nBinary 16-bit per pixel RGB565 format `.bin` and `.clk` images are now supported. This has the benefit of using only 2/3rds of the file space a 24 BPP `.bmp` occupies.\nThe drawback is this format cannot be handled by common image programs and an extra conversion step is needed.\nYou can use https://lvgl.io/tools/imageconverter to convert your .bmp to a .bin file (settings `True color` and `Binary RGB565`).  \nThank you to @RedNax67 for adding .bin and .clk support.  \nFor most clockface designs, using 4 or 8 BPP BMP format will reduce file size even more:\n\n| Bits per pixel | File size in kB (for 135x240 img) | % of 24 BPP BMP | Max unique colors\n| --- | --- | --- | --- |\n24 | 98 | 100% | 16M (66K)\n16 (.clk) | 64.8 | 66% | 66K\n8 | 33.7 | 34% | 256\n4 | 16.4 | 17% | 16\n1 | 4.9 | 5% | 2\n\nComparison 1 vs. 4 vs. 8 vs. 24 BPP. With this clockface on the actual clock, 4 bit looks good, and 8 bit is almost indistinguishable from 24 bit.\n\n![comparison](https://user-images.githubusercontent.com/21045690/156899667-5b55ed9f-6e03-4066-b2aa-1260e9570369.png)\n"
  },
  {
    "path": "usermods/Enclosure_with_OLED_temp_ESP07/assets/readme.md",
    "content": "# Enclosure and PCB\n\n## IP67 rated enclosure\n![Enclosure](controller.jpg)\n\n## PCB\n![PCB](pcb.png)\n"
  },
  {
    "path": "usermods/Enclosure_with_OLED_temp_ESP07/readme.md",
    "content": "# Almost universal controller board for outdoor applications\nThis usermod is using ideas from @mrVanboy and @400killer\n\nInstallation of file: Copy and replace file in wled00 directory.\n\nFor BME280 sensor use usermod_bme280.cpp. Copy to wled00 and rename to usermod.cpp\n\n## Project repository\n-   [Original repository](https://github.com/srg74/Controller-for-WLED-firmware) - Main controller repository\n## Features\n-   SSD1306 128x32 and 128x64 I2C OLED display\n-   On screen IP address, SSID and controller status (e.g. ON or OFF, recent effect)\n-   Auto display shutoff for extending display lifetime\n-   Dallas temperature sensor\n-   Reporting temperature to MQTT broker\n\n## Hardware\n![Hardware connection](assets/controller.jpg)\n\n## Functionality checked with\n-   ESP-07S\n-   PlatformIO\n-   SSD1306 128x32 I2C OLED display\n-   DS18B20 (temperature sensor)\n-   BME280 (temperature, humidity and pressure sensor)\n-   KY-022 (infrared receiver)\n-   Push button (N.O. momentary switch)\n\nFor Dallas sensor uncomment `U8g2@~2.27.3`,`DallasTemperature@~3.8.0`,`OneWire@~2.3.5 under` `[common]` section in `platformio.ini`:\n```ini\n# platformio.ini\n...\n[platformio]\n...\ndefault_envs = esp07\n; default_envs = d1_mini\n...\n[common]\n...\nlib_deps_external =\n  ...\n  #To use the SSD1306 OLED display, uncomment following\n  U8g2@~2.27.3\n  #For Dallas sensor, uncomment the following 2 lines\n  DallasTemperature@~3.8.0\n  OneWire@~2.3.5\n...\n```\n\nFor BME280 sensor, uncomment `U8g2@~2.27.3`,`BME280@~3.0.0 under` `[common]` section in `platformio.ini`:\n```ini\n# platformio.ini\n...\n[platformio]\n...\ndefault_envs = esp07\n; default_envs = d1_mini\n...\n[common]\n...\nlib_deps_external =\n  ...\n  #To use the SSD1306 OLED display, uncomment following\n  U8g2@~2.27.3\n  #For BME280 sensor uncomment following\n  BME280@~3.0.0\n...\n```\n"
  },
  {
    "path": "usermods/Enclosure_with_OLED_temp_ESP07/usermod.cpp",
    "content": "#include \"wled.h\"\n#include <Arduino.h>\n#include <U8x8lib.h> // from https://github.com/olikraus/u8g2/\n#include <DallasTemperature.h> //Dallastemperature sensor\n\n#ifdef WLED_DISABLE_MQTT\n#error \"This user mod requires MQTT to be enabled.\"\n#endif\n\n//The SCL and SDA pins are defined here. \n//Lolin32 boards use SCL=5 SDA=4 \n#define U8X8_PIN_SCL 5\n#define U8X8_PIN_SDA 4\n// Dallas sensor\nOneWire oneWire(13); \nDallasTemperature sensor(&oneWire);\nlong temptimer = millis();\nlong lastMeasure = 0;\n#define Celsius // Show temperature measurement in Celsius otherwise is in Fahrenheit \n\n// If display does not work or looks corrupted check the\n// constructor reference:\n// https://github.com/olikraus/u8g2/wiki/u8x8setupcpp\n// or check the gallery:\n// https://github.com/olikraus/u8g2/wiki/gallery\n// --> First choice of cheap I2C OLED 128X32 0.91\"\nU8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA\n// --> Second choice of cheap I2C OLED 128X64 0.96\" or 1.3\"\n//U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA\n// gets called once at boot. Do all initialization that doesn't depend on\n// network here\nvoid userSetup() {\n  sensor.begin(); //Start Dallas temperature sensor\n  u8x8.begin();\n  //u8x8.setFlipMode(1); //Un-comment if using WLED Wemos shield \n  u8x8.setPowerSave(0);\n  u8x8.setContrast(10); //Contrast setup will help to preserve OLED lifetime. In case OLED need to be brighter increase number up to 255\n  u8x8.setFont(u8x8_font_chroma48medium8_r);\n  u8x8.drawString(0, 0, \"Loading...\");\n}\n\n// gets called every time WiFi is (re-)connected. Initialize own network\n// interfaces here\nvoid userConnected() {}\n\n// needRedraw marks if redraw is required to prevent often redrawing.\nbool needRedraw = true;\n\n// Next variables hold the previous known values to determine if redraw is\n// required.\nString knownSsid = \"\";\nIPAddress knownIp;\nuint8_t knownBrightness = 0;\nuint8_t knownMode = 0;\nuint8_t knownPalette = 0;\n\nlong lastUpdate = 0;\nlong lastRedraw = 0;\nbool displayTurnedOff = false;\n// How often we are redrawing screen\n#define USER_LOOP_REFRESH_RATE_MS 5000\n\nvoid userLoop() {\n\n//----> Dallas temperature sensor MQTT publishing\n  temptimer = millis();  \n// Timer to publishe new temperature every 60 seconds\n  if (temptimer - lastMeasure > 60000) \n  {\n    lastMeasure = temptimer;    \n//Check if MQTT Connected, otherwise it will crash the 8266\n    if (mqtt != nullptr)\n    {\n      sensor.requestTemperatures();\n//Gets preferred temperature scale based on selection in definitions section\n      #ifdef Celsius\n      float board_temperature = sensor.getTempCByIndex(0);\n      #else\n      float board_temperature = sensor.getTempFByIndex(0);\n      #endif\n//Create character string populated with user defined device topic from the UI, and the read temperature. Then publish to MQTT server.\n      char subuf[38];\n      strcpy(subuf, mqttDeviceTopic);\n      strcat(subuf, \"/temperature\");\n      mqtt->publish(subuf, 0, true, String(board_temperature).c_str());\n    }\n  }\n\n  // Check if we time interval for redrawing passes.\n  if (millis() - lastUpdate < USER_LOOP_REFRESH_RATE_MS) {\n    return;\n  }\n  lastUpdate = millis();\n  \n  // Turn off display after 3 minutes with no change.\n  if(!displayTurnedOff && millis() - lastRedraw > 3*60*1000) {\n    u8x8.setPowerSave(1);\n    displayTurnedOff = true;\n  }\n\n  // Check if values which are shown on display changed from the last time.\n  if (((apActive) ? String(apSSID) : WiFi.SSID()) != knownSsid) {\n    needRedraw = true;\n  } else if (knownIp != (apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP())) {\n    needRedraw = true;\n  } else if (knownBrightness != bri) {\n    needRedraw = true;\n  } else if (knownMode != strip.getMainSegment().mode) {\n    needRedraw = true;\n  } else if (knownPalette != strip.getMainSegment().palette) {\n    needRedraw = true;\n  }\n\n  if (!needRedraw) {\n    return;\n  }\n  needRedraw = false;\n  \n  if (displayTurnedOff)\n  {\n    u8x8.setPowerSave(0);\n    displayTurnedOff = false;\n  }\n  lastRedraw = millis();\n\n  // Update last known values.\n  #if defined(ESP8266)\n  knownSsid = apActive ? WiFi.softAPSSID() : WiFi.SSID();\n  #else\n  knownSsid = WiFi.SSID();\n  #endif\n  knownIp = apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP();\n  knownBrightness = bri;\n  knownMode = strip.getMainSegment().mode;\n  knownPalette = strip.getMainSegment().palette;\n  u8x8.clear();\n  u8x8.setFont(u8x8_font_chroma48medium8_r);\n\n  // First row with Wifi name\n  u8x8.setCursor(1, 0);\n  u8x8.print(knownSsid.substring(0, u8x8.getCols() > 1 ? u8x8.getCols() - 2 : 0));\n  // Print `~` char to indicate that SSID is longer than our display\n  if (knownSsid.length() > u8x8.getCols())\n    u8x8.print(\"~\");\n\n  // Second row with IP or Password\n  u8x8.setCursor(1, 1);\n  // Print password in AP mode and if led is OFF.\n  if (apActive && bri == 0)\n    u8x8.print(apPass);\n  else\n    u8x8.print(knownIp);\n\n  // Third row with mode name\n  u8x8.setCursor(2, 2);\n  char lineBuffer[17];\n  extractModeName(knownMode, JSON_mode_names, lineBuffer, 16);\n  u8x8.print(lineBuffer);\n\n  // Fourth row with palette name\n  u8x8.setCursor(2, 3);\n  extractModeName(knownPalette, JSON_palette_names, lineBuffer, 16);\n  u8x8.print(lineBuffer);\n\n  u8x8.setFont(u8x8_font_open_iconic_embedded_1x1);\n  u8x8.drawGlyph(0, 0, 80); // wifi icon\n  u8x8.drawGlyph(0, 1, 68); // home icon\n  u8x8.setFont(u8x8_font_open_iconic_weather_2x2);\n  u8x8.drawGlyph(0, 2, 66 + (bri > 0 ? 3 : 0)); // sun/moon icon\n}"
  },
  {
    "path": "usermods/Enclosure_with_OLED_temp_ESP07/usermod_bme280.cpp",
    "content": "#include \"wled.h\"\n#include <Arduino.h>\n#include <U8x8lib.h> // from https://github.com/olikraus/u8g2/\n#include <Wire.h>\n#include <BME280I2C.h> //BME280 sensor\n\n#ifdef WLED_DISABLE_MQTT\n#error \"This user mod requires MQTT to be enabled.\"\n#endif\n\nvoid UpdateBME280Data();\n\n#define Celsius // Show temperature measurement in Celsius otherwise is in Fahrenheit \nBME280I2C bme;    // Default : forced mode, standby time = 1000 ms\n                  // Oversampling = pressure ×1, temperature ×1, humidity ×1, filter off,\n\n#ifdef ARDUINO_ARCH_ESP32 //ESP32 boards\nuint8_t SCL_PIN = 22;\nuint8_t SDA_PIN = 21;\n#else //ESP8266 boards\nuint8_t SCL_PIN = 5;\nuint8_t SDA_PIN = 4;\n// uint8_t RST_PIN = 16; // Un-comment for Heltec WiFi-Kit-8\n#endif\n\n//The SCL and SDA pins are defined here.\n//ESP8266 Wemos D1 mini board use SCL=5 SDA=4 while ESP32 Wemos32 mini board use SCL=22 SDA=21\n#define U8X8_PIN_SCL SCL_PIN\n#define U8X8_PIN_SDA SDA_PIN\n//#define U8X8_PIN_RESET RST_PIN // Un-comment for Heltec WiFi-Kit-8\n\n// If display does not work or looks corrupted check the\n// constructor reference:\n// https://github.com/olikraus/u8g2/wiki/u8x8setupcpp\n// or check the gallery:\n// https://github.com/olikraus/u8g2/wiki/gallery\n// --> First choise of cheap I2C OLED 128X32 0.91\"\nU8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA\n// --> Second choice of cheap I2C OLED 128X64 0.96\" or 1.3\"\n//U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA\n// --> Third choice of Heltec WiFi-Kit-8 OLED 128X32 0.91\"\n//U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_RESET, U8X8_PIN_SCL, U8X8_PIN_SDA); // Constructor for Heltec WiFi-Kit-8\n// gets called once at boot. Do all initialization that doesn't depend on network here\n\n// BME280 sensor timer\nlong tempTimer = millis();\nlong lastMeasure = 0;\n\nfloat SensorPressure(NAN);\nfloat SensorTemperature(NAN);\nfloat SensorHumidity(NAN);\n\nvoid userSetup() {\n  u8x8.begin();\n  u8x8.setPowerSave(0);\n  u8x8.setFlipMode(1);\n  u8x8.setContrast(10); //Contrast setup will help to preserve OLED lifetime. In case OLED need to be brighter increase number up to 255\n  u8x8.setFont(u8x8_font_chroma48medium8_r);\n  u8x8.drawString(0, 0, \"Loading...\");\n  Wire.begin(SDA_PIN,SCL_PIN);\n\nwhile(!bme.begin())\n  {\n    Serial.println(\"Could not find BME280I2C sensor!\");\n    delay(1000);\n  }\nswitch(bme.chipModel())\n  {\n    case BME280::ChipModel_BME280:\n      Serial.println(\"Found BME280 sensor! Success.\");\n      break;\n    case BME280::ChipModel_BMP280:\n      Serial.println(\"Found BMP280 sensor! No Humidity available.\");\n      break;\n    default:\n      Serial.println(\"Found UNKNOWN sensor! Error!\");\n  }\n}\n\n// gets called every time WiFi is (re-)connected. Initialize own network\n// interfaces here\nvoid userConnected() {}\n\n// needRedraw marks if redraw is required to prevent often redrawing.\nbool needRedraw = true;\n\n// Next variables hold the previous known values to determine if redraw is\n// required.\nString knownSsid = \"\";\nIPAddress knownIp;\nuint8_t knownBrightness = 0;\nuint8_t knownMode = 0;\nuint8_t knownPalette = 0;\n\nlong lastUpdate = 0;\nlong lastRedraw = 0;\nbool displayTurnedOff = false;\n// How often we are redrawing screen\n#define USER_LOOP_REFRESH_RATE_MS 5000\n\nvoid userLoop() {\n\n// BME280 sensor MQTT publishing\n  tempTimer = millis();  \n// Timer to publish new sensor data every 60 seconds\n  if (tempTimer - lastMeasure > 60000) \n  {\n    lastMeasure = tempTimer;    \n\n// Check if MQTT Connected, otherwise it will crash the 8266\n    if (mqtt != nullptr)\n    {\n      UpdateBME280Data();\n      float board_temperature = SensorTemperature;\n      float board_pressure = SensorPressure;\n      float board_humidity = SensorHumidity;\n\n// Create string populated with user defined device topic from the UI, and the read temperature, humidity and pressure. Then publish to MQTT server.\n      String t = String(mqttDeviceTopic);\n      t += \"/temperature\";\n      mqtt->publish(t.c_str(), 0, true, String(board_temperature).c_str());\n      String p = String(mqttDeviceTopic);\n      p += \"/pressure\";\n      mqtt->publish(p.c_str(), 0, true, String(board_pressure).c_str());\n      String h = String(mqttDeviceTopic);\n      h += \"/humidity\";\n      mqtt->publish(h.c_str(), 0, true, String(board_humidity).c_str());\n    }\n  }\n\n  // Check if we time interval for redrawing passes.\n  if (millis() - lastUpdate < USER_LOOP_REFRESH_RATE_MS) {\n    return;\n  }\n  lastUpdate = millis();\n  \n  // Turn off display after 3 minutes with no change.\n  if(!displayTurnedOff && millis() - lastRedraw > 3*60*1000) {\n    u8x8.setPowerSave(1);\n    displayTurnedOff = true;\n  }\n\n  // Check if values which are shown on display changed from the last time.\n  if (((apActive) ? String(apSSID) : WiFi.SSID()) != knownSsid) {\n    needRedraw = true;\n  } else if (knownIp != (apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP())) {\n    needRedraw = true;\n  } else if (knownBrightness != bri) {\n    needRedraw = true;\n  } else if (knownMode != strip.getMainSegment().mode) {\n    needRedraw = true;\n  } else if (knownPalette != strip.getMainSegment().palette) {\n    needRedraw = true;\n  }\n\n  if (!needRedraw) {\n    return;\n  }\n  needRedraw = false;\n  \n  if (displayTurnedOff)\n  {\n    u8x8.setPowerSave(0);\n    displayTurnedOff = false;\n  }\n  lastRedraw = millis();\n\n  // Update last known values.\n  #if defined(ESP8266)\n  knownSsid = apActive ? WiFi.softAPSSID() : WiFi.SSID();\n  #else\n  knownSsid = WiFi.SSID();\n  #endif\n  knownIp = apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP();\n  knownBrightness = bri;\n  knownMode = strip.getMainSegment().mode;\n  knownPalette = strip.getMainSegment().palette;\n  u8x8.clear();\n  u8x8.setFont(u8x8_font_chroma48medium8_r);\n\n  // First row with Wifi name\n  u8x8.setCursor(1, 0);\n  u8x8.print(knownSsid.substring(0, u8x8.getCols() > 1 ? u8x8.getCols() - 2 : 0));\n  // Print `~` char to indicate that SSID is longer than our display\n  if (knownSsid.length() > u8x8.getCols())\n    u8x8.print(\"~\");\n\n  // Second row with IP or Password\n  u8x8.setCursor(1, 1);\n  // Print password in AP mode and if led is OFF.\n  if (apActive && bri == 0)\n    u8x8.print(apPass);\n  else\n    u8x8.print(knownIp);\n\n  // Third row with mode name\n  u8x8.setCursor(2, 2);\n  char lineBuffer[17];\n  extractModeName(knownMode, JSON_mode_names, lineBuffer, 16);\n  u8x8.print(lineBuffer);\n\n  // Fourth row with palette name\n  u8x8.setCursor(2, 3);\n  extractModeName(knownPalette, JSON_palette_names, lineBuffer, 16);\n  u8x8.print(lineBuffer);\n\n  u8x8.setFont(u8x8_font_open_iconic_embedded_1x1);\n  u8x8.drawGlyph(0, 0, 80); // wifi icon\n  u8x8.drawGlyph(0, 1, 68); // home icon\n  u8x8.setFont(u8x8_font_open_iconic_weather_2x2);\n  u8x8.drawGlyph(0, 2, 66 + (bri > 0 ? 3 : 0)); // sun/moon icon\n}\n\nvoid UpdateBME280Data() {\n  float temp(NAN), hum(NAN), pres(NAN);\n#ifdef Celsius\n  BME280::TempUnit tempUnit(BME280::TempUnit_Celsius);\n#else\n  BME280::TempUnit tempUnit(BME280::TempUnit_Fahrenheit);\n#endif\n  BME280::PresUnit presUnit(BME280::PresUnit_Pa);\n  bme.read(pres, temp, hum, tempUnit, presUnit);\n  SensorTemperature=temp;\n  SensorHumidity=hum;\n  SensorPressure=pres;\n}\n"
  },
  {
    "path": "usermods/Fix_unreachable_netservices_v2/library.json",
    "content": "{\n  \"name\": \"Fix_unreachable_netservices_v2\",\n  \"platforms\": [\"espressif8266\"]\n}\n"
  },
  {
    "path": "usermods/Fix_unreachable_netservices_v2/readme.md",
    "content": "# Fix unreachable net services V2\n\n**Attention: This usermod compiles only for ESP8266**\n\nThis usermod-v2 modification performs a ping request to a local IP address every 60 seconds. This ensures WLED net services remain accessible in some problematic WiFi environments.\n\nThe modification works with static or DHCP IP address configuration.\n\n_Story:_\n\nUnfortunately, with many ESP projects where a web server or other network services are running, after some time, the connecton to the web server is lost.\nThe connection can be reestablished with a ping request from the device.\n\nWith this modification, in the worst case, the network functions are not available until the next ping request. (60 seconds)\n\n## Webinterface\n\nThe number of pings and reconnects is displayed on the info page in the web interface.\nThe ping delay can be changed. Changes persist after a reboot.\n\n## JSON API\n\nThe usermod supports the following state changes:\n\n| JSON key    | Value range      | Description                     |\n|-------------|------------------|---------------------------------|\n| PingDelayMs | 5000 to 18000000 | Deactivate/activate the sensor  |\n\n Changes also persist after a reboot.\n\n## Installation\n\n1. Add `Fix_unreachable_netservices` to `custom_usermods` in your PlatformIO environment.\n\nHopefully I can help someone with that - @gegu\n"
  },
  {
    "path": "usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.cpp",
    "content": "#include \"wled.h\"\n\n#if defined(ESP8266)\n#include <ping.h>\n\n/*\n * This usermod performs a ping request to the local IP address every 60 seconds. \n * By this procedure the net services of WLED remains accessible in some problematic WLAN environments.\n * \n * Usermods allow you to add own functionality to WLED more easily\n * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality\n * \n * v2 usermods are class inheritance based and can (but don't have to) implement more functions, each of them is shown in this example.\n * Multiple v2 usermods can be added to one compilation easily.\n * \n * Creating a usermod:\n * This file serves as an example. If you want to create a usermod, it is recommended to use usermod_v2_empty.h from the usermods folder as a template.\n * Please remember to rename the class and file to a descriptive name.\n * You may also use multiple .h and .cpp files.\n * \n * Using a usermod:\n * 1. Copy the usermod into the sketch folder (same folder as wled00.ino)\n * 2. Register the usermod by adding #include \"usermod_filename.h\" in the top and registerUsermod(new MyUsermodClass()) in the bottom of usermods_list.cpp\n */\n\nclass FixUnreachableNetServices : public Usermod\n{\nprivate:\n  //Private class members. You can declare variables and functions only accessible to your usermod here\n  unsigned long m_lastTime = 0;\n\n  // declare required variables\n  unsigned long m_pingDelayMs = 60000;\n  unsigned long m_connectedWiFi = 0;\n  ping_option m_pingOpt;\n  unsigned int m_pingCount = 0;\n  bool m_updateConfig = false;\n\npublic:\n  //Functions called by WLED\n\n  /**\n   * setup() is called once at boot. WiFi is not yet connected at this point.\n   * You can use it to initialize variables, sensors or similar.\n   */\n  void setup()\n  {\n    //Serial.println(\"Hello from my usermod!\");\n  }\n\n  /**\n   * connected() is called every time the WiFi is (re)connected\n   * Use it to initialize network interfaces\n   */\n  void connected()\n  {\n    //Serial.println(\"Connected to WiFi!\");\n\n    ++m_connectedWiFi;\n\n    // initialize ping_options structure\n    memset(&m_pingOpt, 0, sizeof(struct ping_option));\n    m_pingOpt.count = 1;\n    m_pingOpt.ip = WiFi.localIP();\n  }\n\n  /**\n   * loop\n   */\n  void loop()\n  {\n    if (m_connectedWiFi > 0 && millis() - m_lastTime > m_pingDelayMs)\n    {\n      ping_start(&m_pingOpt);\n      m_lastTime = millis();\n      ++m_pingCount;\n    }\n    if (m_updateConfig)\n    {\n      serializeConfig();\n      m_updateConfig = false;\n    }\n  }\n\n  /**\n   * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.\n   * Creating an \"u\" object allows you to add custom key/value pairs to the Info section of the WLED web UI.\n   * Below it is shown how this could be used for e.g. a light sensor\n   */\n  void addToJsonInfo(JsonObject &root)\n  {\n    //this code adds \"u\":{\"&#x26A1; Ping fix pings\": m_pingCount} to the info object\n    JsonObject user = root[\"u\"];\n    if (user.isNull())\n      user = root.createNestedObject(\"u\");\n\n    String uiDomString = \"&#x26A1; Ping fix pings<span style=\\\"display:block;padding-left:25px;\\\">\\\nDelay <input type=\\\"number\\\" min=\\\"5\\\" max=\\\"300\\\" value=\\\"\";\n    uiDomString += (unsigned long)(m_pingDelayMs / 1000);\n    uiDomString += \"\\\" onchange=\\\"requestJson({PingDelay:parseInt(this.value)});\\\">sec</span>\";\n\n    JsonArray infoArr = user.createNestedArray(uiDomString); //name\n    infoArr.add(m_pingCount);                                              //value\n\n    //this code adds \"u\":{\"&#x26A1; Reconnects\": m_connectedWiFi - 1} to the info object\n    infoArr = user.createNestedArray(\"&#x26A1; Reconnects\"); //name\n    infoArr.add(m_connectedWiFi - 1);                        //value\n  }\n\n  /**\n   * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).\n   * Values in the state object may be modified by connected clients\n   */\n  void addToJsonState(JsonObject &root)\n  {\n    root[\"PingDelay\"] = (m_pingDelayMs/1000);\n  }\n\n  /**\n   * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).\n   * Values in the state object may be modified by connected clients\n   */\n  void readFromJsonState(JsonObject &root)\n  {\n    if (root[\"PingDelay\"] != nullptr)\n    {\n      m_pingDelayMs = (1000 * max(1UL, min(300UL, root[\"PingDelay\"].as<unsigned long>())));\n      m_updateConfig = true;\n    }\n  }\n\n  /**\n   * provide the changeable values\n   */\n  void addToConfig(JsonObject &root)\n  {\n    JsonObject top = root.createNestedObject(\"FixUnreachableNetServices\");\n    top[\"PingDelayMs\"] = m_pingDelayMs;\n  }\n\n  /**\n   * restore the changeable values\n   */\n  bool readFromConfig(JsonObject &root)\n  {\n    JsonObject top = root[\"FixUnreachableNetServices\"];\n    if (top.isNull()) return false;\n    m_pingDelayMs = top[\"PingDelayMs\"] | m_pingDelayMs;\n    m_pingDelayMs = max(5000UL, min(18000000UL, m_pingDelayMs));\n    // use \"return !top[\"newestParameter\"].isNull();\" when updating Usermod with new features\n    return true;\n  }\n\n  /**\n   * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).\n   * This could be used in the future for the system to determine whether your usermod is installed.\n   */\n  uint16_t getId()\n  {\n    return USERMOD_ID_FIXNETSERVICES;\n  }\n};\n\nstatic FixUnreachableNetServices fix_unreachable_net_services;\nREGISTER_USERMOD(fix_unreachable_net_services);\n\n#else /* !ESP8266 */\n#warning \"Usermod FixUnreachableNetServices works only with ESP8266 builds\"\n#endif\n\n"
  },
  {
    "path": "usermods/INA226_v2/INA226_v2.cpp",
    "content": "#include \"wled.h\"\n#include <INA226_WE.h>\n\n#define INA226_ADDRESS 0x40 // Default I2C address for INA226\n\n#define DEFAULT_CHECKINTERVAL 60000\n#define DEFAULT_INASAMPLES 128\n#define DEFAULT_INASAMPLESENUM AVERAGE_128\n#define DEFAULT_INACONVERSIONTIME 1100\n#define DEFAULT_INACONVERSIONTIMEENUM CONV_TIME_1100\n\n// A packed version of all INA settings enums and their human friendly counterparts packed into a 32 bit structure\n// Some values are shifted and need to be preprocessed before usage\nstruct InaSettingLookup\n{\n    uint16_t avgSamples : 11;          // Max 1024, which could be in 10 bits if we shifted by 1; if we somehow handle the edge case with \"1\"\n    uint8_t avgEnum : 4;               // Shift by 8 to get the INA226_AVERAGES value, accepts 0x00 to 0x0F, we need 0x00 to 0x0E\n    uint16_t convTimeUs : 14;          // We could save 2 bits by shifting this, but we won't save anything at present.\n    INA226_CONV_TIME convTimeEnum : 3; // Only the lowest 3 bits are defined in the conversion time enumerations\n};\n\nconst InaSettingLookup _inaSettingsLookup[] = {\n    {1024, AVERAGE_1024 >> 8, 8244, CONV_TIME_8244},\n    {512, AVERAGE_512 >> 8, 4156, CONV_TIME_4156},\n    {256, AVERAGE_256 >> 8, 2116, CONV_TIME_2116},\n    {128, AVERAGE_128 >> 8, 1100, CONV_TIME_1100},\n    {64, AVERAGE_64 >> 8, 588, CONV_TIME_588},\n    {16, AVERAGE_16 >> 8, 332, CONV_TIME_332},\n    {4, AVERAGE_4 >> 8, 204, CONV_TIME_204},\n    {1, AVERAGE_1 >> 8, 140, CONV_TIME_140}};\n\n// Note: Will update the provided arg to be the correct value\nINA226_AVERAGES getAverageEnum(uint16_t &samples)\n{\n    for (const auto &setting : _inaSettingsLookup)\n    {\n        // If a user supplies 2000 samples, we serve up the highest possible value\n        if (samples >= setting.avgSamples)\n        {\n            samples = setting.avgSamples;\n            return static_cast<INA226_AVERAGES>(setting.avgEnum << 8);\n        }\n    }\n    // Default value if not found\n    samples = DEFAULT_INASAMPLES;\n    return DEFAULT_INASAMPLESENUM;\n}\n\nINA226_CONV_TIME getConversionTimeEnum(uint16_t &timeUs)\n{\n    for (const auto &setting : _inaSettingsLookup)\n    {\n        // If a user supplies 9000 μs, we serve up the highest possible value\n        if (timeUs >= setting.convTimeUs)\n        {\n            timeUs = setting.convTimeUs;\n            return setting.convTimeEnum;\n        }\n    }\n    // Default value if not found\n    timeUs = DEFAULT_INACONVERSIONTIME;\n    return DEFAULT_INACONVERSIONTIMEENUM;\n}\n\nclass UsermodINA226 : public Usermod\n{\nprivate:\n    static const char _name[];\n\n    unsigned long _lastLoopCheck = 0;\n    unsigned long _lastTriggerTime = 0;\n\n    bool _settingEnabled : 1;                  // Enable the usermod\n    bool _mqttPublish : 1;                     // Publish MQTT values\n    bool _mqttPublishAlways : 1;               // Publish always, regardless if there is a change\n    bool _mqttHomeAssistant : 1;               // Enable Home Assistant docs\n    bool _initDone : 1;                        // Initialization is done\n    bool _isTriggeredOperationMode : 1;        // false = continuous, true = triggered\n    bool _measurementTriggered : 1;            // if triggered mode, then true indicates we're waiting for measurements\n    uint16_t _settingInaConversionTimeUs : 12; // Conversion time, shift by 2\n    uint16_t _settingInaSamples : 11;          // Number of samples for averaging, max 1024\n\n    uint8_t _i2cAddress;\n    uint16_t _checkInterval; // milliseconds, user settings is in seconds\n    float _decimalFactor;    // a power of 10 factor. 1 would be no change, 10 is one decimal, 100 is two etc. User sees a power of 10 (0, 1, 2, ..)\n    uint16_t _shuntResistor; // Shunt resistor value in milliohms\n    uint16_t _currentRange;  // Expected maximum current in milliamps\n\n    uint8_t _lastStatus = 0;\n    float _lastCurrent = 0;\n    float _lastVoltage = 0;\n    float _lastPower = 0;\n    float _lastShuntVoltage = 0;\n    bool _lastOverflow = false;\n\n#ifndef WLED_MQTT_DISABLE\n    float _lastCurrentSent = 0;\n    float _lastVoltageSent = 0;\n    float _lastPowerSent = 0;\n    float _lastShuntVoltageSent = 0;\n    bool _lastOverflowSent = false;\n#endif\n\n    INA226_WE *_ina226 = nullptr;\n\n    float truncateDecimals(float val)\n    {\n        return roundf(val * _decimalFactor) / _decimalFactor;\n    }\n\n    void initializeINA226()\n    {\n        if (_ina226 != nullptr)\n        {\n            delete _ina226;\n        }\n\n        _ina226 = new INA226_WE(_i2cAddress);\n        if (!_ina226->init())\n        {\n            DEBUG_PRINTLN(F(\"INA226 initialization failed!\"));\n            return;\n        }\n        _ina226->setCorrectionFactor(1.0);\n\n        uint16_t tmpShort = _settingInaSamples;\n        _ina226->setAverage(getAverageEnum(tmpShort));\n\n        tmpShort = _settingInaConversionTimeUs << 2;\n        _ina226->setConversionTime(getConversionTimeEnum(tmpShort));\n\n        if (_checkInterval >= 20000)\n        {\n            _isTriggeredOperationMode = true;\n            _ina226->setMeasureMode(TRIGGERED);\n        }\n        else\n        {\n            _isTriggeredOperationMode = false;\n            _ina226->setMeasureMode(CONTINUOUS);\n        }\n\n        _ina226->setResistorRange(static_cast<float>(_shuntResistor) / 1000.0, static_cast<float>(_currentRange) / 1000.0);\n    }\n\n    void fetchAndPushValues()\n    {\n        _lastStatus = _ina226->getI2cErrorCode();\n\n        if (_lastStatus != 0)\n            return;\n\n        float current = truncateDecimals(_ina226->getCurrent_mA() / 1000.0);\n        float voltage = truncateDecimals(_ina226->getBusVoltage_V());\n        float power = truncateDecimals(_ina226->getBusPower() / 1000.0);\n        float shuntVoltage = truncateDecimals(_ina226->getShuntVoltage_V());\n        bool overflow = _ina226->overflow;\n\n#ifndef WLED_DISABLE_MQTT\n        mqttPublishIfChanged(F(\"current\"), _lastCurrentSent, current, 0.01f);\n        mqttPublishIfChanged(F(\"voltage\"), _lastVoltageSent, voltage, 0.01f);\n        mqttPublishIfChanged(F(\"power\"), _lastPowerSent, power, 0.1f);\n        mqttPublishIfChanged(F(\"shunt_voltage\"), _lastShuntVoltageSent, shuntVoltage, 0.01f);\n        mqttPublishIfChanged(F(\"overflow\"), _lastOverflowSent, overflow);\n#endif\n\n        _lastCurrent = current;\n        _lastVoltage = voltage;\n        _lastPower = power;\n        _lastShuntVoltage = shuntVoltage;\n        _lastOverflow = overflow;\n    }\n\n    void handleTriggeredMode(unsigned long currentTime)\n    {\n        if (_measurementTriggered)\n        {\n            // Test if we have a measurement every 400ms\n            if (currentTime - _lastTriggerTime >= 400)\n            {\n                _lastTriggerTime = currentTime;\n                if (_ina226->isBusy())\n                    return;\n\n                fetchAndPushValues();\n                _measurementTriggered = false;\n            }\n        }\n        else\n        {\n            if (currentTime - _lastLoopCheck >= _checkInterval)\n            {\n                // Start a measurement and use isBusy() later to determine when it is done\n                _ina226->startSingleMeasurementNoWait();\n                _lastLoopCheck = currentTime;\n                _lastTriggerTime = currentTime;\n                _measurementTriggered = true;\n            }\n        }\n    }\n\n    void handleContinuousMode(unsigned long currentTime)\n    {\n        if (currentTime - _lastLoopCheck >= _checkInterval)\n        {\n            _lastLoopCheck = currentTime;\n            fetchAndPushValues();\n        }\n    }\n\n#ifndef WLED_DISABLE_MQTT\n    void mqttInitialize()\n    {\n        if (!WLED_MQTT_CONNECTED || !_mqttPublish || !_mqttHomeAssistant)\n            return;\n\n        char topic[128];\n        snprintf_P(topic, 127, \"%s/current\", mqttDeviceTopic);\n        mqttCreateHassSensor(F(\"Current\"), topic, F(\"current\"), F(\"A\"));\n\n        snprintf_P(topic, 127, \"%s/voltage\", mqttDeviceTopic);\n        mqttCreateHassSensor(F(\"Voltage\"), topic, F(\"voltage\"), F(\"V\"));\n\n        snprintf_P(topic, 127, \"%s/power\", mqttDeviceTopic);\n        mqttCreateHassSensor(F(\"Power\"), topic, F(\"power\"), F(\"W\"));\n\n        snprintf_P(topic, 127, \"%s/shunt_voltage\", mqttDeviceTopic);\n        mqttCreateHassSensor(F(\"Shunt Voltage\"), topic, F(\"voltage\"), F(\"V\"));\n\n        snprintf_P(topic, 127, \"%s/overflow\", mqttDeviceTopic);\n        mqttCreateHassBinarySensor(F(\"Overflow\"), topic);\n    }\n\n    void mqttPublishIfChanged(const __FlashStringHelper *topic, float &lastState, float state, float minChange)\n    {\n        if (WLED_MQTT_CONNECTED && _mqttPublish && (_mqttPublishAlways || fabsf(lastState - state) > minChange))\n        {\n            char subuf[128];\n            snprintf_P(subuf, 127, PSTR(\"%s/%s\"), mqttDeviceTopic, (const char *)topic);\n            mqtt->publish(subuf, 0, false, String(state).c_str());\n\n            lastState = state;\n        }\n    }\n\n    void mqttPublishIfChanged(const __FlashStringHelper *topic, bool &lastState, bool state)\n    {\n        if (WLED_MQTT_CONNECTED && _mqttPublish && (_mqttPublishAlways || lastState != state))\n        {\n            char subuf[128];\n            snprintf_P(subuf, 127, PSTR(\"%s/%s\"), mqttDeviceTopic, (const char *)topic);\n            mqtt->publish(subuf, 0, false, state ? \"true\" : \"false\");\n\n            lastState = state;\n        }\n    }\n\n    void mqttCreateHassSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement)\n    {\n        String t = String(F(\"homeassistant/sensor/\")) + mqttClientID + \"/\" + name + F(\"/config\");\n\n        StaticJsonDocument<600> doc;\n\n        doc[F(\"name\")] = name;\n        doc[F(\"state_topic\")] = topic;\n        doc[F(\"unique_id\")] = String(mqttClientID) + name;\n        if (unitOfMeasurement != \"\")\n            doc[F(\"unit_of_measurement\")] = unitOfMeasurement;\n        if (deviceClass != \"\")\n            doc[F(\"device_class\")] = deviceClass;\n        doc[F(\"expire_after\")] = 1800;\n\n        JsonObject device = doc.createNestedObject(F(\"device\"));\n        device[F(\"name\")] = serverDescription;\n        device[F(\"identifiers\")] = \"wled-sensor-\" + String(mqttClientID);\n        device[F(\"manufacturer\")] = F(WLED_BRAND);\n        device[F(\"model\")] = F(WLED_PRODUCT_NAME);\n        device[F(\"sw_version\")] = versionString;\n\n        String temp;\n        serializeJson(doc, temp);\n        DEBUG_PRINTLN(t);\n        DEBUG_PRINTLN(temp);\n\n        mqtt->publish(t.c_str(), 0, true, temp.c_str());\n    }\n\n    void mqttCreateHassBinarySensor(const String &name, const String &topic)\n    {\n        String t = String(F(\"homeassistant/binary_sensor/\")) + mqttClientID + \"/\" + name + F(\"/config\");\n\n        StaticJsonDocument<600> doc;\n\n        doc[F(\"name\")] = name;\n        doc[F(\"state_topic\")] = topic;\n        doc[F(\"unique_id\")] = String(mqttClientID) + name;\n\n        JsonObject device = doc.createNestedObject(F(\"device\"));\n        device[F(\"name\")] = serverDescription;\n        device[F(\"identifiers\")] = \"wled-sensor-\" + String(mqttClientID);\n        device[F(\"manufacturer\")] = F(WLED_BRAND);\n        device[F(\"model\")] = F(WLED_PRODUCT_NAME);\n        device[F(\"sw_version\")] = versionString;\n\n        String temp;\n        serializeJson(doc, temp);\n        DEBUG_PRINTLN(t);\n        DEBUG_PRINTLN(temp);\n\n        mqtt->publish(t.c_str(), 0, true, temp.c_str());\n    }\n#endif\n\npublic:\n    UsermodINA226()\n    {\n        // Default values\n        _settingInaSamples = DEFAULT_INASAMPLES;\n        _settingInaConversionTimeUs = DEFAULT_INACONVERSIONTIME;\n\n        _i2cAddress = INA226_ADDRESS;\n        _checkInterval = DEFAULT_CHECKINTERVAL;\n        _decimalFactor = 100;\n        _shuntResistor = 1000;\n        _currentRange = 1000;\n    }\n\n    void setup()\n    {\n        initializeINA226();\n    }\n\n    void loop()\n    {\n        if (!_settingEnabled || strip.isUpdating())\n            return;\n\n        unsigned long currentTime = millis();\n\n        if (_isTriggeredOperationMode)\n        {\n            handleTriggeredMode(currentTime);\n        }\n        else\n        {\n            handleContinuousMode(currentTime);\n        }\n    }\n\n#ifndef WLED_DISABLE_MQTT\n    void onMqttConnect(bool sessionPresent)\n    {\n        mqttInitialize();\n    }\n#endif\n\n    uint16_t getId()\n    {\n        return USERMOD_ID_INA226;\n    }\n\n    void addToJsonInfo(JsonObject &root) override\n    {\n        JsonObject user = root[\"u\"];\n        if (user.isNull())\n            user = root.createNestedObject(\"u\");\n\n#ifdef USERMOD_INA226_DEBUG\n        JsonArray temp = user.createNestedArray(F(\"INA226 last loop\"));\n        temp.add(_lastLoopCheck);\n\n        temp = user.createNestedArray(F(\"INA226 last status\"));\n        temp.add(_lastStatus);\n\n        temp = user.createNestedArray(F(\"INA226 average samples\"));\n        temp.add(_settingInaSamples);\n        temp.add(F(\"samples\"));\n\n        temp = user.createNestedArray(F(\"INA226 conversion time\"));\n        temp.add(_settingInaConversionTimeUs << 2);\n        temp.add(F(\"μs\"));\n\n        // INA226 uses (2 * conversion time * samples) time to take a reading.\n        temp = user.createNestedArray(F(\"INA226 expected sample time\"));\n        uint32_t sampleTimeNeededUs = (static_cast<uint32_t>(_settingInaConversionTimeUs) << 2) * _settingInaSamples * 2;\n        temp.add(truncateDecimals(sampleTimeNeededUs / 1000.0));\n        temp.add(F(\"ms\"));\n\n        temp = user.createNestedArray(F(\"INA226 mode\"));\n        temp.add(_isTriggeredOperationMode ? F(\"triggered\") : F(\"continuous\"));\n\n        if (_isTriggeredOperationMode)\n        {\n            temp = user.createNestedArray(F(\"INA226 triggered\"));\n            temp.add(_measurementTriggered ? F(\"waiting for measurement\") : F(\"\"));\n        }\n#endif\n\n        JsonArray jsonCurrent = user.createNestedArray(F(\"Current\"));\n        JsonArray jsonVoltage = user.createNestedArray(F(\"Voltage\"));\n        JsonArray jsonPower = user.createNestedArray(F(\"Power\"));\n        JsonArray jsonShuntVoltage = user.createNestedArray(F(\"Shunt Voltage\"));\n        JsonArray jsonOverflow = user.createNestedArray(F(\"Overflow\"));\n\n        if (_lastLoopCheck == 0)\n        {\n            jsonCurrent.add(F(\"Not read yet\"));\n            jsonVoltage.add(F(\"Not read yet\"));\n            jsonPower.add(F(\"Not read yet\"));\n            jsonShuntVoltage.add(F(\"Not read yet\"));\n            jsonOverflow.add(F(\"Not read yet\"));\n            return;\n        }\n\n        if (_lastStatus != 0)\n        {\n            jsonCurrent.add(F(\"An error occurred\"));\n            jsonVoltage.add(F(\"An error occurred\"));\n            jsonPower.add(F(\"An error occurred\"));\n            jsonShuntVoltage.add(F(\"An error occurred\"));\n            jsonOverflow.add(F(\"An error occurred\"));\n            return;\n        }\n\n        jsonCurrent.add(_lastCurrent);\n        jsonCurrent.add(F(\"A\"));\n\n        jsonVoltage.add(_lastVoltage);\n        jsonVoltage.add(F(\"V\"));\n\n        jsonPower.add(_lastPower);\n        jsonPower.add(F(\"W\"));\n\n        jsonShuntVoltage.add(_lastShuntVoltage);\n        jsonShuntVoltage.add(F(\"V\"));\n\n        jsonOverflow.add(_lastOverflow ? F(\"true\") : F(\"false\"));\n    }\n\n    void addToConfig(JsonObject &root)\n    {\n        JsonObject top = root.createNestedObject(FPSTR(_name));\n        top[F(\"Enabled\")] = _settingEnabled;\n        top[F(\"I2CAddress\")] = static_cast<uint8_t>(_i2cAddress);\n        top[F(\"CheckInterval\")] = _checkInterval / 1000;\n        top[F(\"INASamples\")] = _settingInaSamples;\n        top[F(\"INAConversionTime\")] = _settingInaConversionTimeUs << 2;\n        top[F(\"Decimals\")] = log10f(_decimalFactor);\n        top[F(\"ShuntResistor\")] = _shuntResistor;\n        top[F(\"CurrentRange\")] = _currentRange;\n#ifndef WLED_DISABLE_MQTT\n        top[F(\"MqttPublish\")] = _mqttPublish;\n        top[F(\"MqttPublishAlways\")] = _mqttPublishAlways;\n        top[F(\"MqttHomeAssistantDiscovery\")] = _mqttHomeAssistant;\n#endif\n\n        DEBUG_PRINTLN(F(\"INA226 config saved.\"));\n    }\n\n    bool readFromConfig(JsonObject &root) override\n    {\n        JsonObject top = root[FPSTR(_name)];\n\n        bool configComplete = !top.isNull();\n        if (!configComplete)\n            return false;\n\n        bool tmpBool;\n        if (getJsonValue(top[F(\"Enabled\")], tmpBool))\n            _settingEnabled = tmpBool;\n        else\n            configComplete = false;\n\n        configComplete &= getJsonValue(top[F(\"I2CAddress\")], _i2cAddress);\n        if (getJsonValue(top[F(\"CheckInterval\")], _checkInterval))\n        {\n            if (1 <= _checkInterval && _checkInterval <= 600)\n                _checkInterval *= 1000;\n            else\n                _checkInterval = DEFAULT_CHECKINTERVAL;\n        }\n        else\n            configComplete = false;\n\n        uint16_t tmpShort;\n        if (getJsonValue(top[F(\"INASamples\")], tmpShort))\n        {\n            // The method below will fix the provided value to a valid one\n            getAverageEnum(tmpShort);\n            _settingInaSamples = tmpShort;\n        }\n        else\n            configComplete = false;\n\n        if (getJsonValue(top[F(\"INAConversionTime\")], tmpShort))\n        {\n            // The method below will fix the provided value to a valid one\n            getConversionTimeEnum(tmpShort);\n            _settingInaConversionTimeUs = tmpShort >> 2;\n        }\n        else\n            configComplete = false;\n\n        if (getJsonValue(top[F(\"Decimals\")], _decimalFactor))\n        {\n            if (0 <= _decimalFactor && _decimalFactor <= 5)\n                _decimalFactor = pow10f(_decimalFactor);\n            else\n                _decimalFactor = 100;\n        }\n        else\n            configComplete = false;\n\n        configComplete &= getJsonValue(top[F(\"ShuntResistor\")], _shuntResistor);\n        configComplete &= getJsonValue(top[F(\"CurrentRange\")], _currentRange);\n\n#ifndef WLED_DISABLE_MQTT\n        if (getJsonValue(top[F(\"MqttPublish\")], tmpBool))\n            _mqttPublish = tmpBool;\n        else\n            configComplete = false;\n\n        if (getJsonValue(top[F(\"MqttPublishAlways\")], tmpBool))\n            _mqttPublishAlways = tmpBool;\n        else\n            configComplete = false;\n\n        if (getJsonValue(top[F(\"MqttHomeAssistantDiscovery\")], tmpBool))\n            _mqttHomeAssistant = tmpBool;\n        else\n            configComplete = false;\n#endif\n\n        if (_initDone)\n        {\n            initializeINA226();\n\n#ifndef WLED_DISABLE_MQTT\n            mqttInitialize();\n#endif\n        }\n\n        _initDone = true;\n        return configComplete;\n    }\n\n    ~UsermodINA226()\n    {\n        delete _ina226;\n        _ina226 = nullptr;\n    }\n\n};\n\nconst char UsermodINA226::_name[] PROGMEM = \"INA226\";\n\n\nstatic UsermodINA226 ina226_v2;\nREGISTER_USERMOD(ina226_v2);"
  },
  {
    "path": "usermods/INA226_v2/README.md",
    "content": "# Usermod INA226\n\nThis Usermod is designed to read values from an INA226 sensor and output the following:\n- Current\n- Voltage\n- Power\n- Shunt Voltage\n- Overflow status\n\n## Configuration\n\nThe following settings can be configured in the Usermod Menu:\n- **Enabled**: Enable or disable the usermod.\n- **I2CAddress**: The I2C address in decimal. Default is 64 (0x40).\n- **CheckInterval**: Number of seconds between readings. This should be higher than the time it takes to make a reading, determined by the two next options.\n- **INASamples**: The number of samples to configure the INA226 to use for a measurement. Higher counts provide more accuracy. See the 'Understanding Samples and Conversion Times' section for more details.\n- **INAConversionTime**: The time to use on converting and preparing readings on the INA226. Higher times provide more precision. See the 'Understanding Samples and Conversion Times' section for more details.\n- **Decimals**: Number of decimals in the output.\n- **ShuntResistor**: Shunt resistor value in milliohms. An R100 shunt resistor should be written as \"100\", while R010 should be \"10\".\n- **CurrentRange**: Expected maximum current in milliamps (e.g., 5 A = 5000 mA).\n- **MqttPublish**: Enable or disable MQTT publishing.\n- **MqttPublishAlways**: Publish always, regardless if there is a change.\n- **MqttHomeAssistantDiscovery**: Enable Home Assistant discovery.\n\n\n## Understanding Samples and Conversion Times\n\nThe INA226 uses a programmable ADC with configurable conversion times and averaging to optimize the measurement accuracy and speed. The conversion time and number of samples are determined based on the `INASamples` and `INAConversionTime` settings. The following table outlines the possible combinations:\n\n| Conversion Time (μs) | 1 Sample | 4 Samples | 16 Samples | 64 Samples | 128 Samples | 256 Samples | 512 Samples | 1024 Samples |\n|----------------------|----------|-----------|------------|------------|-------------|-------------|-------------|--------------|\n| 140                  | 0.28 ms  | 1.12 ms   | 4.48 ms    | 17.92 ms   | 35.84 ms    | 71.68 ms    | 143.36 ms   | 286.72 ms    |\n| 204                  | 0.408 ms | 1.632 ms  | 6.528 ms   | 26.112 ms  | 52.224 ms   | 104.448 ms  | 208.896 ms  | 417.792 ms   |\n| 332                  | 0.664 ms | 2.656 ms  | 10.624 ms  | 42.496 ms  | 84.992 ms   | 169.984 ms  | 339.968 ms  | 679.936 ms   |\n| 588                  | 1.176 ms | 4.704 ms  | 18.816 ms  | 75.264 ms  | 150.528 ms  | 301.056 ms  | 602.112 ms  | 1204.224 ms  |\n| 1100                 | 2.2 ms   | 8.8 ms    | 35.2 ms    | 140.8 ms   | 281.6 ms    | 563.2 ms    | 1126.4 ms   | 2252.8 ms    |\n| 2116                 | 4.232 ms | 16.928 ms | 67.712 ms  | 270.848 ms | 541.696 ms  | 1083.392 ms | 2166.784 ms | 4333.568 ms  |\n| 4156                 | 8.312 ms | 33.248 ms | 132.992 ms | 531.968 ms | 1063.936 ms | 2127.872 ms | 4255.744 ms | 8511.488 ms  |\n| 8244                 | 16.488 ms| 65.952 ms | 263.808 ms | 1055.232 ms| 2110.464 ms | 4220.928 ms | 8441.856 ms | 16883.712 ms |\n\nIt is important to pick a combination that provides the needed balance between accuracy and precision while ensuring new readings within the `CheckInterval` setting. When `USERMOD_INA226_DEBUG` is defined, the info pane contains the expected time to make a reading, which can be seen in the table above.\n\nAs an example, if you want a new reading every 5 seconds (`CheckInterval`), a valid combination is `256 samples` and `4156 μs` which would provide new values every 2.1 seconds.\n\nThe picked values also slightly affect power usage. If the `CheckInterval` is set to more than 20 seconds, the INA226 is configured in `triggered` reading mode, where it only uses power as long as it's working. Then the conversion time and average samples counts determine how long the chip stays turned on every `CheckInterval` time.\n\n### Calculating Current and Power\n\nThe INA226 calculates current by measuring the differential voltage across a shunt resistor and using the calibration register value to convert this measurement into current. Power is calculated by multiplying the current by the bus voltage.\n\nFor detailed programming information and register configurations, refer to the [INA226 datasheet](https://www.ti.com/product/INA226).\n\n## Author\n[@LordMike](https://github.com/LordMike)\n\n## Compiling\n\nTo enable, compile with `INA226` in `custom_usermods` (e.g. in `platformio_override.ini`).\n\n```ini\n[env:ina226_example]\nextends = env:esp32dev\ncustom_usermods = ${env:esp32dev.custom_usermods} INA226\nbuild_flags = ${env:esp32dev.build_flags}\n  ; -D USERMOD_INA226_DEBUG ; -- add a debug status to the info modal\n```"
  },
  {
    "path": "usermods/INA226_v2/library.json",
    "content": "{\n  \"name\": \"INA226_v2\",\n  \"build\": { \"libArchive\": false },\n  \"dependencies\": {\n    \"wollewald/INA226_WE\":\"~1.2.9\"\n  }\n}\n"
  },
  {
    "path": "usermods/Internal_Temperature_v2/Internal_Temperature_v2.cpp",
    "content": "#include \"wled.h\"\n\nclass InternalTemperatureUsermod : public Usermod\n{\n\nprivate:\n  static constexpr unsigned long minLoopInterval = 1000;  // minimum allowable interval (ms)\n  unsigned long loopInterval = 10000;\n  unsigned long lastTime = 0;\n  bool isEnabled = false;\n  float temperature = 0.0f;\n  uint8_t previousPlaylist = 0;         // Stores the playlist that was active before high-temperature activation\n  uint8_t previousPreset = 0;           // Stores the preset that was active before high-temperature activation\n  uint8_t presetToActivate = 0;         // Preset to activate when temp goes above threshold (0 = disabled)\n  float activationThreshold = 95.0f;    // Temperature threshold to trigger high-temperature actions\n  float resetMargin = 2.0f;             // Margin below the activation threshold (Prevents frequent toggling when close to threshold)\n  bool isAboveThreshold = false;        // Flag to track if the high temperature preset is currently active\n\n  static const char _name[];\n  static const char _enabled[];\n  static const char _loopInterval[];\n  static const char _activationThreshold[];\n  static const char _presetToActivate[];\n\n  // any private methods should go here (non-inline method should be defined out of class)\n  void publishMqtt(const char *state, bool retain = false); // example for publishing MQTT message\n\npublic:\n  void setup()\n  {\n  }\n\n  void loop()\n  {\n    // if usermod is disabled or called during strip updating just exit\n    // NOTE: on very long strips strip.isUpdating() may always return true so update accordingly\n    if (!isEnabled || strip.isUpdating() || millis() - lastTime <= loopInterval)\n      return;\n\n    lastTime = millis();\n\n// Measure the temperature\n#ifdef ESP8266 // ESP8266\n    // does not seem possible\n    temperature = -1;\n#elif defined(CONFIG_IDF_TARGET_ESP32S2) // ESP32S2\n    temperature = -1;\n#else                                    // ESP32 ESP32S3 and ESP32C3\n    temperature = roundf(temperatureRead() * 10) / 10;\n#endif\n if(presetToActivate != 0){\n    // Check if temperature has exceeded the activation threshold\n    if (temperature >= activationThreshold) {\n      // Update the state flag if not already set\n      if (!isAboveThreshold) {\n        isAboveThreshold = true;\n        }\n      // Check if a 'high temperature' preset is configured and it's not already active\n      if (currentPreset != presetToActivate) {\n        // If a playlist is active, store it for reactivation later\n        if (currentPlaylist > 0) {\n          previousPlaylist = currentPlaylist;\n        }\n        // If a preset is active, store it for reactivation later\n        else if (currentPreset > 0) {\n          previousPreset = currentPreset;\n        // If no playlist or preset is active, save current state for reactivation later\n        } else {\n          saveTemporaryPreset();\n        }\n        // Activate the 'high temperature' preset\n        applyPreset(presetToActivate);\n        }\n      }\n    // Check if temperature is back below the threshold\n    else if (temperature <= (activationThreshold - resetMargin)) {\n      // Update the state flag if not already set\n      if (isAboveThreshold){\n        isAboveThreshold = false;\n        }\n      // Check if the 'high temperature' preset is active\n      if (currentPreset == presetToActivate) {\n        // Check if a previous playlist was stored\n        if (previousPlaylist > 0) {\n          // Reactivate the stored playlist\n          applyPreset(previousPlaylist);\n          // Clear the stored playlist\n          previousPlaylist = 0;\n          }\n        // Check if a previous preset was stored\n        else if (previousPreset > 0) {\n          // Reactivate the stored preset\n          applyPreset(previousPreset);\n          // Clear the stored preset\n          previousPreset = 0;\n          // If no previous playlist or preset was stored, revert to the stored state\n        } else {\n          applyTemporaryPreset();\n          }\n        }\n      }\n }\n\n#ifndef WLED_DISABLE_MQTT\n    if (WLED_MQTT_CONNECTED)\n    {\n      char array[10];\n      snprintf(array, sizeof(array), \"%f\", temperature);\n      publishMqtt(array);\n    }\n#endif\n  }\n\n  void addToJsonInfo(JsonObject &root)\n  {\n    if (!isEnabled)\n      return;\n\n    // if \"u\" object does not exist yet wee need to create it\n    JsonObject user = root[\"u\"];\n    if (user.isNull())\n      user = root.createNestedObject(\"u\");\n\n    JsonArray userTempArr = user.createNestedArray(FPSTR(_name));\n    userTempArr.add(temperature);\n    userTempArr.add(F(\" °C\"));\n\n    // if \"sensor\" object does not exist yet wee need to create it\n    JsonObject sensor = root[F(\"sensor\")];\n    if (sensor.isNull())\n      sensor = root.createNestedObject(F(\"sensor\"));\n\n    JsonArray sensorTempArr = sensor.createNestedArray(FPSTR(_name));\n    sensorTempArr.add(temperature);\n    sensorTempArr.add(F(\"°C\"));\n  }\n\n  void addToConfig(JsonObject &root)\n  {\n    JsonObject top = root.createNestedObject(FPSTR(_name));\n    top[FPSTR(_enabled)] = isEnabled;\n    top[FPSTR(_loopInterval)] = loopInterval;\n    top[FPSTR(_activationThreshold)] = activationThreshold;\n    top[FPSTR(_presetToActivate)] = presetToActivate;\n  }\n\n    // Append useful info to the usermod settings gui\n    void appendConfigData()\n    {\n    // Display 'ms' next to the 'Loop Interval' setting\n    oappend(F(\"addInfo('Internal Temperature:Loop Interval', 1, 'ms');\"));\n    // Display '°C' next to the 'Activation Threshold' setting\n    oappend(F(\"addInfo('Internal Temperature:Activation Threshold', 1, '°C');\"));\n    // Display '0 = Disabled' next to the 'Preset To Activate' setting\n    oappend(F(\"addInfo('Internal Temperature:Preset To Activate', 1, '0 = unused');\"));\n    }\n\n  bool readFromConfig(JsonObject &root)\n  {\n    JsonObject top = root[FPSTR(_name)];\n    bool configComplete = !top.isNull();\n    configComplete &= getJsonValue(top[FPSTR(_enabled)], isEnabled);\n    configComplete &= getJsonValue(top[FPSTR(_loopInterval)], loopInterval);\n    loopInterval = max(loopInterval, minLoopInterval);    // Makes sure the loop interval isn't too small.\n    configComplete &= getJsonValue(top[FPSTR(_presetToActivate)], presetToActivate);\n    configComplete &= getJsonValue(top[FPSTR(_activationThreshold)], activationThreshold);\n    return configComplete;\n  }\n\n  uint16_t getId()\n  {\n    return USERMOD_ID_INTERNAL_TEMPERATURE;\n  }\n};\n\nconst char InternalTemperatureUsermod::_name[] PROGMEM = \"Internal Temperature\";\nconst char InternalTemperatureUsermod::_enabled[] PROGMEM = \"Enabled\";\nconst char InternalTemperatureUsermod::_loopInterval[] PROGMEM = \"Loop Interval\";\nconst char InternalTemperatureUsermod::_activationThreshold[] PROGMEM = \"Activation Threshold\";\nconst char InternalTemperatureUsermod::_presetToActivate[] PROGMEM = \"Preset To Activate\";\n\nvoid InternalTemperatureUsermod::publishMqtt(const char *state, bool retain)\n{\n#ifndef WLED_DISABLE_MQTT\n  // Check if MQTT Connected, otherwise it will crash the 8266\n  if (WLED_MQTT_CONNECTED)\n  {\n    char subuf[64];\n    strcpy(subuf, mqttDeviceTopic);\n    strcat_P(subuf, PSTR(\"/mcutemp\"));\n    mqtt->publish(subuf, 0, retain, state);\n  }\n#endif\n}\n\nstatic InternalTemperatureUsermod internal_temperature_v2;\nREGISTER_USERMOD(internal_temperature_v2);"
  },
  {
    "path": "usermods/Internal_Temperature_v2/library.json",
    "content": "{\n  \"name\": \"Internal_Temperature_v2\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/Internal_Temperature_v2/readme.md",
    "content": "# Internal Temperature Usermod\n\n![Screenshot of WLED info page](assets/screenshot_info.png)\n\n![Screenshot of WLED usermod settings page](assets/screenshot_settings.png)\n\n\n## Features\n - 🌡️ Adds the internal temperature readout of the chip to the `Info` tab\n - 🥵 High temperature indicator/action. (Configurable threshold and preset)\n - 📣 Publishes the internal temperature over the MQTT topic: `mcutemp`\n\n\n## Use Examples\n- Warn of excessive/damaging temperatures by the triggering of a 'warning' preset\n- Activate a cooling fan (when used with the multi-relay usermod)\n\n\n## Compatibility\n- A shown temp of 53,33°C might indicate that the internal temp is not supported\n- ESP8266 does not have a internal temp sensor -> Disabled (Indicated with a readout of '-1')\n- ESP32S2 seems to crash on reading the sensor -> Disabled (Indicated with a readout of '-1')\n\n\n## Installation\n- Add `Internal_Temperature` to `custom_usermods` in your `platformio.ini` (or `platformio_override.ini`).\n\n## 📝 Change Log\n\n2024-06-26\n\n- Added \"high-temperature-indication\" feature\n- Documentation updated\n\n2023-09-01\n\n* \"Internal Temperature\" usermod created\n\n\n## Authors\n- Soeren Willrodt [@lost-hope](https://github.com/lost-hope)\n- Dimitry Zhemkov [@dima-zhemkov](https://github.com/dima-zhemkov)\n- Adam Matthews [@adamsthws](https://github.com/adamsthws)\n"
  },
  {
    "path": "usermods/JSON_IR_remote/21-key_ir.json",
    "content": "{\n  \"desc\": \"21-key\",\n  \"0xFFA25D\": {\n    \"label\": \"On\",\n    \"pos\": \"1x1\",\n    \"cmd\": \"T=1\"\n  },\n  \"0xFF629D\": {\n    \"label\": \"Off\",\n    \"pos\": \"1x2\",\n    \"cmd\": \"T=0\"\n  },\n  \"0xFFE21D\": {\n    \"label\": \"Flash\",\n    \"pos\": \"1x3\",\n    \"cmnt\": \"Cycle Effects\",\n    \"cmd\": \"CY=0&FX=~\"\n  },\n  \"0xFF22DD\": {\n    \"label\": \"Strobe\",\n    \"pos\": \"2x1\",\n    \"cmnt\": \"Sinelon Dual\",\n    \"cmd\": \"CY=0&FX=93\"\n  },\n  \"0xFF02FD\": {\n    \"label\": \"Fade\",\n    \"pos\": \"2x2\",\n    \"cmnt\": \"Rain\",\n    \"cmd\": \"CY=0&FX=43\"\n  },\n  \"0xFFC23D\": {\n    \"label\": \"Smooth\",\n    \"pos\": \"2x3\",\n    \"cmnt\": \"Aurora\",\n    \"cmd\": \"CY=0&FX=38\"\n  },\n  \"0xFFE01F\": {\n    \"label\": \"Bright +\",\n    \"pos\": \"3x1\",\n    \"cmd\": \"A=~16\"\n  },\n  \"0xFFA857\": {\n    \"label\": \"Bright -\",\n    \"pos\": \"3x2\",\n    \"cmd\": \"A=~-16\"\n  },\n  \"0xFF906F\": {\n    \"label\": \"White\",\n    \"pos\": \"3x3\",\n    \"cmd\": \"FP=5&CL=hFFFFFF&C2=hFFFFFF&C3=hA8A8A8\"\n  },\n  \"0xFF6897\": {\n    \"label\": \"Red\",\n    \"pos\": \"4x1\",\n    \"cmnt\": \"Lava\",\n    \"cmd\": \"FP=8\"\n  },\n  \"0xFF9867\": {\n    \"label\": \"Green\",\n    \"pos\": \"4x2\",\n    \"cmnt\": \"Forest\",\n    \"cmd\": \"FP=10\"\n  },\n  \"0xFFB04F\": {\n    \"label\": \"Blue\",\n    \"pos\": \"4x3\",\n    \"cmnt\": \"Breeze\",\n    \"cmd\": \"FP=15\"\n  },\n  \"0xFF30CF\": {\n    \"label\": \"Tomato\",\n    \"pos\": \"5x1\",\n    \"cmd\": \"FP=5&CL=hFF6347&C2=hFFBF47&C3=hA85859\"\n  },\n  \"0xFF18E7\": {\n    \"label\": \"LightGreen\",\n    \"pos\": \"5x2\",\n    \"cmnt\": \"Rivendale\",\n    \"cmd\": \"FP=14\"\n  },\n  \"0xFF7A85\": {\n    \"label\": \"SkyBlue\",\n    \"pos\": \"5x3\",\n    \"cmnt\": \"Ocean\",\n    \"cmd\": \"FP=9\"\n  },\n  \"0xFF10EF\": {\n    \"label\": \"Orange\",\n    \"pos\": \"6x1\",\n    \"cmnt\": \"Orangery\",\n    \"cmd\": \"FP=47\"\n  },\n  \"0xFF38C7\": {\n    \"label\": \"Aqua\",\n    \"pos\": \"6x2\",\n    \"cmd\": \"FP=5&CL=hFFFF&C2=h7FFF&C3=h39A895\"\n  },\n  \"0xFF5AA5\": {\n    \"label\": \"Purple\",\n    \"pos\": \"6x3\",\n    \"cmd\": \"FP=5&CL=h663399&C2=h993399&C3=h473864\"\n  },\n  \"0xFF42BD\": {\n    \"label\": \"Yellow\",\n    \"pos\": \"7x1\",\n    \"cmd\": \"FP=5&CL=hFFFF00&C2=hFFC800&C3=hFDFFDE\"\n  },\n  \"0xFF4AB5\": {\n    \"label\": \"Cyan\",\n    \"pos\": \"7x2\",\n    \"cmnt\": \"Beech\",\n    \"cmd\": \"FP=22\"\n  },\n  \"0xFF52AD\": {\n    \"label\": \"Pink\",\n    \"pos\": \"7x3\",\n    \"cmd\": \"FP=5&CL=hFFC0CB&C2=hFFD4C0&C3=hA88C96\"\n  }\n}"
  },
  {
    "path": "usermods/JSON_IR_remote/24-key_ir.json",
    "content": "{\n  \"desc\": \"24-key\",\n  \"0xF700FF\": {\n    \"label\": \"+\",\n    \"pos\": \"1x1\",\n    \"cmnt\": \"Speed +\",\n    \"cmd\": \"SX=~16\"\n  },\n  \"0xF7807F\": {\n    \"label\": \"-\",\n    \"pos\": \"1x2\",\n    \"cmnt\": \"Speed -\",\n    \"cmd\": \"SX=~-16\"\n  },\n  \"0xF740BF\": {\n    \"label\": \"On/Off\",\n    \"pos\": \"1x3\",\n    \"cmnt\": \"Toggle On/Off\",\n    \"cmd\": \"T=2\"\n  },\n  \"0xF7C03F\": {\n    \"label\": \"W\",\n    \"pos\": \"1x4\",\n    \"cmnt\": \"Cycle color palette\",\n    \"cmd\": \"FP=~\"\n  },\n  \"0xF720DF\": {\n    \"label\": \"R\",\n    \"pos\": \"2x1\",\n    \"cmnt\": \"Lava\",\n    \"cmd\": \"FP=8\"\n  },\n  \"0xF7A05F\": {\n    \"label\": \"G\",\n    \"pos\": \"2x2\",\n    \"cmnt\": \"Forest\",\n    \"cmd\": \"FP=10\"\n  },\n  \"0xF7609F\": {\n    \"label\": \"B\",\n    \"pos\": \"2x3\",\n    \"cmnt\": \"Breeze\",\n    \"cmd\": \"FP=15\"\n  },\n  \"0xF7E01F\": {\n    \"label\": \"Bright -\",\n    \"pos\": \"2x4\",\n    \"cmnt\": \"Bright -\",\n    \"cmd\": \"A=~-16\"\n  },\n  \"0xF710EF\": {\n    \"label\": \"Timer1H\",\n    \"pos\": \"3x1\",\n    \"cmnt\": \"Timer 60 min\",\n    \"cmd\": \"NL=60&NT=0\"\n  },\n  \"0xF7906F\": {\n    \"label\": \"Timer4H\",\n    \"pos\": \"3x2\",\n    \"cmnt\": \"Timer 30 min\",\n    \"cmd\": \"NL=30&NT=0\"\n  },\n  \"0xF750AF\": {\n    \"label\": \"Timer8H\",\n    \"pos\": \"3x3\",\n    \"cmnt\": \"Timer 15 min\",\n    \"cmd\": \"NL=15&NT=0\"\n  },\n  \"0xF7D02F\": {\n    \"label\": \"Bright128\",\n    \"pos\": \"3x4\",\n    \"cmnt\": \"Bright 128\",\n    \"cmd\": \"A=128\"\n  },\n  \"0xF730CF\": {\n    \"label\": \"Music1\",\n    \"pos\": \"4x1\",\n    \"cmnt\": \"Cycle FX +\",\n    \"cmd\": \"FX=~\"\n  },\n  \"0xF7B04F\": {\n    \"label\": \"Music2\",\n    \"pos\": \"4x2\",\n    \"cmnt\": \"Cycle FX -\",\n    \"cmd\": \"FX=~-1\"\n  },\n  \"0xF7708F\": {\n    \"label\": \"Music3\",\n    \"pos\": \"4x3\",\n    \"cmnt\": \"Reset FX and FP\",\n    \"cmd\": \"FX=1&PF=6\"\n  },\n  \"0xF7F00F\": {\n    \"label\": \"Bright +\",\n    \"pos\": \"4x4\",\n    \"cmnt\": \"Bright +\",\n    \"cmd\": \"A=~16\"\n  },\n  \"0xF708F7\": {\n    \"label\": \"Mode1\",\n    \"pos\": \"5x1\",\n    \"cmnt\": \"Preset 1\",\n    \"cmd\": \"PL=1\"\n  },\n  \"0xF78877\": {\n    \"label\": \"Mode2\",\n    \"pos\": \"5x2\",\n    \"cmnt\": \"Preset 2\",\n    \"cmd\": \"PL=2\"\n  },\n  \"0xF748B7\": {\n    \"label\": \"Mode3\",\n    \"pos\": \"5x3\",\n    \"cmnt\": \"Preset 3\",\n    \"cmd\": \"PL=3\"\n  },\n  \"0xF7C837\": {\n    \"label\": \"Up\",\n    \"pos\": \"5x4\",\n    \"cmnt\": \"Intensity +\",\n    \"cmd\": \"IX=~16\"\n  },\n  \"0xF728D7\": {\n    \"label\": \"Mode4\",\n    \"pos\": \"6x1\",\n    \"cmnt\": \"Preset 4\",\n    \"cmd\": \"PL=4\"\n  },\n  \"0xF7A857\": {\n    \"label\": \"Mode5\",\n    \"pos\": \"6x2\",\n    \"cmnt\": \"Preset 5\",\n    \"cmd\": \"PL=5\"\n  },\n  \"0xF76897\": {\n    \"label\": \"Cycle\",\n    \"pos\": \"6x3\",\n    \"cmnt\": \"Toggle preset cycle\",\n    \"cmd\": \"CY=1&PT=60000\"\n  },\n  \"0xF7E817\": {\n    \"label\": \"Down\",\n    \"pos\": \"6x4\",\n    \"cmnt\": \"Intensity -\",\n    \"cmd\": \"IX=~-16\"\n  }\n}"
  },
  {
    "path": "usermods/JSON_IR_remote/32-key_ir.json",
    "content": "{\n  \"desc\": \"32-key\",\n  \"0xFF08F7\": {\n    \"label\": \"On\",\n    \"pos\": \"1x1\",\n    \"cmd\": \"T=1\"\n  },\n  \"0xFFC03F\": {\n    \"label\": \"Off\",\n    \"pos\": \"1x2\",\n    \"cmd\": \"T=0\"\n  },\n  \"0xFF807F\": {\n    \"label\": \"Auto\",\n    \"pos\": \"1x3\",\n    \"cmnt\": \"Toggle preset cycle\",\n    \"cmd\": \"CY=2\"\n  },\n  \"0xFF609F\": {\n    \"label\": \"Mode\",\n    \"pos\": \"1x4\",\n    \"cmnt\": \"Cycle effects\",\n    \"cmd\": \"FX=~&CY=0\"\n  },\n  \"0xFF906F\": {\n    \"label\": \"4H\",\n    \"pos\": \"2x1\",\n    \"cmnt\": \"Timer 60min\",\n    \"cmd\": \"NL=60&NT=0\"\n  },\n  \"0xFFB847\": {\n    \"label\": \"6H\",\n    \"pos\": \"2x2\",\n    \"cmnt\": \"Timer 90min\",\n    \"cmd\": \"NL=90&NT=0\"\n  },\n  \"0xFFF807\": {\n    \"label\": \"8H\",\n    \"pos\": \"2x3\",\n    \"cmnt\": \"Timer 120min\",\n    \"cmd\": \"NL=120&NT=0\"\n  },\n  \"0xFFB04F\": {\n    \"label\": \"Timer Off\",\n    \"pos\": \"2x4\",\n    \"cmd\": \"NL=0\"\n  },\n  \"0xFF9867\": {\n    \"label\": \"Red\",\n    \"pos\": \"3x1\",\n    \"cmnt\": \"Lava\",\n    \"cmd\": \"FP=8\"\n  },\n  \"0xFFD827\": {\n    \"label\": \"Green\",\n    \"pos\": \"3x2\",\n    \"cmnt\": \"Forest\",\n    \"cmd\": \"FP=10\"\n  },\n  \"0xFF8877\": {\n    \"label\": \"Blue\",\n    \"pos\": \"3x3\",\n    \"cmnt\": \"Breeze\",\n    \"cmd\": \"FP=15\"\n  },\n  \"0xFFA857\": {\n    \"label\": \"White\",\n    \"pos\": \"3x4\",\n    \"cmd\": \"FP=5&CL=hFFFFFF&C2=hFFE4CD&C3=hE4E4FF\"\n  },\n  \"0xFFE817\": {\n    \"label\": \"OrangeRed\",\n    \"pos\": \"4x1\",\n    \"cmnt\": \"Sakura\",\n    \"cmd\": \"FP=49\"\n  },\n  \"0xFF48B7\": {\n    \"label\": \"SeaGreen\",\n    \"pos\": \"4x2\",\n    \"cmnt\": \"Rivendale\",\n    \"cmd\": \"FP=14\"\n  },\n  \"0xFF6897\": {\n    \"label\": \"RoyalBlue\",\n    \"pos\": \"4x3\",\n    \"cmnt\": \"Ocean\",\n    \"cmd\": \"FP=9\"\n  },\n  \"0xFFB24D\": {\n    \"label\": \"DarkBlue\",\n    \"pos\": \"4x4\",\n    \"cmnt\": \"Breeze\",\n    \"cmd\": \"FP=15\"\n  },\n  \"0xFF02FD\": {\n    \"label\": \"Orange\",\n    \"pos\": \"5x1\",\n    \"cmnt\": \"Orangery\",\n    \"cmd\": \"FP=47\"\n  },\n  \"0xFF32CD\": {\n    \"label\": \"YellowGreen\",\n    \"pos\": \"5x2\",\n    \"cmnt\": \"Aurora\",\n    \"cmd\": \"FP=37\"\n  },\n  \"0xFF20DF\": {\n    \"label\": \"SkyBlue\",\n    \"pos\": \"5x3\",\n    \"cmnt\": \"Beech\",\n    \"cmd\": \"FP=22\"\n  },\n  \"0xFF00FF\": {\n    \"label\": \"Orchid\",\n    \"pos\": \"5x4\",\n    \"cmd\": \"FP=5&CL=hDA70D6&C2=hDA70A0&C3=h89618F\"\n  },\n  \"0xFF50AF\": {\n    \"label\": \"Yellow\",\n    \"pos\": \"6x1\",\n    \"cmd\": \"FP=5&CL=hFFFF00&C2=hFFC800&C3=hFDFFDE\"\n  },\n  \"0xFF7887\": {\n    \"label\": \"DarkGreen\",\n    \"pos\": \"6x2\",\n    \"cmnt\": \"Orange and Teal\",\n    \"cmd\": \"FP=44\"\n  },\n  \"0xFF708F\": {\n    \"label\": \"RebeccaPurple\",\n    \"pos\": \"6x3\",\n    \"cmd\": \"FP=5&CL=h800080&C2=h800040&C3=h4B1C54\"\n  },\n  \"0xFF58A7\": {\n    \"label\": \"Plum\",\n    \"pos\": \"6x4\",\n    \"cmd\": \"FP=5&CL=hDDA0DD&C2=hDDA0BE&C3=h8D7791\"\n  },\n  \"0xFF38C7\": {\n    \"label\": \"Strobe\",\n    \"pos\": \"7x1\",\n    \"cmnt\": \"Dancing Shadows\",\n    \"cmd\": \"FX=112&CY=0\"\n  },\n  \"0xFF28D7\": {\n    \"label\": \"In Waves\",\n    \"pos\": \"7x2\",\n    \"cmnt\": \"Noise 1\",\n    \"cmd\": \"FX=70&CY=0\"\n  },\n  \"0xFFF00F\": {\n    \"label\": \"Speed +\",\n    \"pos\": \"7x3\",\n    \"cmd\": \"SX=~16\"\n  },\n  \"0xFF30CF\": {\n    \"label\": \"Speed -\",\n    \"pos\": \"7x4\",\n    \"cmd\": \"SX=~-16\"\n  },\n  \"0xFF40BF\": {\n    \"label\": \"Jump\",\n    \"pos\": \"8x1\",\n    \"cmnt\": \"Colortwinkles\",\n    \"cmd\": \"FX=74&CY=0\"\n  },\n  \"0xFF12ED\": {\n    \"label\": \"Fade\",\n    \"pos\": \"8x2\",\n    \"cmnt\": \"Sunrise\",\n    \"cmd\": \"FX=104&CY=0\"\n  },\n  \"0xFF2AD5\": {\n    \"label\": \"Flash\",\n    \"pos\": \"8x3\",\n    \"cmnt\": \"Railway\",\n    \"cmd\": \"FX=78&CY=0\"\n  },\n  \"0xFFA05F\": {\n    \"label\": \"Chase Flash\",\n    \"pos\": \"8x4\",\n    \"cmnt\": \"Washing Machine\",\n    \"cmd\": \"FX=113&CY=0\"\n  }\n}"
  },
  {
    "path": "usermods/JSON_IR_remote/40-key-black_ir.json",
    "content": "{\n  \"desc\": \"40-key-black\",\n  \"0xFF3AC5\": {\n    \"label\": \"Bright +\",\n    \"pos\": \"1x1\",\n    \"cmd\": \"A=~16\"\n  },\n  \"0xFFBA45\": {\n    \"label\": \"Bright -\",\n    \"pos\": \"1x2\",\n    \"cmd\": \"A=~-16\"\n  },\n  \"0xFF827D\": {\n    \"label\": \"Off\",\n    \"pos\": \"1x3\",\n    \"cmd\": \"T=0\"\n  },\n  \"0xFF02FD\": {\n    \"label\": \"On\",\n    \"pos\": \"1x4\",\n    \"cmd\": \"T=1\"\n  },\n  \"0xFF1AE5\": {\n    \"label\": \"Red\",\n    \"pos\": \"2x1\",\n    \"cmnt\": \"Lava\",\n    \"cmd\": \"FP=8\"\n  },\n  \"0xFF9A65\": {\n    \"label\": \"Green\",\n    \"pos\": \"2x2\",\n    \"cmnt\": \"Forest\",\n    \"cmd\": \"FP=10\"\n  },\n  \"0xFFA25D\": {\n    \"label\": \"Blue\",\n    \"pos\": \"2x3\",\n    \"cmnt\": \"Breeze\",\n    \"cmd\": \"FP=15\"\n  },\n  \"0xFF22DD\": {\n    \"label\": \"White\",\n    \"pos\": \"2x4\",\n    \"cmd\": \"FP=5&CL=hFFFFFF&C2=hFFFFFF&C3=hA8A8A8\"\n  },\n  \"0xFF2AD5\": {\n    \"label\": \"Tomato\",\n    \"pos\": \"3x1\",\n    \"cmnt\": \"Yelmag\",\n    \"cmd\": \"FP=5&CL=hFF6347&C2=hFFBF47&C3=hA85859\"\n  },\n  \"0xFFAA55\": {\n    \"label\": \"LightGreen\",\n    \"pos\": \"3x2\",\n    \"cmnt\": \"Rivendale\",\n    \"cmd\": \"FP=14\"\n  },\n  \"0xFF926D\": {\n    \"label\": \"SkyBlue\",\n    \"pos\": \"3x3\",\n    \"cmnt\": \"Ocean\",\n    \"cmd\": \"FP=9\"\n  },\n  \"0xFF12ED\": {\n    \"label\": \"WarmWhite\",\n    \"pos\": \"3x4\",\n    \"cmnt\": \"Warm White\",\n    \"cmd\": \"FP=5&CL=hFFE4CD&C2=hFFFCCD&C3=hA89892\"\n  },\n  \"0xFF0AF5\": {\n    \"label\": \"OrangeRed\",\n    \"pos\": \"4x1\",\n    \"cmnt\": \"Sakura\",\n    \"cmd\": \"FP=49\"\n  },\n  \"0xFF8A75\": {\n    \"label\": \"Cyan\",\n    \"pos\": \"4x2\",\n    \"cmnt\": \"Beech\",\n    \"cmd\": \"FP=22\"\n  },\n  \"0xFFB24D\": {\n    \"label\": \"RebeccaPurple\",\n    \"pos\": \"4x3\",\n    \"cmd\": \"FP=5&CL=h663399&C2=h993399&C3=h473864\"\n  },\n  \"0xFF32CD\": {\n    \"label\": \"CoolWhite\",\n    \"pos\": \"4x4\",\n    \"cmnt\": \"Cool White\",\n    \"cmd\": \"FP=5&CL=hE4E4FF&C2=hF1E4FF&C3=h9C9EA8\"\n  },\n  \"0xFF38C7\": {\n    \"label\": \"Orange\",\n    \"pos\": \"5x1\",\n    \"cmnt\": \"Orangery\",\n    \"cmd\": \"FP=47\"\n  },\n  \"0xFFB847\": {\n    \"label\": \"Turquoise\",\n    \"pos\": \"5x2\",\n    \"cmd\": \"FP=5&CL=h40E0D0&C2=h40A0E0&C3=h4E9381\"\n  },\n  \"0xFF7887\": {\n    \"label\": \"Purple\",\n    \"pos\": \"5x3\",\n    \"cmd\": \"FP=5&CL=h800080&C2=h800040&C3=h4B1C54\"\n  },\n  \"0xFFF807\": {\n    \"label\": \"MedGray\",\n    \"pos\": \"5x4\",\n    \"cmnt\": \"Cycle palette +\",\n    \"cmd\": \"FP=~\"\n  },\n  \"0xFF18E7\": {\n    \"label\": \"Yellow\",\n    \"pos\": \"6x1\",\n    \"cmd\": \"FP=5&CL=hFFFF00&C2=h7FFF00&C3=hA89539\"\n  },\n  \"0xFF9867\": {\n    \"label\": \"DarkCyan\",\n    \"pos\": \"6x2\",\n    \"cmd\": \"FP=5&CL=h8B8B&C2=h458B&C3=h1F5B51\"\n  },\n  \"0xFF58A7\": {\n    \"label\": \"Plum\",\n    \"pos\": \"6x3\",\n    \"cmnt\": \"Magenta\",\n    \"cmd\": \"FP=40\"\n  },\n  \"0xFFD827\": {\n    \"label\": \"DarkGray\",\n    \"pos\": \"6x4\",\n    \"cmnt\": \"Cycle palette -\",\n    \"cmd\": \"FP=~-\"\n  },\n  \"0xFF28D7\": {\n    \"label\": \"Jump3\",\n    \"pos\": \"7x1\",\n    \"cmnt\": \"Colortwinkles\",\n    \"cmd\": \"CY=0&FX=74\"\n  },\n  \"0xFFA857\": {\n    \"label\": \"Fade3\",\n    \"pos\": \"7x2\",\n    \"cmnt\": \"Rain\",\n    \"cmd\": \"CY=0&FX=43\"\n  },\n  \"0xFF6897\": {\n    \"label\": \"Flash\",\n    \"pos\": \"7x3\",\n    \"cmnt\": \"Cycle Effects\",\n    \"cmd\": \"CY=0&FX=~\"\n  },\n  \"0xFFE817\": {\n    \"label\": \"Quick\",\n    \"pos\": \"7x4\",\n    \"cmnt\": \"Fx speed +16\",\n    \"cmd\": \"SX=~16\"\n  },\n  \"0xFF08F7\": {\n    \"label\": \"Jump7\",\n    \"pos\": \"8x1\",\n    \"cmnt\": \"Sinelon Dual\",\n    \"cmd\": \"CY=0&FX=93\"\n  },\n  \"0xFF8877\": {\n    \"label\": \"Fade7\",\n    \"pos\": \"8x2\",\n    \"cmnt\": \"Lighthouse\",\n    \"cmd\": \"CY=0&FX=41\"\n  },\n  \"0xFF48B7\": {\n    \"label\": \"Auto\",\n    \"pos\": \"8x3\",\n    \"cmnt\": \"Toggle preset cycle\",\n    \"cmd\": \"CY=2\"\n  },\n  \"0xFFC837\": {\n    \"label\": \"Slow\",\n    \"pos\": \"8x4\",\n    \"cmnt\": \"FX speed -16\",\n    \"cmd\": \"SX=~-16\"\n  },\n  \"0xFF30CF\": {\n    \"label\": \"Custom1\",\n    \"pos\": \"9x1\",\n    \"cmnt\": \"Noise 1\",\n    \"cmd\": \"CY=0&FX=70\"\n  },\n  \"0xFFB04F\": {\n    \"label\": \"Custom2\",\n    \"pos\": \"9x2\",\n    \"cmnt\": \"Dancing Shadows\",\n    \"cmd\": \"CY=0&FX=112\"\n  },\n  \"0xFF708F\": {\n    \"label\": \"Music +\",\n    \"pos\": \"9x3\",\n    \"cmnt\": \"FX Intensity +16\",\n    \"cmd\": \"IX=~16\"\n  },\n  \"0xFFF00F\": {\n    \"label\": \"Timer60\",\n    \"pos\": \"9x4\",\n    \"cmnt\": \"Timer 60 min\",\n    \"cmd\": \"NL=60&NT=0\"\n  },\n  \"0xFF10EF\": {\n    \"label\": \"Custom3\",\n    \"pos\": \"10x1\",\n    \"cmnt\": \"Twinklefox\",\n    \"cmd\": \"CY=0&FX=80\"\n  },\n  \"0xFF906F\": {\n    \"label\": \"Custom4\",\n    \"pos\": \"10x2\",\n    \"cmnt\": \"Twinklecat\",\n    \"cmd\": \"CY=0&FX=81\"\n  },\n  \"0xFF50AF\": {\n    \"label\": \"Music -\",\n    \"pos\": \"10x3\",\n    \"cmnt\": \"FX Intesity -16\",\n    \"cmd\": \"IX=~-16\"\n  },\n  \"0xFFD02F\": {\n    \"label\": \"Timer120\",\n    \"pos\": \"10x4\",\n    \"cmnt\": \"Timer 120 min\",\n    \"cmd\": \"NL=120&NT=0\"\n  }\n}"
  },
  {
    "path": "usermods/JSON_IR_remote/40-key-blue_ir.json",
    "content": "{\n  \"desc\": \"40-key-blue\",\n  \"0xFF3AC5\": {\n    \"label\": \"Bright +\",\n    \"pos\": \"1x1\",\n    \"cmd\": \"A=~16\"\n  },\n  \"0xFFBA45\": {\n    \"label\": \"Bright -\",\n    \"pos\": \"1x2\",\n    \"cmd\": \"A=~-16\"\n  },\n  \"0xFF827D\": {\n    \"label\": \"Off\",\n    \"pos\": \"1x3\",\n    \"cmd\": \"T=0\"\n  },\n  \"0xFF02FD\": {\n    \"label\": \"On\",\n    \"pos\": \"1x4\",\n    \"cmd\": \"T=1\"\n  },\n  \"0xFF1AE5\": {\n    \"label\": \"Red\",\n    \"pos\": \"2x1\",\n    \"cmnt\": \"Lava\",\n    \"cmd\": \"FP=8\"\n  },\n  \"0xFF9A65\": {\n    \"label\": \"Green\",\n    \"pos\": \"2x2\",\n    \"cmnt\": \"Forest\",\n    \"cmd\": \"FP=10\"\n  },\n  \"0xFFA25D\": {\n    \"label\": \"Blue\",\n    \"pos\": \"2x3\",\n    \"cmnt\": \"Breeze\",\n    \"cmd\": \"FP=15\"\n  },\n  \"0xFF22DD\": {\n    \"label\": \"White\",\n    \"pos\": \"2x4\",\n    \"cmd\": \"FP=5&CL=hFFFFFF&C2=hFFFFFF&C3=hA8A8A8\"\n  },\n  \"0xFF2AD5\": {\n    \"label\": \"Tomato\",\n    \"pos\": \"3x1\",\n    \"cmnt\": \"Yelmag\",\n    \"cmd\": \"FP=5&CL=hFF6347&C2=hFFBF47&C3=hA85859\"\n  },\n  \"0xFFAA55\": {\n    \"label\": \"LightGreen\",\n    \"pos\": \"3x2\",\n    \"cmnt\": \"Rivendale\",\n    \"cmd\": \"FP=14\"\n  },\n  \"0xFF926D\": {\n    \"label\": \"SkyBlue\",\n    \"pos\": \"3x3\",\n    \"cmnt\": \"Ocean\",\n    \"cmd\": \"FP=9\"\n  },\n  \"0xFF12ED\": {\n    \"label\": \"WarmWhite\",\n    \"pos\": \"3x4\",\n    \"cmnt\": \"Warm White\",\n    \"cmd\": \"FP=5&CL=hFFE4CD&C2=hFFFCCD&C3=hA89892\"\n  },\n  \"0xFF0AF5\": {\n    \"label\": \"OrangeRed\",\n    \"pos\": \"4x1\",\n    \"cmnt\": \"Sakura\",\n    \"cmd\": \"FP=49\"\n  },\n  \"0xFF8A75\": {\n    \"label\": \"Cyan\",\n    \"pos\": \"4x2\",\n    \"cmnt\": \"Beech\",\n    \"cmd\": \"FP=22\"\n  },\n  \"0xFFB24D\": {\n    \"label\": \"RebeccaPurple\",\n    \"pos\": \"4x3\",\n    \"cmd\": \"FP=5&CL=h663399&C2=h993399&C3=h473864\"\n  },\n  \"0xFF32CD\": {\n    \"label\": \"CoolWhite\",\n    \"pos\": \"4x4\",\n    \"cmnt\": \"Cool White\",\n    \"cmd\": \"FP=5&CL=hE4E4FF&C2=hF1E4FF&C3=h9C9EA8\"\n  },\n  \"0xFF38C7\": {\n    \"label\": \"Orange\",\n    \"pos\": \"5x1\",\n    \"cmnt\": \"Orangery\",\n    \"cmd\": \"FP=47\"\n  },\n  \"0xFFB847\": {\n    \"label\": \"Turquoise\",\n    \"pos\": \"5x2\",\n    \"cmd\": \"FP=5&CL=h40E0D0&C2=h40A0E0&C3=h4E9381\"\n  },\n  \"0xFF7887\": {\n    \"label\": \"Purple\",\n    \"pos\": \"5x3\",\n    \"cmd\": \"FP=5&CL=h800080&C2=h800040&C3=h4B1C54\"\n  },\n  \"0xFFF807\": {\n    \"label\": \"MedGray\",\n    \"pos\": \"5x4\",\n    \"cmnt\": \"Cycle palette +\",\n    \"cmd\": \"FP=~\"\n  },\n  \"0xFF18E7\": {\n    \"label\": \"Yellow\",\n    \"pos\": \"6x1\",\n    \"cmd\": \"FP=5&CL=hFFFF00&C2=h7FFF00&C3=hA89539\"\n  },\n  \"0xFF9867\": {\n    \"label\": \"DarkCyan\",\n    \"pos\": \"6x2\",\n    \"cmd\": \"FP=5&CL=h8B8B&C2=h458B&C3=h1F5B51\"\n  },\n  \"0xFF58A7\": {\n    \"label\": \"Plum\",\n    \"pos\": \"6x3\",\n    \"cmnt\": \"Magenta\",\n    \"cmd\": \"FP=40\"\n  },\n  \"0xFFD827\": {\n    \"label\": \"DarkGray\",\n    \"pos\": \"6x4\",\n    \"cmnt\": \"Cycle palette -\",\n    \"cmd\": \"FP=~-\"\n  },\n  \"0xFF28D7\": {\n    \"label\": \"W +\",\n    \"pos\": \"7x1\"\n  },\n  \"0xFFA857\": {\n    \"label\": \"W -\",\n    \"pos\": \"7x2\"\n  },\n  \"0xFF6897\": {\n    \"label\": \"W On\",\n    \"pos\": \"7x3\"\n  },\n  \"0xFFE817\": {\n    \"label\": \"W Off\",\n    \"pos\": \"7x4\"\n  },\n  \"0xFF08F7\": {\n    \"label\": \"W25\",\n    \"pos\": \"8x1\"\n  },\n  \"0xFF8877\": {\n    \"label\": \"W50\",\n    \"pos\": \"8x2\"\n  },\n  \"0xFF48B7\": {\n    \"label\": \"W75\",\n    \"pos\": \"8x3\"\n  },\n  \"0xFFC837\": {\n    \"label\": \"W100\",\n    \"pos\": \"8x4\"\n  },\n  \"0xFF30CF\": {\n    \"label\": \"Jump3\",\n    \"pos\": \"9x1\",\n    \"cmnt\": \"Colortwinkles\",\n    \"cmd\": \"CY=0&FX=74\"\n  },\n  \"0xFFB04F\": {\n    \"label\": \"Fade3\",\n    \"pos\": \"9x2\",\n    \"cmnt\": \"Rain\",\n    \"cmd\": \"CY=0&FX=43\"\n  },\n  \"0xFF708F\": {\n    \"label\": \"Jump7\",\n    \"pos\": \"9x3\",\n    \"cmnt\": \"Sinelon Dual\",\n    \"cmd\": \"CY=0&FX=93\"\n  },\n  \"0xFFF00F\": {\n    \"label\": \"Quick\",\n    \"pos\": \"9x4\",\n    \"cmnt\": \"Fx speed +16\",\n    \"cmd\": \"SX=~16\"\n  },\n  \"0xFF10EF\": {\n    \"label\": \"Fade\",\n    \"pos\": \"10x1\",\n    \"cmnt\": \"Lighthouse\",\n    \"cmd\": \"CY=0&FX=41\"\n  },\n  \"0xFF906F\": {\n    \"label\": \"Flash\",\n    \"pos\": \"10x2\",\n    \"cmnt\": \"Cycle Effects\",\n    \"cmd\": \"CY=0&FX=~\"\n  },\n  \"0xFF50AF\": {\n    \"label\": \"Auto\",\n    \"pos\": \"10x3\",\n    \"cmnt\": \"Toggle preset cycle\",\n    \"cmd\": \"CY=2\"\n  },\n  \"0xFFD02F\": {\n    \"label\": \"Slow\",\n    \"pos\": \"10x4\",\n    \"cmnt\": \"Sinelon Dual\",\n    \"cmd\": \"CY=0&FX=93\"\n  }\n}"
  },
  {
    "path": "usermods/JSON_IR_remote/44-key_ir.json",
    "content": "{\n  \"desc\": \"44-key\",\n  \"0xFF3AC5\": {\n    \"label\": \"Bright +\",\n    \"pos\": \"1x1\",\n    \"cmd\": \"A=~16\"\n  },\n  \"0xFFBA45\": {\n    \"label\": \"Bright -\",\n    \"pos\": \"1x2\",\n    \"cmd\": \"A=~-16\"\n  },\n  \"0xFF827D\": {\n    \"label\": \"Off\",\n    \"pos\": \"1x3\",\n    \"cmd\": \"T=0\"\n  },\n  \"0xFF02FD\": {\n    \"label\": \"On\",\n    \"pos\": \"1x4\",\n    \"cmd\": \"T=1\"\n  },\n  \"0xFF1AE5\": {\n    \"label\": \"Red\",\n    \"pos\": \"2x1\",\n    \"cmnt\": \"Lava\",\n    \"cmd\": \"FP=8\"\n  },\n  \"0xFF9A65\": {\n    \"label\": \"Green\",\n    \"pos\": \"2x2\",\n    \"cmnt\": \"Forest\",\n    \"cmd\": \"FP=10\"\n  },\n  \"0xFFA25D\": {\n    \"label\": \"Blue\",\n    \"pos\": \"2x3\",\n    \"cmnt\": \"Breeze\",\n    \"cmd\": \"FP=15\"\n  },\n  \"0xFF22DD\": {\n    \"label\": \"White\",\n    \"pos\": \"2x4\",\n    \"cmd\": \"FP=5&CL=hFFFFFF&C2=hFFFFFF&C3=hA8A8A8\"\n  },\n  \"0xFF2AD5\": {\n    \"label\": \"Tomato\",\n    \"pos\": \"3x1\",\n    \"cmd\": \"FP=5&CL=hFF6347&C2=hFFBF47&C3=hA85859\"\n  },\n  \"0xFFAA55\": {\n    \"label\": \"LightGreen\",\n    \"pos\": \"3x2\",\n    \"cmnt\": \"Rivendale\",\n    \"cmd\": \"FP=14\"\n  },\n  \"0xFF926D\": {\n    \"label\": \"DeepBlue\",\n    \"pos\": \"3x3\",\n    \"cmnt\": \"Ocean\",\n    \"cmd\": \"FP=9\"\n  },\n  \"0xFF12ED\": {\n    \"label\": \"Warmwhite2\",\n    \"pos\": \"3x4\",\n    \"cmnt\": \"Warm White\",\n    \"cmd\": \"FP=5&CL=hFFE4CD&C2=hFFFCCD&C3=hA89892\"\n  },\n  \"0xFF0AF5\": {\n    \"label\": \"Orange\",\n    \"pos\": \"4x1\",\n    \"cmnt\": \"Sakura\",\n    \"cmd\": \"FP=49\"\n  },\n  \"0xFF8A75\": {\n    \"label\": \"Turquoise\",\n    \"pos\": \"4x2\",\n    \"cmnt\": \"Beech\",\n    \"cmd\": \"FP=22\"\n  },\n  \"0xFFB24D\": {\n    \"label\": \"Purple\",\n    \"pos\": \"4x3\",\n    \"cmd\": \"FP=5&CL=h663399&C2=h993399&C3=h473864\"\n  },\n  \"0xFF32CD\": {\n    \"label\": \"WarmWhite\",\n    \"pos\": \"4x4\",\n    \"cmd\": \"FP=5&CL=hE4E4FF&C2=hF1E4FF&C3=h9C9EA8\"\n  },\n  \"0xFF38C7\": {\n    \"label\": \"Yellowish\",\n    \"pos\": \"5x1\",\n    \"cmnt\": \"Orangery\",\n    \"cmd\": \"FP=47\"\n  },\n  \"0xFFB847\": {\n    \"label\": \"Cyan\",\n    \"pos\": \"5x2\",\n    \"cmnt\": \"Beech\",\n    \"cmd\": \"FP=22\"\n  },\n  \"0xFF7887\": {\n    \"label\": \"Magenta\",\n    \"pos\": \"5x3\",\n    \"cmd\": \"FP=5&CL=hFF00FF&C2=hFF007F&C3=h9539A8\"\n  },\n  \"0xFFF807\": {\n    \"label\": \"ColdWhite\",\n    \"pos\": \"5x4\",\n    \"cmd\": \"FP=5&CL=hE4E4FF&C2=hF1E4FF&C3=h9C9EA8\"\n  },\n  \"0xFF18E7\": {\n    \"label\": \"Yellow\",\n    \"pos\": \"6x1\",\n    \"cmd\": \"FP=5&CL=hFFFF00&C2=hFFC800&C3=hFDFFDE\"\n  },\n  \"0xFF9867\": {\n    \"label\": \"Aqua\",\n    \"pos\": \"6x2\",\n    \"cmd\": \"FP=5&CL=hFFFF&C2=h7FFF&C3=h39A895\"\n  },\n  \"0xFF58A7\": {\n    \"label\": \"Pink\",\n    \"pos\": \"6x3\",\n    \"cmd\": \"FP=5&CL=hFFC0CB&C2=hFFD4C0&C3=hA88C96\"\n  },\n  \"0xFFD827\": {\n    \"label\": \"ColdWhite2\",\n    \"pos\": \"6x4\",\n    \"cmd\": \"FP=5&CL=hE4E4FF&C2=hF1E4FF&C3=h9C9EA8\"\n  },\n  \"0xFF28D7\": {\n    \"label\": \"Red +\",\n    \"pos\": \"7x1\",\n    \"cmd\": \"FP=5&R=~16\"\n  },\n  \"0xFFA857\": {\n    \"label\": \"Green +\",\n    \"pos\": \"7x2\",\n    \"cmd\": \"FP=5&G=~16\"\n  },\n  \"0xFF6897\": {\n    \"label\": \"Blue +\",\n    \"pos\": \"7x3\",\n    \"cmd\": \"FP=5&B=~16\"\n  },\n  \"0xFFE817\": {\n    \"label\": \"Quick\",\n    \"pos\": \"7x4\",\n    \"cmnt\": \"Fx speed +16\",\n    \"cmd\": \"SX=~16\"\n  },\n  \"0xFF08F7\": {\n    \"label\": \"Red -\",\n    \"pos\": \"8x1\",\n    \"cmd\": \"FP=5&R=~-16\"\n  },\n  \"0xFF8877\": {\n    \"label\": \"Green -\",\n    \"pos\": \"8x2\",\n    \"cmd\": \"FP=5&G=~-16\"\n  },\n  \"0xFF48B7\": {\n    \"label\": \"Blue -\",\n    \"pos\": \"8x3\",\n    \"cmd\": \"FP=5&B=~-16\"\n  },\n  \"0xFFC837\": {\n    \"label\": \"Slow\",\n    \"pos\": \"8x4\",\n    \"cmnt\": \"FX speed -16\",\n    \"cmd\": \"SX=~-16\"\n  },\n  \"0xFF30CF\": {\n    \"label\": \"Diy1\",\n    \"pos\": \"9x1\",\n    \"cmd\": \"CY=0&PL=1\"\n  },\n  \"0xFFB04F\": {\n    \"label\": \"Diy2\",\n    \"pos\": \"9x2\",\n    \"cmd\": \"CY=0&PL=2\"\n  },\n  \"0xFF708F\": {\n    \"label\": \"Diy3\",\n    \"pos\": \"9x3\",\n    \"cmd\": \"CY=0&PL=3\"\n  },\n  \"0xFFF00F\": {\n    \"label\": \"Auto\",\n    \"pos\": \"9x4\",\n    \"cmnt\": \"Toggle preset cycle\",\n    \"cmd\": \"CY=2\"\n  },\n  \"0xFF10EF\": {\n    \"label\": \"Diy4\",\n    \"pos\": \"10x1\",\n    \"cmd\": \"CY=0&PL=4\"\n  },\n  \"0xFF906F\": {\n    \"label\": \"Diy5\",\n    \"pos\": \"10x2\",\n    \"cmd\": \"CY=0&PL=5\"\n  },\n  \"0xFF50AF\": {\n    \"label\": \"Diy6\",\n    \"pos\": \"10x3\",\n    \"cmd\": \"CY=0&PL=6\"\n  },\n  \"0xFFD02F\": {\n    \"label\": \"Flash\",\n    \"pos\": \"10x4\",\n    \"cmnt\": \"Cycle Effects\",\n    \"cmd\": \"CY=0&FX=~\"\n  },\n  \"0xFF20DF\": {\n    \"label\": \"Jump3\",\n    \"pos\": \"11x1\",\n    \"cmnt\": \"Colortwinkles\",\n    \"cmd\": \"CY=0&FX=74\"\n  },\n  \"0xFFA05F\": {\n    \"label\": \"Jump7\",\n    \"pos\": \"11x2\",\n    \"cmnt\": \"Sinelon Dual\",\n    \"cmd\": \"CY=0&FX=93\"\n  },\n  \"0xFF609F\": {\n    \"label\": \"Fade3\",\n    \"pos\": \"11x3\",\n    \"cmnt\": \"Rain\",\n    \"cmd\": \"CY=0&FX=43\"\n  },\n  \"0xFFE01F\": {\n    \"label\": \"Fade7\",\n    \"pos\": \"11x4\",\n    \"cmnt\": \"Lighthouse\",\n    \"cmd\": \"CY=0&FX=41\"\n  }\n}"
  },
  {
    "path": "usermods/JSON_IR_remote/6-key_ir.json",
    "content": "{\n  \"desc\": \"6-key\",\n  \"0xFF0FF0\": {\n    \"label\": \"Power\",\n    \"pos\": \"1x1\",\n    \"cmd\": \"T=2\"\n  },\n  \"0xFF8F70\": {\n    \"label\": \"Channel +\",\n    \"pos\": \"2x1\",\n    \"cmnt\": \"Cycle palette up\",\n    \"cmd\": \"FP=~\"\n  },\n  \"0xFF4FB0\": {\n    \"label\": \"Channel -\",\n    \"pos\": \"3x1\",\n    \"cmnt\": \"Cycle palette down\",\n    \"cmd\": \"FP=~-\"\n  },\n  \"0xFFCF30\": {\n    \"label\": \"Volume +\",\n    \"pos\": \"4x1\",\n    \"cmnt\": \"Brighten\",\n    \"cmd\": \"A=~16\"\n  },\n  \"0xFF2FD0\": {\n    \"label\": \"Volume -\",\n    \"pos\": \"5x1\",\n    \"cmnt\": \"Dim\",\n    \"cmd\": \"A=~-16\"\n  },\n  \"0xFFAF50\": {\n    \"label\": \"Mute\",\n    \"pos\": \"6x1\",\n    \"cmnt\": \"Cycle effects\",\n    \"cmd\": \"CY=0&FX=~\"\n  }\n}"
  },
  {
    "path": "usermods/JSON_IR_remote/9-key_ir.json",
    "content": "{\n  \"desc\": \"9-key\",\n  \"0xFF629D\": {\n    \"label\": \"Power\",\n    \"cmd\": \"T=2\"\n  },\n  \"0xFF22DD\": {\n    \"label\": \"A\",\n    \"cmnt\": \"Preset 1\",\n    \"cmd\": \"PL=1\"\n  },\n  \"0xFF02FD\": {\n    \"label\": \"B\",\n    \"cmnt\": \"Preset 2\",\n    \"cmd\": \"PL=2\"\n  },\n  \"0xFFC23D\": {\n    \"label\": \"C\",\n    \"cmnt\": \"Preset 3\",\n    \"cmd\": \"PL=3\"\n  },\n  \"0xFF30CF\": {\n    \"label\": \"Left\",\n    \"cmnt\": \"Speed -\",\n    \"cmd\": \"SI=~-16\"\n  },\n  \"0xFF7A85\": {\n    \"label\": \"Right\",\n    \"cmnt\": \"Speed +\",\n    \"cmd\": \"SI=~16\"\n  },\n  \"0xFF9867\": {\n    \"label\": \"Up\",\n    \"cmnt\": \"Bright +\",\n    \"cmd\": \"A=~16\"\n  },\n  \"0xFF38C7\": {\n    \"label\": \"Down\",\n    \"cmnt\": \"Bright -\",\n    \"cmd\": \"A=~-16\"\n  },\n  \"0xFF18E7\": {\n    \"label\": \"Select\",\n    \"cmnt\": \"Cycle effects\",\n    \"cmd\": \"CY=0&FX=~\"\n  }\n}"
  },
  {
    "path": "usermods/JSON_IR_remote/ir_json_maker.py",
    "content": "import colorsys\nimport json\nimport openpyxl\n\nnamed_colors = {'AliceBlue': '0xF0F8FF', 'AntiqueWhite': '0xFAEBD7', 'Aqua': '0x00FFFF',\n                'Aquamarine': '0x7FFFD4', 'Azure': '0xF0FFFF', 'Beige': '0xF5F5DC', 'Bisque': '0xFFE4C4',\n                'Black': '0x000000', 'BlanchedAlmond': '0xFFEBCD', 'Blue': '0x0000FF',\n                'BlueViolet': '0x8A2BE2', 'Brown': '0xA52A2A', 'BurlyWood': '0xDEB887',\n                'CadetBlue': '0x5F9EA0', 'Chartreuse': '0x7FFF00', 'Chocolate': '0xD2691E',\n                'Coral': '0xFF7F50', 'CornflowerBlue': '0x6495ED', 'Cornsilk': '0xFFF8DC',\n                'Crimson': '0xDC143C', 'Cyan': '0x00FFFF', 'DarkBlue': '0x00008B', 'DarkCyan': '0x008B8B',\n                'DarkGoldenRod': '0xB8860B', 'DarkGray': '0xA9A9A9', 'DarkGrey': '0xA9A9A9',\n                'DarkGreen': '0x006400', 'DarkKhaki': '0xBDB76B', 'DarkMagenta': '0x8B008B',\n                'DarkOliveGreen': '0x556B2F', 'DarkOrange': '0xFF8C00', 'DarkOrchid': '0x9932CC',\n                'DarkRed': '0x8B0000', 'DarkSalmon': '0xE9967A', 'DarkSeaGreen': '0x8FBC8F',\n                'DarkSlateBlue': '0x483D8B', 'DarkSlateGray': '0x2F4F4F', 'DarkSlateGrey': '0x2F4F4F',\n                'DarkTurquoise': '0x00CED1', 'DarkViolet': '0x9400D3', 'DeepPink': '0xFF1493',\n                'DeepSkyBlue': '0x00BFFF', 'DimGray': '0x696969', 'DimGrey': '0x696969',\n                'DodgerBlue': '0x1E90FF', 'FireBrick': '0xB22222', 'FloralWhite': '0xFFFAF0',\n                'ForestGreen': '0x228B22', 'Fuchsia': '0xFF00FF', 'Gainsboro': '0xDCDCDC',\n                'GhostWhite': '0xF8F8FF', 'Gold': '0xFFD700', 'GoldenRod': '0xDAA520', 'Gray': '0x808080',\n                'Grey': '0x808080', 'Green': '0x008000', 'GreenYellow': '0xADFF2F', 'HoneyDew': '0xF0FFF0',\n                'HotPink': '0xFF69B4', 'IndianRed': '0xCD5C5C', 'Indigo': '0x4B0082', 'Ivory': '0xFFFFF0',\n                'Khaki': '0xF0E68C', 'Lavender': '0xE6E6FA', 'LavenderBlush': '0xFFF0F5',\n                'LawnGreen': '0x7CFC00', 'LemonChiffon': '0xFFFACD', 'LightBlue': '0xADD8E6',\n                'LightCoral': '0xF08080', 'LightCyan': '0xE0FFFF', 'LightGoldenRodYellow': '0xFAFAD2',\n                'LightGray': '0xD3D3D3', 'LightGrey': '0xD3D3D3', 'LightGreen': '0x90EE90',\n                'LightPink': '0xFFB6C1', 'LightSalmon': '0xFFA07A', 'LightSeaGreen': '0x20B2AA',\n                'LightSkyBlue': '0x87CEFA', 'LightSlateGray': '0x778899', 'LightSlateGrey': '0x778899',\n                'LightSteelBlue': '0xB0C4DE', 'LightYellow': '0xFFFFE0', 'Lime': '0x00FF00',\n                'LimeGreen': '0x32CD32', 'Linen': '0xFAF0E6', 'Magenta': '0xFF00FF', 'Maroon': '0x800000',\n                'MediumAquaMarine': '0x66CDAA', 'MediumBlue': '0x0000CD', 'MediumOrchid': '0xBA55D3',\n                'MediumPurple': '0x9370DB', 'MediumSeaGreen': '0x3CB371', 'MediumSlateBlue': '0x7B68EE',\n                'MediumSpringGreen': '0x00FA9A', 'MediumTurquoise': '0x48D1CC', 'MediumVioletRed': '0xC71585',\n                'MidnightBlue': '0x191970', 'MintCream': '0xF5FFFA', 'MistyRose': '0xFFE4E1',\n                'Moccasin': '0xFFE4B5', 'NavajoWhite': '0xFFDEAD', 'Navy': '0x000080', 'OldLace': '0xFDF5E6',\n                'Olive': '0x808000', 'OliveDrab': '0x6B8E23', 'Orange': '0xFFA500', 'OrangeRed': '0xFF4500',\n                'Orchid': '0xDA70D6', 'PaleGoldenRod': '0xEEE8AA', 'PaleGreen': '0x98FB98',\n                'PaleTurquoise': '0xAFEEEE', 'PaleVioletRed': '0xDB7093', 'PapayaWhip': '0xFFEFD5',\n                'PeachPuff': '0xFFDAB9', 'Peru': '0xCD853F', 'Pink': '0xFFC0CB', 'Plum': '0xDDA0DD',\n                'PowderBlue': '0xB0E0E6', 'Purple': '0x800080', 'RebeccaPurple': '0x663399', 'Red': '0xFF0000',\n                'RosyBrown': '0xBC8F8F', 'RoyalBlue': '0x4169E1', 'SaddleBrown': '0x8B4513', 'Salmon': '0xFA8072',\n                'SandyBrown': '0xF4A460', 'SeaGreen': '0x2E8B57', 'SeaShell': '0xFFF5EE', 'Sienna': '0xA0522D',\n                'Silver': '0xC0C0C0', 'SkyBlue': '0x87CEEB', 'SlateBlue': '0x6A5ACD', 'SlateGray': '0x708090',\n                'SlateGrey': '0x708090', 'Snow': '0xFFFAFA', 'SpringGreen': '0x00FF7F', 'SteelBlue': '0x4682B4',\n                'Tan': '0xD2B48C', 'Teal': '0x008080', 'Thistle': '0xD8BFD8', 'Tomato': '0xFF6347',\n                'Turquoise': '0x40E0D0', 'Violet': '0xEE82EE', 'Wheat': '0xF5DEB3', 'White': '0xFFFFFF',\n                'WhiteSmoke': '0xF5F5F5', 'Yellow': '0xFFFF00', 'YellowGreen': '0x9ACD32'}\n\ndef shift_color(col, shift=30, sat=1.0, val=1.0):\n    r = (col & (255 << 16)) >> 16\n    g = (col & (255 << 8)) >> 8\n    b = col & 255\n    hsv = colorsys.rgb_to_hsv(r, g, b)\n    h = (((hsv[0] * 360) + shift) % 360) / 360\n    rgb = colorsys.hsv_to_rgb(h, hsv[1] * sat, hsv[2] * val)\n    return (int(rgb[0]) << 16) + (int(rgb[1]) << 8) + int(rgb[2])\n\ndef parse_sheet(ws):\n    print(f'Parsing worksheet {ws.title}')\n    ir = {\"desc\": ws.title}\n    rows = ws.rows\n    keys = [col.value.lower() for col in next(rows)]\n    for row in rows:\n        rec = dict(zip(keys, [col.value for col in row]))\n        if rec.get('code') is None:\n            continue\n        cd = {\"label\": rec.get('label')}\n        if rec.get('row'):\n            cd['pos'] = f'{rec[\"row\"]}x{rec[\"col\"]}'\n        if rec.get('comment'):\n            cd['cmnt'] = rec.get('comment')\n        if rec.get('rpt'):\n            cd['rpt'] = bool(rec['rpt'])\n                       \n        if rec.get('cmd'):\n            cd['cmd'] = rec['cmd']\n        elif all((rec.get('primary'), rec.get('secondary'), rec.get('tertiary'))):\n            c1 = int(rec.get('primary'), 16)\n            c2 = int(rec.get('secondary'), 16)\n            c3 = int(rec.get('tertiary'), 16)\n            cd['cmd'] = f'FP=5&CL=h{c1:X}&C2=h{c2:X}&C3=h{c3:X}'\n        elif all((rec.get('primary'), rec.get('secondary'))):\n            c1 = int(rec.get('primary'), 16)\n            c2 = int(rec.get('secondary'), 16)\n            c3 = shift_color(c1, -1, sat=0.66, val=0.66)\n            cd['cmd'] = f'FP=5&CL=h{c1:X}&C2=h{c2:X}&C3=h{c3:X}'\n        elif rec.get('primary'):\n            c1 = int(rec.get('primary'), 16)\n            c2 = shift_color(c1, 30)\n            c3 = shift_color(c1, -10, sat=0.66, val=0.66)\n            cd['cmd'] = f'FP=5&CL=h{c1:X}&C2=h{c2:X}&C3=h{c3:X}'\n        elif rec.get('label') in named_colors:\n            c1 = int(named_colors[rec.get('label')], 16)\n            c2 = shift_color(c1, 30)\n            c3 = shift_color(c1, -10, sat=0.66, val=0.66)\n            cd['cmd'] = f'FP=5&CL=h{c1:X}&C2=h{c2:X}&C3=h{c3:X}'\n        else:\n            print(f'Did not find a command or color for {rec[\"label\"]}. Hint use named CSS colors as labels')\n        ir[rec['code']] = cd\n            \n    with open(f'{ws.title}_ir.json', 'w') as fp:\n        json.dump(ir, fp, indent=2)        \n\nif __name__ == '__main__':\n    wb = openpyxl.load_workbook('IR_Remote_Codes.xlsx')\n    for ws in wb.worksheets:\n        parse_sheet(ws)\n"
  },
  {
    "path": "usermods/JSON_IR_remote/readme.md",
    "content": "# JSON IR remote\n\n## Purpose \n\nThe JSON IR remote enables users to customize IR remote behavior without writing custom code and compiling. \nIt also allows using any remote compatible with your IR receiver. Using the JSON IR remote, you can\nmap buttons from any remote to any HTTP request API or JSON API command.  \n\n## Usage\n\n* Upload the IR config file, named _ir.json_ to your board using the [ip address]/edit url. Pick from one of the included files or create your own.\n* On the config > LED settings page, set the correct IR pin.\n* On the config > Sync Interfaces page, select \"JSON Remote\" as the Infrared remote.\n\n## Modification\n\n* See if there is a json file with the same number of buttons as your remote. Many remotes will have the same internals and emit the same codes but have different labels.\n* In the ir.json file, each key will be the hex encoded IR code.\n* The \"cmd\" property will be the HTTP Request API or JSON API to execute when that button is pressed.\n* A limited number of c functions are supported (!incBrightness, !decBrightness, !presetFallback)\n* When using !presetFallback, include properties PL (preset to load), FX (effect to fall back to) and FP (palette to fall back to)\n* If the command is _repeatable_ and does not contain the \"~\" character, add a \"rpt\": true property.\n* Other properties are ignored, but having a label property may help when editing.\n\n\nSample:\n{\n  \"0xFF629D\": {\"cmd\": \"T=2\", \"rpt\": true, \"label\": \"Toggle on/off\"},  // HTTP command\n  \"0xFF9867\": {\"cmd\": \"A=~16\", \"label\": \"Inc brightness\"},            // HTTP command with incrementing          \n  \"0xFF38C7\": {\"cmd\": {\"bri\": 10}, \"label\": \"Dim to 10\"},             // JSON command \n  \"0xFF22DD\": {\"cmd\": \"!presetFallback\", \"PL\": 1, \"FX\": 16, \"FP\": 6,  \n               \"label\": \"Preset 1 or fallback to Saw - Party\"},       // c function\n}\n"
  },
  {
    "path": "usermods/LD2410_v2/LD2410_v2.cpp",
    "content": "#include \"wled.h\"\n#include <ld2410.h>\n\n#ifdef WLED_DISABLE_MQTT\n#error \"This user mod requires MQTT to be enabled.\"\n#endif\n\nclass LD2410Usermod : public Usermod {\n\n  private:\n\n    bool enabled = true;\n    bool initDone = false;\n    bool sensorFound = false;\n    unsigned long lastTime = 0;\n    unsigned long last_mqtt_sent = 0;\n\n    int8_t default_uart_rx = 19;\n    int8_t default_uart_tx = 18;\n\n\n    String mqttMovementTopic = F(\"\");\n    String mqttStationaryTopic = F(\"\");\n    bool mqttInitialized = false;\n    bool HomeAssistantDiscovery = true; // Publish Home Assistant Discovery messages\n\n\n    ld2410 radar;\n    bool stationary_detected = false;\n    bool last_stationary_state = false;\n    bool movement_detected = false;\n    bool last_movement_state = false;\n\n    // These config variables have defaults set inside readFromConfig()\n    int8_t uart_rx_pin;\n    int8_t uart_tx_pin;\n\n    // string that are used multiple time (this will save some flash memory)\n    static const char _name[];\n    static const char _enabled[];\n\n    void publishMqtt(const char* topic, const char* state, bool retain); // example for publishing MQTT message\n\n    void _mqttInitialize()\n    {\n      mqttMovementTopic = String(mqttDeviceTopic) + F(\"/ld2410/movement\");\n      mqttStationaryTopic = String(mqttDeviceTopic) + F(\"/ld2410/stationary\");\n      if (HomeAssistantDiscovery){\n        _createMqttSensor(F(\"Movement\"), mqttMovementTopic, F(\"motion\"), F(\"\"));\n        _createMqttSensor(F(\"Stationary\"), mqttStationaryTopic, F(\"occupancy\"), F(\"\"));\n      } \n    }\n\n    // Create an MQTT Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop.\n    void _createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement)\n    {\n      String t = String(F(\"homeassistant/binary_sensor/\")) + mqttClientID + F(\"/\") + name + F(\"/config\");\n      \n      StaticJsonDocument<600> doc;\n      \n      doc[F(\"name\")] = String(serverDescription) + F(\" Module\");\n      doc[F(\"state_topic\")] = topic;\n      doc[F(\"unique_id\")] = String(mqttClientID) + name;\n      if (unitOfMeasurement != \"\")\n        doc[F(\"unit_of_measurement\")] = unitOfMeasurement;\n      if (deviceClass != \"\")\n        doc[F(\"device_class\")] = deviceClass;\n      doc[F(\"expire_after\")] = 1800;\n      doc[F(\"payload_off\")] = \"OFF\";\n      doc[F(\"payload_on\")] = \"ON\";\n\n      JsonObject device = doc.createNestedObject(F(\"device\")); // attach the sensor to the same device\n      device[F(\"name\")] = serverDescription;\n      device[F(\"identifiers\")] = \"wled-sensor-\" + String(mqttClientID);\n      device[F(\"manufacturer\")] = F(\"WLED\");\n      device[F(\"model\")] = F(\"FOSS\");\n      device[F(\"sw_version\")] = versionString;\n\n      String temp;\n      serializeJson(doc, temp);\n      DEBUG_PRINTLN(t);\n      DEBUG_PRINTLN(temp);\n\n      mqtt->publish(t.c_str(), 0, true, temp.c_str());\n    }\n\n  public:\n\n    inline bool isEnabled() { return enabled; }\n\n    void setup() {\n      Serial1.begin(256000, SERIAL_8N1, uart_rx_pin, uart_tx_pin);\n      Serial.print(F(\"\\nLD2410 radar sensor initialising: \"));\n      if(radar.begin(Serial1)){\n        Serial.println(F(\"OK\"));\n      } else {\n        Serial.println(F(\"not connected\"));\n      }\n      initDone = true;\n    }\n\n\n    void loop() {\n      // NOTE: on very long strips strip.isUpdating() may always return true so update accordingly\n      if (!enabled || strip.isUpdating()) return;\n      radar.read();\n      unsigned long curr_time = millis();\n      if(curr_time - lastTime > 1000)  //Try to Report every 1000ms\n      {\n        lastTime = curr_time;\n        sensorFound = radar.isConnected();\n        if(!sensorFound) return;\n        stationary_detected = radar.presenceDetected();\n        if(stationary_detected != last_stationary_state){\n          if (WLED_MQTT_CONNECTED){\n            publishMqtt(\"/ld2410/stationary\", stationary_detected ? \"ON\":\"OFF\", false);\n            last_stationary_state = stationary_detected;\n          }\n        }\n        movement_detected = radar.movingTargetDetected();\n        if(movement_detected != last_movement_state){\n          if (WLED_MQTT_CONNECTED){\n            publishMqtt(\"/ld2410/movement\", movement_detected ? \"ON\":\"OFF\", false);\n            last_movement_state = movement_detected;\n          }\n        }\n        // If there hasn't been any activity, send current state to confirm sensor is alive\n        if(curr_time - last_mqtt_sent > 1000*60*5 && WLED_MQTT_CONNECTED){\n          publishMqtt(\"/ld2410/stationary\", stationary_detected ? \"ON\":\"OFF\", false);\n          publishMqtt(\"/ld2410/movement\", movement_detected ? \"ON\":\"OFF\", false);\n        }\n      }\n    }\n\n\n    void addToJsonInfo(JsonObject& root)\n    {\n      // if \"u\" object does not exist yet wee need to create it\n      JsonObject user = root[F(\"u\")];\n      if (user.isNull()) user = root.createNestedObject(F(\"u\"));\n\n      JsonArray ld2410_sta_json = user.createNestedArray(F(\"LD2410 Stationary\"));\n      JsonArray ld2410_mov_json = user.createNestedArray(F(\"LD2410 Movement\"));\n      if (!enabled){\n        ld2410_sta_json.add(F(\"disabled\"));\n        ld2410_mov_json.add(F(\"disabled\"));\n      } else if(!sensorFound){\n        ld2410_sta_json.add(F(\"LD2410\"));\n        ld2410_sta_json.add(\" Not Found\");\n      } else {\n        ld2410_sta_json.add(\"Sta \");\n        ld2410_sta_json.add(stationary_detected ? \"ON\":\"OFF\");\n        ld2410_mov_json.add(\"Mov \");\n        ld2410_mov_json.add(movement_detected ? \"ON\":\"OFF\");\n      }\n    }\n\n    void addToConfig(JsonObject& root)\n    {\n      JsonObject top = root.createNestedObject(FPSTR(_name));\n      top[FPSTR(_enabled)] = enabled;\n      //save these vars persistently whenever settings are saved\n      top[\"uart_rx_pin\"] = default_uart_rx;\n      top[\"uart_tx_pin\"] = default_uart_tx;\n    }\n\n\n    bool readFromConfig(JsonObject& root)\n    {\n      // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor\n      // 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)\n\n      JsonObject top = root[FPSTR(_name)];\n\n      bool configComplete = !top.isNull();\n      if (!configComplete)\n      {\n        DEBUG_PRINT(FPSTR(_name));\n        DEBUG_PRINT(F(\"LD2410\"));\n        DEBUG_PRINTLN(F(\": No config found. (Using defaults.)\"));\n        return false;\n      }\n\n      configComplete &= getJsonValue(top[\"uart_rx_pin\"], uart_rx_pin, default_uart_rx);\n      configComplete &= getJsonValue(top[\"uart_tx_pin\"], uart_tx_pin, default_uart_tx);\n\n      return configComplete;\n    }\n\n\n#ifndef WLED_DISABLE_MQTT\n    /**\n     * onMqttConnect() is called when MQTT connection is established\n     */\n    void onMqttConnect(bool sessionPresent) {\n      // do any MQTT related initialisation here\n      if(!radar.isConnected()) return;\n      publishMqtt(\"/ld2410/status\", \"I am alive!\", false);\n      if (!mqttInitialized)\n      {\n        _mqttInitialize();\n        mqttInitialized = true;\n      }\n    }\n#endif\n\n    uint16_t getId()\n    {\n      return USERMOD_ID_LD2410;\n    }\n};\n\n\n// add more strings here to reduce flash memory usage\nconst char LD2410Usermod::_name[]    PROGMEM = \"LD2410Usermod\";\nconst char LD2410Usermod::_enabled[] PROGMEM = \"enabled\";\n\n\n// implementation of non-inline member methods\n\nvoid LD2410Usermod::publishMqtt(const char* topic, const char* state, bool retain)\n{\n#ifndef WLED_DISABLE_MQTT\n  //Check if MQTT Connected, otherwise it will crash\n  if (WLED_MQTT_CONNECTED) {\n    last_mqtt_sent = millis();\n    char subuf[64];\n    strcpy(subuf, mqttDeviceTopic);\n    strcat(subuf, topic);\n    mqtt->publish(subuf, 0, retain, state);\n  }\n#endif\n}\n\n\nstatic LD2410Usermod ld2410_v2;\nREGISTER_USERMOD(ld2410_v2);"
  },
  {
    "path": "usermods/LD2410_v2/library.json",
    "content": "{\n  \"name\": \"LD2410_v2\",\n  \"build\": { \"libArchive\": false },\n  \"dependencies\": {\n    \"ncmreynolds/ld2410\":\"^0.1.3\"\n  }\n}"
  },
  {
    "path": "usermods/LD2410_v2/readme.md",
    "content": "# BH1750 usermod\n\n> This usermod requires a second UART and was only tested on the ESP32\n\n\nThis usermod will read from a LD2410 movement/presence sensor.\n\nThe movement and presence state are displayed in both the Info section of the web UI, as well as published to the `/movement` and `/stationary` MQTT topics respectively.\n\n## Dependencies\n- Libraries\n  - `ncmreynolds/ld2410@^0.1.3`\n- Data is published over MQTT - make sure you've enabled the MQTT sync interface.\n\n## Compilation\n\nTo enable, compile with `LD2140` in `custom_usermods` (e.g. in `platformio_override.ini`)\n```ini\n[env:usermod_USERMOD_LD2410_esp32dev]\nextends = env:esp32dev\ncustom_usermods = ${env:esp32dev.custom_usermods} LD2140_v2\n```\n\n### Configuration Options\nThe Usermod screen allows you to:\n- enable/disable the usermod\n- Configure the RX/TX pins\n\n## Change log\n-  2024-06 Created by @wesleygas (https://github.com/wesleygas/)\n"
  },
  {
    "path": "usermods/LDR_Dusk_Dawn_v2/LDR_Dusk_Dawn_v2.cpp",
    "content": "#include \"wled.h\"\n\n#ifndef ARDUINO_ARCH_ESP32\n  // 8266 does not support analogRead on user selectable pins \n  #error only ESP32 is supported by usermod LDR_DUSK_DAWN\n#endif\n\nclass LDR_Dusk_Dawn_v2 : public Usermod {\n  private:\n    // Defaults\n    bool ldrEnabled = false;\n    int ldrPin = 34; //A2 on Adafruit Huzzah32\n    int ldrThresholdMinutes = 5; // How many minutes of readings above/below threshold until it switches LED state\n    int ldrThreshold = 1000; // Readings higher than this number will turn off LED. \n    int ldrOnPreset = 1; // Default \"On\" Preset\n    int ldrOffPreset = 2; // Default \"Off\" Preset\n\n    // Variables\n    bool initDone = false;\n    bool ldrEnabledPreviously = false; // Was LDR enabled for the previous check? First check is always no.\n    int ldrOffCount; // Number of readings above the threshold\n    int ldrOnCount; // Number of readings below the threshold\n    int ldrReading = 0; // Last LDR reading\n    int ldrLEDState; // Current LED on/off state\n    unsigned long lastMillis = 0;\n    static const char _name[];\n\n  public:\n    void setup() {\n      // register ldrPin\n      if ((ldrPin >= 0) && (digitalPinToAnalogChannel(ldrPin) >= 0)) {\n        if(!PinManager::allocatePin(ldrPin, false, PinOwner::UM_LDR_DUSK_DAWN)) ldrEnabled = false; // pin already in use -> disable usermod\n        else pinMode(ldrPin, INPUT);                                                               // alloc success -> configure pin for input\n      } else ldrEnabled = false;                                                                   // invalid pin -> disable usermod\n      initDone = true;\n    }\n\n    void loop() {\n      // Only update every 10 seconds\n      if (millis() - lastMillis > 10000) {      \n        if (  (ldrEnabled == true)\n           && (ldrPin >= 0) && (digitalPinToAnalogChannel(ldrPin) >= 0) ) { // make sure that pin is valid for analogread()\n          // Default state is off\n          if (ldrEnabledPreviously == false) {\n              applyPreset(ldrOffPreset);\n              ldrEnabledPreviously = true;\n              ldrLEDState = 0;\n          }\n\n          // Get LDR reading and increment counter by number of seconds since last read\n          ldrReading = analogRead(ldrPin);\n          if (ldrReading <= ldrThreshold) {\n            ldrOnCount = ldrOnCount + 10;\n            ldrOffCount = 0;\n          } else {\n            ldrOffCount = ldrOffCount + 10;\n            ldrOnCount = 0;\n          }\n\n          if (ldrOnCount >= (ldrThresholdMinutes * 60)) {\n            ldrOnCount = 0;\n            // If LEDs were previously off, turn on\n            if (ldrLEDState == 0) {\n              applyPreset(ldrOnPreset);\n              ldrLEDState = 1;\n            }\n          }\n\n          if (ldrOffCount >= (ldrThresholdMinutes * 60)) {\n            ldrOffCount = 0;\n            // If LEDs were previously on, turn off\n            if (ldrLEDState == 1) {\n              applyPreset(ldrOffPreset);\n              ldrLEDState = 0;\n            }\n          }\n        } else {\n          // LDR is disabled, reset variables to default\n          ldrReading = 0;\n          ldrOnCount = 0;\n          ldrOffCount = 0;\n          ldrLEDState = 0;\n          ldrEnabledPreviously = false;\n        }\n        lastMillis = millis();\n      }\n    }\n\n  void addToConfig(JsonObject& root) {\n      JsonObject top = root.createNestedObject(FPSTR(_name));\n      top[\"Enabled\"] = ldrEnabled;\n      top[\"LDR Pin\"] = ldrPin;\n      top[\"Threshold Minutes\"] = ldrThresholdMinutes;\n      top[\"Threshold\"] = ldrThreshold;\n      top[\"On Preset\"] = ldrOnPreset;\n      top[\"Off Preset\"] = ldrOffPreset;\n  }\n\n  bool readFromConfig(JsonObject& root) {\n      int8_t oldLdrPin = ldrPin;\n      JsonObject top = root[FPSTR(_name)];\n      bool configComplete = !top.isNull();\n      configComplete &= getJsonValue(top[\"Enabled\"], ldrEnabled);\n      configComplete &= getJsonValue(top[\"LDR Pin\"], ldrPin);\n      configComplete &= getJsonValue(top[\"Threshold Minutes\"], ldrThresholdMinutes);\n      configComplete &= getJsonValue(top[\"Threshold\"], ldrThreshold);\n      configComplete &= getJsonValue(top[\"On Preset\"], ldrOnPreset);\n      configComplete &= getJsonValue(top[\"Off Preset\"], ldrOffPreset);\n\n      if (initDone && (ldrPin != oldLdrPin)) {\n         // pin changed - un-register previous pin, register new pin\n        if (oldLdrPin >= 0) PinManager::deallocatePin(oldLdrPin, PinOwner::UM_LDR_DUSK_DAWN);\n        setup();             // setup new pin\n      }\n      return configComplete;\n  }\n\n  void addToJsonInfo(JsonObject& root) {\n      // If \"u\" object does not exist yet we need to create it\n      JsonObject user = root[\"u\"];\n      if (user.isNull()) user = root.createNestedObject(\"u\");\n\n      JsonArray LDR_Enabled = user.createNestedArray(\"LDR dusk/dawn enabled\");\n      LDR_Enabled.add(ldrEnabled);\n      if (!ldrEnabled) return; // do not add more if usermod is disabled\n\n      JsonArray LDR_Reading = user.createNestedArray(\"LDR reading\");\n      LDR_Reading.add(ldrReading);\n\n      JsonArray LDR_State = user.createNestedArray(\"LDR turned LEDs on\");\n      LDR_State.add(bool(ldrLEDState));\n\n      // Optional debug information:\n      //JsonArray LDR_On_Count = user.createNestedArray(\"LDR on count\");\n      //LDR_On_Count.add(ldrOnCount);\n\n      //JsonArray LDR_Off_Count = user.createNestedArray(\"LDR off count\");\n      //LDR_Off_Count.add(ldrOffCount);\n\n      //bool pinValid = ((ldrPin >= 0) && (digitalPinToAnalogChannel(ldrPin) >= 0));\n      //if (PinManager::getPinOwner(ldrPin) != PinOwner::UM_LDR_DUSK_DAWN) pinValid = false;\n      //JsonArray LDR_valid = user.createNestedArray(F(\"LDR pin\"));\n      //LDR_valid.add(ldrPin);\n      //LDR_valid.add(pinValid ? F(\" OK\"): F(\" invalid\"));\n  }\n\n  uint16_t getId() {\n    return USERMOD_ID_LDR_DUSK_DAWN;\n  }\n};\n\nconst char LDR_Dusk_Dawn_v2::_name[]    PROGMEM = \"LDR_Dusk_Dawn_v2\";\n\n\nstatic LDR_Dusk_Dawn_v2 ldr_dusk_dawn_v2;\nREGISTER_USERMOD(ldr_dusk_dawn_v2);"
  },
  {
    "path": "usermods/LDR_Dusk_Dawn_v2/README.md",
    "content": "# LDR_Dusk_Dawn_v2\nThis 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.\n\n# Installation\nAdd \"LDR_Dusk_Dawn\" to your platformio.ini environment's custom_usermods and build.\n\nExample:\n```\n[env:usermod_LDR_Dusk_Dawn_esp32dev]\nextends = env:esp32dev\ncustom_usermods = ${env:esp32dev.custom_usermods} \n  LDR_Dusk_Dawn   # Enable LDR Dusk Dawn Usermod\n```\n\n# Usermod Settings\nSetting | Description | Default\n--- | --- | ---\nEnabled | Enable/Disable the LDR functionality. | Disabled\nLDR Pin | The analog capable pin your LDR is connected to. | 34\nThreshold Minutes | The number of minutes of consistent readings above/below the on/off threshold before the LED state will change. | 5\nThreshold | The analog read value threshold from the LDR. Readings lower than this number will count towards changing the LED state to off. You can see the current LDR reading by going into the info section when LDR functionality is enabled. | 1000\nOn Preset | The WLED preset to be used for the LED on state. | 1\nOff Preset | The WLED preset to be used for the LED off state. | 2\n\n## Author\n[@jeffwdh](https://github.com/jeffwdh)  \njeffwdh@tarball.ca\n"
  },
  {
    "path": "usermods/LDR_Dusk_Dawn_v2/library.json",
    "content": "{\n  \"name\": \"LDR_Dusk_Dawn_v2\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/MAX17048_v2/MAX17048_v2.cpp",
    "content": "// force the compiler to show a warning to confirm that this file is included\n#warning **** Included USERMOD_MAX17048 V2.0 ****\n\n#include \"wled.h\"\n#include \"Adafruit_MAX1704X.h\"\n\n\n// the max interval to check battery level, 10 seconds\n#ifndef USERMOD_MAX17048_MAX_MONITOR_INTERVAL\n#define USERMOD_MAX17048_MAX_MONITOR_INTERVAL 10000\n#endif\n\n// the min  interval to check battery level, 500 ms\n#ifndef USERMOD_MAX17048_MIN_MONITOR_INTERVAL\n#define USERMOD_MAX17048_MIN_MONITOR_INTERVAL 500\n#endif\n\n// how many seconds after boot to perform the first check, 10 seconds\n#ifndef USERMOD_MAX17048_FIRST_MONITOR_AT\n#define USERMOD_MAX17048_FIRST_MONITOR_AT 10000\n#endif\n\n/* \n * Usermod to display Battery Life using Adafruit's MAX17048 LiPoly/ LiIon Fuel Gauge and Battery Monitor.\n */\nclass  Usermod_MAX17048 : public Usermod {\n\n  private:\n\n    bool enabled = true;\n\n    unsigned long maxReadingInterval = USERMOD_MAX17048_MAX_MONITOR_INTERVAL;\n    unsigned long minReadingInterval = USERMOD_MAX17048_MIN_MONITOR_INTERVAL;\n    unsigned long lastCheck = UINT32_MAX - (USERMOD_MAX17048_MAX_MONITOR_INTERVAL - USERMOD_MAX17048_FIRST_MONITOR_AT);\n    unsigned long lastSend = UINT32_MAX - (USERMOD_MAX17048_MAX_MONITOR_INTERVAL - USERMOD_MAX17048_FIRST_MONITOR_AT);\n\n\n    unsigned VoltageDecimals = 3;  // Number of decimal places in published voltage values\n    unsigned PercentDecimals = 1;  // Number of decimal places in published percent values\n\n    // string that are used multiple time (this will save some flash memory)\n    static const char _name[];\n    static const char _enabled[];\n    static const char _maxReadInterval[];\n    static const char _minReadInterval[];\n    static const char _HomeAssistantDiscovery[];\n\n    bool monitorFound = false;\n    bool firstReadComplete = false;\n    bool initDone = false;\n\n    Adafruit_MAX17048 maxLipo;\n    float lastBattVoltage = -10;\n    float lastBattPercent = -1;\n\n    // MQTT and Home Assistant Variables\n    bool HomeAssistantDiscovery = false;    // Publish Home Assistant Device Information\n    bool mqttInitialized = false; \n\n    void _mqttInitialize()\n    {\n        char mqttBatteryVoltageTopic[128];\n        char mqttBatteryPercentTopic[128];\n\n        snprintf_P(mqttBatteryVoltageTopic, 127, PSTR(\"%s/batteryVoltage\"), mqttDeviceTopic);\n        snprintf_P(mqttBatteryPercentTopic, 127, PSTR(\"%s/batteryPercent\"), mqttDeviceTopic);\n\n        if (HomeAssistantDiscovery) {\n        _createMqttSensor(F(\"BatteryVoltage\"), mqttBatteryVoltageTopic, \"voltage\", F(\"V\"));\n        _createMqttSensor(F(\"BatteryPercent\"), mqttBatteryPercentTopic, \"battery\", F(\"%\"));\n        }\n    }\n\n    void _createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement)\n    {\n        String t = String(F(\"homeassistant/sensor/\")) + mqttClientID + F(\"/\") + name + F(\"/config\");\n\n        StaticJsonDocument<600> doc;\n\n        doc[F(\"name\")] = String(serverDescription) + \" \" + name;\n        doc[F(\"state_topic\")] = topic;\n        doc[F(\"unique_id\")] = String(mqttClientID) + name;\n        if (unitOfMeasurement != \"\")\n        doc[F(\"unit_of_measurement\")] = unitOfMeasurement;\n        if (deviceClass != \"\")\n        doc[F(\"device_class\")] = deviceClass;\n        doc[F(\"expire_after\")] = 1800;\n\n        JsonObject device = doc.createNestedObject(F(\"device\")); // attach the sensor to the same device\n        device[F(\"name\")] = serverDescription;\n        device[F(\"identifiers\")] = \"wled-sensor-\" + String(mqttClientID);\n        device[F(\"manufacturer\")] = F(\"WLED\");\n        device[F(\"model\")] = F(\"FOSS\");\n        device[F(\"sw_version\")] = versionString;\n\n        String temp;\n        serializeJson(doc, temp);\n        DEBUG_PRINTLN(t);\n        DEBUG_PRINTLN(temp);\n\n        mqtt->publish(t.c_str(), 0, true, temp.c_str());\n    }\n\n    void publishMqtt(const char *topic, const char* state) {\n    #ifndef WLED_DISABLE_MQTT\n      //Check if MQTT Connected, otherwise it will crash the 8266\n      if (WLED_MQTT_CONNECTED){\n        char subuf[128];\n        snprintf_P(subuf, 127, PSTR(\"%s/%s\"), mqttDeviceTopic, topic);\n        mqtt->publish(subuf, 0, false, state);\n      }\n    #endif\n    }\n\n  public:\n\n    inline void enable(bool enable) { enabled = enable; }\n\n    inline bool isEnabled() { return enabled; }\n\n    void setup() {\n      // do your set-up here\n      if (i2c_scl<0 || i2c_sda<0) { enabled = false; return; }\n      monitorFound = maxLipo.begin();\n      initDone = true;\n    }\n\n    void loop() {\n      // if usermod is disabled or called during strip updating just exit\n      // NOTE: on very long strips strip.isUpdating() may always return true so update accordingly\n      if (!enabled || strip.isUpdating()) return;\n\n        unsigned long now = millis();\n\n        if (now - lastCheck < minReadingInterval) { return; }\n\n        bool shouldUpdate = now - lastSend > maxReadingInterval;\n\n        float battVoltage = maxLipo.cellVoltage();\n        float battPercent = maxLipo.cellPercent();\n        lastCheck = millis();\n        firstReadComplete = true;\n\n        if (shouldUpdate)\n        {\n          lastBattVoltage = roundf(battVoltage * powf(10, VoltageDecimals)) / powf(10, VoltageDecimals);\n          lastBattPercent = roundf(battPercent * powf(10, PercentDecimals)) / powf(10, PercentDecimals);\n          lastSend = millis();\n\n          publishMqtt(\"batteryVoltage\", String(lastBattVoltage, VoltageDecimals).c_str());\n          publishMqtt(\"batteryPercent\", String(lastBattPercent, PercentDecimals).c_str());\n          DEBUG_PRINTLN(F(\"Battery Voltage: \") + String(lastBattVoltage, VoltageDecimals) + F(\"V\"));\n          DEBUG_PRINTLN(F(\"Battery Percent: \") + String(lastBattPercent, PercentDecimals) + F(\"%\"));\n        }\n    }\n\n    void onMqttConnect(bool sessionPresent)\n    {\n        if (WLED_MQTT_CONNECTED && !mqttInitialized)\n        {\n            _mqttInitialize();\n            mqttInitialized = true;\n        }\n    }\n\n    inline float getBatteryVoltageV() {\n        return (float) lastBattVoltage;\n    }\n\n    inline float getBatteryPercent() {\n        return (float) lastBattPercent;\n    }\n\n    void addToJsonInfo(JsonObject& root)\n    {\n      // if \"u\" object does not exist yet wee need to create it\n      JsonObject user = root[\"u\"];\n      if (user.isNull()) user = root.createNestedObject(\"u\");\n\n\n      JsonArray battery_json = user.createNestedArray(F(\"Battery Monitor\"));\n      if (!enabled) {\n        battery_json.add(F(\"Disabled\"));\n      }\n      else if(!monitorFound) {\n        battery_json.add(F(\"MAX17048 Not Found\"));\n      }        \n      else if (!firstReadComplete) {\n        // if we haven't read the sensor yet, let the user know\n        // that we are still waiting for the first measurement\n        battery_json.add((USERMOD_MAX17048_FIRST_MONITOR_AT - millis()) / 1000);\n        battery_json.add(F(\" sec until read\"));\n      } else {\n        battery_json.add(F(\"Enabled\"));\n        JsonArray voltage_json = user.createNestedArray(F(\"Battery Voltage\"));\n        voltage_json.add(lastBattVoltage);\n        voltage_json.add(F(\"V\"));\n        JsonArray percent_json = user.createNestedArray(F(\"Battery Percent\"));\n        percent_json.add(lastBattPercent);\n        percent_json.add(F(\"%\"));\n      }\n    }\n\n    void addToJsonState(JsonObject& root)\n    {\n        JsonObject usermod = root[FPSTR(_name)];\n        if (usermod.isNull())\n        {\n        usermod = root.createNestedObject(FPSTR(_name));\n        }\n        usermod[FPSTR(_enabled)] = enabled;\n    }\n\n    void readFromJsonState(JsonObject& root)\n    {\n        JsonObject usermod = root[FPSTR(_name)];\n        if (!usermod.isNull())\n        {\n            if (usermod[FPSTR(_enabled)].is<bool>())\n            {\n                enabled = usermod[FPSTR(_enabled)].as<bool>();\n            }\n        }\n    }\n\n    void addToConfig(JsonObject& root)\n    {\n      JsonObject top = root.createNestedObject(FPSTR(_name));\n      top[FPSTR(_enabled)] = enabled;\n      top[FPSTR(_maxReadInterval)] = maxReadingInterval;\n      top[FPSTR(_minReadInterval)] = minReadingInterval;\n      top[FPSTR(_HomeAssistantDiscovery)] = HomeAssistantDiscovery;\n      DEBUG_PRINT(F(_name));\n      DEBUG_PRINTLN(F(\" config saved.\"));\n    }\n\n    bool readFromConfig(JsonObject& root)\n    {\n      JsonObject top = root[FPSTR(_name)];\n\n      if (top.isNull()) {\n        DEBUG_PRINT(F(_name));\n        DEBUG_PRINTLN(F(\": No config found. (Using defaults.)\"));\n        return false;\n      }\n\n      bool configComplete = !top.isNull();\n\n      configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled);\n      configComplete &= getJsonValue(top[FPSTR(_maxReadInterval)], maxReadingInterval, USERMOD_MAX17048_MAX_MONITOR_INTERVAL);\n      configComplete &= getJsonValue(top[FPSTR(_minReadInterval)], minReadingInterval, USERMOD_MAX17048_MIN_MONITOR_INTERVAL);\n      configComplete &= getJsonValue(top[FPSTR(_HomeAssistantDiscovery)], HomeAssistantDiscovery, false);\n\n      DEBUG_PRINT(FPSTR(_name));\n      if (!initDone) {\n        // first run: reading from cfg.json\n        DEBUG_PRINTLN(F(\" config loaded.\"));\n      } else {\n        DEBUG_PRINTLN(F(\" config (re)loaded.\"));\n        // changing parameters from settings page\n      }\n\n      return configComplete;\n    }\n\n    uint16_t getId()\n    {\n      return USERMOD_ID_MAX17048;\n    }\n\n};\n\n\n// add more strings here to reduce flash memory usage\nconst char Usermod_MAX17048::_name[]    PROGMEM = \"Adafruit MAX17048 Battery Monitor\";\nconst char Usermod_MAX17048::_enabled[] PROGMEM = \"enabled\";\nconst char Usermod_MAX17048::_maxReadInterval[] PROGMEM = \"max-read-interval-ms\";\nconst char Usermod_MAX17048::_minReadInterval[] PROGMEM = \"min-read-interval-ms\";\nconst char Usermod_MAX17048::_HomeAssistantDiscovery[] PROGMEM = \"HomeAssistantDiscovery\";\n\n\nstatic Usermod_MAX17048 max17048_v2;\nREGISTER_USERMOD(max17048_v2);"
  },
  {
    "path": "usermods/MAX17048_v2/library.json",
    "content": "{\n  \"name\": \"MAX17048_v2\",\n  \"build\": { \"libArchive\": false},\n  \"dependencies\": {\n    \"Adafruit_MAX1704X\":\"https://github.com/adafruit/Adafruit_MAX1704X#1.0.2\"\n  }\n}\n"
  },
  {
    "path": "usermods/MAX17048_v2/readme.md",
    "content": "# Adafruit MAX17048 Usermod (LiPo & LiIon Battery Monitor & Fuel Gauge)\nThis usermod reads information from an Adafruit MAX17048  and outputs the following:\n- Battery Voltage\n- Battery Level Percentage\n\n\n## Dependencies\nData is published over MQTT - make sure you've enabled the MQTT sync interface.\n\n## Compilation\n\nAdd \"MAX17048_v2\" to your platformio.ini environment's custom_usermods and build.\nTo 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:\n```ini\n[env:usermod_max17048_d1_mini]\nextends = env:d1_mini\ncustom_usermods = ${env:d1_mini.custom_usermods} MAX17048_v2\n```\n\n### Configuration Options\nThe following settings can be set at compile-time but are configurable on the usermod menu (except First Monitor time):\n- USERMOD_MAX17048_MIN_MONITOR_INTERVAL (the min number of milliseconds between checks, defaults to 10,000 ms)\n- USERMOD_MAX17048_MAX_MONITOR_INTERVAL (the max number of milliseconds between checks, defaults to 10,000 ms)\n- USERMOD_MAX17048_FIRST_MONITOR_AT\n\n\nAdditionally, the Usermod Menu allows you to:\n- Enable or Disable the usermod\n- Enable or Disable Home Assistant Discovery (turn on/off to sent MQTT Discovery entries for Home Assistant)\n- Configure SCL/SDA GPIO Pins\n\n## API\nThe following method is available to interact with the usermod from other code modules:\n- `getBatteryVoltageV` read the last battery voltage (in Volt) obtained from the sensor\n- `getBatteryPercent` reads the last battery percentage obtained from the sensor\n\n## MQTT\nMQTT topics are as follows (`<deviceTopic>` is set in MQTT section of Sync Setup menu):\nMeasurement type | MQTT topic\n--- | ---\nBattery Voltage | `<deviceTopic>/batteryVoltage`\nBattery Percent | `<deviceTopic>/batteryPercent`\n\n## Authors\nCarlos Cruz [@ccruz09](https://github.com/ccruz09)\n\n\n## Revision History\nJan 2024\n- Added Home Assistant Discovery\n- Implemented PinManager to register pins\n- Added API call for other modules to read battery voltage and percentage\n- Added info-screen outputs\n- Updated `readme.md`"
  },
  {
    "path": "usermods/MY9291/MY9291.cpp",
    "content": "#include \"wled.h\"\n#include \"MY92xx.h\"\n\n#define MY92XX_MODEL        MY92XX_MODEL_MY9291\n#define MY92XX_CHIPS        1\n#define MY92XX_DI_PIN       13\n#define MY92XX_DCKI_PIN     15\n\n#define MY92XX_RED          0\n#define MY92XX_GREEN        1\n#define MY92XX_BLUE         2\n#define MY92XX_WHITE        3\n\nclass MY9291Usermod : public Usermod {\n  private:\n    my92xx _my92xx = my92xx(MY92XX_MODEL, MY92XX_CHIPS, MY92XX_DI_PIN, MY92XX_DCKI_PIN, MY92XX_COMMAND_DEFAULT);\n\n  public:\n\n    void setup() {\n      _my92xx.setState(true);\n    }\n\n    void connected() {\n    }\n\n    void loop() {\n      uint32_t c = strip.getPixelColor(0);\n      int w = ((c >> 24) & 0xff) * bri / 255.0;\n      int r = ((c >> 16) & 0xff) * bri / 255.0;\n      int g = ((c >> 8) & 0xff) * bri / 255.0;\n      int b = (c & 0xff) * bri / 255.0;\n      _my92xx.setChannel(MY92XX_RED, r);\n      _my92xx.setChannel(MY92XX_GREEN, g);\n      _my92xx.setChannel(MY92XX_BLUE, b);\n      _my92xx.setChannel(MY92XX_WHITE, w);\n      _my92xx.update();\n    }\n\n    uint16_t getId() {\n      return USERMOD_ID_MY9291;\n    }\n};\n\nstatic MY9291Usermod my9291;\nREGISTER_USERMOD(my9291);"
  },
  {
    "path": "usermods/MY9291/MY92xx.h",
    "content": "/*\n\nMY92XX LED Driver for Arduino\nBased on the C driver by MaiKe Labs\n\nCopyright (c) 2016 - 2026 MaiKe Labs\nCopyright (C) 2017 - 2018 Xose Pérez for the Arduino compatible library\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <http://www.gnu.org/licenses/>.\n\n*/\n\n#ifndef _my92xx_h\n#define _my92xx_h\n\n#include <Arduino.h>\n\n#ifdef DEBUG_MY92XX\n#if ARDUINO_ARCH_ESP8266\n#define DEBUG_MSG_MY92XX(...) DEBUG_MY92XX.printf( __VA_ARGS__ )\n#elif ARDUINO_ARCH_AVR\n#define DEBUG_MSG_MY92XX(...) { char buffer[80]; snprintf(buffer, sizeof(buffer),  __VA_ARGS__ ); DEBUG_MY92XX.print(buffer); }\n#endif\n#else\n#define DEBUG_MSG_MY92XX(...)\n#endif\n\ntypedef enum my92xx_model_t {\n    MY92XX_MODEL_MY9291 = 0X00,\n    MY92XX_MODEL_MY9231 = 0X01,\n} my92xx_model_t;\n\ntypedef enum my92xx_cmd_one_shot_t {\n    MY92XX_CMD_ONE_SHOT_DISABLE = 0X00,\n    MY92XX_CMD_ONE_SHOT_ENFORCE = 0X01,\n} my92xx_cmd_one_shot_t;\n\ntypedef enum my92xx_cmd_reaction_t {\n    MY92XX_CMD_REACTION_FAST = 0X00,\n    MY92XX_CMD_REACTION_SLOW = 0X01,\n} my92xx_cmd_reaction_t;\n\ntypedef enum my92xx_cmd_bit_width_t {\n    MY92XX_CMD_BIT_WIDTH_16 = 0X00,\n    MY92XX_CMD_BIT_WIDTH_14 = 0X01,\n    MY92XX_CMD_BIT_WIDTH_12 = 0X02,\n    MY92XX_CMD_BIT_WIDTH_8 = 0X03,\n} my92xx_cmd_bit_width_t;\n\ntypedef enum my92xx_cmd_frequency_t {\n    MY92XX_CMD_FREQUENCY_DIVIDE_1 = 0X00,\n    MY92XX_CMD_FREQUENCY_DIVIDE_4 = 0X01,\n    MY92XX_CMD_FREQUENCY_DIVIDE_16 = 0X02,\n    MY92XX_CMD_FREQUENCY_DIVIDE_64 = 0X03,\n} my92xx_cmd_frequency_t;\n\ntypedef enum my92xx_cmd_scatter_t {\n    MY92XX_CMD_SCATTER_APDM = 0X00,\n    MY92XX_CMD_SCATTER_PWM = 0X01,\n} my92xx_cmd_scatter_t;\n\ntypedef struct {\n    my92xx_cmd_scatter_t scatter : 1;\n    my92xx_cmd_frequency_t frequency : 2;\n    my92xx_cmd_bit_width_t bit_width : 2;\n    my92xx_cmd_reaction_t reaction : 1;\n    my92xx_cmd_one_shot_t one_shot : 1;\n    unsigned char resv : 1;\n} __attribute__((aligned(1), packed)) my92xx_cmd_t;\n\n#define MY92XX_COMMAND_DEFAULT { \\\n    .scatter = MY92XX_CMD_SCATTER_APDM, \\\n    .frequency = MY92XX_CMD_FREQUENCY_DIVIDE_1, \\\n    .bit_width = MY92XX_CMD_BIT_WIDTH_8, \\\n    .reaction = MY92XX_CMD_REACTION_FAST, \\\n    .one_shot = MY92XX_CMD_ONE_SHOT_DISABLE, \\\n    .resv = 0 \\\n}\n\nclass my92xx {\n\npublic:\n\n    my92xx(my92xx_model_t model, unsigned char chips, unsigned char di, unsigned char dcki, my92xx_cmd_t command);\n    unsigned char getChannels();\n    void setChannel(unsigned char channel, unsigned int value);\n    unsigned int getChannel(unsigned char channel);\n    void setState(bool state);\n    bool getState();\n    void update();\n\nprivate:\n\n    void _di_pulse(unsigned int times);\n    void _dcki_pulse(unsigned int times);\n    void _set_cmd(my92xx_cmd_t command);\n    void _send();\n    void _write(unsigned int data, unsigned char bit_length);\n\n    my92xx_cmd_t _command;\n    my92xx_model_t _model = MY92XX_MODEL_MY9291;\n    unsigned char _chips = 1;\n    unsigned char _channels;\n    uint16_t* _value;\n    bool _state = false;\n    unsigned char _pin_di;\n    unsigned char _pin_dcki;\n\n\n};\n\n\n#if ARDUINO_ARCH_ESP8266\n\nextern \"C\" {\n    void os_delay_us(unsigned int);\n}\n\n#elif ARDUINO_ARCH_AVR\n\n#define os_delay_us delayMicroseconds\n\n#endif\n\nvoid my92xx::_di_pulse(unsigned int times) {\n    for (unsigned int i = 0; i < times; i++) {\n        digitalWrite(_pin_di, HIGH);\n        digitalWrite(_pin_di, LOW);\n    }\n}\n\nvoid my92xx::_dcki_pulse(unsigned int times) {\n    for (unsigned int i = 0; i < times; i++) {\n        digitalWrite(_pin_dcki, HIGH);\n        digitalWrite(_pin_dcki, LOW);\n    }\n}\n\nvoid my92xx::_write(unsigned int data, unsigned char bit_length) {\n\n    unsigned int mask = (0x01 << (bit_length - 1));\n\n    for (unsigned int i = 0; i < bit_length / 2; i++) {\n        digitalWrite(_pin_dcki, LOW);\n        digitalWrite(_pin_di, (data & mask) ? HIGH : LOW);\n        digitalWrite(_pin_dcki, HIGH);\n        data = data << 1;\n        digitalWrite(_pin_di, (data & mask) ? HIGH : LOW);\n        digitalWrite(_pin_dcki, LOW);\n        digitalWrite(_pin_di, LOW);\n        data = data << 1;\n    }\n\n}\n\nvoid my92xx::_set_cmd(my92xx_cmd_t command) {\n\n    // ets_intr_lock();\n\n    // TStop > 12us.\n    os_delay_us(12);\n\n    // Send 12 DI pulse, after 6 pulse's falling edge store duty data, and 12\n    // pulse's rising edge convert to command mode.\n    _di_pulse(12);\n\n    // Delay >12us, begin send CMD data\n    os_delay_us(12);\n\n    // Send CMD data\n    unsigned char command_data = *(unsigned char*)(&command);\n    for (unsigned char i = 0; i < _chips; i++) {\n        _write(command_data, 8);\n    }\n\n    // TStart > 12us. Delay 12 us.\n    os_delay_us(12);\n\n    // Send 16 DI pulse，at 14 pulse's falling edge store CMD data, and\n    // at 16 pulse's falling edge convert to duty mode.\n    _di_pulse(16);\n\n    // TStop > 12us.\n    os_delay_us(12);\n\n    // ets_intr_unlock();\n\n}\n\nvoid my92xx::_send() {\n\n#ifdef DEBUG_MY92XX\n    DEBUG_MSG_MY92XX(\"[MY92XX] Refresh: %s (\", _state ? \"ON\" : \"OFF\");\n    for (unsigned char channel = 0; channel < _channels; channel++) {\n        DEBUG_MSG_MY92XX(\" %d\", _value[channel]);\n    }\n    DEBUG_MSG_MY92XX(\" )\\n\");\n#endif\n\n    unsigned char bit_length = 8;\n    switch (_command.bit_width) {\n    case MY92XX_CMD_BIT_WIDTH_16:\n        bit_length = 16;\n        break;\n    case MY92XX_CMD_BIT_WIDTH_14:\n        bit_length = 14;\n        break;\n    case MY92XX_CMD_BIT_WIDTH_12:\n        bit_length = 12;\n        break;\n    case MY92XX_CMD_BIT_WIDTH_8:\n        bit_length = 8;\n        break;\n    default:\n        bit_length = 8;\n        break;\n    }\n\n    // ets_intr_lock();\n\n    // TStop > 12us.\n    os_delay_us(12);\n\n    // Send color data\n    for (unsigned char channel = 0; channel < _channels; channel++) {\n        _write(_state ? _value[channel] : 0, bit_length);\n    }\n\n    // TStart > 12us. Ready for send DI pulse.\n    os_delay_us(12);\n\n    // Send 8 DI pulse. After 8 pulse falling edge, store old data.\n    _di_pulse(8);\n\n    // TStop > 12us.\n    os_delay_us(12);\n\n    // ets_intr_unlock();\n\n}\n\n// -----------------------------------------------------------------------------\n\nunsigned char my92xx::getChannels() {\n    return _channels;\n}\n\nvoid my92xx::setChannel(unsigned char channel, unsigned int value) {\n    if (channel < _channels) {\n        _value[channel] = value;\n    }\n}\n\nunsigned int my92xx::getChannel(unsigned char channel) {\n    if (channel < _channels) {\n        return _value[channel];\n    }\n    return 0;\n}\n\nbool my92xx::getState() {\n    return _state;\n}\n\nvoid my92xx::setState(bool state) {\n    _state = state;\n}\n\nvoid my92xx::update() {\n    _send();\n}\n\n// -----------------------------------------------------------------------------\n\nmy92xx::my92xx(my92xx_model_t model, unsigned char chips, unsigned char di, unsigned char dcki, my92xx_cmd_t command) : _command(command) {\n\n    _model = model;\n    _chips = chips;\n    _pin_di = di;\n    _pin_dcki = dcki;\n\n    // Init channels\n    if (_model == MY92XX_MODEL_MY9291) {\n        _channels = 4 * _chips;\n    }\n    else if (_model == MY92XX_MODEL_MY9231) {\n        _channels = 3 * _chips;\n    }\n    _value = new uint16_t[_channels];\n    for (unsigned char i = 0; i < _channels; i++) {\n        _value[i] = 0;\n    }\n\n    // Init GPIO\n    pinMode(_pin_di, OUTPUT);\n    pinMode(_pin_dcki, OUTPUT);\n    digitalWrite(_pin_di, LOW);\n    digitalWrite(_pin_dcki, LOW);\n\n    // Clear all duty register\n    _dcki_pulse(32 * _chips);\n\n    // Send init command\n    _set_cmd(command);\n\n    DEBUG_MSG_MY92XX(\"[MY92XX] Initialized\\n\");\n\n}\n\n#endif"
  },
  {
    "path": "usermods/MY9291/library.json",
    "content": "{\n  \"name\": \"MY9291\",\n  \"build\": { \"libArchive\": false },\n  \"platforms\": [\"espressif8266\"]\n}"
  },
  {
    "path": "usermods/PIR_sensor_switch/PIR_Highlight_Standby",
    "content": "#pragma once\n\n#include \"wled.h\"\n\n/*\n * --------------------\n * Rawframe edit:\n * - TESTED ON WLED VS.0.10.1 - WHERE ONLY PRESET 16 SAVES SEGMENTS - some macros may not be needed if this changes.\n * - Code has been modified as my usage changed, as such it has poor use of functions vs if thens, but feel free to change it for me :)\n * \n * Edited to SWITCH between two lighting scenes/modes : STANDBY and HIGHLIGHT\n * \n * Usage:\n *  - Standby is the default mode and Highlight is activated when the PIR detects activity.\n *  - PIR delay now set to same value as Nightlight feature on boot but otherwise controlled as normal.\n *  - Standby and Highlight brightness can be set on the fly (default values set on boot via macros calling presets).\n *  - Macros are used to set Standby and Highlight states (macros can load saved presets etc).\n * \n *    - Macro short button press   =  Highlight state default (used on boot only and sets default brightness).\n *    - Macro double button press  =  Standby state default   (used on boot only and sets default brightness).\n *    - Macro long button press    =  Highlight state         (after boot).\n *    - Macro 16                   =  Standby state           (after boot).\n *\n *    ! It is advised not to set 'Apply preset at boot' or a boot macro (that activates a preset) as we will call our own macros on boot.\n * \n *  - When the strip is off before PIR activates the strip will return to off for Standby mode, and vice versa.\n *  - When the strip is turned off while in Highlight mode, it will return to standby mode. (This behaviour could be changed easily if for some reason you wanted the lights to go out when the pir is activated).\n *  - Macros can be chained so you could do almost anything, such as have standby mode also turn on the nightlight function with a new time delay.\n * \n * Segment Notes:\n * - It's easier to save the segment selections in preset than apply via macro while we a limited to preset 16. (Ie, instead of selecting sections at the point of activating standby/highlight modes). \n * - Because only preset 16 saves segments, for now we are having to use addiotional macros to control segments where they are involved. Macros can be chained so this works but it would be better if macros also accepted json-api commands. (Testing http api segement behaviour of SS with SB left me a little confused).\n * \n * Future:\n *  - Maybe a second timer/timetable that turns on/off standby mode also after set inactivity period / date & times. For now this can be achieved others ways so may not be worth eating more processing power.\n * \n * --------------------\n * \n * This usermod handles PIR sensor states.\n * The strip will be switched on and the off timer will be resetted when the sensor goes HIGH. \n * When the sensor state goes LOW, the off timer is started and when it expires, the strip is switched off. \n * \n * \n * Usermods allow you to add own functionality to WLED more easily\n * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality\n * \n * v2 usermods are class inheritance based and can (but don't have to) implement more functions, each of them is shown in this example.\n * Multiple v2 usermods can be added to one compilation easily.\n * \n * Creating a usermod:\n * This file serves as an example. If you want to create a usermod, it is recommended to use usermod_v2_empty.h from the usermods folder as a template.\n * Please remember to rename the class and file to a descriptive name.\n * You may also use multiple .h and .cpp files.\n * \n * Using a usermod:\n * 1. Copy the usermod into the sketch folder (same folder as wled00.ino)\n * 2. Register the usermod by adding #include \"usermod_filename.h\" in the top and registerUsermod(new MyUsermodClass()) in the bottom of usermods_list.cpp\n */\n\nclass PIRsensorSwitch : public Usermod {\n  private:\n    // PIR sensor pin\n    const uint8_t PIRsensorPin = 13; // D7 on D1 mini\n    // notification mode for stateUpdated()\n    const byte NotifyUpdateMode = CALL_MODE_NO_NOTIFY; // CALL_MODE_DIRECT_CHANGE\n    // 1 min delay before switch off after the sensor state goes LOW\n    uint32_t m_switchOffDelay = 60000;\n    // off timer start time\n    uint32_t m_offTimerStart = 0;\n    // current PIR sensor pin state\n    byte m_PIRsensorPinState = LOW;\n    // PIR sensor enabled - ISR attached\n    bool m_PIRenabled = true;\n    // temp standby brightness store. initial value set as nightlight default target brightness\n    byte briStandby _INIT(nightlightTargetBri);\n    // temp hightlight brightness store. initial value set as current brightness\n    byte briHighlight _INIT(bri);\n    // highlight active/deactive monitor \n    bool highlightActive = false;\n    // wled on/off state in standby mode\n    bool standbyoff = false;\n\n    /*\n     * return or change if new PIR sensor state is available\n     */\n    static volatile bool newPIRsensorState(bool changeState = false, bool newState = false) {\n      static volatile bool s_PIRsensorState = false;\n      if (changeState) {\n        s_PIRsensorState = newState;\n      }\n      return s_PIRsensorState;\n    }\n\n    /*\n     * PIR sensor state has changed\n     */\n    static void IRAM_ATTR ISR_PIRstateChange() {\n      newPIRsensorState(true, true);\n    }\n\n    /*\n     * switch strip on/off\n     */\n    // now allowing adjustable standby and highlight brightness\n    void switchStrip(bool switchOn) {\n      //if (switchOn && bri == 0) {\n      if (switchOn) { // **pir sensor is on and activated** \n        //bri = briLast;\n        if (bri != 0) { // is WLED currently on\n          if (highlightActive) { // and is Highlight already on\n            briHighlight = bri; // then update highlight brightness with current brightness\n          }\n          else {\n            briStandby = bri; // else update standby brightness with current brightness\n          }\n        }\n        else { // WLED is currently off\n          if (!highlightActive) { // and Highlight is not already on\n            briStandby = briLast; // then update standby brightness with last active brightness (before turned off)\n            standbyoff = true;\n          }\n          else { // and Highlight is already on\n            briHighlight = briLast; // then set hightlight brightness to last active brightness (before turned off)\n          }\n        }\n        applyMacro(16); // apply highlight lighting without brightness\n        if (bri != briHighlight) { \n          bri = briHighlight; // set current highlight brightness to last set highlight brightness\n        }\n        stateUpdated(NotifyUpdateMode);\n        highlightActive = true; // flag highlight is on\n      }    \n      else { // **pir timer has elapsed**\n        //briLast = bri;\n        //bri = 0;\n        if (bri != 0) { // is WLED currently on\n          briHighlight = bri; // update highlight brightness with current brightness\n          if (!standbyoff) { // \n            bri = briStandby; // set standby brightness to last set standby brightness\n          }\n          else { // \n            briLast = briStandby; // set standby off brightness\n            bri = 0; // set power off in standby\n            standbyoff = false; // turn off flag\n          }\n          applyMacro(macroLongPress); // apply standby lighting without brightness\n        }\n        else { // WLED is currently off\n          briHighlight = briLast; // set last active brightness (before turned off) to highlight lighting brightness\n          if (!standbyoff) { // \n            bri = briStandby; // set standby brightness to last set standby brightness\n          }\n          else { // \n            briLast = briStandby; // set standby off brightness \n            bri = 0; // set power off in standby\n            standbyoff = false; // turn off flag \n          }\n          applyMacro(macroLongPress); // apply standby lighting without brightness\n        }\n        stateUpdated(NotifyUpdateMode);\n        highlightActive = false; // flag highlight is off\n      }\n    }\n\n    /*\n     * Read and update PIR sensor state.\n     * Initilize/reset switch off timer\n     */\n    bool updatePIRsensorState() {\n      if (newPIRsensorState()) {\n        m_PIRsensorPinState = digitalRead(PIRsensorPin);\n        \n        if (m_PIRsensorPinState == HIGH) {\n          m_offTimerStart = 0;\n          switchStrip(true);\n        }\n        else if (bri != 0) {\n          // start switch off timer\n          m_offTimerStart = millis();\n        }\n        newPIRsensorState(true, false);\n        return true;\n      }\n      return false;\n    }\n\n    /* \n     * switch off the strip if the delay has elapsed \n     */\n    bool handleOffTimer() {\n      if (m_offTimerStart > 0) {\n        if ((millis() - m_offTimerStart > m_switchOffDelay) || bri == 0 ) { // now also checking for manual power off during highlight mode\n        switchStrip(false);\n        m_offTimerStart = 0;        \n        return true;\n        }\n      }\n      return false;\n    }\n\n  public:\n    //Functions called by WLED\n\n    /*\n     * setup() is called once at boot. WiFi is not yet connected at this point.\n     * You can use it to initialize variables, sensors or similar.\n     */\n    void setup() {\n      // PIR Sensor mode INPUT_PULLUP\n      pinMode(PIRsensorPin, INPUT_PULLUP);\n      // assign interrupt function and set CHANGE mode\n      attachInterrupt(digitalPinToInterrupt(PIRsensorPin), ISR_PIRstateChange, CHANGE);\n      // set delay to nightlight default duration on boot (after which json PIRoffSec overides if needed)\n      m_switchOffDelay = (nightlightDelayMins*60000);\n      applyMacro(macroButton); // apply default highlight lighting\n      briHighlight = bri;\n      applyMacro(macroDoublePress); // apply default standby lighting with brightness\n      briStandby = bri;\n    }\n\n\n    /*\n     * connected() is called every time the WiFi is (re)connected\n     * Use it to initialize network interfaces\n     */\n    void connected() {\n\n    }\n\n\n    /*\n     * loop() is called continuously. Here you can check for events, read sensors, etc.\n     */\n    void loop() {\n      if (!updatePIRsensorState()) {\n        handleOffTimer();\n      }\n    }\n\n    /*\n     * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.\n     * \n     * Add PIR sensor state and switch off timer duration to jsoninfo\n     */\n    void addToJsonInfo(JsonObject& root)\n    {\n      //this code adds \"u\":{\"&#x23F2; PIR sensor state\":uiDomString} to the info object\n      // the value contains a button to toggle the sensor enabled/disabled\n      JsonObject user = root[\"u\"];\n      if (user.isNull()) user = root.createNestedObject(\"u\");\n\n      JsonArray infoArr = user.createNestedArray(\"&#x23F2; PIR sensor state\"); //name\n      String uiDomString = \"<button class=\\\"btn infobtn\\\" onclick=\\\"requestJson({PIRenabled:\";\n      String sensorStateInfo;\n\n      // PIR sensor state\n      if (m_PIRenabled) {\n        uiDomString += \"false\";\n        sensorStateInfo = (m_PIRsensorPinState != LOW ? \"active\" : \"inactive\"); //value\n      } else {\n        uiDomString += \"true\";\n        sensorStateInfo = \"Disabled !\";\n      }\n      uiDomString += \"});return false;\\\">\";\n      uiDomString +=  sensorStateInfo;\n      uiDomString += \"</button>\";\n      infoArr.add(uiDomString); //value\n\n      //this code adds \"u\":{\"&#x23F2; switch off timer\":uiDomString} to the info object\n      infoArr = user.createNestedArray(\"&#x23F2; switch off timer\"); //name\n\n      // off timer\n      if (m_offTimerStart > 0) {\n        uiDomString = \"\";\n        unsigned int offSeconds = (m_switchOffDelay - (millis() - m_offTimerStart)) / 1000;\n        if (offSeconds >= 3600) {\n          uiDomString += (offSeconds / 3600); \n          uiDomString += \" hours \"; \n          offSeconds %= 3600;\n        }\n        if (offSeconds >= 60) {\n          uiDomString += (offSeconds / 60); \n          offSeconds %= 60;\n        } else if (uiDomString.length() > 0){\n          uiDomString += 0; \n        }\n        if (uiDomString.length() > 0){\n          uiDomString += \" min \";\n        }\n        uiDomString += (offSeconds); \n        infoArr.add(uiDomString + \" sec\");\n      } else {\n        infoArr.add(\"inactive\");\n      }\n    }\n\n\n    /*\n     * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).\n     * Values in the state object may be modified by connected clients\n     * Add \"PIRenabled\" to json state. This can be used to disable/enable the sensor.\n     * Add \"PIRoffSec\" to json state. This can be used to adjust <m_switchOffDelay> milliseconds .\n     */\n    void addToJsonState(JsonObject& root)\n    {\n      root[\"PIRenabled\"] = m_PIRenabled;\n      root[\"PIRoffSec\"] = (m_switchOffDelay / 1000);\n    }\n\n\n    /*\n     * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).\n     * Values in the state object may be modified by connected clients\n     * Read \"PIRenabled\" from json state and switch enable/disable the PIR sensor.\n     * Read \"PIRoffSec\" from json state and adjust <m_switchOffDelay> milliseconds .\n     */\n    void readFromJsonState(JsonObject& root)\n    {\n      if (root[\"PIRoffSec\"] != nullptr) {\n        m_switchOffDelay = (1000 * max(60UL, min(43200UL, root[\"PIRoffSec\"].as<unsigned long>())));\n      }\n      \n      if (root[\"PIRenabled\"] != nullptr) {\n        if (root[\"PIRenabled\"] && !m_PIRenabled) {\n          attachInterrupt(digitalPinToInterrupt(PIRsensorPin), ISR_PIRstateChange, CHANGE);\n          newPIRsensorState(true, true);\n        } \n        else if(m_PIRenabled) {\n          detachInterrupt(PIRsensorPin);\n        }\n        m_PIRenabled = root[\"PIRenabled\"];          \n      }\n    }\n    \n   \n    /*\n     * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).\n     * This could be used in the future for the system to determine whether your usermod is installed.\n     */\n    uint16_t getId()\n    {\n      return USERMOD_ID_PIRSWITCH;\n    }\n\n   //More methods can be added in the future, this example will then be extended.\n   //Your usermod will remain compatible as it does not need to implement all methods from the Usermod base class!\n};\n"
  },
  {
    "path": "usermods/PIR_sensor_switch/PIR_sensor_switch.cpp",
    "content": "#include \"wled.h\"\r\n\r\n#ifndef PIR_SENSOR_PIN\r\n  // compatible with QuinLED-Dig-Uno\r\n  #ifdef ARDUINO_ARCH_ESP32\r\n    #define PIR_SENSOR_PIN 23 // Q4\r\n  #else //ESP8266 boards\r\n    #define PIR_SENSOR_PIN 13 // Q4 (D7 on D1 mini)\r\n  #endif\r\n#endif\r\n\r\n#ifndef PIR_SENSOR_OFF_SEC\r\n  #define PIR_SENSOR_OFF_SEC 600\r\n#endif\r\n\r\n#ifndef PIR_SENSOR_MAX_SENSORS\r\n  #define PIR_SENSOR_MAX_SENSORS 1\r\n#endif\r\n\r\n/*\r\n * This usermod handles PIR sensor states.\r\n * The strip will be switched on and the off timer will be resetted when the sensor goes HIGH. \r\n * When the sensor state goes LOW, the off timer is started and when it expires, the strip is switched off. \r\n * Maintained by: @blazoncek\r\n * \r\n * Usermods allow you to add own functionality to WLED more easily\r\n * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality\r\n * \r\n * v2 usermods are class inheritance based and can (but don't have to) implement more functions, each of them is shown in this example.\r\n * Multiple v2 usermods can be added to one compilation easily.\r\n */\r\n\r\nclass PIRsensorSwitch : public Usermod\r\n{\r\npublic:\r\n  // constructor\r\n  PIRsensorSwitch() {}\r\n  // destructor\r\n  ~PIRsensorSwitch() {}\r\n\r\n  //Enable/Disable the PIR sensor\r\n  inline void EnablePIRsensor(bool en) { enabled = en; }\r\n  \r\n  // Get PIR sensor enabled/disabled state\r\n  inline bool PIRsensorEnabled() { return enabled; }\r\n\r\nprivate:\r\n\r\n  byte prevPreset   = 0;\r\n  byte prevPlaylist = 0;\r\n\r\n  volatile unsigned long offTimerStart = 0;     // off timer start time\r\n  volatile bool PIRtriggered           = false; // did PIR trigger?\r\n  bool          initDone               = false; // status of initialization\r\n  unsigned long lastLoop               = 0;\r\n  bool sensorPinState[PIR_SENSOR_MAX_SENSORS] = {LOW}; // current PIR sensor pin state\r\n\r\n  // configurable parameters\r\n#if PIR_SENSOR_PIN < 0\r\n  bool enabled              = false;          // PIR sensor disabled\r\n#else\r\n  bool enabled              = true;           // PIR sensor enabled\r\n#endif\r\n  int8_t PIRsensorPin[PIR_SENSOR_MAX_SENSORS] = {PIR_SENSOR_PIN}; // PIR sensor pin\r\n  uint32_t m_switchOffDelay = PIR_SENSOR_OFF_SEC*1000;  // delay before switch off after the sensor state goes LOW (10min)\r\n  uint8_t m_onPreset        = 0;              // on preset\r\n  uint8_t m_offPreset       = 0;              // off preset\r\n  bool m_nightTimeOnly      = false;          // flag to indicate that PIR sensor should activate WLED during nighttime only\r\n  bool m_mqttOnly           = false;          // flag to send MQTT message only (assuming it is enabled)\r\n  // flag to enable triggering only if WLED is initially off (LEDs are not on, preventing running effect being overwritten by PIR)\r\n  bool m_offOnly            = false;\r\n  bool m_offMode            = offMode;\r\n  bool m_override           = false;\r\n\r\n  // Home Assistant\r\n  bool HomeAssistantDiscovery = false;        // is HA discovery turned on\r\n  int16_t idx = -1; // Domoticz virtual switch idx\r\n\r\n  // strings to reduce flash memory usage (used more than twice)\r\n  static const char _name[];\r\n  static const char _switchOffDelay[];\r\n  static const char _enabled[];\r\n  static const char _onPreset[];\r\n  static const char _offPreset[];\r\n  static const char _nightTime[];\r\n  static const char _mqttOnly[];\r\n  static const char _offOnly[];\r\n  static const char _haDiscovery[];\r\n  static const char _override[];\r\n  static const char _domoticzIDX[];\r\n\r\n  /**\r\n   * check if it is daytime\r\n   * if sunrise/sunset is not defined (no NTP or lat/lon) default to nighttime\r\n   */\r\n  static bool isDayTime();\r\n\r\n  /**\r\n   * switch strip on/off\r\n   */\r\n  void switchStrip(bool switchOn);\r\n  void publishMqtt(bool switchOn);\r\n\r\n  // Create an MQTT Binary Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop.\r\n  void publishHomeAssistantAutodiscovery();\r\n\r\n  /**\r\n   * Read and update PIR sensor state.\r\n   * Initialize/reset switch off timer\r\n   */\r\n  bool updatePIRsensorState();\r\n\r\n  /**\r\n   * switch off the strip if the delay has elapsed \r\n   */\r\n  bool handleOffTimer();\r\n\r\npublic:\r\n  //Functions called by WLED\r\n\r\n  /**\r\n   * setup() is called once at boot. WiFi is not yet connected at this point.\r\n   * You can use it to initialize variables, sensors or similar.\r\n   */\r\n  void setup() override;\r\n\r\n  /**\r\n   * connected() is called every time the WiFi is (re)connected\r\n   * Use it to initialize network interfaces\r\n   */\r\n  //void connected();\r\n\r\n  /**\r\n   * onMqttConnect() is called when MQTT connection is established\r\n   */\r\n  void onMqttConnect(bool sessionPresent) override;\r\n\r\n  /**\r\n   * loop() is called continuously. Here you can check for events, read sensors, etc.\r\n   */\r\n  void loop() override;\r\n\r\n  /**\r\n   * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.\r\n   * \r\n   * Add PIR sensor state and switch off timer duration to jsoninfo\r\n   */\r\n  void addToJsonInfo(JsonObject &root) override;\r\n\r\n  /**\r\n   * onStateChanged() is used to detect WLED state change\r\n   */\r\n  void onStateChange(uint8_t mode) override;\r\n\r\n  /**\r\n   * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).\r\n   * Values in the state object may be modified by connected clients\r\n   */\r\n  //void addToJsonState(JsonObject &root);\r\n\r\n  /**\r\n   * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).\r\n   * Values in the state object may be modified by connected clients\r\n   */\r\n  void readFromJsonState(JsonObject &root) override;\r\n\r\n  /**\r\n   * provide the changeable values\r\n   */\r\n  void addToConfig(JsonObject &root) override;\r\n\r\n  /**\r\n   * provide UI information and allow extending UI options\r\n   */\r\n  void appendConfigData() override;\r\n\r\n  /**\r\n   * restore the changeable values\r\n   * readFromConfig() is called before setup() to populate properties from values stored in cfg.json\r\n   *\r\n   * The function should return true if configuration was successfully loaded or false if there was no configuration.\r\n   */\r\n  bool readFromConfig(JsonObject &root) override;\r\n\r\n  /**\r\n   * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).\r\n   * This could be used in the future for the system to determine whether your usermod is installed.\r\n   */\r\n  uint16_t getId() override { return USERMOD_ID_PIRSWITCH; }\r\n};\r\n\r\n// strings to reduce flash memory usage (used more than twice)\r\nconst char PIRsensorSwitch::_name[]           PROGMEM = \"PIRsensorSwitch\";\r\nconst char PIRsensorSwitch::_enabled[]        PROGMEM = \"PIRenabled\";\r\nconst char PIRsensorSwitch::_switchOffDelay[] PROGMEM = \"PIRoffSec\";\r\nconst char PIRsensorSwitch::_onPreset[]       PROGMEM = \"on-preset\";\r\nconst char PIRsensorSwitch::_offPreset[]      PROGMEM = \"off-preset\";\r\nconst char PIRsensorSwitch::_nightTime[]      PROGMEM = \"nighttime-only\";\r\nconst char PIRsensorSwitch::_mqttOnly[]       PROGMEM = \"mqtt-only\";\r\nconst char PIRsensorSwitch::_offOnly[]        PROGMEM = \"off-only\";\r\nconst char PIRsensorSwitch::_haDiscovery[]    PROGMEM = \"HA-discovery\";\r\nconst char PIRsensorSwitch::_override[]       PROGMEM = \"override\";\r\nconst char PIRsensorSwitch::_domoticzIDX[]    PROGMEM = \"domoticz-idx\";\r\n\r\nbool PIRsensorSwitch::isDayTime() {\r\n  updateLocalTime();\r\n  uint8_t hr = hour(localTime);\r\n  uint8_t mi = minute(localTime);\r\n\r\n  if (sunrise && sunset) {\r\n    if (hour(sunrise)<hr && hour(sunset)>hr) {\r\n      return true;\r\n    } else {\r\n      if (hour(sunrise)==hr && minute(sunrise)<mi) {\r\n        return true;\r\n      }\r\n      if (hour(sunset)==hr && minute(sunset)>mi) {\r\n        return true;\r\n      }\r\n    }\r\n  }\r\n  return false;\r\n}\r\n\r\nvoid PIRsensorSwitch::switchStrip(bool switchOn)\r\n{\r\n  if (m_offOnly && bri && (switchOn || (!PIRtriggered && !switchOn))) return; //if lights on and off only, do nothing\r\n  if (PIRtriggered && switchOn) return; //if already on and triggered before, do nothing\r\n  PIRtriggered = switchOn;\r\n  DEBUG_PRINT(F(\"PIR: strip=\")); DEBUG_PRINTLN(switchOn?\"on\":\"off\");\r\n  if (switchOn) {\r\n    if (m_onPreset) {\r\n      if (currentPlaylist>0 && !offMode) {\r\n        prevPlaylist = currentPlaylist;\r\n        unloadPlaylist();\r\n      } else if (currentPreset>0 && !offMode) {\r\n        prevPreset   = currentPreset;\r\n      } else {\r\n        saveTemporaryPreset();\r\n        prevPlaylist = 0;\r\n        prevPreset   = 255;\r\n      }\r\n      applyPreset(m_onPreset, CALL_MODE_BUTTON_PRESET);\r\n      return;\r\n    }\r\n    // preset not assigned\r\n    if (bri == 0) {\r\n      bri = briLast;\r\n      stateUpdated(CALL_MODE_BUTTON);\r\n    }\r\n  } else {\r\n    if (m_offPreset) {\r\n      applyPreset(m_offPreset, CALL_MODE_BUTTON_PRESET);\r\n      return;\r\n    } else if (prevPlaylist) {\r\n      if (currentPreset==m_onPreset || currentPlaylist==m_onPreset) applyPreset(prevPlaylist, CALL_MODE_BUTTON_PRESET);\r\n      prevPlaylist = 0;\r\n      return;\r\n    } else if (prevPreset) {\r\n      if (prevPreset<255) { if (currentPreset==m_onPreset || currentPlaylist==m_onPreset) applyPreset(prevPreset, CALL_MODE_BUTTON_PRESET); }\r\n      else                { if (currentPreset==m_onPreset || currentPlaylist==m_onPreset) applyTemporaryPreset(); }\r\n      prevPreset = 0;\r\n      return;\r\n    }\r\n    // preset not assigned\r\n    if (bri != 0) {\r\n      briLast = bri;\r\n      bri = 0;\r\n      stateUpdated(CALL_MODE_BUTTON);\r\n    }\r\n  }\r\n}\r\n\r\nvoid PIRsensorSwitch::publishMqtt(bool switchOn)\r\n{\r\n#ifndef WLED_DISABLE_MQTT\r\n  //Check if MQTT Connected, otherwise it will crash the 8266\r\n  if (WLED_MQTT_CONNECTED) {\r\n    char buf[128];\r\n    sprintf_P(buf, PSTR(\"%s/motion\"), mqttDeviceTopic);   //max length: 33 + 7 = 40\r\n    mqtt->publish(buf, 0, false, switchOn?\"on\":\"off\");\r\n    // Domoticz formatted message\r\n    if (idx > 0) {\r\n      StaticJsonDocument <128> msg;\r\n      msg[F(\"idx\")]       = idx;\r\n      msg[F(\"RSSI\")]      = WiFi.RSSI();\r\n      msg[F(\"command\")]   = F(\"switchlight\");\r\n      msg[F(\"switchcmd\")] = switchOn ? F(\"On\") : F(\"Off\");\r\n      serializeJson(msg, buf, 128);\r\n      mqtt->publish(\"domoticz/in\", 0, false, buf);\r\n    }\r\n  }\r\n#endif\r\n}\r\n\r\nvoid PIRsensorSwitch::publishHomeAssistantAutodiscovery()\r\n{\r\n#ifndef WLED_DISABLE_MQTT\r\n  if (WLED_MQTT_CONNECTED) {\r\n    StaticJsonDocument<600> doc;\r\n    char uid[24], json_str[1024], buf[128];\r\n\r\n    sprintf_P(buf, PSTR(\"%s Motion\"), serverDescription); //max length: 33 + 7 = 40\r\n    doc[F(\"name\")] = buf;\r\n    sprintf_P(buf, PSTR(\"%s/motion\"), mqttDeviceTopic);   //max length: 33 + 7 = 40\r\n    doc[F(\"stat_t\")] = buf;\r\n    doc[F(\"pl_on\")]  = \"on\";\r\n    doc[F(\"pl_off\")] = \"off\";\r\n    sprintf_P(uid, PSTR(\"%s_motion\"), escapedMac.c_str());\r\n    doc[F(\"uniq_id\")] = uid;\r\n    doc[F(\"dev_cla\")] = F(\"motion\");\r\n    doc[F(\"exp_aft\")] = 1800;\r\n\r\n    JsonObject device = doc.createNestedObject(F(\"device\")); // attach the sensor to the same device\r\n    device[F(\"name\")] = serverDescription;\r\n    device[F(\"ids\")]  = String(F(\"wled-sensor-\")) + mqttClientID;\r\n    device[F(\"mf\")]   = F(WLED_BRAND);\r\n    device[F(\"mdl\")]  = F(WLED_PRODUCT_NAME);\r\n    device[F(\"sw\")]   = versionString;\r\n    \r\n    sprintf_P(buf, PSTR(\"homeassistant/binary_sensor/%s/config\"), uid);\r\n    DEBUG_PRINTLN(buf);\r\n    size_t payload_size = serializeJson(doc, json_str);\r\n    DEBUG_PRINTLN(json_str);\r\n\r\n    mqtt->publish(buf, 0, true, json_str, payload_size); // do we really need to retain?\r\n  }\r\n#endif\r\n}\r\n\r\nbool PIRsensorSwitch::updatePIRsensorState()\r\n{\r\n  bool stateChanged = false;\r\n  bool allOff = true;\r\n  for (int i = 0; i < PIR_SENSOR_MAX_SENSORS; i++) {\r\n    if (PIRsensorPin[i] < 0) continue;\r\n\r\n    bool pinState = digitalRead(PIRsensorPin[i]);\r\n    if (pinState != sensorPinState[i]) {\r\n      sensorPinState[i] = pinState; // change previous state\r\n      stateChanged = true;\r\n\r\n      if (sensorPinState[i] == HIGH) {\r\n        offTimerStart = 0;\r\n        allOff = false;\r\n        if (!m_mqttOnly && (!m_nightTimeOnly || (m_nightTimeOnly && !isDayTime()))) switchStrip(true);\r\n      }\r\n    }\r\n  }\r\n  if (stateChanged) {\r\n    publishMqtt(!allOff);\r\n    // start switch off timer\r\n    if (allOff) offTimerStart = millis();\r\n  }\r\n  return stateChanged;\r\n}\r\n\r\nbool PIRsensorSwitch::handleOffTimer()\r\n{\r\n  if (offTimerStart > 0 && millis() - offTimerStart > m_switchOffDelay) {\r\n    offTimerStart = 0;\r\n    if (!m_mqttOnly && (!m_nightTimeOnly || (m_nightTimeOnly && !isDayTime()) || PIRtriggered)) switchStrip(false);\r\n    return true;\r\n  }\r\n  return false;\r\n}\r\n\r\n//Functions called by WLED\r\n\r\nvoid PIRsensorSwitch::setup()\r\n{\r\n  for (int i = 0; i < PIR_SENSOR_MAX_SENSORS; i++) {\r\n    sensorPinState[i] = LOW;\r\n    if (PIRsensorPin[i] < 0) continue;\r\n    // pin retrieved from cfg.json (readFromConfig()) prior to running setup()\r\n    if (PinManager::allocatePin(PIRsensorPin[i], false, PinOwner::UM_PIR)) {\r\n      // PIR Sensor mode INPUT_PULLDOWN\r\n      #ifdef ESP8266\r\n      pinMode(PIRsensorPin[i], PIRsensorPin[i]==16 ? INPUT_PULLDOWN_16 : INPUT_PULLUP); // ESP8266 has INPUT_PULLDOWN on GPIO16 only\r\n      #else\r\n      pinMode(PIRsensorPin[i], INPUT_PULLDOWN);\r\n      #endif\r\n      sensorPinState[i] = digitalRead(PIRsensorPin[i]);\r\n    } else {\r\n      DEBUG_PRINT(F(\"PIRSensorSwitch pin \")); DEBUG_PRINTLN(i); DEBUG_PRINTLN(F(\" allocation failed.\"));\r\n      PIRsensorPin[i] = -1;  // allocation failed\r\n    }\r\n  }\r\n  initDone = true;\r\n}\r\n\r\nvoid PIRsensorSwitch::onMqttConnect(bool sessionPresent)\r\n{\r\n  if (HomeAssistantDiscovery) {\r\n    publishHomeAssistantAutodiscovery();\r\n  }\r\n}\r\n\r\nvoid PIRsensorSwitch::loop()\r\n{\r\n  // only check sensors 5x/s\r\n  if (!enabled || millis() - lastLoop < 200) return;\r\n  lastLoop = millis();\r\n\r\n  if (!updatePIRsensorState()) {\r\n    handleOffTimer();\r\n  }\r\n}\r\n\r\nvoid PIRsensorSwitch::addToJsonInfo(JsonObject &root)\r\n{\r\n  JsonObject user = root[\"u\"];\r\n  if (user.isNull()) user = root.createNestedObject(\"u\");\r\n\r\n  bool state = LOW;\r\n  for (int i = 0; i < PIR_SENSOR_MAX_SENSORS; i++)\r\n    if (PIRsensorPin[i] >= 0) state |= sensorPinState[i];\r\n\r\n  JsonArray infoArr = user.createNestedArray(FPSTR(_name));\r\n\r\n  String uiDomString;\r\n  if (enabled) {\r\n    if (offTimerStart > 0) {\r\n      uiDomString = \"\";\r\n      unsigned int offSeconds = (m_switchOffDelay - (millis() - offTimerStart)) / 1000;\r\n      if (offSeconds >= 3600) {\r\n        uiDomString += (offSeconds / 3600);\r\n        uiDomString += F(\"h \");\r\n        offSeconds %= 3600;\r\n      }\r\n      if (offSeconds >= 60) {\r\n        uiDomString += (offSeconds / 60);\r\n        offSeconds %= 60;\r\n      } else if (uiDomString.length() > 0) {\r\n        uiDomString += 0;\r\n      }\r\n      if (uiDomString.length() > 0) {\r\n        uiDomString += F(\"min \");\r\n      }\r\n      uiDomString += (offSeconds);\r\n      infoArr.add(uiDomString + F(\"s\"));\r\n    } else {\r\n      infoArr.add(state ? F(\"sensor on\") : F(\"inactive\"));\r\n    }\r\n  } else {\r\n    infoArr.add(F(\"disabled\"));\r\n  }\r\n\r\n  uiDomString  = F(\" <button class=\\\"btn btn-xs\\\" onclick=\\\"requestJson({\");\r\n  uiDomString += FPSTR(_name);\r\n  uiDomString += F(\":{\");\r\n  uiDomString += FPSTR(_enabled);\r\n  if (enabled) {\r\n    uiDomString += F(\":false}});\\\">\");\r\n    uiDomString += F(\"<i class=\\\"icons on\\\">&#xe325;</i>\");\r\n  } else {\r\n    uiDomString += F(\":true}});\\\">\");\r\n    uiDomString += F(\"<i class=\\\"icons off\\\">&#xe08f;</i>\");\r\n  }\r\n  uiDomString += F(\"</button>\");\r\n  infoArr.add(uiDomString);\r\n\r\n  if (enabled) {\r\n    JsonObject sensor = root[F(\"sensor\")];\r\n    if (sensor.isNull()) sensor = root.createNestedObject(F(\"sensor\"));\r\n    sensor[F(\"motion\")] = state || offTimerStart>0 ? true : false;\r\n  }\r\n}\r\n\r\nvoid PIRsensorSwitch::onStateChange(uint8_t mode) {\r\n  if (!initDone) return;\r\n  DEBUG_PRINT(F(\"PIR: offTimerStart=\")); DEBUG_PRINTLN(offTimerStart);\r\n  if (m_override && PIRtriggered && offTimerStart) { // debounce\r\n    // checking PIRtriggered and offTimerStart will prevent cancellation upon On trigger\r\n    DEBUG_PRINTLN(F(\"PIR: Canceled.\"));\r\n    offTimerStart = 0;\r\n    PIRtriggered = false;\r\n  }\r\n}\r\n\r\nvoid PIRsensorSwitch::readFromJsonState(JsonObject &root)\r\n{\r\n  if (!initDone) return;  // prevent crash on boot applyPreset()\r\n  JsonObject usermod = root[FPSTR(_name)];\r\n  if (!usermod.isNull()) {\r\n    if (usermod[FPSTR(_enabled)].is<bool>()) {\r\n      enabled = usermod[FPSTR(_enabled)].as<bool>();\r\n    }\r\n  }\r\n}\r\n\r\nvoid PIRsensorSwitch::addToConfig(JsonObject &root)\r\n{\r\n  JsonObject top = root.createNestedObject(FPSTR(_name));\r\n  top[FPSTR(_enabled)]        = enabled;\r\n  top[FPSTR(_switchOffDelay)] = m_switchOffDelay / 1000;\r\n  JsonArray pinArray          = top.createNestedArray(\"pin\");\r\n  for (int i = 0; i < PIR_SENSOR_MAX_SENSORS; i++) pinArray.add(PIRsensorPin[i]);\r\n  top[FPSTR(_onPreset)]       = m_onPreset;\r\n  top[FPSTR(_offPreset)]      = m_offPreset;\r\n  top[FPSTR(_nightTime)]      = m_nightTimeOnly;\r\n  top[FPSTR(_mqttOnly)]       = m_mqttOnly;\r\n  top[FPSTR(_offOnly)]        = m_offOnly;\r\n  top[FPSTR(_override)]       = m_override;\r\n  top[FPSTR(_haDiscovery)]    = HomeAssistantDiscovery;\r\n  top[FPSTR(_domoticzIDX)]    = idx;\r\n  DEBUG_PRINTLN(F(\"PIR config saved.\"));\r\n}\r\n\r\nvoid PIRsensorSwitch::appendConfigData()\r\n{\r\n  oappend(F(\"addInfo('PIRsensorSwitch:HA-discovery',1,'HA=Home Assistant');\"));     // 0 is field type, 1 is actual field\r\n  oappend(F(\"addInfo('PIRsensorSwitch:override',1,'Cancel timer on change');\"));    // 0 is field type, 1 is actual field\r\n  for (int i = 0; i < PIR_SENSOR_MAX_SENSORS; i++) {\r\n    char str[128];\r\n    sprintf_P(str, PSTR(\"addInfo('PIRsensorSwitch:pin[]',%d,'','#%d');\"), i, i);\r\n    oappend(str);\r\n  }\r\n}\r\n\r\nbool PIRsensorSwitch::readFromConfig(JsonObject &root)\r\n{\r\n  int8_t oldPin[PIR_SENSOR_MAX_SENSORS];\r\n  for (int i = 0; i < PIR_SENSOR_MAX_SENSORS; i++) {\r\n    oldPin[i] = PIRsensorPin[i];\r\n    PIRsensorPin[i] = -1;\r\n  }\r\n\r\n  DEBUG_PRINT(FPSTR(_name));\r\n  JsonObject top = root[FPSTR(_name)];\r\n  if (top.isNull()) {\r\n    DEBUG_PRINTLN(F(\": No config found. (Using defaults.)\"));\r\n    return false;\r\n  }\r\n\r\n  JsonArray pins = top[\"pin\"];\r\n  if (!pins.isNull()) {\r\n    for (size_t i = 0; i < PIR_SENSOR_MAX_SENSORS; i++)\r\n      if (i < pins.size()) PIRsensorPin[i] = pins[i] | PIRsensorPin[i];\r\n  } else {\r\n    PIRsensorPin[0] = top[\"pin\"] | oldPin[0];\r\n  }\r\n\r\n  enabled = top[FPSTR(_enabled)] | enabled;\r\n\r\n  m_switchOffDelay = (top[FPSTR(_switchOffDelay)] | m_switchOffDelay/1000) * 1000;\r\n\r\n  m_onPreset = top[FPSTR(_onPreset)] | m_onPreset;\r\n  m_onPreset = max(0,min(250,(int)m_onPreset));\r\n  m_offPreset = top[FPSTR(_offPreset)] | m_offPreset;\r\n  m_offPreset = max(0,min(250,(int)m_offPreset));\r\n\r\n  m_nightTimeOnly = top[FPSTR(_nightTime)] | m_nightTimeOnly;\r\n  m_mqttOnly      = top[FPSTR(_mqttOnly)] | m_mqttOnly;\r\n  m_offOnly       = top[FPSTR(_offOnly)] | m_offOnly;\r\n  m_override      = top[FPSTR(_override)] | m_override;\r\n  HomeAssistantDiscovery = top[FPSTR(_haDiscovery)] | HomeAssistantDiscovery;\r\n  idx             = top[FPSTR(_domoticzIDX)] | idx;\r\n\r\n  if (!initDone) {\r\n    // reading config prior to setup()\r\n    DEBUG_PRINTLN(F(\" config loaded.\"));\r\n  } else {\r\n    for (int i = 0; i < PIR_SENSOR_MAX_SENSORS; i++)\r\n      if (oldPin[i] >= 0) PinManager::deallocatePin(oldPin[i], PinOwner::UM_PIR);\r\n    setup();\r\n    DEBUG_PRINTLN(F(\" config (re)loaded.\"));\r\n  }\r\n  // use \"return !top[\"newestParameter\"].isNull();\" when updating Usermod with new features\r\n  return !(pins.isNull() || pins.size() != PIR_SENSOR_MAX_SENSORS);\r\n}\r\n\r\n\r\nstatic PIRsensorSwitch pir_sensor_switch;\r\nREGISTER_USERMOD(pir_sensor_switch);"
  },
  {
    "path": "usermods/PIR_sensor_switch/library.json",
    "content": "{\n  \"name\": \"PIR_sensor_switch\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/PIR_sensor_switch/readme.md",
    "content": "# PIR sensor switch\n\nThis usermod-v2 modification allows the connection of a PIR sensor to switch on the LED strip when motion is detected. The switch-off occurs ten minutes after no more motion is detected.\n\n_Story:_\n\nI use the PIR Sensor to automatically turn on the WLED analog clock in my home office room when I am there.\nThe 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.\n\n## Web interface\n\nThe info page in the web interface shows the remaining time of the off timer. Usermod can also be temporarily disbled/enabled from the info page by clicking PIR button.\n\n## Sensor connection\n\nMy setup uses an HC-SR501 or HC-SR602 sensor, an HC-SR505 should also work.\n\nThe usermod uses GPIO13 (D1 mini pin D7) by default for the sensor signal, but can be changed in the Usermod settings page.\n[This example page](http://www.esp8266learning.com/wemos-mini-pir-sensor-example.php) describes how to connect the sensor.\n\nUse the potentiometers on the sensor to set the time delay to the minimum and the sensitivity to about half, or slightly above.\nYou can also use usermod's off timer instead of sensor's. In such case rotate the potentiometer to its shortest time possible (or use SR602 which lacks such potentiometer).\n\n## Usermod installation\n\n**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`.\n\n## API to enable/disable the PIR sensor from outside. For example from another usermod\n\nTo query or change the PIR sensor state the methods `bool PIRsensorEnabled()` and `void EnablePIRsensor(bool enable)` are available.\n\nWhen the PIR sensor state changes an MQTT message is broadcasted with topic `wled/deviceMAC/motion` and message `on` or `off`.\nUsermod 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\n(assuming NTP and latitude/longitude are set to determine sunrise/sunset times).\n\n### There are two options to get access to the usermod instance\n\n_1._ Include `usermod_PIR_sensor_switch.h` **before** you include other usermods in `usermods_list.cpp'\n\nor\n\n_2._ Use `#include \"usermod_PIR_sensor_switch.h\"` at the top of the `usermod.h` where you need it.\n\n**Example usermod.h :**\n\n```cpp\n#include \"wled.h\"\n\n#include \"usermod_PIR_sensor_switch.h\"\n\nclass MyUsermod : public Usermod {\n  //...\n\n  void togglePIRSensor() {\n    #ifdef USERMOD_PIR_SENSOR_SWITCH\n    PIRsensorSwitch *PIRsensor = (PIRsensorSwitch::*) UsermodManager::lookup(USERMOD_ID_PIRSWITCH);\n    if (PIRsensor != nullptr) {\n      PIRsensor->EnablePIRsensor(!PIRsensor->PIRsensorEnabled());\n    }\n    #endif\n  }\n  //...\n};\n```\n\n### Configuration options\n\nUsermod can be configured via the Usermods settings page.\n\n* `PIRenabled` - enable/disable usermod\n* `pin` - dynamically change GPIO pin where PIR sensor is attached to ESP\n* `PIRoffSec` - number of seconds after PIR sensor deactivates when usermod triggers Off preset (or turns WLED off)\n* `on-preset` - preset triggered when PIR activates (if this is 0 it will just turn WLED on)\n* `off-preset` - preset triggered when PIR deactivates (if this is 0 it will just turn WLED off)\n* `nighttime-only` - enable triggering only between sunset and sunrise (you will need to set up _NTP_, _Lat_ & _Lon_ in Time & Macro settings)\n* `mqtt-only` - send only MQTT messages, do not interact with WLED\n* `off-only` - only trigger presets or turn WLED on/off if WLED is not already on (displaying effect)\n* `notifications` - enable or disable sending notifications to other WLED instances using Sync button\n* `HA-discovery` - enable automatic discovery in Home Assistant\n* `override` - override PIR input when WLED state is changed using UI\n* `domoticz-idx` - Domoticz virtual switch ID (used with MQTT `domoticz/in`)\n\nHave fun - @gegu & @blazoncek\n\n## Change log\n\n2021-04\n\n* Adaptation for runtime configuration.\n\n2021-11\n\n* Added information about dynamic configuration options\n* Added option to temporary enable/disable usermod from WLED UI (Info dialog)\n\n2022-11\n\n* Added compile time option for off timer.\n* Added Home Assistant autodiscovery MQTT broadcast.\n* Updated info on compiling.\n\n2023-??\n\n* Override option\n* Domoticz virtual switch ID (used with MQTT `domoticz/in`)\n\n2024-02\n\n* Added compile time option to expand number of PIR sensors (they are logically ORed) `-D PIR_SENSOR_MAX_SENSORS=3`\n"
  },
  {
    "path": "usermods/PS_Comet/PS_Comet.cpp",
    "content": "#include \"wled.h\"\n#include \"FXparticleSystem.h\"\n\nunsigned long nextCometCreationTime = 0;\n\n#define FX_FALLBACK_STATIC { SEGMENT.fill(SEGCOLOR(0)); return; }\n// Use UINT32_MAX - 1 for the \"no comet\" case so we can add 1 later and not have it overflow\n#define NULL_INDEX UINT32_MAX - 1\n\n///////////////////////\n//  Effect Function  //\n///////////////////////\n\nvoid mode_pscomet() {\n  ParticleSystem2D *PartSys = nullptr;\n  uint32_t i;\n\n  if (SEGMENT.call == 0) { // Initialization\n    // Try to allocate one comet for every column\n    if (!initParticleSystem2D(PartSys, SEGMENT.vWidth())) {\n      FX_FALLBACK_STATIC; // Allocation failed or not 2D\n    }\n    PartSys->setMotionBlur(170); // Enable motion blur\n    PartSys->setParticleSize(0); // Allow small comets to be a single pixel wide\n  }\n  else {\n    PartSys = reinterpret_cast<ParticleSystem2D *>(SEGENV.data); // If not first call, use existing data\n  }\n  if (PartSys == nullptr || SEGMENT.vHeight() < 2 || SEGMENT.vWidth() < 2) {\n    FX_FALLBACK_STATIC;\n  }\n\n  PartSys->updateSystem(); // Update system properties (dimensions and data pointers)\n\n  auto has_fallen_off_screen = [PartSys](uint32_t particleIndex) {\n    return particleIndex < PartSys->numSources\n      ? PartSys->sources[particleIndex].source.y < PartSys->maxY * -1\n      : true;\n  };\n\n  // This will be SEGMENT.vWidth() unless the particle system had insufficient memory\n  uint32_t numComets = PartSys->numSources;\n  // Pick a random column for a new comet to spawn, but reset it to null if it's not time yet or there's already a\n  // comet nearby\n  uint32_t chosenIndex = hw_random(numComets);\n  if (\n    strip.now < nextCometCreationTime\n    || !has_fallen_off_screen(chosenIndex - 1)\n    || !has_fallen_off_screen(chosenIndex)\n    || !has_fallen_off_screen(chosenIndex + 1)\n  ) {\n    chosenIndex = NULL_INDEX;\n  } else {\n    uint16_t cometFrequencyDelay = 2040 - (SEGMENT.intensity << 3);\n    nextCometCreationTime = strip.now + cometFrequencyDelay + hw_random16(cometFrequencyDelay);\n  }\n  uint8_t canLargeCometSpawn =\n    // Slider 3 determines % of large comets with extra particle sources on their sides\n    SEGMENT.custom1 > hw_random8(254)\n    && chosenIndex != 0\n    && chosenIndex != numComets - 1;\n  uint8_t fallingSpeed = 1 + (SEGMENT.speed >> 2);\n\n  // Update the comets\n  for (i = 0; i < numComets; i++) {\n    auto& source = PartSys->sources[i];\n    auto& sourceParticle = source.source;\n\n    if (!has_fallen_off_screen(i)) {\n      // Active comets fall downwards and emit flames\n      sourceParticle.y -= fallingSpeed;\n      source.vy = (SEGMENT.speed >> 5) - fallingSpeed; // Emitting speed (upwards)\n      PartSys->flameEmit(PartSys->sources[i]);\n      continue;\n    }\n\n    bool isChosenComet = i == chosenIndex;\n    bool isChosenSideComet =\n      canLargeCometSpawn &&\n      (i == chosenIndex - 1 || i == chosenIndex + 1);\n\n    // Chosen comets respawn at the top\n    if (isChosenComet || isChosenSideComet) {\n      // Map the comet index into an output pixel index\n      sourceParticle.x = i * PartSys->maxX / (SEGMENT.vWidth() - 1);\n      // Spawn a bit above the top to avoid popping into view\n      sourceParticle.y = PartSys->maxY + (2 * fallingSpeed);\n      if (isChosenComet) {\n        // Slider 4 controls comet length via particle lifetime and fire intensity adjustments\n        source.maxLife = 16 + (SEGMENT.custom2 >> 2);\n        source.minLife = source.maxLife >> 1;\n        sourceParticle.ttl = 16 - (SEGMENT.custom2 >> 4);\n      } else {\n        // Side comets have fixed length\n        source.maxLife = 18;\n        source.minLife = 14;\n        sourceParticle.ttl = 16;\n        // Shift side comets up by 1 pixel\n        sourceParticle.y += 2 * PartSys->maxY / (SEGMENT.vHeight() - 1);\n      }\n    }\n  }\n\n  // Slider 4 controls comet length via particle lifetime and fire intensity adjustments\n  PartSys->updateFire(max(255U - SEGMENT.custom2, 45U));\n}\nstatic const char _data_FX_MODE_PSCOMET[] PROGMEM = \"PS Comet@Falling Speed,Comet Frequency,Large Comet Probability,Comet Length;;!;2;pal=35,sx=128,ix=255,c1=32,c2=128\";\n\n/////////////////////\n//  UserMod Class  //\n/////////////////////\n\nclass PSCometUsermod : public Usermod {\n public:\n  void setup() override {\n    strip.addEffect(255, &mode_pscomet, _data_FX_MODE_PSCOMET);\n  }\n\n  void loop() override {}\n};\n\nstatic PSCometUsermod ps_comet;\nREGISTER_USERMOD(ps_comet);\n"
  },
  {
    "path": "usermods/PS_Comet/README.md",
    "content": "## Description\n\nA 2D falling comet effect similar to \"Matrix\" but with a fire particle simulation to enhance the comet trail visuals. Works with custom color palettes, defaulting to \"Fire\". Supports \"small\" and \"large\" comets which are 1px and 3px wide respectively.\n\nDemo: [https://imgur.com/a/i1v5WAy](https://imgur.com/a/i1v5WAy)\n\n## Installation\n\nTo activate the usermod, add the following line to your platformio_override.ini\n```ini\ncustom_usermods = ps_comet\n```\nOr if you are already using a usermod, append ps_comet to the list\n```ini\ncustom_usermods = audioreactive ps_comet\n```\n\nYou should now see \"PS Comet\" appear in your effect list.\n\n## Parameters\n\n1. **Falling Speed** sets how fast the comets fall\n2. **Comet Frequency** determines how many comets are on screen at a time\n3. **Large Comet Probability** determines how often large 3px wide comets spawn\n4. **Comet Length** sets how far comet trails stretch vertically"
  },
  {
    "path": "usermods/PS_Comet/library.json",
    "content": "{\n  \"name\": \"PS Comet\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/PWM_fan/PWM_fan.cpp",
    "content": "#include \"wled.h\"\n\n#if defined(USERMOD_DALLASTEMPERATURE) \n#include \"UsermodTemperature.h\"\n#elif defined(USERMOD_SHT)\n#include \"ShtUsermod.h\"\n#else\n#error The \"PWM fan\" usermod requires \"Dallas Temeprature\" or \"SHT\" usermod to function properly.\n#endif\n\n\n\n// PWM & tacho code curtesy of @KlausMu\n// https://github.com/KlausMu/esp32-fan-controller/tree/main/src\n// adapted for WLED usermod by @blazoncek\n\n#ifndef TACHO_PIN\n  #define TACHO_PIN -1\n#endif\n\n#ifndef PWM_PIN\n  #define PWM_PIN -1\n#endif\n\n// tacho counter\nstatic volatile unsigned long counter_rpm = 0;\n// Interrupt counting every rotation of the fan\n// https://desire.giesecke.tk/index.php/2018/01/30/change-global-variables-from-isr/\nstatic void IRAM_ATTR rpm_fan() {\n  counter_rpm++;\n}\n\n\nclass PWMFanUsermod : public Usermod {\n\n  private:\n\n    bool initDone = false;\n    bool enabled = true;\n    unsigned long msLastTachoMeasurement = 0;\n    uint16_t last_rpm = 0;\n    #ifdef ARDUINO_ARCH_ESP32\n    uint8_t pwmChannel = 255;\n    #endif\n    bool lockFan = false;\n\n    #ifdef USERMOD_DALLASTEMPERATURE\n    UsermodTemperature* tempUM;\n    #elif defined(USERMOD_SHT)\n    ShtUsermod* tempUM;\n    #endif\n\n    // configurable parameters\n    int8_t  tachoPin          = TACHO_PIN;\n    int8_t  pwmPin            = PWM_PIN;\n    uint8_t tachoUpdateSec    = 30;\n    float   targetTemperature = 35.0;\n    uint8_t minPWMValuePct    = 0;\n    uint8_t maxPWMValuePct    = 100;\n    uint8_t numberOfInterrupsInOneSingleRotation = 2;     // Number of interrupts ESP32 sees on tacho signal on a single fan rotation. All the fans I've seen trigger two interrups.\n    uint8_t pwmValuePct       = 0;\n\n    // constant values\n    static const uint8_t _pwmMaxValue     = 255;\n    static const uint8_t _pwmMaxStepCount = 7;\n    float _pwmTempStepSize = 0.5f;\n\n    // strings to reduce flash memory usage (used more than twice)\n    static const char _name[];\n    static const char _enabled[];\n    static const char _tachoPin[];\n    static const char _pwmPin[];\n    static const char _temperature[];\n    static const char _tachoUpdateSec[];\n    static const char _minPWMValuePct[];\n    static const char _maxPWMValuePct[];\n    static const char _IRQperRotation[];\n    static const char _speed[];\n    static const char _lock[];\n\n    void initTacho(void) {\n      if (tachoPin < 0 || !PinManager::allocatePin(tachoPin, false, PinOwner::UM_Unspecified)){\n        tachoPin = -1;\n        return;\n      }\n      pinMode(tachoPin, INPUT);\n      digitalWrite(tachoPin, HIGH);\n      attachInterrupt(digitalPinToInterrupt(tachoPin), rpm_fan, FALLING);\n      DEBUG_PRINTLN(F(\"Tacho sucessfully initialized.\"));\n    }\n\n    void deinitTacho(void) {\n      if (tachoPin < 0) return;\n      detachInterrupt(digitalPinToInterrupt(tachoPin));\n      PinManager::deallocatePin(tachoPin, PinOwner::UM_Unspecified);\n      tachoPin = -1;\n    }\n\n    void updateTacho(void) {\n      // store milliseconds when tacho was measured the last time\n      msLastTachoMeasurement = millis();\n      if (tachoPin < 0) return;\n\n      // start of tacho measurement\n      // detach interrupt while calculating rpm\n      detachInterrupt(digitalPinToInterrupt(tachoPin)); \n      // calculate rpm\n      last_rpm = (counter_rpm * 60) / numberOfInterrupsInOneSingleRotation;\n      last_rpm /= tachoUpdateSec;\n      // reset counter\n      counter_rpm = 0; \n      // attach interrupt again\n      attachInterrupt(digitalPinToInterrupt(tachoPin), rpm_fan, FALLING);\n    }\n\n    // https://randomnerdtutorials.com/esp32-pwm-arduino-ide/\n    void initPWMfan(void) {\n      if (pwmPin < 0 || !PinManager::allocatePin(pwmPin, true, PinOwner::UM_Unspecified)) {\n        enabled = false;\n        pwmPin = -1;\n        return;\n      }\n\n      #ifdef ESP8266\n      analogWriteRange(255);\n      analogWriteFreq(WLED_PWM_FREQ);\n      #else\n      pwmChannel = PinManager::allocateLedc(1);\n      if (pwmChannel == 255) { //no more free LEDC channels\n        deinitPWMfan(); return;\n      }\n      // configure LED PWM functionalitites\n      ledcSetup(pwmChannel, 25000, 8);\n      // attach the channel to the GPIO to be controlled\n      ledcAttachPin(pwmPin, pwmChannel);\n      #endif\n      DEBUG_PRINTLN(F(\"Fan PWM sucessfully initialized.\"));\n    }\n\n    void deinitPWMfan(void) {\n      if (pwmPin < 0) return;\n\n      PinManager::deallocatePin(pwmPin, PinOwner::UM_Unspecified);\n      #ifdef ARDUINO_ARCH_ESP32\n      PinManager::deallocateLedc(pwmChannel, 1);\n      #endif\n      pwmPin = -1;\n    }\n\n    void updateFanSpeed(uint8_t pwmValue){\n      if (!enabled || pwmPin < 0) return;\n\n      #ifdef ESP8266\n      analogWrite(pwmPin, pwmValue);\n      #else\n      ledcWrite(pwmChannel, pwmValue);\n      #endif\n    }\n\n    float getActualTemperature(void) {\n      #if defined(USERMOD_DALLASTEMPERATURE) || defined(USERMOD_SHT)\n      if (tempUM != nullptr)\n        return tempUM->getTemperatureC();\n      #endif\n      return -127.0f;\n    }\n\n    void setFanPWMbasedOnTemperature(void) {\n      float temp = getActualTemperature();\n      // dividing minPercent and maxPercent into equal pwmvalue sizes\n      int pwmStepSize = ((maxPWMValuePct - minPWMValuePct) * _pwmMaxValue) / (_pwmMaxStepCount*100);\n      int pwmStep = calculatePwmStep(temp - targetTemperature);\n      // minimum based on full speed - not entered MaxPercent \n      int pwmMinimumValue = (minPWMValuePct * _pwmMaxValue) / 100;\n      updateFanSpeed(pwmMinimumValue + pwmStep*pwmStepSize);\n    }\n\n    uint8_t calculatePwmStep(float diffTemp){\n      if ((diffTemp == NAN) || (diffTemp <= -100.0)) {\n        DEBUG_PRINTLN(F(\"WARNING: no temperature value available. Cannot do temperature control. Will set PWM fan to 255.\"));\n        return _pwmMaxStepCount;\n      }\n      if(diffTemp <=0){\n        return 0;\n      }\n      int calculatedStep = (diffTemp / _pwmTempStepSize)+1;\n      // anything greater than max stepcount gets max \n      return (uint8_t)min((int)_pwmMaxStepCount,calculatedStep);      \n    }\n\n  public:\n\n    // gets called once at boot. Do all initialization that doesn't depend on\n    // network here\n    void setup() override {\n      #ifdef USERMOD_DALLASTEMPERATURE   \n      // This Usermod requires Temperature usermod\n      tempUM = (UsermodTemperature*) UsermodManager::lookup(USERMOD_ID_TEMPERATURE);\n      #elif defined(USERMOD_SHT)\n      tempUM = (ShtUsermod*) UsermodManager::lookup(USERMOD_ID_SHT);\n      #endif\n      initTacho();\n      initPWMfan();\n      updateFanSpeed((minPWMValuePct * 255) / 100); // inital fan speed\n      initDone = true;\n    }\n\n    // gets called every time WiFi is (re-)connected. Initialize own network\n    // interfaces here\n    void connected() override {}\n\n    /*\n     * Da loop.\n     */\n    void loop() override {\n      if (!enabled || strip.isUpdating()) return;\n\n      unsigned long now = millis();\n      if ((now - msLastTachoMeasurement) < (tachoUpdateSec * 1000)) return;\n\n      updateTacho();\n      if (!lockFan) setFanPWMbasedOnTemperature();\n    }\n\n    /*\n     * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.\n     * Creating an \"u\" object allows you to add custom key/value pairs to the Info section of the WLED web UI.\n     * Below it is shown how this could be used for e.g. a light sensor\n     */\n    void addToJsonInfo(JsonObject& root) override {\n      JsonObject user = root[\"u\"];\n      if (user.isNull()) user = root.createNestedObject(\"u\");\n\n      JsonArray infoArr = user.createNestedArray(FPSTR(_name));\n      String uiDomString = F(\"<button class=\\\"btn btn-xs\\\" onclick=\\\"requestJson({'\");\n      uiDomString += FPSTR(_name);\n      uiDomString += F(\"':{'\");\n      uiDomString += FPSTR(_enabled);\n      uiDomString += F(\"':\");\n      uiDomString += enabled ? \"false\" : \"true\";\n      uiDomString += F(\"}});\\\"><i class=\\\"icons \");\n      uiDomString += enabled ? \"on\" : \"off\";\n      uiDomString += F(\"\\\">&#xe08f;</i></button>\");\n      infoArr.add(uiDomString);\n\n      if (enabled) {\n        JsonArray infoArr = user.createNestedArray(F(\"Manual\"));\n        String uiDomString = F(\"<div class=\\\"slider\\\"><div class=\\\"sliderwrap il\\\"><input class=\\\"noslide\\\" onchange=\\\"requestJson({'\");\n        uiDomString += FPSTR(_name);\n        uiDomString += F(\"':{'\");\n        uiDomString += FPSTR(_speed);\n        uiDomString += F(\"':parseInt(this.value)}});\\\" oninput=\\\"updateTrail(this);\\\" max=100 min=0 type=\\\"range\\\" value=\");\n        uiDomString += pwmValuePct;\n        uiDomString += F(\" /><div class=\\\"sliderdisplay\\\"></div></div></div>\"); //<output class=\\\"sliderbubble\\\"></output>\n        infoArr.add(uiDomString);\n\n        JsonArray data = user.createNestedArray(F(\"Speed\"));\n        if (tachoPin >= 0) {\n          data.add(last_rpm);\n          data.add(F(\"rpm\"));\n        } else {\n          if (lockFan) data.add(F(\"locked\"));\n          else         data.add(F(\"auto\"));\n        }\n      }\n    }\n\n    /*\n     * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).\n     * Values in the state object may be modified by connected clients\n     */\n    //void addToJsonState(JsonObject& root) {\n    //}\n\n    /*\n     * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).\n     * Values in the state object may be modified by connected clients\n     */\n    void readFromJsonState(JsonObject& root) override {\n      if (!initDone) return;  // prevent crash on boot applyPreset()\n      JsonObject usermod = root[FPSTR(_name)];\n      if (!usermod.isNull()) {\n        if (usermod[FPSTR(_enabled)].is<bool>()) {\n          enabled = usermod[FPSTR(_enabled)].as<bool>();\n          if (!enabled) updateFanSpeed(0);\n        }\n        if (enabled && !usermod[FPSTR(_speed)].isNull() && usermod[FPSTR(_speed)].is<int>()) {\n          pwmValuePct = usermod[FPSTR(_speed)].as<int>();\n          updateFanSpeed((constrain(pwmValuePct,0,100) * 255) / 100);\n          if (pwmValuePct) lockFan = true;\n        }\n        if (enabled && !usermod[FPSTR(_lock)].isNull() && usermod[FPSTR(_lock)].is<bool>()) {\n          lockFan = usermod[FPSTR(_lock)].as<bool>();\n        }\n      }\n    }\n\n    /*\n     * addToConfig() can be used to add custom persistent settings to the cfg.json file in the \"um\" (usermod) object.\n     * It will be called by WLED when settings are actually saved (for example, LED settings are saved)\n     * If you want to force saving the current state, use serializeConfig() in your loop().\n     * \n     * CAUTION: serializeConfig() will initiate a filesystem write operation.\n     * It might cause the LEDs to stutter and will cause flash wear if called too often.\n     * Use it sparingly and always in the loop, never in network callbacks!\n     * \n     * addToConfig() will also not yet add your setting to one of the settings pages automatically.\n     * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually.\n     * \n     * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings!\n     */\n    void addToConfig(JsonObject& root) override {\n      JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname\n      top[FPSTR(_enabled)]        = enabled;\n      top[FPSTR(_pwmPin)]         = pwmPin;\n      top[FPSTR(_tachoPin)]       = tachoPin;\n      top[FPSTR(_tachoUpdateSec)] = tachoUpdateSec;\n      top[FPSTR(_temperature)]    = targetTemperature;\n      top[FPSTR(_minPWMValuePct)] = minPWMValuePct;\n      top[FPSTR(_maxPWMValuePct)] = maxPWMValuePct;\n      top[FPSTR(_IRQperRotation)] = numberOfInterrupsInOneSingleRotation;\n      DEBUG_PRINTLN(F(\"Autosave config saved.\"));\n    }\n\n    /*\n     * readFromConfig() can be used to read back the custom settings you added with addToConfig().\n     * This is called by WLED when settings are loaded (currently this only happens once immediately after boot)\n     * \n     * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes),\n     * 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.\n     * If you don't know what that is, don't fret. It most likely doesn't affect your use case :)\n     * \n     * The function should return true if configuration was successfully loaded or false if there was no configuration.\n     */\n    bool readFromConfig(JsonObject& root) override {\n      int8_t newTachoPin = tachoPin;\n      int8_t newPwmPin   = pwmPin;\n\n      JsonObject top = root[FPSTR(_name)];\n      DEBUG_PRINT(FPSTR(_name));\n      if (top.isNull()) {\n        DEBUG_PRINTLN(F(\": No config found. (Using defaults.)\"));\n        return false;\n      }\n\n      enabled           = top[FPSTR(_enabled)] | enabled;\n      newTachoPin       = top[FPSTR(_tachoPin)] | newTachoPin;\n      newPwmPin         = top[FPSTR(_pwmPin)] | newPwmPin;\n      tachoUpdateSec    = top[FPSTR(_tachoUpdateSec)] | tachoUpdateSec;\n      tachoUpdateSec    = (uint8_t) max(1,(int)tachoUpdateSec); // bounds checking\n      targetTemperature = top[FPSTR(_temperature)] | targetTemperature;\n      minPWMValuePct    = top[FPSTR(_minPWMValuePct)] | minPWMValuePct;\n      minPWMValuePct    = (uint8_t) min(100,max(0,(int)minPWMValuePct)); // bounds checking\n      maxPWMValuePct    = top[FPSTR(_maxPWMValuePct)] | maxPWMValuePct;\n      maxPWMValuePct    = (uint8_t) min(100,max((int)minPWMValuePct,(int)maxPWMValuePct)); // bounds checking\n      numberOfInterrupsInOneSingleRotation = top[FPSTR(_IRQperRotation)] | numberOfInterrupsInOneSingleRotation;\n      numberOfInterrupsInOneSingleRotation = (uint8_t) max(1,(int)numberOfInterrupsInOneSingleRotation); // bounds checking\n\n      if (!initDone) {\n        // first run: reading from cfg.json\n        tachoPin = newTachoPin;\n        pwmPin   = newPwmPin;\n        DEBUG_PRINTLN(F(\" config loaded.\"));\n      } else {\n        DEBUG_PRINTLN(F(\" config (re)loaded.\"));\n        // changing paramters from settings page\n        if (tachoPin != newTachoPin || pwmPin != newPwmPin) {\n          DEBUG_PRINTLN(F(\"Re-init pins.\"));\n          // deallocate pin and release interrupts\n          deinitTacho();\n          deinitPWMfan();\n          tachoPin = newTachoPin;\n          pwmPin   = newPwmPin;\n          // initialise\n          setup();\n        }\n      }\n\n      // use \"return !top[\"newestParameter\"].isNull();\" when updating Usermod with new features\n      return !top[FPSTR(_IRQperRotation)].isNull();\n  }\n\n    /*\n     * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).\n     * This could be used in the future for the system to determine whether your usermod is installed.\n     */\n    uint16_t getId() override {\n        return USERMOD_ID_PWM_FAN;\n    }\n};\n\n// strings to reduce flash memory usage (used more than twice)\nconst char PWMFanUsermod::_name[]           PROGMEM = \"PWM-fan\";\nconst char PWMFanUsermod::_enabled[]        PROGMEM = \"enabled\";\nconst char PWMFanUsermod::_tachoPin[]       PROGMEM = \"tacho-pin\";\nconst char PWMFanUsermod::_pwmPin[]         PROGMEM = \"PWM-pin\";\nconst char PWMFanUsermod::_temperature[]    PROGMEM = \"target-temp-C\";\nconst char PWMFanUsermod::_tachoUpdateSec[] PROGMEM = \"tacho-update-s\";\nconst char PWMFanUsermod::_minPWMValuePct[] PROGMEM = \"min-PWM-percent\";\nconst char PWMFanUsermod::_maxPWMValuePct[] PROGMEM = \"max-PWM-percent\";\nconst char PWMFanUsermod::_IRQperRotation[] PROGMEM = \"IRQs-per-rotation\";\nconst char PWMFanUsermod::_speed[]          PROGMEM = \"speed\";\nconst char PWMFanUsermod::_lock[]           PROGMEM = \"lock\";\n\n\nstatic PWMFanUsermod pwm_fan;\nREGISTER_USERMOD(pwm_fan);"
  },
  {
    "path": "usermods/PWM_fan/library.json",
    "content": "{\n  \"name\": \"PWM_fan\",\n  \"build\": {\n    \"libArchive\": false,\n    \"extraScript\": \"setup_deps.py\"\n  }\n}"
  },
  {
    "path": "usermods/PWM_fan/readme.md",
    "content": "# PWM fan\n\nv2 Usermod to to control PWM fan with RPM feedback and temperature control\n\nThis usermod requires the Dallas Temperature usermod to obtain temperature information. If it's not available, the fan will run at 100% speed.\nIf the fan does not have _tachometer_ (RPM) output you can set the _tachometer-pin_ to -1 to disable that feature.\n\nYou can also set the threshold temperature at which fan runs at lowest speed. If the measured temperature is 3°C greater than the threshold temperature, the fan will run at 100%.\n\nIf the _tachometer_ is supported, the current speed (in RPM) will be displayed on the WLED Info page.\n\n## Installation\n\nAdd the `PWM_fan` to `custom_usermods` in your `platformio.ini` (or `platformio_override.ini`)\nYou will also need `Temperature` or `sht`.\n\n### Define Your Options\n\nAll of the parameters are configured during run-time using Usermods settings page.\nThis includes:\n\n* PWM output pin (can be configured at compile time `-D PWM_PIN=xx`)\n* tachometer input pin (can be configured at compile time `-D TACHO_PIN=xx`)\n* sampling frequency in seconds\n* threshold temperature in degrees Celsius\n\n_NOTE:_ You may also need to tweak Dallas Temperature usermod sampling frequency to match PWM fan sampling frequency.\n\n### PlatformIO requirements\n\nNo special requirements.\n\n## Control PWM fan speed using JSON API\n\ne.g. you can use `{\"PWM-fan\":{\"speed\":30,\"lock\":true}}` to lock fan speed to 30 percent of maximum. (replace 30 with an arbitrary value between 0 and 100)\nIf you include `speed` property you can set fan speed as a percentage (%) of maximum speed.\nIf you include `lock` property you can lock (_true_) or unlock (_false_) the fan speed.\nIf the fan speed is unlocked, it will revert to temperature controlled speed on the next update cycle. Once fan speed is locked it will remain so until it is unlocked by the next API call.\n\n## Change Log\n\n2021-10\n\n* First public release\n\n2022-05\n\n* Added JSON API call to allow changing of speed\n"
  },
  {
    "path": "usermods/PWM_fan/setup_deps.py",
    "content": "from platformio.package.meta import PackageSpec\nImport('env')\n\n\nlibs = [PackageSpec(lib).name for lib in env.GetProjectOption(\"lib_deps\",[])]\n# Check for dependencies\nif \"Temperature\" in libs:\n    env.Append(CPPDEFINES=[(\"USERMOD_DALLASTEMPERATURE\")])\nelif \"sht\" in libs:\n    env.Append(CPPDEFINES=[(\"USERMOD_SHT\")])\nelif \"PWM_fan\" in libs:  # The script can be run if this module was previously selected\n    raise RuntimeError(\"PWM_fan usermod requires Temperature or sht to be enabled\")\n"
  },
  {
    "path": "usermods/RTC/RTC.cpp",
    "content": "#include \"src/dependencies/time/DS1307RTC.h\"\n#include \"wled.h\"\n\n//Connect DS1307 to standard I2C pins (ESP32: GPIO 21 (SDA)/GPIO 22 (SCL))\n\nclass RTCUsermod : public Usermod {\n  private:\n    unsigned long lastTime = 0;\n    bool disabled = false;\n  public:\n\n    void setup() {\n      if (i2c_scl<0 || i2c_sda<0) { disabled = true; return; }\n      RTC.begin();\n      time_t rtcTime = RTC.get();\n      if (rtcTime) {\n        toki.setTime(rtcTime,TOKI_NO_MS_ACCURACY,TOKI_TS_RTC);\n        updateLocalTime();\n      } else {\n        if (!RTC.chipPresent()) disabled = true; //don't waste time if H/W error\n      }\n    }\n\n    void loop() {\n      if (disabled || strip.isUpdating()) return;\n      if (toki.isTick()) {\n        time_t t = toki.second();\n        if (t != RTC.get()) RTC.set(t); //set RTC to NTP/UI-provided value\n      }\n    }\n\n    /*\n     * addToConfig() can be used to add custom persistent settings to the cfg.json file in the \"um\" (usermod) object.\n     * It will be called by WLED when settings are actually saved (for example, LED settings are saved)\n     * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings!\n     */\n//    void addToConfig(JsonObject& root)\n//    {\n//      JsonObject top = root.createNestedObject(\"RTC\");\n//      JsonArray pins = top.createNestedArray(\"pin\");\n//      pins.add(i2c_scl);\n//      pins.add(i2c_sda);\n//    }\n\n    uint16_t getId()\n    {\n      return USERMOD_ID_RTC;\n    }\n};\n\nstatic RTCUsermod rtc;\nREGISTER_USERMOD(rtc);"
  },
  {
    "path": "usermods/RTC/library.json",
    "content": "{\n  \"name\": \"RTC\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/RTC/readme.md",
    "content": "# DS1307/DS3231 Real time clock\n\nGets the time from I2C RTC module on boot. This allows clock operation if WiFi is not available.\nThe stored time is updated each time NTP is synced. \n\n## Installation \n\nAdd the build flag `-D USERMOD_RTC` to your platformio environment.\n"
  },
  {
    "path": "usermods/RelayBlinds/index.htm",
    "content": "<!DOCTYPE html>\n<html>\n<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1, minimum-scale=1\">\n  <meta charset=\"utf-8\">\n  <title>Blinds</title>\n  <script>\n      strA = \"\";\n    function send()\n    {\n      nocache = \"&nocache=\" + Math.random() * 1000000;\n      var request = new XMLHttpRequest();\n      // send HTTP request\n      request.open(\"GET\", \"win/\" + strA +nocache, true);\n      request.send(null);\n      strA = \"\";\n    }\n    function up()\n    {\n      strA = \"&U0=2\";\n      send();\n    }\n    function down()\n    {\n      strA = \"&U0=1\";\n      send();\n    }\n    function OpenSettings()\n    {\n      window.open(\"/settings\", \"_self\");\n    }\n  </script>\n  <style>\n    body {\n      text-align: center;\n      background: linear-gradient(45deg,#0ca,#0ac);\n      height: 100%;\n      margin: 0;\n      background-repeat: no-repeat;\n      background-attachment: fixed;\n    }\n    html {\n      height: 100%;\n    }\n    svg {\n      width: 30vw;\n      padding: 2vh;\n    }\n    .tool_box {\n      position: absolute;\n      top: 50%;\n      left: 50%;\n      transform: translate(-50%, -50%);\n    }\n  </style>\n<style id=\"holderjs-style\" type=\"text/css\"></style></head>\n<body class=\" __plain_text_READY__\">\n<svg style=\"position: absolute; width: 0; height: 0; overflow: hidden;\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\">\n<defs>\n<symbol id=\"icon-box-add\" viewBox=\"0 0 32 32\">\n<path d=\"M26 2h-20l-6 6v21c0 0.552 0.448 1 1 1h30c0.552 0 1-0.448 1-1v-21l-6-6zM16 26l-10-8h6v-6h8v6h6l-10 8zM4.828 6l2-2h18.343l2 2h-22.343z\"></path>\n</symbol>\n<symbol id=\"icon-box-remove\" viewBox=\"0 0 32 32\">\n<path d=\"M26 2h-20l-6 6v21c0 0.552 0.448 1 1 1h30c0.552 0 1-0.448 1-1v-21l-6-6zM20 20v6h-8v-6h-6l10-8 10 8h-6zM4.828 6l2-2h18.343l2 2h-22.343z\"></path>\n</symbol>\n<symbol id=\"icon-cog\" viewBox=\"0 0 32 32\">\n<path d=\"M29.181 19.070c-1.679-2.908-0.669-6.634 2.255-8.328l-3.145-5.447c-0.898 0.527-1.943 0.829-3.058 0.829-3.361 0-6.085-2.742-6.085-6.125h-6.289c0.008 1.044-0.252 2.103-0.811 3.070-1.679 2.908-5.411 3.897-8.339 2.211l-3.144 5.447c0.905 0.515 1.689 1.268 2.246 2.234 1.676 2.903 0.672 6.623-2.241 8.319l3.145 5.447c0.895-0.522 1.935-0.82 3.044-0.82 3.35 0 6.067 2.725 6.084 6.092h6.289c-0.003-1.034 0.259-2.080 0.811-3.038 1.676-2.903 5.399-3.894 8.325-2.219l3.145-5.447c-0.899-0.515-1.678-1.266-2.232-2.226zM16 22.479c-3.578 0-6.479-2.901-6.479-6.479s2.901-6.479 6.479-6.479c3.578 0 6.479 2.901 6.479 6.479s-2.901 6.479-6.479 6.479z\"></path>\n</symbol>\n</defs>\n</svg>\n  <div id=\"tbB\" class=\"tool_box\">\n    <svg id=\"upb\" onclick=\"up()\"><use xlink:href=\"#icon-box-remove\"></use></svg>\n    <svg id=\"dnb\" onclick=\"down()\"><use xlink:href=\"#icon-box-add\"></use></svg>\n    <svg id=\"stb\" onclick=\"OpenSettings()\"><use xlink:href=\"#icon-cog\"></use></svg>\n  </div>\n</body>\n</html>"
  },
  {
    "path": "usermods/RelayBlinds/presets.json",
    "content": "{\"0\":{},\"2\":{\"n\":\"▲\",\"win\":\"U0=2\"},\"1\":{\"n\":\"▼\",\"win\":\"U0=1\"}}"
  },
  {
    "path": "usermods/RelayBlinds/readme.md",
    "content": "# RelayBlinds usermod\n\nThis simple usermod toggles two relay pins momentarily (defaults to 500ms) when `userVar0` is set.  \ne.g. can be used to \"push\" the buttons of a window blinds motor controller.\n\nv1 usermod. Please replace usermod.cpp in the `wled00` directory with the one in this file.\nYou may upload `index.htm` to `[WLED-IP]/edit` to replace the default lighting UI with a simple Up/Down button one.  \nA simple `presets.json` file is available. This makes the relay actions controllable via two presets to facilitate control e.g. the default UI or Alexa.\n"
  },
  {
    "path": "usermods/RelayBlinds/usermod.cpp",
    "content": "#include \"wled.h\"\n\n//Use userVar0 and userVar1 (API calls &U0=,&U1=, uint16_t)\n\n//gets called once at boot. Do all initialization that doesn't depend on network here\nvoid userSetup()\n{\n  \n}\n\n//gets called every time WiFi is (re-)connected. Initialize own network interfaces here\nvoid userConnected()\n{\n\n}\n\n/*\n * Physical IO\n */\n#define PIN_UP_RELAY 4\n#define PIN_DN_RELAY 5\n#define PIN_ON_TIME  500\nbool upActive = false, upActiveBefore = false, downActive = false, downActiveBefore = false;\nunsigned long upStartTime = 0, downStartTime = 0;\n\nvoid handleRelay()\n{\n  //up and down relays\n  if (userVar0) {\n    upActive = true;\n    if (userVar0 == 1) {\n      upActive = false;\n      downActive = true;\n    }\n    userVar0 = 0;\n  }\n  \n  if (upActive)\n  {\n    if(!upActiveBefore)\n    {\n      pinMode(PIN_UP_RELAY, OUTPUT);\n      digitalWrite(PIN_UP_RELAY, LOW);\n      upActiveBefore = true;\n      upStartTime = millis();\n      DEBUG_PRINTLN(F(\"UPA\"));\n    }\n    if (millis()- upStartTime > PIN_ON_TIME)\n    {\n      upActive = false;\n      DEBUG_PRINTLN(F(\"UPN\"));\n    }\n  } else if (upActiveBefore)\n  {\n    pinMode(PIN_UP_RELAY, INPUT);\n    upActiveBefore = false;\n  }\n\n  if (downActive)\n  {\n    if(!downActiveBefore)\n    {\n      pinMode(PIN_DN_RELAY, OUTPUT);\n      digitalWrite(PIN_DN_RELAY, LOW);\n      downActiveBefore = true;\n      downStartTime = millis();\n    }\n    if (millis()- downStartTime > PIN_ON_TIME)\n    {\n      downActive = false;\n    }\n  } else if (downActiveBefore)\n  {\n    pinMode(PIN_DN_RELAY, INPUT);\n    downActiveBefore = false;\n  }\n}\n\n//loop. You can use \"if (WLED_CONNECTED)\" to check for successful connection\nvoid userLoop()\n{\n  handleRelay();\n}"
  },
  {
    "path": "usermods/SN_Photoresistor/SN_Photoresistor.cpp",
    "content": "#include \"wled.h\"\n#include \"SN_Photoresistor.h\"\n\n//Pin defaults for QuinLed Dig-Uno (A0)\n#ifndef PHOTORESISTOR_PIN\n#define PHOTORESISTOR_PIN A0\n#endif\n\nstatic bool checkBoundSensor(float newValue, float prevValue, float maxDiff)\n{\n  return isnan(prevValue) || newValue <= prevValue - maxDiff || newValue >= prevValue + maxDiff;\n}\n\nuint16_t Usermod_SN_Photoresistor::getLuminance()\n{\n  // http://forum.arduino.cc/index.php?topic=37555.0\n  // https://forum.arduino.cc/index.php?topic=185158.0\n  float volts = analogRead(PHOTORESISTOR_PIN) * (referenceVoltage / adcPrecision);\n  float amps = volts / resistorValue;\n  float lux = amps * 1000000 * 2.0;\n\n  lastMeasurement = millis();\n  getLuminanceComplete = true;\n  return uint16_t(lux);\n}\n\nvoid Usermod_SN_Photoresistor::setup()\n{\n  // set pinmode\n  pinMode(PHOTORESISTOR_PIN, INPUT);\n}\n\nvoid Usermod_SN_Photoresistor::loop()\n{\n  if (disabled || strip.isUpdating())\n    return;\n\n  unsigned long now = millis();\n\n  // check to see if we are due for taking a measurement\n  // lastMeasurement will not be updated until the conversion\n  // is complete the the reading is finished\n  if (now - lastMeasurement < readingInterval)\n  {\n    return;\n  }\n\n  uint16_t currentLDRValue = getLuminance();\n  if (checkBoundSensor(currentLDRValue, lastLDRValue, offset))\n  {\n    lastLDRValue = currentLDRValue;\n\n#ifndef WLED_DISABLE_MQTT\n    if (WLED_MQTT_CONNECTED)\n    {\n      char subuf[45];\n      strcpy(subuf, mqttDeviceTopic);\n      strcat_P(subuf, PSTR(\"/luminance\"));\n      mqtt->publish(subuf, 0, true, String(lastLDRValue).c_str());\n    }\n    else\n    {\n      DEBUG_PRINTLN(F(\"Missing MQTT connection. Not publishing data\"));\n    }\n  }\n#endif\n}\n\n\nvoid Usermod_SN_Photoresistor::addToJsonInfo(JsonObject &root)\n{\n  JsonObject user = root[F(\"u\")];\n  if (user.isNull())\n    user = root.createNestedObject(F(\"u\"));\n\n  JsonArray lux = user.createNestedArray(F(\"Luminance\"));\n\n  if (!getLuminanceComplete)\n  {\n    // if we haven't read the sensor yet, let the user know\n    // that we are still waiting for the first measurement\n    lux.add((USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT - millis()) / 1000);\n    lux.add(F(\" sec until read\"));\n    return;\n  }\n\n  lux.add(lastLDRValue);\n  lux.add(F(\" lux\"));\n}\n\n\n/**\n   * addToConfig() (called from set.cpp) stores persistent properties to cfg.json\n   */\nvoid Usermod_SN_Photoresistor::addToConfig(JsonObject &root)\n{\n  // we add JSON object.\n  JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname\n  top[FPSTR(_enabled)] = !disabled;\n  top[FPSTR(_readInterval)] = readingInterval / 1000;\n  top[FPSTR(_referenceVoltage)] = referenceVoltage;\n  top[FPSTR(_resistorValue)] = resistorValue;\n  top[FPSTR(_adcPrecision)] = adcPrecision;\n  top[FPSTR(_offset)] = offset;\n\n  DEBUG_PRINTLN(F(\"Photoresistor config saved.\"));\n}\n\n/**\n* readFromConfig() is called before setup() to populate properties from values stored in cfg.json\n*/\nbool Usermod_SN_Photoresistor::readFromConfig(JsonObject &root)\n{\n  // we look for JSON object.\n  JsonObject top = root[FPSTR(_name)];\n  if (top.isNull()) {\n    DEBUG_PRINT(FPSTR(_name));\n    DEBUG_PRINTLN(F(\": No config found. (Using defaults.)\"));\n    return false;\n  }\n\n  disabled         = !(top[FPSTR(_enabled)] | !disabled);\n  readingInterval  = (top[FPSTR(_readInterval)] | readingInterval/1000) * 1000; // convert to ms\n  referenceVoltage = top[FPSTR(_referenceVoltage)] | referenceVoltage;\n  resistorValue    = top[FPSTR(_resistorValue)] | resistorValue;\n  adcPrecision     = top[FPSTR(_adcPrecision)] | adcPrecision;\n  offset           = top[FPSTR(_offset)] | offset;\n  DEBUG_PRINT(FPSTR(_name));\n  DEBUG_PRINTLN(F(\" config (re)loaded.\"));\n\n  // use \"return !top[\"newestParameter\"].isNull();\" when updating Usermod with new features\n  return true;\n}\n\n\n// strings to reduce flash memory usage (used more than twice)\nconst char Usermod_SN_Photoresistor::_name[] PROGMEM = \"Photoresistor\";\nconst char Usermod_SN_Photoresistor::_enabled[] PROGMEM = \"enabled\";\nconst char Usermod_SN_Photoresistor::_readInterval[] PROGMEM = \"read-interval-s\";\nconst char Usermod_SN_Photoresistor::_referenceVoltage[] PROGMEM = \"supplied-voltage\";\nconst char Usermod_SN_Photoresistor::_resistorValue[] PROGMEM = \"resistor-value\";\nconst char Usermod_SN_Photoresistor::_adcPrecision[] PROGMEM = \"adc-precision\";\nconst char Usermod_SN_Photoresistor::_offset[] PROGMEM = \"offset\";\n\nstatic Usermod_SN_Photoresistor sn_photoresistor;\nREGISTER_USERMOD(sn_photoresistor);"
  },
  {
    "path": "usermods/SN_Photoresistor/SN_Photoresistor.h",
    "content": "#pragma once\r\n#include \"wled.h\"\r\n\r\n// the frequency to check photoresistor, 10 seconds\r\n#ifndef USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL\r\n#define USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL 10000\r\n#endif\r\n\r\n// how many seconds after boot to take first measurement, 10 seconds\r\n#ifndef USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT\r\n#define USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT 10000\r\n#endif\r\n\r\n// supplied voltage\r\n#ifndef USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE\r\n#define USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE 5\r\n#endif\r\n\r\n// 10 bits\r\n#ifndef USERMOD_SN_PHOTORESISTOR_ADC_PRECISION\r\n#define USERMOD_SN_PHOTORESISTOR_ADC_PRECISION 1024.0f\r\n#endif\r\n\r\n// resistor size 10K hms\r\n#ifndef USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE\r\n#define USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE 10000.0f\r\n#endif\r\n\r\n// only report if difference grater than offset value\r\n#ifndef USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE\r\n#define USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE 5\r\n#endif\r\n\r\nclass Usermod_SN_Photoresistor : public Usermod\r\n{\r\nprivate:\r\n  float referenceVoltage = USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE;\r\n  float resistorValue = USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE;\r\n  float adcPrecision = USERMOD_SN_PHOTORESISTOR_ADC_PRECISION;\r\n  int8_t offset = USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE;\r\n\r\n  unsigned long readingInterval = USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL;\r\n  // set last reading as \"40 sec before boot\", so first reading is taken after 20 sec\r\n  unsigned long lastMeasurement = UINT32_MAX - (USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL - USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT);\r\n  // flag to indicate we have finished the first getTemperature call\r\n  // allows this library to report to the user how long until the first\r\n  // measurement\r\n  bool getLuminanceComplete = false;\r\n  uint16_t lastLDRValue = 65535;\r\n\r\n  // flag set at startup\r\n  bool disabled = false;\r\n\r\n  // strings to reduce flash memory usage (used more than twice)\r\n  static const char _name[];\r\n  static const char _enabled[];\r\n  static const char _readInterval[];\r\n  static const char _referenceVoltage[];\r\n  static const char _resistorValue[];\r\n  static const char _adcPrecision[];\r\n  static const char _offset[];\r\n\r\n  uint16_t getLuminance();\r\n\r\npublic:\r\n  void setup();\r\n  void loop();\r\n\r\n  uint16_t getLastLDRValue()\r\n  {\r\n    return lastLDRValue;\r\n  }\r\n\r\n  void addToJsonInfo(JsonObject &root);\r\n\r\n  uint16_t getId()\r\n  {\r\n    return USERMOD_ID_SN_PHOTORESISTOR;\r\n  }\r\n\r\n  /**\r\n     * addToConfig() (called from set.cpp) stores persistent properties to cfg.json\r\n     */\r\n  void addToConfig(JsonObject &root);\r\n\r\n  /**\r\n  * readFromConfig() is called before setup() to populate properties from values stored in cfg.json\r\n  */\r\n  bool readFromConfig(JsonObject &root);\r\n};\r\n"
  },
  {
    "path": "usermods/SN_Photoresistor/library.json",
    "content": "{\n  \"name\": \"SN_Photoresistor\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/SN_Photoresistor/readme.md",
    "content": "# SN_Photoresistor usermod\n\nThis usermod will read from an attached photoresistor sensor like the KY-018.\nThe luminance is displayed in both the Info section of the web UI as well as published to the `/luminance` MQTT topic, if enabled.\n\n## Installation\n\nCopy the example `platformio_override.ini` to the root directory.  This file should be placed in the same directory as `platformio.ini`.\n\n### Define Your Options\n\n* `USERMOD_SN_PHOTORESISTOR`                      - Enables this user mod. wled00\\usermods_list.cpp\n* `USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL` - Number of milliseconds between measurements. Defaults to 60000 ms\n* `USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT` - Number of milliseconds after boot to take first measurement. Defaults to 20000 ms\n* `USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE`    - Voltage supplied to the sensor. Defaults to 5v\n* `USERMOD_SN_PHOTORESISTOR_ADC_PRECISION`        - ADC precision. Defaults to 10 bits\n* `USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE`       - Resistor size, defaults to 10000.0 (10K Ohms)\n* `USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE`         - Offset value to report on. Defaults to 25\n\nAll parameters can be configured at runtime via the Usermods settings page.\n\n## Project link\n\n* [QuinLED-Dig-Uno](https://quinled.info/2018/09/15/quinled-dig-uno/) - Project link\n\n### PlatformIO requirements\n\nIf you are using `platformio_override.ini`, you should be able to refresh the task list and see your custom task, for example `env:usermod_sn_photoresistor_d1_mini`.\n\n## Change Log\n"
  },
  {
    "path": "usermods/ST7789_display/README.md",
    "content": "# Using the ST7789 TFT IPS 240x240 pixel color display with ESP32 boards\n\nThis usermod enables display of the following:\n\n* Current date and time;\n* Network SSID;\n* IP address;\n* WiFi signal strength;\n* Brightness;\n* Selected effect;\n* Selected palette;\n* Effect speed and intensity;\n* Estimated current in mA;\n\n## Hardware\n\n***\n![Hardware](images/ST7789_Guide.jpg)\n\n## Library used\n\n[Bodmer/TFT_eSPI](https://github.com/Bodmer/TFT_eSPI)\n\n## Setup\n\n***\n\n### Platformio.ini changes\n\n\nIn the `platformio.ini` file, you must change the environment setup to build for just the esp32dev platform as follows:\n\nAdd the following lines to section:\n\n```ini\ndefault_envs = esp32dev\nbuild_flags = ${common.build_flags_esp32}\n  -D USERMOD_ST7789_DISPLAY\n    -DUSER_SETUP_LOADED=1\n    -DST7789_DRIVER=1\n    -DTFT_WIDTH=240\n    -DTFT_HEIGHT=240\n    -DCGRAM_OFFSET=1\n    -DTFT_MOSI=21\n    -DTFT_SCLK=22\n    -DTFT_DC=27\n    -DTFT_RST=26\n    -DTFT_BL=14\n    -DLOAD_GLCD=1\n    ;optional for WROVER\n    ;-DCONFIG_SPIRAM_SUPPORT=1\n```\n\nSave the `platformio.ini` file.  Once saved, the required library files should be automatically downloaded for modifications in a later step.\n\n### TFT_eSPI Library Adjustments\n\nIf you are not using PlatformIO, you need to modify a file in the `TFT_eSPI` library. If you followed the directions to modify and save the `platformio.ini` file above, the `Setup24_ST7789.h` file can be found in the `/.pio/libdeps/esp32dev/TFT_eSPI/User_Setups/` folder.\n\nEdit `Setup_ST7789.h` file and uncomment and change GPIO pin numbers in lines containing `TFT_MOSI`, `TFT_SCLK`, `TFT_RST`, `TFT_DC`.\n\nModify the `User_Setup_Select.h` by uncommenting the line containing `#include <User_Setups/Setup24_ST7789.h>` and commenting out the line containing `#include <User_Setup.h>`.\n\nIf your display uses the backlight enable pin, add this definition: #define TFT_BL with backlight enable GPIO number.\n"
  },
  {
    "path": "usermods/ST7789_display/ST7789_display.cpp",
    "content": "// Credits to @mrVanboy, @gwaland and my dearest friend @westward\n// Also for @spiff72 for usermod TTGO-T-Display\n// 210217\n#include \"wled.h\"\n#include <TFT_eSPI.h>\n#include <SPI.h>\n\n#ifndef USER_SETUP_LOADED\n    #ifndef ST7789_DRIVER\n        #error Please define ST7789_DRIVER\n    #endif\n    #ifndef TFT_WIDTH\n        #error Please define TFT_WIDTH\n    #endif\n    #ifndef TFT_HEIGHT\n        #error Please define TFT_HEIGHT\n    #endif\n    #ifndef TFT_DC\n        #error Please define TFT_DC\n    #endif\n    #ifndef TFT_RST\n        #error Please define TFT_RST\n    #endif\n    #ifndef TFT_CS\n        #error Please define TFT_CS\n    #endif\n    #ifndef LOAD_GLCD\n        #error Please define LOAD_GLCD\n    #endif\n#endif\n#ifndef TFT_BL\n    #define TFT_BL -1\n#endif\n\n#define USERMOD_ID_ST7789_DISPLAY 97\n\nTFT_eSPI tft = TFT_eSPI(TFT_WIDTH, TFT_HEIGHT); // Invoke custom library\n\n// Extra char (+1) for null\n#define LINE_BUFFER_SIZE          20\n\n// How often we are redrawing screen\n#define USER_LOOP_REFRESH_RATE_MS 1000\n\nextern int getSignalQuality(int rssi);\n\n\n//class name. Use something descriptive and leave the \": public Usermod\" part :)\nclass St7789DisplayUsermod : public Usermod {\n  private:\n    //Private class members. You can declare variables and functions only accessible to your usermod here\n    unsigned long lastTime = 0;\n    bool enabled = true;\n\n    bool displayTurnedOff = false;\n    long lastRedraw = 0;\n    // needRedraw marks if redraw is required to prevent often redrawing.\n    bool needRedraw = true;\n    // Next variables hold the previous known values to determine if redraw is required.\n    String knownSsid = \"\";\n    IPAddress knownIp;\n    uint8_t knownBrightness = 0;\n    uint8_t knownMode = 0;\n    uint8_t knownPalette = 0;\n    uint8_t knownEffectSpeed = 0;\n    uint8_t knownEffectIntensity = 0;\n    uint8_t knownMinute = 99;\n    uint8_t knownHour = 99;\n\n    const uint8_t tftcharwidth = 19;  // Number of chars that fit on screen with text size set to 2\n    long lastUpdate = 0;\n\n    void center(String &line, uint8_t width) {\n      int len = line.length();\n      if (len<width) for (byte i=(width-len)/2; i>0; i--) line = ' ' + line;\n      for (byte i=line.length(); i<width; i++) line += ' ';\n    }\n\n    /**\n     * Display the current date and time in large characters\n     * on the middle rows. Based 24 or 12 hour depending on\n     * the useAMPM configuration.\n     */\n    void showTime() {\n        if (!ntpEnabled) return;\n        char lineBuffer[LINE_BUFFER_SIZE];\n\n        updateLocalTime();\n        byte minuteCurrent = minute(localTime);\n        byte hourCurrent   = hour(localTime);\n        //byte secondCurrent = second(localTime);\n        knownMinute = minuteCurrent;\n        knownHour = hourCurrent;\n\n        byte currentMonth = month(localTime);\n        sprintf_P(lineBuffer, PSTR(\"%s %2d \"), monthShortStr(currentMonth), day(localTime));\n        tft.setTextColor(TFT_SILVER);\n        tft.setCursor(84, 0);\n        tft.setTextSize(2);\n        tft.print(lineBuffer);\n\n        byte showHour = hourCurrent;\n        boolean isAM = false;\n        if (useAMPM) {\n            if (showHour == 0) {\n                showHour = 12;\n                isAM = true;\n            } else if (showHour > 12) {\n                showHour -= 12;\n                isAM = false;\n            } else {\n                isAM = true;\n            }\n        }\n\n        sprintf_P(lineBuffer, PSTR(\"%2d:%02d\"), (useAMPM ? showHour : hourCurrent), minuteCurrent);\n        tft.setTextColor(TFT_WHITE);\n        tft.setTextSize(4);\n        tft.setCursor(60, 24);\n        tft.print(lineBuffer);\n\n        tft.setTextSize(2);\n        tft.setCursor(186, 24);\n        //sprintf_P(lineBuffer, PSTR(\"%02d\"), secondCurrent);\n        if (useAMPM) tft.print(isAM ? \"AM\" : \"PM\");\n        //else         tft.print(lineBuffer);\n    }\n\n  public:\n    //Functions called by WLED\n\n    /*\n     * setup() is called once at boot. WiFi is not yet connected at this point.\n     * You can use it to initialize variables, sensors or similar.\n     */\n    void setup() override\n    {\n        PinManagerPinType spiPins[] = { { spi_mosi, true }, { spi_miso, false}, { spi_sclk, true } };\n        if (!PinManager::allocateMultiplePins(spiPins, 3, PinOwner::HW_SPI)) { enabled = false; return; }\n        PinManagerPinType displayPins[] = { { TFT_CS, true}, { TFT_DC, true}, { TFT_RST, true }, { TFT_BL, true } };\n        if (!PinManager::allocateMultiplePins(displayPins, sizeof(displayPins)/sizeof(PinManagerPinType), PinOwner::UM_FourLineDisplay)) {\n            PinManager::deallocateMultiplePins(spiPins, 3, PinOwner::HW_SPI);\n            enabled = false;\n            return;\n        }\n\n        tft.init();\n        tft.setRotation(0);  //Rotation here is set up for the text to be readable with the port on the left. Use 1 to flip.\n        tft.fillScreen(TFT_BLACK);\n        tft.setTextColor(TFT_RED);\n        tft.setCursor(60, 100);\n        tft.setTextDatum(MC_DATUM);\n        tft.setTextSize(2);\n        tft.print(\"Loading...\");\n        if (TFT_BL >= 0) \n        {\n            pinMode(TFT_BL, OUTPUT); // Set backlight pin to output mode\n            digitalWrite(TFT_BL, HIGH); // Turn backlight on.\n        }\n    }\n\n    /*\n     * connected() is called every time the WiFi is (re)connected\n     * Use it to initialize network interfaces\n     */\n    void connected() override {\n      //Serial.println(\"Connected to WiFi!\");\n    }\n\n    /*\n     * loop() is called continuously. Here you can check for events, read sensors, etc.\n     *\n     * Tips:\n     * 1. You can use \"if (WLED_CONNECTED)\" to check for a successful network connection.\n     *    Additionally, \"if (WLED_MQTT_CONNECTED)\" is available to check for a connection to an MQTT broker.\n     *\n     * 2. Try to avoid using the delay() function. NEVER use delays longer than 10 milliseconds.\n     *    Instead, use a timer check as shown here.\n     */\n    void loop() override {\n        char buff[LINE_BUFFER_SIZE];\n\n        // Check if we time interval for redrawing passes.\n        if (millis() - lastUpdate < USER_LOOP_REFRESH_RATE_MS)\n        {\n            return;\n        }\n        lastUpdate = millis();\n  \n        // Turn off display after 5 minutes with no change.\n        if (!displayTurnedOff && millis() - lastRedraw > 5*60*1000)\n        {\n            if (TFT_BL >= 0) digitalWrite(TFT_BL, LOW); // Turn backlight off. \n            displayTurnedOff = true;\n        } \n\n        // Check if values which are shown on display changed from the last time.\n        if ((((apActive) ? String(apSSID) : WiFi.SSID()) != knownSsid) ||\n            (knownIp != (apActive ? IPAddress(4, 3, 2, 1) : Network.localIP())) ||\n            (knownBrightness != bri) ||\n            (knownEffectSpeed != strip.getMainSegment().speed) ||\n            (knownEffectIntensity != strip.getMainSegment().intensity) ||\n            (knownMode != strip.getMainSegment().mode) ||\n            (knownPalette != strip.getMainSegment().palette))\n        {\n            needRedraw = true;\n        }\n\n        if (!needRedraw)\n        {\n            return;\n        }\n        needRedraw = false;\n    \n        if (displayTurnedOff)\n        {\n            digitalWrite(TFT_BL, HIGH); // Turn backlight on.\n            displayTurnedOff = false;\n        }\n        lastRedraw = millis();\n\n        // Update last known values.\n        #if defined(ESP8266)\n            knownSsid = apActive ? WiFi.softAPSSID() : WiFi.SSID();\n        #else\n            knownSsid = WiFi.SSID();\n        #endif\n        knownIp = apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP();\n        knownBrightness = bri;\n        knownMode = strip.getMainSegment().mode;\n        knownPalette = strip.getMainSegment().palette;\n        knownEffectSpeed = strip.getMainSegment().speed;\n        knownEffectIntensity = strip.getMainSegment().intensity;\n\n        tft.fillScreen(TFT_BLACK);\n\n        showTime();\n\n        tft.setTextSize(2);\n\n        // Wifi name\n        tft.setTextColor(TFT_GREEN);\n        tft.setCursor(0, 60);\n        String line = knownSsid.substring(0, tftcharwidth-1);\n        // Print `~` char to indicate that SSID is longer, than our display\n        if (knownSsid.length() > tftcharwidth) line = line.substring(0, tftcharwidth-1) + '~';\n        center(line, tftcharwidth);\n        tft.print(line.c_str());\n\n        // Print AP IP and password in AP mode or knownIP if AP not active.\n        if (apActive)\n        {\n            tft.setCursor(0, 84);\n            tft.print(\"AP IP: \");\n            tft.print(knownIp);\n            tft.setCursor(0,108);\n            tft.print(\"AP Pass:\");\n            tft.print(apPass);\n        }\n        else\n        {\n            tft.setCursor(0, 84);\n            line = knownIp.toString();\n            center(line, tftcharwidth);\n            tft.print(line.c_str());\n            // percent brightness\n            tft.setCursor(0, 120);\n            tft.setTextColor(TFT_WHITE);\n            tft.print(\"Bri: \");\n            tft.print((((int)bri*100)/255));\n            tft.print(\"%\");\n            // signal quality\n            tft.setCursor(124,120);\n            tft.print(\"Sig: \");\n            if (getSignalQuality(WiFi.RSSI()) < 10) {\n                tft.setTextColor(TFT_RED);\n            } else if (getSignalQuality(WiFi.RSSI()) < 25) {\n                tft.setTextColor(TFT_ORANGE);\n            } else {\n                tft.setTextColor(TFT_GREEN);\n            }\n            tft.print(getSignalQuality(WiFi.RSSI()));\n            tft.setTextColor(TFT_WHITE);\n            tft.print(\"%\");\n        }\n\n        // mode name\n        tft.setTextColor(TFT_CYAN);\n        tft.setCursor(0, 144);\n        char lineBuffer[tftcharwidth+1];\n        extractModeName(knownMode, JSON_mode_names, lineBuffer, tftcharwidth);\n        tft.print(lineBuffer);\n\n        // palette name\n        tft.setTextColor(TFT_YELLOW);\n        tft.setCursor(0, 168);\n        extractModeName(knownPalette, JSON_palette_names, lineBuffer, tftcharwidth);\n        tft.print(lineBuffer);\n\n        tft.setCursor(0, 192);\n        tft.setTextColor(TFT_SILVER);\n        sprintf_P(buff, PSTR(\"FX  Spd:%3d Int:%3d\"), effectSpeed, effectIntensity);\n        tft.print(buff);\n\n        // Fifth row with estimated mA usage\n        tft.setTextColor(TFT_SILVER);\n        tft.setCursor(0, 216);\n        // Print estimated milliamp usage (must specify the LED type in LED prefs for this to be a reasonable estimate).\n        tft.print(\"Current: \");\n        tft.setTextColor(TFT_ORANGE);\n        tft.print(BusManager::currentMilliamps());\n        tft.print(\"mA\");\n    }\n\n    /*\n     * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.\n     * Creating an \"u\" object allows you to add custom key/value pairs to the Info section of the WLED web UI.\n     * Below it is shown how this could be used for e.g. a light sensor\n     */\n    void addToJsonInfo(JsonObject& root) override\n    {\n      JsonObject user = root[\"u\"];\n      if (user.isNull()) user = root.createNestedObject(\"u\");\n\n      JsonArray lightArr = user.createNestedArray(\"ST7789\"); //name\n      lightArr.add(enabled?F(\"installed\"):F(\"disabled\")); //unit\n    }\n\n\n    /*\n     * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).\n     * Values in the state object may be modified by connected clients\n     */\n    void addToJsonState(JsonObject& root) override\n    {\n      //root[\"user0\"] = userVar0;\n    }\n\n\n    /*\n     * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).\n     * Values in the state object may be modified by connected clients\n     */\n    void readFromJsonState(JsonObject& root) override\n    {\n      //userVar0 = root[\"user0\"] | userVar0; //if \"user0\" key exists in JSON, update, else keep old value\n      //if (root[\"bri\"] == 255) Serial.println(F(\"Don't burn down your garage!\"));\n    }\n\n\n    /*\n     * addToConfig() can be used to add custom persistent settings to the cfg.json file in the \"um\" (usermod) object.\n     * It will be called by WLED when settings are actually saved (for example, LED settings are saved)\n     * If you want to force saving the current state, use serializeConfig() in your loop().\n     *\n     * CAUTION: serializeConfig() will initiate a filesystem write operation.\n     * It might cause the LEDs to stutter and will cause flash wear if called too often.\n     * Use it sparingly and always in the loop, never in network callbacks!\n     *\n     * addToConfig() will also not yet add your setting to one of the settings pages automatically.\n     * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually.\n     *\n     * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings!\n     */\n    void addToConfig(JsonObject& root) override\n    {\n      JsonObject top = root.createNestedObject(\"ST7789\");\n      JsonArray pins = top.createNestedArray(\"pin\");\n      pins.add(TFT_CS);\n      pins.add(TFT_DC);\n      pins.add(TFT_RST);\n      pins.add(TFT_BL);\n      //top[\"great\"] = userVar0; //save this var persistently whenever settings are saved\n    }\n\n\n    void appendConfigData() override {\n      oappend(F(\"addInfo('ST7789:pin[]',0,'','SPI CS');\"));\n      oappend(F(\"addInfo('ST7789:pin[]',1,'','SPI DC');\"));\n      oappend(F(\"addInfo('ST7789:pin[]',2,'','SPI RST');\"));\n      oappend(F(\"addInfo('ST7789:pin[]',3,'','SPI BL');\"));\n    }\n\n    /*\n     * readFromConfig() can be used to read back the custom settings you added with addToConfig().\n     * This is called by WLED when settings are loaded (currently this only happens once immediately after boot)\n     *\n     * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes),\n     * 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.\n     * If you don't know what that is, don't fret. It most likely doesn't affect your use case :)\n     */\n    bool readFromConfig(JsonObject& root) override\n    {\n      //JsonObject top = root[\"top\"];\n      //userVar0 = top[\"great\"] | 42; //The value right of the pipe \"|\" is the default value in case your setting was not present in cfg.json (e.g. first boot)\n      return true;\n    }\n\n\n    /*\n     * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).\n     * This could be used in the future for the system to determine whether your usermod is installed.\n     */\n    uint16_t getId() override\n    {\n      return USERMOD_ID_ST7789_DISPLAY;\n    }\n\n   //More methods can be added in the future, this example will then be extended.\n   //Your usermod will remain compatible as it does not need to implement all methods from the Usermod base class!\n};\n\nstatic name. st7789_display;\nREGISTER_USERMOD(st7789_display);"
  },
  {
    "path": "usermods/ST7789_display/library.json.disabled",
    "content": "{\n  \"name:\": \"ST7789_display\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/Si7021_MQTT_HA/Si7021_MQTT_HA.cpp",
    "content": "// this is remixed from usermod_v2_SensorsToMqtt.h (sensors_to_mqtt usermod)\n// and usermod_multi_relay.h (multi_relay usermod)\n\n#include \"wled.h\"\n#include <Adafruit_Si7021.h>\n#include <EnvironmentCalculations.h> // EnvironmentCalculations::HeatIndex(), ::DewPoint(), ::AbsoluteHumidity()\n\n#ifdef WLED_DISABLE_MQTT\n#error \"This user mod requires MQTT to be enabled.\"\n#endif\n\nstatic Adafruit_Si7021 si7021;\n\nclass Si7021_MQTT_HA : public Usermod\n{\n  private:\n    bool sensorInitialized = false;\n    bool mqttInitialized = false;\n    float sensorTemperature = 0;\n    float sensorHumidity = 0;\n    float sensorHeatIndex = 0;\n    float sensorDewPoint = 0;\n    float sensorAbsoluteHumidity= 0;\n    String mqttTemperatureTopic = \"\";\n    String mqttHumidityTopic = \"\";\n    String mqttHeatIndexTopic = \"\";\n    String mqttDewPointTopic = \"\";\n    String mqttAbsoluteHumidityTopic = \"\";\n    unsigned long nextMeasure = 0;\n    bool enabled = false;\n    bool haAutoDiscovery = true;\n    bool sendAdditionalSensors = true;\n\n    // strings to reduce flash memory usage (used more than twice)\n    static const char _name[];\n    static const char _enabled[];\n    static const char _sendAdditionalSensors[];\n    static const char _haAutoDiscovery[];\n\n    void _initializeSensor()\n    {\n      sensorInitialized = si7021.begin();\n      Serial.printf(\"Si7021_MQTT_HA: sensorInitialized = %d\\n\", sensorInitialized);\n    }\n\n    void _initializeMqtt()\n    {\n      mqttTemperatureTopic = String(mqttDeviceTopic) + \"/si7021_temperature\";\n      mqttHumidityTopic = String(mqttDeviceTopic) + \"/si7021_humidity\";\n      mqttHeatIndexTopic = String(mqttDeviceTopic) + \"/si7021_heat_index\";\n      mqttDewPointTopic = String(mqttDeviceTopic) + \"/si7021_dew_point\";\n      mqttAbsoluteHumidityTopic = String(mqttDeviceTopic) + \"/si7021_absolute_humidity\";\n\n      // Update and publish sensor data\n      _updateSensorData();\n      _publishSensorData();\n\n      if (haAutoDiscovery) {\n        _publishHAMqttSensor(\"temperature\", \"Temperature\", mqttTemperatureTopic, \"temperature\", \"°C\");\n        _publishHAMqttSensor(\"humidity\", \"Humidity\", mqttHumidityTopic, \"humidity\", \"%\");\n        if (sendAdditionalSensors) {\n          _publishHAMqttSensor(\"heat_index\", \"Heat Index\", mqttHeatIndexTopic, \"temperature\", \"°C\");\n          _publishHAMqttSensor(\"dew_point\", \"Dew Point\", mqttDewPointTopic, \"\", \"°C\");\n          _publishHAMqttSensor(\"absolute_humidity\", \"Absolute Humidity\", mqttAbsoluteHumidityTopic, \"\", \"g/m³\");\n        }\n      }\n      \n      mqttInitialized = true;\n    }\n\n    void _publishHAMqttSensor(\n      const String &name, \n      const String &friendly_name, \n      const String &state_topic, \n      const String &deviceClass, \n      const String &unitOfMeasurement)\n    {\n      if (WLED_MQTT_CONNECTED) {\n        String topic = String(\"homeassistant/sensor/\") + mqttClientID + \"/\" + name + \"/config\";\n\n        StaticJsonDocument<300> doc;\n\n        doc[\"name\"] = String(serverDescription) + \" \" + friendly_name;\n        doc[\"state_topic\"] = state_topic;\n        doc[\"unique_id\"] = String(mqttClientID) + name;\n        if (unitOfMeasurement != \"\")\n          doc[\"unit_of_measurement\"] = unitOfMeasurement;\n        if (deviceClass != \"\")\n          doc[\"device_class\"] = deviceClass;\n        doc[\"expire_after\"] = 1800;\n\n        JsonObject device = doc.createNestedObject(\"device\"); // attach the sensor to the same device\n        device[\"name\"] = String(serverDescription);\n        device[\"model\"] = F(WLED_PRODUCT_NAME);\n        device[\"manufacturer\"] = F(WLED_BRAND);\n        device[\"identifiers\"] = String(\"wled-\") + String(serverDescription);\n        device[\"sw_version\"] = VERSION;\n\n        String payload;\n        serializeJson(doc, payload);\n\n        mqtt->publish(topic.c_str(), 0, true, payload.c_str());\n      }\n    }\n\n    void _updateSensorData()\n    {\n      sensorTemperature = si7021.readTemperature();\n      sensorHumidity = si7021.readHumidity();\n\n      // Serial.print(\"Si7021_MQTT_HA: Temperature: \");\n      // Serial.print(sensorTemperature, 2);\n      // Serial.print(\"\\tHumidity: \");\n      // Serial.print(sensorHumidity, 2);\n\n      if (sendAdditionalSensors) {\n        EnvironmentCalculations::TempUnit envTempUnit(EnvironmentCalculations::TempUnit_Celsius);\n        sensorHeatIndex = EnvironmentCalculations::HeatIndex(sensorTemperature, sensorHumidity, envTempUnit);\n        sensorDewPoint = EnvironmentCalculations::DewPoint(sensorTemperature, sensorHumidity, envTempUnit);\n        sensorAbsoluteHumidity = EnvironmentCalculations::AbsoluteHumidity(sensorTemperature, sensorHumidity, envTempUnit);\n\n        // Serial.print(\"\\tHeat Index: \");\n        // Serial.print(sensorHeatIndex, 2);\n        // Serial.print(\"\\tDew Point: \");\n        // Serial.print(sensorDewPoint, 2);\n        // Serial.print(\"\\tAbsolute Humidity: \");\n        // Serial.println(sensorAbsoluteHumidity, 2);\n      }\n      // else\n      //   Serial.println(\"\");\n    }\n\n    void _publishSensorData()\n    {\n      if (WLED_MQTT_CONNECTED) {\n        mqtt->publish(mqttTemperatureTopic.c_str(), 0, false, String(sensorTemperature).c_str());\n        mqtt->publish(mqttHumidityTopic.c_str(), 0, false, String(sensorHumidity).c_str());\n        if (sendAdditionalSensors) {\n          mqtt->publish(mqttHeatIndexTopic.c_str(), 0, false, String(sensorHeatIndex).c_str());\n          mqtt->publish(mqttDewPointTopic.c_str(), 0, false, String(sensorDewPoint).c_str());\n          mqtt->publish(mqttAbsoluteHumidityTopic.c_str(), 0, false, String(sensorAbsoluteHumidity).c_str());\n        }\n      }\n    }\n\n  public:\n    void addToConfig(JsonObject& root)\n    {\n      JsonObject top = root.createNestedObject(FPSTR(_name));\n      \n      top[FPSTR(_enabled)] = enabled;\n      top[FPSTR(_sendAdditionalSensors)] = sendAdditionalSensors;\n      top[FPSTR(_haAutoDiscovery)] = haAutoDiscovery;\n    }\n\n    bool readFromConfig(JsonObject& root)\n    {\n      JsonObject top = root[FPSTR(_name)];\n      \n      bool configComplete = !top.isNull();\n      configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled);\n      configComplete &= getJsonValue(top[FPSTR(_sendAdditionalSensors)], sendAdditionalSensors);\n      configComplete &= getJsonValue(top[FPSTR(_haAutoDiscovery)], haAutoDiscovery);\n\n      return configComplete;\n    }\n\n    void onMqttConnect(bool sessionPresent) {\n      if (mqttDeviceTopic[0] != 0)\n        _initializeMqtt();\n    }\n\n    void setup()\n    {\n      if (enabled) {\n        Serial.println(\"Si7021_MQTT_HA: Starting!\");\n        Serial.println(\"Si7021_MQTT_HA: Initializing sensors.. \");\n        _initializeSensor();\n      }\n    }\n\n    // gets called every time WiFi is (re-)connected.\n    void connected()\n    {\n      nextMeasure = millis() + 5000; // Schedule next measure in 5 seconds\n    }\n\n    void loop()\n    {\n      yield();\n      if (!enabled || strip.isUpdating()) return; // !sensorFound || \n\n      unsigned long tempTimer = millis();\n\n      if (tempTimer > nextMeasure) {\n        nextMeasure = tempTimer + 60000; // Schedule next measure in 60 seconds\n\n        if (!sensorInitialized) {\n          Serial.println(\"Si7021_MQTT_HA: Error! Sensors not initialized in loop()!\");\n          _initializeSensor();\n          return; // lets try again next loop\n        }\n\n        if (WLED_MQTT_CONNECTED) {\n          if (!mqttInitialized)\n            _initializeMqtt();\n\n          // Update and publish sensor data\n          _updateSensorData();\n          _publishSensorData();\n        }\n        else {\n          Serial.println(\"Si7021_MQTT_HA: Missing MQTT connection. Not publishing data\");\n          mqttInitialized = false;\n        }\n      }\n    }\n    \n    uint16_t getId()\n    {\n      return USERMOD_ID_SI7021_MQTT_HA;\n    }\n};\n\n// strings to reduce flash memory usage (used more than twice)\nconst char Si7021_MQTT_HA::_name[]                   PROGMEM = \"Si7021 MQTT (Home Assistant)\";\nconst char Si7021_MQTT_HA::_enabled[]                PROGMEM = \"enabled\";\nconst char Si7021_MQTT_HA::_sendAdditionalSensors[]  PROGMEM = \"Send Dew Point, Abs. Humidity and Heat Index\";\nconst char Si7021_MQTT_HA::_haAutoDiscovery[]        PROGMEM = \"Home Assistant MQTT Auto-Discovery\";\n\n\nstatic Si7021_MQTT_HA si7021_mqtt_ha;\nREGISTER_USERMOD(si7021_mqtt_ha);"
  },
  {
    "path": "usermods/Si7021_MQTT_HA/library.json",
    "content": "{\n  \"name\": \"Si7021_MQTT_HA\",\n  \"build\": { \"libArchive\": false },\n  \"dependencies\": {\n    \"finitespace/BME280\":\"3.0.0\",\n    \"adafruit/Adafruit Si7021 Library\" : \"1.5.3\",\n    \"SPI\":\"*\",\n    \"adafruit/Adafruit BusIO\": \"1.17.1\"\n  }\n}"
  },
  {
    "path": "usermods/Si7021_MQTT_HA/readme.md",
    "content": "# Si7021 to MQTT (with Home Assistant Auto Discovery) usermod\n\nThis usermod implements support for [Si7021 I²C temperature and humidity sensors](https://www.silabs.com/documents/public/data-sheets/Si7021-A20.pdf).\n\nAs of this writing, the sensor data will *not* be shown on the WLED UI, but it _is_ published via MQTT to WLED's \"built-in\" MQTT device topic. \n\n```\ntemperature: $mqttDeviceTopic/si7021_temperature\nhumidity: $mqttDeviceTopic/si7021_humidity\n```\n\nThe following sensors can also be published:\n\n```\nheat_index: $mqttDeviceTopic/si7021_heat_index\ndew_point: $mqttDeviceTopic/si7021_dew_point\nabsolute_humidity: $mqttDeviceTopic/si7021_absolute_humidity\n```\n\nSensor data will be updated/sent every 60 seconds.\n\nThis usermod also supports Home Assistant Auto Discovery.\n\n## Settings via Usermod Setup\n\n- `enabled`: Enables this usermod\n- `Send Dew Point, Abs. Humidity and Heat Index`: Enables additional sensors\n- `Home Assistant MQTT Auto-Discovery`: Enables Home Assistant Auto Discovery\n\n# Installation\n\n## Hardware\n\nAttach the Si7021 sensor to the I²C interface.\n\nDefault PINs ESP32:\n\n```\nSCL_PIN = 22;\nSDA_PIN = 21;\n```\n\nDefault PINs ESP8266:\n\n```\nSCL_PIN = 5;\nSDA_PIN = 4;\n```\n\n## Software\n\nAdd `Si7021_MQTT_HA` to custom_usermods\n\n\n# Credits\n\n- Aircoookie for making WLED\n- Other usermod creators for example code (`sensors_to_mqtt` and `multi_relay` especially)\n- You, for reading this\n"
  },
  {
    "path": "usermods/TTGO-T-Display/README.md",
    "content": "# TTGO T-Display ESP32 with 240x135 TFT via SPI with TFT_eSPI\nThis usermod enables use of the TTGO 240x135 T-Display ESP32 module\nfor controlling WLED and showing the following information: \n* Current SSID\n* IP address, if obtained\n  * If connected to a network, current brightness percentage is shown \n  * In AP mode, AP, IP and password are shown\n* Current effect\n* Current palette\n* Estimated current in mA (NOTE: for this to be a reasonable value, the correct LED type must be specified in the LED Prefs section)\n\nButton pin is mapped to the onboard button adjacent to the reset button of the TTGO T-Display board.\n\nI have designed a 3D printed case around this board and an [\"ElectroCookie\"](https://amzn.to/2WCNeeA) project board, a [level shifter](https://amzn.to/3hbKu18), a [buck regulator](https://amzn.to/3mLMy0W), and a DC [power jack](https://amzn.to/3phj9NZ).  I use 12V WS2815 LED strips for my projects, and power them with 12V power supplies. The regulator supplies 5V for the ESP module and the level shifter.  If there is any interest in this case which elevates the board and display on custom extended standoffs to place the screen at the top of the enclosure (with accessible buttons), let me know, and I will post the STL files.  It is a bit tricky to get the height correct, so I also designed a one-time use 3D printed solder fixture to set the board in the right location and at the correct height for the housing.  (It is one-time use because it has to be cut off after soldering to be able to remove it).  I didn't think the effort to make it in multiple pieces was worthwhile.\n\nBased on a rework of the ssd1306_i2c_oled_u8g2 usermod from the WLED repo.\n\n## Hardware\n![Hardware](assets/ttgo_hardware1.png)\n![Hardware](assets/ttgo-tdisplay-enclosure1a.png)\n![Hardware](assets/ttgo-tdisplay-enclosure2a.png)\n![Hardware](assets/ttgo-tdisplay-enclosure3a.png)\n![Hardware](assets/ttgo-tdisplay-enclosure3a.png)\n\n## Github reference for TTGO-Tdisplay\n\n* [TTGO T-Display](https://github.com/Xinyuan-LilyGO/TTGO-T-Display)\n\n## Requirements\nFunctionality checked with:\n* TTGO T-Display\n* PlatformIO\n* Group of 4 individual Neopixels from Adafruit and several full strings of 12v WS2815 LEDs.\n* The hardware design shown above should be limited to shorter strings.  For larger strings, I use a different setup with a dedicated 12v power supply and power them directly from said supply (in addition to dropping the 12v to 5v with a buck regulator for the ESP module and level shifter).\n\n## Setup Needed:\n* As with all usermods, copy the usermod.cpp file from the TTGO-T-Display usermod folder to the wled00 folder (replacing the default usermod.cpp file).\n\n## Platformio Requirements\n### Platformio.ini changes\nUnder the root folder of the project, in the `platformio.ini` file, uncomment the `TFT_eSPI` line within the [common] section, under `lib_deps`:\n```ini\n# platformio.ini\n...\n[common]\n...\nlib_deps =\n    ...\n  #For use of the TTGO T-Display ESP32 Module with integrated TFT display uncomment the following line  \n    #TFT_eSPI\n...\n```\n\nIn the `platformio.ini` file, you must change the environment setup to build for just the esp32dev platform as follows:\n\nComment out the line described below:\n```ini\n# Release binaries\n; default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, esp32dev, esp32_eth, esp32s2_saola, esp32c3\n```\nand uncomment the following line in the 'Single binaries' section:\n```ini\ndefault_envs = esp32dev\n```\nSave the `platformio.ini` file.  Once saved, the required library files should be automatically downloaded for modifications in a later step.\n\n### Platformio_overrides.ini (added)\nCopy the `platformio_overrides.ini` file which is contained in the `usermods/TTGO-T-Display/` folder into the root of your project folder. This file contains an override that remaps the button pin of WLED to use the on-board button to the right of the USB-C connector (when viewed with the port oriented downward - see hardware photo).\n\n### TFT_eSPI Library Adjustments (board selection)\nYou need to modify a file in the `TFT_eSPI` library to select the correct board.  If you followed the directions to modify and save the `platformio.ini` file above, the `User_Setup_Select.h` file can be found in the `/.pio/libdeps/esp32dev/TFT_eSPI_ID1559` folder.\n\nModify the  `User_Setup_Select.h` file as follows:\n* Comment out the following line (which is the 'default' setup file):\n```ini\n//#include <User_Setup.h>           // Default setup is root library folder\n```\n* Uncomment the following line (which points to the setup file for the TTGO T-Display):\n```ini\n#include <User_Setups/Setup25_TTGO_T_Display.h>    // Setup file for ESP32 and TTGO T-Display ST7789V SPI bus TFT\n```\n\nBuild the file.  If you see a failure like this:\n```ini\nxtensa-esp32-elf-g++: error: wled00\\wled00.ino.cpp: No such file or directory\nxtensa-esp32-elf-g++: fatal error: no input files\n```\ntry building again. Sometimes this happens on the first build attempt and subsequent attempts build correctly.\n\n## Arduino IDE\n- UNTESTED\n"
  },
  {
    "path": "usermods/TTGO-T-Display/usermod.cpp",
    "content": "\n/*\n * This file allows you to add own functionality to WLED more easily\n * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality\n * EEPROM bytes 2750+ are reserved for your custom use case. (if you extend #define EEPSIZE in const.h)\n * bytes 2400+ are currently unused, but might be used for future wled features\n */\n\n/*\n * Pin 2 of the TTGO T-Display serves as the data line for the LED string.\n * Pin 35 is set up as the button pin in the platformio_overrides.ini file.\n * The button can be set up via the macros section in the web interface.\n * I use the button to cycle between presets.\n * The Pin 35 button is the one on the RIGHT side of the USB-C port on the board,\n * when the port is oriented downwards.  See readme.md file for photo.\n * The display is set up to turn off after 5 minutes, and turns on automatically \n * when a change in the dipslayed info is detected (within a 5 second interval).\n */\n \n\n//Use userVar0 and userVar1 (API calls &U0=,&U1=, uint16_t)\n\n#include \"wled.h\"\n#include <TFT_eSPI.h>\n#include <SPI.h>\n#include \"WiFi.h\"\n#include <Wire.h>\n\n#ifndef TFT_DISPOFF\n#define TFT_DISPOFF 0x28\n#endif\n\n#ifndef TFT_SLPIN\n#define TFT_SLPIN   0x10\n#endif\n\n#define TFT_MOSI            19\n#define TFT_SCLK            18\n#define TFT_CS              5\n#define TFT_DC              16\n#define TFT_RST             23\n\n#define TFT_BL          4  // Display backlight control pin\n#define ADC_EN          14  // Used for enabling battery voltage measurements - not used in this program\n\nTFT_eSPI tft = TFT_eSPI(135, 240); // Invoke custom library\n\n//gets called once at boot. Do all initialization that doesn't depend on network here\nvoid userSetup() {\n    Serial.begin(115200);\n    Serial.println(\"Start\");\n    tft.init();\n    tft.setRotation(3);  //Rotation here is set up for the text to be readable with the port on the left. Use 1 to flip.\n    tft.fillScreen(TFT_BLACK);\n    tft.setTextSize(2);\n    tft.setTextColor(TFT_WHITE);\n    tft.setCursor(1, 10);\n    tft.setTextDatum(MC_DATUM);\n    tft.setTextSize(3);\n    tft.print(\"Loading...\");\n\n    if (TFT_BL > 0) { // TFT_BL has been set in the TFT_eSPI library in the User Setup file TTGO_T_Display.h\n         pinMode(TFT_BL, OUTPUT); // Set backlight pin to output mode\n         digitalWrite(TFT_BL, HIGH); // Turn backlight on. \n    }\n\n    // tft.setRotation(3);\n}\n\n// gets called every time WiFi is (re-)connected. Initialize own network\n// interfaces here\nvoid userConnected() {}\n\n// needRedraw marks if redraw is required to prevent often redrawing.\nbool needRedraw = true;\n\n// Next variables hold the previous known values to determine if redraw is\n// required.\nString knownSsid = \"\";\nIPAddress knownIp;\nuint8_t knownBrightness = 0;\nuint8_t knownMode = 0;\nuint8_t knownPalette = 0;\nuint8_t tftcharwidth = 19;  // Number of chars that fit on screen with text size set to 2\n\nlong lastUpdate = 0;\nlong lastRedraw = 0;\nbool displayTurnedOff = false;\n// How often we are redrawing screen\n#define USER_LOOP_REFRESH_RATE_MS 5000\n\nvoid userLoop() {\n\n  // Check if we time interval for redrawing passes.\n  if (millis() - lastUpdate < USER_LOOP_REFRESH_RATE_MS) {\n    return;\n  }\n  lastUpdate = millis();\n  \n  // Turn off display after 5 minutes with no change.\n   if(!displayTurnedOff && millis() - lastRedraw > 5*60*1000) {\n    digitalWrite(TFT_BL, LOW); // Turn backlight off. \n    displayTurnedOff = true;\n  } \n\n  // Check if values which are shown on display changed from the last time.\n  if (((apActive) ? String(apSSID) : WiFi.SSID()) != knownSsid) {\n    needRedraw = true;\n  } else if (knownIp != (apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP())) {\n    needRedraw = true;\n  } else if (knownBrightness != bri) {\n    needRedraw = true;\n  } else if (knownMode != strip.getMainSegment().mode) {\n    needRedraw = true;\n  } else if (knownPalette != strip.getMainSegment().palette) {\n    needRedraw = true;\n  }\n\n  if (!needRedraw) {\n    return;\n  }\n  needRedraw = false;\n  \n  if (displayTurnedOff)\n  {\n    digitalWrite(TFT_BL, TFT_BACKLIGHT_ON); // Turn backlight on.\n    displayTurnedOff = false;\n  }\n  lastRedraw = millis();\n\n  // Update last known values.\n  #if defined(ESP8266)\n  knownSsid = apActive ? WiFi.softAPSSID() : WiFi.SSID();\n  #else\n  knownSsid = WiFi.SSID();\n  #endif\n  knownIp = apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP();\n  knownBrightness = bri;\n  knownMode = strip.getMainSegment().mode;\n  knownPalette = strip.getMainSegment().palette;\n\n  tft.fillScreen(TFT_BLACK);\n  tft.setTextSize(2);\n  // First row with Wifi name\n  tft.setCursor(1, 1);\n  tft.print(knownSsid.substring(0, tftcharwidth > 1 ? tftcharwidth - 1 : 0));\n  // Print `~` char to indicate that SSID is longer than our display\n  if (knownSsid.length() > tftcharwidth)\n    tft.print(\"~\");\n\n  // Second row with AP IP and Password or IP\n  tft.setTextSize(2);\n  tft.setCursor(1, 24);\n  // Print AP IP and password in AP mode or knownIP if AP not active.\n  // if (apActive && bri == 0)\n  //   tft.print(apPass);\n  // else\n  //   tft.print(knownIp);\n\n  if (apActive) {\n    tft.print(\"AP IP: \");\n    tft.print(knownIp);\n    tft.setCursor(1,46);\n    tft.print(\"AP Pass:\");\n    tft.print(apPass);\n  }\n  else {\n    tft.print(\"IP: \");\n    tft.print(knownIp);\n    tft.setCursor(1,46);\n    //tft.print(\"Signal Strength: \");\n    //tft.print(i.wifi.signal);\n    tft.print(\"Brightness: \");\n    tft.print(((float(bri)/255)*100));\n    tft.print(\"%\");\n  }\n\n  // Third row with mode name\n  tft.setCursor(1, 68);\n  char lineBuffer[tftcharwidth+1];\n  extractModeName(knownMode, JSON_mode_names, lineBuffer, tftcharwidth);\n  tft.print(lineBuffer);\n\n  // Fourth row with palette name\n  tft.setCursor(1, 90);\n  extractModeName(knownPalette, JSON_palette_names, lineBuffer, tftcharwidth);\n  tft.print(lineBuffer);\n\n  // Fifth row with estimated mA usage\n  tft.setCursor(1, 112);\n  // Print estimated milliamp usage (must specify the LED type in LED prefs for this to be a reasonable estimate).\n  tft.print(strip.currentMilliamps);\n  tft.print(\"mA (estimated)\");\n  \n}\n"
  },
  {
    "path": "usermods/Temperature/Temperature.cpp",
    "content": "#include \"UsermodTemperature.h\"\n\nstatic void mode_temperature();\n\n//Dallas sensor quick (& dirty) reading. Credit to - Author: Peter Scargill, August 17th, 2013\nfloat UsermodTemperature::readDallas() {\n  byte data[9];\n  int16_t result;                         // raw data from sensor\n  float retVal = -127.0f;\n  if (oneWire->reset()) {                 // if reset() fails there are no OneWire devices\n    oneWire->skip();                      // skip ROM\n    oneWire->write(0xBE);                 // read (temperature) from EEPROM\n    oneWire->read_bytes(data, 9);         // first 2 bytes contain temperature\n    #ifdef WLED_DEBUG\n    if (OneWire::crc8(data,8) != data[8]) {\n      DEBUG_PRINTLN(F(\"CRC error reading temperature.\"));\n      for (unsigned i=0; i < 9; i++) DEBUG_PRINTF_P(PSTR(\"0x%02X \"), data[i]);\n      DEBUG_PRINT(F(\" => \"));\n      DEBUG_PRINTF_P(PSTR(\"0x%02X\\n\"), OneWire::crc8(data,8));\n    }\n    #endif\n    switch(sensorFound) {\n      case 0x10:  // DS18S20 has 9-bit precision - 1-bit fraction part\n        result = (data[1] << 8) | data[0];\n        retVal = float(result) * 0.5f;\n        break;\n      case 0x22:  // DS1822\n      case 0x28:  // DS18B20\n      case 0x3B:  // DS1825\n      case 0x42:  // DS28EA00\n        // 12-bit precision - 4-bit fraction part\n        result = (data[1] << 8) | data[0];\n        // Clear LSBs to match desired precision (9/10/11-bit) rounding towards negative infinity\n        result &= 0xFFFF << (3 - (resolution & 3));\n        retVal = float(result) * 0.0625f; // 2^(-4)\n        break;\n    }\n  }\n  for (unsigned i=1; i<9; i++) data[0] &= data[i];\n  return data[0]==0xFF ? -127.0f : retVal;\n}\n\nvoid UsermodTemperature::requestTemperatures() {\n  DEBUG_PRINTLN(F(\"Requesting temperature.\"));\n  oneWire->reset();\n  oneWire->skip();                        // skip ROM\n  oneWire->write(0x44,parasite);          // request new temperature reading\n  if (parasite && parasitePin >=0 ) digitalWrite(parasitePin, HIGH); // has to happen within 10us (open MOSFET)\n  lastTemperaturesRequest = millis();\n  waitingForConversion = true;\n}\n\nvoid UsermodTemperature::readTemperature() {\n  if (parasite && parasitePin >=0 ) digitalWrite(parasitePin, LOW); // deactivate power (close MOSFET)\n  temperature = readDallas();\n  lastMeasurement = millis();\n  waitingForConversion = false;\n  //DEBUG_PRINTF_P(PSTR(\"Read temperature %2.1f.\\n\"), temperature); // does not work properly on 8266\n  DEBUG_PRINT(F(\"Read temperature \"));\n  DEBUG_PRINTLN(temperature);\n}\n\nbool UsermodTemperature::findSensor() {\n  DEBUG_PRINTLN(F(\"Searching for sensor...\"));\n  uint8_t deviceAddress[8] = {0,0,0,0,0,0,0,0};\n  // find out if we have DS18xxx sensor attached\n  oneWire->reset_search();\n  delay(10);\n  while (oneWire->search(deviceAddress)) {\n    DEBUG_PRINTLN(F(\"Found something...\"));\n    if (oneWire->crc8(deviceAddress, 7) == deviceAddress[7]) {\n      switch (deviceAddress[0]) {\n        case 0x10:  // DS18S20\n        case 0x22:  // DS1822\n        case 0x28:  // DS18B20\n        case 0x3B:  // DS1825\n        case 0x42:  // DS28EA00\n          DEBUG_PRINTLN(F(\"Sensor found.\"));\n          sensorFound = deviceAddress[0];\n          DEBUG_PRINTF_P(PSTR(\"0x%02X\\n\"), sensorFound);\n          return true;\n      }\n    }\n  }\n  DEBUG_PRINTLN(F(\"Sensor NOT found.\"));\n  return false;\n}\n\n#ifndef WLED_DISABLE_MQTT\nvoid UsermodTemperature::publishHomeAssistantAutodiscovery() {\n  if (!WLED_MQTT_CONNECTED) return;\n\n  char json_str[1024], buf[128];\n  size_t payload_size;\n  StaticJsonDocument<1024> json;\n\n  sprintf_P(buf, PSTR(\"%s Temperature\"), serverDescription);\n  json[F(\"name\")] = buf;\n  strcpy(buf, mqttDeviceTopic);\n  strcat_P(buf, _Temperature);\n  json[F(\"state_topic\")] = buf;\n  json[F(\"device_class\")] = FPSTR(_temperature);\n  json[F(\"unique_id\")] = escapedMac.c_str();\n  json[F(\"unit_of_measurement\")] = F(\"°C\");\n  payload_size = serializeJson(json, json_str);\n\n  sprintf_P(buf, PSTR(\"homeassistant/sensor/%s/config\"), escapedMac.c_str());\n  mqtt->publish(buf, 0, true, json_str, payload_size);\n  HApublished = true;\n}\n#endif\n\nvoid UsermodTemperature::setup() {\n  int retries = 10;\n  sensorFound = 0;\n  temperature = -127.0f; // default to -127, DS18B20 only goes down to -50C\n  if (enabled) {\n    // config says we are enabled\n    DEBUG_PRINTLN(F(\"Allocating temperature pin...\"));\n    // pin retrieved from cfg.json (readFromConfig()) prior to running setup()\n    if (temperaturePin >= 0 && PinManager::allocatePin(temperaturePin, true, PinOwner::UM_Temperature)) {\n      oneWire = new OneWire(temperaturePin);\n      if (oneWire->reset()) {\n        while (!findSensor() && retries--) {\n          delay(25); // try to find sensor\n        }\n      }\n      if (parasite && PinManager::allocatePin(parasitePin, true, PinOwner::UM_Temperature)) {\n        pinMode(parasitePin, OUTPUT);\n        digitalWrite(parasitePin, LOW); // deactivate power (close MOSFET)\n      } else {\n        parasitePin = -1;\n      }\n    } else {\n      if (temperaturePin >= 0) {\n        DEBUG_PRINTLN(F(\"Temperature pin allocation failed.\"));\n      }\n      temperaturePin = -1;  // allocation failed\n    }\n    if (sensorFound && !initDone) strip.addEffect(255, &mode_temperature, _data_fx);\n  }\n  lastMeasurement = millis() - readingInterval + 10000;\n  initDone = true;\n}\n\nvoid UsermodTemperature::loop() {\n  if (!enabled || !sensorFound || strip.isUpdating()) return;\n\n  static uint8_t errorCount = 0;\n  unsigned long now = millis();\n\n  // check to see if we are due for taking a measurement\n  // lastMeasurement will not be updated until the conversion\n  // is complete the the reading is finished\n  if (now - lastMeasurement < readingInterval) return;\n\n  // we are due for a measurement, if we are not already waiting\n  // for a conversion to complete, then make a new request for temps\n  if (!waitingForConversion) {\n    requestTemperatures();\n    return;\n  }\n\n  // we were waiting for a conversion to complete, have we waited log enough?\n  if (now - lastTemperaturesRequest >= 750 /* 93.75ms per the datasheet but can be up to 750ms */) {\n    readTemperature();\n    if (getTemperatureC() < -100.0f) {\n      if (++errorCount > 10) sensorFound = 0;\n      lastMeasurement = now - readingInterval + 300; // force new measurement in 300ms\n      return;\n    }\n    errorCount = 0;\n\n#ifndef WLED_DISABLE_MQTT\n    if (WLED_MQTT_CONNECTED) {\n      char subuf[128];\n      strcpy(subuf, mqttDeviceTopic);\n      if (temperature > -100.0f) {\n        // dont publish super low temperature as the graph will get messed up\n        // the DallasTemperature library returns -127C or -196.6F when problem\n        // reading the sensor\n        strcat_P(subuf, _Temperature);\n        mqtt->publish(subuf, 0, false, String(getTemperatureC()).c_str());\n        strcat_P(subuf, PSTR(\"_f\"));\n        mqtt->publish(subuf, 0, false, String(getTemperatureF()).c_str());\n        if (idx > 0) {\n          StaticJsonDocument <128> msg;\n          msg[F(\"idx\")]    = idx;\n          msg[F(\"RSSI\")]   = WiFi.RSSI();\n          msg[F(\"nvalue\")] = 0;\n          msg[F(\"svalue\")] = String(getTemperatureC());\n          serializeJson(msg, subuf, 127);\n          mqtt->publish(\"domoticz/in\", 0, false, subuf);\n        }\n      } else {\n        // publish something else to indicate status?\n      }\n    }\n#endif\n  }\n}\n\n/**\n * connected() is called every time the WiFi is (re)connected\n * Use it to initialize network interfaces\n */\n//void UsermodTemperature::connected() {}\n\n#ifndef WLED_DISABLE_MQTT\n/**\n * subscribe to MQTT topic if needed\n */\nvoid UsermodTemperature::onMqttConnect(bool sessionPresent) {\n  //(re)subscribe to required topics\n  //char subuf[64];\n  if (mqttDeviceTopic[0] != 0) {\n    publishHomeAssistantAutodiscovery();\n  }\n}\n#endif\n\n/*\n  * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.\n  * Creating an \"u\" object allows you to add custom key/value pairs to the Info section of the WLED web UI.\n  * Below it is shown how this could be used for e.g. a light sensor\n  */\nvoid UsermodTemperature::addToJsonInfo(JsonObject& root) {\n  // dont add temperature to info if we are disabled\n  if (!enabled) return;\n\n  JsonObject user = root[\"u\"];\n  if (user.isNull()) user = root.createNestedObject(\"u\");\n\n  JsonArray temp = user.createNestedArray(FPSTR(_name));\n\n  if (temperature <= -100.0f) {\n    temp.add(0);\n    temp.add(F(\" Sensor Error!\"));\n    return;\n  }\n\n  temp.add(getTemperature());\n  temp.add(getTemperatureUnit());\n\n  JsonObject sensor = root[FPSTR(_sensor)];\n  if (sensor.isNull()) sensor = root.createNestedObject(FPSTR(_sensor));\n  temp = sensor.createNestedArray(FPSTR(_temperature));\n  temp.add(getTemperature());\n  temp.add(getTemperatureUnit());\n}\n\n/**\n * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).\n * Values in the state object may be modified by connected clients\n */\n//void UsermodTemperature::addToJsonState(JsonObject &root)\n//{\n//}\n\n/**\n * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).\n * Values in the state object may be modified by connected clients\n * Read \"<usermodname>_<usermodparam>\" from json state and and change settings (i.e. GPIO pin) used.\n */\n//void UsermodTemperature::readFromJsonState(JsonObject &root) {\n//  if (!initDone) return;  // prevent crash on boot applyPreset()\n//}\n\n/**\n * addToConfig() (called from set.cpp) stores persistent properties to cfg.json\n */\nvoid UsermodTemperature::addToConfig(JsonObject &root) {\n  // we add JSON object: {\"Temperature\": {\"pin\": 0, \"degC\": true}}\n  JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname\n  top[FPSTR(_enabled)] = enabled;\n  top[\"pin\"]  = temperaturePin;     // usermodparam\n  top[F(\"degC\")] = degC;  // usermodparam\n  top[FPSTR(_readInterval)] = readingInterval / 1000;\n  top[FPSTR(_parasite)] = parasite;\n  top[FPSTR(_parasitePin)] = parasitePin;\n  top[FPSTR(_domoticzIDX)] = idx;\n  top[FPSTR(_resolution)] = resolution;\n  DEBUG_PRINTLN(F(\"Temperature config saved.\"));\n}\n\n/**\n * readFromConfig() is called before setup() to populate properties from values stored in cfg.json\n *\n * The function should return true if configuration was successfully loaded or false if there was no configuration.\n */\nbool UsermodTemperature::readFromConfig(JsonObject &root) {\n  // we look for JSON object: {\"Temperature\": {\"pin\": 0, \"degC\": true}}\n  int8_t newTemperaturePin = temperaturePin;\n  DEBUG_PRINT(FPSTR(_name));\n\n  JsonObject top = root[FPSTR(_name)];\n  if (top.isNull()) {\n    DEBUG_PRINTLN(F(\": No config found. (Using defaults.)\"));\n    return false;\n  }\n\n  enabled           = top[FPSTR(_enabled)] | enabled;\n  newTemperaturePin = top[\"pin\"] | newTemperaturePin;\n  degC              = top[F(\"degC\")] | degC;\n  readingInterval   = top[FPSTR(_readInterval)] | readingInterval/1000;\n  readingInterval   = min(120,max(10,(int)readingInterval)) * 1000;  // convert to ms\n  parasite          = top[FPSTR(_parasite)] | parasite;\n  parasitePin       = top[FPSTR(_parasitePin)] | parasitePin;\n  idx               = top[FPSTR(_domoticzIDX)] | idx;\n  resolution        = top[FPSTR(_resolution)] | resolution;\n\n  if (!initDone) {\n    // first run: reading from cfg.json\n    temperaturePin = newTemperaturePin;\n    DEBUG_PRINTLN(F(\" config loaded.\"));\n  } else {\n    DEBUG_PRINTLN(F(\" config (re)loaded.\"));\n    // changing paramters from settings page\n    if (newTemperaturePin != temperaturePin) {\n      DEBUG_PRINTLN(F(\"Re-init temperature.\"));\n      // deallocate pin and release memory\n      delete oneWire;\n      PinManager::deallocatePin(temperaturePin, PinOwner::UM_Temperature);\n      temperaturePin = newTemperaturePin;\n      PinManager::deallocatePin(parasitePin, PinOwner::UM_Temperature);\n      // initialise\n      setup();\n    }\n  }\n  // use \"return !top[\"newestParameter\"].isNull();\" when updating Usermod with new features\n  return !top[FPSTR(_resolution)].isNull();\n}\n\nvoid UsermodTemperature::appendConfigData() {\n  oappend(F(\"addInfo('\")); oappend(String(FPSTR(_name)).c_str()); oappend(F(\":\")); oappend(String(FPSTR(_parasite)).c_str());\n  oappend(F(\"',1,'<i>(if no Vcc connected)</i>');\"));  // 0 is field type, 1 is actual field\n  oappend(F(\"addInfo('\")); oappend(String(FPSTR(_name)).c_str()); oappend(F(\":\")); oappend(String(FPSTR(_parasitePin)).c_str());\n  oappend(F(\"',1,'<i>(for external MOSFET)</i>');\"));  // 0 is field type, 1 is actual field\n  oappend(F(\"dd=addDD('\")); oappend(String(FPSTR(_name)).c_str()); \n    oappend(F(\"','\")); oappend(String(FPSTR(_resolution)).c_str()); oappend(F(\"');\"));\n  oappend(F(\"addO(dd,'0.5 °C (9-bit)',0);\"));\n  oappend(F(\"addO(dd,'0.25°C (10-bit)',1);\"));\n  oappend(F(\"addO(dd,'0.125°C (11-bit)',2);\"));\n  oappend(F(\"addO(dd,'0.0625°C (12-bit)',3);\"));\n  oappend(F(\"addInfo('\")); oappend(String(FPSTR(_name)).c_str()); oappend(F(\":\")); oappend(String(FPSTR(_resolution)).c_str());\n  oappend(F(\"',1,'<i>(ignored on DS18S20)</i>');\"));  // 0 is field type, 1 is actual field\n}\n\nfloat UsermodTemperature::getTemperature() {\n  return degC ? getTemperatureC() : getTemperatureF();\n}\n\nconst char *UsermodTemperature::getTemperatureUnit() {\n  return degC ? \"°C\" : \"°F\";\n}\n\nUsermodTemperature* UsermodTemperature::_instance = nullptr;\n\n// strings to reduce flash memory usage (used more than twice)\nconst char UsermodTemperature::_name[]         PROGMEM = \"Temperature\";\nconst char UsermodTemperature::_enabled[]      PROGMEM = \"enabled\";\nconst char UsermodTemperature::_readInterval[] PROGMEM = \"read-interval-s\";\nconst char UsermodTemperature::_parasite[]     PROGMEM = \"parasite-pwr\";\nconst char UsermodTemperature::_parasitePin[]  PROGMEM = \"parasite-pwr-pin\";\nconst char UsermodTemperature::_domoticzIDX[]  PROGMEM = \"domoticz-idx\";\nconst char UsermodTemperature::_resolution[]   PROGMEM = \"resolution\";\nconst char UsermodTemperature::_sensor[]       PROGMEM = \"sensor\";\nconst char UsermodTemperature::_temperature[]  PROGMEM = \"temperature\";\nconst char UsermodTemperature::_Temperature[]  PROGMEM = \"/temperature\";\nconst char UsermodTemperature::_data_fx[]      PROGMEM = \"Temperature@Min,Max;;!;01;pal=54,sx=255,ix=0\";\n\nstatic void mode_temperature() {\n  float low  = roundf(mapf((float)SEGMENT.speed, 0.f, 255.f, -150.f, 150.f));    // default: 15°C, range: -15°C to 15°C\n  float high = roundf(mapf((float)SEGMENT.intensity, 0.f, 255.f, 300.f, 600.f));  // default: 30°C, range 30°C to 60°C\n  float temp = constrain(UsermodTemperature::getInstance()->getTemperatureC()*10.f, low, high);   // get a little better resolution (*10)\n  unsigned i = map(roundf(temp), (unsigned)low, (unsigned)high, 0, 248);\n  SEGMENT.fill(SEGMENT.color_from_palette(i, false, false, 255));\n}\n\n\nstatic UsermodTemperature temperature;\nREGISTER_USERMOD(temperature);"
  },
  {
    "path": "usermods/Temperature/UsermodTemperature.h",
    "content": "#pragma once\n#include \"wled.h\"\n#include \"OneWire.h\"\n\n//Pin defaults for QuinLed Dig-Uno if not overriden\n#ifndef TEMPERATURE_PIN\n  #ifdef ARDUINO_ARCH_ESP32\n    #define TEMPERATURE_PIN 18\n  #else //ESP8266 boards\n    #define TEMPERATURE_PIN 14\n  #endif\n#endif\n\n// the frequency to check temperature, 1 minute\n#ifndef USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL\n#define USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL 60000\n#endif\n\nclass UsermodTemperature : public Usermod {\n\n  private:\n\n    bool initDone = false;\n    OneWire *oneWire;\n    // GPIO pin used for sensor (with a default compile-time fallback)\n    int8_t temperaturePin = TEMPERATURE_PIN;\n    // measurement unit (true==°C, false==°F)\n    bool degC = true;\n    // using parasite power on the sensor\n    bool parasite = false;\n    int8_t parasitePin = -1;\n    // how often do we read from sensor?\n    unsigned long readingInterval = USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL;\n    // set last reading as \"40 sec before boot\", so first reading is taken after 20 sec\n    unsigned long lastMeasurement = UINT32_MAX - USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL;\n    // last time requestTemperatures was called\n    // used to determine when we can read the sensors temperature\n    // we have to wait at least 93.75 ms after requestTemperatures() is called\n    unsigned long lastTemperaturesRequest;\n    float temperature;\n    // indicates requestTemperatures has been called but the sensor measurement is not complete\n    bool waitingForConversion = false;\n    // flag set at startup if DS18B20 sensor not found, avoids trying to keep getting\n    // temperature if flashed to a board without a sensor attached\n    byte sensorFound;\n\n    bool enabled = true;\n\n    bool HApublished = false;\n    int16_t idx = -1;   // Domoticz virtual sensor idx\n    uint8_t resolution = 0; // 9bits=0, 10bits=1, 11bits=2, 12bits=3\n\n    // strings to reduce flash memory usage (used more than twice)\n    static const char _name[];\n    static const char _enabled[];\n    static const char _readInterval[];\n    static const char _parasite[];\n    static const char _parasitePin[];\n    static const char _domoticzIDX[];\n    static const char _resolution[];\n    static const char _sensor[];\n    static const char _temperature[];\n    static const char _Temperature[];\n    static const char _data_fx[];\n    \n    //Dallas sensor quick (& dirty) reading. Credit to - Author: Peter Scargill, August 17th, 2013\n    float readDallas();\n    void requestTemperatures();\n    void readTemperature();\n    bool findSensor();\n#ifndef WLED_DISABLE_MQTT\n    void publishHomeAssistantAutodiscovery();\n#endif\n\n    static UsermodTemperature* _instance; // to overcome nonstatic getTemperatureC() method and avoid UsermodManager::lookup(USERMOD_ID_TEMPERATURE);\n\n  public:\n\n    UsermodTemperature() { _instance = this; }\n    static UsermodTemperature *getInstance() { return UsermodTemperature::_instance; }\n\n    /*\n     * API calls te enable data exchange between WLED modules\n     */\n    inline float getTemperatureC() { return temperature; }\n    inline float getTemperatureF() { return temperature * 1.8f + 32.0f; }\n    float getTemperature();\n    const char *getTemperatureUnit();\n    uint16_t getId() override { return USERMOD_ID_TEMPERATURE; }\n\n    void setup() override;\n    void loop() override;\n    //void connected() override;\n#ifndef WLED_DISABLE_MQTT\n    void onMqttConnect(bool sessionPresent) override;\n#endif\n    //void onUpdateBegin(bool init) override;\n\n    //bool handleButton(uint8_t b) override;\n    //void handleOverlayDraw() override;\n\n    void addToJsonInfo(JsonObject& root) override;\n    //void addToJsonState(JsonObject &root) override;\n    //void readFromJsonState(JsonObject &root) override;\n    void addToConfig(JsonObject &root) override;\n    bool readFromConfig(JsonObject &root) override;\n\n    void appendConfigData() override;\n};\n\n"
  },
  {
    "path": "usermods/Temperature/library.json",
    "content": "{\n  \"name\": \"Temperature\",\n  \"build\": { \"libArchive\": false},\n  \"dependencies\": {\n    \"paulstoffregen/OneWire\":\"~2.3.8\"\n  }\n}\n"
  },
  {
    "path": "usermods/Temperature/readme.md",
    "content": "# Temperature usermod\n\nBased on the excellent `QuinLED_Dig_Uno_Temp_MQTT` usermod by srg74 and 400killer!  \nReads an attached DS18B20 temperature sensor (as available on the QuinLED Dig-Uno)  \nTemperature is displayed in both the Info section of the web UI as well as published to the `/temperature` MQTT topic, if enabled.  \nMay be expanded with support for different sensor types in the future.\n\nIf temperature sensor is not detected during boot, this usermod will be disabled.\n\nMaintained by @blazoncek\n\n## Installation\n\nAdd `Temperature` to `custom_usermods` in your platformio_override.ini.\n\nExample **platformio_override.ini**:\n\n```ini\n[env:usermod_temperature_esp32dev]\nextends = env:esp32dev\ncustom_usermods = ${env:esp32dev.custom_usermods} \n  Temperature\n```\n\n### Define Your Options\n\n* `USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL` - number of milliseconds between measurements, defaults to 60000 ms (60s)\n\nAll parameters can be configured at runtime via the Usermods settings page, including pin, temperature in degrees Celsius or Fahrenheit and measurement interval.\n\n## Project link\n\n* [QuinLED-Dig-Uno](https://quinled.info/2018/09/15/quinled-dig-uno/) - Project link\n* [Srg74-WLED-Wemos-shield](https://github.com/srg74/WLED-wemos-shield) - another great DIY WLED board\n\n## Change Log\n\n2020-09-12\n\n* Changed to use async non-blocking implementation\n* Do not report erroneous low temperatures to MQTT\n* Disable plugin if temperature sensor not detected\n* Report the number of seconds until the first read in the info screen instead of sensor error\n\n2021-04\n\n* Adaptation for runtime configuration.\n\n2023-05\n\n* Rewrite to conform to newer recommendations.\n* Recommended @blazoncek fork of OneWire for ESP32 to avoid Sensor error\n\n2024-09\n\n* Update OneWire to version 2.3.8, which includes stickbreaker's and garyd9's ESP32 fixes:\n  blazoncek's fork is no longer needed\n"
  },
  {
    "path": "usermods/TetrisAI_v2/TetrisAI_v2.cpp",
    "content": "#include \"wled.h\"\n#include \"FX.h\"\n#include \"fcn_declare.h\"\n\n#include \"tetrisaigame.h\"\n// By: muebau\n\nbool noFlashOnClear = false;\n\ntypedef struct TetrisAI_data\n{\n  unsigned long lastTime = 0;\n  unsigned long clearingStartTime = 0;\n  TetrisAIGame tetris;\n  uint8_t   intelligence;\n  uint8_t   rotate;\n  bool      showNext;\n  bool      showBorder;\n  uint8_t   colorOffset;\n  uint8_t   colorInc;\n  uint8_t   mistaceCountdown;\n  uint16_t segcols;\n  uint16_t segrows;\n  uint16_t segOffsetX;\n  uint16_t segOffsetY;\n  uint16_t effectWidth;\n  uint16_t effectHeight;\n} tetrisai_data;\n\nvoid drawGrid(TetrisAIGame* tetris, TetrisAI_data* tetrisai_data)\n{\n  SEGMENT.fill(SEGCOLOR(1));\n\n  //GRID\n  for (auto index_y = 4; index_y < tetris->grid.height; index_y++)\n  {\n    bool isRowClearing = tetris->grid.gridBW.clearingRows[index_y];\n    for (auto index_x = 0; index_x < tetris->grid.width; index_x++)\n    {\n      CRGB color;\n      uint8_t gridPixel = *tetris->grid.getPixel(index_x, index_y);\n      if (isRowClearing) {\n        if (noFlashOnClear) {\n          color = CRGB::Gray; \n        } else {\n          //flash color white and black every 200ms\n          color = (strip.now % 200) < 150\n            ? CRGB::Gray\n            : CRGB::Black;\n        }\n      }\n      else if (gridPixel == 0) {\n        //BG color\n        color = SEGCOLOR(1);\n      }\n      //game over animation\n      else if (gridPixel == 254)\n      {\n        //use fg\n        color = SEGCOLOR(0);\n      }\n      else\n      {\n        //spread the color over the whole palette\n        uint8_t colorIndex = gridPixel * 32;\n        colorIndex += tetrisai_data->colorOffset;\n        color = ColorFromPalette(SEGPALETTE, colorIndex, 255, NOBLEND);\n      }\n\n      SEGMENT.setPixelColorXY(tetrisai_data->segOffsetX + index_x, tetrisai_data->segOffsetY + index_y - 4, color);\n    }\n  }\n  tetrisai_data->colorOffset += tetrisai_data->colorInc;\n\n  //NEXT PIECE AREA\n  if (tetrisai_data->showNext)\n  {\n    //BORDER\n    if (tetrisai_data->showBorder)\n    {\n      //draw a line 6 pixels from right with the border color\n      for (auto index_y = 0; index_y < tetrisai_data->effectHeight; index_y++)\n      {\n        SEGMENT.setPixelColorXY(tetrisai_data->segOffsetX + tetrisai_data->effectWidth - 6, tetrisai_data->segOffsetY + index_y, SEGCOLOR(2));\n      }\n    }\n\n    //NEXT PIECE\n    int piecesOffsetX = tetrisai_data->effectWidth - 4;\n    int piecesOffsetY = 1;\n    for (uint8_t nextPieceIdx = 1; nextPieceIdx < tetris->nLookAhead; nextPieceIdx++)\n    {\n      uint8_t pieceNbrOffsetY = (nextPieceIdx - 1) * 5;\n\n      Piece piece(tetris->bag.piecesQueue[nextPieceIdx]);\n\n      for (uint8_t pieceY = 0; pieceY < piece.getRotation().height; pieceY++)\n      {\n        for (uint8_t pieceX = 0; pieceX < piece.getRotation().width; pieceX++)\n        {\n          if (piece.getPixel(pieceX, pieceY))\n          {\n            uint8_t colIdx = ((piece.pieceData->colorIndex * 32) + tetrisai_data->colorOffset);\n            SEGMENT.setPixelColorXY(tetrisai_data->segOffsetX + piecesOffsetX + pieceX, tetrisai_data->segOffsetY + piecesOffsetY + pieceNbrOffsetY + pieceY, ColorFromPalette(SEGPALETTE, colIdx, 255, NOBLEND));\n          }\n        }\n      }\n    }\n  }\n}\n\n////////////////////////////\n//     2D Tetris AI       //\n////////////////////////////\nvoid mode_2DTetrisAI()\n{\n  if (!strip.isMatrix || !SEGENV.allocateData(sizeof(tetrisai_data)))\n  {\n    // not a 2D set-up\n    SEGMENT.fill(SEGCOLOR(0));\n    return;\n  }\n  TetrisAI_data* tetrisai_data = reinterpret_cast<TetrisAI_data*>(SEGENV.data);\n\n  const uint16_t cols = SEGMENT.virtualWidth();\n  const uint16_t rows = SEGMENT.virtualHeight();\n\n  //range 0 - 1024ms => 1024/255 ~ 4\n  uint16_t msDelayMove = 1024 - (4 * SEGMENT.speed);\n  int16_t msDelayGameOver = msDelayMove / 4;\n\n  //range 0 - 2 (not including current)\n  uint8_t nLookAhead = SEGMENT.intensity ? (SEGMENT.intensity >> 7) + 2 : 1;\n  //range 0 - 16\n  tetrisai_data->colorInc = SEGMENT.custom2 >> 4;\n\n  if (tetrisai_data->tetris.nLookAhead != nLookAhead\n    || tetrisai_data->segcols != cols\n    || tetrisai_data->segrows != rows\n    || tetrisai_data->showNext != SEGMENT.check1\n    || tetrisai_data->showBorder != SEGMENT.check2\n  )\n  {\n    tetrisai_data->segcols = cols;\n    tetrisai_data->segrows = rows;\n    tetrisai_data->showNext = SEGMENT.check1;\n    tetrisai_data->showBorder = SEGMENT.check2;\n\n    //not more than 32 columns and 255 rows as this is the limit of this implementation\n    uint8_t gridWidth = cols > 32 ? 32 : cols;\n    uint8_t gridHeight = rows > 255 ? 255 : rows;\n\n    tetrisai_data->effectWidth = 0;\n    tetrisai_data->effectHeight = 0;\n\n    // do we need space for the 'next' section?\n    if (tetrisai_data->showNext)\n    {\n      //does it get to tight?\n      if (gridWidth + 5 > cols)\n      {\n        // yes, so make the grid smaller\n        // make space for the piece and one pixel of space\n        gridWidth = (gridWidth - ((gridWidth + 5) - cols));\n      }\n      tetrisai_data->effectWidth += 5;\n\n      // do we need space for a border?\n      if (tetrisai_data->showBorder)\n      {\n        if (gridWidth + 5 + 1 > cols)\n        {\n          gridWidth -= 1;\n        }\n        tetrisai_data->effectWidth += 1;\n      }\n    }\n\n    tetrisai_data->effectWidth += gridWidth;\n    tetrisai_data->effectHeight += gridHeight;\n\n    tetrisai_data->segOffsetX = cols > tetrisai_data->effectWidth ? ((cols - tetrisai_data->effectWidth) / 2) : 0;\n    tetrisai_data->segOffsetY = rows > tetrisai_data->effectHeight ? ((rows - tetrisai_data->effectHeight) / 2) : 0;\n\n    tetrisai_data->tetris = TetrisAIGame(gridWidth, gridHeight, nLookAhead, piecesData, numPieces);\n    tetrisai_data->tetris.state = TetrisAIGame::States::INIT;\n    tetrisai_data->clearingStartTime = 0;\n    SEGMENT.fill(SEGCOLOR(1));\n  }\n\n  if (tetrisai_data->intelligence != SEGMENT.custom1)\n  {\n    tetrisai_data->intelligence = SEGMENT.custom1;\n    float dui = 0.2f - (0.2f * (tetrisai_data->intelligence / 255.0f));\n\n    tetrisai_data->tetris.ai.aHeight = -0.510066f + dui;\n    tetrisai_data->tetris.ai.fullLines = 0.760666f - dui;\n    tetrisai_data->tetris.ai.holes = -0.35663f + dui;\n    tetrisai_data->tetris.ai.bumpiness = -0.184483f + dui;\n  }\n\n  //end line clearing flashing effect if needed\n  if (tetrisai_data->tetris.grid.gridBW.hasClearingRows())\n  {\n    if (tetrisai_data->clearingStartTime == 0) {\n      tetrisai_data->clearingStartTime = strip.now;\n    }\n    if (strip.now - tetrisai_data->clearingStartTime > 750)\n    {\n      tetrisai_data->tetris.grid.gridBW.clearedLinesReadyForRemoval = true;\n      tetrisai_data->tetris.grid.cleanupFullLines();\n      tetrisai_data->clearingStartTime = 0;\n    }\n    drawGrid(&tetrisai_data->tetris, tetrisai_data);\n  }\n  else if (tetrisai_data->tetris.state == TetrisAIGame::ANIMATE_MOVE)\n  {\n    \n    if (strip.now - tetrisai_data->lastTime > msDelayMove)\n    {\n      drawGrid(&tetrisai_data->tetris, tetrisai_data);\n      tetrisai_data->lastTime = strip.now;\n      tetrisai_data->tetris.poll();\n    }\n  }\n  else if (tetrisai_data->tetris.state == TetrisAIGame::ANIMATE_GAME_OVER)\n  {\n    if (strip.now - tetrisai_data->lastTime > msDelayGameOver)\n    {\n      drawGrid(&tetrisai_data->tetris, tetrisai_data);\n      tetrisai_data->lastTime = strip.now;\n      tetrisai_data->tetris.poll();\n    }\n  }\n  else if (tetrisai_data->tetris.state == TetrisAIGame::FIND_BEST_MOVE)\n  {\n    if (SEGMENT.check3)\n    {\n      if(tetrisai_data->mistaceCountdown == 0)\n      {\n        tetrisai_data->tetris.ai.findWorstMove = true;\n        tetrisai_data->tetris.poll();\n        tetrisai_data->tetris.ai.findWorstMove = false;\n        tetrisai_data->mistaceCountdown = SEGMENT.custom3;\n      }\n      tetrisai_data->mistaceCountdown--;      \n    }\n    tetrisai_data->tetris.poll();\n  }\n  else\n  {\n    tetrisai_data->tetris.poll();\n  }\n} // mode_2DTetrisAI()\nstatic 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\";\n\nclass TetrisAIUsermod : public Usermod\n{\n\nprivate:\n  static const char _name[];\n\npublic:\n  void setup()\n  {\n    strip.addEffect(255, &mode_2DTetrisAI, _data_FX_MODE_2DTETRISAI);\n  }\n\n  void addToConfig(JsonObject& root) override\n  {\n    JsonObject top = root.createNestedObject(FPSTR(_name));\n    top[\"noFlashOnClear\"] = noFlashOnClear;\n  }\n\n  bool readFromConfig(JsonObject& root) override\n  {\n    JsonObject top = root[FPSTR(_name)];\n    bool configComplete = !top.isNull();\n    configComplete &= getJsonValue(top[\"noFlashOnClear\"], noFlashOnClear);\n    return configComplete;\n  }\n\n  void loop()\n  {\n\n  }\n\n  uint16_t getId()\n  {\n    return USERMOD_ID_TETRISAI;\n  }\n};\n\nconst char TetrisAIUsermod::_name[] PROGMEM = \"TetrisAI_v2\";\n\nstatic TetrisAIUsermod tetrisai_v2;\nREGISTER_USERMOD(tetrisai_v2);"
  },
  {
    "path": "usermods/TetrisAI_v2/gridbw.h",
    "content": "/******************************************************************************\n  * @file           : gridbw.h\n  * @brief          : contains the tetris grid as binary so black and white version\n  ******************************************************************************\n  * @attention\n  *\n  * Copyright (c) muebau 2023\n  * All rights reserved.</center></h2>\n  *\n  ******************************************************************************\n*/\n\n#ifndef __GRIDBW_H__\n#define __GRIDBW_H__\n\n#include <vector>\n#include \"pieces.h\"\n\nusing namespace std;\n\nclass GridBW\n{\nprivate:\npublic:\n    uint8_t width;\n    uint8_t height;\n    std::vector<uint32_t> pixels;\n    // When a row fills, we mark it here first so it can flash before being\n    // fully removed.\n    std::vector<bool> clearingRows;\n    // True when a line clearing flashing effect is over and we're ready to\n    // fully clean up the lines\n    bool clearedLinesReadyForRemoval = false;\n\n    GridBW(uint8_t width, uint8_t height):\n        width(width),\n        height(height),\n        pixels(height),\n        clearingRows(height)\n    {\n        if (width > 32)\n        {\n            this->width = 32;\n        }\n    }\n\n    void placePiece(Piece* piece, uint8_t x, uint8_t y)\n    {\n        for (uint8_t row = 4 - piece->getRotation().height; row < 4; row++)\n        {\n            pixels[y + (row - (4 - piece->getRotation().height))] |= piece->getGridRow(x, row, width);\n        }\n    }\n\n    void erasePiece(Piece* piece, uint8_t x, uint8_t y)\n    {\n        for (uint8_t row = 4 - piece->getRotation().height; row < 4; row++)\n        {\n            pixels[y + (row - (4 - piece->getRotation().height))] &= ~piece->getGridRow(x, row, width);\n        }\n    }\n\n    bool noCollision(Piece* piece, uint8_t x, uint8_t y)\n    {\n        //if it touches a wall it is a collision\n        if (x > (this->width - piece->getRotation().width) || y > this->height - piece->getRotation().height)\n        {\n            return false;\n        }\n\n        for (uint8_t row = 4 - piece->getRotation().height; row < 4; row++)\n        {\n            if (piece->getGridRow(x, row, width) & pixels[y + (row - (4 - piece->getRotation().height))])\n            {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    void findLandingPosition(Piece* piece)\n    {\n        // move down until the piece bumps into some occupied pixels or the 'wall'\n        while (noCollision(piece, piece->x, piece->landingY))\n        {\n            piece->landingY++;\n        }\n\n        //at this point the positon is 'in the wall' or 'over some occupied pixel'\n        //so the previous position was the last correct one (clamped to 0 as minimum).\n        piece->landingY = piece->landingY > 0 ? piece->landingY - 1 : 0;\n    }\n\n    bool hasClearingRows()\n    {\n        for (bool rowClearing : clearingRows)\n        {\n            if (rowClearing)\n            {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    void cleanupFullLines()\n    {\n        // Skip cleanup if there are rows clearing\n        if (hasClearingRows() && !clearedLinesReadyForRemoval) {\n            return;\n        }\n        uint8_t offset = 0;\n        bool doneRemovingClearedLines = false;\n\n        //from \"height - 1\" to \"0\", so from bottom row to top\n        for (uint8_t row = height; row-- > 0; )\n        {\n            //full line?\n            if (isLineFull(row))\n            {\n                if (clearedLinesReadyForRemoval) {\n                    offset++;\n                    pixels[row] = 0x0;\n                    doneRemovingClearedLines = true;\n                } else {\n                    clearingRows[row] = true;\n                }\n                continue;\n            }\n\n            if (offset > 0)\n            {\n                pixels[row + offset] = pixels[row];\n                pixels[row] = 0x0;\n            }\n        }\n        if (doneRemovingClearedLines) {\n            clearingRows.assign(height, false);\n            clearedLinesReadyForRemoval = false;\n        }\n    }\n\n    bool isLineFull(uint8_t y)\n    {\n        return pixels[y] == (width >= 32 ? UINT32_MAX : (1U << width) - 1);\n    }\n\n    bool isLineReadyForRemoval(uint8_t y)\n    {\n        return clearedLinesReadyForRemoval && isLineFull(y);\n    }\n\n    void reset()\n    {\n        if (width > 32)\n        {\n            width = 32;\n        }\n\n        pixels.clear();\n        pixels.resize(height);\n        clearingRows.assign(height, false);\n        clearedLinesReadyForRemoval = false;\n    }\n};\n\n#endif /* __GRIDBW_H__ */"
  },
  {
    "path": "usermods/TetrisAI_v2/gridcolor.h",
    "content": "/******************************************************************************\n  * @file           : gridcolor.h\n  * @brief          : contains the tetris grid as 8bit indexed color version\n  ******************************************************************************\n  * @attention\n  *\n  * Copyright (c) muebau 2023\n  * All rights reserved.</center></h2>\n  *\n  ******************************************************************************\n*/\n\n#ifndef __GRIDCOLOR_H__\n#define __GRIDCOLOR_H__\n#include <stdint.h>\n#include <stdbool.h>\n#include <vector>\n#include \"gridbw.h\"\n#include \"gridcolor.h\"\n\nusing namespace std;\n\nclass GridColor\n{\nprivate:\npublic:\n    uint8_t width;\n    uint8_t height;\n    GridBW gridBW;\n    std::vector<uint8_t> pixels;\n\n    GridColor(uint8_t width, uint8_t height):\n        width(width),\n        height(height),\n        gridBW(width, height),\n        pixels(width* height)\n    {}\n\n    void clear()\n    {\n        for (uint8_t y = 0; y < height; y++)\n        {\n            gridBW.pixels[y] = 0x0;\n            for (int8_t x = 0; x < width; x++)\n            {\n                *getPixel(x, y) = 0;\n            }\n        }\n    }\n\n    void placePiece(Piece* piece, uint8_t x, uint8_t y)\n    {\n        for (uint8_t pieceY = 0; pieceY < piece->getRotation().height; pieceY++)\n        {\n            for (uint8_t pieceX = 0; pieceX < piece->getRotation().width; pieceX++)\n            {\n                if (piece->getPixel(pieceX, pieceY))\n                {\n                    *getPixel(x + pieceX, y + pieceY) = piece->pieceData->colorIndex;\n                }\n            }\n        }\n    }\n\n    void erasePiece(Piece* piece, uint8_t x, uint8_t y)\n    {\n        for (uint8_t pieceY = 0; pieceY < piece->getRotation().height; pieceY++)\n        {\n            for (uint8_t pieceX = 0; pieceX < piece->getRotation().width; pieceX++)\n            {\n                if (piece->getPixel(pieceX, pieceY))\n                {\n                    *getPixel(x + pieceX, y + pieceY) = 0;\n                }\n            }\n        }\n    }\n\n    void cleanupFullLines()\n    {\n        uint8_t offset = 0;\n        //from \"height - 1\" to \"0\", so from bottom row to top\n        for (uint8_t y = height; y-- > 0; )\n        {\n            if (gridBW.isLineReadyForRemoval(y))\n            {\n                offset++;\n                for (uint8_t x = 0; x < width; x++)\n                {\n                    pixels[y * width + x] = 0;\n                }\n                continue;\n            }\n\n            if (offset > 0)\n            {\n                if (gridBW.pixels[y])\n                {\n                    for (uint8_t x = 0; x < width; x++)\n                    {\n                        pixels[(y + offset) * width + x] = pixels[y * width + x];\n                        pixels[y * width + x] = 0;\n                    }\n                }\n            }\n        }\n        gridBW.cleanupFullLines();\n    }\n\n    uint8_t* getPixel(uint8_t x, uint8_t y)\n    {\n        return &pixels[y * width + x];\n    }\n\n    void sync()\n    {\n        for (uint8_t y = 0; y < height; y++)\n        {\n            gridBW.pixels[y] = 0x0;\n            for (int8_t x = 0; x < width; x++)\n            {\n                gridBW.pixels[y] <<= 1;\n                if (*getPixel(x, y) != 0)\n                {\n                    gridBW.pixels[y] |= 0x1;\n                }\n            }\n        }\n    }\n\n    void reset()\n    {\n        gridBW.reset();\n        pixels.clear();\n        pixels.resize(width* height);\n        clear();\n    }\n};\n\n#endif /* __GRIDCOLOR_H__ */"
  },
  {
    "path": "usermods/TetrisAI_v2/library.json",
    "content": "{\n  \"name\": \"TetrisAI_v2\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/TetrisAI_v2/pieces.h",
    "content": "/******************************************************************************\n  * @file           : pieces.h\n  * @brief          : contains the tetris pieces with their colors indecies\n  ******************************************************************************\n  * @attention\n  *\n  * Copyright (c) muebau 2022\n  * All rights reserved.</center></h2>\n  *\n  ******************************************************************************\n*/\n\n#ifndef __PIECES_H__\n#define __PIECES_H__\n\n#include <stdint.h>\n#include <stdbool.h>\n\n#include <bitset>\n#include <cstddef>\n#include <cassert>\n\n#define numPieces 7\n\nstruct PieceRotation\n{\n    uint8_t width;\n    uint8_t height;\n    uint16_t rows;\n};\n\nstruct PieceData\n{\n    uint8_t rotCount;\n    uint8_t colorIndex;\n    PieceRotation rotations[4];\n};\n\nPieceData piecesData[numPieces] = {\n    // I\n    {\n            2,\n            1,\n            {\n                { 1, 4, 0b0001000100010001},\n                { 4, 1, 0b0000000000001111}\n            }\n    },\n    // O\n    {\n            1,\n            2,\n            {\n                { 2, 2, 0b0000000000110011}\n            }\n    },\n    // Z\n    {\n            2,\n            3,\n            {\n                { 3, 2, 0b0000000001100011},\n                { 2, 3, 0b0000000100110010}\n            }\n    },\n    // S\n    {\n            2,\n            4,\n            {\n                { 3, 2, 0b0000000000110110},\n                { 2, 3, 0b0000001000110001}\n            }\n    },\n    // L\n    {\n            4,\n            5,\n            {\n                { 2, 3, 0b0000001000100011},\n                { 3, 2, 0b0000000001110100},\n                { 2, 3, 0b0000001100010001},\n                { 3, 2, 0b0000000000010111}\n            }\n    },\n    // J\n    {\n            4,\n            6,\n            {\n                { 2, 3, 0b0000000100010011},\n                { 3, 2, 0b0000000001000111},\n                { 2, 3, 0b0000001100100010},\n                { 3, 2, 0b0000000001110001}\n            }\n    },\n    // T\n    {\n            4,\n            7,\n            {\n                { 3, 2, 0b0000000001110010},\n                { 2, 3, 0b0000000100110001},\n                { 3, 2, 0b0000000000100111},\n                { 2, 3, 0b0000001000110010}\n            }\n    },\n};\n\nclass Piece\n{\nprivate:\npublic:\n    uint8_t x;\n    uint8_t y;\n    PieceData* pieceData;\n    uint8_t rotation;\n    uint8_t landingY;\n\n    Piece(uint8_t pieceIndex = 0):\n        x(0),\n        y(0),\n        rotation(0),\n        landingY(0)\n    {\n        this->pieceData = &piecesData[pieceIndex];\n    }\n\n    void reset()\n    {\n        this->rotation = 0;\n        this->x = 0;\n        this->y = 0;\n        this->landingY = 0;\n    }\n\n    uint32_t getGridRow(uint8_t x, uint8_t y, uint8_t width)\n    {\n        if (x < width)\n        {\n            //shift the row with the \"top-left\" position to the \"x\" position\n            auto shiftx = (width - 1) - x;\n            auto topleftx = (getRotation().width - 1);\n\n            auto finalShift = shiftx - topleftx;\n            auto row = getRow(y);\n            auto finalResult = row << finalShift;\n\n            return finalResult;\n        }\n        return 0xffffffff;\n    }\n\n    uint8_t getRow(uint8_t y)\n    {\n        if (y < 4)\n        {\n            return (getRotation().rows >> (12 - (4 * y))) & 0xf;\n        }\n        return 0xf;\n    }\n\n    bool getPixel(uint8_t x, uint8_t y)\n    {\n        if(x > getRotation().width - 1 || y > getRotation().height - 1 )\n        {\n            return false;\n        }\n        \n        if (x < 4 && y < 4)\n        {\n            return (getRow((4 - getRotation().height) + y) >> (3 - ((4 - getRotation().width) + x))) & 0x1;\n        }\n        return false;\n    }\n\n    PieceRotation getRotation()\n    {\n        return this->pieceData->rotations[rotation];\n    }\n};\n\n#endif /* __PIECES_H__ */\n"
  },
  {
    "path": "usermods/TetrisAI_v2/rating.h",
    "content": "/******************************************************************************\n  * @file           : rating.h\n  * @brief          : contains the tetris rating of a grid\n  ******************************************************************************\n  * @attention\n  *\n  * Copyright (c) muebau 2022\n  * All rights reserved.</center></h2>\n  *\n  ******************************************************************************\n*/\n\n#ifndef __RATING_H__\n#define __RATING_H__\n\n#include <stdint.h>\n#include <float.h>\n#include <stdbool.h>\n#include <math.h>\n#include <vector>\n#include \"rating.h\"\n\nusing namespace std;\n\nclass Rating\n{\nprivate:\npublic:\n    uint8_t minHeight;\n    uint8_t maxHeight;\n    uint16_t holes;\n    uint8_t fullLines;\n    uint16_t bumpiness;\n    uint16_t aggregatedHeight;\n    float score;\n    uint8_t width;\n    std::vector<uint8_t> lineHights;\n\n    Rating(uint8_t width):\n        width(width),\n        lineHights(width)\n    {\n        reset();\n    }\n\n    void reset()\n    {\n        this->minHeight = 0;\n        this->maxHeight = 0;\n\n        for (uint8_t line = 0; line < this->width; line++)\n        {\n            this->lineHights[line] = 0;\n        }\n\n        this->holes = 0;\n        this->fullLines = 0;\n        this->bumpiness = 0;\n        this->aggregatedHeight = 0;\n        this->score = -FLT_MAX;\n    }\n};\n\n#endif /* __RATING_H__ */\n"
  },
  {
    "path": "usermods/TetrisAI_v2/readme.md",
    "content": "# Tetris AI effect usermod\n\nThis usermod adds a self-playing Tetris game as an 'effect'. The mod requires version 0.14 or higher as it relies on matrix support. The effect was tested on an ESP32 4MB with a WS2812B 16x16 matrix.\n\nPHOTOSENSITIVE EPILEPSY WARNING: By default the effect features a flashing animation on line clear. This can be disabled\nfrom the usermod settings page in WLED.\n\n## Installation \n\nTo activate the usermod, add the following line to your platformio_override.ini\n`custom_usermods = tetrisai_v2`\nThe effect will then 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)).\n\nIf needed simply add to `platformio_override.ini`:\n\n```ini\nboard_build.partitions = tools/WLED_ESP32_4MB_256KB_FS.csv\n```\n\n## Usage\n\nIt is best to set the background color to black 🖤, the border color to light grey 🤍, the game over color (foreground) to dark grey 🩶, and color palette to 'Rainbow' 🌈.\n\n### Sliders and boxes\n\n#### Sliders\n\n* speed: speed the game plays\n* look ahead: how many pieces is the AI allowed to know the next pieces (0 - 2)\n* intelligence: how good the AI will play\n* Rotate color: make the colors shift (rotate) every few moves\n* Mistakes free: how many good moves between mistakes (if enabled)\n\n#### Checkboxes\n\n* show next: if true, a space of 5 pixels from the right will be used to show the next pieces. Otherwise the whole segment is used for the grid.\n* show border: if true an additional column of 1 pixel is used to draw a border between the grid and the next pieces\n* mistakes: if true, the worst decision will be made every few moves instead of the best (see above).\n\n## Best results\n\n If the speed is set to be a little bit faster than a good human could play with maximal intelligence and very few mistakes it makes people furious/happy at a party 😉.\n\n## Limits\nThe game grid is limited to a maximum width of 32 and a maximum height of 255 due to the internal structure of the code. The canvas of the effect will be centred in the segment if the segment exceeds the maximum width or height."
  },
  {
    "path": "usermods/TetrisAI_v2/tetrisai.h",
    "content": "/******************************************************************************\n  * @file           : ai.h\n  * @brief          : contains the heuristic\n  ******************************************************************************\n  * @attention\n  *\n  * Copyright (c) muebau 2023\n  * All rights reserved.</center></h2>\n  *\n  ******************************************************************************\n*/\n\n#ifndef __AI_H__\n#define __AI_H__\n\n#include \"gridbw.h\"\n#include \"rating.h\"\n\nusing namespace std;\n\nclass TetrisAI\n{\nprivate:\npublic:\n    float aHeight;\n    float fullLines;\n    float holes;\n    float bumpiness;\n    bool findWorstMove = false;\n\n    uint8_t countOnes(uint32_t vector)\n    {\n        uint8_t count = 0;\n        while (vector)\n        {\n            vector &= (vector - 1);\n            count++;\n        }\n        return count;\n    }\n\n    void updateRating(GridBW grid, Rating* rating)\n    {\n        rating->minHeight = 0;\n        rating->maxHeight = 0;\n        rating->holes = 0;\n        rating->fullLines = 0;\n        rating->bumpiness = 0;\n        rating->aggregatedHeight = 0;\n        fill(rating->lineHights.begin(), rating->lineHights.end(), 0);\n\n        uint32_t columnvector = 0x0;\n        uint32_t lastcolumnvector = 0x0;\n        for (uint8_t row = 0; row < grid.height; row++)\n        {\n            columnvector |= grid.pixels[row];\n\n            //first (highest) column makes it\n            if (rating->maxHeight == 0 && columnvector)\n            {\n                rating->maxHeight = grid.height - row;\n            }\n\n            //if column vector is full we found the minimal height (or it stays zero)\n            if (rating->minHeight == 0 && (columnvector == (uint32_t)((1 << grid.width) - 1)))\n            {\n                rating->minHeight = grid.height - row;\n            }\n\n            //line full if all ones in mask :-)\n            if (grid.isLineReadyForRemoval(row))\n            {\n                rating->fullLines++;\n            }\n\n            //holes are basically a XOR with the \"full\" columns\n            rating->holes += countOnes(columnvector ^ grid.pixels[row]);\n\n            //calculate the difference (XOR) between the current column vector and the last one\n            uint32_t columnDelta = columnvector ^ lastcolumnvector;\n\n            //process every new column\n            uint8_t index = 0;\n            while (columnDelta)\n            {\n                //if this is a new column\n                if (columnDelta & 0x1)\n                {\n                    //update hight of this column\n                    rating->lineHights[(grid.width - 1) - index] = grid.height - row;\n\n                    // update aggregatedHeight\n                    rating->aggregatedHeight += grid.height - row;\n                }\n                index++;\n                columnDelta >>= 1;\n            }\n            lastcolumnvector = columnvector;\n        }\n\n        //compare every two columns to get the difference and add them up\n        for (uint8_t column = 1; column < grid.width; column++)\n        {\n            rating->bumpiness += abs(rating->lineHights[column - 1] - rating->lineHights[column]);\n        }\n\n        rating->score = (aHeight * (rating->aggregatedHeight)) + (fullLines * (rating->fullLines)) + (holes * (rating->holes)) + (bumpiness * (rating->bumpiness));\n    }\n\n    TetrisAI(): TetrisAI(-0.510066f, 0.760666f, -0.35663f, -0.184483f)\n    {}\n\n    TetrisAI(float aHeight, float fullLines, float holes, float bumpiness):\n        aHeight(aHeight),\n        fullLines(fullLines),\n        holes(holes),\n        bumpiness(bumpiness)\n    {}\n\n    void findBestMove(GridBW grid, Piece *piece)\n    {\n        vector<Piece> pieces = {*piece};\n        findBestMove(grid, &pieces);\n        *piece = pieces[0];\n    }\n\n    void findBestMove(GridBW grid, std::vector<Piece> *pieces)\n    {\n        findBestMove(grid, pieces->begin(), pieces->end());\n    }\n\n    void findBestMove(GridBW grid, std::vector<Piece>::iterator start, std::vector<Piece>::iterator end)\n    {\n        Rating bestRating(grid.width);\n        findBestMove(grid, start, end, &bestRating);\n    }\n\n    void findBestMove(GridBW grid, std::vector<Piece>::iterator start, std::vector<Piece>::iterator end, Rating* bestRating)\n    {\n        grid.cleanupFullLines();\n        Rating curRating(grid.width);\n        Rating deeperRating(grid.width);\n        Piece piece = *start;\n\n        // for every rotation of the piece\n        for (piece.rotation = 0; piece.rotation < piece.pieceData->rotCount; piece.rotation++)\n        {\n            // put piece to top left corner\n            piece.x = 0;\n            piece.y = 0;\n\n            //test for every column\n            for (piece.x = 0; piece.x <= grid.width - piece.getRotation().width; piece.x++)\n            {\n                //todo optimise by the use of the previous grids height\n                piece.landingY = 0;\n                //will set landingY to final position\n                grid.findLandingPosition(&piece);\n\n                // draw piece\n                grid.placePiece(&piece, piece.x, piece.landingY);\n\n                if(start == end - 1)\n                {\n                    //at the deepest level\n                    updateRating(grid, &curRating);\n                }\n                else\n                {\n                    //go deeper to take another piece into account\n                    findBestMove(grid, start + 1, end, &deeperRating);\n                    curRating = deeperRating;\n                }\n\n                // eraese piece\n                grid.erasePiece(&piece, piece.x, piece.landingY);\n\n                if(findWorstMove)\n                {\n                    //init rating for worst\n                    if(bestRating->score == -FLT_MAX)\n                    {\n                        bestRating->score = FLT_MAX;\n                    }\n\n                    // update if we found a worse one\n                    if (bestRating->score > curRating.score)\n                    {\n                        *bestRating = curRating;\n                        (*start) = piece;\n                    }\n                }\n                else\n                {\n                    // update if we found a better one\n                    if (bestRating->score < curRating.score)\n                    {\n                        *bestRating = curRating;\n                        (*start) = piece;\n                    }\n                }\n            }\n        }\n    }\n};\n\n#endif /* __AI_H__ */"
  },
  {
    "path": "usermods/TetrisAI_v2/tetrisaigame.h",
    "content": "/******************************************************************************\n  * @file           : tetrisaigame.h\n  * @brief          : main tetris functions\n  ******************************************************************************\n  * @attention\n  *\n  * Copyright (c) muebau 2022\n  * All rights reserved.</center></h2>\n  *\n  ******************************************************************************\n*/\n\n#ifndef __TETRISAIGAME_H__\n#define __TETRISAIGAME_H__\n\n#include <stdint.h>\n#include <stdbool.h>\n#include <vector>\n#include \"pieces.h\"\n#include \"gridcolor.h\"\n#include \"tetrisbag.h\"\n#include \"tetrisai.h\"\n\nusing namespace std;\n\nclass TetrisAIGame\n{\nprivate:\n    bool animateFallOfPiece(Piece* piece, bool skip)\n    {\n        if (skip || piece->y >= piece->landingY)\n        {\n            piece->y = piece->landingY;\n            grid.gridBW.placePiece(piece, piece->x, piece->landingY);\n            grid.placePiece(piece, piece->x, piece->y);\n            return false;\n        }\n        else\n        {\n            // eraese last drawing\n            grid.erasePiece(piece, piece->x, piece->y);\n\n            //move piece down\n            piece->y++;\n\n            // draw piece\n            grid.placePiece(piece, piece->x, piece->y);\n\n            return true;\n        }\n    }\n\npublic:\n    uint8_t width;\n    uint8_t height;\n    uint8_t nLookAhead;\n    uint8_t nPieces;\n    TetrisBag bag;\n    GridColor grid;\n    TetrisAI ai;\n    Piece curPiece;\n    PieceData* piecesData;\n    enum States { INIT, TEST_GAME_OVER, GET_NEXT_PIECE, FIND_BEST_MOVE, ANIMATE_MOVE, ANIMATE_GAME_OVER } state = INIT;\n\n    TetrisAIGame(uint8_t width, uint8_t height, uint8_t nLookAhead, PieceData* piecesData, uint8_t nPieces):\n        width(width),\n        height(height),\n        nLookAhead(nLookAhead),\n        nPieces(nPieces),\n        bag(nPieces, 1, nLookAhead),\n        grid(width, height + 4),\n        ai(),\n        piecesData(piecesData)\n    {\n    }\n\n    void nextPiece()\n    {\n        grid.cleanupFullLines();\n        bag.queuePiece();\n    }\n\n    void findBestMove()\n    {\n        ai.findBestMove(grid.gridBW, &bag.piecesQueue);\n    }\n\n    bool animateFall(bool skip)\n    {\n        return animateFallOfPiece(&(bag.piecesQueue[0]), skip);\n    }\n\n    bool isGameOver()\n    {\n        //if there is something in the 4 lines of the hidden area the game is over\n        return grid.gridBW.pixels[0] || grid.gridBW.pixels[1] || grid.gridBW.pixels[2] || grid.gridBW.pixels[3];\n    }\n\n    void poll()\n    {\n        switch (state)\n        {\n            case INIT:\n                reset();\n                state = TEST_GAME_OVER;\n                break;\n            case TEST_GAME_OVER:\n                if (isGameOver())\n                {\n                    state = ANIMATE_GAME_OVER;\n                }\n                else\n                {\n                    state = GET_NEXT_PIECE;\n                }\n                break;\n            case GET_NEXT_PIECE:\n                nextPiece();\n                state = FIND_BEST_MOVE;\n                break;\n            case FIND_BEST_MOVE:\n                findBestMove();\n                state = ANIMATE_MOVE;\n                break;\n            case ANIMATE_MOVE:\n                if (!animateFall(false))\n                {\n                    state = TEST_GAME_OVER;\n                }\n                break;\n            case ANIMATE_GAME_OVER:\n                static auto curPixel = grid.pixels.size();\n                grid.pixels[curPixel] = 254;\n\n                if (curPixel == 0)\n                {\n                    state = INIT;\n                    curPixel = grid.pixels.size();\n                }\n                curPixel--;\n                break;\n        }\n    }\n\n    void reset()\n    {\n        grid.width = width;\n        grid.height = height + 4;\n        grid.reset();\n        bag.reset();\n    }\n};\n\n#endif /* __TETRISAIGAME_H__ */\n"
  },
  {
    "path": "usermods/TetrisAI_v2/tetrisbag.h",
    "content": "/******************************************************************************\n  * @file           : tetrisbag.h\n  * @brief          : the tetris implementation of a random piece generator\n  ******************************************************************************\n  * @attention\n  *\n  * Copyright (c) muebau 2022\n  * All rights reserved.</center></h2>\n  *\n  ******************************************************************************\n*/\n\n#ifndef __TETRISBAG_H__\n#define __TETRISBAG_H__\n\n#include <stdint.h>\n#include <vector>\n\n#include \"tetrisbag.h\"\n\nclass TetrisBag\n{\nprivate:\npublic:\n    uint8_t nPieces;\n    uint8_t nBagLength;\n    uint8_t queueLength;\n    uint8_t bagIdx;\n    std::vector<uint8_t> bag;\n    std::vector<Piece> piecesQueue;\n\n    TetrisBag(uint8_t nPieces, uint8_t nBagLength, uint8_t queueLength):\n        nPieces(nPieces),\n        nBagLength(nBagLength),\n        queueLength(queueLength),\n        bag(nPieces * nBagLength),\n        piecesQueue(queueLength)\n    {\n        init();\n    }\n\n    void init()\n    {\n        //will shuffle the bag at first use\n        bagIdx = nPieces - 1;\n\n        for (uint8_t bagIndex = 0; bagIndex < nPieces * nBagLength; bagIndex++)\n        {\n            bag[bagIndex] = bagIndex % nPieces;\n        }\n\n        //will init the queue\n        for (uint8_t index = 0; index < piecesQueue.size(); index++)\n        {\n            queuePiece();\n        }\n    }\n\n    void shuffleBag()\n    {\n        uint8_t temp;\n        uint8_t swapIdx;\n        for (int index = nPieces - 1; index > 0; index--)\n        {\n            //get candidate to swap\n            swapIdx = rand() % index;\n\n            //swap it!\n            temp = bag[swapIdx];\n            bag[swapIdx] = bag[index];\n            bag[index] = temp;\n        }\n    }\n\n    Piece getNextPiece()\n    {\n        bagIdx++;\n        if (bagIdx >= nPieces)\n        {\n            shuffleBag();\n            bagIdx = 0;\n        }\n        return Piece(bag[bagIdx]);\n    }\n\n    void queuePiece()\n    {\n        //move vector to left\n        for (uint8_t i = 1; i < piecesQueue.size(); i++) {\n            piecesQueue[i - 1] = piecesQueue[i];\n        }\n        piecesQueue[piecesQueue.size() - 1] = getNextPiece();\n    }\n\n    void reset()\n    {\n        bag.clear();\n        bag.resize(nPieces * nBagLength);\n        piecesQueue.clear();\n        piecesQueue.resize(queueLength);\n        init();\n    }\n};\n\n#endif /* __TETRISBAG_H__ */\n"
  },
  {
    "path": "usermods/VL53L0X_gestures/VL53L0X_gestures.cpp",
    "content": "/*\n * That usermod implements support of simple hand gestures with VL53L0X sensor: on/off and brightness correction.\n * It can be useful for kitchen strips to avoid any touches.\n * - on/off - just swipe a hand below your sensor (\"shortPressAction\" is called and can be customized through WLED macros)\n * - brightness correction - keep your hand below sensor for 1 second to switch to \"brightness\" mode.\n        Configure brightness by changing distance to the sensor (see parameters below for customization).\n *\n * Enabling this usermod:\n * 1. Attach VL53L0X sensor to i2c pins according to default pins for your board.\n * 2. Add `-D USERMOD_VL53L0X_GESTURES` to your build flags at platformio.ini (plaformio_override.ini) for needed environment.\n * In my case, for example: `build_flags = ${env.build_flags} -D USERMOD_VL53L0X_GESTURES`\n * 3. Add \"pololu/VL53L0X\" dependency below to `lib_deps` like this:\n * lib_deps = ${env.lib_deps}\n *     pololu/VL53L0X @ ^1.3.0\n */\n#include \"wled.h\"\n\n#include <Wire.h>\n#include <VL53L0X.h>\n\n#ifndef VL53L0X_MAX_RANGE_MM\n#define VL53L0X_MAX_RANGE_MM 230 // max height in millimeters to react for motions\n#endif\n\n#ifndef VL53L0X_MIN_RANGE_OFFSET\n#define VL53L0X_MIN_RANGE_OFFSET 60 // minimal range in millimeters that sensor can detect. Used in long motions to correct brightness calculation.\n#endif\n\n#ifndef VL53L0X_DELAY_MS\n#define VL53L0X_DELAY_MS 100 // how often to get data from sensor\n#endif\n\n#ifndef VL53L0X_LONG_MOTION_DELAY_MS\n#define VL53L0X_LONG_MOTION_DELAY_MS 1000 // switch onto \"long motion\" action after this delay\n#endif\n\nclass UsermodVL53L0XGestures : public Usermod {\n  private:\n    //Private class members. You can declare variables and functions only accessible to your usermod here\n    unsigned long lastTime = 0;\n    VL53L0X sensor;\n    bool enabled = true;\n\n    bool wasMotionBefore = false;\n    bool isLongMotion = false;\n    unsigned long motionStartTime = 0;\n\n  public:\n\n    void setup() {\n      if (i2c_scl<0 || i2c_sda<0) { enabled = false; return; }\n\n      sensor.setTimeout(150);\n      if (!sensor.init())\n      {\n        DEBUG_PRINTLN(F(\"Failed to detect and initialize VL53L0X sensor!\"));\n      } else {\n        sensor.setMeasurementTimingBudget(20000); // set high speed mode\n      }\n    }\n\n\n    void loop() {\n      if (!enabled || strip.isUpdating()) return;\n      if (millis() - lastTime > VL53L0X_DELAY_MS)\n      {\n        lastTime = millis();\n\n        int range = sensor.readRangeSingleMillimeters();\n        DEBUG_PRINTF(\"range: %d, brightness: %d\\r\\n\", range, bri);\n\n        if (range < VL53L0X_MAX_RANGE_MM)\n        {\n          if (!wasMotionBefore)\n          {\n            motionStartTime = millis();\n            DEBUG_PRINTF(\"motionStartTime: %d\\r\\n\", motionStartTime);\n          }\n          wasMotionBefore = true;\n\n          if (millis() - motionStartTime > VL53L0X_LONG_MOTION_DELAY_MS) //long motion\n          {\n            DEBUG_PRINTF(\"long motion: %d\\r\\n\", motionStartTime);\n            if (!isLongMotion)\n            {\n              isLongMotion = true;\n            }\n\n            // set brightness according to range\n            bri = (VL53L0X_MAX_RANGE_MM - max(range, VL53L0X_MIN_RANGE_OFFSET)) * 255 / (VL53L0X_MAX_RANGE_MM - VL53L0X_MIN_RANGE_OFFSET);\n            DEBUG_PRINTF(\"new brightness: %d\", bri);\n            stateUpdated(1);\n          }\n        } else if (wasMotionBefore) { //released\n          if (!isLongMotion)\n          { //short press\n            DEBUG_PRINTLN(F(\"shortPressAction...\"));\n            shortPressAction();\n          }\n          wasMotionBefore = false;\n          isLongMotion = false;\n        }\n      }\n    }\n\n    /*\n     * addToConfig() can be used to add custom persistent settings to the cfg.json file in the \"um\" (usermod) object.\n     * It will be called by WLED when settings are actually saved (for example, LED settings are saved)\n     * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings!\n     */\n//    void addToConfig(JsonObject& root)\n//    {\n//      JsonObject top = root.createNestedObject(\"VL53L0x\");\n//      JsonArray pins = top.createNestedArray(\"pin\");\n//      pins.add(i2c_scl);\n//      pins.add(i2c_sda);\n//    }\n\n    /*\n     * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).\n     * This could be used in the future for the system to determine whether your usermod is installed.\n     */\n    uint16_t getId()\n    {\n      return USERMOD_ID_VL53L0X;\n    }\n};\n\nstatic UsermodVL53L0XGestures vl53l0x_gestures;\nREGISTER_USERMOD(vl53l0x_gestures);"
  },
  {
    "path": "usermods/VL53L0X_gestures/library.json",
    "content": "{\n  \"name\": \"VL53L0X_gestures\",\n  \"build\": { \"libArchive\": false},\n  \"dependencies\": {\n    \"pololu/VL53L0X\" : \"^1.3.0\"\n  }\n}\n"
  },
  {
    "path": "usermods/VL53L0X_gestures/readme.md",
    "content": "# Description\n\nImplements support of simple hand gestures via a VL53L0X sensor: on/off and brightness adjustment.\nUseful for controlling strips when you want to avoid touching anything.\n - on/off - swipe your hand below the sensor (\"shortPressAction\" is called. Can be customized via WLED macros)\n - brightness adjustment - hold your hand below the sensor for 1 second to switch to \"brightness\" mode.\n                           adjust the brightness by changing the distance between your hand and the sensor (see parameters below for customization).\n   \n## Installation\n\n1. Attach VL53L0X sensor to i2c pins according to default pins for your board.\n2. Add `-D USERMOD_VL53L0X_GESTURES` to your build flags at platformio.ini (plaformio_override.ini) for needed environment.\n\n"
  },
  {
    "path": "usermods/Wemos_D1_mini+Wemos32_mini_shield/readme.md",
    "content": "# Wemos D1 mini and Wemos32 mini shield\n-   Installation of file: Copy and replace file in wled00 directory\n-   For BME280 sensor use usermod_bme280.cpp. Copy to wled00 and rename to usermod.cpp\n-   Added third choice of controller Heltec WiFi-Kit-8. Totally DIY but with OLED display.\n## Project repository\n-   [Original repository](https://github.com/srg74/WLED-wemos-shield) - WLED Wemos shield repository\n-   [Wemos shield project Wiki](https://github.com/srg74/WLED-wemos-shield/wiki)\n-   [Precompiled WLED firmware](https://github.com/srg74/WLED-wemos-shield/tree/master/resources/Firmware)\n## Features\n-   SSD1306 128x32 or 128x64 I2C OLED display\n-   On screen IP address, SSID and controller status (e.g. ON or OFF, recent effect)\n-   Auto display shutoff for extending display lifetime\n-   Dallas temperature sensor\n-   Reporting temperature to MQTT broker\n-   Relay for saving energy\n\n## Hardware\n![Shield](https://github.com/srg74/WLED-wemos-shield/blob/master/resources/Images/Assembly_8.jpg)\n\n## Functionality checked with\n\n-   Wemos D1 mini original v3.1 and clones\n-   Wemos32 mini\n-   PlatformIO\n-   SSD1306 128x32 I2C OLED display\n-   DS18B20 (temperature sensor)\n-   BME280 (temperature, humidity and pressure sensor)\n-   Push button (N.O. momentary switch)\n\n### Platformio requirements\n\nFor Dallas sensor uncomment `U8g2@~2.27.3`,`DallasTemperature@~3.8.0`,`OneWire@~2.3.5 under` `[common]` section in `platformio.ini`:\n```ini\n# platformio.ini\n...\n[platformio]\n...\n; default_envs = esp07\ndefault_envs = d1_mini\n...\n[common]\n...\nlib_deps_external =\n  ...\n  #For use SSD1306 OLED display uncomment following\n  U8g2@~2.27.3\n  #For Dallas sensor uncomment following 2 lines\n  DallasTemperature@~3.8.0\n  OneWire@~2.3.5\n...\n```\n\nFor BME280 sensor uncomment `U8g2@~2.27.3`,`BME280@~3.0.0 under` `[common]` section in `platformio.ini`:\n```ini\n# platformio.ini\n...\n[platformio]\n...\n; default_envs = esp07\ndefault_envs = d1_mini\n...\n[common]\n...\nlib_deps_external =\n  ...\n  #For use SSD1306 OLED display uncomment following\n  U8g2@~2.27.3\n  #For BME280 sensor uncomment following\n  BME280@~3.0.0\n...\n```\n"
  },
  {
    "path": "usermods/Wemos_D1_mini+Wemos32_mini_shield/usermod.cpp",
    "content": "#include \"wled.h\"\n#include <Arduino.h>\n#include <U8x8lib.h> // from https://github.com/olikraus/u8g2/\n#include <OneWire.h> // Dallas temperature sensor\n\n//Dallas sensor quick reading. Credit to - Author: Peter Scargill, August 17th, 2013\nint16_t Dallas(int x, byte start)\n{\n    OneWire DallasSensor(x);\n    byte i;\n    byte data[2];\n    int16_t result;\n    do\n    {\n      DallasSensor.reset();\n      DallasSensor.write(0xCC);\n      DallasSensor.write(0xBE);\n      for ( i = 0; i < 2; i++) data[i] = DallasSensor.read();\n      result=(data[1]<<8)|data[0];\n      result>>=4; if (data[1]&128) result|=61440;\n      if (data[0]&8) ++result;\n      DallasSensor.reset();\n      DallasSensor.write(0xCC);\n      DallasSensor.write(0x44,1);\n      if (start) delay(1000);\n    } while (start--);\n      return result;\n}\n#ifdef ARDUINO_ARCH_ESP32\nuint8_t SCL_PIN = 22; \nuint8_t SDA_PIN = 21;\nuint8_t DALLAS_PIN =23;\n#else\nuint8_t SCL_PIN = 5;\nuint8_t SDA_PIN = 4;\nuint8_t DALLAS_PIN =13;\n// uint8_t RST_PIN = 16; // Un-comment for Heltec WiFi-Kit-8\n#endif\n\n//The SCL and SDA pins are defined here.\n//ESP8266 Wemos D1 mini board use SCL=5 SDA=4 while ESP32 Wemos32 mini board use SCL=22 SDA=21\n#define U8X8_PIN_SCL SCL_PIN\n#define U8X8_PIN_SDA SDA_PIN\n//#define U8X8_PIN_RESET RST_PIN // Un-comment for Heltec WiFi-Kit-8\n\n// Dallas sensor reading timer\nlong temptimer = millis();\nlong lastMeasure = 0;\n#define Celsius // Show temperature measurement in Celsius otherwise is in Fahrenheit \n\n// If display does not work or looks corrupted check the\n// constructor reference:\n// https://github.com/olikraus/u8g2/wiki/u8x8setupcpp\n// or check the gallery:\n// https://github.com/olikraus/u8g2/wiki/gallery\n// --> First choice of cheap I2C OLED 128X32 0.91\"\nU8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA\n// --> Second choice of cheap I2C OLED 128X64 0.96\" or 1.3\"\n//U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA\n// --> Third choice of Heltec WiFi-Kit-8 OLED 128X32 0.91\"\n//U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_RESET, U8X8_PIN_SCL, U8X8_PIN_SDA); // Constructor for Heltec WiFi-Kit-8\n// gets called once at boot. Do all initialization that doesn't depend on network here\nvoid userSetup() {\n//Serial.begin(115200);\n\n  Dallas (DALLAS_PIN,1);\n  u8x8.begin();\n  u8x8.setPowerSave(0);\n  u8x8.setFlipMode(1);\n  u8x8.setContrast(10); //Contrast setup will help to preserve OLED lifetime. In case OLED need to be brighter increase number up to 255\n  u8x8.setFont(u8x8_font_chroma48medium8_r);\n  u8x8.drawString(0, 0, \"Loading...\");\n}\n\n// gets called every time WiFi is (re-)connected. Initialize own network\n// interfaces here\nvoid userConnected() {}\n\n// needRedraw marks if redraw is required to prevent often redrawing.\nbool needRedraw = true;\n\n// Next variables hold the previous known values to determine if redraw is\n// required.\nString knownSsid = \"\";\nIPAddress knownIp;\nuint8_t knownBrightness = 0;\nuint8_t knownMode = 0;\nuint8_t knownPalette = 0;\n\nlong lastUpdate = 0;\nlong lastRedraw = 0;\nbool displayTurnedOff = false;\n// How often we are redrawing screen\n#define USER_LOOP_REFRESH_RATE_MS 5000\n\nvoid userLoop() {\n\n//----> Dallas temperature sensor MQTT publishing\n  temptimer = millis();  \n// Timer to publish new temperature every 60 seconds\n  if (temptimer - lastMeasure > 60000) \n  {\n    lastMeasure = temptimer;    \n#ifndef WLED_DISABLE_MQTT\n//Check if MQTT Connected, otherwise it will crash the 8266\n    if (mqtt != nullptr)\n    {\n//      Serial.println(Dallas(DALLAS_PIN,0));\n//Gets preferred temperature scale based on selection in definitions section\n        #ifdef Celsius\n        int16_t board_temperature = Dallas(DALLAS_PIN,0);\n        #else\n        int16_t board_temperature = (Dallas(DALLAS_PIN,0)* 1.8 + 32);\n        #endif\n//Create character string populated with user defined device topic from the UI, and the read temperature. Then publish to MQTT server.\n        String t = String(mqttDeviceTopic);\n        t += \"/temperature\";\n        mqtt->publish(t.c_str(), 0, true, String(board_temperature).c_str());\n    }\n  #endif\n  }\n\n  // Check if we time interval for redrawing passes.\n  if (millis() - lastUpdate < USER_LOOP_REFRESH_RATE_MS) {\n    return;\n  }\n  lastUpdate = millis();\n  \n  // Turn off display after 3 minutes with no change.\n  if(!displayTurnedOff && millis() - lastRedraw > 3*60*1000) {\n    u8x8.setPowerSave(1);\n    displayTurnedOff = true;\n  }\n\n  // Check if values which are shown on display changed from the last time.\n  if (((apActive) ? String(apSSID) : WiFi.SSID()) != knownSsid) {\n    needRedraw = true;\n  } else if (knownIp != (apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP())) {\n    needRedraw = true;\n  } else if (knownBrightness != bri) {\n    needRedraw = true;\n  } else if (knownMode != strip.getMainSegment().mode) {\n    needRedraw = true;\n  } else if (knownPalette != strip.getMainSegment().palette) {\n    needRedraw = true;\n  }\n\n  if (!needRedraw) {\n    return;\n  }\n  needRedraw = false;\n  \n  if (displayTurnedOff)\n  {\n    u8x8.setPowerSave(0);\n    displayTurnedOff = false;\n  }\n  lastRedraw = millis();\n\n  // Update last known values.\n  #ifdef ARDUINO_ARCH_ESP32\n  knownSsid = WiFi.SSID();\n  #else\n  knownSsid = apActive ? WiFi.softAPSSID() : WiFi.SSID();\n  #endif\n  knownIp = apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP();\n  knownBrightness = bri;\n  knownMode = strip.getMainSegment().mode;\n  knownPalette = strip.getMainSegment().palette;\n  u8x8.clear();\n  u8x8.setFont(u8x8_font_chroma48medium8_r);\n\n  // First row with Wifi name\n  u8x8.setCursor(1, 0);\n  u8x8.print(knownSsid.substring(0, u8x8.getCols() > 1 ? u8x8.getCols() - 2 : 0));\n  // Print `~` char to indicate that SSID is longer than our display\n  if (knownSsid.length() > u8x8.getCols())\n    u8x8.print(\"~\");\n\n  // Second row with IP or Password\n  u8x8.setCursor(1, 1);\n  // Print password in AP mode and if led is OFF.\n  if (apActive && bri == 0)\n    u8x8.print(apPass);\n  else\n    u8x8.print(knownIp);\n\n  // Third row with mode name\n  u8x8.setCursor(2, 2);\n  char lineBuffer[17];\n  extractModeName(knownMode, JSON_mode_names, lineBuffer, 16);\n  u8x8.print(lineBuffer);\n\n  // Fourth row with palette name\n  u8x8.setCursor(2, 3);\n  extractModeName(knownPalette, JSON_palette_names, lineBuffer, 16);\n  u8x8.print(lineBuffer);\n\n  u8x8.setFont(u8x8_font_open_iconic_embedded_1x1);\n  u8x8.drawGlyph(0, 0, 80); // wifi icon\n  u8x8.drawGlyph(0, 1, 68); // home icon\n  u8x8.setFont(u8x8_font_open_iconic_weather_2x2);\n  u8x8.drawGlyph(0, 2, 66 + (bri > 0 ? 3 : 0)); // sun/moon icon\n}\n"
  },
  {
    "path": "usermods/Wemos_D1_mini+Wemos32_mini_shield/usermod_bme280.cpp",
    "content": "#include \"wled.h\"\n#include <Arduino.h>\n#include <U8x8lib.h> // from https://github.com/olikraus/u8g2/\n#include <Wire.h>\n#include <BME280I2C.h> //BME280 sensor\n\nvoid UpdateBME280Data();\n\n#define Celsius // Show temperature measurement in Celsius otherwise is in Fahrenheit \nBME280I2C bme;    // Default : forced mode, standby time = 1000 ms\n                  // Oversampling = pressure ×1, temperature ×1, humidity ×1, filter off,\n\n#ifdef ARDUINO_ARCH_ESP32 //ESP32 boards\nuint8_t SCL_PIN = 22;\nuint8_t SDA_PIN = 21;\n#else //ESP8266 boards\nuint8_t SCL_PIN = 5;\nuint8_t SDA_PIN = 4;\n// uint8_t RST_PIN = 16; // Un-comment for Heltec WiFi-Kit-8\n#endif\n\n//The SCL and SDA pins are defined here.\n//ESP8266 Wemos D1 mini board use SCL=5 SDA=4 while ESP32 Wemos32 mini board use SCL=22 SDA=21\n#define U8X8_PIN_SCL SCL_PIN\n#define U8X8_PIN_SDA SDA_PIN\n//#define U8X8_PIN_RESET RST_PIN // Un-comment for Heltec WiFi-Kit-8\n\n// If display does not work or looks corrupted check the\n// constructor reference:\n// https://github.com/olikraus/u8g2/wiki/u8x8setupcpp\n// or check the gallery:\n// https://github.com/olikraus/u8g2/wiki/gallery\n// --> First choice of cheap I2C OLED 128X32 0.91\"\nU8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA\n// --> Second choice of cheap I2C OLED 128X64 0.96\" or 1.3\"\n//U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(U8X8_PIN_NONE, U8X8_PIN_SCL, U8X8_PIN_SDA); // Pins are Reset, SCL, SDA\n// --> Third choice of Heltec WiFi-Kit-8 OLED 128X32 0.91\"\n//U8X8_SSD1306_128X32_UNIVISION_HW_I2C u8x8(U8X8_PIN_RESET, U8X8_PIN_SCL, U8X8_PIN_SDA); // Constructor for Heltec WiFi-Kit-8\n// gets called once at boot. Do all initialization that doesn't depend on network here\n\n// BME280 sensor timer\nlong tempTimer = millis();\nlong lastMeasure = 0;\n\nfloat SensorPressure(NAN);\nfloat SensorTemperature(NAN);\nfloat SensorHumidity(NAN);\n\nvoid userSetup() {\n  u8x8.begin();\n  u8x8.setPowerSave(0);\n  u8x8.setFlipMode(1);\n  u8x8.setContrast(10); //Contrast setup will help to preserve OLED lifetime. In case OLED need to be brighter increase number up to 255\n  u8x8.setFont(u8x8_font_chroma48medium8_r);\n  u8x8.drawString(0, 0, \"Loading...\");\n  Wire.begin(SDA_PIN,SCL_PIN);\n\nwhile(!bme.begin())\n  {\n    Serial.println(\"Could not find BME280I2C sensor!\");\n    delay(1000);\n  }\nswitch(bme.chipModel())\n  {\n    case BME280::ChipModel_BME280:\n      Serial.println(\"Found BME280 sensor! Success.\");\n      break;\n    case BME280::ChipModel_BMP280:\n      Serial.println(\"Found BMP280 sensor! No Humidity available.\");\n      break;\n    default:\n      Serial.println(\"Found UNKNOWN sensor! Error!\");\n  }\n}\n\n// gets called every time WiFi is (re-)connected. Initialize own network\n// interfaces here\nvoid userConnected() {}\n\n// needRedraw marks if redraw is required to prevent often redrawing.\nbool needRedraw = true;\n\n// Next variables hold the previous known values to determine if redraw is\n// required.\nString knownSsid = \"\";\nIPAddress knownIp;\nuint8_t knownBrightness = 0;\nuint8_t knownMode = 0;\nuint8_t knownPalette = 0;\n\nlong lastUpdate = 0;\nlong lastRedraw = 0;\nbool displayTurnedOff = false;\n// How often we are redrawing screen\n#define USER_LOOP_REFRESH_RATE_MS 5000\n\nvoid userLoop() {\n\n// BME280 sensor MQTT publishing\n  tempTimer = millis();  \n// Timer to publish new sensor data every 60 seconds\n  if (tempTimer - lastMeasure > 60000) \n  {\n    lastMeasure = tempTimer;    \n\n#ifndef WLED_DISABLE_MQTT\n// Check if MQTT Connected, otherwise it will crash the 8266\n    if (mqtt != nullptr)\n    {\n      UpdateBME280Data();\n      float board_temperature = SensorTemperature;\n      float board_pressure = SensorPressure;\n      float board_humidity = SensorHumidity;\n\n// Create string populated with user defined device topic from the UI, and the read temperature, humidity and pressure. Then publish to MQTT server.\n      String t = String(mqttDeviceTopic);\n      t += \"/temperature\";\n      mqtt->publish(t.c_str(), 0, true, String(board_temperature).c_str());\n      String p = String(mqttDeviceTopic);\n      p += \"/pressure\";\n      mqtt->publish(p.c_str(), 0, true, String(board_pressure).c_str());\n      String h = String(mqttDeviceTopic);\n      h += \"/humidity\";\n      mqtt->publish(h.c_str(), 0, true, String(board_humidity).c_str());\n    }\n  #endif\n  }\n\n  // Check if we time interval for redrawing passes.\n  if (millis() - lastUpdate < USER_LOOP_REFRESH_RATE_MS) {\n    return;\n  }\n  lastUpdate = millis();\n  \n  // Turn off display after 3 minutes with no change.\n  if(!displayTurnedOff && millis() - lastRedraw > 3*60*1000) {\n    u8x8.setPowerSave(1);\n    displayTurnedOff = true;\n  }\n\n  // Check if values which are shown on display changed from the last time.\n  if (((apActive) ? String(apSSID) : WiFi.SSID()) != knownSsid) {\n    needRedraw = true;\n  } else if (knownIp != (apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP())) {\n    needRedraw = true;\n  } else if (knownBrightness != bri) {\n    needRedraw = true;\n  } else if (knownMode != strip.getMainSegment().mode) {\n    needRedraw = true;\n  } else if (knownPalette != strip.getMainSegment().palette) {\n    needRedraw = true;\n  }\n\n  if (!needRedraw) {\n    return;\n  }\n  needRedraw = false;\n  \n  if (displayTurnedOff)\n  {\n    u8x8.setPowerSave(0);\n    displayTurnedOff = false;\n  }\n  lastRedraw = millis();\n\n  // Update last known values.\n  #if defined(ESP8266)\n  knownSsid = apActive ? WiFi.softAPSSID() : WiFi.SSID();\n  #else\n  knownSsid = WiFi.SSID();\n  #endif\n  knownIp = apActive ? IPAddress(4, 3, 2, 1) : WiFi.localIP();\n  knownBrightness = bri;\n  knownMode = strip.getMainSegment().mode;\n  knownPalette = strip.getMainSegment().palette;\n  u8x8.clear();\n  u8x8.setFont(u8x8_font_chroma48medium8_r);\n\n  // First row with Wifi name\n  u8x8.setCursor(1, 0);\n  u8x8.print(knownSsid.substring(0, u8x8.getCols() > 1 ? u8x8.getCols() - 2 : 0));\n  // Print `~` char to indicate that SSID is longer, than our display\n  if (knownSsid.length() > u8x8.getCols())\n    u8x8.print(\"~\");\n\n  // Second row with IP or Password\n  u8x8.setCursor(1, 1);\n  // Print password in AP mode and if led is OFF.\n  if (apActive && bri == 0)\n    u8x8.print(apPass);\n  else\n    u8x8.print(knownIp);\n\n  // Third row with mode name\n  u8x8.setCursor(2, 2);\n  char lineBuffer[17];\n  extractModeName(knownMode, JSON_mode_names, lineBuffer, 16);\n  u8x8.print(lineBuffer);\n\n  // Fourth row with palette name\n  u8x8.setCursor(2, 3);\n  extractModeName(knownPalette, JSON_palette_names, lineBuffer, 16);\n  u8x8.print(lineBuffer);\n\n  u8x8.setFont(u8x8_font_open_iconic_embedded_1x1);\n  u8x8.drawGlyph(0, 0, 80); // wifi icon\n  u8x8.drawGlyph(0, 1, 68); // home icon\n  u8x8.setFont(u8x8_font_open_iconic_weather_2x2);\n  u8x8.drawGlyph(0, 2, 66 + (bri > 0 ? 3 : 0)); // sun/moon icon\n}\n\nvoid UpdateBME280Data() {\n  float temp(NAN), hum(NAN), pres(NAN);\n#ifdef Celsius\n  BME280::TempUnit tempUnit(BME280::TempUnit_Celsius);\n  BME280::PresUnit presUnit(BME280::PresUnit_Pa);\n  bme.read(pres, temp, hum, tempUnit, presUnit);\n#else\n  BME280::TempUnit tempUnit(BME280::TempUnit_Fahrenheit);\n  BME280::PresUnit presUnit(BME280::PresUnit_Pa);\n  bme.read(pres, temp, hum, tempUnit, presUnit);\n#endif\n  SensorTemperature=temp;\n  SensorHumidity=hum;\n  SensorPressure=pres;\n}\n"
  },
  {
    "path": "usermods/audioreactive/audio_reactive.cpp",
    "content": "\n#include \"wled.h\"\n\n#ifdef ARDUINO_ARCH_ESP32\n\n#include <driver/i2s.h>\n#include <driver/adc.h>\n\n#endif\n\n#if defined(ARDUINO_ARCH_ESP32) && (defined(WLED_DEBUG) || defined(SR_DEBUG))\n#include <esp_timer.h>\n#endif\n\n/*\n * Usermods allow you to add own functionality to WLED more easily\n * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality\n * \n * This is an audioreactive v2 usermod.\n * ....\n */\n\n#if !defined(FFTTASK_PRIORITY)\n#define FFTTASK_PRIORITY 1 // standard: looptask prio\n//#define FFTTASK_PRIORITY 2 // above looptask, below asyc_tcp\n//#define FFTTASK_PRIORITY 4 // above asyc_tcp\n#endif\n\n// Comment/Uncomment to toggle usb serial debugging\n// #define MIC_LOGGER                   // MIC sampling & sound input debugging (serial plotter)\n// #define FFT_SAMPLING_LOG             // FFT result debugging\n// #define SR_DEBUG                     // generic SR DEBUG messages\n\n#ifdef SR_DEBUG\n  #define DEBUGSR_PRINT(x) DEBUGOUT.print(x)\n  #define DEBUGSR_PRINTLN(x) DEBUGOUT.println(x)\n  #define DEBUGSR_PRINTF(x...) DEBUGOUT.printf(x)\n#else\n  #define DEBUGSR_PRINT(x)\n  #define DEBUGSR_PRINTLN(x)\n  #define DEBUGSR_PRINTF(x...)\n#endif\n\n#if defined(MIC_LOGGER) || defined(FFT_SAMPLING_LOG)\n  #define PLOT_PRINT(x) DEBUGOUT.print(x)\n  #define PLOT_PRINTLN(x) DEBUGOUT.println(x)\n  #define PLOT_PRINTF(x...) DEBUGOUT.printf(x)\n#else\n  #define PLOT_PRINT(x)\n  #define PLOT_PRINTLN(x)\n  #define PLOT_PRINTF(x...)\n#endif\n\n#define MAX_PALETTES 3\n\nstatic volatile bool disableSoundProcessing = false;      // if true, sound processing (FFT, filters, AGC) will be suspended. \"volatile\" as its shared between tasks.\nstatic uint8_t audioSyncEnabled = 0;          // bit field: bit 0 - send, bit 1 - receive (config value)\nstatic bool udpSyncConnected = false;         // UDP connection status -> true if connected to multicast group\n\n#define NUM_GEQ_CHANNELS 16                                           // number of frequency channels. Don't change !!\n\n// audioreactive variables\n#ifdef ARDUINO_ARCH_ESP32\n    #ifndef SR_AGC // Automatic gain control mode\n    #define SR_AGC 0 // default mode = off\n    #endif\nstatic float    micDataReal = 0.0f;             // MicIn data with full 24bit resolution - lowest 8bit after decimal point\nstatic float    multAgc = 1.0f;                 // sample * multAgc = sampleAgc. Our AGC multiplier\nstatic float    sampleAvg = 0.0f;               // Smoothed Average sample - sampleAvg < 1 means \"quiet\" (simple noise gate)\nstatic float    sampleAgc = 0.0f;               // Smoothed AGC sample\nstatic uint8_t  soundAgc = SR_AGC;              // Automatic gain control: 0 - off, 1 - normal, 2 - vivid, 3 - lazy (config value)\n#endif\n//static float    volumeSmth = 0.0f;              // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample\nstatic float FFT_MajorPeak = 1.0f;              // FFT: strongest (peak) frequency\nstatic float FFT_Magnitude = 0.0f;              // FFT: volume (magnitude) of peak frequency\nstatic bool samplePeak = false;      // Boolean flag for peak - used in effects. Responding routine may reset this flag. Auto-reset after strip.getFrameTime()\nstatic bool udpSamplePeak = false;   // Boolean flag for peak. Set at the same time as samplePeak, but reset by transmitAudioData\nstatic unsigned long timeOfPeak = 0; // time of last sample peak detection.\nstatic uint8_t fftResult[NUM_GEQ_CHANNELS]= {0};// Our calculated freq. channel result table to be used by effects\n\n// TODO: probably best not used by receive nodes\n//static float agcSensitivity = 128;            // AGC sensitivity estimation, based on agc gain (multAgc). calculated by getSensitivity(). range 0..255\n\n// user settable parameters for limitSoundDynamics()\n#ifdef UM_AUDIOREACTIVE_DYNAMICS_LIMITER_OFF\nstatic bool limiterOn = false;                 // bool: enable / disable dynamics limiter\n#else\nstatic bool limiterOn = true;\n#endif\nstatic uint16_t attackTime =  80;             // int: attack time in milliseconds. Default 0.08sec\nstatic uint16_t decayTime = 1400;             // int: decay time in milliseconds.  Default 1.40sec\n\n// peak detection\n#ifdef ARDUINO_ARCH_ESP32\nstatic void detectSamplePeak(void);  // peak detection function (needs scaled FFT results in vReal[]) - no used for 8266 receive-only mode\n#endif\nstatic void autoResetPeak(void);     // peak auto-reset function\nstatic uint8_t maxVol = 31;          // (was 10) Reasonable value for constant volume for 'peak detector', as it won't always trigger  (deprecated)\nstatic uint8_t binNum = 8;           // Used to select the bin for FFT based beat detection  (deprecated)\n\n#ifdef ARDUINO_ARCH_ESP32\n\n// use audio source class (ESP32 specific)\n#include \"audio_source.h\"\nconstexpr i2s_port_t I2S_PORT = I2S_NUM_0;       // I2S port to use (do not change !)\nconstexpr int BLOCK_SIZE = 128;                  // I2S buffer size (samples)\n\n// globals\nstatic uint8_t inputLevel = 128;              // UI slider value\n#ifndef SR_SQUELCH\n  uint8_t soundSquelch = 10;                  // squelch value for volume reactive routines (config value)\n#else\n  uint8_t soundSquelch = SR_SQUELCH;          // squelch value for volume reactive routines (config value)\n#endif\n#ifndef SR_GAIN\n  uint8_t sampleGain = 60;                    // sample gain (config value)\n#else\n  uint8_t sampleGain = SR_GAIN;               // sample gain (config value)\n#endif\n// user settable options for FFTResult scaling\nstatic uint8_t FFTScalingMode = 3;            // 0 none; 1 optimized logarithmic; 2 optimized linear; 3 optimized square root\n\n// \n// AGC presets\n//  Note: in C++, \"const\" implies \"static\" - no need to explicitly declare everything as \"static const\"\n// \n#define AGC_NUM_PRESETS 3 // AGC presets:          normal,   vivid,    lazy\nconst double agcSampleDecay[AGC_NUM_PRESETS]  = { 0.9994f, 0.9985f, 0.9997f}; // decay factor for sampleMax, in case the current sample is below sampleMax\nconst float agcZoneLow[AGC_NUM_PRESETS]       = {      32,      28,      36}; // low volume emergency zone\nconst float agcZoneHigh[AGC_NUM_PRESETS]      = {     240,     240,     248}; // high volume emergency zone\nconst float agcZoneStop[AGC_NUM_PRESETS]      = {     336,     448,     304}; // disable AGC integrator if we get above this level\nconst float agcTarget0[AGC_NUM_PRESETS]       = {     112,     144,     164}; // first AGC setPoint -> between 40% and 65%\nconst float agcTarget0Up[AGC_NUM_PRESETS]     = {      88,      64,     116}; // setpoint switching value (a poor man's bang-bang)\nconst float agcTarget1[AGC_NUM_PRESETS]       = {     220,     224,     216}; // second AGC setPoint -> around 85%\nconst double agcFollowFast[AGC_NUM_PRESETS]   = { 1/192.f, 1/128.f, 1/256.f}; // quickly follow setpoint - ~0.15 sec\nconst double agcFollowSlow[AGC_NUM_PRESETS]   = {1/6144.f,1/4096.f,1/8192.f}; // slowly follow setpoint  - ~2-15 secs\nconst double agcControlKp[AGC_NUM_PRESETS]    = {    0.6f,    1.5f,   0.65f}; // AGC - PI control, proportional gain parameter\nconst double agcControlKi[AGC_NUM_PRESETS]    = {    1.7f,   1.85f,    1.2f}; // AGC - PI control, integral gain parameter\nconst float agcSampleSmooth[AGC_NUM_PRESETS]  = {  1/12.f,   1/6.f,  1/16.f}; // smoothing factor for sampleAgc (use rawSampleAgc if you want the non-smoothed value)\n// AGC presets end\n\nstatic AudioSource *audioSource = nullptr;\nstatic bool useBandPassFilter = false;                    // if true, enables a bandpass filter 80Hz-16Khz to remove noise. Applies before FFT.\n\n////////////////////\n// Begin FFT Code //\n////////////////////\n\n// some prototypes, to ensure consistent interfaces\nstatic float fftAddAvg(int from, int to);   // average of several FFT result bins\nvoid FFTcode(void * parameter);      // audio processing task: read samples, run FFT, fill GEQ channels from FFT results\nstatic void runMicFilter(uint16_t numSamples, float *sampleBuffer);          // pre-filtering of raw samples (band-pass)\nstatic void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels); // post-processing and post-amp of GEQ channels\n\nstatic TaskHandle_t FFT_Task = nullptr;\n\n// Table of multiplication factors so that we can even out the frequency response.\nstatic float fftResultPink[NUM_GEQ_CHANNELS] = { 1.70f, 1.71f, 1.73f, 1.78f, 1.68f, 1.56f, 1.55f, 1.63f, 1.79f, 1.62f, 1.80f, 2.06f, 2.47f, 3.35f, 6.83f, 9.55f };\n\n// globals and FFT Output variables shared with animations\n#if defined(WLED_DEBUG) || defined(SR_DEBUG)\nstatic uint64_t fftTime = 0;\nstatic uint64_t sampleTime = 0;\n#endif\n\n// FFT Task variables (filtering and post-processing)\nstatic float   fftCalc[NUM_GEQ_CHANNELS] = {0.0f};                    // Try and normalize fftBin values to a max of 4096, so that 4096/16 = 256.\nstatic float   fftAvg[NUM_GEQ_CHANNELS] = {0.0f};                     // Calculated frequency channel results, with smoothing (used if dynamics limiter is ON)\n#ifdef SR_DEBUG\nstatic float   fftResultMax[NUM_GEQ_CHANNELS] = {0.0f};               // A table used for testing to determine how our post-processing is working.\n#endif\n\n// audio source parameters and constant\nconstexpr SRate_t SAMPLE_RATE = 22050;        // Base sample rate in Hz - 22Khz is a standard rate. Physical sample time -> 23ms\n//constexpr SRate_t SAMPLE_RATE = 16000;        // 16kHz - use if FFTtask takes more than 20ms. Physical sample time -> 32ms\n//constexpr SRate_t SAMPLE_RATE = 20480;        // Base sample rate in Hz - 20Khz is experimental.    Physical sample time -> 25ms\n//constexpr SRate_t SAMPLE_RATE = 10240;        // Base sample rate in Hz - previous default.         Physical sample time -> 50ms\n#define FFT_MIN_CYCLE 21                      // minimum time before FFT task is repeated. Use with 22Khz sampling\n//#define FFT_MIN_CYCLE 30                      // Use with 16Khz sampling\n//#define FFT_MIN_CYCLE 23                      // minimum time before FFT task is repeated. Use with 20Khz sampling\n//#define FFT_MIN_CYCLE 46                      // minimum time before FFT task is repeated. Use with 10Khz sampling\n\n// FFT Constants\nconstexpr uint16_t samplesFFT = 512;            // Samples in an FFT batch - This value MUST ALWAYS be a power of 2\nconstexpr uint16_t samplesFFT_2 = 256;          // meaningfull part of FFT results - only the \"lower half\" contains useful information.\n// the following are observed values, supported by a bit of \"educated guessing\"\n//#define FFT_DOWNSCALE 0.65f                             // 20kHz - downscaling factor for FFT results - \"Flat-Top\" window @20Khz, old freq channels \n#define FFT_DOWNSCALE 0.46f                             // downscaling factor for FFT results - for \"Flat-Top\" window @22Khz, new freq channels\n#define LOG_256  5.54517744f                            // log(256)\n\n// These are the input and output vectors.  Input vectors receive computed results from FFT.\nstatic float* vReal = nullptr;                  // FFT sample inputs / freq output -  these are our raw result bins\nstatic float* vImag = nullptr;                  // imaginary parts\n\n// Create FFT object\n// lib_deps += https://github.com/kosme/arduinoFFT#develop @ 1.9.2\n// these options actually cause slow-downs on all esp32 processors, don't use them.\n// #define FFT_SPEED_OVER_PRECISION     // enables use of reciprocals (1/x etc) - not faster on ESP32\n// #define FFT_SQRT_APPROXIMATION       // enables \"quake3\" style inverse sqrt  - slower on ESP32\n// Below options are forcing ArduinoFFT to use sqrtf() instead of sqrt()\n// #define sqrt_internal sqrtf          // see https://github.com/kosme/arduinoFFT/pull/83 - since v2.0.0 this must be done in build_flags\n\n#include <arduinoFFT.h>             // FFT object is created in FFTcode\n// Helper functions\n\n// compute average of several FFT result bins\nstatic float fftAddAvg(int from, int to) {\n  float result = 0.0f;\n  for (int i = from; i <= to; i++) {\n    result += vReal[i];\n  }\n  return result / float(to - from + 1);\n}\n\n//\n// FFT main task\n//\nvoid FFTcode(void * parameter)\n{\n  DEBUGSR_PRINT(\"FFT started on core: \"); DEBUGSR_PRINTLN(xPortGetCoreID());\n\n  // allocate FFT buffers on first call\n  if (vReal == nullptr) vReal = (float*) calloc(samplesFFT, sizeof(float));\n  if (vImag == nullptr) vImag = (float*) calloc(samplesFFT, sizeof(float));\n  if ((vReal == nullptr) || (vImag == nullptr)) {\n    // something went wrong\n    if (vReal) free(vReal); vReal = nullptr;\n    if (vImag) free(vImag); vImag = nullptr;\n    return;\n  }\n  // Create FFT object with weighing factor storage\n  ArduinoFFT<float> FFT = ArduinoFFT<float>( vReal, vImag, samplesFFT, SAMPLE_RATE, true);\n\n  // see https://www.freertos.org/vtaskdelayuntil.html\n  const TickType_t xFrequency = FFT_MIN_CYCLE * portTICK_PERIOD_MS;  \n\n  TickType_t xLastWakeTime = xTaskGetTickCount();\n  for(;;) {\n    delay(1);           // DO NOT DELETE THIS LINE! It is needed to give the IDLE(0) task enough time and to keep the watchdog happy.\n                        // taskYIELD(), yield(), vTaskDelay() and esp_task_wdt_feed() didn't seem to work.\n\n    // Don't run FFT computing code if we're in Receive mode or in realtime mode\n    if (disableSoundProcessing || (audioSyncEnabled & 0x02)) {\n      vTaskDelayUntil( &xLastWakeTime, xFrequency);        // release CPU, and let I2S fill its buffers\n      continue;\n    }\n\n#if defined(WLED_DEBUG) || defined(SR_DEBUG)\n    uint64_t start = esp_timer_get_time();\n    bool haveDoneFFT = false; // indicates if second measurement (FFT time) is valid\n#endif\n\n    // get a fresh batch of samples from I2S\n    if (audioSource) audioSource->getSamples(vReal, samplesFFT);\n    memset(vImag, 0, samplesFFT * sizeof(float));   // set imaginary parts to 0\n\n#if defined(WLED_DEBUG) || defined(SR_DEBUG)\n    if (start < esp_timer_get_time()) { // filter out overflows\n      uint64_t sampleTimeInMillis = (esp_timer_get_time() - start +5ULL) / 10ULL; // \"+5\" to ensure proper rounding\n      sampleTime = (sampleTimeInMillis*3 + sampleTime*7)/10; // smooth\n    }\n    start = esp_timer_get_time(); // start measuring FFT time\n#endif\n\n    xLastWakeTime = xTaskGetTickCount();       // update \"last unblocked time\" for vTaskDelay\n\n    // band pass filter - can reduce noise floor by a factor of 50\n    // downside: frequencies below 100Hz will be ignored\n    if (useBandPassFilter) runMicFilter(samplesFFT, vReal);\n\n    // find highest sample in the batch\n    float maxSample = 0.0f;                         // max sample from FFT batch\n    for (int i=0; i < samplesFFT; i++) {\n\t    // pick our  our current mic sample - we take the max value from all samples that go into FFT\n\t    if ((vReal[i] <= (INT16_MAX - 1024)) && (vReal[i] >= (INT16_MIN + 1024)))  //skip extreme values - normally these are artefacts\n        if (fabsf((float)vReal[i]) > maxSample) maxSample = fabsf((float)vReal[i]);\n    }\n    // release highest sample to volume reactive effects early - not strictly necessary here - could also be done at the end of the function\n    // early release allows the filters (getSample() and agcAvg()) to work with fresh values - we will have matching gain and noise gate values when we want to process the FFT results.\n    micDataReal = maxSample;\n\n#ifdef SR_DEBUG\n    if (true) {  // this allows measure FFT runtimes, as it disables the \"only when needed\" optimization \n#else\n    if (sampleAvg > 0.25f) { // noise gate open means that FFT results will be used. Don't run FFT if results are not needed.\n#endif\n\n      // run FFT (takes 3-5ms on ESP32, ~12ms on ESP32-S2)\n      FFT.dcRemoval();                                            // remove DC offset\n      FFT.windowing( FFTWindow::Flat_top, FFTDirection::Forward); // Weigh data using \"Flat Top\" function - better amplitude accuracy\n      //FFT.windowing(FFTWindow::Blackman_Harris, FFTDirection::Forward);  // Weigh data using \"Blackman- Harris\" window - sharp peaks due to excellent sideband rejection\n      FFT.compute( FFTDirection::Forward );                       // Compute FFT\n      FFT.complexToMagnitude();                                   // Compute magnitudes\n      vReal[0] = 0;   // The remaining DC offset on the signal produces a strong spike on position 0 that should be eliminated to avoid issues.\n\n      FFT.majorPeak(&FFT_MajorPeak, &FFT_Magnitude);                // let the effects know which freq was most dominant\n      FFT_MajorPeak = constrain(FFT_MajorPeak, 1.0f, 11025.0f);   // restrict value to range expected by effects\n\n#if defined(WLED_DEBUG) || defined(SR_DEBUG)\n      haveDoneFFT = true;\n#endif\n\n    } else { // noise gate closed - only clear results as FFT was skipped. MIC samples are still valid when we do this.\n      memset(vReal, 0, samplesFFT * sizeof(float));\n      FFT_MajorPeak = 1;\n      FFT_Magnitude = 0.001;\n    }\n\n    for (int i = 0; i < samplesFFT; i++) {\n      float t = fabsf(vReal[i]);                      // just to be sure - values in fft bins should be positive any way\n      vReal[i] = t / 16.0f;                           // Reduce magnitude. Want end result to be scaled linear and ~4096 max.\n    } // for()\n\n    // mapping of FFT result bins to frequency channels\n    if (fabsf(sampleAvg) > 0.5f) { // noise gate open\n#if 0\n    /* This FFT post processing is a DIY endeavour. What we really need is someone with sound engineering expertise to do a great job here AND most importantly, that the animations look GREAT as a result.\n    *\n    * Andrew's updated mapping of 256 bins down to the 16 result bins with Sample Freq = 10240, samplesFFT = 512 and some overlap.\n    * Based on testing, the lowest/Start frequency is 60 Hz (with bin 3) and a highest/End frequency of 5120 Hz in bin 255.\n    * Now, Take the 60Hz and multiply by 1.320367784 to get the next frequency and so on until the end. Then determine the bins.\n    * End frequency = Start frequency * multiplier ^ 16\n    * Multiplier = (End frequency/ Start frequency) ^ 1/16\n    * Multiplier = 1.320367784\n    */                                    //  Range\n      fftCalc[ 0] = fftAddAvg(2,4);       // 60 - 100\n      fftCalc[ 1] = fftAddAvg(4,5);       // 80 - 120\n      fftCalc[ 2] = fftAddAvg(5,7);       // 100 - 160\n      fftCalc[ 3] = fftAddAvg(7,9);       // 140 - 200\n      fftCalc[ 4] = fftAddAvg(9,12);      // 180 - 260\n      fftCalc[ 5] = fftAddAvg(12,16);     // 240 - 340\n      fftCalc[ 6] = fftAddAvg(16,21);     // 320 - 440\n      fftCalc[ 7] = fftAddAvg(21,29);     // 420 - 600\n      fftCalc[ 8] = fftAddAvg(29,37);     // 580 - 760\n      fftCalc[ 9] = fftAddAvg(37,48);     // 740 - 980\n      fftCalc[10] = fftAddAvg(48,64);     // 960 - 1300\n      fftCalc[11] = fftAddAvg(64,84);     // 1280 - 1700\n      fftCalc[12] = fftAddAvg(84,111);    // 1680 - 2240\n      fftCalc[13] = fftAddAvg(111,147);   // 2220 - 2960\n      fftCalc[14] = fftAddAvg(147,194);   // 2940 - 3900\n      fftCalc[15] = fftAddAvg(194,250);   // 3880 - 5000 // avoid the last 5 bins, which are usually inaccurate\n#else\n      /* new mapping, optimized for 22050 Hz by softhack007 */\n                                                    // bins frequency  range\n      if (useBandPassFilter) {\n        // skip frequencies below 100hz\n        fftCalc[ 0] = 0.8f * fftAddAvg(3,4);\n        fftCalc[ 1] = 0.9f * fftAddAvg(4,5);\n        fftCalc[ 2] = fftAddAvg(5,6);\n        fftCalc[ 3] = fftAddAvg(6,7);\n        // don't use the last bins from 206 to 255. \n        fftCalc[15] = fftAddAvg(165,205) * 0.75f;   // 40 7106 - 8828 high             -- with some damping\n      } else {\n        fftCalc[ 0] = fftAddAvg(1,2);               // 1    43 - 86   sub-bass\n        fftCalc[ 1] = fftAddAvg(2,3);               // 1    86 - 129  bass\n        fftCalc[ 2] = fftAddAvg(3,5);               // 2   129 - 216  bass\n        fftCalc[ 3] = fftAddAvg(5,7);               // 2   216 - 301  bass + midrange\n        // don't use the last bins from 216 to 255. They are usually contaminated by aliasing (aka noise) \n        fftCalc[15] = fftAddAvg(165,215) * 0.70f;   // 50 7106 - 9259 high             -- with some damping\n      }\n      fftCalc[ 4] = fftAddAvg(7,10);                // 3   301 - 430  midrange\n      fftCalc[ 5] = fftAddAvg(10,13);               // 3   430 - 560  midrange\n      fftCalc[ 6] = fftAddAvg(13,19);               // 5   560 - 818  midrange\n      fftCalc[ 7] = fftAddAvg(19,26);               // 7   818 - 1120 midrange -- 1Khz should always be the center !\n      fftCalc[ 8] = fftAddAvg(26,33);               // 7  1120 - 1421 midrange\n      fftCalc[ 9] = fftAddAvg(33,44);               // 9  1421 - 1895 midrange\n      fftCalc[10] = fftAddAvg(44,56);               // 12 1895 - 2412 midrange + high mid\n      fftCalc[11] = fftAddAvg(56,70);               // 14 2412 - 3015 high mid\n      fftCalc[12] = fftAddAvg(70,86);               // 16 3015 - 3704 high mid\n      fftCalc[13] = fftAddAvg(86,104);              // 18 3704 - 4479 high mid\n      fftCalc[14] = fftAddAvg(104,165) * 0.88f;     // 61 4479 - 7106 high mid + high  -- with slight damping\n#endif\n    } else {  // noise gate closed - just decay old values\n      for (int i=0; i < NUM_GEQ_CHANNELS; i++) {\n        fftCalc[i] *= 0.85f;  // decay to zero\n        if (fftCalc[i] < 4.0f) fftCalc[i] = 0.0f;\n      }\n    }\n\n    // post-processing of frequency channels (pink noise adjustment, AGC, smoothing, scaling)\n    postProcessFFTResults((fabsf(sampleAvg) > 0.25f)? true : false , NUM_GEQ_CHANNELS);\n\n#if defined(WLED_DEBUG) || defined(SR_DEBUG)\n    if (haveDoneFFT && (start < esp_timer_get_time())) { // filter out overflows\n      uint64_t fftTimeInMillis = ((esp_timer_get_time() - start) +5ULL) / 10ULL; // \"+5\" to ensure proper rounding\n      fftTime  = (fftTimeInMillis*3 + fftTime*7)/10; // smooth\n    }\n#endif\n    // run peak detection\n    autoResetPeak();\n    detectSamplePeak();\n    \n    #if !defined(I2S_GRAB_ADC1_COMPLETELY)    \n    if ((audioSource == nullptr) || (audioSource->getType() != AudioSource::Type_I2SAdc))  // the \"delay trick\" does not help for analog ADC\n    #endif\n      vTaskDelayUntil( &xLastWakeTime, xFrequency);        // release CPU, and let I2S fill its buffers\n\n  } // for(;;)ever\n} // FFTcode() task end\n\n\n///////////////////////////\n// Pre / Postprocessing  //\n///////////////////////////\n\nstatic void runMicFilter(uint16_t numSamples, float *sampleBuffer)          // pre-filtering of raw samples (band-pass)\n{\n  // low frequency cutoff parameter - see https://dsp.stackexchange.com/questions/40462/exponential-moving-average-cut-off-frequency\n  //constexpr float alpha = 0.04f;   // 150Hz\n  //constexpr float alpha = 0.03f;   // 110Hz\n  constexpr float alpha = 0.0225f; // 80hz\n  //constexpr float alpha = 0.01693f;// 60hz\n  // high frequency cutoff  parameter\n  //constexpr float beta1 = 0.75f;   // 11Khz\n  //constexpr float beta1 = 0.82f;   // 15Khz\n  //constexpr float beta1 = 0.8285f; // 18Khz\n  constexpr float beta1 = 0.85f;  // 20Khz\n\n  constexpr float beta2 = (1.0f - beta1) / 2.0f;\n  static float last_vals[2] = { 0.0f }; // FIR high freq cutoff filter\n  static float lowfilt = 0.0f;          // IIR low frequency cutoff filter\n\n  for (int i=0; i < numSamples; i++) {\n        // FIR lowpass, to remove high frequency noise\n        float highFilteredSample;\n        if (i < (numSamples-1)) highFilteredSample = beta1*sampleBuffer[i] + beta2*last_vals[0] + beta2*sampleBuffer[i+1];  // smooth out spikes\n        else highFilteredSample = beta1*sampleBuffer[i] + beta2*last_vals[0]  + beta2*last_vals[1];                  // special handling for last sample in array\n        last_vals[1] = last_vals[0];\n        last_vals[0] = sampleBuffer[i];\n        sampleBuffer[i] = highFilteredSample;\n        // IIR highpass, to remove low frequency noise\n        lowfilt += alpha * (sampleBuffer[i] - lowfilt);\n        sampleBuffer[i] = sampleBuffer[i] - lowfilt;\n  }\n}\n\nstatic void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels) // post-processing and post-amp of GEQ channels\n{\n    for (int i=0; i < numberOfChannels; i++) {\n\n      if (noiseGateOpen) { // noise gate open\n        // Adjustment for frequency curves.\n        fftCalc[i] *= fftResultPink[i];\n        if (FFTScalingMode > 0) fftCalc[i] *= FFT_DOWNSCALE;  // adjustment related to FFT windowing function\n        // Manual linear adjustment of gain using sampleGain adjustment for different input types.\n        fftCalc[i] *= soundAgc ? multAgc : ((float)sampleGain/40.0f * (float)inputLevel/128.0f + 1.0f/16.0f); //apply gain, with inputLevel adjustment\n        if(fftCalc[i] < 0) fftCalc[i] = 0;\n      }\n\n      // smooth results - rise fast, fall slower\n      if(fftCalc[i] > fftAvg[i])   // rise fast \n        fftAvg[i] = fftCalc[i] *0.75f + 0.25f*fftAvg[i];  // will need approx 2 cycles (50ms) for converging against fftCalc[i]\n      else {                       // fall slow\n        if (decayTime < 1000) fftAvg[i] = fftCalc[i]*0.22f + 0.78f*fftAvg[i];       // approx  5 cycles (225ms) for falling to zero\n        else if (decayTime < 2000) fftAvg[i] = fftCalc[i]*0.17f + 0.83f*fftAvg[i];  // default - approx  9 cycles (225ms) for falling to zero\n        else if (decayTime < 3000) fftAvg[i] = fftCalc[i]*0.14f + 0.86f*fftAvg[i];  // approx 14 cycles (350ms) for falling to zero\n        else fftAvg[i] = fftCalc[i]*0.1f  + 0.9f*fftAvg[i];                         // approx 20 cycles (500ms) for falling to zero\n      }\n      // constrain internal vars - just to be sure\n      fftCalc[i] = constrain(fftCalc[i], 0.0f, 1023.0f);\n      fftAvg[i] = constrain(fftAvg[i], 0.0f, 1023.0f);\n\n      float currentResult;\n      if(limiterOn == true)\n        currentResult = fftAvg[i];\n      else\n        currentResult = fftCalc[i];\n\n      switch (FFTScalingMode) {\n        case 1:\n            // Logarithmic scaling\n            currentResult *= 0.42f;                      // 42 is the answer ;-)\n            currentResult -= 8.0f;                       // this skips the lowest row, giving some room for peaks\n            if (currentResult > 1.0f) currentResult = logf(currentResult); // log to base \"e\", which is the fastest log() function\n            else currentResult = 0.0f;                   // special handling, because log(1) = 0; log(0) = undefined\n            currentResult *= 0.85f + (float(i)/18.0f);  // extra up-scaling for high frequencies\n            currentResult = mapf(currentResult, 0, LOG_256, 0, 255); // map [log(1) ... log(255)] to [0 ... 255]\n        break;\n        case 2:\n            // Linear scaling\n            currentResult *= 0.30f;                     // needs a bit more damping, get stay below 255\n            currentResult -= 4.0f;                       // giving a bit more room for peaks\n            if (currentResult < 1.0f) currentResult = 0.0f;\n            currentResult *= 0.85f + (float(i)/1.8f);   // extra up-scaling for high frequencies\n        break;\n        case 3:\n            // square root scaling\n            currentResult *= 0.38f;\n            currentResult -= 6.0f;\n            if (currentResult > 1.0f) currentResult = sqrtf(currentResult);\n            else currentResult = 0.0f;                   // special handling, because sqrt(0) = undefined\n            currentResult *= 0.85f + (float(i)/4.5f);   // extra up-scaling for high frequencies\n            currentResult = mapf(currentResult, 0.0, 16.0, 0.0, 255.0); // map [sqrt(1) ... sqrt(256)] to [0 ... 255]\n        break;\n\n        case 0:\n        default:\n            // no scaling - leave freq bins as-is\n            currentResult -= 4; // just a bit more room for peaks\n        break;\n      }\n\n      // Now, let's dump it all into fftResult. Need to do this, otherwise other routines might grab fftResult values prematurely.\n      if (soundAgc > 0) {  // apply extra \"GEQ Gain\" if set by user\n        float post_gain = (float)inputLevel/128.0f;\n        if (post_gain < 1.0f) post_gain = ((post_gain -1.0f) * 0.8f) +1.0f;\n        currentResult *= post_gain;\n      }\n      fftResult[i] = constrain((int)currentResult, 0, 255);\n    }\n}\n////////////////////\n// Peak detection //\n////////////////////\n\n// peak detection is called from FFT task when vReal[] contains valid FFT results\nstatic void detectSamplePeak(void) {\n  bool havePeak = false;\n  // softhack007: this code continuously triggers while amplitude in the selected bin is above a certain threshold. So it does not detect peaks - it detects high activity in a frequency bin.\n  // Poor man's beat detection by seeing if sample > Average + some value.\n  // This goes through ALL of the 255 bins - but ignores stupid settings\n  // Then we got a peak, else we don't. The peak has to time out on its own in order to support UDP sound sync.\n  if ((sampleAvg > 1) && (maxVol > 0) && (binNum > 4) && (vReal[binNum] > maxVol) && ((millis() - timeOfPeak) > 100)) {\n    havePeak = true;\n  }\n\n  if (havePeak) {\n    samplePeak    = true;\n    timeOfPeak    = millis();\n    udpSamplePeak = true;\n  }\n}\n\n#endif\n\nstatic void autoResetPeak(void) {\n  uint16_t peakDelay = max(uint16_t(50), strip.getFrameTime());\n  if (millis() - timeOfPeak > peakDelay) {          // Auto-reset of samplePeak after at least one complete frame has passed.\n    samplePeak = false;\n    if (audioSyncEnabled == 0) udpSamplePeak = false;  // this is normally reset by transmitAudioData\n  }\n}\n\n\n////////////////////\n// usermod class  //\n////////////////////\n\n//class name. Use something descriptive and leave the \": public Usermod\" part :)\nclass AudioReactive : public Usermod {\n\n  private:\n#ifdef ARDUINO_ARCH_ESP32\n\n    #ifndef AUDIOPIN\n    int8_t audioPin = -1;\n    #else\n    int8_t audioPin = AUDIOPIN;\n    #endif\n    #ifndef SR_DMTYPE // I2S mic type\n    uint8_t dmType = 1; // 0=none/disabled/analog; 1=generic I2S\n    #define SR_DMTYPE 1 // default type = I2S\n    #else\n    uint8_t dmType = SR_DMTYPE;\n    #endif\n    #ifndef I2S_SDPIN // aka DOUT\n    int8_t i2ssdPin = 32;\n    #else\n    int8_t i2ssdPin = I2S_SDPIN;\n    #endif\n    #ifndef I2S_WSPIN // aka LRCL\n    int8_t i2swsPin = 15;\n    #else\n    int8_t i2swsPin = I2S_WSPIN;\n    #endif\n    #ifndef I2S_CKPIN // aka BCLK\n    int8_t i2sckPin = 14; /*PDM: set to I2S_PIN_NO_CHANGE*/\n    #else\n    int8_t i2sckPin = I2S_CKPIN;\n    #endif\n    #ifndef MCLK_PIN\n    int8_t mclkPin = I2S_PIN_NO_CHANGE;  /* ESP32: only -1, 0, 1, 3 allowed*/\n    #else\n    int8_t mclkPin = MCLK_PIN;\n    #endif\n#endif\n\n    // new \"V2\" audiosync struct - 44 Bytes\n    struct __attribute__ ((packed)) audioSyncPacket {  // \"packed\" ensures that there are no additional gaps\n      char    header[6];      //  06 Bytes  offset 0\n      uint8_t reserved1[2];   //  02 Bytes, offset 6  - gap required by the compiler - not used yet \n      float   sampleRaw;      //  04 Bytes  offset 8  - either \"sampleRaw\" or \"rawSampleAgc\" depending on soundAgc setting\n      float   sampleSmth;     //  04 Bytes  offset 12 - either \"sampleAvg\" or \"sampleAgc\" depending on soundAgc setting\n      uint8_t samplePeak;     //  01 Bytes  offset 16 - 0 no peak; >=1 peak detected. In future, this will also provide peak Magnitude\n      uint8_t reserved2;      //  01 Bytes  offset 17 - for future extensions - not used yet\n      uint8_t fftResult[16];  //  16 Bytes  offset 18\n      uint16_t reserved3;     //  02 Bytes, offset 34 - gap required by the compiler - not used yet\n      float  FFT_Magnitude;   //  04 Bytes  offset 36\n      float  FFT_MajorPeak;   //  04 Bytes  offset 40\n    };\n\n    // old \"V1\" audiosync struct - 83 Bytes payload, 88 bytes total (with padding added by compiler) - for backwards compatibility\n    struct audioSyncPacket_v1 {\n      char header[6];         //  06 Bytes\n      uint8_t myVals[32];     //  32 Bytes\n      int sampleAgc;          //  04 Bytes\n      int sampleRaw;          //  04 Bytes\n      float sampleAvg;        //  04 Bytes\n      bool samplePeak;        //  01 Bytes\n      uint8_t fftResult[16];  //  16 Bytes\n      double FFT_Magnitude;   //  08 Bytes\n      double FFT_MajorPeak;   //  08 Bytes\n    };\n\n    #define UDPSOUND_MAX_PACKET 88 // max packet size for audiosync\n\n    // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer)\n    #ifdef UM_AUDIOREACTIVE_ENABLE\n    bool     enabled = true;\n    #else\n    bool     enabled = false;\n    #endif\n\n    bool     initDone = false;\n    bool     addPalettes = false;\n    int8_t   palettes = 0;\n\n    // variables  for UDP sound sync\n    WiFiUDP fftUdp;               // UDP object for sound sync (from WiFi UDP, not Async UDP!) \n    unsigned long lastTime = 0;   // last time of running UDP Microphone Sync\n    const uint16_t delayMs = 10;  // I don't want to sample too often and overload WLED\n    uint16_t audioSyncPort= 11988;// default port for UDP sound sync\n\n    bool updateIsRunning = false; // true during OTA.\n\n#ifdef ARDUINO_ARCH_ESP32\n    // used for AGC\n    int      last_soundAgc = -1;   // used to detect AGC mode change (for resetting AGC internal error buffers)\n    double   control_integrated = 0.0;   // persistent across calls to agcAvg(); \"integrator control\" = accumulated error\n\n\n    // variables used by getSample() and agcAvg()\n    int16_t  micIn = 0;           // Current sample starts with negative values and large values, which is why it's 16 bit signed\n    double   sampleMax = 0.0;     // Max sample over a few seconds. Needed for AGC controller.\n    double   micLev = 0.0;        // Used to convert returned value to have '0' as minimum. A leveller\n    float    expAdjF = 0.0f;      // Used for exponential filter.\n    float    sampleReal = 0.0f;\t  // \"sampleRaw\" as float, to provide bits that are lost otherwise (before amplification by sampleGain or inputLevel). Needed for AGC.\n    int16_t  sampleRaw = 0;       // Current sample. Must only be updated ONCE!!! (amplified mic value by sampleGain and inputLevel)\n    int16_t  rawSampleAgc = 0;    // not smoothed AGC sample\n#endif\n\n    // variables used in effects\n    float   volumeSmth = 0.0f;    // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample\n    int16_t  volumeRaw = 0;       // either sampleRaw or rawSampleAgc depending on soundAgc\n    float my_magnitude =0.0f;     // FFT_Magnitude, scaled by multAgc\n\n    // used to feed \"Info\" Page\n    unsigned long last_UDPTime = 0;    // time of last valid UDP sound sync datapacket\n    int receivedFormat = 0;            // last received UDP sound sync format - 0=none, 1=v1 (0.13.x), 2=v2 (0.14.x)\n    float maxSample5sec = 0.0f;        // max sample (after AGC) in last 5 seconds \n    unsigned long sampleMaxTimer = 0;  // last time maxSample5sec was reset\n    #define CYCLE_SAMPLEMAX 3500       // time window for merasuring\n\n    // strings to reduce flash memory usage (used more than twice)\n    static const char _name[];\n    static const char _enabled[];\n    static const char _config[];\n    static const char _dynamics[];\n    static const char _frequency[];\n    static const char _inputLvl[];\n#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)\n    static const char _analogmic[];\n#endif\n    static const char _digitalmic[];\n    static const char _addPalettes[];\n    static const char UDP_SYNC_HEADER[];\n    static const char UDP_SYNC_HEADER_v1[];\n\n    // private methods\n    void removeAudioPalettes(void);\n    void createAudioPalettes(void);\n    CRGB getCRGBForBand(int x, int pal);\n    void fillAudioPalettes(void);\n\n    ////////////////////\n    // Debug support  //\n    ////////////////////\n    void logAudio()\n    {\n      if (disableSoundProcessing && (!udpSyncConnected || ((audioSyncEnabled & 0x02) == 0))) return;   // no audio availeable\n    #ifdef MIC_LOGGER\n      // Debugging functions for audio input and sound processing. Comment out the values you want to see\n      PLOT_PRINT(\"micReal:\");     PLOT_PRINT(micDataReal); PLOT_PRINT(\"\\t\");\n      PLOT_PRINT(\"volumeSmth:\");  PLOT_PRINT(volumeSmth);  PLOT_PRINT(\"\\t\");\n      //PLOT_PRINT(\"volumeRaw:\");   PLOT_PRINT(volumeRaw);   PLOT_PRINT(\"\\t\");\n      PLOT_PRINT(\"DC_Level:\");    PLOT_PRINT(micLev);      PLOT_PRINT(\"\\t\");\n      //PLOT_PRINT(\"sampleAgc:\");   PLOT_PRINT(sampleAgc);   PLOT_PRINT(\"\\t\");\n      //PLOT_PRINT(\"sampleAvg:\");   PLOT_PRINT(sampleAvg);   PLOT_PRINT(\"\\t\");\n      //PLOT_PRINT(\"sampleReal:\");  PLOT_PRINT(sampleReal);  PLOT_PRINT(\"\\t\");\n    #ifdef ARDUINO_ARCH_ESP32\n      //PLOT_PRINT(\"micIn:\");       PLOT_PRINT(micIn);       PLOT_PRINT(\"\\t\");\n      //PLOT_PRINT(\"sample:\");      PLOT_PRINT(sample);      PLOT_PRINT(\"\\t\");\n      //PLOT_PRINT(\"sampleMax:\");   PLOT_PRINT(sampleMax);   PLOT_PRINT(\"\\t\");\n      //PLOT_PRINT(\"samplePeak:\");  PLOT_PRINT((samplePeak!=0) ? 128:0);   PLOT_PRINT(\"\\t\");\n      //PLOT_PRINT(\"multAgc:\");     PLOT_PRINT(multAgc, 4);  PLOT_PRINT(\"\\t\");\n    #endif\n      PLOT_PRINTLN();\n    #endif\n\n    #ifdef FFT_SAMPLING_LOG\n      #if 0\n        for(int i=0; i<NUM_GEQ_CHANNELS; i++) {\n          PLOT_PRINT(fftResult[i]);\n          PLOT_PRINT(\"\\t\");\n        }\n        PLOT_PRINTLN();\n      #endif\n\n      // OPTIONS are in the following format: Description \\n Option\n      //\n      // Set true if wanting to see all the bands in their own vertical space on the Serial Plotter, false if wanting to see values in Serial Monitor\n      const bool mapValuesToPlotterSpace = false;\n      // Set true to apply an auto-gain like setting to to the data (this hasn't been tested recently)\n      const bool scaleValuesFromCurrentMaxVal = false;\n      // prints the max value seen in the current data\n      const bool printMaxVal = false;\n      // prints the min value seen in the current data\n      const bool printMinVal = false;\n      // if !scaleValuesFromCurrentMaxVal, we scale values from [0..defaultScalingFromHighValue] to [0..scalingToHighValue], lower this if you want to see smaller values easier\n      const int defaultScalingFromHighValue = 256;\n      // Print values to terminal in range of [0..scalingToHighValue] if !mapValuesToPlotterSpace, or [(i)*scalingToHighValue..(i+1)*scalingToHighValue] if mapValuesToPlotterSpace\n      const int scalingToHighValue = 256;\n      // set higher if using scaleValuesFromCurrentMaxVal and you want a small value that's also the current maxVal to look small on the plotter (can't be 0 to avoid divide by zero error)\n      const int minimumMaxVal = 1;\n\n      int maxVal = minimumMaxVal;\n      int minVal = 0;\n      for(int i = 0; i < NUM_GEQ_CHANNELS; i++) {\n        if(fftResult[i] > maxVal) maxVal = fftResult[i];\n        if(fftResult[i] < minVal) minVal = fftResult[i];\n      }\n      for(int i = 0; i < NUM_GEQ_CHANNELS; i++) {\n        PLOT_PRINT(i); PLOT_PRINT(\":\");\n        PLOT_PRINTF(\"%04ld \", map(fftResult[i], 0, (scaleValuesFromCurrentMaxVal ? maxVal : defaultScalingFromHighValue), (mapValuesToPlotterSpace*i*scalingToHighValue)+0, (mapValuesToPlotterSpace*i*scalingToHighValue)+scalingToHighValue-1));\n      }\n      if(printMaxVal) {\n        PLOT_PRINTF(\"maxVal:%04d \", maxVal + (mapValuesToPlotterSpace ? 16*256 : 0));\n      }\n      if(printMinVal) {\n        PLOT_PRINTF(\"%04d:minVal \", minVal);  // printed with value first, then label, so negative values can be seen in Serial Monitor but don't throw off y axis in Serial Plotter\n      }\n      if(mapValuesToPlotterSpace)\n        PLOT_PRINTF(\"max:%04d \", (printMaxVal ? 17 : 16)*256); // print line above the maximum value we expect to see on the plotter to avoid autoscaling y axis\n      else {\n        PLOT_PRINTF(\"max:%04d \", 256);\n      }\n      PLOT_PRINTLN();\n    #endif // FFT_SAMPLING_LOG\n    } // logAudio()\n\n\n#ifdef ARDUINO_ARCH_ESP32\n    //////////////////////\n    // Audio Processing //\n    //////////////////////\n\n    /*\n    * A \"PI controller\" multiplier to automatically adjust sound sensitivity.\n    * \n    * A few tricks are implemented so that sampleAgc does't only utilize 0% and 100%:\n    * 0. don't amplify anything below squelch (but keep previous gain)\n    * 1. gain input = maximum signal observed in the last 5-10 seconds\n    * 2. we use two setpoints, one at ~60%, and one at ~80% of the maximum signal\n    * 3. the amplification depends on signal level:\n    *    a) normal zone - very slow adjustment\n    *    b) emergency zone (<10% or >90%) - very fast adjustment\n    */\n    void agcAvg(unsigned long the_time)\n    {\n      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\n\n      float lastMultAgc = multAgc;      // last multiplier used\n      float multAgcTemp = multAgc;      // new multiplier\n      float tmpAgc = sampleReal * multAgc;        // what-if amplified signal\n\n      float control_error;                        // \"control error\" input for PI control\n\n      if (last_soundAgc != soundAgc)\n        control_integrated = 0.0;                // new preset - reset integrator\n\n      // For PI controller, we need to have a constant \"frequency\"\n      // so let's make sure that the control loop is not running at insane speed\n      static unsigned long last_time = 0;\n      unsigned long time_now = millis();\n      if ((the_time > 0) && (the_time < time_now)) time_now = the_time;  // allow caller to override my clock\n\n      if (time_now - last_time > 2)  {\n        last_time = time_now;\n\n        if((fabsf(sampleReal) < 2.0f) || (sampleMax < 1.0)) {\n          // MIC signal is \"squelched\" - deliver silence\n          tmpAgc = 0;\n          // we need to \"spin down\" the intgrated error buffer\n          if (fabs(control_integrated) < 0.01)  control_integrated  = 0.0;\n          else                                  control_integrated *= 0.91;\n        } else {\n          // compute new setpoint\n          if (tmpAgc <= agcTarget0Up[AGC_preset])\n            multAgcTemp = agcTarget0[AGC_preset] / sampleMax;   // Make the multiplier so that sampleMax * multiplier = first setpoint\n          else\n            multAgcTemp = agcTarget1[AGC_preset] / sampleMax;   // Make the multiplier so that sampleMax * multiplier = second setpoint\n        }\n        // limit amplification\n        if (multAgcTemp > 32.0f)      multAgcTemp = 32.0f;\n        if (multAgcTemp < 1.0f/64.0f) multAgcTemp = 1.0f/64.0f;\n\n        // compute error terms\n        control_error = multAgcTemp - lastMultAgc;\n        \n        if (((multAgcTemp > 0.085f) && (multAgcTemp < 6.5f))    //integrator anti-windup by clamping\n            && (multAgc*sampleMax < agcZoneStop[AGC_preset]))   //integrator ceiling (>140% of max)\n          control_integrated += control_error * 0.002 * 0.25;   // 2ms = integration time; 0.25 for damping\n        else\n          control_integrated *= 0.9;                            // spin down that beasty integrator\n\n        // apply PI Control \n        tmpAgc = sampleReal * lastMultAgc;                      // check \"zone\" of the signal using previous gain\n        if ((tmpAgc > agcZoneHigh[AGC_preset]) || (tmpAgc < soundSquelch + agcZoneLow[AGC_preset])) {  // upper/lower energy zone\n          multAgcTemp = lastMultAgc + agcFollowFast[AGC_preset] * agcControlKp[AGC_preset] * control_error;\n          multAgcTemp += agcFollowFast[AGC_preset] * agcControlKi[AGC_preset] * control_integrated;\n        } else {                                                                         // \"normal zone\"\n          multAgcTemp = lastMultAgc + agcFollowSlow[AGC_preset] * agcControlKp[AGC_preset] * control_error;\n          multAgcTemp += agcFollowSlow[AGC_preset] * agcControlKi[AGC_preset] * control_integrated;\n        }\n\n        // limit amplification again - PI controller sometimes \"overshoots\"\n        //multAgcTemp = constrain(multAgcTemp, 0.015625f, 32.0f); // 1/64 < multAgcTemp < 32\n        if (multAgcTemp > 32.0f)      multAgcTemp = 32.0f;\n        if (multAgcTemp < 1.0f/64.0f) multAgcTemp = 1.0f/64.0f;\n      }\n\n      // NOW finally amplify the signal\n      tmpAgc = sampleReal * multAgcTemp;                  // apply gain to signal\n      if (fabsf(sampleReal) < 2.0f) tmpAgc = 0.0f;        // apply squelch threshold\n      //tmpAgc = constrain(tmpAgc, 0, 255);\n      if (tmpAgc > 255) tmpAgc = 255.0f;                  // limit to 8bit\n      if (tmpAgc < 1)   tmpAgc = 0.0f;                    // just to be sure\n\n      // update global vars ONCE - multAgc, sampleAGC, rawSampleAgc\n      multAgc = multAgcTemp;\n      rawSampleAgc = 0.8f * tmpAgc + 0.2f * (float)rawSampleAgc;\n      // update smoothed AGC sample\n      if (fabsf(tmpAgc) < 1.0f) \n        sampleAgc =  0.5f * tmpAgc + 0.5f * sampleAgc;    // fast path to zero\n      else\n        sampleAgc += agcSampleSmooth[AGC_preset] * (tmpAgc - sampleAgc); // smooth path\n\n      sampleAgc = fabsf(sampleAgc);                                      // // make sure we have a positive value\n      last_soundAgc = soundAgc;\n    } // agcAvg()\n\n    // post-processing and filtering of MIC sample (micDataReal) from FFTcode()\n    void getSample()\n    {\n      float    sampleAdj;           // Gain adjusted sample value\n      float    tmpSample;           // An interim sample variable used for calculations.\n      const float weighting = 0.2f; // Exponential filter weighting. Will be adjustable in a future release.\n      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\n\n      #ifdef WLED_DISABLE_SOUND\n        micIn = perlin8(millis(), millis());          // Simulated analog read\n        micDataReal = micIn;\n      #else\n        #ifdef ARDUINO_ARCH_ESP32\n        micIn = int(micDataReal);      // micDataSm = ((micData * 3) + micData)/4;\n        #else\n        // this is the minimal code for reading analog mic input on 8266.\n        // warning!! Absolutely experimental code. Audio on 8266 is still not working. Expects a million follow-on problems. \n        static unsigned long lastAnalogTime = 0;\n        static float lastAnalogValue = 0.0f;\n        if (millis() - lastAnalogTime > 20) {\n            micDataReal = analogRead(A0); // read one sample with 10bit resolution. This is a dirty hack, supporting volumereactive effects only.\n            lastAnalogTime = millis();\n            lastAnalogValue = micDataReal;\n            yield();\n        } else micDataReal = lastAnalogValue;\n        micIn = int(micDataReal);\n        #endif\n      #endif\n\n      micLev += (micDataReal-micLev) / 12288.0f;\n      if(micIn < micLev) micLev = ((micLev * 31.0f) + micDataReal) / 32.0f; // align MicLev to lowest input signal\n\n      micIn -= micLev;                                  // Let's center it to 0 now\n      // Using an exponential filter to smooth out the signal. We'll add controls for this in a future release.\n      float micInNoDC = fabsf(micDataReal - micLev);\n      expAdjF = (weighting * micInNoDC + (1.0f-weighting) * expAdjF);\n      expAdjF = fabsf(expAdjF);                         // Now (!) take the absolute value\n\n      expAdjF = (expAdjF <= soundSquelch) ? 0: expAdjF; // simple noise gate\n      if ((soundSquelch == 0) && (expAdjF < 0.25f)) expAdjF = 0; // do something meaningfull when \"squelch = 0\"\n\n      tmpSample = expAdjF;\n      micIn = abs(micIn);                               // And get the absolute value of each sample\n\n      sampleAdj = tmpSample * sampleGain / 40.0f * inputLevel/128.0f + tmpSample / 16.0f; // Adjust the gain. with inputLevel adjustment\n      sampleReal = tmpSample;\n\n      sampleAdj = fmax(fmin(sampleAdj, 255), 0);        // Question: why are we limiting the value to 8 bits ???\n      sampleRaw = (int16_t)sampleAdj;                   // ONLY update sample ONCE!!!!\n\n      // keep \"peak\" sample, but decay value if current sample is below peak\n      if ((sampleMax < sampleReal) && (sampleReal > 0.5f)) {\n        sampleMax = sampleMax + 0.5f * (sampleReal - sampleMax);  // new peak - with some filtering\n        // another simple way to detect samplePeak - cannot detect beats, but reacts on peak volume\n        if (((binNum < 12) || ((maxVol < 1))) && (millis() - timeOfPeak > 80) && (sampleAvg > 1)) {\n          samplePeak    = true;\n          timeOfPeak    = millis();\n          udpSamplePeak = true;\n        }\n      } else {\n        if ((multAgc*sampleMax > agcZoneStop[AGC_preset]) && (soundAgc > 0))\n          sampleMax += 0.5f * (sampleReal - sampleMax);        // over AGC Zone - get back quickly\n        else\n          sampleMax *= agcSampleDecay[AGC_preset];             // signal to zero --> 5-8sec\n      }\n      if (sampleMax < 0.5f) sampleMax = 0.0f;\n\n      sampleAvg = ((sampleAvg * 15.0f) + sampleAdj) / 16.0f;   // Smooth it out over the last 16 samples.\n      sampleAvg = fabsf(sampleAvg);                            // make sure we have a positive value\n    } // getSample()\n\n#endif\n\n    /* Limits the dynamics of volumeSmth (= sampleAvg or sampleAgc). \n     * does not affect FFTResult[] or volumeRaw ( = sample or rawSampleAgc) \n    */\n    // effects: Gravimeter, Gravcenter, Gravcentric, Noisefire, Plasmoid, Freqpixels, Freqwave, Gravfreq, (2D Swirl, 2D Waverly)\n    void limitSampleDynamics(void) {\n      const float bigChange = 196;                  // just a representative number - a large, expected sample value\n      static unsigned long last_time = 0;\n      static float last_volumeSmth = 0.0f;\n\n      if (limiterOn == false) return;\n\n      long delta_time = millis() - last_time;\n      delta_time = constrain(delta_time , 1, 1000); // below 1ms -> 1ms; above 1sec -> sily lil hick-up\n      float deltaSample = volumeSmth - last_volumeSmth;\n\n      if (attackTime > 0) {                         // user has defined attack time > 0\n        float maxAttack =   bigChange * float(delta_time) / float(attackTime);\n        if (deltaSample > maxAttack) deltaSample = maxAttack;\n      }\n      if (decayTime > 0) {                          // user has defined decay time > 0\n        float maxDecay  = - bigChange * float(delta_time) / float(decayTime);\n        if (deltaSample < maxDecay) deltaSample = maxDecay;\n      }\n\n      volumeSmth = last_volumeSmth + deltaSample; \n\n      last_volumeSmth = volumeSmth;\n      last_time = millis();\n    }\n\n\n    //////////////////////\n    // UDP Sound Sync   //\n    //////////////////////\n\n    // try to establish UDP sound sync connection\n    void connectUDPSoundSync(void) {\n      // This function tries to establish a UDP sync connection if needed\n      // necessary as we also want to transmit in \"AP Mode\", but the standard \"connected()\" callback only reacts on STA connection\n      static unsigned long last_connection_attempt = 0;\n\n      if ((audioSyncPort <= 0) || ((audioSyncEnabled & 0x03) == 0)) return;  // Sound Sync not enabled\n      if (udpSyncConnected) return;                                          // already connected\n      if (!(apActive || interfacesInited)) return;                           // neither AP nor other connections availeable\n      if (millis() - last_connection_attempt < 15000) return;                // only try once in 15 seconds\n      if (updateIsRunning) return; \n\n      // if we arrive here, we need a UDP connection but don't have one\n      last_connection_attempt = millis();\n      connected(); // try to start UDP\n    }\n\n#ifdef ARDUINO_ARCH_ESP32\n    void transmitAudioData()\n    {\n      if (!udpSyncConnected) return;\n      //DEBUGSR_PRINTLN(\"Transmitting UDP Mic Packet\");\n\n      audioSyncPacket transmitData;\n      memset(reinterpret_cast<void *>(&transmitData), 0, sizeof(transmitData)); // make sure that the packet - including \"invisible\" padding bytes added by the compiler - is fully initialized\n\n      strncpy_P(transmitData.header, PSTR(UDP_SYNC_HEADER), 6);\n      // transmit samples that were not modified by limitSampleDynamics()\n      transmitData.sampleRaw   = (soundAgc) ? rawSampleAgc: sampleRaw;\n      transmitData.sampleSmth  = (soundAgc) ? sampleAgc   : sampleAvg;\n      transmitData.samplePeak  = udpSamplePeak ? 1:0;\n      udpSamplePeak            = false;           // Reset udpSamplePeak after we've transmitted it\n\n      for (int i = 0; i < NUM_GEQ_CHANNELS; i++) {\n        transmitData.fftResult[i] = (uint8_t)constrain(fftResult[i], 0, 254);\n      }\n\n      transmitData.FFT_Magnitude = my_magnitude;\n      transmitData.FFT_MajorPeak = FFT_MajorPeak;\n\n      if (fftUdp.beginMulticastPacket() != 0) { // beginMulticastPacket returns 0 in case of error\n        fftUdp.write(reinterpret_cast<uint8_t *>(&transmitData), sizeof(transmitData));\n        fftUdp.endPacket();\n      }\n      return;\n    } // transmitAudioData()\n\n#endif\n\n    static bool isValidUdpSyncVersion(const char *header) {\n      return strncmp_P(header, UDP_SYNC_HEADER, 6) == 0;\n    }\n    static bool isValidUdpSyncVersion_v1(const char *header) {\n      return strncmp_P(header, UDP_SYNC_HEADER_v1, 6) == 0;\n    }\n\n    void decodeAudioData(int packetSize, uint8_t *fftBuff) {\n      audioSyncPacket receivedPacket;\n      memset(&receivedPacket, 0, sizeof(receivedPacket));                                  // start clean\n      memcpy(&receivedPacket, fftBuff, min((unsigned)packetSize, (unsigned)sizeof(receivedPacket))); // don't violate alignment - thanks @willmmiles#\n\n      // update samples for effects\n      volumeSmth   = fmaxf(receivedPacket.sampleSmth, 0.0f);\n      volumeRaw    = fmaxf(receivedPacket.sampleRaw, 0.0f);\n#ifdef ARDUINO_ARCH_ESP32\n      // update internal samples\n      sampleRaw    = volumeRaw;\n      sampleAvg    = volumeSmth;\n      rawSampleAgc = volumeRaw;\n      sampleAgc    = volumeSmth;\n      multAgc      = 1.0f;   \n#endif\n      // Only change samplePeak IF it's currently false.\n      // If it's true already, then the animation still needs to respond.\n      autoResetPeak();\n      if (!samplePeak) {\n            samplePeak = receivedPacket.samplePeak >0 ? true:false;\n            if (samplePeak) timeOfPeak = millis();\n            //userVar1 = samplePeak;\n      }\n      //These values are only computed by ESP32\n      for (int i = 0; i < NUM_GEQ_CHANNELS; i++) fftResult[i] = receivedPacket.fftResult[i];\n      my_magnitude  = fmaxf(receivedPacket.FFT_Magnitude, 0.0f);\n      FFT_Magnitude = my_magnitude;\n      FFT_MajorPeak = constrain(receivedPacket.FFT_MajorPeak, 1.0f, 11025.0f);  // restrict value to range expected by effects\n    }\n\n    void decodeAudioData_v1(int packetSize, uint8_t *fftBuff) {\n      audioSyncPacket_v1 *receivedPacket = reinterpret_cast<audioSyncPacket_v1*>(fftBuff);\n      // update samples for effects\n      volumeSmth   = fmaxf(receivedPacket->sampleAgc, 0.0f);\n      volumeRaw    = volumeSmth;   // V1 format does not have \"raw\" AGC sample\n#ifdef ARDUINO_ARCH_ESP32\n      // update internal samples\n      sampleRaw    = fmaxf(receivedPacket->sampleRaw, 0.0f);\n      sampleAvg    = fmaxf(receivedPacket->sampleAvg, 0.0f);;\n      sampleAgc    = volumeSmth;\n      rawSampleAgc = volumeRaw;\n      multAgc      = 1.0f;\n#endif \n      // Only change samplePeak IF it's currently false.\n      // If it's true already, then the animation still needs to respond.\n      autoResetPeak();\n      if (!samplePeak) {\n            samplePeak = receivedPacket->samplePeak >0 ? true:false;\n            if (samplePeak) timeOfPeak = millis();\n            //userVar1 = samplePeak;\n      }\n      //These values are only available on the ESP32\n      for (int i = 0; i < NUM_GEQ_CHANNELS; i++) fftResult[i] = receivedPacket->fftResult[i];\n      my_magnitude  = fmaxf(receivedPacket->FFT_Magnitude, 0.0);\n      FFT_Magnitude = my_magnitude;\n      FFT_MajorPeak = constrain(receivedPacket->FFT_MajorPeak, 1.0, 11025.0);  // restrict value to range expected by effects\n    }\n\n    bool receiveAudioData()   // check & process new data. return TRUE in case that new audio data was received. \n    {\n      if (!udpSyncConnected) return false;\n      bool haveFreshData = false;\n\n      size_t packetSize = fftUdp.parsePacket();\n#ifdef ARDUINO_ARCH_ESP32\n      if ((packetSize > 0) && ((packetSize < 5) || (packetSize > UDPSOUND_MAX_PACKET))) fftUdp.flush(); // discard invalid packets (too small or too big) - only works on esp32\n#endif\n      if ((packetSize > 5) && (packetSize <= UDPSOUND_MAX_PACKET)) {\n        //DEBUGSR_PRINTLN(\"Received UDP Sync Packet\");\n        uint8_t fftBuff[UDPSOUND_MAX_PACKET+1] = { 0 }; // fixed-size buffer for receiving (stack), to avoid heap fragmentation caused by variable sized arrays\n        fftUdp.read(fftBuff, packetSize);\n\n        // VERIFY THAT THIS IS A COMPATIBLE PACKET\n        if (packetSize == sizeof(audioSyncPacket) && (isValidUdpSyncVersion((const char *)fftBuff))) {\n          decodeAudioData(packetSize, fftBuff);\n          //DEBUGSR_PRINTLN(\"Finished parsing UDP Sync Packet v2\");\n          haveFreshData = true;\n          receivedFormat = 2;\n        } else {\n          if (packetSize == sizeof(audioSyncPacket_v1) && (isValidUdpSyncVersion_v1((const char *)fftBuff))) {\n            decodeAudioData_v1(packetSize, fftBuff);\n            //DEBUGSR_PRINTLN(\"Finished parsing UDP Sync Packet v1\");\n            haveFreshData = true;\n            receivedFormat = 1;\n          } else receivedFormat = 0; // unknown format\n        }\n      }\n      return haveFreshData;\n    }\n\n\n    //////////////////////\n    // usermod functions//\n    //////////////////////\n\n  public:\n    //Functions called by WLED or other usermods\n\n    /*\n     * setup() is called once at boot. WiFi is not yet connected at this point.\n     * You can use it to initialize variables, sensors or similar.\n     * It is called *AFTER* readFromConfig()\n     */\n    void setup() override\n    {\n      disableSoundProcessing = true; // just to be sure\n      if (!initDone) {\n        // usermod exchangeable data\n        // we will assign all usermod exportable data here as pointers to original variables or arrays and allocate memory for pointers\n        um_data = new um_data_t;\n        um_data->u_size = 8;\n        um_data->u_type = new um_types_t[um_data->u_size];\n        um_data->u_data = new void*[um_data->u_size];\n        um_data->u_data[0] = &volumeSmth;      //*used (New)\n        um_data->u_type[0] = UMT_FLOAT;\n        um_data->u_data[1] = &volumeRaw;      // used (New)\n        um_data->u_type[1] = UMT_UINT16;\n        um_data->u_data[2] = fftResult;        //*used (Blurz, DJ Light, Noisemove, GEQ_base, 2D Funky Plank, Akemi)\n        um_data->u_type[2] = UMT_BYTE_ARR;\n        um_data->u_data[3] = &samplePeak;      //*used (Puddlepeak, Ripplepeak, Waterfall)\n        um_data->u_type[3] = UMT_BYTE;\n        um_data->u_data[4] = &FFT_MajorPeak;   //*used (Ripplepeak, Freqmap, Freqmatrix, Freqpixels, Freqwave, Gravfreq, Rocktaves, Waterfall)\n        um_data->u_type[4] = UMT_FLOAT;\n        um_data->u_data[5] = &my_magnitude;   // used (New)\n        um_data->u_type[5] = UMT_FLOAT;\n        um_data->u_data[6] = &maxVol;          // assigned in effect function from UI element!!! (Puddlepeak, Ripplepeak, Waterfall)\n        um_data->u_type[6] = UMT_BYTE;\n        um_data->u_data[7] = &binNum;          // assigned in effect function from UI element!!! (Puddlepeak, Ripplepeak, Waterfall)\n        um_data->u_type[7] = UMT_BYTE;\n      }\n\n\n#ifdef ARDUINO_ARCH_ESP32\n\n      // Reset I2S peripheral for good measure\n      i2s_driver_uninstall(I2S_NUM_0);   // E (696) I2S: i2s_driver_uninstall(2006): I2S port 0 has not installed\n      #if !defined(CONFIG_IDF_TARGET_ESP32C3)\n        delay(100);\n        periph_module_reset(PERIPH_I2S0_MODULE);   // not possible on -C3\n      #endif\n      delay(100);         // Give that poor microphone some time to setup.\n\n      useBandPassFilter = false;\n\n      #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3)\n        if ((i2sckPin == I2S_PIN_NO_CHANGE) && (i2ssdPin >= 0) && (i2swsPin >= 0) && ((dmType == 1) || (dmType == 4)) ) dmType = 5;   // dummy user support: SCK == -1 --means--> PDM microphone\n      #endif\n\n      switch (dmType) {\n      #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3)\n        // stub cases for not-yet-supported I2S modes on other ESP32 chips\n        case 0:  //ADC analog\n        #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3)\n        case 5:  //PDM Microphone\n        #endif\n      #endif\n        case 1:\n          DEBUGSR_PRINT(F(\"AR: Generic I2S Microphone - \")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT));\n          audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE);\n          delay(100);\n          if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin);\n          break;\n        case 2:\n          DEBUGSR_PRINTLN(F(\"AR: ES7243 Microphone (right channel only).\"));\n          audioSource = new ES7243(SAMPLE_RATE, BLOCK_SIZE);\n          delay(100);\n          if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin);\n          break;\n        case 3:\n          DEBUGSR_PRINT(F(\"AR: SPH0645 Microphone - \")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT));\n          audioSource = new SPH0654(SAMPLE_RATE, BLOCK_SIZE);\n          delay(100);\n          audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin);\n          break;\n        case 4:\n          DEBUGSR_PRINT(F(\"AR: Generic I2S Microphone with Master Clock - \")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT));\n          audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/24.0f);\n          delay(100);\n          if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin);\n          break;\n        #if  !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3)\n        case 5:\n          DEBUGSR_PRINT(F(\"AR: Generic PDM Microphone - \")); DEBUGSR_PRINTLN(F(I2S_PDM_MIC_CHANNEL_TEXT));\n          audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/4.0f);\n          useBandPassFilter = true;  // this reduces the noise floor on SPM1423 from 5% Vpp (~380) down to 0.05% Vpp (~5)\n          delay(100);\n          if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin);\n          break;\n        #endif\n        case 6:\n          DEBUGSR_PRINTLN(F(\"AR: ES8388 Source\"));\n          audioSource = new ES8388Source(SAMPLE_RATE, BLOCK_SIZE);\n          delay(100);\n          if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin);\n          break;\n\n        #if  !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)\n        // ADC over I2S is only possible on \"classic\" ESP32\n        case 0:\n          DEBUGSR_PRINTLN(F(\"AR: Analog Microphone (left channel only).\"));\n          audioSource = new I2SAdcSource(SAMPLE_RATE, BLOCK_SIZE);\n          delay(100);\n          useBandPassFilter = true;  // PDM bandpass filter seems to help for bad quality analog\n          if (audioSource) audioSource->initialize(audioPin);\n          break;\n        #endif\n\n        case 254: // dummy \"network receive only\" mode\n          if (audioSource) delete audioSource; audioSource = nullptr;\n          disableSoundProcessing = true;\n          audioSyncEnabled = 2; // force udp sound receive mode\n          enabled = true;\n          break;\n\n        case 255: // 255 = -1 = no audio source\n          // falls through to default\n        default:\n          if (audioSource) delete audioSource; audioSource = nullptr;\n          disableSoundProcessing = true;\n          enabled = false;\n        break;\n      }\n      delay(250); // give microphone enough time to initialise\n\n      if (!audioSource && (dmType != 254)) enabled = false;// audio failed to initialise\n#endif\n      if (enabled) onUpdateBegin(false);                 // create FFT task, and initialize network\n\n\n#ifdef ARDUINO_ARCH_ESP32\n      if (FFT_Task == nullptr) enabled = false;          // FFT task creation failed\n      if((!audioSource) || (!audioSource->isInitialized())) {  // audio source failed to initialize. Still stay \"enabled\", as there might be input arriving via UDP Sound Sync \n      #ifdef WLED_DEBUG\n        DEBUG_PRINTLN(F(\"AR: Failed to initialize sound input driver. Please check input PIN settings.\"));\n      #else\n        DEBUGSR_PRINTLN(F(\"AR: Failed to initialize sound input driver. Please check input PIN settings.\"));\n      #endif\n        disableSoundProcessing = true;\n      }\n#endif\n      if (enabled) disableSoundProcessing = false;       // all good - enable audio processing\n      if (enabled) connectUDPSoundSync();\n      if (enabled && addPalettes) createAudioPalettes();\n      initDone = true;\n    }\n\n\n    /*\n     * connected() is called every time the WiFi is (re)connected\n     * Use it to initialize network interfaces\n     */\n    void connected() override\n    {\n      if (udpSyncConnected) {   // clean-up: if open, close old UDP sync connection\n        udpSyncConnected = false;\n        fftUdp.stop();\n      }\n      \n      if (audioSyncPort > 0 && (audioSyncEnabled & 0x03)) {\n      #ifdef ARDUINO_ARCH_ESP32\n        udpSyncConnected = fftUdp.beginMulticast(IPAddress(239, 0, 0, 1), audioSyncPort);\n      #else\n        udpSyncConnected = fftUdp.beginMulticast(WiFi.localIP(), IPAddress(239, 0, 0, 1), audioSyncPort);\n      #endif\n      }\n    }\n\n\n    /*\n     * loop() is called continuously. Here you can check for events, read sensors, etc.\n     * \n     * Tips:\n     * 1. You can use \"if (WLED_CONNECTED)\" to check for a successful network connection.\n     *    Additionally, \"if (WLED_MQTT_CONNECTED)\" is available to check for a connection to an MQTT broker.\n     * \n     * 2. Try to avoid using the delay() function. NEVER use delays longer than 10 milliseconds.\n     *    Instead, use a timer check as shown here.\n     */\n    void loop() override\n    {\n      static unsigned long lastUMRun = millis();\n\n      if (!enabled) {\n        disableSoundProcessing = true;   // keep processing suspended (FFT task)\n        lastUMRun = millis();            // update time keeping\n        return;\n      }\n      // We cannot wait indefinitely before processing audio data\n      if (strip.isUpdating() && (millis() - lastUMRun < 2)) return;   // be nice, but not too nice\n\n      // suspend local sound processing when \"real time mode\" is active (E131, UDP, ADALIGHT, ARTNET)\n      if (  (realtimeOverride == REALTIME_OVERRIDE_NONE)  // please add other overrides here if needed\n          &&( (realtimeMode == REALTIME_MODE_GENERIC)\n            ||(realtimeMode == REALTIME_MODE_E131)\n            ||(realtimeMode == REALTIME_MODE_UDP)\n            ||(realtimeMode == REALTIME_MODE_ADALIGHT)\n            ||(realtimeMode == REALTIME_MODE_ARTNET) ) )  // please add other modes here if needed\n      {\n        #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG)\n        if ((disableSoundProcessing == false) && (audioSyncEnabled == 0)) {  // we just switched to \"disabled\"\n          DEBUG_PRINTLN(F(\"[AR userLoop]  realtime mode active - audio processing suspended.\"));\n          DEBUG_PRINTF_P(PSTR(\"               RealtimeMode = %d; RealtimeOverride = %d\\n\"), int(realtimeMode), int(realtimeOverride));\n        }\n        #endif\n        disableSoundProcessing = true;\n      } else {\n        #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG)\n        if ((disableSoundProcessing == true) && (audioSyncEnabled == 0) && audioSource && audioSource->isInitialized()) {    // we just switched to \"enabled\"\n          DEBUG_PRINTLN(F(\"[AR userLoop]  realtime mode ended - audio processing resumed.\"));\n          DEBUG_PRINTF_P(PSTR(\"               RealtimeMode = %d; RealtimeOverride = %d\\n\"), int(realtimeMode), int(realtimeOverride));\n        }\n        #endif\n        if ((disableSoundProcessing == true) && (audioSyncEnabled == 0)) lastUMRun = millis();  // just left \"realtime mode\" - update timekeeping\n        disableSoundProcessing = false;\n      }\n\n      if (audioSyncEnabled & 0x02) disableSoundProcessing = true;   // make sure everything is disabled IF in audio Receive mode\n      if (audioSyncEnabled & 0x01) disableSoundProcessing = false;  // keep running audio IF we're in audio Transmit mode\n#ifdef ARDUINO_ARCH_ESP32\n      if (!audioSource || !audioSource->isInitialized()) disableSoundProcessing = true;  // no audio source\n\n\n      // Only run the sampling code IF we're not in Receive mode or realtime mode\n      if (!(audioSyncEnabled & 0x02) && !disableSoundProcessing) {\n        if (soundAgc > AGC_NUM_PRESETS) soundAgc = 0; // make sure that AGC preset is valid (to avoid array bounds violation)\n\n        unsigned long t_now = millis();      // remember current time\n        int userloopDelay = int(t_now - lastUMRun);\n        if (lastUMRun == 0) userloopDelay=0; // startup - don't have valid data from last run.\n\n        #ifdef WLED_DEBUG\n          // complain when audio userloop has been delayed for long time. Currently we need userloop running between 500 and 1500 times per second. \n          // softhack007 disabled temporarily - avoid serial console spam with MANY leds and low FPS\n          //if ((userloopDelay > 65) && !disableSoundProcessing && (audioSyncEnabled == 0)) {\n          //  DEBUG_PRINTF_P(PSTR(\"[AR userLoop] hiccup detected -> was inactive for last %d millis!\\n\"), userloopDelay);\n          //}\n        #endif\n\n        // run filters, and repeat in case of loop delays (hick-up compensation)\n        if (userloopDelay <2) userloopDelay = 0;      // minor glitch, no problem\n        if (userloopDelay >200) userloopDelay = 200;  // limit number of filter re-runs  \n        do {\n          getSample();                        // run microphone sampling filters\n          agcAvg(t_now - userloopDelay);      // Calculated the PI adjusted value as sampleAvg\n          userloopDelay -= 2;                 // advance \"simulated time\" by 2ms\n        } while (userloopDelay > 0);\n        lastUMRun = t_now;                    // update time keeping\n\n        // update samples for effects (raw, smooth) \n        volumeSmth = (soundAgc) ? sampleAgc   : sampleAvg;\n        volumeRaw  = (soundAgc) ? rawSampleAgc: sampleRaw;\n        // update FFTMagnitude, taking into account AGC amplification\n        my_magnitude = FFT_Magnitude; // / 16.0f, 8.0f, 4.0f done in effects\n        if (soundAgc) my_magnitude *= multAgc;\n        if (volumeSmth < 1 ) my_magnitude = 0.001f;  // noise gate closed - mute\n\n        limitSampleDynamics();\n      }  // if (!disableSoundProcessing)\n#endif\n\n      autoResetPeak();          // auto-reset sample peak after strip minShowDelay\n      if (!udpSyncConnected) udpSamplePeak = false;  // reset UDP samplePeak while UDP is unconnected\n\n      connectUDPSoundSync();  // ensure we have a connection - if needed\n\n      // UDP Microphone Sync  - receive mode\n      if ((audioSyncEnabled & 0x02) && udpSyncConnected) {\n          // Only run the audio listener code if we're in Receive mode\n          static float syncVolumeSmth = 0;\n          bool have_new_sample = false;\n          if (millis() - lastTime > delayMs) {\n            have_new_sample = receiveAudioData();\n            if (have_new_sample) last_UDPTime = millis();\n#ifdef ARDUINO_ARCH_ESP32\n            else fftUdp.flush(); // Flush udp input buffers if we haven't read it - avoids hickups in receive mode. Does not work on 8266.\n#endif\n            lastTime = millis();\n          }\n          if (have_new_sample) syncVolumeSmth = volumeSmth;   // remember received sample\n          else volumeSmth = syncVolumeSmth;                   // restore originally received sample for next run of dynamics limiter\n          limitSampleDynamics();                              // run dynamics limiter on received volumeSmth, to hide jumps and hickups\n      }\n\n      #if defined(MIC_LOGGER) || defined(MIC_SAMPLING_LOG) || defined(FFT_SAMPLING_LOG)\n      static unsigned long lastMicLoggerTime = 0;\n      if (millis()-lastMicLoggerTime > 20) {\n        lastMicLoggerTime = millis();\n        logAudio();\n      }\n      #endif\n\n      // Info Page: keep max sample from last 5 seconds\n#ifdef ARDUINO_ARCH_ESP32\n      if ((millis() -  sampleMaxTimer) > CYCLE_SAMPLEMAX) {\n        sampleMaxTimer = millis();\n        maxSample5sec = (0.15f * maxSample5sec) + 0.85f *((soundAgc) ? sampleAgc : sampleAvg); // reset, and start with some smoothing\n        if (sampleAvg < 1) maxSample5sec = 0; // noise gate \n      } else {\n         if ((sampleAvg >= 1)) maxSample5sec = fmaxf(maxSample5sec, (soundAgc) ? rawSampleAgc : sampleRaw); // follow maximum volume\n      }\n#else  // similar functionality for 8266 receive only - use VolumeSmth instead of raw sample data\n      if ((millis() -  sampleMaxTimer) > CYCLE_SAMPLEMAX) {\n        sampleMaxTimer = millis();\n        maxSample5sec = (0.15 * maxSample5sec) + 0.85 * volumeSmth; // reset, and start with some smoothing\n        if (volumeSmth < 1.0f) maxSample5sec = 0; // noise gate\n        if (maxSample5sec < 0.0f) maxSample5sec = 0; // avoid negative values\n      } else {\n         if (volumeSmth >= 1.0f) maxSample5sec = fmaxf(maxSample5sec, volumeRaw); // follow maximum volume\n      }\n#endif\n\n#ifdef ARDUINO_ARCH_ESP32\n      //UDP Microphone Sync  - transmit mode\n      if ((audioSyncEnabled & 0x01) && (millis() - lastTime > 20)) {\n        // Only run the transmit code IF we're in Transmit mode\n        transmitAudioData();\n        lastTime = millis();\n      }\n#endif\n\n      fillAudioPalettes();\n    }\n\n\n    bool getUMData(um_data_t **data) override\n    {\n      if (!data || !enabled) return false; // no pointer provided by caller or not enabled -> exit\n      *data = um_data;\n      return true;\n    }\n\n#ifdef ARDUINO_ARCH_ESP32\n    void onUpdateBegin(bool init) override\n    {\n#ifdef WLED_DEBUG\n      fftTime = sampleTime = 0;\n#endif\n      // gracefully suspend FFT task (if running)\n      disableSoundProcessing = true;\n\n      // reset sound data\n      micDataReal = 0.0f;\n      volumeRaw = 0; volumeSmth = 0;\n      sampleAgc = 0; sampleAvg = 0;\n      sampleRaw = 0; rawSampleAgc = 0;\n      my_magnitude = 0; FFT_Magnitude = 0; FFT_MajorPeak = 1;\n      multAgc = 1;\n      // reset FFT data\n      memset(fftCalc, 0, sizeof(fftCalc)); \n      memset(fftAvg, 0, sizeof(fftAvg)); \n      memset(fftResult, 0, sizeof(fftResult)); \n      for(int i=(init?0:1); i<NUM_GEQ_CHANNELS; i+=2) fftResult[i] = 16; // make a tiny pattern\n      inputLevel = 128;                                    // reset level slider to default\n      autoResetPeak();\n\n      if (init && FFT_Task) {\n        delay(25);                // give some time for I2S driver to finish sampling before we suspend it\n        vTaskSuspend(FFT_Task);   // update is about to begin, disable task to prevent crash\n        if (udpSyncConnected) {   // close UDP sync connection (if open)\n          udpSyncConnected = false;\n          fftUdp.stop();\n        }\n      } else {\n        // update has failed or create task requested\n        if (FFT_Task) {\n          vTaskResume(FFT_Task);\n          connected(); // resume UDP\n        } else\n          xTaskCreateUniversal(               // xTaskCreateUniversal also works on -S2 and -C3 with single core\n            FFTcode,                          // Function to implement the task\n            \"FFT\",                            // Name of the task\n            3592,                             // Stack size in words // 3592 leaves 800-1024 bytes of task stack free\n            NULL,                             // Task input parameter\n            FFTTASK_PRIORITY,                 // Priority of the task\n            &FFT_Task                         // Task handle\n            , 0                               // Core where the task should run\n          );\n      }\n      micDataReal = 0.0f;                     // just to be sure\n      if (enabled) disableSoundProcessing = false;\n      updateIsRunning = init;\n    }\n\n#else // reduced function for 8266\n    void onUpdateBegin(bool init)\n    {\n      // gracefully suspend audio (if running)\n      disableSoundProcessing = true;\n      // reset sound data\n      volumeRaw = 0; volumeSmth = 0;\n      for(int i=(init?0:1); i<NUM_GEQ_CHANNELS; i+=2) fftResult[i] = 16; // make a tiny pattern\n      autoResetPeak();\n      if (init) {\n        if (udpSyncConnected) {   // close UDP sync connection (if open)\n          udpSyncConnected = false;\n          fftUdp.stop();\n          DEBUGSR_PRINTLN(F(\"AR onUpdateBegin(true): UDP connection closed.\"));\n          receivedFormat = 0;\n        }\n      }\n      if (enabled) disableSoundProcessing = init; // init = true means that OTA is just starting --> don't process audio\n      updateIsRunning = init;\n    }\n#endif\n\n#ifdef ARDUINO_ARCH_ESP32\n    /**\n     * handleButton() can be used to override default button behaviour. Returning true\n     * will prevent button working in a default way.\n     */\n    bool handleButton(uint8_t b) override {\n      yield();\n      // crude way of determining if audio input is analog\n      // better would be for AudioSource to implement getType()\n      if (enabled\n          && dmType == 0 && audioPin>=0\n          && (buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED)\n         ) {\n        return true;\n      }\n      return false;\n    }\n\n#endif\n    ////////////////////////////\n    // Settings and Info Page //\n    ////////////////////////////\n\n    /*\n     * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.\n     * Creating an \"u\" object allows you to add custom key/value pairs to the Info section of the WLED web UI.\n     * Below it is shown how this could be used for e.g. a light sensor\n     */\n    void addToJsonInfo(JsonObject& root) override\n    {\n#ifdef ARDUINO_ARCH_ESP32\n      char myStringBuffer[16]; // buffer for snprintf() - not used yet on 8266\n#endif\n      JsonObject user = root[\"u\"];\n      if (user.isNull()) user = root.createNestedObject(\"u\");\n\n      JsonArray infoArr = user.createNestedArray(FPSTR(_name));\n\n      String uiDomString = F(\"<button class=\\\"btn btn-xs\\\" onclick=\\\"requestJson({\");\n      uiDomString += FPSTR(_name);\n      uiDomString += F(\":{\");\n      uiDomString += FPSTR(_enabled);\n      uiDomString += enabled ? F(\":false}});\\\">\") : F(\":true}});\\\">\");\n      uiDomString += F(\"<i class=\\\"icons\");\n      uiDomString += enabled ? F(\" on\") : F(\" off\");\n      uiDomString += F(\"\\\">&#xe08f;</i>\");\n      uiDomString += F(\"</button>\");\n      infoArr.add(uiDomString);\n\n      if (enabled) {\n#ifdef ARDUINO_ARCH_ESP32\n        // Input Level Slider\n        if (disableSoundProcessing == false) {                                 // only show slider when audio processing is running\n          if (soundAgc > 0) {\n            infoArr = user.createNestedArray(F(\"GEQ Input Level\"));           // if AGC is on, this slider only affects fftResult[] frequencies\n          } else {\n            infoArr = user.createNestedArray(F(\"Audio Input Level\"));\n          }\n          uiDomString = F(\"<div class=\\\"slider\\\"><div class=\\\"sliderwrap il\\\"><input class=\\\"noslide\\\" onchange=\\\"requestJson({\");\n          uiDomString += FPSTR(_name);\n          uiDomString += F(\":{\");\n          uiDomString += FPSTR(_inputLvl);\n          uiDomString += F(\":parseInt(this.value)}});\\\" oninput=\\\"updateTrail(this);\\\" max=255 min=0 type=\\\"range\\\" value=\");\n          uiDomString += inputLevel;\n          uiDomString += F(\" /><div class=\\\"sliderdisplay\\\"></div></div></div>\"); //<output class=\\\"sliderbubble\\\"></output>\n          infoArr.add(uiDomString);\n        } \n#endif\n        // The following can be used for troubleshooting user errors and is so not enclosed in #ifdef WLED_DEBUG\n\n        // current Audio input\n        infoArr = user.createNestedArray(F(\"Audio Source\"));\n        if (audioSyncEnabled & 0x02) {\n          // UDP sound sync - receive mode\n          infoArr.add(F(\"UDP sound sync\"));\n          if (udpSyncConnected) {\n            if (millis() - last_UDPTime < 2500)\n              infoArr.add(F(\" - receiving\"));\n            else\n              infoArr.add(F(\" - idle\"));\n          } else {\n            infoArr.add(F(\" - no connection\"));\n          }\n#ifndef ARDUINO_ARCH_ESP32  // substitute for 8266\n        } else {\n          infoArr.add(F(\"sound sync Off\"));\n        }\n#else  // ESP32 only\n        } else {\n          // Analog or I2S digital input\n          if (audioSource && (audioSource->isInitialized())) {\n            // audio source successfully configured\n            if (audioSource->getType() == AudioSource::Type_I2SAdc) {\n              infoArr.add(F(\"ADC analog\"));\n            } else {\n              if (dmType == 5) infoArr.add(F(\"PDM digital\")); // dmType 5 => generic PDM microphone\n              else infoArr.add(F(\"I2S digital\"));\n            }\n            // input level or \"silence\"\n            if (maxSample5sec > 1.0f) {\n              float my_usage = 100.0f * (maxSample5sec / 255.0f);\n              snprintf_P(myStringBuffer, 15, PSTR(\" - peak %3d%%\"), int(my_usage));\n              infoArr.add(myStringBuffer);\n            } else {\n              infoArr.add(F(\" - quiet\"));\n            }\n          } else {\n            // error during audio source setup\n            infoArr.add(F(\"not initialized\"));\n            infoArr.add(F(\" - check pin settings\"));\n          }\n        }\n\n        // Sound processing (FFT and input filters)\n        infoArr = user.createNestedArray(F(\"Sound Processing\"));\n        if (audioSource && (disableSoundProcessing == false)) {\n          infoArr.add(F(\"running\"));\n        } else {\n          infoArr.add(F(\"suspended\"));\n        }\n\n        // AGC or manual Gain\n        if ((soundAgc==0) && (disableSoundProcessing == false) && !(audioSyncEnabled & 0x02)) {\n          infoArr = user.createNestedArray(F(\"Manual Gain\"));\n          float myGain = ((float)sampleGain/40.0f * (float)inputLevel/128.0f) + 1.0f/16.0f;     // non-AGC gain from presets\n          infoArr.add(roundf(myGain*100.0f) / 100.0f);\n          infoArr.add(\"x\");\n        }\n        if (soundAgc && (disableSoundProcessing == false) && !(audioSyncEnabled & 0x02)) {\n          infoArr = user.createNestedArray(F(\"AGC Gain\"));\n          infoArr.add(roundf(multAgc*100.0f) / 100.0f);\n          infoArr.add(\"x\");\n        }\n#endif\n        // UDP Sound Sync status\n        infoArr = user.createNestedArray(F(\"UDP Sound Sync\"));\n        if (audioSyncEnabled) {\n          if (audioSyncEnabled & 0x01) {\n            infoArr.add(F(\"send mode\"));\n            if ((udpSyncConnected) && (millis() - lastTime < 2500)) infoArr.add(F(\" v2\"));\n          } else if (audioSyncEnabled & 0x02) {\n              infoArr.add(F(\"receive mode\"));\n          }\n        } else\n          infoArr.add(\"off\");\n        if (audioSyncEnabled && !udpSyncConnected) infoArr.add(\" <i>(unconnected)</i>\");\n        if (audioSyncEnabled && udpSyncConnected && (millis() - last_UDPTime < 2500)) {\n            if (receivedFormat == 1) infoArr.add(F(\" v1\"));\n            if (receivedFormat == 2) infoArr.add(F(\" v2\"));\n        }\n\n        #if defined(WLED_DEBUG) || defined(SR_DEBUG)\n        #ifdef ARDUINO_ARCH_ESP32\n        infoArr = user.createNestedArray(F(\"Sampling time\"));\n        infoArr.add(float(sampleTime)/100.0f);\n        infoArr.add(\" ms\");\n\n        infoArr = user.createNestedArray(F(\"FFT time\"));\n        infoArr.add(float(fftTime)/100.0f);\n        if ((fftTime/100) >= FFT_MIN_CYCLE) // FFT time over budget -> I2S buffer will overflow \n          infoArr.add(\"<b style=\\\"color:red;\\\">! ms</b>\");\n        else if ((fftTime/80 + sampleTime/80) >= FFT_MIN_CYCLE) // FFT time >75% of budget -> risk of instability\n          infoArr.add(\"<b style=\\\"color:orange;\\\"> ms!</b>\");\n        else\n          infoArr.add(\" ms\");\n\n        DEBUGSR_PRINTF(\"AR Sampling time: %5.2f ms\\n\", float(sampleTime)/100.0f);\n        DEBUGSR_PRINTF(\"AR FFT time     : %5.2f ms\\n\", float(fftTime)/100.0f);\n        #endif\n        #endif\n      }\n    }\n\n\n    /*\n     * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).\n     * Values in the state object may be modified by connected clients\n     */\n    void addToJsonState(JsonObject& root) override\n    {\n      if (!initDone) return;  // prevent crash on boot applyPreset()\n      JsonObject usermod = root[FPSTR(_name)];\n      if (usermod.isNull()) {\n        usermod = root.createNestedObject(FPSTR(_name));\n      }\n      usermod[\"on\"] = enabled;\n    }\n\n\n    /*\n     * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).\n     * Values in the state object may be modified by connected clients\n     */\n    void readFromJsonState(JsonObject& root) override\n    {\n      if (!initDone) return;  // prevent crash on boot applyPreset()\n      bool prevEnabled = enabled;\n      JsonObject usermod = root[FPSTR(_name)];\n      if (!usermod.isNull()) {\n        if (usermod[FPSTR(_enabled)].is<bool>()) {\n          enabled = usermod[FPSTR(_enabled)].as<bool>();\n          if (prevEnabled != enabled) onUpdateBegin(!enabled);\n          if (addPalettes) {\n            // add/remove custom/audioreactive palettes\n            if (prevEnabled && !enabled) removeAudioPalettes();\n            if (!prevEnabled && enabled) createAudioPalettes();\n          }\n        }\n#ifdef ARDUINO_ARCH_ESP32\n        if (usermod[FPSTR(_inputLvl)].is<int>()) {\n          inputLevel = min(255,max(0,usermod[FPSTR(_inputLvl)].as<int>()));\n        }\n#endif\n      }\n      if (palettes > 0 && root.containsKey(F(\"rmcpal\"))) {\n        // handle removal of custom palettes from JSON call so we don't break things\n        removeAudioPalettes();\n      }\n    }\n\n    void onStateChange(uint8_t callMode) override {\n      if (initDone && enabled && addPalettes && palettes==0 && customPalettes.size()<WLED_MAX_CUSTOM_PALETTES) {\n        // if palettes were removed during JSON call re-add them\n        createAudioPalettes();\n      }\n    }\n\n    /*\n     * addToConfig() can be used to add custom persistent settings to the cfg.json file in the \"um\" (usermod) object.\n     * It will be called by WLED when settings are actually saved (for example, LED settings are saved)\n     * If you want to force saving the current state, use serializeConfig() in your loop().\n     * \n     * CAUTION: serializeConfig() will initiate a filesystem write operation.\n     * It might cause the LEDs to stutter and will cause flash wear if called too often.\n     * Use it sparingly and always in the loop, never in network callbacks!\n     * \n     * addToConfig() will make your settings editable through the Usermod Settings page automatically.\n     *\n     * Usermod Settings Overview:\n     * - Numeric values are treated as floats in the browser.\n     *   - If the numeric value entered into the browser contains a decimal point, it will be parsed as a C float\n     *     before being returned to the Usermod.  The float data type has only 6-7 decimal digits of precision, and\n     *     doubles are not supported, numbers will be rounded to the nearest float value when being parsed.\n     *     The range accepted by the input field is +/- 1.175494351e-38 to +/- 3.402823466e+38.\n     *   - If the numeric value entered into the browser doesn't contain a decimal point, it will be parsed as a\n     *     C int32_t (range: -2147483648 to 2147483647) before being returned to the usermod.\n     *     Overflows or underflows are truncated to the max/min value for an int32_t, and again truncated to the type\n     *     used in the Usermod when reading the value from ArduinoJson.\n     * - Pin values can be treated differently from an integer value by using the key name \"pin\"\n     *   - \"pin\" can contain a single or array of integer values\n     *   - On the Usermod Settings page there is simple checking for pin conflicts and warnings for special pins\n     *     - Red color indicates a conflict.  Yellow color indicates a pin with a warning (e.g. an input-only pin)\n     *   - Tip: use int8_t to store the pin value in the Usermod, so a -1 value (pin not set) can be used\n     *\n     * See usermod_v2_auto_save.h for an example that saves Flash space by reusing ArduinoJson key name strings\n     * \n     * If you need a dedicated settings page with custom layout for your Usermod, that takes a lot more work.  \n     * You will have to add the setting to the HTML, xml.cpp and set.cpp manually.\n     * See the WLED Soundreactive fork (code and wiki) for reference.  https://github.com/atuline/WLED\n     * \n     * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings!\n     */\n    void addToConfig(JsonObject& root) override\n    {\n      JsonObject top = root.createNestedObject(FPSTR(_name));\n      top[FPSTR(_enabled)] = enabled;\n      top[FPSTR(_addPalettes)] = addPalettes;\n\n#ifdef ARDUINO_ARCH_ESP32\n    #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)\n      JsonObject amic = top.createNestedObject(FPSTR(_analogmic));\n      amic[\"pin\"] = audioPin;\n    #endif\n\n      JsonObject dmic = top.createNestedObject(FPSTR(_digitalmic));\n      dmic[\"type\"] = dmType;\n      JsonArray pinArray = dmic.createNestedArray(\"pin\");\n      pinArray.add(i2ssdPin);\n      pinArray.add(i2swsPin);\n      pinArray.add(i2sckPin);\n      pinArray.add(mclkPin);\n\n      JsonObject cfg = top.createNestedObject(FPSTR(_config));\n      cfg[F(\"squelch\")] = soundSquelch;\n      cfg[F(\"gain\")] = sampleGain;\n      cfg[F(\"AGC\")] = soundAgc;\n\n      JsonObject freqScale = top.createNestedObject(FPSTR(_frequency));\n      freqScale[F(\"scale\")] = FFTScalingMode;\n#endif\n\n      JsonObject dynLim = top.createNestedObject(FPSTR(_dynamics));\n      dynLim[F(\"limiter\")] = limiterOn;\n      dynLim[F(\"rise\")] = attackTime;\n      dynLim[F(\"fall\")] = decayTime;\n\n      JsonObject sync = top.createNestedObject(\"sync\");\n      sync[\"port\"] = audioSyncPort;\n      sync[\"mode\"] = audioSyncEnabled;\n    }\n\n\n    /*\n     * readFromConfig() can be used to read back the custom settings you added with addToConfig().\n     * This is called by WLED when settings are loaded (currently this only happens immediately after boot, or after saving on the Usermod Settings page)\n     * \n     * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes),\n     * 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.\n     * If you don't know what that is, don't fret. It most likely doesn't affect your use case :)\n     * \n     * Return true in case the config values returned from Usermod Settings were complete, or false if you'd like WLED to save your defaults to disk (so any missing values are editable in Usermod Settings)\n     * \n     * getJsonValue() returns false if the value is missing, or copies the value into the variable provided and returns true if the value is present\n     * The configComplete variable is true only if the \"exampleUsermod\" object and all values are present.  If any values are missing, WLED will know to call addToConfig() to save them\n     * \n     * This function is guaranteed to be called on boot, but could also be called every time settings are updated\n     */\n    bool readFromConfig(JsonObject& root) override\n    {\n      JsonObject top = root[FPSTR(_name)];\n      bool configComplete = !top.isNull();\n      bool oldEnabled = enabled;\n      bool oldAddPalettes = addPalettes;\n\n      configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled);\n      configComplete &= getJsonValue(top[FPSTR(_addPalettes)], addPalettes);\n\n#ifdef ARDUINO_ARCH_ESP32\n    #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)\n      configComplete &= getJsonValue(top[FPSTR(_analogmic)][\"pin\"], audioPin);\n    #else\n      audioPin = -1; // MCU does not support analog mic\n    #endif\n\n      configComplete &= getJsonValue(top[FPSTR(_digitalmic)][\"type\"],   dmType);\n    #if  defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3)\n      if (dmType == 0) dmType = SR_DMTYPE;   // MCU does not support analog\n      #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3)\n      if (dmType == 5) dmType = SR_DMTYPE;   // MCU does not support PDM\n      #endif\n    #endif\n\n      configComplete &= getJsonValue(top[FPSTR(_digitalmic)][\"pin\"][0], i2ssdPin);\n      configComplete &= getJsonValue(top[FPSTR(_digitalmic)][\"pin\"][1], i2swsPin);\n      configComplete &= getJsonValue(top[FPSTR(_digitalmic)][\"pin\"][2], i2sckPin);\n      configComplete &= getJsonValue(top[FPSTR(_digitalmic)][\"pin\"][3], mclkPin);\n\n      configComplete &= getJsonValue(top[FPSTR(_config)][F(\"squelch\")], soundSquelch);\n      configComplete &= getJsonValue(top[FPSTR(_config)][F(\"gain\")],    sampleGain);\n      configComplete &= getJsonValue(top[FPSTR(_config)][F(\"AGC\")],     soundAgc);\n\n      configComplete &= getJsonValue(top[FPSTR(_frequency)][F(\"scale\")], FFTScalingMode);\n\n      configComplete &= getJsonValue(top[FPSTR(_dynamics)][F(\"limiter\")], limiterOn);\n      configComplete &= getJsonValue(top[FPSTR(_dynamics)][F(\"rise\")],  attackTime);\n      configComplete &= getJsonValue(top[FPSTR(_dynamics)][F(\"fall\")],  decayTime);\n#endif\n      configComplete &= getJsonValue(top[\"sync\"][\"port\"], audioSyncPort);\n      configComplete &= getJsonValue(top[\"sync\"][\"mode\"], audioSyncEnabled);\n\n      if (initDone) {\n        // add/remove custom/audioreactive palettes\n        if ((oldAddPalettes && !addPalettes) || (oldAddPalettes && !enabled)) removeAudioPalettes();\n        if ((addPalettes && !oldAddPalettes && enabled) || (addPalettes && !oldEnabled && enabled)) createAudioPalettes();\n      } // else setup() will create palettes\n      return configComplete;\n    }\n\n\n    void appendConfigData(Print& uiScript) override\n    {\n      uiScript.print(F(\"ux='AudioReactive';\"));         // ux = shortcut for Audioreactive - fingers crossed that \"ux\" isn't already used as JS var, html post parameter or css style\n#ifdef ARDUINO_ARCH_ESP32\n      uiScript.print(F(\"uxp=ux+':digitalmic:pin[]';\")); // uxp = shortcut for AudioReactive:digitalmic:pin[]\n      uiScript.print(F(\"dd=addDropdown(ux,'digitalmic:type');\"));\n    #if  !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)\n      uiScript.print(F(\"addOption(dd,'Generic Analog',0);\"));\n    #endif\n      uiScript.print(F(\"addOption(dd,'Generic I2S',1);\"));\n      uiScript.print(F(\"addOption(dd,'ES7243',2);\"));\n      uiScript.print(F(\"addOption(dd,'SPH0654',3);\"));\n      uiScript.print(F(\"addOption(dd,'Generic I2S with Mclk',4);\"));\n    #if  !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3)\n      uiScript.print(F(\"addOption(dd,'Generic PDM',5);\"));\n    #endif\n    uiScript.print(F(\"addOption(dd,'ES8388',6);\"));\n    \n      uiScript.print(F(\"dd=addDropdown(ux,'config:AGC');\"));\n      uiScript.print(F(\"addOption(dd,'Off',0);\"));\n      uiScript.print(F(\"addOption(dd,'Normal',1);\"));\n      uiScript.print(F(\"addOption(dd,'Vivid',2);\"));\n      uiScript.print(F(\"addOption(dd,'Lazy',3);\"));\n\n      uiScript.print(F(\"dd=addDropdown(ux,'dynamics:limiter');\"));\n      uiScript.print(F(\"addOption(dd,'Off',0);\"));\n      uiScript.print(F(\"addOption(dd,'On',1);\"));\n      uiScript.print(F(\"addInfo(ux+':dynamics:limiter',0,' On ');\"));  // 0 is field type, 1 is actual field\n      uiScript.print(F(\"addInfo(ux+':dynamics:rise',1,'ms <i>(&#x266A; effects only)</i>');\"));\n      uiScript.print(F(\"addInfo(ux+':dynamics:fall',1,'ms <i>(&#x266A; effects only)</i>');\"));\n\n      uiScript.print(F(\"dd=addDropdown(ux,'frequency:scale');\"));\n      uiScript.print(F(\"addOption(dd,'None',0);\"));\n      uiScript.print(F(\"addOption(dd,'Linear (Amplitude)',2);\"));\n      uiScript.print(F(\"addOption(dd,'Square Root (Energy)',3);\"));\n      uiScript.print(F(\"addOption(dd,'Logarithmic (Loudness)',1);\"));\n#endif\n\n      uiScript.print(F(\"dd=addDropdown(ux,'sync:mode');\"));\n      uiScript.print(F(\"addOption(dd,'Off',0);\"));\n#ifdef ARDUINO_ARCH_ESP32\n      uiScript.print(F(\"addOption(dd,'Send',1);\"));\n#endif\n      uiScript.print(F(\"addOption(dd,'Receive',2);\"));\n#ifdef ARDUINO_ARCH_ESP32\n      uiScript.print(F(\"addInfo(ux+':digitalmic:type',1,'<i>requires reboot!</i>');\"));  // 0 is field type, 1 is actual field\n      uiScript.print(F(\"addInfo(uxp,0,'<i>sd/data/dout</i>','I2S SD');\"));\n      uiScript.print(F(\"addInfo(uxp,1,'<i>ws/clk/lrck</i>','I2S WS');\"));\n      uiScript.print(F(\"addInfo(uxp,2,'<i>sck/bclk</i>','I2S SCK');\"));\n      #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)\n        uiScript.print(F(\"addInfo(uxp,3,'<i>only use -1, 0, 1 or 3</i>','I2S MCLK');\"));\n      #else\n        uiScript.print(F(\"addInfo(uxp,3,'<i>master clock</i>','I2S MCLK');\"));\n      #endif\n#endif\n    }\n\n\n    /*\n     * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors.\n     * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode.\n     * Commonly used for custom clocks (Cronixie, 7 segment)\n     */\n    //void handleOverlayDraw() override\n    //{\n      //strip.setPixelColor(0, RGBW32(0,0,0,0)) // set the first pixel to black\n    //}\n\n   \n    /*\n     * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).\n     * This could be used in the future for the system to determine whether your usermod is installed.\n     */\n    uint16_t getId() override\n    {\n      return USERMOD_ID_AUDIOREACTIVE;\n    }\n};\n\nvoid AudioReactive::removeAudioPalettes(void) {\n  DEBUG_PRINTLN(F(\"Removing audio palettes.\"));\n  while (palettes>0) {\n    customPalettes.pop_back();\n    DEBUG_PRINTLN(palettes);\n    palettes--;\n  }\n  DEBUG_PRINT(F(\"Total # of palettes: \")); DEBUG_PRINTLN(customPalettes.size());\n}\n\nvoid AudioReactive::createAudioPalettes(void) {\n  DEBUG_PRINT(F(\"Total # of palettes: \")); DEBUG_PRINTLN(customPalettes.size());\n  if (palettes) return;\n  DEBUG_PRINTLN(F(\"Adding audio palettes.\"));\n  for (int i=0; i<MAX_PALETTES; i++)\n    if (customPalettes.size() < WLED_MAX_CUSTOM_PALETTES) {\n      customPalettes.push_back(CRGBPalette16(CRGB(BLACK)));\n      palettes++;\n      DEBUG_PRINTLN(palettes);\n    } else break;\n}\n\n// credit @netmindz ar palette, adapted for usermod @blazoncek\nCRGB AudioReactive::getCRGBForBand(int x, int pal) {\n  CRGB value;\n  CHSV hsv;\n  int b;\n  switch (pal) {\n    case 2:\n      b = map(x, 0, 255, 0, NUM_GEQ_CHANNELS/2); // convert palette position to lower half of freq band\n      hsv = CHSV(fftResult[b], 255, x);\n      hsv2rgb_rainbow(hsv, value);  // convert to R,G,B\n      break;\n    case 1:\n      b = map(x, 1, 255, 0, 10); // convert palette position to lower half of freq band\n      hsv = CHSV(fftResult[b], 255, map(fftResult[b], 0, 255, 30, 255));  // pick hue\n      hsv2rgb_rainbow(hsv, value);  // convert to R,G,B\n      break;\n    default:\n      if (x == 1) {\n        value = CRGB(fftResult[10]/2, fftResult[4]/2, fftResult[0]/2);\n      } else if(x == 255) {\n        value = CRGB(fftResult[10]/2, fftResult[0]/2, fftResult[4]/2);\n      } else {\n        value = CRGB(fftResult[0]/2, fftResult[4]/2, fftResult[10]/2);\n      }\n      break;\n  }\n  return value;\n}\n\nvoid AudioReactive::fillAudioPalettes() {\n  if (!palettes) return;\n  size_t lastCustPalette = customPalettes.size();\n  if (int(lastCustPalette) >= palettes) lastCustPalette -= palettes;\n  for (int pal=0; pal<palettes; pal++) {\n    uint8_t tcp[16];  // Needs to be 4 times however many colors are being used.\n                      // 3 colors = 12, 4 colors = 16, etc.\n\n    tcp[0] = 0;  // anchor of first color - must be zero\n    tcp[1] = 0;\n    tcp[2] = 0;\n    tcp[3] = 0;\n    \n    CRGB rgb = getCRGBForBand(1, pal);\n    tcp[4] = 1;  // anchor of first color\n    tcp[5] = rgb.r;\n    tcp[6] = rgb.g;\n    tcp[7] = rgb.b;\n    \n    rgb = getCRGBForBand(128, pal);\n    tcp[8] = 128;\n    tcp[9] = rgb.r;\n    tcp[10] = rgb.g;\n    tcp[11] = rgb.b;\n    \n    rgb = getCRGBForBand(255, pal);\n    tcp[12] = 255;  // anchor of last color - must be 255\n    tcp[13] = rgb.r;\n    tcp[14] = rgb.g;\n    tcp[15] = rgb.b;\n\n    customPalettes[lastCustPalette+pal].loadDynamicGradientPalette(tcp);\n  }\n}\n\n// strings to reduce flash memory usage (used more than twice)\nconst char AudioReactive::_name[]       PROGMEM = \"AudioReactive\";\nconst char AudioReactive::_enabled[]    PROGMEM = \"enabled\";\nconst char AudioReactive::_config[]     PROGMEM = \"config\";\nconst char AudioReactive::_dynamics[]   PROGMEM = \"dynamics\";\nconst char AudioReactive::_frequency[]  PROGMEM = \"frequency\";\nconst char AudioReactive::_inputLvl[]   PROGMEM = \"inputLevel\";\n#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)\nconst char AudioReactive::_analogmic[]  PROGMEM = \"analogmic\";\n#endif\nconst char AudioReactive::_digitalmic[] PROGMEM = \"digitalmic\";\nconst char AudioReactive::_addPalettes[]       PROGMEM = \"add-palettes\";\nconst char AudioReactive::UDP_SYNC_HEADER[]    PROGMEM = \"00002\"; // new sync header version, as format no longer compatible with previous structure\nconst char AudioReactive::UDP_SYNC_HEADER_v1[] PROGMEM = \"00001\"; // old sync header version - need to add backwards-compatibility feature\n\nstatic AudioReactive ar_module;\nREGISTER_USERMOD(ar_module);\n"
  },
  {
    "path": "usermods/audioreactive/audio_source.h",
    "content": "#pragma once\n#ifdef ARDUINO_ARCH_ESP32\n#include \"wled.h\"\n#include <driver/i2s.h>\n#include <driver/adc.h>\n#include <soc/i2s_reg.h>  // needed for SPH0465 timing workaround (classic ESP32)\n#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)\n#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32C3)\n#include <driver/adc_deprecated.h>\n#include <driver/adc_types_deprecated.h>\n#endif\n// type of i2s_config_t.SampleRate was changed from \"int\" to \"unsigned\" in IDF 4.4.x\n#define SRate_t uint32_t\n#else\n#define SRate_t int\n#endif\n\n//#include <driver/i2s_std.h>\n//#include <driver/i2s_pdm.h>\n//#include <driver/i2s_tdm.h>\n//#include <driver/gpio.h>\n\n// see https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/hw-reference/chip-series-comparison.html#related-documents\n// and https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/i2s.html#overview-of-all-modes\n#if defined(CONFIG_IDF_TARGET_ESP32C2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2) || defined(ESP8266) || defined(ESP8265)\n  // there are two things in these MCUs that could lead to problems with audio processing:\n  // * no floating point hardware (FPU) support - FFT uses float calculations. If done in software, a strong slow-down can be expected (between 8x and 20x)\n  // * single core, so FFT task might slow down other things like LED updates\n  #if !defined(SOC_I2S_NUM) || (SOC_I2S_NUM < 1)\n  #error This audio reactive usermod does not support ESP32-C2 or ESP32-C3.\n  #else\n  #warning This audio reactive usermod does not support ESP32-C2 and ESP32-C3.\n  #endif\n#endif\n\n/* ToDo: remove. ES7243 is controlled via compiler defines\n   Until this configuration is moved to the webinterface\n*/\n\n// if you have problems to get your microphone work on the left channel, uncomment the following line\n//#define I2S_USE_RIGHT_CHANNEL    // (experimental) define this to use right channel (digital mics only)\n\n// Uncomment the line below to utilize ADC1 _exclusively_ for I2S sound input.\n// benefit: analog mic inputs will be sampled contiously -> better response times and less \"glitches\"\n// WARNING: this option WILL lock-up your device in case that any other analogRead() operation is performed; \n//          for example if you want to read \"analog buttons\"\n//#define I2S_GRAB_ADC1_COMPLETELY // (experimental) continuously sample analog ADC microphone. WARNING will cause analogRead() lock-up\n\n// data type requested from the I2S driver - currently we always use 32bit\n//#define I2S_USE_16BIT_SAMPLES   // (experimental) define this to request 16bit - more efficient but possibly less compatible\n\n#ifdef I2S_USE_16BIT_SAMPLES\n#define I2S_SAMPLE_RESOLUTION I2S_BITS_PER_SAMPLE_16BIT\n#define I2S_datatype int16_t\n#define I2S_unsigned_datatype uint16_t\n#define I2S_data_size I2S_BITS_PER_CHAN_16BIT\n#undef  I2S_SAMPLE_DOWNSCALE_TO_16BIT\n#else\n#define I2S_SAMPLE_RESOLUTION I2S_BITS_PER_SAMPLE_32BIT\n//#define I2S_SAMPLE_RESOLUTION I2S_BITS_PER_SAMPLE_24BIT \n#define I2S_datatype int32_t\n#define I2S_unsigned_datatype uint32_t\n#define I2S_data_size I2S_BITS_PER_CHAN_32BIT\n#define I2S_SAMPLE_DOWNSCALE_TO_16BIT\n#endif\n\n/* There are several (confusing) options  in IDF 4.4.x:\n * I2S_CHANNEL_FMT_RIGHT_LEFT, I2S_CHANNEL_FMT_ALL_RIGHT and I2S_CHANNEL_FMT_ALL_LEFT stands for stereo mode, which means two channels will transport different data.\n * I2S_CHANNEL_FMT_ONLY_RIGHT and I2S_CHANNEL_FMT_ONLY_LEFT they are mono mode, both channels will only transport same data.\n * I2S_CHANNEL_FMT_MULTIPLE means TDM channels, up to 16 channel will available, and they are stereo as default.\n * if you want to receive two channels, one is the actual data from microphone and another channel is suppose to receive 0, it's different data in two channels, you need to choose I2S_CHANNEL_FMT_RIGHT_LEFT in this case.\n*/\n\n#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 5, 0))\n// espressif bug: only_left has no sound, left and right are swapped \n// https://github.com/espressif/esp-idf/issues/9635  I2S mic not working since 4.4 (IDFGH-8138)\n// https://github.com/espressif/esp-idf/issues/8538  I2S channel selection issue? (IDFGH-6918)\n// https://github.com/espressif/esp-idf/issues/6625  I2S: left/right channels are swapped for read (IDFGH-4826)\n#ifdef I2S_USE_RIGHT_CHANNEL\n#define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_LEFT\n#define I2S_MIC_CHANNEL_TEXT \"right channel only (work-around swapped channel bug in IDF 4.4).\"\n#define I2S_PDM_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_RIGHT\n#define I2S_PDM_MIC_CHANNEL_TEXT \"right channel only\"\n#else\n//#define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ALL_LEFT\n//#define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_RIGHT_LEFT\n#define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_RIGHT\n#define I2S_MIC_CHANNEL_TEXT \"left channel only (work-around swapped channel bug in IDF 4.4).\"\n#define I2S_PDM_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_LEFT\n#define I2S_PDM_MIC_CHANNEL_TEXT \"left channel only.\"\n#endif\n\n#else\n// not swapped\n#ifdef I2S_USE_RIGHT_CHANNEL\n#define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_RIGHT\n#define I2S_MIC_CHANNEL_TEXT \"right channel only.\"\n#else\n#define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_LEFT\n#define I2S_MIC_CHANNEL_TEXT \"left channel only.\"\n#endif\n#define I2S_PDM_MIC_CHANNEL I2S_MIC_CHANNEL\n#define I2S_PDM_MIC_CHANNEL_TEXT I2S_MIC_CHANNEL_TEXT\n\n#endif\n\n\n/* Interface class\n   AudioSource serves as base class for all microphone types\n   This enables accessing all microphones with one single interface\n   which simplifies the caller code\n*/\nclass AudioSource {\n  public:\n    /* All public methods are virtual, so they can be overridden\n       Everything but the destructor is also removed, to make sure each mic\n       Implementation provides its version of this function\n    */\n    virtual ~AudioSource() {};\n\n    /* Initialize\n       This function needs to take care of anything that needs to be done\n       before samples can be obtained from the microphone.\n    */\n    virtual void initialize(int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE) = 0;\n\n    /* Deinitialize\n       Release all resources and deactivate any functionality that is used\n       by this microphone\n    */\n    virtual void deinitialize() = 0;\n\n    /* getSamples\n       Read num_samples from the microphone, and store them in the provided\n       buffer\n    */\n    virtual void getSamples(float *buffer, uint16_t num_samples) = 0;\n\n    /* check if the audio source driver was initialized successfully */\n    virtual bool isInitialized(void) {return(_initialized);}\n\n    /* identify Audiosource type - I2S-ADC or I2S-digital */\n    typedef enum{Type_unknown=0, Type_I2SAdc=1, Type_I2SDigital=2} AudioSourceType;\n    virtual AudioSourceType getType(void) {return(Type_I2SDigital);}               // default is \"I2S digital source\" - ADC type overrides this method\n \n  protected:\n    /* Post-process audio sample - currently on needed for I2SAdcSource*/\n    virtual I2S_datatype postProcessSample(I2S_datatype sample_in) {return(sample_in);}   // default method can be overriden by instances (ADC) that need sample postprocessing\n\n    // Private constructor, to make sure it is not callable except from derived classes\n    AudioSource(SRate_t sampleRate, int blockSize, float sampleScale) :\n      _sampleRate(sampleRate),\n      _blockSize(blockSize),\n      _initialized(false),\n      _sampleScale(sampleScale)\n    {};\n\n    SRate_t _sampleRate;            // Microphone sampling rate\n    int _blockSize;                 // I2S block size\n    bool _initialized;              // Gets set to true if initialization is successful\n    float _sampleScale;             // pre-scaling factor for I2S samples\n};\n\n/* Basic I2S microphone source\n   All functions are marked virtual, so derived classes can replace them\n*/\nclass I2SSource : public AudioSource {\n  public:\n    I2SSource(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) :\n      AudioSource(sampleRate, blockSize, sampleScale) {\n      _config = {\n        .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX),\n        .sample_rate = _sampleRate,\n        .bits_per_sample = I2S_SAMPLE_RESOLUTION,\n        .channel_format = I2S_MIC_CHANNEL,\n#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0)\n        .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_STAND_I2S),\n        //.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,\n        .intr_alloc_flags = ESP_INTR_FLAG_LEVEL2,\n        .dma_buf_count = 8,\n        .dma_buf_len = _blockSize,\n        .use_apll = 0,\n        .bits_per_chan = I2S_data_size,\n#else\n        .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB),\n        .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,\n        .dma_buf_count = 8,\n        .dma_buf_len = _blockSize,\n        .use_apll = false\n#endif\n      };\n    }\n\n    virtual void initialize(int8_t i2swsPin = I2S_PIN_NO_CHANGE, int8_t i2ssdPin = I2S_PIN_NO_CHANGE, int8_t i2sckPin = I2S_PIN_NO_CHANGE, int8_t mclkPin = I2S_PIN_NO_CHANGE) {\n      DEBUGSR_PRINTLN(F(\"I2SSource:: initialize().\"));\n      if (i2swsPin != I2S_PIN_NO_CHANGE && i2ssdPin != I2S_PIN_NO_CHANGE) {\n        if (!PinManager::allocatePin(i2swsPin, true, PinOwner::UM_Audioreactive) ||\n            !PinManager::allocatePin(i2ssdPin, false, PinOwner::UM_Audioreactive)) { // #206\n          DEBUGSR_PRINTF(\"\\nAR: Failed to allocate I2S pins: ws=%d, sd=%d\\n\",  i2swsPin, i2ssdPin); \n          return;\n        }\n      }\n\n      // i2ssckPin needs special treatment, since it might be unused on PDM mics\n      if (i2sckPin != I2S_PIN_NO_CHANGE) {\n        if (!PinManager::allocatePin(i2sckPin, true, PinOwner::UM_Audioreactive)) {\n          DEBUGSR_PRINTF(\"\\nAR: Failed to allocate I2S pins: sck=%d\\n\",  i2sckPin); \n          return;\n        }\n      } else {\n        #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0)\n          #if !defined(SOC_I2S_SUPPORTS_PDM_RX)\n          #warning this MCU does not support PDM microphones\n          #endif\n        #endif\n        #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3)\n        // This is an I2S PDM microphone, these microphones only use a clock and\n        // data line, to make it simpler to debug, use the WS pin as CLK and SD pin as DATA\n        // example from espressif: https://github.com/espressif/esp-idf/blob/release/v4.4/examples/peripherals/i2s/i2s_audio_recorder_sdcard/main/i2s_recorder_main.c\n\n        // note to self: PDM has known bugs on S3, and does not work on C3 \n        //  * S3: PDM sample rate only at 50% of expected rate: https://github.com/espressif/esp-idf/issues/9893\n        //  * S3: I2S PDM has very low amplitude: https://github.com/espressif/esp-idf/issues/8660\n        //  * C3: does not support PDM to PCM input. SoC would allow PDM RX, but there is no hardware to directly convert to PCM so it will not work. https://github.com/espressif/esp-idf/issues/8796\n\n        _config.mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM); // Change mode to pdm if clock pin not provided. PDM is not supported on ESP32-S2. PDM RX not supported on ESP32-C3\n        _config.channel_format =I2S_PDM_MIC_CHANNEL;                             // seems that PDM mono mode always uses left channel.\n        _config.use_apll = false;                                                // don't use aPLL clock source (fix for #5391)\n        #endif\n      }\n\n#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0)\n      if (mclkPin != I2S_PIN_NO_CHANGE) {\n        #if !defined(WLED_USE_ETHERNET)  // fix for #5391 aPLL resource conflict - aPLL is needed for ethernet boards with internal RMII clock\n        _config.use_apll = true; // experimental - use aPLL clock source to improve sampling quality, and to avoid glitches.\n        // //_config.fixed_mclk = 512 * _sampleRate;\n        // //_config.fixed_mclk = 256 * _sampleRate;\n        #endif\n      }\n      \n      #if !defined(SOC_I2S_SUPPORTS_APLL)\n        #warning this MCU does not have an APLL high accuracy clock for audio\n        // S3: not supported; S2: supported; C3: not supported\n        _config.use_apll = false; // APLL not supported on this MCU\n      #endif\n      #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3)\n      if (ESP.getChipRevision() == 0) _config.use_apll = false; // APLL is broken on ESP32 revision 0\n      #endif\n#endif\n\n      // Reserve the master clock pin if provided\n      _mclkPin = mclkPin;\n      if (mclkPin != I2S_PIN_NO_CHANGE) {\n        if(!PinManager::allocatePin(mclkPin, true, PinOwner::UM_Audioreactive)) { \n          DEBUGSR_PRINTF(\"\\nAR: Failed to allocate I2S pin: MCLK=%d\\n\",  mclkPin); \n          return;\n        } else\n        _routeMclk(mclkPin);\n      }\n\n      _pinConfig = {\n#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)\n        .mck_io_num = mclkPin,            // \"classic\" ESP32 supports setting MCK on GPIO0/GPIO1/GPIO3 only. i2s_set_pin() will fail if wrong mck_io_num is provided.\n#endif\n        .bck_io_num = i2sckPin,\n        .ws_io_num = i2swsPin,\n        .data_out_num = I2S_PIN_NO_CHANGE,\n        .data_in_num = i2ssdPin\n      };\n\n      //DEBUGSR_PRINTF(\"[AR] I2S: SD=%d, WS=%d, SCK=%d, MCLK=%d\\n\", i2ssdPin, i2swsPin, i2sckPin, mclkPin);\n\n      esp_err_t err = i2s_driver_install(I2S_NUM_0, &_config, 0, nullptr);\n      if (err != ESP_OK) {\n        DEBUGSR_PRINTF(\"AR: Failed to install i2s driver: %d\\n\", err);\n        return;\n      }\n\n      DEBUGSR_PRINTF(\"AR: I2S#0 driver %s aPLL; fixed_mclk=%d.\\n\", _config.use_apll? \"uses\":\"without\", _config.fixed_mclk);\n      DEBUGSR_PRINTF(\"AR: %d bits, Sample scaling factor = %6.4f\\n\",  _config.bits_per_sample, _sampleScale);\n      if (_config.mode & I2S_MODE_PDM) {\n          DEBUGSR_PRINTLN(F(\"AR: I2S#0 driver installed in PDM MASTER mode.\"));\n      } else { \n          DEBUGSR_PRINTLN(F(\"AR: I2S#0 driver installed in MASTER mode.\"));\n      }\n\n      err = i2s_set_pin(I2S_NUM_0, &_pinConfig);\n      if (err != ESP_OK) {\n        DEBUGSR_PRINTF(\"AR: Failed to set i2s pin config: %d\\n\", err);\n        i2s_driver_uninstall(I2S_NUM_0);  // uninstall already-installed driver\n        return;\n      }\n\n#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0)\n      err = i2s_set_clk(I2S_NUM_0, _sampleRate, I2S_SAMPLE_RESOLUTION, I2S_CHANNEL_MONO);  // set bit clocks. Also takes care of MCLK routing if needed.\n      if (err != ESP_OK) {\n        DEBUGSR_PRINTF(\"AR: Failed to configure i2s clocks: %d\\n\", err);\n        i2s_driver_uninstall(I2S_NUM_0);  // uninstall already-installed driver\n        return;\n      }\n#endif\n      _initialized = true;\n    }\n\n    virtual void deinitialize() {\n      _initialized = false;\n      esp_err_t err = i2s_driver_uninstall(I2S_NUM_0);\n      if (err != ESP_OK) {\n        DEBUGSR_PRINTF(\"Failed to uninstall i2s driver: %d\\n\", err);\n        return;\n      }\n      if (_pinConfig.ws_io_num   != I2S_PIN_NO_CHANGE) PinManager::deallocatePin(_pinConfig.ws_io_num,   PinOwner::UM_Audioreactive);\n      if (_pinConfig.data_in_num != I2S_PIN_NO_CHANGE) PinManager::deallocatePin(_pinConfig.data_in_num, PinOwner::UM_Audioreactive);\n      if (_pinConfig.bck_io_num  != I2S_PIN_NO_CHANGE) PinManager::deallocatePin(_pinConfig.bck_io_num,  PinOwner::UM_Audioreactive);\n      // Release the master clock pin\n      if (_mclkPin != I2S_PIN_NO_CHANGE) PinManager::deallocatePin(_mclkPin, PinOwner::UM_Audioreactive);\n    }\n\n    virtual void getSamples(float *buffer, uint16_t num_samples) {\n      if (_initialized) {\n        esp_err_t err;\n        size_t bytes_read = 0;        /* Counter variable to check if we actually got enough data */\n        I2S_datatype newSamples[num_samples]; /* Intermediary sample storage */\n\n        err = i2s_read(I2S_NUM_0, (void *)newSamples, sizeof(newSamples), &bytes_read, portMAX_DELAY);\n        if (err != ESP_OK) {\n          DEBUGSR_PRINTF(\"Failed to get samples: %d\\n\", err);\n          return;\n        }\n\n        // For correct operation, we need to read exactly sizeof(samples) bytes from i2s\n        if (bytes_read != sizeof(newSamples)) {\n          DEBUGSR_PRINTF(\"Failed to get enough samples: wanted: %d read: %d\\n\", sizeof(newSamples), bytes_read);\n          return;\n        }\n\n        // Store samples in sample buffer and update DC offset\n        for (int i = 0; i < num_samples; i++) {\n\n          newSamples[i] = postProcessSample(newSamples[i]);  // perform postprocessing (needed for ADC samples)\n          \n          float currSample = 0.0f;\n#ifdef I2S_SAMPLE_DOWNSCALE_TO_16BIT\n              currSample = (float) newSamples[i] / 65536.0f;      // 32bit input -> 16bit; keeping lower 16bits as decimal places\n#else\n              currSample = (float) newSamples[i];                 // 16bit input -> use as-is\n#endif\n          buffer[i] = currSample;\n          buffer[i] *= _sampleScale;                              // scale samples\n        }\n      }\n    }\n\n  protected:\n    void _routeMclk(int8_t mclkPin) {\n#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)\n  // MCLK routing by writing registers is not needed any more with IDF > 4.4.0\n  #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 4, 0)\n    // this way of MCLK routing only works on \"classic\" ESP32\n      /* Enable the mclk routing depending on the selected mclk pin (ESP32: only 0,1,3)\n          Only I2S_NUM_0 is supported\n      */\n      if (mclkPin == GPIO_NUM_0) {\n        PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO0_U, FUNC_GPIO0_CLK_OUT1);\n        WRITE_PERI_REG(PIN_CTRL,0xFFF0);\n      } else if (mclkPin == GPIO_NUM_1) {\n        PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_U0TXD_CLK_OUT3);\n        WRITE_PERI_REG(PIN_CTRL, 0xF0F0);\n      } else {\n        PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0RXD_U, FUNC_U0RXD_CLK_OUT2);\n        WRITE_PERI_REG(PIN_CTRL, 0xFF00);\n      }\n  #endif\n#endif\n    }\n\n    i2s_config_t _config;\n    i2s_pin_config_t _pinConfig;\n    int8_t _mclkPin;\n};\n\n/* ES7243 Microphone\n   This is an I2S microphone that requires initialization over\n   I2C before I2S data can be received\n*/\nclass ES7243 : public I2SSource {\n  private:\n\n    void _es7243I2cWrite(uint8_t reg, uint8_t val) {\n      #ifndef ES7243_ADDR\n        #define ES7243_ADDR 0x13   // default address\n      #endif\n      Wire.beginTransmission(ES7243_ADDR);\n      Wire.write((uint8_t)reg);\n      Wire.write((uint8_t)val);\n      uint8_t i2cErr = Wire.endTransmission();  // i2cErr == 0 means OK\n      if (i2cErr != 0) {\n        DEBUGSR_PRINTF(\"AR: ES7243 I2C write failed with error=%d  (addr=0x%X, reg 0x%X, val 0x%X).\\n\", i2cErr, ES7243_ADDR, reg, val);\n      }\n    }\n\n    void _es7243InitAdc() {\n      _es7243I2cWrite(0x00, 0x01);\n      _es7243I2cWrite(0x06, 0x00);\n      _es7243I2cWrite(0x05, 0x1B);\n      _es7243I2cWrite(0x01, 0x00); // 0x00 for 24 bit to match INMP441 - not sure if this needs adjustment to get 16bit samples from I2S\n      _es7243I2cWrite(0x08, 0x43);\n      _es7243I2cWrite(0x05, 0x13);\n    }\n\npublic:\n    ES7243(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) :\n      I2SSource(sampleRate, blockSize, sampleScale) {\n      _config.channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT;\n    };\n\n    void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) {\n      DEBUGSR_PRINTLN(F(\"ES7243:: initialize();\"));\n      if ((i2sckPin < 0) || (mclkPin < 0)) {\n        DEBUGSR_PRINTF(\"\\nAR: invalid I2S pin: SCK=%d, MCLK=%d\\n\", i2sckPin, mclkPin); \n        return;\n      }\n\n      // First route mclk, then configure ADC over I2C, then configure I2S\n      _es7243InitAdc();\n      I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin);\n    }\n\n    void deinitialize() {\n      I2SSource::deinitialize();\n    }\n};\n\n/* ES8388 Sound Module\n   This is an I2S sound processing unit that requires initialization over\n   I2C before I2S data can be received. \n*/\nclass ES8388Source : public I2SSource {\n  private:\n\n    void _es8388I2cWrite(uint8_t reg, uint8_t val) {\n#ifndef ES8388_ADDR\n      Wire.beginTransmission(0x10);\n      #define ES8388_ADDR 0x10   // default address\n#else\n      Wire.beginTransmission(ES8388_ADDR);\n#endif\n      Wire.write((uint8_t)reg);\n      Wire.write((uint8_t)val);\n      uint8_t i2cErr = Wire.endTransmission();  // i2cErr == 0 means OK\n      if (i2cErr != 0) {\n        DEBUGSR_PRINTF(\"AR: ES8388 I2C write failed with error=%d  (addr=0x%X, reg 0x%X, val 0x%X).\\n\", i2cErr, ES8388_ADDR, reg, val);\n      }\n    }\n\n    void _es8388InitAdc() {\n      // https://dl.radxa.com/rock2/docs/hw/ds/ES8388%20user%20Guide.pdf Section 10.1\n      // http://www.everest-semi.com/pdf/ES8388%20DS.pdf Better spec sheet, more clear. \n      // https://docs.google.com/spreadsheets/d/1CN3MvhkcPVESuxKyx1xRYqfUit5hOdsG45St9BCUm-g/edit#gid=0 generally\n      // Sets ADC to around what AudioReactive expects, and loops line-in to line-out/headphone for monitoring.\n      // Registries are decimal, settings are binary as that's how everything is listed in the docs\n      // ...which makes it easier to reference the docs.\n      //\n      _es8388I2cWrite( 8,0b00000000); // I2S to slave\n      _es8388I2cWrite( 2,0b11110011); // Power down DEM and STM\n      _es8388I2cWrite(43,0b10000000); // Set same LRCK\n      _es8388I2cWrite( 0,0b00000101); // Set chip to Play & Record Mode\n      _es8388I2cWrite(13,0b00000010); // Set MCLK/LRCK ratio to 256\n      _es8388I2cWrite( 1,0b01000000); // Power up analog and lbias\n      _es8388I2cWrite( 3,0b00000000); // Power up ADC, Analog Input, and Mic Bias\n      _es8388I2cWrite( 4,0b11111100); // Power down DAC, Turn on LOUT1 and ROUT1 and LOUT2 and ROUT2 power\n      _es8388I2cWrite( 2,0b01000000); // Power up DEM and STM and undocumented bit for \"turn on line-out amp\"\n\n      // #define use_es8388_mic\n\n    #ifdef use_es8388_mic\n      // The mics *and* line-in are BOTH connected to LIN2/RIN2 on the AudioKit\n      // so there's no way to completely eliminate the mics. It's also hella noisy. \n      // Line-in works OK on the AudioKit, generally speaking, as the mics really need\n      // amplification to be noticeable in a quiet room. If you're in a very loud room, \n      // the mics on the AudioKit WILL pick up sound even in line-in mode. \n      // TL;DR: Don't use the AudioKit for anything, use the LyraT. \n      //\n      // The LyraT does a reasonable job with mic input as configured below.\n\n      // Pick one of these. If you have to use the mics, use a LyraT over an AudioKit if you can:\n      _es8388I2cWrite(10,0b00000000); // Use Lin1/Rin1 for ADC input (mic on LyraT)\n      //_es8388I2cWrite(10,0b01010000); // Use Lin2/Rin2 for ADC input (mic *and* line-in on AudioKit)\n      \n      _es8388I2cWrite( 9,0b10001000); // Select Analog Input PGA Gain for ADC to +24dB (L+R)\n      _es8388I2cWrite(16,0b00000000); // Set ADC digital volume attenuation to 0dB (left)\n      _es8388I2cWrite(17,0b00000000); // Set ADC digital volume attenuation to 0dB (right)\n      _es8388I2cWrite(38,0b00011011); // Mixer - route LIN1/RIN1 to output after mic gain\n\n      _es8388I2cWrite(39,0b01000000); // Mixer - route LIN to mixL, +6dB gain\n      _es8388I2cWrite(42,0b01000000); // Mixer - route RIN to mixR, +6dB gain\n      _es8388I2cWrite(46,0b00100001); // LOUT1VOL - 0b00100001 = +4.5dB\n      _es8388I2cWrite(47,0b00100001); // ROUT1VOL - 0b00100001 = +4.5dB\n      _es8388I2cWrite(48,0b00100001); // LOUT2VOL - 0b00100001 = +4.5dB\n      _es8388I2cWrite(49,0b00100001); // ROUT2VOL - 0b00100001 = +4.5dB\n\n      // Music ALC - the mics like Auto Level Control\n      // You can also use this for line-in, but it's not really needed.\n      //\n      _es8388I2cWrite(18,0b11111000); // ALC: stereo, max gain +35.5dB, min gain -12dB \n      _es8388I2cWrite(19,0b00110000); // ALC: target -1.5dB, 0ms hold time\n      _es8388I2cWrite(20,0b10100110); // ALC: gain ramp up = 420ms/93ms, gain ramp down = check manual for calc\n      _es8388I2cWrite(21,0b00000110); // ALC: use \"ALC\" mode, no zero-cross, window 96 samples\n      _es8388I2cWrite(22,0b01011001); // ALC: noise gate threshold, PGA gain constant, noise gate enabled \n    #else\n      _es8388I2cWrite(10,0b01010000); // Use Lin2/Rin2 for ADC input (\"line-in\")\n      _es8388I2cWrite( 9,0b00000000); // Select Analog Input PGA Gain for ADC to 0dB (L+R)\n      _es8388I2cWrite(16,0b01000000); // Set ADC digital volume attenuation to -32dB (left)\n      _es8388I2cWrite(17,0b01000000); // Set ADC digital volume attenuation to -32dB (right)\n      _es8388I2cWrite(38,0b00001001); // Mixer - route LIN2/RIN2 to output\n\n      _es8388I2cWrite(39,0b01010000); // Mixer - route LIN to mixL, 0dB gain\n      _es8388I2cWrite(42,0b01010000); // Mixer - route RIN to mixR, 0dB gain\n      _es8388I2cWrite(46,0b00011011); // LOUT1VOL - 0b00011110 = +0dB, 0b00011011 = LyraT balance fix\n      _es8388I2cWrite(47,0b00011110); // ROUT1VOL - 0b00011110 = +0dB\n      _es8388I2cWrite(48,0b00011110); // LOUT2VOL - 0b00011110 = +0dB\n      _es8388I2cWrite(49,0b00011110); // ROUT2VOL - 0b00011110 = +0dB\n    #endif\n\n    }\n\n  public:\n    ES8388Source(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f, bool i2sMaster=true) :\n      I2SSource(sampleRate, blockSize, sampleScale) {\n      _config.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT;\n    };\n\n    void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t mclkPin) {\n      DEBUGSR_PRINTLN(F(\"ES8388Source:: initialize();\"));\n      if ((i2sckPin < 0) || (mclkPin < 0)) {\n        DEBUGSR_PRINTF(\"\\nAR: invalid I2S pin: SCK=%d, MCLK=%d\\n\", i2sckPin, mclkPin); \n        return;\n      }\n\n      // First route mclk, then configure ADC over I2C, then configure I2S\n      _es8388InitAdc();\n      I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin);\n    }\n\n    void deinitialize() {\n      I2SSource::deinitialize();\n    }\n\n};\n\n#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0)\n#if !defined(SOC_I2S_SUPPORTS_ADC) && !defined(SOC_I2S_SUPPORTS_ADC_DAC)\n  #warning this MCU does not support analog sound input\n#endif\n#endif\n\n#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)\n// ADC over I2S is only availeable in \"classic\" ESP32\n\n/* ADC over I2S Microphone\n   This microphone is an ADC pin sampled via the I2S interval\n   This allows to use the I2S API to obtain ADC samples with high sample rates\n   without the need of manual timing of the samples\n*/\nclass I2SAdcSource : public I2SSource {\n  public:\n    I2SAdcSource(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) :\n      I2SSource(sampleRate, blockSize, sampleScale) {\n      _config = {\n        .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN),\n        .sample_rate = _sampleRate,\n        .bits_per_sample = I2S_SAMPLE_RESOLUTION,\n        .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,\n#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0)\n        .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_STAND_I2S),\n#else\n        .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB),\n#endif\n        .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,\n        .dma_buf_count = 8,\n        .dma_buf_len = _blockSize,\n        .use_apll = false,\n        .tx_desc_auto_clear = false,\n        .fixed_mclk = 0        \n      };\n    }\n\n    /* identify Audiosource type - I2S-ADC*/\n    AudioSourceType getType(void) {return(Type_I2SAdc);}\n\n    void initialize(int8_t audioPin, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE) {\n      DEBUGSR_PRINTLN(F(\"I2SAdcSource:: initialize().\"));\n      _myADCchannel = 0x0F;\n      if(!PinManager::allocatePin(audioPin, false, PinOwner::UM_Audioreactive)) {\n         DEBUGSR_PRINTF(\"failed to allocate GPIO for audio analog input: %d\\n\", audioPin);\n        return;\n      }\n      _audioPin = audioPin;\n\n      // Determine Analog channel. Only Channels on ADC1 are supported\n      int8_t channel = digitalPinToAnalogChannel(_audioPin);\n      if (channel > 9) {\n        DEBUGSR_PRINTF(\"Incompatible GPIO used for analog audio input: %d\\n\", _audioPin);\n        return;\n      } else {\n        adc_gpio_init(ADC_UNIT_1, adc_channel_t(channel));\n        _myADCchannel = channel;\n      }\n\n      // Install Driver\n      esp_err_t err = i2s_driver_install(I2S_NUM_0, &_config, 0, nullptr);\n      if (err != ESP_OK) {\n        DEBUGSR_PRINTF(\"Failed to install i2s driver: %d\\n\", err);\n        return;\n      }\n\n      adc1_config_width(ADC_WIDTH_BIT_12);   // ensure that ADC runs with 12bit resolution\n\n      // Enable I2S mode of ADC\n      err = i2s_set_adc_mode(ADC_UNIT_1, adc1_channel_t(channel));\n      if (err != ESP_OK) {\n        DEBUGSR_PRINTF(\"Failed to set i2s adc mode: %d\\n\", err);\n        return;\n      }\n\n      // see example in https://github.com/espressif/arduino-esp32/blob/master/libraries/ESP32/examples/I2S/HiFreq_ADC/HiFreq_ADC.ino\n      adc1_config_channel_atten(adc1_channel_t(channel), ADC_ATTEN_DB_12);   // configure ADC input amplification\n\n      #if defined(I2S_GRAB_ADC1_COMPLETELY)\n      // according to docs from espressif, the ADC needs to be started explicitly\n      // fingers crossed\n        err = i2s_adc_enable(I2S_NUM_0);\n        if (err != ESP_OK) {\n            DEBUGSR_PRINTF(\"Failed to enable i2s adc: %d\\n\", err);\n            //return;\n        }\n      #else\n        // bugfix: do not disable ADC initially - its already disabled after driver install.\n        //err = i2s_adc_disable(I2S_NUM_0);\n\t\t    // //err = i2s_stop(I2S_NUM_0);\n        //if (err != ESP_OK) {\n        //    DEBUGSR_PRINTF(\"Failed to initially disable i2s adc: %d\\n\", err);\n        //}\n      #endif\n\n      _initialized = true;\n    }\n\n\n    I2S_datatype postProcessSample(I2S_datatype sample_in) {\n      static I2S_datatype lastADCsample = 0;          // last good sample\n      static unsigned int broken_samples_counter = 0; // number of consecutive broken (and fixed) ADC samples\n      I2S_datatype sample_out = 0;\n\n      // bring sample down down to 16bit unsigned\n      I2S_unsigned_datatype rawData = * reinterpret_cast<I2S_unsigned_datatype *> (&sample_in); // C++ acrobatics to get sample as \"unsigned\"\n      #ifndef I2S_USE_16BIT_SAMPLES\n        rawData = (rawData >> 16) & 0xFFFF;                       // scale input down from 32bit -> 16bit\n        I2S_datatype lastGoodSample = lastADCsample / 16384 ;     // prepare \"last good sample\" accordingly (26bit-> 12bit with correct sign handling)\n      #else\n        rawData = rawData & 0xFFFF;                               // input is already in 16bit, just mask off possible junk\n        I2S_datatype lastGoodSample = lastADCsample * 4;          // prepare \"last good sample\" accordingly (10bit-> 12bit)\n      #endif\n\n      // decode ADC sample data fields\n      uint16_t the_channel = (rawData >> 12) & 0x000F;           // upper 4 bit = ADC channel\n      uint16_t the_sample  =  rawData & 0x0FFF;                  // lower 12bit -> ADC sample (unsigned)\n      I2S_datatype finalSample = (int(the_sample) - 2048);       // convert unsigned sample to signed (centered at 0);\n\n      if ((the_channel != _myADCchannel) && (_myADCchannel != 0x0F)) { // 0x0F means \"don't know what my channel is\" \n        // fix bad sample\n        finalSample = lastGoodSample;                             // replace with last good ADC sample\n        broken_samples_counter ++;\n        if (broken_samples_counter > 256) _myADCchannel = 0x0F;   // too  many bad samples in a row -> disable sample corrections\n        //Serial.print(\"\\n!ADC rogue sample 0x\"); Serial.print(rawData, HEX); Serial.print(\"\\tchannel:\");Serial.println(the_channel);\n      } else broken_samples_counter = 0;                          // good sample - reset counter\n\n      // back to original resolution\n      #ifndef I2S_USE_16BIT_SAMPLES\n        finalSample = finalSample << 16;                          // scale up from 16bit -> 32bit;\n      #endif\n\n      finalSample = finalSample / 4;                              // mimic old analog driver behaviour (12bit -> 10bit)\n      sample_out = (3 * finalSample + lastADCsample) / 4;         // apply low-pass filter (2-tap FIR)\n      //sample_out = (finalSample + lastADCsample) / 2;             // apply stronger low-pass filter (2-tap FIR)\n\n      lastADCsample = sample_out;                                 // update ADC last sample\n      return(sample_out);\n    }\n\n\n    void getSamples(float *buffer, uint16_t num_samples) {\n      /* Enable ADC. This has to be enabled and disabled directly before and\n       * after sampling, otherwise Wifi dies\n       */\n      if (_initialized) {\n        #if !defined(I2S_GRAB_ADC1_COMPLETELY)\n          // old code - works for me without enable/disable, at least on ESP32.\n          //esp_err_t err = i2s_start(I2S_NUM_0);\n          esp_err_t err = i2s_adc_enable(I2S_NUM_0);\n          if (err != ESP_OK) {\n            DEBUGSR_PRINTF(\"Failed to enable i2s adc: %d\\n\", err);\n            return;\n          }\n        #endif\n\n        I2SSource::getSamples(buffer, num_samples);\n\n        #if !defined(I2S_GRAB_ADC1_COMPLETELY)\n          // old code - works for me without enable/disable, at least on ESP32.\n          err = i2s_adc_disable(I2S_NUM_0);  //i2s_adc_disable() may cause crash with IDF 4.4 (https://github.com/espressif/arduino-esp32/issues/6832)\n          //err = i2s_stop(I2S_NUM_0);\n          if (err != ESP_OK) {\n            DEBUGSR_PRINTF(\"Failed to disable i2s adc: %d\\n\", err);\n            return;\n          }\n        #endif\n      }\n    }\n\n    void deinitialize() {\n      PinManager::deallocatePin(_audioPin, PinOwner::UM_Audioreactive);\n      _initialized = false;\n      _myADCchannel = 0x0F;\n      \n      esp_err_t err;\n      #if defined(I2S_GRAB_ADC1_COMPLETELY)\n        // according to docs from espressif, the ADC needs to be stopped explicitly\n        // fingers crossed\n        err = i2s_adc_disable(I2S_NUM_0);\n        if (err != ESP_OK) {\n          DEBUGSR_PRINTF(\"Failed to disable i2s adc: %d\\n\", err);\n        }\n      #endif\n\n      i2s_stop(I2S_NUM_0);\n      err = i2s_driver_uninstall(I2S_NUM_0);\n      if (err != ESP_OK) {\n        DEBUGSR_PRINTF(\"Failed to uninstall i2s driver: %d\\n\", err);\n        return;\n      }\n    }\n\n  private:\n    int8_t _audioPin;\n    int8_t _myADCchannel = 0x0F;       // current ADC channel for analog input. 0x0F means \"undefined\"\n};\n#endif\n\n/* SPH0645 Microphone\n   This is an I2S microphone with some timing quirks that need\n   special consideration.\n*/\n\n// https://github.com/espressif/esp-idf/issues/7192  SPH0645 i2s microphone issue when migrate from legacy esp-idf version (IDFGH-5453)\n// a user recommended this: Try to set .communication_format to I2S_COMM_FORMAT_STAND_I2S and call i2s_set_clk() after i2s_set_pin().\nclass SPH0654 : public I2SSource {\n  public:\n    SPH0654(SRate_t sampleRate, int blockSize, float sampleScale = 1.0f) :\n      I2SSource(sampleRate, blockSize, sampleScale)\n    {}\n\n    void initialize(int8_t i2swsPin, int8_t i2ssdPin, int8_t i2sckPin, int8_t = I2S_PIN_NO_CHANGE) {\n      DEBUGSR_PRINTLN(F(\"SPH0654:: initialize();\"));\n      I2SSource::initialize(i2swsPin, i2ssdPin, i2sckPin);\n#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3)\n// these registers are only existing in \"classic\" ESP32\n      REG_SET_BIT(I2S_TIMING_REG(I2S_NUM_0), BIT(9));\n      REG_SET_BIT(I2S_CONF_REG(I2S_NUM_0), I2S_RX_MSB_SHIFT);\n#else\n      #warning FIX ME! Please.\n#endif\n    }\n};\n#endif\n"
  },
  {
    "path": "usermods/audioreactive/library.json",
    "content": "{\n  \"name\": \"audioreactive\",\n  \"build\": {\n    \"libArchive\": false,\n    \"extraScript\": \"override_sqrt.py\"\n  },\n  \"dependencies\": [\n    {\n      \"owner\": \"kosme\",      \n      \"name\": \"arduinoFFT\",\n      \"version\": \"2.0.1\",\n      \"platforms\": \"espressif32\"\n    }\n  ]\n}\n"
  },
  {
    "path": "usermods/audioreactive/override_sqrt.py",
    "content": "Import('env')\n\nfor lb in env.GetLibBuilders():\n    if lb.name == \"arduinoFFT\":\n        lb.env.Append(CPPDEFINES=[(\"sqrt_internal\", \"sqrtf\")])\n"
  },
  {
    "path": "usermods/audioreactive/readme.md",
    "content": "# Audioreactive usermod\n\nEnables controlling LEDs via audio input. Audio source can be a microphone or analog-in (AUX) using an appropriate adapter.\nSupported microphones range from analog (MAX4466, MAX9814, ...) to digital (INMP441, ICS-43434, ...).\n\nDoes audio processing and provides data structure that specially written effects can use.\n\n**does not** provide effects or draw anything to an LED strip/matrix.\n\n## Additional Documentation\n\nThis 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):\n\n* [getting started with audio](https://github.com/atuline/WLED/wiki/First-Time-Setup#sound)\n* [Sound settings](https://github.com/atuline/WLED/wiki/Sound-Settings) - similar to options on the usemod settings page in WLED.\n* [Digital Audio](https://github.com/atuline/WLED/wiki/Digital-Microphone-Hookup)\n* [Analog Audio](https://github.com/atuline/WLED/wiki/Analog-Audio-Input-Options)\n* [UDP Sound sync](https://github.com/atuline/WLED/wiki/UDP-Sound-Sync)\n\n## Supported MCUs\n\nThis audioreactive usermod works best on \"classic ESP32\" (dual core), and on ESP32-S3 which also has dual core and hardware floating point support.\n\nIt 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.\n\nAnalog audio is only possible on \"classic\" ESP32, but not on other MCUs like ESP32-S3.\n\nCurrently ESP8266 is not supported, due to low speed and small RAM of this chip.\nThere are however plans to create a lightweight audioreactive for the 8266, with reduced features.\n\n## Installation\n\nAdd 'ADS1115_v2' to `custom_usermods` in your platformio environment.\n\n## Configuration\n\nAll parameters are runtime configurable. Some may require a hard reset after changing them (I2S microphone or selected GPIOs).\n\nIf you want to define default GPIOs during compile time, use the following (default values in parentheses):\n\n* `-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\n* `-D AUDIOPIN=x`  : GPIO for analog microphone/AUX-in (36)\n* `-D I2S_SDPIN=x` : GPIO for SD pin on digital microphone (32)\n* `-D I2S_WSPIN=x` : GPIO for WS pin on digital microphone (15)\n* `-D I2S_CKPIN=x` : GPIO for SCK pin on digital microphone (14)\n* `-D MCLK_PIN=x`  : GPIO for master clock pin on digital Line-In boards (-1)\n* `-D ES7243_SDAPIN` : GPIO for I2C SDA pin on ES7243 microphone (-1)\n* `-D ES7243_SCLPIN` : GPIO for I2C SCL pin on ES7243 microphone (-1)\n\nOther options:\n\n* `-D UM_AUDIOREACTIVE_ENABLE` : makes usermod default enabled (not the same as include into build option!)\n* `-D UM_AUDIOREACTIVE_DYNAMICS_LIMITER_OFF` : disables rise/fall limiter default\n\n**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.\n\n### Advanced Compile-Time Options\n\nYou can use the following additional flags in your `build_flags`\n\n* `-D SR_SQUELCH=x`  : Default \"squelch\" setting (10)\n* `-D SR_GAIN=x`     : Default \"gain\" setting (60)\n* `-D SR_AGC=x`      : (Only ESP32) Default \"AGC (Automatic Gain Control)\" setting (0): 0=off, 1=normal, 2=vivid, 3=lazy\n* `-D I2S_USE_RIGHT_CHANNEL`: Use RIGHT instead of LEFT channel (not recommended unless you strictly need this).\n* `-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).\n* `-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.\n* `-D MIC_LOGGER`     : (debugging) Logs samples from the microphone to serial USB. Use with serial plotter (Arduino IDE)\n* `-D SR_DEBUG`       : (debugging) Additional error diagnostics and debug info on serial USB.\n\n## Release notes\n\n* 2022-06 Ported from [soundreactive WLED](https://github.com/atuline/WLED) - by @blazoncek (AKA Blaz Kristan) and the [SR-WLED team](https://github.com/atuline/WLED/wiki#sound-reactive-wled-fork-team).\n* 2022-11 Updated to align with \"[MoonModules/WLED](https://amg.wled.me)\" audioreactive usermod - by @softhack007 (AKA Frank M&ouml;hle).\n"
  },
  {
    "path": "usermods/battery_keypad_controller/README.md",
    "content": "# Battery powered controller with keypad\n\nI'm using this controller for a festival totem. Runs on 3 18650 Cells, can deliver >5A current.\n\nVia keypad one can select 8 presets, change effect, effect speed, effect intensity and palette. Brightness can be\nadjusted with a potentiometer.\n\n## Pictures\n\n![bat-key-ctrl-1](assets/bat-key-ctrl-1.jpg)\n![bat-key-ctrl-2](assets/bat-key-ctrl-2.jpg)\n![bat-key-ctrl-3](assets/bat-key-ctrl-3.jpg)\n"
  },
  {
    "path": "usermods/battery_keypad_controller/wled06_usermod.ino",
    "content": "/*\n *  WLED usermod for keypad and brightness-pot.\n *  3'2020 https://github.com/hobbyquaker\n */\n\n#include <Keypad.h>\nconst byte keypad_rows = 4;\nconst byte keypad_cols = 4;\nchar keypad_keys[keypad_rows][keypad_cols] = {\n        {'1', '2', '3', 'A'},\n        {'4', '5', '6', 'B'},\n        {'7', '8', '9', 'C'},\n        {'*', '0', '#', 'D'}\n};\n\nbyte keypad_colPins[keypad_rows] = {D3, D2, D1, D0};\nbyte keypad_rowPins[keypad_cols] = {D7, D6, D5, D4};\n\nKeypad myKeypad = Keypad(makeKeymap(keypad_keys), keypad_rowPins, keypad_colPins, keypad_rows, keypad_cols);\n\nvoid userSetup()\n{\n\n}\n\nvoid userConnected()\n{\n\n}\n\nlong lastTime = 0;\nint delayMs = 20; //we want to do something every 2 seconds\n\nvoid userLoop()\n{\n    if (millis()-lastTime > delayMs)\n    {\n\n        long analog = analogRead(0);\n        int new_bri = 1;\n        if (analog > 900) {\n            new_bri = 255;\n        } else if (analog > 30) {\n            new_bri = dim8_video(map(analog, 31, 900, 16, 255));\n        }\n        if (bri != new_bri) {\n            bri = new_bri;\n            colorUpdated(1);\n\n        }\n\n        char myKey = myKeypad.getKey();\n        if (myKey != NULL) {\n            switch (myKey) {\n                case '1':\n                    applyPreset(1);\n                    break;\n                case '2':\n                    applyPreset(2);\n                    break;\n                case '3':\n                    applyPreset(3);\n                    break;\n                case '4':\n                    applyPreset(4);\n                    break;\n                case '5':\n                    applyPreset(5);\n                    break;\n                case '6':\n                    applyPreset(6);\n                    break;\n                case 'A':\n                    applyPreset(7);\n                    break;\n                case 'B':\n                    applyPreset(8);\n                    break;\n\n                case '7':\n                    effectCurrent += 1;\n                    if (effectCurrent >= MODE_COUNT) effectCurrent = 0;\n                    colorUpdated(CALL_MODE_FX_CHANGED);\n                    break;\n                case '*':\n                    effectCurrent -= 1;\n                    if (effectCurrent < 0) effectCurrent = (MODE_COUNT-1);\n                    colorUpdated(CALL_MODE_FX_CHANGED);\n                    break;\n\n                case '8':\n                    if (effectSpeed < 240) {\n                        effectSpeed += 12;\n                    } else if (effectSpeed < 255) {\n                        effectSpeed += 1;\n                    }\n                    colorUpdated(CALL_MODE_FX_CHANGED);\n                    break;\n                case '0':\n                    if (effectSpeed > 15) {\n                        effectSpeed -= 12;\n                    } else if (effectSpeed > 0) {\n                        effectSpeed -= 1;\n                    }\n                    colorUpdated(CALL_MODE_FX_CHANGED);\n                    break;\n\n                case '9':\n                    if (effectIntensity < 240) {\n                        effectIntensity += 12;\n                    } else if (effectIntensity < 255) {\n                        effectIntensity += 1;\n                    }\n                    colorUpdated(CALL_MODE_FX_CHANGED);\n                    break;\n                case '#':\n                    if (effectIntensity > 15) {\n                        effectIntensity -= 12;\n                    } else if (effectIntensity > 0) {\n                        effectIntensity -= 1;\n                    }\n                    colorUpdated(CALL_MODE_FX_CHANGED);\n                    break;\n\n                case 'C':\n                    effectPalette += 1;\n                    if (effectPalette >= 50) effectPalette = 0;\n                    colorUpdated(CALL_MODE_FX_CHANGED);\n                    break;\n                case 'D':\n                    effectPalette -= 1;\n                    if (effectPalette <= 0) effectPalette = 50;\n                    colorUpdated(CALL_MODE_FX_CHANGED);\n                    break;\n\n            }\n\n        }\n\n        lastTime = millis();\n    }\n\n}"
  },
  {
    "path": "usermods/boblight/boblight.cpp",
    "content": "#include \"wled.h\"\n\n/*\n * Usermod that implements BobLight \"ambilight\" protocol\n * \n * See the accompanying README.md file for more info.\n */\n\n#ifndef BOB_PORT\n  #define BOB_PORT 19333       // Default boblightd port\n#endif\n\nclass BobLightUsermod : public Usermod {\n  typedef struct _LIGHT {\n    char lightname[5];\n    float hscan[2];\n    float vscan[2];\n  } light_t;\n\n  private:\n    unsigned long lastTime = 0;\n    bool enabled  = false;\n    bool initDone = false;\n\n    light_t *lights = nullptr;\n    uint16_t numLights = 0;  // 16 + 9 + 16 + 9\n    uint16_t top, bottom, left, right;  // will be filled in readFromConfig()\n    uint16_t pct;\n\n    WiFiClient bobClient;\n    WiFiServer *bob;\n    uint16_t   bobPort = BOB_PORT;\n\n    static const char _name[];\n    static const char _enabled[];\n\n    /*\n    # boblight\n    # Copyright (C) Bob  2009 \n    #\n    # makeboblight.sh created by Adam Boeglin <adamrb@gmail.com>\n    #\n    # boblight is free software: you can redistribute it and/or modify it\n    # under the terms of the GNU General Public License as published by the\n    # Free Software Foundation, either version 3 of the License, or\n    # (at your option) any later version.\n    # \n    # boblight is distributed in the hope that it will be useful, but\n    # WITHOUT ANY WARRANTY; without even the implied warranty of\n    # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n    # See the GNU General Public License for more details.\n    # \n    # You should have received a copy of the GNU General Public License along\n    # with this program.  If not, see <http://www.gnu.org/licenses/>.\n    */\n\n    // fills the lights[] array with position & depth of scan for each LED\n    void fillBobLights(int bottom, int left, int top, int right, float pct_scan) {\n\n      int lightcount = 0;\n      int total = top+left+right+bottom;\n      int bcount;\n\n      if (total > strip.getLengthTotal()) {\n        DEBUG_PRINTLN(F(\"BobLight: Too many lights.\"));\n        return;\n      }\n\n      // start left part of bottom strip (clockwise direction, 1st half)\n      if (bottom > 0) {\n        bcount = 1;\n        float brange = 100.0/bottom;\n        float bcurrent = 50.0;\n        if (bottom < top) {\n          int diff = top - bottom;\n          brange = 100.0/top;\n          bcurrent -= (diff/2)*brange;\n        }\n        while (bcount <= bottom/2) {\n          float btop = bcurrent - brange;\n          String name = \"b\"+String(bcount);\n          strncpy(lights[lightcount].lightname, name.c_str(), 4);\n          lights[lightcount].hscan[0] = btop;\n          lights[lightcount].hscan[1] = bcurrent;\n          lights[lightcount].vscan[0] = 100 - pct_scan;\n          lights[lightcount].vscan[1] = 100;\n          lightcount+=1;\n          bcurrent = btop;\n          bcount+=1;\n        }\n      }\n\n      // left side\n      if (left > 0) {\n        int lcount = 1;\n        float lrange = 100.0/left;\n        float lcurrent = 100.0;\n        while (lcount <= left) {\n          float ltop = lcurrent - lrange;\n          String name = \"l\"+String(lcount);\n          strncpy(lights[lightcount].lightname, name.c_str(), 4);\n          lights[lightcount].hscan[0] = 0;\n          lights[lightcount].hscan[1] = pct_scan;\n          lights[lightcount].vscan[0] = ltop;\n          lights[lightcount].vscan[1] = lcurrent;\n          lightcount+=1;\n          lcurrent = ltop;\n          lcount+=1;\n        }\n      }\n\n      // top side\n      if (top > 0) {\n        int tcount = 1;\n        float trange = 100.0/top;\n        float tcurrent = 0;\n        while (tcount <= top) {\n          float ttop = tcurrent + trange;\n          String name = \"t\"+String(tcount);\n          strncpy(lights[lightcount].lightname, name.c_str(), 4);\n          lights[lightcount].hscan[0] = tcurrent;\n          lights[lightcount].hscan[1] = ttop;\n          lights[lightcount].vscan[0] = 0;\n          lights[lightcount].vscan[1] = pct_scan;\n          lightcount+=1;\n          tcurrent = ttop;\n          tcount+=1;\n        }\n      }\n\n      // right side\n      if (right > 0) {\n        int rcount = 1;\n        float rrange = 100.0/right;\n        float rcurrent = 0;\n        while (rcount <= right) {\n          float rtop = rcurrent + rrange;\n          String name = \"r\"+String(rcount);\n          strncpy(lights[lightcount].lightname, name.c_str(), 4);\n          lights[lightcount].hscan[0] = 100-pct_scan;\n          lights[lightcount].hscan[1] = 100;\n          lights[lightcount].vscan[0] = rcurrent;\n          lights[lightcount].vscan[1] = rtop;\n          lightcount+=1;\n          rcurrent = rtop;\n          rcount+=1;\n        }\n      }\n      \n      // right side of bottom strip (2nd half)\n      if (bottom > 0) {\n        float brange = 100.0/bottom;\n        float bcurrent = 100;\n        if (bottom < top) {\n          brange = 100.0/top;\n        }\n        while (bcount <= bottom) {\n          float btop = bcurrent - brange;\n          String name = \"b\"+String(bcount);\n          strncpy(lights[lightcount].lightname, name.c_str(), 4);\n          lights[lightcount].hscan[0] = btop;\n          lights[lightcount].hscan[1] = bcurrent;\n          lights[lightcount].vscan[0] = 100 - pct_scan;\n          lights[lightcount].vscan[1] = 100;\n          lightcount+=1;\n          bcurrent = btop;\n          bcount+=1;\n        }\n      }\n\n      numLights = lightcount;\n\n      #if WLED_DEBUG\n      DEBUG_PRINTLN(F(\"Fill light data: \"));\n      DEBUG_PRINTF_P(PSTR(\" lights %d\\n\"), numLights);\n      for (int i=0; i<numLights; i++) {\n        DEBUG_PRINTF_P(PSTR(\" light %s scan %2.1f %2.1f %2.1f %2.1f\\n\"), lights[i].lightname, lights[i].vscan[0], lights[i].vscan[1], lights[i].hscan[0], lights[i].hscan[1]);\n      }\n      #endif\n    }\n\n    void BobSync()  { yield(); } // allow other tasks, should also be used to force pixel redraw (not with WLED)\n    void BobClear() { for (size_t i=0; i<numLights; i++) setRealtimePixel(i, 0, 0, 0, 0); }\n    void pollBob();\n\n  public:\n\n    void setup() override {\n      uint16_t totalLights = bottom + left + top + right;\n      if ( totalLights > strip.getLengthTotal() ) {\n        DEBUG_PRINTLN(F(\"BobLight: Too many lights.\"));\n        DEBUG_PRINTF_P(PSTR(\"%d+%d+%d+%d>%d\\n\"), bottom, left, top, right, strip.getLengthTotal());\n        totalLights = strip.getLengthTotal();\n        top = bottom = (uint16_t) roundf((float)totalLights * 16.0f / 50.0f);\n        left = right = (uint16_t) roundf((float)totalLights *  9.0f / 50.0f);\n      }\n      lights = new light_t[totalLights];\n      if (lights) fillBobLights(bottom, left, top, right, float(pct)); // will fill numLights\n      else        enable(false);\n      initDone = true;\n    }\n\n    void connected() override {\n      // we can only start server when WiFi is connected\n      if (!bob) bob = new WiFiServer(bobPort, 1);\n      bob->begin();\n      bob->setNoDelay(true);\n    }\n\n    void loop() override {\n      if (!enabled || strip.isUpdating()) return;\n      if (millis() - lastTime > 10) {\n        lastTime = millis();\n        pollBob();\n      }\n    }\n\n    void enable(bool en) { enabled = en; }\n    \n#ifndef WLED_DISABLE_MQTT\n    /**\n     * handling of MQTT message\n     * topic only contains stripped topic (part after /wled/MAC)\n     * topic should look like: /swipe with amessage of [up|down]\n     */\n    bool onMqttMessage(char* topic, char* payload) override {\n      //if (strlen(topic) == 6 && strncmp_P(topic, PSTR(\"/subtopic\"), 6) == 0) {\n      //  String action = payload;\n      //  if (action == \"on\") {\n      //    enable(true);\n      //    return true;\n      //  } else if (action == \"off\") {\n      //    enable(false);\n      //    return true;\n      //  }\n      //}\n      return false;\n    }\n\n    /**\n     * subscribe to MQTT topic for controlling usermod\n     */\n    void onMqttConnect(bool sessionPresent) override {\n      //char subuf[64];\n      //if (mqttDeviceTopic[0] != 0) {\n      //  strcpy(subuf, mqttDeviceTopic);\n      //  strcat_P(subuf, PSTR(\"/subtopic\"));\n      //  mqtt->subscribe(subuf, 0);\n      //}\n    }\n#endif\n\n    void addToJsonInfo(JsonObject& root) override\n    {\n      JsonObject user = root[\"u\"];\n      if (user.isNull()) user = root.createNestedObject(\"u\");\n\n      JsonArray infoArr = user.createNestedArray(FPSTR(_name));\n      String uiDomString = F(\"<button class=\\\"btn btn-xs\\\" onclick=\\\"requestJson({\");\n      uiDomString += FPSTR(_name);\n      uiDomString += F(\":{\");\n      uiDomString += FPSTR(_enabled);\n      uiDomString += enabled ? F(\":false}});\\\">\") : F(\":true}});\\\">\");\n      uiDomString += F(\"<i class=\\\"icons \");\n      uiDomString += enabled ? \"on\" : \"off\";\n      uiDomString += F(\"\\\">&#xe08f;</i></button>\");\n      infoArr.add(uiDomString);\n    }\n\n    /*\n     * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).\n     * Values in the state object may be modified by connected clients\n     */\n    void addToJsonState(JsonObject& root) override\n    {\n    }\n\n    /*\n     * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).\n     * Values in the state object may be modified by connected clients\n     */\n    void readFromJsonState(JsonObject& root) override {\n      if (!initDone) return;  // prevent crash on boot applyPreset()\n      bool en = enabled;\n      JsonObject um = root[FPSTR(_name)];\n      if (!um.isNull()) {\n        if (um[FPSTR(_enabled)].is<bool>()) {\n          en = um[FPSTR(_enabled)].as<bool>();\n        } else {\n          String str = um[FPSTR(_enabled)]; // checkbox -> off or on\n          en = (bool)(str!=\"off\"); // off is guaranteed to be present\n        }\n        if (en != enabled && lights) {\n          enable(en);\n          if (!enabled && bob && bob->hasClient()) {\n            if (bobClient) bobClient.stop();\n            bobClient = bob->available();\n            BobClear();\n            exitRealtime();\n          }\n        }\n      }\n    }\n\n    void appendConfigData() override {\n      //oappend(F(\"dd=addDropdown('usermod','selectfield');\"));\n      //oappend(F(\"addOption(dd,'1st value',0);\"));\n      //oappend(F(\"addOption(dd,'2nd value',1);\"));\n      oappend(F(\"addInfo('BobLight:top',1,'LEDs');\"));                // 0 is field type, 1 is actual field\n      oappend(F(\"addInfo('BobLight:bottom',1,'LEDs');\"));             // 0 is field type, 1 is actual field\n      oappend(F(\"addInfo('BobLight:left',1,'LEDs');\"));               // 0 is field type, 1 is actual field\n      oappend(F(\"addInfo('BobLight:right',1,'LEDs');\"));              // 0 is field type, 1 is actual field\n      oappend(F(\"addInfo('BobLight:pct',1,'Depth of scan [%]');\"));   // 0 is field type, 1 is actual field\n    }\n\n    void addToConfig(JsonObject& root) override {\n      JsonObject umData = root.createNestedObject(FPSTR(_name));\n      umData[FPSTR(_enabled)] = enabled;\n      umData[  \"port\" ]       = bobPort;\n      umData[F(\"top\")]        = top;\n      umData[F(\"bottom\")]     = bottom;\n      umData[F(\"left\")]       = left;\n      umData[F(\"right\")]      = right;\n      umData[F(\"pct\")]        = pct;\n    }\n\n    bool readFromConfig(JsonObject& root) override {\n      JsonObject umData = root[FPSTR(_name)];\n      bool configComplete = !umData.isNull();\n\n      bool en = enabled;\n      configComplete &= getJsonValue(umData[FPSTR(_enabled)], en);\n      enable(en);\n\n      configComplete &= getJsonValue(umData[  \"port\" ],   bobPort);\n      configComplete &= getJsonValue(umData[F(\"bottom\")], bottom,    16);\n      configComplete &= getJsonValue(umData[F(\"top\")],    top,       16);\n      configComplete &= getJsonValue(umData[F(\"left\")],   left,       9);\n      configComplete &= getJsonValue(umData[F(\"right\")],  right,      9);\n      configComplete &= getJsonValue(umData[F(\"pct\")],    pct,        5); // Depth of scan [%]\n      pct = MIN(50,MAX(1,pct));\n\n      uint16_t totalLights = bottom + left + top + right;\n      if (initDone && numLights != totalLights) {\n        if (lights) delete[] lights;\n        setup();\n      }\n      return configComplete;\n    }\n\n    /*\n     * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors.\n     * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode.\n     * Commonly used for custom clocks (Cronixie, 7 segment)\n     */\n    void handleOverlayDraw() override {\n      //strip.setPixelColor(0, RGBW32(0,0,0,0)) // set the first pixel to black\n    }\n\n    uint16_t getId() override { return USERMOD_ID_BOBLIGHT; }\n\n};\n\n// strings to reduce flash memory usage (used more than twice)\nconst char BobLightUsermod::_name[]    PROGMEM = \"BobLight\";\nconst char BobLightUsermod::_enabled[] PROGMEM = \"enabled\";\n\n// main boblight handling (definition here prevents inlining)\nvoid BobLightUsermod::pollBob() {\n  \n  //check if there are any new clients\n  if (bob && bob->hasClient()) {\n    //find free/disconnected spot\n    if (!bobClient || !bobClient.connected()) {\n      if (bobClient) bobClient.stop();\n      bobClient = bob->available();\n      DEBUG_PRINTLN(F(\"Boblight: Client connected.\"));\n    }\n    //no free/disconnected spot so reject\n    WiFiClient bobClientTmp = bob->available();\n    bobClientTmp.stop();\n    BobClear();\n    exitRealtime();\n  }\n  \n  //check clients for data\n  if (bobClient && bobClient.connected()) {\n    realtimeLock(realtimeTimeoutMs); // lock strip as we have a client connected\n\n    //get data from the client\n    while (bobClient.available()) {\n      String input = bobClient.readStringUntil('\\n');\n      // DEBUG_PRINT(F(\"Client: \")); DEBUG_PRINTLN(input); // may be to stressful on Serial\n      if (input.startsWith(F(\"hello\"))) {\n        DEBUG_PRINTLN(F(\"hello\"));\n        bobClient.print(F(\"hello\\n\"));\n      } else if (input.startsWith(F(\"ping\"))) {\n        DEBUG_PRINTLN(F(\"ping 1\"));\n        bobClient.print(F(\"ping 1\\n\"));\n      } else if (input.startsWith(F(\"get version\"))) {\n        DEBUG_PRINTLN(F(\"version 5\"));\n        bobClient.print(F(\"version 5\\n\"));\n      } else if (input.startsWith(F(\"get lights\"))) {\n        char tmp[64];\n        String answer = \"\";\n        sprintf_P(tmp, PSTR(\"lights %d\\n\"), numLights);\n        DEBUG_PRINT(tmp);\n        answer.concat(tmp);\n        for (int i=0; i<numLights; i++) {\n          sprintf_P(tmp, PSTR(\"light %s scan %2.1f %2.1f %2.1f %2.1f\\n\"), lights[i].lightname, lights[i].vscan[0], lights[i].vscan[1], lights[i].hscan[0], lights[i].hscan[1]);\n          DEBUG_PRINT(tmp);\n          answer.concat(tmp);\n        }\n        bobClient.print(answer);\n      } else if (input.startsWith(F(\"set priority\"))) {\n        DEBUG_PRINTLN(F(\"set priority not implemented\"));\n        // not implemented\n      } else if (input.startsWith(F(\"set light \"))) { // <id> <cmd in rgb, speed, interpolation> <value> ...\n        input.remove(0,10);\n        String tmp = input.substring(0,input.indexOf(' '));\n        \n        int light_id = -1;\n        for (uint16_t i=0; i<numLights; i++) {\n          if (strncmp(lights[i].lightname, tmp.c_str(), 4) == 0) {\n            light_id = i;\n            break;\n          }\n        }\n        if (light_id == -1) return;\n\n        input.remove(0,input.indexOf(' ')+1);\n        if (input.startsWith(F(\"rgb \"))) {\n          input.remove(0,4);\n          tmp = input.substring(0,input.indexOf(' '));\n          uint8_t red = (uint8_t)(255.0f*tmp.toFloat());\n          input.remove(0,input.indexOf(' ')+1);        // remove first float value\n          tmp = input.substring(0,input.indexOf(' '));\n          uint8_t green = (uint8_t)(255.0f*tmp.toFloat());\n          input.remove(0,input.indexOf(' ')+1);        // remove second float value\n          tmp = input.substring(0,input.indexOf(' '));\n          uint8_t blue = (uint8_t)(255.0f*tmp.toFloat());\n\n          //strip.setPixelColor(light_id, RGBW32(red, green, blue, 0));\n          setRealtimePixel(light_id, red, green, blue, 0);\n        } // currently no support for interpolation or speed, we just ignore this\n      } else if (input.startsWith(\"sync\")) {\n        BobSync();\n      } else {\n        // Client sent gibberish\n        DEBUG_PRINTLN(F(\"Client sent gibberish.\"));\n        bobClient.stop();\n        bobClient = bob->available();\n        BobClear();\n      }\n    }\n  }\n}\n\n\nstatic BobLightUsermod boblight;\nREGISTER_USERMOD(boblight);"
  },
  {
    "path": "usermods/boblight/library.json",
    "content": "{\n  \"name\": \"boblight\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/boblight/readme.md",
    "content": "# BobLight usermod\n\nThis usermod allows displaying BobLight ambilight protocol on WLED device with a limited command set (not a full implementation).\nBobLight protocol uses a TCP connection which guarantees packet delivery at the possible expense of latency delays. It is not very efficient (as it uses plaintext comands) so is not suited for large number of LEDs.\n\nThis implementation is intended for TV backlight in combination with XBMC/Kodi BobLight add-on.\n\nThe LEDs can be configured in usermod settings page. The configuration is simple: you enter the number of LED pixels on each side of your TV (top, right, bottom, left).\nThe LEDs should be wired in a clockwise orientation starting in the middle of bottom side (left half of bottom leds is where the string should start).\n\n```\n+-------->-------+\n|                |\n^                v\n|                |\n+---<--+  ---<---+\n       ^\n     start\n```\n\n## Installation \n\nAdd `boblight` to `custom_usermods` in your PlatformIO environment.\n\n## Configuration\n\nAll parameters are runtime configurable though changing port may require reboot.\n\nIf you want to define default port during compile time use the following (default values in parentheses):\n\n- `BOB_PORT=x` : defines default TCP port for usermod to listen on (19333)\n\n\n## Release notes\n\n2022-11 Initial implementation by @blazoncek (AKA Blaz Kristan)\n"
  },
  {
    "path": "usermods/buzzer/buzzer.cpp",
    "content": "#include \"wled.h\"\n#include \"Arduino.h\"\n\n#include <deque>\n\n#define USERMOD_ID_BUZZER 900\n#ifndef USERMOD_BUZZER_PIN\n#ifdef GPIO_NUM_32\n#define USERMOD_BUZZER_PIN GPIO_NUM_32\n#else\n#define USERMOD_BUZZER_PIN 21\n#endif\n#endif\n\n/*\n * Usermods allow you to add own functionality to WLED more easily\n * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality\n * \n */\n\nclass BuzzerUsermod : public Usermod {\n  private:\n    unsigned long lastTime_ = 0;\n    unsigned long delay_ = 0;\n    std::deque<std::pair<uint8_t, unsigned long>> sequence_ {};\n  public:\n    /*\n     * setup() is called once at boot. WiFi is not yet connected at this point.\n     * You can use it to initialize variables, sensors or similar.\n     */\n    void setup() {\n      // Setup the pin, and default to LOW\n      pinMode(USERMOD_BUZZER_PIN, OUTPUT);\n      digitalWrite(USERMOD_BUZZER_PIN, LOW);\n\n      // Beep on startup\n      sequence_.push_back({ HIGH, 50 });\n      sequence_.push_back({ LOW, 0 });\n    }\n\n\n    /*\n     * connected() is called every time the WiFi is (re)connected\n     * Use it to initialize network interfaces\n     */\n    void connected() {\n      // Double beep on WiFi\n      sequence_.push_back({ LOW, 100 });\n      sequence_.push_back({ HIGH, 50 });\n      sequence_.push_back({ LOW, 30 });\n      sequence_.push_back({ HIGH, 50 });\n      sequence_.push_back({ LOW, 0 });\n    }\n\n    /*\n     * loop() is called continuously. Here you can check for events, read sensors, etc.\n     */\n    void loop() {\n      if (sequence_.size() < 1) return; // Wait until there is a sequence\n      if (millis() - lastTime_ <= delay_) return; // Wait until delay has elapsed\n\n      auto event = sequence_.front();\n      sequence_.pop_front();\n\n      digitalWrite(USERMOD_BUZZER_PIN, event.first);\n      delay_ = event.second;\n\n      lastTime_ = millis();\n    }\n\n\n    /*\n     * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).\n     * This could be used in the future for the system to determine whether your usermod is installed.\n     */\n    uint16_t getId()\n    {\n      return USERMOD_ID_BUZZER;\n    }\n};\n\nstatic BuzzerUsermod buzzer;\nREGISTER_USERMOD(buzzer);"
  },
  {
    "path": "usermods/buzzer/library.json",
    "content": "{\n  \"name\": \"buzzer\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/deep_sleep/deep_sleep.cpp",
    "content": "#include \"wled.h\"\n#include \"driver/rtc_io.h\"\n#ifndef CONFIG_IDF_TARGET_ESP32C3\n  #include \"soc/touch_sensor_periph.h\"\n#endif\n#ifdef ESP8266\n#error The \"Deep Sleep\" usermod does not support ESP8266\n#endif\n\n#ifndef DEEPSLEEP_WAKEUPPIN\n#define DEEPSLEEP_WAKEUPPIN 0\n#endif\n#ifndef DEEPSLEEP_WAKEWHENHIGH\n#define DEEPSLEEP_WAKEWHENHIGH 0\n#endif\n#ifndef DEEPSLEEP_DISABLEPULL\n#define DEEPSLEEP_DISABLEPULL 1\n#endif\n#ifndef DEEPSLEEP_WAKEUPINTERVAL\n#define DEEPSLEEP_WAKEUPINTERVAL 0\n#endif\n#ifndef DEEPSLEEP_DELAY\n#define DEEPSLEEP_DELAY 1\n#endif\n\n#ifndef DEEPSLEEP_WAKEUP_TOUCH_PIN\n#define DEEPSLEEP_WAKEUP_TOUCH_PIN 1\n#endif\nRTC_DATA_ATTR bool powerup = true; // this is first boot after power cycle. note: variable in RTC data persists on a reboot\nRTC_DATA_ATTR uint8_t wakeupPreset = 0; // preset to apply after deep sleep wakeup (0 = none), set to timer macro preset\n\nclass DeepSleepUsermod : public Usermod {\n\n  private:\n    bool enabled = false; // do not enable by default\n    bool initDone = false;\n    uint8_t wakeupPin = DEEPSLEEP_WAKEUPPIN;\n    uint8_t wakeWhenHigh = DEEPSLEEP_WAKEWHENHIGH; // wake up when pin goes high if 1, triggers on low if 0\n    bool noPull = true; // use pullup/pulldown resistor\n    bool enableTouchWakeup = false;\n    uint8_t touchPin = DEEPSLEEP_WAKEUP_TOUCH_PIN;\n    int wakeupAfter = DEEPSLEEP_WAKEUPINTERVAL; // in seconds, <=0: button only\n    bool presetWake = true; // wakeup timer for preset\n    int sleepDelay = DEEPSLEEP_DELAY; // in seconds, 0 = immediate\n    int delaycounter = 10; // delay deep sleep at bootup until preset settings are applied, force wake up if offmode persists after bootup\n    uint32_t lastLoopTime = 0;\n\n    // string that are used multiple time (this will save some flash memory)\n    static const char _name[];\n    static const char _enabled[];\n\n    bool pin_is_valid(uint8_t wakePin) {\n    #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\n      if (wakePin == 0 || wakePin == 2 || wakePin == 4 || (wakePin >= 12 && wakePin <= 15) || (wakePin >= 25 && wakePin <= 27) || (wakePin >= 32 && wakePin <= 39)) {\n          return true;\n      }\n    #endif\n    #if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32S2) //ESP32 S3 & S3: GPIOs 0-21 can be used for wake-up\n      if (wakePin <= 21) {\n          return true;\n      }\n    #endif\n    #ifdef CONFIG_IDF_TARGET_ESP32C3 // ESP32 C3: GPIOs 0-5 can be used for wake-up\n      if (wakePin <= 5) {\n          return true;\n      }\n    #endif\n      DEBUG_PRINTLN(F(\"Error: unsupported deep sleep wake-up pin\"));\n      return false;\n    }\n\n    // functions to calculate time difference between now and next scheduled timer event\n    int calculateTimeDifference(int hour1, int minute1, int hour2, int minute2) {\n      int totalMinutes1 = hour1 * 60 + minute1;\n      int totalMinutes2 = hour2 * 60 + minute2;\n      if (totalMinutes2 < totalMinutes1) {\n        totalMinutes2 += 24 * 60;\n      }\n      return totalMinutes2 - totalMinutes1;\n    }\n\n    int findNextTimerInterval() {\n      if (toki.getTimeSource() == TOKI_TS_NONE) {\n        DEBUG_PRINTLN(\"DeepSleep: local time not yet synchronized, skipping timer check.\");\n        return -1;\n      }\n      int currentHour = hour(localTime);\n      int currentMinute = minute(localTime);\n      int currentWeekday = weekdayMondayFirst(); // 1=Monday ... 7=Sunday\n      int minDifference = INT_MAX;\n\n      for (uint8_t i = 0; i < 8; i++) {\n        // check if timer is enabled and date is in range, also wakes up if no macro is used\n        if ((timerWeekday[i] & 0x01) && isTodayInDateRange(((timerMonth[i] >> 4) & 0x0F), timerDay[i], timerMonth[i] & 0x0F, timerDayEnd[i])) {\n\n          // if timer is enabled (bit0 of timerWeekday) and date is in range, check all weekdays it is set for\n          for (int dayOffset = 0; dayOffset < 7; dayOffset++) {\n            int checkWeekday = ((currentWeekday + dayOffset) % 7); // 1-7, check all weekdays starting from today\n            if (checkWeekday == 0) {\n              checkWeekday = 7; // sunday is 7 not 0\n            }\n\n            int targetHour = timerHours[i];\n            int targetMinute = timerMinutes[i];\n            if ((timerWeekday[i] >> (checkWeekday)) & 0x01) {\n              if (dayOffset == 0 && (targetHour < currentHour || (targetHour == currentHour && targetMinute <= currentMinute)))\n                continue; // skip if time has already passed today\n\n              int timeDifference = calculateTimeDifference(currentHour, currentMinute, targetHour + (dayOffset * 24), targetMinute);\n              if (timeDifference < minDifference) {\n                minDifference = timeDifference;\n                wakeupPreset = timerMacro[i];\n              }\n            }\n          }\n        }\n      }\n      return minDifference;\n    }\n\n  public:\n    inline void enable(bool enable) { enabled = enable; } // Enable/Disable the usermod\n    inline bool isEnabled() { return enabled; } //Get usermod enabled/disabled state\n\n    // setup is called at boot (or in this case after every exit of sleep mode)\n    void setup() {\n      //TODO: if the de-init of RTC pins is required to do it could be done here\n      //rtc_gpio_deinit(wakeupPin);\n      #ifdef WLED_DEBUG\n        DEBUG_PRINTF(\"sleep wakeup cause: %d\\n\", esp_sleep_get_wakeup_cause());\n      #endif\n      if (esp_sleep_get_wakeup_cause() != ESP_SLEEP_WAKEUP_TIMER)\n        wakeupPreset = 0; // not a timed wakeup, don't apply preset\n      initDone = true;\n    }\n\n    void loop() {\n      if (!enabled) return;\n      if (!offMode) { // LEDs are on\n        lastLoopTime = 0; // reset timer\n        if (delaycounter)\n          delaycounter--; // decrease delay counter if LEDs are on (they are always turned on after a wake-up, see below)\n        else if (wakeupPreset)\n          applyPreset(wakeupPreset); // apply preset if set, this ensures macro is applied even if we missed the wake-up time\n        return;\n      }\n\n      if (sleepDelay > 0) {\n        powerup = false; // disable \"safety\" powerup sleep if delay is set\n        if (lastLoopTime == 0)\n          lastLoopTime = millis(); // initialize\n        if (millis() - lastLoopTime < sleepDelay * 1000)\n          return; // wait until delay is over\n      } else if (powerup && delaycounter) {\n        delaycounter--; // on first boot without sleepDelay set, do not force-turn on\n        delay(1000);    // just in case: give user a short ~10s window to turn LEDs on in UI (delaycounter is 10 by default)\n        return;\n      }\n      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)\n        delaycounter--;\n        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)\n          if (briS == 0) bri = 10; // turn on and set low brightness to avoid automatic turn off\n          else bri = briS;\n          strip.setBrightness(bri); // needed to make handleIO() not turn off LEDs (really? does not help in bootup preset)\n          offMode = false;\n          applyPresetWithFallback(0, CALL_MODE_INIT, FX_MODE_STATIC, 0); // try to apply preset 0, fallback to static\n          if (rlyPin >= 0) {\n            digitalWrite(rlyPin, (rlyMde ? HIGH : LOW)); // turn relay on TODO: this should be done by wled, what function to call?\n          }\n        }\n        return;\n      }\n      DEBUG_PRINTLN(F(\"DeepSleep UM: entering deep sleep...\"));\n      powerup = false; // turn leds on in all subsequent bootups (overrides Turn LEDs on after power up/reset' at reboot)\n      if (!pin_is_valid(wakeupPin)) return;\n      esp_err_t halerror = ESP_OK;\n      pinMode(wakeupPin, INPUT); // make sure GPIO is input with pullup/pulldown disabled\n      esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); //disable all wake-up sources (just in case)\n\n      uint32_t wakeupAfterSec = 0;\n      if (presetWake) {\n        int nextInterval = findNextTimerInterval();\n        if (nextInterval > 1 && nextInterval < INT_MAX)\n          wakeupAfterSec = (nextInterval - 1) * 60; // wakeup before next preset\n      }\n      if (wakeupAfter > 0) { // user-defined interval\n        if (wakeupAfterSec == 0 || (uint32_t)wakeupAfter < wakeupAfterSec) {\n          wakeupAfterSec = wakeupAfter;\n        }\n      }\n      if (wakeupAfterSec > 0) {\n        esp_sleep_enable_timer_wakeup(wakeupAfterSec * (uint64_t)1e6);\n        DEBUG_PRINTF(\"wakeup after %d seconds\\n\", wakeupAfterSec);\n      }\n\n    #if defined(CONFIG_IDF_TARGET_ESP32C3) // ESP32 C3\n      gpio_hold_dis((gpio_num_t)wakeupPin); // disable hold and configure pin\n      if (wakeWhenHigh)\n        halerror = esp_deep_sleep_enable_gpio_wakeup(1<<wakeupPin, ESP_GPIO_WAKEUP_GPIO_HIGH);\n      else\n        halerror = esp_deep_sleep_enable_gpio_wakeup(1<<wakeupPin, ESP_GPIO_WAKEUP_GPIO_LOW);\n      // note: on C3 calling esp_deep_sleep_enable_gpio_wakeup() automatically enables pullup/pulldown unless we call gpio_hold_en() which overrides that\n      gpio_pullup_dis((gpio_num_t)wakeupPin); // disable pull resistors by default\n      gpio_pulldown_dis((gpio_num_t)wakeupPin);\n      if (!noPull)  {\n        if (wakeWhenHigh) {\n          gpio_pulldown_en((gpio_num_t)wakeupPin);\n        } else {\n          gpio_pullup_en((gpio_num_t)wakeupPin);\n        }\n      }\n      gpio_hold_en((gpio_num_t)wakeupPin); // hold the configured GPIO state during deep sleep, overrides the automatic pullup/pulldown, see note above\n    #else // ESP32, S2, S3\n      rtc_gpio_hold_dis((gpio_num_t)wakeupPin); // disable hold so we can (re)configure pin\n      rtc_gpio_init((gpio_num_t)wakeupPin);     // hand the pin over to RTC module\n      rtc_gpio_set_direction((gpio_num_t)wakeupPin, RTC_GPIO_MODE_INPUT_ONLY);\n      rtc_gpio_pullup_dis((gpio_num_t)wakeupPin); // disable pull resistors by default\n      rtc_gpio_pulldown_dis((gpio_num_t)wakeupPin);\n      if (!noPull) {\n        if (wakeWhenHigh)\n          rtc_gpio_pulldown_en((gpio_num_t)wakeupPin);\n        else\n          rtc_gpio_pullup_en((gpio_num_t)wakeupPin);\n      }\n      if (wakeWhenHigh)\n        halerror = esp_sleep_enable_ext1_wakeup(1ULL << wakeupPin, ESP_EXT1_WAKEUP_ANY_HIGH); // use ext1 as ext0 does not work with touch wakeup\n      else\n        halerror = esp_sleep_enable_ext1_wakeup(1ULL << wakeupPin, ESP_EXT1_WAKEUP_ALL_LOW);\n\n      if (enableTouchWakeup) {\n      #ifdef SOC_TOUCH_VERSION_2 // S2 and S3 use much higher thresholds, see notes in pin_manager\n        touchSleepWakeUpEnable(touchPin, touchThreshold  << 4); // ESP32 S2 & S3: lower threshold = more sensitive\n      #else\n        touchSleepWakeUpEnable(touchPin, touchThreshold); // ESP32: use normal threshold (higher = more sensitive)\n      #endif\n      }\n      delay(1); // wait for pins to be ready\n      rtc_gpio_hold_en((gpio_num_t)wakeupPin); // latch and hold the configured GPIO state during deep sleep\n    #endif\n      WiFi.mode(WIFI_OFF);  // Completely shut down the Wi-Fi module\n      if (halerror == ESP_OK) esp_deep_sleep_start(); // go into deep sleep\n      else DEBUG_PRINTLN(F(\"sleep failed\"));\n    }\n\n    //void connected() {} //unused, this is called every time the WiFi is (re)connected\n\n    void addToConfig(JsonObject& root) override\n    {\n      JsonObject top = root.createNestedObject(FPSTR(_name));\n      top[FPSTR(_enabled)] = enabled;\n      //save these vars persistently whenever settings are saved\n      top[\"gpio\"] = wakeupPin;\n      top[\"wakeWhen\"] = wakeWhenHigh;\n      top[\"pull\"] = noPull;\n    #ifndef CONFIG_IDF_TARGET_ESP32C3\n      top[\"enableTouchWakeup\"] = enableTouchWakeup;\n      top[\"touchPin\"] = touchPin;\n    #endif\n      top[\"presetWake\"] = presetWake;\n      top[\"wakeAfter\"] = wakeupAfter;\n      top[\"delaySleep\"] = sleepDelay;\n    }\n\n    bool readFromConfig(JsonObject& root) override\n    {\n      // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor\n      // 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)\n      JsonObject top = root[FPSTR(_name)];\n      bool configComplete = !top.isNull();\n\n      configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled);\n      configComplete &= getJsonValue(top[\"gpio\"], wakeupPin, DEEPSLEEP_WAKEUPPIN);\n      if (!pin_is_valid(wakeupPin)) {\n          wakeupPin = 0; // set to 0 if invalid\n          configComplete = false; // Mark config as incomplete if pin is invalid\n      }\n      configComplete &= getJsonValue(top[\"wakeWhen\"], wakeWhenHigh, DEEPSLEEP_WAKEWHENHIGH); // default to wake on low\n      configComplete &= getJsonValue(top[\"pull\"], noPull, DEEPSLEEP_DISABLEPULL); // default to no pullup/pulldown\n    #ifndef CONFIG_IDF_TARGET_ESP32C3\n      configComplete &= getJsonValue(top[\"enableTouchWakeup\"], enableTouchWakeup);\n      configComplete &= getJsonValue(top[\"touchPin\"], touchPin, DEEPSLEEP_WAKEUP_TOUCH_PIN);\n    #endif\n      configComplete &= getJsonValue(top[\"presetWake\"], presetWake);\n      configComplete &= getJsonValue(top[\"wakeAfter\"], wakeupAfter, DEEPSLEEP_WAKEUPINTERVAL);\n      configComplete &= getJsonValue(top[\"delaySleep\"], sleepDelay, DEEPSLEEP_DELAY);\n\n      return configComplete;\n    }\n\n    /*\n     * appendConfigData() is called when user enters usermod settings page\n     * it may add additional metadata for certain entry fields (adding drop down is possible)\n     * be careful not to add too much as oappend() buffer is limited to 3k\n     */\n    void appendConfigData() override\n    {\n      // dropdown for wakeupPin\n      oappend(SET_F(\"dd=addDropdown('DeepSleep','gpio');\"));\n      for (int pin = 0; pin < 40; pin++) { // possible pins are in range 0-39\n        if (pin_is_valid(pin)) {\n          oappend(SET_F(\"addOption(dd,'\"));\n          oappend(String(pin).c_str());\n          oappend(SET_F(\"',\"));\n          oappend(String(pin).c_str());\n          oappend(SET_F(\");\"));\n        }\n      }\n    #ifndef CONFIG_IDF_TARGET_ESP32C3\n      // dropdown for touch wakeupPin\n      oappend(SET_F(\"dd=addDropdown('DeepSleep','touchPin');\"));\n      for (int touchchannel = 0; touchchannel < SOC_TOUCH_SENSOR_NUM; touchchannel++) {\n        if (touch_sensor_channel_io_map[touchchannel] >= 0) {\n          oappend(SET_F(\"addOption(dd,'\"));\n          oappend(String(touch_sensor_channel_io_map[touchchannel]).c_str());\n          oappend(SET_F(\"',\"));\n          oappend(String(touch_sensor_channel_io_map[touchchannel]).c_str());\n          oappend(SET_F(\");\"));\n        }\n      }\n    #endif\n\n      oappend(SET_F(\"dd=addDropdown('DeepSleep','wakeWhen');\"));\n      oappend(SET_F(\"addOption(dd,'Low',0);\"));\n      oappend(SET_F(\"addOption(dd,'High',1);\"));\n\n      oappend(SET_F(\"addInfo('DeepSleep:pull',1,'','-up/down disable: ');\")); // first string is suffix, second string is prefix\n      oappend(SET_F(\"addInfo('DeepSleep:wakeAfter',1,'seconds <i>(0 = never)<i>');\"));\n      oappend(SET_F(\"addInfo('DeepSleep:presetWake',1,'<i>(wake up before next preset timer)<i>');\"));\n      oappend(SET_F(\"addInfo('DeepSleep:delaySleep',1,'seconds <i>(0 = sleep at powerup)<i>');\")); // first string is suffix, second string is prefix\n    }\n\n    /*\n     * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).\n     * This could be used in the future for the system to determine whether your usermod is installed.\n     */\n    uint16_t getId() {\n        return USERMOD_ID_DEEP_SLEEP;\n    }\n};\n\n// add more strings here to reduce flash memory usage\nconst char DeepSleepUsermod::_name[]    PROGMEM = \"DeepSleep\";\nconst char DeepSleepUsermod::_enabled[] PROGMEM = \"enabled\";\n\nstatic DeepSleepUsermod deep_sleep;\nREGISTER_USERMOD(deep_sleep);"
  },
  {
    "path": "usermods/deep_sleep/library.json",
    "content": "{\n  \"name\": \"deep_sleep\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/deep_sleep/readme.md",
    "content": "# Deep Sleep usermod\n\nThis 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.\nDuring 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.***\n\n# A word of warning\n\nWhen 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.\nIf 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.\n\n# Power Consumption in deep sleep\n\nThe 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):\n- ESP32: 10uA\n- ESP32 S3: 8uA\n- ESP32 S2: 20uA\n- ESP32 C3: 5uA\n- ESP8266: 20uA (not supported in this usermod)\n\nHowever, there is usually additional components on a controller that increase the value:\n- Power LED: the power LED on a ESP board draws 500uA - 1mA\n- 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\n- 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\n\nFor 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.\n\n# Useable GPIOs\n\nThe 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:\n\n- 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\n- ESP32 S3: GPIO 0-21\n- ESP32 S2: GPIO 0-21\n- ESP32 C3: GPIO 0-5\n- ESP8266 is not supported in this usermod\n\nYou 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.\n\n# Limitations\n\nTo 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.\n\n## Usermod installation\n\nAdd `deep_sleep` to `custom_usermods` in your platformio.ini. Settings can be changed in the usermod config UI.\n\n### Define Settings\n\nThere are five parameters you can set:\n\n- GPIO: the pin to use for wake-up\n- WakeWhen High/Low: the pin state that triggers the wake-up\n- Pull-up/down disable: enable or disable the internal pullup resistors during sleep (does not affect normal use while running)\n- 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)\n- 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.\n\nTo override the default settings, place the `#define` in wled.h or add `-D DEEPSLEEP_xxx` to your platformio_override.ini build flags\n\n* `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.\n* `DEEPSLEEP_WAKEWHENHIGH`   - if defined, wakes up when pin goes high (default is low)\n* `DEEPSLEEP_DISABLEPULL`    - if defined, internal pullup/pulldown is disabled in deep sleep (default is ebnabled)\n* `DEEPSLEEP_WAKEUPINTERVAL` - number of seconds after which a wake-up happens automatically, sooner if button is pressed. 0 = never. accuracy is about 2%\n* `DEEPSLEEP_DELAY`          - delay between power-off and sleep\n* `DEEPSLEEP_WAKEUP_TOUCH_PIN` - specify GPIO pin used for touch-based wakeup\n\nexample for env build flags:\n `-D USERMOD_DEEP_SLEEP`\n `-D DEEPSLEEP_WAKEUPPIN=4`\n `-D DEEPSLEEP_DISABLEPULL=0` ;enable pull-up/down resistors by default\n `-D DEEPSLEEP_WAKEUPINTERVAL=43200` ;wake up after 12 hours (or when button is pressed)\n\n### Hardware Setup\n\nTo 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.\n\nUsing sensors like PIR, IR, touch sensors or any other sensor with a digital output can be used instead of a button.\n\nnow go on and save some power\n@dedehai\n\n## Change log\n2024-09\n* Initial version\n2024-10\n* Changed from #define configuration to UI configuration"
  },
  {
    "path": "usermods/mpu6050_imu/library.json",
    "content": "{\n  \"name\": \"mpu6050_imu\",\n  \"build\": { \"libArchive\": false},\n  \"dependencies\": {\n    \"electroniccats/MPU6050\":\"1.0.1\"\n  }\n}\n"
  },
  {
    "path": "usermods/mpu6050_imu/mpu6050_imu.cpp",
    "content": "#include \"wled.h\"\n\n/* This driver reads quaternion data from the MPU6060 and adds it to the JSON\n   This example is adapted from:\n   https://github.com/jrowberg/i2cdevlib/tree/master/Arduino/MPU6050/examples/MPU6050_DMP6_ESPWiFi\n\n   Tested with a d1 mini esp-12f\n\n  GY-521  NodeMCU\n  MPU6050 devkit 1.0\n  board   Lolin         Description\n  ======= ==========    ====================================================\n  VCC     VU (5V USB)   Not available on all boards so use 3.3V if needed.\n  GND     G             Ground\n  SCL     D1 (GPIO05)   I2C clock\n  SDA     D2 (GPIO04)   I2C data\n  XDA     not connected\n  XCL     not connected\n  AD0     not connected\n  INT     D8 (GPIO15)   Interrupt pin\n\n  Using usermod:\n  1. Copy the usermod into the sketch folder (same folder as wled00.ino)\n  2. Register the usermod by adding #include \"usermod_filename.h\" in the top and registerUsermod(new MyUsermodClass()) in the bottom of usermods_list.cpp\n  3. I2Cdev and MPU6050 must be installed as libraries, or else the .cpp/.h file\n     for both classes must be in the include path of your project. To install the\n     libraries add I2Cdevlib-MPU6050@fbde122cc5 to lib_deps in the platformio.ini file.\n  4. You also need to change lib_compat_mode from strict to soft in platformio.ini (This ignores that I2Cdevlib-MPU6050 doesn't list platform compatibility)\n  5. Wire up the MPU6050 as detailed above.\n*/\n\n#include \"I2Cdev.h\"\n\n#undef DEBUG_PRINT\n#undef DEBUG_PRINTLN\n#undef DEBUG_PRINTF\n#include \"MPU6050_6Axis_MotionApps20.h\"\n//#include \"MPU6050.h\" // not necessary if using MotionApps include file\n\n// Arduino Wire library is required if I2Cdev I2CDEV_ARDUINO_WIRE implementation\n// is used in I2Cdev.h\n#if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE\n    #include \"Wire.h\"\n#endif\n\n// Restore debug macros\n// MPU6050 unfortunately uses the same macro names as WLED :(\n#undef DEBUG_PRINT\n#undef DEBUG_PRINTLN\n#undef DEBUG_PRINTF\n#ifdef WLED_DEBUG\n  #define DEBUG_PRINT(x) DEBUGOUT.print(x)\n  #define DEBUG_PRINTLN(x) DEBUGOUT.println(x)\n  #define DEBUG_PRINTF(x...) DEBUGOUT.printf(x)\n#else\n  #define DEBUG_PRINT(x)\n  #define DEBUG_PRINTLN(x)\n  #define DEBUG_PRINTF(x...)\n#endif\n\n\n\n// ================================================================\n// ===               INTERRUPT DETECTION ROUTINE                ===\n// ================================================================\n\nvolatile bool mpuInterrupt = false;     // indicates whether MPU interrupt pin has gone high\nvoid IRAM_ATTR dmpDataReady() {\n    mpuInterrupt = true;\n}\n\n\n\nclass MPU6050Driver : public Usermod {\n  private:\n    MPU6050 mpu;\n\n    // configuration state\n    // default values are set in readFromConfig\n    // By making this a struct, we enable easy backup and comparison in the readFromConfig class\n    struct config_t {\n      bool enabled;\n      int8_t interruptPin;    \n      int16_t gyro_offset[3]; \n      int16_t accel_offset[3];\n    };\n    config_t config;\n    bool configDirty = true; // does the configuration need an update?\n\n    // MPU control/status vars\n    bool irqBound = false; // set true if we have bound the IRQ pin\n    bool dmpReady = false;  // set true if DMP init was successful\n    uint16_t packetSize;    // expected DMP packet size (default is 42 bytes)\n    uint16_t fifoCount;     // count of all bytes currently in FIFO\n    uint8_t fifoBuffer[64]; // FIFO storage buffer\n\n    // TODO: some of these can be removed to save memory, processing time if the measurement isn't needed\n    Quaternion qat;         // [w, x, y, z]         quaternion container\n    float euler[3];         // [psi, theta, phi]    Euler angle container\n    float ypr[3];           // [yaw, pitch, roll]   yaw/pitch/roll container\n    VectorInt16 aa;         // [x, y, z]            accel sensor measurements\n    VectorInt16 gy;         // [x, y, z]            gyro sensor measurements\n    VectorInt16 aaReal;     // [x, y, z]            gravity-free accel sensor measurements\n    VectorInt16 aaWorld;    // [x, y, z]            world-frame accel sensor measurements\n    VectorFloat gravity;    // [x, y, z]            gravity vector\n    uint32_t sample_count;\n\n    // Usermod output\n    um_data_t um_data;\n\n    // config element names as progmem strs\n    static const char _name[];\n    static const char _enabled[];\n    static const char _interrupt_pin[];\n    static const char _x_acc_bias[];\n    static const char _y_acc_bias[];\n    static const char _z_acc_bias[];\n    static const char _x_gyro_bias[];\n    static const char _y_gyro_bias[];\n    static const char _z_gyro_bias[];\n\n  public:\n\n    inline bool initDone() { return um_data.u_size != 0; };   // recycle this instead of storing an extra variable\n\n    //Functions called by WLED\n    /*\n     * setup() is called once at boot. WiFi is not yet connected at this point.\n     */\n    void setup() {\n      dmpReady = false;   // Start clean\n\n      // one time init\n      if (!initDone()) {\n        um_data.u_size = 9;\n        um_data.u_type = new um_types_t[um_data.u_size];\n        um_data.u_data = new void*[um_data.u_size];\n        um_data.u_data[0] = &qat;\n        um_data.u_type[0] = UMT_FLOAT_ARR;\n        um_data.u_data[1] = &euler;\n        um_data.u_type[1] = UMT_FLOAT_ARR;\n        um_data.u_data[2] = &ypr;\n        um_data.u_type[2] = UMT_FLOAT_ARR;\n        um_data.u_data[3] = &aa;\n        um_data.u_type[3] = UMT_INT16_ARR;\n        um_data.u_data[4] = &gy;\n        um_data.u_type[4] = UMT_INT16_ARR;\n        um_data.u_data[5] = &aaReal;\n        um_data.u_type[5] = UMT_INT16_ARR;\n        um_data.u_data[6] = &aaWorld;\n        um_data.u_type[6] = UMT_INT16_ARR;\n        um_data.u_data[7] = &gravity;\n        um_data.u_type[7] = UMT_FLOAT_ARR;\n        um_data.u_data[8] = &sample_count;\n        um_data.u_type[8] = UMT_UINT32;\n      }\n\n      configDirty = false;  // we have now accepted the current configuration, success or not\n      \n      if (!config.enabled) return;\n      // TODO: notice if these have changed ??\n      if (i2c_scl<0 || i2c_sda<0) { DEBUG_PRINTLN(F(\"MPU6050: I2C is no good.\"));  return; }\n      // Check the interrupt pin\n      if (config.interruptPin >= 0) {\n        irqBound = PinManager::allocatePin(config.interruptPin, false, PinOwner::UM_IMU);\n        if (!irqBound) { DEBUG_PRINTLN(F(\"MPU6050: IRQ pin already in use.\")); return; }\n        pinMode(config.interruptPin, INPUT);\n      };\n\n      #if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE\n        Wire.setClock(400000U); // 400kHz I2C clock. Comment this line if having compilation difficulties\n      #elif I2CDEV_IMPLEMENTATION == I2CDEV_BUILTIN_FASTWIRE\n        Fastwire::setup(400, true);\n      #endif\n\n      // initialize device\n      DEBUG_PRINTLN(F(\"Initializing I2C devices...\"));\n      mpu.initialize();\n\n      // verify connection\n      DEBUG_PRINTLN(F(\"Testing device connections...\"));\n      DEBUG_PRINTLN(mpu.testConnection() ? F(\"MPU6050 connection successful\") : F(\"MPU6050 connection failed\"));\n\n      // load and configure the DMP\n      DEBUG_PRINTLN(F(\"Initializing DMP...\"));\n      auto devStatus = mpu.dmpInitialize();\n\n      // set offsets (from config)\n      mpu.setXGyroOffset(config.gyro_offset[0]);\n      mpu.setYGyroOffset(config.gyro_offset[1]);\n      mpu.setZGyroOffset(config.gyro_offset[2]);\n      mpu.setXAccelOffset(config.accel_offset[0]);\n      mpu.setYAccelOffset(config.accel_offset[1]);\n      mpu.setZAccelOffset(config.accel_offset[2]);\n\n      // set sample rate\n      mpu.setRate(16);  // ~100Hz\n\n      // make sure it worked (returns 0 if so)\n      if (devStatus == 0) {\n        // turn on the DMP, now that it's ready\n        DEBUG_PRINTLN(F(\"Enabling DMP...\"));\n        mpu.setDMPEnabled(true);\n\n        mpuInterrupt = true;\n        if (irqBound) {\n          // enable Arduino interrupt detection\n          DEBUG_PRINTLN(F(\"Enabling interrupt detection (Arduino external interrupt 0)...\"));          \n          attachInterrupt(digitalPinToInterrupt(config.interruptPin), dmpDataReady, RISING);\n        }\n\n        // get expected DMP packet size for later comparison\n        packetSize = mpu.dmpGetFIFOPacketSize();\n\n        // set our DMP Ready flag so the main loop() function knows it's okay to use it\n        DEBUG_PRINTLN(F(\"DMP ready!\"));\n        dmpReady = true;\n      } else {\n        // ERROR!\n        // 1 = initial memory load failed\n        // 2 = DMP configuration updates failed\n        // (if it's going to break, usually the code will be 1)\n        DEBUG_PRINT(F(\"DMP Initialization failed (code \"));\n        DEBUG_PRINT(devStatus);\n        DEBUG_PRINTLN(\")\");\n      }\n\n      fifoCount = 0;\n      sample_count = 0;\n    }\n\n    /*\n     * connected() is called every time the WiFi is (re)connected\n     * Use it to initialize network interfaces\n     */\n    void connected() {\n      //DEBUG_PRINTLN(F(\"Connected to WiFi!\"));\n    }\n\n\n    /*\n     * loop() is called continuously. Here you can check for events, read sensors, etc.\n     */\n    void loop() {\n      if (configDirty) setup();\n\n      // if programming failed, don't try to do anything\n      if (!config.enabled || !dmpReady || strip.isUpdating()) return;\n\n      // wait for MPU interrupt or extra packet(s) available\n      // mpuInterrupt is fixed on if interrupt pin is disabled\n      if (!mpuInterrupt && fifoCount < packetSize) return;\n\n      // reset interrupt flag and get INT_STATUS byte\n      auto mpuIntStatus = mpu.getIntStatus();\n      // Update current FIFO count\n      fifoCount = mpu.getFIFOCount();\n\n      // check for overflow (this should never happen unless our code is too inefficient)\n      if ((mpuIntStatus & 0x10) || fifoCount == 1024) {\n        // reset so we can continue cleanly\n        mpu.resetFIFO();\n        DEBUG_PRINTLN(F(\"MPU6050: FIFO overflow!\"));\n\n        // otherwise, check for data ready\n      } else if (fifoCount >= packetSize) {\n        // clear local interrupt pending status, if not polling\n        mpuInterrupt = !irqBound;\n\n        // DEBUG_PRINT(F(\"MPU6050: Processing packet: \"));\n        // DEBUG_PRINT(fifoCount);\n        // DEBUG_PRINTLN(F(\" bytes in FIFO\"));\n\n        // read a packet from FIFO\n        mpu.getFIFOBytes(fifoBuffer, packetSize);\n\n        // track FIFO count here in case there is > 1 packet available\n        // (this lets us immediately read more without waiting for an interrupt)\n        fifoCount -= packetSize;\n\n        //NOTE: some of these can be removed to save memory, processing time\n        //      if the measurement isn't needed\n        mpu.dmpGetQuaternion(&qat, fifoBuffer);\n        mpu.dmpGetEuler(euler, &qat);\n        mpu.dmpGetGravity(&gravity, &qat);\n        mpu.dmpGetGyro(&gy, fifoBuffer);\n        mpu.dmpGetAccel(&aa, fifoBuffer);\n        mpu.dmpGetLinearAccel(&aaReal, &aa, &gravity);\n        mpu.dmpGetLinearAccelInWorld(&aaWorld, &aaReal, &qat);\n        mpu.dmpGetYawPitchRoll(ypr, &qat, &gravity);\n        ++sample_count;\n      }\n    }\n\n    void addToJsonInfo(JsonObject& root)\n    {\n      JsonObject user = root[\"u\"];\n      if (user.isNull()) user = root.createNestedObject(\"u\");\n\n      // Unfortunately the web UI doesn't know how to print sub-objects: you just see '[object Object]'\n      // For now, we just put everything in the root userdata object.\n      //auto imu_meas = user.createNestedObject(\"IMU\");\n      auto& imu_meas = user;\n\n      // If an element is an array, the UI expects two elements in the form [value, unit]\n      // Since our /value/ is an array, wrap it, eg. [[a, b, c]]\n      JsonArray quat_json = imu_meas.createNestedArray(\"Quat\").createNestedArray();\n      quat_json.add(qat.w);\n      quat_json.add(qat.x);\n      quat_json.add(qat.y);\n      quat_json.add(qat.z);\n      JsonArray euler_json = imu_meas.createNestedArray(\"Euler\").createNestedArray();\n      euler_json.add(euler[0]);\n      euler_json.add(euler[1]);\n      euler_json.add(euler[2]);\n      JsonArray accel_json = imu_meas.createNestedArray(\"Accel\").createNestedArray();\n      accel_json.add(aa.x);\n      accel_json.add(aa.y);\n      accel_json.add(aa.z);\n      JsonArray gyro_json = imu_meas.createNestedArray(\"Gyro\").createNestedArray();\n      gyro_json.add(gy.x);\n      gyro_json.add(gy.y);\n      gyro_json.add(gy.z);\n      JsonArray world_json = imu_meas.createNestedArray(\"WorldAccel\").createNestedArray();\n      world_json.add(aaWorld.x);\n      world_json.add(aaWorld.y);\n      world_json.add(aaWorld.z);\n      JsonArray real_json = imu_meas.createNestedArray(\"RealAccel\").createNestedArray();\n      real_json.add(aaReal.x);\n      real_json.add(aaReal.y);\n      real_json.add(aaReal.z);\n      JsonArray grav_json = imu_meas.createNestedArray(\"Gravity\").createNestedArray();\n      grav_json.add(gravity.x);\n      grav_json.add(gravity.y);\n      grav_json.add(gravity.z);\n      JsonArray orient_json = imu_meas.createNestedArray(\"Orientation\").createNestedArray();\n      orient_json.add(ypr[0]);\n      orient_json.add(ypr[1]);\n      orient_json.add(ypr[2]);\n    }\n\n\n    /*\n     * addToConfig() can be used to add custom persistent settings to the cfg.json file in the \"um\" (usermod) object.\n     * It will be called by WLED when settings are actually saved (for example, LED settings are saved)\n     * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings!\n     */\n    void addToConfig(JsonObject& root)\n    {\n      JsonObject top = root.createNestedObject(FPSTR(_name));\n\n      //save these vars persistently whenever settings are saved\n      top[FPSTR(_enabled)] = config.enabled;\n      top[FPSTR(_interrupt_pin)] = config.interruptPin;\n      top[FPSTR(_x_acc_bias)] = config.accel_offset[0];\n      top[FPSTR(_y_acc_bias)] = config.accel_offset[1];\n      top[FPSTR(_z_acc_bias)] = config.accel_offset[2];\n      top[FPSTR(_x_gyro_bias)] = config.gyro_offset[0];\n      top[FPSTR(_y_gyro_bias)] = config.gyro_offset[1];\n      top[FPSTR(_z_gyro_bias)] = config.gyro_offset[2];\n    }\n\n    /*\n     * readFromConfig() can be used to read back the custom settings you added with addToConfig().\n     * This is called by WLED when settings are loaded (currently this only happens immediately after boot, or after saving on the Usermod Settings page)\n     *\n     * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes),\n     * 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.\n     * If you don't know what that is, don't fret. It most likely doesn't affect your use case :)\n     *\n     * Return true in case the config values returned from Usermod Settings were complete, or false if you'd like WLED to save your defaults to disk (so any missing values are editable in Usermod Settings)\n     *\n     * getJsonValue() returns false if the value is missing, or copies the value into the variable provided and returns true if the value is present\n     * The configComplete variable is true only if the \"exampleUsermod\" object and all values are present.  If any values are missing, WLED will know to call addToConfig() to save them\n     *\n     * This function is guaranteed to be called on boot, but could also be called every time settings are updated\n     */\n    bool readFromConfig(JsonObject& root)\n    {\n      // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor\n      // 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)\n      auto old_cfg = config;\n\n      JsonObject top = root[FPSTR(_name)];\n\n      bool configComplete = top.isNull();\n      // Ensure default configuration is loaded\n      configComplete &= getJsonValue(top[FPSTR(_enabled)], config.enabled, true);\n      configComplete &= getJsonValue(top[FPSTR(_interrupt_pin)], config.interruptPin, -1);\n      configComplete &= getJsonValue(top[FPSTR(_x_acc_bias)], config.accel_offset[0], 0);\n      configComplete &= getJsonValue(top[FPSTR(_y_acc_bias)], config.accel_offset[1], 0);\n      configComplete &= getJsonValue(top[FPSTR(_z_acc_bias)], config.accel_offset[2], 0);\n      configComplete &= getJsonValue(top[FPSTR(_x_gyro_bias)], config.gyro_offset[0], 0);\n      configComplete &= getJsonValue(top[FPSTR(_y_gyro_bias)], config.gyro_offset[1], 0);\n      configComplete &= getJsonValue(top[FPSTR(_z_gyro_bias)], config.gyro_offset[2], 0);\n\n      DEBUG_PRINT(FPSTR(_name));\n      if (top.isNull()) {\n        DEBUG_PRINTLN(F(\": No config found. (Using defaults.)\"));\n      } else if (!initDone()) {\n        DEBUG_PRINTLN(F(\": config loaded.\"));\n      } else if (memcmp(&config, &old_cfg, sizeof(config)) == 0) {\n        DEBUG_PRINTLN(F(\": config unchanged.\"));\n      } else {\n        DEBUG_PRINTLN(F(\": config updated.\"));\n        // Previously loaded and config changed\n        if (irqBound && ((old_cfg.interruptPin != config.interruptPin) || !config.enabled)) {\n          detachInterrupt(old_cfg.interruptPin);\n          PinManager::deallocatePin(old_cfg.interruptPin, PinOwner::UM_IMU);            \n          irqBound = false;\n        }\n\n        // Re-call setup on the next loop()\n        configDirty = true;\n      }\n\n      return configComplete;\n    }\n\n    bool getUMData(um_data_t **data)\n    {\n      if (!data || !config.enabled || !dmpReady) return false; // no pointer provided by caller or not enabled -> exit\n      *data = &um_data;\n      return true;\n    }\n\n    /*\n     * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).\n     */\n    uint16_t getId()\n    {\n      return USERMOD_ID_IMU;\n    }\n\n};\n\n\nconst char MPU6050Driver::_name[] PROGMEM = \"MPU6050_IMU\";\nconst char MPU6050Driver::_enabled[] PROGMEM = \"enabled\";\nconst char MPU6050Driver::_interrupt_pin[] PROGMEM = \"interrupt_pin\";\nconst char MPU6050Driver::_x_acc_bias[] PROGMEM = \"x_acc_bias\";\nconst char MPU6050Driver::_y_acc_bias[] PROGMEM = \"y_acc_bias\";\nconst char MPU6050Driver::_z_acc_bias[] PROGMEM = \"z_acc_bias\";\nconst char MPU6050Driver::_x_gyro_bias[] PROGMEM = \"x_gyro_bias\";\nconst char MPU6050Driver::_y_gyro_bias[] PROGMEM = \"y_gyro_bias\";\nconst char MPU6050Driver::_z_gyro_bias[] PROGMEM = \"z_gyro_bias\";\n\n\nstatic MPU6050Driver mpu6050_imu;\nREGISTER_USERMOD(mpu6050_imu);"
  },
  {
    "path": "usermods/mpu6050_imu/readme.md",
    "content": "# MPU-6050 Six-Axis (Gyro + Accelerometer) Driver\n\nv2 of this usermod enables connection of a MPU-6050 IMU sensor to\nwork with effects controlled by the orientation or motion of the WLED Device.\n\nThe MPU6050 has a built in \"Digital Motion Processor\" which does the \"heavy lifting\"\nintegrating the gyro and accelerometer measurements to get potentially more\nuseful gravity vector and orientation output.\n\nIt is fairly straightforward to comment out variables being read from the device if they're not needed. Saves CPU/Memory/Bandwidth.\n\n_Story:_\n\nAs a memento to a long trip I was on, I built an icosahedron globe. I put lights inside to indicate cities I travelled to.\n\nI wanted to integrate an IMU to allow either on-board, or off-board effects that would\nreact to the globes orientation. See the blog post on building it <https://www.robopenguins.com/icosahedron-travel-globe/> or a video demo <https://youtu.be/zYjybxHBsHM> .\n\n## Wiring\n\nThe connections needed to the MPU6050 are as follows:\n```\n  VCC     VU (5V USB)   Not available on all boards so use 3.3V if needed.\n  GND     G             Ground\n  SCL     D1 (GPIO05)   I2C clock\n  SDA     D2 (GPIO04)   I2C data\n  XDA     not connected\n  XCL     not connected\n  AD0     not connected\n  INT     D8 (GPIO15)   Interrupt pin\n```\n\nYou could probably modify the code not to need an interrupt, but I used the\nsetup directly from the example.\n\n## JSON API\n\nThis code adds:\n```json\n\"u\":{\n  \"IMU\":{\n    \"Quat\":        [w, x, y, z],\n    \"Euler\":       [psi, theta, phi],\n    \"Gyro\":        [x, y, z],\n    \"Accel\":       [x, y, z],\n    \"RealAccel\":   [x, y, z],\n    \"WorldAccel\":  [x, y, z],\n    \"Gravity\":     [x, y, z],\n    \"Orientation\": [yaw, pitch, roll]\n  }\n}\n```\nto the info object\n\n## Usermod installation\n\nAdd `mpu6050_imu` to `custom_usermods` in your platformio_override.ini.\n\nExample **platformio_override.ini**:\n\n```ini\n[env:usermod_mpu6050_imu_esp32dev]\nextends = env:esp32dev\ncustom_usermods = ${env:esp32dev.custom_usermods} \n  mpu6050_imu\n```\n"
  },
  {
    "path": "usermods/mpu6050_imu/usermod_gyro_surge.h",
    "content": "#pragma once\n\n/* This usermod uses gyro data to provide a \"surge\" effect on movement\n\nRequires lib_deps = bolderflight/Bolder Flight Systems Eigen@^3.0.0\n\n*/\n\n#include \"wled.h\"\n\n// Eigen include block\n#ifdef A0\nnamespace { constexpr size_t A0_temp {A0}; }\n#undef A0\nstatic constexpr size_t A0 {A0_temp};\n#endif\n\n#ifdef A1\nnamespace { constexpr size_t A1_temp {A1}; }\n#undef A1\nstatic constexpr size_t A1 {A1_temp};\n#endif\n\n#ifdef B0\nnamespace { constexpr size_t B0_temp {B0}; }\n#undef B0\nstatic constexpr size_t B0 {B0_temp};\n#endif\n\n#ifdef B1\nnamespace { constexpr size_t B1_temp {B1}; }\n#undef B1\nstatic constexpr size_t B1 {B1_temp};\n#endif\n\n#ifdef D0\nnamespace { constexpr size_t D0_temp {D0}; }\n#undef D0\nstatic constexpr size_t D0 {D0_temp};\n#endif\n\n#ifdef D1\nnamespace { constexpr size_t D1_temp {D1}; }\n#undef D1\nstatic constexpr size_t D1 {D1_temp};\n#endif\n\n#ifdef D2\nnamespace { constexpr size_t D2_temp {D2}; }\n#undef D2\nstatic constexpr size_t D2 {D2_temp};\n#endif\n\n#ifdef D3\nnamespace { constexpr size_t D3_temp {D3}; }\n#undef D3\nstatic constexpr size_t D3 {D3_temp};\n#endif\n\n#include \"eigen.h\"\n#include <Eigen/Geometry>\n\nconstexpr auto ESTIMATED_G = 9.801;  // m/s^2\nconstexpr auto ESTIMATED_G_COUNTS = 8350.;\nconstexpr auto ESTIMATED_ANGULAR_RATE = (M_PI * 2000) / (INT16_MAX * 180); // radians per second\n\n// Horribly lame digital filter code\n// Currently implements a static IIR filter.\ntemplate<typename T, unsigned C>\nclass xir_filter {\n    typedef Eigen::Array<T, C, 1> array_t;\n    const array_t a_coeff, b_coeff;\n    const T gain;\n    array_t x, y;\n\n    public:\n    xir_filter(T gain_, array_t a, array_t b) : a_coeff(std::move(a)), b_coeff(std::move(b)), gain(gain_), x(array_t::Zero()), y(array_t::Zero()) {};\n\n    T operator()(T input) {\n        x.head(C-1) = x.tail(C-1);  // shift by one\n        x(C-1) = input / gain;\n        y.head(C-1) = y.tail(C-1);  // shift by one\n        y(C-1) = (x * b_coeff).sum();\n        y(C-1) -= (y.head(C-1) * a_coeff.head(C-1)).sum();\n        return y(C-1);\n    }\n\n    T last() { return y(C-1); };\n};\n\n\n\nclass GyroSurge : public Usermod {\n  private:\n    static const char _name[];\n    bool enabled = true;\n\n    // Params\n    uint8_t max = 0;\n    float sensitivity = 0;\n\n    // State\n    uint32_t last_sample;\n    // 100hz input\n    // butterworth low pass filter at 20hz\n    xir_filter<float, 3> filter = { 1., { -0.36952738, 0.19581571, 1.}, {0.20657208, 0.41314417, 0.20657208} };\n                                  // { 1., { 0., 0., 1.}, { 0., 0., 1. } }; // no filter\n\n\n  public:\n\n    /*\n     * setup() is called once at boot. WiFi is not yet connected at this point.\n     */\n    void setup() {};\n\n\n    /*\n     * addToConfig() can be used to add custom persistent settings to the cfg.json file in the \"um\" (usermod) object.\n     * It will be called by WLED when settings are actually saved (for example, LED settings are saved)\n     * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings!\n     */\n    void addToConfig(JsonObject& root)\n    {\n      JsonObject top = root.createNestedObject(FPSTR(_name));\n\n      //save these vars persistently whenever settings are saved\n      top[\"max\"] = max;\n      top[\"sensitivity\"] = sensitivity;\n    }\n\n\n    /*\n     * readFromConfig() can be used to read back the custom settings you added with addToConfig().\n     * This is called by WLED when settings are loaded (currently this only happens immediately after boot, or after saving on the Usermod Settings page)\n     * \n     * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes),\n     * 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.\n     * If you don't know what that is, don't fret. It most likely doesn't affect your use case :)\n     * \n     * Return true in case the config values returned from Usermod Settings were complete, or false if you'd like WLED to save your defaults to disk (so any missing values are editable in Usermod Settings)\n     * \n     * getJsonValue() returns false if the value is missing, or copies the value into the variable provided and returns true if the value is present\n     * The configComplete variable is true only if the \"exampleUsermod\" object and all values are present.  If any values are missing, WLED will know to call addToConfig() to save them\n     * \n     * This function is guaranteed to be called on boot, but could also be called every time settings are updated\n     */\n    bool readFromConfig(JsonObject& root)\n    {\n      // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor\n      // 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)\n\n      JsonObject top = root[FPSTR(_name)];\n\n      bool configComplete = !top.isNull();\n\n      configComplete &= getJsonValue(top[\"max\"], max, 0);\n      configComplete &= getJsonValue(top[\"sensitivity\"], sensitivity, 10);\n\n      return configComplete;\n    }\n\n    void loop() {\n      // get IMU data\n      um_data_t *um_data;\n      if (!UsermodManager::getUMData(&um_data, USERMOD_ID_IMU)) {\n        // Apply max\n        strip.getSegment(0).fadeToBlackBy(max);\n        return;\n      }\n      uint32_t sample_count = *(uint32_t*)(um_data->u_data[8]);\n\n      if (sample_count != last_sample) {        \n        last_sample = sample_count;\n        // Calculate based on new data\n        // We use the raw gyro data (angular rate)        \n        auto gyros = (int16_t*)um_data->u_data[4];  // 16384 == 2000 deg/s\n\n        // Compute the overall rotation rate\n        // For my application (a plasma sword) we ignore X axis rotations (eg. around the long axis)\n        auto gyro_q = Eigen::AngleAxis<float> {\n                        //Eigen::AngleAxis<float>(ESTIMATED_ANGULAR_RATE * gyros[0], Eigen::Vector3f::UnitX()) *\n                        Eigen::AngleAxis<float>(ESTIMATED_ANGULAR_RATE * gyros[1], Eigen::Vector3f::UnitY()) *\n                        Eigen::AngleAxis<float>(ESTIMATED_ANGULAR_RATE * gyros[2], Eigen::Vector3f::UnitZ()) };\n        \n        // Filter the results\n        filter(std::min(sensitivity * gyro_q.angle(), 1.0f));   // radians per second\n/*\n        Serial.printf(\"[%lu] Gy: %d, %d, %d -- \", millis(), (int)gyros[0], (int)gyros[1], (int)gyros[2]);\n        Serial.print(gyro_q.angle());\n        Serial.print(\", \");\n        Serial.print(sensitivity * gyro_q.angle());\n        Serial.print(\" --> \");\n        Serial.println(filter.last());\n*/\n      }\n    }; // noop\n\n    /*\n     * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors.\n     * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode.\n     * Commonly used for custom clocks (Cronixie, 7 segment)\n     */\n    void handleOverlayDraw()\n    {\n\n      // TODO: some kind of timing analysis for filtering ...\n\n      // Calculate brightness boost\n      auto r_float = std::max(std::min(filter.last(), 1.0f), 0.f);\n      auto result = (uint8_t) (r_float * max);\n      //Serial.printf(\"[%lu] %d -- \", millis(), result);\n      //Serial.println(r_float);\n      // TODO - multiple segment handling??\n      strip.getSegment(0).fadeToBlackBy(max - result);\n    }\n};\n\nconst char GyroSurge::_name[] PROGMEM = \"GyroSurge\";"
  },
  {
    "path": "usermods/multi_relay/library.json",
    "content": "{\n  \"name\": \"multi_relay\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/multi_relay/multi_relay.cpp",
    "content": "#include \"wled.h\"\n\n#define COUNT_OF(x) ((sizeof(x)/sizeof(0[x])) / ((size_t)(!(sizeof(x) % sizeof(0[x])))))\n\n#ifndef MULTI_RELAY_MAX_RELAYS\n  #define MULTI_RELAY_MAX_RELAYS 4\n#else\n  #if MULTI_RELAY_MAX_RELAYS>8\n    #undef MULTI_RELAY_MAX_RELAYS\n    #define MULTI_RELAY_MAX_RELAYS 8\n    #warning Maximum relays set to 8\n  #endif\n#endif\n\n#ifndef MULTI_RELAY_PINS\n  #define MULTI_RELAY_PINS -1\n  #define MULTI_RELAY_ENABLED false\n#else\n  #define MULTI_RELAY_ENABLED true\n#endif\n\n#ifndef MULTI_RELAY_HA_DISCOVERY\n  #define MULTI_RELAY_HA_DISCOVERY false\n#endif\n\n#ifndef MULTI_RELAY_DELAYS\n  #define MULTI_RELAY_DELAYS 0\n#endif\n\n#ifndef MULTI_RELAY_EXTERNALS\n  #define MULTI_RELAY_EXTERNALS false\n#endif\n\n#ifndef MULTI_RELAY_INVERTS\n  #define MULTI_RELAY_INVERTS false\n#endif\n\n#define WLED_DEBOUNCE_THRESHOLD 50 //only consider button input of at least 50ms as valid (debouncing)\n\n#define ON  true\n#define OFF false\n\n#ifndef USERMOD_USE_PCF8574\n  #undef USE_PCF8574\n  #define USE_PCF8574 false\n#else\n  #undef USE_PCF8574\n  #define USE_PCF8574 true\n#endif\n\n#ifndef PCF8574_ADDRESS\n  #define PCF8574_ADDRESS 0x20  // some may start at 0x38\n#endif\n\n/*\n * This usermod handles multiple relay outputs.\n * These outputs complement built-in relay output in a way that the activation can be delayed.\n * They can also activate/deactivate in reverse logic independently.\n * \n * Written and maintained by @blazoncek\n */\n\n\ntypedef struct relay_t {\n  int8_t pin;\n  struct { // reduces memory footprint\n    bool active   : 1;  // is the relay waiting to be switched\n    bool invert   : 1;  // does On mean 1 or 0\n    bool state    : 1;  // 1 relay is On, 0 relay is Off\n    bool external : 1;  // is the relay externally controlled\n    int8_t button : 4;  // which button triggers relay\n  };\n  uint16_t delay;       // amount of ms to wait after it is activated\n} Relay;\n\n\nclass MultiRelay : public Usermod {\n\n  private:\n    // array of relays\n    Relay    _relay[MULTI_RELAY_MAX_RELAYS];\n\n    uint32_t _switchTimerStart; // switch timer start time\n    bool     _oldMode;          // old brightness\n    bool     enabled;           // usermod enabled\n    bool     initDone;          // status of initialisation\n    bool     usePcf8574;\n    uint8_t  addrPcf8574;\n    bool     HAautodiscovery;\n    uint16_t periodicBroadcastSec;\n    unsigned long lastBroadcast;\n\n    // strings to reduce flash memory usage (used more than twice)\n    static const char _name[];\n    static const char _enabled[];\n    static const char _relay_str[];\n    static const char _delay_str[];\n    static const char _activeHigh[];\n    static const char _external[];\n    static const char _button[];\n    static const char _broadcast[];\n    static const char _HAautodiscovery[];\n    static const char _pcf8574[];\n    static const char _pcfAddress[];\n    static const char _switch[];\n    static const char _toggle[];\n    static const char _Command[];\n\n    void handleOffTimer();\n    void InitHtmlAPIHandle();\n    int getValue(String data, char separator, int index);\n    uint8_t getActiveRelayCount();\n\n    byte IOexpanderWrite(byte address, byte _data);\n    byte IOexpanderRead(int address);\n\n    void publishMqtt(int relay);\n#ifndef WLED_DISABLE_MQTT\n    void publishHomeAssistantAutodiscovery();\n#endif\n\n  public:\n    /**\n     * constructor\n     */\n    MultiRelay();\n\n    /**\n     * desctructor\n     */\n    //~MultiRelay() {}\n\n    /**\n     * Enable/Disable the usermod\n     */\n    inline void enable(bool enable) { enabled = enable; }\n\n    /**\n     * Get usermod enabled/disabled state\n     */\n    inline bool isEnabled() { return enabled; }\n\n    /**\n     * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).\n     * This could be used in the future for the system to determine whether your usermod is installed.\n     */\n    inline uint16_t getId() override { return USERMOD_ID_MULTI_RELAY; }\n\n    /**\n     * switch relay on/off\n     */\n    void switchRelay(uint8_t relay, bool mode);\n\n    /**\n     * toggle relay\n     */\n    inline void toggleRelay(uint8_t relay) {\n      switchRelay(relay, !_relay[relay].state);\n    }\n\n    /**\n     * setup() is called once at boot. WiFi is not yet connected at this point.\n     * You can use it to initialize variables, sensors or similar.\n     */\n    void setup() override;\n\n    /**\n     * connected() is called every time the WiFi is (re)connected\n     * Use it to initialize network interfaces\n     */\n    inline void connected() override { InitHtmlAPIHandle(); }\n\n    /**\n     * loop() is called continuously. Here you can check for events, read sensors, etc.\n     */\n    void loop() override;\n\n#ifndef WLED_DISABLE_MQTT\n    bool onMqttMessage(char* topic, char* payload) override;\n    void onMqttConnect(bool sessionPresent) override;\n#endif\n\n    /**\n     * handleButton() can be used to override default button behaviour. Returning true\n     * will prevent button working in a default way.\n     * Replicating button.cpp\n     */\n    bool handleButton(uint8_t b) override;\n\n    /**\n     * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.\n     */\n    void addToJsonInfo(JsonObject &root) override;\n\n    /**\n     * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).\n     * Values in the state object may be modified by connected clients\n     */\n    void addToJsonState(JsonObject &root) override;\n\n    /**\n     * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).\n     * Values in the state object may be modified by connected clients\n     */\n    void readFromJsonState(JsonObject &root) override;\n\n    /**\n     * provide the changeable values\n     */\n    void addToConfig(JsonObject &root) override;\n\n    void appendConfigData() override;\n\n    /**\n     * restore the changeable values\n     * readFromConfig() is called before setup() to populate properties from values stored in cfg.json\n     * \n     * The function should return true if configuration was successfully loaded or false if there was no configuration.\n     */\n    bool readFromConfig(JsonObject &root) override;\n};\n\n\n// class implementation\n\nvoid MultiRelay::publishMqtt(int relay) {\n#ifndef WLED_DISABLE_MQTT\n  //Check if MQTT Connected, otherwise it will crash the 8266\n  if (WLED_MQTT_CONNECTED){\n    char subuf[64];\n    sprintf_P(subuf, PSTR(\"%s/relay/%d\"), mqttDeviceTopic, relay);\n    mqtt->publish(subuf, 0, false, _relay[relay].state ? \"on\" : \"off\");\n  }\n#endif\n}\n\n/**\n * switch off the strip if the delay has elapsed \n */\nvoid MultiRelay::handleOffTimer() {\n  unsigned long now = millis();\n  bool activeRelays = false;\n  for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {\n    if (_relay[i].active && _switchTimerStart > 0 && now - _switchTimerStart > (_relay[i].delay*1000)) {\n      if (!_relay[i].external) switchRelay(i, !offMode);\n      _relay[i].active = false;\n    } else if (periodicBroadcastSec && now - lastBroadcast > (periodicBroadcastSec*1000)) {\n      if (_relay[i].pin>=0) publishMqtt(i);\n    }\n    activeRelays = activeRelays || _relay[i].active;\n  }\n  if (!activeRelays) _switchTimerStart = 0;\n  if (periodicBroadcastSec && now - lastBroadcast > (periodicBroadcastSec*1000)) lastBroadcast = now;\n}\n\n/**\n * HTTP API handler\n * borrowed from:\n * https://github.com/gsieben/WLED/blob/master/usermods/GeoGab-Relays/usermod_GeoGab.h\n */\n#define GEOGABVERSION \"0.1.3\"\nvoid MultiRelay::InitHtmlAPIHandle() {  // https://github.com/me-no-dev/ESPAsyncWebServer\n  DEBUG_PRINTLN(F(\"Relays: Initialize HTML API\"));\n\n  server.on(F(\"/relays\"), HTTP_GET, [this](AsyncWebServerRequest *request) {\n    DEBUG_PRINTLN(F(\"Relays: HTML API\"));\n    String janswer;\n    String error = \"\";\n    //int params = request->params();\n    janswer = F(\"{\\\"NoOfRelays\\\":\");\n    janswer += String(MULTI_RELAY_MAX_RELAYS) + \",\";\n\n    if (getActiveRelayCount()) {\n      // Commands\n      if (request->hasParam(FPSTR(_switch))) {\n        /**** Switch ****/\n        AsyncWebParameter* p = request->getParam(FPSTR(_switch));\n        // Get Values\n        for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {\n          int value = getValue(p->value(), ',', i);\n          if (value==-1) {\n            error = F(\"There must be as many arguments as relays\");\n          } else {\n            // Switch\n            if (_relay[i].external) switchRelay(i, (bool)value);\n          }\n        }\n      } else if (request->hasParam(FPSTR(_toggle))) {\n        /**** Toggle ****/\n        AsyncWebParameter* p = request->getParam(FPSTR(_toggle));\n        // Get Values\n        for (int i=0;i<MULTI_RELAY_MAX_RELAYS;i++) {\n          int value = getValue(p->value(), ',', i);\n          if (value==-1) {\n            error = F(\"There must be as many arguments as relays\");\n          } else {\n            // Toggle\n            if (value && _relay[i].external) toggleRelay(i);\n          }\n        }\n      } else {\n        error = F(\"No valid command found\");\n      }\n    } else {\n      error = F(\"No active relays\");\n    }\n\n    // Status response\n    char sbuf[16];\n    for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {\n      sprintf_P(sbuf, PSTR(\"\\\"%d\\\":%d,\"), i, (_relay[i].pin<0 ? -1 : (int)_relay[i].state));\n      janswer += sbuf;\n    }\n    janswer += F(\"\\\"error\\\":\\\"\");\n    janswer += error;\n    janswer += F(\"\\\",\");\n    janswer += F(\"\\\"SW Version\\\":\\\"\");\n    janswer += String(F(GEOGABVERSION));\n    janswer += F(\"\\\"}\");\n    request->send(200, \"application/json\", janswer);\n  });\n}\n\nint MultiRelay::getValue(String data, char separator, int index) {\n  int found = 0;\n  int strIndex[] = {0, -1};\n  int maxIndex = data.length()-1;\n\n  for(int i=0; i<=maxIndex && found<=index; i++){\n    if(data.charAt(i)==separator || i==maxIndex){\n        found++;\n        strIndex[0] = strIndex[1]+1;\n        strIndex[1] = (i == maxIndex) ? i+1 : i;\n    }\n  }\n  return found>index ? data.substring(strIndex[0], strIndex[1]).toInt() : -1;\n}\n\n//Write a byte to the IO expander\nbyte MultiRelay::IOexpanderWrite(byte address, byte _data ) {\n  Wire.beginTransmission(address);\n  Wire.write(_data);\n  return Wire.endTransmission(); \n}\n\n//Read a byte from the IO expander\nbyte MultiRelay::IOexpanderRead(int address) {\n  byte _data = 0;\n  Wire.requestFrom(address, 1);\n  if (Wire.available()) {\n    _data = Wire.read();\n  }\n  return _data;\n}\n\n\n// public methods\n\nMultiRelay::MultiRelay()\n  : _switchTimerStart(0)\n  , enabled(MULTI_RELAY_ENABLED)\n  , initDone(false)\n  , usePcf8574(USE_PCF8574)\n  , addrPcf8574(PCF8574_ADDRESS)\n  , HAautodiscovery(MULTI_RELAY_HA_DISCOVERY)\n  , periodicBroadcastSec(60)\n  , lastBroadcast(0)\n{\n  const int8_t defPins[] = {MULTI_RELAY_PINS};\n  const int8_t relayDelays[] = {MULTI_RELAY_DELAYS};\n  const bool relayExternals[] = {MULTI_RELAY_EXTERNALS};\n  const bool relayInverts[] = {MULTI_RELAY_INVERTS};\n\n  for (size_t i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {\n    _relay[i].pin      = i < COUNT_OF(defPins) ? defPins[i] : -1;\n    _relay[i].delay    = i < COUNT_OF(relayDelays) ? relayDelays[i] : 0;\n    _relay[i].invert   = i < COUNT_OF(relayInverts) ? relayInverts[i] : false;\n    _relay[i].active   = false;\n    _relay[i].state    = false;\n    _relay[i].external = i < COUNT_OF(relayExternals) ? relayExternals[i] : false;\n    _relay[i].button   = -1;\n  }\n}\n\n/**\n * switch relay on/off\n */\nvoid MultiRelay::switchRelay(uint8_t relay, bool mode) {\n  if (relay>=MULTI_RELAY_MAX_RELAYS || _relay[relay].pin<0) return;\n  _relay[relay].state = mode;\n  if (usePcf8574 && _relay[relay].pin >= 100) {\n    // we need to send all outputs at the same time\n    uint8_t state = 0;\n    for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {\n      if (_relay[i].pin < 100) continue;\n      uint8_t pin = _relay[i].pin - 100;\n      state |= (_relay[i].invert ? !_relay[i].state : _relay[i].state) << pin; // fill relay states for all pins\n    }\n    IOexpanderWrite(addrPcf8574, state);\n    DEBUG_PRINT(F(\"Writing to PCF8574: \")); DEBUG_PRINTLN(state);\n  } else if (_relay[relay].pin < 100) {\n    pinMode(_relay[relay].pin, OUTPUT);\n    digitalWrite(_relay[relay].pin, _relay[relay].invert ? !_relay[relay].state : _relay[relay].state);\n  } else return;\n  publishMqtt(relay);\n}\n\nuint8_t MultiRelay::getActiveRelayCount() {\n  uint8_t count = 0;\n  for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) if (_relay[i].pin>=0) count++;\n  return count;\n}\n\n\n//Functions called by WLED\n\n#ifndef WLED_DISABLE_MQTT\n/**\n * handling of MQTT message\n * topic only contains stripped topic (part after /wled/MAC)\n * topic should look like: /relay/X/command; where X is relay number, 0 based\n */\nbool MultiRelay::onMqttMessage(char* topic, char* payload) {\n  if (strlen(topic) > 8 && strncmp_P(topic, PSTR(\"/relay/\"), 7) == 0 && strncmp_P(topic+8, _Command, 8) == 0) {\n    uint8_t relay = strtoul(topic+7, NULL, 10);\n    if (relay<MULTI_RELAY_MAX_RELAYS) {\n      String action = payload;\n      if (action == \"on\") {\n        if (_relay[relay].external) switchRelay(relay, true);\n        return true;\n      } else if (action == \"off\") {\n        if (_relay[relay].external) switchRelay(relay, false);\n        return true;\n      } else if (action == FPSTR(_toggle)) {\n        if (_relay[relay].external) toggleRelay(relay);\n        return true;\n      }\n    }\n  }\n  return false;\n}\n\n/**\n * subscribe to MQTT topic for controlling relays\n */\nvoid MultiRelay::onMqttConnect(bool sessionPresent) {\n  //(re)subscribe to required topics\n  char subuf[64];\n  if (mqttDeviceTopic[0] != 0) {\n    strcpy(subuf, mqttDeviceTopic);\n    strcat_P(subuf, PSTR(\"/relay/#\"));\n    mqtt->subscribe(subuf, 0);\n    if (HAautodiscovery) publishHomeAssistantAutodiscovery();\n    for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {\n      if (_relay[i].pin<0) continue;\n      publishMqtt(i); //publish current state\n    }\n  }\n}\n\nvoid MultiRelay::publishHomeAssistantAutodiscovery() {\n  for (int i = 0; i < MULTI_RELAY_MAX_RELAYS; i++) {\n    char uid[24], json_str[1024], buf[128];\n    size_t payload_size;\n    sprintf_P(uid, PSTR(\"%s_sw%d\"), escapedMac.c_str(), i);\n\n    if (_relay[i].pin >= 0 && _relay[i].external) {\n      StaticJsonDocument<1024> json;\n      sprintf_P(buf, PSTR(\"%s Switch %d\"), serverDescription, i); //max length: 33 + 8 + 3 = 44\n      json[F(\"name\")] = buf;\n\n      sprintf_P(buf, PSTR(\"%s/relay/%d\"), mqttDeviceTopic, i); //max length: 33 + 7 + 3 = 43\n      json[\"~\"] = buf;\n      strcat_P(buf, _Command);\n      mqtt->subscribe(buf, 0);\n\n      json[F(\"stat_t\")]  = \"~\";\n      json[F(\"cmd_t\")]   = F(\"~/command\");\n      json[F(\"pl_off\")]  = \"off\";\n      json[F(\"pl_on\")]   = \"on\";\n      json[F(\"uniq_id\")] = uid;\n\n      strcpy(buf, mqttDeviceTopic); //max length: 33 + 7 = 40\n      strcat_P(buf, PSTR(\"/status\"));\n      json[F(\"avty_t\")]       = buf;\n      json[F(\"pl_avail\")]     = F(\"online\");\n      json[F(\"pl_not_avail\")] = F(\"offline\");\n      //TODO: dev\n      payload_size = serializeJson(json, json_str);\n    } else {\n      //Unpublish disabled or internal relays\n      json_str[0]  = 0;\n      payload_size = 0;\n    }\n    sprintf_P(buf, PSTR(\"homeassistant/switch/%s/config\"), uid);\n    mqtt->publish(buf, 0, true, json_str, payload_size);\n  }\n}\n#endif\n\n/**\n * setup() is called once at boot. WiFi is not yet connected at this point.\n * You can use it to initialize variables, sensors or similar.\n */\nvoid MultiRelay::setup() {\n  // pins retrieved from cfg.json (readFromConfig()) prior to running setup()\n  // if we want PCF8574 expander I2C pins need to be valid\n  if (i2c_sda<0 || i2c_scl<0) usePcf8574 = false;\n\n  uint8_t state = 0;\n  for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {\n    if (usePcf8574 && _relay[i].pin >= 100) {\n      uint8_t pin = _relay[i].pin - 100;\n      if (!_relay[i].external) _relay[i].state = !offMode;\n      state |= (uint8_t)(_relay[i].invert ? !_relay[i].state : _relay[i].state) << pin;\n    } else if (_relay[i].pin<100 && _relay[i].pin>=0) {\n      if (PinManager::allocatePin(_relay[i].pin,true, PinOwner::UM_MultiRelay)) {\n        if (!_relay[i].external) _relay[i].state = !offMode;\n        switchRelay(i, _relay[i].state);\n        _relay[i].active = false;\n      } else {\n        _relay[i].pin = -1;  // allocation failed\n      }\n    }\n  }\n  if (usePcf8574) {\n    IOexpanderWrite(addrPcf8574, state);  // init expander (set all outputs)\n    DEBUG_PRINTLN(F(\"PCF8574(s) inited.\"));\n  }\n  _oldMode = offMode;\n  initDone = true;\n}\n\n/**\n * loop() is called continuously. Here you can check for events, read sensors, etc.\n */\nvoid MultiRelay::loop() {\n  static unsigned long lastUpdate = 0;\n  yield();\n  if (!enabled || (strip.isUpdating() && millis() - lastUpdate < 100)) return;\n\n  if (millis() - lastUpdate < 100) return;  // update only 10 times/s\n  lastUpdate = millis();\n\n  //set relay when LEDs turn on\n  if (_oldMode != offMode) {\n    _oldMode = offMode;\n    _switchTimerStart = millis();\n    for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {\n      if ((_relay[i].pin>=0) && !_relay[i].external) _relay[i].active = true;\n    }\n  }\n\n  handleOffTimer();\n}\n\n/**\n * handleButton() can be used to override default button behaviour. Returning true\n * will prevent button working in a default way.\n * Replicating button.cpp\n */\nbool MultiRelay::handleButton(uint8_t b) {\n  yield();\n  if (!enabled\n    || buttons[b].type == BTN_TYPE_NONE\n    || buttons[b].type == BTN_TYPE_RESERVED\n    || buttons[b].type == BTN_TYPE_PIR_SENSOR\n    || buttons[b].type == BTN_TYPE_ANALOG\n    || buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {\n    return false;\n  }\n\n  bool handled = false;\n  for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {\n    if (_relay[i].button == b && _relay[i].external) {\n      handled = true;\n    }\n  }\n  if (!handled) return false;\n\n  unsigned long now = millis();\n\n  //button is not momentary, but switch. This is only suitable on pins whose on-boot state does not matter (NOT gpio0)\n  if (buttons[b].type == BTN_TYPE_SWITCH) {\n    //handleSwitch(b);\n    if (buttons[b].pressedBefore != isButtonPressed(b)) {\n      buttons[b].pressedTime = now;\n      buttons[b].pressedBefore = !buttons[b].pressedBefore;\n    }\n\n    if (buttons[b].longPressed == buttons[b].pressedBefore) return handled;\n      \n    if (now - buttons[b].pressedTime > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)\n      for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {\n        if (_relay[i].button == b) {\n          switchRelay(i, buttons[b].pressedBefore);\n          buttons[b].longPressed = buttons[b].pressedBefore; //save the last \"long term\" switch state\n        }\n      }\n    }\n    return handled;\n  }\n\n  //momentary button logic\n  if (isButtonPressed(b)) { //pressed\n\n    if (!buttons[b].pressedBefore) buttons[b].pressedTime = now;\n    buttons[b].pressedBefore = true;\n\n    if (now - buttons[b].pressedTime > 600) { //long press\n      //longPressAction(b); //not exposed\n      //handled = false; //use if you want to pass to default behaviour\n      buttons[b].longPressed = true;\n    }\n\n  } else if (!isButtonPressed(b) && buttons[b].pressedBefore) { //released\n\n    long dur = now - buttons[b].pressedTime;\n    if (dur < WLED_DEBOUNCE_THRESHOLD) {\n      buttons[b].pressedBefore = false;\n      return handled;\n    } //too short \"press\", debounce\n    bool doublePress = buttons[b].waitTime; //did we have short press before?\n    buttons[b].waitTime = 0;\n\n    if (!buttons[b].longPressed) { //short press\n      // if this is second release within 350ms it is a double press (buttonWaitTime!=0)\n      if (doublePress) {\n        //doublePressAction(b); //not exposed\n        //handled = false; //use if you want to pass to default behaviour\n      } else  {\n        buttons[b].waitTime = now;\n      }\n    }\n    buttons[b].pressedBefore = false;\n    buttons[b].longPressed = false;\n  }\n  // if 350ms elapsed since last press/release it is a short press\n  if (buttons[b].waitTime && now - buttons[b].waitTime > 350 && !buttons[b].pressedBefore) {\n    buttons[b].waitTime = 0;\n    //shortPressAction(b); //not exposed\n    for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {\n      if (_relay[i].button == b) {\n        toggleRelay(i);\n      }\n    }\n  }\n  return handled;\n}\n\n/**\n * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.\n */\nvoid MultiRelay::addToJsonInfo(JsonObject &root) {\n  if (enabled) {\n    JsonObject user = root[\"u\"];\n    if (user.isNull())\n      user = root.createNestedObject(\"u\");\n\n    JsonArray infoArr = user.createNestedArray(FPSTR(_name)); //name\n    infoArr.add(String(getActiveRelayCount()));\n    infoArr.add(F(\" relays\"));\n\n    String uiDomString;\n    for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {\n      if (_relay[i].pin<0 || !_relay[i].external) continue;\n      uiDomString = F(\"Relay \"); uiDomString += i;\n      infoArr = user.createNestedArray(uiDomString); // timer value\n\n      uiDomString = F(\"<button class=\\\"btn btn-xs\\\" onclick=\\\"requestJson({\");\n      uiDomString += FPSTR(_name);\n      uiDomString += F(\":{\");\n      uiDomString += FPSTR(_relay_str);\n      uiDomString += F(\":\");\n      uiDomString += i;\n      uiDomString += F(\",on:\");\n      uiDomString += _relay[i].state ? \"false\" : \"true\";\n      uiDomString += F(\"}});\\\">\");\n      uiDomString += F(\"<i class=\\\"icons \");\n      uiDomString += _relay[i].state ? \"on\" : \"off\";\n      uiDomString += F(\"\\\">&#xe08f;</i></button>\");\n      infoArr.add(uiDomString);\n    }\n  }\n}\n\n/**\n * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).\n * Values in the state object may be modified by connected clients\n */\nvoid MultiRelay::addToJsonState(JsonObject &root) {\n  if (!initDone || !enabled) return;  // prevent crash on boot applyPreset()\n  JsonObject multiRelay = root[FPSTR(_name)];\n  if (multiRelay.isNull()) {\n    multiRelay = root.createNestedObject(FPSTR(_name));\n  }\n  #if MULTI_RELAY_MAX_RELAYS > 1\n  JsonArray rel_arr = multiRelay.createNestedArray(F(\"relays\"));\n  for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {\n    if (_relay[i].pin < 0) continue;\n    JsonObject relay = rel_arr.createNestedObject();\n    relay[FPSTR(_relay_str)] = i;\n    relay[\"state\"] = _relay[i].state;\n  }\n  #else\n  multiRelay[FPSTR(_relay_str)] = 0;\n  multiRelay[\"state\"] = _relay[0].state;\n  #endif\n}\n\n/**\n * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).\n * Values in the state object may be modified by connected clients\n */\nvoid MultiRelay::readFromJsonState(JsonObject &root) {\n  if (!initDone || !enabled) return;  // prevent crash on boot applyPreset()\n  JsonObject usermod = root[FPSTR(_name)];\n  if (!usermod.isNull()) {\n    if (usermod[FPSTR(_relay_str)].is<int>() && usermod[FPSTR(_relay_str)].as<int>()>=0) {\n      int rly = usermod[FPSTR(_relay_str)].as<int>();\n      if (usermod[\"on\"].is<bool>()) {\n        switchRelay(rly, usermod[\"on\"].as<bool>());\n      } else if (usermod[\"on\"].is<const char*>() && usermod[\"on\"].as<const char*>()[0] == 't') {\n        toggleRelay(rly);\n      }\n    }\n  } else if (root[FPSTR(_name)].is<JsonArray>()) {\n    JsonArray relays = root[FPSTR(_name)].as<JsonArray>();\n    for (JsonVariant r : relays) {\n      if (r[FPSTR(_relay_str)].is<int>() && r[FPSTR(_relay_str)].as<int>()>=0) {\n        int rly = r[FPSTR(_relay_str)].as<int>();\n        if (r[\"on\"].is<bool>()) {\n          switchRelay(rly, r[\"on\"].as<bool>());\n        } else if (r[\"on\"].is<const char*>() && r[\"on\"].as<const char*>()[0] == 't') {\n          toggleRelay(rly);\n        }\n      }\n    }\n  }\n}\n\n/**\n * provide the changeable values\n */\nvoid MultiRelay::addToConfig(JsonObject &root) {\n  JsonObject top = root.createNestedObject(FPSTR(_name));\n\n  top[FPSTR(_enabled)] = enabled;\n  top[FPSTR(_pcf8574)] = usePcf8574;\n  top[FPSTR(_pcfAddress)] = addrPcf8574;\n  top[FPSTR(_broadcast)] = periodicBroadcastSec;\n  top[FPSTR(_HAautodiscovery)] = HAautodiscovery;\n  for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {\n    String parName = FPSTR(_relay_str); parName += '-'; parName += i;\n    JsonObject relay = top.createNestedObject(parName);\n    relay[\"pin\"]              = _relay[i].pin;\n    relay[FPSTR(_activeHigh)] = _relay[i].invert;\n    relay[FPSTR(_delay_str)]  = _relay[i].delay;\n    relay[FPSTR(_external)]   = _relay[i].external;\n    relay[FPSTR(_button)]     = _relay[i].button;\n  }\n  DEBUG_PRINTLN(F(\"MultiRelay config saved.\"));\n}\n\nvoid MultiRelay::appendConfigData() {\n  oappend(F(\"addInfo('MultiRelay:PCF8574-address',1,'<i>(not hex!)</i>');\"));\n  oappend(F(\"addInfo('MultiRelay:broadcast-sec',1,'(MQTT message)');\"));\n  //oappend(F(\"addInfo('MultiRelay:relay-0:pin',1,'(use -1 for PCF8574)');\"));\n  oappend(F(\"d.extra.push({'MultiRelay':{pin:[['P0',100],['P1',101],['P2',102],['P3',103],['P4',104],['P5',105],['P6',106],['P7',107]]}});\"));\n}\n\n/**\n * restore the changeable values\n * readFromConfig() is called before setup() to populate properties from values stored in cfg.json\n * \n * The function should return true if configuration was successfully loaded or false if there was no configuration.\n */\nbool MultiRelay::readFromConfig(JsonObject &root) {\n  int8_t oldPin[MULTI_RELAY_MAX_RELAYS];\n\n  JsonObject top = root[FPSTR(_name)];\n  if (top.isNull()) {\n    DEBUG_PRINT(FPSTR(_name));\n    DEBUG_PRINTLN(F(\": No config found. (Using defaults.)\"));\n    return false;\n  }\n\n  //bool configComplete = !top.isNull();\n  //configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled);\n  enabled = top[FPSTR(_enabled)] | enabled;\n  usePcf8574 = top[FPSTR(_pcf8574)] | usePcf8574;\n  addrPcf8574 = top[FPSTR(_pcfAddress)] | addrPcf8574;\n  // if I2C is not globally initialised just ignore\n  if (i2c_sda<0 || i2c_scl<0) usePcf8574 = false;\n  periodicBroadcastSec = top[FPSTR(_broadcast)] | periodicBroadcastSec;\n  periodicBroadcastSec = min(900,max(0,(int)periodicBroadcastSec));\n  HAautodiscovery = top[FPSTR(_HAautodiscovery)] | HAautodiscovery;\n\n  for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++) {\n    String parName = FPSTR(_relay_str); parName += '-'; parName += i;\n    oldPin[i]          = _relay[i].pin;\n    _relay[i].pin      = top[parName][\"pin\"] | _relay[i].pin;\n    _relay[i].invert   = top[parName][FPSTR(_activeHigh)] | _relay[i].invert;\n    _relay[i].external = top[parName][FPSTR(_external)]   | _relay[i].external;\n    _relay[i].delay    = top[parName][FPSTR(_delay_str)]  | _relay[i].delay;\n    _relay[i].button   = top[parName][FPSTR(_button)]     | _relay[i].button;\n    _relay[i].delay    = min(600,max(0,abs((int)_relay[i].delay))); // bounds checking max 10min\n  }\n\n  DEBUG_PRINT(FPSTR(_name));\n  if (!initDone) {\n    // reading config prior to setup()\n    DEBUG_PRINTLN(F(\" config loaded.\"));\n  } else {\n    // deallocate all pins 1st\n    for (int i=0; i<MULTI_RELAY_MAX_RELAYS; i++)\n      if (oldPin[i]>=0 && oldPin[i]<100) {\n        PinManager::deallocatePin(oldPin[i], PinOwner::UM_MultiRelay);\n      }\n    // allocate new pins\n    setup();\n    DEBUG_PRINTLN(F(\" config (re)loaded.\"));\n  }\n  // use \"return !top[\"newestParameter\"].isNull();\" when updating Usermod with new features\n  return !top[FPSTR(_pcf8574)].isNull();\n}\n\n// strings to reduce flash memory usage (used more than twice)\nconst char MultiRelay::_name[]            PROGMEM = \"MultiRelay\";\nconst char MultiRelay::_enabled[]         PROGMEM = \"enabled\";\nconst char MultiRelay::_relay_str[]       PROGMEM = \"relay\";\nconst char MultiRelay::_delay_str[]       PROGMEM = \"delay-s\";\nconst char MultiRelay::_activeHigh[]      PROGMEM = \"active-high\";\nconst char MultiRelay::_external[]        PROGMEM = \"external\";\nconst char MultiRelay::_button[]          PROGMEM = \"button\";\nconst char MultiRelay::_broadcast[]       PROGMEM = \"broadcast-sec\";\nconst char MultiRelay::_HAautodiscovery[] PROGMEM = \"HA-autodiscovery\";\nconst char MultiRelay::_pcf8574[]         PROGMEM = \"use-PCF8574\";\nconst char MultiRelay::_pcfAddress[]      PROGMEM = \"PCF8574-address\";\nconst char MultiRelay::_switch[]          PROGMEM = \"switch\";\nconst char MultiRelay::_toggle[]          PROGMEM = \"toggle\";\nconst char MultiRelay::_Command[]         PROGMEM = \"/command\";\n\n\nstatic MultiRelay multi_relay;\nREGISTER_USERMOD(multi_relay);"
  },
  {
    "path": "usermods/multi_relay/readme.md",
    "content": "# Multi Relay\n\nThis usermod-v2 modification allows the connection of multiple relays, each with individual delay and on/off mode.\nUsermod supports PCF8574 I2C port expander to reduce GPIO use.\nPCF8574 supports 8 outputs and each output corresponds to a relay in WLED (relay 0 = port 0, etc). I you are using more than 8 relays with multiple PCF8574 make sure their addresses are set in sequence (e.g. 0x20 and 0x21). You can set address of first expander in settings.\n(**NOTE:** Will require Wire library and global I2C pins defined.)\n\n## HTTP API\nAll responses are returned in JSON format. \n\n* Status Request: `http://[device-ip]/relays`\n* Switch Command: `http://[device-ip]/relays?switch=1,0,1,1`\n\nThe number of values behind the switch parameter must correspond to the number of relays. The value 1 switches the relay on, 0 switches it off. \n\n* Toggle Command: `http://[device-ip]/relays?toggle=1,0,1,1`\n\nThe number of values behind the parameter switch must correspond to the number of relays. The value 1 causes the relay to toggle, 0 leaves its state unchanged.\n\nExamples:\n1. total of 4 relays, relay 2 will be toggled: `http://[device-ip]/relays?toggle=0,1,0,0`\n2. total of 3 relays, relay 1&3 will be switched on: `http://[device-ip]/relays?switch=1,0,1`\n\n## JSON API\nYou can toggle the relay state by sending the following JSON object to: `http://[device-ip]/json`\n\nSwitch relay 0 on: `{\"MultiRelay\":{\"relay\":0,\"on\":true}}`\n\nSwitch relay 3 and 4 off: `{\"MultiRelay\":[{\"relay\":2,\"on\":false},{\"relay\":3,\"on\":false}]}`\n\n\n## MQTT API\n\n* `wled`/_deviceMAC_/`relay`/`0`/`command` `on`|`off`|`toggle`\n* `wled`/_deviceMAC_/`relay`/`1`/`command` `on`|`off`|`toggle`\n\nWhen a relay is switched, a message is published:\n\n* `wled`/_deviceMAC_/`relay`/`0` `on`|`off`\n\n\n## Usermod installation\n\nAdd `multi_relay` to the `custom_usermods` of your platformio.ini environment.\n\nYou can override the default maximum number of relays (which is 4) by defining MULTI_RELAY_MAX_RELAYS.\n\nSome settings can be defined (defaults) at compile time by setting the following defines:\n\n```cpp\n// enable or disable HA discovery for externally controlled relays\n#define MULTI_RELAY_HA_DISCOVERY true\n```\n\nThe following definitions should be a list of values (maximum number of entries is MULTI_RELAY_MAX_RELAYS) that will be applied to the relays in order:\n(e.g. assuming MULTI_RELAY_MAX_RELAYS=2)\n\n```cpp\n#define MULTI_RELAY_PINS 12,18\n#define MULTI_RELAY_DELAYS 0,0\n#define MULTI_RELAY_EXTERNALS false,true\n#define MULTI_RELAY_INVERTS false,false\n```\nThese can be set via your `platformio_override.ini` file or as `#define` in your `my_config.h` (remember to set `WLED_USE_MY_CONFIG` in your `platformio_override.ini`)\n\n## Configuration\n\nUsermod can be configured via the Usermods settings page.\n\n* `enabled` - enable/disable usermod\n* `use-PCF8574` - use PCF8574 port expander instead of GPIO pins\n* `first-PCF8574` - I2C address of first expander (WARNING: enter *decimal* value)\n* `broadcast`- time in seconds between MQTT relay-state broadcasts\n* `HA-discovery`- enable Home Assistant auto discovery\n* `pin` - ESP GPIO pin the relay is connected to (can be configured at compile time `-D MULTI_RELAY_PINS=xx,xx,...`)\n* `delay-s` - delay in seconds after on/off command is received\n* `active-high` - assign high/low activation of relay (can be used to reverse relay states)\n* `external` - if enabled, WLED does not control relay, it can only be triggered by an external command (MQTT, HTTP, JSON or button)\n* `button` - button (from LED Settings) that controls this relay\n\nIf there is no MultiRelay section, just save current configuration and re-open Usermods settings page. \n\nHave fun - @blazoncek\n\n## Change log\n2021-04\n* First implementation.\n\n2021-11\n* Added information about dynamic configuration options\n* Added button support.\n\n2023-05\n* Added support for PCF8574 I2C port expander (multiple)\n\n2023-11\n* @chrisburrows Added support for compile time defaults for setting DELAY, EXTERNAL, INVERTS and HA discovery"
  },
  {
    "path": "usermods/photoresistor_sensor_mqtt_v1/README.md",
    "content": "# Photoresister sensor with MQTT\n\nEnables attaching a photoresistor sensor like the KY-018 and publishing the readings as a percentage, via MQTT. The frequency of MQTT messages is user definable.\nA threshold value can be set so significant changes in the readings are published immediately vice waiting for the next update. This was found to be a good compromise between excessive MQTT traffic and delayed updates.\n\nI also found it useful to limit the frequency of analog pin reads, otherwise the board hangs.\n\nThis usermod has only been tested with the KY-018 sensor though it should work for any other analog pin sensor.\nNote: this does not control the LED strip directly, it only publishes MQTT readings for use with other integrations like Home Assistant.\n\n## Installation\n\nCopy and replace the file `usermod.cpp` in wled00 directory.\n"
  },
  {
    "path": "usermods/photoresistor_sensor_mqtt_v1/usermod.cpp",
    "content": "#include \"wled.h\"\n/*\n * This v1 usermod file allows you to add own functionality to WLED more easily\n * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality\n * EEPROM bytes 2750+ are reserved for your custom use case. (if you extend #define EEPSIZE in const.h)\n * If you just need 8 bytes, use 2551-2559 (you do not need to increase EEPSIZE)\n * \n * Consider the v2 usermod API if you need a more advanced feature set!\n */\n\n//Use userVar0 and userVar1 (API calls &U0=,&U1=, uint16_t)\n\nconst int LIGHT_PIN = A0; // define analog pin\nconst long UPDATE_MS = 30000; // Upper threshold between mqtt messages\nconst char MQTT_TOPIC[] = \"/light\"; // MQTT topic for sensor values\nconst int CHANGE_THRESHOLD = 5; // Change threshold in percentage to send before UPDATE_MS\n\n// variables\nlong lastTime = 0;\nlong timeDiff = 0;\nlong readTime = 0;\nint lightValue = 0; \nfloat lightPercentage = 0;\nfloat lastPercentage = 0;\n\n//gets called once at boot. Do all initialization that doesn't depend on network here\nvoid userSetup()\n{\n  pinMode(LIGHT_PIN, INPUT);\n}\n\n//gets called every time WiFi is (re-)connected. Initialize own network interfaces here\nvoid userConnected()\n{\n\n}\n\nvoid publishMqtt(float state)\n{\n  //Check if MQTT Connected, otherwise it will crash the 8266\n  if (mqtt != nullptr){\n    char subuf[38];\n    strcpy(subuf, mqttDeviceTopic);\n    strcat(subuf, MQTT_TOPIC);\n    mqtt->publish(subuf, 0, true, String(state).c_str());\n  }\n}\n\n//loop. You can use \"if (WLED_CONNECTED)\" to check for successful connection\nvoid userLoop()\n{\n   // Read only every 500ms, otherwise it causes the board to hang\n  if (millis() - readTime > 500)\n  {\n    readTime = millis();\n    timeDiff = millis() - lastTime;\n    \n    // Convert value to percentage\n    lightValue = analogRead(LIGHT_PIN);\n    lightPercentage = ((float)lightValue * -1 + 1024)/(float)1024 *100;\n    \n    // Send MQTT message on significant change or after UPDATE_MS\n    if (abs(lightPercentage - lastPercentage) > CHANGE_THRESHOLD || timeDiff > UPDATE_MS) \n    {\n      publishMqtt(lightPercentage);\n      lastTime = millis();\n      lastPercentage = lightPercentage;\n    }\n  }\n}\n"
  },
  {
    "path": "usermods/pixels_dice_tray/BLE_REQUIREMENT.md",
    "content": "# pixels_dice_tray Usermod - BLE Requirement Notice\n\n## Important: This Usermod Requires Special Configuration\n\nThe `pixels_dice_tray` usermod requires **ESP32 BLE (Bluetooth Low Energy)** support, which is not available in all WLED build configurations.\n\n### Why is library.json disabled?\n\nThe `library.json` file has been renamed to `library.json.disabled` to prevent this usermod from being automatically included in builds that use `custom_usermods = *` (like the `usermods` environment in platformio.ini).\n\nThe Tasmota Arduino ESP32 platform used by WLED does not include Arduino BLE library by default, which causes compilation failures when this usermod is auto-included.\n\n### How to Use This Usermod\n\nThis usermod **requires a custom build configuration**. You cannot simply enable it with `custom_usermods = *`.\n\n1. **Copy the sample configuration:**\n   ```bash\n   cp platformio_override.ini.sample ../../../platformio_override.ini\n   ```\n\n2. **Edit `platformio_override.ini`** to match your ESP32 board configuration\n\n3. **Build with the custom environment:**\n   ```bash\n   pio run -e t_qt_pro_8MB_dice\n   # or\n   pio run -e esp32s3dev_8MB_qspi_dice\n   ```\n\n### Platform Requirements\n\n- ESP32-S3 or compatible ESP32 board with BLE support\n- Custom platformio environment (see `platformio_override.ini.sample`)\n- Cannot be used with ESP8266 or ESP32-S2\n\n### Re-enabling for Custom Builds\n\nIf you want to use this usermod in a custom build:\n\n1. Rename `library.json.disabled` back to `library.json`\n2. Manually add it to your custom environment's `custom_usermods` list\n3. Ensure your platform includes BLE support\n\n### References\n\n- See `README.md` for full usermod documentation\n- See `platformio_override.ini.sample` for build configuration examples\n"
  },
  {
    "path": "usermods/pixels_dice_tray/README.md",
    "content": "# A mod for using Pixel Dice with ESP32S3 boards\n\nA usermod to connect to and handle rolls from [Pixels Dice](https://gamewithpixels.com/). WLED acts as both an display controller, and a gateway to connect the die to the Wifi network.\n\nHigh level features:\n\n* Several LED effects that respond to die rolls\n  * Effect color and parameters can be modified like any other effect\n  * Different die can be set to control different segments\n* An optional GUI on a TFT screen with custom button controls\n  * Gives die connection and roll status\n  * Can do basic LED effect controls\n  * Can display custom info for different roll types (ie. RPG stats/spell info)\n* Publish MQTT events from die rolls\n  * Also report the selected roll type\n* Control settings through the WLED web\n\nSee <https://www.robopenguins.com/pixels-dice-box/> for a write up of the design process of the hardware and software I used this with.\n\nI also set up a custom web installer for the usermod at <https://axlan.github.io/WLED-WebInstaller/> for 8MB ESP32-S3 boards.\n\n## Table of Contents\n\n<!-- TOC start (generated with https://github.com/derlin/bitdowntoc) -->\n* [Demos](#demos)\n  + [TFT GUI](#tft-gui)\n  + [Multiple Die Controlling Different Segments](#multiple-die-controlling-different-segments)\n* [Hardware](#hardware)\n* [Library used](#library-used)\n* [Compiling](#compiling)\n  + [platformio_override.ini](#platformio_overrideini)\n  + [Manual platformio.ini changes](#manual-platformioini-changes)\n* [Configuration](#configuration)\n  + [Controlling Dice Connections](#controlling-dice-connections)\n  + [Controlling Effects](#controlling-effects)\n      - [DieSimple](#diesimple)\n      - [DiePulse](#diepulse)\n      - [DieCheck](#diecheck)\n* [TFT GUI](#tft-gui-1)\n  + [Status](#status)\n  + [Effect Menu](#effect-menu)\n  + [Roll Info](#roll-info)\n* [MQTT](#mqtt)\n* [Potential Modifications and Additional Features](#potential-modifications-and-additional-features)\n* [ESP32 Issues](#esp32-issues)\n<!-- TOC end -->\n\n<!-- TOC --><a name=\"demos\"></a>\n## Demos\n\n<!-- TOC --><a name=\"tft-gui\"></a>\n### TFT GUI\n[![Watch the video](https://img.youtube.com/vi/VNsHq1TbiW8/0.jpg)](https://youtu.be/VNsHq1TbiW8)\n\n<!-- TOC --><a name=\"multiple-die-controlling-different-segments\"></a>\n### Multiple Die Controlling Different Segments\n[![Watch the video](https://img.youtube.com/vi/oCDr44C-qwM/0.jpg)](https://youtu.be/oCDr44C-qwM)\n\n<!-- TOC --><a name=\"hardware\"></a>\n## Hardware\n\nThe main purpose of this mod is to support [Pixels Dice](https://gamewithpixels.com/). The board acts as a BLE central for the dice acting as peripherals. While any ESP32 variant with BLE capabilities should be able to support this usermod, in practice I found that the original ESP32 did not work. See [ESP32 Issues](#esp32-issues) for a deeper dive.\n\nThe only other ESP32 variant I tested was the ESP32-S3, which worked without issue. While there's still concern over the contention between BLE and WiFi for the radio, I haven't noticed any performance impact in practice. The only special behavior that was needed was setting `noWifiSleep = false;` to allow the OS to sleep the WiFi when the BLE is active.\n\nIn addition, the BLE stack requires a lot of flash. This build takes 1.9MB with the TFT code, or 1.85MB without it. This makes it too big to fit in the `tools/WLED_ESP32_4MB_256KB_FS.csv` partition layout, and I needed to make a `WLED_ESP32_4MB_64KB_FS.csv` to even fit on 4MB devices. This only has 64KB of file system space, which is functional, but users with more than a handful of presets would run into problems with 64KB only. This means that while 4MB can be supported, larger flash sizes are needed for full functionality. \n\nThe basic build of this usermod doesn't require any special hardware. However, the LCD status GUI was specifically designed for the [LILYGO T-QT Pro](https://www.lilygo.cc/products/t-qt-pro).\n\nIt should be relatively easy to support other displays, though the positioning of the text may need to be adjusted.\n\n<!-- TOC --><a name=\"library-used\"></a>\n## Library used\n\n[axlan/pixels-dice-interface](https://github.com/axlan/arduino-pixels-dice)\n\nOptional: [Bodmer/TFT_eSPI](https://github.com/Bodmer/TFT_eSPI)\n\n<!-- TOC --><a name=\"compiling\"></a>\n## Compiling\n\n<!-- TOC --><a name=\"platformio_overrideini\"></a>\n### platformio_override.ini\n\nCopy and update the example `platformio_override.ini.sample` to the root directory of your particular build (renaming it `platformio_override.ini`).\nThis file should be placed in the same directory as `platformio.ini`. This file is set up for the [LILYGO T-QT Pro](https://www.lilygo.cc/products/t-qt-pro). Specifically, the 8MB flash version. See the next section for notes on setting the build flags. For other boards, you may want to use a different environment as the basis.\n\n<!-- TOC --><a name=\"manual-platformioini-changes\"></a>\n### Manual platformio.ini changes\n\nUsing the `platformio_override.ini.sample` as a reference, you'll need to update the `build_flags` and `lib_deps` of the target you're building for.\n\nIf you don't need the TFT GUI, you just need to add \n\n\n```ini\n...\nbuild_flags =\n  ...\n  -D USERMOD_PIXELS_DICE_TRAY ;; Enables this UserMod\nlib_deps =\n  ...\n  ESP32 BLE Arduino\n  axlan/pixels-dice-interface @ 1.2.0\n...\n```\n\nFor the TFT support you'll need to add `Bodmer/TFT_eSPI` to `lib_deps`, and all of the required TFT parameters to `build_flags` (see `platformio_override.ini.sample`).\n\nSave the `platformio.ini` file, and perform the desired build.\n\n<!-- TOC --><a name=\"configuration\"></a>\n## Configuration\n\nIn addition to configuring which dice to connect to, this mod uses a lot of the built in WLED features:\n* The LED segments, effects, and customization parameters\n* The buttons for the UI\n* The MQTT settings for reporting the dice rolls\n\n<!-- TOC --><a name=\"controlling-dice-connections\"></a>\n### Controlling Dice Connections\n\n**NOTE:** To configure the die itself (set its name, the die LEDs, etc.), you still need to use the Pixels Dice phone App.\n\nThe usermods settings page has the configuration for controlling the dice and the display:\n * Ble Scan Duration - The time to look for BLE broadcasts before taking a break\n * Rotation - If display used, set this parameter to rotate the display.\n\nThe main setting here though are the Die 0 and 1 settings. A slot is disabled if it's left blank. Putting the name of a die will make that slot only connect to die with that name. Alteratively, if the name is set to `*` the slot will use the first unassociated die it sees. Saving the configuration while a wildcard slot is connected to a die will replace the `*` with that die's name.\n\n**NOTE:** The slot a die is in is important since that's how they're identified for controlling LED effects. Effects can be set to respond to die 0, 1, or any.\n\nThe configuration also includes the pins configured in the TFT build flags. These are just so the UI recognizes that these pins are being used. The [Bodmer/TFT_eSPI](https://github.com/Bodmer/TFT_eSPI) requires that these are set at build time and changing these values is ignored.\n\n<!-- TOC --><a name=\"controlling-effects\"></a>\n### Controlling Effects\n\nThe die effects for rolls take advantage of most of the normal WLED effect features: <https://kno.wled.ge/features/effects/>.\n\nIf you have different segments, they can have different effects driven by the same die, or different dice.\n\n<!-- TOC --><a name=\"diesimple\"></a>\n#### DieSimple\nTurn off LEDs while rolling, than light up solid LEDs in proportion to die roll.\n\n* Color 1 - Selects the \"good\" color that increases based on the die roll\n* Color 2 - Selects the \"background\" color for the rest of the segment\n* Custom 1 - Sets which die should control this effect. If the value is greater then 1, it will respond to both dice.\n\n<!-- TOC --><a name=\"diepulse\"></a>\n#### DiePulse\nPlay `breath` effect while rolling, than apply `blend` effect in proportion to die roll.\n\n* Color 1 - See `breath` and `blend`\n* Color 2 - Selects the \"background\" color for the rest of the segment\n* Palette - See `breath` and `blend`\n* Custom 1 - Sets which die should control this effect. If the value is greater then 1, it will respond to both dice.\n\n<!-- TOC --><a name=\"diecheck\"></a>\n#### DieCheck\nPlay `running` effect while rolling, than apply `glitter` effect if roll passes threshold, or `gravcenter` if roll is below.\n\n* Color 1 - See `glitter` and `gravcenter`, used as first color for `running`\n* Color 2 - See `glitter` and `gravcenter`\n* Color 3 - Used as second color for `running`\n* Palette - See `glitter` and `gravcenter`\n* Custom 1 - Sets which die should control this effect. If the value is greater then 1, it will respond to both dice.\n* Custom 2 - Sets the threshold for success animation. For example if 10, success plays on rolls of 10 or above.\n\n<!-- TOC --><a name=\"tft-gui-1\"></a>\n## TFT GUI\n\nThe optional TFT GUI currently supports 3 \"screens\":\n1. Status\n2. Effect Control\n3. Roll Info\n\nDouble pressing the right button goes forward through the screens, and double pressing left goes back (with rollover).\n\n<!-- TOC --><a name=\"status\"></a>\n### Status\n<img src=\"images/status.webp\" alt=\"Status Menu\" width=\"200\"/>\n\nShows the status of each die slot (0 on top and 1 on the bottom).\n\nIf a die is connected, its roll stats and battery status are shown. The rolls will continue to be tracked even when viewing other screens.\n\nLong press either button to clear the roll stats.\n\n<!-- TOC --><a name=\"effect-menu\"></a>\n### Effect Menu\n<img src=\"images/effect.webp\" alt=\"Effect Menu\" width=\"200\"/>\n\nAllows limited customization of the die effect for the currently selected LED segment.\n\nThe left button moves the cursor (blue box) up and down the options for the current field.\n\nThe right button updates the value for the field. \n\nThe first field is the effect. Updating it will switch between the die effects.\n\nThe DieCheck effect has an additional field \"PASS\". Pressing the right button on this field will copy the current face up value from the most recently rolled die.\n\nLong pressing either value will set the effect parameters (color, palette, controlling dice, etc.) to a default set of values.\n\n<!-- TOC --><a name=\"roll-info\"></a>\n### Roll Info\n<img src=\"images/info.webp\" alt=\"Roll Info Menu\" width=\"200\"/>\n\nSets the \"roll type\" reported by MQTT events and can show additional info.\n\nPressing the right button goes forward through the rolls, and double pressing left goes back (with rollover).\n\nThe names and info for the rolls are generated from the `usermods/pixels_dice_tray/generate_roll_info.py` script. It updates `usermods/pixels_dice_tray/roll_info.h` with code generated from a simple markdown language.\n\n<!-- TOC --><a name=\"mqtt\"></a>\n## MQTT\n\nSee <https://kno.wled.ge/interfaces/mqtt/> for general MQTT configuration for WLED.\n\nThe usermod produces two types of events\n\n* `$mqttDeviceTopic/dice/roll` - JSON that reports each die roll event with the following keys.\n  - name - The name of the die that triggered the event\n  - state - Integer indicating the die state `[UNKNOWN = 0, ON_FACE = 1, HANDLING = 2, ROLLING = 3, CROOKED = 4]`\n  - val - The value on the die's face. For d20 1-20\n  - time - The uptime timestamp the roll was received in milliseconds. \n* `$mqttDeviceTopic/dice/roll_label` - A string that indicates the roll type selected in the [Roll Info](#roll-info) TFT menu.\n\nWhere `$mqttDeviceTopic` is the topic set in the WLED MQTT configuration.\n\nEvents can be logged to a CSV file using the script `usermods/pixels_dice_tray/mqtt_client/mqtt_logger.py`. These can then be used to generate interactive HTML plots with `usermods/pixels_dice_tray/mqtt_client/mqtt_plotter.py`.\n\n<img src=\"images/roll_plot.png\" alt=\"Roll Plot\"/>\n\n<!-- TOC --><a name=\"potential-modifications-and-additional-features\"></a>\n## Potential Modifications and Additional Features\n\nThis usermod is in support of a particular dice box project, but it would be fairly straightforward to extend for other applications.\n* Add more dice - There's no reason that several more dice slots couldn't be allowed. In addition LED effects that use multiple dice could be added (e.g. a contested roll).\n* Better support for die other then d20's. There's a few places where I assume the die is a d20. It wouldn't be that hard to support arbitrary die sizes.\n* TFT Menu - The menu system is pretty extensible. I put together some basic things I found useful, and was mainly limited by the screen size.\n* Die controlled UI - I originally planned to make an alternative UI that used the die directly. You'd press a button, and the current face up on the die would trigger an action. This was an interesting idea, but didn't seem to practical since I could more flexibly reproduce this by responding to the dice MQTT events.\n\n<!-- TOC --><a name=\"esp32-issues\"></a>\n## ESP32 Issues\n\nI really wanted to have this work on the original ESP32 boards to lower the barrier to entry, but there were several issues.\n\nFirst there are the issues with the partition sizes for 4MB mentioned in the [Hardware](#hardware) section.\n\nThe bigger issue is that the build consistently crashes if the BLE scan task starts up. It's a bit unclear to me exactly what is failing since the backtrace is showing an exception in `new[]` memory allocation in the UDP stack. There appears to be a ton of heap available, so my guess is that this is a synchronization issue of some sort from the tasks running in parallel. I tried messing with the task core affinity a bit but didn't make much progress. It's not really clear what difference between the ESP32S3 and ESP32 would cause this difference.\n\nAt the end of the day, its generally not advised to run the BLE and Wifi at the same time anyway (though it appears to work without issue on the ESP32S3). Probably the best path forward would be to switch between them. This would actually not be too much of an issue, since discovering and getting data from the die should be possible to do in bursts (at least in theory).\n"
  },
  {
    "path": "usermods/pixels_dice_tray/WLED_ESP32_4MB_64KB_FS.csv",
    "content": "# Name,   Type, SubType, Offset,  Size, Flags\nnvs,      data, nvs,     0x9000,  0x5000,\notadata,  data, ota,     0xe000,  0x2000,\napp0,     app,  ota_0,   0x10000, 0x1F0000,\napp1,     app,  ota_1,   0x200000,0x1F0000,\nspiffs,   data, spiffs,  0x3F0000,0x10000,"
  },
  {
    "path": "usermods/pixels_dice_tray/dice_state.h",
    "content": "/**\n * Structs for passing around usermod state\n */\n#pragma once\n\n#include <pixels_dice_interface.h>  // https://github.com/axlan/arduino-pixels-dice\n\n/**\n * Here's how the rolls are tracked in this usermod.\n * 1. The arduino-pixels-dice library reports rolls and state mapped to\n *    PixelsDieID.\n * 2. The \"configured_die_names\" sets which die to connect to and their order.\n * 3. The rest of the usermod references the die by this order (ie. the LED\n *    effect is triggered for rolls for die 0).\n */\n\nstatic constexpr size_t MAX_NUM_DICE = 2;\nstatic constexpr uint8_t INVALID_ROLL_VALUE = 0xFF;\n\n/**\n * The state of the connected die, and new events since the last update.\n */\nstruct DiceUpdate {\n  // The vectors to hold results queried from the library\n  // Since vectors allocate data, it's more efficient to keep reusing an instance\n  // instead of declaring them on the stack.\n  std::vector<pixels::PixelsDieID> dice_list;\n  pixels::RollUpdates roll_updates;\n  pixels::BatteryUpdates battery_updates;\n  // The PixelsDieID for each dice index. 0 if the die isn't connected.\n  // The ordering here matches configured_die_names.\n  std::array<pixels::PixelsDieID, MAX_NUM_DICE> connected_die_ids{0, 0};\n};\n\nstruct DiceSettings {\n  // The mapping of dice names, to the index of die used for effects (ie. The\n  // die named \"Cat\" is die 0). BLE discovery will stop when all the dice are\n  // found. The die slot is disabled if the name is empty. If the name is \"*\",\n  // the slot will use the first unassociated die it sees.\n  std::array<std::string, MAX_NUM_DICE> configured_die_names{\"*\", \"*\"};\n  // A label set to describe the next die roll. Index into GetRollName().\n  uint8_t roll_label = INVALID_ROLL_VALUE;\n};\n\n// These are updated in the main loop, but accessed by the effect functions as\n// well. My understand is that both of these accesses should be running on the\n// same \"thread/task\" since WLED doesn't directly create additional threads. The\n// exception would be network callbacks and interrupts, but I don't believe\n// these accesses are triggered by those. If synchronization was needed, I could\n// look at the example in `requestJSONBufferLock()`.\nstd::array<pixels::RollEvent, MAX_NUM_DICE> last_die_events;\n\nstatic pixels::RollEvent GetLastRoll() {\n  pixels::RollEvent last_roll;\n  for (const auto& event : last_die_events) {\n    if (event.timestamp > last_roll.timestamp) {\n      last_roll = event;\n    }\n  }\n  return last_roll;\n}\n\n/**\n * Returns true if the container has an item that matches the value.\n */\ntemplate <typename C, typename T>\nstatic bool Contains(const C& container, T value) {\n  return std::find(container.begin(), container.end(), value) !=\n         container.end();\n}\n\n// These aren't known until runtime since they're being added dynamically.\nstatic uint8_t FX_MODE_SIMPLE_D20 = 0xFF;\nstatic uint8_t FX_MODE_PULSE_D20 = 0xFF;\nstatic uint8_t FX_MODE_CHECK_D20 = 0xFF;\nstd::array<uint8_t, 3> DIE_LED_MODES = {0xFF, 0xFF, 0xFF};\n"
  },
  {
    "path": "usermods/pixels_dice_tray/generate_roll_info.py",
    "content": "'''\nFile for generating roll labels and info text for the InfoMenu.\n\nUses a very limited markdown language for styling text.\n'''\nimport math\nfrom pathlib import Path\nimport re\nfrom textwrap import indent\n\n# Variables for calculating values in info text\nCASTER_LEVEL = 9\nSPELL_ABILITY_MOD = 6\nBASE_ATK_BONUS = 6\nSIZE_BONUS = 1\nSTR_BONUS = 2\nDEX_BONUS = -1\n\n# TFT library color values\nTFT_BLACK       =0x0000\nTFT_NAVY        =0x000F\nTFT_DARKGREEN   =0x03E0\nTFT_DARKCYAN    =0x03EF\nTFT_MAROON      =0x7800\nTFT_PURPLE      =0x780F\nTFT_OLIVE       =0x7BE0\nTFT_LIGHTGREY   =0xD69A\nTFT_DARKGREY    =0x7BEF\nTFT_BLUE        =0x001F\nTFT_GREEN       =0x07E0\nTFT_CYAN        =0x07FF\nTFT_RED         =0xF800\nTFT_MAGENTA     =0xF81F\nTFT_YELLOW      =0xFFE0\nTFT_WHITE       =0xFFFF\nTFT_ORANGE      =0xFDA0\nTFT_GREENYELLOW =0xB7E0\nTFT_PINK        =0xFE19\nTFT_BROWN       =0x9A60\nTFT_GOLD        =0xFEA0\nTFT_SILVER      =0xC618\nTFT_SKYBLUE     =0x867D\nTFT_VIOLET      =0x915C\n\n\nclass Size:\n    def __init__(self, w, h):\n        self.w = w\n        self.h = h\n\n\n# Font 1 6x8\n# Font 2 12x16\nCHAR_SIZE = {\n    1: Size(6, 8),\n    2: Size(12, 16),\n}\n\nSCREEN_SIZE = Size(128, 128)\n\n# Calculates distance for short range spell.\ndef short_range() -> int:\n    return 25 + 5 * CASTER_LEVEL\n\n# Entries in markdown language.\n# Parameter 0 of the tuple is the roll name\n# Parameter 1 of the tuple is the roll info.\n# The text will be shown when the roll type is selected. An error will be raised\n# if the text would unexpectedly goes past the end of the screen. There are a\n# few styling parameters that need to be on their own lines:\n# $COLOR - The color for the text\n# $SIZE - Sets the text size (see CHAR_SIZE)\n# $WRAP - By default text won't wrap and generate an error. This enables text wrapping. Lines will wrap mid-word.\nENTRIES = [\n    tuple([\"Barb Chain\", f'''\\\n$COLOR({TFT_RED})\nBarb Chain\n$COLOR({TFT_WHITE})\nAtk/CMD {BASE_ATK_BONUS + SPELL_ABILITY_MOD}\nRange: {short_range()}\n$WRAP(1)\n$SIZE(1)\nSummon {1 + math.floor((CASTER_LEVEL-1)/3)} chains. Make a melee atk 1d6 or a trip CMD=AT. On a hit make Will save or shaken 1d4 rnds.\n''']),\n    tuple([\"Saves\", f'''\\\n$COLOR({TFT_GREEN})\nSaves\n$COLOR({TFT_WHITE})\nFORT 8\nREFLEX 8\nWILL 9\n''']),\n    tuple([\"Skill\", f'''\\\nSkill\n''']),\n    tuple([\"Attack\", f'''\\\nAttack\nMelee +{BASE_ATK_BONUS + SIZE_BONUS + STR_BONUS}\nRange +{BASE_ATK_BONUS + SIZE_BONUS + DEX_BONUS}\n''']),\n    tuple([\"Cure\", f'''\\\nCure\nLit 1d8+{min(5, CASTER_LEVEL)}\nMod 2d8+{min(10, CASTER_LEVEL)}\nSer 3d8+{min(15, CASTER_LEVEL)}\n''']),\n    tuple([\"Concentrate\", f'''\\\nConcentrat\n+{CASTER_LEVEL + SPELL_ABILITY_MOD}\n$SIZE(1)\nDefensive 15+2*SP_LV\nDmg 10+DMG+SP_LV\nGrapple 10+CMB+SP_LV\n''']),\n]\n\nRE_SIZE = re.compile(r'\\$SIZE\\(([0-9])\\)')\nRE_COLOR = re.compile(r'\\$COLOR\\(([0-9]+)\\)')\nRE_WRAP = re.compile(r'\\$WRAP\\(([0-9])\\)')\n\nEND_HEADER_TXT = '// GENERATED\\n'\n\ndef main():\n    roll_info_file = Path(__file__).parent / 'roll_info.h'\n    old_contents = open(roll_info_file, 'r').read()\n\n    end_header = old_contents.index(END_HEADER_TXT)\n\n    with open(roll_info_file, 'w') as fd:\n        fd.write(old_contents[:end_header+len(END_HEADER_TXT)])\n\n        for key, entry in enumerate(ENTRIES):\n            size = 2\n            wrap = False\n            y_loc = 0\n            results = []\n            for line in entry[1].splitlines():\n                if line.startswith('$'):\n                    m_size = RE_SIZE.match(line)\n                    m_color = RE_COLOR.match(line)\n                    m_wrap = RE_WRAP.match(line)\n                    if m_size:\n                        size = int(m_size.group(1))\n                        results.append(f'tft.setTextSize({size});')\n                    elif m_color:\n                        results.append(\n                            f'tft.setTextColor({int(m_color.group(1))});')\n                    elif m_wrap:\n                        wrap = bool(int(m_wrap.group(1)))\n                    else:\n                        print(f'Entry {key} unknown modifier \"{line}\".')\n                        exit(1)\n                else:\n                    max_chars_per_line = math.floor(\n                        SCREEN_SIZE.w / CHAR_SIZE[size].w)\n                    if len(line) > max_chars_per_line:\n                        if wrap:\n                            while len(line) > max_chars_per_line:\n                                results.append(\n                                    f'tft.println(\"{line[:max_chars_per_line]}\");')\n                                line = line[max_chars_per_line:].lstrip()\n                                y_loc += CHAR_SIZE[size].h\n                        else:\n                            print(f'Entry {key} line \"{line}\" too long.')\n                            exit(1)\n\n                    if len(line) > 0:\n                        y_loc += CHAR_SIZE[size].h\n                        results.append(f'tft.println(\"{line}\");')\n\n                    if y_loc > SCREEN_SIZE.h:\n                        print(\n                            f'Entry {key} line \"{line}\" went past bottom of screen.')\n                        exit(1)\n\n            result = indent('\\n'.join(results), '  ')\n\n            fd.write(f'''\\\nstatic void PrintRoll{key}() {{\n{result}\n}}\n\n''')\n\n        results = []\n        for key, entry in enumerate(ENTRIES):\n            results.append(f'''\\\ncase {key}:\n  return \"{entry[0]}\";''')\n\n        cases = indent('\\n'.join(results), '    ')\n\n        fd.write(f'''\\\nstatic const char* GetRollName(uint8_t key) {{\n  switch (key) {{\n{cases}\n  }}\n  return \"\";\n}}\n\n''')\n\n        results = []\n        for key, entry in enumerate(ENTRIES):\n            results.append(f'''\\\ncase {key}:\n  PrintRoll{key}();\n  return;''')\n\n        cases = indent('\\n'.join(results), '    ')\n\n        fd.write(f'''\\\nstatic void PrintRollInfo(uint8_t key) {{\n  tft.setTextColor(TFT_WHITE);\n  tft.setCursor(0, 0);\n  tft.setTextSize(2);\n  switch (key) {{\n{cases}\n  }}\n  tft.setTextColor(TFT_RED);\n  tft.setCursor(0, 60);\n  tft.println(\"Unknown\");\n}}\n\n''')\n\n        fd.write(f'static constexpr size_t NUM_ROLL_INFOS = {len(ENTRIES)};\\n')\n\n\nmain()\n"
  },
  {
    "path": "usermods/pixels_dice_tray/led_effects.h",
    "content": "/**\n * The LED effects influenced by dice rolls.\n */\n#pragma once\n\n#include \"wled.h\"\n\n#include \"dice_state.h\"\n\n// Reuse FX display functions.\nextern void mode_breath();\nextern void mode_blends();\nextern void mode_glitter();\nextern void mode_gravcenter();\n\nstatic constexpr uint8_t USER_ANY_DIE = 0xFF;\n/**\n * Two custom effect parameters are used.\n * c1 - Source Die. Sets which die from [0 - MAX_NUM_DICE) controls this effect.\n *      If this is set to 0xFF, use the latest event regardless of which die it\n *      came from.\n * c2 - Target Roll. Sets the \"success\" criteria for a roll to >= this value.\n */\n\n/**\n * Return the last die roll based on the custom1 effect setting.\n */\nstatic pixels::RollEvent GetLastRollForSegment() {\n  // If an invalid die is selected, fallback to using the most recent roll from\n  // any die.\n  if (SEGMENT.custom1 >= MAX_NUM_DICE) {\n    return GetLastRoll();\n  } else {\n    return last_die_events[SEGMENT.custom1];\n  }\n}\n\n\n/*\n * Alternating pixels running function (copied static function).\n */\n// paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined)\n#define PALETTE_SOLID_WRAP   (paletteBlend == 1 || paletteBlend == 3)\nstatic void running_copy(uint32_t color1, uint32_t color2, bool theatre = false) {\n  int width = (theatre ? 3 : 1) + (SEGMENT.intensity >> 4);  // window\n  uint32_t cycleTime = 50 + (255 - SEGMENT.speed);\n  uint32_t it = strip.now / cycleTime;\n  bool usePalette = color1 == SEGCOLOR(0);\n\n  for (int i = 0; i < SEGLEN; i++) {\n    uint32_t col = color2;\n    if (usePalette) color1 = SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0);\n    if (theatre) {\n      if ((i % width) == SEGENV.aux0) col = color1;\n    } else {\n      int pos = (i % (width<<1));\n      if ((pos < SEGENV.aux0-width) || ((pos >= SEGENV.aux0) && (pos < SEGENV.aux0+width))) col = color1;\n    }\n    SEGMENT.setPixelColor(i,col);\n  }\n\n  if (it != SEGENV.step) {\n    SEGENV.aux0 = (SEGENV.aux0 +1) % (theatre ? width : (width<<1));\n    SEGENV.step = it;\n  }\n}\n\nstatic void simple_roll() {\n  auto roll = GetLastRollForSegment();\n  if (roll.state != pixels::RollState::ON_FACE) {\n    SEGMENT.fill(0);\n  } else {\n    uint16_t num_segments = float(roll.current_face + 1) / 20.0 * SEGLEN;\n    for (int i = 0; i <= num_segments; i++) {\n      SEGMENT.setPixelColor(i, SEGCOLOR(0));\n    }\n    for (int i = num_segments; i < SEGLEN; i++) {\n      SEGMENT.setPixelColor(i, SEGCOLOR(1));\n    }\n  }\n}\n// See https://kno.wled.ge/interfaces/json-api/#effect-metadata\n// Name - DieSimple\n// Parameters -\n//   * Selected Die (custom1)\n// Colors - Uses color1 and color2\n// Palette - Not used\n// Flags - Effect is optimized for use on 1D LED strips.\n// Defaults - Selected Die set to 0xFF (USER_ANY_DIE)\nstatic const char _data_FX_MODE_SIMPLE_DIE[] PROGMEM =\n    \"DieSimple@,,Selected Die;!,!;;1;c1=255\";\n\nstatic void pulse_roll() {\n  auto roll = GetLastRollForSegment();\n  if (roll.state != pixels::RollState::ON_FACE) {\n    mode_breath();\n    return;\n  } else {\n    mode_blends();\n    uint16_t num_segments = float(roll.current_face + 1) / 20.0 * SEGLEN;\n    for (int i = num_segments; i < SEGLEN; i++) {\n      SEGMENT.setPixelColor(i, SEGCOLOR(1));\n    }\n  }\n}\nstatic const char _data_FX_MODE_PULSE_DIE[] PROGMEM =\n    \"DiePulse@!,!,Selected Die;!,!;!;1;sx=24,pal=50,c1=255\";\n\nstatic void check_roll() {\n  auto roll = GetLastRollForSegment();\n  if (roll.state != pixels::RollState::ON_FACE) {\n    running_copy(SEGCOLOR(0), SEGCOLOR(2));\n    return;\n  } else {\n    if (roll.current_face + 1 >= SEGMENT.custom2) {\n      mode_glitter();\n      return;\n    } else {\n      mode_gravcenter();\n      return;\n    }\n  }\n}\nstatic const char _data_FX_MODE_CHECK_DIE[] PROGMEM =\n    \"DieCheck@!,!,Selected Die,Target Roll;1,2,3;!;1;pal=0,ix=128,m12=2,si=0,c1=255,c2=10\";\n"
  },
  {
    "path": "usermods/pixels_dice_tray/mqtt_client/mqtt_logger.py",
    "content": "#!/usr/bin/env python\nimport argparse\nimport json\nimport os\nfrom pathlib import Path\nimport time\n\n# Dependency installed with `pip install paho-mqtt`.\n# https://pypi.org/project/paho-mqtt/\nimport paho.mqtt.client as mqtt\n\nstate = {\"label\": \"None\"}\n\n\n# Define MQTT callbacks\ndef on_connect(client, userdata, connect_flags, reason_code, properties):\n    print(\"Connected with result code \" + str(reason_code))\n    state[\"start_time\"] = None\n    client.subscribe(f\"{state['root_topic']}#\")\n\n\ndef on_message(client, userdata, msg):\n    if msg.topic.endswith(\"roll_label\"):\n        state[\"label\"] = msg.payload.decode(\"ascii\")\n        print(f\"Label set to {state['label']}\")\n    elif msg.topic.endswith(\"roll\"):\n        json_str = msg.payload.decode(\"ascii\")\n        msg_data = json.loads(json_str)\n        # Convert the relative timestamps reported to the dice to an approximate absolute time.\n        # The \"last_time\" check is to detect if the ESP32 was restarted or the counter rolled over.\n        if state[\"start_time\"] is None or msg_data[\"time\"] < state[\"last_time\"]:\n            state[\"start_time\"] = time.time() - (msg_data[\"time\"] / 1000.0)\n        state[\"last_time\"] = msg_data[\"time\"]\n        timestamp = state[\"start_time\"] + (msg_data[\"time\"] / 1000.0)\n        state[\"csv_fd\"].write(\n            f\"{timestamp:.3f}, {msg_data['name']}, {state['label']}, {msg_data['state']}, {msg_data['val']}\\n\"\n        )\n        state[\"csv_fd\"].flush()\n        if msg_data[\"state\"] == 1:\n            print(\n                f\"{timestamp:.3f}: {msg_data['name']} rolled {msg_data['val']}\")\n\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description=\"Log die rolls from WLED MQTT events to CSV.\")\n\n    # IP address (with a default value)\n    parser.add_argument(\n        \"--host\",\n        type=str,\n        default=\"127.0.0.1\",\n        help=\"Host address of broker (default: 127.0.0.1)\",\n    )\n    parser.add_argument(\n        \"--port\", type=int, default=1883, help=\"Broker TCP port (default: 1883)\"\n    )\n    parser.add_argument(\"--user\", type=str, help=\"Optional MQTT username\")\n    parser.add_argument(\"--password\", type=str, help=\"Optional MQTT password\")\n    parser.add_argument(\n        \"--topic\",\n        type=str,\n        help=\"Optional MQTT topic to listen to. For example if topic is 'wled/e5a658/dice/', subscript to  to 'wled/e5a658/dice/#'. By default, listen to all topics looking for ones that end in 'roll_label' and 'roll'.\",\n    )\n    parser.add_argument(\n        \"-o\",\n        \"--output-dir\",\n        type=Path,\n        default=Path(__file__).absolute().parent / \"logs\",\n        help=\"Directory to log to\",\n    )\n    args = parser.parse_args()\n\n    timestr = time.strftime(\"%Y-%m-%d\")\n    os.makedirs(args.output_dir, exist_ok=True)\n    state[\"csv_fd\"] = open(args.output_dir / f\"roll_log_{timestr}.csv\", \"a\")\n\n    # Create `an MQTT client\n    client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)\n\n    # Set MQTT callbacks\n    client.on_connect = on_connect\n    client.on_message = on_message\n\n    if args.user and args.password:\n        client.username_pw_set(args.user, args.password)\n\n    state[\"root_topic\"] = \"\"\n\n    # Connect to the MQTT broker\n    client.connect(args.host, args.port, 60)\n\n    try:\n        while client.loop(timeout=1.0) == mqtt.MQTT_ERR_SUCCESS:\n            time.sleep(0.1)\n    except KeyboardInterrupt:\n        exit(0)\n\n    print(\"Connection Failure\")\n    exit(1)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "usermods/pixels_dice_tray/mqtt_client/mqtt_plotter.py",
    "content": "import argparse\nfrom http import server\nimport os\nfrom pathlib import Path\nimport socketserver\n\nimport pandas as pd\nimport plotly.express as px\n\n# python -m http.server 8000 --directory /tmp/\n\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description=\"Generate an html plot of rolls captured by mqtt_logger.py\")\n    parser.add_argument(\"input_file\", type=Path, help=\"Log file to plot\")\n    parser.add_argument(\n        \"-s\",\n        \"--start-server\",\n        action=\"store_true\",\n        help=\"After generating the plot, run a webserver pointing to it\",\n    )\n    parser.add_argument(\n        \"-o\",\n        \"--output-dir\",\n        type=Path,\n        default=Path(__file__).absolute().parent / \"logs\",\n        help=\"Directory to log to\",\n    )\n    args = parser.parse_args()\n\n    df = pd.read_csv(\n        args.input_file, names=[\"timestamp\", \"die\", \"label\", \"state\", \"roll\"]\n    )\n\n    df_filt = df[df[\"state\"] == 1]\n\n    time = (df_filt[\"timestamp\"] - df_filt[\"timestamp\"].min()) / 60 / 60\n\n    fig = px.bar(\n        df_filt,\n        x=time,\n        y=\"roll\",\n        color=\"label\",\n        labels={\n            \"x\": \"Game Time (min)\",\n        },\n        title=f\"Roll Report: {args.input_file.name}\",\n    )\n\n    output_path = args.output_dir / (args.input_file.stem + \".html\")\n\n    fig.write_html(output_path)\n    if args.start_server:\n        PORT = 8000\n        os.chdir(args.output_dir)\n        try:\n            with socketserver.TCPServer(\n                (\"\", PORT), server.SimpleHTTPRequestHandler\n            ) as httpd:\n                print(\n                    f\"Serving HTTP on http://0.0.0.0:{PORT}/{output_path.name}\")\n                httpd.serve_forever()\n        except KeyboardInterrupt:\n            pass\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "usermods/pixels_dice_tray/mqtt_client/requirements.txt",
    "content": "plotly-express\npaho-mqtt"
  },
  {
    "path": "usermods/pixels_dice_tray/pixels_dice_tray.cpp",
    "content": "#include <pixels_dice_interface.h>  // https://github.com/axlan/arduino-pixels-dice\n#include \"wled.h\"\n\n#include \"dice_state.h\"\n#include \"led_effects.h\"\n#include \"tft_menu.h\"\n\n// Set this parameter to rotate the display. 1-3 rotate by 90,180,270 degrees.\n#ifndef USERMOD_PIXELS_DICE_TRAY_ROTATION\n  #define USERMOD_PIXELS_DICE_TRAY_ROTATION 0\n#endif\n\n// How often we are redrawing screen\n#ifndef USERMOD_PIXELS_DICE_TRAY_REFRESH_RATE_MS\n  #define USERMOD_PIXELS_DICE_TRAY_REFRESH_RATE_MS 200\n#endif\n\n// Time with no updates before screen turns off (-1 to disable)\n#ifndef USERMOD_PIXELS_DICE_TRAY_TIMEOUT_MS\n  #define USERMOD_PIXELS_DICE_TRAY_TIMEOUT_MS 5 * 60 * 1000\n#endif\n\n// Duration of each search for BLE devices.\n#ifndef BLE_SCAN_DURATION_SEC\n  #define BLE_SCAN_DURATION_SEC 4\n#endif\n\n// Time between searches for BLE devices.\n#ifndef BLE_TIME_BETWEEN_SCANS_SEC\n  #define BLE_TIME_BETWEEN_SCANS_SEC 5\n#endif\n\n#define WLED_DEBOUNCE_THRESHOLD \\\n  50  // only consider button input of at least 50ms as valid (debouncing)\n#define WLED_LONG_PRESS \\\n  600  // long press if button is released after held for at least 600ms\n#define WLED_DOUBLE_PRESS \\\n  350  // double press if another press within 350ms after a short press\n\nclass PixelsDiceTrayUsermod : public Usermod {\n private:\n  bool enabled = true;\n\n  DiceUpdate dice_update;\n\n  // Settings\n  uint32_t ble_scan_duration_sec = BLE_SCAN_DURATION_SEC;\n  unsigned rotation = USERMOD_PIXELS_DICE_TRAY_ROTATION;\n  DiceSettings dice_settings;\n\n#if USING_TFT_DISPLAY\n  MenuController menu_ctrl;\n#endif\n\n  static void center(String& line, uint8_t width) {\n    int len = line.length();\n    if (len < width)\n      for (byte i = (width - len) / 2; i > 0; i--)\n        line = ' ' + line;\n    for (byte i = line.length(); i < width; i++)\n      line += ' ';\n  }\n\n  // NOTE: THIS MOD DOES NOT SUPPORT CHANGING THE SPI PINS FROM THE UI! The\n  // TFT_eSPI library requires that they are compiled in.\n  static void SetSPIPinsFromMacros() {\n#if USING_TFT_DISPLAY\n    spi_mosi = TFT_MOSI;\n    // Done in TFT library.\n    if (TFT_MISO == TFT_MOSI) {\n      spi_miso = -1;\n    }\n    spi_sclk = TFT_SCLK;\n#endif\n  }\n\n  void UpdateDieNames(\n      const std::array<const std::string, MAX_NUM_DICE>& new_die_names) {\n    for (size_t i = 0; i < MAX_NUM_DICE; i++) {\n      // If the saved setting was a wildcard, and that connected to a die, use\n      // the new name instead of the wildcard. Saving this \"locks\" the name in.\n      bool overriden_wildcard =\n          new_die_names[i] == \"*\" && dice_update.connected_die_ids[i] != 0;\n      if (!overriden_wildcard &&\n          new_die_names[i] != dice_settings.configured_die_names[i]) {\n        dice_settings.configured_die_names[i] = new_die_names[i];\n        dice_update.connected_die_ids[i] = 0;\n        last_die_events[i] = pixels::RollEvent();\n      }\n    }\n  }\n\n public:\n  PixelsDiceTrayUsermod()\n#if USING_TFT_DISPLAY\n      : menu_ctrl(&dice_settings)\n#endif\n  {\n  }\n\n  // Functions called by WLED\n\n  /*\n   * setup() is called once at boot. WiFi is not yet connected at this point.\n   * You can use it to initialize variables, sensors or similar.\n   */\n  void setup() override {\n    DEBUG_PRINTLN(F(\"DiceTray: init\"));\n#if USING_TFT_DISPLAY\n    SetSPIPinsFromMacros();\n    PinManagerPinType spiPins[] = {\n        {spi_mosi, true}, {spi_miso, false}, {spi_sclk, true}};\n    if (!PinManager::allocateMultiplePins(spiPins, 3, PinOwner::HW_SPI)) {\n      enabled = false;\n    } else {\n      PinManagerPinType displayPins[] = {\n          {TFT_CS, true}, {TFT_DC, true}, {TFT_RST, true}, {TFT_BL, true}};\n      if (!PinManager::allocateMultiplePins(\n              displayPins, sizeof(displayPins) / sizeof(PinManagerPinType),\n              PinOwner::UM_FourLineDisplay)) {\n        PinManager::deallocateMultiplePins(spiPins, 3, PinOwner::HW_SPI);\n        enabled = false;\n      }\n    }\n\n    if (!enabled) {\n      DEBUG_PRINTLN(F(\"DiceTray: TFT Display pin allocations failed.\"));\n      return;\n    }\n#endif\n\n    // Need to enable WiFi sleep:\n    // \"E (1513) wifi:Error! Should enable WiFi modem sleep when both WiFi and Bluetooth are enabled!!!!!!\"\n    noWifiSleep = false;\n\n    // Get the mode indexes that the effects are registered to.\n    FX_MODE_SIMPLE_D20 = strip.addEffect(255, &simple_roll, _data_FX_MODE_SIMPLE_DIE);\n    FX_MODE_PULSE_D20 = strip.addEffect(255, &pulse_roll, _data_FX_MODE_PULSE_DIE);\n    FX_MODE_CHECK_D20 = strip.addEffect(255, &check_roll, _data_FX_MODE_CHECK_DIE);\n    DIE_LED_MODES = {FX_MODE_SIMPLE_D20, FX_MODE_PULSE_D20, FX_MODE_CHECK_D20};\n\n    // Start a background task scanning for dice.\n    // On completion the discovered dice are connected to.\n    pixels::ScanForDice(ble_scan_duration_sec, BLE_TIME_BETWEEN_SCANS_SEC);\n\n#if USING_TFT_DISPLAY\n    menu_ctrl.Init(rotation);\n#endif\n  }\n\n  /*\n   * connected() is called every time the WiFi is (re)connected\n   * Use it to initialize network interfaces\n   */\n  void connected() override {\n    // Serial.println(\"Connected to WiFi!\");\n  }\n\n  /*\n   * loop() is called continuously. Here you can check for events, read sensors,\n   * etc.\n   *\n   * Tips:\n   * 1. You can use \"if (WLED_CONNECTED)\" to check for a successful network\n   * connection. Additionally, \"if (WLED_MQTT_CONNECTED)\" is available to check\n   * for a connection to an MQTT broker.\n   *\n   * 2. Try to avoid using the delay() function. NEVER use delays longer than 10\n   * milliseconds. Instead, use a timer check as shown here.\n   */\n  void loop() override {\n    static long last_loop_time = 0;\n    static long last_die_connected_time = millis();\n\n    char mqtt_topic_buffer[MQTT_MAX_TOPIC_LEN + 16];\n    char mqtt_data_buffer[128];\n\n    // Check if we time interval for redrawing passes.\n    if (millis() - last_loop_time < USERMOD_PIXELS_DICE_TRAY_REFRESH_RATE_MS) {\n      return;\n    }\n    last_loop_time = millis();\n\n    // Update dice_list with the connected dice\n    pixels::ListDice(dice_update.dice_list);\n    // Get all the roll/battery updates since the last loop\n    pixels::GetDieRollUpdates(dice_update.roll_updates);\n    pixels::GetDieBatteryUpdates(dice_update.battery_updates);\n\n    // Go through list of connected die.\n    // TODO: Blacklist die that are connected to, but don't match the configured\n    //       names.\n    std::array<bool, MAX_NUM_DICE> die_connected = {false, false};\n    for (auto die_id : dice_update.dice_list) {\n      bool matched = false;\n      // First check if we've already matched this ID to a connected die.\n      for (size_t i = 0; i < MAX_NUM_DICE; i++) {\n        if (die_id == dice_update.connected_die_ids[i]) {\n          die_connected[i] = true;\n          matched = true;\n          break;\n        }\n      }\n\n      // If this isn't already matched, check if its name matches an expected name.\n      if (!matched) {\n        auto die_name = pixels::GetDieDescription(die_id).name;\n        for (size_t i = 0; i < MAX_NUM_DICE; i++) {\n          if (0 == dice_update.connected_die_ids[i] &&\n              die_name == dice_settings.configured_die_names[i]) {\n            dice_update.connected_die_ids[i] = die_id;\n            die_connected[i] = true;\n            matched = true;\n            DEBUG_PRINTF_P(PSTR(\"DiceTray: %u (%s) connected.\\n\"), i,\n                           die_name.c_str());\n            break;\n          }\n        }\n\n        // If it doesn't match any expected names, check if there's any wildcards to match.\n        if (!matched) {\n          auto description = pixels::GetDieDescription(die_id);\n          for (size_t i = 0; i < MAX_NUM_DICE; i++) {\n            if (dice_settings.configured_die_names[i] == \"*\") {\n              dice_update.connected_die_ids[i] = die_id;\n              die_connected[i] = true;\n              dice_settings.configured_die_names[i] = die_name;\n              DEBUG_PRINTF_P(PSTR(\"DiceTray: %u (%s) connected as wildcard.\\n\"),\n                             i, die_name.c_str());\n              break;\n            }\n          }\n        }\n      }\n    }\n\n    // Clear connected die that aren't still present.\n    bool all_found = true;\n    bool none_found = true;\n    for (size_t i = 0; i < MAX_NUM_DICE; i++) {\n      if (!die_connected[i]) {\n        if (dice_update.connected_die_ids[i] != 0) {\n          dice_update.connected_die_ids[i] = 0;\n          last_die_events[i] = pixels::RollEvent();\n          DEBUG_PRINTF_P(PSTR(\"DiceTray: %u disconnected.\\n\"), i);\n        }\n\n        if (!dice_settings.configured_die_names[i].empty()) {\n          all_found = false;\n        }\n      } else {\n        none_found = false;\n      }\n    }\n\n    // Update last_die_events\n    for (const auto& roll : dice_update.roll_updates) {\n      for (size_t i = 0; i < MAX_NUM_DICE; i++) {\n        if (dice_update.connected_die_ids[i] == roll.first) {\n          last_die_events[i] = roll.second;\n        }\n      }\n      if (WLED_MQTT_CONNECTED) {\n        snprintf(mqtt_topic_buffer, sizeof(mqtt_topic_buffer), PSTR(\"%s/%s\"),\n                 mqttDeviceTopic, \"dice/roll\");\n        const char* name = pixels::GetDieDescription(roll.first).name.c_str();\n        snprintf(mqtt_data_buffer, sizeof(mqtt_data_buffer),\n                 \"{\\\"name\\\":\\\"%s\\\",\\\"state\\\":%d,\\\"val\\\":%d,\\\"time\\\":%d}\", name,\n                 int(roll.second.state), roll.second.current_face + 1,\n                 roll.second.timestamp);\n        mqtt->publish(mqtt_topic_buffer, 0, false, mqtt_data_buffer);\n      }\n    }\n\n#if USERMOD_PIXELS_DICE_TRAY_TIMEOUT_MS > 0 && USING_TFT_DISPLAY\n    // If at least one die is configured, but none are found\n    if (none_found) {\n      if (millis() - last_die_connected_time >\n          USERMOD_PIXELS_DICE_TRAY_TIMEOUT_MS) {\n        // Turn off LEDs and backlight and go to sleep.\n        // Since none of the wake up pins are wired up, expect to sleep\n        // until power cycle or reset, so don't need to handle normal\n        // wakeup.\n        bri = 0;\n        applyFinalBri();\n        menu_ctrl.EnableBacklight(false);\n        gpio_hold_en((gpio_num_t)TFT_BL);\n        gpio_deep_sleep_hold_en();\n        esp_deep_sleep_start();\n      }\n    } else {\n      last_die_connected_time = millis();\n    }\n#endif\n\n    if (pixels::IsScanning() && all_found) {\n      DEBUG_PRINTF_P(PSTR(\"DiceTray: All dice found. Stopping search.\\n\"));\n      pixels::StopScanning();\n    } else if (!pixels::IsScanning() && !all_found) {\n      DEBUG_PRINTF_P(PSTR(\"DiceTray: Resuming dice search.\\n\"));\n      pixels::ScanForDice(ble_scan_duration_sec, BLE_TIME_BETWEEN_SCANS_SEC);\n    }\n#if USING_TFT_DISPLAY\n    menu_ctrl.Update(dice_update);\n#endif\n  }\n\n  /*\n   * addToJsonInfo() can be used to add custom entries to the /json/info part of\n   * the JSON API. Creating an \"u\" object allows you to add custom key/value\n   * pairs to the Info section of the WLED web UI. Below it is shown how this\n   * could be used for e.g. a light sensor\n   */\n  void addToJsonInfo(JsonObject& root) override {\n    JsonObject user = root[\"u\"];\n    if (user.isNull())\n      user = root.createNestedObject(\"u\");\n\n    JsonArray lightArr = user.createNestedArray(\"DiceTray\");  // name\n    lightArr.add(enabled ? F(\"installed\") : F(\"disabled\"));   // unit\n  }\n\n  /*\n   * addToJsonState() can be used to add custom entries to the /json/state part\n   * of the JSON API (state object). Values in the state object may be modified\n   * by connected clients\n   */\n  void addToJsonState(JsonObject& root) override {\n    // root[\"user0\"] = userVar0;\n  }\n\n  /*\n   * readFromJsonState() can be used to receive data clients send to the\n   * /json/state part of the JSON API (state object). Values in the state object\n   * may be modified by connected clients\n   */\n  void readFromJsonState(JsonObject& root) override {\n    // userVar0 = root[\"user0\"] | userVar0; //if \"user0\" key exists in JSON,\n    // update, else keep old value if (root[\"bri\"] == 255)\n    // Serial.println(F(\"Don't burn down your garage!\"));\n  }\n\n  /*\n   * addToConfig() can be used to add custom persistent settings to the cfg.json\n   * file in the \"um\" (usermod) object. It will be called by WLED when settings\n   * are actually saved (for example, LED settings are saved) If you want to\n   * force saving the current state, use serializeConfig() in your loop().\n   *\n   * CAUTION: serializeConfig() will initiate a filesystem write operation.\n   * It might cause the LEDs to stutter and will cause flash wear if called too\n   * often. Use it sparingly and always in the loop, never in network callbacks!\n   *\n   * addToConfig() will also not yet add your setting to one of the settings\n   * pages automatically. To make that work you still have to add the setting to\n   * the HTML, xml.cpp and set.cpp manually.\n   *\n   * I highly recommend checking out the basics of ArduinoJson serialization and\n   * deserialization in order to use custom settings!\n   */\n  void addToConfig(JsonObject& root) override {\n    JsonObject top = root.createNestedObject(\"DiceTray\");\n    top[\"ble_scan_duration\"] = ble_scan_duration_sec;\n    top[\"die_0\"] = dice_settings.configured_die_names[0];\n    top[\"die_1\"] = dice_settings.configured_die_names[1];\n#if USING_TFT_DISPLAY\n    top[\"rotation\"] = rotation;\n    JsonArray pins = top.createNestedArray(\"pin\");\n    pins.add(TFT_CS);\n    pins.add(TFT_DC);\n    pins.add(TFT_RST);\n    pins.add(TFT_BL);\n#endif\n  }\n\n  void appendConfigData() override {\n    // Slightly annoying that you can't put text before an element.\n    // The an item on the usermod config page has the following HTML:\n    // ```html\n    // Die 0\n    // <input type=\"hidden\" name=\"DiceTray:die_0\" value=\"text\">\n    // <input type=\"text\" name=\"DiceTray:die_0\" value=\"*\" style=\"width:250px;\" oninput=\"check(this,'DiceTray')\">\n    // ```\n    // addInfo let's you add data before or after the two input fields.\n    //\n    // To work around this, add info text to the end of the preceding item.\n    //\n    // See addInfo in wled00/data/settings_um.htm for details on what this function does.\n    oappend(F(\n        \"addInfo('DiceTray:ble_scan_duration',1,'<br><br><i>Set to \\\"*\\\" to \"\n        \"connect to any die.<br>Leave Blank to disable.</i><br><i \"\n        \"class=\\\"warn\\\">Saving will replace \\\"*\\\" with die names.</i>','');\"));\n#if USING_TFT_DISPLAY\n    oappend(F(\"ddr=addDropdown('DiceTray','rotation');\"));\n    oappend(F(\"addOption(ddr,'0 deg',0);\"));\n    oappend(F(\"addOption(ddr,'90 deg',1);\"));\n    oappend(F(\"addOption(ddr,'180 deg',2);\"));\n    oappend(F(\"addOption(ddr,'270 deg',3);\"));\n    oappend(F(\n        \"addInfo('DiceTray:rotation',1,'<br><i class=\\\"warn\\\">DO NOT CHANGE \"\n        \"SPI PINS.</i><br><i class=\\\"warn\\\">CHANGES ARE IGNORED.</i>','');\"));\n    oappend(F(\"addInfo('TFT:pin[]',0,'','SPI CS');\"));\n    oappend(F(\"addInfo('TFT:pin[]',1,'','SPI DC');\"));\n    oappend(F(\"addInfo('TFT:pin[]',2,'','SPI RST');\"));\n    oappend(F(\"addInfo('TFT:pin[]',3,'','SPI BL');\"));\n#endif\n  }\n\n  /*\n   * readFromConfig() can be used to read back the custom settings you added\n   * with addToConfig(). This is called by WLED when settings are loaded\n   * (currently this only happens once immediately after boot)\n   *\n   * readFromConfig() is called BEFORE setup(). This means you can use your\n   * persistent values in setup() (e.g. pin assignments, buffer sizes), but also\n   * that if you want to write persistent values to a dynamic buffer, you'd need\n   * to allocate it here instead of in setup. If you don't know what that is,\n   * don't fret. It most likely doesn't affect your use case :)\n   */\n  bool readFromConfig(JsonObject& root) override {\n    // we look for JSON object:\n    // {\"DiceTray\":{\"rotation\":0,\"font_size\":1}}\n    JsonObject top = root[\"DiceTray\"];\n    if (top.isNull()) {\n      DEBUG_PRINTLN(F(\"DiceTray: No config found. (Using defaults.)\"));\n      return false;\n    }\n\n    if (top.containsKey(\"die_0\") && top.containsKey(\"die_1\")) {\n      const std::array<const std::string, MAX_NUM_DICE> new_die_names{\n          top[\"die_0\"], top[\"die_1\"]};\n      UpdateDieNames(new_die_names);\n    } else {\n      DEBUG_PRINTLN(F(\"DiceTray: No die names found.\"));\n    }\n\n#if USING_TFT_DISPLAY\n    unsigned new_rotation = min(top[\"rotation\"] | rotation, 3u);\n\n    // Restore the SPI pins to their compiled in defaults.\n    SetSPIPinsFromMacros();\n\n    if (new_rotation != rotation) {\n      rotation = new_rotation;\n      menu_ctrl.Init(rotation);\n    }\n\n    // Update with any modified settings.\n    menu_ctrl.Redraw();\n#endif\n\n    // use \"return !top[\"newestParameter\"].isNull();\" when updating Usermod with\n    // new features\n    return !top[\"DiceTray\"].isNull();\n  }\n\n  /**\n   * handleButton() can be used to override default button behaviour. Returning true\n   * will prevent button working in a default way.\n   * Replicating button.cpp\n   */\n#if USING_TFT_DISPLAY\n  bool handleButton(uint8_t b) override {\n    if (!enabled || b > 1  // buttons 0,1 only\n        || buttons[b].type == BTN_TYPE_SWITCH || buttons[b].type == BTN_TYPE_NONE ||\n        buttons[b].type == BTN_TYPE_RESERVED ||\n        buttons[b].type == BTN_TYPE_PIR_SENSOR ||\n        buttons[b].type == BTN_TYPE_ANALOG ||\n        buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {\n      return false;\n    }\n\n    unsigned long now = millis();\n    static bool buttonPressedBefore[2] = {false};\n    static bool buttonLongPressed[2] = {false};\n    static unsigned long buttonPressedTime[2] = {0};\n    static unsigned long buttonWaitTime[2] = {0};\n\n    //momentary button logic\n    if (!buttons[b].longPressed && isButtonPressed(b)) {  //pressed\n      if (!buttons[b].pressedBefore) {\n        buttons[b].pressedTime = now;\n      }\n      buttons[b].pressedBefore = true;\n\n      if (now - buttons[b].pressedTime > WLED_LONG_PRESS) {  //long press\n        menu_ctrl.HandleButton(ButtonType::LONG, b);\n        buttons[b].longPressed = true;\n        return true;\n      }\n    } else if (!isButtonPressed(b) && buttons[b].pressedBefore) {  //released\n\n      long dur = now - buttons[b].pressedTime;\n      if (dur < WLED_DEBOUNCE_THRESHOLD) {\n        buttons[b].pressedBefore = false;\n        return true;\n      }  //too short \"press\", debounce\n\n      bool doublePress = buttons[b].waitTime;  //did we have short press before?\n      buttons[b].waitTime = 0;\n\n      if (!buttons[b].longPressed) {  //short press\n        // if this is second release within 350ms it is a double press (buttonWaitTime!=0)\n        if (doublePress) {\n          menu_ctrl.HandleButton(ButtonType::DOUBLE, b);\n        } else {\n          buttons[b].waitTime = now;\n        }\n      }\n      buttons[b].pressedBefore = false;\n      buttons[b].longPressed = false;\n    }\n    // if 350ms elapsed since last press/release it is a short press\n    if (buttons[b].waitTime && now - buttons[b].waitTime > WLED_DOUBLE_PRESS &&\n        !buttons[b].pressedBefore) {\n      buttons[b].waitTime = 0;\n      menu_ctrl.HandleButton(ButtonType::SINGLE, b);\n    }\n\n    return true;\n  }\n#endif\n\n  /*\n   * getId() allows you to optionally give your V2 usermod an unique ID (please\n   * define it in const.h!). This could be used in the future for the system to\n   * determine whether your usermod is installed.\n   */\n  uint16_t getId() { return USERMOD_ID_PIXELS_DICE_TRAY; }\n\n  // More methods can be added in the future, this example will then be\n  // extended. Your usermod will remain compatible as it does not need to\n  // implement all methods from the Usermod base class!\n};\n\n\nstatic PixelsDiceTrayUsermod pixels_dice_tray;\nREGISTER_USERMOD(pixels_dice_tray);"
  },
  {
    "path": "usermods/pixels_dice_tray/platformio_override.ini.sample",
    "content": "[platformio]\ndefault_envs = t_qt_pro_8MB_dice, esp32s3dev_8MB_qspi_dice\n\n# ------------------------------------------------------------------------------\n# T-QT Pro 8MB with integrated 128x128 TFT screen\n# ------------------------------------------------------------------------------\n[env:t_qt_pro_8MB_dice]\nboard = esp32-s3-devkitc-1 ;; generic dev board;\nplatform = ${esp32s3.platform}\nupload_speed = 921600\nbuild_unflags = ${common.build_unflags}\nboard_build.partitions = ${esp32.large_partitions}\nboard_build.f_flash = 80000000L\nboard_build.flash_mode = qio\nmonitor_filters = esp32_exception_decoder\nbuild_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=T-QT-PRO-8MB\n  -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0\n  -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1      ;; for boards with USB-OTG connector only (USBCDC or \"TinyUSB\")\n  \n  -D USERMOD_PIXELS_DICE_TRAY ;; Enables this UserMod\n  -D USERMOD_PIXELS_DICE_TRAY_BL_ACTIVE_LOW=1\n  -D USERMOD_PIXELS_DICE_TRAY_ROTATION=2\n\n  ;-D WLED_DEBUG\n  ;;;;;;;;;;;;;;;;;; TFT_eSPI Settings ;;;;;;;;;;;;;;;;;;;;;;;;\n  ;-DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG\n  -D USER_SETUP_LOADED=1\n  \n  ; Define the TFT driver, pins etc. from: https://github.com/Bodmer/TFT_eSPI/blob/master/User_Setups/Setup211_LilyGo_T_QT_Pro_S3.h\n  ; GC9A01 128 x 128 display with no chip select line\n  -D USER_SETUP_ID=211\n  -D GC9A01_DRIVER=1\n  -D TFT_WIDTH=128\n  -D TFT_HEIGHT=128\n\n  -D TFT_BACKLIGHT_ON=0\n  -D TFT_ROTATION=3\n  -D CGRAM_OFFSET=1\n\n  -D TFT_MISO=-1\n  -D TFT_MOSI=2\n  -D TFT_SCLK=3\n  -D TFT_CS=5\n  -D TFT_DC=6\n  -D TFT_RST=1\n  -D TFT_BL=10\n  -D LOAD_GLCD=1\n  -D LOAD_FONT2=1\n  -D LOAD_FONT4=1\n  -D LOAD_FONT6=1\n  -D LOAD_FONT7=1\n  -D LOAD_FONT8=1\n  -D LOAD_GFXFF=1\n  ; Avoid SPIFFS dependancy that was causing compile issues.\n  ;-D SMOOTH_FONT=1\n  -D SPI_FREQUENCY=40000000\n  -D SPI_READ_FREQUENCY=20000000\n  -D SPI_TOUCH_FREQUENCY=2500000\n\nlib_deps = ${esp32s3.lib_deps}\n  ${esp32.AR_lib_deps}\n  ESP32 BLE Arduino\n  bodmer/TFT_eSPI @ 2.5.43\n  axlan/pixels-dice-interface @ 1.2.0\n\n# ------------------------------------------------------------------------------\n# ESP32S3 dev board with 8MB flash and no extended RAM.\n# ------------------------------------------------------------------------------\n[env:esp32s3dev_8MB_qspi_dice]\nboard = esp32-s3-devkitc-1 ;; generic dev board;\nplatform = ${esp32s3.platform}\nupload_speed = 921600\nbuild_unflags = ${common.build_unflags}\nboard_build.partitions = ${esp32.large_partitions}\nboard_build.f_flash = 80000000L\nboard_build.flash_mode = qio\nmonitor_filters = esp32_exception_decoder\nbuild_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_8MB_qspi\n  -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0\n  -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1      ;; for boards with USB-OTG connector only (USBCDC or \"TinyUSB\")\n  \n  -D USERMOD_PIXELS_DICE_TRAY ;; Enables this UserMod\n\n  ;-D WLED_DEBUG\nlib_deps = ${esp32s3.lib_deps}\n  ${esp32.AR_lib_deps}\n  ESP32 BLE Arduino\n  axlan/pixels-dice-interface @ 1.2.0\n\n# ------------------------------------------------------------------------------\n# ESP32 dev board without screen\n# ------------------------------------------------------------------------------\n# THIS DOES NOT WORK!!!!!!\n# While it builds and programs onto the device, I ran into a series of issues\n# trying to actually run.\n# Right after the AP init there's an allocation exception which claims to be in\n# the UDP server. There seems to be a ton of heap remaining, so the exact error\n# might be a red herring.\n# It appears that the BLE scanning task is conflicting with the networking tasks.\n# I was successfully running simple applications with the pixels-dice-interface\n# on ESP32 dev boards, so it may be an issue with too much being scheduled in\n# parallel. Also not clear exactly what difference between the ESP32 and the\n# ESP32S3 would be causing this, though they do run different BLE versions.\n# May be related to some of the issues discussed in:\n# https://github.com/wled-dev/WLED/issues/1382\n; [env:esp32dev_dice]\n; extends = env:esp32dev\n; build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=ESP32\n;   ; Enable Pixels dice mod\n;   -D USERMOD_PIXELS_DICE_TRAY\n; lib_deps = ${esp32.lib_deps}\n;   ESP32 BLE Arduino\n;   axlan/pixels-dice-interface @ 1.2.0\n; ; Tiny file system partition, no core dump to fit BLE library.\n; board_build.partitions = usermods/pixels_dice_tray/WLED_ESP32_4MB_64KB_FS.csv\n"
  },
  {
    "path": "usermods/pixels_dice_tray/roll_info.h",
    "content": "#pragma once\n\n#include <TFT_eSPI.h>\n\nextern TFT_eSPI tft;\n\n// The following functions are generated by:\n// usermods/pixels_dice_tray/generate_roll_info.py\n\n// GENERATED\nstatic void PrintRoll0() {\n  tft.setTextColor(63488);\n  tft.println(\"Barb Chain\");\n  tft.setTextColor(65535);\n  tft.println(\"Atk/CMD 12\");\n  tft.println(\"Range: 70\");\n  tft.setTextSize(1);\n  tft.println(\"Summon 3 chains. Make\");\n  tft.println(\"a melee atk 1d6 or a \");\n  tft.println(\"trip CMD=AT. On a hit\");\n  tft.println(\"make Will save or sha\");\n  tft.println(\"ken 1d4 rnds.\");\n}\n\nstatic void PrintRoll1() {\n  tft.setTextColor(2016);\n  tft.println(\"Saves\");\n  tft.setTextColor(65535);\n  tft.println(\"FORT 8\");\n  tft.println(\"REFLEX 8\");\n  tft.println(\"WILL 9\");\n}\n\nstatic void PrintRoll2() {\n  tft.println(\"Skill\");\n}\n\nstatic void PrintRoll3() {\n  tft.println(\"Attack\");\n  tft.println(\"Melee +9\");\n  tft.println(\"Range +6\");\n}\n\nstatic void PrintRoll4() {\n  tft.println(\"Cure\");\n  tft.println(\"Lit 1d8+5\");\n  tft.println(\"Mod 2d8+9\");\n  tft.println(\"Ser 3d8+9\");\n}\n\nstatic void PrintRoll5() {\n  tft.println(\"Concentrat\");\n  tft.println(\"+15\");\n  tft.setTextSize(1);\n  tft.println(\"Defensive 15+2*SP_LV\");\n  tft.println(\"Dmg 10+DMG+SP_LV\");\n  tft.println(\"Grapple 10+CMB+SP_LV\");\n}\n\nstatic const char* GetRollName(uint8_t key) {\n  switch (key) {\n    case 0:\n      return \"Barb Chain\";\n    case 1:\n      return \"Saves\";\n    case 2:\n      return \"Skill\";\n    case 3:\n      return \"Attack\";\n    case 4:\n      return \"Cure\";\n    case 5:\n      return \"Concentrate\";\n  }\n  return \"\";\n}\n\nstatic void PrintRollInfo(uint8_t key) {\n  tft.setTextColor(TFT_WHITE);\n  tft.setCursor(0, 0);\n  tft.setTextSize(2);\n  switch (key) {\n    case 0:\n      PrintRoll0();\n      return;\n    case 1:\n      PrintRoll1();\n      return;\n    case 2:\n      PrintRoll2();\n      return;\n    case 3:\n      PrintRoll3();\n      return;\n    case 4:\n      PrintRoll4();\n      return;\n    case 5:\n      PrintRoll5();\n      return;\n  }\n  tft.setTextColor(TFT_RED);\n  tft.setCursor(0, 60);\n  tft.println(\"Unknown\");\n}\n\nstatic constexpr size_t NUM_ROLL_INFOS = 6;\n"
  },
  {
    "path": "usermods/pixels_dice_tray/tft_menu.h",
    "content": "/**\n * Code for using the 128x128 LCD and two buttons on the T-QT Pro as a GUI.\n */\n#pragma once\n\n#ifndef TFT_WIDTH\n  #warning TFT parameters not specified, not using screen.\n#else\n  #include <TFT_eSPI.h>\n  #include <pixels_dice_interface.h>  // https://github.com/axlan/arduino-pixels-dice\n  #include \"wled.h\"\n\n  #include \"dice_state.h\"\n  #include \"roll_info.h\"\n\n  #define USING_TFT_DISPLAY 1\n\n  #ifndef TFT_BL\n    #define TFT_BL -1\n  #endif\n\n// Bitmask for icon\nconst uint8_t LIGHTNING_ICON_8X8[] PROGMEM = {\n    0b00001111, 0b00010010, 0b00100100, 0b01001111,\n    0b10000001, 0b11110010, 0b00010100, 0b00011000,\n};\n\nTFT_eSPI tft = TFT_eSPI(TFT_WIDTH, TFT_HEIGHT);\n\n/**\n * Print text with box surrounding it.\n * \n * @param txt Text to draw\n * @param color Color for box lines\n */\nstatic void PrintLnInBox(const char* txt, uint32_t color) {\n  int16_t sx = tft.getCursorX();\n  int16_t sy = tft.getCursorY();\n  tft.setCursor(sx + 2, sy);\n  tft.print(txt);\n  int16_t w = tft.getCursorX() - sx + 1;\n  tft.println();\n  int16_t h = tft.getCursorY() - sy - 1;\n  tft.drawRect(sx, sy, w, h, color);\n}\n\n/**\n * Override the current colors for the selected segment to the defaults for the\n * selected die effect.\n */\nvoid SetDefaultColors(uint8_t mode) {\n  Segment& seg = strip.getFirstSelectedSeg();\n  if (mode == FX_MODE_SIMPLE_D20) {\n    seg.setColor(0, GREEN);\n    seg.setColor(1, 0);\n  } else if (mode == FX_MODE_PULSE_D20) {\n    seg.setColor(0, GREEN);\n    seg.setColor(1, RED);\n  } else if (mode == FX_MODE_CHECK_D20) {\n    seg.setColor(0, RED);\n    seg.setColor(1, 0);\n    seg.setColor(2, GREEN);\n  }\n}\n\n/**\n * Get the pointer to the custom2 value for the current LED segment. This is\n * used to set the target roll for relevant effects.\n */\nstatic uint8_t* GetCurrentRollTarget() {\n  return &strip.getFirstSelectedSeg().custom2;\n}\n\n/**\n * Class for drawing a histogram of roll results.\n */\nclass RollCountWidget {\n private:\n  int16_t xs = 0;\n  int16_t ys = 0;\n  uint16_t border_color = TFT_RED;\n  uint16_t bar_color = TFT_GREEN;\n  uint16_t bar_width = 6;\n  uint16_t max_bar_height = 60;\n  unsigned roll_counts[20] = {0};\n  unsigned total = 0;\n  unsigned max_count = 0;\n\n public:\n  RollCountWidget(int16_t xs = 0, int16_t ys = 0,\n                  uint16_t border_color = TFT_RED,\n                  uint16_t bar_color = TFT_GREEN, uint16_t bar_width = 6,\n                  uint16_t max_bar_height = 60)\n      : xs(xs),\n        ys(ys),\n        border_color(border_color),\n        bar_color(bar_color),\n        bar_width(bar_width),\n        max_bar_height(max_bar_height) {}\n\n  void Clear() {\n    memset(roll_counts, 0, sizeof(roll_counts));\n    total = 0;\n    max_count = 0;\n  }\n\n  unsigned GetNumRolls() const { return total; }\n\n  void AddRoll(unsigned val) {\n    if (val > 19) {\n      return;\n    }\n    roll_counts[val]++;\n    total++;\n    max_count = max(roll_counts[val], max_count);\n  }\n\n  void Draw() {\n    // Add 2 pixels to lengths for boarder width.\n    tft.drawRect(xs, ys, bar_width * 20 + 2, max_bar_height + 2, border_color);\n    for (size_t i = 0; i < 20; i++) {\n      if (roll_counts[i] > 0) {\n        // Scale bar by highest count.\n        uint16_t bar_height = round(float(roll_counts[i]) / float(max_count) *\n                                    float(max_bar_height));\n        // Add space between bars\n        uint16_t padding = (bar_width > 1) ? 1 : 0;\n        // Need to start from top of bar and draw down\n        tft.fillRect(xs + 1 + bar_width * i,\n                     ys + 1 + max_bar_height - bar_height, bar_width - padding,\n                     bar_height, bar_color);\n      }\n    }\n  }\n};\n\nenum class ButtonType { SINGLE, DOUBLE, LONG };\n\n// Base class for different menu pages.\nclass MenuBase {\n public:\n  /**\n   * Handle new die events and connections. Called even when menu isn't visible.\n   */\n  virtual void Update(const DiceUpdate& dice_update) = 0;\n\n  /**\n   * Draw menu to the screen.\n   */\n  virtual void Draw(const DiceUpdate& dice_update, bool force_redraw) = 0;\n\n  /**\n   * Handle button presses if the menu is currently active.\n   */\n  virtual void HandleButton(ButtonType type, uint8_t b) = 0;\n\n protected:\n  static DiceSettings* settings;\n  friend class MenuController;\n};\nDiceSettings* MenuBase::settings = nullptr;\n\n/**\n * Menu to show connection status and roll histograms.\n */\nclass DiceStatusMenu : public MenuBase {\n public:\n  DiceStatusMenu()\n      : die_roll_counts{RollCountWidget{0, 20, TFT_BLUE, TFT_GREEN, 6, 40},\n                        RollCountWidget{0, SECTION_HEIGHT + 20, TFT_BLUE,\n                                        TFT_GREEN, 6, 40}} {}\n\n  void Update(const DiceUpdate& dice_update) override {\n    for (size_t i = 0; i < MAX_NUM_DICE; i++) {\n      const auto die_id = dice_update.connected_die_ids[i];\n      const auto connected = die_id != 0;\n      // Redraw if connection status changed.\n      die_updated[i] |= die_id != last_die_ids[i];\n      last_die_ids[i] = die_id;\n\n      if (connected) {\n        bool charging = false;\n        for (const auto& battery : dice_update.battery_updates) {\n          if (battery.first == die_id) {\n            if (die_battery[i].battery_level == INVALID_BATTERY ||\n                battery.second.is_charging != die_battery[i].is_charging) {\n              die_updated[i] = true;\n            }\n            die_battery[i] = battery.second;\n          }\n        }\n\n        for (const auto& roll : dice_update.roll_updates) {\n          if (roll.first == die_id &&\n              roll.second.state == pixels::RollState::ON_FACE) {\n            die_roll_counts[i].AddRoll(roll.second.current_face);\n            die_updated[i] = true;\n          }\n        }\n      }\n    }\n  }\n\n  void Draw(const DiceUpdate& dice_update, bool force_redraw) override {\n    // This could probably be optimized for partial redraws.\n    for (size_t i = 0; i < MAX_NUM_DICE; i++) {\n      const int16_t ys = SECTION_HEIGHT * i;\n      const auto die_id = dice_update.connected_die_ids[i];\n      const auto connected = die_id != 0;\n      // Screen updates might be slow, yield in case network task needs to do\n      // work.\n      yield();\n      bool battery_update =\n          connected && (millis() - last_update[i] > BATTERY_REFRESH_RATE_MS);\n      if (force_redraw || die_updated[i] || battery_update) {\n        last_update[i] = millis();\n        tft.fillRect(0, ys, TFT_WIDTH, SECTION_HEIGHT, TFT_BLACK);\n        tft.drawRect(0, ys, TFT_WIDTH, SECTION_HEIGHT, TFT_BLUE);\n        if (settings->configured_die_names[i].empty()) {\n          tft.setTextColor(TFT_RED);\n          tft.setCursor(2, ys + 4);\n          tft.setTextSize(2);\n          tft.println(\"Connection\");\n          tft.setCursor(2, tft.getCursorY());\n          tft.println(\"Disabled\");\n        } else if (!connected) {\n          tft.setTextColor(TFT_RED);\n          tft.setCursor(2, ys + 4);\n          tft.setTextSize(2);\n          tft.println(settings->configured_die_names[i].c_str());\n          tft.setCursor(2, tft.getCursorY());\n          tft.print(\"Waiting...\");\n        } else {\n          tft.setTextColor(TFT_WHITE);\n          tft.setCursor(0, ys + 2);\n          tft.setTextSize(1);\n          tft.println(settings->configured_die_names[i].c_str());\n          tft.print(\"Cnt \");\n          tft.print(die_roll_counts[i].GetNumRolls());\n          if (die_battery[i].battery_level != INVALID_BATTERY) {\n            tft.print(\" Bat \");\n            tft.print(die_battery[i].battery_level);\n            tft.print(\"%\");\n            if (die_battery[i].is_charging) {\n              tft.drawBitmap(tft.getCursorX(), tft.getCursorY(),\n                             LIGHTNING_ICON_8X8, 8, 8, TFT_YELLOW);\n            }\n          }\n          die_roll_counts[i].Draw();\n        }\n        die_updated[i] = false;\n      }\n    }\n  }\n\n  void HandleButton(ButtonType type, uint8_t b) override {\n    if (type == ButtonType::LONG) {\n      for (size_t i = 0; i < MAX_NUM_DICE; i++) {\n        die_roll_counts[i].Clear();\n        die_updated[i] = true;\n      }\n    }\n  };\n\n private:\n  static constexpr long BATTERY_REFRESH_RATE_MS = 60 * 1000;\n  static constexpr int16_t SECTION_HEIGHT = TFT_HEIGHT / MAX_NUM_DICE;\n  static constexpr uint8_t INVALID_BATTERY = 0xFF;\n  std::array<long, MAX_NUM_DICE> last_update{0, 0};\n  std::array<pixels::PixelsDieID, MAX_NUM_DICE> last_die_ids{0, 0};\n  std::array<bool, MAX_NUM_DICE> die_updated{false, false};\n  std::array<pixels::BatteryEvent, MAX_NUM_DICE> die_battery = {\n      pixels::BatteryEvent{INVALID_BATTERY, false},\n      pixels::BatteryEvent{INVALID_BATTERY, false}};\n  std::array<RollCountWidget, MAX_NUM_DICE> die_roll_counts;\n};\n\n/**\n * Some limited controls for setting the die effects on the current LED\n * segment.\n */\nclass EffectMenu : public MenuBase {\n public:\n  EffectMenu() = default;\n\n  void Update(const DiceUpdate& dice_update) override {}\n\n  void Draw(const DiceUpdate& dice_update, bool force_redraw) override {\n    // NOTE: This doesn't update automatically if the effect is updated on the\n    // web UI and vice-versa.\n    if (force_redraw) {\n      tft.fillScreen(TFT_BLACK);\n      uint8_t mode = strip.getFirstSelectedSeg().mode;\n      if (Contains(DIE_LED_MODES, mode)) {\n        char lineBuffer[CHAR_WIDTH_BIG + 1];\n        extractModeName(mode, JSON_mode_names, lineBuffer, CHAR_WIDTH_BIG);\n        tft.setTextColor(TFT_WHITE);\n        tft.setCursor(0, 0);\n        tft.setTextSize(2);\n        PrintLnInBox(lineBuffer, (field_idx == 0) ? TFT_BLUE : TFT_BLACK);\n        if (mode == FX_MODE_CHECK_D20) {\n          snprintf(lineBuffer, sizeof(lineBuffer), \"PASS: %u\",\n                   *GetCurrentRollTarget());\n          PrintLnInBox(lineBuffer, (field_idx == 1) ? TFT_BLUE : TFT_BLACK);\n        }\n      } else {\n        char lineBuffer[CHAR_WIDTH_SMALL + 1];\n        extractModeName(mode, JSON_mode_names, lineBuffer, CHAR_WIDTH_SMALL);\n        tft.setTextColor(TFT_WHITE);\n        tft.setCursor(0, 0);\n        tft.setTextSize(1);\n        tft.println(lineBuffer);\n      }\n    }\n  }\n\n  /**\n   * Button 0 navigates up and down the settings for the effect.\n   * Button 1 changes the value for the selected settings.\n   * Long pressing a button resets the effect parameters to their defaults for\n   * the current die effect.\n   */\n  void HandleButton(ButtonType type, uint8_t b) override {\n    Segment& seg = strip.getFirstSelectedSeg();\n    auto mode_itr =\n        std::find(DIE_LED_MODES.begin(), DIE_LED_MODES.end(), seg.mode);\n    if (mode_itr != DIE_LED_MODES.end()) {\n      mode_idx = mode_itr - DIE_LED_MODES.begin();\n    }\n\n    if (mode_itr == DIE_LED_MODES.end()) {\n      seg.setMode(DIE_LED_MODES[mode_idx]);\n    } else {\n      if (type == ButtonType::LONG) {\n        // Need to set mode to different value so defaults are actually loaded.\n        seg.setMode(0);\n        seg.setMode(DIE_LED_MODES[mode_idx], true);\n        SetDefaultColors(DIE_LED_MODES[mode_idx]);\n      } else if (b == 0) {\n        field_idx = (field_idx + 1) % DIE_LED_MODE_NUM_FIELDS[mode_idx];\n      } else {\n        if (field_idx == 0) {\n          mode_idx = (mode_idx + 1) % DIE_LED_MODES.size();\n          seg.setMode(DIE_LED_MODES[mode_idx]);\n        } else if (DIE_LED_MODES[mode_idx] == FX_MODE_CHECK_D20 &&\n                   field_idx == 1) {\n          *GetCurrentRollTarget() = GetLastRoll().current_face + 1;\n        }\n      }\n    }\n  };\n\n private:\n  static constexpr std::array<uint8_t, 3> DIE_LED_MODE_NUM_FIELDS = {1, 1, 2};\n  static constexpr size_t CHAR_WIDTH_BIG = 10;\n  static constexpr size_t CHAR_WIDTH_SMALL = 21;\n  size_t mode_idx = 0;\n  size_t field_idx = 0;\n};\n\nconstexpr std::array<uint8_t, 3> EffectMenu::DIE_LED_MODE_NUM_FIELDS;\n\n/**\n * Menu for setting the roll label and some info for that roll type.\n */\nclass InfoMenu : public MenuBase {\n public:\n  InfoMenu() = default;\n\n  void Update(const DiceUpdate& dice_update) override {}\n\n  void Draw(const DiceUpdate& dice_update, bool force_redraw) override {\n    if (force_redraw) {\n      tft.fillScreen(TFT_BLACK);\n      if (settings->roll_label != INVALID_ROLL_VALUE) {\n        PrintRollInfo(settings->roll_label);\n      } else {\n        tft.setTextColor(TFT_RED);\n        tft.setCursor(0, 60);\n        tft.setTextSize(2);\n        tft.println(\"Set Roll\");\n      }\n    }\n  }\n\n  /**\n   * Single clicking navigates through the roll types. Button 0 goes down, and\n   * button 1 goes up with wrapping.\n   */\n  void HandleButton(ButtonType type, uint8_t b) override {\n    if (settings->roll_label >= NUM_ROLL_INFOS) {\n      settings->roll_label = 0;\n    } else if (b == 0) {\n      settings->roll_label = (settings->roll_label == 0)\n                                 ? NUM_ROLL_INFOS - 1\n                                 : settings->roll_label - 1;\n    } else if (b == 1) {\n      settings->roll_label = (settings->roll_label + 1) % NUM_ROLL_INFOS;\n    }\n    if (WLED_MQTT_CONNECTED) {\n      char mqtt_topic_buffer[MQTT_MAX_TOPIC_LEN + 16];\n      snprintf(mqtt_topic_buffer, sizeof(mqtt_topic_buffer), PSTR(\"%s/%s\"),\n               mqttDeviceTopic, \"dice/settings->roll_label\");\n      mqtt->publish(mqtt_topic_buffer, 0, false,\n                    GetRollName(settings->roll_label));\n    }\n  };\n};\n\n/**\n * Interface for the rest of the app to update the menus.\n */\nclass MenuController {\n public:\n  MenuController(DiceSettings* settings) { MenuBase::settings = settings; }\n\n  void Init(unsigned rotation) {\n    tft.init();\n    tft.setRotation(rotation);\n    tft.fillScreen(TFT_BLACK);\n    tft.setTextColor(TFT_RED);\n    tft.setCursor(0, 60);\n    tft.setTextDatum(MC_DATUM);\n    tft.setTextSize(2);\n    EnableBacklight(true);\n\n    force_redraw = true;\n  }\n\n  // Set the pin to turn the backlight on or off if available.\n  static void EnableBacklight(bool enable) {\n  #if TFT_BL > 0\n    #if USERMOD_PIXELS_DICE_TRAY_BL_ACTIVE_LOW\n    enable = !enable;\n    #endif\n    digitalWrite(TFT_BL, enable);\n  #endif\n  }\n\n  /**\n   * Double clicking navigates between menus. Button 0 goes down, and button 1\n   * goes up with wrapping.\n   */\n  void HandleButton(ButtonType type, uint8_t b) {\n    force_redraw = true;\n    // Switch menus with double click\n    if (ButtonType::DOUBLE == type) {\n      if (b == 0) {\n        current_index =\n            (current_index == 0) ? menu_ptrs.size() - 1 : current_index - 1;\n      } else {\n        current_index = (current_index + 1) % menu_ptrs.size();\n      }\n    } else {\n      menu_ptrs[current_index]->HandleButton(type, b);\n    }\n  }\n\n  void Update(const DiceUpdate& dice_update) {\n    for (auto menu_ptr : menu_ptrs) {\n      menu_ptr->Update(dice_update);\n    }\n    menu_ptrs[current_index]->Draw(dice_update, force_redraw);\n    force_redraw = false;\n  }\n\n  void Redraw() { force_redraw = true; }\n\n private:\n  size_t current_index = 0;\n  bool force_redraw = true;\n\n  DiceStatusMenu status_menu;\n  EffectMenu effect_menu;\n  InfoMenu info_menu;\n  const std::array<MenuBase*, 3> menu_ptrs = {&status_menu, &effect_menu,\n                                              &info_menu};\n};\n#endif\n"
  },
  {
    "path": "usermods/platformio_override.usermods.ini",
    "content": "[platformio]\ndefault_envs = usermods_esp32, usermods_esp32c3, usermods_esp32s2, usermods_esp32s3\n\n[env:usermods_esp32]\nextends = env:esp32dev\ncustom_usermods = ${usermods.custom_usermods}\nboard_build.partitions = ${esp32.extreme_partitions}  ; We're gonna need a bigger boat\n\n\n[env:usermods_esp32c3]\nextends = env:esp32c3dev\nboard = esp32-c3-devkitm-1\ncustom_usermods = ${usermods.custom_usermods}\nboard_build.partitions = ${esp32.extreme_partitions}  ; We're gonna need a bigger boat\n\n\n[env:usermods_esp32s2]\nextends = env:lolin_s2_mini\ncustom_usermods = ${usermods.custom_usermods}\nboard_build.partitions = ${esp32.extreme_partitions}  ; We're gonna need a bigger boat\n\n\n[env:usermods_esp32s3]\nextends = env:esp32s3dev_16MB_opi\ncustom_usermods = ${usermods.custom_usermods}\nboard_build.partitions = ${esp32.extreme_partitions}  ; We're gonna need a bigger boat\n\n\n\n[usermods]\n# Added in CI\n"
  },
  {
    "path": "usermods/pov_display/README.md",
    "content": "## POV Display usermod\n\nThis usermod adds a new effect called “POV Image”.\n\n![the usermod at work](pov_display.gif?raw=true)\n\n###How does it work?\nWith proper configuration (see below) the main segment will display a single row of pixels from an image stored on the ESP.\nIt displays the image row by row at a high refresh rate.\nIf 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.\nRGB LEDs only (no RGBW), with grouping set to 1 and spacing set to 0.\nBest results with high-density strips (e.g., 144 LEDs/m).\n\nTo get it working:\n- Resize your image. The height must match the number of LEDs in your strip/segment.\n- Rotate your image 90° clockwise (height becomes width).\n- Upload a BMP image (24-bit, uncompressed) to the ESP filesystem using the “/edit” URL.\n- Select the “POV Image” effect.\n- Set the segment name to the absolute filesystem path of the image (e.g., “/myimage.bmp”).\n- The path is case-sensitive and must start with “/”.\n- Rotate the pixel strip at approximately 20 RPM.\n- Tune as needed so that one full revolution maps to the image width (if the image appears stretched or compressed, adjust RPM slightly).\n- Enjoy the show!\n\nNotes:\n- Only 24-bit uncompressed BMP files are supported.\n- The image must fit into ~64 KB of RAM (width × height × 3 bytes, plus row padding to a 4-byte boundary).\n- Examples (approximate, excluding row padding):\n  - 128×128 (49,152 bytes) fits.\n  - 160×160 (76,800 bytes) does NOT fit.\n  - 96×192 (55,296 bytes) fits; padding may add a small overhead.\n- If the rendered image appears mirrored or upside‑down, rotate 90° the other way or flip horizontally in your editor and try again.\n- The path must be absolute.\n\n### Requirements\n- 1D rotating LED strip/segment (POV setup). Ensure the segment length equals the number of physical LEDs.\n- BMP image saved as 24‑bit, uncompressed (no alpha, no palette).\n- Sufficient free RAM (~64 KB) for the image buffer.\n\n### Troubleshooting\n- Nothing displays: verify the file exists at the exact absolute path (case‑sensitive) and is a 24‑bit uncompressed BMP.\n- Garbled colors or wrong orientation: re‑export as 24‑bit BMP and retry the rotation/flip guidance above.\n- Image too large: reduce width and/or height until it fits within ~64 KB (see examples).\n- Path issues: confirm you uploaded the file via the “/edit” URL and can see it in the filesystem browser.\n\n### Safety\n- Secure the rotating assembly and keep clear of moving parts.\n- Balance the strip/hub to minimize vibration before running at speed."
  },
  {
    "path": "usermods/pov_display/bmpimage.cpp",
    "content": "#include \"bmpimage.h\"\n#define BUF_SIZE 64000\n\nbyte * _buffer = nullptr;\n\nuint16_t read16(File &f) {\n  uint16_t result;\n  f.read((uint8_t *)&result,2);\n  return result;\n}\n\nuint32_t read32(File &f) {\n  uint32_t result;\n  f.read((uint8_t *)&result,4);\n  return result;\n}\n\nbool BMPimage::init(const char * fn) {\n    File bmpFile;\n    int bmpDepth;\n    //first, check if filename exists\n    if (!WLED_FS.exists(fn)) {\n      return false;\n    }\n    \n    bmpFile = WLED_FS.open(fn);\n    if (!bmpFile) {\n      _valid=false;\n      return false;\n    }\n\n    //so, the file exists and is opened\n    // Parse BMP header\n    uint16_t header = read16(bmpFile);\n    if(header != 0x4D42) { // BMP signature\n      _valid=false;\n      bmpFile.close();\n      return false;\n    }\n\n    //read and ingnore file size\n    read32(bmpFile);\n    (void)read32(bmpFile); // Read & ignore creator bytes\n    _imageOffset = read32(bmpFile); // Start of image data\n    // Read DIB header\n    read32(bmpFile);\n    _width  = read32(bmpFile);\n    _height = read32(bmpFile);\n    if(read16(bmpFile) != 1) { // # planes -- must be '1'\n        _valid=false;\n        bmpFile.close();\n        return false;\n    }\n    bmpDepth = read16(bmpFile); // bits per pixel\n    if((bmpDepth != 24) || (read32(bmpFile) != 0)) { // 0 = uncompressed {\n        _width=0;\n        _valid=false;\n        bmpFile.close();\n        return false;\n    }\n    // If _height is negative, image is in top-down order.\n    // This is not canon but has been observed in the wild.\n    if(_height < 0) {\n        _height = -_height;\n    }\n    //now, we have successfully got all the basics\n    // BMP rows are padded (if needed) to 4-byte boundary\n    _rowSize = (_width * 3 + 3) & ~3;\n    //check image size - if it is too large, it will be unusable\n    if (_rowSize*_height>BUF_SIZE) {\n      _valid=false;\n      bmpFile.close();\n      return false;\n    }\n\n    bmpFile.close();\n    // Ensure filename fits our buffer (segment name length constraint).\n    size_t len = strlen(fn);\n    if (len > WLED_MAX_SEGNAME_LEN) {\n      return false;\n    }\n    strncpy(filename, fn, sizeof(filename));\n    filename[sizeof(filename) - 1] = '\\0';\n    _valid = true;\n    return true;\n}\n\nvoid BMPimage::clear(){\n    strcpy(filename, \"\");\n    _width=0;\n    _height=0;\n    _rowSize=0;\n    _imageOffset=0;\n    _loaded=false;\n    _valid=false;\n}\n\nbool BMPimage::load(){\n    const size_t size = (size_t)_rowSize * (size_t)_height;\n    if (size > BUF_SIZE) {\n        return false;\n    }\n    File bmpFile = WLED_FS.open(filename);\n    if (!bmpFile) {\n        return false;\n    }\n\n    if (_buffer != nullptr) free(_buffer);\n    _buffer = (byte*)malloc(size);\n    if (_buffer == nullptr) return false;\n\n    bmpFile.seek(_imageOffset);\n    const size_t readBytes = bmpFile.read(_buffer, size);\n    bmpFile.close();\n    if (readBytes != size) {\n        _loaded = false;\n        return false;\n    }\n    _loaded = true;\n    return true;\n}\n\nbyte* BMPimage::line(uint16_t n){\n    if (_loaded) {\n        return (_buffer+n*_rowSize);\n    } else {\n        return NULL;\n    }\n}\n\nuint32_t BMPimage::pixelColor(uint16_t x, uint16_t  y){\n    uint32_t pos;\n    byte b,g,r; //colors\n    if (! _loaded) {\n      return 0;\n    }\n    if ( (x>=_width) || (y>=_height) ) {\n      return 0;\n    }\n    pos=y*_rowSize + 3*x;\n    //get colors. Note that in BMP files, they go in BGR order\n    b= _buffer[pos++];\n    g= _buffer[pos++];\n    r= _buffer[pos];\n    return (r<<16|g<<8|b);\n}\n"
  },
  {
    "path": "usermods/pov_display/bmpimage.h",
    "content": "#ifndef _BMPIMAGE_H\n#define _BMPIMAGE_H\n#include \"Arduino.h\"\n#include \"wled.h\"\n\n/*\n * This class describes a bitmap image. Each object refers to a bmp file on\n * filesystem fatfs.\n * To initialize, call init(), passign to it name of a bitmap file\n * at the root of fatfs filesystem:\n *\n * BMPimage myImage;\n * myImage.init(\"logo.bmp\");\n *\n * For performance reasons, before actually usign the image, you need to load\n * it from filesystem to RAM:\n * myImage.load();\n * All load() operations use the same reserved buffer in RAM, so you can only\n * have one file loaded at a time. Before loading a new file, always unload the\n * previous one:\n * myImage.unload();\n */\n\nclass BMPimage {\n    public:\n        int height()    {return _height; }\n        int width()     {return _width;  }\n        int rowSize()   {return _rowSize;}\n        bool isLoaded() {return _loaded; }\n        bool load();\n        void unload()   {_loaded=false;  }\n        byte * line(uint16_t n);\n        uint32_t pixelColor(uint16_t x,uint16_t  y);\n        bool init(const char* fn);\n        void clear();\n        char * getFilename() {return filename;};\n\n    private:\n        char filename[WLED_MAX_SEGNAME_LEN+1]=\"\";\n        int _width=0;\n        int _height=0;\n        int _rowSize=0;\n        int _imageOffset=0;\n        bool _loaded=false;\n        bool _valid=false;\n};\n\nextern byte * _buffer;\n\n#endif\n"
  },
  {
    "path": "usermods/pov_display/library.json",
    "content": "{\n  \"name:\": \"pov_display\",\n  \"build\": { \"libArchive\": false},\n  \"platforms\": [\"espressif32\"]\n}\n"
  },
  {
    "path": "usermods/pov_display/pov.cpp",
    "content": "#include \"pov.h\"\n\nPOV::POV() {}\n\nvoid POV::showLine(const byte * line, uint16_t size){\n    uint16_t i, pos;\n    uint8_t r, g, b;\n    if (!line) {\n        // All-black frame on null input\n        for (i = 0; i < SEGLEN; i++) {\n            SEGMENT.setPixelColor(i, CRGB::Black);\n        }\n        strip.show();\n        lastLineUpdate = micros();\n        return;\n    }\n    for (i = 0; i < SEGLEN; i++) {\n        if (i < size) {\n            pos = 3 * i;\n            // using bgr order\n            b = line[pos++];\n            g = line[pos++];\n            r = line[pos];\n            SEGMENT.setPixelColor(i, CRGB(r, g, b));\n        } else {\n            SEGMENT.setPixelColor(i, CRGB::Black);\n        }\n    }\n    strip.show();\n    lastLineUpdate = micros();\n}\n\nbool POV::loadImage(const char * filename){\n  if(!image.init(filename)) return false;\n  if(!image.load()) return false;\n  currentLine=0;\n  return true;\n}\n\nint16_t POV::showNextLine(){\n    if (!image.isLoaded()) return 0;\n    //move to next line\n    showLine(image.line(currentLine), image.width());\n    currentLine++;\n    if (currentLine == image.height()) {currentLine=0;}\n    return currentLine;\n}\n"
  },
  {
    "path": "usermods/pov_display/pov.h",
    "content": "#ifndef _POV_H\n#define _POV_H\n#include \"bmpimage.h\"\n\n\nclass POV {\n    public:\n        POV();\n\n        /* Shows one line. line should be pointer to array which holds  pixel colors\n         * (3 bytes per pixel, in BGR order). Note: 3, not 4!!!\n         *  size should be size of array (number of pixels, not number of bytes)\n         */\n        void showLine(const byte * line, uint16_t size);\n\n        /* Reads from file an image and making it current image */\n        bool loadImage(const char * filename);\n\n        /* Show next line of active image\n           Retunrs the index of next line to be shown (not yet shown!)\n           If it retunrs 0, it means we have completed showing the image and\n            next call will start again\n        */\n        int16_t showNextLine();\n\n        //time since strip was last updated, in micro sec\n        uint32_t timeSinceUpdate() {return (micros()-lastLineUpdate);}\n\n\n        BMPimage * currentImage() {return &image;}\n\n        char * getFilename() {return image.getFilename();}\n\n    private:\n        BMPimage image;\n        int16_t  currentLine=0;     //next line to be shown\n        uint32_t lastLineUpdate=0; //time in microseconds\n};\n\n\n\n#endif\n"
  },
  {
    "path": "usermods/pov_display/pov_display.cpp",
    "content": "#include \"wled.h\"\n#include \"pov.h\"\n\nstatic const char _data_FX_MODE_POV_IMAGE[] PROGMEM = \"POV Image@!;;;;\";\n\nstatic POV s_pov;\n\nvoid mode_pov_image(void) {\n  Segment& mainseg = strip.getMainSegment();\n  const char* segName = mainseg.name;\n  if (!segName) {\n     return;\n   }\n  // Only proceed for files ending with .bmp (case-insensitive)\n  size_t segLen = strlen(segName);\n  if (segLen < 4) return;\n  const char* ext = segName + (segLen - 4);\n  // compare case-insensitive to \".bmp\"\n  if (!((ext[0]=='.') &&\n        (ext[1]=='b' || ext[1]=='B') &&\n        (ext[2]=='m' || ext[2]=='M') &&\n        (ext[3]=='p' || ext[3]=='P'))) {\n    return;\n  }\n\n  const char* current = s_pov.getFilename();\n  if (current && strcmp(segName, current) == 0) {\n     s_pov.showNextLine();\n     return;\n   }\n\n  static unsigned long s_lastLoadAttemptMs = 0;\n  unsigned long nowMs = millis();\n  // Retry at most twice per second if the image is not yet loaded.\n  if (nowMs - s_lastLoadAttemptMs < 500) return;\n  s_lastLoadAttemptMs = nowMs;\n  s_pov.loadImage(segName);\n  return;\n}\n\nclass PovDisplayUsermod : public Usermod {\nprotected:\n  bool enabled = false; //WLEDMM\n  const char *_name; //WLEDMM\n  bool initDone = false; //WLEDMM\n  unsigned long lastTime = 0; //WLEDMM\npublic:\n\n  PovDisplayUsermod(const char *name, bool enabled)\n    : enabled(enabled) , _name(name) {}\n  \n  void setup() override {\n    strip.addEffect(255, &mode_pov_image, _data_FX_MODE_POV_IMAGE);\n    //initDone removed (unused)\n  }\n\n\n  void loop() override {\n    // if usermod is disabled or called during strip updating just exit\n    // NOTE: on very long strips strip.isUpdating() may always return true so update accordingly\n    if (!enabled || strip.isUpdating()) return;\n\n    // do your magic here\n    if (millis() - lastTime > 1000) {\n      lastTime = millis();\n    }\n  }\n\n  uint16_t getId() override {\n    return USERMOD_ID_POV_DISPLAY;\n  }\n};\n\nstatic PovDisplayUsermod pov_display(\"POV Display\", false);\nREGISTER_USERMOD(pov_display);\n"
  },
  {
    "path": "usermods/project_cars_shiftlight/readme.md",
    "content": "# Shift Light for Project Cars\n\nTurn your WLED lights into a rev light and shift indicator for Project Cars.\nIt's easy to use.\n\n_1._ Make sure your WLED device and your PC/console are on the same network and can talk to each other\n\n_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.\n\n| Number | Updates/Second |\n| ------ | -------------- |\n| 1      | 60             |\n| 2      | 50             |\n| 3      | 40             |\n| 4      | 30             |\n| 5      | 20             |\n| 6      | 15             |\n| 7      | 10             |\n| 8      | 05             |\n| 9      | 1              |\n\n_3._ Once you enter a race, WLED should automatically shift to PCARS mode.\n_4._ Done.\n"
  },
  {
    "path": "usermods/project_cars_shiftlight/wled06_usermod.ino",
    "content": "/*\n *          Car rev display and shift indicator for Project Cars\n *          \n * This works via the UDP telemetry function. You'll need to enable it in the settings of the game.\n * I've had good results with settings around 5 (20 fps).\n * \n */\n#include \"wled.h\"\n\nconst uint8_t PCARS_dimcolor = 20;\nWiFiUDP UDP;\nconst unsigned int PCARS_localUdpPort = 5606; // local port to listen on\nchar PCARS_packet[2048];\n\nchar PCARS_tempChar[2]; // Temporary array for u16 conversion\n\nu16 PCARS_RPM;\nu16 PCARS_maxRPM;\n\nlong PCARS_lastRead = millis() - 2001;\nfloat PCARS_rpmRatio;\n\nvoid PCARS_readValues() {\n\n  int PCARS_packetSize = UDP.parsePacket();\n  if (PCARS_packetSize) {\n    int len = UDP.read(PCARS_packet, PCARS_packetSize);\n    if (len > 0) {\n      PCARS_packet[len] = 0;\n    }\n    if (len == 1367) { // Telemetry packet. Ignoring everything else.\n      PCARS_lastRead = millis();\n\n      realtimeLock(realtimeTimeoutMs, REALTIME_MODE_GENERIC);\n      // current RPM\n      memcpy(&PCARS_tempChar, &PCARS_packet[124], 2);\n      PCARS_RPM = (PCARS_tempChar[1] << 8) + PCARS_tempChar[0];\n\n      // max RPM\n      memcpy(&PCARS_tempChar, &PCARS_packet[126], 2);\n      PCARS_maxRPM = (PCARS_tempChar[1] << 8) + PCARS_tempChar[0];\n\n      if (PCARS_maxRPM) {\n        PCARS_rpmRatio = constrain((float)PCARS_RPM / (float)PCARS_maxRPM, 0, 1);\n      } else {\n        PCARS_rpmRatio = 0.0;\n      }\n    }\n  }\n}\nvoid PCARS_buildcolorbars() {\n  boolean activated = false;\n  float ledratio = 0;\n  uint16_t totalLen = strip.getLengthTotal();\n\n  for (uint16_t i = 0; i < totalLen; i++) {\n    if (PCARS_rpmRatio < .95 || (millis() % 100 > 70 )) {\n\n      ledratio = (float)i / (float)totalLen;\n      if (ledratio < PCARS_rpmRatio) {\n        activated = true;\n      } else {\n        activated = false;\n      }\n      if (ledratio > 0.66) {\n        setRealtimePixel(i, 0, 0, PCARS_dimcolor + ((255 - PCARS_dimcolor)*activated), 0);\n      } else if (ledratio > 0.33) {\n        setRealtimePixel(i, PCARS_dimcolor + ((255 - PCARS_dimcolor)*activated), 0, 0, 0);\n      } else {\n        setRealtimePixel(i, 0, PCARS_dimcolor + ((255 - PCARS_dimcolor)*activated), 0, 0);\n      }\n    }\n    else {\n      setRealtimePixel(i, 0, 0, 0, 0);\n\n    }\n  }\n  colorUpdated(5);\n  strip.show();\n}\n\nvoid userSetup()\n{\n  UDP.begin(PCARS_localUdpPort);\n}\n\nvoid userConnected()\n{\n  // new wifi, who dis?\n}\n\nvoid userLoop()\n{\n  PCARS_readValues();\n  if (PCARS_lastRead > millis() - 2000) {\n    PCARS_buildcolorbars();\n  }\n}"
  },
  {
    "path": "usermods/pwm_outputs/library.json",
    "content": "{\n  \"name\": \"pwm_outputs\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/pwm_outputs/pwm_outputs.cpp",
    "content": "#include \"wled.h\"\n\n#ifndef ESP32\n  #error This usermod does not support the ESP8266.\n#endif\n\n#ifndef USERMOD_PWM_OUTPUT_PINS\n  #define USERMOD_PWM_OUTPUT_PINS 3\n#endif\n\n\nclass PwmOutput {\n  public:\n\n    void open(int8_t pin, uint32_t freq) {\n\n      if (enabled_) {\n        if (pin == pin_ && freq == freq_) {\n          return;  // PWM output is already open\n        } else {\n          close();  // Config has changed, close and reopen\n        }\n      }\n\n      pin_ = pin;\n      freq_ = freq;\n      if (pin_ < 0)\n        return;\n\n      DEBUG_PRINTF(\"pwm_output[%d]: setup to freq %d\\n\", pin_, freq_);\n      if (!PinManager::allocatePin(pin_, true, PinOwner::UM_PWM_OUTPUTS))\n        return;\n      \n      channel_ = PinManager::allocateLedc(1);\n      if (channel_ == 255) {\n        DEBUG_PRINTF(\"pwm_output[%d]: failed to quire ledc\\n\", pin_);\n        PinManager::deallocatePin(pin_, PinOwner::UM_PWM_OUTPUTS);\n        return;\n      }\n\n      ledcSetup(channel_, freq_, bit_depth_);\n      ledcAttachPin(pin_, channel_);\n      DEBUG_PRINTF(\"pwm_output[%d]: init successful\\n\", pin_);\n      enabled_ = true;\n    }\n\n    void close() {\n      DEBUG_PRINTF(\"pwm_output[%d]: close\\n\", pin_);\n      if (!enabled_)\n        return;\n      PinManager::deallocatePin(pin_, PinOwner::UM_PWM_OUTPUTS);\n      if (channel_ != 255)\n        PinManager::deallocateLedc(channel_, 1);\n      channel_ = 255;\n      duty_ = 0.0f;\n      enabled_ = false;\n    }\n\n    void setDuty(const float duty) {\n      DEBUG_PRINTF(\"pwm_output[%d]: set duty %f\\n\", pin_, duty);\n      if (!enabled_)\n        return;\n      duty_ = min(1.0f, max(0.0f, duty));\n      const uint32_t value = static_cast<uint32_t>((1 << bit_depth_) * duty_);\n      ledcWrite(channel_, value);\n    }\n\n    void setDuty(const uint16_t duty) {\n      setDuty(static_cast<float>(duty) / 65535.0f);\n    }\n\n    bool isEnabled() const {\n      return enabled_;\n    }\n\n    void addToJsonState(JsonObject& pwmState) const {\n      pwmState[F(\"duty\")] = duty_;\n    }\n\n    void readFromJsonState(JsonObject& pwmState) {\n      if (pwmState.isNull()) {\n        return;\n      }\n      float duty;\n      if (getJsonValue(pwmState[F(\"duty\")], duty)) {\n        setDuty(duty);\n      }\n    }\n\n    void addToJsonInfo(JsonObject& user) const {\n      if (!enabled_)\n        return;\n      char buffer[12];\n      sprintf_P(buffer, PSTR(\"PWM pin %d\"), pin_);\n      JsonArray data = user.createNestedArray(buffer);\n      data.add(1e2f * duty_);\n      data.add(F(\"%\"));\n    }\n\n    void addToConfig(JsonObject& pwmConfig) const {\n      pwmConfig[F(\"pin\")] = pin_;\n      pwmConfig[F(\"freq\")] = freq_;\n    }\n\n    bool readFromConfig(JsonObject& pwmConfig) {\n      if (pwmConfig.isNull())\n        return false;\n        \n      bool configComplete = true;\n      int8_t newPin = pin_;\n      uint32_t newFreq = freq_;\n      configComplete &= getJsonValue(pwmConfig[F(\"pin\")], newPin);  \n      configComplete &= getJsonValue(pwmConfig[F(\"freq\")], newFreq);\n\n      open(newPin, newFreq);\n\n      return configComplete;\n    }\n\n  private:\n    int8_t pin_ {-1};\n    uint32_t freq_ {50};\n    static const uint8_t bit_depth_ {12};\n    uint8_t channel_ {255};\n    float duty_ {0.0f};\n    bool enabled_ {false};\n};\n\n\nclass PwmOutputsUsermod : public Usermod {\n  public:\n\n    static const char USERMOD_NAME[];\n    static const char PWM_STATE_NAME[];\n\n    void setup() {\n      // By default all PWM outputs are disabled, no setup do be done\n    }\n\n    void loop() {\n    }\n\n    void addToJsonState(JsonObject& root) {\n      JsonObject pwmStates = root.createNestedObject(PWM_STATE_NAME);\n      for (int i = 0; i < USERMOD_PWM_OUTPUT_PINS; i++) {\n        const PwmOutput& pwm = pwms_[i];\n        if (!pwm.isEnabled())\n          continue;\n        char buffer[4];\n        sprintf_P(buffer, PSTR(\"%d\"), i);\n        JsonObject pwmState = pwmStates.createNestedObject(buffer);\n        pwm.addToJsonState(pwmState);\n      }\n    }\n\n    void readFromJsonState(JsonObject& root) {\n      JsonObject pwmStates = root[PWM_STATE_NAME];\n      if (pwmStates.isNull())\n        return;\n\n      for (int i = 0; i < USERMOD_PWM_OUTPUT_PINS; i++) {\n        PwmOutput& pwm = pwms_[i];\n        if (!pwm.isEnabled())\n          continue;\n        char buffer[4];\n        sprintf_P(buffer, PSTR(\"%d\"), i);\n        JsonObject pwmState = pwmStates[buffer];\n        pwm.readFromJsonState(pwmState);\n      }\n    }\n\n    void addToJsonInfo(JsonObject& root) {\n      JsonObject user = root[F(\"u\")];\n      if (user.isNull())\n        user = root.createNestedObject(F(\"u\"));\n\n      for (int i = 0; i < USERMOD_PWM_OUTPUT_PINS; i++) {\n        const PwmOutput& pwm = pwms_[i];\n        pwm.addToJsonInfo(user);\n      }\n    }\n\n    void addToConfig(JsonObject& root) {\n      JsonObject top = root.createNestedObject(USERMOD_NAME);\n      for (int i = 0; i < USERMOD_PWM_OUTPUT_PINS; i++) {\n        const PwmOutput& pwm = pwms_[i];\n        char buffer[8];\n        sprintf_P(buffer, PSTR(\"PWM %d\"), i);\n        JsonObject pwmConfig = top.createNestedObject(buffer);\n        pwm.addToConfig(pwmConfig);\n      }\n    }\n\n    bool readFromConfig(JsonObject& root) {\n      JsonObject top = root[USERMOD_NAME];\n      if (top.isNull())\n        return false;\n\n      bool configComplete = true;\n      for (int i = 0; i < USERMOD_PWM_OUTPUT_PINS; i++) {\n        PwmOutput& pwm = pwms_[i];\n        char buffer[8];\n        sprintf_P(buffer, PSTR(\"PWM %d\"), i);\n        JsonObject pwmConfig = top[buffer];\n        configComplete &= pwm.readFromConfig(pwmConfig);\n      }\n      return configComplete;\n    }\n\n    uint16_t getId() {\n      return USERMOD_ID_PWM_OUTPUTS;\n    }\n\n  private:\n    PwmOutput pwms_[USERMOD_PWM_OUTPUT_PINS];\n\n};\n\nconst char PwmOutputsUsermod::USERMOD_NAME[] PROGMEM = \"PwmOutputs\";\nconst char PwmOutputsUsermod::PWM_STATE_NAME[] PROGMEM = \"pwm\";\n\n\nstatic PwmOutputsUsermod pwm_outputs;\nREGISTER_USERMOD(pwm_outputs);"
  },
  {
    "path": "usermods/pwm_outputs/readme.md",
    "content": "# PWM outputs\n\nv2 Usermod to add generic PWM outputs to WLED. Usermode could be used to control servo motors, LED brightness or any other device controlled by PWM signal.\n\n## Installation\n\nAdd the compile-time option `-D USERMOD_PWM_OUTPUTS` to your `platformio.ini` (or `platformio_override.ini`). By default upt to 3 PWM outputs could be configured, to increase that limit add build argument `-D USERMOD_PWM_OUTPUT_PINS=10` (replace 10 by desired amount).\n\nCurrently only ESP32 is supported.\n\n## Configuration\n\nBy default PWM outputs are disabled, navigate to Usermods settings and configure desired PWM pins and frequencies.\n\n## Usage\n\nIf PWM output is configured, it starts to publish its duty cycle value (0-1) both to state JSON and to info JSON (visible in UI info panel). To set PWM duty cycle, use JSON api (over HTTP or over Serial)\n\n```json\n{\n    \"pwm\": {\n        \"0\": {\"duty\": 0.1},\n        \"1\": {\"duty\": 0.2},\n        ...\n    }\n}\n```\n"
  },
  {
    "path": "usermods/quinled-an-penta/library.json",
    "content": "{\n  \"name\": \"quinled-an-penta\",\n  \"build\": { \"libArchive\": false},\n  \"dependencies\": {\n    \"olikraus/U8g2\":\"~2.28.8\",\n    \"robtillaart/SHT85\":\"~0.3.3\"\n  }\n}\n"
  },
  {
    "path": "usermods/quinled-an-penta/quinled-an-penta.cpp",
    "content": "#include \"U8g2lib.h\"\n#include \"SHT85.h\"\n#include \"Wire.h\"\n#include \"wled.h\"\n\nclass QuinLEDAnPentaUsermod : public Usermod\n{\n  private:\n    bool enabled = false;\n    bool firstRunDone = false;\n    bool initDone = false;\n    U8G2 *oledDisplay = nullptr;\n    SHT *sht30TempHumidSensor;\n\n    // Network info vars\n    bool networkHasChanged = false;\n    bool lastKnownNetworkConnected;\n    IPAddress lastKnownIp;\n    bool lastKnownWiFiConnected;\n    String lastKnownSsid;\n    bool lastKnownApActive;\n    char *lastKnownApSsid;\n    char *lastKnownApPass;\n    byte lastKnownApChannel;\n    int lastKnownEthType;\n    bool lastKnownEthLinkUp;\n\n    // Brightness / LEDC vars\n    byte lastKnownBri = 0;\n    int8_t currentBussesNumPins[5] = {0, 0, 0, 0, 0};\n    int8_t currentLedPins[5] = {0, 0, 0, 0, 0};\n    uint8_t currentLedcReads[5] = {0, 0, 0, 0, 0};\n    uint8_t lastKnownLedcReads[5] = {0, 0, 0, 0, 0};\n\n    // OLED vars\n    bool oledEnabled = false;\n    bool oledInitDone = false;\n    bool oledUseProgressBars = false;\n    bool oledFlipScreen = false;\n    bool oledFixBuggedScreen = false;\n    byte oledMaxPage = 3;\n    byte oledCurrentPage = 3; // Start with the network page to help identifying the IP\n    byte oledSecondsPerPage = 10;\n    unsigned long oledLogoDrawn = 0;\n    unsigned long oledLastTimeUpdated = 0;\n    unsigned long oledLastTimePageChange = 0;\n    unsigned long oledLastTimeFixBuggedScreen = 0;\n\n    // SHT30 vars\n    bool shtEnabled = false;\n    bool shtInitDone = false;\n    bool shtReadDataSuccess = false;\n    byte shtI2cAddress = 0x44;\n    unsigned long shtLastTimeUpdated = 0;\n    bool shtDataRequested = false;\n    float shtCurrentTemp = 0;\n    float shtLastKnownTemp = 0;\n    float shtCurrentHumidity = 0;\n    float shtLastKnownHumidity = 0;\n\n    // Pin/IO vars\n    const int8_t anPentaLEDPins[5] = {14, 13, 12, 4, 2};\n    int8_t oledSpiClk = 15;\n    int8_t oledSpiData = 16;\n    int8_t oledSpiCs = 27;\n    int8_t oledSpiDc = 32;\n    int8_t oledSpiRst = 33;\n    int8_t shtSda = 1;\n    int8_t shtScl = 3;\n\n\n    bool isAnPentaLedPin(int8_t pin)\n    {\n      for(int8_t i = 0; i <= 4; i++)\n      {\n        if(anPentaLEDPins[i] == pin)\n          return true;\n      }\n      return false;\n    }\n\n    void getCurrentUsedLedPins()\n    {\n      for (int8_t lp = 0; lp <= 4; lp++) currentLedPins[lp] = 0;\n      byte numBusses = BusManager::getNumBusses();\n      byte numUsedPins = 0;\n\n      for (int8_t b = 0; b < numBusses; b++) {\n        Bus* curBus = BusManager::getBus(b);\n        if (curBus != nullptr) {\n          uint8_t pins[5] = {0, 0, 0, 0, 0};\n          currentBussesNumPins[b] = curBus->getPins(pins);\n          for (int8_t p = 0; p < currentBussesNumPins[b]; p++) {\n            if (isAnPentaLedPin(pins[p])) {\n              currentLedPins[numUsedPins] = pins[p];\n              numUsedPins++;\n            }\n          }\n        }\n      }\n    }\n\n    void getCurrentLedcValues()\n    {\n      byte numBusses = BusManager::getNumBusses();\n      byte numLedc = 0;\n\n      for (int8_t b = 0; b < numBusses; b++) {\n        Bus* curBus = BusManager::getBus(b);\n        if (curBus != nullptr) {\n          uint32_t curPixColor = curBus->getPixelColor(0);\n          uint8_t _data[5] = {255, 255, 255, 255, 255};\n          _data[3] = curPixColor >> 24;\n          _data[0] = curPixColor >> 16;\n          _data[1] = curPixColor >> 8;\n          _data[2] = curPixColor;\n\n          for (uint8_t i = 0; i < currentBussesNumPins[b]; i++) {\n            currentLedcReads[numLedc] = (_data[i] * bri) / 255;\n            numLedc++;\n          }\n        }\n      }\n    }\n\n\n    void initOledDisplay()\n    {\n      PinManagerPinType pins[5] = { { oledSpiClk, true }, { oledSpiData, true }, { oledSpiCs, true }, { oledSpiDc, true }, { oledSpiRst, true } };\n      if (!PinManager::allocateMultiplePins(pins, 5, PinOwner::UM_QuinLEDAnPenta)) {\n        DEBUG_PRINTF(\"[%s] OLED pin allocation failed!\\n\", _name);\n        oledEnabled = oledInitDone = false;\n        return;\n      }\n\n      oledDisplay = (U8G2 *) new U8G2_SSD1306_128X64_NONAME_2_4W_SW_SPI(U8G2_R0, oledSpiClk, oledSpiData, oledSpiCs, oledSpiDc, oledSpiRst);\n      if (oledDisplay == nullptr) {\n        DEBUG_PRINTF(\"[%s] OLED init failed!\\n\", _name);\n        oledEnabled = oledInitDone = false;\n        return;\n      }\n\n      oledDisplay->begin();\n      oledDisplay->setBusClock(40 * 1000 * 1000);\n      oledDisplay->setContrast(10);\n      oledDisplay->setPowerSave(0);\n      oledDisplay->setFont(u8g2_font_6x10_tf);\n      oledDisplay->setFlipMode(oledFlipScreen);\n\n      oledDisplay->firstPage();\n      do {\n        oledDisplay->drawXBMP(0, 16, 128, 36, quinLedLogo);\n      } while (oledDisplay->nextPage());\n      oledLogoDrawn = millis();\n\n      oledInitDone = true;\n    }\n\n    void cleanupOledDisplay()\n    {\n      if (oledInitDone) {\n        oledDisplay->clear();\n      }\n\n      PinManager::deallocatePin(oledSpiClk, PinOwner::UM_QuinLEDAnPenta);\n      PinManager::deallocatePin(oledSpiData, PinOwner::UM_QuinLEDAnPenta);\n      PinManager::deallocatePin(oledSpiCs, PinOwner::UM_QuinLEDAnPenta);\n      PinManager::deallocatePin(oledSpiDc, PinOwner::UM_QuinLEDAnPenta);\n      PinManager::deallocatePin(oledSpiRst, PinOwner::UM_QuinLEDAnPenta);\n\n      delete oledDisplay;\n\n      oledEnabled = false;\n      oledInitDone = false;\n    }\n\n    bool isOledReady()\n    {\n      return oledEnabled && oledInitDone;\n    }\n\n    void initSht30TempHumiditySensor()\n    {\n      PinManagerPinType pins[2] = { { shtSda, true }, { shtScl, true } };\n      if (!PinManager::allocateMultiplePins(pins, 2, PinOwner::UM_QuinLEDAnPenta)) {\n        DEBUG_PRINTF(\"[%s] SHT30 pin allocation failed!\\n\", _name);\n        shtEnabled = shtInitDone = false;\n        return;\n      }\n\n      TwoWire *wire = new TwoWire(1);\n      wire->setClock(400000);\n\n      sht30TempHumidSensor = (SHT *) new SHT30();\n      sht30TempHumidSensor->begin(shtI2cAddress, wire);\n      // The SHT lib calls wire.begin() again without the SDA and SCL pins... So call it again here...\n      wire->begin(shtSda, shtScl);\n      if (sht30TempHumidSensor->readStatus() == 0xFFFF) {\n        DEBUG_PRINTF(\"[%s] SHT30 init failed!\\n\", _name);\n        shtEnabled = shtInitDone = false;\n        return;\n      }\n\n      shtInitDone = true;\n    }\n\n    void cleanupSht30TempHumiditySensor()\n    {\n      if (shtInitDone) {\n        sht30TempHumidSensor->reset();\n      }\n\n      PinManager::deallocatePin(shtSda, PinOwner::UM_QuinLEDAnPenta);\n      PinManager::deallocatePin(shtScl, PinOwner::UM_QuinLEDAnPenta);\n\n      delete sht30TempHumidSensor;\n\n      shtEnabled = false;\n      shtInitDone = false;\n    }\n\n    void cleanup()\n    {\n      if (isOledReady()) {\n        cleanupOledDisplay();\n      }\n\n      if (isShtReady()) {\n        cleanupSht30TempHumiditySensor();\n      }\n\n      enabled = false;\n    }\n\n    bool oledCheckForNetworkChanges()\n    {\n      if (lastKnownNetworkConnected != Network.isConnected() || lastKnownIp != Network.localIP()\n          || lastKnownWiFiConnected != WiFi.isConnected() || lastKnownSsid != WiFi.SSID()\n          || lastKnownApActive != apActive || lastKnownApSsid != apSSID || lastKnownApPass != apPass || lastKnownApChannel != apChannel) {\n        lastKnownNetworkConnected = Network.isConnected();\n        lastKnownIp = Network.localIP();\n        lastKnownWiFiConnected = WiFi.isConnected();\n        lastKnownSsid = WiFi.SSID();\n        lastKnownApActive = apActive;\n        lastKnownApSsid = apSSID;\n        lastKnownApPass = apPass;\n        lastKnownApChannel = apChannel;\n\n        return networkHasChanged = true;\n      }\n      #ifdef WLED_USE_ETHERNET\n      if (lastKnownEthType != ethernetType || lastKnownEthLinkUp != ETH.linkUp()) {\n        lastKnownEthType = ethernetType;\n        lastKnownEthLinkUp = ETH.linkUp();\n\n        return networkHasChanged = true;\n      }\n      #endif\n\n      return networkHasChanged = false;\n    }\n\n    byte oledGetNextPage()\n    {\n      return oledCurrentPage + 1 <= oledMaxPage ? oledCurrentPage + 1 : 1;\n    }\n\n    void oledShowPage(byte page, bool updateLastTimePageChange = false)\n    {\n      oledCurrentPage = page;\n      updateOledDisplay();\n      oledLastTimeUpdated = millis();\n      if (updateLastTimePageChange) oledLastTimePageChange = oledLastTimeUpdated;\n    }\n\n    /*\n     * Page 1: Overall brightness and LED outputs\n     * Page 2: General info like temp, humidity and others\n     * Page 3: Network info\n     */\n    void updateOledDisplay()\n    {\n      if (!isOledReady()) return;\n\n      oledDisplay->firstPage();\n      do {\n        oledDisplay->setFont(u8g2_font_chroma48medium8_8r);\n        oledDisplay->drawStr(0, 8, serverDescription);\n        oledDisplay->drawHLine(0, 13, 127);\n        oledDisplay->setFont(u8g2_font_6x10_tf);\n\n        byte charPerRow = 21;\n        byte oledRow = 23;\n        switch (oledCurrentPage) {\n          // LED Outputs\n          case 1:\n          {\n            char charCurrentBrightness[charPerRow+1] = \"Brightness:\";\n            if (oledUseProgressBars) {\n              oledDisplay->drawStr(0, oledRow, charCurrentBrightness);\n              // There is no method to draw a filled box with rounded corners. So draw the rounded frame first, then fill that frame accordingly to LED percentage\n              oledDisplay->drawRFrame(68, oledRow - 6, 60, 7, 2);\n              oledDisplay->drawBox(69, oledRow - 5, int(round(58*getPercentageForBrightness(bri)) / 100), 5);\n            }\n            else {\n              sprintf(charCurrentBrightness, \"%s %d%%\", charCurrentBrightness, getPercentageForBrightness(bri));\n              oledDisplay->drawStr(0, oledRow, charCurrentBrightness);\n            }\n            oledRow += 8;\n\n            byte drawnLines = 0;\n            for (int8_t app = 0; app <= 4; app++) {\n              for (int8_t clp = 0; clp <= 4; clp++) {\n                if (anPentaLEDPins[app] == currentLedPins[clp]) {\n                  char charCurrentLedcReads[17];\n                  sprintf(charCurrentLedcReads, \"LED %d:\", app+1);\n                  if (oledUseProgressBars) {\n                    oledDisplay->drawStr(0, oledRow+(drawnLines*8), charCurrentLedcReads);\n                    oledDisplay->drawRFrame(38, oledRow - 6 + (drawnLines * 8), 90, 7, 2);\n                    oledDisplay->drawBox(39, oledRow - 5 + (drawnLines * 8), int(round(88*getPercentageForBrightness(currentLedcReads[clp])) / 100), 5);\n                  }\n                  else {\n                    sprintf(charCurrentLedcReads, \"%s %d%%\", charCurrentLedcReads, getPercentageForBrightness(currentLedcReads[clp]));\n                    oledDisplay->drawStr(0, oledRow+(drawnLines*8), charCurrentLedcReads);\n                  }\n\n                  drawnLines++;\n                }\n              }\n            }\n            break;\n          }\n\n          // Various info\n          case 2:\n          {\n            if (isShtReady() && shtReadDataSuccess) {\n              char charShtCurrentTemp[charPerRow+4]; // Reserve 3 more bytes than usual as we gonna have one UTF8 char which can be up to 4 bytes.\n              sprintf(charShtCurrentTemp, \"Temperature: %.02f°C\", shtCurrentTemp);\n              char charShtCurrentHumidity[charPerRow+1];\n              sprintf(charShtCurrentHumidity, \"Humidity: %.02f RH\", shtCurrentHumidity);\n\n              oledDisplay->drawUTF8(0, oledRow, charShtCurrentTemp);\n              oledDisplay->drawStr(0, oledRow + 10, charShtCurrentHumidity);\n              oledRow += 20;\n            }\n\n            if (mqttEnabled && mqttServer[0] != 0) {\n              char charMqttStatus[charPerRow+1];\n              sprintf(charMqttStatus, \"MQTT: %s\", (WLED_MQTT_CONNECTED ? \"Connected\" : \"Disconnected\"));\n              oledDisplay->drawStr(0, oledRow, charMqttStatus);\n              oledRow += 10;\n            }\n\n            // Always draw these two on the bottom\n            char charUptime[charPerRow+1];\n            sprintf(charUptime, \"Uptime: %ds\", int(millis()/1000 + rolloverMillis*4294967)); // From json.cpp\n            oledDisplay->drawStr(0, 53, charUptime);\n\n            char charWledVersion[charPerRow+1];\n            sprintf(charWledVersion, \"WLED v%s\", versionString);\n            oledDisplay->drawStr(0, 63, charWledVersion);\n            break;\n          }\n\n          // Network Info\n          case 3:\n            #ifdef WLED_USE_ETHERNET\n              if (lastKnownEthType == WLED_ETH_NONE) {\n                oledDisplay->drawStr(0, oledRow, \"Ethernet: No board selected\");\n                oledRow += 10;\n              }\n              else if (!lastKnownEthLinkUp) {\n                oledDisplay->drawStr(0, oledRow, \"Ethernet: Link Down\");\n                oledRow += 10;\n              }\n            #endif\n\n            if (lastKnownNetworkConnected) {\n              #ifdef WLED_USE_ETHERNET\n                if (lastKnownEthLinkUp) {\n                  oledDisplay->drawStr(0, oledRow, \"Ethernet: Link Up\");\n                  oledRow += 10;\n                }\n                else\n              #endif\n              // Wi-Fi can be active with ETH being connected, but we don't mind...\n              if (lastKnownWiFiConnected) {\n                #ifdef WLED_USE_ETHERNET\n                  if (!lastKnownEthLinkUp) {\n                #endif\n\n                oledDisplay->drawStr(0, oledRow, \"Wi-Fi: Connected\");\n                char currentSsidChar[lastKnownSsid.length() + 1];\n                lastKnownSsid.toCharArray(currentSsidChar, lastKnownSsid.length() + 1);\n                char charCurrentSsid[50];\n                sprintf(charCurrentSsid, \"SSID: %s\", currentSsidChar);\n                oledDisplay->drawStr(0, oledRow + 10, charCurrentSsid);\n                oledRow += 20;\n\n                #ifdef WLED_USE_ETHERNET\n                  }\n                #endif\n              }\n\n              String currentIpStr = lastKnownIp.toString();\n              char currentIpChar[currentIpStr.length() + 1];\n              currentIpStr.toCharArray(currentIpChar, currentIpStr.length() + 1);\n              char charCurrentIp[30];\n              sprintf(charCurrentIp, \"IP: %s\", currentIpChar);\n              oledDisplay->drawStr(0, oledRow, charCurrentIp);\n            }\n            // If WLED AP is active. Theoretically, it can even be active with ETH being connected, but we don't mind...\n            else if (lastKnownApActive) {\n              char charCurrentApStatus[charPerRow+1];\n              sprintf(charCurrentApStatus, \"WLED AP: %s (Ch: %d)\", (lastKnownApActive ? \"On\" : \"Off\"), lastKnownApChannel);\n              oledDisplay->drawStr(0, oledRow, charCurrentApStatus);\n\n              char charCurrentApSsid[charPerRow+1];\n              sprintf(charCurrentApSsid, \"SSID: %s\", lastKnownApSsid);\n              oledDisplay->drawStr(0, oledRow + 10, charCurrentApSsid);\n\n              char charCurrentApPass[charPerRow+1];\n              sprintf(charCurrentApPass, \"PW: %s\", lastKnownApPass);\n              oledDisplay->drawStr(0, oledRow + 20, charCurrentApPass);\n\n              // IP is hardcoded / no var exists in WLED at the time this mod was coded, so also hardcode it here\n              oledDisplay->drawStr(0, oledRow + 30, \"IP: 4.3.2.1\");\n            }\n\n            break;\n        }\n      } while (oledDisplay->nextPage());\n    }\n\n    bool isShtReady()\n    {\n      return shtEnabled && shtInitDone;\n    }\n\n\n  public:\n    // strings to reduce flash memory usage (used more than twice)\n    static const char _name[];\n    static const char _enabled[];\n    static const char _oledEnabled[];\n    static const char _oledUseProgressBars[];\n    static const char _oledFlipScreen[];\n    static const char _oledSecondsPerPage[];\n    static const char _oledFixBuggedScreen[];\n    static const char _shtEnabled[];\n    static const unsigned char quinLedLogo[];\n\n\n    static int8_t getPercentageForBrightness(byte brightness)\n    {\n      return int(((float)brightness / (float)255) * 100);\n    }\n\n\n    /*\n      * setup() is called once at boot. WiFi is not yet connected at this point.\n      * You can use it to initialize variables, sensors or similar.\n      */\n    void setup()\n    {\n      if (enabled) {\n        lastKnownBri = bri;\n\n        if (oledEnabled) {\n          initOledDisplay();\n        }\n\n        if (shtEnabled) {\n          initSht30TempHumiditySensor();\n        }\n\n        getCurrentUsedLedPins();\n\n        initDone = true;\n      }\n\n      firstRunDone = true;\n    }\n\n    /*\n      * loop() is called continuously. Here you can check for events, read sensors, etc.\n      *\n      * Tips:\n      * 1. You can use \"if (WLED_CONNECTED)\" to check for a successful network connection.\n      *    Additionally, \"if (WLED_MQTT_CONNECTED)\" is available to check for a connection to an MQTT broker.\n      *\n      * 2. Try to avoid using the delay() function. NEVER use delays longer than 10 milliseconds.\n      *    Instead, use a timer check as shown here.\n      */\n    void loop()\n    {\n      if (!enabled || !initDone || strip.isUpdating()) return;\n\n      if (isShtReady()) {\n        if (millis() - shtLastTimeUpdated > 30000 && !shtDataRequested) {\n          sht30TempHumidSensor->requestData();\n          shtDataRequested = true;\n\n          shtLastTimeUpdated = millis();\n        }\n\n        if (shtDataRequested) {\n          if (sht30TempHumidSensor->dataReady()) {\n            if (sht30TempHumidSensor->readData()) {\n              shtCurrentTemp = sht30TempHumidSensor->getTemperature();\n              shtCurrentHumidity = sht30TempHumidSensor->getHumidity();\n              shtReadDataSuccess = true;\n            }\n            else {\n              shtReadDataSuccess = false;\n            }\n\n            shtDataRequested = false;\n          }\n        }\n      }\n\n      if (isOledReady() && millis() - oledLogoDrawn > 3000) {\n        // Check for changes on the current page and update the OLED if a change is detected\n        if (millis() - oledLastTimeUpdated > 150) {\n          // If there was a network change, force page 3 (network page)\n          if (oledCheckForNetworkChanges()) {\n            oledCurrentPage = 3;\n          }\n          // Only redraw a page if there was a change for that page\n          switch (oledCurrentPage) {\n            case 1:\n              lastKnownBri = bri;\n              // Probably causes lag to always do ledcRead(), so rather re-do the math, 'cause we can't easily get it...\n              getCurrentLedcValues();\n\n              if (bri != lastKnownBri || lastKnownLedcReads[0] != currentLedcReads[0] || lastKnownLedcReads[1] != currentLedcReads[1] || lastKnownLedcReads[2] != currentLedcReads[2]\n                  || lastKnownLedcReads[3] != currentLedcReads[3] || lastKnownLedcReads[4] != currentLedcReads[4]) {\n                lastKnownLedcReads[0] = currentLedcReads[0]; lastKnownLedcReads[1] = currentLedcReads[1]; lastKnownLedcReads[2] = currentLedcReads[2]; lastKnownLedcReads[3] = currentLedcReads[3]; lastKnownLedcReads[4] = currentLedcReads[4];\n\n                oledShowPage(1);\n              }\n              break;\n\n            case 2:\n              if (shtLastKnownTemp != shtCurrentTemp || shtLastKnownHumidity != shtCurrentHumidity) {\n                shtLastKnownTemp = shtCurrentTemp;\n                shtLastKnownHumidity = shtCurrentHumidity;\n\n                oledShowPage(2);\n              }\n              break;\n\n            case 3:\n              if (networkHasChanged) {\n                networkHasChanged = false;\n\n                oledShowPage(3, true);\n              }\n              break;\n          }\n        }\n        // Cycle through OLED pages\n        if (millis() - oledLastTimePageChange > oledSecondsPerPage * 1000) {\n          // Periodically fixing a \"bugged out\" OLED. More details in the ReadMe\n          if (oledFixBuggedScreen && millis() - oledLastTimeFixBuggedScreen > 60000) {\n            oledDisplay->begin();\n            oledLastTimeFixBuggedScreen = millis();\n          }\n          oledShowPage(oledGetNextPage(), true);\n        }\n      }\n    }\n\n    void addToConfig(JsonObject &root)\n    {\n      JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname\n\n      top[FPSTR(_enabled)] = enabled;\n      top[FPSTR(_oledEnabled)] = oledEnabled;\n      top[FPSTR(_oledUseProgressBars)] = oledUseProgressBars;\n      top[FPSTR(_oledFlipScreen)] = oledFlipScreen;\n      top[FPSTR(_oledSecondsPerPage)] = oledSecondsPerPage;\n      top[FPSTR(_oledFixBuggedScreen)] = oledFixBuggedScreen;\n      top[FPSTR(_shtEnabled)] = shtEnabled;\n\n      // Update LED pins on config save\n      getCurrentUsedLedPins();\n    }\n\n    /**\n     * readFromConfig() is called before setup() to populate properties from values stored in cfg.json\n     *\n     * The function should return true if configuration was successfully loaded or false if there was no configuration.\n     */\n    bool readFromConfig(JsonObject &root)\n    {\n      JsonObject top = root[FPSTR(_name)];\n      if (top.isNull()) {\n        DEBUG_PRINTF(\"[%s] No config found. (Using defaults.)\\n\", _name);\n        return false;\n      }\n\n      bool oldEnabled = enabled;\n      bool oldOledEnabled = oledEnabled;\n      bool oldOledFlipScreen = oledFlipScreen;\n      bool oldShtEnabled = shtEnabled;\n\n      getJsonValue(top[FPSTR(_enabled)], enabled);\n      getJsonValue(top[FPSTR(_oledEnabled)], oledEnabled);\n      getJsonValue(top[FPSTR(_oledUseProgressBars)], oledUseProgressBars);\n      getJsonValue(top[FPSTR(_oledFlipScreen)], oledFlipScreen);\n      getJsonValue(top[FPSTR(_oledSecondsPerPage)], oledSecondsPerPage);\n      getJsonValue(top[FPSTR(_oledFixBuggedScreen)], oledFixBuggedScreen);\n      getJsonValue(top[FPSTR(_shtEnabled)], shtEnabled);\n\n      // First run: reading from cfg.json, nothing to do here, will be all done in setup()\n      if (!firstRunDone) {\n        DEBUG_PRINTF(\"[%s] First run, nothing to do\\n\", _name);\n      }\n      // Check if mod has been en-/disabled\n      else if (enabled != oldEnabled) {\n        enabled ? setup() : cleanup();\n        DEBUG_PRINTF(\"[%s] Usermod has been en-/disabled\\n\", _name);\n      }\n      // Config has been changed, so adopt to changes\n      else if (enabled) {\n        if (oldOledEnabled != oledEnabled) {\n          oledEnabled ? initOledDisplay() : cleanupOledDisplay();\n        }\n        else if (oledEnabled && oldOledFlipScreen != oledFlipScreen) {\n          oledDisplay->clear();\n          oledDisplay->setFlipMode(oledFlipScreen);\n          oledShowPage(oledCurrentPage);\n        }\n\n        if (oldShtEnabled != shtEnabled) {\n          shtEnabled ? initSht30TempHumiditySensor() : cleanupSht30TempHumiditySensor();\n        }\n\n        DEBUG_PRINTF(\"[%s] Config (re)loaded\\n\", _name);\n      }\n\n      return true;\n    }\n\n    void addToJsonInfo(JsonObject& root)\n    {\n      if (!enabled && !isShtReady()) {\n        return;\n      }\n\n      JsonObject user = root[\"u\"];\n      if (user.isNull()) user = root.createNestedObject(\"u\");\n\n      JsonArray jsonTemp = user.createNestedArray(\"Temperature\");\n      JsonArray jsonHumidity = user.createNestedArray(\"Humidity\");\n\n      if (shtLastTimeUpdated == 0 || !shtReadDataSuccess) {\n        jsonTemp.add(0);\n        jsonHumidity.add(0);\n        if (shtLastTimeUpdated == 0) {\n          jsonTemp.add(\" Not read yet\");\n          jsonHumidity.add(\" Not read yet\");\n        }\n        else {\n          jsonTemp.add(\" Error\");\n          jsonHumidity.add(\" Error\");\n        }\n\n        return;\n      }\n\n      jsonHumidity.add(shtCurrentHumidity);\n      jsonHumidity.add(\" RH\");\n\n      jsonTemp.add(shtCurrentTemp);\n      jsonTemp.add(\" °C\");\n    }\n\n    /*\n      * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).\n      * This could be used in the future for the system to determine whether your usermod is installed.\n      */\n    uint16_t getId()\n    {\n      return USERMOD_ID_QUINLED_AN_PENTA;\n    }\n};\n\n// strings to reduce flash memory usage (used more than twice)\n// Config settings\nconst char QuinLEDAnPentaUsermod::_name[]                PROGMEM = \"QuinLED-An-Penta\";\nconst char QuinLEDAnPentaUsermod::_enabled[]             PROGMEM = \"Enabled\";\nconst char QuinLEDAnPentaUsermod::_oledEnabled[]         PROGMEM = \"Enable-OLED\";\nconst char QuinLEDAnPentaUsermod::_oledUseProgressBars[] PROGMEM = \"OLED-Use-Progress-Bars\";\nconst char QuinLEDAnPentaUsermod::_oledFlipScreen[]      PROGMEM = \"OLED-Flip-Screen-180\";\nconst char QuinLEDAnPentaUsermod::_oledSecondsPerPage[]  PROGMEM = \"OLED-Seconds-Per-Page\";\nconst char QuinLEDAnPentaUsermod::_oledFixBuggedScreen[] PROGMEM = \"OLED-Fix-Bugged-Screen\";\nconst char QuinLEDAnPentaUsermod::_shtEnabled[]          PROGMEM = \"Enable-SHT30-Temp-Humidity-Sensor\";\n// Other strings\n\nconst unsigned char QuinLEDAnPentaUsermod::quinLedLogo[] PROGMEM = {\n  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,\n  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,\n  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x9F, 0xFD, 0xFF,\n  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,\n  0xFF, 0x03, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,\n  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,\n  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0x80, 0xFF,\n  0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,\n  0x3F, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF,\n  0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0x07, 0xFE, 0xFF, 0xFF, 0x0F, 0xFC,\n  0xFF, 0xFF, 0xF3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xFC, 0x0F, 0xFE,\n  0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0xE3, 0xFF, 0xA5, 0xFF, 0xFF, 0xFF,\n  0x0F, 0xFC, 0x1F, 0xFE, 0xFF, 0xFF, 0x1F, 0xFC, 0xFF, 0xFF, 0xE1, 0xFF,\n  0x00, 0xF0, 0xE3, 0xFF, 0x0F, 0xFE, 0x1F, 0xFE, 0xFF, 0xFF, 0x3F, 0xFF,\n  0xFF, 0xFF, 0xE3, 0xFF, 0x00, 0xF0, 0x00, 0xFF, 0x07, 0xFE, 0x1F, 0xFC,\n  0xF9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE1, 0xFF, 0x00, 0xF0, 0x00, 0xFE,\n  0x07, 0xFF, 0x1F, 0xFC, 0xF0, 0xC7, 0x3F, 0xFF, 0xFF, 0xFF, 0xE3, 0xFF,\n  0xF1, 0xFF, 0x00, 0xFC, 0x07, 0xFF, 0x1F, 0xFE, 0xF0, 0xC3, 0x1F, 0xFE,\n  0x00, 0xFF, 0xE1, 0xFF, 0xF1, 0xFF, 0x30, 0xF8, 0x07, 0xFF, 0x1F, 0xFE,\n  0xF0, 0xC3, 0x1F, 0xFE, 0x00, 0xFC, 0xC3, 0xFF, 0xE1, 0xFF, 0xF0, 0xF0,\n  0x03, 0xFF, 0x0F, 0x7E, 0xF0, 0xC3, 0x1F, 0x7E, 0x00, 0xF8, 0xE3, 0xFF,\n  0xE1, 0xFF, 0xF1, 0xF1, 0x83, 0xFF, 0x0F, 0x7E, 0xF0, 0xC3, 0x1F, 0x7E,\n  0x00, 0xF0, 0xC3, 0xFF, 0xE1, 0xFF, 0xF1, 0xE1, 0x83, 0xFF, 0x0F, 0xFE,\n  0xF0, 0xC3, 0x1F, 0xFE, 0xF8, 0xF0, 0xC3, 0xFF, 0xA1, 0xFF, 0xF1, 0xE3,\n  0x81, 0xFF, 0x0F, 0x7E, 0xF0, 0xC1, 0x1F, 0x7E, 0xF0, 0xF0, 0xC3, 0xFF,\n  0x01, 0xF8, 0xE1, 0xC3, 0x83, 0xFF, 0x0F, 0x7F, 0xF8, 0xC3, 0x1F, 0x7E,\n  0xF8, 0xF0, 0xC3, 0xFF, 0x03, 0xF8, 0xE1, 0xC7, 0x81, 0xE4, 0x0F, 0x7F,\n  0xF0, 0xC3, 0x1F, 0xFE, 0xF8, 0xF0, 0xC3, 0xFF, 0x01, 0xF8, 0xE3, 0xC7,\n  0x01, 0xC0, 0x07, 0x7F, 0xF8, 0xC1, 0x1F, 0x7E, 0xF0, 0xE1, 0xC3, 0xFF,\n  0xC3, 0xFD, 0xE1, 0x87, 0x01, 0x00, 0x07, 0x7F, 0xF8, 0xC3, 0x1F, 0x7E,\n  0xF8, 0xF0, 0xC3, 0xFF, 0xE3, 0xFF, 0xE3, 0x87, 0x01, 0x00, 0x82, 0x3F,\n  0xF8, 0xE1, 0x1F, 0xFE, 0xF8, 0xE1, 0xC3, 0xFF, 0xC3, 0xFF, 0xC3, 0x87,\n  0x01, 0x00, 0x80, 0x3F, 0xF8, 0xC1, 0x1F, 0x7E, 0xF0, 0xF1, 0xC3, 0xFF,\n  0xC3, 0xFF, 0xC3, 0x87, 0x03, 0x0F, 0x80, 0x3F, 0xF8, 0xE1, 0x0F, 0x7E,\n  0xF8, 0xE1, 0x87, 0xFF, 0xC3, 0xFF, 0xC7, 0x87, 0x03, 0x04, 0xC0, 0x7F,\n  0xF0, 0xE1, 0x0F, 0xFF, 0xF8, 0xF1, 0x87, 0xFF, 0xC3, 0xFF, 0xC3, 0x87,\n  0x07, 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0x1F, 0x7E, 0xF0, 0xE0, 0xC3, 0xFF,\n  0xC7, 0xFF, 0x87, 0x87, 0x0F, 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0x0F, 0x7F,\n  0xF8, 0xE1, 0x07, 0x80, 0x07, 0xEA, 0x87, 0xC1, 0x0F, 0x00, 0x80, 0xFF,\n  0x00, 0xE0, 0x1F, 0x7E, 0xF0, 0xE1, 0x07, 0x00, 0x03, 0x80, 0x07, 0xC0,\n  0x7F, 0x00, 0x00, 0xFF, 0x01, 0xE0, 0x1F, 0xFF, 0xF8, 0xE1, 0x07, 0x00,\n  0x07, 0x00, 0x07, 0xE0, 0xFF, 0xF7, 0x01, 0xFF, 0x57, 0xF7, 0x9F, 0xFF,\n  0xFC, 0xF1, 0x0F, 0x00, 0x07, 0x80, 0x0F, 0xE0, 0xFF, 0xFF, 0x03, 0xFF,\n  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xBF, 0xFE,\n  0xFF, 0xFF, 0x8F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,\n  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,\n  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,\n  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,\n};\n\nstatic QuinLEDAnPentaUsermod quinled_an_penta;\nREGISTER_USERMOD(quinled_an_penta);"
  },
  {
    "path": "usermods/quinled-an-penta/readme.md",
    "content": "# QuinLED-An-Penta\nThe (un)official usermod to get the best out of the QuinLED-An-Penta (https://quinled.info/quinled-an-penta/), e.g. using the OLED and the SHT30 temperature/humidity sensor.\n\n## Requirements\n* \"u8g2\" by olikraus, v2.28 or higher: https://github.com/olikraus/u8g2\n* \"SHT85\" by Rob Tillaart, v0.2 or higher: https://github.com/RobTillaart/SHT85\n\n## Some words about the (optional) OLED\nThis 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.\nI 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.\nNote: you _must_ use an **SPI** driven OLED, **not an i2c one**!\n\n### Limitations combined with Ethernet\nThe initial development of this mod was done with a beta version of the QuinLED-An-Penta, which had a different IO layout for the OLED: The CS pin _was_ IO_0, but has been changed to IO27 with the first v1 public release. Unfortunately, IO27 is used by Ethernet boards, so WLED will not let you enable the OLED screen, if you're using it with Ethernet. Unfortunately, that makes the development I've done to support/show Ethernet information invalid, as it cannot be used.\nHowever, (and I've not tried this, as I don't own a v1 board) you can modify this usermod and try to use IO27 for the OLED and share it with the Ethernet board. It is \"just\" the chip select pin, so there is a chance that both can coexist and use the same IO. You need to skip WLEDs PinManager for the CS pin, so WLED will not block using it. If you don't know how this works, don't change it. If you know what I'm talking about, try it and please let me know on the Intermit.Tech (QuinLED) Discord server: https://discord.gg/WdbAauG\n\n### My OLED flickers after some time, what should I do?\nThat's a tricky one. During development I saw that the OLED sometimes starts to \"drop out\" / flicker and won't work anymore. This seems to be caused by the high PWM interference the board produces. It seems to lose its settings then doesn't know how to draw anymore. Turns out the only way to fix this is to call the libraries `begin()` method again which re-initializes the display.\nIf you're facing this issue, you can enable a setting which will call the `begin()` roughly every 60 seconds between page changes. This will make the page change take ~500ms, but will fix the display.\n\n\n## Configuration\nNavigate to the \"Config\" and then to the \"Usermods\" section. If you compiled WLED with `-D QUINLED_AN_PENTA`, you will see the config for it there:\n* Enable-OLED:\n  * What it does: Enables the optional SPI driven OLED that can be mounted to the 7-pin female header. Won't work with Ethernet, read above.\n  * Possible values: Enabled/Disabled\n  * Default: Disabled\n* OLED-Use-Progress-Bars:\n  * What it does: Toggle between showing percentage numbers or a progress-bar-like visualization for overall brightness and each LED channels brightness level\n  * Possible values: Enabled/Disabled\n  * Default: Disabled\n* OLED-Flip-Screen-180:\n  * What it does: Flips the screen 180°\n  * Possible values: Enabled/Disabled\n  * Default: Disabled\n* OLED-Seconds-Per-Page:\n  * What it does: Number of seconds the OLED should stay on one page before changing pages\n  * Possible values: Enabled/Disabled\n  * Default: 10\n* OLED-Fix-Bugged-Screen:\n  * What it does: Enable this if your OLED flickers after some time. For more info read above under [\"My OLED flickers after some time, what should I do?\"](#My-OLED-flickers-after-some-time-what-should-I-do)\n  * Possible values: Enabled/Disabled\n  * Default: Disabled\n* Enable-SHT30-Temp-Humidity-Sensor:\n  * What it does: Enables the onboard SHT30 temperature and humidity sensor\n  * Possible values: Enabled/Disabled\n  * Default: Disabled\n\n## Change log\n2021-12\n* Adjusted IO layout to match An-Penta v1r1\n2021-10\n* First implementation.\n\n## Credits\nezcGman | Andy: Find me on the Intermit.Tech (QuinLED) Discord server: https://discord.gg/WdbAauG\n"
  },
  {
    "path": "usermods/readme.md",
    "content": "# Usermods\n\nThis folder serves as a repository for usermods (custom `usermod.cpp` files)!\n\nIf you have created a usermod you believe is useful (for example to support a particular sensor, display, feature...), feel free to contribute by opening a pull request!\n\nIn order for other people to be able to have fun with your usermod, please keep these points in mind:\n\n* Create a folder in this folder with a descriptive name (for example `usermod_ds18b20_temp_sensor_mqtt`)  \n* Include your custom files \n* If your usermod requires changes to other WLED files, please write a `readme.md` outlining the steps one needs to take  \n* Create a pull request!  \n* If your feature is useful for the majority of WLED users, I will consider adding it to the base code!  \n\nWhile I do my best to not break too much, keep in mind that as WLED is updated, usermods might break.  \nI am not actively maintaining any usermod in this directory, that is your responsibility as the creator of the usermod.\n\nFor new usermods, I would recommend trying out the new v2 usermod API, which allows installing multiple usermods at once and new functions!\nYou can take a look at `EXAMPLE_v2` for some documentation and at `Temperature` for a completed v2 usermod!\n\nThank you for your help :)\n"
  },
  {
    "path": "usermods/rgb-rotary-encoder/library.json",
    "content": "{\n  \"name\": \"rgb-rotary-encoder\",\n  \"build\": { \"libArchive\": false},\n  \"dependencies\": {\n    \"lennarthennigs/ESP Rotary\":\"^2.1.1\"\n  }\n}\n"
  },
  {
    "path": "usermods/rgb-rotary-encoder/readme.md",
    "content": "# RGB Encoder Board\n\nThis usermod-v2 adds support for the awesome RGB Rotary Encoder Board by Adam Zeloof / \"Isotope Engineering\" to control the overall brightness of your WLED instance: https://github.com/isotope-engineering/RGB-Encoder-Board. A great DIY rotary encoder with 20 tiny SK6805 / \"NeoPixel Nano\" LEDs.\n\nhttps://user-images.githubusercontent.com/3090131/124680599-0180ab80-dec7-11eb-9065-a6d08ebe0287.mp4\n\n## Credits\nThe 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.\nIt was quite a bit more work than I hoped, but I got there eventually :)\n\n## How to connect the board to your ESP\nWe'll need (minimum) three or (maximum) four GPIOs for the board:\n* \"ea\": reports the encoder direction\n* \"eb\": Same thing, opposite direction\n* \"di\": LED data in.\n* *(optional)* \"sw\": The integrated switch in the rotary encoder. Can be omitted for the bare functionality of controlling only the brightness\n\nWe'll also need power:\n\n* \"vdd\": Needs to be connected to **+5V**.\n* \"gnd\": Ground.\n\nYou can freely pick the GPIOs, it doesn't matter. Those will be configured in the \"Usermods\" section of the WLED web panel:\n\n## Configuration\nNavigate to the \"Config\" and then to the \"Usermods\" section. If you compiled WLED with `-D RGB_ROTARY_ENCODER`, you will see the config for it there. The settings there are the aforementioned GPIOs, (*Note: The switch pin is not there, as this can just be configured the \"normal\" button on the \"LED Preferences\" page*) plus a few more:\n* LED pin:\n  * Possible values: Any valid and available GPIO\n  * Default: 3\n  * What it does: controls the LED ring\n* ea pin:\n  * Possible values: Any valid and available GPIO\n  * Default: 15\n  * What it does: First of the two rotary encoder pins\n* eb pin:\n  * Possible values: Any valid and available GPIO\n  * Default: 32\n  * What it does: Second of the two rotary encoder pins\n* LED Mode:\n  * Possible values: 1-3\n  * Default: 3\n  * What it does: The usermod provides three different modes of how the LEDs can appear. Here's an example: https://github.com/isotope-engineering/RGB-Encoder-Board/blob/master/images/rgb-encoder-animations.gif\n    * Up left is \"1\"\n    * Up right is not supported / doesn't make sense for brightness control\n    * Bottom left is \"2\"\n    * Bottom right is \"3\"\n* LED Brightness:\n  * Possible values: 1-255\n  * Default: 64\n  * What it does: sets LED ring Brightness\n* Steps per click:\n  * Possible values: Any positive number\n  * Default: 4\n  * What it does: With each \"click\", a rotary encoder actually increments its \"steps\". Most rotary encoders produce four \"steps\" per \"click\". Leave this at the default value unless your rotary encoder behaves strangely. e.g. with one click, it makes two LEDs light up, or you need two clicks for one LED. If that's the case, adjust this value or write a small sketch using the same \"ESP Rotary\" library and read out the steps it produce.\n* Increment per click:\n  * Possible values: Any positive number\n  * Default: 5\n  * What it does: Most rotary encoders have 20 \"clicks\" or positions. This value should be set to 100/`number of clicks`\n\n## Change log\n2021-07\n* First implementation.\n"
  },
  {
    "path": "usermods/rgb-rotary-encoder/rgb-rotary-encoder.cpp",
    "content": "#include \"ESPRotary.h\"\n#include <math.h>\n#include \"wled.h\"\n\nclass RgbRotaryEncoderUsermod : public Usermod\n{\n  private:\n    bool enabled = false;\n    bool initDone = false;\n    bool isDirty = false;\n    BusDigital *ledBus;\n    /*\n    * Green - eb - Q4 - 32\n    * Red   - ea - Q1 - 15\n    * Black - sw - Q2 - 12\n    */\n    ESPRotary *rotaryEncoder;\n    int8_t ledIo = 3; // GPIO to control the LEDs\n    int8_t eaIo = 15; // \"ea\" from RGB Encoder Board\n    int8_t ebIo = 32; // \"eb\" from RGB Encoder Board\n    byte stepsPerClick = 4; // How many \"steps\" your rotary encoder does per click. This varies per rotary encoder\n    /* This could vary per rotary encoder: Usually rotary encoders have 20 \"clicks\".\n      If yours has less/more, adjust this to: 100% = 20 LEDs * incrementPerClick */\n    byte incrementPerClick = 5;\n    byte ledMode = 3;\n    byte ledBrightness = 64;\n\n    // This is all needed to calculate the brightness, rotary position, etc.\n    const byte minPos = 5; // minPos is not zero, because if we want to turn the LEDs off, we use the built-in button ;)\n    const byte maxPos = 100; // maxPos=100, like 100%\n    const byte numLeds = 20;\n    byte lastKnownPos = 0;\n\n    byte currentColors[3];\n    byte lastKnownBri = 0;\n\n\n    void initRotaryEncoder()\n    {\n      PinManagerPinType pins[2] = { { eaIo, false }, { ebIo, false } };\n      if (!PinManager::allocateMultiplePins(pins, 2, PinOwner::UM_RGBRotaryEncoder)) {\n        eaIo = -1;\n        ebIo = -1;\n        cleanup();\n        return;\n      }\n\n      // I don't know why, but setting the upper bound here does not work. It results into 1717922932 O_o\n      rotaryEncoder = new ESPRotary(eaIo, ebIo, stepsPerClick, incrementPerClick, maxPos, currentPos, incrementPerClick);\n      rotaryEncoder->setUpperBound(maxPos); // I have to again set it here and then it works / is actually 100...\n\n      rotaryEncoder->setChangedHandler(RgbRotaryEncoderUsermod::cbRotate);\n    }\n\n    void initLedBus()\n    {\n      // Initialize all pins to the sentinel value first…\n      byte _pins[OUTPUT_MAX_PINS];\n      std::fill(std::begin(_pins), std::end(_pins), 255);\n      // …then set only the LED pin\n      _pins[0] = static_cast<byte>(ledIo);\n      BusConfig busCfg = BusConfig(TYPE_WS2812_RGB, _pins, 0, numLeds, COL_ORDER_GRB, false, 0);\n      busCfg.iType = BusManager::getI(busCfg.type, busCfg.pins, busCfg.driverType); // assign internal bus type and output driver\n      ledBus = new BusDigital(busCfg);\n      if (!ledBus->isOk()) {\n        cleanup();\n        return;\n      }\n\n      ledBus->setBrightness(ledBrightness);\n    }\n\n    void updateLeds()\n    {\n      switch (ledMode) {\n        case 2:\n          {\n            currentColors[0] = 255; currentColors[1] = 0; currentColors[2] = 0;\n            for (int i = 0; i < currentPos / incrementPerClick - 1; i++) {\n              ledBus->setPixelColor(i, 0);\n            }\n            ledBus->setPixelColor(currentPos / incrementPerClick - 1, colorFromRgbw(currentColors));\n            for (int i = currentPos / incrementPerClick; i < numLeds; i++) {\n              ledBus->setPixelColor(i, 0);\n            }\n          }\n          break;\n\n        default:\n        case 1:\n        case 3:\n          // WLED orange (of course), which we will use in mode 1\n          currentColors[0] = 255; currentColors[1] = 160; currentColors[2] = 0;\n          for (int i = 0; i < currentPos / incrementPerClick; i++) {\n            if (ledMode == 3) {\n              hsv2rgb((i) / float(numLeds), 1, .25);\n            }\n            ledBus->setPixelColor(i, colorFromRgbw(currentColors));\n          }\n          for (int i = currentPos / incrementPerClick; i < numLeds; i++) {\n            ledBus->setPixelColor(i, 0);\n          }\n          break;\n      }\n\n      isDirty = true;\n    }\n\n    void cleanup()\n    {\n      // Only deallocate pins if we allocated them ;)\n      if (eaIo != -1) {\n        PinManager::deallocatePin(eaIo, PinOwner::UM_RGBRotaryEncoder);\n        eaIo = -1;\n      }\n      if (ebIo != -1) {\n        PinManager::deallocatePin(ebIo, PinOwner::UM_RGBRotaryEncoder);\n        ebIo = -1;\n      }\n\n      delete rotaryEncoder;\n      delete ledBus;\n\n      enabled = false;\n    }\n\n    int getPositionForBrightness()\n    {\n      return int(((float)bri / (float)255) * 100);\n    }\n\n    float fract(float x) { return x - int(x); }\n\n    float mix(float a, float b, float t) { return a + (b - a) * t; }\n\n    void hsv2rgb(float h, float s, float v) {\n      currentColors[0] = int((v * mix(1.0, constrain(abs(fract(h + 1.0) * 6.0 - 3.0) - 1.0, 0.0, 1.0), s)) * 255);\n      currentColors[1] = int((v * mix(1.0, constrain(abs(fract(h + 0.6666666) * 6.0 - 3.0) - 1.0, 0.0, 1.0), s)) * 255);\n      currentColors[2] = int((v * mix(1.0, constrain(abs(fract(h + 0.3333333) * 6.0 - 3.0) - 1.0, 0.0, 1.0), s)) * 255);\n    }\n\n  public:\n    static byte currentPos;\n\n    // strings to reduce flash memory usage (used more than twice)\n    static const char _name[];\n    static const char _enabled[];\n    static const char _ledIo[];\n    static const char _eaIo[];\n    static const char _ebIo[];\n    static const char _ledMode[];\n    static const char _ledBrightness[];\n    static const char _stepsPerClick[];\n    static const char _incrementPerClick[];\n\n\n    static void cbRotate(ESPRotary& r) {\n      currentPos = r.getPosition();\n    }\n\n    /**\n     * Enable/Disable the usermod\n     */\n    // inline void enable(bool enable) { enabled = enable; }\n    /**\n     * Get usermod enabled/disabled state\n     */\n    // inline bool isEnabled() { return enabled; }\n\n    /*\n      * setup() is called once at boot. WiFi is not yet connected at this point.\n      * You can use it to initialize variables, sensors or similar.\n      */\n    void setup()\n    {\n      if (enabled) {\n        currentPos = getPositionForBrightness();\n        lastKnownBri = bri;\n\n        initRotaryEncoder();\n        initLedBus();\n\n        // No updating of LEDs here, as that's sometimes not working; loop() will take care of that\n\n        initDone = true;\n      }\n    }\n\n    /*\n      * loop() is called continuously. Here you can check for events, read sensors, etc.\n      * \n      * Tips:\n      * 1. You can use \"if (WLED_CONNECTED)\" to check for a successful network connection.\n      *    Additionally, \"if (WLED_MQTT_CONNECTED)\" is available to check for a connection to an MQTT broker.\n      * \n      * 2. Try to avoid using the delay() function. NEVER use delays longer than 10 milliseconds.\n      *    Instead, use a timer check as shown here.\n      */\n    void loop()\n    {\n      if (!enabled || strip.isUpdating()) return;\n\n      rotaryEncoder->loop();\n\n      // If the rotary was changed\n      if(lastKnownPos != currentPos) {\n        lastKnownPos = currentPos;\n\n        bri = min(int(round((2.55 * currentPos))), 255);\n        lastKnownBri = bri;\n\n        updateLeds();\n        colorUpdated(CALL_MODE_DIRECT_CHANGE);\n      }\n\n      // If the brightness is changed not with the rotary, update the rotary\n      if (bri != lastKnownBri) {\n        currentPos = lastKnownPos = getPositionForBrightness();\n        lastKnownBri = bri;\n        rotaryEncoder->resetPosition(currentPos);\n        updateLeds();\n      }\n\n      // Update LEDs here in loop to also validate that we can update/show\n      if (isDirty && ledBus->canShow()) {\n        isDirty = false;\n        ledBus->show();\n      }\n    }\n\n    void addToConfig(JsonObject &root)\n    {\n      JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname\n\n      top[FPSTR(_enabled)] = enabled;\n      top[FPSTR(_ledIo)] = ledIo;\n      top[FPSTR(_eaIo)] = eaIo;\n      top[FPSTR(_ebIo)] = ebIo;\n      top[FPSTR(_ledMode)] = ledMode;\n      top[FPSTR(_ledBrightness)] = ledBrightness;\n      top[FPSTR(_stepsPerClick)] = stepsPerClick;\n      top[FPSTR(_incrementPerClick)] = incrementPerClick;\n    }\n\n    /**\n     * readFromConfig() is called before setup() to populate properties from values stored in cfg.json\n     *\n     * The function should return true if configuration was successfully loaded or false if there was no configuration.\n     */\n    bool readFromConfig(JsonObject &root)\n    {\n      JsonObject top = root[FPSTR(_name)];\n      if (top.isNull()) {\n        DEBUG_PRINTF(\"[%s] No config found. (Using defaults.)\\n\", _name);\n        return false;\n      }\n\n      bool oldEnabled = enabled;\n      int8_t oldLedIo = ledIo;\n      int8_t oldEaIo =  eaIo;\n      int8_t oldEbIo =  ebIo;\n      byte oldLedMode = ledMode;\n      byte oldStepsPerClick = stepsPerClick;\n      byte oldIncrementPerClick = incrementPerClick;\n      byte oldLedBrightness = ledBrightness;\n\n      getJsonValue(top[FPSTR(_enabled)], enabled);\n      getJsonValue(top[FPSTR(_ledIo)], ledIo);\n      getJsonValue(top[FPSTR(_eaIo)], eaIo);\n      getJsonValue(top[FPSTR(_ebIo)], ebIo);\n      getJsonValue(top[FPSTR(_stepsPerClick)], stepsPerClick);\n      getJsonValue(top[FPSTR(_incrementPerClick)], incrementPerClick);\n      ledMode = top[FPSTR(_ledMode)] > 0 && top[FPSTR(_ledMode)] < 4 ? top[FPSTR(_ledMode)] : ledMode;\n      ledBrightness = top[FPSTR(_ledBrightness)] > 0 && top[FPSTR(_ledBrightness)] <= 255 ? top[FPSTR(_ledBrightness)] : ledBrightness;\n\n      if (!initDone) {\n        // First run: reading from cfg.json\n        // Nothing to do here, will be all done in setup() \n      }\n      // Mod was disabled, so run setup()\n      else if (enabled && enabled != oldEnabled) {\n        DEBUG_PRINTF(\"[%s] Usermod has been re-enabled\\n\", _name);\n        setup();\n      }\n      // Config has been changed, so adopt to changes\n      else {\n        if (!enabled) {\n          DEBUG_PRINTF(\"[%s] Usermod has been disabled\\n\", _name);\n          cleanup();\n        }\n        else {\n          DEBUG_PRINTF(\"[%s] Usermod is enabled\\n\", _name);\n          if (ledIo != oldLedIo) {\n            delete ledBus;\n            initLedBus();\n          }\n\n          if (ledBrightness != oldLedBrightness) {\n            ledBus->setBrightness(ledBrightness);\n            isDirty = true;\n          }\n\n          if (ledMode != oldLedMode) {\n            updateLeds();\n          }\n\n          if (eaIo != oldEaIo || ebIo != oldEbIo || stepsPerClick != oldStepsPerClick || incrementPerClick != oldIncrementPerClick) {\n            PinManager::deallocatePin(oldEaIo, PinOwner::UM_RGBRotaryEncoder);\n            PinManager::deallocatePin(oldEbIo, PinOwner::UM_RGBRotaryEncoder);\n            \n            delete rotaryEncoder;\n            initRotaryEncoder();\n          }\n        }\n\n        DEBUG_PRINTF(\"[%s] Config (re)loaded\\n\", _name);\n      }\n      \n      return true;\n    }\n\n    /*\n      * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).\n      * This could be used in the future for the system to determine whether your usermod is installed.\n      */\n    uint16_t getId()\n    {\n      return USERMOD_RGB_ROTARY_ENCODER;\n    }\n\n    //More methods can be added in the future, this example will then be extended.\n    //Your usermod will remain compatible as it does not need to implement all methods from the Usermod base class!\n};\n\nbyte RgbRotaryEncoderUsermod::currentPos = 5;\n// strings to reduce flash memory usage (used more than twice)\nconst char RgbRotaryEncoderUsermod::_name[]              PROGMEM = \"RGB-Rotary-Encoder\";\nconst char RgbRotaryEncoderUsermod::_enabled[]           PROGMEM = \"Enabled\";\nconst char RgbRotaryEncoderUsermod::_ledIo[]             PROGMEM = \"LED-pin\";\nconst char RgbRotaryEncoderUsermod::_eaIo[]              PROGMEM = \"ea-pin\";\nconst char RgbRotaryEncoderUsermod::_ebIo[]              PROGMEM = \"eb-pin\";\nconst char RgbRotaryEncoderUsermod::_ledMode[]           PROGMEM = \"LED-Mode\";\nconst char RgbRotaryEncoderUsermod::_ledBrightness[]     PROGMEM = \"LED-Brightness\";\nconst char RgbRotaryEncoderUsermod::_stepsPerClick[]     PROGMEM = \"Steps-per-Click\";\nconst char RgbRotaryEncoderUsermod::_incrementPerClick[] PROGMEM = \"Increment-per-Click\";\n\nstatic RgbRotaryEncoderUsermod rgb_rotary_encoder;\nREGISTER_USERMOD(rgb_rotary_encoder);"
  },
  {
    "path": "usermods/rotary_encoder_change_effect/wled06_usermod.ino",
    "content": "//Use userVar0 and userVar1 (API calls &U0=,&U1=, uint16_t)\n\nlong lastTime = 0;\nint delayMs = 10;\nconst int pinA = D6; //data\nconst int pinB = D7; //clk\nint oldA = LOW;\n\n//gets called once at boot. Do all initialization that doesn't depend on network here\nvoid userSetup() {\n  pinMode(pinA, INPUT_PULLUP);\n  pinMode(pinB, INPUT_PULLUP);\n}\n\n//gets called every time WiFi is (re-)connected. Initialize own network interfaces here\nvoid userConnected() {\n}\n\n//loop. You can use \"if (WLED_CONNECTED)\" to check for successful connection\nvoid userLoop() {\n  if (millis()-lastTime > delayMs) {\n    int A = digitalRead(pinA);\n    int B = digitalRead(pinB);\n\n    if (oldA == LOW && A == HIGH) {\n      if (oldB == HIGH) {\n      // bri += 10;\n      // if (bri > 250) bri = 10;\n      effectCurrent += 1;\n      if (effectCurrent >= MODE_COUNT) effectCurrent = 0;\n    }\n    else {\n      // bri -= 10;\n      // if (bri < 10) bri = 250;\n      effectCurrent -= 1;\n      if (effectCurrent < 0) effectCurrent = (MODE_COUNT-1);\n    }\n    oldA = A;\n\n    //call for notifier -> 0: init 1: direct change 2: button 3: notification 4: nightlight 5: other (No notification)\n    // 6: fx changed 7: hue 8: preset cycle 9: blynk 10: alexa\n    colorUpdated(CALL_MODE_FX_CHANGED);\n    lastTime = millis();\n  }\n}\n"
  },
  {
    "path": "usermods/sd_card/library.json",
    "content": "{\n  \"name\": \"sd_card\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/sd_card/readme.md",
    "content": "# SD-card mod\n\n## Build\n- modify `platformio.ini` and add to the `build_flags` of your configuration the following\n- choose the way your SD is connected\n  1. via `-D WLED_USE_SD_MMC` when connected via MMC\n  2. via `-D WLED_USE_SD_SPI` when connected via SPI (use usermod page to setup SPI pins)\n\n### Test\n- enable `-D SD_PRINT_HOME_DIR` and `-D WLED_DEBUG`\n- this will print all files in `/` on boot via serial\n\n## Configuration\n### MMC\n- The MMC port / pins needs no configuration as they are specified by Espressif\n### SPI\n- The SPI port / pins can be modified via the WLED web-UI: `Config → Usermod → SD Card`\n  | option            | effect                                                                                           | default |\n  | ----------------- | ------------------------------------------------------------------------------------------------ | ------- |\n  | `pinSourceSelect` | GPIO that is connected to SD's `SS`(source select) / `CS`(chip select)                           | 16      |\n  | `pinSourceClock`  | GPIO that is connected to SD's `SCLK` (source clock) / `CLK`(clock)                              | 14      |\n  | `pinPoci`         | GPIO that is connected to SD's `POCI`<sup>☨</sup> (Peripheral-Out-Ctrl-In) / `MISO` (deprecated) | 36      |\n  | `pinPico`         | GPIO that is connected to SD's `PICO`<sup>☨</sup> (Peripheral-In-Ctrl-Out) / `MOSI` (deprecated) | 15      |\n  | `sdEnable`        | Enable to read data from the SD-card                                                             | true    |\n\n  <sup>☨</sup><sub>Following new naming convention of [OSHWA](https://www.oshwa.org/a-resolution-to-redefine-spi-signal-names/)</sub>\n\n## Usage in other mods\n- creates a macro `SD_ADAPTER` which is either mapped to `SD` or `SD_MMC` (see `SD_Test.ino` how to use SD / SD_MMC functions)\n\n-  checks if the specified file is available on the SD card\n   ```cpp\n   bool file_onSD(const char *filepath) {...}\n   ```\n"
  },
  {
    "path": "usermods/sd_card/sd_card.cpp",
    "content": "#include \"wled.h\"\n\n// SD connected via MMC / SPI\n#if defined(WLED_USE_SD_MMC)\n  #define USED_STORAGE_FILESYSTEMS \"SD MMC, LittleFS\"\n  #define SD_ADAPTER SD_MMC\n  #include \"SD_MMC.h\"\n// SD connected via SPI (adjustable via usermod config)\n#elif defined(WLED_USE_SD_SPI)\n  #define SD_ADAPTER SD\n  #define USED_STORAGE_FILESYSTEMS \"SD SPI, LittleFS\"\n  #include \"SD.h\"\n  #include \"SPI.h\"\n#endif\n\n#ifdef WLED_USE_SD_MMC\n#elif defined(WLED_USE_SD_SPI)\n  SPIClass spiPort = SPIClass(VSPI);\n#endif\n\nvoid listDir( const char * dirname, uint8_t levels);\n\nclass UsermodSdCard : public Usermod {\n  private:\n    bool sdInitDone = false;\n\n    #ifdef WLED_USE_SD_SPI\n      int8_t configPinSourceSelect = 16;\n      int8_t configPinSourceClock = 14;\n      int8_t configPinPoci = 36; // confusing names? Then have a look :)\n      int8_t configPinPico = 15; // https://www.oshwa.org/a-resolution-to-redefine-spi-signal-names/\n\n      //acquired and initialize the SPI port\n      void init_SD_SPI()\n      {\n        if(!configSdEnabled) return;\n        if(sdInitDone) return;\n\n        PinManagerPinType pins[5] = {\n        { configPinSourceSelect, true },\n        { configPinSourceClock, true },\n        { configPinPoci, false },\n        { configPinPico, true }\n        };\n\n        if (!PinManager::allocateMultiplePins(pins, 4, PinOwner::UM_SdCard)) {\n            DEBUG_PRINTF(\"[%s] SD (SPI) pin allocation failed!\\n\", _name);\n            sdInitDone = false;\n            return;\n        }\n\n        bool returnOfInitSD = false;\n\n        #if defined(WLED_USE_SD_SPI)\n          spiPort.begin(configPinSourceClock, configPinPoci, configPinPico, configPinSourceSelect);\n          returnOfInitSD = SD_ADAPTER.begin(configPinSourceSelect, spiPort);\n        #endif\n\n        if(!returnOfInitSD) {\n          DEBUG_PRINTF(\"[%s] SPI begin failed!\\n\", _name);\n          sdInitDone = false;\n          return;\n        }\n\n        sdInitDone = true;\n      }\n\n      //deinitialize the acquired SPI port\n      void deinit_SD_SPI()\n      {\n        if(!sdInitDone) return;\n\n        SD_ADAPTER.end();\n\n        DEBUG_PRINTF(\"[%s] deallocate pins!\\n\", _name);\n        PinManager::deallocatePin(configPinSourceSelect, PinOwner::UM_SdCard);\n        PinManager::deallocatePin(configPinSourceClock,  PinOwner::UM_SdCard);\n        PinManager::deallocatePin(configPinPoci,         PinOwner::UM_SdCard);\n        PinManager::deallocatePin(configPinPico,         PinOwner::UM_SdCard);\n\n        sdInitDone = false;\n      }\n\n      // some SPI pin was changed, while SPI was initialized, reinit to new port\n      void reinit_SD_SPI()\n      {\n          deinit_SD_SPI();\n          init_SD_SPI();\n      }\n    #endif\n\n    #ifdef WLED_USE_SD_MMC\n      void init_SD_MMC() {\n        if(sdInitDone) return;\n        bool returnOfInitSD = false;\n        returnOfInitSD = SD_ADAPTER.begin();\n        DEBUG_PRINTF(\"[%s] MMC begin\\n\", _name);\n\n        if(!returnOfInitSD) {\n          DEBUG_PRINTF(\"[%s] MMC begin failed!\\n\", _name);\n          sdInitDone = false;\n          return;\n        }\n\n        sdInitDone = true;\n      }\n    #endif\n\n  public:\n    static bool configSdEnabled;\n    static const char _name[];\n\n    void setup() {\n      DEBUG_PRINTF(\"[%s] usermod loaded \\n\", _name);\n      #if defined(WLED_USE_SD_SPI)\n        init_SD_SPI();\n      #elif defined(WLED_USE_SD_MMC)\n        init_SD_MMC();\n      #endif\n \n      #if defined(SD_ADAPTER) && defined(SD_PRINT_HOME_DIR)  \n        listDir(\"/\", 0);        \n      #endif\n    }\n\n    void loop(){\n      \n    }\n\n    uint16_t getId()\n    {\n      return USERMOD_ID_SD_CARD;\n    }\n\n    void addToConfig(JsonObject& root)\n    {\n      #ifdef WLED_USE_SD_SPI\n      JsonObject top = root.createNestedObject(FPSTR(_name));\n      top[\"pinSourceSelect\"] = configPinSourceSelect;\n      top[\"pinSourceClock\"] = configPinSourceClock;\n      top[\"pinPoci\"] = configPinPoci;\n      top[\"pinPico\"] = configPinPico;\n      top[\"sdEnabled\"] = configSdEnabled;\n      #endif\n    }\n\n    bool readFromConfig(JsonObject &root)\n    {\n      #ifdef WLED_USE_SD_SPI\n        JsonObject top = root[FPSTR(_name)];\n        if (top.isNull()) {\n          DEBUG_PRINTF(\"[%s] No config found. (Using defaults.)\\n\", _name);\n          return false;\n        }\n\n        uint8_t oldPinSourceSelect  = configPinSourceSelect;\n        uint8_t oldPinSourceClock = configPinSourceClock;\n        uint8_t oldPinPoci = configPinPoci;\n        uint8_t oldPinPico = configPinPico;\n        bool    oldSdEnabled = configSdEnabled;\n\n        getJsonValue(top[\"pinSourceSelect\"], configPinSourceSelect);\n        getJsonValue(top[\"pinSourceClock\"],  configPinSourceClock);\n        getJsonValue(top[\"pinPoci\"],         configPinPoci);\n        getJsonValue(top[\"pinPico\"],         configPinPico);\n        getJsonValue(top[\"sdEnabled\"],       configSdEnabled);\n\n        if(configSdEnabled != oldSdEnabled) {\n          configSdEnabled ? init_SD_SPI() : deinit_SD_SPI();\n          DEBUG_PRINTF(\"[%s] SD card %s\\n\", _name, configSdEnabled ? \"enabled\" : \"disabled\");\n        }\n\n        if( configSdEnabled && (\n            oldPinSourceSelect  != configPinSourceSelect ||\n            oldPinSourceClock   != configPinSourceClock  ||\n            oldPinPoci          != configPinPoci         ||\n            oldPinPico          != configPinPico)\n          )\n        {\n          DEBUG_PRINTF(\"[%s] Init SD card based of config\\n\", _name);\n          DEBUG_PRINTF(\"[%s] Config changes \\n - SS: %d -> %d\\n - MI: %d -> %d\\n - MO: %d -> %d\\n - En: %d -> %d\\n\", _name, oldPinSourceSelect, configPinSourceSelect, oldPinSourceClock, configPinSourceClock, oldPinPoci, configPinPoci, oldPinPico, configPinPico);\n          reinit_SD_SPI();\n        }\n      #endif\n\n      return true;\n    }\n};\n\nconst char UsermodSdCard::_name[] PROGMEM = \"SD Card\";\nbool UsermodSdCard::configSdEnabled = true;\n\n#ifdef SD_ADAPTER\n//checks if the file is available on SD card\nbool file_onSD(const char *filepath)\n{\n  #ifdef WLED_USE_SD_SPI\n    if(!UsermodSdCard::configSdEnabled) return false;\n  #endif\n\n  uint8_t cardType = SD_ADAPTER.cardType();\n  if(cardType == CARD_NONE) {\n    DEBUG_PRINTF(\"[%s] not attached / cardType none\\n\", UsermodSdCard::_name);\n    return false; // no SD card attached\n  }\n  if(cardType == CARD_MMC || cardType == CARD_SD || cardType == CARD_SDHC)\n  {\n    return SD_ADAPTER.exists(filepath);\n  }\n\n  return false; // unknown card type\n}\n\nvoid listDir( const char * dirname, uint8_t levels){\n    DEBUG_PRINTF(\"Listing directory: %s\\n\", dirname);\n\n    File root = SD_ADAPTER.open(dirname);\n    if(!root){\n        DEBUG_PRINTF(\"Failed to open directory\\n\");\n        return;\n    }\n    if(!root.isDirectory()){\n        DEBUG_PRINTF(\"Not a directory\\n\");\n        return;\n    }\n\n    File file = root.openNextFile();\n    while(file){\n        if(file.isDirectory()){\n            DEBUG_PRINTF(\"  DIR : %s\\n\",file.name());\n            if(levels){\n                listDir(file.name(), levels -1);\n            }\n        } else {\n            DEBUG_PRINTF(\"  FILE: %s  SIZE: %d\\n\",file.name(), file.size());\n        }\n        file = root.openNextFile();\n    }\n}\n\n#endif\n\nstatic UsermodSdCard sd_card;\nREGISTER_USERMOD(sd_card);"
  },
  {
    "path": "usermods/sensors_to_mqtt/library.json",
    "content": "{\n  \"name\": \"sensors_to_mqtt\",\n  \"build\": { \"libArchive\": false},\n  \"dependencies\": {\n    \"adafruit/Adafruit BMP280 Library\":\"2.6.8\",\n    \"adafruit/Adafruit CCS811 Library\":\"1.1.3\",\n    \"adafruit/Adafruit Si7021 Library\":\"1.5.3\",\n    \"adafruit/Adafruit Unified Sensor\":\"^1.1.15\"\n  }\n}\n"
  },
  {
    "path": "usermods/sensors_to_mqtt/readme.md",
    "content": "# Send sensor data To Home Assistant\n\nPublishes BMP280, CCS811 and Si7021 measurements to Home Assistant via MQTT.\n\nUses Home Assistant Automatic Device Discovery.\n\nThe use of Home Assistant is not mandatory. The mod will publish sensor values via MQTT just fine without it.\n\nUses the MQTT connection set in the WLED web user interface.\n\n## Maintainer\n\ntwitter.com/mpronk89\n\n## Features\n\n- Reads BMP280, CCS811 and Si7021 senors\n- Publishes via MQTT, configured via WLED webUI\n- Announces device in Home Assistant for easy setup\n- Efficient energy usage\n- Updates every 60 seconds\n\n## Example MQTT topics:\n\n`$mqttDeviceTopic` is set in webui of WLED!\n\n```\ntemperature: $mqttDeviceTopic/temperature\npressure: $mqttDeviceTopic/pressure\nhumidity: $mqttDeviceTopic/humidity\ntvoc: $mqttDeviceTopic/tvoc\neCO2:  $mqttDeviceTopic/eco2\nIAQ:  $mqttDeviceTopic/iaq\n```\n\n# Installation\n\n## Hardware\n\n### Requirements\n\n1. BMP280/CCS811/Si7021 sensor. E.g. https://aliexpress.com/item/32979998543.html\n2. A microcontroller that supports i2c. e.g. esp32\n\n### installation\n\nAttach the sensor to the i2c interface.\n\nDefault PINs esp32:\n\n```\nSCL_PIN = 22;\nSDA_PIN = 21;\n```\n\nDefault PINs ESP8266:\n\n```\nSCL_PIN = 5;\nSDA_PIN = 4;\n```\n\n# Credits\n\n- Aircoookie for making WLED\n- Other usermod creators for example code\n- Bouke_Regnerus for https://community.home-assistant.io/t/example-indoor-air-quality-text-sensor-using-ccs811-sensor/125854\n- You, for reading this\n"
  },
  {
    "path": "usermods/sensors_to_mqtt/sensors_to_mqtt.cpp",
    "content": "#include \"wled.h\"\n#include <Arduino.h>\n#include <Adafruit_Sensor.h>\n#include <Adafruit_BMP280.h>\n#include <Adafruit_CCS811.h>\n#include <Adafruit_Si7021.h>\n\n#ifdef WLED_DISABLE_MQTT\n#error \"This user mod requires MQTT to be enabled.\"\n#endif\n\nstatic Adafruit_BMP280 bmp;\nstatic Adafruit_Si7021 si7021;\nstatic Adafruit_CCS811 ccs811;\n\nclass UserMod_SensorsToMQTT : public Usermod\n{\nprivate:\n  bool initialized = false;\n  bool mqttInitialized = false;\n  float SensorPressure = 0;\n  float SensorTemperature = 0;\n  float SensorHumidity = 0;\n  const char *SensorIaq = \"Unknown\";\n  String mqttTemperatureTopic = \"\";\n  String mqttHumidityTopic = \"\";\n  String mqttPressureTopic = \"\";\n  String mqttTvocTopic = \"\";\n  String mqttEco2Topic = \"\";\n  String mqttIaqTopic = \"\";\n  unsigned int SensorTvoc = 0;\n  unsigned int SensorEco2 = 0;\n  unsigned long nextMeasure = 0;\n\n  void _initialize()\n  {\n    initialized = bmp.begin(BMP280_ADDRESS_ALT);\n    bmp.setSampling(Adafruit_BMP280::MODE_NORMAL,      /* Operating Mode. */\n                    Adafruit_BMP280::SAMPLING_X16,     /* Temp. oversampling */\n                    Adafruit_BMP280::SAMPLING_X16,     /* Pressure oversampling */\n                    Adafruit_BMP280::FILTER_X16,       /* Filtering. */\n                    Adafruit_BMP280::STANDBY_MS_2000); /* Refresh values every 20 seconds */\n\n    initialized &= si7021.begin();\n    initialized &= ccs811.begin();\n    ccs811.setDriveMode(CCS811_DRIVE_MODE_10SEC); /* Refresh values every 10s */\n    Serial.print(initialized);\n  }\n\n  void _mqttInitialize()\n  {\n    mqttTemperatureTopic = String(mqttDeviceTopic) + \"/temperature\";\n    mqttPressureTopic = String(mqttDeviceTopic) + \"/pressure\";\n    mqttHumidityTopic = String(mqttDeviceTopic) + \"/humidity\";\n    mqttTvocTopic = String(mqttDeviceTopic) + \"/tvoc\";\n    mqttEco2Topic = String(mqttDeviceTopic) + \"/eco2\";\n    mqttIaqTopic = String(mqttDeviceTopic) + \"/iaq\";\n\n    String t = String(\"homeassistant/sensor/\") + mqttClientID + \"/temperature/config\";\n\n    _createMqttSensor(\"temperature\", mqttTemperatureTopic, \"temperature\", \"°C\");\n    _createMqttSensor(\"pressure\", mqttPressureTopic, \"pressure\", \"hPa\");\n    _createMqttSensor(\"humidity\", mqttHumidityTopic, \"humidity\", \"%\");\n    _createMqttSensor(\"tvoc\", mqttTvocTopic, \"\", \"ppb\");\n    _createMqttSensor(\"eco2\", mqttEco2Topic, \"\", \"ppm\");\n    _createMqttSensor(\"iaq\", mqttIaqTopic, \"\", \"\");\n  }\n\n  void _createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement)\n  {\n    String t = String(\"homeassistant/sensor/\") + mqttClientID + \"/\" + name + \"/config\";\n\n    StaticJsonDocument<300> doc;\n\n    doc[\"name\"] = name;\n    doc[\"state_topic\"] = topic;\n    doc[\"unique_id\"] = String(mqttClientID) + name;\n    if (unitOfMeasurement != \"\")\n      doc[\"unit_of_measurement\"] = unitOfMeasurement;\n    if (deviceClass != \"\")\n      doc[\"device_class\"] = deviceClass;\n    doc[\"expire_after\"] = 1800;\n\n    JsonObject device = doc.createNestedObject(\"device\"); // attach the sensor to the same device\n    device[\"identifiers\"] = String(\"wled-sensor-\") + mqttClientID;\n    device[\"manufacturer\"] = F(WLED_BRAND);\n    device[\"model\"] = F(WLED_PRODUCT_NAME);\n    device[\"sw_version\"] = VERSION;\n    device[\"name\"] = mqttClientID;\n\n    String temp;\n    serializeJson(doc, temp);\n    Serial.println(t);\n    Serial.println(temp);\n\n    mqtt->publish(t.c_str(), 0, true, temp.c_str());\n  }\n\n  void _updateSensorData()\n  {\n    SensorTemperature = bmp.readTemperature();\n    SensorHumidity = si7021.readHumidity();\n    SensorPressure = (bmp.readPressure() / 100.0F);\n    ccs811.setEnvironmentalData(SensorHumidity, SensorTemperature);\n    ccs811.readData();\n    SensorTvoc = ccs811.getTVOC();\n    SensorEco2 = ccs811.geteCO2();\n    SensorIaq = _getIaqIndex(SensorHumidity, SensorTvoc, SensorEco2);\n\n    Serial.printf(\"%f c, %f humidity, %f hPA, %u tvoc, %u Eco2, %s iaq\\n\",\n                  SensorTemperature, SensorHumidity, SensorPressure,\n                  SensorTvoc, SensorEco2, SensorIaq);\n  }\n\n  /**\n   * Credits: Bouke_Regnerus @ https://community.home-assistant.io/t/example-indoor-air-quality-text-sensor-using-ccs811-sensor/125854 \n   */\n  const char *_getIaqIndex(float humidity, int tvoc, int eco2)\n  {\n    int iaq_index = 0;\n\n    /*\n       * Transform indoor humidity values to IAQ points according to Indoor Air Quality UK: \n       * http://www.iaquk.org.uk/\n       */\n    if (humidity < 10 or humidity > 90)\n    {\n      iaq_index += 1;\n    }\n    else if (humidity < 20 or humidity > 80)\n    {\n      iaq_index += 2;\n    }\n    else if (humidity < 30 or humidity > 70)\n    {\n      iaq_index += 3;\n    }\n    else if (humidity < 40 or humidity > 60)\n    {\n      iaq_index += 4;\n    }\n    else if (humidity >= 40 and humidity <= 60)\n    {\n      iaq_index += 5;\n    }\n\n    /*\n       * Transform eCO2 values to IAQ points according to Indoor Air Quality UK: \n       * http://www.iaquk.org.uk/\n       */\n    if (eco2 <= 600)\n    {\n      iaq_index += 5;\n    }\n    else if (eco2 <= 800)\n    {\n      iaq_index += 4;\n    }\n    else if (eco2 <= 1500)\n    {\n      iaq_index += 3;\n    }\n    else if (eco2 <= 1800)\n    {\n      iaq_index += 2;\n    }\n    else if (eco2 > 1800)\n    {\n      iaq_index += 1;\n    }\n\n    /*\n       * Transform TVOC values to IAQ points according to German environmental guidelines: \n       * https://www.repcomsrl.com/wp-content/uploads/2017/06/Environmental_Sensing_VOC_Product_Brochure_EN.pdf\n       */\n    if (tvoc <= 65)\n    {\n      iaq_index += 5;\n    }\n    else if (tvoc <= 220)\n    {\n      iaq_index += 4;\n    }\n    else if (tvoc <= 660)\n    {\n      iaq_index += 3;\n    }\n    else if (tvoc <= 2200)\n    {\n      iaq_index += 2;\n    }\n    else if (tvoc > 2200)\n    {\n      iaq_index += 1;\n    }\n\n    if (iaq_index <= 6)\n    {\n      return \"Unhealty\";\n    }\n    else if (iaq_index <= 9)\n    {\n      return \"Poor\";\n    }\n    else if (iaq_index <= 12)\n    {\n      return \"Moderate\";\n    }\n    else if (iaq_index <= 14)\n    {\n      return \"Good\";\n    }\n    else if (iaq_index > 14)\n    {\n      return \"Excellent\";\n    }\n    return \"Unknown\";\n  }\n\npublic:\n  void setup()\n  {\n    Serial.println(\"Starting!\");\n    Serial.println(\"Initializing sensors.. \");\n    _initialize();\n  }\n\n  // gets called every time WiFi is (re-)connected.\n  void connected()\n  {\n    nextMeasure = millis() + 5000; // Schedule next measure in 5 seconds\n  }\n\n  void loop()\n  {\n    unsigned long tempTimer = millis();\n\n    if (tempTimer > nextMeasure)\n    {\n      nextMeasure = tempTimer + 60000; // Schedule next measure in 60 seconds\n\n      if (!initialized)\n      {\n        Serial.println(\"Error! Sensors not initialized in loop()!\");\n        _initialize();\n        return; // lets try again next loop\n      }\n\n      if (mqtt != nullptr && mqtt->connected())\n      {\n        if (!mqttInitialized)\n        {\n          _mqttInitialize();\n          mqttInitialized = true;\n        }\n\n        // Update sensor data\n        _updateSensorData();\n\n        // Create string populated with user defined device topic from the UI,\n        // and the read temperature, humidity and pressure.\n        // Then publish to MQTT server.\n        mqtt->publish(mqttTemperatureTopic.c_str(), 0, true, String(SensorTemperature).c_str());\n        mqtt->publish(mqttPressureTopic.c_str(), 0, true, String(SensorPressure).c_str());\n        mqtt->publish(mqttHumidityTopic.c_str(), 0, true, String(SensorHumidity).c_str());\n        mqtt->publish(mqttTvocTopic.c_str(), 0, true, String(SensorTvoc).c_str());\n        mqtt->publish(mqttEco2Topic.c_str(), 0, true, String(SensorEco2).c_str());\n        mqtt->publish(mqttIaqTopic.c_str(), 0, true, String(SensorIaq).c_str());\n      }\n      else\n      {\n        Serial.println(\"Missing MQTT connection. Not publishing data\");\n        mqttInitialized = false;\n      }\n    }\n  }\n};\n\n\nstatic UserMod_SensorsToMQTT sensors_to_mqtt;\nREGISTER_USERMOD(sensors_to_mqtt);"
  },
  {
    "path": "usermods/seven_segment_display/library.json",
    "content": "{\n  \"name\": \"seven_segment_display\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/seven_segment_display/readme.md",
    "content": "# Seven Segment Display\n\nUses the overlay feature to create a configurable seven segment display.  \nThis has only been tested on a single configuration. Colon support has _not_ been tested. \n\n## Installation\n\nAdd the compile-time option `-D USERMOD_SEVEN_SEGMENT` to your `platformio.ini` (or `platformio_override.ini`) or use `#define USERMOD_SEVEN_SEGMENT` in `my_config.h`.\n\n## Settings\nSettings can be controlled via both the usermod setting page and through MQTT with a raw payload.\n##### Example\n Topic ```<mqttDeviceTopic||mqttGroupTopic>/sevenSeg/perSegment/set```  \n Payload ```3```\n#### perSegment -- ssLEDPerSegment\nThe number of individual LEDs per segment. 7 segments per digit.  \n#### perPeriod -- ssLEDPerPeriod\nThe number of individual LEDs per period. A ':' (colon) has two periods.\n#### startIdx -- ssStartLED\nIndex of the LED the display starts at. Enables a seven segment display to be in the middle of a string.\n#### timeEnable -- ssTimeEnabled\nWhen true, when displayMask is configured for a time output and no message is set, the time will be displayed.\n#### scrollSpd -- ssScrollSpeed\nTime, in milliseconds, between message shifts when the length of displayMsg exceeds the length of the displayMask.\n#### displayMask -- ssDisplayMask\nThis should represent the configuration of the physical display. \n<pre>\nHH - 0-23. hh - 1-12, kk - 1-24 hours  \nMM or mm - 0-59 minutes  \nSS or ss = 0-59 seconds  \n: for a colon  \nAll others for alpha numeric, (will be blank when displaying time)\n</pre>\n##### Example\n```HHMMSS ```  \n```hh:MM:SS ```\n#### displayMsg -- ssDisplayMessage\nMessage to be displayed. If the message length exceeds the length of displayMask, the message will scroll at scrollSpd. To 'remove' a message or revert  back to time, if timeEnabled is true, set the message to '~'.\n#### displayCfg -- ssDisplayConfig\nThe order your LEDs are configured in. All segments in the display need to be wired the same way.\n<pre>\n           -------\n         /   A   /          0 - EDCGFAB\n        / F     / B         1 - EDCBAFG\n       /       /            2 - GCDEFAB\n       -------              3 - GBAFEDC\n     /   G   /              4 - FABGEDC\n    / E     / C             5 - FABCDEG\n   /       /\n   -------\n      D\n</pre>\n\n## Version\n20211009 - Initial release\n"
  },
  {
    "path": "usermods/seven_segment_display/seven_segment_display.cpp",
    "content": "#include \"wled.h\"\n\n#ifdef WLED_DISABLE_MQTT\n#error \"This user mod requires MQTT to be enabled.\"\n#endif\n\nclass SevenSegmentDisplay : public Usermod\n{\n\n#define WLED_SS_BUFFLEN 6\n#define REFRESHTIME 497\nprivate:\n  //Runtime variables.\n  unsigned long lastRefresh = 0;\n  unsigned long lastCharacterStep = 0;\n  String ssDisplayBuffer = \"\";\n  char ssCharacterMask[36] = {0x77, 0x11, 0x6B, 0x3B, 0x1D, 0x3E, 0x7E, 0x13, 0x7F, 0x1F, 0x5F, 0x7C, 0x66, 0x79, 0x6E, 0x4E, 0x76, 0x5D, 0x44, 0x71, 0x5E, 0x64, 0x27, 0x58, 0x77, 0x4F, 0x1F, 0x48, 0x3E, 0x6C, 0x75, 0x25, 0x7D, 0x2A, 0x3D, 0x6B};\n  int ssDisplayMessageIdx = 0; //Position of the start of the message to be physically displayed.\n  bool ssDoDisplayTime = true;\n  int ssVirtualDisplayMessageIdxStart = 0;\n  int ssVirtualDisplayMessageIdxEnd = 0;\n  unsigned long resfreshTime = 497;\n\n  // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer)\n  int ssLEDPerSegment = 1; //The number of LEDs in each segment of the 7 seg (total per digit is 7 * ssLedPerSegment)\n  int ssLEDPerPeriod = 1;  //A Period will have 1x and a Colon will have 2x\n  int ssStartLED = 0;      //The pixel that the display starts at.\n  /*  HH - 0-23. hh - 1-12, kk - 1-24 hours\n    //  MM or mm - 0-59 minutes\n    //  SS or ss = 0-59 seconds\n    //  : for a colon\n    //  All others for alpha numeric, (will be blank when displaying time)\n    */\n  String ssDisplayMask = \"HHMMSS\"; //Physical Display Mask, this should reflect physical equipment.\n  /* ssDisplayConfig\n    //           -------\n    //         /   A   /          0 - EDCGFAB\n    //        / F     / B         1 - EDCBAFG\n    //       /       /            2 - GCDEFAB\n    //       -------              3 - GBAFEDC\n    //     /   G   /              4 - FABGEDC\n    //    / E     / C             5 - FABCDEG\n    //   /       /\n    //   -------\n    //      D\n    */\n  int ssDisplayConfig = 5; //Physical configuration of the Seven segment display\n  String ssDisplayMessage = \"~\";\n  bool ssTimeEnabled = true;         //If not, display message.\n  unsigned int ssScrollSpeed = 1000; //Time between advancement of extended message scrolling, in milliseconds.\n\n  //String to reduce flash memory usage\n  static const char _str_perSegment[];\n  static const char _str_perPeriod[];\n  static const char _str_startIdx[];\n  static const char _str_displayCfg[];\n  static const char _str_timeEnabled[];\n  static const char _str_scrollSpd[];\n  static const char _str_displayMask[];\n  static const char _str_displayMsg[];\n  static const char _str_sevenSeg[];\n  static const char _str_subFormat[];\n  static const char _str_topicFormat[];\n\n  unsigned long _overlaySevenSegmentProcess()\n  {\n    //Do time for now.\n    if (ssDoDisplayTime)\n    {\n      //Format the ssDisplayBuffer based on ssDisplayMask\n      int displayMaskLen = static_cast<int>(ssDisplayMask.length());\n      for (int index = 0; index < displayMaskLen; index++)\n      {\n        //Only look for time formatting if there are at least 2 characters left in the buffer.\n        if ((index < displayMaskLen - 1) && (ssDisplayMask[index] == ssDisplayMask[index + 1]))\n        {\n          int timeVar = 0;\n          switch (ssDisplayMask[index])\n          {\n          case 'h':\n            timeVar = hourFormat12(localTime);\n            break;\n          case 'H':\n            timeVar = hour(localTime);\n            break;\n          case 'k':\n            timeVar = hour(localTime) + 1;\n            break;\n          case 'M':\n          case 'm':\n            timeVar = minute(localTime);\n            break;\n          case 'S':\n          case 's':\n            timeVar = second(localTime);\n            break;\n          }\n\n          //Only want to leave a blank in the hour formatting.\n          if ((ssDisplayMask[index] == 'h' || ssDisplayMask[index] == 'H' || ssDisplayMask[index] == 'k') && timeVar < 10)\n            ssDisplayBuffer[index] = ' ';\n          else\n            ssDisplayBuffer[index] = 0x30 + (timeVar / 10);\n          ssDisplayBuffer[index + 1] = 0x30 + (timeVar % 10);\n\n          //Need to increment the index because of the second digit.\n          index++;\n        }\n        else\n        {\n          ssDisplayBuffer[index] = (ssDisplayMask[index] == ':' ? ':' : ' ');\n        }\n      }\n      return REFRESHTIME;\n    }\n    else\n    {\n      /* This will handle displaying a message and the scrolling of the message if its longer than the buffer length */\n\n      //Check to see if the message has scrolled completely\n      int len = static_cast<int>(ssDisplayMessage.length());\n      if (ssDisplayMessageIdx > len)\n      {\n        //If it has scrolled the whole message, reset it.\n        setSevenSegmentMessage(ssDisplayMessage);\n        return REFRESHTIME;\n      }\n      //Display message\n      int displayMaskLen = static_cast<int>(ssDisplayMask.length());\n      for (int index = 0; index < displayMaskLen; index++)\n      {\n        if (ssDisplayMessageIdx + index < len && ssDisplayMessageIdx + index >= 0)\n          ssDisplayBuffer[index] = ssDisplayMessage[ssDisplayMessageIdx + index];\n        else\n          ssDisplayBuffer[index] = ' ';\n      }\n\n      //Increase the displayed message index to progress it one character if the length exceeds the display length.\n      if (len > displayMaskLen)\n        ssDisplayMessageIdx++;\n\n      return ssScrollSpeed;\n    }\n  }\n\n  void _overlaySevenSegmentDraw()\n  {\n\n    //Start pixels at ssStartLED, Use ssLEDPerSegment, ssLEDPerPeriod, ssDisplayBuffer\n    int indexLED = ssStartLED;\n    int displayMaskLen = static_cast<int>(ssDisplayMask.length());\n    for (int indexBuffer = 0; indexBuffer < displayMaskLen; indexBuffer++)\n    {\n      if (ssDisplayBuffer[indexBuffer] == 0)\n        break;\n      else if (ssDisplayBuffer[indexBuffer] == '.')\n      {\n        //Won't ever turn off LED lights for a period. (or will we?)\n        indexLED += ssLEDPerPeriod;\n        continue;\n      }\n      else if (ssDisplayBuffer[indexBuffer] == ':')\n      {\n        //Turn off colon if odd second?\n        indexLED += ssLEDPerPeriod * 2;\n      }\n      else if (ssDisplayBuffer[indexBuffer] == ' ')\n      {\n        //Turn off all 7 segments.\n        _overlaySevenSegmentLEDOutput(0, indexLED);\n        indexLED += ssLEDPerSegment * 7;\n      }\n      else\n      {\n        //Turn off correct segments.\n        _overlaySevenSegmentLEDOutput(_overlaySevenSegmentGetCharMask(ssDisplayBuffer[indexBuffer]), indexLED);\n        indexLED += ssLEDPerSegment * 7;\n      }\n    }\n  }\n\n  void _overlaySevenSegmentLEDOutput(char mask, int indexLED)\n  {\n    for (char index = 0; index < 7; index++)\n    {\n      if ((mask & (0x40 >> index)) != (0x40 >> index))\n      {\n        for (int numPerSeg = 0; numPerSeg < ssLEDPerSegment; numPerSeg++)\n        {\n          strip.setPixelColor(indexLED + numPerSeg, 0x000000);\n        }\n      }\n      indexLED += ssLEDPerSegment;\n    }\n  }\n\n  char _overlaySevenSegmentGetCharMask(char var)\n  {\n    if (var >= 0x30 && var <= 0x39)\n    { /*If its a number, shift to index 0.*/\n      var -= 0x30;\n    }\n    else if (var >= 0x41 && var <= 0x5a)\n    { /*If its an Upper case, shift to index 0xA.*/\n      var -= 0x37;\n    }\n    else if (var >= 0x61 && var <= 0x7A)\n    { /*If its a lower case, shift to index 0xA.*/\n      var -= 0x57;\n    }\n    else\n    { /* Else unsupported, return 0; */\n      return 0;\n    }\n    char mask = ssCharacterMask[static_cast<int>(var)];\n    /*\n      0 - EDCGFAB\n      1 - EDCBAFG\n      2 - GCDEFAB\n      3 - GBAFEDC\n      4 - FABGEDC\n      5 - FABCDEG\n      */\n    switch (ssDisplayConfig)\n    {\n    case 1:\n      mask = _overlaySevenSegmentSwapBits(mask, 0, 3, 1);\n      mask = _overlaySevenSegmentSwapBits(mask, 1, 2, 1);\n      break;\n    case 2:\n      mask = _overlaySevenSegmentSwapBits(mask, 3, 6, 1);\n      mask = _overlaySevenSegmentSwapBits(mask, 4, 5, 1);\n      break;\n    case 3:\n      mask = _overlaySevenSegmentSwapBits(mask, 0, 4, 3);\n      mask = _overlaySevenSegmentSwapBits(mask, 3, 6, 1);\n      mask = _overlaySevenSegmentSwapBits(mask, 4, 5, 1);\n      break;\n    case 4:\n      mask = _overlaySevenSegmentSwapBits(mask, 0, 4, 3);\n      break;\n    case 5:\n      mask = _overlaySevenSegmentSwapBits(mask, 0, 4, 3);\n      mask = _overlaySevenSegmentSwapBits(mask, 0, 3, 1);\n      mask = _overlaySevenSegmentSwapBits(mask, 1, 2, 1);\n      break;\n    }\n    return mask;\n  }\n\n  char _overlaySevenSegmentSwapBits(char x, char p1, char p2, char n)\n  {\n    /* Move all bits of first set to rightmost side */\n    char set1 = (x >> p1) & ((1U << n) - 1);\n\n    /* Move all bits of second set to rightmost side */\n    char set2 = (x >> p2) & ((1U << n) - 1);\n\n    /* Xor the two sets */\n    char Xor = (set1 ^ set2);\n\n    /* Put the Xor bits back to their original positions */\n    Xor = (Xor << p1) | (Xor << p2);\n\n    /* Xor the 'Xor' with the original number so that the \n        two sets are swapped */\n    char result = x ^ Xor;\n\n    return result;\n  }\n\n  void _publishMQTTint_P(const char *subTopic, int value)\n  {\n    if(mqtt == NULL) return;\n      \n    char buffer[64];\n    char valBuffer[12];\n    sprintf_P(buffer, PSTR(\"%s/%S/%S\"), mqttDeviceTopic, _str_sevenSeg, subTopic);\n    sprintf_P(valBuffer, PSTR(\"%d\"), value);\n    mqtt->publish(buffer, 2, true, valBuffer);\n  }\n\n  void _publishMQTTstr_P(const char *subTopic, String Value)\n  {\n    if(mqtt == NULL) return;\n    char buffer[64];\n    sprintf_P(buffer, PSTR(\"%s/%S/%S\"), mqttDeviceTopic, _str_sevenSeg, subTopic);\n    mqtt->publish(buffer, 2, true, Value.c_str(), Value.length());\n  }\n\n  void _updateMQTT()\n  {\n    _publishMQTTint_P(_str_perSegment, ssLEDPerSegment);\n    _publishMQTTint_P(_str_perPeriod, ssLEDPerPeriod);\n    _publishMQTTint_P(_str_startIdx, ssStartLED);\n    _publishMQTTint_P(_str_displayCfg, ssDisplayConfig);\n    _publishMQTTint_P(_str_timeEnabled, ssTimeEnabled);\n    _publishMQTTint_P(_str_scrollSpd, ssScrollSpeed);\n\n    _publishMQTTstr_P(_str_displayMask, ssDisplayMask);\n    _publishMQTTstr_P(_str_displayMsg, ssDisplayMessage);\n  }\n\n  bool _cmpIntSetting_P(char *topic, char *payload, const char *setting, void *value)\n  {\n    if (strcmp_P(topic, setting) == 0)\n    {\n      *((int *)value) = strtol(payload, NULL, 10);\n      _publishMQTTint_P(setting, *((int *)value));\n      return true;\n    }\n    return false;\n  }\n\n  bool _handleSetting(char *topic, char *payload)\n  {\n    if (_cmpIntSetting_P(topic, payload, _str_perSegment, &ssLEDPerSegment))\n      return true;\n    if (_cmpIntSetting_P(topic, payload, _str_perPeriod, &ssLEDPerPeriod))\n      return true;\n    if (_cmpIntSetting_P(topic, payload, _str_startIdx, &ssStartLED))\n      return true;\n    if (_cmpIntSetting_P(topic, payload, _str_displayCfg, &ssDisplayConfig))\n      return true;\n    if (_cmpIntSetting_P(topic, payload, _str_timeEnabled, &ssTimeEnabled))\n      return true;\n    if (_cmpIntSetting_P(topic, payload, _str_scrollSpd, &ssScrollSpeed))\n      return true;\n    if (strcmp_P(topic, _str_displayMask) == 0)\n    {\n      ssDisplayMask = String(payload);\n      ssDisplayBuffer = ssDisplayMask;\n      _publishMQTTstr_P(_str_displayMask, ssDisplayMask);\n      return true;\n    }\n    if (strcmp_P(topic, _str_displayMsg) == 0)\n    {\n      setSevenSegmentMessage(String(payload));\n      return true;\n    }\n    return false;\n  }\n\npublic:\n  void setSevenSegmentMessage(String message)\n  {\n    //If the message isn't blank display it otherwise show time, if enabled.\n    if (message.length() < 1 || message == \"~\")\n      ssDoDisplayTime = ssTimeEnabled;\n    else\n      ssDoDisplayTime = false;\n\n    //Determine is the message is longer than the display, if it is configure it to scroll the message.\n    if (message.length() > ssDisplayMask.length())\n      ssDisplayMessageIdx = -ssDisplayMask.length();\n    else\n      ssDisplayMessageIdx = 0;\n\n    //If the message isn't the same, update runtime/mqtt (most calls will be resetting message scroll)\n    if (!ssDisplayMessage.equals(message))\n    {\n      _publishMQTTstr_P(_str_displayMsg, message);\n      ssDisplayMessage = message;\n    }\n  }\n  //Functions called by WLED\n\n  /*\n     * setup() is called once at boot. WiFi is not yet connected at this point.\n     * You can use it to initialize variables, sensors or similar.\n     */\n  void setup()\n  {\n    ssDisplayBuffer = ssDisplayMask;\n  }\n\n  /*\n     * loop() is called continuously. Here you can check for events, read sensors, etc.\n     */\n  void loop()\n  {\n    if (millis() - lastRefresh > resfreshTime)\n    {\n      //In theory overlaySevenSegmentProcess should return the amount of time until it changes next.\n      //So we should be okay to trigger the stripi on every process loop.\n      resfreshTime = _overlaySevenSegmentProcess();\n      lastRefresh = millis();\n      strip.trigger();\n    }\n  }\n\n  void handleOverlayDraw()\n  {\n    _overlaySevenSegmentDraw();\n  }\n\n  void onMqttConnect(bool sessionPresent)\n  {\n    char subBuffer[48];\n    if (mqttDeviceTopic[0] != 0)\n    {\n      _updateMQTT();\n      //subscribe for sevenseg messages on the device topic\n      sprintf_P(subBuffer, PSTR(\"%s/%S/+/set\"), mqttDeviceTopic, _str_sevenSeg);\n      mqtt->subscribe(subBuffer, 2);\n    }\n\n    if (mqttGroupTopic[0] != 0)\n    {\n      //subscribe for sevenseg messages on the group topic\n      sprintf_P(subBuffer, PSTR(\"%s/%S/+/set\"), mqttGroupTopic, _str_sevenSeg);\n      mqtt->subscribe(subBuffer, 2);\n    }\n  }\n\n  bool onMqttMessage(char *topic, char *payload)\n  {\n    //If topic beings with sevenSeg cut it off, otherwise not our message.\n    size_t topicPrefixLen = strlen_P(PSTR(\"/sevenSeg/\"));\n    if (strncmp_P(topic, PSTR(\"/sevenSeg/\"), topicPrefixLen) == 0)\n      topic += topicPrefixLen;\n    else\n      return false;\n    //We only care if the topic ends with /set\n    size_t topicLen = strlen(topic);\n    if (topicLen > 4 &&\n        topic[topicLen - 4] == '/' &&\n        topic[topicLen - 3] == 's' &&\n        topic[topicLen - 2] == 'e' &&\n        topic[topicLen - 1] == 't')\n    {\n      //Trim /set and handle it\n      topic[topicLen - 4] = '\\0';\n      _handleSetting(topic, payload);\n    }\n    return true;\n  }\n\n  void addToConfig(JsonObject &root)\n  {\n    JsonObject top = root[FPSTR(_str_sevenSeg)];\n    if (top.isNull())\n    {\n      top = root.createNestedObject(FPSTR(_str_sevenSeg));\n    }\n    top[FPSTR(_str_perSegment)] = ssLEDPerSegment;\n    top[FPSTR(_str_perPeriod)] = ssLEDPerPeriod;\n    top[FPSTR(_str_startIdx)] = ssStartLED;\n    top[FPSTR(_str_displayMask)] = ssDisplayMask;\n    top[FPSTR(_str_displayCfg)] = ssDisplayConfig;\n    top[FPSTR(_str_displayMsg)] = ssDisplayMessage;\n    top[FPSTR(_str_timeEnabled)] = ssTimeEnabled;\n    top[FPSTR(_str_scrollSpd)] = ssScrollSpeed;\n  }\n\n  bool readFromConfig(JsonObject &root)\n  {\n    JsonObject top = root[FPSTR(_str_sevenSeg)];\n\n    bool configComplete = !top.isNull();\n\n    //if sevenseg section doesn't exist return\n    if (!configComplete)\n      return configComplete;\n\n    configComplete &= getJsonValue(top[FPSTR(_str_perSegment)], ssLEDPerSegment);\n    configComplete &= getJsonValue(top[FPSTR(_str_perPeriod)], ssLEDPerPeriod);\n    configComplete &= getJsonValue(top[FPSTR(_str_startIdx)], ssStartLED);\n    configComplete &= getJsonValue(top[FPSTR(_str_displayMask)], ssDisplayMask);\n    configComplete &= getJsonValue(top[FPSTR(_str_displayCfg)], ssDisplayConfig);\n\n    String newDisplayMessage;\n    configComplete &= getJsonValue(top[FPSTR(_str_displayMsg)], newDisplayMessage);\n    setSevenSegmentMessage(newDisplayMessage);\n\n    configComplete &= getJsonValue(top[FPSTR(_str_timeEnabled)], ssTimeEnabled);\n    configComplete &= getJsonValue(top[FPSTR(_str_scrollSpd)], ssScrollSpeed);\n    return configComplete;\n  }\n\n  /*\n     * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).\n     * This could be used in the future for the system to determine whether your usermod is installed.\n     */\n  uint16_t getId()\n  {\n    return USERMOD_ID_SEVEN_SEGMENT_DISPLAY;\n  }\n};\n\nconst char SevenSegmentDisplay::_str_perSegment[] PROGMEM = \"perSegment\";\nconst char SevenSegmentDisplay::_str_perPeriod[] PROGMEM = \"perPeriod\";\nconst char SevenSegmentDisplay::_str_startIdx[] PROGMEM = \"startIdx\";\nconst char SevenSegmentDisplay::_str_displayCfg[] PROGMEM = \"displayCfg\";\nconst char SevenSegmentDisplay::_str_timeEnabled[] PROGMEM = \"timeEnabled\";\nconst char SevenSegmentDisplay::_str_scrollSpd[] PROGMEM = \"scrollSpd\";\nconst char SevenSegmentDisplay::_str_displayMask[] PROGMEM = \"displayMask\";\nconst char SevenSegmentDisplay::_str_displayMsg[] PROGMEM = \"displayMsg\";\nconst char SevenSegmentDisplay::_str_sevenSeg[] PROGMEM = \"sevenSeg\";\n\nstatic SevenSegmentDisplay seven_segment_display;\nREGISTER_USERMOD(seven_segment_display);"
  },
  {
    "path": "usermods/seven_segment_display_reloaded/library.json",
    "content": "{\n  \"name\": \"seven_segment_display_reloaded\",\n  \"build\": {\n    \"libArchive\": false,\n    \"extraScript\": \"setup_deps.py\"\n  }\n}"
  },
  {
    "path": "usermods/seven_segment_display_reloaded/readme.md",
    "content": "# Seven Segment Display Reloaded\n\nUses the overlay feature to create a configurable seven segment display.\nOptimized for maximum configurability and use with seven segment clocks by parallyze (https://www.instructables.com/member/parallyze/instructables/)\nVery loosely based on the existing usermod \"seven segment display\".\n\n\n## Installation\n\nAdd the compile-time option `-D USERMOD_SSDR` to your `platformio.ini` (or `platformio_override.ini`) or use `#define USERMOD_SSDR` in `my_config.h`.\n\nFor 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.\n\n## Settings\nAll settings can be controlled via the usermod settings page.\nPart of the settings can be controlled through MQTT with a raw payload or through a json request to /json/state.\n\n### enabled\nEnables/disables this usermod\n\n### inverted\nEnables the inverted mode in which the background should be enabled and the digits should be black (LEDs off)\n\n### Colon-blinking\nEnables the blinking colon(s) if they are defined\n\n### Leading-Zero\nShows the leading zero of the hour if it exists (i.e. shows `07` instead of `7`)\n\n### enable-auto-brightness\nEnables the auto brightness feature. Can be used only when the usermods SN_Photoresistor or BH1750_V2 are installed.\n\n### auto-brightness-min / auto-brightness-max\nThe lux value calculated from usermod SN_Photoresistor or BH1750_V2 will be mapped to the values defined here.\nThe mapping, 0 - 1000 lux, will be mapped to auto-brightness-min and auto-brightness-max\n\nWLED current protection will override the calculated value if it is too high.\n\n### Display-Mask\nDefines the type of the time/date display. \nFor example \"H:m\" (default)\n- H - 00-23 hours\n- h - 01-12 hours\n- k - 01-24 hours\n- m - 00-59 minutes\n- s - 00-59 seconds\n- d - 01-31 day of month\n- M - 01-12 month\n- y - 21 last two positions of year\n- Y - 2021 year\n- : for a colon\n\n### LED-Numbers\n- LED-Numbers-Hours\n- LED-Numbers-Minutes\n- LED-Numbers-Seconds\n- LED-Numbers-Colons\n- LED-Numbers-Day\n- LED-Numbers-Month\n- LED-Numbers-Year\n\nSee following example for usage.\n\n\n## Example\n\nExample of an LED definition:\n```\n  <  A  >\n/\\       /\\\nF        B\n\\/       \\/\n  <  G  >\n/\\       /\\\nE        C\n\\/       \\/\n  <  D  >\n```\n\nLEDs or Range of LEDs are separated by a comma \",\"\n\nSegments are separated by a semicolon \";\" and are read as A;B;C;D;E;F;G\n\nDigits are separated by colon \":\" -> A;B;C;D;E;F;G:A;B;C;D;E;F;G\n\nRanges are defined as lower to higher (lower first)\n\nFor example, a clock definition for the following clock (https://www.instructables.com/Lazy-7-Quick-Build-Edition/) is\n\n- hour \"59,46;47-48;50-51;52-53;54-55;57-58;49,56:0,13;1-2;4-5;6-7;8-9;11-12;3,10\"\n\n- minute \"37-38;39-40;42-43;44,31;32-33;35-36;34,41:21-22;23-24;26-27;28,15;16-17;19-20;18,25\"\n\nor\n\n- hour \"6,7;8,9;11,12;13,0;1,2;4,5;3,10:52,53;54,55;57,58;59,46;47,48;50,51;49,56\"\n\n- minute \"15,28;16,17;19,20;21,22;23,24;26,27;18,25:31,44;32,33;35,36;37,38;39,40;42,43;34,41\"\n\ndepending on the orientation.\n\n# Example details:\nhour \"59,46;47-48;50-51;52-53;54-55;57-58;49,56:0,13;1-2;4-5;6-7;8-9;11-12;3,10\"\n\nthere are two digits separated by \":\"\n\n- 59,46;47-48;50-51;52-53;54-55;57-58;49,56\n- 0,13;1-2;4-5;6-7;8-9;11-12;3,10\n\nIn the first digit, \nthe **segment A** consists of the LEDs number **59 and 46**., **segment B** consists of the LEDs number **47, 48** and so on\n\nThe second digit starts again with **segment A** and LEDs **0 and 13**, **segment B** consists of the LEDs number **1 and 2** and so on\n\n### first digit of the hour\n- Segment A: 59, 46\n- Segment B: 47, 48\n- Segment C: 50, 51\n- Segment D: 52, 53\n- Segment E: 54, 55\n- Segment F: 57, 58\n- Segment G: 49, 56\n\n### second digit of the hour\n\n- Segment A: 0, 13\n- Segment B: 1, 2\n- Segment C: 4, 5\n- Segment D: 6, 7\n- Segment E: 8, 9\n- Segment F: 11, 12\n- Segment G: 3, 10\n"
  },
  {
    "path": "usermods/seven_segment_display_reloaded/setup_deps.py",
    "content": "from platformio.package.meta import PackageSpec\nImport('env')\n\n\nlibs = [PackageSpec(lib).name for lib in env.GetProjectOption(\"lib_deps\",[])]\n# Check for partner usermods\nif \"SN_Photoresistor\" in libs:\n    env.Append(CPPDEFINES=[(\"USERMOD_SN_PHOTORESISTOR\")])\nif any(mod in (\"BH1750_v2\", \"BH1750\") for mod in libs):    \n    env.Append(CPPDEFINES=[(\"USERMOD_BH1750\")])\n"
  },
  {
    "path": "usermods/seven_segment_display_reloaded/seven_segment_display_reloaded.cpp",
    "content": "#include \"wled.h\"\n#ifdef USERMOD_SN_PHOTORESISTOR\n  #include \"SN_Photoresistor.h\"\n#endif\n#ifdef USERMOD_BH1750\n  #include \"BH1750_v2.h\"\n#endif\n\n#ifdef WLED_DISABLE_MQTT\n#error \"This user mod requires MQTT to be enabled.\"\n#endif\n\nclass UsermodSSDR : public Usermod {\n\n//#define REFRESHTIME 497\n\nprivate:\n  //Runtime variables.\n  unsigned long umSSDRLastRefresh = 0;\n  unsigned long umSSDRResfreshTime = 3000;\n  bool umSSDRDisplayTime = false;\n  bool umSSDRInverted = false;\n  bool umSSDRColonblink = true;\n  bool umSSDRLeadingZero = false;\n  bool umSSDREnableLDR = false;\n  String umSSDRHours = \"\";\n  String umSSDRMinutes = \"\";\n  String umSSDRSeconds = \"\";\n  String umSSDRColons = \"\";\n  String umSSDRDays = \"\";\n  String umSSDRMonths = \"\";\n  String umSSDRYears = \"\";\n  uint16_t umSSDRLength = 0;\n  uint16_t umSSDRBrightnessMin = 0;\n  uint16_t umSSDRBrightnessMax = 255;\n\n  bool* umSSDRMask = 0;\n\n  /*//  H - 00-23 hours\n    //  h - 01-12 hours\n    //  k - 01-24 hours\n    //  m - 00-59 minutes\n    //  s - 00-59 seconds\n    //  d - 01-31 day of month\n    //  M - 01-12 month\n    //  y - 21 last two positions of year\n    //  Y - 2021 year\n    //  : for a colon\n    */\n  String umSSDRDisplayMask = \"H:m\"; //This should reflect physical equipment.\n\n  /* Segment order, seen from the front:\n\n      <  A  >\n    /\\       /\\\n    F        B\n    \\/       \\/\n      <  G  >\n    /\\       /\\\n    E        C\n    \\/       \\/\n      <  D  >\n\n  */\n\n  uint8_t umSSDRNumbers[11][7] = {\n    //  A    B    C    D    E    F    G\n    {   1,   1,   1,   1,   1,   1,   0 },  // 0\n    {   0,   1,   1,   0,   0,   0,   0 },  // 1\n    {   1,   1,   0,   1,   1,   0,   1 },  // 2\n    {   1,   1,   1,   1,   0,   0,   1 },  // 3\n    {   0,   1,   1,   0,   0,   1,   1 },  // 4\n    {   1,   0,   1,   1,   0,   1,   1 },  // 5\n    {   1,   0,   1,   1,   1,   1,   1 },  // 6\n    {   1,   1,   1,   0,   0,   0,   0 },  // 7\n    {   1,   1,   1,   1,   1,   1,   1 },  // 8\n    {   1,   1,   1,   1,   0,   1,   1 },  // 9\n    {   0,   0,   0,   0,   0,   0,   0 }   // blank\n  };\n\n  //String to reduce flash memory usage\n  static const char _str_name[];\n  static const char _str_ldrEnabled[];\n  static const char _str_timeEnabled[];\n  static const char _str_inverted[];\n  static const char _str_colonblink[];\n  static const char _str_leadingZero[];\n  static const char _str_displayMask[];\n  static const char _str_hours[];\n  static const char _str_minutes[];\n  static const char _str_seconds[];\n  static const char _str_colons[];\n  static const char _str_days[];\n  static const char _str_months[];\n  static const char _str_years[];\n  static const char _str_minBrightness[];\n  static const char _str_maxBrightness[];\n\n#ifdef USERMOD_SN_PHOTORESISTOR\n  Usermod_SN_Photoresistor *ptr;\n#else\n  void* ptr = nullptr;\n#endif\n#ifdef USERMOD_BH1750\n  Usermod_BH1750* bh1750 = nullptr;\n#else\n  void* bh1750 = nullptr;\n#endif\n\n  void _overlaySevenSegmentDraw() {\n    int displayMaskLen = static_cast<int>(umSSDRDisplayMask.length());\n    bool colonsDone = false;\n    _setAllFalse();\n    for (int index = 0; index < displayMaskLen; index++) {\n      int timeVar = 0;\n      switch (umSSDRDisplayMask[index]) {\n        case 'h':\n          timeVar = hourFormat12(localTime);\n          _showElements(&umSSDRHours, timeVar, 0, !umSSDRLeadingZero);\n          break;\n        case 'H':\n          timeVar = hour(localTime);\n          _showElements(&umSSDRHours, timeVar, 0, !umSSDRLeadingZero);\n          break;\n        case 'k':\n          timeVar = hour(localTime) + 1;\n          _showElements(&umSSDRHours, timeVar, 0, !umSSDRLeadingZero);\n          break;\n        case 'm':\n          timeVar = minute(localTime);\n          _showElements(&umSSDRMinutes, timeVar, 0, 0);\n          break;\n        case 's':\n          timeVar = second(localTime);\n          _showElements(&umSSDRSeconds, timeVar, 0, 0);\n          break;\n        case 'd':\n          timeVar = day(localTime);\n          _showElements(&umSSDRDays, timeVar, 0, 0);\n          break;\n        case 'M':\n          timeVar = month(localTime);\n          _showElements(&umSSDRMonths, timeVar, 0, 0);\n          break;\n        case 'y':\n          timeVar = second(localTime);\n          _showElements(&umSSDRYears, timeVar, 0, 0);\n          break;\n        case 'Y':\n          timeVar = year(localTime);\n          _showElements(&umSSDRYears, timeVar, 0, 0);\n          break;\n        case ':':\n          if (!colonsDone) { // only call _setColons once as all colons are printed when the first colon is found\n            _setColons();\n            colonsDone = true;\n          }\n          break;\n      }\n    }\n    _setMaskToLeds();\n  }\n\n  void _setColons() {\n    if ( umSSDRColonblink ) {\n      if ( second(localTime) % 2 == 0 ) {\n        _showElements(&umSSDRColons, 0, 1, 0);\n      }\n    } else {\n      _showElements(&umSSDRColons, 0, 1, 0);\n    }\n  }\n\n  void _showElements(String *map, int timevar, bool isColon, bool removeZero\n\n) {\n    if ((map != nullptr) && (*map != nullptr) && !(*map).equals(\"\")) {\n      int length = String(timevar).length();\n      bool addZero = false;\n      if (length == 1) {\n        length = 2;\n        addZero = true;\n      }\n      int timeArr[length];\n      if(addZero) {\n        if(removeZero)\n          {\n            timeArr[1] = 10;\n            timeArr[0] = timevar;\n          }\n        else\n        {\n          timeArr[1] = 0;\n          timeArr[0] = timevar;\n        }\n      } else {\n        int count = 0;\n        while (timevar) {\n          timeArr[count] = timevar%10;\n          timevar /= 10;\n          count++;\n        };\n      }\n\n\n      int colonsLen = static_cast<int>((*map).length());\n      int count = 0;\n      int countSegments = 0;\n      int countDigit = 0;\n      bool range = false;\n      int lastSeenLedNr = 0;\n\n      for (int index = 0; index < colonsLen; index++) {\n        switch ((*map)[index]) {\n          case '-':\n            lastSeenLedNr = _checkForNumber(count, index, map);\n            count = 0;\n            range = true;\n            break;\n          case ':':\n            _setLeds(_checkForNumber(count, index, map), lastSeenLedNr, range, countSegments, timeArr[countDigit], isColon);\n            count = 0;\n            range = false;\n            countDigit++;\n            countSegments = 0;\n            break;\n          case ';':\n            _setLeds(_checkForNumber(count, index, map), lastSeenLedNr, range, countSegments, timeArr[countDigit], isColon);\n            count = 0;\n            range = false;\n            countSegments++;\n            break;\n          case ',':\n            _setLeds(_checkForNumber(count, index, map), lastSeenLedNr, range, countSegments, timeArr[countDigit], isColon);\n            count = 0;\n            range = false;\n            break;\n          default:\n            count++;\n            break;\n        }\n      }\n      _setLeds(_checkForNumber(count, colonsLen, map), lastSeenLedNr, range, countSegments, timeArr[countDigit], isColon);\n    }\n  }\n\n  void _setLeds(int lednr, int lastSeenLedNr, bool range, int countSegments, int number, bool colon) {\n    if ((lednr < 0) || (lednr >= umSSDRLength)) return;                                   // prevent array bounds violation\n\n    if (!(colon && umSSDRColonblink) && ((number < 0) || (countSegments < 0))) return;\n    if ((colon && umSSDRColonblink) || umSSDRNumbers[number][countSegments]) {\n      \n      if (range) {\n        for(int i = max(0, lastSeenLedNr); i <= lednr; i++) {\n          umSSDRMask[i] = true;\n        }\n      } else {\n        umSSDRMask[lednr] = true;\n      }\n    }\n  }\n\n  void _setMaskToLeds() {\n    for(int i = 0; i <= umSSDRLength; i++) {\n      if ((!umSSDRInverted && !umSSDRMask[i]) || (umSSDRInverted && umSSDRMask[i])) {\n        strip.setPixelColor(i, 0x000000);\n      }\n    }\n  }\n\n  void _setAllFalse() {\n    for(int i = 0; i <= umSSDRLength; i++) {\n      umSSDRMask[i] = false;\n    }\n  }\n\n  int _checkForNumber(int count, int index, String *map) {\n    String number = (*map).substring(index - count, index);\n    return number.toInt();\n  }\n\n  void _publishMQTTint_P(const char *subTopic, int value)\n  {\n    if(mqtt == NULL) return;\n      \n    char buffer[64];\n    char valBuffer[12];\n    sprintf_P(buffer, PSTR(\"%s/%S/%S\"), mqttDeviceTopic, _str_name, subTopic);\n    sprintf_P(valBuffer, PSTR(\"%d\"), value);\n    mqtt->publish(buffer, 2, true, valBuffer);\n  }\n\n  void _publishMQTTstr_P(const char *subTopic, String Value)\n  {\n    if(mqtt == NULL) return;\n    char buffer[64];\n    sprintf_P(buffer, PSTR(\"%s/%S/%S\"), mqttDeviceTopic, _str_name, subTopic);\n    mqtt->publish(buffer, 2, true, Value.c_str(), Value.length());\n  }\n\n  bool _cmpIntSetting_P(char *topic, char *payload, const char *setting, void *value)\n  {\n    if (strcmp_P(topic, setting) == 0)\n    {\n      *((int *)value) = strtol(payload, NULL, 10);\n      _publishMQTTint_P(setting, *((int *)value));\n      return true;\n    }\n    return false;\n  }\n\n  bool _handleSetting(char *topic, char *payload) {\n    if (_cmpIntSetting_P(topic, payload, _str_timeEnabled, &umSSDRDisplayTime)) {\n      return true;\n    }\n    if (_cmpIntSetting_P(topic, payload, _str_ldrEnabled, &umSSDREnableLDR)) {\n      return true;\n    }\n    if (_cmpIntSetting_P(topic, payload, _str_inverted, &umSSDRInverted)) {\n      return true;\n    }\n    if (_cmpIntSetting_P(topic, payload, _str_colonblink, &umSSDRColonblink)) {\n      return true;\n    }\n    if (_cmpIntSetting_P(topic, payload, _str_leadingZero, &umSSDRLeadingZero)) {\n      return true;\n    }\n    if (strcmp_P(topic, _str_displayMask) == 0) {\n      umSSDRDisplayMask = String(payload);\n      _publishMQTTstr_P(_str_displayMask, umSSDRDisplayMask);\n      return true;\n    }\n    return false;\n  }\n\n  void _updateMQTT()\n  {\n    _publishMQTTint_P(_str_timeEnabled, umSSDRDisplayTime);\n    _publishMQTTint_P(_str_ldrEnabled, umSSDREnableLDR);\n    _publishMQTTint_P(_str_inverted, umSSDRInverted);\n    _publishMQTTint_P(_str_colonblink, umSSDRColonblink);\n    _publishMQTTint_P(_str_leadingZero, umSSDRLeadingZero);\n\n    _publishMQTTstr_P(_str_hours, umSSDRHours);\n    _publishMQTTstr_P(_str_minutes, umSSDRMinutes);\n    _publishMQTTstr_P(_str_seconds, umSSDRSeconds);\n    _publishMQTTstr_P(_str_colons, umSSDRColons);\n    _publishMQTTstr_P(_str_days, umSSDRDays);\n    _publishMQTTstr_P(_str_months, umSSDRMonths);\n    _publishMQTTstr_P(_str_years, umSSDRYears);\n    _publishMQTTstr_P(_str_displayMask, umSSDRDisplayMask);\n\n    _publishMQTTint_P(_str_minBrightness, umSSDRBrightnessMin);\n    _publishMQTTint_P(_str_maxBrightness, umSSDRBrightnessMax);\n  }\n\n  void _addJSONObject(JsonObject& root) {\n    JsonObject ssdrObj = root[FPSTR(_str_name)];\n    if (ssdrObj.isNull()) {\n      ssdrObj = root.createNestedObject(FPSTR(_str_name));\n    }\n\n    ssdrObj[FPSTR(_str_timeEnabled)] = umSSDRDisplayTime;\n    ssdrObj[FPSTR(_str_ldrEnabled)] = umSSDREnableLDR;\n    ssdrObj[FPSTR(_str_inverted)] = umSSDRInverted;\n    ssdrObj[FPSTR(_str_colonblink)] = umSSDRColonblink;\n    ssdrObj[FPSTR(_str_leadingZero)] = umSSDRLeadingZero;\n    ssdrObj[FPSTR(_str_displayMask)] = umSSDRDisplayMask;\n    ssdrObj[FPSTR(_str_hours)] = umSSDRHours;\n    ssdrObj[FPSTR(_str_minutes)] = umSSDRMinutes;\n    ssdrObj[FPSTR(_str_seconds)] = umSSDRSeconds;\n    ssdrObj[FPSTR(_str_colons)] = umSSDRColons;\n    ssdrObj[FPSTR(_str_days)] = umSSDRDays;\n    ssdrObj[FPSTR(_str_months)] = umSSDRMonths;\n    ssdrObj[FPSTR(_str_years)] = umSSDRYears;\n    ssdrObj[FPSTR(_str_minBrightness)] = umSSDRBrightnessMin;\n    ssdrObj[FPSTR(_str_maxBrightness)] = umSSDRBrightnessMax;\n  }\n\npublic:\n  //Functions called by WLED\n\n  /*\n     * setup() is called once at boot. WiFi is not yet connected at this point.\n     * You can use it to initialize variables, sensors or similar.\n     */\n  void setup() {\n    umSSDRLength = strip.getLengthTotal();\n    if (umSSDRMask != 0) {\n      umSSDRMask = (bool*) realloc(umSSDRMask, umSSDRLength * sizeof(bool));\n    } else {\n      umSSDRMask = (bool*) malloc(umSSDRLength * sizeof(bool));\n    }\n    _setAllFalse();\n\n    #ifdef USERMOD_SN_PHOTORESISTOR\n      ptr = (Usermod_SN_Photoresistor*) UsermodManager::lookup(USERMOD_ID_SN_PHOTORESISTOR);\n    #endif\n    #ifdef USERMOD_BH1750\n      bh1750 = (Usermod_BH1750*) UsermodManager::lookup(USERMOD_ID_BH1750);\n    #endif\n    DEBUG_PRINTLN(F(\"Setup done\"));\n  }\n\n  /*\n     * loop() is called continuously. Here you can check for events, read sensors, etc.\n     */\n  void loop() {\n    if (!umSSDRDisplayTime || strip.isUpdating()) {\n      return;\n    }\n    #ifdef USERMOD_SN_PHOTORESISTOR\n      if(bri != 0 && umSSDREnableLDR && (millis() - umSSDRLastRefresh > umSSDRResfreshTime)) {\n        if (ptr != nullptr) {\n          uint16_t lux = ptr->getLastLDRValue();\n          uint16_t brightness = map(lux, 0, 1000, umSSDRBrightnessMin, umSSDRBrightnessMax);\n          if (bri != brightness) {\n            bri = brightness;\n            stateUpdated(1);\n          }\n        }\n        umSSDRLastRefresh = millis();\n      }\n    #endif\n    #ifdef USERMOD_BH1750\n      if(bri != 0 && umSSDREnableLDR && (millis() - umSSDRLastRefresh > umSSDRResfreshTime)) {\n        if (bh1750 != nullptr) {\n          float lux = bh1750->getIlluminance();\n          uint16_t brightness = map(lux, 0, 1000, umSSDRBrightnessMin, umSSDRBrightnessMax);\n          if (bri != brightness) {\n            DEBUG_PRINTF(\"Adjusting brightness based on lux value: %.2f lx, new brightness: %d\\n\", lux, brightness);\n            bri = brightness;\n            stateUpdated(1);\n          }\n        }\n        umSSDRLastRefresh = millis();\n      }\n    #endif\n  }\n\n  void handleOverlayDraw() {\n    if (umSSDRDisplayTime) {\n      _overlaySevenSegmentDraw();\n    }\n  }\n\n/*\n  * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.\n  * Creating an \"u\" object allows you to add custom key/value pairs to the Info section of the WLED web UI.\n  * Below it is shown how this could be used for e.g. a light sensor\n  */\n  void addToJsonInfo(JsonObject& root) {\n    JsonObject user = root[F(\"u\")];\n    if (user.isNull()) {\n      user = root.createNestedObject(F(\"u\"));\n    }\n    JsonArray enabled = user.createNestedArray(\"Time enabled\");\n    enabled.add(umSSDRDisplayTime);\n    JsonArray invert = user.createNestedArray(\"Time inverted\");\n    invert.add(umSSDRInverted);\n    JsonArray blink = user.createNestedArray(\"Blinking colon\");\n    blink.add(umSSDRColonblink);\n    JsonArray zero = user.createNestedArray(\"Show the hour leading zero\");\n    zero.add(umSSDRLeadingZero);\n    JsonArray ldrEnable = user.createNestedArray(\"Auto Brightness enabled\");\n    ldrEnable.add(umSSDREnableLDR);\n\n  }\n\n /*\n  * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).\n  * Values in the state object may be modified by connected clients\n  */\n  void addToJsonState(JsonObject& root) {\n    JsonObject user = root[F(\"u\")];\n    if (user.isNull()) {\n      user = root.createNestedObject(F(\"u\"));\n    }\n    _addJSONObject(user);\n  }\n  \n /*\n  * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).\n  * Values in the state object may be modified by connected clients\n  */\n  void readFromJsonState(JsonObject& root) {\n    JsonObject user = root[F(\"u\")];\n    if (!user.isNull()) {\n      JsonObject ssdrObj = user[FPSTR(_str_name)];\n      umSSDRDisplayTime = ssdrObj[FPSTR(_str_timeEnabled)] | umSSDRDisplayTime;\n      umSSDREnableLDR = ssdrObj[FPSTR(_str_ldrEnabled)] | umSSDREnableLDR;\n      umSSDRInverted = ssdrObj[FPSTR(_str_inverted)] | umSSDRInverted;\n      umSSDRColonblink = ssdrObj[FPSTR(_str_colonblink)] | umSSDRColonblink;\n      umSSDRLeadingZero = ssdrObj[FPSTR(_str_leadingZero)] | umSSDRLeadingZero;\n      umSSDRDisplayMask = ssdrObj[FPSTR(_str_displayMask)] | umSSDRDisplayMask;\n    }\n  }\n\n  void onMqttConnect(bool sessionPresent) {\n    char subBuffer[48];\n    if (mqttDeviceTopic[0] != 0)\n    {\n      _updateMQTT();\n      //subscribe for sevenseg messages on the device topic\n      sprintf_P(subBuffer, PSTR(\"%s/%S/+/set\"), mqttDeviceTopic, _str_name);\n      mqtt->subscribe(subBuffer, 2);\n    }\n\n    if (mqttGroupTopic[0] != 0)\n    {\n      //subscribe for sevenseg messages on the group topic\n      sprintf_P(subBuffer, PSTR(\"%s/%S/+/set\"), mqttGroupTopic, _str_name);\n      mqtt->subscribe(subBuffer, 2);\n    }\n  }\n\n  bool onMqttMessage(char *topic, char *payload) {\n    //If topic begins with sevenSeg cut it off, otherwise not our message.\n    size_t topicPrefixLen = strlen_P(PSTR(\"/wledSS/\"));\n    if (strncmp_P(topic, PSTR(\"/wledSS/\"), topicPrefixLen) == 0) {\n      topic += topicPrefixLen;\n    } else {\n      return false;\n    }\n    //We only care if the topic ends with /set\n    size_t topicLen = strlen(topic);\n    if (topicLen > 4 &&\n        topic[topicLen - 4] == '/' &&\n        topic[topicLen - 3] == 's' &&\n        topic[topicLen - 2] == 'e' &&\n        topic[topicLen - 1] == 't')\n    {\n      //Trim /set and handle it\n      topic[topicLen - 4] = '\\0';\n      _handleSetting(topic, payload);\n    }\n    return true;\n  }\n\n  void addToConfig(JsonObject &root) {\n     _addJSONObject(root);\n  }\n\n  bool readFromConfig(JsonObject &root) {\n    JsonObject top = root[FPSTR(_str_name)];\n\n    if (top.isNull()) {\n      DEBUG_PRINT(FPSTR(_str_name));\n      DEBUG_PRINTLN(F(\": No config found. (Using defaults.)\"));\n      return false;\n    }\n\n    umSSDRDisplayTime      = (top[FPSTR(_str_timeEnabled)] | umSSDRDisplayTime);\n    umSSDREnableLDR        = (top[FPSTR(_str_ldrEnabled)] | umSSDREnableLDR);\n    umSSDRInverted         = (top[FPSTR(_str_inverted)] | umSSDRInverted);\n    umSSDRColonblink       = (top[FPSTR(_str_colonblink)] | umSSDRColonblink);\n    umSSDRLeadingZero      = (top[FPSTR(_str_leadingZero)] | umSSDRLeadingZero);\n\n    umSSDRDisplayMask      = top[FPSTR(_str_displayMask)] | umSSDRDisplayMask;\n    umSSDRHours            = top[FPSTR(_str_hours)] | umSSDRHours;\n    umSSDRMinutes          = top[FPSTR(_str_minutes)] | umSSDRMinutes;\n    umSSDRSeconds          = top[FPSTR(_str_seconds)] | umSSDRSeconds;\n    umSSDRColons           = top[FPSTR(_str_colons)] | umSSDRColons;\n    umSSDRDays             = top[FPSTR(_str_days)] | umSSDRDays;\n    umSSDRMonths           = top[FPSTR(_str_months)] | umSSDRMonths;\n    umSSDRYears            = top[FPSTR(_str_years)] | umSSDRYears;\n    umSSDRBrightnessMin    = top[FPSTR(_str_minBrightness)] | umSSDRBrightnessMin;\n    umSSDRBrightnessMax    = top[FPSTR(_str_maxBrightness)] | umSSDRBrightnessMax;\n\n    DEBUG_PRINT(FPSTR(_str_name));\n    DEBUG_PRINTLN(F(\" config (re)loaded.\"));\n\n    return true;\n  }\n  /*\n     * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).\n     * This could be used in the future for the system to determine whether your usermod is installed.\n     */\n  uint16_t getId() {\n    return USERMOD_ID_SSDR;\n  }\n};\n\nconst char UsermodSSDR::_str_name[]        PROGMEM = \"UsermodSSDR\";\nconst char UsermodSSDR::_str_timeEnabled[] PROGMEM = \"enabled\";\nconst char UsermodSSDR::_str_inverted[]    PROGMEM = \"inverted\";\nconst char UsermodSSDR::_str_colonblink[]  PROGMEM = \"Colon-blinking\";\nconst char UsermodSSDR::_str_leadingZero[] PROGMEM = \"Leading-Zero\";\nconst char UsermodSSDR::_str_displayMask[] PROGMEM = \"Display-Mask\";\nconst char UsermodSSDR::_str_hours[]       PROGMEM = \"LED-Numbers-Hours\";\nconst char UsermodSSDR::_str_minutes[]     PROGMEM = \"LED-Numbers-Minutes\";\nconst char UsermodSSDR::_str_seconds[]     PROGMEM = \"LED-Numbers-Seconds\";\nconst char UsermodSSDR::_str_colons[]      PROGMEM = \"LED-Numbers-Colons\";\nconst char UsermodSSDR::_str_days[]        PROGMEM = \"LED-Numbers-Day\";\nconst char UsermodSSDR::_str_months[]      PROGMEM = \"LED-Numbers-Month\";\nconst char UsermodSSDR::_str_years[]       PROGMEM = \"LED-Numbers-Year\";\nconst char UsermodSSDR::_str_ldrEnabled[]  PROGMEM = \"enable-auto-brightness\";\nconst char UsermodSSDR::_str_minBrightness[]  PROGMEM = \"auto-brightness-min\";\nconst char UsermodSSDR::_str_maxBrightness[]  PROGMEM = \"auto-brightness-max\";\n\n\nstatic UsermodSSDR seven_segment_display_reloaded;\nREGISTER_USERMOD(seven_segment_display_reloaded);"
  },
  {
    "path": "usermods/sht/ShtUsermod.h",
    "content": "#pragma once\n#include \"wled.h\"\n\n#ifdef WLED_DISABLE_MQTT\n#error \"This user mod requires MQTT to be enabled.\"\n#endif\n\n#define USERMOD_SHT_TYPE_SHT30 0\n#define USERMOD_SHT_TYPE_SHT31 1\n#define USERMOD_SHT_TYPE_SHT35 2\n#define USERMOD_SHT_TYPE_SHT85 3\n\nclass SHT;\n\nclass ShtUsermod : public Usermod\n{\n  private:\n    bool enabled = false; // Is usermod enabled or not\n    bool firstRunDone = false; // Remembers if the first config load run had been done\n    bool initDone = false; // Remembers if the mod has been completely initialised\n    bool haMqttDiscovery = false; // Is MQTT discovery enabled or not\n    bool haMqttDiscoveryDone = false; // Remembers if we already published the HA discovery topics\n\n    // SHT vars\n    SHT *shtTempHumidSensor = nullptr; // Instance of SHT lib\n    byte shtType = 0; // SHT sensor type to be used. Default: SHT30\n    byte unitOfTemp = 0; // Temperature unit to be used. Default: Celsius (0 = Celsius, 1 = Fahrenheit)\n    bool shtInitDone = false; // Remembers if SHT sensor has been initialised\n    bool shtReadDataSuccess = false; // Did we have a successful data read and is a valid temperature and humidity available?\n    const byte shtI2cAddress = 0x44; // i2c address of the sensor. 0x44 is the default for all SHT sensors. Change this, if needed\n    unsigned long shtLastTimeUpdated = 0; // Remembers when we read data the last time\n    bool shtDataRequested = false; // Reading data is done async. This remembers if we asked the sensor to read data\n    float shtCurrentTempC = 0.0f; // Last read temperature in Celsius\n    float shtCurrentHumidity = 0.0f; // Last read humidity in RH%\n\n\n    void initShtTempHumiditySensor();\n    void cleanupShtTempHumiditySensor();\n    void cleanup();\n    inline bool isShtReady() { return shtInitDone; } // Checks if the SHT sensor has been initialised.\n\n    void publishTemperatureAndHumidityViaMqtt();\n    void publishHomeAssistantAutodiscovery();\n    void appendDeviceToMqttDiscoveryMessage(JsonDocument& root);\n\n  public:\n    // Strings to reduce flash memory usage (used more than twice)\n    static const char _name[];\n    static const char _enabled[];\n    static const char _shtType[];\n    static const char _unitOfTemp[];\n    static const char _haMqttDiscovery[];\n\n    void setup();\n    void loop();\n    void onMqttConnect(bool sessionPresent);\n    void appendConfigData();\n    void addToConfig(JsonObject &root);\n    bool readFromConfig(JsonObject &root);\n    void addToJsonInfo(JsonObject& root);\n\n    bool isEnabled() { return enabled; }\n\n    float getTemperature();\n    float getTemperatureC() { return roundf(shtCurrentTempC * 10.0f) / 10.0f; }\n    float getTemperatureF() { return (getTemperatureC() * 1.8f) + 32.0f; }\n    float getHumidity() { return roundf(shtCurrentHumidity * 10.0f) / 10.0f; }\n    const char* getUnitString();\n\n    uint16_t getId() { return USERMOD_ID_SHT; }\n};\n"
  },
  {
    "path": "usermods/sht/library.json",
    "content": "{\n  \"name\": \"sht\",\n  \"build\": { \"libArchive\": false },\n  \"dependencies\": {\n    \"robtillaart/SHT85\": \"~0.3.3\"\n  }\n}"
  },
  {
    "path": "usermods/sht/readme.md",
    "content": "# SHT\n\nUsermod to support various SHT i2c sensors like the SHT30, SHT31, SHT35 and SHT85\n\n## Requirements\n\n* \"SHT85\" by Rob Tillaart, v0.2 or higher: <https://github.com/RobTillaart/SHT85>\n\n## Usermod installation\n\nSimply 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`.\n\nESP32:\n\n```ini\n[env:custom_esp32dev_usermod_sht]\nextends = env:esp32dev\ncustom_usermods = ${env:esp32dev.custom_usermods} sht\n```\n\nESP8266:\n\n```ini\n[env:custom_d1_mini_usermod_sht]\nextends = env:d1_mini\ncustom_usermods = ${env:d1_mini.custom_usermods} sht\n```\n\n## MQTT Discovery for Home Assistant\n\nIf 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.\n\n### Publishing readings via MQTT\n\nRegardless 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.\n\n## Configuration\n\nNavigate 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:\n\n* SHT-Type:\n  * What it does: Select the SHT sensor type you want to use\n  * Possible values: SHT30, SHT31, SHT35, SHT85\n  * Default: SHT30\n* Unit:\n  * What it does: Select which unit should be used to display the temperature in the info section. Also used when sending via MQTT discovery, see below.\n  * Possible values: Celsius, Fahrenheit\n  * Default: Celsius\n* Add-To-HA-MQTT-Discovery:\n  * What it does: Makes the temperature and humidity available via MQTT discovery, so they're automatically added to Home Assistant, because that way it's typesafe.\n  * Possible values: Enabled/Disabled\n  * Default: Disabled\n\n## Change log\n\n2022-12\n\n* First implementation.\n\n## Credits\n\nezcGman | Andy: Find me on the Intermit.Tech (QuinLED) Discord server: <https://discord.gg/WdbAauG>\n"
  },
  {
    "path": "usermods/sht/sht.cpp",
    "content": "#include \"ShtUsermod.h\"\n#include \"SHT85.h\"\n\n// Strings to reduce flash memory usage (used more than twice)\nconst char ShtUsermod::_name[]            PROGMEM = \"SHT-Sensor\";\nconst char ShtUsermod::_enabled[]         PROGMEM = \"Enabled\";\nconst char ShtUsermod::_shtType[]         PROGMEM = \"SHT-Type\";\nconst char ShtUsermod::_unitOfTemp[]      PROGMEM = \"Unit\";\nconst char ShtUsermod::_haMqttDiscovery[] PROGMEM = \"Add-To-HA-MQTT-Discovery\";\n\n/**\n * Initialise SHT sensor.\n *\n * Using the correct constructor according to config and initialises it using the\n * global i2c pins.\n *\n * @return void\n */\nvoid ShtUsermod::initShtTempHumiditySensor()\n{\n  switch (shtType) {\n    case USERMOD_SHT_TYPE_SHT30: shtTempHumidSensor = (SHT *) new SHT30(); break;\n    case USERMOD_SHT_TYPE_SHT31: shtTempHumidSensor = (SHT *) new SHT31(); break;\n    case USERMOD_SHT_TYPE_SHT35: shtTempHumidSensor = (SHT *) new SHT35(); break;\n    case USERMOD_SHT_TYPE_SHT85: shtTempHumidSensor = (SHT *) new SHT85(); break;\n  }\n\n  shtTempHumidSensor->begin(shtI2cAddress); // uses &Wire\n  if (shtTempHumidSensor->readStatus() == 0xFFFF) {\n    DEBUG_PRINTF(\"[%s] SHT init failed!\\n\", _name);\n    cleanup();\n    return;\n  }\n\n  shtInitDone = true;\n}\n\n/**\n * Cleanup the SHT sensor.\n *\n * Properly calls \"reset\" for the sensor then releases it from memory.\n *\n * @return void\n */\nvoid ShtUsermod::cleanupShtTempHumiditySensor()\n{\n  if (isShtReady()) {\n    shtTempHumidSensor->reset();\n    delete shtTempHumidSensor;\n    shtTempHumidSensor = nullptr;\n  }\n  shtInitDone = false;\n}\n\n/**\n * Cleanup the mod completely.\n *\n * Calls ::cleanupShtTempHumiditySensor() to cleanup the SHT sensor and\n * deallocates pins.\n *\n * @return void\n */\nvoid ShtUsermod::cleanup()\n{\n  cleanupShtTempHumiditySensor();\n  enabled = false;\n}\n\n/**\n * Publish temperature and humidity to WLED device topic.\n *\n * Will add a \"/temperature\" and \"/humidity\" topic to the WLED device topic.\n * Temperature will be written in configured unit.\n *\n * @return void\n */\nvoid ShtUsermod::publishTemperatureAndHumidityViaMqtt() {\n  if (!WLED_MQTT_CONNECTED) return;\n  char buf[128];\n\n  snprintf_P(buf, 127, PSTR(\"%s/temperature\"), mqttDeviceTopic);\n  mqtt->publish(buf, 0, false, String(getTemperature()).c_str());\n  snprintf_P(buf, 127, PSTR(\"%s/humidity\"), mqttDeviceTopic);\n  mqtt->publish(buf, 0, false, String(getHumidity()).c_str());\n}\n\n/**\n * If enabled, publishes HA MQTT device discovery topics.\n *\n * Will make Home Assistant add temperature and humidity as entities automatically.\n *\n * Note: Whenever usermods are part of the WLED integration in HA, this can be dropped.\n *\n * @return void\n */\nvoid ShtUsermod::publishHomeAssistantAutodiscovery() {\n  if (!WLED_MQTT_CONNECTED) return;\n\n  char json_str[1024], buf[128];\n  size_t payload_size;\n  StaticJsonDocument<1024> json;\n\n  snprintf_P(buf, 127, PSTR(\"%s Temperature\"), serverDescription);\n  json[F(\"name\")] = buf;\n  snprintf_P(buf, 127, PSTR(\"%s/temperature\"), mqttDeviceTopic);\n  json[F(\"stat_t\")] = buf;\n  json[F(\"dev_cla\")] = F(\"temperature\");\n  json[F(\"stat_cla\")] = F(\"measurement\");\n  snprintf_P(buf, 127, PSTR(\"%s-temperature\"), escapedMac.c_str());\n  json[F(\"uniq_id\")] = buf;\n  json[F(\"unit_of_meas\")] = unitOfTemp ? F(\"°F\") : F(\"°C\");\n  appendDeviceToMqttDiscoveryMessage(json);\n  payload_size = serializeJson(json, json_str);\n  snprintf_P(buf, 127, PSTR(\"homeassistant/sensor/%s/%s-temperature/config\"), escapedMac.c_str(), escapedMac.c_str());\n  mqtt->publish(buf, 0, true, json_str, payload_size);\n\n  json.clear();\n\n  snprintf_P(buf, 127, PSTR(\"%s Humidity\"), serverDescription);\n  json[F(\"name\")] = buf;\n  snprintf_P(buf, 127, PSTR(\"%s/humidity\"), mqttDeviceTopic);\n  json[F(\"stat_t\")] = buf;\n  json[F(\"dev_cla\")] = F(\"humidity\");\n  json[F(\"stat_cla\")] = F(\"measurement\");\n  snprintf_P(buf, 127, PSTR(\"%s-humidity\"), escapedMac.c_str());\n  json[F(\"uniq_id\")] = buf;\n  json[F(\"unit_of_meas\")] = F(\"%\");\n  appendDeviceToMqttDiscoveryMessage(json);\n  payload_size = serializeJson(json, json_str);\n  snprintf_P(buf, 127, PSTR(\"homeassistant/sensor/%s/%s-humidity/config\"), escapedMac.c_str(), escapedMac.c_str());\n  mqtt->publish(buf, 0, true, json_str, payload_size);\n\n  haMqttDiscoveryDone = true;\n}\n\n/**\n * Helper to add device information to MQTT discovery topic.\n *\n * @return void\n */\nvoid ShtUsermod::appendDeviceToMqttDiscoveryMessage(JsonDocument& root) {\n  JsonObject device = root.createNestedObject(F(\"dev\"));\n  device[F(\"ids\")] = escapedMac.c_str();\n  device[F(\"name\")] = serverDescription;\n  device[F(\"sw\")] = versionString;\n  device[F(\"mdl\")] = ESP.getChipModel();\n  device[F(\"mf\")] = F(\"espressif\");\n}\n\n/**\n * Setup the mod.\n *\n * Allocates i2c pins as PinOwner::HW_I2C, so they can be allocated multiple times.\n * And calls ::initShtTempHumiditySensor() to initialise the sensor.\n *\n * @see Usermod::setup()\n * @see UsermodManager::setup()\n *\n * @return void\n */\nvoid ShtUsermod::setup()\n{\n  if (enabled) {\n    // GPIOs can be set to -1 , so check they're gt zero\n    if (i2c_sda < 0 || i2c_scl < 0) {\n      DEBUG_PRINTF(\"[%s] I2C bus not initialised!\\n\", _name);\n      cleanup();\n      return;\n    }\n\n    initShtTempHumiditySensor();\n\n    initDone = true;\n  }\n\n  firstRunDone = true;\n}\n\n/**\n * Actually reading data (async) from the sensor every 30 seconds.\n *\n * If last reading is at least 30 seconds, it will trigger a reading using\n * SHT::requestData(). We will then continiously check SHT::dataReady() if\n * data is ready to be read. If so, it's read, stored locally and published\n * via MQTT.\n *\n * @see Usermod::loop()\n * @see UsermodManager::loop()\n *\n * @return void\n */\nvoid ShtUsermod::loop()\n{\n  if (!enabled || !initDone || strip.isUpdating()) return;\n\n  if (isShtReady()) {\n    if (millis() - shtLastTimeUpdated > 30000 && !shtDataRequested) {\n      shtTempHumidSensor->requestData();\n      shtDataRequested = true;\n\n      shtLastTimeUpdated = millis();\n    }\n\n    if (shtDataRequested) {\n      if (shtTempHumidSensor->dataReady()) {\n        if (shtTempHumidSensor->readData(false)) {\n          shtCurrentTempC = shtTempHumidSensor->getTemperature();\n          shtCurrentHumidity = shtTempHumidSensor->getHumidity();\n\n          publishTemperatureAndHumidityViaMqtt();\n          shtReadDataSuccess = true;\n        } else {\n          shtReadDataSuccess = false;\n        }\n\n        shtDataRequested = false;\n      }\n    }\n  }\n}\n\n/**\n * Whenever MQTT is connected, publish HA autodiscovery topics.\n *\n * Is only done once.\n *\n * @see Usermod::onMqttConnect()\n * @see UsermodManager::onMqttConnect()\n *\n * @return void\n */\nvoid ShtUsermod::onMqttConnect(bool sessionPresent) {\n  if (haMqttDiscovery && !haMqttDiscoveryDone) publishHomeAssistantAutodiscovery();\n}\n\n/**\n * Add dropdown for sensor type and unit to UM config page.\n *\n * @see Usermod::appendConfigData()\n * @see UsermodManager::appendConfigData()\n *\n * @return void\n */\nvoid ShtUsermod::appendConfigData() {\n  oappend(F(\"dd=addDropdown('\"));\n  oappend(_name);\n  oappend(F(\"','\"));\n  oappend(_shtType);\n  oappend(F(\"');\"));\n  oappend(F(\"addOption(dd,'SHT30',0);\"));\n  oappend(F(\"addOption(dd,'SHT31',1);\"));\n  oappend(F(\"addOption(dd,'SHT35',2);\"));\n  oappend(F(\"addOption(dd,'SHT85',3);\"));\n  oappend(F(\"dd=addDropdown('\"));\n  oappend(_name);\n  oappend(F(\"','\"));\n  oappend(_unitOfTemp);\n  oappend(F(\"');\"));\n  oappend(F(\"addOption(dd,'Celsius',0);\"));\n  oappend(F(\"addOption(dd,'Fahrenheit',1);\"));\n}\n\n/**\n * Add config data to be stored in cfg.json.\n *\n * @see Usermod::addToConfig()\n * @see UsermodManager::addToConfig()\n *\n * @return void\n */\nvoid ShtUsermod::addToConfig(JsonObject &root)\n{\n  JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname\n\n  top[FPSTR(_enabled)] = enabled;\n  top[FPSTR(_shtType)] = shtType;\n  top[FPSTR(_unitOfTemp)] = unitOfTemp;\n  top[FPSTR(_haMqttDiscovery)] = haMqttDiscovery;\n}\n\n/**\n * Apply config on boot or save of UM config page.\n *\n * This is called whenever WLED boots and loads cfg.json, or when the UM config\n * page is saved. Will properly re-instantiate the SHT class upon type change and\n * publish HA discovery after enabling.\n *\n * @see Usermod::readFromConfig()\n * @see UsermodManager::readFromConfig()\n *\n * @return bool\n */\nbool ShtUsermod::readFromConfig(JsonObject &root)\n{\n  JsonObject top = root[FPSTR(_name)];\n  if (top.isNull()) {\n    DEBUG_PRINTF(\"[%s] No config found. (Using defaults.)\\n\", _name);\n    return false;\n  }\n\n  bool oldEnabled = enabled;\n  byte oldShtType = shtType;\n  byte oldUnitOfTemp = unitOfTemp;\n  bool oldHaMqttDiscovery = haMqttDiscovery;\n\n  getJsonValue(top[FPSTR(_enabled)], enabled);\n  getJsonValue(top[FPSTR(_shtType)], shtType);\n  getJsonValue(top[FPSTR(_unitOfTemp)], unitOfTemp);\n  getJsonValue(top[FPSTR(_haMqttDiscovery)], haMqttDiscovery);\n\n  // First run: reading from cfg.json, nothing to do here, will be all done in setup()\n  if (!firstRunDone) {\n    DEBUG_PRINTF(\"[%s] First run, nothing to do\\n\", _name);\n  }\n  // Check if mod has been en-/disabled\n  else if (enabled != oldEnabled) {\n    enabled ? setup() : cleanup();\n    DEBUG_PRINTF(\"[%s] Usermod has been en-/disabled\\n\", _name);\n  }\n  // Config has been changed, so adopt to changes\n  else if (enabled) {\n    if (oldShtType != shtType) {\n      cleanupShtTempHumiditySensor();\n      initShtTempHumiditySensor();\n    }\n\n    if (oldUnitOfTemp != unitOfTemp) {\n      publishTemperatureAndHumidityViaMqtt();\n      publishHomeAssistantAutodiscovery();\n    }\n\n    if (oldHaMqttDiscovery != haMqttDiscovery && haMqttDiscovery) {\n      publishHomeAssistantAutodiscovery();\n    }\n\n    DEBUG_PRINTF(\"[%s] Config (re)loaded\\n\", _name);\n  }\n\n  return true;\n}\n\n/**\n * Adds the temperature and humidity actually to the info section and /json info.\n *\n * This is called every time the info section is opened ot /json is called.\n *\n * @see Usermod::addToJsonInfo()\n * @see UsermodManager::addToJsonInfo()\n *\n * @return void\n */\nvoid ShtUsermod::addToJsonInfo(JsonObject& root)\n{\n  if (!enabled && !isShtReady()) {\n    return;\n  }\n\n  JsonObject user = root[\"u\"];\n  if (user.isNull()) user = root.createNestedObject(\"u\");\n\n  JsonArray jsonTemp = user.createNestedArray(F(\"Temperature\"));\n  JsonArray jsonHumidity = user.createNestedArray(F(\"Humidity\"));\n\n  if (shtLastTimeUpdated == 0 || !shtReadDataSuccess) {\n    jsonTemp.add(0);\n    jsonHumidity.add(0);\n    if (shtLastTimeUpdated == 0) {\n      jsonTemp.add(F(\" Not read yet\"));\n      jsonHumidity.add(F(\" Not read yet\"));\n    } else {\n      jsonTemp.add(F(\" Error\"));\n      jsonHumidity.add(F(\" Error\"));\n    }\n    return;\n  }\n\n  jsonHumidity.add(getHumidity());\n  jsonHumidity.add(F(\" RH\"));\n\n  jsonTemp.add(getTemperature());\n  jsonTemp.add(getUnitString());\n\n  // sensor object\n  JsonObject sensor = root[F(\"sensor\")];\n  if (sensor.isNull()) sensor = root.createNestedObject(F(\"sensor\"));\n\n  jsonTemp = sensor.createNestedArray(F(\"temp\"));\n  jsonTemp.add(getTemperature());\n  jsonTemp.add(getUnitString());\n\n  jsonHumidity = sensor.createNestedArray(F(\"humidity\"));\n  jsonHumidity.add(getHumidity());\n  jsonHumidity.add(F(\" RH\"));\n}\n\n/**\n * Getter for last read temperature for configured unit.\n *\n * @return float\n */\nfloat ShtUsermod::getTemperature() {\n  return unitOfTemp ? getTemperatureF() : getTemperatureC();\n}\n\n/**\n * Returns the current configured unit as human readable string.\n *\n * @return const char*\n */\nconst char* ShtUsermod::getUnitString() {\n  return unitOfTemp ? \"°F\" : \"°C\";\n}\n\nstatic ShtUsermod sht;\nREGISTER_USERMOD(sht);\n"
  },
  {
    "path": "usermods/smartnest/library.json",
    "content": "{\n  \"name\": \"smartnest\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/smartnest/readme.md",
    "content": "# Smartnest\n\nEnables integration with `smartnest.cz` service which provides MQTT integration with voice assistants, for example Google Home, Alexa, Siri, Home Assistant and more!\n\nIn order to setup Smartnest follow the [documentation](https://www.docu.smartnest.cz/).\n - You can create up to 5 different devices\n - To add the project to Google Home you can find the information [here](https://www.docu.smartnest.cz/google-home-integration)\n - To add the project to Alexa you can find the information [here](https://www.docu.smartnest.cz/alexa-integration)\n\n## MQTT API\n\nThe API is described in the Smartnest [Github repo](https://github.com/aososam/Smartnest/blob/master/Devices/lightRgb/lightRgb.ino).\n\n## Usermod installation\n\n1. Use `#define USERMOD_SMARTNEST` in wled.h or `-D USERMOD_SMARTNEST` in your platformio.ini (recommended).\n\n## Configuration\n\nUsermod has no configuration, but it relies on the MQTT configuration.\\\nUnder Config > Sync Interfaces > MQTT:\n\n* Enable `MQTT` check box.\n* Set the `Broker` field to: `smartnest.cz` or `3.122.209.170`(both work).\n* Set the `Port` field to: `1883`\n* The `Username` and `Password` fields are the login information from the `smartnest.cz` website (It is located above in the 3 points).\n* `Client ID` field is obtained from the device configuration panel in `smartnest.cz`.\n* `Device Topic` is obtained by entering the ClientID/report , remember to replace ClientId with your real information (Because they can ban your device).\n* `Group Topic` keep the same Group Topic.\n\nWait `1 minute` after turning it on, as it usually takes a while.  \n\n## Change log\n\n2022-09\n * First implementation.\n  \n2024-05\n * Solved code.\n * Updated documentation.\n * Second implementation.\n"
  },
  {
    "path": "usermods/smartnest/smartnest.cpp",
    "content": "#include \"wled.h\"\n\n#ifdef WLED_DISABLE_MQTT\n#error \"This user mod requires MQTT to be enabled.\"\n#endif\n\nclass Smartnest : public Usermod\n{\nprivate:\n  bool initialized = false;\n  unsigned long lastMqttReport = 0;\n  unsigned long mqttReportInterval = 60000; // Report every minute\n\n  void sendToBroker(const char *const topic, const char *const message)\n  {\n    if (!WLED_MQTT_CONNECTED)\n    {\n      return;\n    }\n\n    String topic_ = String(mqttClientID) + \"/\" + String(topic);\n    mqtt->publish(topic_.c_str(), 0, true, message);\n  }\n\n  void turnOff()\n  {\n    setBrightness(0);\n    turnOnAtBoot = false;\n    offMode = true;\n    sendToBroker(\"report/powerState\", \"OFF\");\n  }\n\n  void turnOn()\n  {\n    setBrightness(briLast);\n    turnOnAtBoot = true;\n    offMode = false;\n    sendToBroker(\"report/powerState\", \"ON\");\n  }\n\n  void setBrightness(int value)\n  {\n    if (value == 0 && bri > 0) briLast = bri;\n    bri = value;\n    stateUpdated(CALL_MODE_DIRECT_CHANGE);\n  }\n\n  void setColor(int r, int g, int b)\n  {\n    strip.getMainSegment().setColor(0, RGBW32(r, g, b, 0));\n    stateUpdated(CALL_MODE_DIRECT_CHANGE);\n    char msg[18] {};\n    sprintf(msg, \"rgb(%d,%d,%d)\", r, g, b);\n    sendToBroker(\"report/color\", msg);\n  }\n\n  int splitColor(const char *const color, int * const rgb)\n  {\n    char *color_ = NULL;\n    const char delim[] = \",\";\n    char *cxt = NULL;\n    char *token = NULL;\n    int position = 0;\n\n    // We need to copy the string in order to keep it read only as strtok_r function requires mutable string\n    color_ = (char *)malloc(strlen(color) + 1);\n    if (NULL == color_) {\n      return -1;\n    }\n\n    strcpy(color_, color);\n    token = strtok_r(color_, delim, &cxt);\n\n    while (token != NULL)\n    {\n      rgb[position++] = (int)strtoul(token, NULL, 10);\n      token = strtok_r(NULL, delim, &cxt);\n    }\n    free(color_);\n\n    return position;\n  }\n\npublic:\n  // Functions called by WLED\n\n  /**\n   * handling of MQTT message\n   * topic should look like: /<mqttClientID>/<Command>/<Message>\n   */\n  bool onMqttMessage(char *topic, char *message)\n  {\n    String topic_{topic};\n    String topic_prefix{mqttClientID + String(\"/directive/\")};\n\n    if (!topic_.startsWith(topic_prefix))\n    {\n      return false;\n    }\n\n    String subtopic = topic_.substring(topic_prefix.length());\n    String message_(message);\n\n    if (subtopic == \"powerState\")\n    {\n      if (strcmp(message, \"ON\") == 0)\n      {\n        turnOn();\n      }\n      else if (strcmp(message, \"OFF\") == 0)\n      {\n        turnOff();\n      }\n      return true;\n    }\n\n    if (subtopic == \"percentage\")\n    {\n      int val = (int)strtoul(message, NULL, 10);\n      if (val >= 0 && val <= 100)\n      {\n        setBrightness(map(val, 0, 100, 0, 255));\n      }\n      return true;\n    }\n\n    if (subtopic == \"color\")\n    {\n      // Parse the message which is in the format \"rgb(<0-255>,<0-255>,<0-255>)\"\n      int rgb[3] = {};\n      String colors = message_.substring(String(\"rgb(\").length(), message_.lastIndexOf(')'));\n      if (3 != splitColor(colors.c_str(), rgb))\n      {\n        return false;\n      }\n      setColor(rgb[0], rgb[1], rgb[2]);\n      return true;\n    }\n\n    return false;\n  }\n\n  /**\n   * subscribe to MQTT topic and send publish current status.\n   */\n  void onMqttConnect(bool sessionPresent)\n  {\n    String topic = String(mqttClientID) + \"/#\";\n\n    mqtt->subscribe(topic.c_str(), 0);\n    sendToBroker(\"report/online\", (bri ? \"true\" : \"false\")); // Reports that the device is online\n    delay(100);\n    sendToBroker(\"report/firmware\", versionString); // Reports the firmware version\n    delay(100);\n    sendToBroker(\"report/ip\", (char *)WiFi.localIP().toString().c_str()); // Reports the IP\n    delay(100);\n    sendToBroker(\"report/network\", (char *)WiFi.SSID().c_str()); // Reports the network name\n    delay(100);\n\n    String signal(WiFi.RSSI(), 10);\n    sendToBroker(\"report/signal\", signal.c_str()); // Reports the signal strength\n    delay(100);\n  }\n\n  /**\n   * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).\n   * This could be used in the future for the system to determine whether your usermod is installed.\n   */\n  uint16_t getId()\n  {\n    return USERMOD_ID_SMARTNEST;\n  }\n\n  /**\n   * setup() is called once at startup to initialize the usermod.\n   */\n  void setup() {\n      DEBUG_PRINTF(\"Smartnest usermod setup initializing...\");\n      \n      // Publish initial status\n      sendToBroker(\"report/status\", \"Smartnest usermod initialized\");\n  }\n\n  /**\n   * loop() is called continuously to keep the usermod running.\n   */\n  void loop() {\n    // Periodically report status to MQTT broker\n    unsigned long currentMillis = millis();\n    if (currentMillis - lastMqttReport >= mqttReportInterval) {\n      lastMqttReport = currentMillis;\n      \n      // Report current brightness\n      char brightnessMsg[11];\n      sprintf(brightnessMsg, \"%u\", bri);\n      sendToBroker(\"report/brightness\", brightnessMsg);\n      \n      // Report current signal strength\n      String signal(WiFi.RSSI(), 10);\n      sendToBroker(\"report/signal\", signal.c_str());\n    }\n  }\n};\n\n\nstatic Smartnest smartnest;\nREGISTER_USERMOD(smartnest);"
  },
  {
    "path": "usermods/stairway_wipe_basic/library.json",
    "content": "{\n  \"name\": \"stairway_wipe_basic\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/stairway_wipe_basic/readme.md",
    "content": "# Stairway lighting\n\n## Install\nAdd the buildflag `-D USERMOD_STAIRCASE_WIPE` to your enviroment to activate it.\n\n### Configuration\n`-D STAIRCASE_WIPE_OFF` \n<br>Have the LEDs wipe off instead of fading out\n\n## Description\nQuick usermod to accomplish something similar to [this video](https://www.youtube.com/watch?v=NHkju5ncC4A).\n\nThis usermod enables you to add a lightstrip alongside or on the steps of a staircase.\nWhen the `userVar0` variable is set, the LEDs will gradually turn on in a Wipe effect.\nBoth directions are supported by setting userVar0 to 1 and 2, respectively (HTTP API commands `U0=1` and `U0=2`).\n\nAfter the Wipe is complete, the light will either stay on (Solid effect) indefinitely or extinguish after `userVar1` seconds have elapsed.\nIf userVar0 is updated (e.g. by triggering a second sensor) the light will fade slowly until it's off.\nThis could be extended to also run a Wipe effect in reverse order to turn the LEDs off.\n\nThis is just a basic version to accomplish this using HTTP API calls `U0` and `U1` and/or macros.\nIt should be easy to adapt this code to interface with motion sensors or other input devices.\n"
  },
  {
    "path": "usermods/stairway_wipe_basic/stairway_wipe_basic.cpp",
    "content": "#include \"wled.h\"\n\n/*\n * Usermods allow you to add own functionality to WLED more easily\n * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality\n * \n * This is Stairway-Wipe as a v2 usermod.\n * \n * Using this usermod:\n * 1. Copy the usermod into the sketch folder (same folder as wled00.ino)\n * 2. Register the usermod by adding #include \"stairway-wipe-usermod-v2.h\" in the top and registerUsermod(new StairwayWipeUsermod()) in the bottom of usermods_list.cpp\n */\n\nclass StairwayWipeUsermod : public Usermod {\n  private:\n    //Private class members. You can declare variables and functions only accessible to your usermod here\n    unsigned long lastTime = 0;\n    byte wipeState = 0; //0: inactive 1: wiping 2: solid\n    unsigned long timeStaticStart = 0;\n    uint16_t previousUserVar0 = 0;\n\n//moved to buildflag\n//comment this out if you want the turn off effect to be just fading out instead of reverse wipe\n//#define STAIRCASE_WIPE_OFF\n  public:\nvoid setup() {\n    }\n    void loop() {\n  //userVar0 (U0 in HTTP API):\n  //has to be set to 1 if movement is detected on the PIR that is the same side of the staircase as the ESP8266\n  //has to be set to 2 if movement is detected on the PIR that is the opposite side\n  //can be set to 0 if no movement is detected. Otherwise LEDs will turn off after a configurable timeout (userVar1 seconds)\n\n  if (userVar0 > 0)\n  {\n    if ((previousUserVar0 == 1 && userVar0 == 2) || (previousUserVar0 == 2 && userVar0 == 1)) wipeState = 3; //turn off if other PIR triggered\n    previousUserVar0 = userVar0;\n    \n    if (wipeState == 0) {\n      startWipe();\n      wipeState = 1;\n    } else if (wipeState == 1) { //wiping\n      uint32_t cycleTime = 360 + (255 - effectSpeed)*75; //this is how long one wipe takes (minus 25 ms to make sure we switch in time)\n      if (millis() + strip.timebase > (cycleTime - 25)) { //wipe complete\n        effectCurrent = FX_MODE_STATIC;\n        timeStaticStart = millis();\n        colorUpdated(CALL_MODE_NOTIFICATION);\n        wipeState = 2;\n      }\n    } else if (wipeState == 2) { //static\n      if (userVar1 > 0) //if U1 is not set, the light will stay on until second PIR or external command is triggered\n      {\n        if (millis() - timeStaticStart > userVar1*1000) wipeState = 3;\n      }\n    } else if (wipeState == 3) { //switch to wipe off\n      #ifdef STAIRCASE_WIPE_OFF\n      effectCurrent = FX_MODE_COLOR_WIPE;\n      strip.timebase = 360 + (255 - effectSpeed)*75 - millis(); //make sure wipe starts fully lit\n      colorUpdated(CALL_MODE_NOTIFICATION);\n      wipeState = 4;\n      #else\n      turnOff();\n      #endif\n    } else { //wiping off\n      if (millis() + strip.timebase > (725 + (255 - effectSpeed)*150)) turnOff(); //wipe complete\n    }\n  } else {\n    wipeState = 0; //reset for next time\n    if (previousUserVar0) {\n      #ifdef STAIRCASE_WIPE_OFF\n      userVar0 = previousUserVar0;\n      wipeState = 3;\n      #else\n      turnOff();\n      #endif\n    }\n    previousUserVar0 = 0;\n  }\n}\n\n    void readFromJsonState(JsonObject& root)\n    {\n      userVar0 = root[\"user0\"] | userVar0; //if \"user0\" key exists in JSON, update, else keep old value\n      //if (root[\"bri\"] == 255) Serial.println(F(\"Don't burn down your garage!\"));\n    }\n\n    uint16_t getId()\n    {\n      return USERMOD_ID_STAIRWAY_WIPE;\n    }\n\n\n    void startWipe()\n    {\n    bri = briLast; //turn on\n    jsonTransitionOnce = true;\n    strip.setTransition(0); //no transition\n    effectCurrent = FX_MODE_COLOR_WIPE;\n    strip.resetTimebase(); //make sure wipe starts from beginning\n\n    //set wipe direction\n    Segment& seg = strip.getSegment(0);\n    bool doReverse = (userVar0 == 2);\n    seg.setOption(1, doReverse);\n\n    colorUpdated(CALL_MODE_NOTIFICATION);\n    }\n\n    void turnOff()\n    {\n    jsonTransitionOnce = true;\n    #ifdef STAIRCASE_WIPE_OFF\n    strip.setTransition(0); //turn off immediately after wipe completed\n    #else\n    strip.setTransition(4000); //fade out slowly\n    #endif\n    bri = 0;\n    stateUpdated(CALL_MODE_NOTIFICATION);\n    wipeState = 0;\n    userVar0 = 0;\n    previousUserVar0 = 0;\n    }\n\n\n\n   //More methods can be added in the future, this example will then be extended.\n   //Your usermod will remain compatible as it does not need to implement all methods from the Usermod base class!\n};\n\n\nstatic StairwayWipeUsermod stairway_wipe_basic;\nREGISTER_USERMOD(stairway_wipe_basic);"
  },
  {
    "path": "usermods/udp_name_sync/library.json",
    "content": "{\n  \"name\": \"udp_name_sync\",\n  \"build\": { \"libArchive\": false },\n  \"dependencies\": {}\n}\n"
  },
  {
    "path": "usermods/udp_name_sync/udp_name_sync.cpp",
    "content": "#include \"wled.h\"\n\nclass UdpNameSync : public Usermod {\n\n  private:\n\n    bool enabled = false;\n    char segmentName[WLED_MAX_SEGNAME_LEN] = {0};\n    static constexpr uint8_t kPacketType = 200; // custom usermod packet type\n    static const char _name[];\n    static const char _enabled[];\n\n  public:\n    /**\n     * Enable/Disable the usermod\n     */\n    inline void enable(bool value) { enabled = value; }\n\n    /**\n     * Get usermod enabled/disabled state\n     */\n    inline bool isEnabled() const { return enabled; }\n\n    void setup() override {\n      // Enabled when this usermod is compiled, set to false if you prefer runtime opt-in\n      enable(true);\n    }\n\n    void loop() override {\n      if (!enabled) return;\n      if (!WLED_CONNECTED) return;\n      if (!udpConnected) return;\n      Segment& mainseg = strip.getMainSegment();\n      if (segmentName[0] == '\\0' && !mainseg.name) return; //name was never set, do nothing\n\n      const char* curName = mainseg.name ? mainseg.name : \"\";\n      if (strncmp(curName, segmentName, sizeof(segmentName)) == 0) return; // same name, do nothing\n\n      IPAddress broadcastIp = uint32_t(Network.localIP()) | ~uint32_t(Network.subnetMask());\n      byte udpOut[WLED_MAX_SEGNAME_LEN + 2];\n      udpOut[0] = kPacketType; // custom usermod packet type (avoid 0..5 used by core protocols)\n\n      if (segmentName[0] != '\\0' && !mainseg.name) { // name cleared\n        notifierUdp.beginPacket(broadcastIp, udpPort);\n        segmentName[0] = '\\0';\n        DEBUG_PRINTLN(F(\"UdpNameSync: sending empty name\"));\n        udpOut[1] = 0; // explicit empty string\n        notifierUdp.write(udpOut, 2);\n        notifierUdp.endPacket();\n        return;\n      }\n\n      notifierUdp.beginPacket(broadcastIp, udpPort);\n      DEBUG_PRINT(F(\"UdpNameSync: saving segment name \"));\n      DEBUG_PRINTLN(curName);\n      strlcpy(segmentName, curName, sizeof(segmentName));\n      strlcpy((char *)&udpOut[1], segmentName, sizeof(udpOut) - 1); // leave room for header byte\n      size_t nameLen = strnlen((char *)&udpOut[1], sizeof(udpOut) - 1);\n      notifierUdp.write(udpOut, 2 + nameLen);\n      notifierUdp.endPacket();\n      DEBUG_PRINT(F(\"UdpNameSync: Sent segment name : \"));\n      DEBUG_PRINTLN(segmentName);\n      return;\n    }\n\n    bool onUdpPacket(uint8_t * payload, size_t len) override {\n      DEBUG_PRINT(F(\"UdpNameSync: Received packet\"));\n      if (!enabled) return false;\n      if (receiveDirect) return false;\n      if (len < 2) return false;                 // need type + at least 1 byte for name (can be 0)\n      if (payload[0] != kPacketType) return false;\n      Segment& mainseg = strip.getMainSegment();\n      char tmp[WLED_MAX_SEGNAME_LEN] = {0};\n      size_t copyLen = len - 1;\n      if (copyLen > sizeof(tmp) - 1) copyLen = sizeof(tmp) - 1;\n      memcpy(tmp, &payload[1], copyLen);\n      tmp[copyLen] = '\\0';\n      mainseg.setName(tmp);\n      DEBUG_PRINT(F(\"UdpNameSync: set segment name\"));\n      return true;\n     }\n};\n\nstatic UdpNameSync udp_name_sync;\nREGISTER_USERMOD(udp_name_sync);\n"
  },
  {
    "path": "usermods/user_fx/README.md",
    "content": "# Usermod user FX\n\nThis 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.\n\nMultiple 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.\n\n* [Installation](./README.md#installation)\n* [How The Usermod Works](./README.md#how-the-usermod-works)\n* [Basic Syntax for WLED Effect Creation](./README.md#basic-syntax-for-wled-effect-creation)\n* [Understanding 2D WLED Effects](./README.md#understanding-2d-wled-effects)\n* [The Metadata String](./README.md#the-metadata-string)\n* [Understanding 1D WLED Effects](./README.md#understanding-1d-wled-effects)\n* [Combining Multiple Effects in this Usermod](./README.md#combining-multiple-effects-in-this-usermod)\n* [Compiling](./README.md#compiling)\n* [Change Log](./README.md#change-log)\n* [Contact Us](./README.md#contact-us)\n\n## Installation\n\nTo activate the usermod, add the following line to your platformio_override.ini\n```ini\ncustom_usermods = user_fx\n```\nOr if you are already using a usermod, append user_fx to the list\n```ini\ncustom_usermods = audioreactive user_fx\n```\n\n## How The Usermod Works\n\nThe `user_fx.cpp` file can be broken down into four main parts:\n* **static effect definition** - This is a static LED setting that is displayed if an effect fails to initialize.\n* **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). \n* **Usermod Class definition(s)** - The class definition defines the blueprint from which all your custom Effects (or any usermod, for that matter) are created.\n* **Usermod registration** - All usermods have to be registered so that they are able to be compiled into your binary.\n\nWe 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.\n\n\n## Basic Syntax for WLED Effect Creation\n\nWLED effects generally follow a certain procedure for their operation:\n1. Determine dimension of segment\n2. Calculate new state if needed\n3. Implement a loop that calculates color for each pixel and sets it using `SEGMENT.setPixelColor()`\n4. The function is called at current frame rate.\n\nBelow are some helpful variables and functions to know as you start your journey towards WLED effect creation:\n\n| Syntax Element                                  | Size   | Description |\n| :---------------------------------------------- | :----- | :---------- |\n| [`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. |\n| [`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.  |\n| [`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.  |\n| [`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. |\n| [`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. |\n| [`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. |\n| [`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. |\n| [`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. |\n| [`SEGPALETTE`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX.h#L115) | --- | Macro that gets the currently selected palette for the currently processing segment. |\n| [`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.) |\n| [`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. |\n| [`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. |\n| [`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. |\n| [`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.) |\n| [`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%.  |\n| [`fadeToBlackBy()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1043) | ---    | can be used to fade all pixels to black.  |\n|  [`fadeToSecondaryBy()`](https://github.com/wled/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp#L1043) | ---    | fades all pixels to secondary color.  |\n| [`move()`](https://github.com/WLED/WLED/blob/75f6de9dc29fc7da5f301fc1388ada228dcb3b6e/wled00/FX_fcn.cpp) | ---    | Moves/shifts pixels in the desired direction. |\n| [`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). |\n\n You will see how these syntax elements work in the examples below.\n\n\n\n## Understanding 2D WLED Effects\n\nIn 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.\n(Special thanks to @mryndzionek for offering this \"Diffusion Fire\" 2D Effect for this tutorial.)\n\n### Imports\nThe 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.\n\n```cpp\n#include \"wled.h\"\n```\n\n### Static Effect Definition\nThe 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.\n\n`FX_FALLBACK_STATIC` is a macro that calls `mode_static()` and then returns.\n\n### User Effect Definitions\nPre-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.)\nThe 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:\n```cpp\nif (!strip.isMatrix || !SEGMENT.is2D()) \n  FX_FALLBACK_STATIC;  // not a 2D set-up\n```\nThe 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):\n```cpp\nconst int cols = SEG_W;\nconst int rows = SEG_H;\nconst auto XY = [&](int x, int y) { return x + y * cols; };\n```\n* The first line assigns the number of columns (width) in the active segment to cols.\n  * 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.\n* Next, we assign the number of rows (height) in the segment to rows.\n  * SEG_H is a macro for SEGMENT.height(). Combined with cols, this allows pixel addressing in 2D (x, y) space.\n* 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).\n  * This lambda helps with mapping a local 1D array to a 2D one.\n\nThe 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):\n```cpp\nconst uint8_t refresh_hz = map(SEGMENT.speed, 0, 255, 20, 80);\nconst unsigned refresh_ms = 1000 / refresh_hz;\nconst int16_t diffusion = map(SEGMENT.custom1, 0, 255, 0, 100);\nconst uint8_t spark_rate = SEGMENT.intensity;\nconst uint8_t turbulence = SEGMENT.custom2;\n```\n* The first line maps the SEGMENT.speed (user-controllable parameter from 0–255) to a value between 20 and 80 Hz.\n  * This determines how often the effect should refresh per second (Higher speed = more frames per second).\n* Next we convert refresh rate from Hz to milliseconds. (It’s easier to schedule animation updates in WLED using elapsed time in milliseconds.)\n  * This value is used to time when to update the effect.\n* The third line utilizes the `custom1` control (0–255 range, usually exposed via sliders) to define the diffusion rate, mapped to 0–100.\n  * This controls how much \"heat\" spreads to neighboring pixels — more diffusion = smoother flame spread.\n* Next we assign `SEGMENT.intensity` (user input 0–255) to a variable named `spark_rate`.\n  * This controls how frequently new \"spark\" pixels appear at the bottom of the matrix.\n  * A higher value means more frequent ignition of flame points.\n* The final line stores the user-defined `custom2` value to a variable called `turbulence`.\n  * This is used to introduce randomness in spark generation or flow — more turbulence means more chaotic behavior.\n\nNext we will look at some lines of code that handle memory allocation and effect initialization:\n\n```cpp\nunsigned dataSize = cols * rows;  // SEGLEN (virtual length) is equivalent to vWidth()*vHeight() for 2D\n```\n* This part calculates how much memory we need to represent per-pixel state.\n  * `cols * rows` or `(or SEGLEN)` returns the total number of pixels in the current segment.\n  * 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.\n  > **_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.\n\n```cpp\nif (!SEGENV.allocateData(dataSize))\n  FX_FALLBACK_STATIC; // allocation failed\n```\n* 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.\n* The syntax `SEGENV.allocateData(n)` requests a buffer of size n bytes (1 byte per pixel here).\n* If allocation fails (e.g., out of memory), it returns false, and the effect can’t proceed.\n* 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.\n\n\nThe next lines of code clear the LEDs and initialize timing:\n```cpp\nif (SEGENV.call == 0) {\n  SEGMENT.fill(BLACK);\n  SEGENV.step = 0;\n}\n```\n* 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.\n* 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).\n* This gives a clean starting point for the fire animation.\n* 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).\n  \n\nThe next block of code is where the animation update logic starts to kick in:\n```cpp\nif ((strip.now - SEGENV.step) >= refresh_ms) {\n  uint8_t tmp_row[cols];  // Keep for ≤~1 KiB; otherwise consider heap or reuse SEGENV.data as scratch.\n  SEGENV.step = strip.now;\n  // scroll up\n  for (unsigned y = 1; y < rows; y++)\n    for (unsigned x = 0; x < cols; x++) {\n      unsigned src = XY(x, y);\n      unsigned dst = XY(x, y - 1);\n      SEGENV.data[dst] = SEGENV.data[src];\n    }\n```\n* 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.\n* 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.\n* 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.\n* You'll see later that it writes results here before updating `SEGENV.data`.\n  * Note: this is allocated on the stack each frame. Keep such VLAs ≤ ~1 KiB; for larger sizes, prefer a buffer in `SEGENV.data`.\n\n> **_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.\n\n\nNow we get to the spark generation portion, where new bursts of heat appear at the bottom of the matrix:\n```cpp\nif (hw_random8() > turbulence) {\n  // create new sparks at bottom row\n  for (unsigned x = 0; x < cols; x++) {\n    uint8_t p = hw_random8();\n    if (p < spark_rate) {\n      unsigned dst = XY(x, rows - 1);\n      SEGENV.data[dst] = 255;\n    }\n  }\n}\n```\n* The first line randomizes whether we even attempt to spawn sparks this frame.\n  * `hw_random8()` gives a random number between 0–255 using a fast hardware RNG.\n  * `turbulence` is a user-controlled parameter (SEGMENT.custom2, set earlier).\n  * Higher turbulence means this block is less likely to run (because `hw_random8()` is less likely to exceed a high threshold).\n  * This adds randomness to when sparks appear — simulating natural flicker and chaotic fire.\n* The next line loops over all columns in the bottom row (row `rows - 1`).\n* Another random number, `p`, is used to probabilistically decide whether a spark appears at this (x, `rows-1`) position.\n* Next is a conditional statement.  The lower spark_rate is, the fewer sparks will appear.\n  * `spark_rate` comes from `SEGMENT.intensity` (0–255).\n  * High intensity means more frequent ignition.\n* `dst` calculates the destination index in the bottom row at column x.\n* The final line here sets the heat at this pixel to maximum (255).\n  * This simulates a fresh burst of flame, which will diffuse and move upward over time in subsequent frames.\n\nNext we reach the first part of the core of the fire simulation, which is diffusion (how heat spreads to neighboring pixels):\n```cpp\n// diffuse\nfor (unsigned y = 0; y < rows; y++) {\n  for (unsigned x = 0; x < cols; x++) {\n    unsigned v = SEGENV.data[XY(x, y)];\n    if (x > 0) {\n      v += SEGENV.data[XY(x - 1, y)];\n    }\n    if (x < (cols - 1)) {\n      v += SEGENV.data[XY(x + 1, y)];\n    }\n    tmp_row[x] = min(255, (int)(v * 100 / (300 + diffusion)));\n  }\n```\n* This block of code starts by looping over each row from top to bottom.  (We will do diffusion for each pixel row.)\n* Next we start an inner loop which iterates across each column in the current row.\n* Starting with the current heat value of pixel (x, y) assigned `v`:\n  * if there’s a pixel to the left, add its heat to the total.\n  * If there’s a pixel to the right, add its heat as well.\n  * So essentially, what the two `if` statements accomplish is: `v = center + left + right`.\n* The final line of code applies diffusion smoothing:\n  * 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).\n  * The `v * 100` scales things before dividing (preserving some dynamic range).\n  * `min(255, ...)` clamps the result to 8-bit range.\n  * This entire line of code stores the smoothed heat into the temporary row buffer.\n\nAfter calculating tmp_row, we now handle rendering the pixels by updating the actual segment data and turning 'heat' into visible colors:\n```cpp\n  for (unsigned x = 0; x < cols; x++) {\n    SEGENV.data[XY(x, y)] = tmp_row[x];\n    if (SEGMENT.check1) {\n      uint32_t color = SEGMENT.color_from_palette(tmp_row[x], true, false, 0);\n      SEGMENT.setPixelColorXY(x, y, color);\n    } else {\n      uint32_t base = SEGCOLOR(0);\n      SEGMENT.setPixelColorXY(x, y, color_fade(base, tmp_row[x]));\n    }\n  }\n}\n```\n* This next loop starts iterating over each row from top to bottom.  (We're now doing this for color-rendering for each pixel row.)\n* Next we update the main segment data with the smoothed value for this pixel.\n* 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.\n* 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.\n  * This creates rich gradient flames (e.g., yellow → red → black).\n* Finally we set the rendered color for the pixel (x, y).\n  * This repeats for each pixel  in each row.\n* If palette use is disabled, we fallback to fading a base color.\n* `SEGCOLOR(0)` gets the first user-selected color for the segment.\n* The final line of code fades that base color according to the heat value (acts as brightness multiplier).\n\n* 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.\n\nIf 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.\n```cpp\n//limit update rate\nif (strip.now - SEGENV.step < FRAMETIME_FIXED) return;\nSEGENV.step = strip.now;\n```\n\n### The Metadata String\nAt the end of every effect is an important line of code called the **metadata string**.\nIt defines how the effect is to be interacted with in the UI:\n```cpp\nstatic const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = \"Diffusion Fire@!,Spark rate,Diffusion Speed,Turbulence,,Use palette;;Color;;2;pal=35\";\n```\nThis metadata string is passed into `strip.addEffect()` and parsed by WLED to determine how your effect appears and behaves in the UI. \nThe string follows the syntax of `<Effect Parameters>;<Colors>;<Palette>;<Flags>;<Defaults>`, where Effect Parameters are specified by a comma-separated list.\nThe values for Effect Parameters will always follow the convention in the table below:\n\n| Parameter\t| Default tooltip label |\n| :-------- | :-------------------- |\n| sx\t       | Effect Speed |\n| ix\t       | Effect Intensity |\n| c1\t       | Custom 1 |\n| c2\t       | Custom 2 |\n| c3\t       | Custom 3 |\n| o1       \t| Checkbox 1 |\n| o2\t       | Checkbox 2 |\n| o3\t       | Checkbox 3 |\n\nUsing this info, let’s split the Metadata string above into logical sections:\n\n| Syntax Element                                  | Description |\n| :---------------------------------------------- | :---------- |\n| \"Diffusion Fire@!      | Name.  (The @ symbol marks the end of the Effect Name, and the beginning of the Parameter String elements.) |\n| !, | Use default UI entry; for the first space, this will automatically create a slider for Speed |\n| 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. |\n| (blank),                        | unused (empty field with not even a space)  |\n| 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. |\n| Color;                  | Custom color field `(SEGCOLOR(0))` |\n| (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.  |\n| 2;                  | Flag specifying that the effect requires a 2D matrix setup |\n| pal=35\"                  | Default Palette ID.  this is the setting that the effect starts up with. |\n\nMore information on metadata strings can be found [here](https://kno.wled.ge/interfaces/json-api/#effect-metadata).\n\n\n## Understanding 1D WLED Effects\n\nNext, 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.\nWe will break this effect down step by step.\n(This effect was originally one of the FastLED example effects; more information on FastLED can be found [here](https://fastled.io/).)\n\n```cpp\nstatic void sinelon_base(bool dual, bool rainbow=false) {\n```\n* The first line of code defines `sinelon base` as static helper function.  This is how all effects are initially defined.\n* Notice that it has some optional flags; these parameters will allow us to easily define the effect in different ways in the UI.\n\n```cpp\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n```\n* If segment length ≤ 1, there’s nothing to animate. Just show static mode.\n\nThe line of code helps create the \"Fade Out\" Trail:\n```cpp\n  SEGMENT.fade_out(SEGMENT.intensity);\n```\n* Gradually dims all LEDs each frame using SEGMENT.intensity as fade amount.\n* Creates the trailing \"comet\" effect by leaving a fading path behind the moving dot.\n\nNext, the effect computes some position information for the actively changing pixel, and the rest of the pixels as well:\n```cpp\n  unsigned pos = beatsin16_t(SEGMENT.speed/10, 0, SEGLEN-1);\n  if (SEGENV.call == 0) SEGENV.aux0 = pos;\n```\n* Calculates a sine-based oscillation to move the dot smoothly back and forth.\n  * `beatsin16_t` is an improved version of FastLED’s beatsin16 function, generating smooth oscillations\n  * SEGMENT.speed / 10: affects oscillation speed. Higher = faster.\n  * 0: minimum position.\n  * SEGLEN-1: maximum position.\n* 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.)\n\nThe next lines of code help determine the colors to be used:\n```cpp\n  uint32_t color1 = SEGMENT.color_from_palette(pos, true, false, 0);\n  uint32_t color2 = SEGCOLOR(2);\n```\n* `color1`: main moving dot color, chosen from palette using the current position as index.\n* `color2`: secondary color from user-configured color slot 2.\n\nThe next part takes into account the optional argument for if a Rainbow colored palette is in use:\n```cpp\n  if (rainbow) {\n    color1 = SEGMENT.color_wheel((pos & 0x07) * 32);\n  }\n```\n* If `rainbow` is true, override color1 using a rainbow wheel, producing rainbow cycling colors.\n* `(pos & 0x07) * 32` ensures the color changes gradually with position.\n\n```cpp\n    SEGMENT.setPixelColor(pos, color1);\n```\n* Lights up the computed position with the selected color.\n\nThe next line takes into account another one of the optional arguments for the effect to potentially handle dual mirrored dots which create the animation:\n```cpp\n  if (dual) {\n    if (!color2) color2 = SEGMENT.color_from_palette(pos, true, false, 0);\n    if (rainbow) color2 = color1; // share rainbow color\n    SEGMENT.setPixelColor(SEGLEN-1-pos, color2);\n  }\n```\n* If dual is true:\n  * Uses `color2` for mirrored dot on opposite side.\n  * If `color2` is not set (0), fallback to same palette color as `color1`.\n  * In `rainbow` mode, force both dots to share the rainbow color.\n  * Sets pixel at `SEGLEN-1-pos` to `color2`.\n\nThis final part of the effect function will fill in the 'trailing' pixels to complete the animation:\n```cpp\n    if (SEGENV.aux0 < pos) {\n      for (unsigned i = SEGENV.aux0; i < pos ; i++) {\n        SEGMENT.setPixelColor(i, color1);\n        if (dual) SEGMENT.setPixelColor(SEGLEN-1-i, color2);\n      }\n    } else {\n      for (unsigned i = SEGENV.aux0; i > pos ; i--) {\n        SEGMENT.setPixelColor(i, color1);\n        if (dual) SEGMENT.setPixelColor(SEGLEN-1-i, color2);\n      }\n    }\n    SEGENV.aux0 = pos;\n  }\n```\n* 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.\n* Fills in all pixels between previous position (SEGENV.aux0) and new position (pos) to ensure smooth continuous trail.\n  * Works in both directions: Forward (if new pos > old pos), and Backward (if new pos < old pos).\n* Updates `SEGENV.aux0` to current position at the end.\n\nThe last part of this effect has the Wrapper functions for different Sinelon modes.\nNotice that there are three different modes that we can define from the single effect definition by leveraging the arguments in the function:\n```cpp\nvoid mode_sinelon(void) {\n  sinelon_base(false);\n}\n// Calls sinelon_base with dual = false and rainbow = false \n\nvoid mode_sinelon_dual(void) {\n  sinelon_base(true);\n}\n// Calls sinelon_base with dual = true and rainbow = false \n\nvoid mode_sinelon_rainbow(void) {\n  sinelon_base(false, true);\n}\n// Calls sinelon_base with dual = false and rainbow = true \n```\n\nAnd then the last part defines the metadata strings for each effect to specify how it will be portrayed in the UI:\n```cpp\nstatic const char _data_FX_MODE_SINELON[] PROGMEM = \"Sinelon@!,Trail;!,!,!;!\";\nstatic const char _data_FX_MODE_SINELON_DUAL[] PROGMEM = \"Sinelon Dual@!,Trail;!,!,!;!\";\nstatic const char _data_FX_MODE_SINELON_RAINBOW[] PROGMEM = \"Sinelon Rainbow@!,Trail;,,!;!\";\n```\nRefer to the section above for guidance on understanding metadata strings.\n\n\n### The UserFxUsermod Class\n\nThe `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:\n```cpp\nclass UserFxUsermod : public Usermod {\n private:\n public:\n  void setup() override {\n    strip.addEffect(255, &mode_diffusionfire, _data_FX_MODE_DIFFUSIONFIRE);\n\n    ////////////////////////////////////////\n    //  add your effect function(s) here  //\n    ////////////////////////////////////////\n\n    // use id=255 for all custom user FX (the final id is assigned when adding the effect)\n\n    // strip.addEffect(255, &mode_your_effect, _data_FX_MODE_YOUR_EFFECT);\n    // strip.addEffect(255, &mode_your_effect2, _data_FX_MODE_YOUR_EFFECT2);\n    // strip.addEffect(255, &mode_your_effect3, _data_FX_MODE_YOUR_EFFECT3);\n  }\n  void loop() override {} // nothing to do in the loop\n  uint16_t getId() override { return USERMOD_ID_USER_FX; }\n};\n```\n* 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.\n  * This makes UserFxUsermod a valid WLED extension that can hook into `setup()`, `loop()`, and other lifecycle events.\n* The `void setup()` function runs once when WLED initializes the usermod.\n  * It's where you should register your effects, initialize hardware, or do any other setup logic.\n  * `override` ensures that this matches the Usermod base class definition.\n* The `strip.addEffect` line is an important one that registers the custom effect so WLED knows about it.\n  * 255: Temporary ID — WLED will assign a unique ID automatically.  (**Create all custom effects with the 255 ID.**)\n  * `&mode_diffusionfire`: Pointer to the effect function.\n  * `_data_FX_MODE_DIFFUSIONFIRE`: Metadata string stored in PROGMEM, describing the effect name and UI fields (like sliders).\n  * After this, your custom effect shows up in the WLED effects list.\n* 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.\n  * If your usermod had to respond to input or update state, you'd do it here.\n* The last part returns a unique ID constant used to identify this usermod.\n  * 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.\n\nThe final part of this file handles instantiation and initialization:\n```cpp\nstatic UserFxUsermod user_fx;\nREGISTER_USERMOD(user_fx);\n```\n* The first line creates a single, global instance of your usermod class.\n* The last line is a macro that tells WLED: “This is a valid usermod — load it during startup.”\n  * WLED adds it to the list of active usermods, calls `setup()` and `loop()`, and lets it interact with the system.\n\n\n\n## Combining Multiple Effects in this Usermod\n\nSo now let's say that you wanted add the effects  \"Diffusion Fire\" and \"Sinelon\" through this same Usermod file:\n* Navigate to [the code for Sinelon](https://github.com/wled/WLED/blob/7b0075d3754fa883fc1bbc9fbbe82aa23a9b97b8/wled00/FX.cpp#L3110).\n* 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.)\n* Register the effect using the `addEffect` function in the Usermod class.\n* Compile the code!\n\n## Compiling\nCompiling 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.\n\n## Change Log\n\n### Version 1.0.0\n\n* First version of the custom effect creation guide\n\n## Contact Us\n\nThis custom effect tutorial guide is still in development.\nIf 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.\n"
  },
  {
    "path": "usermods/user_fx/library.json",
    "content": "{\n  \"name\": \"user_fx\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/user_fx/user_fx.cpp",
    "content": "#include \"wled.h\"\n\n// for information how FX metadata strings work see https://kno.wled.ge/interfaces/json-api/#effect-metadata\n\n// paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined)\n#define PALETTE_SOLID_WRAP   (paletteBlend == 1 || paletteBlend == 3)\n\n#define indexToVStrip(index, stripNr) ((index) | (int((stripNr)+1)<<16))\n\n// static effect, used if an effect fails to initialize\nstatic void mode_static(void) {\n  SEGMENT.fill(SEGCOLOR(0));\n}\n\n#define FX_FALLBACK_STATIC { mode_static(); return; }\n\n// If you define configuration options in your class and need to reference them in your effect function, add them here.\n// If you only need to use them in your class you can define them as class members instead.\n// bool myConfigValue = false;\n\n/////////////////////////\n//  User FX functions  //\n/////////////////////////\n\n// Diffusion Fire: fire effect intended for 2D setups smaller than 16x16\nstatic void mode_diffusionfire(void) {\n  if (!strip.isMatrix || !SEGMENT.is2D())\n    FX_FALLBACK_STATIC;  // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n  const auto XY = [&](int x, int y) { return x + y * cols; };\n\n  const uint8_t refresh_hz = map(SEGMENT.speed, 0, 255, 20, 80);\n  const unsigned refresh_ms = 1000 / refresh_hz;\n  const int16_t diffusion = map(SEGMENT.custom1, 0, 255, 0, 100);\n  const uint8_t spark_rate = SEGMENT.intensity;\n  const uint8_t turbulence = SEGMENT.custom2;\n\nunsigned dataSize = cols * rows;  // SEGLEN (virtual length) is equivalent to vWidth()*vHeight() for 2D\n  if (!SEGENV.allocateData(dataSize))\n    FX_FALLBACK_STATIC;  // allocation failed\n\n  if (SEGENV.call == 0) {\n    SEGMENT.fill(BLACK);\n    SEGENV.step = 0;\n  }\n\n  if ((strip.now - SEGENV.step) >= refresh_ms) {\n    // Keep for ≤~1 KiB; otherwise consider heap or reuse SEGENV.data as scratch.\n    uint8_t tmp_row[cols];\n    SEGENV.step = strip.now;\n    // scroll up\n    for (unsigned y = 1; y < rows; y++)\n      for (unsigned x = 0; x < cols; x++) {\n        unsigned src = XY(x, y);\n        unsigned dst = XY(x, y - 1);\n        SEGENV.data[dst] = SEGENV.data[src];\n      }\n\n    if (hw_random8() > turbulence) {\n      // create new sparks at bottom row\n      for (unsigned x = 0; x < cols; x++) {\n        uint8_t p = hw_random8();\n        if (p < spark_rate) {\n          unsigned dst = XY(x, rows - 1);\n          SEGENV.data[dst] = 255;\n        }\n      }\n    }\n\n    // diffuse\n    for (unsigned y = 0; y < rows; y++) {\n      for (unsigned x = 0; x < cols; x++) {\n        unsigned v = SEGENV.data[XY(x, y)];\n        if (x > 0) {\n          v += SEGENV.data[XY(x - 1, y)];\n        }\n        if (x < (cols - 1)) {\n          v += SEGENV.data[XY(x + 1, y)];\n        }\n        tmp_row[x] = min(255, (int)(v * 100 / (300 + diffusion)));\n      }\n\n      for (unsigned x = 0; x < cols; x++) {\n        SEGENV.data[XY(x, y)] = tmp_row[x];\n        if (SEGMENT.check1) {\n          uint32_t color = SEGMENT.color_from_palette(tmp_row[x], true, false, 0);\n          SEGMENT.setPixelColorXY(x, y, color);\n        } else {\n          uint32_t base = SEGCOLOR(0);\n          SEGMENT.setPixelColorXY(x, y, color_fade(base, tmp_row[x]));\n        }\n      }\n    }\n  }\n}\nstatic const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = \"Diffusion Fire@!,Spark rate,Diffusion Speed,Turbulence,,Use palette;;Color;;2;pal=35\";\n\n\n/*\n * Spinning Wheel effect - LED animates around 1D strip (or each column in a 2D matrix), slows down and stops at random position\n *  Created by Bob Loeffler and claude.ai\n *  First slider (Spin speed) is for the speed of the moving/spinning LED (random number within a narrow speed range).\n *     If value is 0, a random speed will be selected from the full range of values.\n *  Second slider (Spin slowdown start time) is for how long before the slowdown phase starts (random number within a narrow time range).\n *     If value is 0, a random time will be selected from the full range of values.\n *  Third slider (Spinner size) is for the number of pixels that make up the spinner.\n *  Fourth slider (Spin delay) is for how long it takes for the LED to start spinning again after the previous spin.\n *  The first checkbox allows the spinner to spin. If it's enabled, the spinner will do its thing. If it's not enabled, it will wait for the user to enable\n *     it either by clicking the checkbox or by pressing a physical button (e.g. using a playlist to run a couple presets that have JSON API codes).\n *  The second checkbox sets \"color per block\" mode. Enabled means that each spinner block will be the same color no matter what its LED position is.\n *  The third checkbox enables synchronized restart (all spinners restart together instead of individually).\n *  aux0 stores the settings checksum to detect changes\n *  aux1 stores the color scale for performance\n */\n\nstatic void mode_spinning_wheel(void) {\n  if (SEGLEN < 1) FX_FALLBACK_STATIC;\n  \n  unsigned strips = SEGMENT.nrOfVStrips();\n  if (strips == 0) FX_FALLBACK_STATIC;\n\n  constexpr unsigned stateVarsPerStrip = 8;\n  unsigned dataSize = sizeof(uint32_t) * stateVarsPerStrip;\n  if (!SEGENV.allocateData(dataSize * strips)) FX_FALLBACK_STATIC;\n  uint32_t* state = reinterpret_cast<uint32_t*>(SEGENV.data);\n  // state[0] = current position (fixed point: upper 16 bits = position, lower 16 bits = fraction)\n  // state[1] = velocity (fixed point: pixels per frame * 65536)\n  // state[2] = phase (0=fast spin, 1=slowing, 2=wobble, 3=stopped)\n  // state[3] = stop time (when phase 3 was entered)\n  // state[4] = wobble step (0=at stop pos, 1=moved back, 2=returned to stop)\n  // state[5] = slowdown start time (when to transition from phase 0 to phase 1)\n  // state[6] = wobble timing (for 200ms / 400ms / 300ms delays)\n  // state[7] = store the stop position per strip\n\n  // state[] index values for easier readability\n  constexpr unsigned CUR_POS_IDX       = 0;  // state[0]\n  constexpr unsigned VELOCITY_IDX      = 1;\n  constexpr unsigned PHASE_IDX         = 2;\n  constexpr unsigned STOP_TIME_IDX     = 3;\n  constexpr unsigned WOBBLE_STEP_IDX   = 4;\n  constexpr unsigned SLOWDOWN_TIME_IDX = 5;\n  constexpr unsigned WOBBLE_TIME_IDX   = 6;\n  constexpr unsigned STOP_POS_IDX      = 7;\n\n  SEGMENT.fill(SEGCOLOR(1));\n\n  // Handle random seeding globally (outside the virtual strip)\n  if (SEGENV.call == 0) {\n    random16_set_seed(hw_random16());\n    SEGENV.aux1 = (255 << 8) / SEGLEN; // Cache the color scaling\n  }\n\n  // Check if settings changed (do this once, not per virtual strip)\n  uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom3 + SEGMENT.check1 + SEGMENT.check3;\n  bool settingsChanged = (SEGENV.aux0 != settingssum);\n  if (settingsChanged) {\n    random16_add_entropy(hw_random16());\n    SEGENV.aux0 = settingssum;\n  }\n\n  // Check if all spinners are stopped and ready to restart (for synchronized restart)\n  bool allReadyToRestart = true;\n  if (SEGMENT.check3) {\n    uint8_t spinnerSize = map(SEGMENT.custom1, 0, 255, 1, 10);\n    uint16_t spin_delay = map(SEGMENT.custom3, 0, 31, 2000, 15000);\n    uint32_t now = strip.now;\n    \n    for (unsigned stripNr = 0; stripNr < strips; stripNr += spinnerSize) {\n      uint32_t* stripState = &state[stripNr * stateVarsPerStrip];\n      // Check if this spinner is stopped AND has waited its delay\n      if (stripState[PHASE_IDX] != 3 || stripState[STOP_TIME_IDX] == 0) {\n        allReadyToRestart = false;\n        break;\n      }\n      // Check if delay has elapsed\n      if ((now - stripState[STOP_TIME_IDX]) < spin_delay) {\n        allReadyToRestart = false;\n        break;\n      }\n    }\n  }\n \n  struct virtualStrip {\n    static void runStrip(uint16_t stripNr, uint32_t* state, bool settingsChanged, bool allReadyToRestart, unsigned strips) {\n      uint8_t phase = state[PHASE_IDX];\n      uint32_t now = strip.now;\n\n      // Check for restart conditions\n      bool needsReset = false;\n      if (SEGENV.call == 0) {\n        needsReset = true;\n      } else if (settingsChanged && SEGMENT.check1) {\n        needsReset = true;\n      } else if (phase == 3 && state[STOP_TIME_IDX] != 0) {\n          // If synchronized restart is enabled, only restart when all strips are ready\n          if (SEGMENT.check3) {\n            if (allReadyToRestart) {\n              needsReset = true;\n            }\n          } else {\n            // Normal mode: restart after individual strip delay\n            uint16_t spin_delay = map(SEGMENT.custom3, 0, 31, 2000, 15000);\n            if ((now - state[STOP_TIME_IDX]) >= spin_delay) {\n              needsReset = true;\n            }\n          }\n      }\n\n      // Initialize or restart\n      if (needsReset && SEGMENT.check1) {   // spin the spinner(s) only if the \"Spin me!\" checkbox is enabled\n        state[CUR_POS_IDX] = 0;\n        \n        // Set velocity\n        uint16_t speed = map(SEGMENT.speed, 0, 255, 300, 800);\n        if (speed == 300) {  // random speed (user selected 0 on speed slider)\n          state[VELOCITY_IDX] = random16(200, 900) * 655;   // fixed-point velocity scaling (approx. 65536/100) \n        } else {\n          state[VELOCITY_IDX] = random16(speed - 100, speed + 100) * 655;\n        }\n        \n        // Set slowdown start time\n        uint16_t slowdown = map(SEGMENT.intensity, 0, 255, 3000, 5000);\n        if (slowdown == 3000) {  // random slowdown start time (user selected 0 on intensity slider)\n          state[SLOWDOWN_TIME_IDX] = now + random16(2000, 6000);\n        } else {\n          state[SLOWDOWN_TIME_IDX] = now + random16(slowdown - 1000, slowdown + 1000);\n        }\n        \n        state[PHASE_IDX] = 0;\n        state[STOP_TIME_IDX] = 0;\n        state[WOBBLE_STEP_IDX] = 0;\n        state[WOBBLE_TIME_IDX] = 0;\n        state[STOP_POS_IDX] = 0; // Initialize stop position\n        phase = 0;\n      }\n\n      uint32_t pos_fixed = state[CUR_POS_IDX];\n      uint32_t velocity = state[VELOCITY_IDX];\n      \n      // Phase management\n      if (phase == 0) {\n        // Fast spinning phase\n        if ((int32_t)(now - state[SLOWDOWN_TIME_IDX]) >= 0) {\n          phase = 1;\n          state[PHASE_IDX] = 1;\n        }\n      } else if (phase == 1) {\n        // Slowing phase - apply deceleration\n        uint32_t decel = velocity / 80;\n        if (decel < 100) decel = 100;\n        \n        velocity = (velocity > decel) ? velocity - decel : 0;\n        state[VELOCITY_IDX] = velocity;\n        \n        // Check if stopped\n        if (velocity < 2000) {\n          velocity = 0;\n          state[VELOCITY_IDX] = 0;\n          phase = 2;\n          state[PHASE_IDX] = 2;\n          state[WOBBLE_STEP_IDX] = 0;\n          uint16_t stop_pos = (pos_fixed >> 16) % SEGLEN;\n          state[STOP_POS_IDX] = stop_pos;\n          state[WOBBLE_TIME_IDX] = now;\n        }\n      } else if (phase == 2) {\n        // Wobble phase (moves the LED back one and then forward one)\n        uint32_t wobble_step = state[WOBBLE_STEP_IDX];\n        uint16_t stop_pos = state[STOP_POS_IDX];\n        uint32_t elapsed = now - state[WOBBLE_TIME_IDX];\n        \n        if (wobble_step == 0 && elapsed >= 200) {\n          // Move back one LED from stop position\n          uint16_t back_pos = (stop_pos == 0) ? SEGLEN - 1 : stop_pos - 1;\n          pos_fixed = ((uint32_t)back_pos) << 16;\n          state[CUR_POS_IDX] = pos_fixed;\n          state[WOBBLE_STEP_IDX] = 1;\n          state[WOBBLE_TIME_IDX] = now;\n        } else if (wobble_step == 1 && elapsed >= 400) {\n          // Move forward to the stop position\n          pos_fixed = ((uint32_t)stop_pos) << 16;\n          state[CUR_POS_IDX] = pos_fixed;\n          state[WOBBLE_STEP_IDX] = 2;\n          state[WOBBLE_TIME_IDX] = now;\n        } else if (wobble_step == 2 && elapsed >= 300) {\n          // Wobble complete, enter stopped phase\n          phase = 3;\n          state[PHASE_IDX] = 3;\n          state[STOP_TIME_IDX] = now;\n        }\n      }\n      \n      // Update position (phases 0 and 1 only)\n      if (phase == 0 || phase == 1) {\n        pos_fixed += velocity;\n        state[CUR_POS_IDX] = pos_fixed;\n      }\n      \n      // Draw LED for all phases\n      uint16_t pos = (pos_fixed >> 16) % SEGLEN;\n\n      uint8_t spinnerSize = map(SEGMENT.custom1, 0, 255, 1, 10);\n\n      // Calculate color once per spinner block (based on strip number, not position)\n      uint8_t hue;\n      if (SEGMENT.check2) {\n        // Each spinner block gets its own color based on strip number\n        uint16_t numSpinners = max(1U, (strips + spinnerSize - 1) / spinnerSize);\n        hue = (uint32_t)(255) * (stripNr / spinnerSize) / numSpinners;\n      } else {\n        // Color changes with position\n        hue = (SEGENV.aux1 * pos) >> 8;\n      }\n\n      uint32_t color = ColorFromPaletteWLED(SEGPALETTE, hue, 255, LINEARBLEND);\n\n      // Draw the spinner with configurable size (1-10 LEDs)\n      for (int8_t x = 0; x < spinnerSize; x++) {\n        for (uint8_t y = 0; y < spinnerSize; y++) {\n          uint16_t drawPos = (pos + y) % SEGLEN;\n          int16_t drawStrip = stripNr + x;\n          \n          // Wrap horizontally if needed, or skip if out of bounds\n          if (drawStrip >= 0 && drawStrip < strips) {\n            SEGMENT.setPixelColor(indexToVStrip(drawPos, drawStrip), color);\n          }\n        }\n      }\n    }\n  };\n\n  for (unsigned stripNr = 0; stripNr < strips; stripNr++) {\n    // Only run on strips that are multiples of spinnerSize to avoid overlap\n    uint8_t spinnerSize = map(SEGMENT.custom1, 0, 255, 1, 10);\n    if (stripNr % spinnerSize == 0) {\n      virtualStrip::runStrip(stripNr, &state[stripNr * stateVarsPerStrip], settingsChanged, allReadyToRestart, strips);\n    }\n  }\n}\nstatic const char _data_FX_MODE_SPINNINGWHEEL[] PROGMEM = \"Spinning Wheel@Speed (0=random),Slowdown (0=random),Spinner size,,Spin delay,Spin me!,Color per block,Sync restart;!,!;!;;m12=1,c1=1,c3=8,o1=1,o3=1\";\n\n\n/*\n/  Lava Lamp 2D effect\n*   Uses particles to simulate rising blobs of \"lava\" or wax\n*   Particles slowly rise, merge to create organic flowing shapes, and then fall to the bottom to start again\n*   Created by Bob Loeffler using claude.ai\n*   The first slider sets the number of active blobs\n*   The second slider sets the size range of the blobs\n*   The third slider sets the damping value for horizontal blob movement\n*   The Attract checkbox sets the attraction of blobs (checked will make the blobs attract other close blobs horizontally)\n*   The Keep Color Ratio checkbox sets whether we preserve the color ratio when displaying pixels that are in 2 or more overlapping blobs\n*   aux0 keeps track of the blob size value\n*   aux1 keeps track of the number of blobs\n*/\n\ntypedef struct LavaParticle {\n  float    x, y;         // Position\n  float    vx, vy;       // Velocity\n  float    size;         // Blob size\n  uint8_t  hue;          // Color\n  bool     active;       // will not be displayed if false\n  uint16_t delayTop;     // number of frames to wait at top before falling again\n  bool     idleTop;      // sitting idle at the top\n  uint16_t delayBottom;  // number of frames to wait at bottom before rising again\n  bool     idleBottom;   // sitting idle at the bottom\n} LavaParticle;\n\nstatic void mode_2D_lavalamp(void) {\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n  \n  const uint16_t cols = SEG_W;\n  const uint16_t rows = SEG_H;\n  constexpr float MAX_BLOB_RADIUS = 20.0f;  // cap to prevent frame rate drops on large matrices\n  constexpr size_t MAX_LAVA_PARTICLES = 34;  // increasing this value could cause slowness for large matrices\n  constexpr size_t MAX_TOP_FPS_DELAY = 900;  // max delay when particles are at the top\n  constexpr size_t MAX_BOTTOM_FPS_DELAY = 1200;  // max delay when particles are at the bottom\n\n  // Allocate per-segment storage\n  if (!SEGENV.allocateData(sizeof(LavaParticle) * MAX_LAVA_PARTICLES)) FX_FALLBACK_STATIC;\n  LavaParticle* lavaParticles = reinterpret_cast<LavaParticle*>(SEGENV.data);\n\n  // Initialize particles on first call\n  if (SEGENV.call == 0) {\n    for (int i = 0; i < MAX_LAVA_PARTICLES; i++) {\n      lavaParticles[i].active = false;\n    }\n  }\n\n  // Track particle size and particle count slider changes, re-initialize if either changes\n  uint8_t currentNumParticles = (SEGMENT.intensity >> 3) + 3;\n  uint8_t currentSize = SEGMENT.custom1;\n  if (currentNumParticles > MAX_LAVA_PARTICLES) currentNumParticles = MAX_LAVA_PARTICLES;\n  bool needsReinit = (currentSize != SEGENV.aux0) || (currentNumParticles != SEGENV.aux1);\n\n  if (needsReinit) {\n    for (int i = 0; i < MAX_LAVA_PARTICLES; i++) {\n      lavaParticles[i].active = false;\n    }\n    SEGENV.aux0 = currentSize;\n    SEGENV.aux1 = currentNumParticles;\n  }\n\n  uint8_t size = currentSize;\n  uint8_t numParticles = currentNumParticles;\n  \n  // blob size based on matrix width\n  const float minSize = cols * 0.15f; // Minimum 15% of width\n  const float maxSize = cols * 0.4f;  // Maximum 40% of width\n  float sizeRange = (maxSize - minSize) * (size / 255.0f);\n  int rangeInt = max(1, (int)(sizeRange));\n\n  // calculate the spawning area for the particles\n  const float spawnXStart = cols * 0.20f;\n  const float spawnXWidth = cols * 0.60f;\n  int spawnX = max(1, (int)(spawnXWidth));\n\n  bool preserveColorRatio = SEGMENT.check3;\n\n  // Spawn new particles at the bottom near the center\n  for (int i = 0; i < MAX_LAVA_PARTICLES; i++) {\n    if (!lavaParticles[i].active && hw_random8() < 32) { // spawn when slot available\n      // Spawn in the middle 60% of the matrix width\n      lavaParticles[i].x = spawnXStart + (float)hw_random16(spawnX);\n      lavaParticles[i].y = rows - 1;\n      lavaParticles[i].vx = (hw_random16(7) - 3) / 250.0f;\n      lavaParticles[i].vy = -(hw_random16(20) + 10) / 100.0f * 0.3f;\n      \n      lavaParticles[i].size = minSize + (float)hw_random16(rangeInt);\n      if (lavaParticles[i].size > MAX_BLOB_RADIUS) lavaParticles[i].size = MAX_BLOB_RADIUS;\n\n      lavaParticles[i].hue = hw_random8();\n      lavaParticles[i].active = true;\n\n      // Set random delays when particles are at top and bottom\n      lavaParticles[i].delayTop = hw_random16(MAX_TOP_FPS_DELAY);\n      lavaParticles[i].delayBottom = hw_random16(MAX_BOTTOM_FPS_DELAY);\n      lavaParticles[i].idleBottom = true;\n      break;\n    }\n  }\n\n  // Fade background slightly for trailing effect\n  SEGMENT.fadeToBlackBy(40);\n  \n  // Update and draw particles\n  int activeCount = 0;\n  unsigned long currentMillis = strip.now;\n  for (int i = 0; i < MAX_LAVA_PARTICLES; i++) {\n    if (!lavaParticles[i].active) continue;\n    activeCount++;\n\n    // Keep particle count on target by deactivating excess particles\n    if (activeCount > numParticles) {\n      lavaParticles[i].active = false;\n      activeCount--;\n      continue;\n    }\n\n    LavaParticle *p = &lavaParticles[i];\n    \n    // Physics update\n    p->x += p->vx;\n    p->y += p->vy;\n    \n    // Optional particle/blob attraction\n    if (SEGMENT.check2) {\n      for (int j = 0; j < MAX_LAVA_PARTICLES; j++) {\n        if (i == j || !lavaParticles[j].active) continue;\n        \n        LavaParticle *other = &lavaParticles[j];\n        \n        // Skip attraction if moving in same vertical direction (both up or both down)\n        if ((p->vy < 0 && other->vy < 0) || (p->vy > 0 && other->vy > 0)) continue;\n        \n        float dx = other->x - p->x;\n        float dy = other->y - p->y;\n\n        // Apply weak horizontal attraction only\n        float attractRange = p->size + other->size;\n        float distSq = dx*dx + dy*dy;\n        float attractRangeSq = attractRange * attractRange;\n        if (distSq > 0 && distSq < attractRangeSq) {\n          float dist = sqrt(distSq); // Only compute sqrt when needed\n          float force = (1.0f - (dist / attractRange)) * 0.0001f;\n          p->vx += (dx / dist) * force;\n        }\n      }\n    }\n\n    // Horizontal oscillation (makes it more organic)\n    float damping= map(SEGMENT.custom2, 0, 255, 97, 87) / 100.0f;\n    p->vx += sin((currentMillis / 1000.0f + i) * 0.5f) * 0.002f; // Reduced oscillation\n    p->vx *= damping; // damping for more or less horizontal drift\n\n    // Bounce off sides (don't affect vertical velocity)\n    if (p->x < 0) {\n      p->x = 0;\n      p->vx = abs(p->vx); // reverse horizontal\n    }\n    if (p->x >= cols) {\n      p->x = cols - 1;\n      p->vx = -abs(p->vx); // reverse horizontal\n    }\n\n    // Adjust rise/fall velocity depending on approx distance from heat source (at bottom)\n    // In top 1/4th of rows...\n    if (p->y < rows * .25f) {\n      if (p->vy >= 0) {  // if going down, delay the particles so they won't go down immediately\n        if (p->delayTop > 0 && p->idleTop) {\n          p->vy = 0.0f;\n          p->delayTop--;\n          p->idleTop = true;\n        } else {\n          p->vy = 0.01f;\n          p->delayTop = hw_random16(MAX_TOP_FPS_DELAY);\n          p->idleTop = false;\n        }\n      } else if (p->vy <= 0) {  // if going up, slow down the rise rate\n        p->vy = -0.03f;\n      }\n    }\n\n    // In next 1/4th of rows...\n    if (p->y <= rows * .50f && p->y >= rows * .25f) {\n      if (p->vy > 0) {  // if going down, speed up the fall rate\n        p->vy = 0.03f;\n      } else if (p->vy <= 0) {  // if going up, speed up the rise rate a little more\n        p->vy = -0.05f;\n      }\n    }\n\n    // In next 1/4th of rows...\n    if (p->y <= rows * .75f && p->y >= rows * .50f) {\n      if (p->vy > 0) {  // if going down, speed up the fall rate a little more\n        p->vy = 0.04f;\n      } else if (p->vy <= 0) {  // if going up, speed up the rise rate\n        p->vy = -0.03f;\n      }\n    }\n\n    // In bottom 1/4th of rows...\n    if (p->y > rows * .75f) {\n      if (p->vy >= 0) {  // if going down, slow down the fall rate\n        p->vy = 0.02f;\n      } else if (p->vy <= 0) {  // if going up, delay the particles so they won't go up immediately\n        if (p->delayBottom > 0 && p->idleBottom) {\n          p->vy = 0.0f;\n          p->delayBottom--;\n          p->idleBottom = true;\n        } else {\n          p->vy = -0.01f;\n          p->delayBottom = hw_random16(MAX_BOTTOM_FPS_DELAY);\n          p->idleBottom = false;\n        }\n      }\n    }\n\n    // Boundary handling with reversal of direction\n    // When reaching TOP (y=0 area), reverse to fall back down, but need to delay first\n    if (p->y <= 0.5f * p->size) {\n      p->y = 0.5f * p->size;\n      if (p->vy < 0) {\n        p->vy = 0.005f;  // set to a tiny positive value to start falling very slowly\n        p->idleTop = true;\n      }\n    }\n\n    // When reaching BOTTOM (y=rows-1 area), reverse to rise back up, but need to delay first\n    if (p->y >= rows - 0.5f * p->size) {\n      p->y = rows - 0.5f * p->size;\n      if (p->vy > 0) {\n        p->vy = -0.005f;  // set to a tiny negative value to start rising very slowly\n        p->idleBottom = true;\n      }\n    }\n\n    // Get color\n    uint32_t color;\n    color = SEGMENT.color_from_palette(p->hue, true, PALETTE_SOLID_WRAP, 0);\n    \n    // Extract RGB and apply life/opacity\n    uint8_t w = (W(color) * 255) >> 8;\n    uint8_t r = (R(color) * 255) >> 8;\n    uint8_t g = (G(color) * 255) >> 8;\n    uint8_t b = (B(color) * 255) >> 8;\n\n    // Draw blob with sub-pixel accuracy using bilinear distribution\n    float sizeSq = p->size * p->size;\n\n    // Get fractional offsets of particle center\n    float fracX = p->x - floorf(p->x);\n    float fracY = p->y - floorf(p->y);\n    int centerX = (int)floorf(p->x);\n    int centerY = (int)floorf(p->y);\n\n    for (int dy = -(int)p->size - 1; dy <= (int)p->size + 1; dy++) {\n      for (int dx = -(int)p->size - 1; dx <= (int)p->size + 1; dx++) {\n        int px = centerX + dx;\n        int py = centerY + dy;\n        \n        if (px < 0 || px >= cols || py < 0 || py >= rows) continue;\n\n        // Sub-pixel distance: measure from true float center to pixel center\n        float subDx = dx - fracX;  // distance from true center to this pixel's center\n        float subDy = dy - fracY;\n        float distSq = subDx * subDx + subDy * subDy;\n\n        if (distSq < sizeSq) {\n          float intensity = 1.0f - (distSq / sizeSq);\n          intensity = intensity * intensity; // smooth falloff\n\n          uint8_t bw = (uint8_t)(w * intensity);\n          uint8_t br = (uint8_t)(r * intensity);\n          uint8_t bg = (uint8_t)(g * intensity);\n          uint8_t bb = (uint8_t)(b * intensity);\n\n          uint32_t existing = SEGMENT.getPixelColorXY(px, py);\n          uint32_t newColor = RGBW32(br, bg, bb, bw);\n          SEGMENT.setPixelColorXY(px, py, color_add(existing, newColor, preserveColorRatio ? true : false));\n        }\n      }\n    }\n  }\n}\nstatic const char _data_FX_MODE_2D_LAVALAMP[] PROGMEM = \"Lava Lamp@,# of blobs,Blob size,H. Damping,,,Attract,Keep Color Ratio;;!;2;ix=64,c2=192,o2=1,o3=1,pal=47\";\n\n\n/*\n/  Magma effect\n*   2D magma/lava animation\n*   Adapted from FireLamp_JeeUI implementation (https://github.com/DmytroKorniienko/FireLamp_JeeUI/tree/dev)\n*   Original idea by SottNick, remastered by kostyamat\n*   Adapted to WLED by Bob Loeffler and claude.ai\n*   First slider (speed) is for the speed or flow rate of the moving magma.\n*   Second slider (intensity) is for the height of the magma.\n*   Third slider (lava bombs) is for the number of lava bombs (particles).  The max # is 1/2 the number of columns on the 2D matrix.\n*   Fourth slider (gravity) is for how high the lava bombs will go.\n*   The checkbox (check2) is for whether the lava bombs can be seen in the magma or behind it.\n*/\n\n// Draw the magma\nstatic void drawMagma(const uint16_t width, const uint16_t height, float *ff_y, float *ff_z, uint8_t *shiftHue) {\n  // Noise parameters - adjust these for different magma characteristics\n  // deltaValue: higher = more detailed/turbulent magma\n  // deltaHue: higher = taller magma structures\n  constexpr uint8_t magmaDeltaValue = 12U;\n  constexpr uint8_t magmaDeltaHue   = 10U;\n\n  uint16_t ff_y_int = (uint16_t)*ff_y;\n  uint16_t ff_z_int = (uint16_t)*ff_z;\n\n  for (uint16_t i = 0; i < width; i++) {\n    for (uint16_t j = 0; j < height; j++) {\n      // Generate Perlin noise value (0-255)\n      uint8_t noise = perlin8(i * magmaDeltaValue, (j + ff_y_int + hw_random8(2)) * magmaDeltaHue, ff_z_int);\n      uint8_t paletteIndex = qsub8(noise, shiftHue[j]);  // Apply the vertical fade gradient\n      CRGB col = SEGMENT.color_from_palette(paletteIndex, false, PALETTE_SOLID_WRAP, 0);  // Get color from palette\n      SEGMENT.addPixelColorXY(i, height - 1 - j, col);  // magma rises from bottom of display\n    }\n  }\n}\n\n// Move and draw lava bombs (particles)\nstatic void drawLavaBombs(const uint16_t width, const uint16_t height, float *particleData, float gravity, uint8_t particleCount) {\n  for (uint16_t i = 0; i < particleCount; i++) {\n    uint16_t idx = i * 4;\n    \n    particleData[idx + 3] -= gravity;\n    particleData[idx + 0] += particleData[idx + 2];\n    particleData[idx + 1] += particleData[idx + 3];\n    \n    float posX = particleData[idx + 0];\n    float posY = particleData[idx + 1];\n    \n    if (posY > height + height / 4) {\n      particleData[idx + 3] = -particleData[idx + 3] * 0.8f;\n    }\n    \n    if (posY < (float)(height / 8) - 1.0f || posX < 0 || posX >= width) {\n      particleData[idx + 0] = hw_random(0, width * 100) / 100.0f;\n      particleData[idx + 1] = hw_random(0, height * 25) / 100.0f;\n      particleData[idx + 2] = hw_random(-75, 75) / 100.0f;\n      \n      float baseVelocity = hw_random(60, 120) / 100.0f;\n      if (hw_random8() < 50) {\n        baseVelocity *= 1.6f;\n      }\n      particleData[idx + 3] = baseVelocity;\n      continue;\n    }\n    \n    int16_t xi = (int16_t)posX;\n    int16_t yi = (int16_t)posY;\n    \n    if (xi >= 0 && xi < width && yi >= 0 && yi < height) {\n      // Get a random color from the current palette\n      uint8_t randomIndex = hw_random8(64, 128);\n      CRGB pcolor = ColorFromPaletteWLED(SEGPALETTE, randomIndex, 255, LINEARBLEND);\n\n      // Pre-calculate anti-aliasing weights\n      float xf = posX - xi;\n      float yf = posY - yi;\n      float ix = 1.0f - xf;\n      float iy = 1.0f - yf;\n      \n      uint8_t w0 = 255 * ix * iy;\n      uint8_t w1 = 255 * xf * iy;\n      uint8_t w2 = 255 * ix * yf;\n      uint8_t w3 = 255 * xf * yf;\n      \n      int16_t yFlipped = height - 1 - yi;  // Flip Y coordinate\n  \n      SEGMENT.addPixelColorXY(xi, yFlipped, pcolor.scale8(w0));\n      if (xi + 1 < width) \n        SEGMENT.addPixelColorXY(xi + 1, yFlipped, pcolor.scale8(w1));\n      if (yFlipped - 1 >= 0)\n        SEGMENT.addPixelColorXY(xi, yFlipped - 1, pcolor.scale8(w2));\n      if (xi + 1 < width && yFlipped - 1 >= 0) \n        SEGMENT.addPixelColorXY(xi + 1, yFlipped - 1, pcolor.scale8(w3));\n    }\n  }\n} \n\nstatic void mode_2D_magma(void) {\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC;  // not a 2D set-up\n  const uint16_t width = SEG_W;\n  const uint16_t height = SEG_H;\n  const uint8_t MAGMA_MAX_PARTICLES = width / 2;\n  if (MAGMA_MAX_PARTICLES < 2) FX_FALLBACK_STATIC;  // matrix too narrow for lava bombs\n  constexpr size_t SETTINGS_SUM_BYTES = 4; // 4 bytes for settings sum\n\n  // Allocate memory: particles (4 floats each) + 2 floats for noise counters + shiftHue cache + settingsSum\n  const uint16_t dataSize = (MAGMA_MAX_PARTICLES * 4 + 2) * sizeof(float) + height * sizeof(uint8_t) + SETTINGS_SUM_BYTES;\n  if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC;  // allocation failed\n\n  float* particleData = reinterpret_cast<float*>(SEGENV.data);\n  float* ff_y = &particleData[MAGMA_MAX_PARTICLES * 4];\n  float* ff_z = &particleData[MAGMA_MAX_PARTICLES * 4 + 1];\n  uint32_t* settingsSumPtr = reinterpret_cast<uint32_t*>(&particleData[MAGMA_MAX_PARTICLES * 4 + 2]);\n  uint8_t* shiftHue = reinterpret_cast<uint8_t*>(reinterpret_cast<uint8_t*>(settingsSumPtr) + SETTINGS_SUM_BYTES);\n\n  // Check if settings changed\n  uint32_t settingsKey = (uint32_t)SEGMENT.speed | ((uint32_t)SEGMENT.intensity << 8) |\n      ((uint32_t)SEGMENT.custom1 << 16) | ((uint32_t)SEGMENT.custom2 << 24);\n  bool settingsChanged = (*settingsSumPtr != settingsKey);\n \n  if (SEGENV.call == 0 || settingsChanged) {\n    // Intensity slider controls magma height\n    uint16_t intensity = SEGMENT.intensity;\n    uint16_t fadeRange = map(intensity, 0, 255, height / 3, height);\n\n    // shiftHue controls the vertical color gradient (magma fades out toward top)\n    for (uint16_t j = 0; j < height; j++) {\n      if (j < fadeRange) {\n        // prevent division issues and ensure smooth gradient\n        if (fadeRange > 1) {\n          shiftHue[j] = (uint8_t)(j * 255 / (fadeRange - 1));\n        } else {\n          shiftHue[j] = 0;  // Single row magma = no fade\n        }\n      } else {\n        shiftHue[j] = 255;\n      }\n    }\n\n    // Initialize all particles\n    for (uint16_t i = 0; i < MAGMA_MAX_PARTICLES; i++) {\n      uint16_t idx = i * 4;\n      particleData[idx + 0] = hw_random(0, width * 100) / 100.0f;\n      particleData[idx + 1] = hw_random(0, height * 25) / 100.0f;\n      particleData[idx + 2] = hw_random(-75, 75) / 100.0f;\n      \n      float baseVelocity = hw_random(60, 120) / 100.0f;\n      if (hw_random8() < 50) {\n        baseVelocity *= 1.6f;\n      }\n      particleData[idx + 3] = baseVelocity;\n    }\n    *ff_y = 0.0f;\n    *ff_z = 0.0f;\n    *settingsSumPtr = settingsKey;\n  }\n\n  if (!shiftHue) FX_FALLBACK_STATIC;   // safety check\n\n  // Speed control\n  float speedfactor = SEGMENT.speed / 255.0f;\n  speedfactor = speedfactor * speedfactor * 1.5f;\n  if (speedfactor < 0.001f) speedfactor = 0.001f;\n\n  // Gravity control\n  float gravity = map(SEGMENT.custom2, 0, 255, 5, 20) / 100.0f;\n  \n  // Number of particles (lava bombs)\n  uint8_t particleCount = map(SEGMENT.custom1, 0, 255, 0, MAGMA_MAX_PARTICLES);\n  particleCount = constrain(particleCount, 0, MAGMA_MAX_PARTICLES);\n\n  // Draw lava bombs in front of magma (or behind it)\n  if (SEGMENT.check2) {\n    drawMagma(width, height, ff_y, ff_z, shiftHue);\n    SEGMENT.fadeToBlackBy(70);    // Dim the entire display to create trailing effect\n    if (particleCount > 0) drawLavaBombs(width, height, particleData, gravity, particleCount);\n  }\n  else {\n    if (particleCount > 0) drawLavaBombs(width, height, particleData, gravity, particleCount);\n    SEGMENT.fadeToBlackBy(70);    // Dim the entire display to create trailing effect\n    drawMagma(width, height, ff_y, ff_z, shiftHue);\n  }\n\n  // noise counters based on speed slider\n  *ff_y += speedfactor * 2.0f;\n  *ff_z += speedfactor;\n\n  SEGENV.step++;\n}\nstatic const char _data_FX_MODE_2D_MAGMA[] PROGMEM = \"Magma@Flow rate,Magma height,Lava bombs,Gravity,,,Bombs in front;;!;2;ix=192,c2=32,o2=1,pal=35\";\n\n\n/*\n/  Ants (created by making modifications to the Rolling Balls code) - Bob Loeffler 2025\n*   First slider is for the ants' speed.\n*   Second slider is for the # of ants.\n*   Third slider is for the Ants' size.\n*   Fourth slider (custom2) is for blurring the LEDs in the segment.\n*   Checkbox1 is for Gathering food (enabled if you want the ants to gather food, disabled if they are just walking).\n*     We will switch directions when they get to the beginning or end of the segment when gathering food.\n*     When gathering food, the Pass By option will automatically be enabled so they can drop off their food easier (and look for more food).\n*   Checkbox2 is for Smear mode (enabled is smear pixel colors, disabled is no smearing)\n*   Checkbox3 is for whether the ants will bump into each other (disabled) or just pass by each other (enabled)\n*/\n\n// Ant structure representing each ant's state\nstruct Ant {\n  unsigned long lastBumpUpdate;  // the last time the ant bumped into another ant\n  bool hasFood;\n  float velocity;\n  float position;  // (0.0 to 1.0 range)\n};\n\nconstexpr unsigned MAX_ANTS = 32;\nconstexpr float MIN_COLLISION_TIME_MS = 2.0f;\nconstexpr float VELOCITY_MIN = 2.0f;\nconstexpr float VELOCITY_MAX = 10.0f;\nconstexpr unsigned ANT_SIZE_MIN = 1;\nconstexpr unsigned ANT_SIZE_MAX = 20;\n\n// Helper function to get food pixel color based on ant and background colors\nstatic uint32_t getFoodColor(uint32_t antColor, uint32_t backgroundColor) {\n  if (antColor == WHITE)\n    return (backgroundColor == YELLOW) ? GRAY : YELLOW;\n  return (backgroundColor == WHITE) ? YELLOW : WHITE;\n}\n\n// Helper function to handle ant boundary wrapping or bouncing\nstatic void handleBoundary(Ant& ant, float& position, bool gatherFood, bool atStart, unsigned long currentTime) {\n  if (gatherFood) {\n    // Bounce mode: reverse direction and update food status\n    position = atStart ? 0.0f : 1.0f;\n    ant.velocity = -ant.velocity;\n    ant.lastBumpUpdate = currentTime;\n    ant.position = position;\n    ant.hasFood = atStart;  // Has food when leaving start, drops it at end\n  } else {\n    // Wrap mode: teleport to opposite end\n    position = atStart ? 1.0f : 0.0f;\n    ant.lastBumpUpdate = currentTime;\n    ant.position = position;\n  }\n}\n\n// Helper function to calculate ant color\nstatic uint32_t getAntColor(int antIndex, int numAnts, bool usePalette) {\n  if (usePalette)\n    return SEGMENT.color_from_palette(antIndex * 255 / numAnts, false, (paletteBlend == 1 || paletteBlend == 3), 255);\n  // Alternate between two colors for default palette\n  return (antIndex % 3 == 1) ? SEGCOLOR(0) : SEGCOLOR(2);\n}\n\n// Helper function to render a single ant pixel with food handling\nstatic void renderAntPixel(int pixelIndex, int pixelOffset, int antSize, const Ant& ant, uint32_t antColor, uint32_t backgroundColor, bool gatherFood) {\n  bool isMovingBackward = (ant.velocity < 0);\n  bool isFoodPixel = gatherFood && ant.hasFood && ((isMovingBackward && pixelOffset == 0) || (!isMovingBackward && pixelOffset == antSize - 1));\n  if (isFoodPixel) {\n    SEGMENT.setPixelColor(pixelIndex, getFoodColor(antColor, backgroundColor));\n  } else {\n    SEGMENT.setPixelColor(pixelIndex, antColor);\n  }\n}\n\nstatic void mode_ants(void) {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n\n  // Allocate memory for ant data\n  uint32_t backgroundColor = SEGCOLOR(1);\n  unsigned dataSize = sizeof(Ant) * MAX_ANTS;\n  if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC;  // Allocation failed\n\n  Ant* ants = reinterpret_cast<Ant*>(SEGENV.data);\n\n  // Extract configuration from segment settings\n  unsigned numAnts = min(1 + (SEGLEN * SEGMENT.intensity >> 12), MAX_ANTS);\n  bool gatherFood = SEGMENT.check1;\n  bool SmearMode = SEGMENT.check2;\n  bool passBy = SEGMENT.check3 || gatherFood;  // global no‑collision when gathering food is enabled\n  unsigned antSize = map(SEGMENT.custom1, 0, 255, ANT_SIZE_MIN, ANT_SIZE_MAX) + (gatherFood ? 1 : 0);\n\n  // Initialize ants on first call\n  if (SEGENV.call == 0) {\n    int confusedAntIndex = hw_random(0, numAnts);   // the first random ant to go backwards\n\n    for (int i = 0; i < MAX_ANTS; i++) {\n      ants[i].lastBumpUpdate = strip.now;\n\n      // Random velocity\n      float velocity = VELOCITY_MIN + (VELOCITY_MAX - VELOCITY_MIN) * hw_random16(1000, 5000) / 5000.0f;\n      // One random ant moves in opposite direction\n      ants[i].velocity = (i == confusedAntIndex) ? -velocity : velocity;\n      // Random starting position (0.0 to 1.0)\n      ants[i].position = hw_random16(0, 10000) / 10000.0f;\n      // Ants don't have food yet\n      ants[i].hasFood = false;\n    }\n  }\n\n  // Calculate time conversion factor based on speed slider\n  float timeConversionFactor = float(scale8(8, 255 - SEGMENT.speed) + 1) * 20000.0f;\n\n  // Clear background if not in Smear mode\n  if (!SmearMode) SEGMENT.fill(backgroundColor);\n\n  // Update and render each ant\n  for (int i = 0; i < numAnts; i++) {\n    float timeSinceLastUpdate = float(int(strip.now - ants[i].lastBumpUpdate)) / timeConversionFactor;\n    float newPosition = ants[i].position + ants[i].velocity * timeSinceLastUpdate;\n\n    // Reset ants that wandered too far off-track (e.g., after intensity change)\n    if (newPosition < -0.5f || newPosition > 1.5f) {\n      newPosition = ants[i].position = hw_random16(0, 10000) / 10000.0f;\n      ants[i].lastBumpUpdate = strip.now;\n    }\n\n    // Handle boundary conditions (bounce or wrap)\n    if (newPosition <= 0.0f && ants[i].velocity < 0.0f) {\n      handleBoundary(ants[i], newPosition, gatherFood, true, strip.now);\n    } else if (newPosition >= 1.0f && ants[i].velocity > 0.0f) {\n      handleBoundary(ants[i], newPosition, gatherFood, false, strip.now);\n    }\n\n    // Handle collisions between ants (if not passing by)\n    if (!passBy) {\n      for (int j = i + 1; j < numAnts; j++) {\n        if (fabsf(ants[j].velocity - ants[i].velocity) < 0.001f) continue;  // Moving in same direction at same speed; avoids tiny denominators\n\n        // Calculate collision time using physics -  collisionTime formula adapted from rolling_balls\n        float timeOffset = float(int(ants[j].lastBumpUpdate - ants[i].lastBumpUpdate));\n        float collisionTime = (timeConversionFactor * (ants[i].position - ants[j].position) + ants[i].velocity * timeOffset) / (ants[j].velocity - ants[i].velocity);\n\n        // Check if collision occurred in valid time window\n        float timeSinceJ = float(int(strip.now - ants[j].lastBumpUpdate));\n        if (collisionTime > MIN_COLLISION_TIME_MS && collisionTime < timeSinceJ) {\n          // Update positions to collision point\n          float adjustedTime = (collisionTime + float(int(ants[j].lastBumpUpdate - ants[i].lastBumpUpdate))) / timeConversionFactor;\n          ants[i].position += ants[i].velocity * adjustedTime;\n          ants[j].position = ants[i].position;\n\n          // Update collision time\n          unsigned long collisionMoment = static_cast<unsigned long>(collisionTime + 0.5f) + ants[j].lastBumpUpdate;\n          ants[i].lastBumpUpdate = collisionMoment;\n          ants[j].lastBumpUpdate = collisionMoment;\n\n          // Reverse the ant with greater speed magnitude\n          if (fabsf(ants[i].velocity) > fabsf(ants[j].velocity)) {\n            ants[i].velocity = -ants[i].velocity;\n          } else {\n            ants[j].velocity = -ants[j].velocity;\n          }\n\n          // Recalculate position after collision\n          newPosition = ants[i].position + ants[i].velocity * float(int(strip.now - ants[i].lastBumpUpdate)) / timeConversionFactor;\n        }\n      }\n    }\n\n    // Clamp position to valid range\n    newPosition = constrain(newPosition, 0.0f, 1.0f);\n    unsigned pixelPosition = roundf(newPosition * (SEGLEN - 1));\n\n    // Determine ant color\n    uint32_t antColor = getAntColor(i, numAnts, SEGMENT.palette != 0);\n\n    // Render ant pixels\n    for (int pixelOffset = 0; pixelOffset < antSize; pixelOffset++) {\n      unsigned currentPixel = pixelPosition + pixelOffset;\n      if (currentPixel >= SEGLEN) break;\n      renderAntPixel(currentPixel, pixelOffset, antSize, ants[i], antColor, backgroundColor, gatherFood);\n    }\n\n    // Update ant state\n    ants[i].lastBumpUpdate = strip.now;\n    ants[i].position = newPosition;\n  }\n\n  SEGMENT.blur(SEGMENT.custom2>>1);\n}\nstatic const char _data_FX_MODE_ANTS[] PROGMEM = \"Ants@Ant speed,# of ants,Ant size,Blur,,Gathering food,Smear,Pass by;!,!,!;!;1;sx=192,ix=255,c1=32,c2=0,o1=1,o3=1\";\n\n\n/*\n/  Morse Code by Bob Loeffler\n*   Adapted from code by automaticaddison.com and then optimized by claude.ai\n*   aux0 is the pattern offset for scrolling\n*   aux1 saves settings: check2 (1 bit), check3 (1 bit), text hash (4 bits) and pattern length (10 bits)\n*   The first slider (sx) selects the scrolling speed\n*   The second slider selects the color mode (lower half selects color wheel, upper half selects color palettes)\n*   Checkbox1 displays all letters in a word with the same color\n*   Checkbox2 displays punctuation or not\n*   Checkbox3 displays the End-of-message code or not\n*   We get the text from the SEGMENT.name and convert it to morse code\n*   This effect uses a bit array, instead of bool array, for efficient storage - 8x memory reduction (128 bytes vs 1024 bytes)\n*\n*   Morse Code rules:\n*    - a dot is 1 pixel/LED; a dash is 3 pixels/LEDs\n*    - there is 1 space between each dot or dash that make up a letter/number/punctuation\n*    - there are 3 spaces between each letter/number/punctuation\n*    - there are 7 spaces between each word\n*/\n\n// Bit manipulation macros\n#define SET_BIT8(arr, i) ((arr)[(i) >> 3] |= (1 << ((i) & 7)))\n#define GET_BIT8(arr, i) (((arr)[(i) >> 3] & (1 << ((i) & 7))) != 0)\n\n// Build morse code pattern into a buffer\nstatic void build_morsecode_pattern(const char *morse_code, uint8_t *pattern, uint8_t *wordIndex, uint16_t &index, uint8_t currentWord, int maxSize) {\n  const char *c = morse_code;\n  \n  // Build the dots and dashes into pattern array\n  while (*c != '\\0') {\n    // it's a dot which is 1 pixel\n    if (*c == '.') {\n      if (index >= maxSize - 1) return;\n      SET_BIT8(pattern, index);\n      wordIndex[index] = currentWord;\n      index++;\n    }\n    else { // Must be a dash which is 3 pixels\n      if (index >= maxSize - 3) return;\n      SET_BIT8(pattern, index);\n      wordIndex[index] = currentWord;\n      index++;\n      SET_BIT8(pattern, index);\n      wordIndex[index] = currentWord;\n      index++;\n      SET_BIT8(pattern, index);\n      wordIndex[index] = currentWord;\n      index++;\n    }\n\n    c++;\n\n    // 1 space between parts of a letter/number/punctuation (but not after the last one)\n    if (*c != '\\0') {\n      if (index >= maxSize) return;\n      wordIndex[index] = currentWord;\n      index++;\n    }\n  }\n\n  // 3 spaces between two letters/numbers/punctuation\n  if (index >= maxSize - 2) return;\n  wordIndex[index] = currentWord;\n  index++;\n  if (index >= maxSize - 1) return;\n  wordIndex[index] = currentWord;\n  index++;\n  if (index >= maxSize) return;\n  wordIndex[index] = currentWord;\n  index++;\n}\n\nstatic void mode_morsecode(void) {\n  if (SEGLEN < 1) FX_FALLBACK_STATIC;\n  \n  // A-Z in Morse Code\n  static const char * letters[] = {\".-\", \"-...\", \"-.-.\", \"-..\", \".\", \"..-.\", \"--.\", \"....\", \"..\", \".---\", \"-.-\", \".-..\", \"--\",\n                     \"-.\", \"---\", \".--.\", \"--.-\", \".-.\", \"...\", \"-\", \"..-\", \"...-\", \".--\", \"-..-\", \"-.--\", \"--..\"};\n  // 0-9 in Morse Code\n  static const char * numbers[] = {\"-----\", \".----\", \"..---\", \"...--\", \"....-\", \".....\", \"-....\", \"--...\", \"---..\", \"----.\"};\n\n  // Punctuation in Morse Code\n  struct PunctuationMapping {\n    char character;\n    const char* code;\n  };\n\n  static const PunctuationMapping punctuation[] = {\n    {'.', \".-.-.-\"}, {',', \"--..--\"}, {'?', \"..--..\"}, \n    {':', \"---...\"}, {'-', \"-....-\"}, {'!', \"-.-.--\"},\n    {'&', \".-...\"}, {'@', \".--.-.\"}, {')', \"-.--.-\"},\n    {'(', \"-.--.\"}, {'/', \"-..-.\"}, {'\\'', \".----.\"}\n  };\n\n  // Get the text to display\n  char text[WLED_MAX_SEGNAME_LEN+1] = {'\\0'};\n  size_t len = 0;\n\n  if (SEGMENT.name) len = strlen(SEGMENT.name);\n  if (len == 0) {\n    strcpy_P(text, PSTR(\"I Love WLED!\"));\n  } else {\n    strcpy(text, SEGMENT.name);\n  }\n\n  // Convert to uppercase in place\n  for (char *p = text; *p; p++) {\n    *p = toupper(*p);\n  }\n\n  // Allocate per-segment storage for pattern (1023 bits = 127 bytes) + word index array (1024 bytes) + word count (1 byte)\n  constexpr size_t MORSECODE_MAX_PATTERN_SIZE = 1023;\n  constexpr size_t MORSECODE_PATTERN_BYTES = (MORSECODE_MAX_PATTERN_SIZE + 7) / 8; // 128 bytes\n  constexpr size_t MORSECODE_WORD_INDEX_BYTES = MORSECODE_MAX_PATTERN_SIZE; // 1 byte per bit position\n  constexpr size_t MORSECODE_WORD_COUNT_BYTES = 1; // 1 byte for word count\n  if (!SEGENV.allocateData(MORSECODE_PATTERN_BYTES + MORSECODE_WORD_INDEX_BYTES + MORSECODE_WORD_COUNT_BYTES)) FX_FALLBACK_STATIC;\n  uint8_t* morsecodePattern = reinterpret_cast<uint8_t*>(SEGENV.data);\n  uint8_t* wordIndexArray = reinterpret_cast<uint8_t*>(SEGENV.data + MORSECODE_PATTERN_BYTES);\n  uint8_t* wordCountPtr = reinterpret_cast<uint8_t*>(SEGENV.data + MORSECODE_PATTERN_BYTES + MORSECODE_WORD_INDEX_BYTES);\n\n  // SEGENV.aux1 stores: [bit 15: check2] [bit 14: check3] [bits 10-13: text hash (4 bits)] [bits 0-9: pattern length]\n  bool lastCheck2 = (SEGENV.aux1 & 0x8000) != 0;\n  bool lastCheck3 = (SEGENV.aux1 & 0x4000) != 0;\n  uint16_t lastHashBits = (SEGENV.aux1 >> 10) & 0xF; // 4 bits of hash\n  uint16_t patternLength = SEGENV.aux1 & 0x3FF; // Lower 10 bits for length (up to 1023)\n\n  // Compute text hash\n  uint16_t textHash = 0;\n  for (char *p = text; *p; p++) {\n    textHash = ((textHash << 5) + textHash) + *p;\n  }\n  uint16_t currentHashBits = (textHash >> 12) & 0xF; // Use upper 4 bits of hash\n\n  bool textChanged = (currentHashBits != lastHashBits) && (SEGENV.call > 0);\n\n  // Check if we need to rebuild the pattern\n  bool needsRebuild = (SEGENV.call == 0) || textChanged || (SEGMENT.check2 != lastCheck2) || (SEGMENT.check3 != lastCheck3);\n\n  // Initialize on first call or rebuild pattern\n  if (needsRebuild) {\n    patternLength = 0;\n\n    // Clear the bit array and word index array first\n    memset(morsecodePattern, 0, MORSECODE_PATTERN_BYTES);\n    memset(wordIndexArray, 0, MORSECODE_WORD_INDEX_BYTES);\n\n    // Track current word index\n    uint8_t currentWordIndex = 0;\n\n    // Build complete morse code pattern\n    for (char *c = text; *c; c++) {\n      if (patternLength >= MORSECODE_MAX_PATTERN_SIZE - 10) break;\n\n      if (*c >= 'A' && *c <= 'Z') {\n        build_morsecode_pattern(letters[*c - 'A'], morsecodePattern, wordIndexArray, patternLength, currentWordIndex, MORSECODE_MAX_PATTERN_SIZE);\n      }\n      else if (*c >= '0' && *c <= '9') {\n        build_morsecode_pattern(numbers[*c - '0'], morsecodePattern, wordIndexArray, patternLength, currentWordIndex, MORSECODE_MAX_PATTERN_SIZE);\n      }\n      else if (*c == ' ') {\n        // Space between words - increment word index for next word\n        currentWordIndex++;\n        // Add 4 additional spaces (7 total with the 3 after each letter)\n        for (int x = 0; x < 4; x++) {\n          if (patternLength >= MORSECODE_MAX_PATTERN_SIZE) break;\n          wordIndexArray[patternLength] = currentWordIndex;\n          patternLength++;\n        }\n      }\n      else if (SEGMENT.check2) {\n        const char *punctuationCode = nullptr;\n        for (const auto& p : punctuation) {\n          if (*c == p.character) {\n            punctuationCode = p.code;\n            break;\n          }\n        }\n        if (punctuationCode) {\n          build_morsecode_pattern(punctuationCode, morsecodePattern, wordIndexArray, patternLength, currentWordIndex, MORSECODE_MAX_PATTERN_SIZE);\n        }\n      }\n    }\n\n    if (SEGMENT.check3) {\n      build_morsecode_pattern(\".-.-.\", morsecodePattern, wordIndexArray, patternLength, currentWordIndex, MORSECODE_MAX_PATTERN_SIZE);\n    }\n\n    for (int x = 0; x < 7; x++) {\n      if (patternLength >= MORSECODE_MAX_PATTERN_SIZE) break;\n      wordIndexArray[patternLength] = currentWordIndex;\n      patternLength++;\n    }\n\n    // Store the total number of words (currentWordIndex + 1 because it's 0-indexed)\n    *wordCountPtr = currentWordIndex + 1;\n\n    // Store pattern length, checkbox states, and hash bits in aux1\n    SEGENV.aux1 = patternLength | (currentHashBits << 10) | (SEGMENT.check2 ? 0x8000 : 0) | (SEGMENT.check3 ? 0x4000 : 0);\n\n    // Reset the scroll offset\n    SEGENV.aux0 = 0;\n  }\n\n  // if pattern is empty for some reason, display black background only\n  if (patternLength == 0) {\n    SEGMENT.fill(BLACK);\n    return;\n  }\n\n  // Update offset to make the morse code scroll\n  // Use step for scroll timing only\n  uint32_t cycleTime = 50 + (255 - SEGMENT.speed)*3;\n  uint32_t it = strip.now / cycleTime;\n  if (SEGENV.step != it) {\n    SEGENV.aux0++;\n    SEGENV.step = it;\n  }\n\n  // Clear background\n  SEGMENT.fill(BLACK);\n\n  // Draw the scrolling pattern\n  int offset = SEGENV.aux0 % patternLength;\n\n  // Get the word count and calculate color spacing\n  uint8_t wordCount = *wordCountPtr;\n  if (wordCount == 0) wordCount = 1;\n  uint8_t colorSpacing = 255 / wordCount; // Distribute colors evenly across color wheel/palette\n\n  for (int i = 0; i < SEGLEN; i++) {\n    int patternIndex = (offset + i) % patternLength;\n    if (GET_BIT8(morsecodePattern, patternIndex)) {\n      uint8_t wordIdx = wordIndexArray[patternIndex];\n      if (SEGMENT.check1) {  // make each word a separate color\n        if (SEGMENT.custom3 < 16)\n          // use word index to select base color, add slight offset for animation\n          SEGMENT.setPixelColor(i, SEGMENT.color_wheel((wordIdx * colorSpacing) + (SEGENV.aux0 / 4)));\n        else\n          SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(wordIdx * colorSpacing, true, PALETTE_SOLID_WRAP, 0));\n      }\n      else {\n        if (SEGMENT.custom3 < 16)\n          SEGMENT.setPixelColor(i, SEGMENT.color_wheel(SEGENV.aux0 + i));\n        else\n          SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0));\n      }\n    }\n  }\n}\nstatic const char _data_FX_MODE_MORSECODE[] PROGMEM = \"Morse Code@Speed,,,,Color mode,Color by Word,Punctuation,EndOfMessage;;!;1;sx=192,c3=8,o1=1,o2=1\";\n\n\n/////////////////////\n//  UserMod Class  //\n/////////////////////\n\nclass UserFxUsermod : public Usermod {\n private:\n public:\n  void setup() override {\n    strip.addEffect(255, &mode_diffusionfire, _data_FX_MODE_DIFFUSIONFIRE);\n    strip.addEffect(255, &mode_spinning_wheel, _data_FX_MODE_SPINNINGWHEEL);\n    strip.addEffect(255, &mode_2D_lavalamp, _data_FX_MODE_2D_LAVALAMP);\n    strip.addEffect(255, &mode_2D_magma, _data_FX_MODE_2D_MAGMA);\n    strip.addEffect(255, &mode_ants, _data_FX_MODE_ANTS);\n    strip.addEffect(255, &mode_morsecode, _data_FX_MODE_MORSECODE);\n\n    ////////////////////////////////////////\n    //  add your effect function(s) here  //\n    ////////////////////////////////////////\n\n    // use id=255 for all custom user FX (the final id is assigned when adding the effect)\n\n    // strip.addEffect(255, &mode_your_effect, _data_FX_MODE_YOUR_EFFECT);\n    // strip.addEffect(255, &mode_your_effect2, _data_FX_MODE_YOUR_EFFECT2);\n    // strip.addEffect(255, &mode_your_effect3, _data_FX_MODE_YOUR_EFFECT3);\n  }\n\n  \n  ///////////////////////////////////////////////////////////////////////////////////////////////\n  //  If you want configuration options in the usermod settings page, implement these methods  //\n  ///////////////////////////////////////////////////////////////////////////////////////////////\n\n  // void addToConfig(JsonObject& root) override\n  // {\n  //   JsonObject top = root.createNestedObject(FPSTR(\"User FX\"));\n  //   top[\"myConfigValue\"] = myConfigValue;\n  // }\n  // bool readFromConfig(JsonObject& root) override\n  // {\n  //   JsonObject top = root[FPSTR(\"User FX\")];\n  //   bool configComplete = !top.isNull();\n  //   configComplete &= getJsonValue(top[\"myConfigValue\"], myConfigValue);\n  //   return configComplete;\n  // }\n\n  void loop() override {} // nothing to do in the loop\n  uint16_t getId() override { return USERMOD_ID_USER_FX; }\n};\n\nstatic UserFxUsermod user_fx;\nREGISTER_USERMOD(user_fx);\n"
  },
  {
    "path": "usermods/usermod_rotary_brightness_color/README.md",
    "content": "# Rotary Encoder (Brightness and Color)\n\nV2 usermod that enables changing brightness and color using a rotary encoder \nchange between modes by pressing a button (many encoders have one included)\n\nit will wait for AUTOSAVE_SETTLE_MS milliseconds. a \"settle\" \nperiod in case there are other changes (any change will \nextend the \"settle\" period).\n\nIt will additionally load preset AUTOSAVE_PRESET_NUM at startup.\nduring the first `loop()`.  Reasoning below.\n\nAutoSaveUsermod is standalone, but if FourLineDisplayUsermod is installed, it will notify the user of the saved changes.\n\nNote: WLED doesn't respect the brightness of the preset being auto loaded, so the AutoSaveUsermod will set the AUTOSAVE_PRESET_NUM preset in the first loop, so brightness IS honored. This means WLED will effectively ignore Default brightness and Apply N preset at boot when the AutoSaveUsermod is installed.\n\n## Installation\n\ndefine `USERMOD_ROTARY_ENCODER_BRIGHTNESS_COLOR` e.g.\n\n`#define USERMOD_ROTARY_ENCODER_BRIGHTNESS_COLOR` in my_config.h\n\nor add `-D USERMOD_ROTARY_ENCODER_BRIGHTNESS_COLOR` to `build_flags` in platformio_override.ini\n\n### Define Your Options\n\nOpen Usermod Settings in WLED to change settings:\n\n`fadeAmount` - how many points to fade the Neopixel with each step of the rotary encoder (default 5)\n`pin[3]` - pins to connect to the rotary encoder:\n- `pin[0]` is pin A on your rotary encoder\n- `pin[1]` is pin B on your rotary encoder\n- `pin[2]` is the button on your rotary encoder (optional, set to -1 to disable the button and the rotary encoder will control brightness only)\n\n### PlatformIO requirements\n\nNo special requirements.\n\n## Change Log\n- 2021-07<br>\nUpgraded to work with the latest WLED code, and make settings configurable in Usermod Settings\n- 2025-03<br>\nUpgraded to work with the latest WLED code\n"
  },
  {
    "path": "usermods/usermod_rotary_brightness_color/library.json",
    "content": "{\n  \"name\": \"usermod_rotary_brightness_color\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.cpp",
    "content": "#include \"wled.h\"\n\n//v2 usermod that allows to change brightness and color using a rotary encoder, \n//change between modes by pressing a button (many encoders have one included)\nclass RotaryEncoderBrightnessColor : public Usermod\n{\nprivate:\n  //Private class members. You can declare variables and functions only accessible to your usermod here\n  unsigned long lastTime = 0;\n  unsigned long currentTime;\n  unsigned long loopTime;\n\n  unsigned char select_state = 0; // 0 = brightness 1 = color\n  unsigned char button_state = HIGH;\n  unsigned char prev_button_state = HIGH;\n  CRGB fastled_col;\n  CHSV prim_hsv;\n  int16_t new_val;\n\n  unsigned char Enc_A;\n  unsigned char Enc_B;\n  unsigned char Enc_A_prev = 0;\n\n  // private class members configurable by Usermod Settings (defaults set inside readFromConfig())\n  int8_t pins[3]; // pins[0] = DT from encoder, pins[1] = CLK from encoder, pins[2] = CLK from encoder (optional)\n  int fadeAmount; // how many points to fade the Neopixel with each step\n\npublic:\n  //Functions called by WLED\n\n  /*\n   * setup() is called once at boot. WiFi is not yet connected at this point.\n   * You can use it to initialize variables, sensors or similar.\n   */\n  void setup()\n  {\n    //Serial.println(\"Hello from my usermod!\");\n    pinMode(pins[0], INPUT_PULLUP);\n    pinMode(pins[1], INPUT_PULLUP);\n    if(pins[2] >= 0) pinMode(pins[2], INPUT_PULLUP);\n    currentTime = millis();\n    loopTime = currentTime;\n  }\n\n  /*\n   * loop() is called continuously. Here you can check for events, read sensors, etc.\n   * \n   * Tips:\n   * 1. You can use \"if (WLED_CONNECTED)\" to check for a successful network connection.\n   *    Additionally, \"if (WLED_MQTT_CONNECTED)\" is available to check for a connection to an MQTT broker.\n   * \n   * 2. Try to avoid using the delay() function. NEVER use delays longer than 10 milliseconds.\n   *    Instead, use a timer check as shown here.\n   */\n  void loop()\n  {\n    currentTime = millis(); // get the current elapsed time\n\n    if (currentTime >= (loopTime + 2)) // 2ms since last check of encoder = 500Hz\n    {\n      if(pins[2] >= 0) {\n        button_state = digitalRead(pins[2]);\n        if (prev_button_state != button_state)\n        {\n          if (button_state == LOW)\n          {\n            if (select_state == 1)\n            {\n              select_state = 0;\n            }\n            else\n            {\n              select_state = 1;\n            }\n            prev_button_state = button_state;\n          }\n          else\n          {\n            prev_button_state = button_state;\n          }\n        }\n      }\n      int Enc_A = digitalRead(pins[0]); // Read encoder pins\n      int Enc_B = digitalRead(pins[1]);\n      if ((!Enc_A) && (Enc_A_prev))\n      { // A has gone from high to low\n        if (Enc_B == HIGH)\n        { // B is high so clockwise\n          if (select_state == 0)\n          {\n            if (bri + fadeAmount <= 255)\n              bri += fadeAmount; // increase the brightness, dont go over 255\n          }\n          else\n          {\n            fastled_col.red = colPri[0];\n            fastled_col.green = colPri[1];\n            fastled_col.blue = colPri[2];\n            prim_hsv = rgb2hsv_approximate(fastled_col);\n            new_val = (int16_t)prim_hsv.h + fadeAmount;\n            if (new_val > 255)\n              new_val -= 255; // roll-over if  bigger than 255\n            if (new_val < 0)\n              new_val += 255; // roll-over if smaller than 0\n            prim_hsv.h = (byte)new_val;\n            hsv2rgb_rainbow(prim_hsv, fastled_col);\n            colPri[0] = fastled_col.red;\n            colPri[1] = fastled_col.green;\n            colPri[2] = fastled_col.blue;\n          }\n        }\n        else if (Enc_B == LOW)\n        { // B is low so counter-clockwise\n          if (select_state == 0)\n          {\n            if (bri - fadeAmount >= 0)\n              bri -= fadeAmount; // decrease the brightness, dont go below 0\n          }\n          else\n          {\n            fastled_col.red = colPri[0];\n            fastled_col.green = colPri[1];\n            fastled_col.blue = colPri[2];\n            prim_hsv = rgb2hsv_approximate(fastled_col);\n            new_val = (int16_t)prim_hsv.h - fadeAmount;\n            if (new_val > 255)\n              new_val -= 255; // roll-over if  bigger than 255\n            if (new_val < 0)\n              new_val += 255; // roll-over if smaller than 0\n            prim_hsv.h = (byte)new_val;\n            hsv2rgb_rainbow(prim_hsv, fastled_col);\n            colPri[0] = fastled_col.red;\n            colPri[1] = fastled_col.green;\n            colPri[2] = fastled_col.blue;\n          }\n        }\n        //call for notifier -> 0: init 1: direct change 2: button 3: notification 4: nightlight 5: other (No notification)\n        // 6: fx changed 7: hue 8: preset cycle 9: blynk 10: alexa\n        colorUpdated(CALL_MODE_BUTTON);\n        updateInterfaces(CALL_MODE_BUTTON);\n      }\n      Enc_A_prev = Enc_A;     // Store value of A for next time\n      loopTime = currentTime; // Updates loopTime\n    }\n  }\n\n  void addToConfig(JsonObject& root)\n  {\n    JsonObject top = root.createNestedObject(\"rotEncBrightness\");\n    top[\"fadeAmount\"] = fadeAmount;\n    JsonArray pinArray = top.createNestedArray(\"pin\");\n    pinArray.add(pins[0]);\n    pinArray.add(pins[1]); \n    pinArray.add(pins[2]); \n  }\n\n  /* \n   * This example uses a more robust method of checking for missing values in the config, and setting back to defaults:\n   * - The getJsonValue() function copies the value to the variable only if the key requested is present, returning false with no copy if the value isn't present\n   * - configComplete is used to return false if any value is missing, not just if the main object is missing\n   * - The defaults are loaded every time readFromConfig() is run, not just once after boot\n   * \n   * This ensures that missing values are added to the config, with their default values, in the rare but plausible cases of:\n   * - a single value being missing at boot, e.g. if the Usermod was upgraded and a new setting was added\n   * - a single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed)\n   * \n   * If configComplete is false, the default values are already set, and by returning false, WLED now knows it needs to save the defaults by calling addToConfig()\n   */\n  bool readFromConfig(JsonObject& root)\n  {\n    // set defaults here, they will be set before setup() is called, and if any values parsed from ArduinoJson below are missing, the default will be used instead\n    fadeAmount = 5;\n    pins[0] = -1;\n    pins[1] = -1;\n    pins[2] = -1;\n\n    JsonObject top = root[\"rotEncBrightness\"];\n\n    bool configComplete = !top.isNull();\n    configComplete &= getJsonValue(top[\"fadeAmount\"], fadeAmount);\n    configComplete &= getJsonValue(top[\"pin\"][0], pins[0]);\n    configComplete &= getJsonValue(top[\"pin\"][1], pins[1]);\n    configComplete &= getJsonValue(top[\"pin\"][2], pins[2]);\n\n    return configComplete;\n  }\n};\n\n\nstatic RotaryEncoderBrightnessColor usermod_rotary_brightness_color;\nREGISTER_USERMOD(usermod_rotary_brightness_color);"
  },
  {
    "path": "usermods/usermod_v2_HttpPullLightControl/library.json",
    "content": "{\n  \"name\": \"usermod_v2_HttpPullLightControl\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/usermod_v2_HttpPullLightControl/readme.md",
    "content": "# usermod_v2_HttpPullLightControl\n\nThe `usermod_v2_HttpPullLightControl` is a custom user module for WLED that enables remote control over the lighting state and color through HTTP requests. It periodically polls a specified URL to obtain a JSON response containing instructions for controlling individual lights.\n\n## Features\n\n* Configure the URL endpoint (only support HTTP for now, no HTTPS) and polling interval via the WLED user interface.\n* 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/)\n* The ability to control the brightness of all lights and the state (on/off) and color of individual lights remotely.\n* Start or stop an effect and when you run the same effect when its's already running, it won't restart.\n* The ability to control all these settings per segment.\n* Remotely turn on/off relays, change segments or presets.\n* Unique ID generation based on the device's MAC address and a configurable salt value, appended to the request URL for identification.\n\n## Configuration\n\n* Enable the `usermod_v2_HttpPullLightControl` via the WLED user interface.\n* Specify the URL endpoint and polling interval.\n\n## JSON Format and examples\n\n* 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.\n\n* 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/)\nAfter 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.\n\n* 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.\n`{\n  \"seg\":\n    {\n      \"i\": [\n        0, \"FF0000\",\n        12, \"00FF00\",\n        14, \"0000FF\"\n      ]\n    }\n}`\n\n* Another example setting the first 10 LEDs to RED, LED 40 to a PURPLE (using RGB values) and all LEDs in between OFF (black color)\n`{\n  \"seg\":\n    {\n      \"i\": [\n        0,10, \"FF0000\",\n        10,40, \"00FF00\",\n        40, [0,100,100]\n      ]\n    }\n}`\n\n* Or first set all lights to black (off), then the LED5 to color RED:\n`{\n  \"seg\":\n    {\n      \"i\": [\n        0,40, \"000000\",\n        5, \"FF0000\"\n      ]\n    }\n}`\n\n* 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.\n\n```json\n`{\n  \"seg\":\n    {\n    \"frz\": false,\n    \"fx\": 28,\n    \"sx\": 200,\n    \"ix\": 128,\n    \"col\": [\n      \"FF0000\",\n      \"000000\",\n      \"FFFFFF\"\n      ]\n    }\n}`\n```\n\n## Installation\n\n1. Add `usermod_v2_HttpPullLightControl` to your WLED project following the instructions provided in the WLED documentation.\n2. Compile by setting the build_flag: -D USERMOD_HTTP_PULL_LIGHT_CONTROL and upload to your ESP32/ESP8266!\n3. There are several compile options which you can put in your platformio.ini or platformio_override.ini:\n\n* -DUSERMOD_HTTP_PULL_LIGHT_CONTROL   ;To Enable the usermod\n* -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\n* -DHTTP_PULL_LIGHT_CONTROL_SALT=\"\\\"my_very-S3cret_C0de\\\"\"  ; A secret SALT which will help by making the ID more safe\n* -DHTTP_PULL_LIGHT_CONTROL_INTERVAL=30 ; The interval at which the URL is requested in seconds\n* -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\n\n* -DWLED_AP_SSID=\"\\\"Christmas Card\\\"\" ; These flags are not just for my Usermod but you probably want to set them\n* -DWLED_AP_PASS=\"\\\"christmas\\\"\"\n* -DWLED_OTA_PASS=\"\\\"otapw-secret\\\"\"\n* -DMDNS_NAME=\"\\\"christmascard\\\"\"\n* -DSERVERNAME=\"\\\"CHRISTMASCARD\\\"\"\n* -D ABL_MILLIAMPS_DEFAULT=450\n* -D DEFAULT_LED_COUNT=60 ; For a LED Ring of 60 LEDs\n* -D BTNPIN=41  ; The M5Stack Atom S3 Lite has a button on GPIO41\n* -D DATA_PINS=2 ; The M5Stack Atom S3 Lite has a Grove connector on the front, we use this GPIO2\n* -D STATUSLED=35 ; The M5Stack Atom S3 Lite has a Multi-Color LED on GPIO35, although I didnt managed to control it\n* -D IRPIN=4  ; The M5Stack Atom S3 Lite has a IR LED on GPIO4\n\n* -D DEBUG=1  ; Set these DEBUG flags ONLY if you want to debug and read out Serial (using Visual Studio Code - Serial Monitor)\n* -DDEBUG_LEVEL=5\n* -DWLED_DEBUG\n\n## Use Case: Interactive Christmas Cards\n\nImagine distributing interactive Christmas cards embedded with a tiny ESP32 and a string of 20 LEDs to 20 friends. When a friend powers on their card, it connects to their Wi-Fi network and starts polling your server via the `usermod_v2_HttpPullLightControl`. (Tip: Let them scan a QR code to connect to the WLED WiFi, from there they configure their own WiFi).\n\nYour 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.\n\nThis setup demonstrates a unique way to blend traditional holiday sentiments with modern technology, offering an engaging and memorable experience.\n"
  },
  {
    "path": "usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.cpp",
    "content": "#include \"usermod_v2_HttpPullLightControl.h\"\n\n// add more strings here to reduce flash memory usage\nconst char HttpPullLightControl::_name[]    PROGMEM = \"HttpPullLightControl\";\nconst char HttpPullLightControl::_enabled[] PROGMEM = \"Enable\";\n\nstatic HttpPullLightControl http_pull_usermod;\nREGISTER_USERMOD(http_pull_usermod);\n\nvoid HttpPullLightControl::setup() {\n  //Serial.begin(115200);\n\n  // Print version number\n  DEBUG_PRINT(F(\"HttpPullLightControl version: \"));\n  DEBUG_PRINTLN(HTTP_PULL_LIGHT_CONTROL_VERSION);\n\n  // Start a nice chase so we know its booting and searching for its first http pull.\n  DEBUG_PRINTLN(F(\"Starting a nice chase so we now it is booting.\"));\n  Segment& seg = strip.getMainSegment();\n  seg.setMode(28); // Set to chase\n  seg.speed = 200;\n  seg.intensity = 255;\n  seg.setPalette(128);\n  seg.setColor(0, 5263440);\n  seg.setColor(1, 0);\n  seg.setColor(2, 4605510);\n\n  // Go on with generating a unique ID and splitting the URL into parts\n  uniqueId = generateUniqueId();  // Cache the unique ID\n  DEBUG_PRINT(F(\"UniqueId calculated: \"));\n  DEBUG_PRINTLN(uniqueId);\n  parseUrl();\n  DEBUG_PRINTLN(F(\"HttpPullLightControl successfully setup\"));\n}\n\n// This is the main loop function, from here we check the URL and handle the response.\n// Effects or individual lights are set as a result from this.\nvoid HttpPullLightControl::loop() {\n  if (!enabled || offMode) return; // Do nothing when not enabled or powered off\n  if (millis() - lastCheck >= checkInterval * 1000) {\n    DEBUG_PRINTLN(F(\"Calling checkUrl function\"));\n    checkUrl();\n    lastCheck = millis();\n  }\n\n}\n\n// Generate a unique ID based on the MAC address and a SALT\nString HttpPullLightControl::generateUniqueId() {\n  uint8_t mac[6];\n  WiFi.macAddress(mac);\n  char macStr[18];\n  sprintf(macStr, \"%02x:%02x:%02x:%02x:%02x:%02x\", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);\n  // Set the MAC Address to a string and make it UPPERcase\n  String macString = String(macStr);\n  macString.toUpperCase();\n  DEBUG_PRINT(F(\"WiFi MAC address is: \"));\n  DEBUG_PRINTLN(macString);\n  DEBUG_PRINT(F(\"Salt is: \"));\n  DEBUG_PRINTLN(salt);\n  String input = macString + salt;\n\n  #ifdef ESP8266\n    // For ESP8266 we use the Hash.h library which is built into the ESP8266 Core\n    return sha1(input);\n  #endif\n\n  #ifdef ESP32\n    // For ESP32 we use the mbedtls library which is built into the ESP32 core\n    int status = 0;\n    unsigned char shaResult[20]; // SHA1 produces a hash of 20 bytes  (which is 40 HEX characters)\n    mbedtls_sha1_context ctx;\n    mbedtls_sha1_init(&ctx);\n    status = mbedtls_sha1_starts_ret(&ctx);\n    if (status != 0) {\n      DEBUG_PRINTLN(F(\"Error starting SHA1 checksum calculation\"));\n    }\n    status = mbedtls_sha1_update_ret(&ctx, reinterpret_cast<const unsigned char*>(input.c_str()), input.length());\n    if (status != 0) {\n      DEBUG_PRINTLN(F(\"Error feeding update buffer into ongoing SHA1 checksum calculation\"));\n    }\n    status = mbedtls_sha1_finish_ret(&ctx, shaResult);\n    if (status != 0) {\n      DEBUG_PRINTLN(F(\"Error finishing SHA1 checksum calculation\"));\n    }\n    mbedtls_sha1_free(&ctx);\n\n    // Convert the Hash to a hexadecimal string\n    char buf[41];\n    for (int i = 0; i < 20; i++) {\n      sprintf(&buf[i*2], \"%02x\", shaResult[i]);\n    }\n    return String(buf);\n  #endif\n}\n\n// This function is called when the user updates the Sald and so we need to re-calculate the unique ID\nvoid HttpPullLightControl::updateSalt(String newSalt) {\n  DEBUG_PRINTLN(F(\"Salt updated\"));\n  this->salt = newSalt;\n  uniqueId = generateUniqueId();\n  DEBUG_PRINT(F(\"New UniqueId is: \"));\n  DEBUG_PRINTLN(uniqueId);\n}\n\n// The function is used to separate the URL in a host part and a path part\nvoid HttpPullLightControl::parseUrl() {\n  int firstSlash = url.indexOf('/', 7);  // Skip http(s)://\n  host = url.substring(7, firstSlash);\n  path = url.substring(firstSlash);\n}\n\n// This function is called by WLED when the USERMOD config is read\nbool HttpPullLightControl::readFromConfig(JsonObject& root) {\n  // Attempt to retrieve the nested object for this usermod\n  JsonObject top = root[FPSTR(_name)];\n  bool configComplete = !top.isNull();  // check if the object exists\n\n  // Retrieve the values using the getJsonValue function for better error handling\n  configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled, enabled);  // default value=enabled\n  configComplete &= getJsonValue(top[\"checkInterval\"], checkInterval, checkInterval);  // default value=60\n  #ifndef HTTP_PULL_LIGHT_CONTROL_HIDE_URL\n    configComplete &= getJsonValue(top[\"url\"], url, url);  // default value=\"http://example.com\"\n  #endif\n  #ifndef HTTP_PULL_LIGHT_CONTROL_HIDE_SALT\n    configComplete &= getJsonValue(top[\"salt\"], salt, salt);  // default value=your_salt_here\n  #endif\n\n  return configComplete;\n}\n\n// This function is called by WLED when the USERMOD config is saved in the frontend\nvoid HttpPullLightControl::addToConfig(JsonObject& root) {\n  // Create a nested object for this usermod\n  JsonObject top = root.createNestedObject(FPSTR(_name));\n\n  // Write the configuration parameters to the nested object\n  top[FPSTR(_enabled)] = enabled;\n  if (enabled==false)\n    // To make it a bit more user-friendly, we unfreeze the main segment after disabling the module. Because individual light control (like for a christmas card) might have been done.\n    strip.getMainSegment().freeze=false;\n  top[\"checkInterval\"] = checkInterval;\n  #ifndef HTTP_PULL_LIGHT_CONTROL_HIDE_URL\n    top[\"url\"] = url;\n  #endif\n  #ifndef HTTP_PULL_LIGHT_CONTROL_HIDE_SALT\n    top[\"salt\"] = salt;\n    updateSalt(salt);  // Update the UniqueID\n  #endif\n  parseUrl();  // Re-parse the URL, maybe path and host is changed\n}\n\n// Do the http request here. Note that we can not do https requests with the AsyncTCP library\n// We do everything Asynchronous, so all callbacks are defined here\nvoid HttpPullLightControl::checkUrl() {\n  // Extra Inactivity check to see if AsyncCLient hangs\n  if (client != nullptr && ( millis() - lastActivityTime > inactivityTimeout ) ) {\n      DEBUG_PRINTLN(F(\"Inactivity detected, deleting client.\"));\n      delete client;\n      client = nullptr;\n  }\n  if (client != nullptr && client->connected()) {\n      DEBUG_PRINTLN(F(\"We are still connected, do nothing\"));\n      // Do nothing, Client is still connected\n      return;\n  }\n\n  if (client != nullptr) {\n    // Delete previous client instance if exists, just to prevent any memory leaks\n    DEBUG_PRINTLN(F(\"Delete previous instances\"));\n    delete client;\n    client = nullptr;\n  }\n\n  DEBUG_PRINTLN(F(\"Creating new AsyncClient instance.\"));\n  client = new AsyncClient();\n  if(client) {\n    client->onData([](void *arg, AsyncClient *c, void *data, size_t len) {\n      DEBUG_PRINTLN(F(\"Data received.\"));\n      // Cast arg back to the usermod class instance\n      HttpPullLightControl *instance = (HttpPullLightControl *)arg;\n      instance->lastActivityTime = millis(); // Update lastactivity time when data is received\n      // Convertert to Safe-String\n      char *strData = new char[len + 1];\n      strncpy(strData, (char*)data, len);\n      strData[len] = '\\0';\n      String responseData = String(strData);\n      //String responseData = String((char *)data);\n      // Make sure its zero-terminated String\n      //responseData[len] = '\\0';\n      delete[] strData; // Do not forget to remove this one\n      instance->handleResponse(responseData);\n    }, this);\n    client->onDisconnect([](void *arg, AsyncClient *c) {\n      DEBUG_PRINTLN(F(\"Disconnected.\"));\n      //Set the class-own client pointer to nullptr if its the current client\n      HttpPullLightControl *instance = static_cast<HttpPullLightControl*>(arg);\n      if (instance->client == c) {\n        delete instance->client; // Delete the client instance\n        instance->client = nullptr;\n      }\n    }, this);\n    client->onTimeout([](void *arg, AsyncClient *c, uint32_t time) {\n      DEBUG_PRINTLN(F(\"Timeout\"));\n      //Set the class-own client pointer to nullptr if its the current client\n      HttpPullLightControl *instance = static_cast<HttpPullLightControl*>(arg);\n      if (instance->client == c) {\n        delete instance->client; // Delete the client instance\n        instance->client = nullptr;\n      }\n    }, this);\n    client->onError([](void *arg, AsyncClient *c, int8_t error) {\n      DEBUG_PRINTLN(\"Connection error occurred!\");\n      DEBUG_PRINT(\"Error code: \");\n      DEBUG_PRINTLN(error);\n      //Set the class-own client pointer to nullptr if its the current client\n      HttpPullLightControl *instance = static_cast<HttpPullLightControl*>(arg);\n      if (instance->client == c) {\n        delete instance->client;\n        instance->client = nullptr;\n      }\n      // Do not remove client here, it is maintained by AsyncClient\n    }, this);\n    client->onConnect([](void *arg, AsyncClient *c) {\n      // Cast arg back to the usermod class instance\n      HttpPullLightControl *instance = (HttpPullLightControl *)arg;\n      instance->onClientConnect(c);  // Call a method on the instance when the client connects\n    }, this);\n    client->setAckTimeout(ackTimeout); // Just some safety measures because we do not want any memory fillup\n    client->setRxTimeout(rxTimeout);\n    DEBUG_PRINT(F(\"Connecting to: \"));\n    DEBUG_PRINT(host);\n    DEBUG_PRINT(F(\" via port \"));\n    DEBUG_PRINTLN((url.startsWith(\"https\")) ? 443 : 80);\n    // Update lastActivityTime just before sending the request\n    lastActivityTime = millis();\n    //Try to connect\n    if (!client->connect(host.c_str(), (url.startsWith(\"https\")) ? 443 : 80)) {\n      DEBUG_PRINTLN(F(\"Failed to initiate connection.\"));\n      // Connection failed, so cleanup\n      delete client;\n      client = nullptr;\n    } else {\n      // Connection successfull, wait for callbacks to go on.\n      DEBUG_PRINTLN(F(\"Connection initiated, awaiting response...\"));\n    }\n  } else {\n    DEBUG_PRINTLN(F(\"Failed to create AsyncClient instance.\"));\n  }\n}\n\n// This function is called from the checkUrl function when the connection is establised\n// We request the data here\nvoid HttpPullLightControl::onClientConnect(AsyncClient *c) {\n  DEBUG_PRINT(F(\"Client connected: \"));\n  DEBUG_PRINTLN(c->connected() ? F(\"Yes\") : F(\"No\"));\n\n  if (c->connected()) {\n    String request = \"GET \" + path + (path.indexOf('?') > 0 ? \"&id=\" : \"?id=\") + uniqueId + \" HTTP/1.1\\r\\n\"\n                    \"Host: \" + host + \"\\r\\n\"\n                    \"Connection: close\\r\\n\"\n                    \"Accept: application/json\\r\\n\"\n                    \"Accept-Encoding: identity\\r\\n\" // No compression\n                    \"User-Agent: ESP32 HTTP Client\\r\\n\\r\\n\"; // Optional: User-Agent and end with a double rnrn !\n    DEBUG_PRINT(request.c_str());\n    auto bytesSent  = c->write(request.c_str());\n    if (bytesSent  == 0) {\n      // Connection could not be made\n      DEBUG_PRINT(F(\"Failed to send HTTP request.\"));\n    } else {\n      DEBUG_PRINT(F(\"Request sent successfully, bytes sent: \"));\n      DEBUG_PRINTLN(bytesSent );\n    }\n  }\n}\n\n\n// This function is called when we receive data after connecting and doing our request\n// It parses the JSON data to WLED\nvoid HttpPullLightControl::handleResponse(String& responseStr) {\n  DEBUG_PRINTLN(F(\"Received response for handleResponse.\"));\n\n  // Get a Bufferlock, we can not use doc\n  if (!requestJSONBufferLock(myLockId)) {\n    DEBUG_PRINT(F(\"ERROR: Can not request JSON Buffer Lock, number: \"));\n      DEBUG_PRINTLN(myLockId);\n    return;\n  }\n\n  // Search for two linebreaks between headers and content\n  int bodyPos = responseStr.indexOf(\"\\r\\n\\r\\n\");\n  if (bodyPos > 0) {\n    String jsonStr = responseStr.substring(bodyPos + 4); // +4 Skip the two CRLFs\n    jsonStr.trim();\n\n    DEBUG_PRINTLN(\"Response: \");\n    DEBUG_PRINTLN(jsonStr);\n\n    // Check for valid JSON, otherwise we brick the program runtime\n    if (jsonStr[0] == '{' || jsonStr[0] == '[') {\n      // Attempt to deserialize the JSON response\n      DeserializationError error = deserializeJson(*pDoc, jsonStr);\n      if (error == DeserializationError::Ok) {\n        // Get JSON object from th doc\n        JsonObject obj = pDoc->as<JsonObject>();\n        // Parse the object throuhg deserializeState  (use CALL_MODE_NO_NOTIFY or OR CALL_MODE_DIRECT_CHANGE)\n        deserializeState(obj, CALL_MODE_NO_NOTIFY);\n      } else {\n        // If there is an error in deserialization, exit the function\n        DEBUG_PRINT(F(\"DeserializationError: \"));\n        DEBUG_PRINTLN(error.c_str());\n      }\n    } else {\n      DEBUG_PRINTLN(F(\"Invalid JSON response\"));\n    }\n  } else {\n    DEBUG_PRINTLN(F(\"No body found in the response\"));\n  }\n  // Release the BufferLock again\n  releaseJSONBufferLock();\n}"
  },
  {
    "path": "usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.h",
    "content": "#pragma once\n/*\n * Usermod: HttpPullLightControl\n * Versie: 0.0.4\n * Repository: https://github.com/roelbroersma/WLED-usermodv2_HttpPullLightControl\n * Author: Roel Broersma\n * Website: https://www.roelbroersma.nl\n * Github author: github.com/roelbroersma\n * Description: This usermod for WLED will request a given URL to know which effects\n *              or individual lights it should turn on/off. So you can remote control a WLED\n *              installation without having access to it (if no port forward, vpn or public IP is available).\n * Use Case: Create a WLED 'Ring of Thought' christmas card. Sent a LED ring with 60 LEDs to 60 friends.\n *           When they turn it on and put it at their WiFi, it will contact your server. Now you can reply with a given\n *           number of lights that should turn on. Each light is a friend who did contact your server in the past 5 minutes.\n *           So on each of your friends LED rings, the number of lights will be the number of friends who have it turned on.\n * Features: It sends a unique ID (has of MAC and salt) to the URL, so you can define each client without a need to map their IP address.\n * Tested: Tested on WLED v0.14 with ESP32-S3 (M5Stack Atom S3 Lite), but should also workd for other ESPs and ESP8266.\n */\n\n#include \"wled.h\"\n\n// Use the following for SHA1 computation of our HASH, unfortunatelly PlatformIO doesnt recognize Hash.h while its already in the Core.\n// We use Hash.h for ESP8266 (in the core) and mbedtls/sha256.h for ESP32 (in the core).\n#ifdef ESP8266\n  #include <Hash.h>\n#endif\n#ifdef ESP32\n  #include \"mbedtls/sha1.h\"\n#endif\n\n#define HTTP_PULL_LIGHT_CONTROL_VERSION \"0.0.4\"\n\nclass HttpPullLightControl : public Usermod {\nprivate:\n  static const char _name[];\n  static const char _enabled[];\n  static const char _salt[];\n  static const char _url[];\n\n  bool enabled = true;\n\n  #ifdef HTTP_PULL_LIGHT_CONTROL_INTERVAL\n    uint16_t checkInterval = HTTP_PULL_LIGHT_CONTROL_INTERVAL;\n  #else\n    uint16_t checkInterval = 60;  // Default interval of 1 minute\n  #endif\n\n  #ifdef HTTP_PULL_LIGHT_CONTROL_URL\n    String url = HTTP_PULL_LIGHT_CONTROL_URL;\n  #else\n    String url = \"http://example.org/example.php\";  // Default-URL (http only!), can also be url with IP address in it. HttpS urls are not supported (yet) because of AsyncTCP library\n  #endif\n\n  #ifdef HTTP_PULL_LIGHT_CONTROL_SALT\n    String salt = HTTP_PULL_LIGHT_CONTROL_SALT;\n  #else\n    String salt = \"1just_a_very-secret_salt2\";  // Salt for generating a unique ID when requesting the URL (in this way you can give different answers based on the WLED device who does the request)\n  #endif\n  // NOTE THAT THERE IS ALSO A #ifdef HTTP_PULL_LIGHT_CONTROL_HIDE_URL and a HTTP_PULL_LIGHT_CONTROL_HIDE_SALT IF YOU DO NOT WANT TO SHOW THE OPTIONS IN THE USERMOD SETTINGS\n\n  // Define constants\n  static const uint8_t myLockId = USERMOD_ID_HTTP_PULL_LIGHT_CONTROL ; // Used for the requestJSONBufferLock(id) function\n  static const int16_t ackTimeout = 9000;  // ACK timeout in milliseconds when doing the URL request\n  static const uint16_t rxTimeout = 9000;  // RX timeout in milliseconds when doing the URL request\n  static const unsigned long FNV_offset_basis = 2166136261;\n  static const unsigned long FNV_prime = 16777619;\n  static const unsigned long inactivityTimeout = 30000; // When the AsyncClient is inactive (hanging) for this many milliseconds, we kill it\n\n  unsigned long lastCheck = 0;    // Timestamp of last check\n  unsigned long lastActivityTime = 0; // Time of last activity of AsyncClient\n  String host;                    // Host extracted from the URL\n  String path;                    // Path extracted from the URL\n  String uniqueId;                // Cached unique ID\n  AsyncClient *client = nullptr;  // Used very often, beware of closing and freeing\n  String generateUniqueId();\n\n  void parseUrl();\n  void updateSalt(String newSalt);  // Update the salt value and recalculate the unique ID\n  void checkUrl();                  // Check the specified URL for light control instructions\n  void handleResponse(String& response);\n  void onClientConnect(AsyncClient *c);\n\npublic:\n  void setup();\n  void loop();\n  bool readFromConfig(JsonObject& root);\n  void addToConfig(JsonObject& root);\n  uint16_t getId() { return USERMOD_ID_HTTP_PULL_LIGHT_CONTROL; }\n  inline void enable(bool enable) { enabled = enable; }   // Enable or Disable the usermod\n  inline bool isEnabled() { return enabled; }             // Get usermod enabled or disabled state\n  virtual ~HttpPullLightControl() { \n   // Remove the cached client if needed\n    if (client) {\n      client->onDisconnect(nullptr);\n      client->onError(nullptr);\n      client->onTimeout(nullptr);\n      client->onData(nullptr);\n      client->onConnect(nullptr);\n      // Now it is safe to delete the client.\n      delete client; // This is safe even if client is nullptr.\n      client = nullptr;\n    }\n  }\n};"
  },
  {
    "path": "usermods/usermod_v2_RF433/library.json",
    "content": "{\n  \"name\": \"usermod_v2_RF433\",\n  \"build\": { \"libArchive\": false },\n  \"dependencies\": {\n    \"sui77/rc-switch\":\"2.6.4\"\n  }  \n}"
  },
  {
    "path": "usermods/usermod_v2_RF433/readme.md",
    "content": "# RF433 remote usermod\n\nUsermod for controlling WLED using a generic 433 / 315MHz remote and simple 3-pin receiver\nSee <https://github.com/sui77/rc-switch/> for compatibility details\n\n## Build\n\n- Create a `platformio_override.ini` file at the root of the wled source directory if not already present\n- Copy the `433MHz RF remote example for esp32dev` section from `platformio_override.sample.ini` into it\n- Duplicate/adjust for other boards\n\n## Usage\n\n- Connect receiver to a free pin\n- Set pin in Config->Usermods\n- Info pane will show the last received button code\n- Upload the remote433.json sample file in this folder to the ESP with the file editor at [http://\\[wled-ip\\]/edit](http://ip/edit)\n- 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. "
  },
  {
    "path": "usermods/usermod_v2_RF433/remote433.json",
    "content": "{\n  \"13985576\": {\n    \"cmnt\": \"Toggle Power using HTTP API\",\n    \"cmd\": \"T=2\"\n  },\n  \"3670817\": {\n    \"cmnt\": \"Force Power ON using HTTP API\",\n    \"cmd\": \"T=1\"\n  },\n  \"13985572\": {\n    \"cmnt\": \"Set brightness to 200 using JSON API\",\n    \"cmd\": {\"bri\":200}\n  },\n  \"3670818\": {\n    \"cmnt\": \"Run Preset 1 using JSON API\",\n    \"cmd\": {\"ps\":1}\n  },\n  \"13985570\": {\n    \"cmnt\": \"Increase brightness by 40 using HTTP API\",\n    \"cmd\": \"A=~40\"\n  },\n  \"13985569\": {\n    \"cmnt\": \"Decrease brightness by 40 using HTTP API\",\n    \"cmd\": \"A=~-40\"\n  },\n  \"7608836\": {\n    \"cmnt\": \"Start 1min timer using JSON API\",\n    \"cmd\": {\"nl\":{\"on\":true,\"dur\":1,\"mode\":0}}\n  },\n  \"7608840\": {\n    \"cmnt\": \"Select random effect on all segments using JSON API\",\n    \"cmd\": {\"seg\":{\"fx\":\"r\"}}\n  }\n}"
  },
  {
    "path": "usermods/usermod_v2_RF433/usermod_v2_RF433.cpp",
    "content": "#include \"wled.h\"\n#include \"Arduino.h\"\n#include <RCSwitch.h>\n\n#define RF433_BUSWAIT_TIMEOUT 24\n\nclass RF433Usermod : public Usermod\n{\nprivate:\n  RCSwitch mySwitch = RCSwitch();\n  unsigned long lastCommand = 0;\n  unsigned long lastTime = 0;\n\n  bool modEnabled = true;\n  int8_t receivePin = -1;\n\n  static const char _modName[];\n  static const char _modEnabled[];\n  static const char _receivePin[];\n\n  bool initDone = false;\n\npublic:\n\n  void setup()\n  {\n    mySwitch.disableReceive();\n    if (modEnabled)\n    {\n      mySwitch.enableReceive(receivePin);\n    }\n    initDone = true;\n  }\n\n  /*\n   * connected() is called every time the WiFi is (re)connected\n   * Use it to initialize network interfaces\n   */\n  void connected()\n  {\n  }\n\n  void loop()\n  {\n    if (!modEnabled || strip.isUpdating()) \n      return;\n\n    if (mySwitch.available())\n    {\n      unsigned long receivedCommand = mySwitch.getReceivedValue();\n      mySwitch.resetAvailable();\n\n      // Discard duplicates, limit long press repeat\n      if (lastCommand == receivedCommand && millis() - lastTime < 800)\n        return;\n\n      lastCommand = receivedCommand;\n      lastTime = millis();\n\n      DEBUG_PRINT(F(\"RF433 Receive: \"));\n      DEBUG_PRINTLN(receivedCommand);\n      \n      if(!remoteJson433(receivedCommand))\n        DEBUG_PRINTLN(F(\"RF433: unknown button\"));\n    }\n  }\n\n  // Add last received button to info pane\n  void addToJsonInfo(JsonObject &root)\n  {\n    if (!initDone)\n      return; // prevent crash on boot applyPreset()\n    JsonObject user = root[\"u\"];\n    if (user.isNull())\n      user = root.createNestedObject(\"u\");\n\n    JsonArray switchArr = user.createNestedArray(\"RF433 Last Received\"); // name\n    switchArr.add(lastCommand);\n  }\n\n  void addToConfig(JsonObject &root)\n  {\n    JsonObject top = root.createNestedObject(FPSTR(_modName)); // usermodname\n    top[FPSTR(_modEnabled)] = modEnabled;\n    JsonArray pinArray = top.createNestedArray(\"pin\");\n    pinArray.add(receivePin);\n\n    DEBUG_PRINTLN(F(\" config saved.\"));\n  }\n\n  bool readFromConfig(JsonObject &root)\n  {\n    JsonObject top = root[FPSTR(_modName)];\n    if (top.isNull())\n    {\n      DEBUG_PRINT(FPSTR(_modName));\n      DEBUG_PRINTLN(F(\": No config found. (Using defaults.)\"));\n      return false;\n    }\n    getJsonValue(top[FPSTR(_modEnabled)], modEnabled);\n    getJsonValue(top[\"pin\"][0], receivePin);\n\n    DEBUG_PRINTLN(F(\"config (re)loaded.\"));\n\n    // Redo init on update\n    if(initDone)\n      setup();\n\n    return true;\n  }\n\n  /*\n   * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).\n   * This could be used in the future for the system to determine whether your usermod is installed.\n   */\n  uint16_t getId()\n  {\n    return USERMOD_ID_RF433;\n  }\n\n  // this function follows the same principle as decodeIRJson() / remoteJson()\n  bool remoteJson433(int button)\n  {\n    char objKey[14];\n    bool parsed = false;\n\n    if (!requestJSONBufferLock(JSON_LOCK_REMOTE)) return false;\n\n    sprintf_P(objKey, PSTR(\"\\\"%d\\\":\"), button);\n\n    unsigned long start = millis();\n    while (strip.isUpdating() && millis()-start < RF433_BUSWAIT_TIMEOUT) yield(); // wait for strip to finish updating, accessing FS during sendout causes glitches\n\n    // attempt to read command from remote.json\n    readObjectFromFile(PSTR(\"/remote433.json\"), objKey, pDoc);\n    JsonObject fdo = pDoc->as<JsonObject>();\n    if (fdo.isNull()) {\n      // the received button does not exist\n      releaseJSONBufferLock();\n      return parsed;\n    }\n\n    String cmdStr = fdo[\"cmd\"].as<String>();\n    JsonObject jsonCmdObj = fdo[\"cmd\"]; //object\n\n    if (jsonCmdObj.isNull())  // we could also use: fdo[\"cmd\"].is<String>()\n    {\n      // HTTP API command\n      String apireq = \"win\"; apireq += '&';                        // reduce flash string usage\n      if (!cmdStr.startsWith(apireq)) cmdStr = apireq + cmdStr;    // if no \"win&\" prefix\n      if (!irApplyToAllSelected && cmdStr.indexOf(F(\"SS=\"))<0) {\n        char tmp[10];\n        sprintf_P(tmp, PSTR(\"&SS=%d\"), strip.getMainSegmentId());\n        cmdStr += tmp;\n      }\n      fdo.clear();                                                 // clear JSON buffer (it is no longer needed)\n      handleSet(nullptr, cmdStr, false);                           // no stateUpdated() call here\n      stateUpdated(CALL_MODE_BUTTON);\n      parsed = true;\n    } else {\n    // command is JSON object\n      if (jsonCmdObj[F(\"psave\")].isNull())\n        deserializeState(jsonCmdObj, CALL_MODE_BUTTON_PRESET);\n      else {\n        uint8_t psave = jsonCmdObj[F(\"psave\")].as<int>();\n        char pname[33];\n        sprintf_P(pname, PSTR(\"IR Preset %d\"), psave);\n        fdo.clear();\n        if (psave > 0 && psave < 251) savePreset(psave, pname, fdo);\n      }\n      parsed = true;\n    }\n    releaseJSONBufferLock();\n    return parsed;\n  }\n};\n\nconst char RF433Usermod::_modName[]          PROGMEM = \"RF433 Remote\";\nconst char RF433Usermod::_modEnabled[]       PROGMEM = \"Enabled\";\nconst char RF433Usermod::_receivePin[]       PROGMEM = \"RX Pin\";\n\nstatic RF433Usermod usermod_v2_RF433;\nREGISTER_USERMOD(usermod_v2_RF433);\n"
  },
  {
    "path": "usermods/usermod_v2_animartrix/library.json",
    "content": "{\r\n  \"name\": \"animartrix\",\r\n  \"build\": { \"libArchive\": false },\r\n  \"dependencies\": {\r\n    \"Animartrix\": \"https://github.com/netmindz/animartrix.git#b172586\"\r\n  }    \r\n}\r\n"
  },
  {
    "path": "usermods/usermod_v2_animartrix/readme.md",
    "content": "# ANIMartRIX\n\nAddes the effects from ANIMartRIX to WLED\n\nCC BY-NC 3.0 licensed effects by Stefan Petrick, include this usermod only if you accept the terms!\n\n## Installation \n\nAdd 'animartrix' to 'custom_usermods' in your platformio_override.ini.\n\n"
  },
  {
    "path": "usermods/usermod_v2_animartrix/usermod_v2_animartrix.cpp",
    "content": "#include \"wled.h\"\n#include <ANIMartRIX.h>\n\n#warning WLED usermod: CC BY-NC 3.0 licensed effects by Stefan Petrick, include this usermod only if you accept the terms!\n//========================================================================================================================\n\n\nstatic const char _data_FX_mode_Module_Experiment10[] PROGMEM = \"Z💡Module_Experiment10@Speed;;1;2\";\nstatic const char _data_FX_mode_Module_Experiment9[] PROGMEM = \"Z💡Module_Experiment9@Speed;;1;2\";\nstatic const char _data_FX_mode_Module_Experiment8[] PROGMEM = \"Z💡Module_Experiment8@Speed;;1;2\";\nstatic const char _data_FX_mode_Module_Experiment7[] PROGMEM = \"Z💡Module_Experiment7@Speed;;1;2\";\nstatic const char _data_FX_mode_Module_Experiment6[] PROGMEM = \"Z💡Module_Experiment6@Speed;;1;2\";\nstatic const char _data_FX_mode_Module_Experiment5[] PROGMEM = \"Z💡Module_Experiment5@Speed;;1;2\";\nstatic const char _data_FX_mode_Module_Experiment4[] PROGMEM = \"Z💡Module_Experiment4@Speed;;1;2\";\nstatic const char _data_FX_mode_Zoom2[] PROGMEM = \"Z💡Zoom2@Speed;;1;2\";\nstatic const char _data_FX_mode_Module_Experiment3[] PROGMEM = \"Z💡Module_Experiment3@Speed;;1;2\";\nstatic const char _data_FX_mode_Module_Experiment2[] PROGMEM = \"Z💡Module_Experiment2@Speed;;1;2\";\nstatic const char _data_FX_mode_Module_Experiment1[] PROGMEM = \"Z💡Module_Experiment1@Speed;;1;2\";\nstatic const char _data_FX_mode_Parametric_Water[] PROGMEM = \"Z💡Parametric_Water@Speed;;1;2\";\nstatic const char _data_FX_mode_Water[] PROGMEM = \"Z💡Water@Speed;;1;2\";\nstatic const char _data_FX_mode_Complex_Kaleido_6[] PROGMEM = \"Z💡Complex_Kaleido_6@Speed;;1;2\";\nstatic const char _data_FX_mode_Complex_Kaleido_5[] PROGMEM = \"Z💡Complex_Kaleido_5@Speed;;1;2\";\nstatic const char _data_FX_mode_Complex_Kaleido_4[] PROGMEM = \"Z💡Complex_Kaleido_4@Speed;;1;2\";\nstatic const char _data_FX_mode_Complex_Kaleido_3[] PROGMEM = \"Z💡Complex_Kaleido_3@Speed;;1;2\";\nstatic const char _data_FX_mode_Complex_Kaleido_2[] PROGMEM = \"Z💡Complex_Kaleido_2@Speed;;1;2\";\nstatic const char _data_FX_mode_Complex_Kaleido[] PROGMEM = \"Z💡Complex_Kaleido@Speed;;1;2\";\nstatic const char _data_FX_mode_SM10[] PROGMEM = \"Z💡SM10@Speed;;1;2\";\nstatic const char _data_FX_mode_SM9[] PROGMEM = \"Z💡SM9@Speed;;1;2\";\nstatic const char _data_FX_mode_SM8[] PROGMEM = \"Z💡SM8@Speed;;1;2\";\nstatic const char _data_FX_mode_SM7[] PROGMEM = \"Z💡SM7@Speed;;1;2\";\nstatic const char _data_FX_mode_SM6[] PROGMEM = \"Z💡SM6@Speed;;1;2\";\nstatic const char _data_FX_mode_SM5[] PROGMEM = \"Z💡SM5@Speed;;1;2\";\nstatic const char _data_FX_mode_SM4[] PROGMEM = \"Z💡SM4@Speed;;1;2\";\nstatic const char _data_FX_mode_SM3[] PROGMEM = \"Z💡SM3@Speed;;1;2\";\nstatic const char _data_FX_mode_SM2[] PROGMEM = \"Z💡SM2@Speed;;1;2\";\nstatic const char _data_FX_mode_SM1[] PROGMEM = \"Z💡SM1@Speed;;1;2\";\nstatic const char _data_FX_mode_Big_Caleido[] PROGMEM = \"Z💡Big_Caleido@Speed;;1;2\";\nstatic const char _data_FX_mode_RGB_Blobs5[] PROGMEM = \"Z💡RGB_Blobs5@Speed;;1;2\";\nstatic const char _data_FX_mode_RGB_Blobs4[] PROGMEM = \"Z💡RGB_Blobs4@Speed;;1;2\";\nstatic const char _data_FX_mode_RGB_Blobs3[] PROGMEM = \"Z💡RGB_Blobs3@Speed;;1;2\";\nstatic const char _data_FX_mode_RGB_Blobs2[] PROGMEM = \"Z💡RGB_Blobs2@Speed;;1;2\";\nstatic const char _data_FX_mode_RGB_Blobs[] PROGMEM = \"Z💡RGB_Blobs@Speed;;1;2\";\nstatic const char _data_FX_mode_Polar_Waves[] PROGMEM = \"Z💡Polar_Waves@Speed;;1;2\";\nstatic const char _data_FX_mode_Slow_Fade[] PROGMEM = \"Z💡Slow_Fade@Speed;;1;2\";\nstatic const char _data_FX_mode_Zoom[] PROGMEM = \"Z💡Zoom@Speed;;1;2\";\nstatic const char _data_FX_mode_Hot_Blob[] PROGMEM = \"Z💡Hot_Blob@Speed;;1;2\";\nstatic const char _data_FX_mode_Spiralus2[] PROGMEM = \"Z💡Spiralus2@Speed;;1;2\";\nstatic const char _data_FX_mode_Spiralus[] PROGMEM = \"Z💡Spiralus@Speed;;1;2\";\nstatic const char _data_FX_mode_Yves[] PROGMEM = \"Z💡Yves@Speed;;1;2\";\nstatic const char _data_FX_mode_Scaledemo1[] PROGMEM = \"Z💡Scaledemo1@Speed;;1;2\";\nstatic const char _data_FX_mode_Lava1[] PROGMEM = \"Z💡Lava1@Speed;;1;2\";\nstatic const char _data_FX_mode_Caleido3[] PROGMEM = \"Z💡Caleido3@Speed;;1;2\";\nstatic const char _data_FX_mode_Caleido2[] PROGMEM = \"Z💡Caleido2@Speed;;1;2\";\nstatic const char _data_FX_mode_Caleido1[] PROGMEM = \"Z💡Caleido1@Speed;;1;2\";\nstatic const char _data_FX_mode_Distance_Experiment[] PROGMEM = \"Z💡Distance_Experiment@Speed;;1;2\";\nstatic const char _data_FX_mode_Center_Field[] PROGMEM = \"Z💡Center_Field@Speed;;1;2\";\nstatic const char _data_FX_mode_Waves[] PROGMEM = \"Z💡Waves@Speed;;1;2\";\nstatic const char _data_FX_mode_Chasing_Spirals[] PROGMEM = \"Z💡Chasing_Spirals@Speed;;1;2\";\nstatic const char _data_FX_mode_Rotating_Blob[] PROGMEM = \"Z💡Rotating_Blob@Speed;;1;2\";\n\n\nclass ANIMartRIXMod:public ANIMartRIX {\n\tpublic:\n\tvoid initEffect() {\n\t  if (SEGENV.call == 0) {\n\t\tinit(SEGMENT.virtualWidth(), SEGMENT.virtualHeight(), false);\n\t  }\n\t  float speedFactor = 1.0;\n\t  if (SEGMENT.speed < 128) {\n\t\tspeedFactor = (float) map(SEGMENT.speed,   0, 127, 1, 10) / 10.0f;\n\t  }\n\t  else{\n\t\tspeedFactor = map(SEGMENT.speed, 128, 255, 10, 100) / 10;\n\t  } \n\t  setSpeedFactor(speedFactor);\n\t}\n\tvoid setPixelColor(int x, int y, rgb pixel) {\n\t\tSEGMENT.setPixelColorXY(x, y, CRGB(pixel.red, pixel.green, pixel.blue));\n\t}\n\tvoid setPixelColor(int index, rgb pixel) {\n\t\tSEGMENT.setPixelColor(index, CRGB(pixel.red, pixel.green, pixel.blue));\n  \t}\n\n\t// Add any extra custom effects not part of the ANIMartRIX libary here\n};\nANIMartRIXMod anim;\n\nvoid mode_Module_Experiment10() {\n\tanim.initEffect(); \n\tanim.Module_Experiment10();\n}\nvoid mode_Module_Experiment9() { \n\tanim.initEffect(); \n\tanim.Module_Experiment9();\n}\nvoid mode_Module_Experiment8() { \n\tanim.initEffect(); \n\tanim.Module_Experiment8();\n}\nvoid mode_Module_Experiment7() { \n\tanim.initEffect(); \n\tanim.Module_Experiment7();\n}\nvoid mode_Module_Experiment6() { \n\tanim.initEffect(); \n\tanim.Module_Experiment6();\n}\nvoid mode_Module_Experiment5() { \n\tanim.initEffect(); \n\tanim.Module_Experiment5();\n}\nvoid mode_Module_Experiment4() { \n\tanim.initEffect(); \n\tanim.Module_Experiment4();\n}\nvoid mode_Zoom2() { \n\tanim.initEffect(); \n\tanim.Zoom2();\n}\nvoid mode_Module_Experiment3() { \n\tanim.initEffect(); \n\tanim.Module_Experiment3();\n}\nvoid mode_Module_Experiment2() { \n\tanim.initEffect(); \n\tanim.Module_Experiment2();\n}\nvoid mode_Module_Experiment1() { \n\tanim.initEffect(); \n\tanim.Module_Experiment1();\n}\nvoid mode_Parametric_Water() { \n\tanim.initEffect(); \n\tanim.Parametric_Water();\n}\nvoid mode_Water() { \n\tanim.initEffect(); \n\tanim.Water();\n}\nvoid mode_Complex_Kaleido_6() { \n\tanim.initEffect(); \n\tanim.Complex_Kaleido_6();\n}\nvoid mode_Complex_Kaleido_5() { \n\tanim.initEffect(); \n\tanim.Complex_Kaleido_5();\n}\nvoid mode_Complex_Kaleido_4() { \n\tanim.initEffect(); \n\tanim.Complex_Kaleido_4();\n}\nvoid mode_Complex_Kaleido_3() { \n\tanim.initEffect(); \n\tanim.Complex_Kaleido_3();\n}\nvoid mode_Complex_Kaleido_2() { \n\tanim.initEffect(); \n\tanim.Complex_Kaleido_2();\n}\nvoid mode_Complex_Kaleido() { \n\tanim.initEffect(); \n\tanim.Complex_Kaleido();\n}\nvoid mode_SM10() { \n\tanim.initEffect(); \n\tanim.SM10();\n}\nvoid mode_SM9() { \n\tanim.initEffect(); \n\tanim.SM9();\n}\nvoid mode_SM8() { \n\tanim.initEffect(); \n\tanim.SM8();\n}\n// void mode_SM7() { \n//\tanim.initEffect(); \n// \tanim.SM7();\n//\n// }\nvoid mode_SM6() { \n\tanim.initEffect(); \n\tanim.SM6();\n}\nvoid mode_SM5() { \n\tanim.initEffect(); \n\tanim.SM5();\n}\nvoid mode_SM4() { \n\tanim.initEffect(); \n\tanim.SM4();\n}\nvoid mode_SM3() { \n\tanim.initEffect(); \n\tanim.SM3();\n}\nvoid mode_SM2() { \n\tanim.initEffect(); \n\tanim.SM2();\n}\nvoid mode_SM1() { \n\tanim.initEffect(); \n\tanim.SM1();\n}\nvoid mode_Big_Caleido() { \n\tanim.initEffect(); \t\n\tanim.Big_Caleido();\n}\nvoid mode_RGB_Blobs5() { \n\tanim.initEffect(); \t\n\tanim.RGB_Blobs5();\n}\nvoid mode_RGB_Blobs4() { \n\tanim.initEffect(); \n\tanim.RGB_Blobs4();\n}\nvoid mode_RGB_Blobs3() { \n\tanim.initEffect(); \n\tanim.RGB_Blobs3();\n}\nvoid mode_RGB_Blobs2() { \n\tanim.initEffect(); \n\tanim.RGB_Blobs2();\n}\nvoid mode_RGB_Blobs() { \n\tanim.initEffect(); \n\tanim.RGB_Blobs();\n}\nvoid mode_Polar_Waves() { \n\tanim.initEffect(); \n\tanim.Polar_Waves();\n}\nvoid mode_Slow_Fade() { \n\tanim.initEffect(); \n\tanim.Slow_Fade();\n}\nvoid mode_Zoom() { \n\tanim.initEffect(); \n\tanim.Zoom();\n}\nvoid mode_Hot_Blob() { \n\tanim.initEffect(); \n\tanim.Hot_Blob();\n}\nvoid mode_Spiralus2() { \n\tanim.initEffect(); \n\tanim.Spiralus2();\n}\nvoid mode_Spiralus() { \n\tanim.initEffect(); \n\tanim.Spiralus();\n}\nvoid mode_Yves() { \n\tanim.initEffect(); \n\tanim.Yves();\n}\nvoid mode_Scaledemo1() { \n\tanim.initEffect(); \n\tanim.Scaledemo1();\n}\nvoid mode_Lava1() { \n\tanim.initEffect(); \n\tanim.Lava1();\n}\nvoid mode_Caleido3() { \n\tanim.initEffect(); \n\tanim.Caleido3();\n}\nvoid mode_Caleido2() { \n\tanim.initEffect(); \n\tanim.Caleido2();\n}\nvoid mode_Caleido1() { \n\tanim.initEffect(); \n\tanim.Caleido1();\n}\nvoid mode_Distance_Experiment() { \n\tanim.initEffect(); \n\tanim.Distance_Experiment();\n}\nvoid mode_Center_Field() { \n\tanim.initEffect(); \n\tanim.Center_Field();\n}\nvoid mode_Waves() { \n\tanim.initEffect(); \n\tanim.Waves();\n}\nvoid mode_Chasing_Spirals() { \n\tanim.initEffect(); \n\tanim.Chasing_Spirals();\n}\nvoid mode_Rotating_Blob() { \n\tanim.initEffect(); \n\tanim.Rotating_Blob();\n}\n\n\nclass AnimartrixUsermod : public Usermod {\n  protected:\n\tbool enabled = false; //WLEDMM\n\tconst char *_name; //WLEDMM\n\tbool initDone = false; //WLEDMM\n\tunsigned long lastTime = 0; //WLEDMM\n\n  public:\n\n    AnimartrixUsermod(const char *name, bool enabled) {\n\t\tthis->_name = name;\n\t\tthis->enabled = enabled;\n\t} //WLEDMM\n\t\n\n    void setup() {\n\n      strip.addEffect(255, &mode_Module_Experiment10, _data_FX_mode_Module_Experiment10);\n      strip.addEffect(255, &mode_Module_Experiment9, _data_FX_mode_Module_Experiment9);\n      strip.addEffect(255, &mode_Module_Experiment8, _data_FX_mode_Module_Experiment8);\n      strip.addEffect(255, &mode_Module_Experiment7, _data_FX_mode_Module_Experiment7);\n      strip.addEffect(255, &mode_Module_Experiment6, _data_FX_mode_Module_Experiment6);\n      strip.addEffect(255, &mode_Module_Experiment5, _data_FX_mode_Module_Experiment5);\n      strip.addEffect(255, &mode_Module_Experiment4, _data_FX_mode_Module_Experiment4);\n      strip.addEffect(255, &mode_Zoom2, _data_FX_mode_Zoom2);\n      strip.addEffect(255, &mode_Module_Experiment3, _data_FX_mode_Module_Experiment3);\n      strip.addEffect(255, &mode_Module_Experiment2, _data_FX_mode_Module_Experiment2);\n      strip.addEffect(255, &mode_Module_Experiment1, _data_FX_mode_Module_Experiment1);\n      strip.addEffect(255, &mode_Parametric_Water, _data_FX_mode_Parametric_Water);\n      strip.addEffect(255, &mode_Water, _data_FX_mode_Water);\n      strip.addEffect(255, &mode_Complex_Kaleido_6, _data_FX_mode_Complex_Kaleido_6);\n      strip.addEffect(255, &mode_Complex_Kaleido_5, _data_FX_mode_Complex_Kaleido_5);\n      strip.addEffect(255, &mode_Complex_Kaleido_4, _data_FX_mode_Complex_Kaleido_4);\n      strip.addEffect(255, &mode_Complex_Kaleido_3, _data_FX_mode_Complex_Kaleido_3);\n      strip.addEffect(255, &mode_Complex_Kaleido_2, _data_FX_mode_Complex_Kaleido_2);\n      strip.addEffect(255, &mode_Complex_Kaleido, _data_FX_mode_Complex_Kaleido);\n      strip.addEffect(255, &mode_SM10, _data_FX_mode_SM10);\n      strip.addEffect(255, &mode_SM9, _data_FX_mode_SM9);\n      strip.addEffect(255, &mode_SM8, _data_FX_mode_SM8);\n      // strip.addEffect(255, &mode_SM7, _data_FX_mode_SM7);\n      strip.addEffect(255, &mode_SM6, _data_FX_mode_SM6);\n      strip.addEffect(255, &mode_SM5, _data_FX_mode_SM5);\n      strip.addEffect(255, &mode_SM4, _data_FX_mode_SM4);\n      strip.addEffect(255, &mode_SM3, _data_FX_mode_SM3);\n      strip.addEffect(255, &mode_SM2, _data_FX_mode_SM2);\n      strip.addEffect(255, &mode_SM1, _data_FX_mode_SM1);\n      strip.addEffect(255, &mode_Big_Caleido, _data_FX_mode_Big_Caleido);\n      strip.addEffect(255, &mode_RGB_Blobs5, _data_FX_mode_RGB_Blobs5);\n      strip.addEffect(255, &mode_RGB_Blobs4, _data_FX_mode_RGB_Blobs4);\n      strip.addEffect(255, &mode_RGB_Blobs3, _data_FX_mode_RGB_Blobs3);\n      strip.addEffect(255, &mode_RGB_Blobs2, _data_FX_mode_RGB_Blobs2);\n      strip.addEffect(255, &mode_RGB_Blobs, _data_FX_mode_RGB_Blobs);\n      strip.addEffect(255, &mode_Polar_Waves, _data_FX_mode_Polar_Waves);\n      strip.addEffect(255, &mode_Slow_Fade, _data_FX_mode_Slow_Fade);\n      strip.addEffect(255, &mode_Zoom, _data_FX_mode_Zoom);\n      strip.addEffect(255, &mode_Hot_Blob, _data_FX_mode_Hot_Blob);\n      strip.addEffect(255, &mode_Spiralus2, _data_FX_mode_Spiralus2);\n      strip.addEffect(255, &mode_Spiralus, _data_FX_mode_Spiralus);\n      strip.addEffect(255, &mode_Yves, _data_FX_mode_Yves);\n      strip.addEffect(255, &mode_Scaledemo1, _data_FX_mode_Scaledemo1);\n      strip.addEffect(255, &mode_Lava1, _data_FX_mode_Lava1);\n      strip.addEffect(255, &mode_Caleido3, _data_FX_mode_Caleido3);\n      strip.addEffect(255, &mode_Caleido2, _data_FX_mode_Caleido2);\n      strip.addEffect(255, &mode_Caleido1, _data_FX_mode_Caleido1);\n      strip.addEffect(255, &mode_Distance_Experiment, _data_FX_mode_Distance_Experiment);\n      strip.addEffect(255, &mode_Center_Field, _data_FX_mode_Center_Field);\n      strip.addEffect(255, &mode_Waves, _data_FX_mode_Waves);\n      strip.addEffect(255, &mode_Chasing_Spirals, _data_FX_mode_Chasing_Spirals);\n      strip.addEffect(255, &mode_Rotating_Blob, _data_FX_mode_Rotating_Blob);\n\n      initDone = true;\n    }\n\n    void loop() {\n      if (!enabled || strip.isUpdating()) return;\n\n      // do your magic here\n      if (millis() - lastTime > 1000) {\n        //USER_PRINTLN(\"I'm alive!\");\n        lastTime = millis();\n      }\n    }\n\n    void addToJsonInfo(JsonObject& root)\n    {\n      char myStringBuffer[16]; // buffer for snprintf()\n      JsonObject user = root[\"u\"];\n      if (user.isNull()) user = root.createNestedObject(\"u\");\n\n      JsonArray infoArr = user.createNestedArray(FPSTR(_name));\n\n      String uiDomString = F(\"Animartrix requires the Creative Commons Attribution License CC BY-NC 3.0\");\n      infoArr.add(uiDomString);\n\t}\n\n    uint16_t getId()\n    {\n      return USERMOD_ID_ANIMARTRIX;\n    }\n\n};\n\nstatic AnimartrixUsermod animartrix_module(\"Animartrix\", false);\nREGISTER_USERMOD(animartrix_module);\n\n"
  },
  {
    "path": "usermods/usermod_v2_auto_save/library.json",
    "content": "{\n  \"name\": \"auto_save\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/usermod_v2_auto_save/readme.md",
    "content": "# Auto Save\n\nv2 Usermod to automatically save settings\nto preset number AUTOSAVE_PRESET_NUM after a change to any of:\n\n* brightness\n* effect speed\n* effect intensity\n* mode (effect)\n* palette\n\nbut it will wait for AUTOSAVE_AFTER_SEC seconds,\na \"settle\" period in case there are other changes (any change will extend the \"settle\" period).\n\nIt will additionally load preset AUTOSAVE_PRESET_NUM at startup during the first `loop()`.\n\nAutoSaveUsermod is standalone, but if FourLineDisplayUsermod is installed, it will notify the user of the saved changes.\n\nNote: WLED doesn't respect the brightness of the preset being auto loaded, so the AutoSaveUsermod will set the AUTOSAVE_PRESET_NUM preset in the first loop, so brightness IS honored. This means WLED will effectively ignore Default brightness and Apply N preset at boot when the AutoSaveUsermod is installed.\n\n## Installation\n\nCopy and update the example `platformio_override.ini.sample`\nfrom the Rotary Encoder UI usermode folder to the root directory of your particular build.\nThis file should be placed in the same directory as `platformio.ini`.\n\n### Define Your Options\n\n* `USERMOD_AUTO_SAVE`           - define this to have this usermod included wled00\\usermods_list.cpp\n* `AUTOSAVE_AFTER_SEC`          - define the delay time after the settings auto-saving routine should be executed\n* `AUTOSAVE_PRESET_NUM`         - define the preset number used by autosave usermod\n* `USERMOD_AUTO_SAVE_ON_BOOT`   - define if autosave should be enabled on boot\n* `USERMOD_FOUR_LINE_DISPLAY`   - define this to have this the Four Line Display mod included wled00\\usermods_list.cpp\n                                    also tells this usermod that the display is available\n                                    (see the Four Line Display usermod `readme.md` for more details)\n\nExample to add in platformio_override:\n  -D USERMOD_AUTO_SAVE\n  -D AUTOSAVE_AFTER_SEC=10\n  -D AUTOSAVE_PRESET_NUM=100\n  -D USERMOD_AUTO_SAVE_ON_BOOT=true\n\nYou can also configure auto-save parameters using Usermods settings page.\n\n### PlatformIO requirements\n\nNo special requirements.\n\nNote: the Four Line Display usermod requires the libraries `U8g2` and `Wire`.\n\n## Change Log\n\n2021-02\n\n* First public release\n\n2021-04\n\n* Adaptation for runtime configuration.\n"
  },
  {
    "path": "usermods/usermod_v2_auto_save/usermod_v2_auto_save.cpp",
    "content": "#include \"wled.h\"\n\n// v2 Usermod to automatically save settings \n// to configurable preset after a change to any of\n//\n// * brightness\n// * effect speed\n// * effect intensity\n// * mode (effect)\n// * palette\n//\n// but it will wait for configurable number of seconds, a \"settle\" \n// period in case there are other changes (any change will \n// extend the \"settle\" window).\n//\n// It can be configured to load auto saved preset at startup,\n// during the first `loop()`.\n//\n// By default it will not save the state if an unmodified preset\n// is selected (to not duplicate it). You can change this behaviour\n// by setting autoSaveIgnorePresets=false\n//\n// AutoSaveUsermod is standalone, but if FourLineDisplayUsermod \n// is installed, it will notify the user of the saved changes.\n\n// format: \"~ MM-DD HH:MM:SS ~\"\n#define PRESET_NAME_BUFFER_SIZE 25\n\nclass AutoSaveUsermod : public Usermod {\n\n  private:\n\n    bool firstLoop = true;\n    bool initDone = false;\n    bool enabled = true;\n\n    // configurable parameters\n    #ifdef AUTOSAVE_AFTER_SEC\n    uint16_t autoSaveAfterSec = AUTOSAVE_AFTER_SEC;\n    #else\n    uint16_t autoSaveAfterSec = 15;       // 15s by default\n    #endif\n\n    #ifdef AUTOSAVE_PRESET_NUM\n    uint8_t autoSavePreset = AUTOSAVE_PRESET_NUM;\n    #else\n    uint8_t autoSavePreset = 250;         // last possible preset\n    #endif\n\n    #ifdef USERMOD_AUTO_SAVE_ON_BOOT\n    bool applyAutoSaveOnBoot = USERMOD_AUTO_SAVE_ON_BOOT; \n    #else\n    bool applyAutoSaveOnBoot = false;     // do we load auto-saved preset on boot?\n    #endif\n\n    bool autoSaveIgnorePresets = true;     // ignore by default to not duplicate presets\n\n    // If we've detected the need to auto save, this will be non zero.\n    unsigned long autoSaveAfter = 0;\n\n    uint8_t knownBrightness = 0;\n    uint8_t knownEffectSpeed = 0;\n    uint8_t knownEffectIntensity = 0;\n    uint8_t knownMode = 0;\n    uint8_t knownPalette = 0;\n\n    #ifdef USERMOD_FOUR_LINE_DISPLAY\n    FourLineDisplayUsermod* display;\n    #endif\n\n    // strings to reduce flash memory usage (used more than twice)\n    static const char _name[];\n    static const char _autoSaveEnabled[];\n    static const char _autoSaveAfterSec[];\n    static const char _autoSavePreset[];\n    static const char _autoSaveApplyOnBoot[];\n    static const char _autoSaveIgnorePresets[];\n\n    void inline saveSettings() {\n      char presetNameBuffer[PRESET_NAME_BUFFER_SIZE];\n      updateLocalTime();\n      sprintf_P(presetNameBuffer, \n        PSTR(\"~ %02d-%02d %02d:%02d:%02d ~\"),\n        month(localTime), day(localTime),\n        hour(localTime), minute(localTime), second(localTime));\n      cacheInvalidate++;  // force reload of presets\n      savePreset(autoSavePreset, presetNameBuffer);\n    }\n\n    void inline displayOverlay() {\n      #ifdef USERMOD_FOUR_LINE_DISPLAY\n      if (display != nullptr) {\n        display->wakeDisplay();\n        display->overlay(\"Settings\", \"Auto Saved\", 1500);\n      }\n      #endif\n    }\n\n    void enable(bool enable) {\n      enabled = enable;\n    }\n\n  public:\n\n    // gets called once at boot. Do all initialization that doesn't depend on\n    // network here\n    void setup() {\n      #ifdef USERMOD_FOUR_LINE_DISPLAY    \n      // This Usermod has enhanced functionality if\n      // FourLineDisplayUsermod is available.\n      display = (FourLineDisplayUsermod*) UsermodManager::lookup(USERMOD_ID_FOUR_LINE_DISP);\n      #endif\n      initDone = true;\n      if (enabled && applyAutoSaveOnBoot) applyPreset(autoSavePreset);\n      knownBrightness = bri;\n      knownEffectSpeed = effectSpeed;\n      knownEffectIntensity = effectIntensity;\n      knownMode = strip.getMainSegment().mode;\n      knownPalette = strip.getMainSegment().palette;\n    }\n\n    // gets called every time WiFi is (re-)connected. Initialize own network\n    // interfaces here\n    void connected() {}\n\n    /*\n     * Da loop.\n     */\n    void loop() {\n      static unsigned long lastRun = 0;\n      unsigned long now = millis();\n      if (!autoSaveAfterSec || !enabled || (autoSaveIgnorePresets && currentPreset>0) || (strip.isUpdating() && now - lastRun < 240)) return;  // setting 0 as autosave seconds disables autosave\n      lastRun = now;\n      uint8_t currentMode = strip.getMainSegment().mode;\n      uint8_t currentPalette = strip.getMainSegment().palette;\n\n      unsigned long wouldAutoSaveAfter = now + autoSaveAfterSec*1000;\n      if (knownBrightness != bri) {\n        knownBrightness = bri;\n        autoSaveAfter = wouldAutoSaveAfter;\n      } else if (knownEffectSpeed != effectSpeed) {\n        knownEffectSpeed = effectSpeed;\n        autoSaveAfter = wouldAutoSaveAfter;\n      } else if (knownEffectIntensity != effectIntensity) {\n        knownEffectIntensity = effectIntensity;\n        autoSaveAfter = wouldAutoSaveAfter;\n      } else if (knownMode != currentMode) {\n        knownMode = currentMode;\n        autoSaveAfter = wouldAutoSaveAfter;\n      } else if (knownPalette != currentPalette) {\n        knownPalette = currentPalette;\n        autoSaveAfter = wouldAutoSaveAfter;\n      }\n\n      if (autoSaveAfter && now > autoSaveAfter) {\n        autoSaveAfter = 0;\n        // Time to auto save. You may have some flickery?\n        saveSettings();\n        displayOverlay();\n      }\n    }\n\n    /*\n     * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.\n     * Creating an \"u\" object allows you to add custom key/value pairs to the Info section of the WLED web UI.\n     * Below it is shown how this could be used for e.g. a light sensor\n     */\n    void addToJsonInfo(JsonObject& root) {\n      JsonObject user = root[\"u\"];\n      if (user.isNull()) {\n        user = root.createNestedObject(\"u\");\n      }\n\n      JsonArray infoArr = user.createNestedArray(FPSTR(_name));  // name\n\n      String uiDomString = F(\"<button class=\\\"btn btn-xs\\\" onclick=\\\"requestJson({\");\n      uiDomString += FPSTR(_name);\n      uiDomString += F(\":{\");\n      uiDomString += FPSTR(_autoSaveEnabled);\n      uiDomString += enabled ? F(\":false}});\\\">\") : F(\":true}});\\\">\");\n      uiDomString += F(\"<i class=\\\"icons \");\n      uiDomString += enabled ? \"on\" : \"off\";\n      uiDomString += F(\"\\\">&#xe08f;</i></button>\");\n      infoArr.add(uiDomString);\n    }\n\n    /*\n     * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).\n     * Values in the state object may be modified by connected clients\n     */\n    //void addToJsonState(JsonObject& root) {\n    //}\n\n    /*\n     * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).\n     * Values in the state object may be modified by connected clients\n     */\n    void readFromJsonState(JsonObject& root) {\n      if (!initDone) return;  // prevent crash on boot applyPreset()\n      bool en = enabled;\n      JsonObject um = root[FPSTR(_name)];\n      if (!um.isNull()) {\n        if (um[FPSTR(_autoSaveEnabled)].is<bool>()) {\n          en = um[FPSTR(_autoSaveEnabled)].as<bool>();\n        } else {\n          String str = um[FPSTR(_autoSaveEnabled)]; // checkbox -> off or on\n          en = (bool)(str!=\"off\"); // off is guaranteed to be present\n        }\n        if (en != enabled) enable(en);\n      }\n    }\n\n    /*\n     * addToConfig() can be used to add custom persistent settings to the cfg.json file in the \"um\" (usermod) object.\n     * It will be called by WLED when settings are actually saved (for example, LED settings are saved)\n     * If you want to force saving the current state, use serializeConfig() in your loop().\n     * \n     * CAUTION: serializeConfig() will initiate a filesystem write operation.\n     * It might cause the LEDs to stutter and will cause flash wear if called too often.\n     * Use it sparingly and always in the loop, never in network callbacks!\n     * \n     * addToConfig() will also not yet add your setting to one of the settings pages automatically.\n     * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually.\n     * \n     * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings!\n     */\n    void addToConfig(JsonObject& root) {\n      // we add JSON object: {\"Autosave\": {\"autoSaveAfterSec\": 10, \"autoSavePreset\": 99}}\n      JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname\n      top[FPSTR(_autoSaveEnabled)]       = enabled;\n      top[FPSTR(_autoSaveAfterSec)]      = autoSaveAfterSec;  // usermodparam\n      top[FPSTR(_autoSavePreset)]        = autoSavePreset;    // usermodparam\n      top[FPSTR(_autoSaveApplyOnBoot)]   = applyAutoSaveOnBoot;\n      top[FPSTR(_autoSaveIgnorePresets)] = autoSaveIgnorePresets;\n      DEBUG_PRINTLN(F(\"Autosave config saved.\"));\n    }\n\n    /*\n     * readFromConfig() can be used to read back the custom settings you added with addToConfig().\n     * This is called by WLED when settings are loaded (currently this only happens once immediately after boot)\n     * \n     * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes),\n     * 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.\n     * If you don't know what that is, don't fret. It most likely doesn't affect your use case :)\n     * \n     * The function should return true if configuration was successfully loaded or false if there was no configuration.\n     */\n    bool readFromConfig(JsonObject& root) {\n      // we look for JSON object: {\"Autosave\": {\"enabled\": true, \"autoSaveAfterSec\": 10, \"autoSavePreset\": 250, ...}}\n      JsonObject top = root[FPSTR(_name)];\n      if (top.isNull()) {\n        DEBUG_PRINT(FPSTR(_name));\n        DEBUG_PRINTLN(F(\": No config found. (Using defaults.)\"));\n        return false;\n      }\n\n      enabled               = top[FPSTR(_autoSaveEnabled)] | enabled;\n      autoSaveAfterSec      = top[FPSTR(_autoSaveAfterSec)] | autoSaveAfterSec;\n      autoSaveAfterSec      = (uint16_t) min(3600,max(10,(int)autoSaveAfterSec)); // bounds checking\n      autoSavePreset        = top[FPSTR(_autoSavePreset)] | autoSavePreset;\n      autoSavePreset        = (uint8_t) min(250,max(100,(int)autoSavePreset)); // bounds checking\n      applyAutoSaveOnBoot   = top[FPSTR(_autoSaveApplyOnBoot)] | applyAutoSaveOnBoot;\n      autoSaveIgnorePresets = top[FPSTR(_autoSaveIgnorePresets)] | autoSaveIgnorePresets;\n      DEBUG_PRINT(FPSTR(_name));\n      DEBUG_PRINTLN(F(\" config (re)loaded.\"));\n\n      // use \"return !top[\"newestParameter\"].isNull();\" when updating Usermod with new features\n      return true;\n  }\n\n    /*\n     * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).\n     * This could be used in the future for the system to determine whether your usermod is installed.\n     */\n    uint16_t getId() {\n      return USERMOD_ID_AUTO_SAVE;\n    }\n};\n\n// strings to reduce flash memory usage (used more than twice)\nconst char AutoSaveUsermod::_name[]                  PROGMEM = \"Autosave\";\nconst char AutoSaveUsermod::_autoSaveEnabled[]       PROGMEM = \"enabled\";\nconst char AutoSaveUsermod::_autoSaveAfterSec[]      PROGMEM = \"autoSaveAfterSec\";\nconst char AutoSaveUsermod::_autoSavePreset[]        PROGMEM = \"autoSavePreset\";\nconst char AutoSaveUsermod::_autoSaveApplyOnBoot[]   PROGMEM = \"autoSaveApplyOnBoot\";\nconst char AutoSaveUsermod::_autoSaveIgnorePresets[] PROGMEM = \"autoSaveIgnorePresets\";\n\nstatic AutoSaveUsermod autosave;\nREGISTER_USERMOD(autosave);\n"
  },
  {
    "path": "usermods/usermod_v2_brightness_follow_sun/README.md",
    "content": "# Update Brightness Follow Sun\n\nThis 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.\n\nThis 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.\n\n## Installation\n\ndefine `USERMOD_BRIGHTNESS_FOLLOW_SUN` e.g. `#define USERMOD_BRIGHTNESS_FOLLOW_SUN` in my_config.h\n\nor add `-D USERMOD_BRIGHTNESS_FOLLOW_SUN` to `build_flags` in platformio_override.ini\n\n### Options\n\nOpen Usermod Settings in WLED to change settings:\n\n`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`.\n\n`Update Interval Sec` - The unit is seconds, and the brightness will be automatically refreshed according to the set parameters.\n\n`Min Brightness` - set brightness by map of min-max-min : sunrise-suntop-sunset\n\n`Max Brightness` - It needs to be set to a value greater than `Min Brightness`, otherwise it will always remain at `Min Brightness`.\n\n`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.\n\n### PlatformIO requirements\n\nNo special requirements.\n\n### Change Log\n\n2025-01-02\n\n* init\n"
  },
  {
    "path": "usermods/usermod_v2_brightness_follow_sun/library.json",
    "content": "{\n  \"name\": \"brightness_follow_sun\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/usermod_v2_brightness_follow_sun/usermod_v2_brightness_follow_sun.cpp",
    "content": "#include \"wled.h\"\n\n//v2 usermod that allows to change brightness and color using a rotary encoder, \n//change between modes by pressing a button (many encoders have one included)\nclass UsermodBrightnessFollowSun : public Usermod\n{\nprivate:\n  static const char _name[];\n  static const char _enabled[];\n  static const char _update_interval[];\n  static const char _min_bri[];\n  static const char _max_bri[];\n  static const char _relax_hour[];\n\nprivate:\n  bool enabled = false; //WLEDMM\n  unsigned long update_interval = 60;\n  unsigned long update_interval_ms = 60000;\n  int min_bri = 1;\n  int max_bri = 255;\n  float relax_hour = 0;\n  int relaxSec = 0;\n  unsigned long lastUMRun = 0;\npublic:\n\n  void setup() {};\n\n  float mapFloat(float inputValue, float inMin, float inMax, float outMin, float outMax) {\n    if (inMax == inMin) \n      return outMin;\n    \n    inputValue = constrain(inputValue, inMin, inMax);\n    \n    return ((inputValue - inMin) * (outMax - outMin) / (inMax - inMin)) + outMin;\n  }\n\n  uint16_t getId() override\n  {\n    return USERMOD_ID_BRIGHTNESS_FOLLOW_SUN;\n  }\n\n  void update() \n  {\n    if (sunrise == 0 || sunset == 0 || localTime == 0)\n      return;\n\n    int curSec = elapsedSecsToday(localTime);\n    int sunriseSec = elapsedSecsToday(sunrise);\n    int sunsetSec = elapsedSecsToday(sunset);\n    int sunMiddleSec = sunriseSec + (sunsetSec-sunriseSec)/2;\n\n    int relaxSecH = sunriseSec-relaxSec;\n    int relaxSecE = sunsetSec+relaxSec;\n\n    int briSet = 0;\n    if (curSec >= relaxSecH && curSec <= relaxSecE) {\n      float timeMapToAngle = curSec < sunMiddleSec ?\n                    mapFloat(curSec, sunriseSec, sunMiddleSec, 0, M_PI/2.0) :\n                    mapFloat(curSec, sunMiddleSec, sunsetSec, M_PI/2.0, M_PI);\n      float sinValue = sin_t(timeMapToAngle);\n      briSet = min_bri + (max_bri-min_bri)*sinValue;\n    }\n\n    bri = briSet;\n    stateUpdated(CALL_MODE_DIRECT_CHANGE);\n}\n\n  void loop() override\n  {\n    if (!enabled || strip.isUpdating())\n      return;\n\n    if (millis() - lastUMRun < update_interval_ms)\n      return;\n    lastUMRun = millis();\n\n    update();\n  }\n\n  void addToConfig(JsonObject& root)\n  {\n      JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname\n\n      top[FPSTR(_enabled)] = enabled;\n      top[FPSTR(_update_interval)] = update_interval;\n      top[FPSTR(_min_bri)] = min_bri;\n      top[FPSTR(_max_bri)] = max_bri;\n      top[FPSTR(_relax_hour)] = relax_hour;\n  }\n\n  bool readFromConfig(JsonObject& root)\n  {\n    JsonObject top = root[FPSTR(_name)];\n    if (top.isNull()) {\n      DEBUG_PRINTF(\"[%s] No config found. (Using defaults.)\\n\", _name);\n      return false;\n    }\n\n    bool configComplete = true;\n\n    configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled, false);\n    configComplete &= getJsonValue(top[FPSTR(_update_interval)], update_interval, 60);\n    configComplete &= getJsonValue(top[FPSTR(_min_bri)], min_bri, 1);\n    configComplete &= getJsonValue(top[FPSTR(_max_bri)], max_bri, 255);\n    configComplete &= getJsonValue(top[FPSTR(_relax_hour)], relax_hour, 0);\n    \n    update_interval = constrain(update_interval, 1, SECS_PER_HOUR);\n    min_bri = constrain(min_bri, 1, 255);\n    max_bri = constrain(max_bri, 1, 255);\n    relax_hour = constrain(relax_hour, 0, 6);\n\n    update_interval_ms = update_interval*1000;\n    relaxSec = SECS_PER_HOUR*relax_hour;\n\n    lastUMRun = 0;\n    update();\n\n    return configComplete;\n  }\n};\n\n\nconst char UsermodBrightnessFollowSun::_name[]                PROGMEM = \"Brightness Follow Sun\";\nconst char UsermodBrightnessFollowSun::_enabled[]             PROGMEM = \"Enabled\";\nconst char UsermodBrightnessFollowSun::_update_interval[]     PROGMEM = \"Update Interval Sec\";\nconst char UsermodBrightnessFollowSun::_min_bri[]             PROGMEM = \"Min Brightness\";\nconst char UsermodBrightnessFollowSun::_max_bri[]             PROGMEM = \"Max Brightness\";\nconst char UsermodBrightnessFollowSun::_relax_hour[]          PROGMEM = \"Relax Hour\";\n\nstatic UsermodBrightnessFollowSun usermod_brightness_follow_sun;\nREGISTER_USERMOD(usermod_brightness_follow_sun);\n"
  },
  {
    "path": "usermods/usermod_v2_four_line_display_ALT/4LD_wled_fonts.h",
    "content": "//WLED custom fonts, curtesy of @Benji (https://github.com/Proto-molecule)\n#pragma once\n\n/*\n  Fontname: wled_logo_akemi_4x4\n  Copyright: Benji (https://github.com/proto-molecule)\n  Glyphs: 3/3\n  BBX Build Mode: 3\n  * this logo ...WLED/images/wled_logo_akemi.png\n  * encode map = 1, 2, 3\n*/\nconst uint8_t u8x8_wled_logo_akemi_4x4[388] U8X8_FONT_SECTION(\"u8x8_wled_logo_akemi_4x4\") = \n  \"\\1\\3\\4\\4\\0\\0\\0\\0\\0\\0\\0\\0\\0\\340\\360\\10\\350\\10\\350\\210\\270\\210\\350\\210\\270\\350\\10\\360\\340\\0\\0\\0\"\n  \"\\0\\0\\200\\200\\0\\0@\\340\\300\\340@\\0\\0\\377\\377\\377\\377\\377\\377\\37\\37\\207\\207\\371\\371\\371\\377\\377\\377\\0\\0\\374\"\n  \"\\374\\7\\7\\371\\0\\0\\6\\4\\15\\34x\\340\\200\\177\\177\\377\\351yy\\376\\356\\357\\217\\177\\177\\177o\\377\\377\\0\\70\\77\"\n  \"\\277\\376~\\71\\0\\0\\0\\0\\0\\0\\0\\1\\3\\3\\3\\1\\0\\0\\37\\77\\353\\365\\77\\37\\0\\0\\0\\0\\5\\7\\2\\3\"\n  \"\\7\\4\\0\\0\\300\\300\\300\\300\\200\\200\\200\\0\\0\\0\\0\\0\\0\\0\\200\\200\\300\\300\\300\\300\\200\\200\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\200\\200\\300\\371\\37\\37\\371\\371\\7\\7\\377\\374\\0\\0\\0\\374\\377\\377\\37\\37\\341\\341\\377\\377\\377\\377\\374\\0\\0\\0\\374\"\n  \"\\377\\7\\7\\231\\371\\376>\\371\\371>~\\377\\277\\70\\0\\270\\377\\177\\77\\376\\376\\71\\371\\371\\71\\177\\377\\277\\70\\0\\70\\377\"\n  \"\\177>\\376\\371\\377\\377\\0\\77\\77\\0\\0\\4\\7\\2\\7\\5\\0\\0\\0\\377\\377\\0\\77\\77\\0\\0\\0\\5\\7\\2\\7\\5\"\n  \"\\0\\0\\377\\377\\300\\300\\300\\200\\200\\0\\0\\0\\0\\0\\0\\0\\200\\200\\300\\300\\300\\300\\300\\200\\200\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\231\\231\\231\\371\\377\\377\\374\\0\\0\\0\\374\\377\\347\\347\\371\\1\\1\\371\\371\\7\\7\\377\\374\\0\\0\\0@\\340\"\n  \"\\300\\340@\\0\\71\\371\\371\\71\\177\\377\\277\\70\\0\\70\\277\\377\\177\\71\\371\\370\\70\\371\\371~\\376\\377\\77\\70\\200\\340x\\34\"\n  \"\\15\\4\\6\\0\\0\\77\\77\\0\\0\\0\\5\\7\\2\\7\\5\\0\\0\\0\\377\\377\\0\\77\\77\\0\\0\\1\\3\\3\\1\\1\\0\\0\"\n  \"\\0\\0\\0\";\n\n\n/*\n  Fontname: wled_logo_akemi_5x5\n  Copyright: Benji (https://github.com/proto-molecule)\n  Glyphs: 3/3\n  BBX Build Mode: 3\n  * this logo ...WLED/images/wled_logo_akemi.png\n  * encoded = 1, 2, 3\n*/\n/*\nconst uint8_t u8x8_wled_logo_akemi_5x5[604] U8X8_FONT_SECTION(\"u8x8_wled_logo_akemi_5x5\") = \n  \"\\1\\3\\5\\5\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\340\\340\\374\\14\\354\\14\\354\\14|\\14\\354\\14||\\14\\354\"\n  \"\\14\\374\\340\\340\\0\\0\\0\\0\\0\\0\\0\\200\\0\\0\\0\\200\\200\\0\\200\\200\\0\\0\\0\\0\\377\\377\\377\\376\\377\\376\\377\\377\"\n  \"\\377\\377\\77\\77\\307\\307\\307\\307\\306\\377\\377\\377\\0\\0\\0\\360\\374>\\77\\307\\0\\0\\61cg\\357\\347\\303\\301\\200\\0\\0\"\n  \"\\377\\377\\377\\317\\317\\317\\317\\360\\360\\360\\374\\374\\377\\377\\377\\377\\377\\377\\377\\377\\0\\0\\200\\377\\377\\340\\340\\37\\0\\0\\0\\0\"\n  \"\\0\\0\\1\\3\\17\\77\\374\\360\\357\\357\\177\\36\\14\\17\\357\\377\\376\\376>\\376\\360\\357\\17\\17\\14>\\177o\\340\\300\\343c\"\n  \"{\\77\\17\\3\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\17\\37\\37\\362\\375\\37\\37\\17\\0\\0\"\n  \"\\0\\0\\1\\1\\1\\0\\1\\1\\1\\0\\0\\0\\200\\300\\300\\300\\300\\200\\200\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\200\\200\"\n  \"\\300\\300\\300\\300\\200\\200\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\200\\200\\307\\307\\377\\377\\307\\307\\307\\77>\\374\\360\\0\"\n  \"\\0\\0\\360\\374\\376\\377\\377\\377\\7\\7\\7\\377\\377\\377\\377\\376\\374\\360\\0\\0\\0\\0\\360\\374\\36\\37\\37\\343\\37\\37\\340\\340\"\n  \"\\37\\37\\37\\340\\340\\377\\377\\200\\0\\200\\377\\377\\377\\340\\340\\340\\37\\37\\37\\37\\37\\37\\37\\377\\377\\377\\200\\0\\0\\200\\377\\377\"\n  \"\\340\\340\\340\\34\\377\\377\\3\\3\\377\\377\\3\\17\\77{\\343\\303\\300\\303\\343s\\77\\37\\3\\377\\377\\3\\3\\377\\377\\3\\17\\77\"\n  \"{\\343\\303\\300\\300\\343{\\37\\17\\3\\377\\377\\377\\377\\0\\0\\37\\37\\0\\0\\1\\1\\1\\1\\0\\1\\1\\1\\1\\0\\0\\377\"\n  \"\\377\\0\\0\\37\\37\\0\\0\\1\\1\\1\\1\\0\\0\\1\\1\\1\\0\\0\\377\\377\\300\\300\\300\\200\\200\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\200\\200\\300\\300\\300\\300\\200\\200\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\343\\343\\343\\343\"\n  \"\\343\\377\\376\\374\\360\\0\\0\\0\\360\\374\\376\\77\\77\\307\\307\\7\\7\\307\\307\\307\\77>\\374\\360\\0\\0\\0\\0\\0\\200\\200\\0\"\n  \"\\200\\200\\0\\0\\34\\34\\34\\37\\37\\377\\377\\377\\377\\200\\0\\200\\377\\377\\377\\377\\37\\37\\37\\0\\0\\37\\37\\37\\340\\340\\377\\377\"\n  \"\\200\\0\\0\\0\\1\\303\\347\\357gc\\61\\0\\3\\3\\377\\377\\3\\7\\37\\177s\\343\\300\\303s{\\37\\17\\7\\3\\377\\377\"\n  \"\\3\\3\\377\\377\\3\\37\\77scp<\\36\\17\\3\\1\\0\\0\\0\\0\\0\\0\\0\\37\\37\\0\\0\\0\\1\\1\\1\\0\\1\"\n  \"\\1\\1\\0\\0\\0\\0\\377\\377\\0\\0\\37\\37\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\";\n*/\n\n/*\n  Fontname: wled_logo_2x2\n  Copyright: Benji (https://github.com/proto-molecule)\n  Glyphs: 4/4\n  BBX Build Mode: 3\n  * this logo  https://cdn.discordapp.com/attachments/706623245935444088/927361780613799956/wled_scaled.png\n  * encode map = 1, 2, 3, 4\n*/\nconst uint8_t u8x8_wled_logo_2x2[133] U8X8_FONT_SECTION(\"u8x8_wled_logo_2x2\") = \n  \"\\1\\4\\2\\2\\0\\0\\0\\0\\0\\200\\200\\360\\360\\16\\16\\16\\16\\0\\0\\0\\340\\340\\340\\340\\340\\37\\37\\1\\1\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\360\\360\\16\\16\\16\\200\\200\\16\\16\\16\\360\\360\\0\\0\\0\\200\\37\\37\\340\\340\\340\\37\\37\\340\\340\\340\\37\\37\"\n  \"\\0\\0\\0\\37\\200~~\\0\\0\\0\\0\\0\\0\\0\\360\\360\\216\\216\\216\\216\\37\\340\\340\\340\\340\\340\\340\\340\\0\\0\\37\\37\"\n  \"\\343\\343\\343\\343\\16\\16\\0\\0ppp\\16\\16\\376\\376\\16\\16\\16\\360\\360\\340\\340\\0\\0\\0\\0\\0\\340\\340\\377\\377\\340\"\n  \"\\340\\340\\37\\37\";\n\n\n/*\n  Fontname: wled_logo_4x4\n  Copyright: Created with Fony 1.4.7\n  Glyphs: 4/4\n  BBX Build Mode: 3\n  * this logo  https://cdn.discordapp.com/attachments/706623245935444088/927361780613799956/wled_scaled.png\n  * encode map = 1, 2, 3, 4\n*/\n/*\nconst uint8_t u8x8_wled_logo_4x4[517] U8X8_FONT_SECTION(\"u8x8_wled_logo_4x4\") = \n  \"\\1\\4\\4\\4\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\374\\374\\374\\374\\374\\374\\374\\374\\374\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\300\\300\\300\\300\\300\\377\\377\\377\\377\\377\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\377\\377\\377\\377\\377\\17\\17\\17\\17\\17\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\370\\370\\370\\370\\370\\370\\370\\370\\370\\7\\7\\7\\7\\7\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\374\\374\\374\\374\\374\\0\\0\\0\\0\\0\\374\\374\\374\\374\\374\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\377\\377\\377\\377\\377\\0\\0\\0\\0\\0\\300\\300\\300\\300\\300\\0\\0\\0\\0\\0\\377\\377\\377\\377\\377\\0\\0\"\n  \"\\0\\0\\300\\300\\0\\377\\377\\377\\377\\377\\0\\0\\0\\0\\0\\377\\377\\377\\377\\377\\0\\0\\0\\0\\0\\377\\377\\377\\377\\377\\0\\0\"\n  \"\\0\\0\\377\\377\\0\\7\\7\\7\\7\\7\\370\\370\\370\\370\\370\\7\\7\\7\\7\\7\\370\\370\\370\\370\\370\\7\\7\\7\\7\\7\\0\\0\"\n  \"\\0\\0\\7\\7\\0\\0\\0\\374\\374\\374\\374\\374\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\374\\374\\374\"\n  \"\\374\\374\\374\\374\\300\\300\\300\\77\\77\\77\\77\\77\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\377\\377\\377\\377\\377\\300\\300\\300\"\n  \"\\300\\300\\300\\300\\377\\377\\377\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\377\\377\\377\\377\\377\\37\\37\\37\"\n  \"\\37\\37\\37\\37\\7\\7\\7\\370\\370\\370\\370\\370\\370\\370\\370\\370\\370\\370\\370\\370\\0\\0\\0\\0\\7\\7\\7\\7\\7\\370\\370\\370\"\n  \"\\370\\370\\370\\370\\374\\374\\374\\374\\374\\374\\0\\0\\0\\0\\0\\0\\0\\0\\374\\374\\374\\374\\374\\374\\374\\374\\374\\374\\374\\374\\374\\374\"\n  \"\\0\\0\\0\\0\\300\\300\\0\\0\\0\\0\\0\\0\\0\\77\\77\\77\\77\\77\\0\\0\\0\\0\\377\\377\\377\\377\\377\\0\\0\\0\\0\\377\"\n  \"\\377\\377\\377\\377\\37\\37\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\377\\377\\377\\377\\377\\0\\0\\0\\0\\377\"\n  \"\\377\\377\\377\\377\\370\\370\\370\\370\\370\\370\\0\\0\\0\\0\\0\\0\\0\\0\\370\\370\\370\\370\\377\\377\\377\\377\\377\\370\\370\\370\\370\\377\"\n  \"\\7\\7\\7\\7\";\n*/\n\n\n/*\n  Fontname: 4LineDisplay_WLED_icons_1x\n  Copyright: Benji (https://github.com/proto-molecule)\n  Glyphs: 13/13\n  BBX Build Mode: 3\n  * 1  = sun\n  * 2  = skip forward\n  * 3  = fire\n  * 4  = custom palette\n  * 5  = puzzle piece\n  * 6  = moon\n  * 7  = brush\n  * 8  = contrast\n  * 9  = power-standby\n  * 10 = star\n  * 11 = heart\n  * 12 = Akemi\n  *-----------\n  * 20 = wifi\n  * 21 = media-play\n*/\nconst uint8_t u8x8_4LineDisplay_WLED_icons_1x1[172] U8X8_FONT_SECTION(\"u8x8_4LineDisplay_WLED_icons_1x1\") = \n  \"\\1\\25\\1\\1\\0B\\30<<\\30B\\0~<\\30\\0~<\\30\\0p\\374\\77\\216\\340\\370\\360\\0<n\\372\\377\"\n  \"\\275\\277\\26\\34\\374\\374\\77\\77\\374\\374\\60\\60<~~\\360\\340``\\0\\200\\340\\360p\\14\\16\\6\\1<~\\377\\377\"\n  \"\\201\\201B<\\70D\\200\\217\\200D\\70\\0\\0\\10x<<x\\10\\0\\14\\36>||>\\36\\14\\64 \\336\\67\"\n  \";\\336 \\64\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\2\\1\\11\\311\"\n  \"\\311\\1\\2\\0\\0~<<\\30\\30\\0\";\n\n\n/*\n  Fontname: 4LineDisplay_WLED_icons_2x1\n  Copyright: Benji (https://github.com/proto-molecule)\n  Glyphs: 11/11\n  BBX Build Mode: 3\n  * 1  = sun\n  * 2  = skip forward\n  * 3  = fire\n  * 4  = custom palette\n  * 5  = puzzle piece\n  * 6  = moon\n  * 7  = brush\n  * 8  = contrast\n  * 9  = power-standby\n  * 10 = star\n  * 11 = heart  \n  * 12 = Akemi\n*/\nconst uint8_t u8x8_4LineDisplay_WLED_icons_2x1[196] U8X8_FONT_SECTION(\"u8x8_4LineDisplay_WLED_icons_2x1\") = \n  \"\\1\\14\\2\\1\\20\\20BB\\30\\30<\\275\\275<\\30\\30BB\\20\\20\\377~<<\\70\\30\\20\\0\\377~<<\"\n  \"\\70\\30\\20\\0\\60p\\370\\374\\77>\\236\\214\\300\\340\\370\\360\\360\\340\\0\\0\\34<v\\326\\336\\375\\375\\377\\277\\275=>\"\n  \"\\66\\66<\\34\\374\\374\\374\\374~\\77\\77~\\374\\374\\374\\374 pp \\30<~~\\377\\370\\360\\360\\340\\340\\340\\340\"\n  \"@@ \\0\\200\\300\\340\\360\\360p`\\10\\34\\34\\16\\6\\6\\3\\0\\0\\70|~\\376\\376\\377\\377\\377\\201\\201\\203\\202\"\n  \"\\302Fl\\70\\70xL\\204\\200\\200\\217\\217\\200\\200\\204Lx\\70\\0\\0\\10\\10\\30\\330x|\\77\\77|x\\330\\30\"\n  \"\\10\\10\\0\\0\\14\\36\\37\\77\\77\\177~\\374\\374~\\177\\77\\77\\37\\36\\14\\24\\64 \\60>\\26\\367\\33\\375\\36>\\60\"\n  \" \\64\\24\";\n\n\n/*\n  Fontname: 4LineDisplay_WLED_icons_2x\n  Copyright: \n  Glyphs: 11/11\n  BBX Build Mode: 3\n  * 1  = sun\n  * 2  = skip forward\n  * 3  = fire\n  * 4  = custom palette\n  * 5  = puzzle piece\n  * 6  = moon\n  * 7  = brush\n  * 8  = contrast\n  * 9  = power-standby\n  * 10 = star\n  * 11 = heart  \n  * 12 = Akemi\n*/\nconst uint8_t u8x8_4LineDisplay_WLED_icons_2x2[389] U8X8_FONT_SECTION(\"u8x8_4LineDisplay_WLED_icons_2x2\") = \n  \"\\1\\14\\2\\2\\200\\200\\14\\14\\300\\340\\360\\363\\363\\360\\340\\300\\14\\14\\200\\200\\1\\1\\60\\60\\3\\7\\17\\317\\317\\17\\7\\3\"\n  \"\\60\\60\\1\\1\\374\\370\\360\\340\\340\\300\\200\\0\\374\\370\\360\\340\\340\\300\\200\\0\\77\\37\\17\\7\\7\\3\\1\\0\\77\\37\\17\\7\"\n  \"\\7\\3\\1\\0\\0\\200\\340\\360\\377\\376\\374\\360\\0\\0\\300\\200\\0\\0\\0\\0\\17\\77\\177\\377\\17\\7\\301\\340\\370\\374\\377\\377\"\n  \"\\377|\\0\\0\\360\\370\\234\\236\\376\\363\\363\\377\\377\\363\\363\\376><\\370\\360\\3\\17\\77yy\\377\\377\\377\\377\\317\\17\\17\"\n  \"\\17\\17\\7\\3\\360\\360\\360\\360\\366\\377\\377\\366\\360\\360\\360\\360\\0\\0\\0\\0\\377\\377\\377\\377\\237\\17\\17\\237\\377\\377\\377\\377\"\n  \"\\6\\17\\17\\6\\340\\370\\374\\376\\377\\340\\200\\0\\0\\0\\0\\0\\0\\0\\0\\0\\3\\17\\37\\77\\177\\177\\177\\377\\376|||\"\n  \"\\70\\30\\14\\0\\0\\0\\0\\0\\0\\0\\0``\\360\\370|<\\36\\7\\2\\0\\300\\360\\376\\377\\177\\77\\36\\0\\1\\1\\0\"\n  \"\\0\\0\\0\\0\\340\\370\\374\\376\\376\\377\\377\\377\\3\\3\\7\\6\\16<\\370\\340\\7\\37\\77\\177\\177\\377\\377\\377\\300\\300\\340`\"\n  \"p<\\37\\7\\300\\340p\\30\\0\\0\\377\\377\\0\\0\\30p\\340\\300\\0\\0\\17\\37\\70`\\340\\300\\300\\300\\300\\340`\\70\"\n  \"\\37\\17\\0\\0\\0@\\300\\300\\300\\300\\340\\374\\374\\340\\300\\300\\300\\300@\\0\\0\\0\\0\\1s\\77\\37\\17\\17\\37\\77s\"\n  \"\\1\\0\\0\\0\\360\\370\\374\\374\\374\\374\\370\\360\\360\\370\\374\\374\\374\\374\\370\\360\\0\\1\\3\\7\\17\\37\\77\\177\\177\\77\\37\\17\"\n  \"\\7\\3\\1\\0\\200\\200\\0\\0\\0\\360\\370\\374<\\334\\330\\360\\0\\0\\200\\200\\2\\2\\14\\30\\24\\37\\6~\\7\\177\\7\\37\"\n  \"\\24\\30\\16\\2\";\n\n/*\n  Fontname: 4LineDisplay_WLED_icons_3x\n  Copyright: Benji (https://github.com/proto-molecule)\n  Glyphs: 11/11\n  BBX Build Mode: 3\n  * 1  = sun\n  * 2  = skip forward\n  * 3  = fire\n  * 4  = custom palette\n  * 5  = puzzle piece\n  * 6  = moon\n  * 7  = brush\n  * 8  = contrast\n  * 9  = power-standby\n  * 10 = star\n  * 11 = heart  \n  * 12 = Akemi\n*/\nconst uint8_t u8x8_4LineDisplay_WLED_icons_3x3[868] U8X8_FONT_SECTION(\"u8x8_4LineDisplay_WLED_icons_3x3\") = \n  \"\\1\\14\\3\\3\\0\\0\\34\\34\\34\\0\\200\\300\\300\\340\\347\\347\\347\\340\\300\\300\\200\\0\\34\\34\\34\\0\\0\\0\\34\\34\\34\\0\"\n  \"\\0>\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377>\\0\\0\\34\\34\\34\\0\\0\\0\\16\\16\\16\\0\\0\\1\\1\\3ss\"\n  \"s\\3\\1\\1\\0\\0\\34\\34\\34\\0\\0\\0\\370\\360\\340\\300\\300\\200\\0\\0\\0\\0\\0\\0\\370\\360\\340\\300\\300\\200\\0\\0\"\n  \"\\0\\0\\0\\0\\377\\377\\377\\377\\377\\377\\377\\376~<\\70\\20\\377\\377\\377\\377\\377\\377\\377\\376~<\\70\\20\\37\\17\\17\\7\"\n  \"\\3\\1\\1\\0\\0\\0\\0\\0\\37\\17\\17\\7\\3\\1\\1\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\300\\361\\376\\374\\370\\360\\300\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\300\\370\\374\\376\\377\\377\\377\\377\\377\\177\\77\\17\\6\\0\\200\\342\\374\\370\\360\\340\"\n  \"\\200\\0\\0\\0\\1\\17\\37\\77\\177\\377\\7\\3\\0\\200\\360\\370\\374\\376\\377\\377\\377\\377\\377\\377\\77\\0\\0\\0\\0\\200\\340\\360\"\n  \"\\370\\370\\374\\316\\206\\206\\317\\377\\377\\377\\317\\206\\206\\316\\374\\374\\370\\360\\340\\200<\\377\\377\\371\\360py\\377\\377\\377\\377\\377\"\n  \"\\377\\377\\377\\377\\377\\377\\363\\341\\341\\363\\377\\177\\0\\1\\7\\17\\34\\70x|\\377\\377\\377\\377\\367\\363c\\3\\3\\3\\3\\1\"\n  \"\\1\\1\\0\\0\\300\\300\\300\\300\\300\\300\\300\\316\\377\\377\\377\\316\\300\\300\\300\\300\\300\\300\\0\\0\\0\\0\\0\\0\\377\\377\\377\\377\"\n  \"\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\300\\300\\340\\340\\340\\300\\377\\377\\377\\377\\377\\377\\377\\307\\3\\3\\3\\307\"\n  \"\\377\\377\\377\\377\\377\\377\\1\\1\\3\\3\\3\\1\\0\\300\\340\\370\\374\\374\\376\\377\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0>\\377\\377\\377\\377\\377\\377\\377\\377\\374\\360\\340\\300\\300\\200\\200\\0\\0\\0\\0\\0\\0\\200\\200\\0\\1\\7\\17\"\n  \"\\37\\37\\77\\177\\177\\177\\177\\377\\377\\377\\177\\177\\177\\77\\77\\37\\17\\7\\3\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\200\\200\\300\\340\\340\\360\\370\\374|>\\17\\6\\0\\0\\0\\0\\0\\340\\340\\360\\360\\360\\342\\303\\7\\17\\37\\77\\37\\7\\3\\1\"\n  \"\\0\\0\\0\\0\\0\\200\\340\\360\\377\\377\\377\\377\\177\\77\\37\\17\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\200\\340\\360\"\n  \"\\370\\374\\374\\376\\376\\376\\377\\377\\7\\7\\7\\6\\16\\16\\34\\70\\360\\340\\300\\0|\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\377\\377\\377\\0\\3\\7\\17\\37\\77\\177\\177\\377\\377\\377\\377\\340\\340\\340\\340pp\\70<\"\n  \"\\37\\17\\3\\0\\0\\0\\200\\300\\340\\340\\300\\0\\0\\377\\377\\377\\0\\0\\300\\340\\340\\300\\200\\0\\0\\0\\0\\0\\370\\376\\377\\17\"\n  \"\\3\\0\\0\\0\\0\\17\\17\\17\\0\\0\\0\\0\\0\\3\\17\\377\\376\\370\\0\\0\\0\\7\\17\\37<xp\\340\\340\\340\\340\\340\"\n  \"\\340\\340\\340px<\\37\\17\\3\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\300\\370\\376\\370\\200\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\2\\6\\16\\36\\36>~\\376\\376\\377\\377\\377\\377\\377\\376\\376~>\\36\\16\\6\\6\\2\\0\\0\\0\\0\"\n  \"\\0\\300x<\\37\\17\\17\\7\\3\\7\\17\\17\\37<x\\300\\0\\0\\0\\0\\200\\300\\340\\360\\360\\370\\370\\370\\360\\360\\340\\300\"\n  \"\\200\\300\\340\\360\\360\\370\\370\\370\\360\\360\\340\\200\\17\\37\\77\\177\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\"\n  \"\\177\\77\\37\\17\\0\\0\\0\\0\\0\\1\\3\\7\\17\\37\\77\\177\\177\\77\\37\\17\\7\\3\\1\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\200\\300\\340\\360\\370\\370\\370p`\\300\\200\\0\\0\\0\\0\\0\\0&,f\\300\\0\\0\\300\\377\\377\\357\\357\\357\"\n  \"\\363\\370\\377\\377\\377\\377\\300\\0\\0\\306l&\\0\\0\\0\\1\\7\\16\\14\\6\\7\\1\\177\\177\\0\\177\\177\\1\\7\\6\\14\\16\"\n  \"\\7\\1\\0\";\n\n\n\n/*\n  Fontname: 4LineDisplay_WLED_icons_4x\n  Copyright: Benji (https://github.com/proto-molecule)\n  Glyphs: 11/11\n  BBX Build Mode: 3\n  * 1  = sun\n  * 2  = skip forward\n  * 3  = fire\n  * 4  = custom palette\n  * 5  = puzzle piece\n  * 6  = moon\n  * 7  = brush\n  * 8  = contrast\n  * 9  = power-standby\n  * 10 = star\n  * 11 = heart  \n  * 12 = Akemi\n*/\n/*\nconst uint8_t u8x8_4LineDisplay_WLED_icons_4x4[1540] U8X8_FONT_SECTION(\"u8x8_4LineDisplay_WLED_icons_4x4\") = \n  \"\\1\\14\\4\\4\\0\\0\\0\\0`\\360\\360`\\0\\0\\0\\0\\0\\0\\6\\17\\17\\6\\0\\0\\0\\0\\0\\0`\\360\\360`\"\n  \"\\0\\0\\0\\0\\200\\300\\300\\200\\0\\0\\0\\0\\340\\370\\374\\376\\376\\377\\377\\377\\377\\377\\377\\376\\376\\374\\370\\340\\0\\0\\0\\0\"\n  \"\\200\\300\\300\\200\\1\\3\\3\\1\\0\\0\\0\\0\\7\\37\\77\\177\\177\\377\\377\\377\\377\\377\\377\\177\\177\\77\\37\\7\\0\\0\\0\\0\"\n  \"\\1\\3\\3\\1\\0\\0\\0\\0\\6\\17\\17\\6\\0\\0\\0\\0\\0\\0`\\360\\360`\\0\\0\\0\\0\\0\\0\\6\\17\\17\\6\"\n  \"\\0\\0\\0\\0\\360\\340\\300\\200\\200\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\360\\340\\300\\200\\200\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\377\\377\\377\\377\\377\\377\\376\\374\\374\\370\\360\\340\\340\\300\\200\\0\\377\\377\\377\\377\\377\\377\\376\\374\\374\\370\\360\\340\"\n  \"\\340\\300\\200\\0\\377\\377\\377\\377\\377\\377\\177\\77\\77\\37\\17\\7\\7\\3\\1\\0\\377\\377\\377\\377\\377\\377\\177\\77\\77\\37\\17\\7\"\n  \"\\7\\3\\1\\0\\17\\7\\3\\1\\1\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\17\\7\\3\\1\\1\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\200\\341\\376\\374\\370\\360\\340\\300\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\200\\300\\360\\370\\374\\376\\377\\377\\377\\377\\377\\377\\377\\377~\\0\\0\\0\\0\\20\\340\\300\\200\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0~\\377\\377\\377\\377\\377\\377\\377\\377\\177\\77\\37\\17\\3\\1\\0\\200\\300\\340\\370\\376\\377\\377\\377\\377\\376\\374\\340\"\n  \"\\0\\0\\0\\0\\0\\3\\7\\17\\37\\77\\177\\207\\1\\0\\0\\0\\340\\370\\374\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\177\\77\\17\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\200\\300\\360\\370\\370\\374\\34\\16\\16\\16\\36\\377\\377\\377\\377\\37\\16\\16\\16\\36\\374\\374\\370\\370\\360\"\n  \"\\340\\300\\0\\0\\340\\374\\377\\377\\217\\7\\7\\7\\217\\377\\376\\376\\376\\377\\377\\377\\377\\377\\377\\376\\376\\376\\377\\377\\217\\7\\7\\7\"\n  \"\\217\\377\\377\\374\\17\\177\\377\\377\\377\\37\\17\\17\\17\\37\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\177\\177\\177\\177\\77\\77\"\n  \"\\77\\37\\17\\7\\0\\0\\0\\3\\7\\17\\36>>\\177\\177\\377\\377\\377\\377\\377\\377\\371p\\60\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0<\\376\\377\\377\\377\\377\\376<\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377~~\\377\\377\"\n  \"\\377\\377~<\\377\\377\\377\\377\\377\\377\\377\\377\\303\\1\\0\\0\\0\\0\\1\\303\\377\\377\\377\\377\\377\\377\\377\\377\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\200\\340\\360\\370\\374\\374\\376\\376\\1\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\370\\377\\377\\377\\377\\377\\377\\377\\377\\377\\376\\360\\300\\200\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\7\\77\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\376\\374\\370\\370\\360\\360\\360\\340\\340\\340\\340\\340\\340\"\n  \"\\340\\340\\60\\0\\0\\0\\0\\1\\3\\7\\17\\37\\37\\77\\77\\77\\177\\177\\177\\177\\177\\177\\177\\177\\77\\77\\77\\37\\37\\17\\7\\3\"\n  \"\\1\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\200\\200\\300\\340\\340\\360\\370\\374\\374\"\n  \"~\\77\\16\\4\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\30\\34>~\\377\\377\\377\\377\\177\\77\\37\\7\\3\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\360\\374\\376\\377\\377\\377\\377\\377\\376\\374\\370\\0\\0\\0\\3\\3\\1\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0@@\\340\\370\\374\\377\\377\\377\\177\\177\\177\\77\\37\\17\\7\\1\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\200\\300\\340\\360\\370\\374\\374\\376\\376\\376\\377\\377\\377\\377\\17\\17\\17\\37\\36\\36>|\\374\\370\\360\\340\"\n  \"\\300\\200\\0\\0\\360\\376\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\0\\0\\0\\0\\0\\0\\0\\0\\0\\1\\3\\37\"\n  \"\\377\\377\\376\\360\\17\\177\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\0\\0\\0\\0\\0\\0\\0\\0\\0\\200\\300\\370\"\n  \"\\377\\377\\177\\17\\0\\0\\1\\3\\7\\17\\37\\77\\77\\177\\177\\177\\377\\377\\377\\377\\360\\360\\360\\370xx|>\\77\\37\\17\\7\"\n  \"\\3\\1\\0\\0\\0\\0\\0\\0\\0\\200\\300\\200\\0\\0\\0\\0\\377\\377\\377\\377\\0\\0\\0\\0\\200\\300\\200\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\300\\360\\374\\376\\177\\37\\7\\3\\3\\0\\0\\0\\377\\377\\377\\377\\0\\0\\0\\3\\3\\7\\37\\177\\376\\374\\360\\300\"\n  \"\\0\\0\\0\\0\\77\\377\\377\\377\\340\\200\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\200\\340\\377\\377\\377\\77\"\n  \"\\0\\0\\0\\0\\0\\0\\3\\7\\17\\37><|x\\370\\360\\360\\360\\360\\360\\360\\370x|<>\\37\\17\\7\\3\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\340\\374\\374\\340\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\20\\60p\\360\\360\\360\\360\\360\\360\\360\\360\\370\\377\\377\\377\\377\\377\\377\\370\\360\\360\\360\\360\\360\\360\\360\\360\"\n  \"p\\60\\20\\0\\0\\0\\0\\0\\0\\0\\1\\3\\7\\317\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\317\\7\\3\\1\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0p>\\37\\17\\17\\7\\3\\1\\0\\0\\1\\3\\7\\17\\17\\37>p\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\200\\300\\340\\340\\360\\360\\360\\360\\360\\360\\340\\340\\300\\200\\0\\0\\200\\300\\340\\340\\360\\360\\360\\360\\360\\360\\340\"\n  \"\\340\\300\\200\\0~\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\376\\376\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\"\n  \"\\377\\377\\377~\\0\\1\\3\\7\\17\\37\\77\\177\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\177\\77\\37\\17\"\n  \"\\7\\3\\1\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\1\\3\\7\\17\\37\\77\\177\\177\\77\\37\\17\\7\\3\\1\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\200\\300\\340\\340\\360\\360\\360\\360\\340\\340\\300\\200\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0@\\340\\300\\340@\\0\\0\\0\\376\\377\\377\\177\\177\\177\\237\\207\\347\\371\\371\\371\\377\\376\\0\\0\\0\\0@\"\n  \"\\340\\300\\340@\\2\\4\\4\\35x\\340\\200\\0\\30\\237\\377\\177\\36\\376\\376\\37\\37\\377\\377\\37\\177\\377\\237\\30\\0\\200\\340x\"\n  \"\\34\\5\\4\\2\\0\\0\\0\\0\\0\\1\\3\\3\\3\\1\\0\\0\\0\\17\\17\\0\\0\\17\\17\\0\\0\\0\\1\\3\\3\\3\\1\\0\"\n  \"\\0\\0\\0\";\n*/\n\n/*\n  Fontname: 4LineDisplay_WLED_icons_6x\n  Copyright: Benji (https://github.com/proto-molecule)\n  Glyphs: 11/11\n  BBX Build Mode: 3\n  * 1  = sun\n  * 2  = skip forward\n  * 3  = fire\n  * 4  = custom palette\n  * 5  = puzzle piece\n  * 6  = moon\n  * 7  = brush\n  * 8  = contrast\n  * 9  = power-standby\n  * 10 = star\n  * 11 = heart\n  * 12 = Akemi\n*/\n// you can replace this (wasteful) font by using 3x3 variant with draw2x2Glyph()\nconst uint8_t u8x8_4LineDisplay_WLED_icons_6x6[3460] U8X8_FONT_SECTION(\"u8x8_4LineDisplay_WLED_icons_6x6\") = \n  \"\\1\\14\\6\\6\\0\\0\\0\\0\\0\\0\\200\\300\\300\\300\\300\\200\\0\\0\\0\\0\\0\\0\\0\\0\\0\\36\\77\\77\\77\\77\\36\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\200\\300\\300\\300\\300\\200\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\7\\17\\17\\17\\17\\7\"\n  \"\\0\\0\\0\\0\\200\\300\\340\\340\\340\\360\\360\\360\\360\\360\\360\\340\\340\\340\\300\\200\\0\\0\\0\\0\\7\\17\\17\\17\\17\\7\\0\\0\"\n  \"\\0\\0\\0\\0\\300\\340\\340\\340\\340\\300\\0\\0\\0\\0\\0\\0\\340\\374\\376\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\"\n  \"\\377\\377\\377\\377\\377\\376\\374\\340\\0\\0\\0\\0\\0\\0\\300\\340\\340\\340\\340\\300\\3\\7\\7\\7\\7\\3\\0\\0\\0\\0\\0\\0\"\n  \"\\7\\77\\177\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\177\\77\\7\\0\\0\\0\\0\\0\\0\\3\\7\"\n  \"\\7\\7\\7\\3\\0\\0\\0\\0\\0\\0\\340\\360\\360\\360\\360\\340\\0\\0\\0\\0\\1\\3\\7\\7\\7\\17\\17\\17\\17\\17\\17\\7\"\n  \"\\7\\7\\3\\1\\0\\0\\0\\0\\340\\360\\360\\360\\360\\340\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\1\\3\\3\\3\\3\\1\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0x\\374\\374\\374\\374x\\0\\0\\0\\0\\0\\0\\0\\0\\0\\1\\3\\3\\3\\3\\1\\0\\0\"\n  \"\\0\\0\\0\\0\\300\\200\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\300\\200\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\377\\377\\377\\376\\376\\374\\370\\360\\360\\340\\300\\200\"\n  \"\\200\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\377\\377\\377\\376\\376\\374\\370\\360\\360\\340\\300\\200\\200\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\376\\374\\374\\370\\360\\340\\340\\300\\200\\0\\377\\377\\377\\377\"\n  \"\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\376\\374\\374\\370\\360\\340\\340\\300\\200\\0\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\"\n  \"\\377\\377\\177\\77\\77\\37\\17\\7\\7\\3\\1\\0\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\177\\77\\77\\37\\17\\7\"\n  \"\\7\\3\\1\\0\\377\\377\\377\\177\\177\\77\\37\\17\\17\\7\\3\\1\\1\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\377\\377\\377\\177\"\n  \"\\177\\77\\37\\17\\17\\7\\3\\1\\1\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\3\\1\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\3\\1\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\1\\376\\374\\374\\370\\360\\340\\300\\200\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\200\\340\\360\\374\"\n  \"\\377\\377\\377\\377\\377\\377\\377\\377\\377\\376\\370\\300\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\300\\340\\360\\374\\376\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\37\\0\\0\\0\\0\"\n  \"\\0\\0\\4\\370\\360\\360\\340\\300\\200\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\370\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\"\n  \"\\377\\377\\377\\377\\377\\177\\77\\37\\7\\3\\0\\0\\0\\0\\0\\200\\300\\360\\374\\377\\377\\377\\377\\377\\377\\377\\376\\370\\340\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\3\\37\\177\\377\\377\\377\\377\\377\\377\\377\\377\\377\\77\\17\\7\\1\\0\\0\\0\\0\\0\\200\\300\\360\\370\\374\\376\\377\"\n  \"\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\0\\0\\0\\0\\0\\0\\0\\0\\0\\1\\3\\7\\17\\37\\77\\77\\177\\200\"\n  \"\\0\\0\\0\\0\\0\\0\\340\\374\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\177\\77\\17\\1\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\200\\300\\340\\340\\360\\360\\370|<>>>~\\377\\377\\377\\377\\377\\377\\377\\177\"\n  \"\\77\\36\\36\\36\\36<|\\370\\370\\360\\360\\340\\340\\200\\0\\0\\0\\0\\0\\0\\0\\0\\300\\360\\374\\376\\377\\377\\377\\377\\377\\377\"\n  \"\\377\\360\\340\\300\\300\\300\\300\\340\\360\\377\\377\\377\\377\\377\\377\\370\\360\\340\\340\\340\\340\\360\\370\\377\\377\\377\\377\\377\\377\\377\\377\\377\"\n  \"\\374\\360\\340\\200\\360\\377\\377\\377\\377\\377\\207\\3\\1\\1\\1\\1\\3\\207\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\"\n  \"\\377\\377\\377\\377\\377\\377\\377\\207\\3\\1\\1\\1\\1\\3\\207\\377\\377\\377\\377\\377\\17\\377\\377\\377\\377\\377\\377\\377\\376~>>\"\n  \"\\77\\77\\177\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\376\\376\\376\\376\\377\\377\\377\"\n  \"\\177\\77\\37\\7\\0\\0\\3\\17\\77\\177\\377\\377\\360\\340\\300\\300\\300\\300\\340\\360\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\77\\17\"\n  \"\\17\\7\\7\\7\\7\\7\\7\\7\\7\\7\\3\\3\\3\\3\\1\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\1\\3\\7\\17\\37\"\n  \"\\37\\77\\77\\177\\177\\177\\377\\377\\377\\377\\377\\377\\377\\377\\377~\\30\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\370\\374\\376\\377\\377\\377\\377\\377\\377\\376\\374\\360\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\360\\360\\360\\360\\360\\360\\360\\360\\360\\360\\360\\360\"\n  \"\\360\\363\\377\\377\\377\\377\\377\\377\\377\\377\\363\\360\\360\\360\\360\\360\\360\\360\\360\\360\\360\\360\\360\\360\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\"\n  \"\\377\\377\\377\\377\\377\\377\\377\\377\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\"\n  \"\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\374\\374\\376\\376\\377\\377\\377\\377\"\n  \"\\377\\376\\374\\360\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\177\\77\\37\\17\\17\\17\\17\\17\\17\\37\\77\\177\\377\\377\\377\\377\"\n  \"\\377\\377\\377\\377\\377\\377\\377\\377\\3\\3\\7\\7\\17\\17\\17\\17\\7\\7\\3\\0\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\"\n  \"\\360\\300\\0\\0\\0\\0\\0\\0\\0\\0\\300\\360\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\200\\300\\340\\360\\360\\370\\374\\374\\376\\376\\7\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\300\\360\\374\\376\\377\\377\\377\\377\\377\\377\\377\"\n  \"\\377\\377\\377\\340\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\374\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\374\\360\\300\\200\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\17\\177\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\"\n  \"\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\376\\374\\370\\360\\360\\340\\340\\300\\300\\300\\200\\200\\200\\200\\0\\0\\0\\0\\0\\0\\200\\200\"\n  \"\\200\\200\\0\\0\\0\\0\\1\\7\\37\\77\\177\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\"\n  \"\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\177\\77\\37\\7\\1\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\1\\3\\3\\7\"\n  \"\\17\\17\\37\\37\\37\\77\\77\\77\\77\\177\\177\\177\\177\\177\\177\\77\\77\\77\\77\\37\\37\\37\\17\\17\\7\\3\\3\\1\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\200\\200\\300\\340\\360\\360\\370\\374\\374\\376\\377~\\34\\10\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\200\\300\\300\\340\\360\\360\\370\\374\\376\\376\\377\\377\\377\\377\\377\\377\\177\\77\\17\\7\\3\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\4\\6\\17\\17\\37\\77\\177\\377\"\n  \"\\377\\377\\377\\377\\377\\377\\77\\37\\7\\3\\1\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\300\\370\\374\\376\"\n  \"\\376\\377\\377\\377\\377\\377\\377\\376\\376\\374\\370\\340\\0\\0\\0\\0\\3\\17\\7\\3\\1\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\200\\360\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\177\\17\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0`px\\374\\376\\377\\377\\377\\377\\377\\377\"\n  \"\\177\\177\\177\\77\\77\\37\\17\\7\\3\\1\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\200\\300\\340\\360\\360\\370\\374\\374\\374\\376\\376\\376\\377\\377\\377\\377\\377\\77\\77\\77\\77\"\n  \"\\177~~\\376\\374\\374\\374\\370\\360\\360\\340\\300\\200\\0\\0\\0\\0\\0\\0\\0\\0\\0\\340\\360\\374\\376\\377\\377\\377\\377\\377\\377\"\n  \"\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\0\\0\\0\\0\\0\\0\\0\\0\\0\\1\\1\\3\\7\\17\\37\\177\\377\\377\\376\\374\"\n  \"\\360\\340\\0\\0\\370\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\1\\17\\377\\377\\377\\377\\377\\370\\37\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\"\n  \"\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\200\\360\\377\\377\"\n  \"\\377\\377\\377\\37\\0\\0\\7\\17\\77\\177\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\200\\200\\300\\340\\360\\370\\376\\377\\377\\177\\77\\17\\7\\0\\0\\0\\0\\0\\0\\0\\0\\0\\1\\3\\7\\17\\17\"\n  \"\\37\\77\\77\\77\\177\\177\\177\\377\\377\\377\\377\\377\\374\\374\\374\\374\\376~~\\177\\77\\77\\77\\37\\17\\17\\7\\3\\1\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\377\\377\\377\\377\\377\\377\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\200\\300\\340\\360\\370\\374\\376\\376|\"\n  \"x \\0\\0\\0\\0\\377\\377\\377\\377\\377\\377\\0\\0\\0\\0 x|\\376\\376\\374\\370\\360\\340\\300\\200\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\300\\370\\376\\377\\377\\377\\177\\17\\7\\1\\0\\0\\0\\0\\0\\0\\0\\0\\377\\377\\377\\377\\377\\377\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\1\\7\\37\\177\\377\\377\\377\\376\\370\\200\\0\\0\\0\\0\\0\\0\\177\\377\\377\\377\\377\\377\\200\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\200\\377\\377\\377\\377\\377\\177\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\7\\37\\177\\377\\377\\377\\374\\370\\340\\300\\200\\200\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\200\\200\\300\\340\\370\\374\\377\\377\\377\\177\\37\\7\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\1\\3\\7\\17\\37\\37\\77\"\n  \"\\77\\177~~~\\374\\374\\374\\374\\374\\374\\374\\374~~~\\177\\77\\77\\37\\37\\17\\7\\3\\1\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\340\\374\\374\\340\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\300\\370\\377\\377\\377\\377\\377\\377\\370\\300\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\4\\14\\34<<|\\374\\374\\374\\374\\374\\374\\374\\374\\374\\374\\374\\376\\377\\377\\377\\377\\377\\377\\377\\377\\377\"\n  \"\\377\\376\\374\\374\\374\\374\\374\\374\\374\\374\\374\\374\\374|<<\\34\\14\\4\\0\\0\\0\\0\\0\\0\\0\\0\\0\\1\\3\\3\\7\"\n  \"\\17\\37\\77\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\77\\37\\17\\7\\3\\3\\1\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\300\\370\\377\\377\\377\\377\\377\\377\\177\\77\\37\\17\\17\\37\\77\\177\"\n  \"\\377\\377\\377\\377\\377\\377\\370\\300\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0p>\"\n  \"\\37\\17\\17\\7\\3\\1\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\1\\3\\7\\17\\17\\37>p\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\200\\200\\200\\300\\300\\300\\300\\300\\300\\200\\200\\200\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\200\\200\\200\\300\\300\\300\\300\\300\\300\\200\\200\\200\\0\\0\\0\\0\\0\\0\\200\\360\\370\\374\\376\\377\\377\\377\\377\\377\\377\\377\"\n  \"\\377\\377\\377\\377\\377\\377\\377\\376\\374\\370\\360\\200\\200\\360\\370\\374\\376\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\376\"\n  \"\\374\\370\\360\\200\\37\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\"\n  \"\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\37\\0\\0\\1\\3\\7\\17\\37\\77\\177\\377\\377\\377\"\n  \"\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\177\\77\\37\\17\\7\"\n  \"\\3\\1\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\1\\3\\7\\17\\37\\77\\177\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\"\n  \"\\377\\377\\377\\177\\77\\37\\17\\7\\3\\1\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\1\\3\\7\\17\\37\\77\\77\\37\\17\\7\\3\\1\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\300\\300\\300\\300\\300\\300\\300\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\340\\370\\370\\376\\376\\377\\377\\377\\377\\377\\377\\377\\377\\77\\77\\77>\\376\\370\\370\\340\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0 p\\360\\340\\360p \\0\\0\\0\\0\\0\\0\\377\\377\\377\\377\\177\\177\\177\\177\\177\\207\\207\\340\\340\\377\"\n  \"\\377\\377\\377\\377\\377\\377\\377\\0\\0\\0\\0\\0 p\\360\\340\\360p \\0\\6\\4\\14\\14\\15|x\\360\\200\\200\\0\\0\"\n  \"pp\\177\\177\\377\\377\\374|\\374\\374\\374\\177\\177\\177\\377\\377\\377\\177\\377\\377\\377\\377\\177pp\\0\\0\\200\\200\\360x}\"\n  \"\\14\\14\\4\\6\\0\\0\\0\\0\\0\\0\\0\\3\\37\\37|ppp\\34\\34\\37\\3\\3\\0\\377\\377\\377\\0\\0\\0\\377\\377\"\n  \"\\377\\0\\3\\3\\37\\37\\34ppp~\\37\\37\\3\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\7\\7\\7\\0\\0\\0\\7\\7\\7\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\";\n\n\n\n/*\n  Fontname: akemi_8x8\n  Copyright: Benji (https://github.com/proto-molecule)\n  Glyphs: 1/1\n  BBX Build Mode: 3\n  * 12 = Akemi\n*/\n/*\nconst uint8_t u8x8_akemi_8x8[516] U8X8_FONT_SECTION(\"u8x8_akemi_8x8\") = \n  \"\\14\\14\\10\\10\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\200\\200\\200\\200\\200\\200\\200\\200\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\340\\340\\370\\370\\376\\376\\376\\376\"\n  \"\\377\\377\\377\\377\\377\\377\\377\\377\\376\\376\\376\\376\\370\\370\\340\\340\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\376\\376\\377\\377\\377\\377\\377\\377\\377\\377\"\n  \"\\377\\377\\377\\377\\37\\37\\37\\343\\343\\343\\343\\343\\343\\377\\377\\377\\376\\376\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\30\\30~~\\370\\370~~\\30\\30\\0\\0\\0\\0\\0\\0\\0\\377\\377\\377\\377\\377\\77\\77\\77\\77\\77\"\n  \"\\77\\300\\300\\300\\370\\370\\370\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\0\\0\\0\\0\\0\\0\\0\\30\\0f\\0\\200\\0\\0\"\n  \"\\0\\0\\0\\0\\6\\6\\30\\30\\30\\31\\371\\370\\370\\340\\340\\0\\0\\0\\0\\0\\340\\340\\377\\377\\377\\377\\377\\376\\376\\376\\376\\376\"\n  \"\\376\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\377\\371\\346\\346\\6\\6\\6\\6\\6\\0\\340\\340\\340\\341\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\1\\1\\37\\37\\377\\376\\376\\340\\340\\200\\201\\201\\341\\341\\177\\177\\37\\37\\1\\1\\377\\377\"\n  \"\\377\\377\\1\\1\\1\\1\\377\\377\\377\\377\\1\\1\\37\\37\\177\\177\\341\\341\\201\\201\\200\\200\\370\\370\\376\\376\\37\\37\\1\\1\\0\\0\"\n  \"\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\1\\1\\7\\7\\7\\7\\7\\7\\1\\1\\0\\0\\0\\0\\0\\0\\377\\377\"\n  \"\\377\\377\\0\\0\\0\\0\\377\\377\\377\\377\\0\\0\\0\\0\\0\\0\\1\\1\\7\\7\\7\\7\\7\\7\\1\\1\\0\\0\\0\\0\\0\\0\"\n  \"\\0\\0\\0\";\n*/"
  },
  {
    "path": "usermods/usermod_v2_four_line_display_ALT/library.json",
    "content": "{\n  \"name\": \"four_line_display_ALT\",\n  \"build\": { \"libArchive\": false },\n  \"dependencies\": {\n    \"U8g2\": \"~2.34.4\",\n    \"Wire\": \"\"\n  }  \n}"
  },
  {
    "path": "usermods/usermod_v2_four_line_display_ALT/platformio_override.sample.ini",
    "content": "[platformio]\ndefault_envs = esp32dev_fld\n\n[env:esp32dev_fld]\nextends = env:esp32dev_V4\ncustom_usermods = ${env:esp32dev_V4.custom_usermods} four_line_display_ALT\nbuild_flags =\n    ${env:esp32dev_V4.build_flags}\n    -D FLD_TYPE=SH1106\n    -D I2CSCLPIN=27\n    -D I2CSDAPIN=26\n"
  },
  {
    "path": "usermods/usermod_v2_four_line_display_ALT/readme.md",
    "content": "# I2C/SPI 4 Line Display Usermod ALT\n\nThis usermod could be used in compination with `usermod_v2_rotary_encoder_ui_ALT`.\n\n## Functionalities\n\nPress the encoder to cycle through the options:\n\n* Brightness\n* Speed\n* Intensity\n* Palette\n* Effect\n* Main Color\n* Saturation\n\nPress and hold the encoder to display Network Info. If AP is active, it will display the AP, SSID and Password\n\nAlso shows if the timer is enabled.\n\n[See the pair of usermods in action](https://www.youtube.com/watch?v=ulZnBt9z3TI)\n\n## Installation\n\nCopy the example `platformio_override.sample.ini` to the root directory of your particular build.\n\n## Configuration\n\nThese options are configurable in Config > Usermods\n\n### Usermod Setup\n\n* Global I2C GPIOs (HW) - Set the SDA and SCL pins\n\n### 4LineDisplay\n\n* `enabled` - enable/disable usermod\n* `type` - display type in numeric format\n  * 1 = I2C SSD1306 128x32\n  * 2 = I2C SH1106 128x32\n  * 3 = I2C SSD1306 128x64 (4 double-height lines)\n  * 4 = I2C SSD1305 128x32\n  * 5 = I2C SSD1305 128x64 (4 double-height lines)\n  * 6 = SPI SSD1306 128x32\n  * 7 = SPI SSD1306 128x64 (4 double-height lines)\n  * 8 = SPI SSD1309 128x64 (4 double-height lines)\n  * 9 = I2C SSD1309 128x64 (4 double-height lines)\n* `pin` - GPIO pins used for display; SPI displays can use SCK, MOSI, CS, DC & RST\n* `flip` - flip/rotate display 180°\n* `contrast` - set display contrast (higher contrast may reduce display lifetime)\n* `screenTimeOutSec` - screen saver time-out in seconds\n* `sleepMode` - enable/disable screen saver\n* `clockMode` - enable/disable clock display in screen saver mode\n* `showSeconds` - Show seconds on the clock display\n* `i2c-freq-kHz` - I2C clock frequency in kHz (may help reduce dropped frames, range: 400-3400)\n\n### PlatformIO requirements\n\nNote: the Four Line Display usermod requires the libraries `U8g2` and `Wire`.\n\n## Change Log\n\n2021-10\n\n* First public release\n"
  },
  {
    "path": "usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display.h",
    "content": "#include \"wled.h\"\r\n#undef U8X8_NO_HW_I2C // borrowed from WLEDMM: we do want I2C hardware drivers - if possible\r\n#include <U8x8lib.h> // from https://github.com/olikraus/u8g2/\r\n\r\n#pragma once\r\n\r\n#ifndef FLD_ESP32_NO_THREADS\r\n  #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!!\r\n#endif\r\n\r\n#ifndef FLD_PIN_CS\r\n  #define FLD_PIN_CS 15\r\n#endif\r\n\r\n#ifdef ARDUINO_ARCH_ESP32\r\n  #ifndef FLD_PIN_DC\r\n    #define FLD_PIN_DC 19\r\n  #endif\r\n  #ifndef FLD_PIN_RESET\r\n    #define FLD_PIN_RESET 26\r\n  #endif\r\n#else\r\n  #ifndef FLD_PIN_DC\r\n    #define FLD_PIN_DC 12\r\n  #endif\r\n  #ifndef FLD_PIN_RESET\r\n    #define FLD_PIN_RESET 16\r\n  #endif\r\n#endif\r\n\r\n#ifndef FLD_TYPE\r\n  #ifndef FLD_SPI_DEFAULT\r\n    #define FLD_TYPE SSD1306\r\n  #else\r\n    #define FLD_TYPE SSD1306_SPI\r\n  #endif\r\n#endif\r\n\r\n// When to time out to the clock or blank the screen\r\n// if SLEEP_MODE_ENABLED.\r\n#define SCREEN_TIMEOUT_MS  60*1000    // 1 min\r\n\r\n// Minimum time between redrawing screen in ms\r\n#define REFRESH_RATE_MS 1000\r\n\r\n// Extra char (+1) for null\r\n#define LINE_BUFFER_SIZE            16+1\r\n#define MAX_JSON_CHARS              19+1\r\n#define MAX_MODE_LINE_SPACE         13+1\r\n\r\ntypedef enum {\r\n  NONE = 0,\r\n  SSD1306,          // U8X8_SSD1306_128X32_UNIVISION_HW_I2C\r\n  SH1106,           // U8X8_SH1106_128X64_WINSTAR_HW_I2C\r\n  SSD1306_64,       // U8X8_SSD1306_128X64_NONAME_HW_I2C\r\n  SSD1305,          // U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C\r\n  SSD1305_64,       // U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C\r\n  SSD1306_SPI,      // U8X8_SSD1306_128X32_NONAME_HW_SPI\r\n  SSD1306_SPI64,    // U8X8_SSD1306_128X64_NONAME_HW_SPI\r\n  SSD1309_SPI64,    // U8X8_SSD1309_128X64_NONAME0_4W_HW_SPI\r\n  SSD1309_64        // U8X8_SSD1309_128X64_NONAME0_HW_I2C\r\n} DisplayType;\r\n\r\nclass FourLineDisplayUsermod : public Usermod {\r\n  #if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS)\r\n    public:\r\n      FourLineDisplayUsermod() { if (!instance) instance = this; }\r\n      static FourLineDisplayUsermod* getInstance(void) { return instance; }\r\n  #endif\r\n  \r\n    private:\r\n  \r\n      static FourLineDisplayUsermod *instance;\r\n      bool initDone = false;\r\n      volatile bool drawing = false;\r\n      volatile bool lockRedraw = false;\r\n  \r\n      // HW interface & configuration\r\n      U8X8 *u8x8 = nullptr;           // pointer to U8X8 display object\r\n  \r\n      #ifndef FLD_SPI_DEFAULT\r\n      int8_t ioPin[3] = {-1, -1, -1}; // I2C pins: SCL, SDA\r\n      uint32_t ioFrequency = 400000;  // in Hz (minimum is 100000, baseline is 400000 and maximum should be 3400000)\r\n      #else\r\n      int8_t ioPin[3] = {FLD_PIN_CS, FLD_PIN_DC, FLD_PIN_RESET}; // custom SPI pins: CS, DC, RST\r\n      uint32_t ioFrequency = 1000000;  // in Hz (minimum is 500kHz, baseline is 1MHz and maximum should be 20MHz)\r\n      #endif\r\n  \r\n      DisplayType type = FLD_TYPE;    // display type\r\n      bool flip = false;              // flip display 180°\r\n      uint8_t contrast = 10;          // screen contrast\r\n      uint8_t lineHeight = 1;         // 1 row or 2 rows\r\n      uint16_t refreshRate = REFRESH_RATE_MS;     // in ms\r\n      uint32_t screenTimeout = SCREEN_TIMEOUT_MS; // in ms\r\n      bool sleepMode = true;          // allow screen sleep?\r\n      bool clockMode = false;         // display clock\r\n      bool showSeconds = true;        // display clock with seconds\r\n      bool enabled = true;\r\n      bool contrastFix = false;\r\n  \r\n      // Next variables hold the previous known values to determine if redraw is\r\n      // required.\r\n      String knownSsid = apSSID;\r\n      IPAddress knownIp = IPAddress(4, 3, 2, 1);\r\n      uint8_t knownBrightness = 0;\r\n      uint8_t knownEffectSpeed = 0;\r\n      uint8_t knownEffectIntensity = 0;\r\n      uint8_t knownMode = 0;\r\n      uint8_t knownPalette = 0;\r\n      uint8_t knownMinute = 99;\r\n      uint8_t knownHour = 99;\r\n      byte brightness100;\r\n      byte fxspeed100;\r\n      byte fxintensity100;\r\n      bool knownnightlight = nightlightActive;\r\n      bool wificonnected = interfacesInited;\r\n      bool powerON = true;\r\n  \r\n      bool displayTurnedOff = false;\r\n      unsigned long nextUpdate = 0;\r\n      unsigned long lastRedraw = 0;\r\n      unsigned long overlayUntil = 0;\r\n  \r\n      // Set to 2 or 3 to mark lines 2 or 3. Other values ignored.\r\n      byte markLineNum = 255;\r\n      byte markColNum = 255;\r\n  \r\n      // strings to reduce flash memory usage (used more than twice)\r\n      static const char _name[];\r\n      static const char _enabled[];\r\n      static const char _contrast[];\r\n      static const char _refreshRate[];\r\n      static const char _screenTimeOut[];\r\n      static const char _flip[];\r\n      static const char _sleepMode[];\r\n      static const char _clockMode[];\r\n      static const char _showSeconds[];\r\n      static const char _busClkFrequency[];\r\n      static const char _contrastFix[];\r\n  \r\n      // If display does not work or looks corrupted check the\r\n      // constructor reference:\r\n      // https://github.com/olikraus/u8g2/wiki/u8x8setupcpp\r\n      // or check the gallery:\r\n      // https://github.com/olikraus/u8g2/wiki/gallery\r\n  \r\n      // some displays need this to properly apply contrast\r\n      void setVcomh(bool highContrast);\r\n      void startDisplay();\r\n  \r\n      /**\r\n       * Wrappers for screen drawing\r\n       */\r\n      void setFlipMode(uint8_t mode);\r\n      void setContrast(uint8_t contrast);\r\n      void drawString(uint8_t col, uint8_t row, const char *string, bool ignoreLH=false);\r\n      void draw2x2String(uint8_t col, uint8_t row, const char *string);\r\n      void drawGlyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font, bool ignoreLH=false);\r\n      void draw2x2Glyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font);\r\n      void draw2x2GlyphIcons();\r\n      uint8_t getCols();\r\n      void clear();\r\n      void setPowerSave(uint8_t save);\r\n      void center(String &line, uint8_t width);\r\n  \r\n      /**\r\n       * Display the current date and time in large characters\r\n       * on the middle rows. Based 24 or 12 hour depending on\r\n       * the useAMPM configuration.\r\n       */\r\n      void showTime();\r\n  \r\n      /**\r\n       * Enable sleep (turn the display off) or clock mode.\r\n       */\r\n      void sleepOrClock(bool enabled);\r\n  \r\n    public:\r\n  \r\n      // gets called once at boot. Do all initialization that doesn't depend on\r\n      // network here\r\n      void setup() override;\r\n  \r\n      // gets called every time WiFi is (re-)connected. Initialize own network\r\n      // interfaces here\r\n      void connected() override;\r\n  \r\n      /**\r\n       * Da loop.\r\n       */\r\n      void loop() override;\r\n  \r\n      //function to update lastredraw\r\n      inline void updateRedrawTime() { lastRedraw = millis(); }\r\n  \r\n      /**\r\n       * Redraw the screen (but only if things have changed\r\n       * or if forceRedraw).\r\n       */\r\n      void redraw(bool forceRedraw);\r\n  \r\n      void updateBrightness();\r\n      void updateSpeed();\r\n      void updateIntensity();\r\n      void drawStatusIcons();\r\n  \r\n      /**\r\n       * marks the position of the arrow showing\r\n       * the current setting being changed\r\n       * pass line and colum info\r\n       */\r\n      void setMarkLine(byte newMarkLineNum, byte newMarkColNum);\r\n  \r\n      //Draw the arrow for the current setting being changed\r\n      void drawArrow();\r\n  \r\n      //Display the current effect or palette (desiredEntry)\r\n      // on the appropriate line (row).\r\n      void showCurrentEffectOrPalette(int inputEffPal, const char *qstring, uint8_t row);\r\n  \r\n      /**\r\n       * If there screen is off or in clock is displayed,\r\n       * this will return true. This allows us to throw away\r\n       * the first input from the rotary encoder but\r\n       * to wake up the screen.\r\n       */\r\n      bool wakeDisplay();\r\n  \r\n      /**\r\n       * Allows you to show one line and a glyph as overlay for a period of time.\r\n       * Clears the screen and prints.\r\n       * Used in Rotary Encoder usermod.\r\n       */\r\n      void overlay(const char* line1, long showHowLong, byte glyphType);\r\n  \r\n      /**\r\n       * Allows you to show Akemi WLED logo overlay for a period of time.\r\n       * Clears the screen and prints.\r\n       */\r\n      void overlayLogo(long showHowLong);\r\n  \r\n      /**\r\n       * Allows you to show two lines as overlay for a period of time.\r\n       * Clears the screen and prints.\r\n       * Used in Auto Save usermod\r\n       */\r\n      void overlay(const char* line1, const char* line2, long showHowLong);\r\n  \r\n      void networkOverlay(const char* line1, long showHowLong);\r\n  \r\n      /**\r\n       * handleButton() can be used to override default button behaviour. Returning true\r\n       * will prevent button working in a default way.\r\n       * Replicating button.cpp\r\n       */\r\n      bool handleButton(uint8_t b);\r\n  \r\n      void onUpdateBegin(bool init) override;\r\n  \r\n      /*\r\n       * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.\r\n       * Creating an \"u\" object allows you to add custom key/value pairs to the Info section of the WLED web UI.\r\n       * Below it is shown how this could be used for e.g. a light sensor\r\n       */\r\n      //void addToJsonInfo(JsonObject& root) override;\r\n  \r\n      /*\r\n       * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).\r\n       * Values in the state object may be modified by connected clients\r\n       */\r\n      //void addToJsonState(JsonObject& root) override;\r\n  \r\n      /*\r\n       * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).\r\n       * Values in the state object may be modified by connected clients\r\n       */\r\n      //void readFromJsonState(JsonObject& root) override;\r\n  \r\n      void appendConfigData() override;\r\n  \r\n      /*\r\n       * addToConfig() can be used to add custom persistent settings to the cfg.json file in the \"um\" (usermod) object.\r\n       * It will be called by WLED when settings are actually saved (for example, LED settings are saved)\r\n       * If you want to force saving the current state, use serializeConfig() in your loop().\r\n       *\r\n       * CAUTION: serializeConfig() will initiate a filesystem write operation.\r\n       * It might cause the LEDs to stutter and will cause flash wear if called too often.\r\n       * Use it sparingly and always in the loop, never in network callbacks!\r\n       *\r\n       * addToConfig() will also not yet add your setting to one of the settings pages automatically.\r\n       * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually.\r\n       *\r\n       * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings!\r\n       */\r\n      void addToConfig(JsonObject& root) override;\r\n  \r\n      /*\r\n       * readFromConfig() can be used to read back the custom settings you added with addToConfig().\r\n       * This is called by WLED when settings are loaded (currently this only happens once immediately after boot)\r\n       *\r\n       * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes),\r\n       * 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.\r\n       * If you don't know what that is, don't fret. It most likely doesn't affect your use case :)\r\n       */\r\n      bool readFromConfig(JsonObject& root) override;\r\n  \r\n      /*\r\n       * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).\r\n       * This could be used in the future for the system to determine whether your usermod is installed.\r\n       */\r\n      uint16_t getId() override {\r\n        return USERMOD_ID_FOUR_LINE_DISP;\r\n      }\r\n  };\r\n  "
  },
  {
    "path": "usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.cpp",
    "content": "#include \"usermod_v2_four_line_display.h\"\n#include \"4LD_wled_fonts.h\"\n\n//\n// Inspired by the usermod_v2_four_line_display\n//\n// v2 usermod for using 128x32 or 128x64 i2c\n// OLED displays to provide a four line display\n// for WLED.\n//\n// Dependencies\n// * This Usermod works best, by far, when coupled\n//   with RotaryEncoderUI ALT Usermod.\n//\n// Make sure to enable NTP and set your time zone in WLED Config | Time.\n//\n// If display does not work or looks corrupted check the\n// constructor reference:\n// https://github.com/olikraus/u8g2/wiki/u8x8setupcpp\n// or check the gallery:\n// https://github.com/olikraus/u8g2/wiki/gallery\n\n\n#ifdef ARDUINO_ARCH_ESP32\nstatic TaskHandle_t Display_Task = nullptr;\nvoid DisplayTaskCode(void * parameter);\n#endif\n\n// strings to reduce flash memory usage (used more than twice)\nconst char FourLineDisplayUsermod::_name[]            PROGMEM = \"4LineDisplay\";\nconst char FourLineDisplayUsermod::_enabled[]         PROGMEM = \"enabled\";\nconst char FourLineDisplayUsermod::_contrast[]        PROGMEM = \"contrast\";\nconst char FourLineDisplayUsermod::_refreshRate[]     PROGMEM = \"refreshRate-ms\";\nconst char FourLineDisplayUsermod::_screenTimeOut[]   PROGMEM = \"screenTimeOutSec\";\nconst char FourLineDisplayUsermod::_flip[]            PROGMEM = \"flip\";\nconst char FourLineDisplayUsermod::_sleepMode[]       PROGMEM = \"sleepMode\";\nconst char FourLineDisplayUsermod::_clockMode[]       PROGMEM = \"clockMode\";\nconst char FourLineDisplayUsermod::_showSeconds[]     PROGMEM = \"showSeconds\";\nconst char FourLineDisplayUsermod::_busClkFrequency[] PROGMEM = \"i2c-freq-kHz\";\nconst char FourLineDisplayUsermod::_contrastFix[]     PROGMEM = \"contrastFix\";\n\n#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS)\nFourLineDisplayUsermod *FourLineDisplayUsermod::instance = nullptr;\n#endif\n\n// some displays need this to properly apply contrast\nvoid FourLineDisplayUsermod::setVcomh(bool highContrast) {\n  if (type == NONE || !enabled) return;\n  u8x8_t *u8x8_struct = u8x8->getU8x8();\n  u8x8_cad_StartTransfer(u8x8_struct);\n  u8x8_cad_SendCmd(u8x8_struct, 0x0db); //address of value\n  u8x8_cad_SendArg(u8x8_struct, highContrast ? 0x000 : 0x040); //value 0 for fix, reboot resets default back to 64\n  u8x8_cad_EndTransfer(u8x8_struct);\n}\n\nvoid FourLineDisplayUsermod::startDisplay() {\n  if (type == NONE || !enabled) return;\n  lineHeight = u8x8->getRows() > 4 ? 2 : 1;\n  DEBUG_PRINTLN(F(\"Starting display.\"));\n  u8x8->setBusClock(ioFrequency);  // can be used for SPI too\n  u8x8->begin();\n  setFlipMode(flip);\n  setVcomh(contrastFix);\n  setContrast(contrast); //Contrast setup will help to preserve OLED lifetime. In case OLED need to be brighter increase number up to 255\n  setPowerSave(0);\n  //drawString(0, 0, \"Loading...\");\n  overlayLogo(3500);\n}\n\n/**\n * Wrappers for screen drawing\n */\nvoid FourLineDisplayUsermod::setFlipMode(uint8_t mode) {\n  if (type == NONE || !enabled) return;\n  u8x8->setFlipMode(mode);\n}\nvoid FourLineDisplayUsermod::setContrast(uint8_t contrast) {\n  if (type == NONE || !enabled) return;\n  u8x8->setContrast(contrast);\n}\nvoid FourLineDisplayUsermod::drawString(uint8_t col, uint8_t row, const char *string, bool ignoreLH) {\n  if (type == NONE || !enabled) return;\n  drawing = true;\n  u8x8->setFont(u8x8_font_chroma48medium8_r);\n  if (!ignoreLH && lineHeight==2) u8x8->draw1x2String(col, row, string);\n  else                            u8x8->drawString(col, row, string);\n  drawing = false;\n}\nvoid FourLineDisplayUsermod::draw2x2String(uint8_t col, uint8_t row, const char *string) {\n  if (type == NONE || !enabled) return;\n  drawing = true;\n  u8x8->setFont(u8x8_font_chroma48medium8_r);\n  u8x8->draw2x2String(col, row, string);\n  drawing = false;\n}\nvoid FourLineDisplayUsermod::drawGlyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font, bool ignoreLH) {\n  if (type == NONE || !enabled) return;\n  drawing = true;\n  u8x8->setFont(font);\n  if (!ignoreLH && lineHeight==2) u8x8->draw1x2Glyph(col, row, glyph);\n  else                            u8x8->drawGlyph(col, row, glyph);\n  drawing = false;\n}\nvoid FourLineDisplayUsermod::draw2x2Glyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font) {\n  if (type == NONE || !enabled) return;\n  drawing = true;\n  u8x8->setFont(font);\n  u8x8->draw2x2Glyph(col, row, glyph);\n  drawing = false;\n}\nuint8_t FourLineDisplayUsermod::getCols() {\n  if (type==NONE || !enabled) return 0;\n  return u8x8->getCols();\n}\nvoid FourLineDisplayUsermod::clear() {\n  if (type == NONE || !enabled) return;\n  drawing = true;\n  u8x8->clear();\n  drawing = false;\n}\nvoid FourLineDisplayUsermod::setPowerSave(uint8_t save) {\n  if (type == NONE || !enabled) return;\n  u8x8->setPowerSave(save);\n}\n\nvoid FourLineDisplayUsermod::center(String &line, uint8_t width) {\n  int len = line.length();\n  if (len<width) for (unsigned i=(width-len)/2; i>0; i--) line = ' ' + line;\n  for (unsigned i=line.length(); i<width; i++) line += ' ';\n}\n\nvoid FourLineDisplayUsermod::draw2x2GlyphIcons() {\n  drawing = true;\n  if (lineHeight == 2) {\n    drawGlyph( 1,            0, 1, u8x8_4LineDisplay_WLED_icons_2x2, true); //brightness icon\n    drawGlyph( 5,            0, 2, u8x8_4LineDisplay_WLED_icons_2x2, true); //speed icon\n    drawGlyph( 9,            0, 3, u8x8_4LineDisplay_WLED_icons_2x2, true); //intensity icon\n    drawGlyph(14, 2*lineHeight, 4, u8x8_4LineDisplay_WLED_icons_2x2, true); //palette icon\n    drawGlyph(14, 3*lineHeight, 5, u8x8_4LineDisplay_WLED_icons_2x2, true); //effect icon\n  } else {\n    drawGlyph( 1, 0, 1, u8x8_4LineDisplay_WLED_icons_2x1); //brightness icon\n    drawGlyph( 5, 0, 2, u8x8_4LineDisplay_WLED_icons_2x1); //speed icon\n    drawGlyph( 9, 0, 3, u8x8_4LineDisplay_WLED_icons_2x1); //intensity icon\n    drawGlyph(15, 2, 4, u8x8_4LineDisplay_WLED_icons_1x1); //palette icon\n    drawGlyph(15, 3, 5, u8x8_4LineDisplay_WLED_icons_1x1); //effect icon\n  }\n  drawing = false;\n}\n\n/**\n * Display the current date and time in large characters\n * on the middle rows. Based 24 or 12 hour depending on\n * the useAMPM configuration.\n */\nvoid FourLineDisplayUsermod::showTime() {\n  if (type == NONE || !enabled || !displayTurnedOff) return;\n\n  unsigned long now = millis();\n  while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing\n  if (drawing) return;\n\n  char lineBuffer[LINE_BUFFER_SIZE];\n  static byte lastSecond;\n  byte secondCurrent = second(localTime);\n  byte minuteCurrent = minute(localTime);\n  byte hourCurrent   = hour(localTime);\n\n  if (knownMinute != minuteCurrent) {  //only redraw clock if it has changed\n    //updateLocalTime();\n    byte AmPmHour = hourCurrent;\n    boolean isitAM = true;\n    if (useAMPM) {\n      if (AmPmHour > 11) { AmPmHour -= 12; isitAM = false; }\n      if (AmPmHour == 0) { AmPmHour  = 12; }\n    }\n    if (knownHour != hourCurrent) {\n      // only update date when hour changes\n      sprintf_P(lineBuffer, PSTR(\"%s %2d \"), monthShortStr(month(localTime)), day(localTime));\n      draw2x2String(2, lineHeight==1 ? 0 : lineHeight, lineBuffer); // adjust for 8 line displays, draw month and day\n    }\n    sprintf_P(lineBuffer,PSTR(\"%2d:%02d\"), (useAMPM ? AmPmHour : hourCurrent), minuteCurrent);\n    draw2x2String(2, lineHeight*2, lineBuffer); //draw hour, min. blink \":\" depending on odd/even seconds\n    if (useAMPM) drawString(12, lineHeight*2, (isitAM ? \"AM\" : \"PM\"), true); //draw am/pm if using 12 time\n\n    drawStatusIcons(); //icons power, wifi, timer, etc\n\n    knownMinute = minuteCurrent;\n    knownHour   = hourCurrent;\n  }\n  if (showSeconds && secondCurrent != lastSecond) {\n    lastSecond = secondCurrent;\n    draw2x2String(6, lineHeight*2, secondCurrent%2 ? \" \" : \":\");\n    sprintf_P(lineBuffer, PSTR(\"%02d\"), secondCurrent);\n    drawString(12, lineHeight*2+1, lineBuffer, true); // even with double sized rows print seconds in 1 line\n  }\n}\n\n/**\n * Enable sleep (turn the display off) or clock mode.\n */\nvoid FourLineDisplayUsermod::sleepOrClock(bool enabled) {\n  if (enabled) {\n    displayTurnedOff = true;\n    if (clockMode && ntpEnabled) {\n      knownMinute = knownHour = 99;\n      showTime();\n    } else\n      setPowerSave(1);\n  } else {\n    displayTurnedOff = false;\n    setPowerSave(0);\n  }\n}\n\n// gets called once at boot. Do all initialization that doesn't depend on\n// network here\nvoid FourLineDisplayUsermod::setup() {\n  bool isSPI = (type == SSD1306_SPI || type == SSD1306_SPI64 || type == SSD1309_SPI64);\n\n  // check if pins are -1 and disable usermod as PinManager::allocateMultiplePins() will accept -1 as a valid pin\n  if (isSPI) {\n    if (spi_sclk<0 || spi_mosi<0 || ioPin[0]<0 || ioPin[1]<0 || ioPin[1]<0) {\n      type = NONE;\n    } else {\n      PinManagerPinType cspins[3] = { { ioPin[0], true }, { ioPin[1], true }, { ioPin[2], true } };\n      if (!PinManager::allocateMultiplePins(cspins, 3, PinOwner::UM_FourLineDisplay)) { type = NONE; }\n    }\n  } else {\n    if (i2c_scl<0 || i2c_sda<0) { type=NONE; }\n  }\n\n  DEBUG_PRINTLN(F(\"Allocating display.\"));\n  switch (type) {\n    // U8X8 uses Wire (or Wire1 with 2ND constructor) and will use existing Wire properties (calls Wire.begin() though)\n    case SSD1306:       u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_HW_I2C(); break;\n    case SH1106:        u8x8 = (U8X8 *) new U8X8_SH1106_128X64_WINSTAR_HW_I2C();    break;\n    case SSD1306_64:    u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_HW_I2C();    break;\n    case SSD1305:       u8x8 = (U8X8 *) new U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C();  break;\n    case SSD1305_64:    u8x8 = (U8X8 *) new U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C();  break;\n    case SSD1309_64:    u8x8 = (U8X8 *) new U8X8_SSD1309_128X64_NONAME0_HW_I2C();   break;\n    // U8X8 uses global SPI variable that is attached to VSPI bus on ESP32\n    case SSD1306_SPI:   u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_4W_HW_SPI(ioPin[0], ioPin[1], ioPin[2]); break; // Pins are cs, dc, reset\n    case SSD1306_SPI64: u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_4W_HW_SPI(ioPin[0], ioPin[1], ioPin[2]);    break; // Pins are cs, dc, reset\n    case SSD1309_SPI64: u8x8 = (U8X8 *) new U8X8_SSD1309_128X64_NONAME0_4W_HW_SPI(ioPin[0], ioPin[1], ioPin[2]);   break; // Pins are cs, dc, reset\n    // catchall\n    default:            u8x8 = (U8X8 *) new U8X8_NULL(); enabled = false; break; // catchall to create U8x8 instance\n  }\n\n  if (nullptr == u8x8) {\n    DEBUG_PRINTLN(F(\"Display init failed.\"));\n    if (isSPI) {\n      PinManager::deallocateMultiplePins((const uint8_t*)ioPin, 3, PinOwner::UM_FourLineDisplay);\n    }\n    type = NONE;\n    return;\n  }\n\n  startDisplay();\n  onUpdateBegin(false);  // create Display task\n  initDone = true;\n}\n\n// gets called every time WiFi is (re-)connected. Initialize own network\n// interfaces here\nvoid FourLineDisplayUsermod::connected() {\n  knownSsid = WiFi.SSID();       //apActive ? apSSID : WiFi.SSID(); //apActive ? WiFi.softAPSSID() :\n  knownIp   = Network.localIP(); //apActive ? IPAddress(4, 3, 2, 1) : Network.localIP();\n  networkOverlay(PSTR(\"NETWORK INFO\"),7000);\n}\n\n/**\n * Da loop.\n */\nvoid FourLineDisplayUsermod::loop() {\n#if !(defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS))\n  if (!enabled || strip.isUpdating()) return;\n  unsigned long now = millis();\n  if (now < nextUpdate) return;\n  nextUpdate = now + ((displayTurnedOff && clockMode && showSeconds) ? 1000 : refreshRate);\n  redraw(false);\n#endif\n}\n\n/**\n * Redraw the screen (but only if things have changed\n * or if forceRedraw).\n */\nvoid FourLineDisplayUsermod::redraw(bool forceRedraw) {\n  bool needRedraw = false;\n  unsigned long now = millis();\n\n  if (type == NONE || !enabled) return;\n  if (overlayUntil > 0) {\n    if (now >= overlayUntil) {\n      // Time to display the overlay has elapsed.\n      overlayUntil = 0;\n      forceRedraw = true;\n    } else {\n      // We are still displaying the overlay\n      // Don't redraw.\n      return;\n    }\n  }\n\n  while (drawing && millis()-now < 25) delay(1); // wait if someone else is drawing\n  if (drawing || lockRedraw) return;\n\n  if (apActive && WLED_WIFI_CONFIGURED && now<15000) {\n    knownSsid = apSSID;\n    networkOverlay(PSTR(\"NETWORK INFO\"),30000);\n    return;\n  }\n\n  // Check if values which are shown on display changed from the last time.\n  if (forceRedraw) {\n    needRedraw = true;\n    clear();\n  } else if ((bri == 0 && powerON) || (bri > 0 && !powerON)) {   //trigger power icon\n    powerON = !powerON;\n    drawStatusIcons();\n    return;\n  } else if (knownnightlight != nightlightActive) {   //trigger moon icon\n    knownnightlight = nightlightActive;\n    drawStatusIcons();\n    if (knownnightlight) {\n      String timer = PSTR(\"Timer On\");\n      center(timer,LINE_BUFFER_SIZE-1);\n      overlay(timer.c_str(), 2500, 6);\n    }\n    return;\n  } else if (wificonnected != interfacesInited) {   //trigger wifi icon\n    wificonnected = interfacesInited;\n    drawStatusIcons();\n    return;\n  } else if (knownMode != effectCurrent || knownPalette != effectPalette) {\n    if (displayTurnedOff) needRedraw = true;\n    else {\n      if (knownPalette != effectPalette) { showCurrentEffectOrPalette(effectPalette, JSON_palette_names, 2); knownPalette = effectPalette; }\n      if (knownMode    != effectCurrent) { showCurrentEffectOrPalette(effectCurrent, JSON_mode_names, 3); knownMode = effectCurrent; }\n      lastRedraw = now;\n      return;\n    }\n  } else if (knownBrightness != bri) {\n    if (displayTurnedOff && nightlightActive) { knownBrightness = bri; }\n    else if (!displayTurnedOff) { updateBrightness(); lastRedraw = now; return; }\n  } else if (knownEffectSpeed != effectSpeed) {\n    if (displayTurnedOff) needRedraw = true;\n    else { updateSpeed(); lastRedraw = now; return; }\n  } else if (knownEffectIntensity != effectIntensity) {\n    if (displayTurnedOff) needRedraw = true;\n    else { updateIntensity(); lastRedraw = now; return; }\n  }\n\n  if (!needRedraw) {\n    // Nothing to change.\n    // Turn off display after 1 minutes with no change.\n    if (sleepMode && !displayTurnedOff && (millis() - lastRedraw > screenTimeout)) {\n      // We will still check if there is a change in redraw()\n      // and turn it back on if it changed.\n      clear();\n      sleepOrClock(true);\n    } else if (displayTurnedOff && ntpEnabled) {\n      showTime();\n    }\n    return;\n  }\n\n  lastRedraw = now;\n\n  // Turn the display back on\n  wakeDisplay();\n\n  // Update last known values.\n  knownBrightness      = bri;\n  knownMode            = effectCurrent;\n  knownPalette         = effectPalette;\n  knownEffectSpeed     = effectSpeed;\n  knownEffectIntensity = effectIntensity;\n  knownnightlight      = nightlightActive;\n  wificonnected        = interfacesInited;\n\n  // Do the actual drawing\n  // First row: Icons\n  draw2x2GlyphIcons();\n  drawArrow();\n  drawStatusIcons();\n\n  // Second row\n  updateBrightness();\n  updateSpeed();\n  updateIntensity();\n\n  // Third row\n  showCurrentEffectOrPalette(knownPalette, JSON_palette_names, 2); //Palette info\n\n  // Fourth row\n  showCurrentEffectOrPalette(knownMode, JSON_mode_names, 3); //Effect Mode info\n}\n\nvoid FourLineDisplayUsermod::updateBrightness() {\n#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS)\n  unsigned long now = millis();\n  while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing\n  if (drawing || lockRedraw) return;\n#endif\n  knownBrightness = bri;\n  if (overlayUntil == 0) {\n    lockRedraw = true;\n    brightness100 = ((uint16_t)bri*100)/255;\n    char lineBuffer[4];\n    sprintf_P(lineBuffer, PSTR(\"%-3d\"), brightness100);\n    drawString(1, lineHeight, lineBuffer);\n    lockRedraw = false;\n  }\n}\n\nvoid FourLineDisplayUsermod::updateSpeed() {\n#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS)\n  unsigned long now = millis();\n  while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing\n  if (drawing || lockRedraw) return;\n#endif\n  knownEffectSpeed = effectSpeed;\n  if (overlayUntil == 0) {\n    lockRedraw = true;\n    fxspeed100 = ((uint16_t)effectSpeed*100)/255;\n    char lineBuffer[4];\n    sprintf_P(lineBuffer, PSTR(\"%-3d\"), fxspeed100);\n    drawString(5, lineHeight, lineBuffer);\n    lockRedraw = false;\n  }\n}\n\nvoid FourLineDisplayUsermod::updateIntensity() {\n#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS)\n  unsigned long now = millis();\n  while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing\n  if (drawing || lockRedraw) return;\n#endif\n  knownEffectIntensity = effectIntensity;\n  if (overlayUntil == 0) {\n    lockRedraw = true;\n    fxintensity100 = ((uint16_t)effectIntensity*100)/255;\n    char lineBuffer[4];\n    sprintf_P(lineBuffer, PSTR(\"%-3d\"), fxintensity100);\n    drawString(9, lineHeight, lineBuffer);\n    lockRedraw = false;\n  }\n}\n\nvoid FourLineDisplayUsermod::drawStatusIcons() {\n#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS)\n  unsigned long now = millis();\n  while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing\n  if (drawing || lockRedraw) return;\n#endif\n  uint8_t col = 15;\n  uint8_t row = 0;\n  lockRedraw = true;\n  drawGlyph(col, row,   (wificonnected ? 20 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // wifi icon\n  if (lineHeight==2) { col--; } else { row++; }\n  drawGlyph(col, row,          (bri > 0 ? 9 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // power icon\n  if (lineHeight==2) { col--; } else { col = row = 0; }\n  drawGlyph(col, row, (nightlightActive ? 6 : 0), u8x8_4LineDisplay_WLED_icons_1x1, true); // moon icon for nighlight mode\n  lockRedraw = false;\n}\n\n/**\n * marks the position of the arrow showing\n * the current setting being changed\n * pass line and colum info\n */\nvoid FourLineDisplayUsermod::setMarkLine(byte newMarkLineNum, byte newMarkColNum) {\n  markLineNum = newMarkLineNum;\n  markColNum = newMarkColNum;\n}\n\n//Draw the arrow for the current setting being changed\nvoid FourLineDisplayUsermod::drawArrow() {\n#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS)\n  unsigned long now = millis();\n  while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing\n  if (drawing || lockRedraw) return;\n#endif\n  lockRedraw = true;\n  if (markColNum != 255 && markLineNum !=255) drawGlyph(markColNum, markLineNum*lineHeight, 21, u8x8_4LineDisplay_WLED_icons_1x1);\n  lockRedraw = false;\n}\n\n//Display the current effect or palette (desiredEntry)\n// on the appropriate line (row).\nvoid FourLineDisplayUsermod::showCurrentEffectOrPalette(int inputEffPal, const char *qstring, uint8_t row) {\n#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS)\n  unsigned long now = millis();\n  while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing\n  if (drawing || lockRedraw) return;\n#endif\n  char lineBuffer[MAX_JSON_CHARS];\n  if (overlayUntil == 0) {\n    lockRedraw = true;\n    // Find the mode name in JSON\n    unsigned printedChars = extractModeName(inputEffPal, qstring, lineBuffer, MAX_JSON_CHARS-1);\n    if (lineBuffer[0]=='*' && lineBuffer[1]==' ') {\n      // remove \"* \" from dynamic palettes\n      for (unsigned i=2; i<=printedChars; i++) lineBuffer[i-2] = lineBuffer[i]; //include '\\0'\n      printedChars -= 2;\n    } else if ((lineBuffer[0]==' ' && lineBuffer[1]>127)) {\n      // remove note symbol from effect names\n      for (unsigned i=5; i<=printedChars; i++) lineBuffer[i-5] = lineBuffer[i]; //include '\\0'\n      printedChars -= 5;\n    }\n    if (lineHeight == 2) {                                 // use this code for 8 line display\n      char smallBuffer1[MAX_MODE_LINE_SPACE];\n      char smallBuffer2[MAX_MODE_LINE_SPACE];\n      unsigned smallChars1 = 0;\n      unsigned smallChars2 = 0;\n      if (printedChars < MAX_MODE_LINE_SPACE) {            // use big font if the text fits\n        while (printedChars < (MAX_MODE_LINE_SPACE-1)) lineBuffer[printedChars++]=' ';\n        lineBuffer[printedChars] = 0;\n        drawString(1, row*lineHeight, lineBuffer);\n      } else {                                             // for long names divide the text into 2 lines and print them small\n        bool spaceHit = false;\n        for (unsigned i = 0; i < printedChars; i++) {\n          switch (lineBuffer[i]) {\n            case ' ':\n              if (i > 4 && !spaceHit) {\n                spaceHit = true;\n                break;\n              }\n              if (spaceHit) smallBuffer2[smallChars2++] = lineBuffer[i];\n              else          smallBuffer1[smallChars1++] = lineBuffer[i];\n              break;\n            default:\n              if (spaceHit) smallBuffer2[smallChars2++] = lineBuffer[i];\n              else          smallBuffer1[smallChars1++] = lineBuffer[i];\n              break;\n          }\n        }\n        while (smallChars1 < (MAX_MODE_LINE_SPACE-1)) smallBuffer1[smallChars1++]=' ';\n        smallBuffer1[smallChars1] = 0;\n        drawString(1, row*lineHeight, smallBuffer1, true);\n        while (smallChars2 < (MAX_MODE_LINE_SPACE-1)) smallBuffer2[smallChars2++]=' ';\n        smallBuffer2[smallChars2] = 0;\n        drawString(1, row*lineHeight+1, smallBuffer2, true);\n      }\n    } else {                                             // use this code for 4 ling displays\n      char smallBuffer3[MAX_MODE_LINE_SPACE+1];          // uses 1x1 icon for mode/palette\n      unsigned smallChars3 = 0;\n      for (unsigned i = 0; i < MAX_MODE_LINE_SPACE; i++) smallBuffer3[smallChars3++] = (i >= printedChars) ? ' ' : lineBuffer[i];\n      smallBuffer3[smallChars3] = 0;\n      drawString(1, row*lineHeight, smallBuffer3, true);\n    }\n    lockRedraw = false;\n  }\n}\n\n/**\n * If there screen is off or in clock is displayed,\n * this will return true. This allows us to throw away\n * the first input from the rotary encoder but\n * to wake up the screen.\n */\nbool FourLineDisplayUsermod::wakeDisplay() {\n  if (type == NONE || !enabled) return false;\n  if (displayTurnedOff) {\n  #if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS)\n    unsigned long now = millis();\n    while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing\n    if (drawing || lockRedraw) return false;\n  #endif\n    lockRedraw = true;\n    clear();\n    // Turn the display back on\n    sleepOrClock(false);\n    lockRedraw = false;\n    return true;\n  }\n  return false;\n}\n\n/**\n * Allows you to show one line and a glyph as overlay for a period of time.\n * Clears the screen and prints.\n * Used in Rotary Encoder usermod.\n */\nvoid FourLineDisplayUsermod::overlay(const char* line1, long showHowLong, byte glyphType) {\n#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS)\n  unsigned long now = millis();\n  while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing\n  if (drawing || lockRedraw) return;\n#endif\n  lockRedraw = true;\n  // Turn the display back on\n  if (!wakeDisplay()) clear();\n  // Print the overlay\n  if (glyphType>0 && glyphType<255) {\n    if (lineHeight == 2) drawGlyph(5, 0, glyphType, u8x8_4LineDisplay_WLED_icons_6x6, true); // use 3x3 font with draw2x2Glyph() if flash runs short and comment out 6x6 font\n    else                 drawGlyph(6, 0, glyphType, u8x8_4LineDisplay_WLED_icons_3x3, true);\n  }\n  if (line1) {\n    String buf = line1;\n    center(buf, getCols());\n    drawString(0, (glyphType<255?3:0)*lineHeight, buf.c_str());\n  }\n  overlayUntil = millis() + showHowLong;\n  lockRedraw = false;\n}\n\n/**\n * Allows you to show Akemi WLED logo overlay for a period of time.\n * Clears the screen and prints.\n */\nvoid FourLineDisplayUsermod::overlayLogo(long showHowLong) {\n#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS)\n  unsigned long now = millis();\n  while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing\n  if (drawing || lockRedraw) return;\n#endif\n  lockRedraw = true;\n  // Turn the display back on\n  if (!wakeDisplay()) clear();\n  // Print the overlay\n  if (lineHeight == 2) {\n    //add a bit of randomness\n    switch (millis()%3) {\n      case 0:\n        //WLED\n        draw2x2Glyph( 0, 2, 1, u8x8_wled_logo_2x2);\n        draw2x2Glyph( 4, 2, 2, u8x8_wled_logo_2x2);\n        draw2x2Glyph( 8, 2, 3, u8x8_wled_logo_2x2);\n        draw2x2Glyph(12, 2, 4, u8x8_wled_logo_2x2);\n        break;\n      case 1:\n        //WLED Akemi\n        drawGlyph( 2, 2, 1, u8x8_wled_logo_akemi_4x4, true);\n        drawGlyph( 6, 2, 2, u8x8_wled_logo_akemi_4x4, true);\n        drawGlyph(10, 2, 3, u8x8_wled_logo_akemi_4x4, true);\n        break;\n      case 2:\n        //Akemi\n        //draw2x2Glyph( 5, 0, 12, u8x8_4LineDisplay_WLED_icons_3x3); // use this if flash runs short and comment out 6x6 font\n        drawGlyph( 5, 0, 12, u8x8_4LineDisplay_WLED_icons_6x6, true);\n        drawString(6, 6, \"WLED\");\n        break;\n    }\n  } else {\n    switch (millis()%3) {\n      case 0:\n        //WLED\n        draw2x2Glyph( 0, 0, 1, u8x8_wled_logo_2x2);\n        draw2x2Glyph( 4, 0, 2, u8x8_wled_logo_2x2);\n        draw2x2Glyph( 8, 0, 3, u8x8_wled_logo_2x2);\n        draw2x2Glyph(12, 0, 4, u8x8_wled_logo_2x2);\n        break;\n      case 1:\n        //WLED Akemi\n        drawGlyph( 2, 0, 1, u8x8_wled_logo_akemi_4x4);\n        drawGlyph( 6, 0, 2, u8x8_wled_logo_akemi_4x4);\n        drawGlyph(10, 0, 3, u8x8_wled_logo_akemi_4x4);\n        break;\n      case 2:\n        //Akemi\n        //drawGlyph( 6, 0, 12, u8x8_4LineDisplay_WLED_icons_4x4); // a bit nicer, but uses extra 1.5k flash\n        draw2x2Glyph( 6, 0, 12, u8x8_4LineDisplay_WLED_icons_2x2);\n        break;\n    }\n  }\n  overlayUntil = millis() + showHowLong;\n  lockRedraw = false;\n}\n\n/**\n * Allows you to show two lines as overlay for a period of time.\n * Clears the screen and prints.\n * Used in Auto Save usermod\n */\nvoid FourLineDisplayUsermod::overlay(const char* line1, const char* line2, long showHowLong) {\n#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS)\n  unsigned long now = millis();\n  while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing\n  if (drawing || lockRedraw) return;\n#endif\n  lockRedraw = true;\n  // Turn the display back on\n  if (!wakeDisplay()) clear();\n  // Print the overlay\n  if (line1) {\n    String buf = line1;\n    center(buf, getCols());\n    drawString(0, 1*lineHeight, buf.c_str());\n  }\n  if (line2) {\n    String buf = line2;\n    center(buf, getCols());\n    drawString(0, 2*lineHeight, buf.c_str());\n  }\n  overlayUntil = millis() + showHowLong;\n  lockRedraw = false;\n}\n\nvoid FourLineDisplayUsermod::networkOverlay(const char* line1, long showHowLong) {\n#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS)\n  unsigned long now = millis();\n  while (drawing && millis()-now < 125) delay(1); // wait if someone else is drawing\n  if (drawing || lockRedraw) return;\n#endif\n  lockRedraw = true;\n\n  String line;\n  // Turn the display back on\n  if (!wakeDisplay()) clear();\n  // Print the overlay\n  if (line1) {\n    line = line1;\n    center(line, getCols());\n    drawString(0, 0, line.c_str());\n  }\n  // Second row with Wifi name\n  line = knownSsid.substring(0, getCols() > 1 ? getCols() - 2 : 0);\n  if (line.length() < getCols()) center(line, getCols());\n  drawString(0, lineHeight, line.c_str());\n  // Print `~` char to indicate that SSID is longer, than our display\n  if (knownSsid.length() > getCols()) {\n    drawString(getCols() - 1, 0, \"~\");\n  }\n  // Third row with IP and Password in AP Mode\n  line = knownIp.toString();\n  center(line, getCols());\n  drawString(0, lineHeight*2, line.c_str());\n  line = \"\";\n  if (apActive) {\n    line = apPass;\n  } else if (strcmp(serverDescription, \"WLED\") != 0) {\n    line = serverDescription;\n  }\n  center(line, getCols());\n  drawString(0, lineHeight*3, line.c_str());\n  overlayUntil = millis() + showHowLong;\n  lockRedraw = false;\n}\n\n\n/**\n * handleButton() can be used to override default button behaviour. Returning true\n * will prevent button working in a default way.\n * Replicating button.cpp\n */\nbool FourLineDisplayUsermod::handleButton(uint8_t b) {\n  yield();\n  if (!enabled\n    || b // button 0 only\n    || buttons[b].type == BTN_TYPE_SWITCH\n    || buttons[b].type == BTN_TYPE_NONE\n    || buttons[b].type == BTN_TYPE_RESERVED\n    || buttons[b].type == BTN_TYPE_PIR_SENSOR\n    || buttons[b].type == BTN_TYPE_ANALOG\n    || buttons[b].type == BTN_TYPE_ANALOG_INVERTED) {\n    return false;\n  }\n\n  unsigned long now = millis();\n  static bool buttonPressedBefore = false;\n  static bool buttonLongPressed = false;\n  static unsigned long buttonPressedTime = 0;\n  static unsigned long buttonWaitTime = 0;\n  bool handled = false;\n\n  //momentary button logic\n  if (isButtonPressed(b)) { //pressed\n\n    if (!buttonPressedBefore) buttonPressedTime = now;\n    buttonPressedBefore = true;\n\n    if (now - buttonPressedTime > 600) { //long press\n      //TODO: handleButton() handles button 0 without preset in a different way for double click\n      //so we need to override with same behaviour\n      //DEBUG_PRINTLN(F(\"4LD action.\"));\n      //if (!buttonLongPressed) longPressAction(0);\n      buttonLongPressed = true;\n      return false;\n    }\n\n  } else if (!isButtonPressed(b) && buttonPressedBefore) { //released\n\n    long dur = now - buttonPressedTime;\n    if (dur < 50) {\n      buttonPressedBefore = false;\n      return true;\n    } //too short \"press\", debounce\n\n    bool doublePress = buttonWaitTime; //did we have short press before?\n    buttonWaitTime = 0;\n\n    if (!buttonLongPressed) { //short press\n      // if this is second release within 350ms it is a double press (buttonWaitTime!=0)\n      //TODO: handleButton() handles button 0 without preset in a different way for double click\n      if (doublePress) {\n        networkOverlay(PSTR(\"NETWORK INFO\"),7000);\n        handled = true;\n      } else  {\n        buttonWaitTime = now;\n      }\n    }\n    buttonPressedBefore = false;\n    buttonLongPressed = false;\n  }\n  // if 350ms elapsed since last press/release it is a short press\n  if (buttonWaitTime && now - buttonWaitTime > 350 && !buttonPressedBefore) {\n    buttonWaitTime = 0;\n    //TODO: handleButton() handles button 0 without preset in a different way for double click\n    //so we need to override with same behaviour\n    //shortPressAction(0);\n    //handled = false;\n  }\n  return handled;\n}\n\n#ifndef ARDUINO_RUNNING_CORE\n  #if CONFIG_FREERTOS_UNICORE\n    #define ARDUINO_RUNNING_CORE 0\n  #else\n    #define ARDUINO_RUNNING_CORE 1\n  #endif\n#endif\nvoid FourLineDisplayUsermod::onUpdateBegin(bool init) {\n#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS)\n  if (init && Display_Task) {\n    vTaskSuspend(Display_Task);   // update is about to begin, disable task to prevent crash\n  } else {\n    // update has failed or create task requested\n    if (Display_Task)\n      vTaskResume(Display_Task);\n    else\n      xTaskCreatePinnedToCore(\n        [](void * par) {                  // Function to implement the task\n          // see https://www.freertos.org/vtaskdelayuntil.html\n          const TickType_t xFrequency = REFRESH_RATE_MS * portTICK_PERIOD_MS / 2;\n          TickType_t xLastWakeTime = xTaskGetTickCount();\n          for(;;) {\n            delay(1); // DO NOT DELETE THIS LINE! It is needed to give the IDLE(0) task enough time and to keep the watchdog happy.\n                      // taskYIELD(), yield(), vTaskDelay() and esp_task_wdt_feed() didn't seem to work.\n            vTaskDelayUntil(&xLastWakeTime, xFrequency); // release CPU, by doing nothing for REFRESH_RATE_MS millis\n            FourLineDisplayUsermod::getInstance()->redraw(false);\n          }\n        },\n        \"4LD\",                // Name of the task\n        3072,                 // Stack size in words\n        NULL,                 // Task input parameter\n        1,                    // Priority of the task (not idle)\n        &Display_Task,        // Task handle\n        ARDUINO_RUNNING_CORE\n      );\n  }\n#endif\n}\n\n/*\n  * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.\n  * Creating an \"u\" object allows you to add custom key/value pairs to the Info section of the WLED web UI.\n  * Below it is shown how this could be used for e.g. a light sensor\n  */\n//void FourLineDisplayUsermod::addToJsonInfo(JsonObject& root) {\n  //JsonObject user = root[\"u\"];\n  //if (user.isNull()) user = root.createNestedObject(\"u\");\n  //JsonArray data = user.createNestedArray(F(\"4LineDisplay\"));\n  //data.add(F(\"Loaded.\"));\n//}\n\n/*\n  * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).\n  * Values in the state object may be modified by connected clients\n  */\n//void FourLineDisplayUsermod::addToJsonState(JsonObject& root) {\n//}\n\n/*\n  * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).\n  * Values in the state object may be modified by connected clients\n  */\n//void FourLineDisplayUsermod::readFromJsonState(JsonObject& root) {\n//  if (!initDone) return;  // prevent crash on boot applyPreset()\n//}\n\nvoid FourLineDisplayUsermod::appendConfigData() {\n  oappend(F(\"dd=addDropdown('4LineDisplay','type');\"));\n  oappend(F(\"addOption(dd,'None',0);\"));\n  oappend(F(\"addOption(dd,'SSD1306',1);\"));\n  oappend(F(\"addOption(dd,'SH1106',2);\"));\n  oappend(F(\"addOption(dd,'SSD1306 128x64',3);\"));\n  oappend(F(\"addOption(dd,'SSD1305',4);\"));\n  oappend(F(\"addOption(dd,'SSD1305 128x64',5);\"));\n  oappend(F(\"addOption(dd,'SSD1309 128x64',9);\"));\n  oappend(F(\"addOption(dd,'SSD1306 SPI',6);\"));\n  oappend(F(\"addOption(dd,'SSD1306 SPI 128x64',7);\"));\n  oappend(F(\"addOption(dd,'SSD1309 SPI 128x64',8);\"));\n  oappend(F(\"addInfo('4LineDisplay:type',1,'<br><i class=\\\"warn\\\">Change may require reboot</i>','');\"));\n  oappend(F(\"addInfo('4LineDisplay:pin[]',0,'','SPI CS');\"));\n  oappend(F(\"addInfo('4LineDisplay:pin[]',1,'','SPI DC');\"));\n  oappend(F(\"addInfo('4LineDisplay:pin[]',2,'','SPI RST');\"));\n}\n\n/*\n  * addToConfig() can be used to add custom persistent settings to the cfg.json file in the \"um\" (usermod) object.\n  * It will be called by WLED when settings are actually saved (for example, LED settings are saved)\n  * If you want to force saving the current state, use serializeConfig() in your loop().\n  *\n  * CAUTION: serializeConfig() will initiate a filesystem write operation.\n  * It might cause the LEDs to stutter and will cause flash wear if called too often.\n  * Use it sparingly and always in the loop, never in network callbacks!\n  *\n  * addToConfig() will also not yet add your setting to one of the settings pages automatically.\n  * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually.\n  *\n  * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings!\n  */\nvoid FourLineDisplayUsermod::addToConfig(JsonObject& root) {\n  JsonObject top   = root.createNestedObject(FPSTR(_name));\n  top[FPSTR(_enabled)]       = enabled;\n\n  top[\"type\"]                = type;\n  JsonArray io_pin = top.createNestedArray(\"pin\");\n  for (int i=0; i<3; i++) io_pin.add(ioPin[i]);\n  top[FPSTR(_flip)]          = (bool) flip;\n  top[FPSTR(_contrast)]      = contrast;\n  top[FPSTR(_contrastFix)]   = (bool) contrastFix;\n  #ifndef ARDUINO_ARCH_ESP32\n  top[FPSTR(_refreshRate)]   = refreshRate;\n  #endif\n  top[FPSTR(_screenTimeOut)] = screenTimeout/1000;\n  top[FPSTR(_sleepMode)]     = (bool) sleepMode;\n  top[FPSTR(_clockMode)]     = (bool) clockMode;\n  top[FPSTR(_showSeconds)]   = (bool) showSeconds;\n  top[FPSTR(_busClkFrequency)] = ioFrequency/1000;\n  DEBUG_PRINTLN(F(\"4 Line Display config saved.\"));\n}\n\n/*\n  * readFromConfig() can be used to read back the custom settings you added with addToConfig().\n  * This is called by WLED when settings are loaded (currently this only happens once immediately after boot)\n  *\n  * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes),\n  * 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.\n  * If you don't know what that is, don't fret. It most likely doesn't affect your use case :)\n  */\nbool FourLineDisplayUsermod::readFromConfig(JsonObject& root) {\n  bool needsRedraw    = false;\n  DisplayType newType = type;\n  int8_t oldPin[3]; for (unsigned i=0; i<3; i++) oldPin[i] = ioPin[i];\n\n  JsonObject top = root[FPSTR(_name)];\n  if (top.isNull()) {\n    DEBUG_PRINT(FPSTR(_name));\n    DEBUG_PRINTLN(F(\": No config found. (Using defaults.)\"));\n    return false;\n  }\n\n  enabled       = top[FPSTR(_enabled)] | enabled;\n  newType       = top[\"type\"] | newType;\n  for (unsigned i=0; i<3; i++) ioPin[i] = top[\"pin\"][i] | ioPin[i];\n  flip          = top[FPSTR(_flip)] | flip;\n  contrast      = top[FPSTR(_contrast)] | contrast;\n  #ifndef ARDUINO_ARCH_ESP32\n  refreshRate   = top[FPSTR(_refreshRate)] | refreshRate;\n  refreshRate   = min(5000, max(250, (int)refreshRate));\n  #endif\n  screenTimeout = (top[FPSTR(_screenTimeOut)] | screenTimeout/1000) * 1000;\n  sleepMode     = top[FPSTR(_sleepMode)] | sleepMode;\n  clockMode     = top[FPSTR(_clockMode)] | clockMode;\n  showSeconds   = top[FPSTR(_showSeconds)] | showSeconds;\n  contrastFix   = top[FPSTR(_contrastFix)] | contrastFix;\n  if (newType == SSD1306_SPI || newType == SSD1306_SPI64)\n    ioFrequency = min(20000, max(500, (int)(top[FPSTR(_busClkFrequency)] | ioFrequency/1000))) * 1000;  // limit frequency\n  else\n    ioFrequency = min(3400, max(100, (int)(top[FPSTR(_busClkFrequency)] | ioFrequency/1000))) * 1000;  // limit frequency\n\n  DEBUG_PRINT(FPSTR(_name));\n  if (!initDone) {\n    // first run: reading from cfg.json\n    type = newType;\n    DEBUG_PRINTLN(F(\" config loaded.\"));\n  } else {\n    DEBUG_PRINTLN(F(\" config (re)loaded.\"));\n    // changing parameters from settings page\n    bool pinsChanged = false;\n    for (unsigned i=0; i<3; i++) if (ioPin[i] != oldPin[i]) { pinsChanged = true; break; }\n    if (pinsChanged || type!=newType) {\n      bool isSPI = (type == SSD1306_SPI || type == SSD1306_SPI64 || type == SSD1309_SPI64);\n      bool newSPI = (newType == SSD1306_SPI || newType == SSD1306_SPI64 || newType == SSD1309_SPI64);\n      if (isSPI) {\n        if (pinsChanged || !newSPI) PinManager::deallocateMultiplePins((const uint8_t*)oldPin, 3, PinOwner::UM_FourLineDisplay);\n        if (!newSPI) {\n          // was SPI but is no longer SPI\n          if (i2c_scl<0 || i2c_sda<0) { newType=NONE; }\n        } else {\n          // still SPI but pins changed\n          PinManagerPinType cspins[3] = { { ioPin[0], true }, { ioPin[1], true }, { ioPin[2], true } };\n          if (ioPin[0]<0 || ioPin[1]<0 || ioPin[1]<0) { newType=NONE; }\n          else if (!PinManager::allocateMultiplePins(cspins, 3, PinOwner::UM_FourLineDisplay)) { newType=NONE; }\n        }\n      } else if (newSPI) {\n        // was I2C but is now SPI\n        if (spi_sclk<0 || spi_mosi<0) {\n          newType=NONE;\n        } else {\n          PinManagerPinType pins[3] = { { ioPin[0], true }, { ioPin[1], true }, { ioPin[2], true } };\n          if (ioPin[0]<0 || ioPin[1]<0 || ioPin[1]<0) { newType=NONE; }\n          else if (!PinManager::allocateMultiplePins(pins, 3, PinOwner::UM_FourLineDisplay)) { newType=NONE; }\n        }\n      } else {\n        // just I2C type changed\n      }\n      type = newType;\n      switch (type) {\n        case SSD1306:\n          u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1306_128x32_univision, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino);\n          u8x8_SetPin_HW_I2C(u8x8->getU8x8(), U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE);\n          break;\n        case SH1106:\n          u8x8_Setup(u8x8->getU8x8(), u8x8_d_sh1106_128x64_winstar, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino);\n          u8x8_SetPin_HW_I2C(u8x8->getU8x8(), U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE);\n          break;\n        case SSD1306_64:\n          u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1306_128x64_noname, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino);\n          u8x8_SetPin_HW_I2C(u8x8->getU8x8(), U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE);\n          break;\n        case SSD1305:\n          u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1305_128x32_adafruit, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino);\n          u8x8_SetPin_HW_I2C(u8x8->getU8x8(), U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE);\n          break;\n        case SSD1305_64:\n          u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1305_128x64_adafruit, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino);\n          u8x8_SetPin_HW_I2C(u8x8->getU8x8(), U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE);\n          break;\n        case SSD1309_64:\n          u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1309_128x64_noname0, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino);\n          u8x8_SetPin_HW_I2C(u8x8->getU8x8(), U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE);\n          break;\n        case SSD1306_SPI:\n          u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1306_128x32_univision, u8x8_cad_001, u8x8_byte_arduino_hw_spi, u8x8_gpio_and_delay_arduino);\n          u8x8_SetPin_4Wire_HW_SPI(u8x8->getU8x8(), ioPin[0], ioPin[1], ioPin[2]); // Pins are cs, dc, reset\n          break;\n        case SSD1306_SPI64:\n          u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1306_128x64_noname, u8x8_cad_001, u8x8_byte_arduino_hw_spi, u8x8_gpio_and_delay_arduino);\n          u8x8_SetPin_4Wire_HW_SPI(u8x8->getU8x8(), ioPin[0], ioPin[1], ioPin[2]); // Pins are cs, dc, reset\n          break;\n        case SSD1309_SPI64:\n          u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1309_128x64_noname0, u8x8_cad_001, u8x8_byte_arduino_hw_spi, u8x8_gpio_and_delay_arduino);\n          u8x8_SetPin_4Wire_HW_SPI(u8x8->getU8x8(), ioPin[0], ioPin[1], ioPin[2]); // Pins are cs, dc, reset\n        default:\n          u8x8_Setup(u8x8->getU8x8(), u8x8_d_null_cb, u8x8_cad_empty, u8x8_byte_empty, u8x8_dummy_cb);\n          enabled = false;\n          break;\n      }\n      startDisplay();\n      needsRedraw |= true;\n    } else {\n      u8x8->setBusClock(ioFrequency); // can be used for SPI too\n      setVcomh(contrastFix);\n      setContrast(contrast);\n      setFlipMode(flip);\n    }\n    knownHour = 99;\n    if (needsRedraw && !wakeDisplay()) redraw(true);\n    else overlayLogo(3500);\n  }\n  // use \"return !top[\"newestParameter\"].isNull();\" when updating Usermod with new features\n  return !top[FPSTR(_contrastFix)].isNull();\n}\n\n\nstatic FourLineDisplayUsermod usermod_v2_four_line_display_alt;\nREGISTER_USERMOD(usermod_v2_four_line_display_alt);\n"
  },
  {
    "path": "usermods/usermod_v2_klipper_percentage/library.json",
    "content": "{\n  \"name\": \"usermod_v2_klipper_percentage\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/usermod_v2_klipper_percentage/readme.md",
    "content": "# Klipper Percentage Usermod\nThis usermod polls the Klipper API every 10s for the progressvalue.\nThe leds are then filled with a solid color according to that progress percentage. \nthe solid color is the secondary color of the segment.\n\nA corresponding curl command would be:\n```\ncurl --location --request GET 'http://[]/printer/objects/query?virtual_sdcard=progress'\n```\n## Usage\nCompile the source with the buildflag  `-D USERMOD_KLIPPER_PERCENTAGE` added.\n\nYou can also use the WLBD bot in the Discord by simply extending an existing build environment:\n```\n[env:esp32klipper]\nextends = env:esp32dev\nbuild_flags = ${common.build_flags_esp32} -D USERMOD_KLIPPER_PERCENTAGE\n```\n\n## Settings \n\n### Enabled:\nCheckbox to enable or disable the overlay\n\n### Klipper IP: \nIP address of your Klipper instance you want to poll. ESP has to be restarted after change\n\n### Direction : \n0 = normal\n\n1 = reversed\n\n2 = center\n\n-----\nAuthor:\n\nSören Willrodt\n\nDiscord: Sören#5281"
  },
  {
    "path": "usermods/usermod_v2_klipper_percentage/usermod_v2_klipper_percentage.cpp",
    "content": "#include \"wled.h\"\n\nclass klipper_percentage : public Usermod\n{\nprivate:\n  unsigned long lastTime = 0;\n  String ip = F(\"0.0.0.0\");\n  WiFiClient wifiClient;\n  char errorMessage[100] = \"\";\n  int printPercent = 0;\n  int direction = 0; // 0 for along the strip, 1 for reversed direction\n\n  static const char _name[];\n  static const char _enabled[];\n  bool enabled = false;\n\n  void httpGet(WiFiClient &client, char *errorMessage)\n  {\n    // https://arduinojson.org/v6/example/http-client/\n    // is this the most compact way to do http get and put it in arduinojson object???\n    // would like async response ... ???\n    client.setTimeout(10000);\n    if (!client.connect(ip.c_str(), 80))\n    {\n      strcat(errorMessage, PSTR(\"Connection failed\"));\n    }\n    else\n    {\n      // Send HTTP request\n      client.println(F(\"GET /printer/objects/query?virtual_sdcard=progress HTTP/1.0\"));\n      client.print(F(\"Host: \")); client.println(ip);\n      client.println(F(\"Connection: close\"));\n      if (client.println() == 0)\n      {\n        strcat(errorMessage, PSTR(\"Failed to send request\"));\n      }\n      else\n      {\n        // Check HTTP status\n        char status[32] = {0};\n        client.readBytesUntil('\\r', status, sizeof(status));\n        if (strcmp_P(status, PSTR(\"HTTP/1.1 200 OK\")) != 0)\n        {\n          strcat(errorMessage, PSTR(\"Unexpected response: \"));\n          strcat(errorMessage, status);\n        }\n        else\n        {\n          // Skip HTTP headers\n          char endOfHeaders[] = \"\\r\\n\\r\\n\";\n          if (!client.find(endOfHeaders))\n          {\n            strcat(errorMessage, PSTR(\"Invalid response\"));\n          }\n        }\n      }\n    }\n  }\n\npublic:\n  void setup()\n  {\n  }\n\n  void connected()\n  {\n  }\n\n  void loop()\n  {\n    if (enabled)\n    {\n      if (WLED_CONNECTED)\n      {\n        if (millis() - lastTime > 10000)\n        {\n          httpGet(wifiClient, errorMessage);\n          if (strcmp(errorMessage, \"\") == 0)\n          {\n            PSRAMDynamicJsonDocument klipperDoc(4096); // in practice about 2673\n            DeserializationError error = deserializeJson(klipperDoc, wifiClient);\n            if (error)\n            {\n              strcat(errorMessage, PSTR(\"deserializeJson() failed: \"));\n              strcat(errorMessage, error.c_str());\n            }\n            printPercent = (int)(klipperDoc[F(\"result\")][F(\"status\")][F(\"virtual_sdcard\")][F(\"progress\")].as<float>() * 100);\n\n            DEBUG_PRINT(F(\"Percent: \"));\n            DEBUG_PRINTLN((int)(klipperDoc[F(\"result\")][F(\"status\")][F(\"virtual_sdcard\")][F(\"progress\")].as<float>() * 100));\n            DEBUG_PRINT(F(\"LEDs: \"));\n            DEBUG_PRINTLN(direction == 2 ? (strip.getLengthTotal() / 2) * printPercent / 100 : strip.getLengthTotal() * printPercent / 100);\n          }\n          else\n          {\n            DEBUG_PRINTLN(errorMessage);\n            DEBUG_PRINTLN(ip);\n          }\n          lastTime = millis();\n        }\n      }\n    }\n  }\n\n  void addToConfig(JsonObject &root)\n  {\n    JsonObject top = root.createNestedObject(F(\"Klipper Printing Percentage\"));\n    top[F(\"Enabled\")] = enabled;\n    top[F(\"Klipper IP\")] = ip;\n    top[F(\"Direction\")] = direction;\n  }\n\n  bool readFromConfig(JsonObject &root)\n  {\n    // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor\n    // 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)\n\n    JsonObject top = root[F(\"Klipper Printing Percentage\")];\n\n    bool configComplete = !top.isNull();\n    configComplete &= getJsonValue(top[F(\"Klipper IP\")], ip);\n    configComplete &= getJsonValue(top[F(\"Enabled\")], enabled);\n    configComplete &= getJsonValue(top[F(\"Direction\")], direction);\n    return configComplete;\n  }\n\n  /*\n   * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.\n   * Creating an \"u\" object allows you to add custom key/value pairs to the Info section of the WLED web UI.\n   * Below it is shown how this could be used for e.g. a light sensor\n   */\n  void addToJsonInfo(JsonObject &root)\n  {\n    JsonObject user = root[\"u\"];\n    if (user.isNull())\n      user = root.createNestedObject(\"u\");\n\n    JsonArray infoArr = user.createNestedArray(FPSTR(_name));\n    String uiDomString = F(\"<button class=\\\"btn btn-xs\\\" onclick=\\\"requestJson({\");\n    uiDomString += FPSTR(_name);\n    uiDomString += F(\":{\");\n    uiDomString += FPSTR(_enabled);\n    uiDomString += enabled ? F(\":false}});\\\">\") : F(\":true}});\\\">\");\n    uiDomString += F(\"<i class=\\\"icons\");\n    uiDomString += enabled ? F(\" on\") : F(\" off\");\n    uiDomString += F(\"\\\">&#xe08f;</i>\");\n    uiDomString += F(\"</button>\");\n    infoArr.add(uiDomString);\n  }\n\n  void addToJsonState(JsonObject &root)\n  {\n    JsonObject usermod = root[FPSTR(_name)];\n    if (usermod.isNull())\n    {\n      usermod = root.createNestedObject(FPSTR(_name));\n    }\n    usermod[\"on\"] = enabled;\n  }\n  void readFromJsonState(JsonObject &root)\n  {\n    JsonObject usermod = root[FPSTR(_name)];\n    if (!usermod.isNull())\n    {\n      if (usermod[FPSTR(_enabled)].is<bool>())\n      {\n        enabled = usermod[FPSTR(_enabled)].as<bool>();\n      }\n    }\n  }\n\n  /*\n   * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors.\n   * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode.\n   * Commonly used for custom clocks (Cronixie, 7 segment)\n   */\n  void handleOverlayDraw()\n  {\n    if (enabled)\n    {\n      if (direction == 0) // normal\n      {\n        for (int i = 0; i < strip.getLengthTotal() * printPercent / 100; i++)\n        {\n          strip.setPixelColor(i, strip.getSegment(0).colors[1]);\n        }\n      }\n      else if (direction == 1) // reversed\n      {\n        for (int i = 0; i < strip.getLengthTotal() * printPercent / 100; i++)\n        {\n          strip.setPixelColor(strip.getLengthTotal() - i, strip.getSegment(0).colors[1]);\n        }\n      }\n      else if (direction == 2) // center\n      {\n        for (int i = 0; i < (strip.getLengthTotal() / 2) * printPercent / 100; i++)\n        {\n          strip.setPixelColor((strip.getLengthTotal() / 2) + i, strip.getSegment(0).colors[1]);\n          strip.setPixelColor((strip.getLengthTotal() / 2) - i, strip.getSegment(0).colors[1]);\n        }\n      }\n      else\n      {\n        direction = 0;\n      }\n    }\n  }\n\n  /*\n   * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).\n   * This could be used in the future for the system to determine whether your usermod is installed.\n   */\n  uint16_t getId()\n  {\n    return USERMOD_ID_KLIPPER;\n  }\n};\nconst char klipper_percentage::_name[] PROGMEM = \"Klipper_Percentage\";\nconst char klipper_percentage::_enabled[] PROGMEM = \"enabled\";\n\nstatic klipper_percentage usermod_v2_klipper_percentage;\nREGISTER_USERMOD(usermod_v2_klipper_percentage);"
  },
  {
    "path": "usermods/usermod_v2_ping_pong_clock/library.json",
    "content": "{\n  \"name\": \"usermod_v2_ping_pong_clock\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/usermod_v2_ping_pong_clock/readme.md",
    "content": "# Ping Pong LED Clock\n\nContains a modification to use WLED in combination with the Ping Pong Ball LED Clock as built in [Instructables](https://www.instructables.com/Ping-Pong-Ball-LED-Clock/).\n\n## Installation \n\nTo install this Usermod, you instruct PlatformIO to compile the Project with the USERMOD_PING_PONG_CLOCK flag.\nWLED then automatically provides you with various settings on the Usermod Page.\n\nNote: Depending on the size of your clock, you may have to update the led indices for the individual numbers and the base indices.\n"
  },
  {
    "path": "usermods/usermod_v2_ping_pong_clock/usermod_v2_ping_pong_clock.cpp",
    "content": "#include \"wled.h\"\n\nclass PingPongClockUsermod : public Usermod\n{\nprivate:\n  // Private class members. You can declare variables and functions only accessible to your usermod here\n  unsigned long lastTime = 0;\n  bool colonOn = true;\n\n  // ---- Variables modified by settings below -----\n  // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer)\n  bool pingPongClockEnabled = true;\n  int colorR = 0xFF;\n  int colorG = 0xFF;\n  int colorB = 0xFF;\n\n  // ---- Variables for correct LED numbering below, edit only if your clock is built different ----\n\n  int baseH = 43;  // Address for the one place of the hours\n  int baseHH = 7;  // Address for the tens place of the hours\n  int baseM = 133; // Address for the one place of the minutes\n  int baseMM = 97; // Address for the tens place of the minutes\n  int colon1 = 79; // Address for the first colon led\n  int colon2 = 80; // Address for the second colon led\n\n  // Matrix for the illumination of the numbers\n  // Note: These only define the increments of the base address. e.g. to define the second Minute you have to add the baseMM to every led position\n  const int numbers[10][10] = \n    {\n      {  0,  1,  4,  6, 13, 15, 18, 19, -1, -1 }, // 0: null\n      { 13, 14, 15, 18, 19, -1, -1, -1, -1, -1 }, // 1: eins\n      {  0,  4,  5,  6, 13, 14, 15, 19, -1, -1 }, // 2: zwei\n      {  4,  5,  6, 13, 14, 15, 18, 19, -1, -1 }, // 3: drei\n      {  1,  4,  5, 14, 15, 18, 19, -1, -1, -1 }, // 4: vier\n      {  1,  4,  5,  6, 13, 14, 15, 18, -1, -1 }, // 5: fünf\n      {  0,  5,  6, 10, 13, 14, 15, 18, -1, -1 }, // 6: sechs\n      {  4,  6,  9, 13, 14, 19, -1, -1, -1, -1 }, // 7: sieben\n      {  0,  1,  4,  5,  6, 13, 14, 15, 18, 19 }, // 8: acht\n      {  1,  4,  5,  6,  9, 13, 14, 19, -1, -1 }  // 9: neun\n    };\n\npublic:\n  void setup()\n  { }\n\n  void loop()\n  {\n    if (millis() - lastTime > 1000)\n    {\n      lastTime = millis();\n      colonOn = !colonOn;\n    }\n  }\n\n  void addToJsonInfo(JsonObject& root)\n  {\n    JsonObject user = root[\"u\"];\n    if (user.isNull()) user = root.createNestedObject(\"u\");\n\n    JsonArray lightArr = user.createNestedArray(\"Uhrzeit-Anzeige\"); //name\n    lightArr.add(pingPongClockEnabled ? \"aktiv\" : \"inaktiv\"); //value\n    lightArr.add(\"\"); //unit\n  }\n\n  void addToConfig(JsonObject &root)\n  {\n    JsonObject top = root.createNestedObject(\"Ping Pong Clock\");\n    top[\"enabled\"] = pingPongClockEnabled;\n    top[\"colorR\"]   = colorR;\n    top[\"colorG\"]   = colorG;\n    top[\"colorB\"]   = colorB;\n  }\n\n  bool readFromConfig(JsonObject &root)\n  {\n    JsonObject top = root[\"Ping Pong Clock\"];\n\n      bool configComplete = !top.isNull();\n\n      configComplete &= getJsonValue(top[\"enabled\"], pingPongClockEnabled);\n      configComplete &= getJsonValue(top[\"colorR\"], colorR);\n      configComplete &= getJsonValue(top[\"colorG\"], colorG);\n      configComplete &= getJsonValue(top[\"colorB\"], colorB);\n\n      return configComplete;\n  }\n\n  void drawNumber(int base, int number)\n  {\n    for(int i = 0; i < 10; i++)\n    {\n      if(numbers[number][i] > -1)\n        strip.setPixelColor(numbers[number][i] + base, RGBW32(colorR, colorG, colorB, 0));\n    }\n  }\n\n  void handleOverlayDraw()\n  {\n    if(pingPongClockEnabled){\n      if(colonOn)\n      {\n        strip.setPixelColor(colon1, RGBW32(colorR, colorG, colorB, 0));\n        strip.setPixelColor(colon2, RGBW32(colorR, colorG, colorB, 0));\n      }\n      drawNumber(baseHH, (hour(localTime) / 10) % 10);\n      drawNumber(baseH, hour(localTime) % 10); \n      drawNumber(baseM, (minute(localTime) / 10) % 10);\n      drawNumber(baseMM, minute(localTime) % 10);\n    }\n  }\n\n  uint16_t getId()\n  {\n    return USERMOD_ID_PING_PONG_CLOCK;\n  }\n\n};\n\n\nstatic PingPongClockUsermod usermod_v2_ping_pong_clock;\nREGISTER_USERMOD(usermod_v2_ping_pong_clock);"
  },
  {
    "path": "usermods/usermod_v2_rotary_encoder_ui_ALT/library.json",
    "content": "{\n  \"name\": \"rotary_encoder_ui_ALT\",\n  \"build\": {\n    \"libArchive\": false,\n    \"extraScript\": \"setup_deps.py\"\n  }\n}"
  },
  {
    "path": "usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.sample.ini",
    "content": "[platformio]\ndefault_envs = esp32dev_re\n\n[env:esp32dev_re]\nextends = env:esp32dev_V4\ncustom_usermods = ${env:esp32dev_V4.custom_usermods} rotary_encoder_ui_ALT\nbuild_flags =\n    ${env:esp32dev_V4.build_flags}\n    -D USERMOD_ROTARY_ENCODER_GPIO=INPUT\n    -D ENCODER_DT_PIN=21\n    -D ENCODER_CLK_PIN=23\n    -D ENCODER_SW_PIN=0\n"
  },
  {
    "path": "usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md",
    "content": "# Rotary Encoder UI Usermod ALT\n\nThis usermod supports the UI of the `usermod_v2_rotary_encoder_ui_ALT`.\n\n## Functionalities\n\nPress the encoder to cycle through the options:\n\n* Brightness\n* Speed\n* Intensity\n* Palette\n* Effect\n* Main Color (only if display is used)\n* Saturation (only if display is used)\n\nPress and hold the encoder to display Network Info. If AP is active, it will display the AP, SSID and Password\n\nAlso shows if the timer is enabled.\n\n[See the pair of usermods in action](https://www.youtube.com/watch?v=ulZnBt9z3TI)\n\n## Installation\n\nCopy the example `platformio_override.sample.ini` to the root directory of your particular build.\n\n### Define Your Options\n\n* `ENCODER_DT_PIN`                  - defaults to 18\n* `ENCODER_CLK_PIN`                 - defaults to 5\n* `ENCODER_SW_PIN`                  - defaults to 19\n* `USERMOD_ROTARY_ENCODER_GPIO`     - GPIO functionality:\n                                        `INPUT_PULLUP` to use internal pull-up\n                                        `INPUT` to use pull-up on the PCB\n\n### PlatformIO requirements\n\nNo special requirements.\n\n## Change Log\n\n2021-10\n\n* First public release\n"
  },
  {
    "path": "usermods/usermod_v2_rotary_encoder_ui_ALT/setup_deps.py",
    "content": "from platformio.package.meta import PackageSpec\nImport('env')\n\nlibs = [PackageSpec(lib).name for lib in env.GetProjectOption(\"lib_deps\",[])]\n# Check for partner usermod\n# Allow both \"usermod_v2\" and unqualified syntax\nif any(mod in (\"four_line_display_ALT\", \"usermod_v2_four_line_display_ALT\") for mod in libs):\n    env.Append(CPPDEFINES=[(\"USERMOD_FOUR_LINE_DISPLAY\")])\n"
  },
  {
    "path": "usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp",
    "content": "#include \"wled.h\"\n\n//\n// Inspired by the original v2 usermods\n// * usermod_v2_rotary_encoder_ui\n//\n// v2 usermod that provides a rotary encoder-based UI.\n//\n// This usermod allows you to control:\n// \n// * Brightness\n// * Selected Effect\n// * Effect Speed\n// * Effect Intensity\n// * Palette\n//\n// Change between modes by pressing a button.\n//\n// Dependencies\n// * This Usermod works best coupled with \n//   FourLineDisplayUsermod.\n//\n// If FourLineDisplayUsermod is used the folowing options are also enabled\n//\n// * main color\n// * saturation of main color\n// * display network (long press buttion)\n//\n\n#ifdef USERMOD_FOUR_LINE_DISPLAY\n#include \"usermod_v2_four_line_display.h\"\n#endif\n\n#ifdef USERMOD_MODE_SORT\n  #error \"Usermod Mode Sort is no longer required. Remove -D USERMOD_MODE_SORT from platformio.ini\"\n#endif\n\n#ifndef ENCODER_DT_PIN\n#define ENCODER_DT_PIN 18\n#endif\n\n#ifndef ENCODER_CLK_PIN\n#define ENCODER_CLK_PIN 5\n#endif\n\n#ifndef ENCODER_SW_PIN\n#define ENCODER_SW_PIN 19\n#endif\n\n#ifndef ENCODER_MAX_DELAY_MS    // max delay between polling encoder pins\n#define ENCODER_MAX_DELAY_MS 8  // 8 milliseconds => max 120 change impulses in 1 second, for full turn of a 30/30 encoder (4 changes per segment, 30 segments for one turn)\n#endif\n\n#ifndef USERMOD_USE_PCF8574\n  #undef USE_PCF8574\n  #define USE_PCF8574 false\n#else\n  #undef USE_PCF8574\n  #define USE_PCF8574 true\n#endif\n\n#ifndef PCF8574_ADDRESS\n  #define PCF8574_ADDRESS 0x20  // some may start at 0x38\n#endif\n\n#ifndef PCF8574_INT_PIN\n  #define PCF8574_INT_PIN -1    // GPIO connected to INT pin on PCF8574\n#endif\n\n// The last UI state, remove color and saturation option if display not active (too many options)\n#ifdef USERMOD_FOUR_LINE_DISPLAY\n #define LAST_UI_STATE 11\n#else\n #define LAST_UI_STATE 4\n#endif\n\n// Number of modes at the start of the list to not sort\n#define MODE_SORT_SKIP_COUNT 1\n\n// Which list is being sorted\nstatic const char **listBeingSorted;\n\n/**\n * Modes and palettes are stored as strings that\n * end in a quote character. Compare two of them.\n * We are comparing directly within either\n * JSON_mode_names or JSON_palette_names.\n */\nstatic int re_qstringCmp(const void *ap, const void *bp) {\n  const char *a = listBeingSorted[*((byte *)ap)];\n  const char *b = listBeingSorted[*((byte *)bp)];\n  int i = 0;\n  do {\n    char aVal = pgm_read_byte_near(a + i);\n    if (aVal >= 97 && aVal <= 122) {\n      // Lowercase\n      aVal -= 32;\n    }\n    char bVal = pgm_read_byte_near(b + i);\n    if (bVal >= 97 && bVal <= 122) {\n      // Lowercase\n      bVal -= 32;\n    }\n    // Really we shouldn't ever get to '\\0'\n    if (aVal == '\"' || bVal == '\"' || aVal == '\\0' || bVal == '\\0') {\n      // We're done. one is a substring of the other\n      // or something happenend and the quote didn't stop us.\n      if (aVal == bVal) {\n        // Same value, probably shouldn't happen\n        // with this dataset\n        return 0;\n      }\n      else if (aVal == '\"' || aVal == '\\0') {\n        return -1;\n      }\n      else {\n        return 1;\n      }\n    }\n    if (aVal == bVal) {\n      // Same characters. Move to the next.\n      i++;\n      continue;\n    }\n    // We're done\n    if (aVal < bVal) {\n      return -1;\n    }\n    else {\n      return 1;\n    }\n  } while (true);\n  // We shouldn't get here.\n  return 0;\n}\n\n\nstatic volatile uint8_t pcfPortData = 0;                // port expander port state\nstatic volatile uint8_t addrPcf8574 = PCF8574_ADDRESS;  // has to be accessible in ISR\n\n// Interrupt routine to read I2C rotary state\n// if we are to use PCF8574 port expander we will need to rely on interrupts as polling I2C every 2ms\n// is a waste of resources and causes 4LD to fail.\n// in such case rely on ISR to read pin values and store them into static variable\nstatic void IRAM_ATTR i2cReadingISR() {\n  Wire.requestFrom(addrPcf8574, 1U);\n  if (Wire.available()) {\n    pcfPortData = Wire.read();\n  }\n}\n\n\nclass RotaryEncoderUIUsermod : public Usermod {\n\n  private:\n\n    const int8_t fadeAmount;    // Amount to change every step (brightness)\n    unsigned long loopTime;\n\n    unsigned long buttonPressedTime;\n    unsigned long buttonWaitTime;\n    bool buttonPressedBefore;\n    bool buttonLongPressed;\n\n    int8_t pinA;                // DT from encoder\n    int8_t pinB;                // CLK from encoder\n    int8_t pinC;                // SW from encoder\n\n    unsigned char select_state; // 0: brightness, 1: effect, 2: effect speed, ...\n\n    uint16_t currentHue1;       // default boot color\n    byte currentSat1;\n    uint8_t currentCCT;\n    \n  #ifdef USERMOD_FOUR_LINE_DISPLAY\n    FourLineDisplayUsermod *display;\n  #else\n    void* display;\n  #endif\n\n    // Pointers the start of the mode names within JSON_mode_names\n    const char **modes_qstrings;\n\n    // Array of mode indexes in alphabetical order.\n    byte *modes_alpha_indexes;\n\n    // Pointers the start of the palette names within JSON_palette_names\n    const char **palettes_qstrings;\n\n    // Array of palette indexes in alphabetical order.\n    byte *palettes_alpha_indexes;\n\n    struct { // reduce memory footprint\n      bool Enc_A      : 1;\n      bool Enc_B      : 1;\n      bool Enc_A_prev : 1;\n    };\n\n    bool currentEffectAndPaletteInitialized;\n    uint8_t effectCurrentIndex;\n    uint8_t effectPaletteIndex;\n    uint8_t knownMode;\n    uint8_t knownPalette;\n\n    byte presetHigh;\n    byte presetLow;\n\n    bool applyToAll;\n\n    bool initDone;\n    bool enabled;\n\n    bool usePcf8574;\n    int8_t pinIRQ;\n\n    // strings to reduce flash memory usage (used more than twice)\n    static const char _name[];\n    static const char _enabled[];\n    static const char _DT_pin[];\n    static const char _CLK_pin[];\n    static const char _SW_pin[];\n    static const char _presetHigh[];\n    static const char _presetLow[];\n    static const char _applyToAll[];\n    static const char _pcf8574[];\n    static const char _pcfAddress[];\n    static const char _pcfINTpin[];\n\n    /**\n     * readPin() - read rotary encoder pin value\n     */\n    byte readPin(uint8_t pin);\n\n    /**\n     * Sort the modes and palettes to the index arrays\n     * modes_alpha_indexes and palettes_alpha_indexes.\n     */\n    void sortModesAndPalettes();\n    byte *re_initIndexArray(int numModes);\n\n    /**\n     * Return an array of mode or palette names from the JSON string.\n     * They don't end in '\\0', they end in '\"'. \n     */\n    const char **re_findModeStrings(const char json[], int numModes);\n\n    /**\n     * Sort either the modes or the palettes using quicksort.\n     */\n    void re_sortModes(const char **modeNames, byte *indexes, int count, int numSkip);\n\n  public:\n\n    RotaryEncoderUIUsermod()\n      : fadeAmount(5)\n      , buttonPressedTime(0)\n      , buttonWaitTime(0)\n      , buttonPressedBefore(false)\n      , buttonLongPressed(false)\n      , pinA(ENCODER_DT_PIN)\n      , pinB(ENCODER_CLK_PIN)\n      , pinC(ENCODER_SW_PIN)\n      , select_state(0)\n      , currentHue1(16)\n      , currentSat1(255)\n      , currentCCT(128)\n      , display(nullptr)\n      , modes_qstrings(nullptr)\n      , modes_alpha_indexes(nullptr)\n      , palettes_qstrings(nullptr)\n      , palettes_alpha_indexes(nullptr)\n      , currentEffectAndPaletteInitialized(false)\n      , effectCurrentIndex(0)\n      , effectPaletteIndex(0)\n      , knownMode(0)\n      , knownPalette(0)\n      , presetHigh(0)\n      , presetLow(0)\n      , applyToAll(true)\n      , initDone(false)\n      , enabled(true)\n      , usePcf8574(USE_PCF8574)\n      , pinIRQ(PCF8574_INT_PIN)\n    {}\n\n    /*\n     * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).\n     * This could be used in the future for the system to determine whether your usermod is installed.\n     */\n    uint16_t getId() override { return USERMOD_ID_ROTARY_ENC_UI; }\n    /**\n     * Enable/Disable the usermod\n     */\n    inline void enable(bool enable) { if (!(pinA<0 || pinB<0 || pinC<0)) enabled = enable; }\n\n    /**\n     * Get usermod enabled/disabled state\n     */\n    inline bool isEnabled() { return enabled; }\n\n    /**\n     * setup() is called once at boot. WiFi is not yet connected at this point.\n     * You can use it to initialize variables, sensors or similar.\n     */\n    void setup() override;\n\n    /**\n     * connected() is called every time the WiFi is (re)connected\n     * Use it to initialize network interfaces\n     */\n    //void connected();\n\n    /**\n     * loop() is called continuously. Here you can check for events, read sensors, etc.\n     */\n    void loop() override;\n\n#ifndef WLED_DISABLE_MQTT\n    //bool onMqttMessage(char* topic, char* payload) override;\n    //void onMqttConnect(bool sessionPresent) override;\n#endif\n\n    /**\n     * handleButton() can be used to override default button behaviour. Returning true\n     * will prevent button working in a default way.\n     * Replicating button.cpp\n     */\n    //bool handleButton(uint8_t b) override;\n\n    /**\n     * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.\n     */\n    //void addToJsonInfo(JsonObject &root) override;\n\n    /**\n     * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).\n     * Values in the state object may be modified by connected clients\n     */\n    //void addToJsonState(JsonObject &root) override;\n\n    /**\n     * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).\n     * Values in the state object may be modified by connected clients\n     */\n    //void readFromJsonState(JsonObject &root) override;\n\n    /**\n     * provide the changeable values\n     */\n    void addToConfig(JsonObject &root) override;\n\n    void appendConfigData() override;\n\n    /**\n     * restore the changeable values\n     * readFromConfig() is called before setup() to populate properties from values stored in cfg.json\n     * \n     * The function should return true if configuration was successfully loaded or false if there was no configuration.\n     */\n    bool readFromConfig(JsonObject &root) override;\n\n    // custom methods\n    void displayNetworkInfo();\n    void findCurrentEffectAndPalette();\n    bool changeState(const char *stateName, byte markedLine, byte markedCol, byte glyph);\n    void lampUdated();\n    void changeBrightness(bool increase);\n    void changeEffect(bool increase);\n    void changeEffectSpeed(bool increase);\n    void changeEffectIntensity(bool increase);\n    void changeCustom(uint8_t par, bool increase);\n    void changePalette(bool increase);\n    void changeHue(bool increase);\n    void changeSat(bool increase);\n    void changePreset(bool increase);\n    void changeCCT(bool increase);\n};\n\n\n/**\n * readPin() - read rotary encoder pin value\n */\nbyte RotaryEncoderUIUsermod::readPin(uint8_t pin) {\n  if (usePcf8574) {\n    if (pin >= 100) pin -= 100; // PCF I/O ports\n    return (pcfPortData>>pin) & 1;\n  } else {\n    return digitalRead(pin);\n  }\n}\n\n/**\n * Sort the modes and palettes to the index arrays\n * modes_alpha_indexes and palettes_alpha_indexes.\n */\nvoid RotaryEncoderUIUsermod::sortModesAndPalettes() {\n  DEBUG_PRINT(F(\"Sorting modes: \")); DEBUG_PRINTLN(strip.getModeCount());\n  //modes_qstrings = re_findModeStrings(JSON_mode_names, strip.getModeCount());\n  modes_qstrings = strip.getModeDataSrc();\n  modes_alpha_indexes = re_initIndexArray(strip.getModeCount());\n  re_sortModes(modes_qstrings, modes_alpha_indexes, strip.getModeCount(), MODE_SORT_SKIP_COUNT);\n\n  DEBUG_PRINT(F(\"Sorting palettes: \")); DEBUG_PRINT(getPaletteCount()); DEBUG_PRINT('/'); DEBUG_PRINTLN(customPalettes.size());\n  palettes_qstrings = re_findModeStrings(JSON_palette_names, getPaletteCount()); // allocates memory for all palette names\n  palettes_alpha_indexes = re_initIndexArray(getPaletteCount()); // allocates memory for all palette indexes\n  if (customPalettes.size()) {\n    for (int i=0; i<customPalettes.size(); i++) {\n      palettes_alpha_indexes[FIXED_PALETTE_COUNT+i] = 255-i;\n      palettes_qstrings[FIXED_PALETTE_COUNT+i] = PSTR(\"~Custom~\");\n    }\n  }\n  // How many palette names start with '*' and should not be sorted?\n  // (Also skipping the first one, 'Default').\n  int skipPaletteCount = 1; // could use DYNAMIC_PALETTE_COUNT instead\n  while (pgm_read_byte_near(palettes_qstrings[skipPaletteCount]) == '*') skipPaletteCount++; // legacy code\n  re_sortModes(palettes_qstrings, palettes_alpha_indexes, FIXED_PALETTE_COUNT, skipPaletteCount); // only sort fixed palettes (skip dynamic)\n}\n\nbyte *RotaryEncoderUIUsermod::re_initIndexArray(int numModes) {\n  byte *indexes = (byte *)malloc(sizeof(byte) * numModes);\n  for (unsigned i = 0; i < numModes; i++) {\n    indexes[i] = i;\n  }\n  return indexes;\n}\n\n/**\n * Return an array of mode or palette names from the JSON string.\n * They don't end in '\\0', they end in '\"'. \n */\nconst char **RotaryEncoderUIUsermod::re_findModeStrings(const char json[], int numModes) {\n  const char **modeStrings = (const char **)malloc(sizeof(const char *) * numModes);\n  uint8_t modeIndex = 0;\n  bool insideQuotes = false;\n  // advance past the mark for markLineNum that may exist.\n  char singleJsonSymbol;\n\n  // Find the mode name in JSON\n  bool complete = false;\n  for (size_t i = 0; i < strlen_P(json); i++) {\n    singleJsonSymbol = pgm_read_byte_near(json + i);\n    if (singleJsonSymbol == '\\0') break;\n    switch (singleJsonSymbol) {\n      case '\"':\n        insideQuotes = !insideQuotes;\n        if (insideQuotes) {\n          // We have a new mode or palette\n          modeStrings[modeIndex] = (char *)(json + i + 1);\n        }\n        break;\n      case '[':\n        break;\n      case ']':\n        if (!insideQuotes) complete = true;\n        break;\n      case ',':\n        if (!insideQuotes) modeIndex++;\n      default:\n        if (!insideQuotes) break;\n    }\n    if (complete) break;\n  }\n  return modeStrings;\n}\n\n/**\n * Sort either the modes or the palettes using quicksort.\n */\nvoid RotaryEncoderUIUsermod::re_sortModes(const char **modeNames, byte *indexes, int count, int numSkip) {\n  if (!modeNames) return;\n  listBeingSorted = modeNames;\n  qsort(indexes + numSkip, count - numSkip, sizeof(byte), re_qstringCmp);\n  listBeingSorted = nullptr;\n}\n\n\n// public methods\n\n\n/*\n  * setup() is called once at boot. WiFi is not yet connected at this point.\n  * You can use it to initialize variables, sensors or similar.\n  */\nvoid RotaryEncoderUIUsermod::setup()\n{\n  DEBUG_PRINTLN(F(\"Usermod Rotary Encoder init.\"));\n\n  if (usePcf8574) {\n    if (i2c_sda < 0 || i2c_scl < 0 || pinA < 0 || pinB < 0 || pinC < 0) {\n      DEBUG_PRINTLN(F(\"I2C and/or PCF8574 pins unused, disabling.\"));\n      enabled = false;\n      return;\n    } else {\n      if (pinIRQ >= 0 && PinManager::allocatePin(pinIRQ, false, PinOwner::UM_RotaryEncoderUI)) {\n        pinMode(pinIRQ, INPUT_PULLUP);\n        attachInterrupt(pinIRQ, i2cReadingISR, FALLING); // RISING, FALLING, CHANGE, ONLOW, ONHIGH\n        DEBUG_PRINTLN(F(\"Interrupt attached.\"));\n      } else {\n        DEBUG_PRINTLN(F(\"Unable to allocate interrupt pin, disabling.\"));\n        pinIRQ = -1;\n        enabled = false;\n        return;\n      }\n    }\n  } else {\n    PinManagerPinType pins[3] = { { pinA, false }, { pinB, false }, { pinC, false } };\n    if (pinA<0 || pinB<0 || !PinManager::allocateMultiplePins(pins, 3, PinOwner::UM_RotaryEncoderUI)) {\n      pinA = pinB = pinC = -1;\n      enabled = false;\n      return;\n    }\n\n    #ifndef USERMOD_ROTARY_ENCODER_GPIO\n      #define USERMOD_ROTARY_ENCODER_GPIO INPUT_PULLUP\n    #endif\n    pinMode(pinA, USERMOD_ROTARY_ENCODER_GPIO);\n    pinMode(pinB, USERMOD_ROTARY_ENCODER_GPIO);\n    if (pinC>=0) pinMode(pinC, USERMOD_ROTARY_ENCODER_GPIO);\n  }\n\n  loopTime = millis();\n\n  currentCCT = (approximateKelvinFromRGB(RGBW32(colPri[0], colPri[1], colPri[2], colPri[3])) - 1900) >> 5;\n\n  if (!initDone) sortModesAndPalettes();\n\n#ifdef USERMOD_FOUR_LINE_DISPLAY\n  // This Usermod uses FourLineDisplayUsermod for the best experience.\n  // But it's optional. But you want it.\n  display = (FourLineDisplayUsermod*) UsermodManager::lookup(USERMOD_ID_FOUR_LINE_DISP);\n  if (display != nullptr) {\n    display->setMarkLine(1, 0);\n  }\n#endif\n\n  initDone = true;\n  Enc_A = readPin(pinA); // Read encoder pins\n  Enc_B = readPin(pinB);\n  Enc_A_prev = Enc_A;\n}\n\n/*\n  * loop() is called continuously. Here you can check for events, read sensors, etc.\n  * \n  * Tips:\n  * 1. You can use \"if (WLED_CONNECTED)\" to check for a successful network connection.\n  *    Additionally, \"if (WLED_MQTT_CONNECTED)\" is available to check for a connection to an MQTT broker.\n  * \n  * 2. Try to avoid using the delay() function. NEVER use delays longer than 10 milliseconds.\n  *    Instead, use a timer check as shown here.\n  */\nvoid RotaryEncoderUIUsermod::loop()\n{\n  if (!enabled) return;\n  unsigned long currentTime = millis(); // get the current elapsed time\n  if (strip.isUpdating() && ((currentTime - loopTime) < ENCODER_MAX_DELAY_MS)) return;  // be nice, but not too nice\n\n  // Initialize effectCurrentIndex and effectPaletteIndex to\n  // current state. We do it here as (at least) effectCurrent\n  // is not yet initialized when setup is called.\n  \n  if (!currentEffectAndPaletteInitialized) {\n    findCurrentEffectAndPalette();\n  }\n\n  if (modes_alpha_indexes[effectCurrentIndex] != effectCurrent || palettes_alpha_indexes[effectPaletteIndex] != effectPalette) {\n    DEBUG_PRINTLN(F(\"Current mode or palette changed.\"));\n    currentEffectAndPaletteInitialized = false;\n  }\n\n  if (currentTime >= (loopTime + 2)) // 2ms since last check of encoder = 500Hz\n  {\n    bool buttonPressed = !readPin(pinC); //0=pressed, 1=released\n    if (buttonPressed) {\n      if (!buttonPressedBefore) buttonPressedTime = currentTime;\n      buttonPressedBefore = true;\n      if (currentTime-buttonPressedTime > 3000) {\n        if (!buttonLongPressed) displayNetworkInfo(); //long press for network info\n        buttonLongPressed = true;\n      }\n    } else if (!buttonPressed && buttonPressedBefore) {\n      bool doublePress = buttonWaitTime;\n      buttonWaitTime = 0;\n      if (!buttonLongPressed) {\n        if (doublePress) {\n          toggleOnOff();\n          lampUdated();\n        } else {\n          buttonWaitTime = currentTime;\n        }\n      }\n      buttonLongPressed = false;\n      buttonPressedBefore = false;\n    }\n    if (buttonWaitTime && currentTime-buttonWaitTime>350 && !buttonPressedBefore) { //same speed as in button.cpp\n      buttonWaitTime = 0;\n      char newState = select_state + 1;\n      bool changedState = false;\n      char lineBuffer[64];\n      do {\n        // find new state\n        switch (newState) {\n          case  0: strcpy_P(lineBuffer, PSTR(\"Brightness\")); changedState = true; break;\n          case  1: if (!extractModeSlider(effectCurrent, 0, lineBuffer, 63)) newState++; else changedState = true; break; // speed\n          case  2: if (!extractModeSlider(effectCurrent, 1, lineBuffer, 63)) newState++; else changedState = true; break; // intensity\n          case  3: strcpy_P(lineBuffer, PSTR(\"Color Palette\")); changedState = true; break;\n          case  4: strcpy_P(lineBuffer, PSTR(\"Effect\")); changedState = true; break;\n          case  5: strcpy_P(lineBuffer, PSTR(\"Main Color\")); changedState = true; break;\n          case  6: strcpy_P(lineBuffer, PSTR(\"Saturation\")); changedState = true; break;\n          case  7: \n            if (!(strip.getSegment(applyToAll ? strip.getFirstSelectedSegId() : strip.getMainSegmentId()).getLightCapabilities() & 0x04)) newState++;\n            else { strcpy_P(lineBuffer, PSTR(\"CCT\")); changedState = true; }\n            break;\n          case  8: if (presetHigh==0 || presetLow == 0) newState++; else { strcpy_P(lineBuffer, PSTR(\"Preset\")); changedState = true; } break;\n          case  9:\n          case 10:\n          case 11: if (!extractModeSlider(effectCurrent, newState-7, lineBuffer, 63)) newState++; else changedState = true; break; // custom\n        }\n        if (newState > LAST_UI_STATE) newState = 0;\n      } while (!changedState);\n      if (display != nullptr) {\n        switch (newState) {\n          case  0: changedState = changeState(lineBuffer,   1,   0,  1); break; //1  = sun\n          case  1: changedState = changeState(lineBuffer,   1,   4,  2); break; //2  = skip forward\n          case  2: changedState = changeState(lineBuffer,   1,   8,  3); break; //3  = fire\n          case  3: changedState = changeState(lineBuffer,   2,   0,  4); break; //4  = custom palette\n          case  4: changedState = changeState(lineBuffer,   3,   0,  5); break; //5  = puzzle piece\n          case  5: changedState = changeState(lineBuffer, 255, 255,  7); break; //7  = brush\n          case  6: changedState = changeState(lineBuffer, 255, 255,  8); break; //8  = contrast\n          case  7: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star\n          case  8: changedState = changeState(lineBuffer, 255, 255, 11); break; //11 = heart\n          case  9: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star\n          case 10: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star\n          case 11: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star\n        }\n      }\n      if (changedState) select_state = newState;\n    }\n\n    Enc_A = readPin(pinA); // Read encoder pins\n    Enc_B = readPin(pinB);\n    if ((Enc_A) && (!Enc_A_prev))\n    { // A has gone from high to low\n      if (Enc_B == LOW)    //changes to LOW so that then encoder registers a change at the very end of a pulse\n      { // B is high so clockwise\n        switch(select_state) {\n          case  0: changeBrightness(true);      break;\n          case  1: changeEffectSpeed(true);     break;\n          case  2: changeEffectIntensity(true); break;\n          case  3: changePalette(true);         break;\n          case  4: changeEffect(true);          break;\n          case  5: changeHue(true);             break;\n          case  6: changeSat(true);             break;\n          case  7: changeCCT(true);             break;\n          case  8: changePreset(true);          break;\n          case  9: changeCustom(1,true);        break;\n          case 10: changeCustom(2,true);        break;\n          case 11: changeCustom(3,true);        break;\n        }\n      }\n      else if (Enc_B == HIGH)\n      { // B is low so counter-clockwise\n        switch(select_state) {\n          case  0: changeBrightness(false);      break;\n          case  1: changeEffectSpeed(false);     break;\n          case  2: changeEffectIntensity(false); break;\n          case  3: changePalette(false);         break;\n          case  4: changeEffect(false);          break;\n          case  5: changeHue(false);             break;\n          case  6: changeSat(false);             break;\n          case  7: changeCCT(false);             break;\n          case  8: changePreset(false);          break;\n          case  9: changeCustom(1,false);        break;\n          case 10: changeCustom(2,false);        break;\n          case 11: changeCustom(3,false);        break;\n        }\n      }\n    }\n    Enc_A_prev = Enc_A;     // Store value of A for next time\n    loopTime = currentTime; // Updates loopTime\n  }\n}\n\nvoid RotaryEncoderUIUsermod::displayNetworkInfo() {\n  #ifdef USERMOD_FOUR_LINE_DISPLAY\n  display->networkOverlay(PSTR(\"NETWORK INFO\"), 10000);\n  #endif\n}\n\nvoid RotaryEncoderUIUsermod::findCurrentEffectAndPalette() {\n  DEBUG_PRINTLN(F(\"Finding current mode and palette.\"));\n  currentEffectAndPaletteInitialized = true;\n\n  effectCurrentIndex = 0;\n  for (int i = 0; i < strip.getModeCount(); i++) {\n    if (modes_alpha_indexes[i] == effectCurrent) {\n      effectCurrentIndex = i;\n      DEBUG_PRINTLN(F(\"Found current mode.\"));\n      break;\n    }\n  }\n\n  effectPaletteIndex = 0;\n  DEBUG_PRINTLN(effectPalette);\n  for (unsigned i = 0; i < getPaletteCount()+customPalettes.size(); i++) {\n    if (palettes_alpha_indexes[i] == effectPalette) {\n      effectPaletteIndex = i;\n      DEBUG_PRINTLN(F(\"Found palette.\"));\n      break;\n    }\n  }\n}\n\nbool RotaryEncoderUIUsermod::changeState(const char *stateName, byte markedLine, byte markedCol, byte glyph) {\n#ifdef USERMOD_FOUR_LINE_DISPLAY\n  if (display != nullptr) {\n    if (display->wakeDisplay()) {\n      // Throw away wake up input\n      display->redraw(true);\n      return false;\n    }\n    display->overlay(stateName, 750, glyph);\n    display->setMarkLine(markedLine, markedCol);\n  }\n#endif\n  return true;\n}\n\nvoid RotaryEncoderUIUsermod::lampUdated() {\n  //call for notifier -> 0: init 1: direct change 2: button 3: notification 4: nightlight 5: other (No notification)\n  // 6: fx changed 7: hue 8: preset cycle 9: blynk 10: alexa\n  //setValuesFromFirstSelectedSeg(); //to make transition work on main segment (should no longer be required)\n  stateUpdated(CALL_MODE_BUTTON);\n  updateInterfaces(CALL_MODE_BUTTON);\n}\n\nvoid RotaryEncoderUIUsermod::changeBrightness(bool increase) {\n#ifdef USERMOD_FOUR_LINE_DISPLAY\n  if (display && display->wakeDisplay()) {\n    display->redraw(true);\n    // Throw away wake up input\n    return;\n  }\n  display->updateRedrawTime();\n#endif\n  //bri = max(min((increase ? bri+fadeAmount : bri-fadeAmount), 255), 0);\n  if (bri < 40) bri = max(min((increase ? bri+fadeAmount/2 : bri-fadeAmount/2), 255), 0); // slower steps when brightness < 16%\n  else bri = max(min((increase ? bri+fadeAmount : bri-fadeAmount), 255), 0);\n  lampUdated();\n#ifdef USERMOD_FOUR_LINE_DISPLAY\n  display->updateBrightness();\n#endif\n}\n\n\nvoid RotaryEncoderUIUsermod::changeEffect(bool increase) {\n#ifdef USERMOD_FOUR_LINE_DISPLAY\n  if (display && display->wakeDisplay()) {\n    display->redraw(true);\n    // Throw away wake up input\n    return;\n  }\n  display->updateRedrawTime();\n#endif\n  effectCurrentIndex = max(min((increase ? effectCurrentIndex+1 : effectCurrentIndex-1), strip.getModeCount()-1), 0);\n  effectCurrent = modes_alpha_indexes[effectCurrentIndex];\n  stateChanged = true;\n  if (applyToAll) {\n    for (unsigned i=0; i<strip.getSegmentsNum(); i++) {\n      Segment& seg = strip.getSegment(i);\n      if (!seg.isActive()) continue;\n      seg.setMode(effectCurrent);\n    }\n  } else {\n    Segment& seg = strip.getSegment(strip.getMainSegmentId());\n    seg.setMode(effectCurrent);\n  }\n  lampUdated();\n#ifdef USERMOD_FOUR_LINE_DISPLAY\n  display->showCurrentEffectOrPalette(effectCurrent, JSON_mode_names, 3);\n#endif\n}\n\n\nvoid RotaryEncoderUIUsermod::changeEffectSpeed(bool increase) {\n#ifdef USERMOD_FOUR_LINE_DISPLAY\n  if (display && display->wakeDisplay()) {\n    display->redraw(true);\n    // Throw away wake up input\n    return;\n  }\n  display->updateRedrawTime();\n#endif\n  effectSpeed = max(min((increase ? effectSpeed+fadeAmount : effectSpeed-fadeAmount), 255), 0);\n  stateChanged = true;\n  if (applyToAll) {\n    for (unsigned i=0; i<strip.getSegmentsNum(); i++) {\n      Segment& seg = strip.getSegment(i);\n      if (!seg.isActive()) continue;\n      seg.speed = effectSpeed;\n    }\n  } else {\n    Segment& seg = strip.getSegment(strip.getMainSegmentId());\n    seg.speed = effectSpeed;\n  }\n  lampUdated();\n#ifdef USERMOD_FOUR_LINE_DISPLAY\n  display->updateSpeed();\n#endif\n}\n\n\nvoid RotaryEncoderUIUsermod::changeEffectIntensity(bool increase) {\n#ifdef USERMOD_FOUR_LINE_DISPLAY\n  if (display && display->wakeDisplay()) {\n    display->redraw(true);\n    // Throw away wake up input\n    return;\n  }\n  display->updateRedrawTime();\n#endif\n  effectIntensity = max(min((increase ? effectIntensity+fadeAmount : effectIntensity-fadeAmount), 255), 0);\n  stateChanged = true;\n  if (applyToAll) {\n    for (unsigned i=0; i<strip.getSegmentsNum(); i++) {\n      Segment& seg = strip.getSegment(i);\n      if (!seg.isActive()) continue;\n      seg.intensity = effectIntensity;\n    }\n  } else {\n    Segment& seg = strip.getSegment(strip.getMainSegmentId());\n    seg.intensity = effectIntensity;\n  }\n  lampUdated();\n#ifdef USERMOD_FOUR_LINE_DISPLAY\n  display->updateIntensity();\n#endif\n}\n\n\nvoid RotaryEncoderUIUsermod::changeCustom(uint8_t par, bool increase) {\n  uint8_t val = 0;\n#ifdef USERMOD_FOUR_LINE_DISPLAY\n  if (display && display->wakeDisplay()) {\n    display->redraw(true);\n    // Throw away wake up input\n    return;\n  }\n  display->updateRedrawTime();\n#endif\n  stateChanged = true;\n  if (applyToAll) {\n    uint8_t id = strip.getFirstSelectedSegId();\n    Segment& sid = strip.getSegment(id);\n    switch (par) {\n      case 3:  val = sid.custom3 = max(min((increase ? sid.custom3+fadeAmount : sid.custom3-fadeAmount), 255), 0); break;\n      case 2:  val = sid.custom2 = max(min((increase ? sid.custom2+fadeAmount : sid.custom2-fadeAmount), 255), 0); break;\n      default: val = sid.custom1 = max(min((increase ? sid.custom1+fadeAmount : sid.custom1-fadeAmount), 255), 0); break;\n    }\n    for (unsigned i=0; i<strip.getSegmentsNum(); i++) {\n      Segment& seg = strip.getSegment(i);\n      if (!seg.isActive() || i == id) continue;\n      switch (par) {\n        case 3:  seg.custom3 = sid.custom3; break;\n        case 2:  seg.custom2 = sid.custom2; break;\n        default: seg.custom1 = sid.custom1; break;\n      }\n    }\n  } else {\n    Segment& seg = strip.getMainSegment();\n    switch (par) {\n      case 3:  val = seg.custom3 = max(min((increase ? seg.custom3+fadeAmount : seg.custom3-fadeAmount), 255), 0); break;\n      case 2:  val = seg.custom2 = max(min((increase ? seg.custom2+fadeAmount : seg.custom2-fadeAmount), 255), 0); break;\n      default: val = seg.custom1 = max(min((increase ? seg.custom1+fadeAmount : seg.custom1-fadeAmount), 255), 0); break;\n    }\n  }\n  lampUdated();\n#ifdef USERMOD_FOUR_LINE_DISPLAY\n  char lineBuffer[64];\n  sprintf(lineBuffer, \"%d\", val);\n  display->overlay(lineBuffer, 500, 10); // use star\n#endif\n}\n\n\nvoid RotaryEncoderUIUsermod::changePalette(bool increase) {\n#ifdef USERMOD_FOUR_LINE_DISPLAY\n  if (display && display->wakeDisplay()) {\n    display->redraw(true);\n    // Throw away wake up input\n    return;\n  }\n  display->updateRedrawTime();\n#endif\n  effectPaletteIndex = max(min((unsigned)(increase ? effectPaletteIndex+1 : effectPaletteIndex-1), getPaletteCount()+customPalettes.size()-1), 0U);\n  effectPalette = palettes_alpha_indexes[effectPaletteIndex];\n  stateChanged = true;\n  if (applyToAll) {\n    for (unsigned i=0; i<strip.getSegmentsNum(); i++) {\n      Segment& seg = strip.getSegment(i);\n      if (!seg.isActive()) continue;\n      seg.setPalette(effectPalette);\n    }\n  } else {\n    Segment& seg = strip.getSegment(strip.getMainSegmentId());\n    seg.setPalette(effectPalette);\n  }\n  lampUdated();\n#ifdef USERMOD_FOUR_LINE_DISPLAY\n  display->showCurrentEffectOrPalette(effectPalette, JSON_palette_names, 2);\n#endif\n}\n\n\nvoid RotaryEncoderUIUsermod::changeHue(bool increase){\n#ifdef USERMOD_FOUR_LINE_DISPLAY\n  if (display && display->wakeDisplay()) {\n    display->redraw(true);\n    // Throw away wake up input\n    return;\n  }\n  display->updateRedrawTime();\n#endif\n  currentHue1 = max(min((increase ? currentHue1+fadeAmount : currentHue1-fadeAmount), 255), 0);\n  colorHStoRGB(currentHue1*256, currentSat1, colPri);\n  stateChanged = true; \n  if (applyToAll) {\n    for (unsigned i=0; i<strip.getSegmentsNum(); i++) {\n      Segment& seg = strip.getSegment(i);\n      if (!seg.isActive()) continue;\n      seg.colors[0] = RGBW32(colPri[0], colPri[1], colPri[2], colPri[3]);\n    }\n  } else {\n    Segment& seg = strip.getSegment(strip.getMainSegmentId());\n    seg.colors[0] = RGBW32(colPri[0], colPri[1], colPri[2], colPri[3]);\n  }\n  lampUdated();\n#ifdef USERMOD_FOUR_LINE_DISPLAY\n  char lineBuffer[64];\n  sprintf(lineBuffer, \"%d\", currentHue1);\n  display->overlay(lineBuffer, 500, 7); // use brush\n#endif\n}\n\nvoid RotaryEncoderUIUsermod::changeSat(bool increase){\n#ifdef USERMOD_FOUR_LINE_DISPLAY\n  if (display && display->wakeDisplay()) {\n    display->redraw(true);\n    // Throw away wake up input\n    return;\n  }\n  display->updateRedrawTime();\n#endif\n  currentSat1 = max(min((increase ? currentSat1+fadeAmount : currentSat1-fadeAmount), 255), 0);\n  colorHStoRGB(currentHue1*256, currentSat1, colPri);\n  if (applyToAll) {\n    for (unsigned i=0; i<strip.getSegmentsNum(); i++) {\n      Segment& seg = strip.getSegment(i);\n      if (!seg.isActive()) continue;\n      seg.colors[0] = RGBW32(colPri[0], colPri[1], colPri[2], colPri[3]);\n    }\n  } else {\n    Segment& seg = strip.getSegment(strip.getMainSegmentId());\n    seg.colors[0] = RGBW32(colPri[0], colPri[1], colPri[2], colPri[3]);\n  }\n  lampUdated();\n#ifdef USERMOD_FOUR_LINE_DISPLAY\n  char lineBuffer[64];\n  sprintf(lineBuffer, \"%d\", currentSat1);\n  display->overlay(lineBuffer, 500, 8); // use contrast\n#endif\n}\n\nvoid RotaryEncoderUIUsermod::changePreset(bool increase) {\n#ifdef USERMOD_FOUR_LINE_DISPLAY\n  if (display && display->wakeDisplay()) {\n    display->redraw(true);\n    // Throw away wake up input\n    return;\n  }\n  display->updateRedrawTime();\n#endif\n  if (presetHigh && presetLow && presetHigh > presetLow) {\n    StaticJsonDocument<64> root;\n    char str[64];\n    sprintf_P(str, PSTR(\"%d~%d~%s\"), presetLow, presetHigh, increase?\"\":\"-\");\n    root[\"ps\"] = str;\n    deserializeState(root.as<JsonObject>(), CALL_MODE_BUTTON_PRESET);\n/*\n    String apireq = F(\"win&PL=~\");\n    if (!increase) apireq += '-';\n    apireq += F(\"&P1=\");\n    apireq += presetLow;\n    apireq += F(\"&P2=\");\n    apireq += presetHigh;\n    handleSet(nullptr, apireq, false);\n*/\n    lampUdated();\n  #ifdef USERMOD_FOUR_LINE_DISPLAY\n    sprintf(str, \"%d\", currentPreset);\n    display->overlay(str, 500, 11); // use heart\n  #endif\n  }\n}\n\nvoid RotaryEncoderUIUsermod::changeCCT(bool increase){\n#ifdef USERMOD_FOUR_LINE_DISPLAY\n  if (display && display->wakeDisplay()) {\n    display->redraw(true);\n    // Throw away wake up input\n    return;\n  }\n  display->updateRedrawTime();\n#endif\n  currentCCT = max(min((increase ? currentCCT+fadeAmount : currentCCT-fadeAmount), 255), 0);\n//    if (applyToAll) {\n    for (unsigned i=0; i<strip.getSegmentsNum(); i++) {\n      Segment& seg = strip.getSegment(i);\n      if (!seg.isActive()) continue;\n      seg.setCCT(currentCCT);\n    }\n//    } else {\n//      Segment& seg = strip.getSegment(strip.getMainSegmentId());\n//      seg.setCCT(currentCCT, strip.getMainSegmentId());\n//    }\n  lampUdated();\n#ifdef USERMOD_FOUR_LINE_DISPLAY\n  char lineBuffer[64];\n  sprintf(lineBuffer, \"%d\", currentCCT);\n  display->overlay(lineBuffer, 500, 10); // use star\n#endif\n}\n\n/*\n  * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.\n  * Creating an \"u\" object allows you to add custom key/value pairs to the Info section of the WLED web UI.\n  * Below it is shown how this could be used for e.g. a light sensor\n  */\n/*\nvoid RotaryEncoderUIUsermod::addToJsonInfo(JsonObject& root)\n{\n  int reading = 20;\n  //this code adds \"u\":{\"Light\":[20,\" lux\"]} to the info object\n  JsonObject user = root[\"u\"];\n  if (user.isNull()) user = root.createNestedObject(\"u\");\n  JsonArray lightArr = user.createNestedArray(\"Light\"); //name\n  lightArr.add(reading); //value\n  lightArr.add(\" lux\"); //unit\n}\n*/\n\n/*\n  * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).\n  * Values in the state object may be modified by connected clients\n  */\n/*\nvoid RotaryEncoderUIUsermod::addToJsonState(JsonObject &root)\n{\n  //root[\"user0\"] = userVar0;\n}\n*/\n\n/*\n  * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).\n  * Values in the state object may be modified by connected clients\n  */\n/*\nvoid RotaryEncoderUIUsermod::readFromJsonState(JsonObject &root)\n{\n  //userVar0 = root[\"user0\"] | userVar0; //if \"user0\" key exists in JSON, update, else keep old value\n  //if (root[\"bri\"] == 255) Serial.println(F(\"Don't burn down your garage!\"));\n}\n*/\n\n/**\n * addToConfig() (called from set.cpp) stores persistent properties to cfg.json\n */\nvoid RotaryEncoderUIUsermod::addToConfig(JsonObject &root) {\n  // we add JSON object: {\"Rotary-Encoder\":{\"DT-pin\":12,\"CLK-pin\":14,\"SW-pin\":13}}\n  JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname\n  top[FPSTR(_enabled)] = enabled;\n  top[FPSTR(_DT_pin)]  = pinA;\n  top[FPSTR(_CLK_pin)] = pinB;\n  top[FPSTR(_SW_pin)]  = pinC;\n  top[FPSTR(_presetLow)]  = presetLow;\n  top[FPSTR(_presetHigh)] = presetHigh;\n  top[FPSTR(_applyToAll)] = applyToAll;\n  top[FPSTR(_pcf8574)]    = usePcf8574;\n  top[FPSTR(_pcfAddress)] = addrPcf8574;\n  top[FPSTR(_pcfINTpin)]  = pinIRQ;\n  DEBUG_PRINTLN(F(\"Rotary Encoder config saved.\"));\n}\n\nvoid RotaryEncoderUIUsermod::appendConfigData() {\n  oappend(F(\"addInfo('Rotary-Encoder:PCF8574-address',1,'<i>(not hex!)</i>');\"));\n  oappend(F(\"d.extra.push({'Rotary-Encoder':{pin:[['P0',100],['P1',101],['P2',102],['P3',103],['P4',104],['P5',105],['P6',106],['P7',107]]}});\"));\n}\n\n/**\n * readFromConfig() is called before setup() to populate properties from values stored in cfg.json\n *\n * The function should return true if configuration was successfully loaded or false if there was no configuration.\n */\nbool RotaryEncoderUIUsermod::readFromConfig(JsonObject &root) {\n  // we look for JSON object: {\"Rotary-Encoder\":{\"DT-pin\":12,\"CLK-pin\":14,\"SW-pin\":13}}\n  JsonObject top = root[FPSTR(_name)];\n  if (top.isNull()) {\n    DEBUG_PRINT(FPSTR(_name));\n    DEBUG_PRINTLN(F(\": No config found. (Using defaults.)\"));\n    return false;\n  }\n  int8_t newDTpin  = top[FPSTR(_DT_pin)]  | pinA;\n  int8_t newCLKpin = top[FPSTR(_CLK_pin)] | pinB;\n  int8_t newSWpin  = top[FPSTR(_SW_pin)]  | pinC;\n  int8_t newIRQpin = top[FPSTR(_pcfINTpin)] | pinIRQ;\n  bool   oldPcf8574 = usePcf8574;\n\n  presetHigh = top[FPSTR(_presetHigh)] | presetHigh;\n  presetLow  = top[FPSTR(_presetLow)]  | presetLow;\n  presetHigh = MIN(250,MAX(0,presetHigh));\n  presetLow  = MIN(250,MAX(0,presetLow));\n\n  enabled    = top[FPSTR(_enabled)] | enabled;\n  applyToAll = top[FPSTR(_applyToAll)] | applyToAll;\n\n  usePcf8574 = top[FPSTR(_pcf8574)] | usePcf8574;\n  addrPcf8574 = top[FPSTR(_pcfAddress)] | addrPcf8574;\n\n  DEBUG_PRINT(FPSTR(_name));\n  if (!initDone) {\n    // first run: reading from cfg.json\n    pinA = newDTpin;\n    pinB = newCLKpin;\n    pinC = newSWpin;\n    DEBUG_PRINTLN(F(\" config loaded.\"));\n  } else {\n    DEBUG_PRINTLN(F(\" config (re)loaded.\"));\n    // changing parameters from settings page\n    if (pinA!=newDTpin || pinB!=newCLKpin || pinC!=newSWpin || pinIRQ!=newIRQpin) {\n      if (oldPcf8574) {\n        if (pinIRQ >= 0) {\n          detachInterrupt(pinIRQ);\n          PinManager::deallocatePin(pinIRQ, PinOwner::UM_RotaryEncoderUI);\n          DEBUG_PRINTLN(F(\"Deallocated old IRQ pin.\"));\n        }\n        pinIRQ = newIRQpin<100 ? newIRQpin : -1; // ignore PCF8574 pins\n      } else {\n        PinManager::deallocatePin(pinA, PinOwner::UM_RotaryEncoderUI);\n        PinManager::deallocatePin(pinB, PinOwner::UM_RotaryEncoderUI);\n        PinManager::deallocatePin(pinC, PinOwner::UM_RotaryEncoderUI);\n        DEBUG_PRINTLN(F(\"Deallocated old pins.\"));\n      }\n      pinA = newDTpin;\n      pinB = newCLKpin;\n      pinC = newSWpin;\n      if (pinA<0 || pinB<0 || pinC<0) {\n        enabled = false;\n        return true;\n      }\n      setup();\n    }\n  }\n  // use \"return !top[\"newestParameter\"].isNull();\" when updating Usermod with new features\n  return !top[FPSTR(_pcfINTpin)].isNull();\n}\n\n\n// strings to reduce flash memory usage (used more than twice)\nconst char RotaryEncoderUIUsermod::_name[]       PROGMEM = \"Rotary-Encoder\";\nconst char RotaryEncoderUIUsermod::_enabled[]    PROGMEM = \"enabled\";\nconst char RotaryEncoderUIUsermod::_DT_pin[]     PROGMEM = \"DT-pin\";\nconst char RotaryEncoderUIUsermod::_CLK_pin[]    PROGMEM = \"CLK-pin\";\nconst char RotaryEncoderUIUsermod::_SW_pin[]     PROGMEM = \"SW-pin\";\nconst char RotaryEncoderUIUsermod::_presetHigh[] PROGMEM = \"preset-high\";\nconst char RotaryEncoderUIUsermod::_presetLow[]  PROGMEM = \"preset-low\";\nconst char RotaryEncoderUIUsermod::_applyToAll[] PROGMEM = \"apply-2-all-seg\";\nconst char RotaryEncoderUIUsermod::_pcf8574[]    PROGMEM = \"use-PCF8574\";\nconst char RotaryEncoderUIUsermod::_pcfAddress[] PROGMEM = \"PCF8574-address\";\nconst char RotaryEncoderUIUsermod::_pcfINTpin[]  PROGMEM = \"PCF8574-INT-pin\";\n\n\nstatic RotaryEncoderUIUsermod usermod_v2_rotary_encoder_ui_alt;\nREGISTER_USERMOD(usermod_v2_rotary_encoder_ui_alt);"
  },
  {
    "path": "usermods/usermod_v2_word_clock/library.json",
    "content": "{\n  \"name\": \"usermod_v2_word_clock\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/usermod_v2_word_clock/readme.md",
    "content": "# Word Clock Usermod V2\n\nThis usermod drives an 11x10 pixel matrix wordclock with WLED. There are 4 additional dots for the minutes.\nThe visualisation is described by 4 masks with LED numbers (single dots for minutes, minutes, hours and \"clock\"). The index of the LEDs in the masks always starts at 0, even if the ledOffset is not 0.\nThere are 3 parameters that control behavior:\n\nactive: enable/disable usermod\ndiplayItIs: enable/disable display of \"Es ist\" on the clock\nledOffset: number of LEDs before the wordclock LEDs\n\n## Update for alternative wiring pattern\n\nBased on this fantastic work I added an alternative wiring pattern.\nThe original used a long wire to connect DO to DI, from one line to the next line.\n\nI wired my clock in meander style. So the first LED in the second line is on the right.\nWith this method, every other line was inverted and showed the wrong letter.\n\nI added a switch in usermod called \"meander wiring?\" to enable/disable the alternate wiring pattern.\n\n## Installation\n\nCopy and update the example `platformio_override.ini.sample`\nfrom the Rotary Encoder UI usermod folder to the root directory of your particular build.\nThis file should be placed in the same directory as `platformio.ini`.\n\n### Define Your Options\n\n* `USERMOD_WORDCLOCK`   - define this to have this usermod included wled00\\usermods_list.cpp\n\n### PlatformIO requirements\n\nNo special requirements.\n\n## Change Log\n\n2022/08/18 added meander wiring pattern.\n\n2022/03/30 initial commit\n"
  },
  {
    "path": "usermods/usermod_v2_word_clock/usermod_v2_word_clock.cpp",
    "content": "#include \"wled.h\"\n\n/*\n * Usermods allow you to add own functionality to WLED more easily\n * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality\n * \n * This usermod can be used to drive a wordclock with a 11x10 pixel matrix with WLED. There are also 4 additional dots for the minutes. \n * The visualisation is described in 4 mask with LED numbers (single dots for minutes, minutes, hours and \"clock/Uhr\").\n * There are 2 parameters to change the behaviour:\n * \n * active: enable/disable usermod\n * diplayItIs: enable/disable display of \"Es ist\" on the clock.\n */\n\nclass WordClockUsermod : public Usermod \n{\n  private:\n    unsigned long lastTime = 0;\n    int lastTimeMinutes = -1;\n\n    // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer)\n    bool usermodActive = false;\n    bool displayItIs = false;\n    int ledOffset = 100;\n    bool meander = false;\n    bool nord = false;\n    \n    // defines for mask sizes\n    #define maskSizeLeds        114\n    #define maskSizeMinutes     12\n    #define maskSizeMinutesMea  12\n    #define maskSizeHours       6\n    #define maskSizeHoursMea    6\n    #define maskSizeItIs        5\n    #define maskSizeMinuteDots  4\n\n    // \"minute\" masks\n    // Normal wiring\n    const int maskMinutes[14][maskSizeMinutes] = \n    {\n      {107, 108, 109,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1}, // 0 - 00\n      {  7,   8,   9,  10,  40,  41,  42,  43,  -1,  -1,  -1,  -1}, // 1 - 05 fünf nach\n      { 11,  12,  13,  14,  40,  41,  42,  43,  -1,  -1,  -1,  -1}, // 2 - 10 zehn nach\n      { 26,  27,  28,  29,  30,  31,  32,  -1,  -1,  -1,  -1,  -1}, // 3 - 15 viertel\n      { 15,  16,  17,  18,  19,  20,  21,  40,  41,  42,  43,  -1}, // 4 - 20 zwanzig nach\n      {  7,   8,   9,  10,  33,  34,  35,  44,  45,  46,  47,  -1}, // 5 - 25 fünf vor halb\n      { 44,  45,  46,  47,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1}, // 6 - 30 halb\n      {  7,   8,   9,  10,  40,  41,  42,  43,  44,  45,  46,  47}, // 7 - 35 fünf nach halb\n      { 15,  16,  17,  18,  19,  20,  21,  33,  34,  35,  -1,  -1}, // 8 - 40 zwanzig vor\n      { 22,  23,  24,  25,  26,  27,  28,  29,  30,  31,  32,  -1}, // 9 - 45 dreiviertel\n      { 11,  12,  13,  14,  33,  34,  35,  -1,  -1,  -1,  -1,  -1}, // 10 - 50 zehn vor\n      {  7,   8,   9,  10,  33,  34,  35,  -1,  -1,  -1,  -1,  -1}, // 11 - 55 fünf vor\n      { 26,  27,  28,  29,  30,  31,  32,  40,  41,  42,  43,  -1}, // 12 - 15 alternative viertel nach\n      { 26,  27,  28,  29,  30,  31,  32,  33,  34,  35,  -1,  -1}  // 13 - 45 alternative viertel vor\n    };\n\n    // Meander wiring\n    const int maskMinutesMea[14][maskSizeMinutesMea] = \n    {\n      { 99, 100, 101,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1}, // 0 - 00\n      {  7,   8,   9,  10,  33,  34,  35,  36,  -1,  -1,  -1,  -1}, // 1 - 05 fünf nach\n      { 18,  19,  20,  21,  33,  34,  35,  36,  -1,  -1,  -1,  -1}, // 2 - 10 zehn nach\n      { 26,  27,  28,  29,  30,  31,  32,  -1,  -1,  -1,  -1,  -1}, // 3 - 15 viertel\n      { 11,  12,  13,  14,  15,  16,  17,  33,  34,  35,  36,  -1}, // 4 - 20 zwanzig nach\n      {  7,   8,   9,  10,  41,  42,  43,  44,  45,  46,  47,  -1}, // 5 - 25 fünf vor halb\n      { 44,  45,  46,  47,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1}, // 6 - 30 halb\n      {  7,   8,   9,  10,  33,  34,  35,  36,  44,  45,  46,  47}, // 7 - 35 fünf nach halb\n      { 11,  12,  13,  14,  15,  16,  17,  41,  42,  43,  -1,  -1}, // 8 - 40 zwanzig vor\n      { 22,  23,  24,  25,  26,  27,  28,  29,  30,  31,  32,  -1}, // 9 - 45 dreiviertel\n      { 18,  19,  20,  21,  41,  42,  43,  -1,  -1,  -1,  -1,  -1}, // 10 - 50 zehn vor\n      {  7,   8,   9,  10,  41,  42,  43,  -1,  -1,  -1,  -1,  -1}, // 11 - 55 fünf vor\n      { 26,  27,  28,  29,  30,  31,  32,  33,  34,  35,  36,  -1}, // 12 - 15 alternative viertel nach\n      { 26,  27,  28,  29,  30,  31,  32,  41,  42,  43,  -1,  -1}  // 13 - 45 alternative viertel vor\n    };\n\n\n    // hour masks\n    // Normal wiring\n    const int maskHours[13][maskSizeHours] = \n    {\n      { 55,  56,  57,  -1,  -1,  -1}, // 01: ein\n      { 55,  56,  57,  58,  -1,  -1}, // 01: eins\n      { 62,  63,  64,  65,  -1,  -1}, // 02: zwei\n      { 66,  67,  68,  69,  -1,  -1}, // 03: drei\n      { 73,  74,  75,  76,  -1,  -1}, // 04: vier\n      { 51,  52,  53,  54,  -1,  -1}, // 05: fünf\n      { 77,  78,  79,  80,  81,  -1}, // 06: sechs\n      { 88,  89,  90,  91,  92,  93}, // 07: sieben\n      { 84,  85,  86,  87,  -1,  -1}, // 08: acht\n      {102, 103, 104, 105,  -1,  -1}, // 09: neun\n      { 99, 100, 101, 102,  -1,  -1}, // 10: zehn\n      { 49,  50,  51,  -1,  -1,  -1}, // 11: elf\n      { 94,  95,  96,  97,  98,  -1}  // 12: zwölf and 00: null\n    };\n    // Meander wiring\n    const int maskHoursMea[13][maskSizeHoursMea] = \n    {\n      { 63,  64,  65,  -1,  -1,  -1}, // 01: ein\n      { 62,  63,  64,  65,  -1,  -1}, // 01: eins\n      { 55,  56,  57,  58,  -1,  -1}, // 02: zwei\n      { 66,  67,  68,  69,  -1,  -1}, // 03: drei\n      { 73,  74,  75,  76,  -1,  -1}, // 04: vier\n      { 51,  52,  53,  54,  -1,  -1}, // 05: fünf\n      { 83,  84,  85,  86,  87,  -1}, // 06: sechs\n      { 88,  89,  90,  91,  92,  93}, // 07: sieben\n      { 77,  78,  79,  80,  -1,  -1}, // 08: acht\n      {103, 104, 105, 106,  -1,  -1}, // 09: neun\n      {106, 107, 108, 109,  -1,  -1}, // 10: zehn\n      { 49,  50,  51,  -1,  -1,  -1}, // 11: elf\n      { 94,  95,  96,  97,  98,  -1}  // 12: zwölf and 00: null\n    };\n\n    // mask \"it is\"\n    const int maskItIs[maskSizeItIs] = {0, 1, 3, 4, 5};\n\n    // mask minute dots\n    const int maskMinuteDots[maskSizeMinuteDots] = {110, 111, 112, 113};\n\n    // overall mask to define which LEDs are on\n    int maskLedsOn[maskSizeLeds] = \n    {\n      0,0,0,0,0,0,0,0,0,0,0,\n      0,0,0,0,0,0,0,0,0,0,0,\n      0,0,0,0,0,0,0,0,0,0,0,\n      0,0,0,0,0,0,0,0,0,0,0,\n      0,0,0,0,0,0,0,0,0,0,0,\n      0,0,0,0,0,0,0,0,0,0,0,\n      0,0,0,0,0,0,0,0,0,0,0,\n      0,0,0,0,0,0,0,0,0,0,0,\n      0,0,0,0,0,0,0,0,0,0,0,\n      0,0,0,0,0,0,0,0,0,0,0,\n      0,0,0,0\n    };\n\n    // update led mask\n    void updateLedMask(const int wordMask[], int arraySize)\n    {\n      // loop over array\n      for (int x=0; x < arraySize; x++) \n      {\n        // check if mask has a valid LED number\n        if (wordMask[x] >= 0 && wordMask[x] < maskSizeLeds)\n        {\n          // turn LED on\n          maskLedsOn[wordMask[x]] = 1;\n        }\n      }\n    }\n\n    // set hours\n    void setHours(int hours, bool fullClock)\n    {\n      int index = hours;\n\n      // handle 00:xx as 12:xx\n      if (hours == 0)\n      {\n        index = 12;\n      }\n\n      // check if we get an overrun of 12 o´clock\n      if (hours == 13)\n      {\n        index = 1;\n      }\n\n      // special handling for \"ein Uhr\" instead of \"eins Uhr\"\n      if (hours == 1 && fullClock == true)\n      {\n        index = 0;\n      }\n\n      // update led mask\n      if (meander)\n      {\n        updateLedMask(maskHoursMea[index], maskSizeHoursMea);\n      } else {\n      updateLedMask(maskHours[index], maskSizeHours);\n      }\n    }\n\n    // set minutes\n    void setMinutes(int index)\n    {\n      // update led mask\n      if (meander)\n      {\n        updateLedMask(maskMinutesMea[index], maskSizeMinutesMea);\n      } else {\n      updateLedMask(maskMinutes[index], maskSizeMinutes);\n      }\n    }\n\n    // set minutes dot\n    void setSingleMinuteDots(int minutes)\n    {\n      // modulo to get minute dots\n      int minutesDotCount = minutes % 5;\n\n      // check if minute dots are active\n      if (minutesDotCount > 0)\n      {\n        // activate all minute dots until number is reached\n        for (int i = 0; i < minutesDotCount; i++)\n        {\n          // activate LED\n          maskLedsOn[maskMinuteDots[i]] = 1;  \n        }\n      }\n    }\n\n    // update the display\n    void updateDisplay(uint8_t hours, uint8_t minutes) \n    {\n      // disable complete matrix at the bigging\n      for (int x = 0; x < maskSizeLeds; x++)\n      {\n        maskLedsOn[x] = 0;\n      } \n      \n      // display it is/es ist if activated\n      if (displayItIs)\n      {\n        updateLedMask(maskItIs, maskSizeItIs);\n      }\n\n      // set single minute dots\n      setSingleMinuteDots(minutes);\n\n      // switch minutes\n      switch (minutes / 5) \n      {\n        case 0:\n            // full hour\n            setMinutes(0);\n            setHours(hours, true);\n            break;\n        case 1:\n            // 5 nach\n            setMinutes(1);\n            setHours(hours, false);\n            break;\n        case 2:\n            // 10 nach\n            setMinutes(2);\n            setHours(hours, false);\n            break;\n        case 3:\n            if (nord) {\n              // viertel nach\n              setMinutes(12);\n              setHours(hours, false);\n            } else {\n              // viertel \n              setMinutes(3);\n              setHours(hours + 1, false);\n            };\n            break;\n        case 4:\n            // 20 nach\n            setMinutes(4);\n            setHours(hours, false);\n            break;\n        case 5:\n            // 5 vor halb\n            setMinutes(5);\n            setHours(hours + 1, false);\n            break;\n        case 6:\n            // halb\n            setMinutes(6);\n            setHours(hours + 1, false);\n            break;\n        case 7:\n            // 5 nach halb\n            setMinutes(7);\n            setHours(hours + 1, false);\n            break;\n        case 8:\n            // 20 vor\n            setMinutes(8);\n            setHours(hours + 1, false);\n            break;\n        case 9:\n            // viertel vor\n            if (nord) {\n              setMinutes(13);\n            } \n            // dreiviertel\n              else {\n              setMinutes(9);\n            }\n            setHours(hours + 1, false);\n            break;\n        case 10:\n            // 10 vor\n            setMinutes(10);\n            setHours(hours + 1, false);\n            break;\n        case 11:\n            // 5 vor\n            setMinutes(11);\n            setHours(hours + 1, false);\n            break;\n        }\n    }\n\n  public:\n    //Functions called by WLED\n\n    /*\n     * setup() is called once at boot. WiFi is not yet connected at this point.\n     * You can use it to initialize variables, sensors or similar.\n     */\n    void setup() \n    {\n    }\n\n    /*\n     * connected() is called every time the WiFi is (re)connected\n     * Use it to initialize network interfaces\n     */\n    void connected() \n    {\n    }\n\n    /*\n     * loop() is called continuously. Here you can check for events, read sensors, etc.\n     * \n     * Tips:\n     * 1. You can use \"if (WLED_CONNECTED)\" to check for a successful network connection.\n     *    Additionally, \"if (WLED_MQTT_CONNECTED)\" is available to check for a connection to an MQTT broker.\n     * \n     * 2. Try to avoid using the delay() function. NEVER use delays longer than 10 milliseconds.\n     *    Instead, use a timer check as shown here.\n     */\n    void loop() {\n\n      // do it every 5 seconds\n      if (millis() - lastTime > 5000) \n      {\n        // check the time\n        int minutes = minute(localTime);\n\n        // check if we already updated this minute\n        if (lastTimeMinutes != minutes)\n        {\n          // update the display with new time\n          updateDisplay(hourFormat12(localTime), minute(localTime));\n\n          // remember last update time\n          lastTimeMinutes = minutes;\n        }\n\n        // remember last update\n        lastTime = millis();\n      }\n    }\n\n    /*\n     * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API.\n     * Creating an \"u\" object allows you to add custom key/value pairs to the Info section of the WLED web UI.\n     * Below it is shown how this could be used for e.g. a light sensor\n     */\n    /*\n    void addToJsonInfo(JsonObject& root)\n    {\n    }\n    */\n\n    /*\n     * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object).\n     * Values in the state object may be modified by connected clients\n     */\n    void addToJsonState(JsonObject& root)\n    {\n    }\n\n    /*\n     * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object).\n     * Values in the state object may be modified by connected clients\n     */\n    void readFromJsonState(JsonObject& root)\n    {\n    }\n\n    /*\n     * addToConfig() can be used to add custom persistent settings to the cfg.json file in the \"um\" (usermod) object.\n     * It will be called by WLED when settings are actually saved (for example, LED settings are saved)\n     * If you want to force saving the current state, use serializeConfig() in your loop().\n     * \n     * CAUTION: serializeConfig() will initiate a filesystem write operation.\n     * It might cause the LEDs to stutter and will cause flash wear if called too often.\n     * Use it sparingly and always in the loop, never in network callbacks!\n     * \n     * addToConfig() will make your settings editable through the Usermod Settings page automatically.\n     *\n     * Usermod Settings Overview:\n     * - Numeric values are treated as floats in the browser.\n     *   - If the numeric value entered into the browser contains a decimal point, it will be parsed as a C float\n     *     before being returned to the Usermod.  The float data type has only 6-7 decimal digits of precision, and\n     *     doubles are not supported, numbers will be rounded to the nearest float value when being parsed.\n     *     The range accepted by the input field is +/- 1.175494351e-38 to +/- 3.402823466e+38.\n     *   - If the numeric value entered into the browser doesn't contain a decimal point, it will be parsed as a\n     *     C int32_t (range: -2147483648 to 2147483647) before being returned to the usermod.\n     *     Overflows or underflows are truncated to the max/min value for an int32_t, and again truncated to the type\n     *     used in the Usermod when reading the value from ArduinoJson.\n     * - Pin values can be treated differently from an integer value by using the key name \"pin\"\n     *   - \"pin\" can contain a single or array of integer values\n     *   - On the Usermod Settings page there is simple checking for pin conflicts and warnings for special pins\n     *     - Red color indicates a conflict.  Yellow color indicates a pin with a warning (e.g. an input-only pin)\n     *   - Tip: use int8_t to store the pin value in the Usermod, so a -1 value (pin not set) can be used\n     *\n     * See usermod_v2_auto_save.h for an example that saves Flash space by reusing ArduinoJson key name strings\n     * \n     * If you need a dedicated settings page with custom layout for your Usermod, that takes a lot more work.  \n     * You will have to add the setting to the HTML, xml.cpp and set.cpp manually.\n     * See the WLED Soundreactive fork (code and wiki) for reference.  https://github.com/atuline/WLED\n     * \n     * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings!\n     */\n    void addToConfig(JsonObject& root)\n    {\n      JsonObject top = root.createNestedObject(F(\"WordClockUsermod\"));\n      top[F(\"active\")] = usermodActive;\n      top[F(\"displayItIs\")] = displayItIs;\n      top[F(\"ledOffset\")] = ledOffset;\n      top[F(\"Meander wiring?\")] = meander;\n      top[F(\"Norddeutsch\")] = nord;\n    }\n\n    void appendConfigData()\n    {\n      oappend(F(\"addInfo('WordClockUsermod:ledOffset', 1, 'Number of LEDs before the letters');\"));\n      oappend(F(\"addInfo('WordClockUsermod:Norddeutsch', 1, 'Viertel vor instead of Dreiviertel');\"));\n    }\n\n    /*\n     * readFromConfig() can be used to read back the custom settings you added with addToConfig().\n     * This is called by WLED when settings are loaded (currently this only happens immediately after boot, or after saving on the Usermod Settings page)\n     * \n     * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes),\n     * 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.\n     * If you don't know what that is, don't fret. It most likely doesn't affect your use case :)\n     * \n     * Return true in case the config values returned from Usermod Settings were complete, or false if you'd like WLED to save your defaults to disk (so any missing values are editable in Usermod Settings)\n     * \n     * getJsonValue() returns false if the value is missing, or copies the value into the variable provided and returns true if the value is present\n     * The configComplete variable is true only if the \"exampleUsermod\" object and all values are present.  If any values are missing, WLED will know to call addToConfig() to save them\n     * \n     * This function is guaranteed to be called on boot, but could also be called every time settings are updated\n     */\n    bool readFromConfig(JsonObject& root)\n    {\n      // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor\n      // 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)\n\n      JsonObject top = root[F(\"WordClockUsermod\")];\n\n      bool configComplete = !top.isNull();\n\n      configComplete &= getJsonValue(top[F(\"active\")], usermodActive);\n      configComplete &= getJsonValue(top[F(\"displayItIs\")], displayItIs);\n      configComplete &= getJsonValue(top[F(\"ledOffset\")], ledOffset);\n      configComplete &= getJsonValue(top[F(\"Meander wiring?\")], meander);\n      configComplete &= getJsonValue(top[F(\"Norddeutsch\")], nord);\n\n      return configComplete;\n    }\n\n    /*\n     * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors.\n     * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode.\n     * Commonly used for custom clocks (Cronixie, 7 segment)\n     */\n    void handleOverlayDraw()\n    {\n      // check if usermod is active\n      if (usermodActive == true)\n      {\n        // loop over all leds\n        for (int x = 0; x < maskSizeLeds; x++)\n        {\n          // check mask\n          if (maskLedsOn[x] == 0)\n          {\n            // set pixel off\n            strip.setPixelColor(x + ledOffset, RGBW32(0,0,0,0));\n          }\n        }\n      }\n    }\n\n    /*\n     * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).\n     * This could be used in the future for the system to determine whether your usermod is installed.\n     */\n    uint16_t getId()\n    {\n      return USERMOD_ID_WORDCLOCK;\n    }\n\n   //More methods can be added in the future, this example will then be extended.\n   //Your usermod will remain compatible as it does not need to implement all methods from the Usermod base class!\n};\n\nstatic WordClockUsermod usermod_v2_word_clock;\nREGISTER_USERMOD(usermod_v2_word_clock);"
  },
  {
    "path": "usermods/wireguard/library.json",
    "content": "{\n  \"name\": \"wireguard\",\n  \"build\": { \"libArchive\": false},\n  \"dependencies\": {\n    \"WireGuard-ESP32-Arduino\":\"https://github.com/kienvu58/WireGuard-ESP32-Arduino.git\"\n  }\n}\n"
  },
  {
    "path": "usermods/wireguard/readme.md",
    "content": "# WireGuard VPN\n\nThis usermod will connect your WLED instance to a remote WireGuard subnet.\n\nConfiguration is performed via the Usermod menu. There are no parameters to set in code!\n\n## Installation \n\nCopy the `platformio_override.ini` file to the root project directory, review the build options, and select the `WLED_ESP32-WireGuard` environment.\n\n\n## Author\n\nAiden Vigue [vigue.me](https://vigue.me)\n[@acvigue](https://github.com/acvigue)\naiden@vigue.me\n\n\n\n"
  },
  {
    "path": "usermods/wireguard/wireguard.cpp",
    "content": "#include <WireGuard-ESP32.h>\n\n#include \"wled.h\"\n\nclass WireguardUsermod : public Usermod {\n   public:\n    void setup() { configTzTime(posix_tz, ntpServerName); }\n\n    void connected() {\n        if (wg.is_initialized()) {\n            wg.end();\n        }\n    }\n\n    void loop() {\n        if (millis() - lastTime > 5000) {\n            if (is_enabled && WLED_CONNECTED) {\n                if (!wg.is_initialized()) {\n                    struct tm timeinfo;\n                    if (getLocalTime(&timeinfo, 0)) {\n                        if (strlen(preshared_key) < 1) {\n                            wg.begin(local_ip, private_key, endpoint_address, public_key, endpoint_port, NULL);\n                        } else {\n                            wg.begin(local_ip, private_key, endpoint_address, public_key, endpoint_port, preshared_key);\n                        }\n                    }\n                }\n            }\n\n            lastTime = millis();\n        }\n    }\n\n    void addToJsonInfo(JsonObject& root) {\n        JsonObject user = root[\"u\"];\n        if (user.isNull()) user = root.createNestedObject(\"u\");\n\n        JsonArray infoArr = user.createNestedArray(F(\"WireGuard\"));\n        String uiDomString;\n\n        struct tm timeinfo;\n        if (!getLocalTime(&timeinfo, 0)) {\n            uiDomString = \"Time out of sync!\";\n        } else {\n            if (wg.is_initialized()) {\n                uiDomString = \"netif up!\";\n            } else {\n                uiDomString = \"netif down :(\";\n            }\n        }\n        if (is_enabled) infoArr.add(uiDomString);\n    }\n\n    void appendConfigData() {\n        oappend(F(\"addInfo('WireGuard:host',1,'Server Hostname');\"));           // 0 is field type, 1 is actual field\n        oappend(F(\"addInfo('WireGuard:port',1,'Server Port');\"));               // 0 is field type, 1 is actual field\n        oappend(F(\"addInfo('WireGuard:ip',1,'Device IP');\"));                   // 0 is field type, 1 is actual field\n        oappend(F(\"addInfo('WireGuard:psk',1,'Pre Shared Key (optional)');\"));  // 0 is field type, 1 is actual field\n        oappend(F(\"addInfo('WireGuard:pem',1,'Private Key');\"));                // 0 is field type, 1 is actual field\n        oappend(F(\"addInfo('WireGuard:pub',1,'Public Key');\"));                 // 0 is field type, 1 is actual field\n        oappend(F(\"addInfo('WireGuard:tz',1,'POSIX timezone string');\"));       // 0 is field type, 1 is actual field\n    }\n\n    void addToConfig(JsonObject& root) {\n        JsonObject top = root.createNestedObject(F(\"WireGuard\"));\n        top[F(\"host\")] = endpoint_address;\n        top[\"port\"] = endpoint_port;\n        top[\"ip\"] = local_ip.toString();\n        top[\"psk\"] = preshared_key;\n        top[F(\"pem\")] = private_key;\n        top[F(\"pub\")] = public_key;\n        top[F(\"tz\")] = posix_tz;\n    }\n\n    bool readFromConfig(JsonObject& root) {\n        JsonObject top = root[F(\"WireGuard\")];\n\n        if (top[F(\"host\")].isNull() || top[\"port\"].isNull() || top[\"ip\"].isNull() || top[F(\"pem\")].isNull() || top[F(\"pub\")].isNull() || top[F(\"tz\")].isNull()) {\n            is_enabled = false;\n            return false;\n        } else {\n            const char* host = top[F(\"host\")];\n            strncpy(endpoint_address, host, 100);\n\n            const char* ip_s = top[\"ip\"];\n            uint8_t ip[4];\n            sscanf(ip_s, \"%u.%u.%u.%u\", &ip[0], &ip[1], &ip[2], &ip[3]);\n            local_ip = IPAddress(ip[0], ip[1], ip[2], ip[3]);\n\n            const char* pem = top[F(\"pem\")];\n            strncpy(private_key, pem, 45);\n\n            const char* pub = top[F(\"pub\")];\n            strncpy(public_key, pub, 45);\n\n            const char* tz = top[F(\"tz\")];\n            strncpy(posix_tz, tz, 150);\n\n            endpoint_port = top[F(\"port\")];\n\n            if (!top[\"psk\"].isNull()) {\n                const char* psk = top[\"psk\"];\n                strncpy(preshared_key, psk, 45);\n            }\n\n            is_enabled = true;\n        }\n\n        return is_enabled;\n    }\n\n    uint16_t getId() { return USERMOD_ID_WIREGUARD; }\n\n   private:\n    WireGuard wg;\n    char preshared_key[45];\n    char private_key[45];\n    IPAddress local_ip;\n    char public_key[45];\n    char endpoint_address[100];\n    char posix_tz[150];\n    int endpoint_port = 0;\n    bool is_enabled = false;\n    unsigned long lastTime = 0;\n};\n\nstatic WireguardUsermod wireguard;\nREGISTER_USERMOD(wireguard);"
  },
  {
    "path": "usermods/wizlights/library.json",
    "content": "{\n  \"name\": \"wizlights\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/wizlights/readme.md",
    "content": "# Controlling Wiz lights\n\nEnables controlling [WiZ](https://www.wizconnected.com/en/consumer/) lights that are part of the same network as the WLED controller.\n\nThe mod takes the colors from the first few pixels and sends them to the lights.\n\n## Configuration\n\n- Interval (ms)\n    - How frequently to update the WiZ lights, in milliseconds.\n    - Setting it too low may cause the ESP to become unresponsive.\n- Send Delay (ms)\n    - An optional millisecond delay after updating each WiZ light. \n    - Can help smooth out effects when using a large number of WiZ lights\n- Use Enhanced White\n    - Uses the WiZ lights onboard white LEDs instead of sending maximum RGB values.\n    - Tunable with warm and cool LEDs as supported by WiZ bulbs\n    - Note: Only sent when max RGB value is set, the automatic brightness limiter must be disabled\n    - ToDo: Have better logic for white value mixing to take advantage of the light's capabilities\n- Always Force Update\n    - Can be enabled to always send update message to light even if the new value matches the old value.\n- Force update every x minutes\n    - adjusts the default force update timeout of 5 minutes.\n    - Setting to 0 is the same as enabling Always Force Update\n    - \nNext, enter the IP addresses for the lights to be controlled, in order. The limit is 15 devices, but that number\ncan be easily changed by updating _MAX_WIZ_LIGHTS_.\n\n\n\n\n## Related project\n\nIf you use these lights and python, make sure to check out the [pywizlight](https://github.com/sbidy/pywizlight) project. You can learn how to\nformat the messages to control the lights from that project.\n"
  },
  {
    "path": "usermods/wizlights/wizlights.cpp",
    "content": "#include \"wled.h\"\n#include <WiFiUdp.h>\n\n// Maximum number of lights supported\n#define MAX_WIZ_LIGHTS 15\n\nWiFiUDP UDP;\n\n\n\n\nclass WizLightsUsermod : public Usermod {\n  \n  private:\n    unsigned long lastTime = 0;\n    long updateInterval;\n    long sendDelay;\n    \n    long forceUpdateMinutes;\n    bool forceUpdate;\n\n    bool useEnhancedWhite;\n    long warmWhite;\n    long coldWhite;\n\n    IPAddress lightsIP[MAX_WIZ_LIGHTS];    // Stores Light IP addresses\n    bool      lightsValid[MAX_WIZ_LIGHTS]; // Stores Light IP address validity\n    uint32_t  colorsSent[MAX_WIZ_LIGHTS];  // Stores last color sent for each light\n\n\n\n  public:\n\n\n\n    // Send JSON blob to WiZ Light over UDP\n    // RGB or C/W white\n    // TODO:\n    //   Better utilize WLED existing white mixing logic\n    void wizSendColor(IPAddress ip, uint32_t color) {\n      UDP.beginPacket(ip, 38899);\n\n      // If no LED color, turn light off. Note wiz light setting for \"Off fade-out\" will be applied by the light itself.\n      if (color == 0) { \n        UDP.print(\"{\\\"method\\\":\\\"setPilot\\\",\\\"params\\\":{\\\"state\\\":false}}\");\n\n      // If color is WHITE, try and use the lights WHITE LEDs instead of mixing RGB LEDs\n      } else if (color == 16777215 && useEnhancedWhite){\n\n        // set cold white light only\n        if (coldWhite > 0 && warmWhite == 0){\n          UDP.print(\"{\\\"method\\\":\\\"setPilot\\\",\\\"params\\\":{\\\"c\\\":\"); UDP.print(coldWhite) ;UDP.print(\"}}\");}\n          \n        // set warm white light only\n        if (warmWhite > 0 && coldWhite == 0){\n          UDP.print(\"{\\\"method\\\":\\\"setPilot\\\",\\\"params\\\":{\\\"w\\\":\"); UDP.print(warmWhite) ;UDP.print(\"}}\");}\n          \n        // set combination of warm and cold white light\n        if (coldWhite > 0 && warmWhite > 0){\n          UDP.print(\"{\\\"method\\\":\\\"setPilot\\\",\\\"params\\\":{\\\"c\\\":\"); UDP.print(coldWhite) ;UDP.print(\",\\\"w\\\":\"); UDP.print(warmWhite); UDP.print(\"}}\");}\n\n      // Send color as RGB  \n      } else {\n        UDP.print(\"{\\\"method\\\":\\\"setPilot\\\",\\\"params\\\":{\\\"r\\\":\");\n        UDP.print(R(color));\n        UDP.print(\",\\\"g\\\":\");\n        UDP.print(G(color));\n        UDP.print(\",\\\"b\\\":\");\n        UDP.print(B(color));\n        UDP.print(\"}}\");\n      }\n    \n      UDP.endPacket();\n    }\n\n    // Override definition so it compiles\n    void setup() {\n      \n    }\n\n\n    // TODO: Check millis() rollover\n    void loop() {\n      \n      // Make sure we are connected first\n      if (!WLED_CONNECTED) return;\n      \n      unsigned long ellapsedTime = millis() - lastTime;\n      if (ellapsedTime > updateInterval) {\n        bool update = false;\n        for (uint8_t i = 0; i < MAX_WIZ_LIGHTS; i++) {\n          if (!lightsValid[i]) { continue; }\n          uint32_t newColor = strip.getPixelColor(i);\n          if (forceUpdate || (newColor != colorsSent[i]) || (ellapsedTime > forceUpdateMinutes*60000)){\n            wizSendColor(lightsIP[i], newColor);\n            colorsSent[i] = newColor;\n            update = true;\n            delay(sendDelay);\n            }\n          }\n        if (update) lastTime = millis();\n        }\n    }\n\n\n\n    void addToConfig(JsonObject& root)\n    {\n      JsonObject top = root.createNestedObject(\"wizLightsUsermod\");\n      top[\"Interval (ms)\"]                = updateInterval;\n      top[\"Send Delay (ms)\"]              = sendDelay;\n      top[\"Use Enhanced White *\"]         = useEnhancedWhite;\n      top[\"* Warm White Value (0-255)\"]   = warmWhite;\n      top[\"* Cold White Value (0-255)\"]   = coldWhite;\n      top[\"Always Force Update\"]          = forceUpdate;\n      top[\"Force Update Every x Minutes\"] = forceUpdateMinutes;\n      \n      for (uint8_t i = 0; i < MAX_WIZ_LIGHTS; i++) {\n        top[getJsonLabel(i)] = lightsIP[i].toString();\n      }\n    }\n\n\n\n    bool readFromConfig(JsonObject& root)\n    {\n      JsonObject top = root[\"wizLightsUsermod\"];\n      bool configComplete = !top.isNull();\n\n      configComplete &= getJsonValue(top[\"Interval (ms)\"],                updateInterval,     1000);  // How frequently to update the wiz lights\n      configComplete &= getJsonValue(top[\"Send Delay (ms)\"],              sendDelay,          0);     // Optional delay after sending each UDP message\n      configComplete &= getJsonValue(top[\"Use Enhanced White *\"],         useEnhancedWhite,   false); // When color is white use wiz white LEDs instead of mixing RGB\n      configComplete &= getJsonValue(top[\"* Warm White Value (0-255)\"],   warmWhite,          0);     // Warm White LED value for Enhanced White\n      configComplete &= getJsonValue(top[\"* Cold White Value (0-255)\"],   coldWhite,          50);   // Cold White LED value for Enhanced White\n      configComplete &= getJsonValue(top[\"Always Force Update\"],          forceUpdate,        false); // Update wiz light every loop, even if color value has not changed\n      configComplete &= getJsonValue(top[\"Force Update Every x Minutes\"], forceUpdateMinutes, 5);     // Update wiz light if color value has not changed, every x minutes\n      \n      // Read list of IPs\n      String tempIp;\n      for (uint8_t i = 0; i < MAX_WIZ_LIGHTS; i++) {\n        configComplete &= getJsonValue(top[getJsonLabel(i)], tempIp, \"0.0.0.0\");\n        lightsValid[i] = lightsIP[i].fromString(tempIp);\n        \n        // If the IP is not valid, force the value to be empty\n        if (!lightsValid[i]){lightsIP[i].fromString(\"0.0.0.0\");}\n        }\n\n      return configComplete;\n    }\n\n\n   // Create label for the usermod page (I cannot make it work with JSON arrays...)\n    String getJsonLabel(uint8_t i) {return \"WiZ Light IP #\" + String(i+1);}\n        \n    uint16_t getId(){return USERMOD_ID_WIZLIGHTS;}\n};\n\n\nstatic WizLightsUsermod wizlights;\nREGISTER_USERMOD(wizlights);"
  },
  {
    "path": "usermods/word-clock-matrix/library.json",
    "content": "{\n  \"name\": \"word-clock-matrix\",\n  \"build\": { \"libArchive\": false }\n}"
  },
  {
    "path": "usermods/word-clock-matrix/readme.md",
    "content": "## Word clock usermod\n\nBy @bwente\n\nSee https://www.hackster.io/bwente/word-clock-with-just-two-components-073834 for the hardware guide!<br/>\nIncludes a customizable feature to reduce the brightness at night.\n\n![image](https://user-images.githubusercontent.com/371964/197094071-f8ccaf59-1d85-4dd2-8e09-1389675291e1.png)\n\n\n![image](https://user-images.githubusercontent.com/371964/197094211-6c736257-95ff-491f-9f0d-35d5135ecfea.png)\n\n\n\n\n\n![mini_8x8_word_clock_reverse_stencil_sZFti6chj4(1)](https://user-images.githubusercontent.com/371964/197094410-7c275f3f-743b-477a-bc15-5e7bdbcbd833.svg)\n\n![mini_8x8_word_clock_box_epUWJOBOhr(1)](https://user-images.githubusercontent.com/371964/197094496-fa49b355-164b-4bf5-84fd-f22f5206c645.svg)\n"
  },
  {
    "path": "usermods/word-clock-matrix/word-clock-matrix.cpp",
    "content": "#include \"wled.h\"\n\n/*\n * Things to do...\n * Turn on ntp clock 24h format\n * 64 LEDS\n */\n\n\nclass WordClockMatrix : public Usermod\n{\nprivate:\n  unsigned long lastTime = 0;\n  uint8_t minuteLast = 99;\n  int dayBrightness = 128;\n  int nightBrightness = 16;\n\npublic:\n  void setup()\n  {\n    Serial.println(\"Hello from my usermod!\");\n\n    //saveMacro(14, \"A=128\", false);\n    //saveMacro(15, \"A=64\", false);\n    //saveMacro(16, \"A=16\", false);\n\n    //saveMacro(1, \"&FX=0&R=255&G=255&B=255\", false);\n\n    //strip.getSegment(1).setOption(SEG_OPTION_SELECTED, true);\n\n    //select first two segments (background color + FX settable)\n    Segment &seg = strip.getSegment(0);\n    seg.colors[0] = ((0 << 24) | ((0 & 0xFF) << 16) | ((0 & 0xFF) << 8) | ((0 & 0xFF)));\n    strip.getSegment(0).setOption(0, false);\n    strip.getSegment(0).setOption(2, false);\n    //other segments are text\n    for (int i = 1; i < 10; i++)\n    {\n      Segment &text_seg = strip.getSegment(i);\n      text_seg.colors[0] = ((0 << 24) | ((0 & 0xFF) << 16) | ((190 & 0xFF) << 8) | ((180 & 0xFF)));\n      strip.getSegment(i).setOption(0, true);\n      strip.setBrightness(64);\n    }\n  }\n\n  void connected()\n  {\n    Serial.println(\"Connected to WiFi!\");\n  }\n\n  void selectWordSegments(bool state)\n  {\n    for (int i = 1; i < 10; i++)\n    {\n      //WS2812FX::Segment &seg = strip.getSegment(i);\n      strip.getSegment(i).setOption(0, state);\n      // strip.getSegment(1).setOption(SEG_OPTION_SELECTED, true);\n      //seg.mode = 12;\n      //seg.palette = 1;\n      //strip.setBrightness(255);\n    }\n    strip.getSegment(0).setOption(0, !state);\n  }\n\n  void hourChime()\n  {\n    //strip.resetSegments();\n    selectWordSegments(true);\n    colorUpdated(CALL_MODE_FX_CHANGED);\n    savePreset(13);\n    selectWordSegments(false);\n    //strip.getSegment(0).setOption(0, true);\n    strip.getSegment(0).setOption(2, true);\n    applyPreset(12);\n    colorUpdated(CALL_MODE_FX_CHANGED);\n  }\n\n  void displayTime(byte hour, byte minute)\n  {\n    bool isToHour = false;      //true if minute > 30\n    strip.getSegment(0).setGeometry(0, 64); // background\n    strip.getSegment(1).setGeometry(0, 2);  //It is\n\n    strip.getSegment(2).setGeometry(0, 0);\n    strip.getSegment(3).setGeometry(0, 0); //disable minutes\n    strip.getSegment(4).setGeometry(0, 0); //past\n    strip.getSegment(6).setGeometry(0, 0); //to\n    strip.getSegment(8).setGeometry(0, 0); //disable o'clock\n\n    if (hour < 24) //valid time, display\n    {\n      if (minute == 30)\n      {\n        strip.getSegment(2).setGeometry(3, 6); //half\n        strip.getSegment(3).setGeometry(0, 0); //minutes\n      }\n      else if (minute == 15 || minute == 45)\n      {\n        strip.getSegment(3).setGeometry(0, 0); //minutes\n      }\n      else if (minute == 10)\n      {\n        //strip.getSegment(5).setGeometry(6, 8); //ten\n      }\n      else if (minute == 5)\n      {\n        //strip.getSegment(5).setGeometry(16, 18); //five\n      }\n      else if (minute == 0)\n      {\n        strip.getSegment(3).setGeometry(0, 0); //minutes\n        //hourChime();\n      }\n      else\n      {\n        strip.getSegment(3).setGeometry(18, 22); //minutes\n      }\n\n      //past or to?\n      if (minute == 0)\n      {                              //full hour\n        strip.getSegment(3).setGeometry(0, 0);   //disable minutes\n        strip.getSegment(4).setGeometry(0, 0);   //disable past\n        strip.getSegment(6).setGeometry(0, 0);   //disable to\n        strip.getSegment(8).setGeometry(60, 64); //o'clock\n      }\n      else if (minute > 34)\n      {\n        //strip.getSegment(6).setGeometry(22, 24); //to\n        //minute = 60 - minute;\n        isToHour = true;\n      }\n      else\n      {\n        //strip.getSegment(4).setGeometry(24, 27); //past\n        //isToHour = false;\n      }\n    }\n\n    //byte minuteRem = minute %10;\n\n    if (minute <= 4)\n    {\n      strip.getSegment(3).setGeometry(0, 0);   //nothing\n      strip.getSegment(5).setGeometry(0, 0);   //nothing\n      strip.getSegment(6).setGeometry(0, 0);   //nothing\n      strip.getSegment(8).setGeometry(60, 64); //o'clock\n    }\n    else if (minute <= 9)\n    {\n      strip.getSegment(5).setGeometry(16, 18); // five past\n      strip.getSegment(4).setGeometry(24, 27); //past\n    }\n    else if (minute <= 14)\n    {\n      strip.getSegment(5).setGeometry(6, 8);   // ten past\n      strip.getSegment(4).setGeometry(24, 27); //past\n    }\n    else if (minute <= 19)\n    {\n      strip.getSegment(5).setGeometry(8, 12);  // quarter past\n      strip.getSegment(3).setGeometry(0, 0);   //minutes\n      strip.getSegment(4).setGeometry(24, 27); //past\n    }\n    else if (minute <= 24)\n    {\n      strip.getSegment(5).setGeometry(12, 16); // twenty past\n      strip.getSegment(4).setGeometry(24, 27); //past\n    }\n    else if (minute <= 29)\n    {\n      strip.getSegment(5).setGeometry(12, 18); // twenty-five past\n      strip.getSegment(4).setGeometry(24, 27); //past\n    }\n    else if (minute <= 34)\n    {\n      strip.getSegment(5).setGeometry(3, 6);   // half past\n      strip.getSegment(3).setGeometry(0, 0);   //minutes\n      strip.getSegment(4).setGeometry(24, 27); //past\n    }\n    else if (minute <= 39)\n    {\n      strip.getSegment(5).setGeometry(12, 18); // twenty-five to\n      strip.getSegment(6).setGeometry(22, 24); //to\n    }\n    else if (minute <= 44)\n    {\n      strip.getSegment(5).setGeometry(12, 16); // twenty to\n      strip.getSegment(6).setGeometry(22, 24); //to\n    }\n    else if (minute <= 49)\n    {\n      strip.getSegment(5).setGeometry(8, 12);  // quarter to\n      strip.getSegment(3).setGeometry(0, 0);   //minutes\n      strip.getSegment(6).setGeometry(22, 24); //to\n    }\n    else if (minute <= 54)\n    {\n      strip.getSegment(5).setGeometry(6, 8);   // ten to\n      strip.getSegment(6).setGeometry(22, 24); //to\n    }\n    else if (minute <= 59)\n    {\n      strip.getSegment(5).setGeometry(16, 18); // five to\n      strip.getSegment(6).setGeometry(22, 24); //to\n    }\n\n    //hours\n    if (hour > 23)\n      return;\n    if (isToHour)\n      hour++;\n    if (hour > 12)\n      hour -= 12;\n    if (hour == 0)\n      hour = 12;\n\n    switch (hour)\n    {\n    case 1:\n      strip.getSegment(7).setGeometry(27, 29);\n      break; //one\n    case 2:\n      strip.getSegment(7).setGeometry(35, 37);\n      break; //two\n    case 3:\n      strip.getSegment(7).setGeometry(29, 32);\n      break; //three\n    case 4:\n      strip.getSegment(7).setGeometry(32, 35);\n      break; //four\n    case 5:\n      strip.getSegment(7).setGeometry(37, 40);\n      break; //five\n    case 6:\n      strip.getSegment(7).setGeometry(43, 45);\n      break; //six\n    case 7:\n      strip.getSegment(7).setGeometry(40, 43);\n      break; //seven\n    case 8:\n      strip.getSegment(7).setGeometry(45, 48);\n      break; //eight\n    case 9:\n      strip.getSegment(7).setGeometry(48, 50);\n      break; //nine\n    case 10:\n      strip.getSegment(7).setGeometry(54, 56);\n      break; //ten\n    case 11:\n      strip.getSegment(7).setGeometry(50, 54);\n      break; //eleven\n    case 12:\n      strip.getSegment(7).setGeometry(56, 60);\n      break; //twelve\n    }\n\n    selectWordSegments(true);\n    applyPreset(1);\n  }\n\n  void timeOfDay()\n  {\n    // NOT USED: use timed macros instead\n    //Used to set brightness dependant of time of day - lights dimmed at night\n\n    //monday to thursday and sunday\n\n    if ((weekday(localTime) == 6) | (weekday(localTime) == 7))\n    {\n      if ((hour(localTime) > 0) | (hour(localTime) < 8))\n      {\n        strip.setBrightness(nightBrightness);\n      }\n      else\n      {\n        strip.setBrightness(dayBrightness);\n      }\n    }\n    else\n    {\n      if ((hour(localTime) < 6) | (hour(localTime) >= 22))\n      {\n        strip.setBrightness(nightBrightness);\n      }\n      else\n      {\n        strip.setBrightness(dayBrightness);\n      }\n    }\n  }\n\n  //loop. You can use \"if (WLED_CONNECTED)\" to check for successful connection\n  void loop()\n  {\n\n      if (millis() - lastTime > 1000) {\n        //Serial.println(\"I'm alive!\");\n        Serial.println(hour(localTime));\n        lastTime = millis();\n      }\n\n\n    if (minute(localTime) != minuteLast)\n    {\n      updateLocalTime();\n      //timeOfDay();\n      minuteLast = minute(localTime);\n      displayTime(hour(localTime), minute(localTime));\n      if (minute(localTime) == 0)\n      {\n        hourChime();\n      }\n      if (minute(localTime) == 1)\n      {\n        //turn off background segment;\n        strip.getSegment(0).setOption(2, false);\n        //applyPreset(13);\n      }\n    }\n  }\n\n    void addToConfig(JsonObject& root)\n    {\n      JsonObject modName = root.createNestedObject(\"id\");\n      modName[F(\"mdns\")] = \"wled-word-clock\";\n      modName[F(\"name\")] = \"WLED WORD CLOCK\";\n    }\n\n    uint16_t getId()\n    {\n      return 500;\n    }\n\n\n};\n\n\nstatic WordClockMatrix word_clock_matrix;\nREGISTER_USERMOD(word_clock_matrix);"
  },
  {
    "path": "wled00/FX.cpp",
    "content": "/*\n  WS2812FX.cpp contains all effect methods\n  Harm Aldick - 2016\n  www.aldick.org\n\n  Copyright (c) 2016  Harm Aldick\n  Licensed under the EUPL v. 1.2 or later\n  Adapted from code originally licensed under the MIT license\n\n  Modified heavily for WLED\n*/\n\n#include \"wled.h\"\n#include \"FX.h\"\n#include \"fcn_declare.h\"\n\n#define FX_FALLBACK_STATIC { mode_static(); return; }\n\n#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D))\n  #include \"FXparticleSystem.h\" // include particle system code only if at least one system is enabled\n  #ifdef WLED_DISABLE_PARTICLESYSTEM2D\n    #define WLED_PS_DONT_REPLACE_2D_FX\n  #endif\n  #ifdef WLED_DISABLE_PARTICLESYSTEM1D\n    #define WLED_PS_DONT_REPLACE_1D_FX\n  #endif\n  #ifdef ESP8266\n    #if !defined(WLED_DISABLE_PARTICLESYSTEM2D) && !defined(WLED_DISABLE_PARTICLESYSTEM1D)\n      #error ESP8266 does not support 1D and 2D particle systems simultaneously. Please disable one of them.\n    #endif\n  #endif\n#else\n  #define WLED_PS_DONT_REPLACE_1D_FX\n  #define WLED_PS_DONT_REPLACE_2D_FX\n#endif\n#ifdef WLED_PS_DONT_REPLACE_FX\n  #define WLED_PS_DONT_REPLACE_1D_FX\n  #define WLED_PS_DONT_REPLACE_2D_FX\n#endif\n\n //////////////\n // DEV INFO //\n //////////////\n/*\n  information for FX metadata strings: https://kno.wled.ge/interfaces/json-api/#effect-metadata\n\n  Audio Reactive: use the following code to pass usermod variables to effect\n\n  uint8_t  *binNum = (uint8_t*)&SEGENV.aux1, *maxVol = (uint8_t*)(&SEGENV.aux1+1); // just in case assignment\n  bool      samplePeak = false;\n  float     FFT_MajorPeak = 1.0;\n  uint8_t  *fftResult = nullptr;\n  float    *fftBin = nullptr;\n  um_data_t *um_data = getAudioData();\n  volumeSmth    = *(float*)   um_data->u_data[0];\n  volumeRaw     = *(float*)   um_data->u_data[1];\n  fftResult     =  (uint8_t*) um_data->u_data[2];\n  samplePeak    = *(uint8_t*) um_data->u_data[3];\n  FFT_MajorPeak = *(float*)   um_data->u_data[4];\n  my_magnitude  = *(float*)   um_data->u_data[5];\n  maxVol        =  (uint8_t*) um_data->u_data[6];  // requires UI element (SEGMENT.customX?), changes source element\n  binNum        =  (uint8_t*) um_data->u_data[7];  // requires UI element (SEGMENT.customX?), changes source element\n  fftBin        =  (float*)   um_data->u_data[8];\n*/\n\n#define IBN 5100\n// paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined)\n#define PALETTE_SOLID_WRAP   (paletteBlend == 1 || paletteBlend == 3)\n#define PALETTE_MOVING_WRAP !(paletteBlend == 2 || (paletteBlend == 0 && SEGMENT.speed == 0))\n\n#define indexToVStrip(index, stripNr) ((index) | (int((stripNr)+1)<<16))\n\n// a few constants needed for AudioReactive effects\n// for 22Khz sampling\n#define MAX_FREQUENCY   11025    // sample frequency / 2 (as per Nyquist criterion)\n#define MAX_FREQ_LOG10  4.04238f // log10(MAX_FREQUENCY)\n// for 20Khz sampling\n//#define MAX_FREQUENCY   10240\n//#define MAX_FREQ_LOG10  4.0103f\n// for 10Khz sampling\n//#define MAX_FREQUENCY   5120\n//#define MAX_FREQ_LOG10  3.71f\n\n// effect utility functions\nstatic uint8_t sin_gap(uint16_t in) {\n  if (in & 0x100) return 0;\n  return sin8_t(in + 192); // correct phase shift of sine so that it starts and stops at 0\n}\n\nstatic uint16_t triwave16(uint16_t in) {\n  if (in < 0x8000) return in *2;\n  return 0xFFFF - (in - 0x8000)*2;\n}\n\n/*\n * Generates a tristate square wave w/ attac & decay\n * @param x input value 0-255\n * @param pulsewidth 0-127\n * @param attdec attack & decay, max. pulsewidth / 2\n * @returns signed waveform value\n */\nstatic int8_t tristate_square8(uint8_t x, uint8_t pulsewidth, uint8_t attdec) {\n  int8_t a = 127;\n  if (x > 127) {\n    a = -127;\n    x -= 127;\n  }\n\n  if (x < attdec) { //inc to max\n    return (int16_t) x * a / attdec;\n  }\n  else if (x < pulsewidth - attdec) { //max\n    return a;\n  }\n  else if (x < pulsewidth) { //dec to 0\n    return (int16_t) (pulsewidth - x) * a / attdec;\n  }\n  return 0;\n}\n\nstatic um_data_t* getAudioData() {\n  um_data_t *um_data;\n  if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) {\n    // add support for no audio\n    um_data = simulateSound(SEGMENT.soundSim);\n  }\n  return um_data;\n}\n\n\n// effect functions\n\n/*\n * No blinking. Just plain old static light.\n */\nvoid mode_static(void) {\n  SEGMENT.fill(SEGCOLOR(0));\n}\nstatic const char _data_FX_MODE_STATIC[] PROGMEM = \"Solid\";\n\n/*\n * Copy a segment and perform (optional) color adjustments\n */\nvoid mode_copy_segment(void) {\n  uint32_t sourceid = SEGMENT.custom3;\n  if (sourceid >= strip.getSegmentsNum() || sourceid == strip.getCurrSegmentId()) { // invalid source\n    SEGMENT.fadeToBlackBy(5); // fade out\n  }\n  Segment& sourcesegment = strip.getSegment(sourceid);\n\n  if (sourcesegment.isActive()) {\n    uint32_t sourcecolor;\n    uint32_t destcolor;\n    if(sourcesegment.is2D()) { // 2D source, note: 2D to 1D just copies the first row (or first column if 'Switch axis' is checked in FX)\n      for (unsigned y = 0; y < SEGMENT.vHeight(); y++) {\n        for (unsigned x = 0; x < SEGMENT.vWidth(); x++) {\n          unsigned sx = x; // source coordinates\n          unsigned sy = y;\n          if(SEGMENT.check1) std::swap(sx, sy); // flip axis\n          if(SEGMENT.check2) {\n            sourcecolor = strip.getPixelColorXY(sx + sourcesegment.start, sy + sourcesegment.startY); // read from global buffer (reads the last rendered frame)\n          }\n          else {\n            sourcesegment.setDrawDimensions(); // set to source segment dimensions\n            sourcecolor = sourcesegment.getPixelColorXY(sx, sy); // read from segment buffer\n          }\n          destcolor = adjust_color(sourcecolor, SEGMENT.intensity, SEGMENT.custom1, SEGMENT.custom2);\n          SEGMENT.setDrawDimensions(); // reset to current segment dimensions\n          SEGMENT.setPixelColorXY(x, y, destcolor);\n        }\n      }\n    } else { // 1D source, source can be expanded into 2D\n      for (unsigned i = 0; i < SEGMENT.vLength(); i++) {\n        if(SEGMENT.check2) {\n          sourcecolor = strip.getPixelColorNoMap(i + sourcesegment.start); // read from global buffer (reads the last rendered frame)\n        }\n        else {\n          sourcesegment.setDrawDimensions(); // set to source segment dimensions\n          sourcecolor = sourcesegment.getPixelColor(i);\n        }\n        destcolor = adjust_color(sourcecolor, SEGMENT.intensity, SEGMENT.custom1, SEGMENT.custom2);\n        SEGMENT.setDrawDimensions(); // reset to current segment dimensions\n        SEGMENT.setPixelColor(i, destcolor);\n      }\n    }\n  }\n}\nstatic 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\";\n\n\n/*\n * Blink/strobe function\n * Alternate between color1 and color2\n * if(strobe == true) then create a strobe effect\n */\nvoid blink(uint32_t color1, uint32_t color2, bool strobe, bool do_palette) {\n  uint32_t cycleTime = (255 - SEGMENT.speed)*20;\n  uint32_t onTime = FRAMETIME;\n  if (!strobe) onTime += ((cycleTime * SEGMENT.intensity) >> 8);\n  cycleTime += FRAMETIME*2;\n  uint32_t it = strip.now / cycleTime;\n  uint32_t rem = strip.now % cycleTime;\n\n  bool on = false;\n  if (it != SEGENV.step //new iteration, force on state for one frame, even if set time is too brief\n      || rem <= onTime) {\n    on = true;\n  }\n\n  SEGENV.step = it; //save previous iteration\n\n  uint32_t color = on ? color1 : color2;\n  if (color == color1 && do_palette)\n  {\n    for (unsigned i = 0; i < SEGLEN; i++) {\n      SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0));\n    }\n  } else SEGMENT.fill(color);\n}\n\n\n/*\n * Normal blinking. Intensity sets duty cycle.\n */\nvoid mode_blink(void) {\n  blink(SEGCOLOR(0), SEGCOLOR(1), false, true);\n}\nstatic const char _data_FX_MODE_BLINK[] PROGMEM = \"Blink@!,Duty cycle;!,!;!;01\";\n\n\n/*\n * Classic Blink effect. Cycling through the rainbow.\n */\nvoid mode_blink_rainbow(void) {\n  blink(SEGMENT.color_wheel(SEGENV.call & 0xFF), SEGCOLOR(1), false, false);\n}\nstatic const char _data_FX_MODE_BLINK_RAINBOW[] PROGMEM = \"Blink Rainbow@Frequency,Blink duration;!,!;!;01\";\n\n\n/*\n * Classic Strobe effect.\n */\nvoid mode_strobe(void) {\n  return blink(SEGCOLOR(0), SEGCOLOR(1), true, true);\n}\nstatic const char _data_FX_MODE_STROBE[] PROGMEM = \"Strobe@!;!,!;!;01\";\n\n\n/*\n * Classic Strobe effect. Cycling through the rainbow.\n */\nvoid mode_strobe_rainbow(void) {\n  return blink(SEGMENT.color_wheel(SEGENV.call & 0xFF), SEGCOLOR(1), true, false);\n}\nstatic const char _data_FX_MODE_STROBE_RAINBOW[] PROGMEM = \"Strobe Rainbow@!;,!;!;01\";\n\n\n/*\n * Color wipe function\n * LEDs are turned on (color1) in sequence, then turned off (color2) in sequence.\n * if (bool rev == true) then LEDs are turned off in reverse order\n */\nvoid color_wipe(bool rev, bool useRandomColors) {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  uint32_t cycleTime = 750 + (255 - SEGMENT.speed)*150;\n  uint32_t perc = strip.now % cycleTime;\n  unsigned prog = (perc * 65535) / cycleTime;\n  bool back = (prog > 32767);\n  if (back) {\n    prog -= 32767;\n    if (SEGENV.step == 0) SEGENV.step = 1;\n  } else {\n    if (SEGENV.step == 2) SEGENV.step = 3; //trigger color change\n  }\n\n  if (useRandomColors) {\n    if (SEGENV.call == 0) {\n      SEGENV.aux0 = hw_random8();\n      SEGENV.step = 3;\n    }\n    if (SEGENV.step == 1) { //if flag set, change to new random color\n      SEGENV.aux1 = get_random_wheel_index(SEGENV.aux0);\n      SEGENV.step = 2;\n    }\n    if (SEGENV.step == 3) {\n      SEGENV.aux0 = get_random_wheel_index(SEGENV.aux1);\n      SEGENV.step = 0;\n    }\n  }\n\n  unsigned ledIndex = (prog * SEGLEN) >> 15;\n  uint16_t rem = (prog * SEGLEN) * 2; //mod 0xFFFF by truncating\n  rem /= (SEGMENT.intensity +1);\n  if (rem > 255) rem = 255;\n\n  uint32_t col1 = useRandomColors? SEGMENT.color_wheel(SEGENV.aux1) : SEGCOLOR(1);\n  for (unsigned i = 0; i < SEGLEN; i++)\n  {\n    unsigned index = (rev && back)? SEGLEN -1 -i : i;\n    uint32_t col0 = useRandomColors? SEGMENT.color_wheel(SEGENV.aux0) : SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0);\n\n    if (i < ledIndex)\n    {\n      SEGMENT.setPixelColor(index, back? col1 : col0);\n    } else\n    {\n      SEGMENT.setPixelColor(index, back? col0 : col1);\n      if (i == ledIndex) SEGMENT.setPixelColor(index, color_blend(back? col0 : col1, back? col1 : col0, uint8_t(rem)));\n    }\n  }\n}\n\n\n/*\n * Lights all LEDs one after another.\n */\nvoid mode_color_wipe(void) {\n  color_wipe(false, false);\n}\nstatic const char _data_FX_MODE_COLOR_WIPE[] PROGMEM = \"Wipe@!,!;!,!;!\";\n\n\n/*\n * Lights all LEDs one after another. Turns off opposite\n */\nvoid mode_color_sweep(void) {\n  color_wipe(true, false);\n}\nstatic const char _data_FX_MODE_COLOR_SWEEP[] PROGMEM = \"Sweep@!,!;!,!;!\";\n\n\n/*\n * Turns all LEDs after each other to a random color.\n * Then starts over with another color.\n */\nvoid mode_color_wipe_random(void) {\n  color_wipe(false, true);\n}\nstatic const char _data_FX_MODE_COLOR_WIPE_RANDOM[] PROGMEM = \"Wipe Random@!;;!\";\n\n\n/*\n * Random color introduced alternating from start and end of strip.\n */\nvoid mode_color_sweep_random(void) {\n  color_wipe(true, true);\n}\nstatic const char _data_FX_MODE_COLOR_SWEEP_RANDOM[] PROGMEM = \"Sweep Random@!;;!\";\n\n\n/*\n * Lights all LEDs up in one random color. Then switches them\n * to the next random color.\n */\nvoid mode_random_color(void) {\n  uint32_t cycleTime = 200 + (255 - SEGMENT.speed)*50;\n  uint32_t it = strip.now / cycleTime;\n  uint32_t rem = strip.now % cycleTime;\n  unsigned fadedur = (cycleTime * SEGMENT.intensity) >> 8;\n\n  uint32_t fade = 255;\n  if (fadedur) {\n    fade = (rem * 255) / fadedur;\n    if (fade > 255) fade = 255;\n  }\n\n  if (SEGENV.call == 0) {\n    SEGENV.aux0 = hw_random8();\n    SEGENV.step = 2;\n  }\n  if (it != SEGENV.step) //new color\n  {\n    SEGENV.aux1 = SEGENV.aux0;\n    SEGENV.aux0 = get_random_wheel_index(SEGENV.aux0); //aux0 will store our random color wheel index\n    SEGENV.step = it;\n  }\n\n  SEGMENT.fill(color_blend(SEGMENT.color_wheel(SEGENV.aux1), SEGMENT.color_wheel(SEGENV.aux0), uint8_t(fade)));\n}\nstatic const char _data_FX_MODE_RANDOM_COLOR[] PROGMEM = \"Random Colors@!,Fade time;;!;01\";\n\n\n/*\n * Lights every LED in a random color. Changes all LED at the same time\n * to new random colors.\n */\nvoid mode_dynamic(void) {\n  if (!SEGENV.allocateData(SEGLEN)) FX_FALLBACK_STATIC; //allocation failed\n\n  if(SEGENV.call == 0) {\n    //SEGMENT.fill(BLACK);\n    for (unsigned i = 0; i < SEGLEN; i++) SEGENV.data[i] = hw_random8();\n  }\n\n  uint32_t cycleTime = 50 + (255 - SEGMENT.speed)*15;\n  uint32_t it = strip.now / cycleTime;\n  if (it != SEGENV.step && SEGMENT.speed != 0) //new color\n  {\n    for (unsigned i = 0; i < SEGLEN; i++) {\n      if (hw_random8() <= SEGMENT.intensity) SEGENV.data[i] = hw_random8(); // random color index\n    }\n    SEGENV.step = it;\n  }\n\n  if (SEGMENT.check1) {\n    for (unsigned i = 0; i < SEGLEN; i++) {\n      SEGMENT.blendPixelColor(i, SEGMENT.color_wheel(SEGENV.data[i]), 16);\n    }\n  } else {\n    for (unsigned i = 0; i < SEGLEN; i++) {\n      SEGMENT.setPixelColor(i, SEGMENT.color_wheel(SEGENV.data[i]));\n    }\n  }\n}\nstatic const char _data_FX_MODE_DYNAMIC[] PROGMEM = \"Dynamic@!,!,,,,Smooth;;!\";\n\n\n/*\n * effect \"Dynamic\" with smooth color-fading\n */\nvoid mode_dynamic_smooth(void) {\n  bool old = SEGMENT.check1;\n  SEGMENT.check1 = true;\n  mode_dynamic();\n  SEGMENT.check1 = old;\n }\nstatic const char _data_FX_MODE_DYNAMIC_SMOOTH[] PROGMEM = \"Dynamic Smooth@!,!;;!\";\n\n\n/*\n * Does the \"standby-breathing\" of well known i-Devices.\n */\nvoid mode_breath(void) {\n  unsigned var = 0;\n  unsigned counter = (strip.now * ((SEGMENT.speed >> 3) +10)) & 0xFFFFU;\n  counter = (counter >> 2) + (counter >> 4); //0-16384 + 0-2048\n  if (counter < 16384) {\n    if (counter > 8192) counter = 8192 - (counter - 8192);\n    var = sin16_t(counter) / 103; //close to parabolic in range 0-8192, max val. 23170\n  }\n\n  uint8_t lum = 30 + var;\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), lum));\n  }\n\n}\nstatic const char _data_FX_MODE_BREATH[] PROGMEM = \"Breathe@!;!,!;!;01\";\n\n\n/*\n * Fades the LEDs between two colors\n */\nvoid mode_fade(void) {\n  unsigned counter = (strip.now * ((SEGMENT.speed >> 3) +10));\n  uint8_t lum = triwave16(counter) >> 8;\n\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), lum));\n  }\n}\nstatic const char _data_FX_MODE_FADE[] PROGMEM = \"Fade@!;!,!;!;01\";\n\n\n/*\n * Scan mode parent function\n */\nvoid scan(bool dual) {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  uint32_t cycleTime = 750 + (255 - SEGMENT.speed)*150;\n  uint32_t perc = strip.now % cycleTime;\n  int prog = (perc * 65535) / cycleTime;\n  int size = 1 + ((SEGMENT.intensity * SEGLEN) >> 9);\n  int ledIndex = (prog * ((SEGLEN *2) - size *2)) >> 16;\n\n  if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1));\n\n  int led_offset = ledIndex - (SEGLEN - size);\n  led_offset = abs(led_offset);\n\n  if (dual) {\n    for (int j = led_offset; j < led_offset + size; j++) {\n      unsigned i2 = SEGLEN -1 -j;\n      SEGMENT.setPixelColor(i2, SEGMENT.color_from_palette(i2, true, PALETTE_SOLID_WRAP, (SEGCOLOR(2))? 2:0));\n    }\n  }\n\n  for (int j = led_offset; j < led_offset + size; j++) {\n    SEGMENT.setPixelColor(j, SEGMENT.color_from_palette(j, true, PALETTE_SOLID_WRAP, 0));\n  }\n}\n\n\n/*\n * Runs a single pixel back and forth.\n */\nvoid mode_scan(void) {\n  scan(false);\n}\nstatic const char _data_FX_MODE_SCAN[] PROGMEM = \"Scan@!,# of dots,,,,,Overlay;!,!,!;!\";\n\n\n/*\n * Runs two pixel back and forth in opposite directions.\n */\nvoid mode_dual_scan(void) {\n  scan(true);\n}\nstatic const char _data_FX_MODE_DUAL_SCAN[] PROGMEM = \"Scan Dual@!,# of dots,,,,,Overlay;!,!,!;!\";\n\n\n/*\n * Cycles all LEDs at once through a rainbow.\n */\nvoid mode_rainbow(void) {\n  unsigned counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF;\n  counter = counter >> 8;\n\n  if (SEGMENT.intensity < 128){\n    SEGMENT.fill(color_blend(SEGMENT.color_wheel(counter),WHITE,uint8_t(128-SEGMENT.intensity)));\n  } else {\n    SEGMENT.fill(SEGMENT.color_wheel(counter));\n  }\n}\nstatic const char _data_FX_MODE_RAINBOW[] PROGMEM = \"Colorloop@!,Saturation;;!;01\";\n\n\n/*\n * Cycles a rainbow over the entire string of LEDs.\n */\nvoid mode_rainbow_cycle(void) {\n  unsigned counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF;\n  counter = counter >> 8;\n\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    //intensity/29 = 0 (1/16) 1 (1/8) 2 (1/4) 3 (1/2) 4 (1) 5 (2) 6 (4) 7 (8) 8 (16)\n    uint8_t index = (i * (16 << (SEGMENT.intensity /29)) / SEGLEN) + counter;\n    SEGMENT.setPixelColor(i, SEGMENT.color_wheel(index));\n  }\n}\nstatic const char _data_FX_MODE_RAINBOW_CYCLE[] PROGMEM = \"Rainbow@!,Size;;!\";\n\n\n/*\n * Alternating pixels running function.\n */\nstatic void running(uint32_t color1, uint32_t color2, bool theatre = false) {\n  int width = (theatre ? 3 : 1) + (SEGMENT.intensity >> 4);  // window\n  uint32_t cycleTime = 50 + (255 - SEGMENT.speed);\n  uint32_t it = strip.now / cycleTime;\n  bool usePalette = color1 == SEGCOLOR(0);\n\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    uint32_t col = color2;\n    if (usePalette) color1 = SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0);\n    if (theatre) {\n      if ((i % width) == SEGENV.aux0) col = color1;\n    } else {\n      int pos = (i % (width<<1));\n      if ((pos < SEGENV.aux0-width) || ((pos >= SEGENV.aux0) && (pos < SEGENV.aux0+width))) col = color1;\n    }\n    SEGMENT.setPixelColor(i,col);\n  }\n\n  if (it != SEGENV.step) {\n    SEGENV.aux0 = (SEGENV.aux0 +1) % (theatre ? width : (width<<1));\n    SEGENV.step = it;\n  }\n}\n\n\n/*\n * Theatre-style crawling lights.\n * Inspired by the Adafruit examples.\n */\nvoid mode_theater_chase(void) {\n  running(SEGCOLOR(0), SEGCOLOR(1), true);\n}\nstatic const char _data_FX_MODE_THEATER_CHASE[] PROGMEM = \"Theater@!,Gap size;!,!;!\";\n\n\n/*\n * Theatre-style crawling lights with rainbow effect.\n * Inspired by the Adafruit examples.\n */\nvoid mode_theater_chase_rainbow(void) {\n  running(SEGMENT.color_wheel(SEGENV.step), SEGCOLOR(1), true);\n}\nstatic const char _data_FX_MODE_THEATER_CHASE_RAINBOW[] PROGMEM = \"Theater Rainbow@!,Gap size;,!;!\";\n\n\n/*\n * Running lights effect with smooth sine transition base.\n */\nstatic void running_base(bool saw, bool dual=false) {\n  unsigned x_scale = SEGMENT.intensity >> 2;\n  uint32_t counter = (strip.now * SEGMENT.speed) >> 9;\n\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    unsigned a = i*x_scale - counter;\n    if (saw) {\n      a &= 0xFF;\n      if (a < 16)\n      {\n        a = 192 + a*8;\n      } else {\n        a = map(a,16,255,64,192);\n      }\n      a = 255 - a;\n    }\n    uint8_t s = dual ? sin_gap(a) : sin8_t(a);\n    uint32_t ca = color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), s);\n    if (dual) {\n      unsigned b = (SEGLEN-1-i)*x_scale - counter;\n      uint8_t t = sin_gap(b);\n      uint32_t cb = color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 2), t);\n      ca = color_blend(ca, cb, uint8_t(127));\n    }\n    SEGMENT.setPixelColor(i, ca);\n  }\n}\n\n\n/*\n * Running lights in opposite directions.\n * Idea: Make the gap width controllable with a third slider in the future\n */\nvoid mode_running_dual(void) {\n  running_base(false, true);\n}\nstatic const char _data_FX_MODE_RUNNING_DUAL[] PROGMEM = \"Running Dual@!,Wave width;L,!,R;!\";\n\n\n/*\n * Running lights effect with smooth sine transition.\n */\nvoid mode_running_lights(void) {\n  running_base(false);\n}\nstatic const char _data_FX_MODE_RUNNING_LIGHTS[] PROGMEM = \"Running@!,Wave width;!,!;!\";\n\n\n/*\n * Running lights effect with sawtooth transition.\n */\nvoid mode_saw(void) {\n  running_base(true);\n}\nstatic const char _data_FX_MODE_SAW[] PROGMEM = \"Saw@!,Width;!,!;!\";\n\n\n/*\n * Blink several LEDs in random colors on, reset, repeat.\n * Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/\n */\nvoid mode_twinkle(void) {\n  SEGMENT.fade_out(224);\n\n  uint32_t cycleTime = 20 + (255 - SEGMENT.speed)*5;\n  uint32_t it = strip.now / cycleTime;\n  if (it != SEGENV.step)\n  {\n    unsigned maxOn = map(SEGMENT.intensity, 0, 255, 1, SEGLEN); // make sure at least one LED is on\n    if (SEGENV.aux0 >= maxOn)\n    {\n      SEGENV.aux0 = 0;\n      SEGENV.aux1 = hw_random(); //new seed for our PRNG\n    }\n    SEGENV.aux0++;\n    SEGENV.step = it;\n  }\n\n  uint16_t PRNG16 = SEGENV.aux1;\n\n  for (unsigned i = 0; i < SEGENV.aux0; i++)\n  {\n    PRNG16 = (uint16_t)(PRNG16 * 2053) + 13849; // next 'random' number\n    uint32_t p = (uint32_t)SEGLEN * (uint32_t)PRNG16;\n    unsigned j = p >> 16;\n    SEGMENT.setPixelColor(j, SEGMENT.color_from_palette(j, true, PALETTE_SOLID_WRAP, 0));\n  }\n}\nstatic const char _data_FX_MODE_TWINKLE[] PROGMEM = \"Twinkle@!,!;!,!;!;;m12=0\"; //pixels\n\n\n/*\n * Dissolve function\n */\nvoid dissolve(uint32_t color) {\n  unsigned dataSize = sizeof(uint32_t) * SEGLEN;\n  if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed\n  uint32_t* pixels = reinterpret_cast<uint32_t*>(SEGENV.data);\n\n  if (SEGENV.call == 0) {\n    for (unsigned i = 0; i < SEGLEN; i++) pixels[i] = SEGCOLOR(1);\n    SEGENV.aux0 = 1;\n  }\n\n  for (unsigned j = 0; j <= SEGLEN / 15; j++) {\n    if (hw_random8() <= SEGMENT.intensity) {\n      for (size_t times = 0; times < 10; times++) { //attempt to spawn a new pixel 10 times\n        unsigned i = hw_random16(SEGLEN);\n        if (SEGENV.aux0) { //dissolve to primary/palette\n          if (pixels[i] == SEGCOLOR(1)) {\n            pixels[i] = color == SEGCOLOR(0) ? SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0) : color;\n            break; //only spawn 1 new pixel per frame\n          }\n        } else { //dissolve to secondary\n          if (pixels[i] != SEGCOLOR(1)) {\n            pixels[i] = SEGCOLOR(1);\n            break;\n          }\n        }\n      }\n    }\n  }\n  unsigned incompletePixels = 0;\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    SEGMENT.setPixelColor(i, pixels[i]); // fix for #4401\n    if (SEGMENT.check2) {\n      if (SEGENV.aux0) {\n        if (pixels[i] == SEGCOLOR(1)) incompletePixels++;\n      } else {\n        if (pixels[i] != SEGCOLOR(1)) incompletePixels++;\n      }\n    }\n  }\n\n  if (SEGENV.step > (255 - SEGMENT.speed) + 15U) {\n    SEGENV.aux0 = !SEGENV.aux0;\n    SEGENV.step = 0;\n  } else {\n    if (SEGMENT.check2) {\n      if (incompletePixels == 0)\n        SEGENV.step++; // only advance step once all pixels have changed\n    } else\n      SEGENV.step++;\n  }\n}\n\n\n/*\n * Blink several LEDs on and then off\n */\nvoid mode_dissolve(void) {\n  dissolve(SEGMENT.check1 ? SEGMENT.color_wheel(hw_random8()) : SEGCOLOR(0));\n}\nstatic const char _data_FX_MODE_DISSOLVE[] PROGMEM = \"Dissolve@Repeat speed,Dissolve speed,,,,Random,Complete;!,!;!\";\n\n\n/*\n * Blink several LEDs on and then off in random colors\n */\nvoid mode_dissolve_random(void) {\n  dissolve(SEGMENT.color_wheel(hw_random8()));\n}\nstatic const char _data_FX_MODE_DISSOLVE_RANDOM[] PROGMEM = \"Dissolve Rnd@Repeat speed,Dissolve speed;,!;!\";\n\n/*\n * Blinks one LED at a time.\n * Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/\n */\nvoid mode_sparkle(void) {\n  if (!SEGMENT.check2) for (unsigned i = 0; i < SEGLEN; i++) {\n    SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1));\n  }\n  uint32_t cycleTime = 10 + (255 - SEGMENT.speed)*2;\n  uint32_t it = strip.now / cycleTime;\n  if (it != SEGENV.step)\n  {\n    SEGENV.aux0 = hw_random16(SEGLEN); // aux0 stores the random led index\n    SEGENV.step = it;\n  }\n\n  SEGMENT.setPixelColor(SEGENV.aux0, SEGCOLOR(0));\n}\nstatic const char _data_FX_MODE_SPARKLE[] PROGMEM = \"Sparkle@!,,,,,,Overlay;!,!;!;;m12=0\";\n\n/*\n * Lights all LEDs in the color. Flashes single col 1 pixels randomly. (List name: Sparkle Dark)\n * Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/\n */\nvoid mode_flash_sparkle(void) {\n  if (!SEGMENT.check2) for (unsigned i = 0; i < SEGLEN; i++) {\n    SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0));\n  }\n\n  if (strip.now - SEGENV.aux0 > SEGENV.step) {\n    if(hw_random8((255-SEGMENT.intensity) >> 4) == 0) {\n      SEGMENT.setPixelColor(hw_random16(SEGLEN), SEGCOLOR(1)); //flash\n    }\n    SEGENV.step = strip.now;\n    SEGENV.aux0 = 255-SEGMENT.speed;\n  }\n}\nstatic const char _data_FX_MODE_FLASH_SPARKLE[] PROGMEM = \"Sparkle Dark@!,!,,,,,Overlay;Bg,Fx;!;;m12=0\";\n\n\n/*\n * Like flash sparkle. With more flash.\n * Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/\n */\nvoid mode_hyper_sparkle(void) {\n  if (!SEGMENT.check2) for (unsigned i = 0; i < SEGLEN; i++) {\n    SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0));\n  }\n\n  if (strip.now - SEGENV.aux0 > SEGENV.step) {\n    if (hw_random8((255-SEGMENT.intensity) >> 4) == 0) {\n      int len = max(1, (int)SEGLEN/3);\n      for (int i = 0; i < len; i++) {\n        SEGMENT.setPixelColor(hw_random16(SEGLEN), SEGCOLOR(1));\n      }\n    }\n    SEGENV.step = strip.now;\n    SEGENV.aux0 = 255-SEGMENT.speed;\n  }\n}\nstatic const char _data_FX_MODE_HYPER_SPARKLE[] PROGMEM = \"Sparkle+@!,!,,,,,Overlay;Bg,Fx;!;;m12=0\";\n\n\n/*\n * Strobe effect with different strobe count and pause, controlled by speed.\n */\nvoid mode_multi_strobe(void) {\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1));\n  }\n\n  SEGENV.aux0 = 50 + 20*(uint16_t)(255-SEGMENT.speed);\n  unsigned count = 2 * ((SEGMENT.intensity / 10) + 1);\n  if(SEGENV.aux1 < count) {\n    if((SEGENV.aux1 & 1) == 0) {\n      SEGMENT.fill(SEGCOLOR(0));\n      SEGENV.aux0 = 15;\n    } else {\n      SEGENV.aux0 = 50;\n    }\n  }\n\n  if (strip.now - SEGENV.aux0 > SEGENV.step) {\n    SEGENV.aux1++;\n    if (SEGENV.aux1 > count) SEGENV.aux1 = 0;\n    SEGENV.step = strip.now;\n  }\n}\nstatic const char _data_FX_MODE_MULTI_STROBE[] PROGMEM = \"Strobe Mega@!,!;!,!;!;01\";\n\n\n/*\n * Android loading circle, refactored by @dedehai\n */\nvoid mode_android(void) {\n  if (!SEGENV.allocateData(sizeof(uint32_t))) FX_FALLBACK_STATIC;\n  uint32_t* counter = reinterpret_cast<uint32_t*>(SEGENV.data);\n  unsigned size = SEGENV.aux1 >> 1; // upper 15 bit\n  unsigned shrinking = SEGENV.aux1 & 0x01; // lowest bit\n  if(strip.now >= SEGENV.step) {\n    SEGENV.step = strip.now + 3 + ((8 * (uint32_t)(255 - SEGMENT.speed)) / SEGLEN);\n    if (size > (SEGMENT.intensity * SEGLEN) / 255)\n      shrinking = 1;\n    else if (size < 2)\n      shrinking = 0;\n    if (!shrinking) { // growing\n      if ((*counter % 3) == 1)\n        SEGENV.aux0++; // advance start position\n      else\n        size++;\n    } else { // shrinking\n      SEGENV.aux0++;\n      if ((*counter % 3) != 1)\n        size--;\n    }\n    SEGENV.aux1 = size << 1 | shrinking; // save back\n    (*counter)++;\n    if (SEGENV.aux0 >= SEGLEN) SEGENV.aux0 = 0;\n  }\n  uint32_t start = SEGENV.aux0;\n  uint32_t end = (SEGENV.aux0 + size) % SEGLEN;\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    if ((start < end && i >= start && i < end) || (start >= end && (i >= start || i < end)))\n      SEGMENT.setPixelColor(i, SEGCOLOR(0));\n    else\n      SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1));\n  }\n}\nstatic const char _data_FX_MODE_ANDROID[] PROGMEM = \"Android@!,Width;!,!;!;;m12=1\"; //vertical\n\n/*\n * color chase function.\n * color1 = background color\n * color2 and color3 = colors of two adjacent leds\n */\nstatic void chase(uint32_t color1, uint32_t color2, uint32_t color3, bool do_palette) {\n  uint16_t counter = strip.now * ((SEGMENT.speed >> 2) + 1);\n  uint16_t a = (counter * SEGLEN) >> 16;\n\n  bool chase_random = (SEGMENT.mode == FX_MODE_CHASE_RANDOM);\n  if (chase_random) {\n    if (a < SEGENV.step) //we hit the start again, choose new color for Chase random\n    {\n      SEGENV.aux1 = SEGENV.aux0; //store previous random color\n      SEGENV.aux0 = get_random_wheel_index(SEGENV.aux0);\n    }\n    color1 = SEGMENT.color_wheel(SEGENV.aux0);\n  }\n  SEGENV.step = a;\n\n  // Use intensity setting to vary chase up to 1/2 string length\n  unsigned size = 1 + ((SEGMENT.intensity * SEGLEN) >> 10);\n\n  uint16_t b = a + size; //\"trail\" of chase, filled with color1\n  if (b > SEGLEN) b -= SEGLEN;\n  uint16_t c = b + size;\n  if (c > SEGLEN) c -= SEGLEN;\n\n  //background\n  if (do_palette)\n  {\n    for (unsigned i = 0; i < SEGLEN; i++) {\n      SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1));\n    }\n  } else SEGMENT.fill(color1);\n\n  //if random, fill old background between a and end\n  if (chase_random)\n  {\n    color1 = SEGMENT.color_wheel(SEGENV.aux1);\n    for (unsigned i = a; i < SEGLEN; i++)\n      SEGMENT.setPixelColor(i, color1);\n  }\n\n  //fill between points a and b with color2\n  if (a < b)\n  {\n    for (unsigned i = a; i < b; i++)\n      SEGMENT.setPixelColor(i, color2);\n  } else {\n    for (unsigned i = a; i < SEGLEN; i++) //fill until end\n      SEGMENT.setPixelColor(i, color2);\n    for (unsigned i = 0; i < b; i++) //fill from start until b\n      SEGMENT.setPixelColor(i, color2);\n  }\n\n  //fill between points b and c with color2\n  if (b < c)\n  {\n    for (unsigned i = b; i < c; i++)\n      SEGMENT.setPixelColor(i, color3);\n  } else {\n    for (unsigned i = b; i < SEGLEN; i++) //fill until end\n      SEGMENT.setPixelColor(i, color3);\n    for (unsigned i = 0; i < c; i++) //fill from start until c\n      SEGMENT.setPixelColor(i, color3);\n  }\n}\n\n\n/*\n * Bicolor chase, more primary color.\n */\nvoid mode_chase_color(void) {\n  chase(SEGCOLOR(1), (SEGCOLOR(2)) ? SEGCOLOR(2) : SEGCOLOR(0), SEGCOLOR(0), true);\n}\nstatic const char _data_FX_MODE_CHASE_COLOR[] PROGMEM = \"Chase@!,Width;!,!,!;!\";\n\n\n/*\n * Primary running followed by random color.\n */\nvoid mode_chase_random(void) {\n  chase(SEGCOLOR(1), (SEGCOLOR(2)) ? SEGCOLOR(2) : SEGCOLOR(0), SEGCOLOR(0), false);\n}\nstatic const char _data_FX_MODE_CHASE_RANDOM[] PROGMEM = \"Chase Random@!,Width;!,,!;!\";\n\n\n/*\n * Primary, secondary running on rainbow.\n */\nvoid mode_chase_rainbow(void) {\n  unsigned color_sep = 256 / SEGLEN;\n  if (color_sep == 0) color_sep = 1;                                           // correction for segments longer than 256 LEDs\n  unsigned color_index = SEGENV.call & 0xFF;\n  uint32_t color = SEGMENT.color_wheel(((SEGENV.step * color_sep) + color_index) & 0xFF);\n\n  chase(color, SEGCOLOR(0), SEGCOLOR(1), false);\n}\nstatic const char _data_FX_MODE_CHASE_RAINBOW[] PROGMEM = \"Chase Rainbow@!,Width;!,!;!\";\n\n\n/*\n * Primary running on rainbow.\n */\nvoid mode_chase_rainbow_white(void) {\n  uint16_t n = SEGENV.step;\n  uint16_t m = (SEGENV.step + 1) % SEGLEN;\n  uint32_t color2 = SEGMENT.color_wheel(((n * 256 / SEGLEN) + (SEGENV.call & 0xFF)) & 0xFF);\n  uint32_t color3 = SEGMENT.color_wheel(((m * 256 / SEGLEN) + (SEGENV.call & 0xFF)) & 0xFF);\n\n  chase(SEGCOLOR(0), color2, color3, false);\n}\nstatic const char _data_FX_MODE_CHASE_RAINBOW_WHITE[] PROGMEM = \"Rainbow Runner@!,Size;Bg;!\";\n\n\n/*\n * Red - Amber - Green - Blue lights running\n */\nvoid mode_colorful(void) {\n  unsigned numColors = 4; //3, 4, or 5\n  uint32_t cols[9]{0x00FF0000,0x00EEBB00,0x0000EE00,0x000077CC};\n  if (SEGMENT.intensity > 160 || SEGMENT.palette) { //palette or color\n    if (!SEGMENT.palette) {\n      numColors = 3;\n      for (size_t i = 0; i < 3; i++) cols[i] = SEGCOLOR(i);\n    } else {\n      unsigned fac = 80;\n      if (SEGMENT.palette == 52) {numColors = 5; fac = 61;} //C9 2 has 5 colors\n      for (size_t i = 0; i < numColors; i++) {\n        cols[i] = SEGMENT.color_from_palette(i*fac, false, true, 255);\n      }\n    }\n  } else if (SEGMENT.intensity < 80) //pastel (easter) colors\n  {\n    cols[0] = 0x00FF8040;\n    cols[1] = 0x00E5D241;\n    cols[2] = 0x0077FF77;\n    cols[3] = 0x0077F0F0;\n  }\n  for (size_t i = numColors; i < numColors*2 -1U; i++) cols[i] = cols[i-numColors];\n\n  uint32_t cycleTime = 50 + (8 * (uint32_t)(255 - SEGMENT.speed));\n  uint32_t it = strip.now / cycleTime;\n  if (it != SEGENV.step)\n  {\n    if (SEGMENT.speed > 0) SEGENV.aux0++;\n    if (SEGENV.aux0 >= numColors) SEGENV.aux0 = 0;\n    SEGENV.step = it;\n  }\n\n  for (unsigned i = 0; i < SEGLEN; i+= numColors)\n  {\n    for (unsigned j = 0; j < numColors; j++) SEGMENT.setPixelColor(i + j, cols[SEGENV.aux0 + j]);\n  }\n}\nstatic const char _data_FX_MODE_COLORFUL[] PROGMEM = \"Colorful@!,Saturation;1,2,3;!\";\n\n\n/*\n * Emulates a traffic light.\n */\nvoid mode_traffic_light(void) {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  for (unsigned i=0; i < SEGLEN; i++)\n    SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1));\n  uint32_t mdelay = 500;\n  for (unsigned i = 0; i < SEGLEN-2 ; i+=3)\n  {\n    switch (SEGENV.aux0)\n    {\n      case 0: SEGMENT.setPixelColor(i, 0x00FF0000); mdelay = 150 + (100 * (uint32_t)(255 - SEGMENT.speed));break;\n      case 1: SEGMENT.setPixelColor(i, 0x00FF0000); mdelay = 150 + (20 * (uint32_t)(255 - SEGMENT.speed)); SEGMENT.setPixelColor(i+1, 0x00EECC00); break;\n      case 2: SEGMENT.setPixelColor(i+2, 0x0000FF00); mdelay = 150 + (100 * (uint32_t)(255 - SEGMENT.speed));break;\n      case 3: SEGMENT.setPixelColor(i+1, 0x00EECC00); mdelay = 150 + (20 * (uint32_t)(255 - SEGMENT.speed));break;\n    }\n  }\n\n  if (strip.now - SEGENV.step > mdelay)\n  {\n    SEGENV.aux0++;\n    if (SEGENV.aux0 == 1 && SEGMENT.intensity > 140) SEGENV.aux0 = 2; //skip Red + Amber, to get US-style sequence\n    if (SEGENV.aux0 > 3) SEGENV.aux0 = 0;\n    SEGENV.step = strip.now;\n  }\n}\nstatic const char _data_FX_MODE_TRAFFIC_LIGHT[] PROGMEM = \"Traffic Light@!,US style;,!;!\";\n\n\n/*\n * Sec flashes running on prim.\n */\n#define FLASH_COUNT 4\nvoid mode_chase_flash(void) {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  unsigned now = strip.now; // save time for delay calculation\n  bool advance = true;\n  unsigned flash_step = SEGENV.aux1 % ((FLASH_COUNT * 2) + 1);\n  if (now < SEGENV.step)\n    advance = false; // limit update rate but render every frame for smooth transitions\n  else\n    SEGENV.aux1++;\n\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0));\n  }\n  unsigned index = SEGENV.aux0;\n  unsigned n = index;\n  unsigned m = (index + 1) % SEGLEN;\n\n  unsigned delay = 10 + ((30 * (uint16_t)(255 - SEGMENT.speed)) / SEGLEN);\n  if(flash_step < (FLASH_COUNT * 2)) {\n    if(flash_step % 2 == 0) {\n      SEGMENT.setPixelColor( n, SEGCOLOR(1));\n      SEGMENT.setPixelColor( m, SEGCOLOR(1));\n      delay = 20;\n    } else {\n      delay = 30;\n    }\n  } else if (advance) {\n    SEGENV.aux0 = m; // advance to next position\n  }\n  if (advance)\n    SEGENV.step = now + delay; // set next update time\n}\nstatic const char _data_FX_MODE_CHASE_FLASH[] PROGMEM = \"Chase Flash@!;Bg,Fx;!\";\n\n\n/*\n * Prim flashes running, followed by random color.\n */\nvoid mode_chase_flash_random(void) {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  unsigned now = strip.now; // save time for delay calculation\n  bool advance = true;\n  if (now < SEGENV.step) {\n    SEGENV.call--; // revert increment to skip moving the animation forward and just render the same frame again\n    advance = false;\n  }\n  unsigned flash_step = SEGENV.call % ((FLASH_COUNT * 2) + 1);\n\n  for (int i = 0; i < SEGENV.aux1; i++) {\n    SEGMENT.setPixelColor(i, SEGMENT.color_wheel(SEGENV.aux0));\n  }\n\n  unsigned delay = 1 + ((10 * (uint16_t)(255 - SEGMENT.speed)) / SEGLEN);\n  if(flash_step < (FLASH_COUNT * 2)) {\n    unsigned n = SEGENV.aux1;\n    unsigned m = (SEGENV.aux1 + 1) % SEGLEN;\n    if(flash_step % 2 == 0) {\n      SEGMENT.setPixelColor( n, SEGCOLOR(0));\n      SEGMENT.setPixelColor( m, SEGCOLOR(0));\n      delay = 20;\n    } else {\n      SEGMENT.setPixelColor( n, SEGMENT.color_wheel(SEGENV.aux0));\n      SEGMENT.setPixelColor( m, SEGCOLOR(1));\n      delay = 30;\n    }\n  } else if (advance) {\n    SEGENV.aux1 = (SEGENV.aux1 + 1) % SEGLEN;\n\n    if (SEGENV.aux1 == 0) {\n      SEGENV.aux0 = get_random_wheel_index(SEGENV.aux0);\n    }\n  }\n  if (advance)\n    SEGENV.step = now + delay; // set next update time\n}\nstatic const char _data_FX_MODE_CHASE_FLASH_RANDOM[] PROGMEM = \"Chase Flash Rnd@!;!,!;!\";\n\n\n/*\n * Alternating color/sec pixels running.\n */\nvoid mode_running_color(void) {\n  running(SEGCOLOR(0), SEGCOLOR(1));\n}\nstatic const char _data_FX_MODE_RUNNING_COLOR[] PROGMEM = \"Chase 2@!,Width;!,!;!\";\n\n\n/*\n * Random colored pixels running. (\"Stream\")\n */\nvoid mode_running_random(void) {\n  uint32_t cycleTime = 25 + (3 * (uint32_t)(255 - SEGMENT.speed));\n  uint32_t it = strip.now / cycleTime;\n  if (SEGENV.call == 0) SEGENV.aux0 = hw_random(); // random seed for PRNG on start\n\n  unsigned zoneSize = ((255-SEGMENT.intensity) >> 4) +1;\n  uint16_t PRNG16 = SEGENV.aux0;\n\n  unsigned z = it % zoneSize;\n  bool nzone = (!z && it != SEGENV.aux1);\n  for (int i=SEGLEN-1; i >= 0; i--) {\n    if (nzone || z >= zoneSize) {\n      unsigned lastrand = PRNG16 >> 8;\n      int16_t diff = 0;\n      while (abs(diff) < 42) { // make sure the difference between adjacent colors is big enough\n        PRNG16 = (uint16_t)(PRNG16 * 2053) + 13849; // next zone, next 'random' number\n        diff = (PRNG16 >> 8) - lastrand;\n      }\n      if (nzone) {\n        SEGENV.aux0 = PRNG16; // save next starting seed\n        nzone = false;\n      }\n      z = 0;\n    }\n    SEGMENT.setPixelColor(i, SEGMENT.color_wheel(PRNG16 >> 8));\n    z++;\n  }\n\n  SEGENV.aux1 = it;\n}\nstatic const char _data_FX_MODE_RUNNING_RANDOM[] PROGMEM = \"Stream@!,Zone size;;!\";\n\n\n/*\n * K.I.T.T.\n */\nvoid mode_larson_scanner(void) {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n\n  const unsigned speed  = FRAMETIME * map(SEGMENT.speed, 0, 255, 96, 2); // map into useful range\n  const unsigned pixels = SEGLEN / speed; // how many pixels to advance per frame\n\n  SEGMENT.fade_out(255-SEGMENT.intensity);\n\n  if (SEGENV.step > strip.now) return;  // we have a pause\n\n  unsigned index = SEGENV.aux1 + pixels;\n  // are we slow enough to use frames per pixel?\n  if (pixels == 0) {\n    const unsigned frames = speed / SEGLEN; // how many frames per 1 pixel\n    if (SEGENV.step++ < frames) return;\n    SEGENV.step = 0;\n    index++;\n  }\n\n  if (index > SEGLEN) {\n\n    SEGENV.aux0 = !SEGENV.aux0; // change direction\n    SEGENV.aux1 = 0;            // reset position\n    // set delay\n    if (SEGENV.aux0 || SEGMENT.check2) SEGENV.step = strip.now + SEGMENT.custom1 * 25; // multiply by 25ms\n    else SEGENV.step = 0;\n\n  } else {\n\n    // paint as many pixels as needed\n    for (unsigned i = SEGENV.aux1; i < index; i++) {\n      unsigned j = (SEGENV.aux0) ? i : SEGLEN - 1 - i;\n      uint32_t c = SEGMENT.color_from_palette(j, true, PALETTE_SOLID_WRAP, 0);\n      SEGMENT.setPixelColor(j, c);\n      if (SEGMENT.check1) {\n        SEGMENT.setPixelColor(SEGLEN - 1 - j, SEGCOLOR(2) ? SEGCOLOR(2) : c);\n      }\n    }\n    SEGENV.aux1 = index;\n  }\n}\nstatic const char _data_FX_MODE_LARSON_SCANNER[] PROGMEM = \"Scanner@!,Trail,Delay,,,Dual,Bi-delay;!,!,!;!;;m12=0,c1=0\";\n\n/*\n * Creates two Larson scanners moving in opposite directions\n * Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/DualLarson.h\n */\nvoid mode_dual_larson_scanner(void){\n  SEGMENT.check1 = true;\n  mode_larson_scanner();\n}\nstatic const char _data_FX_MODE_DUAL_LARSON_SCANNER[] PROGMEM = \"Scanner Dual@!,Trail,Delay,,,Dual,Bi-delay;!,!,!;!;;m12=0,c1=0\";\n\n/*\n * Firing comets from one end. \"Lighthouse\"\n */\nvoid mode_comet(void) {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  unsigned counter = (strip.now * ((SEGMENT.speed >>2) +1)) & 0xFFFF;\n  unsigned index = (counter * SEGLEN) >> 16;\n  if (SEGENV.call == 0) SEGENV.aux0 = index;\n\n  SEGMENT.fade_out(SEGMENT.intensity);\n\n  SEGMENT.setPixelColor( index, SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0));\n  if (index > SEGENV.aux0) {\n    for (unsigned i = SEGENV.aux0; i < index ; i++) {\n       SEGMENT.setPixelColor( i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0));\n    }\n  } else if (index < SEGENV.aux0 && index < 10) {\n    for (unsigned i = 0; i < index ; i++) {\n       SEGMENT.setPixelColor( i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0));\n    }\n  }\n  SEGENV.aux0 = index++;\n}\nstatic const char _data_FX_MODE_COMET[] PROGMEM = \"Lighthouse@!,Fade rate;!,!;!\";\n\n/*\n * Fireworks function.\n */\nvoid mode_fireworks() {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  const uint16_t width  = SEGMENT.is2D() ? SEG_W : SEGLEN;\n  const uint16_t height = SEG_H;\n\n  if (SEGENV.call == 0) {\n    SEGENV.aux0 = UINT16_MAX;\n    SEGENV.aux1 = UINT16_MAX;\n  }\n  SEGMENT.fade_out(128);\n\n  uint8_t x = SEGENV.aux0%width, y = SEGENV.aux0/width; // 2D coordinates stored in upper and lower byte\n  if (!SEGENV.step) {\n    // fireworks mode (blur flares)\n    bool valid1 = (SEGENV.aux0 < width*height);\n    bool valid2 = (SEGENV.aux1 < width*height);\n    uint32_t sv1 = 0, sv2 = 0;\n    if (valid1) sv1 = SEGMENT.is2D() ? SEGMENT.getPixelColorXY(x, y) : SEGMENT.getPixelColor(SEGENV.aux0); // get spark color\n    if (valid2) sv2 = SEGMENT.is2D() ? SEGMENT.getPixelColorXY(x, y) : SEGMENT.getPixelColor(SEGENV.aux1);\n    SEGMENT.blur(16); // used in mode_rain()\n    if (valid1) { if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(x, y, sv1); else SEGMENT.setPixelColor(SEGENV.aux0, sv1); } // restore spark color after blur\n    if (valid2) { if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(x, y, sv2); else SEGMENT.setPixelColor(SEGENV.aux1, sv2); } // restore old spark color after blur\n  }\n\n  for (int i=0; i<max(1, width/20); i++) {\n    if (hw_random8(129 - (SEGMENT.intensity >> 1)) == 0) {\n      uint16_t index = hw_random16(width*height);\n      x = index % width;\n      y = index / width;\n      uint32_t col = SEGMENT.color_from_palette(hw_random8(), false, false, 0);\n      if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(x, y, col);\n      else                SEGMENT.setPixelColor(index, col);\n      SEGENV.aux1 = SEGENV.aux0;  // old spark\n      SEGENV.aux0 = index;        // remember where spark occurred\n    }\n  }\n}\nstatic const char _data_FX_MODE_FIREWORKS[] PROGMEM = \"Fireworks@,Frequency;!,!;!;12;ix=192,pal=11\";\n\n//Twinkling LEDs running. Inspired by https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Rain.h\nvoid mode_rain() {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  const unsigned width  = SEG_W;\n  const unsigned height = SEG_H;\n  SEGENV.step += FRAMETIME;\n  if (SEGENV.call && SEGENV.step > SPEED_FORMULA_L) {\n    SEGENV.step = 1;\n    if (SEGMENT.is2D()) {\n      //uint32_t ctemp[width];\n      //for (int i = 0; i<width; i++) ctemp[i] = SEGMENT.getPixelColorXY(i, height-1);\n      SEGMENT.move(6, 1, true);  // move all pixels down\n      //for (int i = 0; i<width; i++) SEGMENT.setPixelColorXY(i, 0, ctemp[i]); // wrap around\n      SEGENV.aux0 = (SEGENV.aux0 % width) + (SEGENV.aux0 / width + 1) * width;\n      SEGENV.aux1 = (SEGENV.aux1 % width) + (SEGENV.aux1 / width + 1) * width;\n    } else {\n      //shift all leds left\n      uint32_t ctemp = SEGMENT.getPixelColor(0);\n      for (unsigned i = 0; i < SEGLEN - 1; i++) {\n        SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1));\n      }\n      SEGMENT.setPixelColor(SEGLEN -1, ctemp); // wrap around\n      SEGENV.aux0++;  // increase spark index\n      SEGENV.aux1++;\n    }\n    if (SEGENV.aux0 == 0) SEGENV.aux0 = UINT16_MAX; // reset previous spark position\n    if (SEGENV.aux1 == 0) SEGENV.aux0 = UINT16_MAX; // reset previous spark position\n    if (SEGENV.aux0 >= width*height) SEGENV.aux0 = 0;     // ignore\n    if (SEGENV.aux1 >= width*height) SEGENV.aux1 = 0;\n  }\n  mode_fireworks();\n}\nstatic const char _data_FX_MODE_RAIN[] PROGMEM = \"Rain@!,Spawning rate;!,!;!;12;ix=128,pal=0\";\n\n/*\n * Fire flicker function\n */\nvoid mode_fire_flicker(void) {\n  uint32_t cycleTime = 40 + (255 - SEGMENT.speed);\n  uint32_t it = strip.now / cycleTime;\n  if (SEGENV.step == it) return;\n\n  byte w = (SEGCOLOR(0) >> 24);\n  byte r = (SEGCOLOR(0) >> 16);\n  byte g = (SEGCOLOR(0) >>  8);\n  byte b = (SEGCOLOR(0)      );\n  byte lum = (SEGMENT.palette == 0) ? MAX(w, MAX(r, MAX(g, b))) : 255;\n  lum /= (((256-SEGMENT.intensity)/16)+1);\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    byte flicker = hw_random8(lum);\n    if (SEGMENT.palette == 0) {\n      SEGMENT.setPixelColor(i, MAX(r - flicker, 0), MAX(g - flicker, 0), MAX(b - flicker, 0), MAX(w - flicker, 0));\n    } else {\n      SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0, 255 - flicker));\n    }\n  }\n\n  SEGENV.step = it;\n}\nstatic const char _data_FX_MODE_FIRE_FLICKER[] PROGMEM = \"Fire Flicker@!,!;!;!;01\";\n\n\n/*\n * Gradient run base function\n */\nvoid gradient_base(bool loading) {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  uint16_t counter = strip.now * ((SEGMENT.speed >> 2) + 1);\n  uint16_t pp = (counter * SEGLEN) >> 16;\n  if (SEGENV.call == 0) pp = 0;\n  int val; //0 = sec 1 = pri\n  int brd = 1 + loading ? SEGMENT.intensity/2 : SEGMENT.intensity/4;\n  //if (brd < 1) brd = 1;\n  int p1 = pp-SEGLEN;\n  int p2 = pp+SEGLEN;\n\n  for (int i = 0; i < (int)SEGLEN; i++) {\n    if (loading) {\n      val = abs(((i>pp) ? p2:pp) - i);\n    } else {\n      val = min(abs(pp-i),min(abs(p1-i),abs(p2-i)));\n    }\n    val = (brd > val) ? (val * 255) / brd : 255;\n    SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(0), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1), uint8_t(val)));\n  }\n}\n\n\n/*\n * Gradient run\n */\nvoid mode_gradient(void) {\n  gradient_base(false);\n}\nstatic const char _data_FX_MODE_GRADIENT[] PROGMEM = \"Gradient@!,Spread;!,!;!;;ix=16\";\n\n\n/*\n * Gradient run with hard transition\n */\nvoid mode_loading(void) {\n  gradient_base(true);\n}\nstatic const char _data_FX_MODE_LOADING[] PROGMEM = \"Loading@!,Fade;!,!;!;;ix=16\";\n\n/*\n * Two dots running\n */\nvoid mode_two_dots() {\n if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  unsigned delay = 1 + (FRAMETIME<<3) / SEGLEN;  // longer segments should change faster\n  uint32_t it = strip.now / map(SEGMENT.speed, 0, 255, delay<<4, delay);\n  unsigned offset = it % SEGLEN;\n  unsigned width = ((SEGLEN*(SEGMENT.intensity+1))>>9); //max width is half the strip\n  if (!width) width = 1;\n  if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(2));\n  const uint32_t color1 = SEGCOLOR(0);\n  const uint32_t color2 = (SEGCOLOR(1) == SEGCOLOR(2)) ? color1 : SEGCOLOR(1);\n  for (unsigned i = 0; i < width; i++) {\n    unsigned indexR = (offset + i) % SEGLEN;\n    unsigned indexB = (offset + i + (SEGLEN>>1)) % SEGLEN;\n    SEGMENT.setPixelColor(indexR, color1);\n    SEGMENT.setPixelColor(indexB, color2);\n  }\n}\nstatic const char _data_FX_MODE_TWO_DOTS[] PROGMEM = \"Two Dots@!,Dot size,,,,,Overlay;1,2,Bg;!\";\n\n\n/*\n * Fairy, inspired by https://www.youtube.com/watch?v=zeOw5MZWq24\n */\n//4 bytes\ntypedef struct Flasher {\n  uint16_t stateStart;\n  uint8_t stateDur;\n  bool stateOn;\n} flasher;\n\n#define FLASHERS_PER_ZONE 6\n#define MAX_SHIMMER 92\n\nvoid mode_fairy() {\n  //set every pixel to a 'random' color from palette (using seed so it doesn't change between frames)\n  uint16_t PRNG16 = 5100 + strip.getCurrSegmentId();\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; //next 'random' number\n    SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(PRNG16 >> 8, false, false, 0));\n  }\n\n  //amount of flasher pixels depending on intensity (0: none, 255: every LED)\n  if (SEGMENT.intensity == 0) return;\n  unsigned flasherDistance = ((255 - SEGMENT.intensity) / 28) +1; //1-10\n  unsigned numFlashers = (SEGLEN / flasherDistance) +1;\n\n  unsigned dataSize = sizeof(flasher) * numFlashers;\n  if (!SEGENV.allocateData(dataSize)) return; //allocation failed\n  Flasher* flashers = reinterpret_cast<Flasher*>(SEGENV.data);\n  unsigned now16 = strip.now & 0xFFFF;\n\n  //Up to 11 flashers in one brightness zone, afterwards a new zone for every 6 flashers\n  unsigned zones = numFlashers/FLASHERS_PER_ZONE;\n  if (!zones) zones = 1;\n  unsigned flashersInZone = numFlashers/zones;\n  uint8_t flasherBri[FLASHERS_PER_ZONE*2 -1];\n\n  for (unsigned z = 0; z < zones; z++) {\n    unsigned flasherBriSum = 0;\n    unsigned firstFlasher = z*flashersInZone;\n    if (z == zones-1) flashersInZone = numFlashers-(flashersInZone*(zones-1));\n\n    for (unsigned f = firstFlasher; f < firstFlasher + flashersInZone; f++) {\n      unsigned stateTime = uint16_t(now16 - flashers[f].stateStart);\n      //random on/off time reached, switch state\n      if (stateTime > flashers[f].stateDur * 10) {\n        flashers[f].stateOn = !flashers[f].stateOn;\n        if (flashers[f].stateOn) {\n          flashers[f].stateDur = 12 + hw_random8(12 + ((255 - SEGMENT.speed) >> 2)); //*10, 250ms to 1250ms\n        } else {\n          flashers[f].stateDur = 20 + hw_random8(6 + ((255 - SEGMENT.speed) >> 2)); //*10, 250ms to 1250ms\n        }\n        //flashers[f].stateDur = 51 + hw_random8(2 + ((255 - SEGMENT.speed) >> 1));\n        flashers[f].stateStart = now16;\n        if (stateTime < 255) {\n          flashers[f].stateStart -= 255 -stateTime; //start early to get correct bri\n          flashers[f].stateDur += 26 - stateTime/10;\n          stateTime = 255 - stateTime;\n        } else {\n          stateTime = 0;\n        }\n      }\n      if (stateTime > 255) stateTime = 255; //for flasher brightness calculation, fades in first 255 ms of state\n      //flasherBri[f - firstFlasher] = (flashers[f].stateOn) ? 255-SEGMENT.gamma8((510 - stateTime) >> 1) : SEGMENT.gamma8((510 - stateTime) >> 1);\n      flasherBri[f - firstFlasher] = (flashers[f].stateOn) ? stateTime : 255 - (stateTime >> 0);\n      flasherBriSum += flasherBri[f - firstFlasher];\n    }\n    //dim factor, to create \"shimmer\" as other pixels get less voltage if a lot of flashers are on\n    unsigned avgFlasherBri = flasherBriSum / flashersInZone;\n    unsigned globalPeakBri = 255 - ((avgFlasherBri * MAX_SHIMMER) >> 8); //183-255, suitable for 1/5th of LEDs flashers\n\n    for (unsigned f = firstFlasher; f < firstFlasher + flashersInZone; f++) {\n      uint8_t bri = (flasherBri[f - firstFlasher] * globalPeakBri) / 255;\n      PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; //next 'random' number\n      unsigned flasherPos = f*flasherDistance;\n      SEGMENT.setPixelColor(flasherPos, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(PRNG16 >> 8, false, false, 0), bri));\n      for (unsigned i = flasherPos+1; i < flasherPos+flasherDistance && i < SEGLEN; i++) {\n        PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; //next 'random' number\n        SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(PRNG16 >> 8, false, false, 0, globalPeakBri));\n      }\n    }\n  }\n}\nstatic const char _data_FX_MODE_FAIRY[] PROGMEM = \"Fairy@!,# of flashers;!,!;!\";\n\n\n/*\n * Fairytwinkle. Like Colortwinkle, but starting from all lit and not relying on strip.getPixelColor\n * Warning: Uses 4 bytes of segment data per pixel\n */\nvoid mode_fairytwinkle() {\n  unsigned dataSize = sizeof(flasher) * SEGLEN;\n  if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed\n  Flasher* flashers = reinterpret_cast<Flasher*>(SEGENV.data);\n  unsigned now16 = strip.now & 0xFFFF;\n  uint16_t PRNG16 = 5100 + strip.getCurrSegmentId();\n\n  unsigned riseFallTime = 400 + (255-SEGMENT.speed)*3;\n  unsigned maxDur = riseFallTime/100 + ((255 - SEGMENT.intensity) >> 2) + 13 + ((255 - SEGMENT.intensity) >> 1);\n\n  for (unsigned f = 0; f < SEGLEN; f++) {\n    uint16_t stateTime = now16 - flashers[f].stateStart;\n    //random on/off time reached, switch state\n    if (stateTime > flashers[f].stateDur * 100) {\n      flashers[f].stateOn = !flashers[f].stateOn;\n      bool init = !flashers[f].stateDur;\n      if (flashers[f].stateOn) {\n        flashers[f].stateDur = riseFallTime/100 + ((255 - SEGMENT.intensity) >> 2) + hw_random8(12 + ((255 - SEGMENT.intensity) >> 1)) +1;\n      } else {\n        flashers[f].stateDur = riseFallTime/100 + hw_random8(3 + ((255 - SEGMENT.speed) >> 6)) +1;\n      }\n      flashers[f].stateStart = now16;\n      stateTime = 0;\n      if (init) {\n        flashers[f].stateStart -= riseFallTime; //start lit\n        flashers[f].stateDur = riseFallTime/100 + hw_random8(12 + ((255 - SEGMENT.intensity) >> 1)) +5; //fire up a little quicker\n        stateTime = riseFallTime;\n      }\n    }\n    if (flashers[f].stateOn && flashers[f].stateDur > maxDur) flashers[f].stateDur = maxDur; //react more quickly on intensity change\n    if (stateTime > riseFallTime) stateTime = riseFallTime; //for flasher brightness calculation, fades in first 255 ms of state\n    unsigned fadeprog = 255 - ((stateTime * 255) / riseFallTime);\n    uint8_t flasherBri = (flashers[f].stateOn) ? 255-gamma8(fadeprog) : gamma8(fadeprog);\n    unsigned lastR = PRNG16;\n    unsigned diff = 0;\n    while (diff < 0x4000) { //make sure colors of two adjacent LEDs differ enough\n      PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; //next 'random' number\n      diff = (PRNG16 > lastR) ? PRNG16 - lastR : lastR - PRNG16;\n    }\n    SEGMENT.setPixelColor(f, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(PRNG16 >> 8, false, false, 0), flasherBri));\n  }\n}\nstatic const char _data_FX_MODE_FAIRYTWINKLE[] PROGMEM = \"Fairytwinkle@!,!;!,!;!;;m12=0\"; //pixels\n\n\n/*\n * Tricolor chase function\n */\nvoid tricolor_chase(uint32_t color1, uint32_t color2) {\n  uint32_t cycleTime = 50 + ((255 - SEGMENT.speed)<<1);\n  uint32_t it = strip.now / cycleTime;  // iterator\n  unsigned width = (1 + (SEGMENT.intensity>>4)); // value of 1-16 for each colour\n  unsigned index = it % (width*3);\n\n  for (unsigned i = 0; i < SEGLEN; i++, index++) {\n    if (index > (width*3)-1) index = 0;\n\n    uint32_t color = color1;\n    if (index > (width<<1)-1) color = SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1);\n    else if (index > width-1) color = color2;\n\n    SEGMENT.setPixelColor(SEGLEN - i -1, color);\n  }\n}\n\n\n/*\n * Tricolor chase mode\n */\nvoid mode_tricolor_chase(void) {\n  tricolor_chase(SEGCOLOR(2), SEGCOLOR(0));\n}\nstatic const char _data_FX_MODE_TRICOLOR_CHASE[] PROGMEM = \"Chase 3@!,Size;1,2,3;!\";\n\n\n/*\n * ICU mode\n */\nvoid mode_icu(void) {\n  // states: 0 = pause1, 1 = blink, 2 = pause2, 3 = move\n\n  uint16_t now = strip.now; // save time for delay calculation, use low16 bits only\n  unsigned dest = SEGENV.aux1;\n  unsigned space = (SEGMENT.intensity >> 3) +2;\n  uint16_t state = SEGENV.step >> 16; // upper bytes of step store current state\n  uint16_t nextUpdate = SEGENV.step & 0xFFFF; // lower bytes store time for next update\n\n  byte pindex = map(dest, 0, SEGLEN-SEGLEN/space, 0, 255);\n  uint32_t col = SEGMENT.color_from_palette(pindex, false, false, 0);\n  uint32_t bgcol = SEGMENT.check2 ? BLACK : SEGCOLOR(1);\n  SEGMENT.fill(bgcol); // apply background color or clear\n  // draw eyes if not blinking\n  if (state != 1) {\n    SEGMENT.setPixelColor(dest, col);\n    SEGMENT.setPixelColor(dest + SEGLEN/space, col);\n    // render next position if moving\n    if (state == 3) {\n      if(SEGENV.aux0 > SEGENV.aux1) {\n        dest++;\n      } else if (SEGENV.aux0 < SEGENV.aux1) {\n        dest--;\n      }\n      SEGMENT.setPixelColor(dest, col);\n      SEGMENT.setPixelColor(dest + SEGLEN/space, col);\n    }\n  }\n\n  // update state\n  if ((int16_t)(now - nextUpdate) >= 0) { // time to update, cast to int to handle wraparound properly\n    switch (state) {\n      case 0: // pause part 1\n        // first pause part finished, blink or pause some more\n        state++;\n        if(hw_random8(6) == 0) { // blink once in a while\n          nextUpdate = uint16_t(now + 200);\n          break;\n        }\n        // fall through if not blinking\n      case 1: // blink\n        // not blinking or finished blinking -> pause part 2\n        nextUpdate = uint16_t(now + 500 + hw_random16(1000));\n        state++;\n        break;\n      case 2: // pause part 2\n        // pause finished, move\n        SEGENV.aux0 = hw_random16(SEGLEN-SEGLEN/space); // choose a new destination\n        nextUpdate = now;\n        state++;\n        break;\n      default: // move (state 3)\n        SEGENV.aux1 = dest; // update destination to moved position\n        nextUpdate = uint16_t(now + SPEED_FORMULA_L);\n        if (SEGENV.aux0 == dest) {\n          // reached destination\n          nextUpdate = uint16_t(now + 500 + hw_random16(1000));\n          state = 0;\n        }\n        break;\n    }\n  }\n\n  // use upper bits of SEGENV.step to store current state, lower bits for next update time\n  SEGENV.step = (state << 16) | nextUpdate;\n}\nstatic const char _data_FX_MODE_ICU[] PROGMEM = \"ICU@!,!,,,,,Overlay;!,!;!\";\n\n\n/*\n * Custom mode by Aircoookie. Color Wipe, but with 3 colors\n */\nvoid mode_tricolor_wipe(void) {\n  uint32_t cycleTime = 1000 + (255 - SEGMENT.speed)*200;\n  uint32_t perc = strip.now % cycleTime;\n  unsigned prog = (perc * 65535) / cycleTime;\n  unsigned ledIndex = (prog * SEGLEN * 3) >> 16;\n  unsigned ledOffset = ledIndex;\n\n  for (unsigned i = 0; i < SEGLEN; i++)\n  {\n    SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 2));\n  }\n\n  if(ledIndex < SEGLEN) { //wipe from 0 to 1\n    for (unsigned i = 0; i < SEGLEN; i++)\n    {\n      SEGMENT.setPixelColor(i, (i > ledOffset)? SEGCOLOR(0) : SEGCOLOR(1));\n    }\n  } else if (ledIndex < SEGLEN*2) { //wipe from 1 to 2\n    ledOffset = ledIndex - SEGLEN;\n    for (unsigned i = ledOffset +1; i < SEGLEN; i++)\n    {\n      SEGMENT.setPixelColor(i, SEGCOLOR(1));\n    }\n  } else //wipe from 2 to 0\n  {\n    ledOffset = ledIndex - SEGLEN*2;\n    for (unsigned i = 0; i <= ledOffset; i++)\n    {\n      SEGMENT.setPixelColor(i, SEGCOLOR(0));\n    }\n  }\n}\nstatic const char _data_FX_MODE_TRICOLOR_WIPE[] PROGMEM = \"Tri Wipe@!;1,2,3;!\";\n\n\n/*\n * Fades between 3 colors\n * Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/TriFade.h\n * Modified by Aircoookie\n */\nvoid mode_tricolor_fade(void) {\n  uint16_t counter = strip.now * ((SEGMENT.speed >> 3) +1);\n  uint32_t prog = (counter * 768) >> 16;\n\n  uint32_t color1 = 0, color2 = 0;\n  unsigned stage = 0;\n\n  if(prog < 256) {\n    color1 = SEGCOLOR(0);\n    color2 = SEGCOLOR(1);\n    stage = 0;\n  } else if(prog < 512) {\n    color1 = SEGCOLOR(1);\n    color2 = SEGCOLOR(2);\n    stage = 1;\n  } else {\n    color1 = SEGCOLOR(2);\n    color2 = SEGCOLOR(0);\n    stage = 2;\n  }\n\n  byte stp = prog; // % 256\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    uint32_t color;\n    if (stage == 2) {\n      color = color_blend(SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 2), color2, stp);\n    } else if (stage == 1) {\n      color = color_blend(color1, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 2), stp);\n    } else {\n      color = color_blend(color1, color2, stp);\n    }\n    SEGMENT.setPixelColor(i, color);\n  }\n}\nstatic const char _data_FX_MODE_TRICOLOR_FADE[] PROGMEM = \"Tri Fade@!;1,2,3;!\";\n\n/*\n * Creates random comets\n * Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/MultiComet.h\n */\n#define MAX_COMETS 8\nvoid mode_multi_comet(void) {\n  uint32_t cycleTime = 10 + (uint32_t)(255 - SEGMENT.speed);\n  uint32_t it = strip.now / cycleTime;\n  if (SEGENV.step == it) return;\n  if (!SEGENV.allocateData(sizeof(uint16_t) * MAX_COMETS)) FX_FALLBACK_STATIC; //allocation failed\n\n  SEGMENT.fade_out(SEGMENT.intensity/2 + 128);\n\n  uint16_t* comets = reinterpret_cast<uint16_t*>(SEGENV.data);\n\n  for (unsigned i=0; i < MAX_COMETS; i++) {\n    if(comets[i] < SEGLEN) {\n      unsigned index = comets[i];\n      if (SEGCOLOR(2) != 0)\n      {\n        SEGMENT.setPixelColor(index, i % 2 ? SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0) : SEGCOLOR(2));\n      } else\n      {\n        SEGMENT.setPixelColor(index, SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0));\n      }\n      comets[i]++;\n    } else {\n      if(!hw_random16(SEGLEN)) {\n        comets[i] = 0;\n      }\n    }\n  }\n\n  SEGENV.step = it;\n}\nstatic const char _data_FX_MODE_MULTI_COMET[] PROGMEM = \"Multi Comet@!,Fade;!,!;!;1\";\n#undef MAX_COMETS\n\n/*\n * Running random pixels (\"Stream 2\")\n * Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/RandomChase.h\n */\nvoid mode_random_chase(void) {\n  if (SEGENV.call == 0) {\n    SEGENV.step = RGBW32(random8(), random8(), random8(), 0);\n    SEGENV.aux0 = random16();\n  }\n  unsigned prevSeed = random16_get_seed(); // save seed so we can restore it at the end of the function\n  uint32_t cycleTime = 25 + (3 * (uint32_t)(255 - SEGMENT.speed));\n  uint32_t it = strip.now / cycleTime;\n  uint32_t color = SEGENV.step;\n  random16_set_seed(SEGENV.aux0);\n\n  for (int i = SEGLEN -1; i >= 0; i--) {\n    uint8_t r = random8(6) != 0 ? (color >> 16 & 0xFF) : random8();\n    uint8_t g = random8(6) != 0 ? (color >> 8  & 0xFF) : random8();\n    uint8_t b = random8(6) != 0 ? (color       & 0xFF) : random8();\n    color = RGBW32(r, g, b, 0);\n    SEGMENT.setPixelColor(i, color);\n    if (i == SEGLEN -1U && SEGENV.aux1 != (it & 0xFFFFU)) { //new first color in next frame\n      SEGENV.step = color;\n      SEGENV.aux0 = random16_get_seed();\n    }\n  }\n\n  SEGENV.aux1 = it & 0xFFFF;\n\n  random16_set_seed(prevSeed); // restore original seed so other effects can use \"random\" PRNG\n}\nstatic const char _data_FX_MODE_RANDOM_CHASE[] PROGMEM = \"Stream 2@!;;\";\n\n\n//7 bytes\ntypedef struct Oscillator {\n  uint16_t pos;\n  uint8_t  size;\n  int8_t   dir;\n  uint8_t  speed;\n} oscillator;\n\n/*\n/  Oscillating bars of color, updated with standard framerate\n*/\nvoid mode_oscillate(void) {\n  constexpr unsigned numOscillators = 3;\n  constexpr unsigned dataSize = sizeof(oscillator) * numOscillators;\n\n  if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed\n\n  Oscillator* oscillators = reinterpret_cast<Oscillator*>(SEGENV.data);\n\n  if (SEGENV.call == 0)\n  {\n    oscillators[0] = {(uint16_t)(SEGLEN/4),   (uint8_t)(SEGLEN/8),  1, 1};\n    oscillators[1] = {(uint16_t)(SEGLEN/4*3), (uint8_t)(SEGLEN/8),  1, 2};\n    oscillators[2] = {(uint16_t)(SEGLEN/4*2), (uint8_t)(SEGLEN/8), -1, 1};\n  }\n\n  uint32_t cycleTime = 20 + (2 * (uint32_t)(255 - SEGMENT.speed));\n  uint32_t it = strip.now / cycleTime;\n\n  for (unsigned i = 0; i < numOscillators; i++) {\n    // if the counter has increased, move the oscillator by the random step\n    if (it != SEGENV.step) oscillators[i].pos += oscillators[i].dir * oscillators[i].speed;\n    oscillators[i].size = SEGLEN/(3+SEGMENT.intensity/8);\n    if((oscillators[i].dir == -1) && (oscillators[i].pos > SEGLEN << 1)) { // use integer overflow\n      oscillators[i].pos = 0;\n      oscillators[i].dir = 1;\n      // make bigger steps for faster speeds\n      oscillators[i].speed = SEGMENT.speed > 100 ? hw_random8(2, 4):hw_random8(1, 3);\n    }\n    if((oscillators[i].dir == 1) && (oscillators[i].pos >= (SEGLEN - 1))) {\n      oscillators[i].pos = SEGLEN - 1;\n      oscillators[i].dir = -1;\n      oscillators[i].speed = SEGMENT.speed > 100 ? hw_random8(2, 4):hw_random8(1, 3);\n    }\n  }\n\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    uint32_t color = BLACK;\n    for (unsigned j = 0; j < numOscillators; j++) {\n      if((int)i >= (int)oscillators[j].pos - oscillators[j].size && i <= oscillators[j].pos + oscillators[j].size) {\n        color = (color == BLACK) ? SEGCOLOR(j) : color_blend(color, SEGCOLOR(j), uint8_t(128));\n      }\n    }\n    SEGMENT.setPixelColor(i, color);\n  }\n\n  SEGENV.step = it;\n}\nstatic const char _data_FX_MODE_OSCILLATE[] PROGMEM = \"Oscillate\";\n\n\nvoid mode_lightning(void) {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  unsigned ledstart = hw_random16(SEGLEN);               // Determine starting location of flash\n  unsigned ledlen = 1 + hw_random16(SEGLEN -ledstart);   // Determine length of flash (not to go beyond NUM_LEDS-1)\n  uint8_t bri = 255/hw_random8(1, 3);\n\n  if (SEGENV.aux1 == 0) //init, leader flash\n  {\n    SEGENV.aux1 = hw_random8(4, 4 + SEGMENT.intensity/20); //number of flashes\n    SEGENV.aux1 *= 2;\n\n    bri = 52; //leader has lower brightness\n    SEGENV.aux0 = 200; //200ms delay after leader\n  }\n\n  if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1));\n\n  if (SEGENV.aux1 > 3 && !(SEGENV.aux1 & 0x01)) { //flash on even number >2\n    for (unsigned i = ledstart; i < ledstart + ledlen; i++)\n    {\n      SEGMENT.setPixelColor(i,SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0, bri));\n    }\n    SEGENV.aux1--;\n\n    SEGENV.step = strip.now;\n    //return hw_random8(4, 10); // each flash only lasts one frame/every 24ms... originally 4-10 milliseconds\n  } else {\n    if (strip.now - SEGENV.step > SEGENV.aux0) {\n      SEGENV.aux1--;\n      if (SEGENV.aux1 < 2) SEGENV.aux1 = 0;\n\n      SEGENV.aux0 = (50 + hw_random8(100)); //delay between flashes\n      if (SEGENV.aux1 == 2) {\n        SEGENV.aux0 = (hw_random8(255 - SEGMENT.speed) * 100); // delay between strikes\n      }\n      SEGENV.step = strip.now;\n    }\n  }\n}\nstatic const char _data_FX_MODE_LIGHTNING[] PROGMEM = \"Lightning@!,!,,,,,Overlay;!,!;!\";\n\n// combined function from original pride and colorwaves\nvoid mode_colorwaves_pride_base(bool isPride2015) {\n  unsigned duration = 10 + SEGMENT.speed;\n  unsigned sPseudotime = SEGENV.step;\n  unsigned sHue16 = SEGENV.aux0;\n\n  uint8_t sat8 = isPride2015 ? beatsin88_t(87, 220, 250) : 255;\n  unsigned brightdepth = beatsin88_t(341, 96, 224);\n  unsigned brightnessthetainc16 = beatsin88_t(203, (25 * 256), (40 * 256));\n  unsigned msmultiplier = beatsin88_t(147, 23, 60);\n\n  unsigned hue16 = sHue16;\n  unsigned hueinc16 = isPride2015 ? beatsin88_t(113, 1, 3000) : \n                                     beatsin88_t(113, 60, 300) * SEGMENT.intensity * 10 / 255;\n\n  sPseudotime += duration * msmultiplier;\n  sHue16 += duration * beatsin88_t(400, 5, 9);\n  unsigned brightnesstheta16 = sPseudotime;\n\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    hue16 += hueinc16;\n    uint8_t hue8;\n\n    if (isPride2015) {\n      hue8 = hue16 >> 8;\n    } else {\n      unsigned h16_128 = hue16 >> 7;\n      hue8 = (h16_128 & 0x100) ? (255 - (h16_128 >> 1)) : (h16_128 >> 1);\n    }\n\n    brightnesstheta16 += brightnessthetainc16;\n    unsigned b16 = sin16_t(brightnesstheta16) + 32768;\n    unsigned bri16 = (uint32_t)((uint32_t)b16 * (uint32_t)b16) / 65536;\n    uint8_t bri8 = (uint32_t)(((uint32_t)bri16) * brightdepth) / 65536;\n    bri8 += (255 - brightdepth);\n\n    if (isPride2015) {\n      CRGBW newcolor = CRGB(CHSV(hue8, sat8, bri8));\n      newcolor.color32 = gamma32inv(newcolor.color32);\n      SEGMENT.blendPixelColor(i, newcolor, 64);\n    } else {\n      SEGMENT.blendPixelColor(i, SEGMENT.color_from_palette(hue8, false, PALETTE_SOLID_WRAP, 0, bri8), 128);\n    }\n  }\n\n  SEGENV.step = sPseudotime;\n  SEGENV.aux0 = sHue16;\n}\n\n// Pride2015\n// Animated, ever-changing rainbows.\n// by Mark Kriegsman: https://gist.github.com/kriegsman/964de772d64c502760e5\nvoid mode_pride_2015(void) {\n  mode_colorwaves_pride_base(true);\n}\nstatic const char _data_FX_MODE_PRIDE_2015[] PROGMEM = \"Pride 2015@!;;\";\n\n// ColorWavesWithPalettes by Mark Kriegsman: https://gist.github.com/kriegsman/8281905786e8b2632aeb\n// This function draws color waves with an ever-changing,\n// widely-varying set of parameters, using a color palette.\nvoid mode_colorwaves() {\n  mode_colorwaves_pride_base(false);\n}\nstatic const char _data_FX_MODE_COLORWAVES[] PROGMEM = \"Colorwaves@!,Hue;!;!;;pal=26\";\n\n\n//eight colored dots, weaving in and out of sync with each other\nvoid mode_juggle(void) {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n\n  SEGMENT.fadeToBlackBy(192 - (3*SEGMENT.intensity/4));\n  CRGB fastled_col;\n  byte dothue = 0;\n  for (int i = 0; i < 8; i++) {\n    int index = 0 + beatsin88_t((16 + SEGMENT.speed)*(i + 7), 0, SEGLEN -1);\n    fastled_col = CRGB(SEGMENT.getPixelColor(index));\n    fastled_col |= (SEGMENT.palette==0)?CHSV(dothue, 220, 255):CRGB(ColorFromPalette(SEGPALETTE, dothue, 255));\n    SEGMENT.setPixelColor(index, fastled_col);\n    dothue += 32;\n  }\n}\nstatic const char _data_FX_MODE_JUGGLE[] PROGMEM = \"Juggle@!,Trail;;!;;sx=64,ix=128\";\n\n\nvoid mode_palette() {\n  // Set up some compile time constants so that we can handle integer and float based modes using the same code base.\n#ifdef ESP8266\n  using mathType = int32_t;\n  using wideMathType = int64_t;\n  using angleType = unsigned;\n  constexpr mathType sInt16Scale             = 0x7FFF;\n  constexpr mathType maxAngle                = 0x8000;\n  constexpr mathType staticRotationScale     = 256;\n  constexpr mathType animatedRotationScale   = 1;\n  constexpr int16_t (*sinFunction)(uint16_t) = &sin16_t;\n  constexpr int16_t (*cosFunction)(uint16_t) = &cos16_t;\n#else\n  using mathType = float;\n  using wideMathType = float;\n  using angleType = float;\n  constexpr mathType sInt16Scale           = 1.0f;\n  constexpr mathType maxAngle              = M_PI / 256.0;\n  constexpr mathType staticRotationScale   = 1.0f;\n  constexpr mathType animatedRotationScale = M_TWOPI / double(0xFFFF);\n  constexpr float (*sinFunction)(float)    = &sin_t;\n  constexpr float (*cosFunction)(float)    = &cos_t;\n#endif\n  const bool isMatrix = strip.isMatrix;\n  const int cols = SEG_W;\n  const int rows = isMatrix ? SEG_H : strip.getActiveSegmentsNum();\n\n  const int  inputShift           = SEGMENT.speed;\n  const int  inputSize            = SEGMENT.intensity;\n  const int  inputRotation        = SEGMENT.custom1;\n  const bool inputAnimateShift    = SEGMENT.check1;\n  const bool inputAnimateRotation = SEGMENT.check2;\n  const bool inputAssumeSquare    = SEGMENT.check3;\n\n  const angleType theta = (!inputAnimateRotation) ? ((inputRotation + 128) * maxAngle / staticRotationScale) : (((strip.now * ((inputRotation >> 4) +1)) & 0xFFFF) * animatedRotationScale);\n  const mathType sinTheta = sinFunction(theta);\n  const mathType cosTheta = cosFunction(theta);\n\n  const mathType maxX    = std::max(1, cols-1);\n  const mathType maxY    = std::max(1, rows-1);\n  // Set up some parameters according to inputAssumeSquare, so that we can handle anamorphic mode using the same code base.\n  const mathType maxXIn  =  inputAssumeSquare ? maxX : mathType(1);\n  const mathType maxYIn  =  inputAssumeSquare ? maxY : mathType(1);\n  const mathType maxXOut = !inputAssumeSquare ? maxX : mathType(1);\n  const mathType maxYOut = !inputAssumeSquare ? maxY : mathType(1);\n  const mathType centerX = sInt16Scale * maxXOut / mathType(2);\n  const mathType centerY = sInt16Scale * maxYOut / mathType(2);\n  // The basic idea for this effect is to rotate a rectangle that is filled with the palette along one axis, then map our\n  // display to it, to find what color a pixel should have.\n  // However, we want a) no areas of solid color (in front of or behind the palette), and b) we want to make use of the full palette.\n  // So the rectangle needs to have exactly the right size. That size depends on the rotation.\n  // This scale computation here only considers one dimension. You can think of it like the rectangle is always scaled so that\n  // the left and right most points always match the left and right side of the display.\n  const mathType scale = std::abs(sinTheta) + (std::abs(cosTheta) * maxYOut / maxXOut);\n  // 2D simulation:\n  // If we are dealing with a 1D setup, we assume that each segment represents one line on a 2-dimensional display.\n  // The function is called once per segments, so we need to handle one line at a time.\n  const int yFrom = isMatrix ? 0 : strip.getCurrSegmentId();\n  const int yTo   = isMatrix ? maxY : yFrom;\n  for (int y = yFrom; y <= yTo; ++y) {\n    // translate, scale, rotate\n    const mathType ytCosTheta = mathType((wideMathType(cosTheta) * wideMathType(y * sInt16Scale - centerY * maxYIn))/wideMathType(maxYIn * scale));\n    for (int x = 0; x < cols; ++x) {\n      // translate, scale, rotate\n      const mathType xtSinTheta = mathType((wideMathType(sinTheta) * wideMathType(x * sInt16Scale - centerX * maxXIn))/wideMathType(maxXIn * scale));\n      // Map the pixel coordinate to an imaginary-rectangle-coordinate.\n      // The y coordinate doesn't actually matter, as our imaginary rectangle is filled with the palette from left to right,\n      // so all points at a given x-coordinate have the same color.\n      const mathType sourceX = xtSinTheta + ytCosTheta + centerX;\n      // The computation was scaled just right so that the result should always be in range [0, maxXOut], but enforce this anyway\n      // to account for imprecision. Then scale it so that the range is [0, 255], which we can use with the palette.\n      int colorIndex = (std::min(std::max(sourceX, mathType(0)), maxXOut * sInt16Scale) * wideMathType(255)) / (sInt16Scale * maxXOut);\n      // inputSize determines by how much we want to scale the palette:\n      // values < 128 display a fraction of a palette,\n      // values > 128 display multiple palettes.\n      if (inputSize <= 128) {\n        colorIndex = (colorIndex * inputSize) / 128;\n      } else {\n        // Linear function that maps colorIndex 128=>1, 256=>9.\n        // With this function every full palette repetition is exactly 16 configuration steps wide.\n        // That allows displaying exactly 2 repetitions for example.\n        colorIndex = ((inputSize - 112) * colorIndex) / 16;\n      }\n      // Finally, shift the palette a bit.\n      const int paletteOffset = (!inputAnimateShift) ? (inputShift) : (((strip.now * ((inputShift >> 3) +1)) & 0xFFFF) >> 8);\n      colorIndex -= paletteOffset;\n      const uint32_t color = SEGMENT.color_wheel((uint8_t)colorIndex);\n      if (isMatrix) {\n        SEGMENT.setPixelColorXY(x, y, color);\n      } else {\n        SEGMENT.setPixelColor(x, color);\n      }\n    }\n  }\n}\nstatic 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\";\n\n#if defined(WLED_PS_DONT_REPLACE_1D_FX) || defined(WLED_PS_DONT_REPLACE_2D_FX)\n// WLED limitation: Analog Clock overlay will NOT work when Fire2012 is active\n// Fire2012 by Mark Kriegsman, July 2012\n// as part of \"Five Elements\" shown here: http://youtu.be/knWiGsmgycY\n////\n// This basic one-dimensional 'fire' simulation works roughly as follows:\n// There's a underlying array of 'heat' cells, that model the temperature\n// at each point along the line.  Every cycle through the simulation,\n// four steps are performed:\n//  1) All cells cool down a little bit, losing heat to the air\n//  2) The heat from each cell drifts 'up' and diffuses a little\n//  3) Sometimes randomly new 'sparks' of heat are added at the bottom\n//  4) The heat from each cell is rendered as a color into the leds array\n//     The heat-to-color mapping uses a black-body radiation approximation.\n//\n// Temperature is in arbitrary units from 0 (cold black) to 255 (white hot).\n//\n// This simulation scales it self a bit depending on SEGLEN; it should look\n// \"OK\" on anywhere from 20 to 100 LEDs without too much tweaking.\n//\n// I recommend running this simulation at anywhere from 30-100 frames per second,\n// meaning an interframe delay of about 10-35 milliseconds.\n//\n// Looks best on a high-density LED setup (60+ pixels/meter).\n//\n//\n// There are two main parameters you can play with to control the look and\n// feel of your fire: COOLING (used in step 1 above) (Speed = COOLING), and SPARKING (used\n// in step 3 above) (Effect Intensity = Sparking).\nvoid mode_fire_2012() {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  const unsigned strips = SEGMENT.nrOfVStrips();\n  if (!SEGENV.allocateData(strips * SEGLEN)) FX_FALLBACK_STATIC; //allocation failed\n  byte* heat = SEGENV.data;\n\n  const uint32_t it = strip.now >> 5; //div 32\n\n  struct virtualStrip {\n    static void runStrip(uint16_t stripNr, byte* heat, uint32_t it) {\n\n      const uint8_t ignition = MAX(3,SEGLEN/10);  // ignition area: 10% of segment length or minimum 3 pixels\n\n      // Step 1.  Cool down every cell a little\n      for (unsigned i = 0; i < SEGLEN; i++) {\n        uint8_t cool = (it != SEGENV.step) ? hw_random8((((20 + SEGMENT.speed/3) * 16) / SEGLEN)+2) : hw_random8(4);\n        uint8_t minTemp = (i<ignition) ? (ignition-i)/4 + 16 : 0;  // should not become black in ignition area\n        uint8_t temp = qsub8(heat[i], cool);\n        heat[i] = temp<minTemp ? minTemp : temp;\n      }\n\n      if (it != SEGENV.step) {\n        // Step 2.  Heat from each cell drifts 'up' and diffuses a little\n        for (int k = SEGLEN -1; k > 1; k--) {\n          heat[k] = (heat[k - 1] + (heat[k - 2]<<1) ) / 3;  // heat[k-2] multiplied by 2\n        }\n\n        // Step 3.  Randomly ignite new 'sparks' of heat near the bottom\n        if (hw_random8() <= SEGMENT.intensity) {\n          uint8_t y = hw_random8(ignition);\n          uint8_t boost = (17+SEGMENT.custom3) * (ignition - y/2) / ignition; // integer math!\n          heat[y] = qadd8(heat[y], hw_random8(96+2*boost,207+boost));\n        }\n      }\n\n      // Step 4.  Map from heat cells to LED colors\n      for (unsigned j = 0; j < SEGLEN; j++) {\n        SEGMENT.setPixelColor(indexToVStrip(j, stripNr), ColorFromPalette(SEGPALETTE, min(heat[j], byte(240)), 255, NOBLEND));\n      }\n    }\n  };\n\n  for (unsigned stripNr=0; stripNr<strips; stripNr++)\n    virtualStrip::runStrip(stripNr, &heat[stripNr * SEGLEN], it);\n\n  if (SEGMENT.is2D()) {\n    uint8_t blurAmount = SEGMENT.custom2 >> 2;\n    if (blurAmount > 48) blurAmount += blurAmount-48;             // extra blur when slider > 192  (bush burn)\n    if (blurAmount < 16) SEGMENT.blurCols(SEGMENT.custom2 >> 1);  // no side-burn when slider < 64 (faster)\n    else SEGMENT.blur(blurAmount);\n  }\n\n  if (it != SEGENV.step)\n    SEGENV.step = it;\n}\nstatic 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\n#endif // WLED_PS_DONT_REPLACE_x_FX\n\n// colored stripes pulsing at a defined Beats-Per-Minute (BPM)\nvoid mode_bpm() {\n  uint32_t stp = (strip.now / 20) & 0xFF;\n  uint8_t beat = beatsin8_t(SEGMENT.speed, 64, 255);\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(stp + (i * 2), false, PALETTE_SOLID_WRAP, 0, beat - stp + (i * 10)));\n  }\n}\nstatic const char _data_FX_MODE_BPM[] PROGMEM = \"Bpm@!;!;!;;sx=64\";\n\n\nvoid mode_fillnoise8() {\n  if (SEGENV.call == 0) SEGENV.step = hw_random();\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    unsigned index = perlin8(i * SEGLEN, SEGENV.step + i * SEGLEN);\n    SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0));\n  }\n  SEGENV.step += beatsin8_t(SEGMENT.speed, 1, 6); //10,1,4\n}\nstatic const char _data_FX_MODE_FILLNOISE8[] PROGMEM = \"Fill Noise@!;!;!\";\n\n\nvoid mode_noise16_1() {\n  unsigned scale = 320;                                       // the \"zoom factor\" for the noise\n  SEGENV.step += (1 + SEGMENT.speed/16);\n\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    unsigned shift_x = beatsin8_t(11);                          // the x position of the noise field swings @ 17 bpm\n    unsigned shift_y = SEGENV.step/42;                        // the y position becomes slowly incremented\n    unsigned real_x = (i + shift_x) * scale;                  // the x position of the noise field swings @ 17 bpm\n    unsigned real_y = (i + shift_y) * scale;                  // the y position becomes slowly incremented\n    uint32_t real_z = SEGENV.step;                            // the z position becomes quickly incremented\n    unsigned noise = perlin16(real_x, real_y, real_z) >> 8;   // get the noise data and scale it down\n    unsigned index = sin8_t(noise * 3);                         // map LED color based on noise data\n\n    SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0));\n  }\n}\nstatic const char _data_FX_MODE_NOISE16_1[] PROGMEM = \"Noise 1@!;!;!;;pal=20\";\n\n\nvoid mode_noise16_2() {\n  unsigned scale = 1000;                                        // the \"zoom factor\" for the noise\n  SEGENV.step += (1 + (SEGMENT.speed >> 1));\n\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    unsigned shift_x = SEGENV.step >> 6;                        // x as a function of time\n    uint32_t real_x = (i + shift_x) * scale;                    // calculate the coordinates within the noise field\n    unsigned noise = perlin16(real_x, 0, 4223) >> 8;            // get the noise data and scale it down\n    unsigned index = sin8_t(noise * 3);                           // map led color based on noise data\n\n    SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0, noise));\n  }\n}\nstatic const char _data_FX_MODE_NOISE16_2[] PROGMEM = \"Noise 2@!;!;!;;pal=43\";\n\n\nvoid mode_noise16_3() {\n  unsigned scale = 800;                                       // the \"zoom factor\" for the noise\n  SEGENV.step += (1 + SEGMENT.speed);\n\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    unsigned shift_x = 4223;                                  // no movement along x and y\n    unsigned shift_y = 1234;\n    uint32_t real_x = (i + shift_x) * scale;                  // calculate the coordinates within the noise field\n    uint32_t real_y = (i + shift_y) * scale;                  // based on the precalculated positions\n    uint32_t real_z = SEGENV.step*8;\n    unsigned noise = perlin16(real_x, real_y, real_z) >> 8;   // get the noise data and scale it down\n    unsigned index = sin8_t(noise * 3);                         // map led color based on noise data\n\n    SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0, noise));\n  }\n}\nstatic const char _data_FX_MODE_NOISE16_3[] PROGMEM = \"Noise 3@!;!;!;;pal=35\";\n\n\n//https://github.com/aykevl/ledstrip-spark/blob/master/ledstrip.ino\nvoid mode_noise16_4() {\n  uint32_t stp = (strip.now * SEGMENT.speed) >> 7;\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    int index = perlin16(uint32_t(i) << 12, stp);\n    SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0));\n  }\n}\nstatic const char _data_FX_MODE_NOISE16_4[] PROGMEM = \"Noise 4@!;!;!;;pal=26\";\n\n\n//based on https://gist.github.com/kriegsman/5408ecd397744ba0393e\nvoid mode_colortwinkle() {\n  unsigned dataSize = (SEGLEN+7) >> 3; //1 bit per LED\n  if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed\n\n  //limit update rate\n  if (strip.now - SEGENV.step < FRAMETIME_FIXED) return;\n  SEGENV.step = strip.now;\n\n  CRGBW col, prev;\n  fract8 fadeUpAmount = strip.getBrightness()>28 ? 8 + (SEGMENT.speed>>2) : 68-strip.getBrightness();\n  fract8 fadeDownAmount = strip.getBrightness()>28 ? 8 + (SEGMENT.speed>>3) : 68-strip.getBrightness();\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    CRGBW cur = SEGMENT.getPixelColor(i);\n    prev = cur;\n    unsigned index = i >> 3;\n    unsigned  bitNum = i & 0x07;\n    bool fadeUp = bitRead(SEGENV.data[index], bitNum);\n\n    if (fadeUp) {\n      CRGBW incrementalColor = color_fade(cur, fadeUpAmount, true);\n      col = color_add(cur, incrementalColor);\n\n      if (col.r == 255 || col.g == 255 || col.b == 255) {\n        bitWrite(SEGENV.data[index], bitNum, false);\n      }\n\n      if (cur == prev) {  //fix \"stuck\" pixels\n        col = color_add(col, col);\n        SEGMENT.setPixelColor(i, col);\n      }\n      else SEGMENT.setPixelColor(i, col);\n    }\n    else {\n      col = color_fade(cur, 255 - fadeDownAmount);\n      SEGMENT.setPixelColor(i, col);\n    }\n  }\n\n  for (unsigned j = 0; j <= SEGLEN / 50; j++) {\n    if (hw_random8() <= SEGMENT.intensity) {\n      for (unsigned times = 0; times < 5; times++) { //attempt to spawn a new pixel 5 times\n        int i = hw_random16(SEGLEN);\n        if (SEGMENT.getPixelColor(i) == 0) {\n          unsigned index = i >> 3;\n          unsigned  bitNum = i & 0x07;\n          bitWrite(SEGENV.data[index], bitNum, true);\n          SEGMENT.setPixelColor(i, ColorFromPalette(SEGPALETTE, hw_random8(), 64, NOBLEND));\n          break; //only spawn 1 new pixel per frame per 50 LEDs\n        }\n      }\n    }\n  }\n}\nstatic const char _data_FX_MODE_COLORTWINKLE[] PROGMEM = \"Colortwinkles@Fade speed,Spawn speed;;!;;m12=0\"; //pixels\n\n\n//Calm effect, like a lake at night\nvoid mode_lake() {\n  unsigned sp = SEGMENT.speed/10;\n  int wave1 = beatsin8_t(sp +2, -64,64);\n  int wave2 = beatsin8_t(sp +1, -64,64);\n  int wave3 = beatsin8_t(sp +2,   0,80);\n\n  for (unsigned i = 0; i < SEGLEN; i++)\n  {\n    int index = cos8_t((i*15)+ wave1)/2 + cubicwave8((i*23)+ wave2)/2;\n    uint8_t lum = (index > wave3) ? index - wave3 : 0;\n    SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, false, 0, lum));\n  }\n}\nstatic const char _data_FX_MODE_LAKE[] PROGMEM = \"Lake@!;Fx;!\";\n\n\n// meteor effect & meteor smooth (merged by @dedehai)\n// send a meteor from begining to to the end of the strip with a trail that randomly decays.\n// adapted from https://www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/#LEDStripEffectMeteorRain\nvoid mode_meteor() {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  if (!SEGENV.allocateData(SEGLEN)) FX_FALLBACK_STATIC; //allocation failed\n  const bool meteorSmooth = SEGMENT.check3;\n  byte* trail = SEGENV.data;\n\n  const unsigned meteorSize = 1 + SEGLEN / 20; // 5%\n  uint16_t meteorstart;\n  if(meteorSmooth) meteorstart = map((SEGENV.step >> 6 & 0xFF), 0, 255, 0, SEGLEN -1);\n  else {\n    unsigned counter = strip.now * ((SEGMENT.speed >> 2) + 8);\n    meteorstart = (counter * SEGLEN) >> 16;\n  }\n\n  const int max = SEGMENT.palette==5 || !SEGMENT.check1 ? 240 : 255;\n  // fade all leds to colors[1] in LEDs one step\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    uint32_t col;\n    if (hw_random8() <= 255 - SEGMENT.intensity) {\n      if(meteorSmooth) {\n        if (trail[i] > 0) {\n          int change = trail[i] + 4 - hw_random8(24); //change each time between -20 and +4\n          trail[i] = constrain(change, 0, max);\n        }\n        col = SEGMENT.check1 ? SEGMENT.color_from_palette(i, true, false, 0, trail[i]) : SEGMENT.color_from_palette(trail[i], false, true, 255);\n      }\n      else {\n        trail[i] = scale8(trail[i], 128 + hw_random8(127));\n        int index = trail[i];\n        int idx = 255;\n        int bri = SEGMENT.palette==35 || SEGMENT.palette==36 ? 255 : trail[i];\n        if (!SEGMENT.check1) {\n          idx = 0;\n          index = map(i,0,SEGLEN,0,max);\n          bri = trail[i];\n        }\n        col = SEGMENT.color_from_palette(index, false, false, idx, bri);  // full brightness for Fire\n      }\n      SEGMENT.setPixelColor(i, col);\n    }\n  }\n\n  // draw meteor\n  for (unsigned j = 0; j < meteorSize; j++) {\n    unsigned index = (meteorstart + j) % SEGLEN;\n    if(meteorSmooth) {\n        trail[index] = max;\n        uint32_t col = SEGMENT.check1 ? SEGMENT.color_from_palette(index, true, false, 0, trail[index]) : SEGMENT.color_from_palette(trail[index], false, true, 255);\n        SEGMENT.setPixelColor(index, col);\n    }\n    else{\n      int idx = 255;\n      int i = trail[index] = max;\n      if (!SEGMENT.check1) {\n        i = map(index,0,SEGLEN,0,max);\n        idx = 0;\n      }\n      uint32_t col = SEGMENT.color_from_palette(i, false, false, idx, 255); // full brightness\n      SEGMENT.setPixelColor(index, col);\n    }\n  }\n\n  SEGENV.step += SEGMENT.speed +1;\n}\nstatic const char _data_FX_MODE_METEOR[] PROGMEM = \"Meteor@!,Trail,,,,Gradient,,Smooth;;!;1\";\n\n\n//Railway Crossing / Christmas Fairy lights\nvoid mode_railway() {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  unsigned dur = (256 - SEGMENT.speed) * 40;\n  uint16_t rampdur = (dur * SEGMENT.intensity) >> 8;\n  if (SEGENV.step > dur)\n  {\n    //reverse direction\n    SEGENV.step = 0;\n    SEGENV.aux0 = !SEGENV.aux0;\n  }\n  unsigned pos = 255;\n  if (rampdur != 0)\n  {\n    unsigned p0 = (SEGENV.step * 255) / rampdur;\n    if (p0 < 255) pos = p0;\n  }\n  if (SEGENV.aux0) pos = 255 - pos;\n  for (unsigned i = 0; i < SEGLEN; i += 2)\n  {\n    SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(255 - pos, false, false, 255)); // do not use color 1 or 2, always use palette\n    if (i < SEGLEN -1)\n    {\n      SEGMENT.setPixelColor(i + 1, SEGMENT.color_from_palette(pos, false, false, 255)); // do not use color 1 or 2, always use palette\n    }\n  }\n  SEGENV.step += FRAMETIME;\n}\nstatic const char _data_FX_MODE_RAILWAY[] PROGMEM = \"Railway@!,Smoothness;1,2;!;;pal=3\";\n\n\n//Water ripple\n//propagation velocity from speed\n//drop rate from intensity\n\n//4 bytes\ntypedef struct Ripple {\n  uint8_t state;\n  uint8_t color;\n  uint16_t pos;\n} ripple;\n\n#ifdef ESP8266\n  #define MAX_RIPPLES   56\n#else\n  #define MAX_RIPPLES  100\n#endif\nstatic void ripple_base(uint8_t blurAmount = 0) {\n  unsigned maxRipples = min(1 + (int)(SEGLEN >> 2), MAX_RIPPLES);  // 56 max for 16 segment ESP8266\n  unsigned dataSize = sizeof(ripple) * maxRipples;\n\n  if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed\n\n  Ripple* ripples = reinterpret_cast<Ripple*>(SEGENV.data);\n\n  //draw wave\n  for (unsigned i = 0; i < maxRipples; i++) {\n    unsigned ripplestate = ripples[i].state;\n    if (ripplestate) {\n      unsigned rippledecay = (SEGMENT.speed >> 4) +1; //faster decay if faster propagation\n      unsigned rippleorigin = ripples[i].pos;\n      uint32_t col = SEGMENT.color_from_palette(ripples[i].color, false, false, 255);\n      unsigned propagation = ((ripplestate/rippledecay - 1) * (SEGMENT.speed + 1));\n      int propI = propagation >> 8;\n      unsigned propF = propagation & 0xFF;\n      unsigned amp = (ripplestate < 17) ? triwave8((ripplestate-1)*8) : map(ripplestate,17,255,255,2);\n\n      #ifndef WLED_DISABLE_2D\n      if (SEGMENT.is2D()) {\n        propI /= 2;\n        unsigned cx = rippleorigin >> 8;\n        unsigned cy = rippleorigin & 0xFF;\n        unsigned mag = scale8(sin8_t((propF>>2)), amp);\n        if (propI > 0) SEGMENT.drawCircle(cx, cy, propI, color_blend(SEGMENT.getPixelColorXY(cx + propI, cy), col, mag), true);\n      } else\n      #endif\n      {\n        int left = rippleorigin - propI -1;\n        int right = rippleorigin + propI +2;\n        for (int v = 0; v < 4; v++) {\n          uint8_t mag = scale8(cubicwave8((propF>>2) + v * 64), amp);\n          SEGMENT.setPixelColor(left + v, color_blend(SEGMENT.getPixelColor(left + v), col, mag)); // TODO\n          SEGMENT.setPixelColor(right - v, color_blend(SEGMENT.getPixelColor(right - v), col, mag)); // TODO\n        }\n      }\n      ripplestate += rippledecay;\n      ripples[i].state = (ripplestate > 254) ? 0 : ripplestate;\n    } else {//randomly create new wave\n      if (hw_random16(IBN + 10000) <= (SEGMENT.intensity >> (SEGMENT.is2D()*3))) {\n        ripples[i].state = 1;\n        ripples[i].pos = SEGMENT.is2D() ? ((hw_random8(SEG_W)<<8) | (hw_random8(SEG_H))) : hw_random16(SEGLEN);\n        ripples[i].color = hw_random8(); //color\n      }\n    }\n  }\n  SEGMENT.blur(blurAmount);\n}\n#undef MAX_RIPPLES\n\n\nvoid mode_ripple(void) {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  if(SEGMENT.custom1 || SEGMENT.check2) // blur or overlay\n    SEGMENT.fade_out(250);\n  else\n    SEGMENT.fill(SEGCOLOR(1));\n\n  ripple_base(SEGMENT.custom1>>1);\n}\nstatic const char _data_FX_MODE_RIPPLE[] PROGMEM = \"Ripple@!,Wave #,Blur,,,,Overlay;,!;!;12;c1=0\";\n\n\nvoid mode_ripple_rainbow(void) {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  if (SEGENV.call ==0) {\n    SEGENV.aux0 = hw_random8();\n    SEGENV.aux1 = hw_random8();\n  }\n  if (SEGENV.aux0 == SEGENV.aux1) {\n    SEGENV.aux1 = hw_random8();\n  } else if (SEGENV.aux1 > SEGENV.aux0) {\n    SEGENV.aux0++;\n  } else {\n    SEGENV.aux0--;\n  }\n  SEGMENT.fill(color_blend(SEGMENT.color_wheel(SEGENV.aux0),BLACK,uint8_t(235)));\n  ripple_base();\n}\nstatic const char _data_FX_MODE_RIPPLE_RAINBOW[] PROGMEM = \"Ripple Rainbow@!,Wave #;;!;12\";\n\n\n//  TwinkleFOX by Mark Kriegsman: https://gist.github.com/kriegsman/756ea6dcae8e30845b5a\n//\n//  TwinkleFOX: Twinkling 'holiday' lights that fade in and out.\n//  Colors are chosen from a palette. Read more about this effect using the link above!\nstatic CRGB twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool cat)\n{\n  // Overall twinkle speed (changed)\n  unsigned ticks = ms / SEGENV.aux0;\n  unsigned fastcycle8 = uint8_t(ticks);\n  uint16_t slowcycle16 = (ticks >> 8) + salt;\n  slowcycle16 += sin8_t(slowcycle16);\n  slowcycle16 = (slowcycle16 * 2053) + 1384;\n  uint8_t slowcycle8 = (slowcycle16 & 0xFF) + (slowcycle16 >> 8);\n\n  // Overall twinkle density.\n  // 0 (NONE lit) to 8 (ALL lit at once).\n  // Default is 5.\n  unsigned twinkleDensity = (SEGMENT.intensity >> 5) +1;\n\n  unsigned bright = 0;\n  if (((slowcycle8 & 0x0E)/2) < twinkleDensity) {\n    unsigned ph = fastcycle8;\n    // This is like 'triwave8', which produces a\n    // symmetrical up-and-down triangle sawtooth waveform, except that this\n    // function produces a triangle wave with a faster attack and a slower decay\n    if (cat) { //twinklecat, variant where the leds instantly turn on and fade off\n      bright = 255 - ph;\n      if (SEGMENT.check2) { //reverse checkbox, reverses the leds to fade on and instantly turn off\n        bright = ph;\n      }\n    } else { //vanilla twinklefox\n      if (ph < 86) {\n      bright = ph * 3;\n      } else {\n        ph -= 86;\n        bright = 255 - (ph + (ph/2));\n      }\n    }\n  }\n\n  unsigned hue = slowcycle8 - salt;\n  CRGB c;\n  if (bright > 0) {\n    c = ColorFromPalette(SEGPALETTE, hue, bright, NOBLEND);\n    if (!SEGMENT.check1) {\n      // This code takes a pixel, and if its in the 'fading down'\n      // part of the cycle, it adjusts the color a little bit like the\n      // way that incandescent bulbs fade toward 'red' as they dim.\n      if (fastcycle8 >= 128)\n      {\n        unsigned cooling = (fastcycle8 - 128) >> 4;\n        c.g = qsub8(c.g, cooling);\n        c.b = qsub8(c.b, cooling * 2);\n      }\n    }\n  } else {\n    c = CRGB::Black;\n  }\n  return c;\n}\n\n//  This function loops over each pixel, calculates the\n//  adjusted 'clock' that this pixel should use, and calls\n//  \"CalculateOneTwinkle\" on each pixel.  It then displays\n//  either the twinkle color of the background color,\n//  whichever is brighter.\nstatic void twinklefox_base(bool cat)\n{\n  // \"PRNG16\" is the pseudorandom number generator\n  // It MUST be reset to the same starting value each time\n  // this function is called, so that the sequence of 'random'\n  // numbers that it generates is (paradoxically) stable.\n  uint16_t PRNG16 = 11337;\n\n  // Calculate speed\n  if (SEGMENT.speed > 100) SEGENV.aux0 = 3 + ((255 - SEGMENT.speed) >> 3);\n  else SEGENV.aux0 = 22 + ((100 - SEGMENT.speed) >> 1);\n\n  // Set up the background color, \"bg\".\n  CRGB bg = CRGB(SEGCOLOR(1));\n  unsigned bglight = bg.getAverageLight();\n  if (bglight > 64) {\n    bg.nscale8_video(16); // very bright, so scale to 1/16th\n  } else if (bglight > 16) {\n    bg.nscale8_video(64); // not that bright, so scale to 1/4th\n  } else {\n    bg.nscale8_video(86); // dim, scale to 1/3rd.\n  }\n\n  unsigned backgroundBrightness = bg.getAverageLight();\n\n  for (unsigned i = 0; i < SEGLEN; i++) {\n\n    PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; // next 'random' number\n    unsigned myclockoffset16= PRNG16; // use that number as clock offset\n    PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; // next 'random' number\n    // use that number as clock speed adjustment factor (in 8ths, from 8/8ths to 23/8ths)\n    unsigned myspeedmultiplierQ5_3 =  ((((PRNG16 & 0xFF)>>4) + (PRNG16 & 0x0F)) & 0x0F) + 0x08;\n    uint32_t myclock30 = (uint32_t)((strip.now * myspeedmultiplierQ5_3) >> 3) + myclockoffset16;\n    unsigned  myunique8 = PRNG16 >> 8; // get 'salt' value for this pixel\n\n    // We now have the adjusted 'clock' for this pixel, now we call\n    // the function that computes what color the pixel should be based\n    // on the \"brightness = f( time )\" idea.\n    CRGB c = twinklefox_one_twinkle(myclock30, myunique8, cat);\n\n    unsigned cbright = c.getAverageLight();\n    int deltabright = cbright - backgroundBrightness;\n    if (deltabright >= 32 || (!bg)) {\n      // If the new pixel is significantly brighter than the background color,\n      // use the new color.\n      SEGMENT.setPixelColor(i, c);\n    } else if (deltabright > 0) {\n      // If the new pixel is just slightly brighter than the background color,\n      // mix a blend of the new color and the background color\n      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)));\n    } else {\n      // if the new pixel is not at all brighter than the background color,\n      // just use the background color.\n      SEGMENT.setPixelColor(i, bg);\n    }\n  }\n}\n\nvoid mode_twinklefox()\n{\n  twinklefox_base(false);\n}\nstatic const char _data_FX_MODE_TWINKLEFOX[] PROGMEM = \"Twinklefox@!,Twinkle rate,,,,Cool;!,!;!\";\n\n\nvoid mode_twinklecat()\n{\n  twinklefox_base(true);\n}\nstatic const char _data_FX_MODE_TWINKLECAT[] PROGMEM = \"Twinklecat@!,Twinkle rate,,,,Cool,Reverse;!,!;!\";\n\n\nvoid mode_halloween_eyes()\n{\n  enum eyeState : uint8_t {\n    initializeOn = 0,\n    on,\n    blink,\n    initializeOff,\n    off,\n\n    count\n  };\n  struct EyeData {\n    eyeState state;\n    uint8_t color;\n    uint16_t startPos;\n    // duration + endTime could theoretically be replaced by a single endTime, however we would lose\n    // the ability to end the animation early when the user reduces the animation time.\n    uint16_t duration;\n    uint32_t startTime;\n    uint32_t blinkEndTime;\n  };\n\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  const unsigned maxWidth = strip.isMatrix ? SEG_W : SEGLEN;\n  const unsigned HALLOWEEN_EYE_SPACE = MAX(2, strip.isMatrix ? SEG_W>>4: SEGLEN>>5);\n  const unsigned HALLOWEEN_EYE_WIDTH = HALLOWEEN_EYE_SPACE/2;\n  unsigned eyeLength = (2*HALLOWEEN_EYE_WIDTH) + HALLOWEEN_EYE_SPACE;\n  if (eyeLength >= maxWidth) FX_FALLBACK_STATIC; //bail if segment too short\n\n  if (!SEGENV.allocateData(sizeof(EyeData))) FX_FALLBACK_STATIC; //allocation failed\n  EyeData& data = *reinterpret_cast<EyeData*>(SEGENV.data);\n\n  if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); //fill background\n\n  data.state = static_cast<eyeState>(data.state % eyeState::count);\n  unsigned duration = max(uint16_t{1u}, data.duration);\n  const uint32_t elapsedTime = strip.now - data.startTime;\n\n  switch (data.state) {\n    case eyeState::initializeOn: {\n      // initialize the eyes-on state:\n      // - select eye position and color\n      // - select a duration\n      // - immediately switch to eyes on state.\n\n      data.startPos = hw_random16(0, maxWidth - eyeLength - 1);\n      data.color = hw_random8();\n      if (strip.isMatrix) SEGMENT.offset = hw_random16(SEG_H-1); // a hack: reuse offset since it is not used in matrices\n      duration = 128u + hw_random16(SEGMENT.intensity*64u);\n      data.duration = duration;\n      data.state = eyeState::on;\n      [[fallthrough]];\n    }\n    case eyeState::on: {\n      // eyes-on steate:\n      // - fade eyes in for some time\n      // - keep eyes on until the pre-selected duration is over\n      // - randomly switch to the blink (sub-)state, and initialize it with a blink duration (more precisely, a blink end time stamp)\n      // - never switch to the blink state if the animation just started or is about to end\n\n      unsigned start2ndEye = data.startPos + HALLOWEEN_EYE_WIDTH + HALLOWEEN_EYE_SPACE;\n      // If the user reduces the input while in this state, limit the duration.\n      duration = min(duration, (128u + (SEGMENT.intensity * 64u)));\n\n      constexpr uint32_t minimumOnTimeBegin = 1024u;\n      constexpr uint32_t minimumOnTimeEnd = 1024u;\n      const uint32_t fadeInAnimationState = elapsedTime * uint32_t{256u * 8u} / duration;\n      const uint32_t backgroundColor = SEGCOLOR(1);\n      const uint32_t eyeColor = SEGMENT.color_from_palette(data.color, false, false, 0);\n      uint32_t c = eyeColor;\n      if (fadeInAnimationState < 256u) {\n        c = color_blend(backgroundColor, eyeColor, uint8_t(fadeInAnimationState));\n      } else if (elapsedTime > minimumOnTimeBegin) {\n        const uint32_t remainingTime = (elapsedTime >= duration) ? 0u : (duration - elapsedTime);\n        if (remainingTime > minimumOnTimeEnd) {\n          if (hw_random8() < 4u)\n          {\n            c = backgroundColor;\n            data.state = eyeState::blink;\n            data.blinkEndTime = strip.now + hw_random8(8, 128);\n          }\n        }\n      }\n\n      if (c != backgroundColor) {\n        // render eyes\n        for (unsigned i = 0; i < HALLOWEEN_EYE_WIDTH; i++) {\n          if (strip.isMatrix) {\n            SEGMENT.setPixelColorXY(data.startPos + i, (unsigned)SEGMENT.offset, c);\n            SEGMENT.setPixelColorXY(start2ndEye   + i, (unsigned)SEGMENT.offset, c);\n          } else {\n            SEGMENT.setPixelColor(data.startPos + i, c);\n            SEGMENT.setPixelColor(start2ndEye   + i, c);\n          }\n        }\n      }\n      break;\n    }\n    case eyeState::blink: {\n      // eyes-on but currently blinking state:\n      // - wait until the blink time is over, then switch back to eyes-on\n\n      if (strip.now >= data.blinkEndTime) {\n        data.state = eyeState::on;\n      }\n      break;\n    }\n    case eyeState::initializeOff: {\n      // initialize eyes-off state:\n      // - select a duration\n      // - immediately switch to eyes-off state\n\n      const unsigned eyeOffTimeBase = SEGMENT.speed*128u;\n      duration = eyeOffTimeBase + hw_random16(eyeOffTimeBase);\n      data.duration = duration;\n      data.state = eyeState::off;\n      [[fallthrough]];\n    }\n    case eyeState::off: {\n      // eyes-off state:\n      // - not much to do here\n\n      // If the user reduces the input while in this state, limit the duration.\n      const unsigned eyeOffTimeBase = SEGMENT.speed*128u;\n      duration = min(duration, (2u * eyeOffTimeBase));\n      break;\n    }\n    case eyeState::count: {\n      // Can't happen, not an actual state.\n      data.state = eyeState::initializeOn;\n      break;\n    }\n  }\n\n  if (elapsedTime > duration) {\n    // The current state duration is over, switch to the next state.\n    switch (data.state) {\n      case eyeState::initializeOn:\n      case eyeState::on:\n      case eyeState::blink:\n        data.state = eyeState::initializeOff;\n        break;\n      case eyeState::initializeOff:\n      case eyeState::off:\n      case eyeState::count:\n      default:\n        data.state = eyeState::initializeOn;\n        break;\n    }\n    data.startTime = strip.now;\n  }\n}\nstatic const char _data_FX_MODE_HALLOWEEN_EYES[] PROGMEM = \"Halloween Eyes@Eye off time,Eye on time,,,,,Overlay;!,!;!;12\";\n\n\n//Speed slider sets amount of LEDs lit, intensity sets unlit\nvoid mode_static_pattern()\n{\n  unsigned lit = 1 + SEGMENT.speed;\n  unsigned unlit = 1 + SEGMENT.intensity;\n  bool drawingLit = true;\n  unsigned cnt = 0;\n\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    SEGMENT.setPixelColor(i, (drawingLit) ? SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0) : SEGCOLOR(1));\n    cnt++;\n    if (cnt >= ((drawingLit) ? lit : unlit)) {\n      cnt = 0;\n      drawingLit = !drawingLit;\n    }\n  }\n}\nstatic const char _data_FX_MODE_STATIC_PATTERN[] PROGMEM = \"Solid Pattern@Fg size,Bg size;Fg,!;!;;pal=0\";\n\n\nvoid mode_tri_static_pattern()\n{\n  unsigned segSize = (SEGMENT.intensity >> 5) +1;\n  unsigned currSeg = 0;\n  unsigned currSegCount = 0;\n\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    if ( currSeg % 3 == 0 ) {\n      SEGMENT.setPixelColor(i, SEGCOLOR(0));\n    } else if( currSeg % 3 == 1) {\n      SEGMENT.setPixelColor(i, SEGCOLOR(1));\n    } else {\n      SEGMENT.setPixelColor(i, SEGCOLOR(2));\n    }\n    currSegCount += 1;\n    if (currSegCount >= segSize) {\n      currSeg +=1;\n      currSegCount = 0;\n    }\n  }\n}\nstatic const char _data_FX_MODE_TRI_STATIC_PATTERN[] PROGMEM = \"Solid Pattern Tri@,Size;1,2,3;;;pal=0\";\n\n\nstatic void spots_base(uint16_t threshold)\n{\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1));\n\n  unsigned maxZones = SEGLEN >> 2;\n  unsigned zones = 1 + ((SEGMENT.intensity * maxZones) >> 8);\n  unsigned zoneLen = SEGLEN / zones;\n  unsigned offset = (SEGLEN - zones * zoneLen) >> 1;\n\n  for (unsigned z = 0; z < zones; z++)\n  {\n    unsigned pos = offset + z * zoneLen;\n    for (unsigned i = 0; i < zoneLen; i++)\n    {\n      unsigned wave = triwave16((i * 0xFFFF) / zoneLen);\n      if (wave > threshold) {\n        unsigned index = 0 + pos + i;\n        unsigned s = (wave - threshold)*255 / (0xFFFF - threshold);\n        SEGMENT.setPixelColor(index, color_blend(SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0), SEGCOLOR(1), uint8_t(255-s)));\n      }\n    }\n  }\n}\n\n\n//Intensity slider sets number of \"lights\", speed sets LEDs per light\nvoid mode_spots()\n{\n  spots_base((255 - SEGMENT.speed) << 8);\n}\nstatic const char _data_FX_MODE_SPOTS[] PROGMEM = \"Spots@Spread,Width,,,,,Overlay;!,!;!\";\n\n\n//Intensity slider sets number of \"lights\", LEDs per light fade in and out\nvoid mode_spots_fade()\n{\n  unsigned counter = strip.now * ((SEGMENT.speed >> 2) +8);\n  unsigned t = triwave16(counter);\n  unsigned tr = (t >> 1) + (t >> 2);\n  spots_base(tr);\n}\nstatic const char _data_FX_MODE_SPOTS_FADE[] PROGMEM = \"Spots Fade@Spread,Width,,,,,Overlay;!,!;!\";\n\n//each needs 12 bytes\ntypedef struct Ball {\n  unsigned long lastBounceTime;\n  float impactVelocity;\n  float height;\n} ball;\n\n/*\n*  Bouncing Balls Effect\n*/\nvoid mode_bouncing_balls(void) {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  //allocate segment data\n  const unsigned strips = SEGMENT.nrOfVStrips(); // adapt for 2D\n  const size_t maxNumBalls = 16;\n  unsigned dataSize = sizeof(ball) * maxNumBalls;\n  if (!SEGENV.allocateData(dataSize * strips)) FX_FALLBACK_STATIC; //allocation failed\n\n  Ball* balls = reinterpret_cast<Ball*>(SEGENV.data);\n\n  if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(2) ? BLACK : SEGCOLOR(1));\n\n  // virtualStrip idea by @ewowi (Ewoud Wijma)\n  // requires virtual strip # to be embedded into upper 16 bits of index in setPixelColor()\n  // the following functions will not work on virtual strips: fill(), fade_out(), fadeToBlack(), blur()\n  struct virtualStrip {\n    static void runStrip(size_t stripNr, Ball* balls) {\n      // number of balls based on intensity setting to max of 7 (cycles colors)\n      // non-chosen color is a random color\n      unsigned numBalls = (SEGMENT.intensity * (maxNumBalls - 1)) / 255 + 1; // minimum 1 ball\n      const float gravity = -9.81f; // standard value of gravity\n      const bool hasCol2 = SEGCOLOR(2);\n      const unsigned long time = strip.now;\n\n      if (SEGENV.call == 0) {\n        for (size_t i = 0; i < maxNumBalls; i++) balls[i].lastBounceTime = time;\n      }\n\n      for (size_t i = 0; i < numBalls; i++) {\n        float timeSinceLastBounce = (time - balls[i].lastBounceTime)/((255-SEGMENT.speed)/64 +1);\n        float timeSec = timeSinceLastBounce/1000.0f;\n        balls[i].height = (0.5f * gravity * timeSec + balls[i].impactVelocity) * timeSec; // avoid use pow(x, 2) - its extremely slow !\n\n        if (balls[i].height <= 0.0f) {\n          balls[i].height = 0.0f;\n          //damping for better effect using multiple balls\n          float dampening = 0.9f - float(i)/float(numBalls * numBalls); // avoid use pow(x, 2) - its extremely slow !\n          balls[i].impactVelocity = dampening * balls[i].impactVelocity;\n          balls[i].lastBounceTime = time;\n\n          if (balls[i].impactVelocity < 0.015f) {\n            float impactVelocityStart = sqrtf(-2.0f * gravity) * hw_random8(5,11)/10.0f; // randomize impact velocity\n            balls[i].impactVelocity = impactVelocityStart;\n          }\n        } else if (balls[i].height > 1.0f) {\n          continue; // do not draw OOB ball\n        }\n\n        uint32_t color = SEGCOLOR(0);\n        if (SEGMENT.palette) {\n          color = SEGMENT.color_wheel(i*(256/MAX(numBalls, 8)));\n        } else if (hasCol2) {\n          color = SEGCOLOR(i % NUM_COLORS);\n        }\n\n        int pos = roundf(balls[i].height * (SEGLEN - 1));\n        #ifdef WLED_USE_AA_PIXELS\n        if (SEGLEN<32) SEGMENT.setPixelColor(indexToVStrip(pos, stripNr), color); // encode virtual strip into index\n        else           SEGMENT.setPixelColor(balls[i].height + (stripNr+1)*10.0f, color);\n        #else\n        SEGMENT.setPixelColor(indexToVStrip(pos, stripNr), color); // encode virtual strip into index\n        #endif\n      }\n    }\n  };\n\n  for (unsigned stripNr=0; stripNr<strips; stripNr++)\n    virtualStrip::runStrip(stripNr, &balls[stripNr * maxNumBalls]);\n}\nstatic const char _data_FX_MODE_BOUNCINGBALLS[] PROGMEM = \"Bouncing Balls@Gravity,# of balls,,,,,Overlay;!,!,!;!;1;m12=1\"; //bar\n\n#ifdef WLED_PS_DONT_REPLACE_1D_FX\n/*\n *  bouncing balls on a track track Effect modified from Aircoookie's bouncing balls\n *  Courtesy of pjhatch (https://github.com/pjhatch)\n *  https://github.com/wled-dev/WLED/pull/1039\n */\n// modified for balltrack mode\ntypedef struct RollingBall {\n  unsigned long lastBounceUpdate;\n  float mass; // could fix this to be = 1. if memory is an issue\n  float velocity;\n  float height;\n} rball_t;\n\nstatic void mode_rolling_balls(void) {\n  //allocate segment data\n  const unsigned maxNumBalls = 16; // 255/16 + 1\n  unsigned dataSize = sizeof(rball_t) * maxNumBalls;\n  if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed\n\n  rball_t *balls = reinterpret_cast<rball_t *>(SEGENV.data);\n\n  // number of balls based on intensity setting to max of 16 (cycles colors)\n  // non-chosen color is a random color\n  unsigned numBalls = SEGMENT.intensity/16 + 1;\n  bool hasCol2 = SEGCOLOR(2);\n\n  if (SEGENV.call == 0) {\n    SEGMENT.fill(hasCol2 ? BLACK : SEGCOLOR(1));                    // start clean\n    for (unsigned i = 0; i < maxNumBalls; i++) {\n      balls[i].lastBounceUpdate = strip.now;\n      balls[i].velocity = 20.0f * float(hw_random16(1000, 10000))/10000.0f;  // number from 1 to 10\n      if (hw_random8()<128) balls[i].velocity = -balls[i].velocity;    // 50% chance of reverse direction\n      balls[i].height = (float(hw_random16(0, 10000)) / 10000.0f);     // from 0. to 1.\n      balls[i].mass   = (float(hw_random16(1000, 10000)) / 10000.0f);  // from .1 to 1.\n    }\n  }\n\n  float cfac = float(scale8(8, 255-SEGMENT.speed) +1)*20000.0f; // this uses the Aircoookie conversion factor for scaling time using speed slider\n\n  if (SEGMENT.check3) SEGMENT.fade_out(250); // 2-8 pixel trails (optional)\n  else {\n  \tif (!SEGMENT.check2) SEGMENT.fill(hasCol2 ? BLACK : SEGCOLOR(1)); // don't fill with background color if user wants to see trails\n  }\n\n  for (unsigned i = 0; i < numBalls; i++) {\n    float timeSinceLastUpdate = float((strip.now - balls[i].lastBounceUpdate))/cfac;\n    float thisHeight = balls[i].height + balls[i].velocity * timeSinceLastUpdate; // this method keeps higher resolution\n    // test if intensity level was increased and some balls are way off the track then put them back\n    if (thisHeight < -0.5f || thisHeight > 1.5f) {\n      thisHeight = balls[i].height = (float(hw_random16(0, 10000)) / 10000.0f); // from 0. to 1.\n      balls[i].lastBounceUpdate = strip.now;\n    }\n    // check if reached ends of the strip\n    if ((thisHeight <= 0.0f && balls[i].velocity < 0.0f) || (thisHeight >= 1.0f && balls[i].velocity > 0.0f)) {\n      balls[i].velocity = -balls[i].velocity; // reverse velocity\n      balls[i].lastBounceUpdate = strip.now;\n      balls[i].height = thisHeight;\n    }\n    // check for collisions\n    if (SEGMENT.check1) {\n      for (unsigned j = i+1; j < numBalls; j++) {\n        if (balls[j].velocity != balls[i].velocity) {\n          //  tcollided + balls[j].lastBounceUpdate is acutal time of collision (this keeps precision with long to float conversions)\n          float tcollided = (cfac*(balls[i].height - balls[j].height) +\n                balls[i].velocity*float(balls[j].lastBounceUpdate - balls[i].lastBounceUpdate))/(balls[j].velocity - balls[i].velocity);\n\n          if ((tcollided > 2.0f) && (tcollided < float(strip.now - balls[j].lastBounceUpdate))) { // 2ms minimum to avoid duplicate bounces\n            balls[i].height = balls[i].height + balls[i].velocity*(tcollided + float(balls[j].lastBounceUpdate - balls[i].lastBounceUpdate))/cfac;\n            balls[j].height = balls[i].height;\n            balls[i].lastBounceUpdate = (unsigned long)(tcollided + 0.5f) + balls[j].lastBounceUpdate;\n            balls[j].lastBounceUpdate = balls[i].lastBounceUpdate;\n            float vtmp = balls[i].velocity;\n            balls[i].velocity = ((balls[i].mass - balls[j].mass)*vtmp              + 2.0f*balls[j].mass*balls[j].velocity)/(balls[i].mass + balls[j].mass);\n            balls[j].velocity = ((balls[j].mass - balls[i].mass)*balls[j].velocity + 2.0f*balls[i].mass*vtmp)             /(balls[i].mass + balls[j].mass);\n            thisHeight = balls[i].height + balls[i].velocity*(strip.now - balls[i].lastBounceUpdate)/cfac;\n          }\n        }\n      }\n    }\n\n    uint32_t color = SEGCOLOR(0);\n    if (SEGMENT.palette) {\n      //color = SEGMENT.color_wheel(i*(256/MAX(numBalls, 8)));\n      color = SEGMENT.color_from_palette(i*255/numBalls, false, PALETTE_SOLID_WRAP, 0);\n    } else if (hasCol2) {\n      color = SEGCOLOR(i % NUM_COLORS);\n    }\n\n    if (thisHeight < 0.0f) thisHeight = 0.0f;\n    if (thisHeight > 1.0f) thisHeight = 1.0f;\n    unsigned pos = round(thisHeight * (SEGLEN - 1));\n    SEGMENT.setPixelColor(pos, color);\n    balls[i].lastBounceUpdate = strip.now;\n    balls[i].height = thisHeight;\n  }\n}\nstatic const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = \"Rolling Balls@!,# of balls,,,,Collide,Overlay,Trails;!,!,!;!;1;m12=1\"; //bar\n#endif // WLED_PS_DONT_REPLACE_1D_FX\n\n\n/*\n/  Pac-Man by Bob Loeffler with help from @dedehai and @blazoncek\n*   speed slider is for speed.\n*   intensity slider is for selecting the number of power dots.\n*   custom1 slider is for selecting the LED where the ghosts will start blinking blue.\n*   custom2 slider is for blurring the LEDs in the segment.\n*   custom3 slider is for selecting the # of ghosts (between 2 and 8).\n*   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).\n*   check2 is for Smear mode (enabled will smear/persist the LED colors, disabled will not).\n*   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.\n*   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.\n*   aux1 is the main counter for timing.\n*/\ntypedef struct PacManChars {\n  signed    pos;\n  signed    topPos;     // LED position of farthest PacMan has moved\n  uint32_t  color;\n  bool      direction;  // true = moving away from first LED\n  bool      blue;       // used for ghosts only\n  bool      eaten;      // used for power dots only\n} pacmancharacters_t;\n\nstatic void mode_pacman(void) {\n  constexpr unsigned ORANGEYELLOW = 0xFFCC00;\n  constexpr unsigned PURPLEISH    = 0xB000B0;\n  constexpr unsigned ORANGEISH    = 0xFF8800;\n  constexpr unsigned WHITEISH     = 0x999999;\n  constexpr unsigned PACMAN = 0;   // PacMan is character[0]\n  constexpr uint32_t ghostColors[] = {RED, PURPLEISH, CYAN, ORANGEISH};\n\n  unsigned maxPowerDots = min(SEGLEN / 10U, 255U);  // cap the max so packed state fits in 8 bits\n  unsigned numPowerDots = map(SEGMENT.intensity, 0, 255, 1, maxPowerDots);\n  unsigned numGhosts = map(SEGMENT.custom3, 0, 31, 2, 8);\n  bool smearMode = SEGMENT.check2;\n\n  // Pack two 8-bit values into one 16-bit field (stored in SEGENV.aux0)\n  uint16_t combined_value = uint16_t(((numPowerDots & 0xFF) << 8) | (numGhosts & 0xFF));\n  if (combined_value != SEGENV.aux0) SEGENV.call = 0;  // Reinitialize on setting change\n  SEGENV.aux0 = combined_value;\n\n  // Allocate segment data\n  unsigned dataSize = sizeof(pacmancharacters_t) * (numGhosts + maxPowerDots + 1);    // +1 is the PacMan character\n  if (SEGLEN <= 16 + (2*numGhosts) || !SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC;\n  pacmancharacters_t *character = reinterpret_cast<pacmancharacters_t *>(SEGENV.data);\n\n  // Calculate when blue ghosts start blinking.\n  // On first call (or after settings change), `topPos` is not known yet, so fall back to the full segment length in that case.\n  int maxBlinkPos = (SEGENV.call == 0) ? (int)SEGLEN - 1 : character[PACMAN].topPos;\n  if (maxBlinkPos < 20) maxBlinkPos = 20;\n  int startBlinkingGhostsLED = (SEGLEN < 64)\n    ? (int)SEGLEN / 3\n    : map(SEGMENT.custom1, 0, 255, 20, maxBlinkPos);\n\n  // Initialize characters on first call\n  if (SEGENV.call == 0) {\n    // Initialize PacMan\n    character[PACMAN].color = YELLOW;\n    character[PACMAN].pos = 0;\n    character[PACMAN].topPos = 0;\n    character[PACMAN].direction = true;\n    character[PACMAN].blue = false;\n\n    // Initialize ghosts with alternating colors\n    for (int i = 1; i <= numGhosts; i++) {\n      character[i].color = ghostColors[(i-1) % 4];\n      character[i].pos = -2 * (i + 1);\n      character[i].direction = true;\n      character[i].blue = false;\n    }\n\n    // Initialize power dots\n    for (int i = 0; i < numPowerDots; i++) {\n      character[i + numGhosts + 1].color = ORANGEYELLOW;\n      character[i + numGhosts + 1].eaten = false;\n    }\n    character[numGhosts + 1].pos = SEGLEN - 1;  // Last power dot at end\n  }\n\n  if (strip.now > SEGENV.step) {\n    SEGENV.step = strip.now;\n    SEGENV.aux1++;\n  }\n\n  // Clear background if not in smear mode\n  if (!smearMode) SEGMENT.fill(BLACK);\n\n  // Draw white dots in front of PacMan if option selected\n  if (SEGMENT.check1) {\n    int step = SEGMENT.check3 ? 1 : 2;  // Compact or spaced dots\n    for (int i = SEGLEN - 1; i > character[PACMAN].topPos; i -= step) {\n      SEGMENT.setPixelColor(i, WHITEISH);\n    }\n  }\n\n  // Update power dot positions dynamically\n  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.\n  for (int i = 1; i < numPowerDots; i++) {\n    character[i + numGhosts + 1].pos = 10 + ((i * everyXLeds) >> 8);\n  }\n\n  // Blink power dots every 10 ticks\n  if (SEGENV.aux1 % 10 == 0) {\n    uint32_t dotColor = (character[numGhosts + 1].color == ORANGEYELLOW) ? BLACK : ORANGEYELLOW;\n    for (int i = 0; i < numPowerDots; i++) {\n      character[i + numGhosts + 1].color = dotColor;\n    }\n  }\n\n  // Blink blue ghosts when nearing start\n  if (SEGENV.aux1 % 15 == 0 && character[1].blue && character[PACMAN].pos <= startBlinkingGhostsLED) {\n    uint32_t ghostColor = (character[1].color == BLUE) ? WHITEISH : BLUE;\n    for (int i = 1; i <= numGhosts; i++) {\n      character[i].color = ghostColor;\n    }\n  }\n\n  // Draw uneaten power dots\n  for (int i = 0; i < numPowerDots; i++) {\n    if (!character[i + numGhosts + 1].eaten && (unsigned)character[i + numGhosts + 1].pos < SEGLEN) {\n      SEGMENT.setPixelColor(character[i + numGhosts + 1].pos, character[i + numGhosts + 1].color);\n    }\n  }\n\n  // Check if PacMan ate a power dot\n  for (int j = 0; j < numPowerDots; j++) {\n    auto &dot = character[j + numGhosts + 1];\n    if (character[PACMAN].pos == dot.pos && !dot.eaten) {\n      // Reverse all characters - PacMan now chases ghosts\n      for (int i = 0; i <= numGhosts; i++) {\n        character[i].direction = false;\n      }\n      // Turn ghosts blue\n      for (int i = 1; i <= numGhosts; i++) {\n        character[i].color = BLUE;\n        character[i].blue = true;\n      }\n      dot.eaten = true;\n      break; // only one power dot per frame\n    }\n  }\n\n  // Reset when PacMan reaches start with blue ghosts\n  if (character[1].blue && character[PACMAN].pos <= 0) {\n    // Reverse direction back\n    for (int i = 0; i <= numGhosts; i++) {\n      character[i].direction = true;\n    }\n    // Reset ghost colors\n    for (int i = 1; i <= numGhosts; i++) {\n      character[i].color = ghostColors[(i-1) % 4];\n      character[i].blue = false;\n    }\n    // Reset power dots if last one was eaten\n    if (character[numGhosts + 1].eaten) {\n      for (int i = 0; i < numPowerDots; i++) {\n        character[i + numGhosts + 1].eaten = false;\n      }\n      character[PACMAN].topPos = 0;    // set the top position of PacMan to LED 0 (beginning of the segment)\n    }\n  }\n\n  // Update and draw characters based on speed setting\n  bool updatePositions = (SEGENV.aux1 % map(SEGMENT.speed, 0, 255, 15, 1) == 0);\n\n  // update positions of characters if it's time to do so\n  if (updatePositions) {\n    character[PACMAN].pos += character[PACMAN].direction ? 1 : -1;\n    for (int i = 1; i <= numGhosts; i++) {\n      character[i].pos += character[i].direction ? 1 : -1;\n    }\n  }\n\n  // Draw PacMan\n  if ((unsigned)character[PACMAN].pos < SEGLEN) {\n    SEGMENT.setPixelColor(character[PACMAN].pos, character[PACMAN].color);\n  }\n\n  // Draw ghosts\n  for (int i = 1; i <= numGhosts; i++) {\n    if ((unsigned)character[i].pos < SEGLEN) {\n      SEGMENT.setPixelColor(character[i].pos, character[i].color);\n    }\n  }\n\n  // Track farthest position of PacMan\n  if (character[PACMAN].topPos < character[PACMAN].pos) {\n    character[PACMAN].topPos = character[PACMAN].pos;\n  }\n\n  SEGMENT.blur(SEGMENT.custom2>>1);\n}\nstatic 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\";\n\n\n/*\n* Sinelon stolen from FASTLED examples\n*/\nstatic void sinelon_base(bool dual, bool rainbow=false) {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  SEGMENT.fade_out(SEGMENT.intensity);\n  unsigned pos = beatsin16_t(SEGMENT.speed/10,0,SEGLEN-1);\n  if (SEGENV.call == 0) SEGENV.aux0 = pos;\n  uint32_t color1 = SEGMENT.color_from_palette(pos, true, false, 0);\n  uint32_t color2 = SEGCOLOR(2);\n  if (rainbow) {\n    color1 = SEGMENT.color_wheel((pos & 0x07) * 32);\n  }\n  SEGMENT.setPixelColor(pos, color1);\n  if (dual) {\n    if (!color2) color2 = SEGMENT.color_from_palette(pos, true, false, 0);\n    if (rainbow) color2 = color1; //rainbow\n    SEGMENT.setPixelColor(SEGLEN-1-pos, color2);\n  }\n  if (SEGENV.aux0 != pos) {\n    if (SEGENV.aux0 < pos) {\n      for (unsigned i = SEGENV.aux0; i < pos ; i++) {\n        SEGMENT.setPixelColor(i, color1);\n        if (dual) SEGMENT.setPixelColor(SEGLEN-1-i, color2);\n      }\n    } else {\n      for (unsigned i = SEGENV.aux0; i > pos ; i--) {\n        SEGMENT.setPixelColor(i, color1);\n        if (dual) SEGMENT.setPixelColor(SEGLEN-1-i, color2);\n      }\n    }\n    SEGENV.aux0 = pos;\n  }\n}\n\n\nvoid mode_sinelon(void) {\n  sinelon_base(false);\n}\nstatic const char _data_FX_MODE_SINELON[] PROGMEM = \"Sinelon@!,Trail;!,!,!;!\";\n\n\nvoid mode_sinelon_dual(void) {\n  sinelon_base(true);\n}\nstatic const char _data_FX_MODE_SINELON_DUAL[] PROGMEM = \"Sinelon Dual@!,Trail;!,!,!;!\";\n\n\nvoid mode_sinelon_rainbow(void) {\n  sinelon_base(false, true);\n}\nstatic const char _data_FX_MODE_SINELON_RAINBOW[] PROGMEM = \"Sinelon Rainbow@!,Trail;,,!;!\";\n\n// utility function that will add random glitter to SEGMENT\nvoid glitter_base(uint8_t intensity, uint32_t col = ULTRAWHITE) {\n  if (intensity > hw_random8()) SEGMENT.setPixelColor(hw_random16(SEGLEN), col);\n}\n\n//Glitter with palette background, inspired by https://gist.github.com/kriegsman/062e10f7f07ba8518af6\nvoid mode_glitter()\n{\n  if (!SEGMENT.check2) { // use \"* Color 1\" palette for solid background (replacing \"Solid glitter\")\n    unsigned counter = 0;\n    if (SEGMENT.speed != 0) {\n      counter = (strip.now * ((SEGMENT.speed >> 3) +1)) & 0xFFFF;\n      counter = counter >> 8;\n    }\n\n    bool noWrap = (paletteBlend == 2 || (paletteBlend == 0 && SEGMENT.speed == 0));\n    for (unsigned i = 0; i < SEGLEN; i++) {\n      unsigned colorIndex = (i * 255 / SEGLEN) - counter;\n      if (noWrap) colorIndex = map(colorIndex, 0, 255, 0, 240); //cut off blend at palette \"end\"\n      SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(colorIndex, false, true, 255));\n    }\n  }\n  glitter_base(SEGMENT.intensity, SEGCOLOR(2) ? SEGCOLOR(2) : ULTRAWHITE);\n}\nstatic const char _data_FX_MODE_GLITTER[] PROGMEM = \"Glitter@!,!,,,,,Overlay;,,Glitter color;!;;pal=11,m12=0\"; //pixels\n\n\n//Solid colour background with glitter (can be replaced by Glitter)\nvoid mode_solid_glitter()\n{\n  SEGMENT.fill(SEGCOLOR(0));\n  glitter_base(SEGMENT.intensity, SEGCOLOR(2) ? SEGCOLOR(2) : ULTRAWHITE);\n}\nstatic const char _data_FX_MODE_SOLID_GLITTER[] PROGMEM = \"Solid Glitter@,!;Bg,,Glitter color;;;m12=0\";\n\n//each needs 20 bytes\n//Spark type is used for popcorn, 1D fireworks, and drip\ntypedef struct Spark {\n  float pos, posX;\n  float vel, velX;\n  uint16_t col;\n  uint8_t colIndex;\n} spark;\n\n#define maxNumPopcorn 21 // max 21 on 16 segment ESP8266\n/*\n*  POPCORN\n*  modified from https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Popcorn.h\n*/\nvoid mode_popcorn(void) {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  //allocate segment data\n  unsigned strips = SEGMENT.nrOfVStrips();\n  unsigned usablePopcorns = maxNumPopcorn;\n  if (usablePopcorns * strips * sizeof(spark) > FAIR_DATA_PER_SEG) usablePopcorns = FAIR_DATA_PER_SEG / (strips * sizeof(spark)) + 1; // at least 1 popcorn per vstrip\n  unsigned dataSize = sizeof(spark) * usablePopcorns; // on a matrix 64x64 this could consume a little less than 27kB when Bar expansion is used\n  if (!SEGENV.allocateData(dataSize * strips)) FX_FALLBACK_STATIC; //allocation failed\n\n  Spark* popcorn = reinterpret_cast<Spark*>(SEGENV.data);\n\n  bool hasCol2 = SEGCOLOR(2);\n  if (!SEGMENT.check2) SEGMENT.fill(hasCol2 ? BLACK : SEGCOLOR(1));\n\n  struct virtualStrip {\n    static void runStrip(uint16_t stripNr, Spark* popcorn, unsigned usablePopcorns) {\n      float gravity = -0.0001f - (SEGMENT.speed/200000.0f); // m/s/s\n      gravity *= SEGLEN;\n\n      unsigned numPopcorn = SEGMENT.intensity * usablePopcorns / 255;\n      if (numPopcorn == 0) numPopcorn = 1;\n\n      for (unsigned i = 0; i < numPopcorn; i++) {\n        if (popcorn[i].pos >= 0.0f) { // if kernel is active, update its position\n          popcorn[i].pos += popcorn[i].vel;\n          popcorn[i].vel += gravity;\n        } else { // if kernel is inactive, randomly pop it\n          if (hw_random8() < 2) { // POP!!!\n            popcorn[i].pos = 0.01f;\n\n            unsigned peakHeight = 128 + hw_random8(128); //0-255\n            peakHeight = (peakHeight * (SEGLEN -1)) >> 8;\n            popcorn[i].vel = sqrtf(-2.0f * gravity * peakHeight);\n\n            if (SEGMENT.palette)\n            {\n              popcorn[i].colIndex = hw_random8();\n            } else {\n              byte col = hw_random8(0, NUM_COLORS);\n              if (!SEGCOLOR(2) || !SEGCOLOR(col)) col = 0;\n              popcorn[i].colIndex = col;\n            }\n          }\n        }\n        if (popcorn[i].pos >= 0.0f) { // draw now active popcorn (either active before or just popped)\n          uint32_t col = SEGMENT.color_wheel(popcorn[i].colIndex);\n          if (!SEGMENT.palette && popcorn[i].colIndex < NUM_COLORS) col = SEGCOLOR(popcorn[i].colIndex);\n          unsigned ledIndex = popcorn[i].pos;\n          if (ledIndex < SEGLEN) SEGMENT.setPixelColor(indexToVStrip(ledIndex, stripNr), col);\n        }\n      }\n    }\n  };\n\n  for (unsigned stripNr=0; stripNr<strips; stripNr++)\n    virtualStrip::runStrip(stripNr, &popcorn[stripNr * usablePopcorns], usablePopcorns);\n}\nstatic const char _data_FX_MODE_POPCORN[] PROGMEM = \"Popcorn@!,!,,,,,Overlay;!,!,!;!;;m12=1\"; //bar\n\n//values close to 100 produce 5Hz flicker, which looks very candle-y\n//Inspired by https://github.com/avanhanegem/ArduinoCandleEffectNeoPixel\n//and https://cpldcpu.wordpress.com/2016/01/05/reverse-engineering-a-real-candle/\n\nvoid candle(bool multi)\n{\n  if (multi && SEGLEN > 1) {\n    //allocate segment data\n    unsigned dataSize = sizeof(uint32_t) + max(1, (int)SEGLEN -1) *3; //max. 1365 pixels (ESP8266)\n    if (!SEGENV.allocateData(dataSize)) candle(false); //allocation failed\n  }\n  uint32_t* lastcall = reinterpret_cast<uint32_t*>(SEGENV.data);\n  uint8_t*  candleData = reinterpret_cast<uint8_t*>(SEGENV.data + sizeof(uint32_t));\n\n  //limit update rate\n  if (strip.now - *lastcall < FRAMETIME_FIXED) return;\n  *lastcall = strip.now;\n\n  //max. flicker range controlled by intensity\n  unsigned valrange = SEGMENT.intensity;\n  unsigned rndval = valrange >> 1; //max 127\n\n  //step (how much to move closer to target per frame) coarsely set by speed\n  unsigned speedFactor = 4;\n  if (SEGMENT.speed > 252) { //epilepsy\n    speedFactor = 1;\n  } else if (SEGMENT.speed > 99) { //regular candle (mode called every ~25 ms, so 4 frames to have a new target every 100ms)\n    speedFactor = 2;\n  } else if (SEGMENT.speed > 49) { //slower fade\n    speedFactor = 3;\n  } //else 4 (slowest)\n\n  unsigned numCandles = (multi) ? SEGLEN : 1;\n\n  for (unsigned i = 0; i < numCandles; i++)\n  {\n    unsigned d = 0; //data location\n\n    unsigned s = SEGENV.aux0, s_target = SEGENV.aux1, fadeStep = SEGENV.step;\n    if (i > 0) {\n      d = (i-1) *3;\n      s = candleData[d]; s_target = candleData[d+1]; fadeStep = candleData[d+2];\n    }\n    if (fadeStep == 0) { //init vals\n      s = 128; s_target = 130 + hw_random8(4); fadeStep = 1;\n    }\n\n    bool newTarget = false;\n    if (s_target > s) { //fade up\n      s = qadd8(s, fadeStep);\n      if (s >= s_target) newTarget = true;\n    } else {\n      s = qsub8(s, fadeStep);\n      if (s <= s_target) newTarget = true;\n    }\n\n    if (newTarget) {\n      s_target = hw_random8(rndval) + hw_random8(rndval); //between 0 and rndval*2 -2 = 252\n      if (s_target < (rndval >> 1)) s_target = (rndval >> 1) + hw_random8(rndval);\n      unsigned offset = (255 - valrange);\n      s_target += offset;\n\n      unsigned dif = (s_target > s) ? s_target - s : s - s_target;\n\n      fadeStep = dif >> speedFactor;\n      if (fadeStep == 0) fadeStep = 1;\n    }\n\n    if (i > 0) {\n      SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), uint8_t(s)));\n      candleData[d] = s; candleData[d+1] = s_target; candleData[d+2] = fadeStep;\n    } else {\n      for (unsigned j = 0; j < SEGLEN; j++) {\n        SEGMENT.setPixelColor(j, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(j, true, PALETTE_SOLID_WRAP, 0), uint8_t(s)));\n      }\n\n      SEGENV.aux0 = s; SEGENV.aux1 = s_target; SEGENV.step = fadeStep;\n    }\n  }\n}\n\n\nvoid mode_candle()\n{\n  candle(false);\n}\nstatic const char _data_FX_MODE_CANDLE[] PROGMEM = \"Candle@!,!;!,!;!;01;sx=96,ix=224,pal=0\";\n\n\nvoid mode_candle_multi()\n{\n  candle(true);\n}\nstatic const char _data_FX_MODE_CANDLE_MULTI[] PROGMEM = \"Candle Multi@!,!;!,!;!;;sx=96,ix=224,pal=0\";\n\n#ifdef WLED_PS_DONT_REPLACE_1D_FX\n/*\n/ Fireworks in starburst effect\n/ based on the video: https://www.reddit.com/r/arduino/comments/c3sd46/i_made_this_fireworks_effect_for_my_led_strips/\n/ Speed sets frequency of new starbursts, intensity is the intensity of the burst\n*/\n#ifdef ESP8266\n  #define STARBURST_MAX_FRAG   8 //52 bytes / star\n#else\n  #define STARBURST_MAX_FRAG  10 //60 bytes / star\n#endif\n//each needs 20+STARBURST_MAX_FRAG*4 bytes\ntypedef struct particle {\n  CRGB     color;\n  uint32_t birth  =0;\n  uint32_t last   =0;\n  float    vel    =0;\n  uint16_t pos    =-1;\n  float    fragment[STARBURST_MAX_FRAG];\n} star;\n\nvoid mode_starburst(void) {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  unsigned maxData = FAIR_DATA_PER_SEG; //ESP8266: 256 ESP32: 640\n  unsigned segs = strip.getActiveSegmentsNum();\n  if (segs <= (strip.getMaxSegments() /2)) maxData *= 2; //ESP8266: 512 if <= 8 segs ESP32: 1280 if <= 16 segs\n  if (segs <= (strip.getMaxSegments() /4)) maxData *= 2; //ESP8266: 1024 if <= 4 segs ESP32: 2560 if <= 8 segs\n  unsigned maxStars = maxData / sizeof(star); //ESP8266: max. 4/9/19 stars/seg, ESP32: max. 10/21/42 stars/seg\n\n  unsigned numStars = 1 + (SEGLEN >> 3);\n  if (numStars > maxStars) numStars = maxStars;\n  unsigned dataSize = sizeof(star) * numStars;\n\n  if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed\n\n  uint32_t it = strip.now;\n\n  star* stars = reinterpret_cast<star*>(SEGENV.data);\n\n  float          maxSpeed                = 375.0f;  // Max velocity\n  float          particleIgnition        = 250.0f;  // How long to \"flash\"\n  float          particleFadeTime        = 1500.0f; // Fade out time\n\n  for (unsigned j = 0; j < numStars; j++)\n  {\n    // speed to adjust chance of a burst, max is nearly always.\n    if (hw_random8((144-(SEGMENT.speed >> 1))) == 0 && stars[j].birth == 0)\n    {\n      // Pick a random color and location.\n      unsigned startPos = hw_random16(SEGLEN-1);\n      float multiplier = (float)(hw_random8())/255.0f * 1.0f;\n\n      stars[j].color = CRGB(SEGMENT.color_wheel(hw_random8()));\n      stars[j].pos = startPos;\n      stars[j].vel = maxSpeed * (float)(hw_random8())/255.0f * multiplier;\n      stars[j].birth = it;\n      stars[j].last = it;\n      // more fragments means larger burst effect\n      int num = hw_random8(3,6 + (SEGMENT.intensity >> 5));\n\n      for (int i=0; i < STARBURST_MAX_FRAG; i++) {\n        if (i < num) stars[j].fragment[i] = startPos;\n        else stars[j].fragment[i] = -1;\n      }\n    }\n  }\n\n  if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1));\n\n  for (unsigned j=0; j<numStars; j++)\n  {\n    if (stars[j].birth != 0) {\n      float dt = (it-stars[j].last)/1000.0;\n\n      for (int i=0; i < STARBURST_MAX_FRAG; i++) {\n        int var = i >> 1;\n\n        if (stars[j].fragment[i] > 0) {\n          //all fragments travel right, will be mirrored on other side\n          stars[j].fragment[i] += stars[j].vel * dt * (float)var/3.0;\n        }\n      }\n      stars[j].last = it;\n      stars[j].vel -= 3*stars[j].vel*dt;\n    }\n\n    CRGB c = stars[j].color;\n\n    // If the star is brand new, it flashes white briefly.\n    // Otherwise it just fades over time.\n    float fade = 0.0f;\n    float age = it-stars[j].birth;\n\n    if (age < particleIgnition) {\n      c = CRGB(color_blend(WHITE, RGBW32(c.r,c.g,c.b,0), uint8_t(254.5f*((age / particleIgnition)))));\n    } else {\n      // Figure out how much to fade and shrink the star based on\n      // its age relative to its lifetime\n      if (age > particleIgnition + particleFadeTime) {\n        fade = 1.0f;                  // Black hole, all faded out\n        stars[j].birth = 0;\n        c = CRGB(SEGCOLOR(1));\n      } else {\n        age -= particleIgnition;\n        fade = (age / particleFadeTime);  // Fading star\n        c = CRGB(color_blend(RGBW32(c.r,c.g,c.b,0), SEGCOLOR(1), uint8_t(254.5f*fade)));\n      }\n    }\n\n    float particleSize = (1.0f - fade) * 2.0f;\n\n    for (size_t index=0; index < STARBURST_MAX_FRAG*2; index++) {\n      bool mirrored = index & 0x1;\n      unsigned i = index >> 1;\n      if (stars[j].fragment[i] > 0) {\n        float loc = stars[j].fragment[i];\n        if (mirrored) loc -= (loc-stars[j].pos)*2;\n        unsigned start = loc - particleSize;\n        unsigned end = loc + particleSize;\n        if (start < 0) start = 0;\n        if (start == end) end++;\n        if (end > SEGLEN) end = SEGLEN;\n        for (unsigned p = start; p < end; p++) {\n          SEGMENT.setPixelColor(p, c);\n        }\n      }\n    }\n  }\n}\n#undef STARBURST_MAX_FRAG\nstatic const char _data_FX_MODE_STARBURST[] PROGMEM = \"Fireworks Starburst@Chance,Fragments,,,,,Overlay;,!;!;;pal=11,m12=0\";\n#endif // WLED_PS_DONT_REPLACE_1DFX\n\n#if defined(WLED_PS_DONT_REPLACE_1D_FX) || defined(WLED_PS_DONT_REPLACE_2D_FX)\n/*\n * Exploding fireworks effect\n * adapted from: http://www.anirama.com/1000leds/1d-fireworks/\n * adapted for 2D WLED by blazoncek (Blaz Kristan (AKA blazoncek))\n */\nvoid mode_exploding_fireworks(void)\n{\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  const int cols = SEGMENT.is2D() ? SEG_W : 1;\n  const int rows = SEGMENT.is2D() ? SEG_H : SEGLEN;\n\n  //allocate segment data\n  unsigned maxData = FAIR_DATA_PER_SEG; //ESP8266: 256 ESP32: 640\n  unsigned segs = strip.getActiveSegmentsNum();\n  if (segs <= (strip.getMaxSegments() /2)) maxData *= 2; //ESP8266: 512 if <= 8 segs ESP32: 1280 if <= 16 segs\n  if (segs <= (strip.getMaxSegments() /4)) maxData *= 2; //ESP8266: 1024 if <= 4 segs ESP32: 2560 if <= 8 segs\n  int maxSparks = maxData / sizeof(spark); //ESP8266: max. 21/42/85 sparks/seg, ESP32: max. 53/106/213 sparks/seg\n\n  unsigned numSparks = min(5 + ((rows*cols) >> 1), maxSparks);\n  unsigned dataSize = sizeof(spark) * numSparks;\n  if (!SEGENV.allocateData(dataSize + sizeof(float))) FX_FALLBACK_STATIC; //allocation failed\n  float *dying_gravity = reinterpret_cast<float*>(SEGENV.data + dataSize);\n\n  if (dataSize != SEGENV.aux1) { //reset to flare if sparks were reallocated (it may be good idea to reset segment if bounds change)\n    *dying_gravity = 0.0f;\n    SEGENV.aux0 = 0;\n    SEGENV.aux1 = dataSize;\n  }\n\n  SEGMENT.fade_out(252);\n\n  Spark* sparks = reinterpret_cast<Spark*>(SEGENV.data);\n  Spark* flare = sparks; //first spark is flare data\n\n  float gravity = -0.0004f - (SEGMENT.speed/800000.0f); // m/s/s\n  gravity *= rows;\n\n  if (SEGENV.aux0 < 2) { //FLARE\n    if (SEGENV.aux0 == 0) { //init flare\n      flare->pos = 0;\n      flare->posX = SEGMENT.is2D() ? hw_random16(2,cols-3) : (SEGMENT.intensity > hw_random8()); // will enable random firing side on 1D\n      unsigned peakHeight = 75 + hw_random8(180); //0-255\n      peakHeight = (peakHeight * (rows -1)) >> 8;\n      flare->vel = sqrtf(-2.0f * gravity * peakHeight);\n      flare->velX = SEGMENT.is2D() ? (hw_random8(9)-4)/64.0f : 0; // no X velocity on 1D\n      flare->col = 255; //brightness\n      SEGENV.aux0 = 1;\n    }\n\n    // launch\n    if (flare->vel > 12 * gravity) {\n      // flare\n      if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(unsigned(flare->posX), rows - uint16_t(flare->pos) - 1, flare->col, flare->col, flare->col);\n      else                SEGMENT.setPixelColor((flare->posX > 0.0f) ? rows - int(flare->pos) - 1 : int(flare->pos), flare->col, flare->col, flare->col);\n      flare->pos  += flare->vel;\n      flare->pos  = constrain(flare->pos, 0, rows-1);\n      if (SEGMENT.is2D()) {\n        flare->posX += flare->velX;\n        flare->posX = constrain(flare->posX, 0, cols-1);\n      }\n      flare->vel  += gravity;\n      flare->col  -= 2;\n    } else {\n      SEGENV.aux0 = 2;  // ready to explode\n    }\n  } else if (SEGENV.aux0 < 4) {\n    /*\n     * Explode!\n     *\n     * Explosion happens where the flare ended.\n     * Size is proportional to the height.\n     */\n    unsigned nSparks = flare->pos + hw_random8(4);\n    nSparks = std::max(nSparks, 4U);  // This is not a standard constrain; numSparks is not guaranteed to be at least 4\n    nSparks = std::min(nSparks, numSparks);\n\n    // initialize sparks\n    if (SEGENV.aux0 == 2) {\n      for (unsigned i = 1; i < nSparks; i++) {\n        sparks[i].pos  = flare->pos;\n        sparks[i].posX = flare->posX;\n        sparks[i].vel  = (float(hw_random16(20001)) / 10000.0f) - 0.9f; // from -0.9 to 1.1\n        sparks[i].vel *= rows<32 ? 0.5f : 1; // reduce velocity for smaller strips\n        sparks[i].velX = SEGMENT.is2D() ? (float(hw_random16(20001)) / 10000.0f) - 1.0f : 0; // from -1 to 1\n        sparks[i].col  = 345;//abs(sparks[i].vel * 750.0); // set colors before scaling velocity to keep them bright\n        //sparks[i].col = constrain(sparks[i].col, 0, 345);\n        sparks[i].colIndex = hw_random8();\n        sparks[i].vel  *= flare->pos/rows; // proportional to height\n        sparks[i].velX *= SEGMENT.is2D() ? flare->posX/cols : 0; // proportional to width\n        sparks[i].vel  *= -gravity *50;\n      }\n      //sparks[1].col = 345; // this will be our known spark\n      *dying_gravity = gravity/2;\n      SEGENV.aux0 = 3;\n    }\n\n    if (sparks[1].col > 4) {//&& sparks[1].pos > 0) { // as long as our known spark is lit, work with all the sparks\n      for (unsigned i = 1; i < nSparks; i++) {\n        sparks[i].pos  += sparks[i].vel;\n        sparks[i].posX += sparks[i].velX;\n        sparks[i].vel  += *dying_gravity;\n        sparks[i].velX += SEGMENT.is2D() ? *dying_gravity : 0;\n        if (sparks[i].col > 3) sparks[i].col -= 4;\n\n        if (sparks[i].pos > 0 && sparks[i].pos < rows) {\n          if (SEGMENT.is2D() && !(sparks[i].posX >= 0 && sparks[i].posX < cols)) continue;\n          unsigned prog = sparks[i].col;\n          uint32_t spColor = (SEGMENT.palette) ? SEGMENT.color_wheel(sparks[i].colIndex) : SEGCOLOR(0);\n          CRGBW c = BLACK; //HeatColor(sparks[i].col);\n          if (prog > 300) { //fade from white to spark color\n            c = color_blend(spColor, WHITE, uint8_t((prog - 300)*5));\n          } else if (prog > 45) { //fade from spark color to black\n            c = color_blend(BLACK, spColor, uint8_t(prog - 45));\n            unsigned cooling = (300 - prog) >> 5;\n            c.g = qsub8(c.g, cooling);\n            c.b = qsub8(c.b, cooling * 2);\n          }\n          if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(int(sparks[i].posX), rows - int(sparks[i].pos) - 1, c);\n          else                SEGMENT.setPixelColor(int(sparks[i].posX) ? rows - int(sparks[i].pos) - 1 : int(sparks[i].pos), c);\n        }\n      }\n      if (SEGMENT.check3) SEGMENT.blur(16);\n      *dying_gravity *= .8f; // as sparks burn out they fall slower\n    } else {\n      SEGENV.aux0 = 6 + hw_random8(10); //wait for this many frames\n    }\n  } else {\n    SEGENV.aux0--;\n    if (SEGENV.aux0 < 4) {\n      SEGENV.aux0 = 0; //back to flare\n    }\n  }\n}\n#undef MAX_SPARKS\nstatic const char _data_FX_MODE_EXPLODING_FIREWORKS[] PROGMEM = \"Fireworks 1D@Gravity,Firing side;!,!;!;12;pal=11,ix=128\";\n#endif // WLED_PS_DONT_REPLACE_x_FX\n\n/*\n * Drip Effect\n * ported of: https://www.youtube.com/watch?v=sru2fXh4r7k\n */\nvoid mode_drip(void)\n{\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  //allocate segment data\n  unsigned strips = SEGMENT.nrOfVStrips();\n  const int maxNumDrops = 4;\n  unsigned dataSize = sizeof(spark) * maxNumDrops;\n  if (!SEGENV.allocateData(dataSize * strips)) FX_FALLBACK_STATIC; //allocation failed\n  Spark* drops = reinterpret_cast<Spark*>(SEGENV.data);\n\n  if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1));\n\n  struct virtualStrip {\n    static void runStrip(uint16_t stripNr, Spark* drops) {\n\n      unsigned numDrops = 1 + (SEGMENT.intensity >> 6); // 255>>6 = 3\n\n      float gravity = -0.0005f - (SEGMENT.speed/50000.0f);\n      gravity *= max(1, (int)SEGLEN-1);\n      int sourcedrop = 12;\n\n      for (unsigned j=0;j<numDrops;j++) {\n        if (drops[j].colIndex == 0) { //init\n          drops[j].pos = SEGLEN-1;    // start at end\n          drops[j].vel = 0;           // speed\n          drops[j].col = sourcedrop;  // brightness\n          drops[j].colIndex = 1;      // drop state (0 init, 1 forming, 2 falling, 5 bouncing)\n        }\n\n        SEGMENT.setPixelColor(indexToVStrip(SEGLEN-1, stripNr), color_blend(BLACK,SEGCOLOR(0), uint8_t(sourcedrop)));// water source\n        if (drops[j].colIndex==1) {\n          if (drops[j].col>255) drops[j].col=255;\n          SEGMENT.setPixelColor(indexToVStrip(uint16_t(drops[j].pos), stripNr), color_blend(BLACK,SEGCOLOR(0),uint8_t(drops[j].col)));\n\n          drops[j].col += map(SEGMENT.speed, 0, 255, 1, 6); // swelling\n\n          if (hw_random8() < drops[j].col/10) {               // random drop\n            drops[j].colIndex=2;               //fall\n            drops[j].col=255;\n          }\n        }\n        if (drops[j].colIndex > 1) {           // falling\n          if (drops[j].pos > 0) {              // fall until end of segment\n            drops[j].pos += drops[j].vel;\n            if (drops[j].pos < 0) drops[j].pos = 0;\n            drops[j].vel += gravity;           // gravity is negative\n\n            for (int i=1;i<7-drops[j].colIndex;i++) { // some minor math so we don't expand bouncing droplets\n              unsigned pos = constrain(unsigned(drops[j].pos) +i, 0, SEGLEN-1); //this is BAD, returns a pos >= SEGLEN occasionally\n              SEGMENT.setPixelColor(indexToVStrip(pos, stripNr), color_blend(BLACK,SEGCOLOR(0),uint8_t(drops[j].col/i))); //spread pixel with fade while falling\n            }\n\n            if (drops[j].colIndex > 2) {       // during bounce, some water is on the floor\n              SEGMENT.setPixelColor(indexToVStrip(0, stripNr), color_blend(SEGCOLOR(0),BLACK,uint8_t(drops[j].col)));\n            }\n          } else {                             // we hit bottom\n            if (drops[j].colIndex > 2) {       // already hit once, so back to forming\n              drops[j].colIndex = 0;\n              drops[j].col = sourcedrop;\n\n            } else {\n\n              if (drops[j].colIndex==2) {      // init bounce\n                drops[j].vel = -drops[j].vel/4;// reverse velocity with damping\n                drops[j].pos += drops[j].vel;\n              }\n              drops[j].col = sourcedrop*2;\n              drops[j].colIndex = 5;           // bouncing\n            }\n          }\n        }\n      }\n    }\n  };\n\n  for (unsigned stripNr=0; stripNr<strips; stripNr++)\n    virtualStrip::runStrip(stripNr, &drops[stripNr*maxNumDrops]);\n}\nstatic const char _data_FX_MODE_DRIP[] PROGMEM = \"Drip@Gravity,# of drips,,,,,Overlay;!,!;!;;m12=1\"; //bar\n\n/*\n * Tetris or Stacking (falling bricks) Effect\n * by Blaz Kristan (AKA blazoncek) (https://github.com/blazoncek, https://blaz.at/home)\n */\n//20 bytes\ntypedef struct Tetris {\n  float    pos;\n  float    speed;\n  uint8_t  col;   // color index\n  uint16_t brick; // brick size in pixels\n  uint16_t stack; // stack size in pixels\n  uint32_t step;  // 2D-fication of SEGENV.step (state)\n} tetris;\n\nvoid mode_tetrix(void) {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  unsigned strips = SEGMENT.nrOfVStrips(); // allow running on virtual strips (columns in 2D segment)\n  unsigned dataSize = sizeof(tetris);\n  if (!SEGENV.allocateData(dataSize * strips)) FX_FALLBACK_STATIC; //allocation failed\n  Tetris* drops = reinterpret_cast<Tetris*>(SEGENV.data);\n\n  //if (SEGENV.call == 0) SEGMENT.fill(SEGCOLOR(1));  // will fill entire segment (1D or 2D), then use drop->step = 0 below\n\n  // virtualStrip idea by @ewowi (Ewoud Wijma)\n  // requires virtual strip # to be embedded into upper 16 bits of index in setPixelcolor()\n  // the following functions will not work on virtual strips: fill(), fade_out(), fadeToBlack(), blur()\n  struct virtualStrip {\n    static void runStrip(size_t stripNr, Tetris *drop) {\n      // initialize dropping on first call or segment full\n      if (SEGENV.call == 0) {\n        drop->stack = 0;                  // reset brick stack size\n        drop->step = strip.now + 2000;     // start by fading out strip\n        if (SEGMENT.check1) drop->col = 0;// use only one color from palette\n      }\n\n      if (drop->step == 0) {              // init brick\n        // speed calculation: a single brick should reach bottom of strip in X seconds\n        // if the speed is set to 1 this should take 5s and at 255 it should take 0.25s\n        // as this is dependant on SEGLEN it should be taken into account and the fact that effect runs every FRAMETIME s\n        int speed = SEGMENT.speed ? SEGMENT.speed : hw_random8(1,255);\n        speed = map(speed, 1, 255, 5000, 250); // time taken for full (SEGLEN) drop\n        drop->speed = float(SEGLEN * FRAMETIME) / float(speed); // set speed\n        drop->pos   = SEGLEN;             // start at end of segment (no need to subtract 1)\n        if (!SEGMENT.check1) drop->col = hw_random8(0,15)<<4;   // limit color choices so there is enough HUE gap\n        drop->step  = 1;                  // drop state (0 init, 1 forming, 2 falling)\n        drop->brick = (SEGMENT.intensity ? (SEGMENT.intensity>>5)+1 : hw_random8(1,5)) * (1+(SEGLEN>>6));  // size of brick\n      }\n\n      if (drop->step == 1) {              // forming\n        if (hw_random8()>>6) {               // random drop\n          drop->step = 2;                 // fall\n        }\n      }\n\n      if (drop->step == 2) {              // falling\n        if (drop->pos > drop->stack) {    // fall until top of stack\n          drop->pos -= drop->speed;       // may add gravity as: speed += gravity\n          if (int(drop->pos) < int(drop->stack)) drop->pos = drop->stack;\n          for (unsigned i = unsigned(drop->pos); i < SEGLEN; i++) {\n            uint32_t col = i < unsigned(drop->pos)+drop->brick ? SEGMENT.color_from_palette(drop->col, false, false, 0) : SEGCOLOR(1);\n            SEGMENT.setPixelColor(indexToVStrip(i, stripNr), col);\n          }\n        } else {                          // we hit bottom\n          drop->step = 0;                 // proceed with next brick, go back to init\n          drop->stack += drop->brick;     // increase the stack size\n          if (drop->stack >= SEGLEN) drop->step = strip.now + 2000; // fade out stack\n        }\n      }\n\n      if (drop->step > 2) {               // fade strip\n        drop->brick = 0;                  // reset brick size (no more growing)\n        if (drop->step > strip.now) {\n          // allow fading of virtual strip\n          for (unsigned i = 0; i < SEGLEN; i++) SEGMENT.blendPixelColor(indexToVStrip(i, stripNr), SEGCOLOR(1), 25); // 10% blend\n        } else {\n          drop->stack = 0;                // reset brick stack size\n          drop->step = 0;                 // proceed with next brick\n          if (SEGMENT.check1) drop->col += 8;   // gradually increase palette index\n        }\n      }\n    }\n  };\n\n  for (unsigned stripNr=0; stripNr<strips; stripNr++)\n    virtualStrip::runStrip(stripNr, &drops[stripNr]);\n}\nstatic const char _data_FX_MODE_TETRIX[] PROGMEM = \"Tetrix@!,Width,,,,One color;!,!;!;;sx=0,ix=0,pal=11,m12=1\";\n\n\n/*\n/ Plasma Effect\n/ adapted from https://github.com/atuline/FastLED-Demos/blob/master/plasma/plasma.ino\n*/\nvoid mode_plasma(void) {\n  // initialize phases on start\n  if (SEGENV.call == 0) {\n    SEGENV.aux0 = hw_random8(0,2);  // add a bit of randomness\n  }\n  unsigned thisPhase = beatsin8_t(6+SEGENV.aux0,-64,64);\n  unsigned thatPhase = beatsin8_t(7+SEGENV.aux0,-64,64);\n\n  for (unsigned i = 0; i < SEGLEN; i++) {   // For each of the LED's in the strand, set color &  brightness based on a wave as follows:\n    unsigned colorIndex = cubicwave8((i*(2+ 3*(SEGMENT.speed >> 5))+thisPhase) & 0xFF)/2   // factor=23 // Create a wave and add a phase change and add another wave with its own phase change.\n                              + cos8_t((i*(1+ 2*(SEGMENT.speed >> 5))+thatPhase) & 0xFF)/2;  // factor=15 // Hey, you can even change the frequencies if you wish.\n    unsigned thisBright = qsub8(colorIndex, beatsin8_t(7,0, (128 - (SEGMENT.intensity>>1))));\n    SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(colorIndex, false, PALETTE_SOLID_WRAP, 0, thisBright));\n  }\n}\nstatic const char _data_FX_MODE_PLASMA[] PROGMEM = \"Plasma@Phase,!;!;!\";\n\n\n/*\n * Percentage display\n * Intensity values from 0-100 turn on the leds.\n */\nvoid mode_percent(void) {\n\n  unsigned percent = SEGMENT.intensity;\n  percent = constrain(percent, 0, 200);\n  unsigned active_leds = (percent < 100) ? roundf(SEGLEN * percent / 100.0f)\n                                         : roundf(SEGLEN * (200 - percent) / 100.0f);\n\n  unsigned size = (1 + ((SEGMENT.speed * SEGLEN) >> 11));\n  if (SEGMENT.speed == 255) size = 255;\n\n  if (percent <= 100) {\n    for (unsigned i = 0; i < SEGLEN; i++) {\n    \tif (i < SEGENV.aux1) {\n        if (SEGMENT.check1)\n          SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(map(percent,0,100,0,255), false, false, 0));\n        else\n          SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0));\n    \t}\n    \telse {\n        SEGMENT.setPixelColor(i, SEGCOLOR(1));\n    \t}\n    }\n  } else {\n    for (unsigned i = 0; i < SEGLEN; i++) {\n    \tif (i < (SEGLEN - SEGENV.aux1)) {\n        SEGMENT.setPixelColor(i, SEGCOLOR(1));\n    \t}\n    \telse {\n        if (SEGMENT.check1)\n          SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(map(percent,100,200,255,0), false, false, 0));\n        else\n          SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0));\n    \t}\n    }\n  }\n\n  if(active_leds > SEGENV.aux1) {  // smooth transition to the target value\n    SEGENV.aux1 += size;\n    if (SEGENV.aux1 > active_leds) SEGENV.aux1 = active_leds;\n  } else if (active_leds < SEGENV.aux1) {\n    if (SEGENV.aux1 > size) SEGENV.aux1 -= size; else SEGENV.aux1 = 0;\n    if (SEGENV.aux1 < active_leds) SEGENV.aux1 = active_leds;\n  }\n}\nstatic const char _data_FX_MODE_PERCENT[] PROGMEM = \"Percent@!,% of fill,,,,One color;!,!;!\";\n\n\n/*\n * Modulates the brightness similar to a heartbeat\n * (unimplemented?) tries to draw an ECG approximation on a 2D matrix\n */\nvoid mode_heartbeat(void) {\n  unsigned bpm = 40 + (SEGMENT.speed >> 3);\n  uint32_t msPerBeat = (60000L / bpm);\n  uint32_t secondBeat = (msPerBeat / 3);\n  uint32_t bri_lower = SEGENV.aux1;\n  unsigned long beatTimer = strip.now - SEGENV.step;\n\n  bri_lower = bri_lower * 2042 / (2048 + SEGMENT.intensity);\n  SEGENV.aux1 = bri_lower;\n\n  if ((beatTimer > secondBeat) && !SEGENV.aux0) { // time for the second beat?\n    SEGENV.aux1 = UINT16_MAX; //3/4 bri\n    SEGENV.aux0 = 1;\n  }\n  if (beatTimer > msPerBeat) { // time to reset the beat timer?\n    SEGENV.aux1 = UINT16_MAX; //full bri\n    SEGENV.aux0 = 0;\n    SEGENV.step = strip.now;\n  }\n\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    SEGMENT.setPixelColor(i, color_blend(SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), SEGCOLOR(1), uint8_t(255 - (SEGENV.aux1 >> 8))));\n  }\n}\nstatic const char _data_FX_MODE_HEARTBEAT[] PROGMEM = \"Heartbeat@!,!;!,!;!;01;m12=1\";\n\n\n//  \"Pacifica\"\n//  Gentle, blue-green ocean waves.\n//  December 2019, Mark Kriegsman and Mary Corey March.\n//  For Dan.\n//\n//\n// In this animation, there are four \"layers\" of waves of light.\n//\n// Each layer moves independently, and each is scaled separately.\n//\n// All four wave layers are added together on top of each other, and then\n// another filter is applied that adds \"whitecaps\" of brightness where the\n// waves line up with each other more.  Finally, another pass is taken\n// over the led array to 'deepen' (dim) the blues and greens.\n//\n// The speed and scale and motion each layer varies slowly within independent\n// hand-chosen ranges, which is why the code has a lot of low-speed 'beatsin8' functions\n// with a lot of oddly specific numeric ranges.\n//\n// These three custom blue-green color palettes were inspired by the colors found in\n// the waters off the southern coast of California, https://goo.gl/maps/QQgd97jjHesHZVxQ7\n//\n// Modified for WLED, based on https://github.com/FastLED/FastLED/blob/master/examples/Pacifica/Pacifica.ino\n//\n// Add one layer of waves into the led array\nstatic CRGB pacifica_one_layer(uint16_t i, const CRGBPalette16& p, uint16_t cistart, uint16_t wavescale, uint8_t bri, uint16_t ioff)\n{\n  unsigned ci = cistart;\n  unsigned waveangle = ioff;\n  unsigned wavescale_half = (wavescale >> 1) + 20;\n\n  waveangle += ((120 + SEGMENT.intensity) * i); //original 250 * i\n  unsigned s16 = sin16_t(waveangle) + 32768;\n  unsigned cs = scale16(s16, wavescale_half) + wavescale_half;\n  ci += (cs * i);\n  unsigned sindex16 = sin16_t(ci) + 32768;\n  unsigned sindex8 = scale16(sindex16, 240);\n  return CRGB(ColorFromPalette(p, sindex8, bri, LINEARBLEND));\n}\n\nvoid mode_pacifica()\n{\n  uint32_t nowOld = strip.now;\n\n  CRGBPalette16 pacifica_palette_1 =\n    { 0x000507, 0x000409, 0x00030B, 0x00030D, 0x000210, 0x000212, 0x000114, 0x000117,\n      0x000019, 0x00001C, 0x000026, 0x000031, 0x00003B, 0x000046, 0x14554B, 0x28AA50 };\n  CRGBPalette16 pacifica_palette_2 =\n    { 0x000507, 0x000409, 0x00030B, 0x00030D, 0x000210, 0x000212, 0x000114, 0x000117,\n      0x000019, 0x00001C, 0x000026, 0x000031, 0x00003B, 0x000046, 0x0C5F52, 0x19BE5F };\n  CRGBPalette16 pacifica_palette_3 =\n    { 0x000208, 0x00030E, 0x000514, 0x00061A, 0x000820, 0x000927, 0x000B2D, 0x000C33,\n      0x000E39, 0x001040, 0x001450, 0x001860, 0x001C70, 0x002080, 0x1040BF, 0x2060FF };\n\n  if (SEGMENT.palette) {\n    pacifica_palette_1 = SEGPALETTE;\n    pacifica_palette_2 = SEGPALETTE;\n    pacifica_palette_3 = SEGPALETTE;\n  }\n\n  // Increment the four \"color index start\" counters, one for each wave layer.\n  // Each is incremented at a different speed, and the speeds vary over time.\n  unsigned sCIStart1 = SEGENV.aux0, sCIStart2 = SEGENV.aux1, sCIStart3 = SEGENV.step & 0xFFFF, sCIStart4 = (SEGENV.step >> 16);\n  uint32_t deltams = (FRAMETIME >> 2) + ((FRAMETIME * SEGMENT.speed) >> 7);\n  uint64_t deltat = (strip.now >> 2) + ((strip.now * SEGMENT.speed) >> 7);\n  strip.now = deltat;\n\n  unsigned speedfactor1 = beatsin16_t(3, 179, 269);\n  unsigned speedfactor2 = beatsin16_t(4, 179, 269);\n  uint32_t deltams1 = (deltams * speedfactor1) / 256;\n  uint32_t deltams2 = (deltams * speedfactor2) / 256;\n  uint32_t deltams21 = (deltams1 + deltams2) / 2;\n  sCIStart1 += (deltams1 * beatsin88_t(1011,10,13));\n  sCIStart2 -= (deltams21 * beatsin88_t(777,8,11));\n  sCIStart3 -= (deltams1 * beatsin88_t(501,5,7));\n  sCIStart4 -= (deltams2 * beatsin88_t(257,4,6));\n  SEGENV.aux0 = sCIStart1; SEGENV.aux1 = sCIStart2;\n  SEGENV.step = (sCIStart4 << 16) | (sCIStart3 & 0xFFFF);\n\n  // Clear out the LED array to a dim background blue-green\n  //SEGMENT.fill(132618);\n\n  unsigned basethreshold = beatsin8_t( 9, 55, 65);\n  unsigned wave = beat8( 7 );\n\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    CRGB c = CRGB(2, 6, 10);\n    // Render each of four layers, with different scales and speeds, that vary over time\n    c += pacifica_one_layer(i, pacifica_palette_1, sCIStart1, beatsin16_t(3, 11 * 256, 14 * 256), beatsin8_t(10, 70, 130), 0-beat16(301));\n    c += pacifica_one_layer(i, pacifica_palette_2, sCIStart2, beatsin16_t(4,  6 * 256,  9 * 256), beatsin8_t(17, 40,  80),   beat16(401));\n    c += pacifica_one_layer(i, pacifica_palette_3, sCIStart3,                         6 * 256 , beatsin8_t(9, 10,38)   , 0-beat16(503));\n    c += pacifica_one_layer(i, pacifica_palette_3, sCIStart4,                         5 * 256 , beatsin8_t(8, 10,28)   ,   beat16(601));\n\n    // Add extra 'white' to areas where the four layers of light have lined up brightly\n    unsigned threshold = scale8( sin8_t( wave), 20) + basethreshold;\n    wave += 7;\n    unsigned l = c.getAverageLight();\n    if (l > threshold) {\n      unsigned overage = l - threshold;\n      unsigned overage2 = qadd8(overage, overage);\n      c += CRGB(overage, overage2, qadd8(overage2, overage2));\n    }\n\n    //deepen the blues and greens\n    c.blue  = scale8(c.blue,  145);\n    c.green = scale8(c.green, 200);\n    c |= CRGB( 2, 5, 7);\n\n    SEGMENT.setPixelColor(i, c);\n  }\n\n  strip.now = nowOld;\n}\nstatic const char _data_FX_MODE_PACIFICA[] PROGMEM = \"Pacifica@!,Angle;;!;;pal=51\";\n\n\n/*\n * Mode simulates a gradual sunrise\n */\nvoid mode_sunrise() {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  //speed 0 - static sun\n  //speed 1 - 60: sunrise time in minutes\n  //speed 60 - 120 : sunset time in minutes - 60;\n  //speed above: \"breathing\" rise and set\n  if (SEGENV.call == 0 || SEGMENT.speed != SEGENV.aux0) {\n    SEGENV.step = millis(); //save starting time, millis() because strip.now can change from sync\n    SEGENV.aux0 = SEGMENT.speed;\n  }\n\n  SEGMENT.fill(BLACK);\n  unsigned stage = 0xFFFF;\n\n  uint32_t s10SinceStart = (millis() - SEGENV.step) /100; //tenths of seconds\n\n  if (SEGMENT.speed > 120) { //quick sunrise and sunset\n    unsigned counter = (strip.now >> 1) * (((SEGMENT.speed -120) >> 1) +1);\n    stage = triwave16(counter);\n  } else if (SEGMENT.speed) { //sunrise\n    unsigned durMins = SEGMENT.speed;\n    if (durMins > 60) durMins -= 60;\n    uint32_t s10Target = durMins * 600;\n    if (s10SinceStart > s10Target) s10SinceStart = s10Target;\n    stage = map(s10SinceStart, 0, s10Target, 0, 0xFFFF);\n    if (SEGMENT.speed > 60) stage = 0xFFFF - stage; //sunset\n  }\n\n  for (unsigned i = 0; i <= SEGLEN/2; i++)\n  {\n    //default palette is Fire    \n    unsigned wave = triwave16((i * stage) / SEGLEN);\n    wave = (wave >> 8) + ((wave * SEGMENT.intensity) >> 15);\n    uint32_t c;\n    if (wave > 240) { //clipped, full white sun\n      c = SEGMENT.color_from_palette( 240, false, true, 255);\n    } else { //transition\n      c = SEGMENT.color_from_palette(wave, false, true, 255);\n    }\n    SEGMENT.setPixelColor(i, c);\n    SEGMENT.setPixelColor(SEGLEN - i - 1, c);\n  }\n}\nstatic const char _data_FX_MODE_SUNRISE[] PROGMEM = \"Sunrise@Time [min],Width;;!;;pal=35,sx=60\";\n\n\n/*\n * Effects by Andrew Tuline\n */\nstatic void phased_base(uint8_t moder) {                  // We're making sine waves here. By Andrew Tuline.\n\n  unsigned allfreq = 16;                                          // Base frequency.\n  float *phase = reinterpret_cast<float*>(&SEGENV.step);         // Phase change value gets calculated (float fits into unsigned long).\n  unsigned cutOff = (255-SEGMENT.intensity);                      // You can change the number of pixels.  AKA INTENSITY (was 192).\n  unsigned modVal = 5;//SEGMENT.fft1/8+1;                         // You can change the modulus. AKA FFT1 (was 5).\n\n  unsigned index = strip.now/64;                                  // Set color rotation speed\n  *phase += SEGMENT.speed/32.0;                                  // You can change the speed of the wave. AKA SPEED (was .4)\n\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    if (moder == 1) modVal = (perlin8(i*10 + i*10) /16);         // Let's randomize our mod length with some Perlin noise.\n    unsigned val = (i+1) * allfreq;                              // This sets the frequency of the waves. The +1 makes sure that led 0 is used.\n    if (modVal == 0) modVal = 1;\n    val += *phase * (i % modVal +1) /2;                          // This sets the varying phase change of the waves. By Andrew Tuline.\n    unsigned b = cubicwave8(val);                                 // Now we make an 8 bit sinewave.\n    b = (b > cutOff) ? (b - cutOff) : 0;                         // A ternary operator to cutoff the light.\n    SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(index, false, false, 0), uint8_t(b)));\n    index += 256 / SEGLEN;\n    if (SEGLEN > 256) index ++;                                  // Correction for segments longer than 256 LEDs\n  }\n}\n\n\nvoid mode_phased(void) {\n  phased_base(0);\n}\nstatic const char _data_FX_MODE_PHASED[] PROGMEM = \"Phased@!,!;!,!;!\";\n\n\nvoid mode_phased_noise(void) {\n  phased_base(1);\n}\nstatic const char _data_FX_MODE_PHASEDNOISE[] PROGMEM = \"Phased Noise@!,!;!,!;!\";\n\n\nvoid mode_twinkleup(void) {                 // A very short twinkle routine with fade-in and dual controls. By Andrew Tuline.\n  unsigned prevSeed = random16_get_seed();      // save seed so we can restore it at the end of the function\n  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.\n\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    unsigned ranstart = random8();               // The starting value (aka brightness) for each pixel. Must be consistent each time through the loop for this to work.\n    unsigned pixBri = sin8_t(ranstart + 16 * strip.now/(256-SEGMENT.speed));\n    if (random8() > SEGMENT.intensity) pixBri = 0;\n    SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(random8()+strip.now/100, false, PALETTE_SOLID_WRAP, 0), pixBri));\n  }\n\n  random16_set_seed(prevSeed); // restore original seed so other effects can use \"random\" PRNG\n}\nstatic const char _data_FX_MODE_TWINKLEUP[] PROGMEM = \"Twinkleup@!,Intensity;!,!;!;;m12=0\";\n\n\n// Peaceful noise that's slow and with gradually changing palettes. Does not support WLED palettes or default colours or controls.\nvoid mode_noisepal(void) {                                    // Slow noise palette by Andrew Tuline.\n  unsigned scale = 15 + (SEGMENT.intensity >> 2); //default was 30\n  //#define scale 30\n\n  unsigned dataSize = sizeof(CRGBPalette16) * 2; //allocate space for 2 Palettes (2 * 16 * 3 = 96 bytes)\n  if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed\n\n  CRGBPalette16* palettes = reinterpret_cast<CRGBPalette16*>(SEGENV.data);\n\n  unsigned changePaletteMs = 4000 + SEGMENT.speed *10; //between 4 - 6.5sec\n  if (strip.now - SEGENV.step > changePaletteMs)\n  {\n    SEGENV.step = strip.now;\n\n    unsigned baseI = hw_random8();\n    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)));\n  }\n\n  //EVERY_N_MILLIS(10) { //(don't have to time this, effect function is only called every 24ms)\n  nblendPaletteTowardPalette(palettes[0], palettes[1], 48);               // Blend towards the target palette over 48 iterations.\n\n  if (SEGMENT.palette > 0) palettes[0] = SEGPALETTE;\n\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    unsigned index = perlin8(i*scale, SEGENV.aux0+i*scale);                // Get a value from the noise function. I'm using both x and y axis.\n    SEGMENT.setPixelColor(i,  ColorFromPalette(palettes[0], index, 255, LINEARBLEND));  // Use my own palette.\n  }\n\n  SEGENV.aux0 += beatsin8_t(10,1,4);                                        // Moving along the distance. Vary it a bit with a sine wave.\n}\nstatic const char _data_FX_MODE_NOISEPAL[] PROGMEM = \"Noise Pal@!,Scale;;!\";\n\n\n// Sine waves that have controllable phase change speed, frequency and cutoff. By Andrew Tuline.\n// SEGMENT.speed ->Speed, SEGMENT.intensity -> Frequency (SEGMENT.fft1 -> Color change, SEGMENT.fft2 -> PWM cutoff)\n//\nvoid mode_sinewave(void) {             // Adjustable sinewave. By Andrew Tuline\n  //#define qsuba(x, b)  ((x>b)?x-b:0)               // Analog Unsigned subtraction macro. if result <0, then => 0\n\n  unsigned colorIndex = strip.now /32;//(256 - SEGMENT.fft1);  // Amount of colour change.\n\n  SEGENV.step += SEGMENT.speed/16;                   // Speed of animation.\n  unsigned freq = SEGMENT.intensity/4;//SEGMENT.fft2/8;                       // Frequency of the signal.\n\n  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:\n    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)..\n    //setPixCol(i, i*colorIndex/255, pixBri);\n    SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i*colorIndex/255, false, PALETTE_SOLID_WRAP, 0), pixBri));\n  }\n}\nstatic const char _data_FX_MODE_SINEWAVE[] PROGMEM = \"Sine@!,Scale;;!\";\n\n\n/*\n * Best of both worlds from Palette and Spot effects. By Aircoookie\n */\nvoid mode_flow(void)\n{\n  unsigned counter = 0;\n  if (SEGMENT.speed != 0)\n  {\n    counter = strip.now * ((SEGMENT.speed >> 2) +1);\n    counter = counter >> 8;\n  }\n\n  unsigned maxZones = SEGLEN / 6; //only looks good if each zone has at least 6 LEDs\n  int zones = (SEGMENT.intensity * maxZones) >> 8;\n  if (zones & 0x01) zones++; //zones must be even\n  if (zones < 2) zones = 2;\n  int zoneLen = SEGLEN / zones;\n  zones += 2; //add two extra zones to cover beginning and end of segment (compensate integer truncation)\n  int offset = ((int)SEGLEN - (zones * zoneLen)) / 2; // center the zones on the segment (can not use bit shift on negative number)\n\n  for (int z = 0; z < zones; z++)\n  {\n    int pos = offset + z * zoneLen;\n    for (int i = 0; i < zoneLen; i++)\n    {\n      unsigned colorIndex = (i * 255 / zoneLen) - counter;\n      int led = (z & 0x01) ? i : (zoneLen -1) -i;\n      if (SEGMENT.reverse) led = (zoneLen -1) -led;\n      SEGMENT.setPixelColor(pos + led, SEGMENT.color_from_palette(colorIndex, false, true, 255));\n    }\n  }\n}\nstatic const char _data_FX_MODE_FLOW[] PROGMEM = \"Flow@!,Zones;;!;;m12=1\"; //vertical\n\n\n/*\n * Dots waving around in a sine/pendulum motion.\n * Little pixel birds flying in a circle. By Aircoookie\n */\nvoid mode_chunchun(void)\n{\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  SEGMENT.fade_out(254); // add a bit of trail\n  unsigned counter = strip.now * (6 + (SEGMENT.speed >> 4));\n  unsigned numBirds = 2 + (SEGLEN >> 3);  // 2 + 1/8 of a segment\n  unsigned span = (SEGMENT.intensity << 8) / numBirds;\n\n  for (unsigned i = 0; i < numBirds; i++)\n  {\n    counter -= span;\n    unsigned megumin = sin16_t(counter) + 0x8000;\n    unsigned bird = uint32_t(megumin * SEGLEN) >> 16;\n    bird = constrain(bird, 0U, SEGLEN-1U);\n    SEGMENT.setPixelColor(bird, SEGMENT.color_from_palette((i * 255)/ numBirds, false, false, 0)); // no palette wrapping\n  }\n}\nstatic const char _data_FX_MODE_CHUNCHUN[] PROGMEM = \"Chunchun@!,Gap size;!,!;!\";\n\n#define SPOT_TYPE_SOLID       0\n#define SPOT_TYPE_GRADIENT    1\n#define SPOT_TYPE_2X_GRADIENT 2\n#define SPOT_TYPE_2X_DOT      3\n#define SPOT_TYPE_3X_DOT      4\n#define SPOT_TYPE_4X_DOT      5\n#define SPOT_TYPES_COUNT      6\n#ifdef ESP8266\n  #define SPOT_MAX_COUNT 17          //Number of simultaneous waves\n#else\n  #define SPOT_MAX_COUNT 49          //Number of simultaneous waves\n#endif\n\n#ifdef WLED_PS_DONT_REPLACE_1D_FX\n//13 bytes\ntypedef struct Spotlight {\n  float speed;\n  uint8_t colorIdx;\n  int16_t position;\n  unsigned long lastUpdateTime;\n  uint8_t width;\n  uint8_t type;\n} spotlight;\n\n/*\n * Spotlights moving back and forth that cast dancing shadows.\n * Shine this through tree branches/leaves or other close-up objects that cast\n * interesting shadows onto a ceiling or tarp.\n *\n * By Steve Pomeroy @xxv\n */\nvoid mode_dancing_shadows(void)\n{\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  unsigned numSpotlights = map(SEGMENT.intensity, 0, 255, 2, SPOT_MAX_COUNT);  // 49 on 32 segment ESP32, 17 on 16 segment ESP8266\n  bool initialize = SEGENV.aux0 != numSpotlights;\n  SEGENV.aux0 = numSpotlights;\n\n  unsigned dataSize = sizeof(spotlight) * numSpotlights;\n  if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed\n  Spotlight* spotlights = reinterpret_cast<Spotlight*>(SEGENV.data);\n\n  SEGMENT.fill(BLACK);\n\n  unsigned long time = strip.now;\n  bool respawn = false;\n\n  for (size_t i = 0; i < numSpotlights; i++) {\n    if (!initialize) {\n      // advance the position of the spotlight\n      int delta = (float)(time - spotlights[i].lastUpdateTime) *\n                  (spotlights[i].speed * ((1.0 + SEGMENT.speed)/100.0));\n\n      if (abs(delta) >= 1) {\n        spotlights[i].position += delta;\n        spotlights[i].lastUpdateTime = time;\n      }\n\n      respawn = (spotlights[i].speed > 0.0 && spotlights[i].position > (int)(SEGLEN + 2))\n             || (spotlights[i].speed < 0.0 && spotlights[i].position < -(spotlights[i].width + 2));\n    }\n\n    if (initialize || respawn) {\n      spotlights[i].colorIdx = hw_random8();\n      spotlights[i].width = hw_random8(1, 10);\n\n      spotlights[i].speed = 1.0/hw_random8(4, 50);\n\n      if (initialize) {\n        spotlights[i].position = hw_random16(SEGLEN);\n        spotlights[i].speed *= hw_random8(2) ? 1.0 : -1.0;\n      } else {\n        if (hw_random8(2)) {\n          spotlights[i].position = SEGLEN + spotlights[i].width;\n          spotlights[i].speed *= -1.0;\n        }else {\n          spotlights[i].position = -spotlights[i].width;\n        }\n      }\n\n      spotlights[i].lastUpdateTime = time;\n      spotlights[i].type = hw_random8(SPOT_TYPES_COUNT);\n    }\n\n    uint32_t color = SEGMENT.color_from_palette(spotlights[i].colorIdx, false, false, 255);\n    int start = spotlights[i].position;\n\n    if (spotlights[i].width <= 1) {\n      if (start >= 0 && start < (int)SEGLEN) {\n        SEGMENT.blendPixelColor(start, color, 128);\n      }\n    } else {\n      switch (spotlights[i].type) {\n        case SPOT_TYPE_SOLID:\n          for (size_t j = 0; j < spotlights[i].width; j++) {\n            if ((start + j) >= 0 && (start + j) < SEGLEN) {\n              SEGMENT.blendPixelColor(start + j, color, 128);\n            }\n          }\n        break;\n\n        case SPOT_TYPE_GRADIENT:\n          for (size_t j = 0; j < spotlights[i].width; j++) {\n            if ((start + j) >= 0 && (start + j) < SEGLEN) {\n              SEGMENT.blendPixelColor(start + j, color, cubicwave8(map(j, 0, spotlights[i].width - 1, 0, 255)));\n            }\n          }\n        break;\n\n        case SPOT_TYPE_2X_GRADIENT:\n          for (size_t j = 0; j < spotlights[i].width; j++) {\n            if ((start + j) >= 0 && (start + j) < SEGLEN) {\n              SEGMENT.blendPixelColor(start + j, color, cubicwave8(2 * map(j, 0, spotlights[i].width - 1, 0, 255)));\n            }\n          }\n        break;\n\n        case SPOT_TYPE_2X_DOT:\n          for (size_t j = 0; j < spotlights[i].width; j += 2) {\n            if ((start + j) >= 0 && (start + j) < SEGLEN) {\n              SEGMENT.blendPixelColor(start + j, color, 128);\n            }\n          }\n        break;\n\n        case SPOT_TYPE_3X_DOT:\n          for (size_t j = 0; j < spotlights[i].width; j += 3) {\n            if ((start + j) >= 0 && (start + j) < SEGLEN) {\n              SEGMENT.blendPixelColor(start + j, color, 128);\n            }\n          }\n        break;\n\n        case SPOT_TYPE_4X_DOT:\n          for (size_t j = 0; j < spotlights[i].width; j += 4) {\n            if ((start + j) >= 0 && (start + j) < SEGLEN) {\n              SEGMENT.blendPixelColor(start + j, color, 128);\n            }\n          }\n        break;\n      }\n    }\n  }\n}\nstatic const char _data_FX_MODE_DANCING_SHADOWS[] PROGMEM = \"Dancing Shadows@!,# of shadows;!;!\";\n#endif // WLED_PS_DONT_REPLACE_1D_FX\n\n/*\n  Imitates a washing machine, rotating same waves forward, then pause, then backward.\n  By Stefan Seegel\n*/\nvoid mode_washing_machine(void) {\n  int speed = tristate_square8(strip.now >> 7, 90, 15);\n\n  SEGENV.step += (speed * 2048) / (512 - SEGMENT.speed);\n\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    uint8_t col = sin8_t(((SEGMENT.intensity / 25 + 1) * 255 * i / SEGLEN) + (SEGENV.step >> 7));\n    SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(col, false, PALETTE_SOLID_WRAP, 3));\n  }\n}\nstatic const char _data_FX_MODE_WASHING_MACHINE[] PROGMEM = \"Washing Machine@!,!;;!\";\n\n\n/*\n  Image effect\n  Draws a .gif image from filesystem on the matrix/strip\n*/\nvoid mode_image(void) {\n  #ifndef WLED_ENABLE_GIF\n  FX_FALLBACK_STATIC;\n  #else\n  renderImageToSegment(SEGMENT);\n  #endif\n  // if (status != 0 && status != 254 && status != 255) {\n  //   Serial.print(\"GIF renderer return: \");\n  //   Serial.println(status);\n  // }\n}\nstatic const char _data_FX_MODE_IMAGE[] PROGMEM = \"Image@!,Blur,;;;12;sx=128,ix=0\";\n\n/*\n  Blends random colors across palette\n  Modified, originally by Mark Kriegsman https://gist.github.com/kriegsman/1f7ccbbfa492a73c015e\n*/\nvoid mode_blends(void) {\n  unsigned pixelLen = SEGLEN > UINT8_MAX ? UINT8_MAX : SEGLEN;\n  unsigned dataSize = sizeof(uint32_t) * (pixelLen + 1);  // max segment length of 56 pixels on 16 segment ESP8266\n  if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed\n  uint32_t* pixels = reinterpret_cast<uint32_t*>(SEGENV.data);\n  uint8_t blendSpeed = map(SEGMENT.intensity, 0, UINT8_MAX, 10, 128);\n  unsigned shift = (strip.now * ((SEGMENT.speed >> 3) +1)) >> 8;\n\n  for (unsigned i = 0; i < pixelLen; i++) {\n    pixels[i] = color_blend(pixels[i], SEGMENT.color_from_palette(shift + quadwave8((i + 1) * 16), false, PALETTE_SOLID_WRAP, 255), blendSpeed);\n    shift += 3;\n  }\n\n  unsigned offset = 0;\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    SEGMENT.setPixelColor(i, pixels[offset++]);\n    if (offset >= pixelLen) offset = 0;\n  }\n}\nstatic const char _data_FX_MODE_BLENDS[] PROGMEM = \"Blends@Shift speed,Blend speed;;!\";\n\n\n/*\n  TV Simulator\n  Modified and adapted to WLED by Def3nder, based on \"Fake TV Light for Engineers\" by Phillip Burgess https://learn.adafruit.com/fake-tv-light-for-engineers/arduino-sketch\n*/\n//43 bytes\ntypedef struct TvSim {\n  uint32_t totalTime = 0;\n  uint32_t fadeTime  = 0;\n  uint32_t startTime = 0;\n  uint32_t elapsed   = 0;\n  uint32_t pixelNum  = 0;\n  uint16_t sliderValues = 0;\n  uint32_t sceeneStart    = 0;\n  uint32_t sceeneDuration = 0;\n  uint16_t sceeneColorHue = 0;\n  uint8_t  sceeneColorSat = 0;\n  uint8_t  sceeneColorBri = 0;\n  uint8_t  actualColorR = 0;\n  uint8_t  actualColorG = 0;\n  uint8_t  actualColorB = 0;\n  uint16_t pr = 0; // Prev R, G, B\n  uint16_t pg = 0;\n  uint16_t pb = 0;\n} tvSim;\n\nvoid mode_tv_simulator(void) {\n  int nr, ng, nb, r, g, b, i, hue;\n  uint8_t  sat, bri, j;\n\n  if (!SEGENV.allocateData(sizeof(tvSim))) FX_FALLBACK_STATIC; //allocation failed\n  TvSim* tvSimulator = reinterpret_cast<TvSim*>(SEGENV.data);\n\n  uint8_t colorSpeed     = map(SEGMENT.speed,     0, UINT8_MAX,  1, 20);\n  uint8_t colorIntensity = map(SEGMENT.intensity, 0, UINT8_MAX, 10, 30);\n\n  i = SEGMENT.speed << 8 | SEGMENT.intensity;\n  if (i != tvSimulator->sliderValues) {\n    tvSimulator->sliderValues = i;\n    SEGENV.aux1 = 0;\n  }\n\n    // create a new sceene\n    if (((strip.now - tvSimulator->sceeneStart) >= tvSimulator->sceeneDuration) || SEGENV.aux1 == 0) {\n      tvSimulator->sceeneStart    = strip.now;                                               // remember the start of the new sceene\n      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)\n      tvSimulator->sceeneColorHue = hw_random16(   0, 768);                                    // random start color-tone for the sceene\n      tvSimulator->sceeneColorSat = hw_random8 ( 100, 130 + colorIntensity);                   // random start color-saturation for the sceene\n      tvSimulator->sceeneColorBri = hw_random8 ( 200, 240);                                    // random start color-brightness for the sceene\n      SEGENV.aux1 = 1;\n      SEGENV.aux0 = 0;\n    }\n\n    // slightly change the color-tone in this sceene\n    if (SEGENV.aux0 == 0) {\n      // hue change in both directions\n      j = hw_random8(4 * colorIntensity);\n      hue = (hw_random8() < 128) ? ((j < tvSimulator->sceeneColorHue)       ? tvSimulator->sceeneColorHue - j : 767 - tvSimulator->sceeneColorHue - j) :  // negative\n                                ((j + tvSimulator->sceeneColorHue) < 767 ? tvSimulator->sceeneColorHue + j : tvSimulator->sceeneColorHue + j - 767) ;  // positive\n\n      // saturation\n      j = hw_random8(2 * colorIntensity);\n      sat = (tvSimulator->sceeneColorSat - j) < 0 ? 0 : tvSimulator->sceeneColorSat - j;\n\n      // brightness\n      j = hw_random8(100);\n      bri = (tvSimulator->sceeneColorBri - j) < 0 ? 0 : tvSimulator->sceeneColorBri - j;\n\n      // calculate R,G,B from HSV\n      // Source: https://blog.adafruit.com/2012/03/14/constant-brightness-hsb-to-rgb-algorithm/\n      { // just to create a local scope for  the variables\n        uint8_t temp[5], n = (hue >> 8) % 3;\n        uint8_t x = ((((hue & 255) * sat) >> 8) * bri) >> 8;\n        uint8_t s = (  (256 - sat) * bri) >> 8;\n        temp[0] = temp[3] =       s;\n        temp[1] = temp[4] =   x + s;\n        temp[2] =           bri - x;\n        tvSimulator->actualColorR = temp[n + 2];\n        tvSimulator->actualColorG = temp[n + 1];\n        tvSimulator->actualColorB = temp[n    ];\n      }\n    }\n    // Apply gamma correction, further expand to 16/16/16\n    nr = (uint8_t)gamma8(tvSimulator->actualColorR) * 257; // New R/G/B\n    ng = (uint8_t)gamma8(tvSimulator->actualColorG) * 257;\n    nb = (uint8_t)gamma8(tvSimulator->actualColorB) * 257;\n\n  if (SEGENV.aux0 == 0) {  // initialize next iteration\n    SEGENV.aux0 = 1;\n\n    // randomize total duration and fade duration for the actual color\n    tvSimulator->totalTime = hw_random16(250, 2500);                   // Semi-random pixel-to-pixel time\n    tvSimulator->fadeTime  = hw_random16(0, tvSimulator->totalTime);   // Pixel-to-pixel transition time\n    if (hw_random8(10) < 3) tvSimulator->fadeTime = 0;                 // Force scene cut 30% of time\n\n    tvSimulator->startTime = strip.now;\n  } // end of initialization\n\n  // how much time is elapsed ?\n  tvSimulator->elapsed = strip.now - tvSimulator->startTime;\n\n  // fade from prev color to next color\n  if (tvSimulator->elapsed < tvSimulator->fadeTime) {\n    r = map(tvSimulator->elapsed, 0, tvSimulator->fadeTime, tvSimulator->pr, nr);\n    g = map(tvSimulator->elapsed, 0, tvSimulator->fadeTime, tvSimulator->pg, ng);\n    b = map(tvSimulator->elapsed, 0, tvSimulator->fadeTime, tvSimulator->pb, nb);\n  } else { // Avoid divide-by-zero in map()\n    r = nr;\n    g = ng;\n    b = nb;\n  }\n\n  // set strip color\n  for (i = 0; i < (int)SEGLEN; i++) {\n    SEGMENT.setPixelColor(i, r >> 8, g >> 8, b >> 8);  // Quantize to 8-bit\n  }\n\n  // if total duration has passed, remember last color and restart the loop\n  if ( tvSimulator->elapsed >= tvSimulator->totalTime) {\n    tvSimulator->pr = nr; // Prev RGB = new RGB\n    tvSimulator->pg = ng;\n    tvSimulator->pb = nb;\n    SEGENV.aux0 = 0;\n  }\n}\nstatic const char _data_FX_MODE_TV_SIMULATOR[] PROGMEM = \"TV Simulator@!,!;;!;01\";\n\n\n/*\n  Aurora effect by @Mazen\n  improved and converted to integer math by @dedehai\n*/\n\n//CONFIG\n#ifdef ESP8266\n  #define W_MAX_COUNT  9          //Number of simultaneous waves\n#else\n  #define W_MAX_COUNT 20          //Number of simultaneous waves\n#endif\n#define W_MAX_SPEED 6             //Higher number, higher speed\n#define W_WIDTH_FACTOR 6          //Higher number, smaller waves\n\n// fixed-point math scaling\n#define AW_SHIFT 16\n#define AW_SCALE (1 << AW_SHIFT)  // 65536 representing 1.0\n\n// 32 bytes\nclass AuroraWave {\n  private:\n    int32_t center;               // scaled by AW_SCALE\n    uint32_t ageFactor_cached;    // cached age factor scaled by AW_SCALE\n    uint16_t ttl;\n    uint16_t age;\n    uint16_t width;\n    uint16_t basealpha;           // scaled by AW_SCALE\n    uint16_t speed_factor;        // scaled by AW_SCALE\n    int16_t  wave_start;          // wave start LED index\n    int16_t  wave_end;            // wave end LED index\n    bool goingleft;\n    bool alive = true;\n    CRGBW basecolor;\n\n  public:\n    void init(uint32_t segment_length, CRGBW color) {\n      ttl = hw_random16(500, 1501);\n      basecolor = color;\n      basealpha = hw_random8(60, 100) * AW_SCALE / 100; // 0-99% note: if using 100% there is risk of integer overflow\n      age = 0;\n      width = hw_random16(segment_length / 20, segment_length / W_WIDTH_FACTOR) + 1;\n      center = (((uint32_t)hw_random8(101) << AW_SHIFT) / 100) * segment_length; // 0-100%\n      goingleft = hw_random8() & 0x01; // 50/50 chance\n      speed_factor = (((uint32_t)hw_random8(10, 31) * W_MAX_SPEED) << AW_SHIFT) / (100 * 255);\n      alive = true;\n    }\n\n    void updateCachedValues() {\n      uint32_t half_ttl = ttl >> 1;\n      if (age < half_ttl) {\n        ageFactor_cached = ((uint32_t)age << AW_SHIFT) / half_ttl;\n      } else {\n        ageFactor_cached = ((uint32_t)(ttl - age) << AW_SHIFT) / half_ttl;\n      }\n      if (ageFactor_cached >= AW_SCALE) ageFactor_cached = AW_SCALE - 1; // prevent overflow\n\n      uint32_t center_led = center >> AW_SHIFT;\n      wave_start = (int16_t)center_led - (int16_t)width;\n      wave_end = (int16_t)center_led + (int16_t)width;\n    }\n\n    CRGBW getColorForLED(int ledIndex) {\n      // linear brightness falloff from center to edge of wave\n      if (ledIndex < wave_start || ledIndex > wave_end) return 0;\n      int32_t ledIndex_scaled = (int32_t)ledIndex << AW_SHIFT;\n      int32_t offset = ledIndex_scaled - center;\n      if (offset < 0) offset = -offset;\n      uint32_t offsetFactor = offset / width;  // scaled by AW_SCALE\n      if (offsetFactor > AW_SCALE) return 0;   // outside of wave\n      uint32_t brightness_factor = (AW_SCALE - offsetFactor);\n      brightness_factor = (brightness_factor * ageFactor_cached) >> AW_SHIFT;\n      brightness_factor = (brightness_factor * basealpha) >> AW_SHIFT;\n\n      CRGBW rgb;\n      rgb.r = (basecolor.r * brightness_factor) >> AW_SHIFT;\n      rgb.g = (basecolor.g * brightness_factor) >> AW_SHIFT;\n      rgb.b = (basecolor.b * brightness_factor) >> AW_SHIFT;\n      rgb.w = (basecolor.w * brightness_factor) >> AW_SHIFT;\n\n      return rgb;\n    };\n\n    //Change position and age of wave\n    //Determine if its still \"alive\"\n    void update(uint32_t segment_length, uint32_t speed) {\n      int32_t step = speed_factor * speed;\n      center += goingleft ? -step : step;\n      age++;\n\n      if (age > ttl) {\n        alive = false;\n      } else {\n        uint32_t width_scaled = (uint32_t)width << AW_SHIFT;\n        uint32_t segment_length_scaled = segment_length << AW_SHIFT;\n\n         if (goingleft) {\n           if (center < - (int32_t)width_scaled) {\n             alive = false;\n           }\n         } else {\n           if (center > (int32_t)segment_length_scaled + (int32_t)width_scaled) {\n             alive = false;\n           }\n         }\n      }\n    };\n\n    bool stillAlive() { return alive; }\n};\n\nvoid mode_aurora(void) {\n  AuroraWave* waves;\n  SEGENV.aux1 = map(SEGMENT.intensity, 0, 255, 2, W_MAX_COUNT); // aux1 = Wavecount\n  if (!SEGENV.allocateData(sizeof(AuroraWave) * SEGENV.aux1)) {\n    FX_FALLBACK_STATIC;\n  }\n  waves = reinterpret_cast<AuroraWave*>(SEGENV.data);\n\n  // note: on first call, SEGENV.data is zero -> all waves are dead and will be initialized\n  for (int i = 0; i < SEGENV.aux1; i++) {\n    waves[i].update(SEGLEN, SEGMENT.speed);\n    if (!(waves[i].stillAlive())) {\n      waves[i].init(SEGLEN, SEGMENT.color_from_palette(hw_random8(), false, false, hw_random8(0, 3)));\n    }\n    waves[i].updateCachedValues();\n  }\n\n  uint8_t backlight = 0; // note: original code used 1, with inverse gamma applied background would never be black\n  if (SEGCOLOR(0)) backlight++;\n  if (SEGCOLOR(1)) backlight++;\n  if (SEGCOLOR(2)) backlight++;\n  backlight = gamma8inv(backlight); // preserve backlight when using gamma correction\n\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    CRGBW mixedRgb = CRGBW(backlight, backlight, backlight);\n\n    for (int j = 0; j < SEGENV.aux1; j++) {\n      CRGBW rgb = waves[j].getColorForLED(i);\n      mixedRgb = color_add(mixedRgb, rgb); // sum all waves influencing this pixel\n    }\n\n    SEGMENT.setPixelColor(i, mixedRgb);\n  }\n}\nstatic const char _data_FX_MODE_AURORA[] PROGMEM = \"Aurora@!,!;1,2,3;!;;sx=24,pal=50\";\n\n\n/** Softly floating colorful clouds.\n * This is a very smooth effect that moves colorful clouds randomly around the LED strip.\n * It was initially intended for rather unobtrusive ambient lights (with very slow speed settings).\n * Nevertheless, it appears completely different and quite vibrant when the sliders are moved near\n * to their limits. No matter in which direction or in which combination...\n * Ported to WLED from https://github.com/JoaDick/EyeCandy/blob/master/ColorClouds.h\n */\nvoid mode_ColorClouds()\n{\n  // Set random start points for clouds and color.\n  if (SEGENV.call == 0) {\n    SEGENV.aux0 = hw_random16();\n    SEGENV.aux1 = hw_random16();\n  }\n  const uint32_t volX0 = SEGENV.aux0;\n  const uint32_t hueX0 = SEGENV.aux1;\n  const uint8_t hueOffset0 = volX0 + hueX0; // derive a 3rd random number\n\n  // Makes a very soft wraparound of the color palette by putting more emphasis on the begin & end\n  // of the palette (or on the red'ish colors in case of a rainbow spectrum).\n  // This gives the effect oftentimes an even more calm perception.\n  const bool cozy = SEGMENT.check3;\n\n  // Higher values make the clouds move faster.\n  const uint32_t volSpeed = 1 + SEGMENT.speed;\n  \n  // Higher values make the color change faster.\n  const uint32_t hueSpeed = 1 + SEGMENT.intensity;\n  \n  // Higher values make more clouds (but smaller ones).\n  const uint32_t volSqueeze = 8 + SEGMENT.custom1;\n  \n  // Higher values make the clouds more colorful.\n  const uint32_t hueSqueeze = SEGMENT.custom2;\n\n  // Higher values make larger gaps between the clouds.\n  const int32_t volCutoff   = 12500 + SEGMENT.custom3 * 900;\n  const int32_t volSaturate = 52000;\n  // Note: When adjusting these calculations, ensure that volCutoff is always smaller than volSaturate.\n\n  const uint32_t now = strip.now;\n  const uint32_t volT = now * volSpeed / 8;\n  const uint32_t hueT = now * hueSpeed / 8;\n  const uint8_t hueOffset = beat88(64) >> 8;\n\n  for (int i = 0; i < SEGLEN; i++) {\n    const uint32_t volX = i * volSqueeze * 64;\n    int32_t vol = perlin16(volX0 + volX, volT);\n    vol = map(vol, volCutoff, volSaturate, 0, 255);\n    vol = constrain(vol, 0, 255);\n\n    const uint32_t hueX = i * hueSqueeze * 8;\n    uint8_t hue = perlin16(hueX0 + hueX, hueT) >> 7;\n    hue += hueOffset0;\n    hue += hueOffset;\n    if (cozy) {\n      hue = cos8_t(128 + hue / 2);\n    }\n\n    uint32_t pixel;\n    if (SEGMENT.palette) { pixel = SEGMENT.color_from_palette(hue, false, true, 0, vol); }\n    else { hsv2rgb(CHSV32(hue, 255, vol), pixel); }\n\n    // Suppress extremely dark pixels to avoid flickering of plain r/g/b.\n    if (int(R(pixel)) + G(pixel) + B(pixel) <= 2) {\n      pixel = 0;\n    }\n\n    SEGMENT.setPixelColor(i, pixel);\n  }\n}\nstatic const char _data_FX_MODE_COLORCLOUDS[] PROGMEM = \"Color Clouds@!,!,Clouds,Colors,Distance,,,Cozy;;!;;sx=24,ix=32,c1=48,c2=64,c3=12,pal=0\";\n\n\n// WLED-SR effects\n\n/////////////////////////\n//     Perlin Move     //\n/////////////////////////\n// 16 bit perlinmove. Use Perlin Noise instead of sinewaves for movement. By Andrew Tuline.\n// Controls are speed, # of pixels, faderate.\nvoid mode_perlinmove(void) {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  SEGMENT.fade_out(255-SEGMENT.custom1);\n  for (int i = 0; i < SEGMENT.intensity/16 + 1; i++) {\n    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.\n    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.\n    SEGMENT.setPixelColor(pixloc, SEGMENT.color_from_palette(pixloc%255, false, PALETTE_SOLID_WRAP, 0));\n  }\n} // mode_perlinmove()\nstatic const char _data_FX_MODE_PERLINMOVE[] PROGMEM = \"Perlin Move@!,# of pixels,Fade rate;!,!;!\";\n\n\n/////////////////////////\n//     Waveins         //\n/////////////////////////\n// Uses beatsin8() + phase shifting. By: Andrew Tuline\nvoid mode_wavesins(void) {\n\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    uint8_t bri = sin8_t(strip.now/4 + i * SEGMENT.intensity);\n    uint8_t index = beatsin8_t(SEGMENT.speed, SEGMENT.custom1, SEGMENT.custom1+SEGMENT.custom2, 0, i * (SEGMENT.custom3<<3)); // custom3 is reduced resolution slider\n    //SEGMENT.setPixelColor(i, ColorFromPalette(SEGPALETTE, index, bri, LINEARBLEND));\n    SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0, bri));\n  }\n} // mode_waveins()\nstatic const char _data_FX_MODE_WAVESINS[] PROGMEM = \"Wavesins@!,Brightness variation,Starting color,Range of colors,Color variation;!;!\";\n\n\n//////////////////////////////\n//     Flow Stripe          //\n//////////////////////////////\n// By: ldirko  https://editor.soulmatelights.com/gallery/392-flow-led-stripe , modifed by: Andrew Tuline, fixed by @DedeHai\nvoid mode_FlowStripe(void) {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  const int hl = SEGLEN * 10 / 13;\n  uint8_t hue = strip.now / (SEGMENT.speed+1);\n  uint32_t t = strip.now / (SEGMENT.intensity/8+1);\n\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    int c = ((abs((int)i - hl) * 127) / hl);\n    c = sin8_t(c);\n    c = sin8_t(c / 2 + t);\n    byte b = sin8_t(c + t/8);\n    SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(b + hue, false, true, 3));\n  }\n} // mode_FlowStripe()\nstatic const char _data_FX_MODE_FLOWSTRIPE[] PROGMEM = \"Flow Stripe@Hue speed,Effect speed;;!;pal=11\";\n\n/*\n  Shimmer effect: moves a gradient with optional modulators across the strip at a given interval, up to 60 seconds\n  It can be used as an overlay to other effects or standalone\n  by DedeHai (Damian Schneider), based on idea from @Charming-Lime (#4905)\n*/\nvoid mode_shimmer() {\n  if(!SEGENV.allocateData(sizeof(uint32_t))) { FX_FALLBACK_STATIC; }\n  uint32_t* lastTime = reinterpret_cast<uint32_t*>(SEGENV.data);\n\n  uint32_t radius = (SEGMENT.custom1 * SEGLEN >> 7) + 1;        // [1, 2*SEGLEN+1] pixels\n  uint32_t traversalDistance = (SEGLEN + 2 * radius) << 8;      // total subpixels to cross, 1 pixel = 256 subpixels\n  uint32_t traversalTime = 200 + (255 - SEGMENT.speed) * 80;    // [200, 20600] ms\n  uint32_t speed = ((traversalDistance << 5) / traversalTime);  // subpixels/512ms\n  int32_t  position = static_cast<int32_t>(SEGENV.step);        // current position in subpixels\n  uint16_t inputstate = (uint16_t(SEGMENT.intensity) << 8) | uint16_t(SEGMENT.custom1); // current user input state\n\n  // init\n  if (SEGENV.call == 0 || inputstate != SEGENV.aux1) {\n    position = -(radius << 8);\n    SEGENV.aux0 = 0; // aux0 is pause timer\n    *lastTime = strip.now;\n    SEGENV.aux1 = inputstate; // save user input state\n  }\n\n  if(SEGMENT.speed) {\n    uint32_t deltaTime = (strip.now - *lastTime) & 0x7F; // clamp to 127ms to avoid overflows. note: speed*deltaTime can still overflow for segments > ~10k pixels\n    *lastTime = strip.now;\n\n    if (SEGENV.aux0 > 0) {\n      SEGENV.aux0 = (SEGENV.aux0 > deltaTime) ? SEGENV.aux0 - deltaTime : 0;\n    } else {\n      // calculate movement step and update position\n      int32_t step = 1 + ((speed * deltaTime) >> 5); // subpixels moved this frame. note >>5 as speed is in subpixels/512ms\n      position += step;\n      int endposition = (SEGLEN + radius) << 8;\n      if (position > endposition) {\n        SEGENV.aux0 = SEGMENT.intensity * 236; // [0, 60180] ms pause\n        if(SEGMENT.check3) SEGENV.aux0 = hw_random(SEGENV.aux0 + 1000); // randomise interval, +1 second to affect low intensity values\n        position = -(radius << 8); // reset to start position (out of frame)\n      }\n      SEGENV.step = (uint32_t)position; // save back\n    }\n\n    if (SEGMENT.check2)\n      position = (SEGLEN << 8) - position;   // invert position (and direction)\n  } else {\n    position = (SEGLEN << 7); // at speed=0, make it static in the center (this enables to use modulators only)\n  }\n\n  for (int i = 0; i < SEGLEN; i++) {\n    uint32_t dist = abs(position - (i << 8));\n    if (dist < (radius << 8)) {\n      uint32_t color = SEGMENT.color_from_palette(i * 255 / SEGLEN, false, false, 0);\n      uint8_t blend = dist / radius; // linear gradient note: dist is in subpixels, radius in pixels, result is [0, 255] since dist < radius*256\n      if (SEGMENT.custom2) {\n        uint8_t modVal; // modulation value\n        if (SEGMENT.check1) {\n          modVal = (sin16_t((i * SEGMENT.custom2 << 6) + (strip.now * SEGMENT.custom3 << 5)) >> 8) + 128; // sine modulation: regular \"Zebra\" stripes\n        } else {\n          modVal = perlin16((i * SEGMENT.custom2 << 7), strip.now * SEGMENT.custom3 << 5) >> 8; // perlin noise modulation\n        }\n        color = color_fade(color, modVal, true); // dim by modulator value\n      }\n      SEGMENT.setPixelColor(i, color_blend(color, SEGCOLOR(1), blend)); // blend to background color\n    } else {\n      SEGMENT.setPixelColor(i, SEGCOLOR(1));\n    }\n  }\n}\nstatic 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\";\n\n#ifndef WLED_DISABLE_2D\n///////////////////////////////////////////////////////////////////////////////\n//***************************  2D routines  ***********************************\n\n\n// Black hole\nvoid mode_2DBlackHole(void) {            // By: Stepko https://editor.soulmatelights.com/gallery/1012 , Modified by: Andrew Tuline\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n  int x, y;\n\n  SEGMENT.fadeToBlackBy(16 + (SEGMENT.speed>>3)); // create fading trails\n  unsigned long t = strip.now/128;                 // timebase\n  // outer stars\n  for (size_t i = 0; i < 8; i++) {\n    x = beatsin8_t(SEGMENT.custom1>>3,   0, cols - 1, 0, ((i % 2) ? 128 : 0) + t * i);\n    y = beatsin8_t(SEGMENT.intensity>>3, 0, rows - 1, 0, ((i % 2) ? 192 : 64) + t * i);\n    SEGMENT.addPixelColorXY(x, y, SEGMENT.color_from_palette(i*32, false, PALETTE_SOLID_WRAP, SEGMENT.check1?0:255));\n  }\n  // inner stars\n  for (size_t i = 0; i < 4; i++) {\n    x = beatsin8_t(SEGMENT.custom2>>3, cols/4, cols - 1 - cols/4, 0, ((i % 2) ? 128 : 0) + t * i);\n    y = beatsin8_t(SEGMENT.custom3   , rows/4, rows - 1 - rows/4, 0, ((i % 2) ? 192 : 64) + t * i);\n    SEGMENT.addPixelColorXY(x, y, SEGMENT.color_from_palette(255-i*64, false, PALETTE_SOLID_WRAP, SEGMENT.check1?0:255));\n  }\n  // central white dot\n  SEGMENT.setPixelColorXY(cols/2, rows/2, WHITE);\n  // blur everything a bit\n  if (SEGMENT.check3) SEGMENT.blur(16, cols*rows < 100);\n} // mode_2DBlackHole()\nstatic 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\";\n\n\n////////////////////////////\n//     2D Colored Bursts  //\n////////////////////////////\nvoid mode_2DColoredBursts() {              // By: ldirko   https://editor.soulmatelights.com/gallery/819-colored-bursts , modified by: Andrew Tuline\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  if (SEGENV.call == 0) {\n    SEGENV.aux0 = 0; // start with red hue\n  }\n\n  bool dot = SEGMENT.check3;\n  bool grad = SEGMENT.check1;\n\n  byte numLines = SEGMENT.intensity/16 + 1;\n\n  SEGENV.aux0++;  // hue\n  SEGMENT.fadeToBlackBy(40 - SEGMENT.check2 * 8);\n  for (size_t i = 0; i < numLines; i++) {\n    byte x1 = beatsin8_t(2 + SEGMENT.speed/16, 0, (cols - 1));\n    byte x2 = beatsin8_t(1 + SEGMENT.speed/16, 0, (rows - 1));\n    byte y1 = beatsin8_t(5 + SEGMENT.speed/16, 0, (cols - 1), 0, i * 24);\n    byte y2 = beatsin8_t(3 + SEGMENT.speed/16, 0, (rows - 1), 0, i * 48 + 64);\n    uint32_t color = ColorFromPalette(SEGPALETTE, i * 255 / numLines + (SEGENV.aux0&0xFF), 255, LINEARBLEND);\n\n    byte xsteps = abs8(x1 - y1) + 1;\n    byte ysteps = abs8(x2 - y2) + 1;\n    byte steps = xsteps >= ysteps ? xsteps : ysteps;\n    //Draw gradient line\n    for (size_t j = 1; j <= steps; j++) {\n      uint8_t rate = j * 255 / steps;\n      byte dx = lerp8by8(x1, y1, rate);\n      byte dy = lerp8by8(x2, y2, rate);\n      //SEGMENT.setPixelColorXY(dx, dy, grad ? color.nscale8_video(255-rate) : color); // use addPixelColorXY for different look\n      SEGMENT.addPixelColorXY(dx, dy, color); // use setPixelColorXY for different look\n      if (grad) SEGMENT.fadePixelColorXY(dx, dy, rate);\n    }\n\n    if (dot) { //add white point at the ends of line\n      SEGMENT.setPixelColorXY(x1, x2, WHITE);\n      SEGMENT.setPixelColorXY(y1, y2, DARKSLATEGRAY);\n    }\n  }\n  SEGMENT.blur(SEGMENT.custom3>>1, SEGMENT.check2);\n} // mode_2DColoredBursts()\nstatic const char _data_FX_MODE_2DCOLOREDBURSTS[] PROGMEM = \"Colored Bursts@Speed,# of lines,,,Blur,Gradient,Smear,Dots;;!;2;c3=16\";\n\n\n/////////////////////\n//      2D DNA     //\n/////////////////////\nvoid mode_2Ddna(void) {         // dna originally by by ldirko at https://pastebin.com/pCkkkzcs. Updated by Preyy. WLED conversion by Andrew Tuline.\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  SEGMENT.fadeToBlackBy(64);\n  for (int i = 0; i < cols; i++) {\n    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));\n    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));\n  }\n  SEGMENT.blur(SEGMENT.intensity / (8 - (SEGMENT.check1 * 2)), SEGMENT.check1);\n} // mode_2Ddna()\nstatic const char _data_FX_MODE_2DDNA[] PROGMEM = \"DNA@Scroll speed,Blur,,,,Smear;;!;2;ix=0\";\n\n/////////////////////////\n//     2D DNA Spiral   //\n/////////////////////////\nvoid mode_2DDNASpiral() {               // By: ldirko  https://editor.soulmatelights.com/gallery/512-dna-spiral-variation , modified by: Andrew Tuline\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  if (SEGENV.call == 0) {\n    SEGMENT.fill(BLACK);\n  }\n\n  unsigned speeds = SEGMENT.speed/2 + 7;\n  unsigned freq = SEGMENT.intensity/8;\n\n  uint32_t ms = strip.now / 20;\n  SEGMENT.fadeToBlackBy(135);\n\n  for (int i = 0; i < rows; i++) {\n    int x  = beatsin8_t(speeds, 0, cols - 1, 0, i * freq) + beatsin8_t(speeds - 7, 0, cols - 1, 0, i * freq + 128);\n    int x1 = beatsin8_t(speeds, 0, cols - 1, 0, 128 + i * freq) + beatsin8_t(speeds - 7, 0, cols - 1, 0, 128 + 64 + i * freq);\n    unsigned hue = (i * 128 / rows) + ms;\n    // skip every 4th row every now and then (fade it more)\n    if ((i + ms / 8) & 3) {\n      // draw a gradient line between x and x1\n      x = x / 2; x1 = x1 / 2;\n      unsigned steps = abs8(x - x1) + 1;\n      bool positive = (x1 >= x);                         // direction of drawing\n      for (size_t k = 1; k <= steps; k++) {\n        unsigned rate = k * 255 / steps;\n        //unsigned dx = lerp8by8(x, x1, rate);\n        unsigned dx = positive? (x + k-1) : (x - k+1);   // behaves the same as \"lerp8by8\" but does not create holes\n        //SEGMENT.setPixelColorXY(dx, i, ColorFromPalette(SEGPALETTE, hue, 255, LINEARBLEND).nscale8_video(rate));\n        SEGMENT.addPixelColorXY(dx, i, ColorFromPalette(SEGPALETTE, hue, 255, LINEARBLEND)); // use setPixelColorXY for different look\n        SEGMENT.fadePixelColorXY(dx, i, rate);\n      }\n      SEGMENT.setPixelColorXY(x, i, DARKSLATEGRAY);\n      SEGMENT.setPixelColorXY(x1, i, WHITE);\n    }\n  }\n  SEGMENT.blur(((uint16_t)SEGMENT.custom1 * 3) / (6 + SEGMENT.check1), SEGMENT.check1);\n} // mode_2DDNASpiral()\nstatic const char _data_FX_MODE_2DDNASPIRAL[] PROGMEM = \"DNA Spiral@Scroll speed,Y frequency,Blur,,,Smear;;!;2;c1=0\";\n\n\n/////////////////////////\n//     2D Drift        //\n/////////////////////////\nvoid mode_2DDrift() {              // By: Stepko   https://editor.soulmatelights.com/gallery/884-drift , Modified by: Andrew Tuline\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  const int colsCenter = (cols>>1) + (cols%2);\n  const int rowsCenter = (rows>>1) + (rows%2);\n\n  SEGMENT.fadeToBlackBy(128);\n  const float maxDim = MAX(cols, rows)/2;\n  unsigned long t = strip.now / (32 - (SEGMENT.speed>>3));\n  unsigned long t_20 = t/20; // softhack007: pre-calculating this gives about 10% speedup\n  for (float i = 1.0f; i < maxDim; i += 0.25f) {\n    float angle = radians(t * (maxDim - i));\n    int mySin = sin_t(angle) * i;\n    int myCos = cos_t(angle) * i;\n    SEGMENT.setPixelColorXY(colsCenter + mySin, rowsCenter + myCos, ColorFromPalette(SEGPALETTE, (i * 20) + t_20, 255, LINEARBLEND));\n    if (SEGMENT.check1) SEGMENT.setPixelColorXY(colsCenter + myCos, rowsCenter + mySin, ColorFromPalette(SEGPALETTE, (i * 20) + t_20, 255, LINEARBLEND));\n  }\n  SEGMENT.blur(SEGMENT.intensity>>(3 - SEGMENT.check2), SEGMENT.check2);\n} // mode_2DDrift()\nstatic const char _data_FX_MODE_2DDRIFT[] PROGMEM = \"Drift@Rotation speed,Blur,,,,Twin,Smear;;!;2;ix=0\";\n\n\n//////////////////////////\n//     2D Firenoise     //\n//////////////////////////\nvoid mode_2Dfirenoise(void) {               // firenoise2d. By Andrew Tuline. Yet another short routine.\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  if (SEGENV.call == 0) {\n    SEGMENT.fill(BLACK);\n  }\n\n  unsigned xscale = SEGMENT.intensity*4;\n  unsigned yscale = SEGMENT.speed*8;\n  unsigned indexx = 0;\n\n  //CRGBPalette16 pal = SEGMENT.check1 ? SEGPALETTE : SEGMENT.loadPalette(pal, 35);  \n  CRGBPalette16 pal = SEGMENT.check1 ? SEGPALETTE : CRGBPalette16(CRGB::Black,     CRGB::Black,      CRGB::Black,  CRGB::Black,\n                                                                  CRGB::Red,       CRGB::Red,        CRGB::Red,    CRGB::DarkOrange,\n                                                                  CRGB::DarkOrange,CRGB::DarkOrange, CRGB::Orange, CRGB::Orange,\n                                                                  CRGB::Yellow,    CRGB::Orange,     CRGB::Yellow, CRGB::Yellow);\n  for (int j=0; j < cols; j++) {\n    for (int i=0; i < rows; i++) {\n      indexx = perlin8(j*yscale*rows/255, i*xscale+strip.now/4);                                               // We're moving along our Perlin map.\n      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.    \n    } // for i\n  } // for j\n} // mode_2Dfirenoise()\nstatic const char _data_FX_MODE_2DFIRENOISE[] PROGMEM = \"Firenoise@X scale,Y scale,,,,Palette;;!;2;pal=66\";\n\n\n//////////////////////////////\n//     2D Frizzles          //\n//////////////////////////////\nvoid mode_2DFrizzles(void) {                 // By: Stepko https://editor.soulmatelights.com/gallery/640-color-frizzles , Modified by: Andrew Tuline\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  SEGMENT.fadeToBlackBy(16 + SEGMENT.check1 * 10);\n  for (size_t i = 8; i > 0; i--) {\n    SEGMENT.addPixelColorXY(beatsin8_t(SEGMENT.speed/8 + i, 0, cols - 1),\n                            beatsin8_t(SEGMENT.intensity/8 - i, 0, rows - 1),\n                            ColorFromPalette(SEGPALETTE, beatsin8_t(12, 0, 255), 255, LINEARBLEND));\n  }\n  SEGMENT.blur(SEGMENT.custom1 >> (3 + SEGMENT.check1), SEGMENT.check1);\n} // mode_2DFrizzles()\nstatic const char _data_FX_MODE_2DFRIZZLES[] PROGMEM = \"Frizzles@X frequency,Y frequency,Blur,,,Smear;;!;2\";\n\n\n///////////////////////////////////////////\n//   2D Cellular Automata Game of life   //\n///////////////////////////////////////////\ntypedef struct Cell {\n    uint8_t alive : 1, faded : 1, toggleStatus : 1, edgeCell: 1, oscillatorCheck : 1, spaceshipCheck : 1, unused : 2;\n} Cell;\n\nvoid mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://natureofcode.com/book/chapter-7-cellular-automata/ \n                                   // and https://github.com/DougHaber/nlife-color , Modified By: Brandon Butler\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n  const int cols = SEG_W, rows = SEG_H;\n  const unsigned maxIndex = cols * rows;\n\n  if (!SEGENV.allocateData(SEGMENT.length() * sizeof(Cell))) FX_FALLBACK_STATIC; // allocation failed\n\n  Cell *cells = reinterpret_cast<Cell*> (SEGENV.data);\n\n  uint16_t& generation = SEGENV.aux0, &gliderLength = SEGENV.aux1; // rename aux variables for clarity\n  bool mutate = SEGMENT.check3;\n  uint8_t blur = map(SEGMENT.custom1, 0, 255, 255, 4);\n\n  uint32_t bgColor    = SEGCOLOR(1);\n  uint32_t birthColor = SEGMENT.color_from_palette(128, false, PALETTE_SOLID_WRAP, 255);\n\n  bool setup = SEGENV.call == 0;\n  if (setup) {\n    // Calculate glider length LCM(rows,cols)*4 once\n    unsigned a = rows, b = cols;\n    while (b) { unsigned t = b; b = a % b; a = t; }\n    gliderLength = (cols * rows / a) << 2;\n  }\n\n  if (abs(long(strip.now) - long(SEGENV.step)) > 2000) SEGENV.step = 0; // Timebase jump fix\n  bool paused = SEGENV.step > strip.now;\n\n  // Setup New Game of Life\n  if ((!paused && generation == 0) || setup) {\n    SEGENV.step = strip.now + 1280; // show initial state for 1.28 seconds\n    generation = 1;\n    paused = true;\n    //Setup Grid\n    memset(cells, 0, maxIndex * sizeof(Cell));\n\n    for (unsigned i = 0; i < maxIndex; i++) {\n      bool isAlive = !hw_random8(3); // ~33%\n      cells[i].alive = isAlive;\n      cells[i].faded = !isAlive;\n      unsigned x = i % cols, y = i / cols;\n      cells[i].edgeCell = (x == 0 || x == cols-1 || y == 0 || y == rows-1);\n\n      SEGMENT.setPixelColor(i, isAlive ? SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 0) : bgColor);\n    }\n  }\n\n  if (paused || (strip.now - SEGENV.step < 1000 / map(SEGMENT.speed,0,255,1,42))) {\n    // Redraw if paused or between updates to remove blur\n    for (unsigned i = maxIndex; i--; ) {\n      if (!cells[i].alive) {\n        uint32_t cellColor = SEGMENT.getPixelColor(i);\n        if (cellColor != bgColor) {\n          uint32_t newColor;\n          bool needsColor = false;\n          if (cells[i].faded) { newColor = bgColor; needsColor = true; }\n          else {\n            uint32_t blended = color_blend(cellColor, bgColor, 2);\n            if (blended == cellColor) { blended = bgColor; cells[i].faded = 1; }\n            newColor = blended; needsColor = true;\n          }\n          if (needsColor) SEGMENT.setPixelColor(i, newColor);\n        }\n      }\n    }\n  }\n\n  // Repeat detection\n  bool updateOscillator = generation % 16 == 0;\n  bool updateSpaceship  = gliderLength && generation % gliderLength == 0;\n  bool repeatingOscillator = true, repeatingSpaceship = true, emptyGrid = true;\n\n  unsigned cIndex = maxIndex-1;\n  for (unsigned y = rows; y--; ) for (unsigned x = cols; x--; cIndex--) {\n    Cell& cell = cells[cIndex];\n\n    if (cell.alive) emptyGrid = false;\n    if (cell.oscillatorCheck != cell.alive) repeatingOscillator = false;\n    if (cell.spaceshipCheck  != cell.alive) repeatingSpaceship  = false;\n    if (updateOscillator) cell.oscillatorCheck = cell.alive;\n    if (updateSpaceship)  cell.spaceshipCheck  = cell.alive;\n\n    unsigned neighbors = 0, aliveParents = 0, parentIdx[3];\n    // Count alive neighbors\n    for (int i = -1; i <= 1; i++) for (int j = -1; j <= 1; j++) if (i || j) {\n      int nX = x + j, nY = y + i;\n      if (cell.edgeCell) {\n        nX = (nX + cols) % cols;\n        nY = (nY + rows) % rows;\n      }\n      unsigned nIndex = nX + nY * cols;\n      Cell& neighbor = cells[nIndex];\n      if (neighbor.alive) {\n        neighbors++;\n        if (!neighbor.toggleStatus && neighbors < 4) { // Alive and not dying\n          parentIdx[aliveParents++] = nIndex;\n        }\n      }\n    }\n\n    uint32_t newColor;\n    bool needsColor = false;\n\n    if (cell.alive && (neighbors < 2 || neighbors > 3)) { // Loneliness or Overpopulation\n      cell.toggleStatus = 1;\n      if (blur == 255) cell.faded = 1;\n      newColor = cell.faded ? bgColor : color_blend(SEGMENT.getPixelColor(cIndex), bgColor, blur);\n      needsColor = true;\n    }\n    else if (!cell.alive) {\n      byte mutationRoll = mutate ? hw_random8(128) : 1; // if 0: 3 neighbor births fail and 2 neighbor births mutate\n      if ((neighbors == 3 && mutationRoll) || (mutate && neighbors == 2 && !mutationRoll)) { // Reproduction or Mutation\n        cell.toggleStatus = 1;\n        cell.faded = 0;\n\n        if (aliveParents) {\n          // Set color based on random neighbor\n          unsigned parentIndex = parentIdx[random8(aliveParents)];\n          birthColor = SEGMENT.getPixelColor(parentIndex);\n        }\n        newColor = birthColor;\n        needsColor = true;\n      }\n      else if (!cell.faded) {// No change, fade dead cells\n          uint32_t cellColor = SEGMENT.getPixelColor(cIndex);\n          uint32_t blended = color_blend(cellColor, bgColor, blur);\n          if (blended == cellColor) { blended = bgColor; cell.faded = 1; }\n          newColor = blended;\n          needsColor = true;\n      }\n    }\n\n    if (needsColor) SEGMENT.setPixelColor(cIndex, newColor);\n  }\n  // Loop through cells, if toggle, swap alive status\n  for (unsigned i = maxIndex; i--; ) {\n    cells[i].alive ^= cells[i].toggleStatus;\n    cells[i].toggleStatus = 0;\n  }\n\n  if (repeatingOscillator || repeatingSpaceship || emptyGrid) {\n    generation = 0; // reset on next call\n    SEGENV.step += 1024; // pause final generation for ~1 second\n  }\n  else {\n    ++generation;\n    SEGENV.step = strip.now;\n  }\n} // mode_2Dgameoflife()\nstatic const char _data_FX_MODE_2DGAMEOFLIFE[] PROGMEM = \"Game Of Life@!,,Blur,,,,,Mutation;!,!;!;2;pal=11,sx=128\";\n\n\n/////////////////////////\n//     2D Hiphotic     //\n/////////////////////////\nvoid mode_2DHiphotic() {                        //  By: ldirko  https://editor.soulmatelights.com/gallery/810 , Modified by: Andrew Tuline\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n  const uint32_t a = strip.now / ((SEGMENT.custom3>>1)+1);\n\n  for (int x = 0; x < cols; x++) {\n    for (int y = 0; y < rows; y++) {\n      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));\n    }\n  }\n} // mode_2DHiphotic()\nstatic const char _data_FX_MODE_2DHIPHOTIC[] PROGMEM = \"Hiphotic@X scale,Y scale,,,Speed;!;!;2\";\n\n\n/////////////////////////\n//     2D Julia        //\n/////////////////////////\n// Sliders are:\n// intensity = Maximum number of iterations per pixel.\n// Custom1 = Location of X centerpoint\n// Custom2 = Location of Y centerpoint\n// Custom3 = Size of the area (small value = smaller area)\ntypedef struct Julia {\n  float xcen;\n  float ycen;\n  float xymag;\n} julia;\n\nvoid mode_2DJulia(void) {                           // An animated Julia set by Andrew Tuline.\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  if (!SEGENV.allocateData(sizeof(julia))) FX_FALLBACK_STATIC;\n  Julia* julias = reinterpret_cast<Julia*>(SEGENV.data);\n\n  float reAl;\n  float imAg;\n\n  if (SEGENV.call == 0) {           // Reset the center if we've just re-started this animation.\n    julias->xcen = 0.;\n    julias->ycen = 0.;\n    julias->xymag = 1.0;\n\n    SEGMENT.custom1 = 128;              // Make sure the location widgets are centered to start.\n    SEGMENT.custom2 = 128;\n    SEGMENT.custom3 = 16;\n    SEGMENT.intensity = 24;\n  }\n\n  julias->xcen  = julias->xcen  + (float)(SEGMENT.custom1 - 128)/100000.f;\n  julias->ycen  = julias->ycen  + (float)(SEGMENT.custom2 - 128)/100000.f;\n  julias->xymag = julias->xymag + (float)((SEGMENT.custom3 - 16)<<3)/100000.f; // reduced resolution slider\n  if (julias->xymag < 0.01f) julias->xymag = 0.01f;\n  if (julias->xymag > 1.0f) julias->xymag = 1.0f;\n\n  float xmin = julias->xcen - julias->xymag;\n  float xmax = julias->xcen + julias->xymag;\n  float ymin = julias->ycen - julias->xymag;\n  float ymax = julias->ycen + julias->xymag;\n\n  // Whole set should be within -1.2,1.2 to -.8 to 1.\n  xmin = constrain(xmin, -1.2f, 1.2f);\n  xmax = constrain(xmax, -1.2f, 1.2f);\n  ymin = constrain(ymin, -0.8f, 1.0f);\n  ymax = constrain(ymax, -0.8f, 1.0f);\n\n  float dx;                       // Delta x is mapped to the matrix size.\n  float dy;                       // Delta y is mapped to the matrix size.\n\n  int maxIterations = 15;         // How many iterations per pixel before we give up. Make it 8 bits to match our range of colours.\n  float maxCalc = 16.0;           // How big is each calculation allowed to be before we give up.\n\n  maxIterations = SEGMENT.intensity/2;\n\n\n  // Resize section on the fly for some animaton.\n  reAl = -0.94299f;               // PixelBlaze example\n  imAg = 0.3162f;\n\n  reAl += (float)sin16_t(strip.now * 34) / 655340.f;\n  imAg += (float)sin16_t(strip.now * 26) / 655340.f;\n\n  dx = (xmax - xmin) / (cols);     // Scale the delta x and y values to our matrix size.\n  dy = (ymax - ymin) / (rows);\n\n  // Start y\n  float y = ymin;\n  for (int j = 0; j < rows; j++) {\n\n    // Start x\n    float x = xmin;\n    for (int i = 0; i < cols; i++) {\n\n      // Now we test, as we iterate z = z^2 + c does z tend towards infinity?\n      float a = x;\n      float b = y;\n      int iter = 0;\n\n      while (iter < maxIterations) {    // Here we determine whether or not we're out of bounds.\n        float aa = a * a;\n        float bb = b * b;\n        float len = aa + bb;\n        if (len > maxCalc) {            // |z| = sqrt(a^2+b^2) OR z^2 = a^2+b^2 to save on having to perform a square root.\n          break;  // Bail\n        }\n\n       // This operation corresponds to z -> z^2+c where z=a+ib c=(x,y). Remember to use 'foil'.\n        b = 2*a*b + imAg;\n        a = aa - bb + reAl;\n        iter++;\n      } // while\n\n      // We color each pixel based on how long it takes to get to infinity, or black if it never gets there.\n      if (iter == maxIterations) {\n        SEGMENT.setPixelColorXY(i, j, 0);\n      } else {\n        SEGMENT.setPixelColorXY(i, j, SEGMENT.color_from_palette(iter*255/maxIterations, false, PALETTE_SOLID_WRAP, 0));\n      }\n      x += dx;\n    }\n    y += dy;\n  }\n  if(SEGMENT.check1)\n    SEGMENT.blur(100, true);\n} // mode_2DJulia()\nstatic 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\";\n\n\n//////////////////////////////\n//     2D Lissajous         //\n//////////////////////////////\nvoid mode_2DLissajous(void) {            // By: Andrew Tuline\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  SEGMENT.fadeToBlackBy(SEGMENT.intensity);\n  uint_fast16_t phase = (strip.now * (1 + SEGENV.custom3)) /32;  // allow user to control rotation speed\n\n  //for (int i=0; i < 4*(cols+rows); i ++) {\n  for (int i=0; i < 256; i ++) {\n    //float xlocn = float(sin8_t(now/4+i*(SEGMENT.speed>>5))) / 255.0f;\n    //float ylocn = float(cos8_t(now/4+i*2)) / 255.0f;\n    uint_fast8_t xlocn = sin8_t(phase/2 + (i*SEGMENT.speed)/32);\n    uint_fast8_t ylocn = cos8_t(phase/2 + i*2);\n    xlocn = (cols < 2) ? 1 : (map(2*xlocn, 0,511, 0,2*(cols-1)) +1) /2;    // softhack007: \"(2* ..... +1) /2\" for proper rounding\n    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()\n    SEGMENT.setPixelColorXY((uint8_t)xlocn, (uint8_t)ylocn, SEGMENT.color_from_palette(strip.now/100+i, false, PALETTE_SOLID_WRAP, 0));\n  }\n  SEGMENT.blur(SEGMENT.custom1 >> (1 + SEGMENT.check1 * 3), SEGMENT.check1);\n} // mode_2DLissajous()\nstatic const char _data_FX_MODE_2DLISSAJOUS[] PROGMEM = \"Lissajous@X frequency,Fade rate,Blur,,Speed,Smear;!;!;2;c1=0\";\n\n\n///////////////////////\n//    2D Matrix      //\n///////////////////////\nvoid mode_2Dmatrix(void) {                  // Matrix2D. By Jeremy Williams. Adapted by Andrew Tuline & improved by merkisoft and ewowi, and softhack007.\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n  const auto XY = [&](int x, int y) { return (x%cols) + (y%rows) * cols; };\n\n  unsigned dataSize = (SEGMENT.length()+7) >> 3; //1 bit per LED for trails\n  if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed\n\n  if (SEGENV.call == 0) {\n    SEGMENT.fill(BLACK);\n    SEGENV.step = 0;\n  }\n\n  uint8_t fade = map(SEGMENT.custom1, 0, 255, 50, 250);    // equals trail size\n  uint8_t speed = (256-SEGMENT.speed) >> map(min(rows, 150), 0, 150, 0, 3);    // slower speeds for small displays\n\n  uint32_t spawnColor;\n  uint32_t trailColor;\n  if (SEGMENT.check1) {\n    spawnColor = SEGCOLOR(0);\n    trailColor = SEGCOLOR(1);\n  } else {\n    spawnColor = RGBW32(175,255,175,0);\n    trailColor = RGBW32(27,130,39,0);\n  }\n\n  bool emptyScreen = true;\n  if (strip.now - SEGENV.step >= speed) {\n    SEGENV.step = strip.now;\n    // move pixels one row down. Falling codes keep color and add trail pixels; all others pixels are faded\n    // TODO: it would be better to paint trails idividually instead of relying on fadeToBlackBy()\n    SEGMENT.fadeToBlackBy(fade);\n    for (int row = rows-1; row >= 0; row--) {\n      for (int col = 0; col < cols; col++) {\n        unsigned index = XY(col, row) >> 3;\n        unsigned bitNum = XY(col, row) & 0x07;\n        if (bitRead(SEGENV.data[index], bitNum)) {\n          SEGMENT.setPixelColorXY(col, row, trailColor);  // create trail\n          bitClear(SEGENV.data[index], bitNum);\n          if (row < rows-1) {\n            SEGMENT.setPixelColorXY(col, row+1, spawnColor);\n            index = XY(col, row+1) >> 3;\n            bitNum = XY(col, row+1) & 0x07;\n            bitSet(SEGENV.data[index], bitNum);\n            emptyScreen = false;\n          }\n        }\n      }\n    }\n\n    // spawn new falling code\n    if (hw_random8() <= SEGMENT.intensity || emptyScreen) {\n      uint8_t spawnX = hw_random8(cols);\n      SEGMENT.setPixelColorXY(spawnX, 0, spawnColor);\n      // update hint for next run\n      unsigned index = XY(spawnX, 0) >> 3;\n      unsigned bitNum = XY(spawnX, 0) & 0x07;\n      bitSet(SEGENV.data[index], bitNum);\n    }\n  }\n} // mode_2Dmatrix()\nstatic const char _data_FX_MODE_2DMATRIX[] PROGMEM = \"Matrix@!,Spawning rate,Trail,,,Custom color;Spawn,Trail;;2\";\n\n\n/////////////////////////\n//     2D Metaballs    //\n/////////////////////////\nvoid mode_2Dmetaballs(void) {   // Metaballs by Stefan Petrick. Cannot have one of the dimensions be 2 or less. Adapted by Andrew Tuline.\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  float speed = 0.25f * (1+(SEGMENT.speed>>6));\n\n  // get some 2 random moving points\n  int x2 = map(perlin8(strip.now * speed, 25355, 685), 0, 255, 0, cols-1);\n  int y2 = map(perlin8(strip.now * speed, 355, 11685), 0, 255, 0, rows-1);\n\n  int x3 = map(perlin8(strip.now * speed, 55355, 6685), 0, 255, 0, cols-1);\n  int y3 = map(perlin8(strip.now * speed, 25355, 22685), 0, 255, 0, rows-1);\n\n  // and one Lissajou function\n  int x1 = beatsin8_t(23 * speed, 0, cols-1);\n  int y1 = beatsin8_t(28 * speed, 0, rows-1);\n\n  for (int y = 0; y < rows; y++) {\n    for (int x = 0; x < cols; x++) {\n      // calculate distances of the 3 points from actual pixel\n      // and add them together with weightening\n      unsigned dx = abs(x - x1);\n      unsigned dy = abs(y - y1);\n      unsigned dist = 2 * sqrt32_bw((dx * dx) + (dy * dy));\n\n      dx = abs(x - x2);\n      dy = abs(y - y2);\n      dist += sqrt32_bw((dx * dx) + (dy * dy));\n\n      dx = abs(x - x3);\n      dy = abs(y - y3);\n      dist += sqrt32_bw((dx * dx) + (dy * dy));\n\n      // inverse result\n      int color = dist ? 1000 / dist : 255;\n\n      // map color between thresholds\n      if (color > 0 and color < 60) {\n        SEGMENT.setPixelColorXY(x, y, SEGMENT.color_from_palette(map(color * 9, 9, 531, 0, 255), false, PALETTE_SOLID_WRAP, 0));\n      } else {\n        SEGMENT.setPixelColorXY(x, y, SEGMENT.color_from_palette(0, false, PALETTE_SOLID_WRAP, 0));\n      }\n      // show the 3 points, too\n      SEGMENT.setPixelColorXY(x1, y1, WHITE);\n      SEGMENT.setPixelColorXY(x2, y2, WHITE);\n      SEGMENT.setPixelColorXY(x3, y3, WHITE);\n    }\n  }\n} // mode_2Dmetaballs()\nstatic const char _data_FX_MODE_2DMETABALLS[] PROGMEM = \"Metaballs@!;;!;2\";\n\n\n//////////////////////\n//    2D Noise      //\n//////////////////////\nvoid mode_2Dnoise(void) {                  // By Andrew Tuline\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  const unsigned scale  = SEGMENT.intensity+2;\n\n  for (int y = 0; y < rows; y++) {\n    for (int x = 0; x < cols; x++) {\n      uint8_t pixelHue8 = perlin8(x * scale, y * scale, strip.now / (16 - SEGMENT.speed/16));\n      SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, pixelHue8));\n    }\n  }\n} // mode_2Dnoise()\nstatic const char _data_FX_MODE_2DNOISE[] PROGMEM = \"Noise2D@!,Scale;;!;2\";\n\n\n//////////////////////////////\n//     2D Plasma Ball       //\n//////////////////////////////\nvoid mode_2DPlasmaball(void) {                   // By: Stepko https://editor.soulmatelights.com/gallery/659-plasm-ball , Modified by: Andrew Tuline\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  SEGMENT.fadeToBlackBy(SEGMENT.custom1>>2);\n  uint_fast32_t t = (strip.now * 8) / (256 - SEGMENT.speed);  // optimized to avoid float\n  for (int i = 0; i < cols; i++) {\n    unsigned thisVal = perlin8(i * 30, t, t);\n    unsigned thisMax = map(thisVal, 0, 255, 0, cols-1);\n    for (int j = 0; j < rows; j++) {\n      unsigned thisVal_ = perlin8(t, j * 30, t);\n      unsigned thisMax_ = map(thisVal_, 0, 255, 0, rows-1);\n      int x = (i + thisMax_ - cols / 2);\n      int y = (j + thisMax - cols / 2);\n      int cx = (i + thisMax_);\n      int cy = (j + thisMax);\n\n      SEGMENT.addPixelColorXY(i, j, ((x - y > -2) && (x - y < 2)) ||\n                                    ((cols - 1 - x - y) > -2 && (cols - 1 - x - y < 2)) ||\n                                    (cols - cx == 0) ||\n                                    (cols - 1 - cx == 0) ||\n                                    ((rows - cy == 0) ||\n                                    (rows - 1 - cy == 0)) ? ColorFromPalette(SEGPALETTE, beat8(5), thisVal, LINEARBLEND) : CRGB::Black);\n    }\n  }\n  SEGMENT.blur(SEGMENT.custom2>>5);\n} // mode_2DPlasmaball()\nstatic const char _data_FX_MODE_2DPLASMABALL[] PROGMEM = \"Plasma Ball@Speed,,Fade,Blur;;!;2\";\n\n\n////////////////////////////////\n//  2D Polar Lights           //\n////////////////////////////////\n\nvoid mode_2DPolarLights(void) {        // By: Kostyantyn Matviyevskyy  https://editor.soulmatelights.com/gallery/762-polar-lights , Modified by: Andrew Tuline & @dedehai (palette support)\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  if (SEGENV.call == 0) {\n    SEGMENT.fill(BLACK);\n    SEGENV.step = 0;\n  }\n\n  float adjustHeight = (float)map(rows, 8, 32, 28, 12); // maybe use mapf() ???\n  unsigned adjScale = map(cols, 8, 64, 310, 63);\n  unsigned _scale = map(SEGMENT.intensity, 0, 255, 30, adjScale);\n  int _speed = map(SEGMENT.speed, 0, 255, 128, 16);\n\n  for (int x = 0; x < cols; x++) {\n    for (int y = 0; y < rows; y++) {\n      SEGENV.step++;\n      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);\n      uint8_t palbrightness = palindex;\n      if(SEGMENT.check1) palindex = 255 - palindex; //flip palette\n      SEGMENT.setPixelColorXY(x, y, SEGMENT.color_from_palette(palindex, false, false, 255, palbrightness));\n    }\n  }\n} // mode_2DPolarLights()\nstatic const char _data_FX_MODE_2DPOLARLIGHTS[] PROGMEM = \"Polar Lights@!,Scale,,,,Flip Palette;;!;2;pal=71\";\n\n\n/////////////////////////\n//     2D Pulser       //\n/////////////////////////\nvoid mode_2DPulser(void) {                       // By: ldirko   https://editor.soulmatelights.com/gallery/878-pulse-test , modifed by: Andrew Tuline\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  SEGMENT.fadeToBlackBy(8 - (SEGMENT.intensity>>5));\n  uint32_t a = strip.now / (18 - SEGMENT.speed / 16);\n  int x = (a / 14) % cols;\n  int y = map((sin8_t(a * 5) + sin8_t(a * 4) + sin8_t(a * 2)), 0, 765, rows-1, 0);\n  SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, map(y, 0, rows-1, 0, 255), 255, LINEARBLEND));\n\n  SEGMENT.blur(SEGMENT.intensity>>4);\n} // mode_2DPulser()\nstatic const char _data_FX_MODE_2DPULSER[] PROGMEM = \"Pulser@!,Blur;;!;2\";\n\n\n/////////////////////////\n//     2D Sindots      //\n/////////////////////////\nvoid mode_2DSindots(void) {                             // By: ldirko   https://editor.soulmatelights.com/gallery/597-sin-dots , modified by: Andrew Tuline\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  if (SEGENV.call == 0) {\n    SEGMENT.fill(BLACK);\n  }\n\n  SEGMENT.fadeToBlackBy((SEGMENT.custom1>>3) + (SEGMENT.check1 * 24));\n\n  byte t1 = strip.now / (257 - SEGMENT.speed); // 20;\n  byte t2 = sin8_t(t1) / 4 * 2;\n  for (int i = 0; i < 13; i++) {\n    int x = sin8_t(t1 + i * SEGMENT.intensity/8)*(cols-1)/255;  // max index now 255x15/255=15!\n    int y = sin8_t(t2 + i * SEGMENT.intensity/8)*(rows-1)/255;  // max index now 255x15/255=15!\n    SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, i * 255 / 13, 255, LINEARBLEND));\n  }\n  SEGMENT.blur(SEGMENT.custom2 >> (3 + SEGMENT.check1), SEGMENT.check1);\n} // mode_2DSindots()\nstatic const char _data_FX_MODE_2DSINDOTS[] PROGMEM = \"Sindots@!,Dot distance,Fade rate,Blur,,Smear;;!;2;\";\n\n\n//////////////////////////////\n//     2D Squared Swirl     //\n//////////////////////////////\n// custom3 affects the blur amount.\nvoid mode_2Dsquaredswirl(void) {            // By: Mark Kriegsman. https://gist.github.com/kriegsman/368b316c55221134b160\n                                                          // Modifed by: Andrew Tuline\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  const uint8_t kBorderWidth = 2;\n\n  SEGMENT.fadeToBlackBy(1 + SEGMENT.intensity / 5);\n  SEGMENT.blur(SEGMENT.custom3>>1);\n\n  // Use two out-of-sync sine waves\n  int i = beatsin8_t(19, kBorderWidth, cols-kBorderWidth);\n  int j = beatsin8_t(22, kBorderWidth, cols-kBorderWidth);\n  int k = beatsin8_t(17, kBorderWidth, cols-kBorderWidth);\n  int m = beatsin8_t(18, kBorderWidth, rows-kBorderWidth);\n  int n = beatsin8_t(15, kBorderWidth, rows-kBorderWidth);\n  int p = beatsin8_t(20, kBorderWidth, rows-kBorderWidth);\n\n  SEGMENT.addPixelColorXY(i, m, ColorFromPalette(SEGPALETTE, strip.now/29, 255, LINEARBLEND));\n  SEGMENT.addPixelColorXY(j, n, ColorFromPalette(SEGPALETTE, strip.now/41, 255, LINEARBLEND));\n  SEGMENT.addPixelColorXY(k, p, ColorFromPalette(SEGPALETTE, strip.now/73, 255, LINEARBLEND));\n} // mode_2Dsquaredswirl()\nstatic const char _data_FX_MODE_2DSQUAREDSWIRL[] PROGMEM = \"Squared Swirl@,Fade,,,Blur;;!;2\";\n\n\n//////////////////////////////\n//     2D Sun Radiation     //\n//////////////////////////////\nvoid mode_2DSunradiation(void) {                   // By: ldirko https://editor.soulmatelights.com/gallery/599-sun-radiation  , modified by: Andrew Tuline\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  if (!SEGENV.allocateData(sizeof(byte)*(cols+2)*(rows+2))) FX_FALLBACK_STATIC; //allocation failed\n  byte *bump = reinterpret_cast<byte*>(SEGENV.data);\n\n  if (SEGENV.call == 0) {\n    SEGMENT.fill(BLACK);\n  }\n\n  unsigned long t = strip.now / 4;\n  unsigned index = 0;\n  uint8_t someVal = SEGMENT.speed/4;             // Was 25.\n  for (int j = 0; j < (rows + 2); j++) {\n    for (int i = 0; i < (cols + 2); i++) {\n      //byte col = (inoise8_raw(i * someVal, j * someVal, t)) / 2;\n      byte col = ((int16_t)perlin8(i * someVal, j * someVal, t) - 0x7F) / 3;\n      bump[index++] = col;\n    }\n  }\n\n  int yindex = cols + 3;\n  int vly = -(rows / 2 + 1);\n  for (int y = 0; y < rows; y++) {\n    ++vly;\n    int vlx = -(cols / 2 + 1);\n    for (int x = 0; x < cols; x++) {\n      ++vlx;\n      int nx = bump[x + yindex + 1] - bump[x + yindex - 1];\n      int ny = bump[x + yindex + (cols + 2)] - bump[x + yindex - (cols + 2)];\n      unsigned difx = abs8(vlx * 7 - nx);\n      unsigned dify = abs8(vly * 7 - ny);\n      int temp = difx * difx + dify * dify;\n      int col = 255 - temp / 8; //8 its a size of effect\n      if (col < 0) col = 0;\n      SEGMENT.setPixelColorXY(x, y, HeatColor(col / (3.0f-(float)(SEGMENT.intensity)/128.f)));\n    }\n    yindex += (cols + 2);\n  }\n} // mode_2DSunradiation()\nstatic const char _data_FX_MODE_2DSUNRADIATION[] PROGMEM = \"Sun Radiation@Variance,Brightness;;;2\";\n\n\n/////////////////////////\n//     2D Tartan       //\n/////////////////////////\nvoid mode_2Dtartan(void) {          // By: Elliott Kember  https://editor.soulmatelights.com/gallery/3-tartan , Modified by: Andrew Tuline\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  if (SEGENV.call == 0) {\n    SEGMENT.fill(BLACK);\n  }\n\n  uint8_t hue, bri;\n  size_t intensity;\n  int offsetX = beatsin16_t(3, -360, 360);\n  int offsetY = beatsin16_t(2, -360, 360);\n  int sharpness = SEGMENT.custom3 / 8; // 0-3\n\n  for (int x = 0; x < cols; x++) {\n    for (int y = 0; y < rows; y++) {\n      hue = x * beatsin16_t(10, 1, 10) + offsetY;\n      intensity = bri = sin8_t(x * SEGMENT.speed/2 + offsetX);\n      for (int i=0; i<sharpness; i++) intensity *= bri;\n      intensity >>= 8*sharpness;\n      SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, hue, intensity, LINEARBLEND));\n      hue = y * 3 + offsetX;\n      intensity = bri = sin8_t(y * SEGMENT.intensity/2 + offsetY);\n      for (int i=0; i<sharpness; i++) intensity *= bri;\n      intensity >>= 8*sharpness;\n      SEGMENT.addPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, hue, intensity, LINEARBLEND));\n    }\n  }\n} // mode_2DTartan()\nstatic const char _data_FX_MODE_2DTARTAN[] PROGMEM = \"Tartan@X scale,Y scale,,,Sharpness;;!;2\";\n\n\n/////////////////////////\n//     2D spaceships   //\n/////////////////////////\nvoid 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)\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  uint32_t tb = strip.now >> 12;  // every ~4s\n  if (tb > SEGENV.step) {\n    int dir = ++SEGENV.aux0;\n    dir  += (int)hw_random8(3)-1;\n    if      (dir > 7) SEGENV.aux0 = 0;\n    else if (dir < 0) SEGENV.aux0 = 7;\n    else              SEGENV.aux0 = dir;\n    SEGENV.step = tb + hw_random8(4);\n  }\n\n  SEGMENT.fadeToBlackBy(map(SEGMENT.speed, 0, 255, 248, 16));\n  SEGMENT.move(SEGENV.aux0, 1);\n\n  for (size_t i = 0; i < 8; i++) {\n    int x = beatsin8_t(12 + i, 2, cols - 3);\n    int y = beatsin8_t(15 + i, 2, rows - 3);\n    uint32_t color = ColorFromPalette(SEGPALETTE, beatsin8_t(12 + i, 0, 255), 255);\n    SEGMENT.addPixelColorXY(x, y, color);\n    if (cols > 24 || rows > 24) {\n      SEGMENT.addPixelColorXY(x+1, y, color);\n      SEGMENT.addPixelColorXY(x-1, y, color);\n      SEGMENT.addPixelColorXY(x, y+1, color);\n      SEGMENT.addPixelColorXY(x, y-1, color);\n    }\n  }\n  SEGMENT.blur(SEGMENT.intensity >> 3, SEGMENT.check1);\n}\nstatic const char _data_FX_MODE_2DSPACESHIPS[] PROGMEM = \"Spaceships@!,Blur,,,,Smear;;!;2\";\n\n\n/////////////////////////\n//     2D Crazy Bees   //\n/////////////////////////\n//// 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\n#define MAX_BEES 5\nvoid mode_2Dcrazybees(void) {\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  byte n = MIN(MAX_BEES, (rows * cols) / 256 + 1);\n\n  typedef struct Bee {\n    uint8_t posX, posY, aimX, aimY, hue;\n    int8_t deltaX, deltaY, signX, signY, error;\n    void aimed(uint16_t w, uint16_t h) {\n      //random16_set_seed(millis());\n      aimX   = random8(0, w);\n      aimY   = random8(0, h);\n      hue    = random8();\n      deltaX = abs(aimX - posX);\n      deltaY = abs(aimY - posY);\n      signX  = posX < aimX ? 1 : -1;\n      signY  = posY < aimY ? 1 : -1;\n      error  = deltaX - deltaY;\n    };\n  } bee_t;\n\n  if (!SEGENV.allocateData(sizeof(bee_t)*MAX_BEES)) FX_FALLBACK_STATIC; //allocation failed\n  bee_t *bee = reinterpret_cast<bee_t*>(SEGENV.data);\n\n  if (SEGENV.call == 0) {\n    random16_set_seed(strip.now);\n    for (size_t i = 0; i < n; i++) {\n      bee[i].posX = random8(0, cols);\n      bee[i].posY = random8(0, rows);\n      bee[i].aimed(cols, rows);\n    }\n  }\n\n  if (strip.now > SEGENV.step) {\n    SEGENV.step = strip.now + (FRAMETIME * 16 / ((SEGMENT.speed>>4)+1));\n    SEGMENT.fadeToBlackBy(32 + ((SEGMENT.check1*SEGMENT.intensity) / 25));\n    SEGMENT.blur(SEGMENT.intensity / (2 + SEGMENT.check1 * 9), SEGMENT.check1);\n    for (size_t i = 0; i < n; i++) {\n      uint32_t flowerCcolor = SEGMENT.color_from_palette(bee[i].hue, false, true, 255);\n      SEGMENT.addPixelColorXY(bee[i].aimX + 1, bee[i].aimY, flowerCcolor);\n      SEGMENT.addPixelColorXY(bee[i].aimX, bee[i].aimY + 1, flowerCcolor);\n      SEGMENT.addPixelColorXY(bee[i].aimX - 1, bee[i].aimY, flowerCcolor);\n      SEGMENT.addPixelColorXY(bee[i].aimX, bee[i].aimY - 1, flowerCcolor);\n      if (bee[i].posX != bee[i].aimX || bee[i].posY != bee[i].aimY) {\n        SEGMENT.setPixelColorXY(bee[i].posX, bee[i].posY, CRGB(CHSV(bee[i].hue, 60, 255)));\n        int error2 = bee[i].error * 2;\n        if (error2 > -bee[i].deltaY) {\n          bee[i].error -= bee[i].deltaY;\n          bee[i].posX += bee[i].signX;\n        }\n        if (error2 < bee[i].deltaX) {\n          bee[i].error += bee[i].deltaX;\n          bee[i].posY += bee[i].signY;\n        }\n      } else {\n        bee[i].aimed(cols, rows);\n      }\n    }\n  }\n}\nstatic const char _data_FX_MODE_2DCRAZYBEES[] PROGMEM = \"Crazy Bees@!,Blur,,,,Smear;;!;2;pal=11,ix=0\";\n#undef MAX_BEES\n\n#ifdef WLED_PS_DONT_REPLACE_2D_FX\n/////////////////////////\n//     2D Ghost Rider  //\n/////////////////////////\n//// Ghost Rider by stepko (c)2021 [https://editor.soulmatelights.com/gallery/716-ghost-rider], adapted by Blaz Kristan (AKA blazoncek)\n#define LIGHTERS_AM 64  // max lighters (adequate for 32x32 matrix)\nvoid mode_2Dghostrider(void) {\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  typedef struct Lighter {\n    int16_t  gPosX;\n    int16_t  gPosY;\n    uint16_t gAngle;\n    int8_t   angleSpeed;\n    uint16_t lightersPosX[LIGHTERS_AM];\n    uint16_t lightersPosY[LIGHTERS_AM];\n    uint16_t Angle[LIGHTERS_AM];\n    uint16_t time[LIGHTERS_AM];\n    bool     reg[LIGHTERS_AM];\n    int8_t   Vspeed;\n  } lighter_t;\n\n  if (!SEGENV.allocateData(sizeof(lighter_t))) FX_FALLBACK_STATIC; //allocation failed\n  lighter_t *lighter = reinterpret_cast<lighter_t*>(SEGENV.data);\n\n  const size_t maxLighters = min(cols + rows, LIGHTERS_AM);\n\n  if (SEGENV.aux0 != cols || SEGENV.aux1 != rows) {\n    SEGENV.aux0 = cols;\n    SEGENV.aux1 = rows;\n    lighter->angleSpeed = hw_random8(0,20) - 10;\n    lighter->gAngle = hw_random16();\n    lighter->Vspeed = 5;\n    lighter->gPosX = (cols/2) * 10;\n    lighter->gPosY = (rows/2) * 10;\n    for (size_t i = 0; i < maxLighters; i++) {\n      lighter->lightersPosX[i] = lighter->gPosX;\n      lighter->lightersPosY[i] = lighter->gPosY + i;\n      lighter->time[i] = i * 2;\n      lighter->reg[i] = false;\n    }\n  }\n\n  if (strip.now > SEGENV.step) {\n    SEGENV.step = strip.now + 1024 / (cols+rows);\n\n    SEGMENT.fadeToBlackBy((SEGMENT.speed>>2)+64);\n\n    CRGB color = CRGB::White;\n    SEGMENT.wu_pixel(lighter->gPosX * 256 / 10, lighter->gPosY * 256 / 10, color);\n\n    lighter->gPosX += lighter->Vspeed * sin_t(radians(lighter->gAngle));\n    lighter->gPosY += lighter->Vspeed * cos_t(radians(lighter->gAngle));\n    lighter->gAngle += lighter->angleSpeed;\n    if (lighter->gPosX < 0)               lighter->gPosX = (cols - 1) * 10;\n    if (lighter->gPosX > (cols - 1) * 10) lighter->gPosX = 0;\n    if (lighter->gPosY < 0)               lighter->gPosY = (rows - 1) * 10;\n    if (lighter->gPosY > (rows - 1) * 10) lighter->gPosY = 0;\n    for (size_t i = 0; i < maxLighters; i++) {\n      lighter->time[i] += hw_random8(5, 20);\n      if (lighter->time[i] >= 255 ||\n        (lighter->lightersPosX[i] <= 0) ||\n          (lighter->lightersPosX[i] >= (cols - 1) * 10) ||\n          (lighter->lightersPosY[i] <= 0) ||\n          (lighter->lightersPosY[i] >= (rows - 1) * 10)) {\n        lighter->reg[i] = true;\n      }\n      if (lighter->reg[i]) {\n        lighter->lightersPosY[i] = lighter->gPosY;\n        lighter->lightersPosX[i] = lighter->gPosX;\n        lighter->Angle[i] = lighter->gAngle + ((int)hw_random8(20) - 10);\n        lighter->time[i] = 0;\n        lighter->reg[i] = false;\n      } else {\n        lighter->lightersPosX[i] += -7 * sin_t(radians(lighter->Angle[i]));\n        lighter->lightersPosY[i] += -7 * cos_t(radians(lighter->Angle[i]));\n      }\n      SEGMENT.wu_pixel(lighter->lightersPosX[i] * 256 / 10, lighter->lightersPosY[i] * 256 / 10, ColorFromPalette(SEGPALETTE, (256 - lighter->time[i])));\n    }\n    SEGMENT.blur(SEGMENT.intensity>>3);\n  }\n}\nstatic const char _data_FX_MODE_2DGHOSTRIDER[] PROGMEM = \"Ghost Rider@Fade rate,Blur;;!;2\";\n#undef LIGHTERS_AM\n\n////////////////////////////\n//     2D Floating Blobs  //\n////////////////////////////\n//// Floating Blobs by stepko (c)2021 [https://editor.soulmatelights.com/gallery/573-blobs], adapted by Blaz Kristan (AKA blazoncek)\n#define MAX_BLOBS 8\nvoid mode_2Dfloatingblobs(void) {\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  typedef struct Blob {\n    float x[MAX_BLOBS], y[MAX_BLOBS];\n    float sX[MAX_BLOBS], sY[MAX_BLOBS]; // speed\n    float r[MAX_BLOBS];\n    bool grow[MAX_BLOBS];\n    byte color[MAX_BLOBS];\n  } blob_t;\n\n  size_t Amount = (SEGMENT.intensity>>5) + 1; // NOTE: be sure to update MAX_BLOBS if you change this\n\n  if (!SEGENV.allocateData(sizeof(blob_t))) FX_FALLBACK_STATIC; //allocation failed\n  blob_t *blob = reinterpret_cast<blob_t*>(SEGENV.data);\n\n  if (SEGENV.aux0 != cols || SEGENV.aux1 != rows) {\n    SEGENV.aux0 = cols; // re-initialise if virtual size changes\n    SEGENV.aux1 = rows;\n    //SEGMENT.fill(BLACK);\n    for (size_t i = 0; i < MAX_BLOBS; i++) {\n      blob->r[i]  = hw_random8(1, cols>8 ? (cols/4) : 2);\n      blob->sX[i] = (float) hw_random8(3, cols) / (float)(256 - SEGMENT.speed); // speed x\n      blob->sY[i] = (float) hw_random8(3, rows) / (float)(256 - SEGMENT.speed); // speed y\n      blob->x[i]  = hw_random8(0, cols-1);\n      blob->y[i]  = hw_random8(0, rows-1);\n      blob->color[i] = hw_random8();\n      blob->grow[i]  = (blob->r[i] < 1.f);\n      if (blob->sX[i] == 0) blob->sX[i] = 1;\n      if (blob->sY[i] == 0) blob->sY[i] = 1;\n    }\n  }\n\n  SEGMENT.fadeToBlackBy((SEGMENT.custom2>>3)+1);\n\n  // Bounce balls around\n  for (size_t i = 0; i < Amount; i++) {\n    if (SEGENV.step < strip.now) blob->color[i] = add8(blob->color[i], 4); // slowly change color\n    // change radius if needed\n    if (blob->grow[i]) {\n      // enlarge radius until it is >= 4\n      blob->r[i] += (fabsf(blob->sX[i]) > fabsf(blob->sY[i]) ? fabsf(blob->sX[i]) : fabsf(blob->sY[i])) * 0.05f;\n      if (blob->r[i] >= MIN(cols/4.f,2.f)) {\n        blob->grow[i] = false;\n      }\n    } else {\n      // reduce radius until it is < 1\n      blob->r[i] -= (fabsf(blob->sX[i]) > fabsf(blob->sY[i]) ? fabsf(blob->sX[i]) : fabsf(blob->sY[i])) * 0.05f;\n      if (blob->r[i] < 1.f) {\n        blob->grow[i] = true;\n      }\n    }\n    uint32_t c = SEGMENT.color_from_palette(blob->color[i], false, false, 0);\n    if (blob->r[i] > 1.f) SEGMENT.fillCircle(roundf(blob->x[i]), roundf(blob->y[i]), roundf(blob->r[i]), c);\n    else                  SEGMENT.setPixelColorXY((int)roundf(blob->x[i]), (int)roundf(blob->y[i]), c);\n    // move x\n    if (blob->x[i] + blob->r[i] >= cols - 1) blob->x[i] += (blob->sX[i] * ((cols - 1 - blob->x[i]) / blob->r[i] + 0.005f));\n    else if (blob->x[i] - blob->r[i] <= 0)   blob->x[i] += (blob->sX[i] * (blob->x[i] / blob->r[i] + 0.005f));\n    else                                     blob->x[i] += blob->sX[i];\n    // move y\n    if (blob->y[i] + blob->r[i] >= rows - 1) blob->y[i] += (blob->sY[i] * ((rows - 1 - blob->y[i]) / blob->r[i] + 0.005f));\n    else if (blob->y[i] - blob->r[i] <= 0)   blob->y[i] += (blob->sY[i] * (blob->y[i] / blob->r[i] + 0.005f));\n    else                                     blob->y[i] += blob->sY[i];\n    // bounce x\n    if (blob->x[i] < 0.01f) {\n      blob->sX[i] = (float)hw_random8(3, cols) / (256 - SEGMENT.speed);\n      blob->x[i]  = 0.01f;\n    } else if (blob->x[i] > (float)cols - 1.01f) {\n      blob->sX[i] = (float)hw_random8(3, cols) / (256 - SEGMENT.speed);\n      blob->sX[i] = -blob->sX[i];\n      blob->x[i]  = (float)cols - 1.01f;\n    }\n    // bounce y\n    if (blob->y[i] < 0.01f) {\n      blob->sY[i] = (float)hw_random8(3, rows) / (256 - SEGMENT.speed);\n      blob->y[i]  = 0.01f;\n    } else if (blob->y[i] > (float)rows - 1.01f) {\n      blob->sY[i] = (float)hw_random8(3, rows) / (256 - SEGMENT.speed);\n      blob->sY[i] = -blob->sY[i];\n      blob->y[i]  = (float)rows - 1.01f;\n    }\n  }\n  SEGMENT.blur(SEGMENT.custom1>>2);\n\n  if (SEGENV.step < strip.now) SEGENV.step = strip.now + 2000; // change colors every 2 seconds\n}\nstatic const char _data_FX_MODE_2DBLOBS[] PROGMEM = \"Blobs@!,# blobs,Blur,Trail;!;!;2;c1=8\";\n#undef MAX_BLOBS\n#endif // WLED_PS_DONT_REPLACE_2D_FX\n\n////////////////////////////\n//     2D Scrolling text  //\n////////////////////////////\nvoid mode_2Dscrollingtext(void) {\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  unsigned letterWidth, rotLW;\n  unsigned letterHeight, rotLH;\n  switch (map(SEGMENT.custom2, 0, 255, 1, 5)) {\n    default:\n    case 1: letterWidth = 4; letterHeight =  6; break;\n    case 2: letterWidth = 5; letterHeight =  8; break;\n    case 3: letterWidth = 6; letterHeight =  8; break;\n    case 4: letterWidth = 7; letterHeight =  9; break;\n    case 5: letterWidth = 5; letterHeight = 12; break;\n  }\n  // letters are rotated\n  const int8_t rotate = map(SEGMENT.custom3, 0, 31, -2, 2);\n  if (rotate == 1 || rotate == -1) {\n    rotLH = letterWidth;\n    rotLW = letterHeight;\n  } else {\n    rotLW = letterWidth;\n    rotLH = letterHeight;\n  }\n\n  char text[WLED_MAX_SEGNAME_LEN+1] = {'\\0'};\n  size_t result_pos = 0;\n  char sec[5];\n  int  AmPmHour = hour(localTime);\n  bool isitAM = true;\n  if (useAMPM) {\n    if (AmPmHour > 11) { AmPmHour -= 12; isitAM = false; }\n    if (AmPmHour == 0) { AmPmHour  = 12; }\n    sprintf_P(sec, PSTR(\" %2s\"), (isitAM ? \"AM\" : \"PM\"));\n  } else {\n    sprintf_P(sec, PSTR(\":%02d\"), second(localTime));\n  }\n\n  size_t len = 0;\n  if (SEGMENT.name) len = strlen(SEGMENT.name); // note: SEGMENT.name is limited to WLED_MAX_SEGNAME_LEN\n  if (len == 0) { // fallback if empty segment name: display date and time\n    sprintf_P(text, PSTR(\"%s %d, %d %d:%02d%s\"), monthShortStr(month(localTime)), day(localTime), year(localTime), AmPmHour, minute(localTime), sec);\n  } else {\n    size_t i = 0;\n    while (i < len) {\n      if (SEGMENT.name[i] == '#') {\n        char token[7]; // copy up to 6 chars + null terminator\n        bool zero = false; // a 0 suffix means display leading zeros\n        size_t j = 0;\n        while (j < 6 && i + j < len) {\n          token[j] = std::toupper(SEGMENT.name[i + j]);\n          if(token[j] == '0')\n            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\n          j++;\n        }\n        token[j] = '\\0';\n        int advance = 5; // number of chars to advance in 'text' after processing the token\n\n        // Process token\n        char temp[32];\n        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));\n        else if (!strncmp_P(token,PSTR(\"#DDMM\"),5))  sprintf_P(temp, zero?PSTR(\"%02d.%02d\")     :PSTR(\"%d.%d\"),      day(localTime),   month(localTime));\n        else if (!strncmp_P(token,PSTR(\"#MMDD\"),5))  sprintf_P(temp, zero?PSTR(\"%02d/%02d\")     :PSTR(\"%d/%d\"),      month(localTime), day(localTime));\n        else if (!strncmp_P(token,PSTR(\"#TIME\"),5))  sprintf_P(temp, zero?PSTR(\"%02d:%02d%s\")   :PSTR(\"%2d:%02d%s\"), AmPmHour,         minute(localTime), sec);\n        else if (!strncmp_P(token,PSTR(\"#HHMM\"),5))  sprintf_P(temp, zero?PSTR(\"%02d:%02d\")     :PSTR(\"%d:%02d\"),    AmPmHour,         minute(localTime));\n        else if (!strncmp_P(token,PSTR(\"#YYYY\"),5))  sprintf_P(temp,          PSTR(\"%04d\")                 ,         year(localTime));\n        else if (!strncmp_P(token,PSTR(\"#MONL\"),5))  sprintf  (temp,          (\"%s\")                       ,         monthStr(month(localTime)));\n        else if (!strncmp_P(token,PSTR(\"#DDDD\"),5))  sprintf  (temp,          (\"%s\")                       ,         dayStr(weekday(localTime)));\n        else if (!strncmp_P(token,PSTR(\"#YY\"),3))  { sprintf  (temp,          (\"%02d\")                     ,         year(localTime)%100); advance = 3; }\n        else if (!strncmp_P(token,PSTR(\"#HH\"),3))  { sprintf  (temp, zero?    (\"%02d\")          :    (\"%d\"),         AmPmHour); advance = 3; }\n        else if (!strncmp_P(token,PSTR(\"#MM\"),3))  { sprintf  (temp, zero?    (\"%02d\")          :    (\"%d\"),         minute(localTime)); advance = 3; }\n        else if (!strncmp_P(token,PSTR(\"#SS\"),3))  { sprintf  (temp, zero?    (\"%02d\")          :    (\"%d\"),         second(localTime)); advance = 3; }\n        else if (!strncmp_P(token,PSTR(\"#MON\"),4)) { sprintf  (temp,          (\"%s\")                       ,         monthShortStr(month(localTime))); advance = 4; }\n        else if (!strncmp_P(token,PSTR(\"#MO\"),3))  { sprintf  (temp, zero?    (\"%02d\")          :    (\"%d\"),         month(localTime)); advance = 3; }\n        else if (!strncmp_P(token,PSTR(\"#DAY\"),4)) { sprintf  (temp,          (\"%s\")                       ,         dayShortStr(weekday(localTime))); advance = 4; }\n        else if (!strncmp_P(token,PSTR(\"#DD\"),3))  { sprintf  (temp, zero?    (\"%02d\")          :    (\"%d\"),         day(localTime)); advance = 3; }\n        else { temp[0] = '#'; temp[1] = '\\0'; zero = false; advance = 1; } // Unknown token, just copy the #\n\n        if(zero) advance++; // skip the '0' suffix\n        size_t temp_len = strlen(temp);\n        if (result_pos + temp_len < WLED_MAX_SEGNAME_LEN) {\n          strcpy(text + result_pos, temp);\n          result_pos += temp_len;\n        }\n\n        i += advance;\n      }\n      else {\n        if (result_pos < WLED_MAX_SEGNAME_LEN) {\n          text[result_pos++] = SEGMENT.name[i++]; // no token, just copy char\n        } else\n          break; // buffer full\n      }\n    }\n  }\n\n  const int  numberOfLetters = strlen(text);\n  int width = (numberOfLetters * rotLW);\n  int yoffset = map(SEGMENT.intensity, 0, 255, -rows/2, rows/2) + (rows-rotLH)/2;\n  if (width <= cols) {\n    // scroll vertically (e.g. ^^ Way out ^^) if it fits\n    int speed = map(SEGMENT.speed, 0, 255, 5000, 1000);\n    int frac = strip.now % speed + 1;\n    if (SEGMENT.intensity == 255) {\n      yoffset = (2 * frac * rows)/speed - rows;\n    } else if (SEGMENT.intensity == 0) {\n      yoffset = rows - (2 * frac * rows)/speed;\n    }\n  }\n\n  if (SEGENV.step < strip.now) {\n    // calculate start offset\n    if (width > cols) {\n      if (SEGMENT.check3) {\n        if (SEGENV.aux0 == 0) SEGENV.aux0  = width + cols - 1;\n        else                --SEGENV.aux0;\n      } else                ++SEGENV.aux0 %= width + cols;\n    } else                    SEGENV.aux0  = (cols + width)/2;\n    ++SEGENV.aux1 &= 0xFF; // color shift\n    SEGENV.step = strip.now + map(SEGMENT.speed, 0, 255, 250, 50); // shift letters every ~250ms to ~50ms\n  }\n\n  SEGMENT.fade_out(255 - (SEGMENT.custom1>>4));  // trail\n  uint32_t col1 = SEGMENT.color_from_palette(SEGENV.aux1, false, PALETTE_SOLID_WRAP, 0);\n  uint32_t col2 = BLACK;\n  // if gradient is selected and palette is default (0) drawCharacter() uses gradient from SEGCOLOR(0) to SEGCOLOR(2)\n  // otherwise col2 == BLACK means use currently selected palette for gradient\n  // if gradient is not selected set both colors the same\n  if (SEGMENT.check1) { // use gradient\n    if (SEGMENT.palette == 0) { // use colors for gradient\n      col1 = SEGCOLOR(0);\n      col2 = SEGCOLOR(2);\n    }\n  } else col2 = col1; // force characters to use single color (from palette)\n\n  for (int i = 0; i < numberOfLetters; i++) {\n    int xoffset = int(cols) - int(SEGENV.aux0) + rotLW*i;\n    if (xoffset + rotLW < 0) continue; // don't draw characters off-screen\n    SEGMENT.drawCharacter(text[i], xoffset, yoffset, letterWidth, letterHeight, col1, col2, rotate);\n  }\n}\nstatic 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\";\n\n\n////////////////////////////\n//     2D Drift Rose      //\n////////////////////////////\n//// Drift Rose by stepko (c)2021 [https://editor.soulmatelights.com/gallery/1369-drift-rose-pattern], adapted by Blaz Kristan (AKA blazoncek) improved by @dedehai\nvoid mode_2Ddriftrose(void) {\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  const float CX = (cols-cols%2)/2.f - .5f;\n  const float CY = (rows-rows%2)/2.f - .5f;\n  const float L = min(cols, rows) / 2.f;\n\n  SEGMENT.fadeToBlackBy(32+(SEGMENT.speed>>3));\n  for (size_t i = 1; i < 37; i++) {\n    float angle = radians(i * 10);\n    uint32_t x = (CX + (sin_t(angle) * (beatsin8_t(i, 0, L*2)-L))) * 255.f;\n    uint32_t y = (CY + (cos_t(angle) * (beatsin8_t(i, 0, L*2)-L))) * 255.f;\n    if(SEGMENT.palette == 0) SEGMENT.wu_pixel(x, y, CHSV(i * 10, 255, 255));\n    else SEGMENT.wu_pixel(x, y, ColorFromPalette(SEGPALETTE, i * 10));\n  }\n  SEGMENT.blur(SEGMENT.intensity >> 4, SEGMENT.check1);\n}\nstatic const char _data_FX_MODE_2DDRIFTROSE[] PROGMEM = \"Drift Rose@Fade,Blur,,,,Smear;;!;2;pal=11\";\n\n/////////////////////////////\n//  2D PLASMA ROTOZOOMER   //\n/////////////////////////////\n// Plasma Rotozoomer by ldirko (c)2020 [https://editor.soulmatelights.com/gallery/457-plasma-rotozoomer], adapted for WLED by Blaz Kristan (AKA blazoncek)\nvoid mode_2Dplasmarotozoom() {\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  unsigned dataSize = SEGMENT.length() + sizeof(float);\n  if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed\n  float *a = reinterpret_cast<float*>(SEGENV.data);\n  byte *plasma = reinterpret_cast<byte*>(SEGENV.data+sizeof(float));\n\n  unsigned ms = strip.now/15;  \n\n  // plasma\n  for (int j = 0; j < rows; j++) {\n    int index = j*cols;\n    for (int i = 0; i < cols; i++) {\n      if (SEGMENT.check1) plasma[index+i] = (i * 4 ^ j * 4) + ms / 6;\n      else                plasma[index+i] = inoise8(i * 40, j * 40, ms);\n    }\n  }\n\n  // rotozoom\n  float f       = (sin_t(*a/2)+((128-SEGMENT.intensity)/128.0f)+1.1f)/1.5f;  // scale factor\n  float kosinus = cos_t(*a) * f;\n  float sinus   = sin_t(*a) * f;\n  for (int i = 0; i < cols; i++) {\n    float u1 = i * kosinus;\n    float v1 = i * sinus;\n    for (int j = 0; j < rows; j++) {\n        byte u = abs8(u1 - j * sinus) % cols;\n        byte v = abs8(v1 + j * kosinus) % rows;\n        SEGMENT.setPixelColorXY(i, j, SEGMENT.color_from_palette(plasma[v*cols+u], false, PALETTE_SOLID_WRAP, 255));\n    }\n  }\n  *a -= 0.03f + float(SEGENV.speed-128)*0.0002f;  // rotation speed\n  if(*a < -6283.18530718f) *a += 6283.18530718f; // 1000*2*PI, protect sin/cos from very large input float values (will give wrong results)\n}\nstatic const char _data_FX_MODE_2DPLASMAROTOZOOM[] PROGMEM = \"Rotozoomer@!,Scale,,,,Alt;;!;2;pal=54\";\n\n#endif // WLED_DISABLE_2D\n\n\n///////////////////////////////////////////////////////////////////////////////\n/********************     audio enhanced routines     ************************/\n///////////////////////////////////////////////////////////////////////////////\n\n\n/////////////////////////////////\n//     * Ripple Peak           //\n/////////////////////////////////\nvoid mode_ripplepeak(void) {                // * Ripple peak. By Andrew Tuline.\n                                                          // This currently has no controls.\n  #define MAXSTEPS 16                                     // Case statement wouldn't allow a variable.\n\n  unsigned maxRipples = 16;\n  unsigned dataSize = sizeof(Ripple) * maxRipples;\n  if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed\n  Ripple* ripples = reinterpret_cast<Ripple*>(SEGENV.data);\n\n  um_data_t *um_data = getAudioData();\n  uint8_t samplePeak    = *(uint8_t*)um_data->u_data[3];\n  #ifdef ESP32\n  float   FFT_MajorPeak = *(float*)  um_data->u_data[4];\n  #endif\n  uint8_t *maxVol       =  (uint8_t*)um_data->u_data[6];\n  uint8_t *binNum       =  (uint8_t*)um_data->u_data[7];\n\n  // printUmData();\n\n  if (SEGENV.call == 0) {\n    SEGMENT.custom1 = *binNum;\n    SEGMENT.custom2 = *maxVol * 2;\n  }\n\n  *binNum = SEGMENT.custom1;                              // Select a bin.\n  *maxVol = SEGMENT.custom2 / 2;                          // Our volume comparator.\n\n  SEGMENT.fade_out(240);                                  // Lower frame rate means less effective fading than FastLED\n  SEGMENT.fade_out(240);\n\n  for (int i = 0; i < SEGMENT.intensity/16; i++) {   // Limit the number of ripples.\n    if (samplePeak) ripples[i].state = 255;\n\n    switch (ripples[i].state) {\n      case 254:     // Inactive mode\n        break;\n\n      case 255:                                           // Initialize ripple variables.\n        ripples[i].pos = hw_random16(SEGLEN);\n        #ifdef ESP32\n          if (FFT_MajorPeak > 1)                          // log10(0) is \"forbidden\" (throws exception)\n          ripples[i].color = (int)(log10f(FFT_MajorPeak)*128);\n          else ripples[i].color = 0;\n        #else\n          ripples[i].color = hw_random8();\n        #endif\n        ripples[i].state = 0;\n        break;\n\n      case 0:\n        SEGMENT.setPixelColor(ripples[i].pos, SEGMENT.color_from_palette(ripples[i].color, false, PALETTE_SOLID_WRAP, 0));\n        ripples[i].state++;\n        break;\n\n      case MAXSTEPS:                                      // At the end of the ripples. 254 is an inactive mode.\n        ripples[i].state = 254;\n        break;\n\n      default:                                            // Middle of the ripples.\n        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)));\n        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)));\n        ripples[i].state++;                               // Next step.\n        break;\n    } // switch step\n  } // for i\n} // mode_ripplepeak()\nstatic 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\n\n\n#ifndef WLED_DISABLE_2D\n/////////////////////////\n//    * 2D Swirl       //\n/////////////////////////\n// By: Mark Kriegsman https://gist.github.com/kriegsman/5adca44e14ad025e6d3b , modified by Andrew Tuline\nvoid mode_2DSwirl(void) {\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  if (SEGENV.call == 0) {\n    SEGMENT.fill(BLACK);\n  }\n\n  const uint8_t borderWidth = 2;\n\n  SEGMENT.blur(SEGMENT.custom1);\n\n  int  i = beatsin8_t( 27*SEGMENT.speed/255, borderWidth, cols - borderWidth);\n  int  j = beatsin8_t( 41*SEGMENT.speed/255, borderWidth, rows - borderWidth);\n  int ni = (cols - 1) - i;\n  int nj = (cols - 1) - j;\n\n  um_data_t *um_data = getAudioData();\n  float volumeSmth  = *(float*)   um_data->u_data[0]; //ewowi: use instead of sampleAvg???\n  int   volumeRaw   = *(int16_t*) um_data->u_data[1];\n\n  SEGMENT.addPixelColorXY( i, j, ColorFromPalette(SEGPALETTE, (strip.now / 11 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 11, 200, 255);\n  SEGMENT.addPixelColorXY( j, i, ColorFromPalette(SEGPALETTE, (strip.now / 13 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 13, 200, 255);\n  SEGMENT.addPixelColorXY(ni,nj, ColorFromPalette(SEGPALETTE, (strip.now / 17 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 17, 200, 255);\n  SEGMENT.addPixelColorXY(nj,ni, ColorFromPalette(SEGPALETTE, (strip.now / 29 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 29, 200, 255);\n  SEGMENT.addPixelColorXY( i,nj, ColorFromPalette(SEGPALETTE, (strip.now / 37 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 37, 200, 255);\n  SEGMENT.addPixelColorXY(ni, j, ColorFromPalette(SEGPALETTE, (strip.now / 41 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 41, 200, 255);\n} // mode_2DSwirl()\nstatic const char _data_FX_MODE_2DSWIRL[] PROGMEM = \"Swirl@!,Sensitivity,Blur;,Bg Swirl;!;2v;ix=64,si=0\"; // Beatsin // TODO: color 1 unused?\n\n\n/////////////////////////\n//    * 2D Waverly     //\n/////////////////////////\n// By: Stepko, https://editor.soulmatelights.com/gallery/652-wave , modified by Andrew Tuline\nvoid mode_2DWaverly(void) {\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  um_data_t *um_data = getAudioData();\n  float   volumeSmth  = *(float*)   um_data->u_data[0];\n\n  SEGMENT.fadeToBlackBy(SEGMENT.speed);\n\n  long t = strip.now / 2;\n  for (int i = 0; i < cols; i++) {\n    unsigned thisVal = (1 + SEGMENT.intensity/64) * perlin8(i * 45 , t , t)/2;\n    // use audio if available\n    if (um_data) {\n      thisVal /= 32; // reduce intensity of perlin8()\n      thisVal *= volumeSmth;\n    }\n    int thisMax = map(thisVal, 0, 512, 0, rows);\n\n    for (int j = 0; j < thisMax; j++) {\n      SEGMENT.addPixelColorXY(i, j, ColorFromPalette(SEGPALETTE, map(j, 0, thisMax, 250, 0), 255, LINEARBLEND));\n      SEGMENT.addPixelColorXY((cols - 1) - i, (rows - 1) - j, ColorFromPalette(SEGPALETTE, map(j, 0, thisMax, 250, 0), 255, LINEARBLEND));\n    }\n  }\n  if (SEGMENT.check3) SEGMENT.blur(16, cols*rows < 100);\n} // mode_2DWaverly()\nstatic const char _data_FX_MODE_2DWAVERLY[] PROGMEM = \"Waverly@Amplification,Sensitivity,,,,,Blur;;!;2v;ix=64,si=0\"; // Beatsin\n\n#endif // WLED_DISABLE_2D\n\n// Gravity struct requited for GRAV* effects\ntypedef struct Gravity {\n  int    topLED;\n  int    gravityCounter;\n} gravity;\n\n///////////////////////\n//   * GRAVCENTER    //\n///////////////////////\n// Gravcenter effects By Andrew Tuline.\n// Gravcenter base function for Gravcenter (0), Gravcentric (1), Gravimeter (2), Gravfreq (3) (merged by @dedehai)\n\nvoid mode_gravcenter_base(unsigned mode) {\n  if (SEGLEN == 1) FX_FALLBACK_STATIC;\n\n  const unsigned dataSize = sizeof(gravity);\n  if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed\n  Gravity* gravcen = reinterpret_cast<Gravity*>(SEGENV.data);\n\n  um_data_t *um_data = getAudioData();\n  float   volumeSmth  = *(float*)  um_data->u_data[0];\n\n  if(mode == 1) SEGMENT.fade_out(253);  // //Gravcentric\n  else if(mode == 2) SEGMENT.fade_out(249);  // Gravimeter\n  else if(mode == 3) SEGMENT.fade_out(250);  // Gravfreq\n  else SEGMENT.fade_out(251);  // Gravcenter\n\n  float mySampleAvg;\n  int tempsamp;\n  float segmentSampleAvg = volumeSmth * (float)SEGMENT.intensity / 255.0f;\n\n  if(mode == 2) { //Gravimeter\n    segmentSampleAvg *= 0.25; // divide by 4, to compensate for later \"sensitivity\" upscaling\n    mySampleAvg = mapf(segmentSampleAvg*2.0, 0, 64, 0, (SEGLEN-1)); // map to pixels availeable in current segment\n    tempsamp = constrain(mySampleAvg,0,SEGLEN-1);       // Keep the sample from overflowing.\n  }\n  else { // Gravcenter or Gravcentric or Gravfreq\n    segmentSampleAvg *= 0.125f; // divide by 8, to compensate for later \"sensitivity\" upscaling\n    mySampleAvg = mapf(segmentSampleAvg*2.0, 0.0f, 32.0f, 0.0f, (float)SEGLEN/2.0f); // map to pixels availeable in current segment\n    tempsamp = constrain(mySampleAvg, 0, SEGLEN/2);     // Keep the sample from overflowing.\n  }\n\n  uint8_t gravity = 8 - SEGMENT.speed/32;\n  int offset = 1;\n  if(mode == 2) offset = 0;  // Gravimeter\n  if (tempsamp >= gravcen->topLED) gravcen->topLED = tempsamp-offset;\n  else if (gravcen->gravityCounter % gravity == 0) gravcen->topLED--;\n  \n  if(mode == 1) {  //Gravcentric\n    for (int i=0; i<tempsamp; i++) {\n      uint8_t index = segmentSampleAvg*24+strip.now/200;\n      SEGMENT.setPixelColor(i+SEGLEN/2, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0));\n      SEGMENT.setPixelColor(SEGLEN/2-1-i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0));\n    }\n    if (gravcen->topLED >= 0) {\n      SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, CRGB::Gray);\n      SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, CRGB::Gray);\n    }\n  }\n  else if(mode == 2) { //Gravimeter\n    for (int i=0; i<tempsamp; i++) {\n      uint8_t index = perlin8(i*segmentSampleAvg+strip.now, 5000+i*segmentSampleAvg);\n      SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0), uint8_t(segmentSampleAvg*8)));\n    }\n    if (gravcen->topLED > 0) {\n      SEGMENT.setPixelColor(gravcen->topLED, SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0));\n    }\n  }\n  else if(mode == 3) { //Gravfreq\n    for (int i=0; i<tempsamp; i++) {\n      float   FFT_MajorPeak = *(float*)um_data->u_data[4]; // used in mode 3: Gravfreq\n      if (FFT_MajorPeak < 1) FFT_MajorPeak = 1;\n      uint8_t index = (log10f(FFT_MajorPeak) - (MAX_FREQ_LOG10 - 1.78f)) * 255;\n      SEGMENT.setPixelColor(i+SEGLEN/2, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0));\n      SEGMENT.setPixelColor(SEGLEN/2-i-1, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0));\n    }\n    if (gravcen->topLED >= 0) {\n      SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, CRGB::Gray);\n      SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, CRGB::Gray);\n    }\n  }\n  else { //Gravcenter\n    for (int i=0; i<tempsamp; i++) {\n      uint8_t index = perlin8(i*segmentSampleAvg+strip.now, 5000+i*segmentSampleAvg);\n      SEGMENT.setPixelColor(i+SEGLEN/2, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0), uint8_t(segmentSampleAvg*8)));\n      SEGMENT.setPixelColor(SEGLEN/2-i-1, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0), uint8_t(segmentSampleAvg*8)));\n    }\n    if (gravcen->topLED >= 0) {\n      SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0));\n      SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0));\n    }\n  } \n  gravcen->gravityCounter = (gravcen->gravityCounter + 1) % gravity;\n}\n\nvoid mode_gravcenter(void) {                // Gravcenter. By Andrew Tuline.\n  mode_gravcenter_base(0);\n}\nstatic const char _data_FX_MODE_GRAVCENTER[] PROGMEM = \"Gravcenter@Rate of fall,Sensitivity;!,!;!;1v;ix=128,m12=2,si=0\"; // Circle, Beatsin\n\n///////////////////////\n//   * GRAVCENTRIC   //\n///////////////////////\nvoid mode_gravcentric(void) {               // Gravcentric. By Andrew Tuline.\n  mode_gravcenter_base(1);\n}\nstatic const char _data_FX_MODE_GRAVCENTRIC[] PROGMEM = \"Gravcentric@Rate of fall,Sensitivity;!,!;!;1v;ix=128,m12=3,si=0\"; // Corner, Beatsin\n\n\n///////////////////////\n//   * GRAVIMETER    //\n///////////////////////\nvoid mode_gravimeter(void) {                // Gravmeter. By Andrew Tuline.\n mode_gravcenter_base(2);\n}\nstatic const char _data_FX_MODE_GRAVIMETER[] PROGMEM = \"Gravimeter@Rate of fall,Sensitivity;!,!;!;1v;ix=128,m12=2,si=0\"; // Circle, Beatsin\n\n\n///////////////////////\n//    ** Gravfreq    //\n///////////////////////\nvoid mode_gravfreq(void) {                  // Gravfreq. By Andrew Tuline.\n  mode_gravcenter_base(3);\n}\nstatic const char _data_FX_MODE_GRAVFREQ[] PROGMEM = \"Gravfreq@Rate of fall,Sensitivity;!,!;!;1f;ix=128,m12=0,si=0\"; // Pixels, Beatsin\n\n\n//////////////////////\n//   * JUGGLES      //\n//////////////////////\nvoid mode_juggles(void) {                   // Juggles. By Andrew Tuline.\n  um_data_t *um_data = getAudioData();\n  float   volumeSmth   = *(float*)  um_data->u_data[0];\n\n  SEGMENT.fade_out(224); // 6.25%\n  uint8_t my_sampleAgc = fmax(fmin(volumeSmth, 255.0), 0);\n\n  for (size_t i=0; i<SEGMENT.intensity/32+1U; i++) {\n    // if SEGLEN equals 1, we will always set color to the first and only pixel, but the effect is still good looking\n    SEGMENT.setPixelColor(beatsin16_t(SEGMENT.speed/4+i*2,0,SEGLEN-1), color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(strip.now/4+i*2, false, PALETTE_SOLID_WRAP, 0), my_sampleAgc));\n  }\n} // mode_juggles()\nstatic const char _data_FX_MODE_JUGGLES[] PROGMEM = \"Juggles@!,# of balls;!,!;!;01v;m12=0,si=0\"; // Pixels, Beatsin\n\n\n//////////////////////\n//   * MATRIPIX     //\n//////////////////////\nvoid mode_matripix(void) {                  // Matripix. By Andrew Tuline.\n  // effect can work on single pixels, we just lose the shifting effect\n  unsigned dataSize = sizeof(uint32_t) * SEGLEN;\n  if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed\n  uint32_t* pixels = reinterpret_cast<uint32_t*>(SEGENV.data);\n\n  um_data_t *um_data = getAudioData();\n  int volumeRaw    = *(int16_t*)um_data->u_data[1];\n\n  if (SEGENV.call == 0) {\n    for (unsigned i = 0; i < SEGLEN; i++) pixels[i] = BLACK;   // may not be needed as resetIfRequired() clears buffer\n  }\n\n  uint8_t secondHand = micros()/(256-SEGMENT.speed)/500 % 16;\n  if(SEGENV.aux0 != secondHand) {\n    SEGENV.aux0 = secondHand;\n\n    int pixBri = volumeRaw * SEGMENT.intensity / 64;\n    unsigned k = SEGLEN-1;\n    // loop will not execute if SEGLEN equals 1\n    for (unsigned i = 0; i < k; i++) {\n      pixels[i] = pixels[i+1]; // shift left\n      SEGMENT.setPixelColor(i, pixels[i]);\n    }\n    pixels[k] = color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0), pixBri);\n    SEGMENT.setPixelColor(k, pixels[k]);\n  }\n} // mode_matripix()\nstatic const char _data_FX_MODE_MATRIPIX[] PROGMEM = \"Matripix@!,Brightness;!,!;!;1v;ix=64,m12=2,si=1\"; //,rev=1,mi=1,rY=1,mY=1 Circle, WeWillRockYou, reverseX\n\n\n//////////////////////\n//   * MIDNOISE     //\n//////////////////////\nvoid mode_midnoise(void) {                  // Midnoise. By Andrew Tuline.\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n// Changing xdist to SEGENV.aux0 and ydist to SEGENV.aux1.\n\n  um_data_t *um_data = getAudioData();\n  float   volumeSmth   = *(float*)  um_data->u_data[0];\n\n  SEGMENT.fade_out(SEGMENT.speed);\n  SEGMENT.fade_out(SEGMENT.speed);\n\n  float tmpSound2 = volumeSmth * (float)SEGMENT.intensity / 256.0;  // Too sensitive.\n  tmpSound2 *= (float)SEGMENT.intensity / 128.0;              // Reduce sensitivity/length.\n\n  unsigned maxLen = mapf(tmpSound2, 0, 127, 0, SEGLEN/2);\n  if (maxLen >SEGLEN/2) maxLen = SEGLEN/2;\n\n  for (unsigned i=(SEGLEN/2-maxLen); i<(SEGLEN/2+maxLen); i++) {\n    uint8_t index = perlin8(i*volumeSmth+SEGENV.aux0, SEGENV.aux1+i*volumeSmth);  // Get a value from the noise function. I'm using both x and y axis.\n    SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0));\n  }\n\n  SEGENV.aux0=SEGENV.aux0+beatsin8_t(5,0,10);\n  SEGENV.aux1=SEGENV.aux1+beatsin8_t(4,0,10);\n} // mode_midnoise()\nstatic const char _data_FX_MODE_MIDNOISE[] PROGMEM = \"Midnoise@Fade rate,Max. length;!,!;!;1v;ix=128,m12=1,si=0\"; // Bar, Beatsin\n\n\n//////////////////////\n//   * NOISEFIRE    //\n//////////////////////\n// I am the god of hellfire. . . Volume (only) reactive fire routine. Oh, look how short this is.\nvoid mode_noisefire(void) {                 // Noisefire. By Andrew Tuline.\n  CRGBPalette16 myPal = CRGBPalette16(CHSV(0,255,2),    CHSV(0,255,4),    CHSV(0,255,8), CHSV(0, 255, 8),  // Fire palette definition. Lower value = darker.\n                                      CHSV(0, 255, 16), CRGB::Red,        CRGB::Red,     CRGB::Red,\n                                      CRGB::DarkOrange, CRGB::DarkOrange, CRGB::Orange,  CRGB::Orange,\n                                      CRGB::Yellow,     CRGB::Orange,     CRGB::Yellow,  CRGB::Yellow);\n\n  um_data_t *um_data = getAudioData();\n  float   volumeSmth   = *(float*)  um_data->u_data[0];\n\n  if (SEGENV.call == 0) SEGMENT.fill(BLACK);\n\n  for (unsigned i = 0; i < SEGLEN; i++) {\n    unsigned index = perlin8(i*SEGMENT.speed/64,strip.now*SEGMENT.speed/64*SEGLEN/255);  // X location is constant, but we move along the Y at the rate of millis(). By Andrew Tuline.\n    index = (255 - i*256/SEGLEN) * index/(256-SEGMENT.intensity);                       // Now we need to scale index so that it gets blacker as we get close to one of the ends.\n                                                                                        // This is a simple y=mx+b equation that's been scaled. index/128 is another scaling.\n\n    SEGMENT.setPixelColor(i, ColorFromPalette(myPal, index, volumeSmth*2, LINEARBLEND)); // Use my own palette.\n  }\n} // mode_noisefire()\nstatic const char _data_FX_MODE_NOISEFIRE[] PROGMEM = \"Noisefire@!,!;;;01v;m12=2,si=0\"; // Circle, Beatsin\n\n\n///////////////////////\n//   * Noisemeter    //\n///////////////////////\nvoid mode_noisemeter(void) {                // Noisemeter. By Andrew Tuline.\n\n  um_data_t *um_data = getAudioData();\n  float   volumeSmth   = *(float*)  um_data->u_data[0];\n  int volumeRaw    = *(int16_t*)um_data->u_data[1];\n\n  //uint8_t fadeRate = map(SEGMENT.speed,0,255,224,255);\n  uint8_t fadeRate = map(SEGMENT.speed,0,255,200,254);\n  SEGMENT.fade_out(fadeRate);\n\n  float tmpSound2 = volumeRaw * 2.0 * (float)SEGMENT.intensity / 255.0;\n  unsigned maxLen = mapf(tmpSound2, 0, 255, 0, SEGLEN); // map to pixels availeable in current segment              // Still a bit too sensitive.\n  if (maxLen < 0) maxLen = 0;\n  if (maxLen > SEGLEN) maxLen = SEGLEN;\n\n  for (unsigned i=0; i<maxLen; i++) {                                    // The louder the sound, the wider the soundbar. By Andrew Tuline.\n    uint8_t index = perlin8(i*volumeSmth+SEGENV.aux0, SEGENV.aux1+i*volumeSmth);  // Get a value from the noise function. I'm using both x and y axis.\n    SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0));\n  }\n\n  SEGENV.aux0+=beatsin8_t(5,0,10);\n  SEGENV.aux1+=beatsin8_t(4,0,10);\n} // mode_noisemeter()\nstatic const char _data_FX_MODE_NOISEMETER[] PROGMEM = \"Noisemeter@Fade rate,Width;!,!;!;1v;ix=128,m12=2,si=0\"; // Circle, Beatsin\n\n\n//////////////////////\n//   * PIXELWAVE    //\n//////////////////////\nvoid mode_pixelwave(void) {                 // Pixelwave. By Andrew Tuline.\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  // even with 1D effect we have to take logic for 2D segments for allocation as fill_solid() fills whole segment\n\n  if (SEGENV.call == 0) {\n    SEGMENT.fill(BLACK);\n  }\n\n  um_data_t *um_data = getAudioData();\n  int volumeRaw    = *(int16_t*)um_data->u_data[1];\n\n  uint8_t secondHand = micros()/(256-SEGMENT.speed)/500+1 % 16;\n  if (SEGENV.aux0 != secondHand) {\n    SEGENV.aux0 = secondHand;\n\n    uint8_t pixBri = volumeRaw * SEGMENT.intensity / 64;\n\n    SEGMENT.setPixelColor(SEGLEN/2, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0), pixBri));\n    for (unsigned i = SEGLEN - 1; i > SEGLEN/2; i--) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i-1)); //move to the left\n    for (unsigned i = 0; i < SEGLEN/2; i++)          SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // move to the right\n  }\n} // mode_pixelwave()\nstatic const char _data_FX_MODE_PIXELWAVE[] PROGMEM = \"Pixelwave@!,Sensitivity;!,!;!;1v;ix=64,m12=2,si=0\"; // Circle, Beatsin\n\n\n//////////////////////\n//   * PLASMOID     //\n//////////////////////\ntypedef struct Plasphase {\n  int16_t    thisphase;\n  int16_t    thatphase;\n} plasphase;\n\nvoid mode_plasmoid(void) {                  // Plasmoid. By Andrew Tuline.\n  // even with 1D effect we have to take logic for 2D segments for allocation as fill_solid() fills whole segment\n  if (!SEGENV.allocateData(sizeof(plasphase))) FX_FALLBACK_STATIC; //allocation failed\n  Plasphase* plasmoip = reinterpret_cast<Plasphase*>(SEGENV.data);\n\n  um_data_t *um_data = getAudioData();\n  float   volumeSmth   = *(float*)  um_data->u_data[0];\n\n  SEGMENT.fadeToBlackBy(32);\n\n  plasmoip->thisphase += beatsin8_t(6,-4,4);                          // You can change direction and speed individually.\n  plasmoip->thatphase += beatsin8_t(7,-4,4);                          // Two phase values to make a complex pattern. By Andrew Tuline.\n\n  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.\n    // updated, similar to \"plasma\" effect - softhack007\n    uint8_t thisbright = cubicwave8(((i*(1 + (3*SEGMENT.speed/32)))+plasmoip->thisphase) & 0xFF)/2;\n    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.\n\n    uint8_t colorIndex=thisbright;\n    if (volumeSmth * SEGMENT.intensity / 64 < thisbright) {thisbright = 0;}\n\n    SEGMENT.addPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(colorIndex, false, PALETTE_SOLID_WRAP, 0), thisbright));\n  }\n} // mode_plasmoid()\nstatic const char _data_FX_MODE_PLASMOID[] PROGMEM = \"Plasmoid@Phase,# of pixels;!,!;!;01v;sx=128,ix=128,m12=0,si=0\"; // Pixels, Beatsin\n\n\n//////////////////////\n//   * PUDDLES      //\n//////////////////////\n// Puddles/Puddlepeak By Andrew Tuline. Merged by @dedehai\nvoid mode_puddles_base(bool peakdetect) {\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  unsigned size = 0;\n  uint8_t fadeVal = map(SEGMENT.speed, 0, 255, 224, 254);\n  unsigned pos = hw_random16(SEGLEN);                          // Set a random starting position.\n  SEGMENT.fade_out(fadeVal);\n\n  um_data_t *um_data = getAudioData();\n  int volumeRaw    = *(int16_t*)um_data->u_data[1];\n  uint8_t samplePeak = *(uint8_t*)um_data->u_data[3];\n  uint8_t *maxVol    =  (uint8_t*)um_data->u_data[6];\n  uint8_t *binNum    =  (uint8_t*)um_data->u_data[7];\n  float   volumeSmth   = *(float*)  um_data->u_data[0];\n\n  if(peakdetect) {                                          // puddles peak\n    *binNum = SEGMENT.custom1;                              // Select a bin.\n    *maxVol = SEGMENT.custom2 / 2;                          // Our volume comparator.\n    if (samplePeak == 1) {\n      size = volumeSmth * SEGMENT.intensity /256 /4 + 1;    // Determine size of the flash based on the volume.\n      if (pos+size>= SEGLEN) size = SEGLEN - pos;\n    }\n  }\n  else {                                                    // puddles  \n    if (volumeRaw > 1) {\n      size = volumeRaw * SEGMENT.intensity /256 /8 + 1;     // Determine size of the flash based on the volume.\n      if (pos+size >= SEGLEN) size = SEGLEN - pos;\n    } \n  }\n  \n  for (unsigned i=0; i<size; i++) {                          // Flash the LED's.\n    SEGMENT.setPixelColor(pos+i, SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0));\n  }\n}\n\nvoid mode_puddlepeak(void) {                // Puddlepeak. By Andrew Tuline.\n  mode_puddles_base(true);\n} \nstatic const char _data_FX_MODE_PUDDLEPEAK[] PROGMEM = \"Puddlepeak@Fade rate,Puddle size,Select bin,Volume (min);!,!;!;1v;c2=0,m12=0,si=0\"; // Pixels, Beatsin\n\nvoid mode_puddles(void) {                   // Puddles. By Andrew Tuline.\n  mode_puddles_base(false);\n} \nstatic const char _data_FX_MODE_PUDDLES[] PROGMEM = \"Puddles@Fade rate,Puddle size;!,!;!;1v;m12=0,si=0\"; // Pixels, Beatsin\n\n\n//////////////////////\n//     * PIXELS     //\n//////////////////////\nvoid mode_pixels(void) {                    // Pixels. By Andrew Tuline.\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n\n  if (!SEGENV.allocateData(32*sizeof(uint8_t))) FX_FALLBACK_STATIC; //allocation failed\n  uint8_t *myVals = reinterpret_cast<uint8_t*>(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.\n\n  um_data_t *um_data;\n  if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) {\n    um_data = simulateSound(SEGMENT.soundSim);\n  }\n  float   volumeSmth   = *(float*)  um_data->u_data[0];\n\n  myVals[strip.now%32] = volumeSmth;    // filling values semi randomly\n\n  SEGMENT.fade_out(64+(SEGMENT.speed>>1));\n\n  for (int i=0; i <SEGMENT.intensity/8; i++) {\n    unsigned segLoc = hw_random16(SEGLEN);                    // 16 bit for larger strands of LED's.\n    SEGMENT.setPixelColor(segLoc, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(myVals[i%32]+i*4, false, PALETTE_SOLID_WRAP, 0), uint8_t(volumeSmth)));\n  }\n} // mode_pixels()\nstatic const char _data_FX_MODE_PIXELS[] PROGMEM = \"Pixels@Fade rate,# of pixels;!,!;!;1v;m12=0,si=0\"; // Pixels, Beatsin\n\n//////////////////////\n//    ** Blurz      //\n//////////////////////\nvoid mode_blurz(void) {                    // Blurz. By Andrew Tuline.\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  // even with 1D effect we have to take logic for 2D segments for allocation as fill_solid() fills whole segment\n\n  um_data_t *um_data = getAudioData();\n  uint8_t *fftResult = (uint8_t*)um_data->u_data[2];\n\n  if (SEGENV.call == 0) {\n    SEGMENT.fill(BLACK);\n    SEGENV.aux0 = 0;\n  }\n\n  int fadeoutDelay = (256 - SEGMENT.speed) / 32;\n  if ((fadeoutDelay <= 1 ) || ((SEGENV.call % fadeoutDelay) == 0)) SEGMENT.fade_out(SEGMENT.speed);\n\n  SEGENV.step += FRAMETIME;\n  if (SEGENV.step > SPEED_FORMULA_L) {\n    unsigned segLoc = hw_random16(SEGLEN);\n    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])));\n    ++(SEGENV.aux0) %= 16; // make sure it doesn't cross 16\n\n    SEGENV.step = 1;\n    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)\n  }\n} // mode_blurz()\nstatic const char _data_FX_MODE_BLURZ[] PROGMEM = \"Blurz@Fade rate,Blur;!,Color mix;!;1f;m12=0,si=0\"; // Pixels, Beatsin\n\n\n/////////////////////////\n//   ** DJLight        //\n/////////////////////////\nvoid mode_DJLight(void) {                   // Written by ??? Adapted by Will Tatam.\n  // No need to prevent from executing on single led strips, only mid will be set (mid = 0)\n  const int mid = SEGLEN / 2;\n\n  um_data_t *um_data = getAudioData();\n  uint8_t *fftResult = (uint8_t*)um_data->u_data[2];\n\n  if (SEGENV.call == 0) {\n    SEGMENT.fill(BLACK);\n  }\n\n  uint8_t secondHand = micros()/(256-SEGMENT.speed)/500+1 % 64;\n  if (SEGENV.aux0 != secondHand) {                        // Triggered millis timing.\n    SEGENV.aux0 = secondHand;\n\n    CRGB color = CRGB(fftResult[15]/2, fftResult[5]/2, fftResult[0]/2); // 16-> 15 as 16 is out of bounds\n    SEGMENT.setPixelColor(mid, color.fadeToBlackBy(map(fftResult[4], 0, 255, 255, 4)));     // TODO - Update\n\n    // if SEGLEN equals 1 these loops won't execute\n    for (int i = SEGLEN - 1; i > mid; i--)   SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i-1)); // move to the left\n    for (int i = 0; i < mid; i++)            SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // move to the right\n  }\n} // mode_DJLight()\nstatic const char _data_FX_MODE_DJLIGHT[] PROGMEM = \"DJ Light@Speed;;;01f;m12=2,si=0\"; // Circle, Beatsin\n\n\n////////////////////\n//   ** Freqmap   //\n////////////////////\nvoid mode_freqmap(void) {                   // Map FFT_MajorPeak to SEGLEN. Would be better if a higher framerate.\n  if (SEGLEN <= 1) FX_FALLBACK_STATIC;\n  // Start frequency = 60 Hz and log10(60) = 1.78\n  // End frequency = MAX_FREQUENCY in Hz and lo10(MAX_FREQUENCY) = MAX_FREQ_LOG10\n\n  um_data_t *um_data = getAudioData();\n  float FFT_MajorPeak = *(float*)um_data->u_data[4];\n  float my_magnitude  = *(float*)um_data->u_data[5] / 4.0f;\n  if (FFT_MajorPeak < 1) FFT_MajorPeak = 1;                                         // log10(0) is \"forbidden\" (throws exception)\n\n  if (SEGENV.call == 0) SEGMENT.fill(BLACK);\n  int fadeoutDelay = (256 - SEGMENT.speed) / 32;\n  if ((fadeoutDelay <= 1 ) || ((SEGENV.call % fadeoutDelay) == 0)) SEGMENT.fade_out(SEGMENT.speed);\n\n  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.\n  if (locn < 1) locn = 0; // avoid underflow\n\n  if (locn >= (int)SEGLEN) locn = SEGLEN-1;\n  unsigned pixCol = (log10f(FFT_MajorPeak) - 1.78f) * 255.0f/(MAX_FREQ_LOG10 - 1.78f);   // Scale log10 of frequency values to the 255 colour index.\n  if (FFT_MajorPeak < 61.0f) pixCol = 0;                                                 // handle underflow\n\n  uint8_t bright = (uint8_t)my_magnitude;\n\n  SEGMENT.setPixelColor(locn, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(SEGMENT.intensity+pixCol, false, PALETTE_SOLID_WRAP, 0), bright));\n} // mode_freqmap()\nstatic const char _data_FX_MODE_FREQMAP[] PROGMEM = \"Freqmap@Fade rate,Starting color;!,!;!;1f;m12=0,si=0\"; // Pixels, Beatsin\n\n\n///////////////////////\n//   ** Freqmatrix   //\n///////////////////////\nvoid mode_freqmatrix(void) {                // Freqmatrix. By Andreas Pleschung.\n  // No need to prevent from executing on single led strips, we simply change pixel 0 each time and avoid the shift\n  um_data_t *um_data = getAudioData();\n  float FFT_MajorPeak = *(float*)um_data->u_data[4];\n  float volumeSmth    = *(float*)um_data->u_data[0];\n\n  if (SEGENV.call == 0) {\n    SEGMENT.fill(BLACK);\n  }\n\n  uint8_t secondHand = micros()/(256-SEGMENT.speed)/500 % 16;\n  if(SEGENV.aux0 != secondHand) {\n    SEGENV.aux0 = secondHand;\n\n    uint8_t sensitivity = map(SEGMENT.custom3, 0, 31, 1, 10); // reduced resolution slider\n    int pixVal = (volumeSmth * SEGMENT.intensity * sensitivity) / 256.0f;\n    if (pixVal > 255) pixVal = 255;\n\n    float intensity = map(pixVal, 0, 255, 0, 100) / 100.0f;  // make a brightness from the last avg\n\n    CRGB color = CRGB::Black;\n\n    if (FFT_MajorPeak > MAX_FREQUENCY) FFT_MajorPeak = 1;\n    // MajorPeak holds the freq. value which is most abundant in the last sample.\n    // With our sampling rate of 10240Hz we have a usable freq range from roughly 80Hz to 10240/2 Hz\n    // we will treat everything with less than 65Hz as 0\n\n    if (FFT_MajorPeak < 80) {\n      color = CRGB::Black;\n    } else {\n      int upperLimit = 80 + 42 * SEGMENT.custom2;\n      int lowerLimit = 80 + 3 * SEGMENT.custom1;\n      uint8_t i =  lowerLimit!=upperLimit ? map(FFT_MajorPeak, lowerLimit, upperLimit, 0, 255) : FFT_MajorPeak;  // may under/overflow - so we enforce uint8_t\n      unsigned b = 255 * intensity;\n      if (b > 255) b = 255;\n      color = CHSV(i, 240, (uint8_t)b); // implicit conversion to RGB supplied by FastLED\n    }\n\n    // shift the pixels one pixel up\n    SEGMENT.setPixelColor(0, color);\n    // if SEGLEN equals 1 this loop won't execute\n    for (int i = SEGLEN - 1; i > 0; i--) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i-1)); //move to the left\n  }\n} // mode_freqmatrix()\nstatic const char _data_FX_MODE_FREQMATRIX[] PROGMEM = \"Freqmatrix@Speed,Sound effect,Low bin,High bin,Sensitivity;;;01f;m12=3,si=0\"; // Corner, Beatsin\n\n\n//////////////////////\n//   ** Freqpixels  //\n//////////////////////\n// Start frequency = 60 Hz and log10(60) = 1.78\n// End frequency = 5120 Hz and lo10(5120) = 3.71\n//  SEGMENT.speed select faderate\n//  SEGMENT.intensity select colour index\nvoid mode_freqpixels(void) {                // Freqpixel. By Andrew Tuline.\n  um_data_t *um_data = getAudioData();\n  float FFT_MajorPeak = *(float*)um_data->u_data[4];\n  float my_magnitude  = *(float*)um_data->u_data[5] / 16.0f;\n  if (FFT_MajorPeak < 1) FFT_MajorPeak = 1.0f; // log10(0) is \"forbidden\" (throws exception)\n\n  // this code translates to speed * (2 - speed/255) which is a) speed*2 or b) speed (when speed is 255)\n  // and since fade_out() can only take 0-255 it will behave incorrectly when speed > 127\n  //uint16_t fadeRate = 2*SEGMENT.speed - SEGMENT.speed*SEGMENT.speed/255;    // Get to 255 as quick as you can.\n  unsigned fadeRate = SEGMENT.speed*SEGMENT.speed; // Get to 255 as quick as you can.\n  fadeRate = map(fadeRate, 0, 65535, 1, 255);\n\n  int fadeoutDelay = (256 - SEGMENT.speed) / 64;\n  if ((fadeoutDelay <= 1 ) || ((SEGENV.call % fadeoutDelay) == 0)) SEGMENT.fade_out(fadeRate);\n\n  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.\n  if (FFT_MajorPeak < 61.0f) pixCol = 0;                                               // handle underflow\n  for (int i=0; i < SEGMENT.intensity/32+1; i++) {\n    unsigned locn = hw_random16(0,SEGLEN);\n    SEGMENT.setPixelColor(locn, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(SEGMENT.intensity+pixCol, false, PALETTE_SOLID_WRAP, 0), (uint8_t)my_magnitude));\n  }\n} // mode_freqpixels()\nstatic const char _data_FX_MODE_FREQPIXELS[] PROGMEM = \"Freqpixels@Fade rate,Starting color and # of pixels;!,!,;!;1f;m12=0,si=0\"; // Pixels, Beatsin\n\n\n//////////////////////\n//   ** Freqwave    //\n//////////////////////\n// Assign a color to the central (starting pixels) based on the predominant frequencies and the volume. The color is being determined by mapping the MajorPeak from the FFT\n// and then mapping this to the HSV color circle. Currently we are sampling at 10240 Hz, so the highest frequency we can look at is 5120Hz.\n//\n// SEGMENT.custom1: the lower cut off point for the FFT. (many, most time the lowest values have very little information since they are FFT conversion artifacts. Suggested value is close to but above 0\n// SEGMENT.custom2: The high cut off point. This depends on your sound profile. Most music looks good when this slider is between 50% and 100%.\n// SEGMENT.custom3: \"preamp\" for the audio signal for audio10.\n//\n// I suggest that for this effect you turn the brightness to 95%-100% but again it depends on your soundprofile you find yourself in.\n// Instead of using colorpalettes, This effect works on the HSV color circle with red being the lowest frequency\n//\n// 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.\n// Depending on the music stream you have you might find it useful to change the frequency mapping.\nvoid mode_freqwave(void) {                  // Freqwave. By Andreas Pleschung.\n  // As before, this effect can also work on single pixels, we just lose the shifting effect\n  um_data_t *um_data = getAudioData();\n  float FFT_MajorPeak = *(float*)um_data->u_data[4];\n  float volumeSmth    = *(float*)um_data->u_data[0];\n\n  if (SEGENV.call == 0) {\n    SEGMENT.fill(BLACK);\n  }\n\n  uint8_t secondHand = micros()/(256-SEGMENT.speed)/500 % 16;\n  if(SEGENV.aux0 != secondHand) {\n    SEGENV.aux0 = secondHand;\n\n    float sensitivity = mapf(SEGMENT.custom3, 1, 31, 1, 10); // reduced resolution slider\n    float pixVal = min(255.0f, volumeSmth * (float)SEGMENT.intensity / 256.0f * sensitivity);\n    float intensity = mapf(pixVal, 0.0f, 255.0f, 0.0f, 100.0f) / 100.0f;  // make a brightness from the last avg\n\n    CRGB color = 0;\n\n    if (FFT_MajorPeak > MAX_FREQUENCY) FFT_MajorPeak = 1.0f;\n    // MajorPeak holds the freq. value which is most abundant in the last sample.\n    // With our sampling rate of 10240Hz we have a usable freq range from roughly 80Hz to 10240/2 Hz\n    // we will treat everything with less than 65Hz as 0\n\n    if (FFT_MajorPeak < 80) {\n      color = CRGB::Black;\n    } else {\n      int upperLimit = 80 + 42 * SEGMENT.custom2;\n      int lowerLimit = 80 + 3 * SEGMENT.custom1;\n      uint8_t i =  lowerLimit!=upperLimit ? map(FFT_MajorPeak, lowerLimit, upperLimit, 0, 255) : FFT_MajorPeak; // may under/overflow - so we enforce uint8_t\n      unsigned b = min(255.0f, 255.0f * intensity);\n      color = CHSV(i, 240, (uint8_t)b); // implicit conversion to RGB supplied by FastLED\n    }\n\n    SEGMENT.setPixelColor(SEGLEN/2, color);\n\n    // shift the pixels one pixel outwards\n    // if SEGLEN equals 1 these loops won't execute\n    for (unsigned i = SEGLEN - 1; i > SEGLEN/2; i--) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i-1)); //move to the left\n    for (unsigned i = 0; i < SEGLEN/2; i++)          SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // move to the right\n  }\n} // mode_freqwave()\nstatic const char _data_FX_MODE_FREQWAVE[] PROGMEM = \"Freqwave@Speed,Sound effect,Low bin,High bin,Pre-amp;;;01f;m12=2,si=0\"; // Circle, Beatsin\n\n\n//////////////////////\n//   ** Noisemove   //\n//////////////////////\nvoid mode_noisemove(void) {                 // Noisemove.    By: Andrew Tuline\n  um_data_t *um_data = getAudioData();\n  uint8_t *fftResult = (uint8_t*)um_data->u_data[2];\n\n  int fadeoutDelay = (256 - SEGMENT.speed) / 96;\n  if ((fadeoutDelay <= 1 ) || ((SEGENV.call % fadeoutDelay) == 0)) SEGMENT.fadeToBlackBy(4+ SEGMENT.speed/4);\n\n  uint8_t numBins = map(SEGMENT.intensity,0,255,0,16);    // Map slider to fftResult bins.\n  for (int i=0; i<numBins; i++) {                         // How many active bins are we using.\n    unsigned locn = perlin16(strip.now*SEGMENT.speed+i*50000, strip.now*SEGMENT.speed);   // Get a new pixel location from moving noise.\n    // if SEGLEN equals 1 locn will be always 0, hence we set the first pixel only\n    locn = map(locn, 7500, 58000, 0, SEGLEN-1);           // Map that to the length of the strand, and ensure we don't go over.\n    SEGMENT.setPixelColor(locn, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i*64, false, PALETTE_SOLID_WRAP, 0), uint8_t(fftResult[i % 16]*4)));\n  }\n} // mode_noisemove()\nstatic const char _data_FX_MODE_NOISEMOVE[] PROGMEM = \"Noisemove@Move speed,Fade rate;!,!;!;01f;m12=0,si=0\"; // Pixels, Beatsin\n\n\n//////////////////////\n//   ** Rocktaves   //\n//////////////////////\nvoid mode_rocktaves(void) {                 // Rocktaves. Same note from each octave is same colour.    By: Andrew Tuline\n  um_data_t *um_data = getAudioData();\n  float   FFT_MajorPeak = *(float*)  um_data->u_data[4];\n  float   my_magnitude  = *(float*)   um_data->u_data[5] / 16.0f;\n\n  SEGMENT.fadeToBlackBy(16);                              // Just in case something doesn't get faded.\n\n  float frTemp = FFT_MajorPeak;\n  uint8_t octCount = 0;                                   // Octave counter.\n  uint8_t volTemp = 0;\n\n  volTemp = 32.0f + my_magnitude * 1.5f;                  // brightness = volume (overflows are handled in next lines)\n  if (my_magnitude < 48) volTemp = 0;                     // We need to squelch out the background noise.\n  if (my_magnitude > 144) volTemp = 255;                  // everything above this is full brightness\n\n  while ( frTemp > 249 ) {\n    octCount++;                                           // This should go up to 5.\n    frTemp = frTemp/2;\n  }\n\n  frTemp -= 132.0f;                                       // This should give us a base musical note of C3\n  frTemp  = fabsf(frTemp * 2.1f);                         // Fudge factors to compress octave range starting at 0 and going to 255;\n\n  unsigned i = map(beatsin8_t(8+octCount*4, 0, 255, 0, octCount*8), 0, 255, 0, SEGLEN-1);\n  i = constrain(i, 0U, SEGLEN-1U);\n  SEGMENT.addPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette((uint8_t)frTemp, false, PALETTE_SOLID_WRAP, 0), volTemp));\n} // mode_rocktaves()\nstatic const char _data_FX_MODE_ROCKTAVES[] PROGMEM = \"Rocktaves@;!,!;!;01f;m12=1,si=0\"; // Bar, Beatsin\n\n\n///////////////////////\n//   ** Waterfall    //\n///////////////////////\n// Combines peak detection with FFT_MajorPeak and FFT_Magnitude.\nvoid mode_waterfall(void) {                   // Waterfall. By: Andrew Tuline\n  // effect can work on single pixels, we just lose the shifting effect\n  unsigned dataSize = sizeof(uint32_t) * SEGLEN;\n  if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed\n  uint32_t* pixels = reinterpret_cast<uint32_t*>(SEGENV.data);\n\n  um_data_t *um_data    = getAudioData();\n  uint8_t samplePeak    = *(uint8_t*)um_data->u_data[3];\n  float   FFT_MajorPeak = *(float*)  um_data->u_data[4];\n  uint8_t *maxVol       =  (uint8_t*)um_data->u_data[6];\n  uint8_t *binNum       =  (uint8_t*)um_data->u_data[7];\n  float   my_magnitude  = *(float*)   um_data->u_data[5] / 8.0f;\n\n  if (FFT_MajorPeak < 1) FFT_MajorPeak = 1;                                         // log10(0) is \"forbidden\" (throws exception)\n\n  if (SEGENV.call == 0) {\n    for (unsigned i = 0; i < SEGLEN; i++) pixels[i] = BLACK;   // may not be needed as resetIfRequired() clears buffer\n    SEGENV.aux0 = 255;\n    SEGMENT.custom1 = *binNum;\n    SEGMENT.custom2 = *maxVol * 2;\n  }\n\n  *binNum = SEGMENT.custom1;                              // Select a bin.\n  *maxVol = SEGMENT.custom2 / 2;                          // Our volume comparator.\n\n  uint8_t secondHand = micros() / (256-SEGMENT.speed)/500 + 1 % 16;\n  if (SEGENV.aux0 != secondHand) {                        // Triggered millis timing.\n    SEGENV.aux0 = secondHand;\n\n    //uint8_t pixCol = (log10f((float)FFT_MajorPeak) - 2.26f) * 177;  // 10Khz sampling - log10 frequency range is from 2.26 (182hz) to 3.7 (5012hz). Let's scale accordingly.\n    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.\n    if (FFT_MajorPeak < 182.0f) pixCol = 0;                           // handle underflow\n\n    unsigned k = SEGLEN-1;\n    if (samplePeak) {\n      pixels[k] = (uint32_t)CRGB(CHSV(92,92,92));\n    } else {\n      pixels[k] = color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(pixCol+SEGMENT.intensity, false, PALETTE_SOLID_WRAP, 0), (uint8_t)my_magnitude);\n    }\n    SEGMENT.setPixelColor(k, pixels[k]);\n    // loop will not execute if SEGLEN equals 1\n    for (unsigned i = 0; i < k; i++) {\n      pixels[i] = pixels[i+1]; // shift left\n      SEGMENT.setPixelColor(i, pixels[i]);\n    }\n  }\n} // mode_waterfall()\nstatic const char _data_FX_MODE_WATERFALL[] PROGMEM = \"Waterfall@!,Adjust color,Select bin,Volume (min);!,!;!;01f;c2=0,m12=2,si=0\"; // Circles, Beatsin\n\n\n#ifndef WLED_DISABLE_2D\n/////////////////////////\n//     ** 2D GEQ       //\n/////////////////////////\nvoid mode_2DGEQ(void) { // By Will Tatam. Code reduction by Ewoud Wijma.\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int NUM_BANDS = map(SEGMENT.custom1, 0, 255, 1, 16);\n  const int CENTER_BIN = map(SEGMENT.custom3, 0, 31, 0, 15);\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  if (!SEGENV.allocateData(cols*sizeof(uint16_t))) FX_FALLBACK_STATIC; //allocation failed\n  uint16_t *previousBarHeight = reinterpret_cast<uint16_t*>(SEGENV.data); //array of previous bar heights per frequency band\n\n  um_data_t *um_data = getAudioData();\n  uint8_t *fftResult = (uint8_t*)um_data->u_data[2];\n\n  if (SEGENV.call == 0) for (int i=0; i<cols; i++) previousBarHeight[i] = 0;\n\n  bool rippleTime = false;\n  if (strip.now - SEGENV.step >= (256U - SEGMENT.intensity)) {\n    SEGENV.step = strip.now;\n    rippleTime = true;\n  }\n\n  int fadeoutDelay = (256 - SEGMENT.speed) / 64;\n  if ((fadeoutDelay <= 1 ) || ((SEGENV.call % fadeoutDelay) == 0)) SEGMENT.fadeToBlackBy(SEGMENT.speed);\n\n  for (int x=0; x < cols; x++) {\n    int band = map(x, 0, cols, 0, NUM_BANDS);\n    if (NUM_BANDS < 16) {\n        int startBin = constrain(CENTER_BIN - NUM_BANDS/2, 0, 15 - NUM_BANDS + 1);\n        if(NUM_BANDS <= 1)\n          band = CENTER_BIN; // map() does not work for single band\n        else\n          band = map(band, 0, NUM_BANDS - 1, startBin, startBin + NUM_BANDS - 1);\n    }\n    band = constrain(band, 0, 15);\n    unsigned colorIndex = band * 17;\n    int barHeight  = map(fftResult[band], 0, 255, 0, rows); // do not subtract -1 from rows here\n    if (barHeight > previousBarHeight[x]) previousBarHeight[x] = barHeight; //drive the peak up\n\n    uint32_t ledColor = BLACK;\n    for (int y=0; y < barHeight; y++) {\n      if (SEGMENT.check1) //color_vertical / color bars toggle\n        colorIndex = map(y, 0, rows-1, 0, 255);\n\n      ledColor = SEGMENT.color_from_palette(colorIndex, false, PALETTE_SOLID_WRAP, 0);\n      SEGMENT.setPixelColorXY(x, rows-1 - y, ledColor);\n    }\n    if (previousBarHeight[x] > 0)\n      SEGMENT.setPixelColorXY(x, rows - previousBarHeight[x], (SEGCOLOR(2) != BLACK) ? SEGCOLOR(2) : ledColor);\n\n    if (rippleTime && previousBarHeight[x]>0) previousBarHeight[x]--;    //delay/ripple effect\n  }\n} // mode_2DGEQ()\nstatic 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\";\n\n\n/////////////////////////\n//  ** 2D Funky plank  //\n/////////////////////////\nvoid mode_2DFunkyPlank(void) {              // Written by ??? Adapted by Will Tatam.\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  int NUMB_BANDS = map(SEGMENT.custom1, 0, 255, 1, 16);\n  int barWidth = (cols / NUMB_BANDS);\n  int bandInc = 1;\n  if (barWidth == 0) {\n    // Matrix narrower than fft bands\n    barWidth = 1;\n    bandInc = (NUMB_BANDS / cols);\n  }\n\n  um_data_t *um_data = getAudioData();\n  uint8_t *fftResult = (uint8_t*)um_data->u_data[2];\n\n  if (SEGENV.call == 0) {\n    SEGMENT.fill(BLACK);\n  }\n\n  uint8_t secondHand = micros()/(256-SEGMENT.speed)/500+1 % 64;\n  if (SEGENV.aux0 != secondHand) {                        // Triggered millis timing.\n    SEGENV.aux0 = secondHand;\n\n    // display values of\n    int b = 0;\n    for (int band = 0; band < NUMB_BANDS; band += bandInc, b++) {\n      int hue = fftResult[band % 16];\n      int v = map(fftResult[band % 16], 0, 255, 10, 255);\n      for (int w = 0; w < barWidth; w++) {\n         int xpos = (barWidth * b) + w;\n         SEGMENT.setPixelColorXY(xpos, 0, CHSV(hue, 255, v));\n      }\n    }\n\n    // Update the display:\n    for (int i = (rows - 1); i > 0; i--) {\n      for (int j = (cols - 1); j >= 0; j--) {\n        SEGMENT.setPixelColorXY(j, i, SEGMENT.getPixelColorXY(j, i-1));\n      }\n    }\n  }\n} // mode_2DFunkyPlank\nstatic const char _data_FX_MODE_2DFUNKYPLANK[] PROGMEM = \"Funky Plank@Scroll speed,,# of bands;;;2f;si=0\"; // Beatsin\n\n\n/////////////////////////\n//     2D Akemi        //\n/////////////////////////\nstatic uint8_t akemi[] PROGMEM = {\n  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,\n  0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,\n  0,0,0,0,0,0,0,0,0,0,0,2,2,3,3,3,3,3,3,2,2,0,0,0,0,0,0,0,0,0,0,0,\n  0,0,0,0,0,0,0,0,0,0,2,3,3,0,0,0,0,0,0,3,3,2,0,0,0,0,0,0,0,0,0,0,\n  0,0,0,0,0,0,0,0,0,2,3,0,0,0,6,5,5,4,0,0,0,3,2,0,0,0,0,0,0,0,0,0,\n  0,0,0,0,0,0,0,0,2,3,0,0,6,6,5,5,5,5,4,4,0,0,3,2,0,0,0,0,0,0,0,0,\n  0,0,0,0,0,0,0,0,2,3,0,6,5,5,5,5,5,5,5,5,4,0,3,2,0,0,0,0,0,0,0,0,\n  0,0,0,0,0,0,0,2,3,0,6,5,5,5,5,5,5,5,5,5,5,4,0,3,2,0,0,0,0,0,0,0,\n  0,0,0,0,0,0,0,3,2,0,6,5,5,5,5,5,5,5,5,5,5,4,0,2,3,0,0,0,0,0,0,0,\n  0,0,0,0,0,0,3,2,3,6,5,5,7,7,5,5,5,5,7,7,5,5,4,3,2,3,0,0,0,0,0,0,\n  0,0,0,0,0,2,3,1,3,6,5,1,7,7,7,5,5,1,7,7,7,5,4,3,1,3,2,0,0,0,0,0,\n  0,0,0,0,0,8,3,1,3,6,5,1,7,7,7,5,5,1,7,7,7,5,4,3,1,3,8,0,0,0,0,0,\n  0,0,0,0,0,8,3,1,3,6,5,5,1,1,5,5,5,5,1,1,5,5,4,3,1,3,8,0,0,0,0,0,\n  0,0,0,0,0,2,3,1,3,6,5,5,5,5,5,5,5,5,5,5,5,5,4,3,1,3,2,0,0,0,0,0,\n  0,0,0,0,0,0,3,2,3,6,5,5,5,5,5,5,5,5,5,5,5,5,4,3,2,3,0,0,0,0,0,0,\n  0,0,0,0,0,0,0,0,0,6,5,5,5,5,5,7,7,5,5,5,5,5,4,0,0,0,0,0,0,0,0,0,\n  0,0,0,0,0,0,0,0,0,6,5,5,5,5,5,5,5,5,5,5,5,5,4,0,0,0,0,0,0,0,0,0,\n  1,0,0,0,0,0,0,0,0,6,5,5,5,5,5,5,5,5,5,5,5,5,4,0,0,0,0,0,0,0,0,2,\n  0,2,2,2,0,0,0,0,0,6,5,5,5,5,5,5,5,5,5,5,5,5,4,0,0,0,0,0,2,2,2,0,\n  0,0,0,3,2,0,0,0,6,5,4,4,4,4,4,4,4,4,4,4,4,4,4,4,0,0,0,2,2,0,0,0,\n  0,0,0,3,2,0,0,0,6,5,5,5,5,5,5,5,5,5,5,5,5,5,5,4,0,0,0,2,3,0,0,0,\n  0,0,0,0,3,2,0,0,0,0,3,3,0,3,3,0,0,3,3,0,3,3,0,0,0,0,2,2,0,0,0,0,\n  0,0,0,0,3,2,0,0,0,0,3,2,0,3,2,0,0,3,2,0,3,2,0,0,0,0,2,3,0,0,0,0,\n  0,0,0,0,0,3,2,0,0,3,2,0,0,3,2,0,0,3,2,0,0,3,2,0,0,2,3,0,0,0,0,0,\n  0,0,0,0,0,3,2,2,2,2,0,0,0,3,2,0,0,3,2,0,0,0,3,2,2,2,3,0,0,0,0,0,\n  0,0,0,0,0,0,3,3,3,0,0,0,0,3,2,0,0,3,2,0,0,0,0,3,3,3,0,0,0,0,0,0,\n  0,0,0,0,0,0,0,0,0,0,0,0,0,3,2,0,0,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0,\n  0,0,0,0,0,0,0,0,0,0,0,0,0,3,2,0,0,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0,\n  0,0,0,0,0,0,0,0,0,0,0,0,0,3,2,0,0,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0,\n  0,0,0,0,0,0,0,0,0,0,0,0,0,3,2,0,0,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0,\n  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,\n  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\n};\n\nvoid mode_2DAkemi(void) {\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  unsigned counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF;\n  counter = counter >> 8;\n\n  const float lightFactor  = 0.15f;\n  const float normalFactor = 0.4f;\n\n  um_data_t *um_data;\n  if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) {\n    um_data = simulateSound(SEGMENT.soundSim);\n  }\n  uint8_t *fftResult = (uint8_t*)um_data->u_data[2];\n  float base = fftResult[0]/255.0f;\n\n  //draw and color Akemi\n  for (int y=0; y < rows; y++) for (int x=0; x < cols; x++) {\n    CRGB color;\n    CRGB soundColor = CRGB::Orange;\n    CRGB faceColor  = CRGB(SEGMENT.color_wheel(counter));\n    CRGB armsAndLegsColor = CRGB(SEGCOLOR(1) > 0 ? SEGCOLOR(1) : 0xFFE0A0); //default warmish white 0xABA8FF; //0xFF52e5;//\n    uint8_t ak = pgm_read_byte_near(akemi + ((y * 32)/rows) * 32 + (x * 32)/cols); // akemi[(y * 32)/rows][(x * 32)/cols]\n    switch (ak) {\n      case 3: armsAndLegsColor.r *= lightFactor;  armsAndLegsColor.g *= lightFactor;  armsAndLegsColor.b *= lightFactor;  color = armsAndLegsColor; break; //light arms and legs 0x9B9B9B\n      case 2: armsAndLegsColor.r *= normalFactor; armsAndLegsColor.g *= normalFactor; armsAndLegsColor.b *= normalFactor; color = armsAndLegsColor; break; //normal arms and legs 0x888888\n      case 1: color = armsAndLegsColor; break; //dark arms and legs 0x686868\n      case 6: faceColor.r *= lightFactor;  faceColor.g *= lightFactor;  faceColor.b *= lightFactor;  color=faceColor; break; //light face 0x31AAFF\n      case 5: faceColor.r *= normalFactor; faceColor.g *= normalFactor; faceColor.b *= normalFactor; color=faceColor; break; //normal face 0x0094FF\n      case 4: color = faceColor; break; //dark face 0x007DC6\n      case 7: color = SEGCOLOR(2) > 0 ? SEGCOLOR(2) : 0xFFFFFF; break; //eyes and mouth default white\n      case 8: if (base > 0.4) {soundColor.r *= base; soundColor.g *= base; soundColor.b *= base; color=soundColor;} else color = armsAndLegsColor; break;\n      default: color = BLACK; break;\n    }\n\n    if (SEGMENT.intensity > 128 && fftResult && fftResult[0] > 128) { //dance if base is high\n      SEGMENT.setPixelColorXY(x, 0, BLACK);\n      SEGMENT.setPixelColorXY(x, y+1, color);\n    } else\n      SEGMENT.setPixelColorXY(x, y, color);\n  }\n\n  //add geq left and right\n  if (um_data && fftResult) {\n    int xMax = cols/8;\n    for (int x=0; x < xMax; x++) {\n      unsigned band = map(x, 0, max(xMax,4), 0, 15);  // map 0..cols/8 to 16 GEQ bands\n      band = constrain(band, 0, 15);\n      int barHeight = map(fftResult[band], 0, 255, 0, 17*rows/32);\n      uint32_t color = SEGMENT.color_from_palette((band * 35), false, PALETTE_SOLID_WRAP, 0);\n\n      for (int y=0; y < barHeight; y++) {\n        SEGMENT.setPixelColorXY(x, rows/2-y, color);\n        SEGMENT.setPixelColorXY(cols-1-x, rows/2-y, color);\n      }\n    }\n  }\n} // mode_2DAkemi\nstatic const char _data_FX_MODE_2DAKEMI[] PROGMEM = \"Akemi@Color speed,Dance;Head palette,Arms & Legs,Eyes & Mouth;Face palette;2f;si=0\"; //beatsin\n\n\n// Distortion waves - ldirko\n// https://editor.soulmatelights.com/gallery/1089-distorsion-waves\n// adapted for WLED by @blazoncek, improvements by @dedehai\nvoid mode_2Ddistortionwaves() {\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  uint8_t speed = SEGMENT.speed/32;\n  uint8_t scale = SEGMENT.intensity/32;\n  if(SEGMENT.check2) scale += 192 / (cols+rows); // zoom out some more. note: not changing scale slider for backwards compatibility\n\n  unsigned a  = strip.now/32;\n  unsigned a2 = a/2;\n  unsigned a3 = a/3;\n  unsigned colsScaled = cols * scale;\n  unsigned rowsScaled = rows * scale;\n\n  unsigned cx =  beatsin16_t(10-speed,0,colsScaled);\n  unsigned cy =  beatsin16_t(12-speed,0,rowsScaled);\n  unsigned cx1 = beatsin16_t(13-speed,0,colsScaled);\n  unsigned cy1 = beatsin16_t(15-speed,0,rowsScaled);\n  unsigned cx2 = beatsin16_t(17-speed,0,colsScaled);\n  unsigned cy2 = beatsin16_t(14-speed,0,rowsScaled);\n\n  byte rdistort, gdistort, bdistort;\n\n  unsigned xoffs = 0;\n  for (int x = 0; x < cols; x++) {\n    xoffs += scale;\n    unsigned yoffs = 0;\n\n    for (int y = 0; y < rows; y++) {\n       yoffs += scale;\n\n      if(SEGMENT.check3) {\n        // alternate mode from original code\n        rdistort = cos8_t (((x+y)*8+a2)&255)>>1;\n        gdistort = cos8_t (((x+y)*8+a3+32)&255)>>1;\n        bdistort = cos8_t (((x+y)*8+a+64)&255)>>1;\n      } else {\n        rdistort = cos8_t((cos8_t(((x<<3)+a )&255)+cos8_t(((y<<3)-a2)&255)+a3   )&255)>>1;\n        gdistort = cos8_t((cos8_t(((x<<3)-a2)&255)+cos8_t(((y<<3)+a3)&255)+a+32 )&255)>>1;\n        bdistort = cos8_t((cos8_t(((x<<3)+a3)&255)+cos8_t(((y<<3)-a) &255)+a2+64)&255)>>1;\n      }\n\n      byte valueR = rdistort + ((a- ( ((xoffs - cx)  * (xoffs - cx)  + (yoffs - cy)  * (yoffs - cy))>>7  ))<<1);\n      byte valueG = gdistort + ((a2-( ((xoffs - cx1) * (xoffs - cx1) + (yoffs - cy1) * (yoffs - cy1))>>7 ))<<1);\n      byte valueB = bdistort + ((a3-( ((xoffs - cx2) * (xoffs - cx2) + (yoffs - cy2) * (yoffs - cy2))>>7 ))<<1);\n\n      valueR = cos8_t(valueR);\n      valueG = cos8_t(valueG);\n      valueB = cos8_t(valueB);\n\n      if(SEGMENT.palette == 0) {\n        // use RGB values (original color mode)\n        SEGMENT.setPixelColorXY(x, y, RGBW32(valueR, valueG, valueB, 0));\n      } else {\n        // use palette\n        uint8_t brightness = (valueR + valueG + valueB) / 3;\n        if(SEGMENT.check1) { // map brightness to palette index\n          SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, brightness, 255, LINEARBLEND_NOWRAP));\n        } else {\n          // color mapping: calculate hue from pixel color, map it to palette index\n          CHSV hsvclr = rgb2hsv_approximate(CRGB(valueR>>2, valueG>>2, valueB>>2)); // scale colors down to not saturate for better hue extraction\n          SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, hsvclr.h, brightness));\n        }\n      }\n    }\n  }\n\n  // palette mode and not filling: smear-blur to cover up palette wrapping artefacts\n  if(!SEGMENT.check1 && SEGMENT.palette)\n    SEGMENT.blur(200, true);\n}\nstatic const char _data_FX_MODE_2DDISTORTIONWAVES[] PROGMEM = \"Distortion Waves@!,Scale,,,,Fill,Zoom,Alt;;!;2;pal=0\";\n\n\n//Soap\n//@Stepko\n//Idea from https://www.youtube.com/watch?v=DiHBgITrZck&ab_channel=StefanPetrick\n// adapted for WLED by @blazoncek, optimization by @dedehai\nstatic void soapPixels(bool isRow, uint8_t *noise3d, CRGB *pixels) {\n  const int  cols = SEG_W;\n  const int  rows = SEG_H;\n  const auto XY   = [&](int x, int y) { return x + y * cols; };\n  const auto abs  = [](int x) { return x<0 ? -x : x; };\n  const int  tRC  = isRow ? rows : cols; // transpose if isRow\n  const int  tCR  = isRow ? cols : rows; // transpose if isRow\n  const int  amplitude = max(1, (tCR - 8) >> 3) * (1 + (SEGMENT.custom1 >> 5));\n  const int  shift = 0; //(128 - SEGMENT.custom2)*2;\n\n  CRGB ledsbuff[tCR];\n\n  for (int i = 0; i < tRC; i++) {\n    int amount   = ((int)noise3d[isRow ? i*cols : i] - 128) * amplitude + shift; // use first row/column: XY(0,i)/XY(i,0)\n    int delta    = abs(amount) >> 8;\n    int fraction = abs(amount) & 255;\n    for (int j = 0; j < tCR; j++) {\n      int zD, zF;\n      if (amount < 0) {\n        zD = j - delta;\n        zF = zD - 1;\n      } else {\n        zD = j + delta;\n        zF = zD + 1;\n      }\n      int yA = abs(zD)%tCR;\n      int yB = abs(zF)%tCR;\n      int xA = i;\n      int xB = i;\n      if (isRow) {\n        std::swap(xA,yA);\n        std::swap(xB,yB);\n      }\n      const int indxA = XY(xA,yA);\n      const int indxB = XY(xB,yB);\n      CRGB PixelA;\n      CRGB PixelB;\n      if ((zD >= 0) && (zD < tCR)) PixelA = pixels[indxA];\n      else                         PixelA = ColorFromPalette(SEGPALETTE, ~noise3d[indxA]*3);\n      if ((zF >= 0) && (zF < tCR)) PixelB = pixels[indxB];\n      else                         PixelB = ColorFromPalette(SEGPALETTE, ~noise3d[indxB]*3);\n      ledsbuff[j] = (PixelA.nscale8(ease8InOutApprox(255 - fraction))) + (PixelB.nscale8(ease8InOutApprox(fraction)));\n    }\n    for (int j = 0; j < tCR; j++) {\n      CRGB c = ledsbuff[j];\n      if (isRow) std::swap(j,i);\n      SEGMENT.setPixelColorXY(i, j, pixels[XY(i,j)] = c);\n      if (isRow) std::swap(j,i);\n    }\n  }\n}\n\nvoid mode_2Dsoap() {\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n  const auto XY = [&](int x, int y) { return x + y * cols; };\n\n  const size_t segSize = SEGMENT.width() * SEGMENT.height(); // prevent reallocation if mirrored or grouped\n  const size_t dataSize = segSize * (sizeof(uint8_t) + sizeof(CRGB)); // pixels and noise\n  if (!SEGENV.allocateData(dataSize + sizeof(uint32_t)*3)) FX_FALLBACK_STATIC; //allocation failed\n\n  uint8_t  *noise3d    = reinterpret_cast<uint8_t*>(SEGENV.data);\n  CRGB     *pixels     = reinterpret_cast<CRGB*>(SEGENV.data + segSize * sizeof(uint8_t));\n  uint32_t *noisecoord = reinterpret_cast<uint32_t*>(SEGENV.data + dataSize); // x, y, z coordinates\n  const uint32_t scale32_x = 160000U/cols;\n  const uint32_t scale32_y = 160000U/rows;\n  const uint32_t mov = MIN(cols,rows)*(SEGMENT.speed+2)/2;\n  const uint8_t  smoothness = MIN(250,SEGMENT.intensity); // limit as >250 produces very little changes\n\n  if (SEGENV.call == 0) for (int i = 0; i < 3; i++) noisecoord[i] = hw_random(); // init\n  else                  for (int i = 0; i < 3; i++) noisecoord[i] += mov;\n\n  for (int i = 0; i < cols; i++) {\n    int32_t ioffset = scale32_x * (i - cols / 2);\n    for (int j = 0; j < rows; j++) {\n      int32_t joffset = scale32_y * (j - rows / 2);\n      uint8_t data = perlin16(noisecoord[0] + ioffset, noisecoord[1] + joffset, noisecoord[2]) >> 8;\n      noise3d[XY(i,j)] = scale8(noise3d[XY(i,j)], smoothness) + scale8(data, 255 - smoothness);\n    }\n  }\n  // init also if dimensions changed\n  if (SEGENV.call == 0 || SEGMENT.aux0 != cols || SEGMENT.aux1 != rows) {\n    SEGMENT.aux0 = cols;\n    SEGMENT.aux1 = rows;\n    for (int i = 0; i < cols; i++) {\n      for (int j = 0; j < rows; j++) {\n        SEGMENT.setPixelColorXY(i, j, ColorFromPalette(SEGPALETTE,~noise3d[XY(i,j)]*3));\n      }\n    }\n  }\n\n  soapPixels(true,  noise3d, pixels); // rows\n  soapPixels(false, noise3d, pixels); // cols\n}\nstatic const char _data_FX_MODE_2DSOAP[] PROGMEM = \"Soap@!,Smoothness,Density;;!;2;pal=11\";\n\n\n//Idea from https://www.youtube.com/watch?v=HsA-6KIbgto&ab_channel=GreatScott%21\n//Octopus (https://editor.soulmatelights.com/gallery/671-octopus)\n//Stepko and Sutaburosu\n// adapted for WLED by @blazoncek\nvoid mode_2Doctopus() {\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n  const auto XY = [&](int x, int y) { return (x%cols) + (y%rows) * cols; };\n  const uint8_t mapp = 180 / MAX(cols,rows);\n\n  typedef struct {\n    uint8_t angle;\n    uint8_t radius;\n  } map_t;\n\n  const size_t dataSize = SEGMENT.width() * SEGMENT.height() * sizeof(map_t); // prevent reallocation if mirrored or grouped\n  if (!SEGENV.allocateData(dataSize + 2)) FX_FALLBACK_STATIC; //allocation failed\n\n  map_t *rMap = reinterpret_cast<map_t*>(SEGENV.data);\n  uint8_t *offsX = reinterpret_cast<uint8_t*>(SEGENV.data + dataSize);\n  uint8_t *offsY = reinterpret_cast<uint8_t*>(SEGENV.data + dataSize + 1);\n\n  // re-init if SEGMENT dimensions or offset changed\n  if (SEGENV.call == 0 || SEGENV.aux0 != cols || SEGENV.aux1 != rows || SEGMENT.custom1 != *offsX || SEGMENT.custom2 != *offsY) {\n    SEGENV.step = 0; // t\n    SEGENV.aux0 = cols;\n    SEGENV.aux1 = rows;\n    *offsX = SEGMENT.custom1;\n    *offsY = SEGMENT.custom2;\n    const int C_X = (cols / 2) + ((SEGMENT.custom1 - 128)*cols)/255;\n    const int C_Y = (rows / 2) + ((SEGMENT.custom2 - 128)*rows)/255;\n    for (int x = 0; x < cols; x++) {\n      for (int y = 0; y < rows; y++) {\n        int dx = (x - C_X);\n        int dy = (y - C_Y);\n        rMap[XY(x, y)].angle  = int(40.7436f * atan2_t(dy, dx));  // avoid 128*atan2()/PI\n        rMap[XY(x, y)].radius = sqrtf(dx * dx + dy * dy) * mapp; //thanks Sutaburosu\n      }\n    }\n  }\n\n  SEGENV.step += SEGMENT.speed / 32 + 1;  // 1-4 range\n  for (int x = 0; x < cols; x++) {\n    for (int y = 0; y < rows; y++) {\n      byte angle = rMap[XY(x,y)].angle;\n      byte radius = rMap[XY(x,y)].radius;\n      //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)));\n      unsigned intensity = sin8_t(sin8_t((angle * 4 - radius) / 4 + SEGENV.step/2) + radius - SEGENV.step + angle * (SEGMENT.custom3/4+1));\n      intensity = map((intensity*intensity) & 0xFFFF, 0, 65535, 0, 255); // add a bit of non-linearity for cleaner display\n      SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, SEGENV.step / 2 - radius, intensity));\n    }\n  }\n}\nstatic const char _data_FX_MODE_2DOCTOPUS[] PROGMEM = \"Octopus@!,,Offset X,Offset Y,Legs,fasttan;;!;2;\";\n\n\n//Waving Cell\n//@Stepko (https://editor.soulmatelights.com/gallery/1704-wavingcells)\n// adapted for WLED by @blazoncek, improvements by @dedehai\nvoid mode_2Dwavingcell() {\n  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up\n\n  const int cols = SEG_W;\n  const int rows = SEG_H;\n\n  uint32_t t = (strip.now*(SEGMENT.speed + 1))>>3;\n  uint32_t aX = SEGMENT.custom1/16 + 9;\n  uint32_t aY = SEGMENT.custom2/16 + 1;\n  uint32_t aZ = SEGMENT.custom3 + 1;\n   for (int x = 0; x < cols; x++) {\n    for (int y = 0; y < rows; y++) {\n      uint32_t wave = sin8_t((x * aX) + sin8_t((((y<<8) + t) * aY)>>8)) + cos8_t(y * aZ); // bit shifts to increase temporal resolution\n      uint8_t colorIndex = wave + (t>>(8-(SEGMENT.check2*3)));\n      SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, colorIndex));\n    }\n  }\n  SEGMENT.blur(SEGMENT.intensity);\n}\nstatic const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = \"Waving Cell@!,Blur,Amplitude 1,Amplitude 2,Amplitude 3,,Flow;;!;2;ix=0\";\n\n#ifndef WLED_DISABLE_PARTICLESYSTEM2D\n\n/*\n  Particle System Vortex\n  Particles sprayed from center with a rotating spray\n  Uses palette for particle color\n  by DedeHai (Damian Schneider)\n*/\n#define NUMBEROFSOURCES 8\nvoid mode_particlevortex(void) {\n  if (SEGLEN == 1)\n    FX_FALLBACK_STATIC;\n  ParticleSystem2D *PartSys = nullptr;\n  uint32_t i, j;\n\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES))\n      FX_FALLBACK_STATIC; // allocation failed\n    #ifdef ESP8266\n    PartSys->setMotionBlur(180);\n    #else\n    PartSys->setMotionBlur(130);\n    #endif\n    for (i = 0; i < min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); i++) {\n      PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center\n      PartSys->sources[i].source.y = (PartSys->maxY + 1) >> 1; // center\n      PartSys->sources[i].maxLife = 900;\n      PartSys->sources[i].minLife = 800;\n    }\n    PartSys->setKillOutOfBounds(true);\n  }\n  else\n    PartSys = reinterpret_cast<ParticleSystem2D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  uint32_t spraycount = min(PartSys->numSources, (uint32_t)(1 + (SEGMENT.custom1 >> 5))); // number of sprays to display, 1-8\n  #ifdef ESP8266\n  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\n    int partindex = (int)PartSys->usedParticles - (int)i;\n    if (partindex >= 0) {\n      PartSys->particles[partindex].x = (PartSys->maxX + 1) >> 1; // center\n      PartSys->particles[partindex].y = (PartSys->maxY + 1) >> 1; // center\n      PartSys->particles[partindex].sat = 230;\n      PartSys->particles[partindex].ttl = 256; //keep alive\n    }\n  }\n  #endif\n\n  if (SEGMENT.check1)\n    PartSys->setSmearBlur(90); // enable smear blur\n  else\n    PartSys->setSmearBlur(0); // disable smear blur\n\n  // update colors of the sprays\n  for (i = 0; i < spraycount; i++) {\n      uint32_t coloroffset = 0xFF / spraycount;\n      PartSys->sources[i].source.hue = coloroffset * i;\n  }\n\n  // set rotation direction and speed\n  // can use direction flag to determine current direction\n  bool direction = SEGMENT.check2; //no automatic direction change, set it to flag\n  int32_t currentspeed = (int32_t)SEGENV.step; // make a signed integer out of step\n\n  if (SEGMENT.custom2 > 0) { // automatic direction change enabled\n    uint32_t changeinterval = 1040 - ((uint32_t)SEGMENT.custom2 << 2);\n    direction = SEGENV.aux1 & 0x01; //set direction according to flag\n\n    if (SEGMENT.check3) // random interval\n      changeinterval = 20 + changeinterval + hw_random16(changeinterval);\n\n    if (SEGMENT.call % changeinterval == 0) { //flip direction on next frame\n      SEGENV.aux1 |= 0x02; // set the update flag (for random interval update)\n      if (direction)\n        SEGENV.aux1 &= ~0x01; // clear the direction flag\n      else\n        SEGENV.aux1 |= 0x01; // set the direction flag\n    }\n  }\n\n  int32_t targetspeed = (direction ? 1 : -1) * (SEGMENT.speed << 3);\n  int32_t speeddiff = targetspeed - currentspeed;\n  int32_t speedincrement = speeddiff / 50;\n\n  if (speedincrement == 0) { //if speeddiff is not zero, make the increment at least 1 so it reaches target speed\n    if (speeddiff < 0)\n      speedincrement = -1;\n    else if (speeddiff > 0)\n      speedincrement = 1;\n  }\n\n  currentspeed += speedincrement;\n  SEGENV.aux0 += currentspeed;\n  SEGENV.step = (uint32_t)currentspeed; //save it back\n\n  uint16_t angleoffset = 0xFFFF / spraycount; // angle offset for an even distribution\n  uint32_t skip = PS_P_HALFRADIUS / (SEGMENT.intensity + 1) + 1; // intensity is emit speed, emit less on low speeds\n  if (SEGMENT.call % skip == 0) {\n    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.\n    for (i = 0; i < spraycount; i++) { // emit one particle per spray (if available)\n      PartSys->sources[j].var = (SEGMENT.custom3 >> 1); //update speed variation\n      #ifdef ESP8266\n      if (SEGMENT.call & 0x01) // every other frame, do not emit to save particles\n      #endif\n      PartSys->angleEmit(PartSys->sources[j], SEGENV.aux0 + angleoffset * j, (SEGMENT.intensity >> 2)+1);\n      j = (j + 1) % spraycount;\n    }\n  }\n  PartSys->update(); //update all particles and render to frame\n}\n#undef NUMBEROFSOURCES\nstatic 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\";\n\n/*\n  Particle Fireworks\n  Rockets shoot up and explode in a random color, sometimes in a defined pattern\n  by DedeHai (Damian Schneider)\n*/\n#define NUMBEROFSOURCES 8\n\nvoid mode_particlefireworks(void) {\n  ParticleSystem2D *PartSys = nullptr;\n  uint32_t numRockets;\n\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES))\n      FX_FALLBACK_STATIC; // allocation failed\n\n    PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting)\n    PartSys->setWallHardness(120); // ground bounce is fixed\n    numRockets = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES);\n    for (uint32_t j = 0; j < numRockets; j++) {\n      PartSys->sources[j].source.ttl = 500 * j; // first rocket starts immediately, others follow soon\n      PartSys->sources[j].source.vy = -1; // at negative speed, no particles are emitted and if rocket dies, it will be relaunched\n    }\n  }\n  else\n    PartSys = reinterpret_cast<ParticleSystem2D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  numRockets = map(SEGMENT.speed, 0 , 255, 4, min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES));\n\n  PartSys->setWrapX(SEGMENT.check1);\n  PartSys->setBounceY(SEGMENT.check2);\n  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\n  PartSys->setMotionBlur(map(SEGMENT.custom2, 0, 255, 0, 245)); // anable motion blur\n\n  // update the rockets, set the speed state\n  for (uint32_t j = 0; j < numRockets; j++) {\n      PartSys->applyGravity(PartSys->sources[j].source);\n      PartSys->particleMoveUpdate(PartSys->sources[j].source, PartSys->sources[j].sourceFlags);\n      if (PartSys->sources[j].source.ttl == 0) {\n        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)\n          PartSys->sources[j].source.vy = 0;\n        }\n        else if (PartSys->sources[j].source.vy < 0) { // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it\n          PartSys->sources[j].source.y = PS_P_RADIUS; // start from bottom\n          PartSys->sources[j].source.x = (PartSys->maxX >> 2) + hw_random(PartSys->maxX >> 1); // centered half\n          PartSys->sources[j].source.vy = (SEGMENT.custom3) + hw_random16(SEGMENT.custom1 >> 3) + 5; // rocket speed TODO: need to adjust for segment height\n          PartSys->sources[j].source.vx = hw_random16(7) - 3; // not perfectly straight up\n          PartSys->sources[j].source.sat = 30; // low saturation -> exhaust is off-white\n          PartSys->sources[j].source.ttl = hw_random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // set fuse time\n          PartSys->sources[j].maxLife = 40; // exhaust particle life\n          PartSys->sources[j].minLife = 10;\n          PartSys->sources[j].vx = 0;  // emitting speed\n          PartSys->sources[j].vy = -5;  // emitting speed\n          PartSys->sources[j].var = 4; // speed variation around vx,vy (+/- var)\n        }\n     }\n  }\n  // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time\n  uint32_t emitparticles, frequency, baseangle, hueincrement; // number of particles to emit for each rocket's state\n  // variables for circular explosions\n  [[maybe_unused]] int32_t speed, currentspeed, speedvariation, percircle;\n  int32_t counter = 0;\n  [[maybe_unused]] uint16_t angle;\n  [[maybe_unused]] unsigned angleincrement;\n  bool circularexplosion = false;\n\n  // emit particles for each rocket\n  for (uint32_t j = 0; j < numRockets; j++) {\n    // determine rocket state by its speed:\n    if (PartSys->sources[j].source.vy > 0) { // moving up, emit exhaust\n      emitparticles = 1;\n    }\n    else if (PartSys->sources[j].source.vy < 0) { // falling down, standby time\n      emitparticles = 0;\n    }\n    else { // speed is zero, explode!\n      PartSys->sources[j].source.hue = hw_random16(); // random color\n      PartSys->sources[j].source.sat = hw_random16(55) + 200;\n      PartSys->sources[j].maxLife = 200;\n      PartSys->sources[j].minLife = 100;\n      PartSys->sources[j].source.ttl = hw_random16((2000 - ((uint32_t)SEGMENT.speed << 2))) + 550 - (SEGMENT.speed << 1); // standby time til next launch\n      PartSys->sources[j].var = ((SEGMENT.intensity >> 4) + 5); // speed variation around vx,vy (+/- var)\n      PartSys->sources[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch\n      #ifdef ESP8266\n      emitparticles = hw_random16(SEGMENT.intensity >> 3) + (SEGMENT.intensity >> 3) + 5; // defines the size of the explosion\n      #else\n      emitparticles = hw_random16(SEGMENT.intensity >> 2) + (SEGMENT.intensity >> 2) + 5; // defines the size of the explosion\n      #endif\n\n      if (random16() & 1) { // 50% chance for circular explosion\n        circularexplosion = true;\n        speed = 2 + hw_random16(3) + ((SEGMENT.intensity >> 6));\n        currentspeed = speed;\n        angleincrement = 2730 + hw_random16(5461); // minimum 15° + random(30°)\n        angle = hw_random16(); // random start angle\n        baseangle = angle; // save base angle for modulation\n        percircle = 0xFFFF / angleincrement + 1; // number of particles to make complete circles\n        hueincrement = hw_random16() & 127; // &127 is equivalent to %128\n        int circles = 1 + hw_random16(3) + ((SEGMENT.intensity >> 6));\n        frequency = hw_random16() & 127; // modulation frequency (= \"waves per circle\"), x.4 fixed point\n        emitparticles = percircle * circles;\n        PartSys->sources[j].var = angle & 1; // 0 or 1 variation, angle is random\n      }\n    }\n    uint32_t i;\n    for (i = 0; i < emitparticles; i++) {\n      if (circularexplosion) {\n        int32_t sineMod = 0xEFFF + sin16_t((uint16_t)(((angle * frequency) >> 4) + baseangle)); // shifted to positive values\n        currentspeed = (speed/2 + ((sineMod * speed) >> 16)) >> 1; // sine modulation on speed based on emit angle\n        PartSys->angleEmit(PartSys->sources[j], angle, currentspeed); // note: compiler warnings can be ignored, variables are set just above\n        counter++;\n        if (counter > percircle) { // full circle completed, increase speed\n          counter = 0;\n          speed += 3 + ((SEGMENT.intensity >> 6)); // increase speed to form a second wave\n          PartSys->sources[j].source.hue += hueincrement; // new color for next circle\n          PartSys->sources[j].source.sat = 100 + hw_random16(156);\n        }\n        angle += angleincrement; // set angle for next particle\n      }\n      else { // random explosion or exhaust\n        PartSys->sprayEmit(PartSys->sources[j]);\n        if ((j % 3) == 0) {\n          PartSys->sources[j].source.hue = hw_random16(); // random color for each particle (this is also true for exhaust, but that is white anyways)\n        }\n      }\n    }\n    if (i == 0) // no particles emitted, this rocket is falling\n      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)\n    circularexplosion = false; // reset for next rocket\n  }\n  if (SEGMENT.check3) { // fast speed, move particles twice\n    for (uint32_t i = 0; i < PartSys->usedParticles; i++) {\n      PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i], nullptr, nullptr);\n    }\n  }\n  PartSys->update(); // update and render\n}\n#undef NUMBEROFSOURCES\nstatic 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\";\n\n/*\n  Particle Volcano\n  Particles are sprayed from below, spray moves back and forth if option is set\n  Uses palette for particle color\n  by DedeHai (Damian Schneider)\n*/\n#define NUMBEROFSOURCES 1\nvoid mode_particlevolcano(void) {\n  ParticleSystem2D *PartSys = nullptr;\n  PSsettings2D volcanosettings;\n  volcanosettings.asByte = 0b00000100; // PS settings for volcano movement: bounceX is enabled\n  uint8_t numSprays; // note: so far only one tested but more is possible\n  uint32_t i = 0;\n\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, no additional data needed\n      FX_FALLBACK_STATIC; // allocation failed or not 2D\n\n    PartSys->setBounceY(true);\n    PartSys->setGravity(); // enable with default gforce\n    PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting)\n    PartSys->setMotionBlur(230); // anable motion blur\n\n    numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); // number of sprays\n    for (i = 0; i < numSprays; i++) {\n      PartSys->sources[i].source.hue = hw_random16();\n      PartSys->sources[i].source.x = PartSys->maxX / (numSprays + 1) * (i + 1); // distribute evenly\n      PartSys->sources[i].maxLife = 300; // lifetime in frames\n      PartSys->sources[i].minLife = 250;\n      PartSys->sources[i].sourceFlags.collide = true; // seeded particles will collide (if enabled)\n      PartSys->sources[i].sourceFlags.perpetual = true; // source never dies\n    }\n  }\n  else\n    PartSys = reinterpret_cast<ParticleSystem2D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); // number of volcanoes\n\n  // change source emitting color from time to time, emit one particle per spray\n  if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) { // every nth frame, cycle color and emit particles (and update the sources)\n    for (i = 0; i < numSprays; i++) {\n      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.\n      PartSys->sources[i].source.vy = 0; //reset speed (so no extra particlesettin is required to keep the source 'afloat')\n      PartSys->sources[i].source.hue++; // = hw_random16(); //change hue of spray source (note: random does not look good)\n      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\n      PartSys->sources[i].vy = SEGMENT.speed >> 2; // emitting speed (upwards)\n      PartSys->sources[i].vx = 0;\n      PartSys->sources[i].var = SEGMENT.custom3 >> 1; // emiting variation = nozzle size (custom 3 goes from 0-31)\n      PartSys->sprayEmit(PartSys->sources[i]);\n      PartSys->setWallHardness(255); // full hardness for source bounce\n      PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &volcanosettings); //move the source\n    }\n  }\n\n  // Particle System settings\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  PartSys->setColorByAge(SEGMENT.check1);\n  PartSys->setBounceX(SEGMENT.check2);\n  PartSys->setWallHardness(SEGMENT.custom2);\n\n  if (SEGMENT.check3) // collisions enabled\n    PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness\n  else\n    PartSys->enableParticleCollisions(false);\n\n  PartSys->update(); // update and render\n}\n#undef NUMBEROFSOURCES\nstatic 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\";\n\n/*\n  Particle Fire\n  realistic fire effect using particles. heat based and using perlin-noise for wind\n  by DedeHai (Damian Schneider)\n*/\nvoid mode_particlefire(void) {\n  ParticleSystem2D *PartSys = nullptr;\n  uint32_t i; // index variable\n  uint32_t numFlames; // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results\n\n  if (SEGMENT.call == 0) { // initialization\n    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)\n      FX_FALLBACK_STATIC; // allocation failed or not 2D\n    SEGENV.aux0 = hw_random16(); // aux0 is wind position (index) in the perlin noise\n  }\n  else\n    PartSys = reinterpret_cast<ParticleSystem2D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  PartSys->setWrapX(SEGMENT.check2);\n  PartSys->setMotionBlur(SEGMENT.check1 * 170); // anable/disable motion blur\n  PartSys->setSmearBlur(!SEGMENT.check1 * 60);  // enable smear blur if motion blur is not enabled\n\n  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)\n  if (SEGMENT.speed < 100) { //slow, limit FPS\n    uint32_t *lastcall = reinterpret_cast<uint32_t *>(PartSys->PSdataEnd);\n    uint32_t period = strip.now - *lastcall;\n    if (period < (uint32_t)map(SEGMENT.speed, 0, 99, 50, 10)) { // limit to 90FPS - 20FPS\n      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)\n      return; //do not update this frame\n    }\n    *lastcall = strip.now;\n  }\n\n  uint32_t spread = (PartSys->maxX >> 5) * (SEGMENT.custom3 + 1); //fire around segment center (in subpixel points)\n  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\n  uint32_t percycle = (numFlames * 2) / 3; // maximum number of particles emitted per cycle (TODO: for ESP826 maybe use flames/2)\n\n  // update the flame sprays:\n  for (i = 0; i < numFlames; i++) {\n    if (SEGMENT.call & 1 && PartSys->sources[i].source.ttl > 0) { // every second frame\n      PartSys->sources[i].source.ttl--;\n    } else { // flame source is dead: initialize new flame: set properties of source\n      PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread >> 1) + hw_random(spread); // change flame position: distribute randomly on chosen width\n      PartSys->sources[i].source.y = -(PS_P_RADIUS << 2); // set the source below the frame\n      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\n      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\n      PartSys->sources[i].minLife = PartSys->sources[i].maxLife >> 1;\n      PartSys->sources[i].vx = hw_random16(5) - 2; // emitting speed (sideways)\n      PartSys->sources[i].vy = (SEGMENT.vHeight() >> 1) + (firespeed >> 4) + (SEGMENT.custom1 >> 4); // emitting speed (upwards)\n      PartSys->sources[i].var = 2 + hw_random16(2 + (firespeed >> 4)); // speed variation around vx,vy (+/- var)\n    }\n  }\n\n  if (SEGMENT.call % 3 == 0) { // update noise position and add wind\n    SEGENV.aux0++; // position in the perlin noise matrix for wind generation\n    if (SEGMENT.call % 10 == 0)\n      SEGENV.aux1++; // move in noise y direction so noise does not repeat as often\n    // add wind force to all particles\n    int8_t windspeed = ((int16_t)(perlin8(SEGENV.aux0, SEGENV.aux1) - 127) * SEGMENT.custom2) >> 7;\n    PartSys->applyForce(windspeed, 0);\n  }\n  SEGENV.step++;\n\n  if (SEGMENT.check3) { //add turbulance (parameters and algorithm found by experimentation)\n    if (SEGMENT.call % map(firespeed, 0, 255, 4, 15) == 0) {\n      for (i = 0; i < PartSys->usedParticles; i++) {\n        if (PartSys->particles[i].y < PartSys->maxY / 4) { // do not apply turbulance everywhere -> bottom quarter seems a good balance\n          int32_t curl = ((int32_t)perlin8(PartSys->particles[i].x, PartSys->particles[i].y, SEGENV.step << 4) - 127);\n          PartSys->particles[i].vx += (curl * (firespeed + 10)) >> 9;\n        }\n      }\n    }\n  }\n\n  // emit faster sparks at first flame position, amount and speed mostly dependends on intensity\n  if(hw_random8() < 10 + (SEGMENT.intensity >> 2)) {\n    for (i = 0; i < PartSys->usedParticles; i++) {\n      if (PartSys->particles[i].ttl == 0) { // find a dead particle\n        PartSys->particles[i].ttl = hw_random16(SEGMENT.vHeight()) + 30;\n        PartSys->particles[i].x = PartSys->sources[0].source.x;\n        PartSys->particles[i].y = PartSys->sources[0].source.y;\n        PartSys->particles[i].vx = PartSys->sources[0].source.vx;\n        PartSys->particles[i].vy = (SEGMENT.vHeight() >> 1) + (firespeed >> 4) + ((30 + (SEGMENT.intensity >> 1) + SEGMENT.custom1) >> 4); // emitting speed (upwards)\n        break; // emit only one particle\n      }\n    }\n  }\n\n  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)\n  for (i = 0; i < percycle; i++) {\n    j = (j + 1) % numFlames;\n    PartSys->flameEmit(PartSys->sources[j]);\n  }\n\n  PartSys->updateFire(SEGMENT.intensity); // update and render the fire\n}\nstatic 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\";\n\n/*\n  PS Ballpit: particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce\n  sliders control falling speed, intensity (number of particles spawned), inter-particle collision hardness (0 means no particle collisions) and render saturation\n  this is quite versatile, can be made to look like rain or snow or confetti etc.\n  Uses palette for particle color\n  by DedeHai (Damian Schneider)\n*/\nvoid mode_particlepit(void) {\n  ParticleSystem2D *PartSys = nullptr;\n\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem2D(PartSys, 0, 0, true, false)) // init\n      FX_FALLBACK_STATIC; // allocation failed or not 2D\n    PartSys->setKillOutOfBounds(true);\n    PartSys->setGravity(); // enable with default gravity\n    PartSys->setUsedParticles(170); // use 75% of available particles\n  }\n  else\n    PartSys = reinterpret_cast<ParticleSystem2D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n\n  PartSys->setWrapX(SEGMENT.check1);\n  PartSys->setBounceX(SEGMENT.check2);\n  PartSys->setBounceY(SEGMENT.check3);\n  PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)150)); // limit to 100 min (if collisions are disabled, still want bouncy)\n  if (SEGMENT.custom2 > 0)\n    PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness\n  else\n    PartSys->enableParticleCollisions(false);\n\n  uint32_t i;\n  if (SEGMENT.call % (128 - (SEGMENT.intensity >> 1)) == 0 && SEGMENT.intensity > 0) { // every nth frame emit particles, stop emitting if set to zero\n    for (i = 0; i < PartSys->usedParticles; i++) { // emit particles\n      if (PartSys->particles[i].ttl == 0) { // find a dead particle\n        // emit particle at random position over the top of the matrix (random16 is not random enough)\n        PartSys->particles[i].ttl = 1500 - (SEGMENT.speed << 2) + hw_random16(500); // if speed is higher, make them die sooner\n        PartSys->particles[i].x = hw_random(PartSys->maxX); //random(PartSys->maxX >> 1) + (PartSys->maxX >> 2);\n        PartSys->particles[i].y = (PartSys->maxY << 1); // particles appear somewhere above the matrix, maximum is double the height\n        PartSys->particles[i].vx = (int16_t)hw_random16(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); // side speed is +/-\n        PartSys->particles[i].vy = map(SEGMENT.speed, 0, 255, -5, -100); // downward speed\n        PartSys->particles[i].hue = hw_random16(); // set random color\n        PartSys->particleFlags[i].collide = true; // enable collision for particle\n        PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7;\n        // set particle size\n        if (SEGMENT.custom1 == 255) {\n          PartSys->perParticleSize = true;\n          PartSys->advPartProps[i].size = hw_random16(SEGMENT.custom1); // set each particle to random size\n        } else {\n          PartSys->setParticleSize(SEGMENT.custom1); // set global size\n          PartSys->advPartProps[i].size = SEGMENT.custom1; // also set individual size for consistency\n        }\n        break; // emit only one particle per round\n      }\n    }\n  }\n\n  uint32_t frictioncoefficient = 1 + SEGMENT.check1; //need more friction if wrapX is set, see below note\n  if (SEGMENT.speed < 50) // for low speeds, apply more friction\n    frictioncoefficient = 50 - SEGMENT.speed;\n\n  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\n    PartSys->applyFriction(frictioncoefficient);\n\n  PartSys->update(); // update and render\n}\nstatic 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\";\n\n/*\n  Particle Waterfall\n  Uses palette for particle color, spray source at top emitting particles, many config options\n  by DedeHai (Damian Schneider)\n*/\nvoid mode_particlewaterfall(void) {\n  ParticleSystem2D *PartSys = nullptr;\n  uint8_t numSprays;\n  uint32_t i = 0;\n\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem2D(PartSys, 12)) // init, request 12 sources, no additional data needed\n      FX_FALLBACK_STATIC; // allocation failed or not 2D\n\n    PartSys->setGravity();  // enable with default gforce\n    PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting)\n    PartSys->setMotionBlur(190); // anable motion blur\n    PartSys->setSmearBlur(30); // enable 2D blurring (smearing)\n    for (i = 0; i < PartSys->numSources; i++) {\n      PartSys->sources[i].source.hue = i*90;\n      PartSys->sources[i].sourceFlags.collide = true; // seeded particles will collide\n    #ifdef ESP8266\n      PartSys->sources[i].maxLife = 250; // lifetime in frames (ESP8266 has less particles, make them short lived to keep the water flowing)\n      PartSys->sources[i].minLife = 100;\n    #else\n      PartSys->sources[i].maxLife = 400; // lifetime in frames\n      PartSys->sources[i].minLife = 150;\n    #endif\n    }\n  }\n  else\n    PartSys = reinterpret_cast<ParticleSystem2D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  // Particle System settings\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  PartSys->setWrapX(SEGMENT.check1);   // cylinder\n  PartSys->setBounceX(SEGMENT.check2); // walls\n  PartSys->setBounceY(SEGMENT.check3); // ground\n  PartSys->setWallHardness(SEGMENT.custom2);\n  numSprays = min((int32_t)PartSys->numSources, max(PartSys->maxXpixel / 6, (int32_t)2)); // number of sprays depends on segment width\n  if (SEGMENT.custom2 > 0) // collisions enabled\n    PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness\n  else {\n    PartSys->enableParticleCollisions(false);\n    PartSys->setWallHardness(120); // set hardness (for ground bounce) to fixed value if not using collisions\n  }\n\n  for (i = 0; i < numSprays; i++) {\n      PartSys->sources[i].source.hue += 1 + hw_random16(SEGMENT.custom1>>1); // change hue of spray source\n  }\n\n  if (SEGMENT.call % (12 - (SEGMENT.intensity >> 5)) == 0 && SEGMENT.intensity > 0) { // every nth frame, emit particles, do not emit if intensity is zero\n    for (i = 0; i < numSprays; i++) {\n      PartSys->sources[i].vy = -SEGMENT.speed >> 3; // emitting speed, down\n      //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\n      PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (PartSys->maxXpixel - numSprays) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position\n      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\n      PartSys->sources[i].var = (SEGMENT.custom1 >> 3); // emiting variation 0-32\n      PartSys->sprayEmit(PartSys->sources[i]);\n    }\n  }\n\n  if (SEGMENT.call % 20 == 0)\n    PartSys->applyFriction(1); // add just a tiny amount of friction to help smooth things\n\n  PartSys->update();   // update and render\n}\nstatic 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\";\n\n/*\n  Particle Box, applies gravity to particles in either a random direction or random but only downwards (sloshing)\n  Uses palette for particle color\n  by DedeHai (Damian Schneider)\n*/\nvoid mode_particlebox(void) {\n  ParticleSystem2D *PartSys = nullptr;\n  uint32_t i;\n\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem2D(PartSys, 1, 0, true)) // init\n      FX_FALLBACK_STATIC; // allocation failed or not 2D\n    PartSys->setBounceX(true);\n    PartSys->setBounceY(true);\n    SEGENV.aux0 = hw_random16(); // position in perlin noise\n  }\n  else\n    PartSys = reinterpret_cast<ParticleSystem2D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); // wall hardness is 200 or more\n  PartSys->enableParticleCollisions(true, max(2, (int)SEGMENT.custom2)); // enable collisions and set particle collision hardness\n  int maxParticleSize = min(((SEGMENT.vWidth() * SEGMENT.vHeight()) >> 2), 255U); // max particle size based on matrix size\n  unsigned currentParticleSize = map(SEGMENT.custom3, 0, 31, 0, maxParticleSize);\n  PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 2, 153) / (1 + (currentParticleSize >> 4))); // 1% - 60%, reduce if using larger size\n  if (SEGMENT.custom3 < 31)\n    PartSys->setParticleSize(currentParticleSize); // set global size if not max (resets perParticleSize)\n  else\n    PartSys->perParticleSize = true; // per particle size, uses advPartProps.size (randomized below)\n\n  // add in new particles if amount has changed\n  for (i = 0; i < PartSys->usedParticles; i++) {\n    if (PartSys->particles[i].ttl < 260) { // initialize dead particles\n      PartSys->particles[i].ttl = 260; // full brigthness\n      PartSys->particles[i].x = hw_random16(PartSys->maxX);\n      PartSys->particles[i].y = hw_random16(PartSys->maxY);\n      PartSys->particles[i].hue = hw_random8(); // make it colorful\n      PartSys->particleFlags[i].perpetual = true; // never die\n      PartSys->particleFlags[i].collide = true; // all particles colllide\n      PartSys->advPartProps[i].size = hw_random8(maxParticleSize); // random size, used only if size is set to max (SEGMENT.custom3=31)\n      break; // only spawn one particle per frame for less chaotic transitions\n    }\n  }\n\n  if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0 && SEGMENT.speed > 0) { // how often the force is applied depends on speed setting\n    int32_t xgravity;\n    int32_t ygravity;\n    int32_t increment = (SEGMENT.speed >> 6) + 1;\n\n    if (SEGMENT.check2) { // washing machine\n      int speed = tristate_square8(strip.now >> 7, 90, 15) / ((400 - SEGMENT.speed) >> 3);\n      SEGENV.aux0 += speed;\n      if (speed == 0) SEGENV.aux0 = 190; //down (= 270°)\n    }\n    else\n      SEGENV.aux0 -= increment;\n\n    if (SEGMENT.check1) { // random, use perlin noise\n      xgravity = ((int16_t)perlin8(SEGENV.aux0) - 127);\n      ygravity = ((int16_t)perlin8(SEGENV.aux0 + 10000) - 127);\n      // scale the gravity force\n      xgravity = (xgravity * SEGMENT.custom1) / 128;\n      ygravity = (ygravity * SEGMENT.custom1) / 128;\n    }\n    else { // go in a circle\n      xgravity = ((int32_t)(SEGMENT.custom1) * cos16_t(SEGENV.aux0 << 8)) / 0xFFFF;\n      ygravity = ((int32_t)(SEGMENT.custom1) * sin16_t(SEGENV.aux0 << 8)) / 0xFFFF;\n    }\n    if (SEGMENT.check3) { // sloshing, y force is always downwards\n      if (ygravity > 0)\n        ygravity = -ygravity;\n    }\n\n    PartSys->applyForce(xgravity, ygravity);\n  }\n\n  if ((SEGMENT.call & 0x0F) == 0) // every 16th frame\n    PartSys->applyFriction(1);\n\n  PartSys->update();   // update and render\n}\nstatic 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\";\n\n/*\n  Fuzzy Noise: Perlin noise 'gravity' mapping as in particles on 'noise hills' viewed from above\n  calculates slope gradient at the particle positions and applies 'downhill' force, resulting in a fuzzy perlin noise display\n  by DedeHai (Damian Schneider)\n*/\nvoid mode_particleperlin(void) {\n  ParticleSystem2D *PartSys = nullptr;\n  uint32_t i;\n\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem2D(PartSys, 1, 0, true)) // init with 1 source and advanced properties\n      FX_FALLBACK_STATIC; // allocation failed or not 2D\n\n    PartSys->setKillOutOfBounds(true); // should never happen, but lets make sure there are no stray particles\n    PartSys->setMotionBlur(230); // anable motion blur\n    PartSys->setBounceY(true);\n    SEGENV.aux0 = rand();\n  }\n  else\n    PartSys = reinterpret_cast<ParticleSystem2D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  PartSys->setWrapX(SEGMENT.check1);\n  PartSys->setBounceX(!SEGMENT.check1);\n  PartSys->setWallHardness(SEGMENT.custom1); // wall hardness\n  PartSys->enableParticleCollisions(SEGMENT.check3, SEGMENT.custom1); // enable collisions and set particle collision hardness\n  PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 128)); // min is 10%, max is 50%\n  PartSys->setSmearBlur(SEGMENT.check2 * 15); // enable 2D blurring (smearing)\n\n  // apply 'gravity' from a 2D perlin noise map\n  SEGENV.aux0 += 1 + (SEGMENT.speed >> 5); // noise z-position\n  // update position in noise\n  for (i = 0; i < PartSys->usedParticles; i++) {\n    if (PartSys->particles[i].ttl == 0) { // revive dead particles (do not keep them alive forever, they can clump up, need to reseed)\n      PartSys->particles[i].ttl = hw_random16(500) + 200;\n      PartSys->particles[i].x = hw_random(PartSys->maxX);\n      PartSys->particles[i].y = hw_random(PartSys->maxY);\n      PartSys->particleFlags[i].collide = true; // particle colllides\n    }\n    uint32_t scale = 16 - ((31 - SEGMENT.custom3) >> 1);\n    uint16_t xnoise = PartSys->particles[i].x / scale; // position in perlin noise, scaled by slider\n    uint16_t ynoise = PartSys->particles[i].y / scale;\n    int16_t baseheight = perlin8(xnoise, ynoise, SEGENV.aux0); // noise value at particle position\n    PartSys->particles[i].hue = baseheight; // color particles to perlin noise value\n    if (SEGMENT.call % 8 == 0) { // do not apply the force every frame, is too chaotic\n      int8_t xslope = (baseheight + (int16_t)perlin8(xnoise - 10, ynoise, SEGENV.aux0));\n      int8_t yslope = (baseheight + (int16_t)perlin8(xnoise, ynoise - 10, SEGENV.aux0));\n      PartSys->applyForce(i, xslope, yslope);\n    }\n  }\n\n  if (SEGMENT.call % (16 - (SEGMENT.custom2 >> 4)) == 0)\n    PartSys->applyFriction(2);\n\n  PartSys->update(); // update and render\n}\nstatic 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\";\n\n/*\n  Particle smashing down like meteors and exploding as they hit the ground, has many parameters to play with\n  by DedeHai (Damian Schneider)\n*/\n#define NUMBEROFSOURCES 8\nvoid mode_particleimpact(void) {\n  ParticleSystem2D *PartSys = nullptr;\n  uint32_t numMeteors;\n  PSsettings2D meteorsettings;\n  meteorsettings.asByte = 0b00101000; // PS settings for meteors: bounceY and gravity enabled\n\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, no additional data needed\n      FX_FALLBACK_STATIC; // allocation failed or not 2D\n    PartSys->setKillOutOfBounds(true);\n    PartSys->setGravity(); // enable default gravity\n    PartSys->setBounceY(true); // always use ground bounce\n    PartSys->setWallRoughness(220); // high roughness\n    numMeteors = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES);\n    for (uint32_t i = 0; i < numMeteors; i++) {\n      PartSys->sources[i].source.ttl = hw_random16(10 * i); // set initial delay for meteors\n      PartSys->sources[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched\n    }\n  }\n  else\n    PartSys = reinterpret_cast<ParticleSystem2D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  // Particle System settings\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  PartSys->setWrapX(SEGMENT.check1);\n  PartSys->setBounceX(SEGMENT.check2);\n  PartSys->setMotionBlur(SEGMENT.custom3<<3);\n  uint8_t hardness = map(SEGMENT.custom2, 0, 255, PS_P_MINSURFACEHARDNESS - 2, 255);\n  PartSys->setWallHardness(hardness);\n  PartSys->enableParticleCollisions(SEGMENT.check3, hardness); // enable collisions and set particle collision hardness\n  numMeteors = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES);\n  uint32_t emitparticles; // number of particles to emit for each rocket's state\n\n  for (uint32_t i = 0; i < numMeteors; i++) {\n    // determine meteor state by its speed:\n    if ( PartSys->sources[i].source.vy < 0) // moving down, emit sparks\n      emitparticles = 1;\n    else if ( PartSys->sources[i].source.vy > 0) // moving up means meteor is on 'standby'\n      emitparticles = 0;\n    else { // speed is zero, explode!\n      PartSys->sources[i].source.vy = 10; // set source speed positive so it goes into timeout and launches again\n      emitparticles = map(SEGMENT.intensity, 0, 255, 10, hw_random16(PartSys->usedParticles>>2)); // defines the size of the explosion\n    }\n    for (int e = emitparticles; e > 0; e--) {\n        PartSys->sprayEmit(PartSys->sources[i]);\n    }\n  }\n\n  // update the meteors, set the speed state\n  for (uint32_t i = 0; i < numMeteors; i++) {\n    if (PartSys->sources[i].source.ttl) {\n      PartSys->sources[i].source.ttl--; // note: this saves an if statement, but moving down particles age twice\n      if (PartSys->sources[i].source.vy < 0) { // move down\n        PartSys->applyGravity(PartSys->sources[i].source);\n        PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &meteorsettings);\n\n        // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above)\n        if (PartSys->sources[i].source.y < PS_P_RADIUS<<1) { // reached the bottom pixel on its way down\n          PartSys->sources[i].source.vy = 0; // set speed zero so it will explode\n          PartSys->sources[i].source.vx = 0;\n          PartSys->sources[i].sourceFlags.collide = true;\n          #ifdef ESP8266\n          PartSys->sources[i].maxLife = 900;\n          PartSys->sources[i].minLife = 100;\n          #else\n          PartSys->sources[i].maxLife = 1250;\n          PartSys->sources[i].minLife = 250;\n          #endif\n          PartSys->sources[i].source.ttl = hw_random16((768 - (SEGMENT.speed << 1))) + 40; // standby time til next launch (in frames)\n          PartSys->sources[i].vy = (SEGMENT.custom1 >> 2);  // emitting speed y\n          PartSys->sources[i].var = (SEGMENT.custom1 >> 2); // speed variation around vx,vy (+/- var)\n        }\n      }\n    }\n    else if (PartSys->sources[i].source.vy > 0) {  // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it\n      // reinitialize meteor\n      PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS << 2); // start 4 pixels above the top\n      PartSys->sources[i].source.x = hw_random(PartSys->maxX);\n      PartSys->sources[i].source.vy = -hw_random16(30) - 30; // meteor downward speed\n      PartSys->sources[i].source.vx = hw_random16(50) - 25; // TODO: make this dependent on position so they do not move out of frame\n      PartSys->sources[i].source.hue = hw_random16(); // random color\n      PartSys->sources[i].source.ttl = 500; // long life, will explode at bottom\n      PartSys->sources[i].sourceFlags.collide = false; // trail particles will not collide\n      PartSys->sources[i].maxLife = 300; // spark particle life\n      PartSys->sources[i].minLife = 100;\n      PartSys->sources[i].vy = -9; // emitting speed (down)\n      PartSys->sources[i].var = 3; // speed variation around vx,vy (+/- var)\n    }\n  }\n\n  for (uint32_t i = 0; i < PartSys->usedParticles; i++) {\n    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\n  }\n\n  PartSys->update(); // update and render\n}\n#undef NUMBEROFSOURCES\nstatic 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\";\n\n/*\n  Particle Attractor, a particle attractor sits in the matrix center, a spray bounces around and seeds particles\n  uses inverse square law like in planetary motion\n  Uses palette for particle color\n  by DedeHai (Damian Schneider)\n*/\nvoid mode_particleattractor(void) {\n  ParticleSystem2D *PartSys = nullptr;\n  PSsettings2D sourcesettings;\n  sourcesettings.asByte = 0b00001100; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not)\n  PSparticleFlags attractorFlags;\n  attractorFlags.asByte = 0; // no flags set\n  PSparticle *attractor; // particle pointer to the attractor\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem2D(PartSys, 1, sizeof(PSparticle), true)) // init using 1 source and advanced particle settings\n      FX_FALLBACK_STATIC; // allocation failed or not 2D\n    PartSys->sources[0].source.hue = hw_random16();\n    PartSys->sources[0].source.vx = -7; // will collied with wall and get random bounce direction\n    PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide\n    PartSys->sources[0].sourceFlags.perpetual = true; //source does not age\n    #ifdef ESP8266\n    PartSys->sources[0].maxLife = 200; // lifetime in frames (ESP8266 has less particles)\n    PartSys->sources[0].minLife = 30;\n    #else\n    PartSys->sources[0].maxLife = 350; // lifetime in frames\n    PartSys->sources[0].minLife = 50;\n    #endif\n    PartSys->sources[0].var = 4; // emiting variation\n    PartSys->setWallHardness(255);  //bounce forever\n    PartSys->setWallRoughness(200); //randomize wall bounce\n  }\n  else {\n    PartSys = reinterpret_cast<ParticleSystem2D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n  }\n\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  // Particle System settings\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  PartSys->setColorByAge(SEGMENT.check1);\n  PartSys->setParticleSize(SEGMENT.custom1 >> 1); //set size globally\n  PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 190));\n  attractor = reinterpret_cast<PSparticle *>(PartSys->PSdataEnd);\n  // set attractor properties\n  attractor->ttl = 100; // never dies\n  if (SEGMENT.check2) {\n    if ((SEGMENT.call % 3) == 0) // move slowly\n      PartSys->particleMoveUpdate(*attractor, attractorFlags, &sourcesettings); // move the attractor\n  }\n  else {\n    attractor->x = PartSys->maxX >> 1; // set to center\n    attractor->y = PartSys->maxY >> 1;\n  }\n  if (SEGMENT.call == 0) {\n    attractor->vx = PartSys->sources[0].source.vy; // set to spray movemement but reverse x and y\n    attractor->vy = PartSys->sources[0].source.vx;\n  }\n\n  if (SEGMENT.custom2 > 0) // collisions enabled\n    PartSys->enableParticleCollisions(true, map(SEGMENT.custom2, 1, 255, 120, 255)); // enable collisions and set particle collision hardness\n  else\n    PartSys->enableParticleCollisions(false);\n\n  if (SEGMENT.call % 5 == 0)\n    PartSys->sources[0].source.hue++;\n\n  SEGENV.aux0 += 256; // emitting angle, one full turn in 255 frames (0xFFFF is 360°)\n  if (SEGMENT.call % 2 == 0) // alternate direction of emit\n    PartSys->angleEmit(PartSys->sources[0], SEGENV.aux0, 12);\n  else\n    PartSys->angleEmit(PartSys->sources[0], SEGENV.aux0 + 0x7FFF, 12); // emit at 180° as well\n  // apply force\n  uint32_t strength = SEGMENT.speed;\n  um_data_t *um_data;\n  if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // AR active, do not use simulated data\n    uint32_t volumeSmth = (uint32_t)(*(float*) um_data->u_data[0]); // 0-255\n    strength = (SEGMENT.speed * volumeSmth) >> 8;\n  }\n  for (uint32_t i = 0; i < PartSys->usedParticles; i++) {\n    PartSys->pointAttractor(i, *attractor, strength, SEGMENT.check3);\n  }\n\n\n  if (SEGMENT.call % (33 - SEGMENT.custom3) == 0)\n    PartSys->applyFriction(2);\n  PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags, &sourcesettings); // move the source\n  PartSys->update(); // update and render\n}\n//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\";\nstatic 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\";\n\n/*\n  Particle Spray, just a particle spray with many parameters\n  Uses palette for particle color\n  by DedeHai (Damian Schneider)\n*/\nvoid mode_particlespray(void) {\n  ParticleSystem2D *PartSys = nullptr;\n  const uint8_t hardness = 200; // collision hardness is fixed\n\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem2D(PartSys, 1)) // init, no additional data needed\n      FX_FALLBACK_STATIC; // allocation failed or not 2D\n    PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting)\n    PartSys->setBounceY(true);\n    PartSys->setMotionBlur(200); // anable motion blur\n    PartSys->setSmearBlur(10); // anable motion blur\n    PartSys->sources[0].source.hue = hw_random16();\n    PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide (if enabled)\n    PartSys->sources[0].var = 3;\n  }\n  else\n    PartSys = reinterpret_cast<ParticleSystem2D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  // Particle System settings\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  PartSys->setBounceX(!SEGMENT.check2);\n  PartSys->setWrapX(SEGMENT.check2);\n  PartSys->setWallHardness(hardness);\n  PartSys->setGravity(8 * SEGMENT.check1); // enable gravity if checked (8 is default strength)\n  //numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays\n\n  if (SEGMENT.check3) // collisions enabled\n    PartSys->enableParticleCollisions(true, hardness); // enable collisions and set particle collision hardness\n  else\n    PartSys->enableParticleCollisions(false);\n\n  //position according to sliders\n  PartSys->sources[0].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX);\n  PartSys->sources[0].source.y = map(SEGMENT.custom2, 0, 255, 0, PartSys->maxY);\n  uint16_t angle = (256 - (((int32_t)SEGMENT.custom3 + 1) << 3)) << 8;\n\n  um_data_t *um_data;\n  if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // get AR data, do not use simulated data\n    uint32_t volumeSmth  = (uint8_t)(*(float*)   um_data->u_data[0]); //0 to 255\n    uint32_t volumeRaw    = *(int16_t*)um_data->u_data[1]; //0 to 255\n    PartSys->sources[0].minLife = 30;\n\n    if (SEGMENT.call % 20 == 0 || SEGMENT.call % (11 - volumeSmth / 25) == 0) { // defines interval of particle emit\n      PartSys->sources[0].maxLife = (volumeSmth >> 1) + (SEGMENT.intensity >> 1); // lifetime in frames\n      PartSys->sources[0].var = 1 + ((volumeRaw * SEGMENT.speed)  >> 12);\n      uint32_t emitspeed = (SEGMENT.speed >> 2) + (volumeRaw >> 3);\n      PartSys->sources[0].source.hue += volumeSmth/30;\n      PartSys->angleEmit(PartSys->sources[0], angle, emitspeed);\n    }\n  }\n  else { //no AR data, fall back to normal mode\n    // change source properties\n    if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) { // every nth frame, cycle color and emit particles\n      PartSys->sources[0].maxLife = 300 + SEGMENT.intensity; // lifetime in frames\n      PartSys->sources[0].minLife = 150 + SEGMENT.intensity;\n      PartSys->sources[0].source.hue++; // = hw_random16(); //change hue of spray source\n      PartSys->angleEmit(PartSys->sources[0], angle, SEGMENT.speed >> 2);\n    }\n  }\n\n  PartSys->update(); // update and render\n}\nstatic 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\";\n\n\n/*\n  Particle base Graphical Equalizer\n  Uses palette for particle color\n  by DedeHai (Damian Schneider)\n*/\nvoid mode_particleGEQ(void) {\n  ParticleSystem2D *PartSys = nullptr;\n\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem2D(PartSys, 1))\n      FX_FALLBACK_STATIC; // allocation failed or not 2D\n    PartSys->setKillOutOfBounds(true);\n    PartSys->setUsedParticles(170); // use 2/3 of available particles\n  }\n  else\n    PartSys = reinterpret_cast<ParticleSystem2D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  uint32_t i;\n  // set particle system properties\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  PartSys->setWrapX(SEGMENT.check1);\n  PartSys->setBounceX(SEGMENT.check2);\n  PartSys->setBounceY(SEGMENT.check3);\n  //PartSys->enableParticleCollisions(false);\n  PartSys->setWallHardness(SEGMENT.custom2);\n  PartSys->setGravity(SEGMENT.custom3 << 2); // set gravity strength\n\n  um_data_t *um_data = getAudioData();\n  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\n\n  //map the bands into 16 positions on x axis, emit some particles according to frequency loudness\n  i = 0;\n  uint32_t binwidth = (PartSys->maxX + 1)>>4; //emit poisition variation for one bin (+/-) is equal to width/16 (for 16 bins)\n  uint32_t threshold = 300 - SEGMENT.intensity;\n  uint32_t emitparticles = 0;\n\n  for (uint32_t bin = 0; bin < 16; bin++) {\n    uint32_t xposition = binwidth*bin + (binwidth>>1); // emit position according to frequency band\n    uint8_t emitspeed = ((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 9; // emit speed according to loudness of band (127 max!)\n    emitparticles = 0;\n\n    if (fftResult[bin] > threshold) {\n      emitparticles = 1;// + (fftResult[bin]>>6);\n    }\n    else if (fftResult[bin] > 0) { // band has low volue\n      uint32_t restvolume = ((threshold - fftResult[bin])>>2) + 2;\n      if (hw_random16() % restvolume == 0)\n        emitparticles = 1;\n    }\n\n    while (i < PartSys->usedParticles && emitparticles > 0) { // emit particles if there are any left, low frequencies take priority\n      if (PartSys->particles[i].ttl == 0) { // find a dead particle\n        //set particle properties TODO: could also use the spray...\n        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\n        PartSys->particles[i].x = xposition + hw_random16(binwidth) - (binwidth>>1); // position randomly, deviating half a bin width\n        PartSys->particles[i].y = 0; // start at the bottom\n        PartSys->particles[i].vx = hw_random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation: +/- custom1/4\n        PartSys->particles[i].vy = emitspeed;\n        PartSys->particles[i].hue = (bin<<4) + hw_random16(17) - 8; // color from palette according to bin\n        emitparticles--;\n      }\n      i++;\n    }\n  }\n\n  PartSys->update(); // update and render\n}\n\nstatic 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\";\n\n/*\n  Particle rotating GEQ\n  Particles sprayed from center with rotating spray\n  Uses palette for particle color\n  by DedeHai (Damian Schneider)\n*/\n#define NUMBEROFSOURCES 16\nvoid mode_particlecenterGEQ(void) {\n  ParticleSystem2D *PartSys = nullptr;\n  uint8_t numSprays;\n  uint32_t i;\n\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES))  // init, request 16 sources\n      FX_FALLBACK_STATIC; // allocation failed or not 2D\n\n    numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES);\n    for (i = 0; i < numSprays; i++) {\n      PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center\n      PartSys->sources[i].source.y = (PartSys->maxY + 1) >> 1; // center\n      PartSys->sources[i].source.hue = i * 16; // even color distribution\n      PartSys->sources[i].maxLife = 400;\n      PartSys->sources[i].minLife = 200;\n    }\n    PartSys->setKillOutOfBounds(true);\n  }\n  else\n    PartSys = reinterpret_cast<ParticleSystem2D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES);\n\n  um_data_t *um_data = getAudioData();\n  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\n  uint32_t threshold = 300 - SEGMENT.intensity;\n\n  if (SEGMENT.check2)\n    SEGENV.aux0 += SEGMENT.custom1 << 2;\n  else\n    SEGENV.aux0 -= SEGMENT.custom1 << 2;\n\n  uint16_t angleoffset = (uint16_t)0xFFFF / (uint16_t)numSprays;\n  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.\n  for (i = 0; i < numSprays; i++) {\n    if (SEGMENT.call % (32 - (SEGMENT.custom2 >> 3)) == 0 && SEGMENT.custom2 > 0)\n      PartSys->sources[j].source.hue += 1 + (SEGMENT.custom2 >> 4);\n\n    PartSys->sources[j].var = SEGMENT.custom3 >> 2;\n    int8_t emitspeed = 5 + (((uint32_t)fftResult[j] * ((uint32_t)SEGMENT.speed + 20)) >> 10); // emit speed according to loudness of band\n    uint16_t emitangle = j * angleoffset + SEGENV.aux0;\n\n    uint32_t emitparticles = 0;\n    if (fftResult[j] > threshold)\n      emitparticles = 1;\n    else if (fftResult[j] > 0) { // band has low value\n      uint32_t restvolume = ((threshold - fftResult[j]) >> 2) + 2;\n      if (hw_random16() % restvolume == 0)\n        emitparticles = 1;\n    }\n    if (emitparticles)\n      PartSys->angleEmit(PartSys->sources[j], emitangle, emitspeed);\n\n    j = (j + 1) % numSprays;\n  }\n  PartSys->update(); // update and render\n}\nstatic 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\";\n\n/*\n  Particle replacement of Ghost Rider by DedeHai (Damian Schneider), original FX by stepko adapted by Blaz Kristan (AKA blazoncek)\n*/\n#define MAXANGLESTEP 2200 //32767 means 180°\nvoid mode_particleghostrider(void) {\n  ParticleSystem2D *PartSys = nullptr;\n  PSsettings2D ghostsettings;\n  ghostsettings.asByte = 0b0000011; //enable wrapX and wrapY\n\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem2D(PartSys, 1)) // init, no additional data needed\n      FX_FALLBACK_STATIC; // allocation failed or not 2D\n    PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting)\n    PartSys->sources[0].maxLife = 260; // lifetime in frames\n    PartSys->sources[0].minLife = 250;\n    PartSys->sources[0].source.x = hw_random16(PartSys->maxX);\n    PartSys->sources[0].source.y = hw_random16(PartSys->maxY);\n    SEGENV.step = hw_random16(MAXANGLESTEP) - (MAXANGLESTEP>>1); // angle increment\n  }\n  else {\n    PartSys = reinterpret_cast<ParticleSystem2D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n  }\n\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  if (SEGMENT.intensity > 0) { // spiraling\n    if (SEGENV.aux1) {\n      SEGENV.step += SEGMENT.intensity>>3;\n      if ((int32_t)SEGENV.step > MAXANGLESTEP)\n        SEGENV.aux1 = 0;\n    }\n    else {\n      SEGENV.step -= SEGMENT.intensity>>3;\n      if ((int32_t)SEGENV.step < -MAXANGLESTEP)\n        SEGENV.aux1 = 1;\n    }\n  }\n  // Particle System settings\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  PartSys->setMotionBlur(SEGMENT.custom1);\n  PartSys->sources[0].var = SEGMENT.custom3 >> 1;\n\n  // color by age (PS 'color by age' always starts with hue = 255, don't want that here)\n  if (SEGMENT.check1) {\n    for (uint32_t i = 0; i < PartSys->usedParticles; i++) {\n      PartSys->particles[i].hue = PartSys->sources[0].source.hue + (PartSys->particles[i].ttl<<2);\n    }\n  }\n\n  // enable/disable walls\n  ghostsettings.bounceX = SEGMENT.check2;\n  ghostsettings.bounceY = SEGMENT.check2;\n\n  SEGENV.aux0 += (int32_t)SEGENV.step; // step is angle increment\n  uint16_t emitangle = SEGENV.aux0 + 32767; // +180°\n  int32_t speed = map(SEGMENT.speed, 0, 255, 12, 64);\n  PartSys->sources[0].source.vx = ((int32_t)cos16_t(SEGENV.aux0) * speed) / (int32_t)32767;\n  PartSys->sources[0].source.vy = ((int32_t)sin16_t(SEGENV.aux0) * speed) / (int32_t)32767;\n  PartSys->sources[0].source.ttl = 500; // source never dies (note: setting 'perpetual' is not needed if replenished each frame)\n  PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags, &ghostsettings);\n  // set head (steal one of the particles)\n  PartSys->particles[PartSys->usedParticles-1].x = PartSys->sources[0].source.x;\n  PartSys->particles[PartSys->usedParticles-1].y = PartSys->sources[0].source.y;\n  PartSys->particles[PartSys->usedParticles-1].ttl = 255;\n  PartSys->particles[PartSys->usedParticles-1].sat = 0; //white\n  // emit two particles\n  PartSys->angleEmit(PartSys->sources[0], emitangle, speed);\n  PartSys->angleEmit(PartSys->sources[0], emitangle, speed);\n  if (SEGMENT.call % (11 - (SEGMENT.custom2 / 25)) == 0) { // every nth frame, cycle color and emit particles\n    PartSys->sources[0].source.hue++;\n  }\n  if (SEGMENT.custom2 > 190) //fast color change\n    PartSys->sources[0].source.hue += (SEGMENT.custom2 - 190) >> 2;\n\n  PartSys->update(); // update and render\n}\nstatic 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\";\n\n/*\n  PS Blobs: large particles bouncing around, changing size and form\n  Uses palette for particle color\n  by DedeHai (Damian Schneider)\n*/\nvoid mode_particleblobs(void) {\n  ParticleSystem2D *PartSys = nullptr;\n\n  if (SEGMENT.call == 0) {\n    if (!initParticleSystem2D(PartSys, 0, 0, true, true)) //init, no additional bytes, advanced size & size control\n      FX_FALLBACK_STATIC; // allocation failed or not 2D\n    PartSys->setBounceX(true);\n    PartSys->setBounceY(true);\n    PartSys->setWallHardness(255);\n    PartSys->setWallRoughness(255);\n    PartSys->setCollisionHardness(255);\n    PartSys->perParticleSize = true; // enable per particle size control\n  }\n  else\n    PartSys = reinterpret_cast<ParticleSystem2D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 128)); // minimum 10%, maximum 50% of available particles (note: PS ensures at least 1)\n  PartSys->enableParticleCollisions(SEGMENT.check2);\n\n  for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // update particles\n    if (SEGENV.aux0 != SEGMENT.speed || PartSys->particles[i].ttl == 0) { // speed changed or dead\n      PartSys->particles[i].vx = (int8_t)hw_random16(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); // +/- speed/4\n      PartSys->particles[i].vy = (int8_t)hw_random16(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2);\n    }\n    if (SEGENV.aux1 != SEGMENT.custom1 || PartSys->particles[i].ttl == 0) // size changed or dead\n      PartSys->advPartSize[i].maxsize = 60 + (SEGMENT.custom1 >> 1) + hw_random16((SEGMENT.custom1 >> 2)); // set each particle to slightly randomized size\n\n    //PartSys->particles[i].perpetual = SEGMENT.check2; //infinite life if set\n    if (PartSys->particles[i].ttl == 0) { // find dead particle, renitialize\n      PartSys->particles[i].ttl = 300 + hw_random16(((uint16_t)SEGMENT.custom2 << 3) + 100);\n      PartSys->particles[i].x = hw_random(PartSys->maxX);\n      PartSys->particles[i].y = hw_random16(PartSys->maxY);\n      PartSys->particles[i].hue = hw_random16(); // set random color\n      PartSys->particleFlags[i].collide = true; // enable collision for particle\n      PartSys->advPartProps[i].size = 0; // start out small\n      PartSys->advPartSize[i].asymmetry = hw_random16(220);\n      PartSys->advPartSize[i].asymdir = hw_random16(255);\n      // set advanced size control properties\n      PartSys->advPartSize[i].grow = true;\n      PartSys->advPartSize[i].growspeed = 1 + hw_random16(9);\n      PartSys->advPartSize[i].shrinkspeed = 1 + hw_random16(9);\n      PartSys->advPartSize[i].wobblespeed = 1 + hw_random16(3);\n    }\n    //PartSys->advPartSize[i].asymmetry++;\n    PartSys->advPartSize[i].pulsate = SEGMENT.check3;\n    PartSys->advPartSize[i].wobble = SEGMENT.check1;\n  }\n  SEGENV.aux0 = SEGMENT.speed; //write state back\n  SEGENV.aux1 = SEGMENT.custom1;\n\n  um_data_t *um_data;\n  if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // get AR data if available, do not use simulated data\n    uint8_t volumeSmth = (uint8_t)(*(float*)um_data->u_data[0]);\n    for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // update particles\n      if (SEGMENT.check3) //pulsate selected\n        PartSys->advPartProps[i].size = volumeSmth;\n    }\n  }\n\n  PartSys->setMotionBlur(((SEGMENT.custom3) << 3) + 7);\n  PartSys->update(); // update and render\n}\nstatic 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\";\n\n/*\n  Particle Galaxy, particles spiral like in a galaxy\n  Uses palette for particle color\n  by DedeHai (Damian Schneider)\n*/\nvoid mode_particlegalaxy(void) {\n  ParticleSystem2D *PartSys = nullptr;\n  PSsettings2D sourcesettings;\n  sourcesettings.asByte = 0b00001100; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not)\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem2D(PartSys, 1, 0, true)) // init using 1 source and advanced particle settings\n      FX_FALLBACK_STATIC; // allocation failed or not 2D\n    PartSys->sources[0].source.vx = -4; // will collide with wall and get random bounce direction\n    PartSys->sources[0].source.x =  PartSys->maxX >> 1; // start in the center\n    PartSys->sources[0].source.y =  PartSys->maxY >> 1;\n    PartSys->sources[0].sourceFlags.perpetual = true; //source does not age\n    PartSys->sources[0].maxLife = 4000; // lifetime in frames\n    PartSys->sources[0].minLife = 800;\n    PartSys->sources[0].source.hue = hw_random16(); // start with random color\n    PartSys->setWallHardness(255);  //bounce forever\n    PartSys->setWallRoughness(200); //randomize wall bounce\n  }\n  else {\n    PartSys = reinterpret_cast<ParticleSystem2D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n  }\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n  // Particle System settings\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  uint8_t particlesize = SEGMENT.custom1;\n  PartSys->setParticleSize(particlesize); // set size globally\n  PartSys->setMotionBlur(250 * SEGMENT.check3); // adds trails to single/quad pixel particles, no effect if size > 1\n\n  if ((SEGMENT.call % ((33 - SEGMENT.custom3) >> 1)) == 0) // change hue of emitted particles\n    PartSys->sources[0].source.hue+=2;\n\n  if (hw_random8() < (10 + (SEGMENT.intensity >> 1))) // 5%-55% chance to emit a particle in this frame\n    PartSys->sprayEmit(PartSys->sources[0]);\n\n  if ((SEGMENT.call & 0x3) == 0) // every 4th frame, move the emitter\n    PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags, &sourcesettings);\n\n  // move alive particles in a spiral motion (or almost straight in fast starfield mode)\n  int32_t centerx = PartSys->maxX >> 1; // center of matrix in subpixel coordinates\n  int32_t centery = PartSys->maxY >> 1;\n  if (SEGMENT.check2) { // starfield mode\n    PartSys->setKillOutOfBounds(true);\n    PartSys->sources[0].var = 7; // emiting variation\n    PartSys->sources[0].source.x =  centerx; // set emitter to center\n    PartSys->sources[0].source.y =  centery;\n  }\n  else {\n    PartSys->setKillOutOfBounds(false);\n    PartSys->sources[0].var = 1; // emiting variation\n  }\n  for (uint32_t i = 0; i < PartSys->usedParticles; i++) { //check all particles\n    if (PartSys->particles[i].ttl == 0) continue; //skip dead particles\n    // (dx/dy): vector pointing from particle to center\n    int32_t dx = centerx - PartSys->particles[i].x;\n    int32_t dy = centery - PartSys->particles[i].y;\n    //speed towards center:\n    int32_t distance = sqrt32_bw(dx * dx + dy * dy); // absolute distance to center\n    if (distance < 20) distance = 20; // avoid division by zero, keep a minimum\n    int32_t speedfactor;\n    if (SEGMENT.check2) { // starfield mode\n      speedfactor = 1 + (1 + (SEGMENT.speed >> 1)) * distance; // speed increases towards edge\n      //apply velocity\n      PartSys->particles[i].x += (-speedfactor * dx) / 400000 - (dy >> 6);\n      PartSys->particles[i].y += (-speedfactor * dy) / 400000 + (dx >> 6);\n    }\n    else {\n      speedfactor = 2 + (((50 + SEGMENT.speed) << 6) / distance); // speed increases towards center\n      // rotate clockwise\n      int32_t tempVx = (-speedfactor * dy); // speed is orthogonal to center vector\n      int32_t tempVy =  (speedfactor * dx);\n      //add speed towards center to make particles spiral in\n      int vxc = (dx << 9) / (distance - 19); // subtract value from distance to make the pull-in force a bit stronger (helps on faster speeds)\n      int vyc = (dy << 9) / (distance - 19);\n      //apply velocity\n      PartSys->particles[i].x += (tempVx + vxc) / 1024; // note: cannot use bit shift as that causes asymmetric rounding\n      PartSys->particles[i].y += (tempVy + vyc) / 1024;\n\n      if (distance < 128) { // close to center\n        if (PartSys->particles[i].ttl > 3)\n          PartSys->particles[i].ttl -= 4; //age fast\n        PartSys->particles[i].sat = distance << 1; // turn white towards center\n      }\n    }\n    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\n      PartSys->particles[i].hue = PartSys->particles[i].ttl >> 2;\n    else if(SEGMENT.custom3 == 0) // color by distance\n      PartSys->particles[i].hue = map(distance, 20, (PartSys->maxX + PartSys->maxY) >> 2, 0, 180); // color by distance to center\n  }\n\n  PartSys->update(); // update and render\n}\nstatic const char _data_FX_MODE_PARTICLEGALAXY[] PROGMEM = \"PS Galaxy@!,!,Size,,Color,,Starfield,Trace;;!;2;pal=59,sx=80,c1=1,c3=4\";\n\n#endif //WLED_DISABLE_PARTICLESYSTEM2D\n#endif // WLED_DISABLE_2D\n\n///////////////////////////\n// 1D Particle System FX //\n///////////////////////////\n\n#ifndef WLED_DISABLE_PARTICLESYSTEM1D\n/*\n  Particle version of Drip and Rain\n  Uses palette for particle color\n  by DedeHai (Damian Schneider)\n*/\nvoid mode_particleDrip(void) {\n  ParticleSystem1D *PartSys = nullptr;\n  //uint8_t numSprays;\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem1D(PartSys, 4)) // init\n      FX_FALLBACK_STATIC; // allocation failed or single pixel\n    PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting)\n    PartSys->sources[0].source.hue = hw_random16();\n    SEGENV.aux1 = 0xFFFF; // invalidate\n  }\n  else\n    PartSys = reinterpret_cast<ParticleSystem1D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  // Particle System settings\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  PartSys->setBounce(true);\n  PartSys->setWallHardness(50);\n\n  PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur\n  PartSys->setGravity(SEGMENT.custom3 >> 1); // set gravity (8 is default strength)\n  PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering\n\n  if (SEGMENT.check2) { //collisions enabled\n    PartSys->enableParticleCollisions(true); //enable, full hardness\n  }\n  else\n    PartSys->enableParticleCollisions(false);\n\n  PartSys->sources[0].sourceFlags.collide = false; //drops do not collide\n\n  if (SEGMENT.check1) { //rain mode, emit at random position, short life (3-8 seconds at 50fps)\n    if (SEGMENT.custom1 == 0) //splash disabled, do not bounce raindrops\n      PartSys->setBounce(false);\n    PartSys->sources[0].var = 5;\n    PartSys->sources[0].v = -(8 + (SEGMENT.speed >> 2)); //speed + var must be < 128, inverted speed (=down)\n    // lifetime in frames\n    PartSys->sources[0].minLife = 30;\n    PartSys->sources[0].maxLife = 200;\n    PartSys->sources[0].source.x = hw_random(PartSys->maxX); //random emit position\n  }\n  else { //drip\n    PartSys->sources[0].var = 0;\n    PartSys->sources[0].v = -(SEGMENT.speed >> 1); //speed + var must be < 128, inverted speed (=down)\n    PartSys->sources[0].minLife = 3000;\n    PartSys->sources[0].maxLife = 3000;\n    PartSys->sources[0].source.x = PartSys->maxX - PS_P_RADIUS_1D;\n  }\n\n  if (SEGENV.aux1 != SEGMENT.intensity) //slider changed\n    SEGENV.aux0 = 1; //must not be zero or \"% 0\" happens below which crashes on ESP32\n\n  SEGENV.aux1 = SEGMENT.intensity; // save state\n\n  // every nth frame emit a particle\n  if (SEGMENT.call % SEGENV.aux0 == 0) {\n    int32_t interval = 300 / ((SEGMENT.intensity) + 1);\n    SEGENV.aux0 = interval + hw_random(interval + 5);\n    // if (SEGMENT.check1) // rain mode\n    //   PartSys->sources[0].source.hue = 0;\n    // else\n    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.\n    PartSys->sprayEmit(PartSys->sources[0]);\n  }\n\n  for (uint32_t i = 0; i < PartSys->usedParticles; i++) { //check all particles\n    if (PartSys->particles[i].ttl && PartSys->particleFlags[i].collide == false) { // use collision flag to identify splash particles\n      if (SEGMENT.custom1 > 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D << 1)) { //splash enabled and reached bottom\n        PartSys->particles[i].ttl = 0; //kill origin particle\n        PartSys->sources[0].maxLife = 80;\n        PartSys->sources[0].minLife = 20;\n        PartSys->sources[0].var = 10 + (SEGMENT.custom1 >> 3);\n        PartSys->sources[0].v = 0;\n        PartSys->sources[0].source.hue = PartSys->particles[i].hue;\n        PartSys->sources[0].source.x = PS_P_RADIUS_1D;\n        PartSys->sources[0].sourceFlags.collide = true; //splashes do collide if enabled\n        for (int j = 0; j < 2 + (SEGMENT.custom1 >> 2); j++) {\n          PartSys->sprayEmit(PartSys->sources[0]);\n        }\n      }\n    }\n\n    if (SEGMENT.check1) { //rain mode, fade hue to max\n      if (PartSys->particles[i].hue < 245)\n        PartSys->particles[i].hue += 8;\n    }\n    //increase speed on high settings by calling the move function twice note: this can lead to missed collisions\n    if (SEGMENT.speed > 200)\n      PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]);\n  }\n\n  PartSys->update(); // update and render\n}\nstatic 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\";\n\n\n/*\n  Particle Version of \"Bouncing Balls by Aircoookie\"\n  Also does rolling balls and juggle (and popcorn)\n  Uses palette for particle color\n  by DedeHai (Damian Schneider)\n*/\nvoid mode_particlePinball(void) {\n  ParticleSystem1D *PartSys = nullptr;\n\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init\n      FX_FALLBACK_STATIC; // allocation failed or is single pixel\n    PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide (if enabled)\n    PartSys->sources[0].source.x = -1000; // shoot up from below\n    //PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting)\n    SEGENV.aux0 = 1;\n    SEGENV.aux1 = 5000; // set settings out of range to ensure uptate on first call\n  }\n  else\n    PartSys = reinterpret_cast<ParticleSystem1D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  // Particle System settings\n  //uint32_t hardness = 240 + (SEGMENT.custom1>>4);\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  PartSys->setGravity(map(SEGMENT.custom3, 0 , 31, 0 , 8)); // set gravity (8 is default strength)\n  PartSys->setBounce(SEGMENT.custom3); // disables bounce if no gravity is used\n  PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur\n  PartSys->enableParticleCollisions(SEGMENT.check1, 255); // enable collisions and set particle collision to high hardness\n  PartSys->setColorByPosition(SEGMENT.check3);\n  uint32_t maxParticles = max(20, SEGMENT.intensity / (1 + (SEGMENT.check2 * (SEGMENT.custom1 >> 5)))); // max particles depends on intensity and rolling balls mode + size\n  if (SEGMENT.custom1 < 255)\n    PartSys->setParticleSize(SEGMENT.custom1); // set size globally\n  else {\n    PartSys->perParticleSize = true; // use random individual particle size (see below)\n    maxParticles *= 2; // use more particles if individual s  ize is used as there is more space\n  }\n  PartSys->setUsedParticles(maxParticles); // reduce if using larger size and rolling balls mode\n\n  bool updateballs = false;\n  if (SEGENV.aux1 != SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1 + PartSys->usedParticles) { // user settings change or more particles are available\n    SEGENV.step = SEGMENT.call; // reset delay\n    updateballs = true;\n    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)\n    PartSys->sources[0].minLife = PartSys->sources[0].maxLife >> 1;\n  }\n\n  if (SEGMENT.check2) { // rolling balls\n    PartSys->setGravity(0);\n    PartSys->setWallHardness(255);\n    int speedsum = 0;\n    for (uint32_t i = 0; i < PartSys->usedParticles; i++) {\n        PartSys->particles[i].ttl = 500; // keep particles alive\n      if (updateballs) { // speed changed or particle is dead, set particle properties\n        PartSys->particleFlags[i].collide = true;\n        if (PartSys->particles[i].x == 0) { // still at initial position\n          PartSys->particles[i].x = hw_random16(PartSys->maxX); // random initial position for all particles\n          PartSys->particles[i].vx = (hw_random16() & 0x01) ? 1 : -1; // random initial direction\n        }\n        PartSys->particles[i].hue = hw_random8(); //set ball colors to random\n        PartSys->advPartProps[i].sat = 255;\n        PartSys->advPartProps[i].size = hw_random8(); // set ball size for individual size mode\n      }\n      speedsum += abs(PartSys->particles[i].vx);\n    }\n    int32_t avgSpeed = speedsum / PartSys->usedParticles;\n    int32_t setSpeed = 2 + (SEGMENT.speed >> 2);\n    if (avgSpeed < setSpeed) { // if balls are slow, speed up some of them at random to keep the animation going\n      for (int i = 0; i < setSpeed - avgSpeed; i++) {\n        int idx = hw_random16(PartSys->usedParticles);\n        if (abs(PartSys->particles[idx].vx) < PS_P_MAXSPEED)\n          PartSys->particles[idx].vx += PartSys->particles[idx].vx >= 0 ? 1 : -1; // add 1, keep direction\n      }\n    }\n    else if (avgSpeed > setSpeed + 8) // if avg speed is too high, apply friction to slow them down\n      PartSys->applyFriction(1);\n  }\n  else { // bouncing balls\n    PartSys->setWallHardness(220);\n    PartSys->sources[0].var = SEGMENT.speed >> 3;\n    int32_t newspeed = 2 + (SEGMENT.speed >> 1) - (SEGMENT.speed >> 3);\n    PartSys->sources[0].v = newspeed;\n    //check for balls that are 'laying on the ground' and remove them\n    for (uint32_t i = 0; i < PartSys->usedParticles; i++) {\n      if (PartSys->particles[i].ttl < 50) PartSys->particles[i].ttl = 0; // no dark particles\n      else if (PartSys->particles[i].vx == 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D + SEGMENT.custom1))\n        PartSys->particles[i].ttl -= 50; // age fast\n\n      if (updateballs) {\n        if (SEGMENT.custom3 == 0) // gravity off, update speed\n          PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction\n      }\n    }\n\n    // every nth frame emit a ball\n    if (SEGMENT.call > SEGENV.step) {\n      int interval = 260 - ((int)SEGMENT.intensity);\n      SEGENV.step += interval + hw_random16(interval);\n      PartSys->sources[0].source.hue = hw_random16(); //set ball color\n      PartSys->sources[0].sat = 255;\n      PartSys->sources[0].size = hw_random8(); //set ball size\n      PartSys->sprayEmit(PartSys->sources[0]);\n    }\n  }\n  SEGENV.aux1 = SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1 + PartSys->usedParticles;\n  //for (uint32_t i = 0; i < PartSys->usedParticles; i++) {\n  //  PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]); // double the speed  note: this leads to bad collisions, also need to run collision detection before\n  //}\n\n  PartSys->update(); // update and render\n}\nstatic 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\";\n\n/*\n  Particle Replacement for original Dancing Shadows:\n  \"Spotlights moving back and forth that cast dancing shadows.\n  Shine this through tree branches/leaves or other close-up objects that cast\n  interesting shadows onto a ceiling or tarp.\n  By Steve Pomeroy @xxv\"\n  Uses palette for particle color\n  by DedeHai (Damian Schneider)\n*/\nvoid mode_particleDancingShadows(void) {\n  ParticleSystem1D *PartSys = nullptr;\n\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem1D(PartSys, 1)) // init, one source\n      FX_FALLBACK_STATIC; // allocation failed or is single pixel\n    PartSys->sources[0].maxLife = 1000; //set long life (kill out of bounds is done in custom way)\n    PartSys->sources[0].minLife = PartSys->sources[0].maxLife;\n  }\n  else {\n    PartSys = reinterpret_cast<ParticleSystem1D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n  }\n\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  // Particle System settings\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  PartSys->setMotionBlur(SEGMENT.custom1);\n  if (SEGMENT.check1)\n    PartSys->setSmearBlur(120); // enable smear blur\n  else\n    PartSys->setSmearBlur(0); // disable smear blur\n  PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering\n  PartSys->setColorByPosition(SEGMENT.check2); // color fixed by position\n  PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, 255)); // set percentage of particles to use\n\n  uint32_t deadparticles = 0;\n  //kill out of bounds and moving away plus change color\n  for (uint32_t i = 0; i < PartSys->usedParticles; i++) {\n    if (((SEGMENT.call & 0x07) == 0) && PartSys->particleFlags[i].outofbounds) { //check if out of bounds particle move away from strip, only update every 8th frame\n      if ((int32_t)PartSys->particles[i].vx * PartSys->particles[i].x > 0) PartSys->particles[i].ttl = 0; //particle is moving away, kill it\n    }\n    PartSys->particleFlags[i].perpetual = true; //particles do not age\n    if (SEGMENT.call % (32 / (1 + (SEGMENT.custom2 >> 3))) == 0)\n       PartSys->particles[i].hue += 2 + (SEGMENT.custom2 >> 5);\n    //note: updating speed on the fly is not accurately possible, since it is unknown which particles are assigned to which spot\n    if (SEGENV.aux0 != SEGMENT.speed) { //speed changed\n      //update all particle speed by setting them to current value\n       PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? SEGMENT.speed >> 3 : -SEGMENT.speed >> 3;\n    }\n    if (PartSys->particles[i].ttl == 0) deadparticles++; // count dead particles\n  }\n  SEGENV.aux0 = SEGMENT.speed;\n\n  //generate a spotlight: generates particles just outside of view\n  if (deadparticles > 5 && (SEGMENT.call & 0x03) == 0) {\n    //random color, random type\n    uint32_t type = hw_random16(SPOT_TYPES_COUNT);\n    int8_t speed = 2 + hw_random16(2 + (SEGMENT.speed >> 1)) + (SEGMENT.speed >> 4);\n    int32_t width = hw_random16(1, 10);\n    uint32_t ttl = 300; //ttl is particle brightness (below perpetual is set so it does not age, i.e. ttl stays at this value)\n    int32_t position;\n    //choose random start position, left and right from the segment\n    if (hw_random() & 0x01) {\n      position = PartSys->maxXpixel;\n      speed = -speed;\n    }\n    else\n      position = -width;\n\n    PartSys->sources[0].v = speed; //emitted particle speed\n    PartSys->sources[0].source.hue = hw_random8(); //random spotlight color\n    for (int32_t i = 0; i < width; i++) {\n      if (width > 1) {\n        switch (type) {\n          case SPOT_TYPE_SOLID:\n            //nothing to do\n            break;\n\n          case SPOT_TYPE_GRADIENT:\n            ttl = cubicwave8(map(i, 0, width - 1, 0, 255));\n            ttl = ttl*ttl >> 8; //make gradient more pronounced\n            break;\n\n          case SPOT_TYPE_2X_GRADIENT:\n            ttl = cubicwave8(2 * map(i, 0, width - 1, 0, 255));\n            ttl = ttl*ttl >> 8;\n            break;\n\n          case SPOT_TYPE_2X_DOT:\n            if (i > 0) position++; //skip one pixel\n            i++;\n            break;\n\n          case SPOT_TYPE_3X_DOT:\n            if (i > 0) position += 2; //skip two pixels\n            i+=2;\n            break;\n\n          case SPOT_TYPE_4X_DOT:\n            if (i > 0) position += 3; //skip three pixels\n            i+=3;\n            break;\n        }\n      }\n      //emit particle\n      //set the particle source position:\n      PartSys->sources[0].source.x = position * PS_P_RADIUS_1D;\n      uint32_t partidx = PartSys->sprayEmit(PartSys->sources[0]);\n      PartSys->particles[partidx].ttl = ttl;\n      position++; //do the next pixel\n    }\n  }\n\n  PartSys->update(); // update and render\n}\nstatic 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\";\n\n/*\n  Particle Fireworks 1D replacement\n  Uses palette for particle color\n  by DedeHai (Damian Schneider)\n*/\nvoid mode_particleFireworks1D(void) {\n  ParticleSystem1D *PartSys = nullptr;\n  uint8_t *forcecounter;\n\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem1D(PartSys, 4, 150, 4, true)) // init advanced particle system\n      FX_FALLBACK_STATIC; // allocation failed or is single pixel\n    PartSys->setKillOutOfBounds(true);\n    PartSys->sources[0].sourceFlags.custom1 = 1; // set rocket state to standby\n  }\n  else\n    PartSys = reinterpret_cast<ParticleSystem1D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  // Particle System settings\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  forcecounter = PartSys->PSdataEnd;\n  PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur\n  int32_t gravity = (1 + (SEGMENT.speed >> 3)); // gravity value used for rocket speed calculation\n  PartSys->setGravity(SEGMENT.speed ? gravity : 0); // set gravity\n  PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering (global size, disables per particle size)\n\n  if (PartSys->sources[0].sourceFlags.custom1 == 1) { // rocket is on standby\n    PartSys->sources[0].source.ttl--;\n    if (PartSys->sources[0].source.ttl == 0) { // time is up, relaunch\n\n      if (hw_random8() < SEGMENT.custom1) // randomly choose direction according to slider, fire at start of segment if true\n        SEGENV.aux0 = 1;\n      else\n        SEGENV.aux0 = 0;\n\n      PartSys->sources[0].sourceFlags.custom1 = 0; //flag used for rocket state\n      PartSys->sources[0].source.hue = hw_random16(); // different color for each launch\n      PartSys->sources[0].var = 10 * SEGMENT.check2; // emit variation, 0 if trail mode is off\n      PartSys->sources[0].v = -10 * SEGMENT.check2; // emit speed, 0 if trail mode is off\n      PartSys->sources[0].minLife = 180;\n      PartSys->sources[0].maxLife = SEGMENT.check2 ? 700 : 240; // exhaust particle life\n      PartSys->sources[0].source.x = SEGENV.aux0 * PartSys->maxX; // start from bottom or top\n      uint32_t speed = sqrt((gravity * ((PartSys->maxX >> 2) + hw_random16(PartSys->maxX >> 1))) >> 4); // set speed such that rocket explods in frame\n      PartSys->sources[0].source.vx = min(speed, (uint32_t)127);\n      PartSys->sources[0].source.ttl = 4000;\n      PartSys->sources[0].sat = 30; // low saturation exhaust\n      PartSys->sources[0].sourceFlags.reversegrav = false ; // normal gravity\n\n      if (SEGENV.aux0) { // inverted rockets launch from end\n        PartSys->sources[0].sourceFlags.reversegrav = true;\n        //PartSys->sources[0].source.x = PartSys->maxX; // start from top\n        PartSys->sources[0].source.vx = -PartSys->sources[0].source.vx; // revert direction\n        PartSys->sources[0].v = -PartSys->sources[0].v; // invert exhaust emit speed\n      }\n    }\n  }\n  else { // rocket is launched\n    int32_t rocketgravity = -gravity;\n    int32_t currentspeed = PartSys->sources[0].source.vx;\n    if (SEGENV.aux0) { // negative speed rocket\n      rocketgravity = -rocketgravity;\n      currentspeed = -currentspeed;\n    }\n    PartSys->applyForce(PartSys->sources[0].source, rocketgravity, forcecounter[0]);\n    PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags);\n    PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags); // increase rocket speed by calling the move function twice, also ages twice\n    uint32_t rocketheight = SEGENV.aux0 ? PartSys->maxX - PartSys->sources[0].source.x : PartSys->sources[0].source.x;\n\n    if (currentspeed < 0 && PartSys->sources[0].source.ttl > 50) // reached apogee\n      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\n\n    if (PartSys->sources[0].source.ttl < 2) { // explode\n      PartSys->sources[0].sourceFlags.custom1 = 1; // set standby state\n      PartSys->sources[0].var = 5 + ((((PartSys->maxX >> 1) + rocketheight) * (20 + (SEGMENT.intensity << 1))) / (PartSys->maxX << 2)); // set explosion particle speed\n      PartSys->sources[0].minLife = 1200;\n      PartSys->sources[0].maxLife = 2600;\n      PartSys->sources[0].source.ttl = 100 + hw_random16(64 - (SEGMENT.speed >> 2)); // standby time til next launch\n      PartSys->sources[0].sat = SEGMENT.custom3 < 16 ? 10 + (SEGMENT.custom3 << 4) : 255; //color saturation\n      PartSys->sources[0].size = SEGMENT.check3 ? hw_random16(SEGMENT.intensity) : 0; // random particle size in explosion\n      uint32_t explosionsize = 8 + (PartSys->maxXpixel >> 2) + (PartSys->sources[0].source.x >> (PS_P_RADIUS_SHIFT_1D - 1));\n      explosionsize += hw_random16((explosionsize * SEGMENT.intensity) >> 8);\n      PartSys->setColorByAge(false); // disable\n      PartSys->setColorByPosition(false); // disable\n      for (uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles\n        int idx = PartSys->sprayEmit(PartSys->sources[0]); // emit a particle\n        if(SEGMENT.custom3 > 23) {\n          if(SEGMENT.custom3 == 31) { // highest slider value\n            PartSys->setColorByAge(SEGMENT.check1); // color by age if colorful mode is enabled\n            PartSys->setColorByPosition(!SEGMENT.check1); // color by position otherwise\n          }\n          else { // if custom3 is set to high value (but not highest), set particle color by initial speed\n            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\n            PartSys->particles[idx].hue += PartSys->sources[0].source.hue; // add hue offset of the rocket (random starting color)\n          }\n        }\n        else {\n          if (SEGMENT.check1) // colorful mode\n            PartSys->sources[0].source.hue = hw_random16(); //random color for each particle\n        }\n      }\n    }\n  }\n  if ((SEGMENT.call & 0x01) == 0 && PartSys->sources[0].sourceFlags.custom1 == false) // every second frame and not in standby\n    PartSys->sprayEmit(PartSys->sources[0]); // emit exhaust particle\n\n  if ((SEGMENT.call & 0x03) == 0) // every fourth frame\n    PartSys->applyFriction(1); // apply friction to all particles\n\n  PartSys->update(); // update and render\n  \n  for (uint32_t i = 0; i < PartSys->usedParticles; i++) {\n    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\n    else PartSys->particles[i].ttl = 0;\n  }\n}\nstatic 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\";\n\n/*\n  Particle based Sparkle effect\n  Uses palette for particle color\n  by DedeHai (Damian Schneider)\n*/\nvoid mode_particleSparkler(void) {\n  ParticleSystem1D *PartSys = nullptr;\n  uint32_t numSparklers;\n  PSsettings1D sparklersettings;\n  sparklersettings.asByte = 0; // PS settings for sparkler (set below)\n\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem1D(PartSys, 16, 128 ,0, true)) // init, no additional data needed\n      FX_FALLBACK_STATIC; // allocation failed or is single pixel\n  } else\n    PartSys = reinterpret_cast<ParticleSystem1D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  // Particle System settings\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n\n  sparklersettings.wrap = !SEGMENT.check2;\n  sparklersettings.bounce = SEGMENT.check2; // note: bounce always takes priority over wrap\n\n  numSparklers = PartSys->numSources;\n  PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur/overlay\n  //PartSys->setSmearBlur(SEGMENT.custom2); // anable smearing blur\n  PartSys->setParticleSize( SEGMENT.check3 ? 60 : 0); // single pixel or large particle rendering\n\n  for (uint32_t i = 0; i < numSparklers; i++) {\n    PartSys->sources[i].source.hue = hw_random16();\n    PartSys->sources[i].var = 0; // sparks stationary\n    PartSys->sources[i].minLife = 150 + SEGMENT.intensity;\n    PartSys->sources[i].maxLife = 250 + (SEGMENT.intensity << 1);\n    int32_t speed = SEGMENT.speed >> 1;\n    if (SEGMENT.check1) // sparks move (slide option)\n      PartSys->sources[i].var = SEGMENT.intensity >> 3;\n    PartSys->sources[i].source.vx = PartSys->sources[i].source.vx > 0 ? speed : -speed; // update speed, do not change direction\n    PartSys->sources[i].source.ttl = 400; // replenish its life (setting it perpetual uses more code)\n    PartSys->sources[i].sat = SEGMENT.custom1; // color saturation\n    if (SEGMENT.speed == 255) // random position at highest speed setting\n      PartSys->sources[i].source.x = hw_random16(PartSys->maxX);\n    else\n      PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &sparklersettings); //move sparkler\n  }\n\n  numSparklers = min(1 + (SEGMENT.custom3 >> 1), (int)numSparklers);  // set used sparklers, 1 to 16\n\n  if (SEGENV.aux0 != SEGMENT.custom3) { //number of used sparklers changed, redistribute\n    for (uint32_t i = 1; i < numSparklers; i++) {\n      PartSys->sources[i].source.x = (PartSys->sources[0].source.x + (PartSys->maxX / numSparklers) * i ) % PartSys->maxX; //distribute evenly\n    }\n  }\n  SEGENV.aux0 = SEGMENT.custom3;\n\n  for (uint32_t i = 0; i < numSparklers; i++) {\n    if (hw_random()  % (((271 - SEGMENT.intensity) >> 4)) == 0)\n      PartSys->sprayEmit(PartSys->sources[i]); //emit a particle\n  }\n\n  PartSys->update(); // update and render\n\n  for (uint32_t i = 0; i < PartSys->usedParticles; i++) {\n    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\n    else PartSys->particles[i].ttl = 0;\n  }\n}\nstatic 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\";\n\n/*\n  Particle based Hourglass, particles falling at defined intervals\n  Uses palette for particle color\n  by DedeHai (Damian Schneider)\n*/\nvoid mode_particleHourglass(void) {\n  ParticleSystem1D *PartSys = nullptr;\n  constexpr int positionOffset = PS_P_RADIUS_1D / 2;; // resting position offset\n  bool* direction;\n  uint32_t* settingTracker;\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem1D(PartSys, 0, 255, 8, false)) // init\n      FX_FALLBACK_STATIC; // allocation failed or is single pixel\n    PartSys->setBounce(true);\n    PartSys->setWallHardness(100);\n  }\n  else\n    PartSys = reinterpret_cast<ParticleSystem1D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  // Particle System settings\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  settingTracker = reinterpret_cast<uint32_t *>(PartSys->PSdataEnd);  //assign data pointer\n  direction = reinterpret_cast<bool *>(PartSys->PSdataEnd + 4);  //assign data pointer\n  PartSys->setUsedParticles(1 + ((SEGMENT.intensity * 255) >> 8));\n  PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur\n  PartSys->setGravity(map(SEGMENT.custom3, 0, 31, 1, 30));\n  PartSys->enableParticleCollisions(true, 64); // hardness value (found by experimentation on different settings)\n\n  uint32_t colormode = SEGMENT.custom1 >> 5; // 0-7\n\n  if (SEGMENT.intensity != *settingTracker) { // initialize\n    *settingTracker = SEGMENT.intensity;\n    for (uint32_t i = 0; i < PartSys->usedParticles; i++) {\n      PartSys->particleFlags[i].reversegrav = true; // resting particles dont fall\n      *direction = 0; // down\n      SEGENV.aux1 = 1; // initialize below\n    }\n    SEGENV.aux0 = PartSys->usedParticles - 1; // initial state, start with highest number particle\n  }\n\n  // re-order particles in case heavy collisions flipped particles (highest number index particle is on the \"bottom\")\n  for (uint32_t i = 0; i < PartSys->usedParticles - 1; i++) {\n    if (PartSys->particles[i].x < PartSys->particles[i+1].x && PartSys->particleFlags[i].fixed == false && PartSys->particleFlags[i+1].fixed == false) {\n      std::swap(PartSys->particles[i].x, PartSys->particles[i+1].x);\n    }\n  }\n  // calculate target position depending on direction\n  auto calcTargetPos = [&](size_t i) {\n    return PartSys->particleFlags[i].reversegrav ?\n          PartSys->maxX - i * PS_P_RADIUS_1D - positionOffset\n        : (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionOffset;\n  };\n\n  for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // check if particle reached target position after falling\n    if (PartSys->particleFlags[i].fixed == false && abs(PartSys->particles[i].vx) < 5) {\n      int32_t targetposition = calcTargetPos(i);\n      bool belowtarget = PartSys->particleFlags[i].reversegrav ? (PartSys->particles[i].x > targetposition) : (PartSys->particles[i].x < targetposition);\n      bool closeToTarget = abs(targetposition - PartSys->particles[i].x) < PS_P_RADIUS_1D;\n      if (belowtarget || closeToTarget) { // overshot target or close to target and slow speed\n        PartSys->particles[i].x = targetposition; // set exact position\n        PartSys->particleFlags[i].fixed = true;   // pin particle\n      }\n    }\n    if (colormode == 7)\n      PartSys->setColorByPosition(true); // color fixed by position\n    else {\n      PartSys->setColorByPosition(false);\n      uint8_t basehue = ((SEGMENT.custom1 & 0x1F) << 3); // use 5 LSBs to select color\n      switch(colormode) {\n        case 0: PartSys->particles[i].hue = 120; break; // fixed at 120, if flip is activated, this can make red and green (use palette 34)\n        case 1: PartSys->particles[i].hue = basehue; break; // fixed selectable color\n        case 2: // 2 colors inverleaved (same code as 3)\n        case 3: PartSys->particles[i].hue = ((SEGMENT.custom1 & 0x1F) << 1) + (i % 3)*74; break; // 3 interleved colors\n        case 4: PartSys->particles[i].hue = basehue + (i * 255) / PartSys->usedParticles;  break; // gradient palette colors\n        case 5: PartSys->particles[i].hue = basehue + (i * 1024) / PartSys->usedParticles;  break; // multi gradient palette colors\n        case 6: PartSys->particles[i].hue = i + (strip.now >> 3);  break; // disco! moving color gradient\n        default: break; // use color by position\n      }\n    }\n    if (SEGMENT.check1 && !PartSys->particleFlags[i].reversegrav) // flip color when fallen\n      PartSys->particles[i].hue += 120;\n  }\n\n  if (SEGENV.aux1 == 1) { // last countdown call before dropping starts, reset all particles\n    for (uint32_t i = 0; i < PartSys->usedParticles; i++) {\n      PartSys->particleFlags[i].collide = true;\n      PartSys->particleFlags[i].perpetual = true;\n      PartSys->particles[i].ttl = 260;\n      PartSys->particles[i].x = calcTargetPos(i);\n      PartSys->particleFlags[i].fixed = true;\n    }\n  }\n\n  if (SEGENV.aux1 == 0) { // countdown passed, run\n    if (strip.now >= SEGENV.step) { // drop a particle\n      // set next drop time\n      if (SEGMENT.check3 && *direction) // fast reset\n        SEGENV.step = strip.now + 100; // drop one particle every 100ms\n      else // normal interval\n        SEGENV.step = strip.now + max(100, SEGMENT.speed * 100); // map speed slider from 0.1s to 25.5s\n      if (SEGENV.aux0 < PartSys->usedParticles) {\n        PartSys->particleFlags[SEGENV.aux0].reversegrav = *direction; // let this particle fall or rise\n        PartSys->particleFlags[SEGENV.aux0].fixed = false; // unpin\n      }\n      else { // overflow\n        *direction = !(*direction); // flip direction\n        SEGENV.aux1 = (SEGMENT.check2) * SEGMENT.vLength() + 100; // set restart countdown, make it short if auto start is unchecked\n      }\n      if (*direction == 0) // down, start dropping the highest number particle\n        SEGENV.aux0--; // next particle\n      else\n        SEGENV.aux0++;\n    }\n  }\n  else if (SEGMENT.check2) // auto start/reset\n    SEGENV.aux1--; // countdown\n\n  PartSys->update(); // update and render\n}\nstatic 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\";\n\n/*\n  Particle based Spray effect (like a volcano, possible replacement for popcorn)\n  Uses palette for particle color\n  by DedeHai (Damian Schneider)\n*/\nvoid mode_particle1Dspray(void) {\n  ParticleSystem1D *PartSys = nullptr;\n\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem1D(PartSys, 1))\n      FX_FALLBACK_STATIC; // allocation failed or is single pixel\n    PartSys->setKillOutOfBounds(true);\n    PartSys->setWallHardness(150);\n    PartSys->setParticleSize(1);\n  }\n  else\n    PartSys = reinterpret_cast<ParticleSystem1D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  // Particle System settings\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  PartSys->setBounce(SEGMENT.check2);\n  PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur\n  int32_t gravity = -((int32_t)SEGMENT.custom3 - 16);  // gravity setting, 0-15 is positive (down), 17 - 31 is negative (up)\n  PartSys->setGravity(abs(gravity)); // use reversgrav setting to invert gravity (for proper 'floor' and out of bounce handling)\n\n  PartSys->sources[0].source.hue = SEGMENT.aux0; // hw_random16();\n  PartSys->sources[0].var = 20;\n  PartSys->sources[0].minLife = 200;\n  PartSys->sources[0].maxLife = 400;\n  PartSys->sources[0].source.x = map(SEGMENT.custom1, 0 , 255, 0, PartSys->maxX); // spray position\n  PartSys->sources[0].v = map(SEGMENT.speed, 0 , 255, -127 + PartSys->sources[0].var, 127 - PartSys->sources[0].var); // particle emit speed\n  PartSys->sources[0].sourceFlags.reversegrav = gravity < 0 ? true : false;\n\n  if (hw_random()  % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) {\n    PartSys->sprayEmit(PartSys->sources[0]); // emit a particle\n    SEGMENT.aux0++; // increment hue\n  }\n\n  //update color settings\n  PartSys->setColorByAge(SEGMENT.check1); // overruled by 'color by position'\n  PartSys->setColorByPosition(SEGMENT.check3);\n  for (uint i = 0; i < PartSys->usedParticles; i++) {\n    PartSys->particleFlags[i].reversegrav = PartSys->sources[0].sourceFlags.reversegrav; // update gravity direction\n  }\n  PartSys->update(); // update and render\n}\nstatic 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\";\n\n/*\n  Particle based balance: particles move back and forth (1D pendent to 2D particle box)\n  Uses palette for particle color\n  by DedeHai (Damian Schneider)\n*/\nvoid mode_particleBalance(void) {\n  ParticleSystem1D *PartSys = nullptr;\n  uint32_t i;\n\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem1D(PartSys, 1, 128)) // init, no additional data needed, use half of max particles\n      FX_FALLBACK_STATIC; // allocation failed or is single pixel\n    PartSys->setParticleSize(1);\n  }\n  else\n    PartSys = reinterpret_cast<ParticleSystem1D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  // Particle System settings\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  PartSys->setMotionBlur(SEGMENT.custom2); // enable motion blur\n  PartSys->setBounce(!SEGMENT.check2);\n  PartSys->setWrap(SEGMENT.check2);\n  uint8_t hardness = SEGMENT.custom1 > 0 ? map(SEGMENT.custom1, 0, 255, 50, 250) : 200; // set hardness,  make the walls hard if collisions are disabled\n  PartSys->enableParticleCollisions(SEGMENT.custom1, hardness); // enable collisions if custom1 > 0\n  PartSys->setWallHardness(200);\n  PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, 255));\n  if (PartSys->usedParticles > SEGENV.aux1) { // more particles, reinitialize\n    for (i = 0; i < PartSys->usedParticles; i++) {\n      PartSys->particles[i].x = i * PS_P_RADIUS_1D;\n      PartSys->particles[i].ttl = 300;\n      PartSys->particleFlags[i].perpetual = true;\n      PartSys->particleFlags[i].collide = true;\n    }\n  }\n  SEGENV.aux1 = PartSys->usedParticles;\n\n  // re-order particles in case collisions flipped particles\n  for (i = 0; i < PartSys->usedParticles - 1; i++) {\n    if (PartSys->particles[i].x > PartSys->particles[i+1].x) {\n      if (SEGMENT.check2) { // check for wrap around\n        if (PartSys->particles[i].x - PartSys->particles[i+1].x > 3 * PS_P_RADIUS_1D)\n          continue;\n      }\n      std::swap(PartSys->particles[i].x, PartSys->particles[i+1].x);\n    }\n  }\n\n  if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0) { // how often the force is applied depends on speed setting\n    int32_t xgravity;\n    int32_t increment = (SEGMENT.speed >> 6) + 1;\n    SEGENV.aux0 += increment;\n    if (SEGMENT.check3) // random, use perlin noise\n      xgravity = ((int16_t)perlin8(SEGENV.aux0) - 128);\n    else // sinusoidal\n      xgravity = (int16_t)cos8_t(SEGENV.aux0) - 128;//((int32_t)(SEGMENT.custom3 << 2) * cos8(SEGENV.aux0)\n    // scale the force\n    xgravity = (xgravity * ((SEGMENT.custom3+1) << 2)) / 128; // xgravity: -127 to +127\n    PartSys->applyForce(xgravity);\n  }\n\n  uint32_t randomindex = hw_random16(PartSys->usedParticles);\n  PartSys->particles[randomindex].vx = ((int32_t)PartSys->particles[randomindex].vx * 200) / 255;  // apply friction to random particle to reduce clumping\n\n  //if (SEGMENT.check2 && (SEGMENT.call & 0x07) == 0) // no walls, apply friction to smooth things out\n  if ((SEGMENT.call & 0x0F) == 0 && SEGMENT.custom3 > 4) // apply friction every 16th frame to smooth things out (except for low tilt)\n    PartSys->applyFriction(1); // apply friction to all particles\n\n  //update colors\n  PartSys->setColorByPosition(SEGMENT.check1);\n  if (!SEGMENT.check1) {\n    for (i = 0; i < PartSys->usedParticles; i++) {\n        PartSys->particles[i].hue = (1024 * i) / PartSys->usedParticles; // color by particle index\n    }\n  }\n  PartSys->update(); // update and render\n}\nstatic 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\";\n\n/*\nParticle based Chase effect\nUses palette for particle color\nby DedeHai (Damian Schneider)\n*/\nvoid mode_particleChase(void) {\n  ParticleSystem1D *PartSys = nullptr;\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem1D(PartSys, 1, 191, 2, true)) // init\n      FX_FALLBACK_STATIC; // allocation failed or is single pixel\n    SEGENV.aux0 = 0xFFFF; // invalidate\n    *PartSys->PSdataEnd = 1; // huedir\n    *(PartSys->PSdataEnd + 1) = 1; // sizedir\n  }\n  else\n    PartSys = reinterpret_cast<ParticleSystem1D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n  // Particle System settings\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  PartSys->setColorByPosition(SEGMENT.check3);\n  PartSys->setMotionBlur(7 + ((SEGMENT.custom3) << 3)); // anable motion blur\n  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\n  numParticles = min(numParticles, PartSys->usedParticles); // limit to available particles\n  int32_t huestep = 1 + ((((uint32_t)SEGMENT.custom2 << 19) / numParticles) >> 16); // hue increment\n  uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3;\n  if (SEGENV.aux0 != settingssum) { // settings changed changed, update\n    if (SEGMENT.check1)\n      SEGENV.step = PartSys->advPartProps[0].size / 2 + (PartSys->maxX / numParticles);\n    else {\n      SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 6)) / numParticles; // spacing between particles\n      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)\n    }\n    for (int32_t i = 0; i < (int32_t)PartSys->usedParticles; i++) {\n      PartSys->advPartProps[i].sat = 255;\n      PartSys->particles[i].x = (i - 1) * SEGENV.step; // distribute evenly (starts out of frame for i=0)\n      PartSys->particles[i].vx =  SEGMENT.speed >> 2;\n      PartSys->advPartProps[i].size = SEGMENT.custom1;\n      if (SEGMENT.custom2 < 255)\n        PartSys->particles[i].hue = i * huestep; // gradient distribution\n      else\n        PartSys->particles[i].hue = hw_random16();\n    }\n    SEGENV.aux0 = settingssum;\n  }\n\n  if(SEGMENT.check1) {\n    huestep = 1 + (max((int)huestep, 3)  * ((int(sin16_t(strip.now * 3) + 32767))) >> 15); // changes gradient spread (scale hue step)\n  }\n\n  // wrap around (cannot use particle system wrap if distributing colors manually, it also wraps rendering which does not look good)\n  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\n    if (PartSys->particles[i].x > PartSys->maxX + PS_P_RADIUS_1D + PartSys->advPartProps[i].size) { // wrap it around\n      uint32_t nextindex = (i + 1) % PartSys->usedParticles;\n      PartSys->particles[i].x = PartSys->particles[nextindex].x - (int)SEGENV.step;\n      if(SEGMENT.check1) // playful mode, vary size\n        PartSys->advPartProps[i].size = max(1 + (SEGMENT.custom1 >> 1), ((int(sin16_t(strip.now << 1) + 32767)) >> 8)); // cycle size\n      if (SEGMENT.custom2 < 255)\n        PartSys->particles[i].hue = PartSys->particles[nextindex].hue - huestep;\n      else\n        PartSys->particles[i].hue = hw_random16();\n    }\n    PartSys->particles[i].ttl = 300; // reset ttl, cannot use perpetual because memmanager can change pointer at any time\n  }\n\n  if (SEGMENT.check1) { // playful mode, changes hue, size, speed, density dynamically\n    int8_t* huedir = reinterpret_cast<int8_t *>(PartSys->PSdataEnd);  //assign data pointer\n    int8_t* stepdir = reinterpret_cast<int8_t *>(PartSys->PSdataEnd + 1);\n    if(*stepdir == 0) *stepdir = 1; // initialize directions\n    if(*huedir == 0) *huedir = 1;\n    if (SEGENV.step >= (PartSys->advPartProps[0].size + PS_P_RADIUS_1D * 4) + PartSys->maxX / numParticles)\n      *stepdir = -1; // increase density (decrease space between particles)\n    else if (SEGENV.step <= (PartSys->advPartProps[0].size >> 1) + ((PartSys->maxX / numParticles)))\n      *stepdir = 1; // decrease density\n    if (SEGENV.aux1 > 512)\n      *huedir = -1;\n    else if (SEGENV.aux1 < 50)\n      *huedir = 1;\n    if (SEGMENT.call % (1024 / (1 + (SEGMENT.speed >> 2))) == 0)\n      SEGENV.aux1 += *huedir;\n    int8_t globalhuestep = 0; // global hue increment\n    if (SEGMENT.call % (1 + (int(sin16_t(strip.now) + 32767) >> 12))  == 0)\n      globalhuestep = 2; // global hue change to add some color variation\n    if ((SEGMENT.call & 0x1F) == 0)\n      SEGENV.step += *stepdir; // change density\n    for(uint32_t i = 0; i < PartSys->usedParticles; i++) {\n      PartSys->particles[i].hue -= globalhuestep; // shift global hue (both directions)\n      PartSys->particles[i].vx = 1 + (SEGMENT.speed >> 2) + ((int32_t(sin16_t(strip.now >> 1) + 32767) * (SEGMENT.speed >> 2)) >> 16);\n    }\n  }\n\n  PartSys->update(); // update and render\n}\nstatic 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\";\n\n/*\n  Particle Fireworks Starburst replacement (smoother rendering, more settings)\n  Uses palette for particle color\n  by DedeHai (Damian Schneider)\n*/\nvoid mode_particleStarburst(void) {\n  ParticleSystem1D *PartSys = nullptr;\n\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem1D(PartSys, 1, 200, 0, true)) // init\n      FX_FALLBACK_STATIC; // allocation failed or is single pixel\n    PartSys->setKillOutOfBounds(true);\n    PartSys->enableParticleCollisions(true, 200);\n    PartSys->sources[0].source.ttl = 1; // set initial standby time\n    PartSys->sources[0].sat = 0; // emitted particles start out white\n  }\n  else\n    PartSys = reinterpret_cast<ParticleSystem1D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  // Particle System settings\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur\n  PartSys->setGravity(SEGMENT.check1 * 8); // enable gravity\n\n  if (PartSys->sources[0].source.ttl-- == 0) { // stanby time elapsed TODO: make it a timer?\n    uint32_t explosionsize = 4 + hw_random16(SEGMENT.intensity >> 2);\n    PartSys->sources[0].source.hue = hw_random16();\n    PartSys->sources[0].var = 10 + (explosionsize << 1);\n    PartSys->sources[0].minLife = 150;\n    PartSys->sources[0].maxLife = 300;\n    PartSys->sources[0].source.x = hw_random(PartSys->maxX); //random explosion position\n    PartSys->sources[0].source.ttl = 10 + hw_random16(255 - SEGMENT.speed);\n    PartSys->sources[0].size = SEGMENT.custom1; // Fragment size\n    PartSys->sources[0].sourceFlags.collide = SEGMENT.check3;\n    for (uint32_t e = 0; e < explosionsize; e++) { // emit particles\n      if (SEGMENT.check2)\n        PartSys->sources[0].source.hue = hw_random16(); //random color for each particle\n      PartSys->sprayEmit(PartSys->sources[0]); //emit a particle\n    }\n  }\n  //shrink all particles\n  for (uint32_t i = 0; i < PartSys->usedParticles; i++) {\n    if (PartSys->advPartProps[i].size)\n      PartSys->advPartProps[i].size --;\n    if (PartSys->advPartProps[i].sat < 250)\n      PartSys->advPartProps[i].sat += 2 + (SEGMENT.custom3 >> 3);\n  }\n\n  if (SEGMENT.call % 5 == 0) {\n    PartSys->applyFriction(1); //slow down particles\n  }\n\n  PartSys->update(); // update and render\n}\nstatic 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\";\n\n/*\n  Particle based 1D GEQ effect, each frequency bin gets an emitter, distributed over the strip\n  Uses palette for particle color\n  by DedeHai (Damian Schneider)\n*/\nvoid mode_particle1DGEQ(void) {\n  ParticleSystem1D *PartSys = nullptr;\n  uint32_t numSources;\n  uint32_t i;\n\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem1D(PartSys, 16, 255, 0, true)) // init, no additional data needed\n      FX_FALLBACK_STATIC; // allocation failed or is single pixel\n  }\n  else\n    PartSys = reinterpret_cast<ParticleSystem1D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  // Particle System settings\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  numSources = PartSys->numSources;\n  PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur\n\n  uint32_t spacing = PartSys->maxX / numSources;\n  for (i = 0; i < numSources; i++) {\n    PartSys->sources[i].source.hue = i * 16; // hw_random16();   //TODO: make adjustable, maybe even colorcycle?\n    PartSys->sources[i].var = SEGMENT.speed >> 2;\n    PartSys->sources[i].minLife = 180 + (SEGMENT.intensity >> 1);\n    PartSys->sources[i].maxLife = 240 + SEGMENT.intensity;\n    PartSys->sources[i].sat = 255;\n    PartSys->sources[i].size = SEGMENT.custom1;\n    PartSys->sources[i].source.x = (spacing >> 1) + spacing * i; //distribute evenly\n  }\n\n  for (i = 0; i < PartSys->usedParticles; i++) {\n    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\n    else PartSys->particles[i].ttl = 0;\n  }\n\n  um_data_t *um_data = getAudioData();\n  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\n\n  //map the bands into 16 positions on x axis, emit some particles according to frequency loudness\n  i = 0;\n  uint32_t bin = hw_random16(numSources); //current bin , start with random one to distribute available particles fairly\n  uint32_t threshold = 300 - SEGMENT.intensity;\n\n  for (i = 0; i < numSources; i++) {\n    bin++;\n    bin = bin % numSources;\n    uint32_t emitparticle = 0;\n    // uint8_t emitspeed = ((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 10; // emit speed according to loudness of band (127 max!)\n    if (fftResult[bin] > threshold) {\n      emitparticle = 1;\n    }\n    else if (fftResult[bin] > 0) { // band has low volue\n      uint32_t restvolume = ((threshold - fftResult[bin]) >> 2) + 2;\n      if (hw_random() % restvolume == 0) {\n        emitparticle = 1;\n      }\n    }\n\n    if (emitparticle)\n      PartSys->sprayEmit(PartSys->sources[bin]);\n  }\n  //TODO: add color control?\n\n  PartSys->update(); // update and render\n}\nstatic 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\";\n\n/*\n  Particle based Fire effect\n  Uses palette for particle color\n  by DedeHai (Damian Schneider)\n*/\nvoid mode_particleFire1D(void) {\n  ParticleSystem1D *PartSys = nullptr;\n\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem1D(PartSys, 5)) // init\n      FX_FALLBACK_STATIC; // allocation failed or is single pixel\n    PartSys->setKillOutOfBounds(true);\n    PartSys->setParticleSize(1);\n  }\n  else\n    PartSys = reinterpret_cast<ParticleSystem1D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  // Particle System settings\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  PartSys->setMotionBlur(128 + (SEGMENT.custom2 >> 1)); // enable motion blur\n  PartSys->setColorByAge(true);\n  uint32_t emitparticles = 1;\n  uint32_t j = hw_random16();\n  for (uint i = 0; i < 3; i++) { // 3 base flames\n    if (PartSys->sources[i].source.ttl > 50)\n      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\n    else\n      PartSys->sources[i].source.ttl = 100 + hw_random16(200);\n  }\n  for (uint i = 0; i < PartSys->numSources; i++) {\n    j = (j + 1) % PartSys->numSources;\n    PartSys->sources[j].source.x = 0;\n    PartSys->sources[j].var = 2 + (SEGMENT.speed >> 4);\n    // base flames\n    if (j > 2) {\n      PartSys->sources[j].minLife = 150 + SEGMENT.intensity + (j << 2); // TODO: in 2D, min life is maxlife/2 and that looks very nice\n      PartSys->sources[j].maxLife = 200 + SEGMENT.intensity + (j << 3);\n      PartSys->sources[j].v = (SEGMENT.speed >> (2 + (j << 1)));\n      if (emitparticles) {\n        emitparticles--;\n        PartSys->sprayEmit(PartSys->sources[j]); // emit a particle\n      }\n    }\n    else {\n      PartSys->sources[j].minLife = PartSys->sources[j].source.ttl + SEGMENT.intensity;\n      PartSys->sources[j].maxLife = PartSys->sources[j].minLife + 50;\n      PartSys->sources[j].v = SEGMENT.speed >> 2;\n      if (SEGENV.call & 0x01) // every second frame\n        PartSys->sprayEmit(PartSys->sources[j]); // emit a particle\n    }\n  }\n\n  for (uint i = 0; i < PartSys->usedParticles; i++) {\n    PartSys->particles[i].x += PartSys->particles[i].ttl >> 7; // 'hot' particles are faster, apply some extra velocity\n    if (PartSys->particles[i].ttl > 3 + ((255 - SEGMENT.custom1) >> 1))\n      PartSys->particles[i].ttl -= map(SEGMENT.custom1, 0, 255, 1, 3); // age faster\n  }\n\n  PartSys->update(); // update and render\n}\nstatic 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\";\n\n/*\n  Particle based AR effect, swoop particles along the strip with selected frequency loudness\n  by DedeHai (Damian Schneider)\n*/\nvoid mode_particle1DsonicStream(void) {\n  ParticleSystem1D *PartSys = nullptr;\n\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem1D(PartSys, 1, 255, 0, true)) // init, no additional data needed\n      FX_FALLBACK_STATIC; // allocation failed or is single pixel\n    PartSys->setKillOutOfBounds(true);\n    PartSys->sources[0].source.x = 0; // at start\n    //PartSys->sources[1].source.x = PartSys->maxX; // at end\n    PartSys->sources[0].var = 0;//SEGMENT.custom1 >> 3;\n  }\n  else\n    PartSys = reinterpret_cast<ParticleSystem1D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  // Particle System settings\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  PartSys->setMotionBlur(20 + (SEGMENT.custom2 >> 1)); // anable motion blur\n  PartSys->setSmearBlur(200); // smooth out the edges\n  PartSys->sources[0].v = 5 + (SEGMENT.speed >> 2);\n\n  // FFT processing\n  um_data_t *um_data = getAudioData();\n  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\n  uint32_t loudness;\n  uint32_t baseBin = SEGMENT.custom3 >> 1; // 0 - 15 map(SEGMENT.custom3, 0, 31, 0, 14);\n\n  loudness = fftResult[baseBin];// + fftResult[baseBin + 1];\n  if (baseBin > 12)\n    loudness = loudness << 2; // double loudness for high frequencies (better detecion)\n\n  uint32_t threshold = 140 - (SEGMENT.intensity >> 1);\n  if (SEGMENT.check2) { // enable low pass filter for dynamic threshold\n    SEGMENT.step = (SEGMENT.step * 31500 + loudness * (32768 - 31500)) >> 15; // low pass filter for simple beat detection: add average to base threshold\n    threshold = 20 + (threshold >> 1) + SEGMENT.step; // add average to threshold\n  }\n\n  // color\n  uint32_t hueincrement = (SEGMENT.custom1 >> 3); // 0-31\n  PartSys->sources[0].sat = SEGMENT.custom1 > 0 ? 255 : 0; // color slider at zero: set to white\n  PartSys->setColorByPosition(SEGMENT.custom1 == 255);\n\n  // particle manipulation\n  for (uint32_t i = 0; i < PartSys->usedParticles; i++) {\n    if (PartSys->sources[0].sourceFlags.perpetual == false) { // age faster if not perpetual\n      if (PartSys->particles[i].ttl > 2) {\n        PartSys->particles[i].ttl -= 2; //ttl is linked to brightness, this allows to use higher brightness but still a short lifespan\n      }\n      else PartSys->particles[i].ttl = 0;\n    }\n    if (SEGMENT.check1) { // modulate colors by mid frequencies\n      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)\n      PartSys->particles[i].hue += (mids * perlin8(PartSys->particles[i].x << 2, SEGMENT.step << 2)) >> 9; // color by perlin noise from mid frequencies\n    }\n  }\n\n  if (loudness > threshold) {\n    SEGMENT.aux0 += hueincrement; // change color\n    PartSys->sources[0].minLife = 100 + (((unsigned)SEGMENT.intensity * loudness * loudness) >> 13);\n    PartSys->sources[0].maxLife = PartSys->sources[0].minLife;\n    PartSys->sources[0].source.hue = SEGMENT.aux0;\n    PartSys->sources[0].size = SEGMENT.speed;\n    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\n      int partindex = PartSys->sprayEmit(PartSys->sources[0]); // emit a particle\n      if (partindex >= 0) SEGMENT.aux1 = partindex; // track last emitted particle\n    }\n  }\n  else loudness = 0; // required for push mode\n\n  PartSys->update(); // update and render (needs to be done before manipulation for initial particle spacing to be right)\n\n  if (SEGMENT.check3) { // push mode\n    PartSys->sources[0].sourceFlags.perpetual = true; // emitted particles dont age\n    PartSys->applyFriction(1); //slow down particles\n    int32_t movestep = (((int)SEGMENT.speed + 2) * loudness) >> 10;\n    if (movestep) {\n      for (uint32_t i = 0; i < PartSys->usedParticles; i++) {\n        if (PartSys->particles[i].ttl) {\n          PartSys->particles[i].x += movestep; // push particles\n          PartSys->particles[i].vx = 10 + (SEGMENT.speed >> 4) ; // give particles some speed for smooth movement (friction will slow them down)\n        }\n      }\n    }\n  }\n  else {\n    PartSys->sources[0].sourceFlags.perpetual = false; // emitted particles age\n    // move all particles (again) to allow faster speeds\n    for (uint32_t i = 0; i < PartSys->usedParticles; i++) {\n      if (PartSys->particles[i].vx == 0)\n        PartSys->particles[i].vx = PartSys->sources[0].v; // move static particles (after disabling push mode)\n      PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i], nullptr, &PartSys->advPartProps[i]);\n    }\n  }\n}\nstatic const char _data_FX_MODE_PS_SONICSTREAM[] PROGMEM = \"PS Sonic Stream@!,!,Color,Blur,Bin,Mod,Filter,Push;,!;!;1f;c3=0,o2=1\";\n\n\n/*\n  Particle based AR effect, creates exploding particles on beats\n  by DedeHai (Damian Schneider)\n*/\nvoid mode_particle1DsonicBoom(void) {\n  ParticleSystem1D *PartSys = nullptr;\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem1D(PartSys, 1, 255, 0, true)) // init, no additional data needed\n      FX_FALLBACK_STATIC; // allocation failed or is single pixel\n    PartSys->setKillOutOfBounds(true);\n  }\n  else\n    PartSys = reinterpret_cast<ParticleSystem1D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n\n  // Particle System settings\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  PartSys->setMotionBlur(180 * SEGMENT.check3);\n  PartSys->setSmearBlur(64 * SEGMENT.check3);\n  PartSys->sources[0].var = map(SEGMENT.speed, 0, 255, 10, 127);\n\n  // FFT processing\n  um_data_t *um_data = getAudioData();\n  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\n  uint32_t loudness;\n  uint32_t baseBin = SEGMENT.custom3 >> 1; // 0 - 15 map(SEGMENT.custom3, 0, 31, 0, 14);\n  loudness = fftResult[baseBin];// + fftResult[baseBin + 1];\n\n  if (baseBin > 12)\n    loudness = loudness << 2; // double loudness for high frequencies (better detecion)\n  uint32_t threshold = 150 - (SEGMENT.intensity >> 1);\n  if (SEGMENT.check2) { // enable low pass filter for dynamic threshold\n    SEGMENT.step = (SEGMENT.step * 31500 + loudness * (32768 - 31500)) >> 15; // low pass filter for simple beat detection: add average to base threshold\n    threshold = 20 + (threshold >> 1) + SEGMENT.step; // add average to threshold\n  }\n\n  // particle manipulation\n  for (uint32_t i = 0; i < PartSys->usedParticles; i++) {\n    if (SEGMENT.check1) { // modulate colors by mid frequencies\n      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)\n      PartSys->particles[i].hue += (mids * perlin8(PartSys->particles[i].x << 2, SEGMENT.step << 2)) >> 9; // color by perlin noise from mid frequencies\n    }\n    if (PartSys->particles[i].ttl > 16) {\n      PartSys->particles[i].ttl -= 16; //ttl is linked to brightness, this allows to use higher brightness but still a (very) short lifespan\n    }\n  }\n\n  if (loudness > threshold) {\n    if (SEGMENT.aux1 == 0) { // edge detected, code only runs once per \"beat\"\n      // update position\n      if (SEGMENT.custom2 < 128) // fixed position\n        PartSys->sources[0].source.x = map(SEGMENT.custom2, 0, 127, 0, PartSys->maxX);\n      else if (SEGMENT.custom2 < 255) { // advances on each \"beat\"\n        int32_t step = PartSys->maxX / (((270 - SEGMENT.custom2) >> 3)); // step: 2 - 33 steps for full segment width\n        PartSys->sources[0].source.x = (PartSys->sources[0].source.x + step) % PartSys->maxX;\n        if (PartSys->sources[0].source.x < step) // align to be symmetrical by making the first position half a step from start\n          PartSys->sources[0].source.x = step >> 1;\n      }\n      else // position set to max, use random postion per beat\n        PartSys->sources[0].source.x = hw_random(PartSys->maxX);\n\n      // update color\n      //PartSys->setColorByPosition(SEGMENT.custom1 == 255);     // color slider at max: particle color by position\n      PartSys->sources[0].sat = SEGMENT.custom1 > 0 ? 255 : 0; // color slider at zero: set to white\n      if (SEGMENT.custom1 == 255) // emit color by position\n        SEGMENT.aux0 = map(PartSys->sources[0].source.x , 0, PartSys->maxX, 0, 255);\n      else if (SEGMENT.custom1 > 0)\n        SEGMENT.aux0 += (SEGMENT.custom1 >> 1); // change emit color per \"beat\"\n    }\n    SEGMENT.aux1 = 1; // track edge detection\n\n    PartSys->sources[0].minLife = 200;\n    PartSys->sources[0].maxLife = PartSys->sources[0].minLife + (((unsigned)SEGMENT.intensity * loudness * loudness) >> 13);\n    PartSys->sources[0].source.hue = SEGMENT.aux0;\n    uint32_t explosionsize = 4 + (PartSys->maxXpixel >> 2);\n    explosionsize = hw_random16((explosionsize * loudness) >> 10);\n    for (uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles\n        PartSys->sprayEmit(PartSys->sources[0]); // emit a particle\n      }\n  }\n  else\n    SEGMENT.aux1 = 0; // reset edge detection\n\n  PartSys->update(); // update and render (needs to be done before manipulation for initial particle spacing to be right)\n}\nstatic const char _data_FX_MODE_PS_SONICBOOM[] PROGMEM = \"PS Sonic Boom@!,!,Color,Position,Bin,Mod,Filter,Blur;,!;!;1f;c2=63,c3=0,o2=1\";\n\n/*\nParticles bound by springs\nby DedeHai (Damian Schneider)\n*/\nvoid mode_particleSpringy(void) {\n  ParticleSystem1D *PartSys = nullptr;\n  if (SEGMENT.call == 0) { // initialization\n    if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init with advanced properties (used for spring forces)\n      FX_FALLBACK_STATIC; // allocation failed or is single pixel\n    SEGENV.aux0 = SEGENV.aux1 = 0xFFFF; // invalidate settings\n  }\n  else\n    PartSys = reinterpret_cast<ParticleSystem1D *>(SEGENV.data); // if not first call, just set the pointer to the PS\n  if (PartSys == nullptr)\n    FX_FALLBACK_STATIC; // something went wrong, no data!\n  // Particle System settings\n  PartSys->updateSystem(); // update system properties (dimensions and data pointers)\n  PartSys->setMotionBlur(220 * SEGMENT.check1); // anable motion blur\n  PartSys->setSmearBlur(50); // smear a little\n  PartSys->setUsedParticles(map(SEGMENT.custom1, 0, 255, 30 >> SEGMENT.check2, 255  >> (SEGMENT.check2*2))); // depends on density and particle size\n  //PartSys->enableParticleCollisions(true, 140); // enable particle collisions, can not be set too hard or impulses will not strech the springs if soft.\n  int32_t springlength = PartSys->maxX / (PartSys->usedParticles); // spring length (spacing between particles)\n  int32_t springK = map(SEGMENT.speed, 0, 255, 5, 35); // spring constant (stiffness)\n\n  uint32_t settingssum = SEGMENT.custom1 + SEGMENT.check2;\n  PartSys->setParticleSize(SEGMENT.check2 ? 120 : 1); // large or small particles\n\n  if (SEGENV.aux0 != settingssum) { // number of particles changed, update distribution\n    for (int32_t i = 0; i < (int32_t)PartSys->usedParticles; i++) {\n      PartSys->advPartProps[i].sat = 255; // full saturation\n      //PartSys->particleFlags[i].collide = true; // enable collision for particles -> results in chaos, removed for now\n      PartSys->particles[i].x = (i+1) * ((PartSys->maxX) / (PartSys->usedParticles)); // distribute\n      //PartSys->particles[i].vx = 0; //reset speed\n      //PartSys->advPartProps[i].size = SEGMENT.check2 ? 190 : 2; // set size, small or big -> use global size\n    }\n    SEGENV.aux0 = settingssum;\n  }\n  int dxlimit = (2 + ((255 - SEGMENT.speed) >> 5)) * springlength; // limit for spring length to avoid overstretching\n\n  int springforce[PartSys->usedParticles]; // spring forces\n  memset(springforce, 0, PartSys->usedParticles * sizeof(int32_t)); // reset spring forces\n\n  // calculate spring forces and limit particle positions\n  if (PartSys->particles[0].x < -springlength)\n    PartSys->particles[0].x = -springlength; // limit the spring length\n  else if (PartSys->particles[0].x > dxlimit)\n    PartSys->particles[0].x = dxlimit; // limit the spring length\n  springforce[0] += ((springlength >> 1) - (PartSys->particles[0].x)) * springK; // first particle anchors to x=0\n\n  for (uint32_t i = 1; i < PartSys->usedParticles; i++) {\n    // reorder particles if they are out of order to prevent chaos\n    if (PartSys->particles[i].x < PartSys->particles[i-1].x)\n        std::swap(PartSys->particles[i].x, PartSys->particles[i-1].x); // swap particle positions to maintain order\n    int dx = PartSys->particles[i].x - PartSys->particles[i-1].x; // distance, always positive\n    if (dx > dxlimit) { // limit the spring length\n      PartSys->particles[i].x = PartSys->particles[i-1].x + dxlimit;\n      dx = dxlimit;\n    }\n    int dxleft = (springlength - dx); // offset from spring resting position\n    springforce[i] += dxleft * springK;\n    springforce[i-1] -= dxleft * springK;\n    if (i == (PartSys->usedParticles - 1)) {\n     if (PartSys->particles[i].x >= PartSys->maxX + springlength)\n        PartSys->particles[i].x = PartSys->maxX + springlength;\n      int dxright = (springlength >> 1) - (PartSys->maxX - PartSys->particles[i].x); // last particle anchors to x=maxX\n      springforce[i] -= dxright * springK;\n    }\n  }\n  // apply spring forces to particles\n  bool dampenoscillations = (SEGMENT.call % (9 - (SEGMENT.speed >> 5))) == 0; // dampen oscillation if particles are slow, more damping on stiffer springs\n  for (uint32_t i = 0; i < PartSys->usedParticles; i++) {\n    springforce[i] = springforce[i] / 64; // scale spring force (cannot use shifts because of negative values)\n    int maxforce = 120; // limit spring force\n    springforce[i] = springforce[i] > maxforce ? maxforce : springforce[i] < -maxforce ? -maxforce : springforce[i]; // limit spring force\n    PartSys->applyForce(PartSys->particles[i], springforce[i], PartSys->advPartProps[i].forcecounter);\n    //dampen slow particles to avoid persisting oscillations on higher stiffness\n    if (dampenoscillations) {\n      if (abs(PartSys->particles[i].vx) < 3 && abs(springforce[i]) < (springK >> 2))\n        PartSys->particles[i].vx = (PartSys->particles[i].vx * 254) / 256; // take out some energy\n    }\n    PartSys->particles[i].ttl = 300; // reset ttl, cannot use perpetual\n  }\n\n  if (SEGMENT.call % ((65 - ((SEGMENT.intensity * (1 + (SEGMENT.speed>>3))) >> 7))) == 0) // more damping for higher stiffness\n    PartSys->applyFriction((SEGMENT.intensity >> 2));\n\n  // add a small resetting force so particles return to resting position even under high damping\n  for (uint32_t i = 1; i < PartSys->usedParticles - 1; i++) {\n    int restposition = (springlength >> 1) + i * springlength; // resting position\n    int dx = restposition - PartSys->particles[i].x; // distance, always positive\n    PartSys->applyForce(PartSys->particles[i], dx > 0 ? 1 : (dx < 0 ? -1 : 0), PartSys->advPartProps[i].forcecounter);\n  }\n\n  // Modes\n  if (SEGMENT.check3) { // use AR, custom 3 becomes frequency band to use, applies velocity to center particle according to loudness\n    um_data_t *um_data = getAudioData();\n    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\n    uint32_t baseBin = map(SEGMENT.custom3, 0, 31, 0, 14);\n    uint32_t loudness = fftResult[baseBin] + fftResult[baseBin+1];\n    uint32_t threshold = 80; //150 - (SEGMENT.intensity >> 1);\n    if (loudness > threshold) {\n        int offset = (PartSys->maxX >> 1) - PartSys->particles[PartSys->usedParticles>>1].x; // offset from center\n        if (abs(offset) < PartSys->maxX >> 5) // push particle around in center sector\n          PartSys->particles[PartSys->usedParticles>>1].vx = ((PartSys->particles[PartSys->usedParticles>>1].vx > 0 ? 1 : -1)) * (loudness >> 3);\n    }\n  }\n  else{\n    if (SEGMENT.custom3 <= 10) { // periodic pulse: 0-5 apply at start, 6-10 apply at center\n      if (strip.now > SEGMENT.step) {\n        int speed = (SEGMENT.custom3 > 5) ? (SEGMENT.custom3 - 6) : SEGMENT.custom3;\n        SEGMENT.step = strip.now + 7500 - ((SEGMENT.speed << 3) + (speed << 10));\n        int amplitude = 40 + (SEGMENT.custom1 >> 2);\n        int index = (SEGMENT.custom3 > 5) ? (PartSys->usedParticles / 2) : 0; // center or start particle\n        PartSys->particles[index].vx += amplitude;\n      }\n    }\n    else if (SEGMENT.custom3 <= 30) { // sinusoidal wave: 11-20 apply at start, 21-30 apply at center\n      int index = (SEGMENT.custom3 > 20) ? (PartSys->usedParticles / 2) : 0; // center or start particle\n      int restposition = 0;\n      if (index > 0) restposition = PartSys->maxX >> 1; // center\n      //int amplitude = 5 + (SEGMENT.speed >> 3) + (SEGMENT.custom1 >> 2); // amplitude depends on density\n      int amplitude = 5 + (SEGMENT.custom1 >> 2); // amplitude depends on density\n      int speed = SEGMENT.custom3 - 10 - (index ? 10 : 0); // map 11-20 and 21-30 to 1-10\n      int phase = strip.now * ((1 + (SEGMENT.speed >> 4)) * speed);\n      if (SEGMENT.check2) amplitude <<= 1; // double amplitude for XL particles\n      PartSys->particles[index].x = restposition + ((sin16_t(phase) * amplitude) >> 12); // apply position\n    }\n    else {\n      if (hw_random16() < 656) { // ~1% chance to add a pulse\n        int amplitude = 60;\n        if (SEGMENT.check2) amplitude <<= 1; // double amplitude for XL particles\n        PartSys->particles[PartSys->usedParticles >> 1].vx += hw_random16(amplitude << 1) - amplitude; // apply acceleration\n      }\n    }\n  }\n\n  for (uint32_t i = 0; i < PartSys->usedParticles; i++) {\n    if (SEGMENT.custom2 == 255) { // map speed to hue\n       int speedclr = ((int8_t(abs(PartSys->particles[i].vx))) >> 2) << 4; // scale for greater color variation, dump small values to avoid flickering\n       //int speed = PartSys->particles[i].vx << 2; // +/- 512\n       if (speedclr > 240) speedclr = 240; // limit color to non-wrapping part of palette\n       PartSys->particles[i].hue = speedclr;\n    }\n    else if (SEGMENT.custom2 > 0)\n      PartSys->particles[i].hue = i * (SEGMENT.custom2 >> 2); // gradient distribution\n    else {\n      // map hue to particle density\n      int deviation;\n      if (i == 0) // First particle: measure density based on distance to anchor point\n        deviation = springlength/2 - PartSys->particles[i].x;\n      else if (i == PartSys->usedParticles - 1) // Last particle: measure density based on distance to right boundary\n        deviation = springlength/2 - (PartSys->maxX - PartSys->particles[i].x);\n      else {\n        // Middle particles: average of compression/expansion from both sides\n        int leftDx = PartSys->particles[i].x - PartSys->particles[i-1].x;\n        int rightDx = PartSys->particles[i+1].x - PartSys->particles[i].x;\n        int avgDistance = (leftDx + rightDx) >> 1;\n        if (avgDistance < 0) avgDistance = 0; // avoid negative distances (not sure why this happens)\n        deviation = (springlength - avgDistance);\n      }\n      deviation = constrain(deviation, -127, 112); // limit deviation to -127..112 (do not go intwo wrapping part of palette)\n      PartSys->particles[i].hue = 127 + deviation; // map density to hue\n    }\n  }\n  PartSys->update(); // update and render\n}\nstatic 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\";\n\n#endif // WLED_DISABLE_PARTICLESYSTEM1D\n\n//////////////////////////////////////////////////////////////////////////////////////////\n// mode data\nstatic const char _data_RESERVED[] PROGMEM = \"RSVD\";\n\n// add (or replace reserved) effect mode and data into vector\n// use id==255 to find unallocated gaps (with \"Reserved\" data string)\n// if vector size() is smaller than id (single) data is appended at the end (regardless of id)\n// return the actual id used for the effect or 255 if the add failed.\nuint8_t WS2812FX::addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name) {\n  if (id == 255) { // find empty slot\n    for (size_t i=1; i<_mode.size(); i++) if (_modeData[i] == _data_RESERVED) { id = i; break; }\n  }\n  if (id < _mode.size()) {\n    if (_modeData[id] != _data_RESERVED) return 255; // do not overwrite an already added effect\n    _mode[id]     = mode_fn;\n    _modeData[id] = mode_name;\n    return id;\n  } else if (_mode.size() < 255) { // 255 is reserved for indicating the effect wasn't added\n    _mode.push_back(mode_fn);\n    _modeData.push_back(mode_name);\n    if (_modeCount < _mode.size()) _modeCount++;\n    return _mode.size() - 1;\n  } else {\n    return 255; // The vector is full so return 255\n  }\n}\n\nvoid WS2812FX::setupEffectData() {\n  // Solid must be first! (assuming vector is empty upon call to setup)\n  _mode.push_back(&mode_static);\n  _modeData.push_back(_data_FX_MODE_STATIC);\n  // fill reserved word in case there will be any gaps in the array\n  for (size_t i=1; i<_modeCount; i++) {\n    _mode.push_back(&mode_static);\n    _modeData.push_back(_data_RESERVED);\n  }\n  // now replace all pre-allocated effects\n  addEffect(FX_MODE_COPY, &mode_copy_segment, _data_FX_MODE_COPY);\n  // --- 1D non-audio effects ---\n  addEffect(FX_MODE_BLINK, &mode_blink, _data_FX_MODE_BLINK);\n  addEffect(FX_MODE_BREATH, &mode_breath, _data_FX_MODE_BREATH);\n  addEffect(FX_MODE_COLOR_WIPE, &mode_color_wipe, _data_FX_MODE_COLOR_WIPE);\n  addEffect(FX_MODE_COLOR_WIPE_RANDOM, &mode_color_wipe_random, _data_FX_MODE_COLOR_WIPE_RANDOM);\n  addEffect(FX_MODE_RANDOM_COLOR, &mode_random_color, _data_FX_MODE_RANDOM_COLOR);\n  addEffect(FX_MODE_COLOR_SWEEP, &mode_color_sweep, _data_FX_MODE_COLOR_SWEEP);\n  addEffect(FX_MODE_DYNAMIC, &mode_dynamic, _data_FX_MODE_DYNAMIC);\n  addEffect(FX_MODE_RAINBOW, &mode_rainbow, _data_FX_MODE_RAINBOW);\n  addEffect(FX_MODE_RAINBOW_CYCLE, &mode_rainbow_cycle, _data_FX_MODE_RAINBOW_CYCLE);\n  addEffect(FX_MODE_SCAN, &mode_scan, _data_FX_MODE_SCAN);\n  addEffect(FX_MODE_DUAL_SCAN, &mode_dual_scan, _data_FX_MODE_DUAL_SCAN);\n  addEffect(FX_MODE_FADE, &mode_fade, _data_FX_MODE_FADE);\n  addEffect(FX_MODE_THEATER_CHASE, &mode_theater_chase, _data_FX_MODE_THEATER_CHASE);\n  addEffect(FX_MODE_THEATER_CHASE_RAINBOW, &mode_theater_chase_rainbow, _data_FX_MODE_THEATER_CHASE_RAINBOW);\n  addEffect(FX_MODE_RUNNING_LIGHTS, &mode_running_lights, _data_FX_MODE_RUNNING_LIGHTS);\n  addEffect(FX_MODE_SAW, &mode_saw, _data_FX_MODE_SAW);\n  addEffect(FX_MODE_TWINKLE, &mode_twinkle, _data_FX_MODE_TWINKLE);\n  addEffect(FX_MODE_DISSOLVE, &mode_dissolve, _data_FX_MODE_DISSOLVE);\n  addEffect(FX_MODE_DISSOLVE_RANDOM, &mode_dissolve_random, _data_FX_MODE_DISSOLVE_RANDOM);\n  addEffect(FX_MODE_FLASH_SPARKLE, &mode_flash_sparkle, _data_FX_MODE_FLASH_SPARKLE);\n  addEffect(FX_MODE_HYPER_SPARKLE, &mode_hyper_sparkle, _data_FX_MODE_HYPER_SPARKLE);\n  addEffect(FX_MODE_STROBE, &mode_strobe, _data_FX_MODE_STROBE);\n  addEffect(FX_MODE_STROBE_RAINBOW, &mode_strobe_rainbow, _data_FX_MODE_STROBE_RAINBOW);\n  addEffect(FX_MODE_MULTI_STROBE, &mode_multi_strobe, _data_FX_MODE_MULTI_STROBE);\n  addEffect(FX_MODE_BLINK_RAINBOW, &mode_blink_rainbow, _data_FX_MODE_BLINK_RAINBOW);\n  addEffect(FX_MODE_ANDROID, &mode_android, _data_FX_MODE_ANDROID);\n  addEffect(FX_MODE_CHASE_COLOR, &mode_chase_color, _data_FX_MODE_CHASE_COLOR);\n  addEffect(FX_MODE_CHASE_RANDOM, &mode_chase_random, _data_FX_MODE_CHASE_RANDOM);\n  addEffect(FX_MODE_CHASE_RAINBOW, &mode_chase_rainbow, _data_FX_MODE_CHASE_RAINBOW);\n  addEffect(FX_MODE_CHASE_FLASH, &mode_chase_flash, _data_FX_MODE_CHASE_FLASH);\n  addEffect(FX_MODE_CHASE_FLASH_RANDOM, &mode_chase_flash_random, _data_FX_MODE_CHASE_FLASH_RANDOM);\n  addEffect(FX_MODE_CHASE_RAINBOW_WHITE, &mode_chase_rainbow_white, _data_FX_MODE_CHASE_RAINBOW_WHITE);\n  addEffect(FX_MODE_COLORFUL, &mode_colorful, _data_FX_MODE_COLORFUL);\n  addEffect(FX_MODE_TRAFFIC_LIGHT, &mode_traffic_light, _data_FX_MODE_TRAFFIC_LIGHT);\n  addEffect(FX_MODE_COLOR_SWEEP_RANDOM, &mode_color_sweep_random, _data_FX_MODE_COLOR_SWEEP_RANDOM);\n  addEffect(FX_MODE_RUNNING_COLOR, &mode_running_color, _data_FX_MODE_RUNNING_COLOR);\n  addEffect(FX_MODE_AURORA, &mode_aurora, _data_FX_MODE_AURORA);\n  addEffect(FX_MODE_COLORCLOUDS, &mode_ColorClouds, _data_FX_MODE_COLORCLOUDS);\n  addEffect(FX_MODE_RUNNING_RANDOM, &mode_running_random, _data_FX_MODE_RUNNING_RANDOM);\n  addEffect(FX_MODE_LARSON_SCANNER, &mode_larson_scanner, _data_FX_MODE_LARSON_SCANNER);\n  addEffect(FX_MODE_RAIN, &mode_rain, _data_FX_MODE_RAIN);\n  addEffect(FX_MODE_PRIDE_2015, &mode_pride_2015, _data_FX_MODE_PRIDE_2015);\n  addEffect(FX_MODE_COLORWAVES, &mode_colorwaves, _data_FX_MODE_COLORWAVES);\n  addEffect(FX_MODE_FIREWORKS, &mode_fireworks, _data_FX_MODE_FIREWORKS);\n  addEffect(FX_MODE_TETRIX, &mode_tetrix, _data_FX_MODE_TETRIX);\n  addEffect(FX_MODE_FIRE_FLICKER, &mode_fire_flicker, _data_FX_MODE_FIRE_FLICKER);\n  addEffect(FX_MODE_GRADIENT, &mode_gradient, _data_FX_MODE_GRADIENT);\n  addEffect(FX_MODE_LOADING, &mode_loading, _data_FX_MODE_LOADING);\n  addEffect(FX_MODE_FAIRY, &mode_fairy, _data_FX_MODE_FAIRY);\n  addEffect(FX_MODE_TWO_DOTS, &mode_two_dots, _data_FX_MODE_TWO_DOTS);\n  addEffect(FX_MODE_FAIRYTWINKLE, &mode_fairytwinkle, _data_FX_MODE_FAIRYTWINKLE);\n  addEffect(FX_MODE_RUNNING_DUAL, &mode_running_dual, _data_FX_MODE_RUNNING_DUAL);\n  #ifdef WLED_ENABLE_GIF\n  addEffect(FX_MODE_IMAGE, &mode_image, _data_FX_MODE_IMAGE);\n  #endif\n  addEffect(FX_MODE_TRICOLOR_CHASE, &mode_tricolor_chase, _data_FX_MODE_TRICOLOR_CHASE);\n  addEffect(FX_MODE_TRICOLOR_WIPE, &mode_tricolor_wipe, _data_FX_MODE_TRICOLOR_WIPE);\n  addEffect(FX_MODE_TRICOLOR_FADE, &mode_tricolor_fade, _data_FX_MODE_TRICOLOR_FADE);\n  addEffect(FX_MODE_LIGHTNING, &mode_lightning, _data_FX_MODE_LIGHTNING);\n  addEffect(FX_MODE_ICU, &mode_icu, _data_FX_MODE_ICU);\n  addEffect(FX_MODE_DUAL_LARSON_SCANNER, &mode_dual_larson_scanner, _data_FX_MODE_DUAL_LARSON_SCANNER);\n  addEffect(FX_MODE_RANDOM_CHASE, &mode_random_chase, _data_FX_MODE_RANDOM_CHASE);\n  addEffect(FX_MODE_OSCILLATE, &mode_oscillate, _data_FX_MODE_OSCILLATE);\n  addEffect(FX_MODE_JUGGLE, &mode_juggle, _data_FX_MODE_JUGGLE);\n  addEffect(FX_MODE_PALETTE, &mode_palette, _data_FX_MODE_PALETTE);\n  addEffect(FX_MODE_BPM, &mode_bpm, _data_FX_MODE_BPM);\n  addEffect(FX_MODE_FILLNOISE8, &mode_fillnoise8, _data_FX_MODE_FILLNOISE8);\n  addEffect(FX_MODE_NOISE16_1, &mode_noise16_1, _data_FX_MODE_NOISE16_1);\n  addEffect(FX_MODE_NOISE16_2, &mode_noise16_2, _data_FX_MODE_NOISE16_2);\n  addEffect(FX_MODE_NOISE16_3, &mode_noise16_3, _data_FX_MODE_NOISE16_3);\n  addEffect(FX_MODE_NOISE16_4, &mode_noise16_4, _data_FX_MODE_NOISE16_4);\n  addEffect(FX_MODE_COLORTWINKLE, &mode_colortwinkle, _data_FX_MODE_COLORTWINKLE);\n  addEffect(FX_MODE_LAKE, &mode_lake, _data_FX_MODE_LAKE);\n  addEffect(FX_MODE_METEOR, &mode_meteor, _data_FX_MODE_METEOR);\n  //addEffect(FX_MODE_METEOR_SMOOTH, &mode_meteor_smooth, _data_FX_MODE_METEOR_SMOOTH); // merged with mode_meteor \n  addEffect(FX_MODE_RAILWAY, &mode_railway, _data_FX_MODE_RAILWAY);\n  addEffect(FX_MODE_RIPPLE, &mode_ripple, _data_FX_MODE_RIPPLE);\n  addEffect(FX_MODE_TWINKLEFOX, &mode_twinklefox, _data_FX_MODE_TWINKLEFOX);\n  addEffect(FX_MODE_TWINKLECAT, &mode_twinklecat, _data_FX_MODE_TWINKLECAT);\n  addEffect(FX_MODE_HALLOWEEN_EYES, &mode_halloween_eyes, _data_FX_MODE_HALLOWEEN_EYES);\n  addEffect(FX_MODE_STATIC_PATTERN, &mode_static_pattern, _data_FX_MODE_STATIC_PATTERN);\n  addEffect(FX_MODE_TRI_STATIC_PATTERN, &mode_tri_static_pattern, _data_FX_MODE_TRI_STATIC_PATTERN);\n  addEffect(FX_MODE_SPOTS, &mode_spots, _data_FX_MODE_SPOTS);\n  addEffect(FX_MODE_SPOTS_FADE, &mode_spots_fade, _data_FX_MODE_SPOTS_FADE);\n  addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET);\n  #if defined(WLED_PS_DONT_REPLACE_1D_FX) || defined(WLED_PS_DONT_REPLACE_2D_FX)\n  addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012);\n  addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS);\n  #endif\n  addEffect(FX_MODE_SPARKLE, &mode_sparkle, _data_FX_MODE_SPARKLE);\n  addEffect(FX_MODE_GLITTER, &mode_glitter, _data_FX_MODE_GLITTER);\n  addEffect(FX_MODE_SOLID_GLITTER, &mode_solid_glitter, _data_FX_MODE_SOLID_GLITTER);\n  addEffect(FX_MODE_MULTI_COMET, &mode_multi_comet, _data_FX_MODE_MULTI_COMET);  \n  #ifdef WLED_PS_DONT_REPLACE_1D_FX\n  addEffect(FX_MODE_ROLLINGBALLS, &mode_rolling_balls, _data_FX_MODE_ROLLINGBALLS);\n  addEffect(FX_MODE_STARBURST, &mode_starburst, _data_FX_MODE_STARBURST);\n  addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS);\n  #endif\n  addEffect(FX_MODE_CANDLE, &mode_candle, _data_FX_MODE_CANDLE);\n  addEffect(FX_MODE_BOUNCINGBALLS, &mode_bouncing_balls, _data_FX_MODE_BOUNCINGBALLS);\n  addEffect(FX_MODE_POPCORN, &mode_popcorn, _data_FX_MODE_POPCORN);\n  addEffect(FX_MODE_DRIP, &mode_drip, _data_FX_MODE_DRIP);\n  addEffect(FX_MODE_SINELON, &mode_sinelon, _data_FX_MODE_SINELON);\n  addEffect(FX_MODE_SINELON_DUAL, &mode_sinelon_dual, _data_FX_MODE_SINELON_DUAL);\n  addEffect(FX_MODE_SINELON_RAINBOW, &mode_sinelon_rainbow, _data_FX_MODE_SINELON_RAINBOW);\n  addEffect(FX_MODE_PLASMA, &mode_plasma, _data_FX_MODE_PLASMA);\n  addEffect(FX_MODE_PERCENT, &mode_percent, _data_FX_MODE_PERCENT);\n  addEffect(FX_MODE_RIPPLE_RAINBOW, &mode_ripple_rainbow, _data_FX_MODE_RIPPLE_RAINBOW);\n  addEffect(FX_MODE_HEARTBEAT, &mode_heartbeat, _data_FX_MODE_HEARTBEAT);\n  addEffect(FX_MODE_PACIFICA, &mode_pacifica, _data_FX_MODE_PACIFICA);\n  addEffect(FX_MODE_CANDLE_MULTI, &mode_candle_multi, _data_FX_MODE_CANDLE_MULTI);\n  addEffect(FX_MODE_SUNRISE, &mode_sunrise, _data_FX_MODE_SUNRISE);\n  addEffect(FX_MODE_PHASED, &mode_phased, _data_FX_MODE_PHASED);\n  addEffect(FX_MODE_TWINKLEUP, &mode_twinkleup, _data_FX_MODE_TWINKLEUP);\n  addEffect(FX_MODE_NOISEPAL, &mode_noisepal, _data_FX_MODE_NOISEPAL);\n  addEffect(FX_MODE_SINEWAVE, &mode_sinewave, _data_FX_MODE_SINEWAVE);\n  addEffect(FX_MODE_PHASEDNOISE, &mode_phased_noise, _data_FX_MODE_PHASEDNOISE);\n  addEffect(FX_MODE_FLOW, &mode_flow, _data_FX_MODE_FLOW);\n  addEffect(FX_MODE_CHUNCHUN, &mode_chunchun, _data_FX_MODE_CHUNCHUN);  \n  addEffect(FX_MODE_WASHING_MACHINE, &mode_washing_machine, _data_FX_MODE_WASHING_MACHINE);\n  addEffect(FX_MODE_BLENDS, &mode_blends, _data_FX_MODE_BLENDS);\n  addEffect(FX_MODE_TV_SIMULATOR, &mode_tv_simulator, _data_FX_MODE_TV_SIMULATOR);\n  addEffect(FX_MODE_DYNAMIC_SMOOTH, &mode_dynamic_smooth, _data_FX_MODE_DYNAMIC_SMOOTH);\n  addEffect(FX_MODE_PACMAN, &mode_pacman, _data_FX_MODE_PACMAN);\n\n  // --- 1D audio effects ---\n  addEffect(FX_MODE_PIXELS, &mode_pixels, _data_FX_MODE_PIXELS);\n  addEffect(FX_MODE_PIXELWAVE, &mode_pixelwave, _data_FX_MODE_PIXELWAVE);\n  addEffect(FX_MODE_JUGGLES, &mode_juggles, _data_FX_MODE_JUGGLES);\n  addEffect(FX_MODE_MATRIPIX, &mode_matripix, _data_FX_MODE_MATRIPIX);\n  addEffect(FX_MODE_GRAVIMETER, &mode_gravimeter, _data_FX_MODE_GRAVIMETER);\n  addEffect(FX_MODE_PLASMOID, &mode_plasmoid, _data_FX_MODE_PLASMOID);\n  addEffect(FX_MODE_PUDDLES, &mode_puddles, _data_FX_MODE_PUDDLES);\n  addEffect(FX_MODE_MIDNOISE, &mode_midnoise, _data_FX_MODE_MIDNOISE);\n  addEffect(FX_MODE_NOISEMETER, &mode_noisemeter, _data_FX_MODE_NOISEMETER);\n  addEffect(FX_MODE_FREQWAVE, &mode_freqwave, _data_FX_MODE_FREQWAVE);\n  addEffect(FX_MODE_FREQMATRIX, &mode_freqmatrix, _data_FX_MODE_FREQMATRIX);\n  addEffect(FX_MODE_WATERFALL, &mode_waterfall, _data_FX_MODE_WATERFALL);\n  addEffect(FX_MODE_FREQPIXELS, &mode_freqpixels, _data_FX_MODE_FREQPIXELS);\n  addEffect(FX_MODE_NOISEFIRE, &mode_noisefire, _data_FX_MODE_NOISEFIRE);\n  addEffect(FX_MODE_PUDDLEPEAK, &mode_puddlepeak, _data_FX_MODE_PUDDLEPEAK);\n  addEffect(FX_MODE_NOISEMOVE, &mode_noisemove, _data_FX_MODE_NOISEMOVE);\n  addEffect(FX_MODE_PERLINMOVE, &mode_perlinmove, _data_FX_MODE_PERLINMOVE);\n  addEffect(FX_MODE_RIPPLEPEAK, &mode_ripplepeak, _data_FX_MODE_RIPPLEPEAK);\n  addEffect(FX_MODE_FREQMAP, &mode_freqmap, _data_FX_MODE_FREQMAP);\n  addEffect(FX_MODE_GRAVCENTER, &mode_gravcenter, _data_FX_MODE_GRAVCENTER);\n  addEffect(FX_MODE_GRAVCENTRIC, &mode_gravcentric, _data_FX_MODE_GRAVCENTRIC);\n  addEffect(FX_MODE_GRAVFREQ, &mode_gravfreq, _data_FX_MODE_GRAVFREQ);\n  addEffect(FX_MODE_DJLIGHT, &mode_DJLight, _data_FX_MODE_DJLIGHT);\n  addEffect(FX_MODE_BLURZ, &mode_blurz, _data_FX_MODE_BLURZ);\n  addEffect(FX_MODE_FLOWSTRIPE, &mode_FlowStripe, _data_FX_MODE_FLOWSTRIPE);\n  addEffect(FX_MODE_WAVESINS, &mode_wavesins, _data_FX_MODE_WAVESINS);\n  addEffect(FX_MODE_ROCKTAVES, &mode_rocktaves, _data_FX_MODE_ROCKTAVES);\n  addEffect(FX_MODE_SHIMMER, &mode_shimmer, _data_FX_MODE_SHIMMER);\n\n  // --- 2D  effects ---\n#ifndef WLED_DISABLE_2D\n  addEffect(FX_MODE_2DPLASMAROTOZOOM, &mode_2Dplasmarotozoom, _data_FX_MODE_2DPLASMAROTOZOOM);\n  addEffect(FX_MODE_2DSPACESHIPS, &mode_2Dspaceships, _data_FX_MODE_2DSPACESHIPS);\n  addEffect(FX_MODE_2DCRAZYBEES, &mode_2Dcrazybees, _data_FX_MODE_2DCRAZYBEES);\n\n  #ifdef WLED_PS_DONT_REPLACE_2D_FX\n  addEffect(FX_MODE_2DGHOSTRIDER, &mode_2Dghostrider, _data_FX_MODE_2DGHOSTRIDER);\n  addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS);\n  #endif\n\n  addEffect(FX_MODE_2DSCROLLTEXT, &mode_2Dscrollingtext, _data_FX_MODE_2DSCROLLTEXT);\n  addEffect(FX_MODE_2DDRIFTROSE, &mode_2Ddriftrose, _data_FX_MODE_2DDRIFTROSE);\n  addEffect(FX_MODE_2DDISTORTIONWAVES, &mode_2Ddistortionwaves, _data_FX_MODE_2DDISTORTIONWAVES);\n  addEffect(FX_MODE_2DGEQ, &mode_2DGEQ, _data_FX_MODE_2DGEQ); // audio\n  addEffect(FX_MODE_2DNOISE, &mode_2Dnoise, _data_FX_MODE_2DNOISE);\n  addEffect(FX_MODE_2DFIRENOISE, &mode_2Dfirenoise, _data_FX_MODE_2DFIRENOISE);\n  addEffect(FX_MODE_2DSQUAREDSWIRL, &mode_2Dsquaredswirl, _data_FX_MODE_2DSQUAREDSWIRL);\n\n  //non audio\n  addEffect(FX_MODE_2DDNA, &mode_2Ddna, _data_FX_MODE_2DDNA);\n  addEffect(FX_MODE_2DMATRIX, &mode_2Dmatrix, _data_FX_MODE_2DMATRIX);\n  addEffect(FX_MODE_2DMETABALLS, &mode_2Dmetaballs, _data_FX_MODE_2DMETABALLS);\n  addEffect(FX_MODE_2DFUNKYPLANK, &mode_2DFunkyPlank, _data_FX_MODE_2DFUNKYPLANK); // audio\n  addEffect(FX_MODE_2DPULSER, &mode_2DPulser, _data_FX_MODE_2DPULSER);\n  addEffect(FX_MODE_2DDRIFT, &mode_2DDrift, _data_FX_MODE_2DDRIFT);\n  addEffect(FX_MODE_2DWAVERLY, &mode_2DWaverly, _data_FX_MODE_2DWAVERLY); // audio\n  addEffect(FX_MODE_2DSUNRADIATION, &mode_2DSunradiation, _data_FX_MODE_2DSUNRADIATION);\n  addEffect(FX_MODE_2DCOLOREDBURSTS, &mode_2DColoredBursts, _data_FX_MODE_2DCOLOREDBURSTS);\n  addEffect(FX_MODE_2DJULIA, &mode_2DJulia, _data_FX_MODE_2DJULIA);\n  addEffect(FX_MODE_2DGAMEOFLIFE, &mode_2Dgameoflife, _data_FX_MODE_2DGAMEOFLIFE);\n  addEffect(FX_MODE_2DTARTAN, &mode_2Dtartan, _data_FX_MODE_2DTARTAN);\n  addEffect(FX_MODE_2DPOLARLIGHTS, &mode_2DPolarLights, _data_FX_MODE_2DPOLARLIGHTS);\n  addEffect(FX_MODE_2DSWIRL, &mode_2DSwirl, _data_FX_MODE_2DSWIRL); // audio\n  addEffect(FX_MODE_2DLISSAJOUS, &mode_2DLissajous, _data_FX_MODE_2DLISSAJOUS);\n  addEffect(FX_MODE_2DFRIZZLES, &mode_2DFrizzles, _data_FX_MODE_2DFRIZZLES);\n  addEffect(FX_MODE_2DPLASMABALL, &mode_2DPlasmaball, _data_FX_MODE_2DPLASMABALL);\n  addEffect(FX_MODE_2DHIPHOTIC, &mode_2DHiphotic, _data_FX_MODE_2DHIPHOTIC);\n  addEffect(FX_MODE_2DSINDOTS, &mode_2DSindots, _data_FX_MODE_2DSINDOTS);\n  addEffect(FX_MODE_2DDNASPIRAL, &mode_2DDNASpiral, _data_FX_MODE_2DDNASPIRAL);\n  addEffect(FX_MODE_2DBLACKHOLE, &mode_2DBlackHole, _data_FX_MODE_2DBLACKHOLE);\n  addEffect(FX_MODE_2DSOAP, &mode_2Dsoap, _data_FX_MODE_2DSOAP);\n  addEffect(FX_MODE_2DOCTOPUS, &mode_2Doctopus, _data_FX_MODE_2DOCTOPUS);\n  addEffect(FX_MODE_2DWAVINGCELL, &mode_2Dwavingcell, _data_FX_MODE_2DWAVINGCELL);\n  addEffect(FX_MODE_2DAKEMI, &mode_2DAkemi, _data_FX_MODE_2DAKEMI); // audio\n\n#ifndef WLED_DISABLE_PARTICLESYSTEM2D\n  addEffect(FX_MODE_PARTICLEVOLCANO, &mode_particlevolcano, _data_FX_MODE_PARTICLEVOLCANO);\n  addEffect(FX_MODE_PARTICLEFIRE, &mode_particlefire, _data_FX_MODE_PARTICLEFIRE);\n  addEffect(FX_MODE_PARTICLEFIREWORKS, &mode_particlefireworks, _data_FX_MODE_PARTICLEFIREWORKS);\n  addEffect(FX_MODE_PARTICLEVORTEX, &mode_particlevortex, _data_FX_MODE_PARTICLEVORTEX);\n  addEffect(FX_MODE_PARTICLEPERLIN, &mode_particleperlin, _data_FX_MODE_PARTICLEPERLIN);\n  addEffect(FX_MODE_PARTICLEPIT, &mode_particlepit, _data_FX_MODE_PARTICLEPIT);\n  addEffect(FX_MODE_PARTICLEBOX, &mode_particlebox, _data_FX_MODE_PARTICLEBOX);\n  addEffect(FX_MODE_PARTICLEATTRACTOR, &mode_particleattractor, _data_FX_MODE_PARTICLEATTRACTOR); // 872 bytes\n  addEffect(FX_MODE_PARTICLEIMPACT, &mode_particleimpact, _data_FX_MODE_PARTICLEIMPACT);\n  addEffect(FX_MODE_PARTICLEWATERFALL, &mode_particlewaterfall, _data_FX_MODE_PARTICLEWATERFALL);\n  addEffect(FX_MODE_PARTICLESPRAY, &mode_particlespray, _data_FX_MODE_PARTICLESPRAY);\n  addEffect(FX_MODE_PARTICLESGEQ, &mode_particleGEQ, _data_FX_MODE_PARTICLEGEQ);\n  addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECIRCULARGEQ);\n  addEffect(FX_MODE_PARTICLEGHOSTRIDER, &mode_particleghostrider, _data_FX_MODE_PARTICLEGHOSTRIDER);\n  addEffect(FX_MODE_PARTICLEBLOBS, &mode_particleblobs, _data_FX_MODE_PARTICLEBLOBS);\n  addEffect(FX_MODE_PARTICLEGALAXY, &mode_particlegalaxy, _data_FX_MODE_PARTICLEGALAXY);\n#endif // WLED_DISABLE_PARTICLESYSTEM2D\n#endif // WLED_DISABLE_2D\n\n#ifndef WLED_DISABLE_PARTICLESYSTEM1D\naddEffect(FX_MODE_PSDRIP, &mode_particleDrip, _data_FX_MODE_PARTICLEDRIP);\naddEffect(FX_MODE_PSPINBALL, &mode_particlePinball, _data_FX_MODE_PSPINBALL); //potential replacement for: bouncing balls, rollingballs, popcorn\naddEffect(FX_MODE_PSDANCINGSHADOWS, &mode_particleDancingShadows, _data_FX_MODE_PARTICLEDANCINGSHADOWS);\naddEffect(FX_MODE_PSFIREWORKS1D, &mode_particleFireworks1D, _data_FX_MODE_PS_FIREWORKS1D);\naddEffect(FX_MODE_PSSPARKLER, &mode_particleSparkler, _data_FX_MODE_PS_SPARKLER);\naddEffect(FX_MODE_PSHOURGLASS, &mode_particleHourglass, _data_FX_MODE_PS_HOURGLASS);\naddEffect(FX_MODE_PS1DSPRAY, &mode_particle1Dspray, _data_FX_MODE_PS_1DSPRAY);\naddEffect(FX_MODE_PSBALANCE, &mode_particleBalance, _data_FX_MODE_PS_BALANCE);\naddEffect(FX_MODE_PSCHASE, &mode_particleChase, _data_FX_MODE_PS_CHASE);\naddEffect(FX_MODE_PSSTARBURST, &mode_particleStarburst, _data_FX_MODE_PS_STARBURST);\naddEffect(FX_MODE_PS1DGEQ, &mode_particle1DGEQ, _data_FX_MODE_PS_1D_GEQ);\naddEffect(FX_MODE_PSFIRE1D, &mode_particleFire1D, _data_FX_MODE_PS_FIRE1D);\naddEffect(FX_MODE_PS1DSONICSTREAM, &mode_particle1DsonicStream, _data_FX_MODE_PS_SONICSTREAM);\naddEffect(FX_MODE_PS1DSONICBOOM, &mode_particle1DsonicBoom, _data_FX_MODE_PS_SONICBOOM);\naddEffect(FX_MODE_PS1DSPRINGY, &mode_particleSpringy, _data_FX_MODE_PS_SPRINGY);\n#endif // WLED_DISABLE_PARTICLESYSTEM1D\n\n}\n"
  },
  {
    "path": "wled00/FX.h",
    "content": "#pragma once\n/*\n  WS2812FX.h - Library for WS2812 LED effects.\n  Harm Aldick - 2016\n  www.aldick.org\n\n  Copyright (c) 2016  Harm Aldick\n  Licensed under the EUPL v. 1.2 or later\n  Adapted from code originally licensed under the MIT license\n\n  Modified for WLED\n\n  Segment class/struct (c) 2022 Blaz Kristan (@blazoncek)\n*/\n\n#ifndef WS2812FX_h\n#define WS2812FX_h\n\n#include <vector>\n#include \"wled.h\"\n\n#ifdef WLED_DEBUG\n  // enable additional debug output\n  #if defined(WLED_DEBUG_HOST)\n    #include \"net_debug.h\"\n    #define DEBUGOUT NetDebug\n  #else\n    #define DEBUGOUT Serial\n  #endif\n  #define DEBUGFX_PRINT(x) DEBUGOUT.print(x)\n  #define DEBUGFX_PRINTLN(x) DEBUGOUT.println(x)\n  #define DEBUGFX_PRINTF(x...) DEBUGOUT.printf(x)\n  #define DEBUGFX_PRINTF_P(x...) DEBUGOUT.printf_P(x)\n#else\n  #define DEBUGFX_PRINT(x)\n  #define DEBUGFX_PRINTLN(x)\n  #define DEBUGFX_PRINTF(x...)\n  #define DEBUGFX_PRINTF_P(x...)\n#endif\n\n#define FASTLED_INTERNAL //remove annoying pragma messages\n#define USE_GET_MILLISECOND_TIMER\n#include \"FastLED.h\"\n\n#define DEFAULT_BRIGHTNESS (uint8_t)127\n#define DEFAULT_MODE       (uint8_t)0\n#define DEFAULT_SPEED      (uint8_t)128\n#define DEFAULT_INTENSITY  (uint8_t)128\n#define DEFAULT_COLOR      (uint32_t)0xFFAA00\n#define DEFAULT_C1         (uint8_t)128\n#define DEFAULT_C2         (uint8_t)128\n#define DEFAULT_C3         (uint8_t)16\n\n#ifndef MIN\n#define MIN(a,b) ((a)<(b)?(a):(b))\n#endif\n#ifndef MAX\n#define MAX(a,b) ((a)>(b)?(a):(b))\n#endif\n\n//color mangling macros\n#ifndef RGBW32\n#define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b))))\n#endif\n\nextern bool realtimeRespectLedMaps; // used in getMappedPixelIndex()\nextern byte realtimeMode;           // used in getMappedPixelIndex()\n\n/* Not used in all effects yet */\n#define WLED_FPS         42\n#define FRAMETIME_FIXED  (1000/WLED_FPS)\n#define FRAMETIME        strip.getFrameTime()\n#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S2)\n  #define MIN_FRAME_DELAY  2                                              // minimum wait between repaints, to keep other functions like WiFi alive \n#elif defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3)\n  #define MIN_FRAME_DELAY  3                                              // S2/C3 are slower than normal esp32, and only have one core\n#else\n  #define MIN_FRAME_DELAY  8                                              // 8266 legacy MIN_SHOW_DELAY\n#endif\n#define FPS_UNLIMITED    0\n\n// FPS calculation (can be defined as compile flag for debugging)\n#ifndef FPS_CALC_AVG\n#define FPS_CALC_AVG 7 // average FPS calculation over this many frames (moving average)\n#endif\n#ifndef FPS_MULTIPLIER\n#define FPS_MULTIPLIER 1 // dev option: multiplier to get sub-frame FPS without floats\n#endif\n#define FPS_CALC_SHIFT 7 // bit shift for fixed point math\n\n// heap memory limit for effects data, pixel buffers try to reserve it if PSRAM is available\n#ifdef ESP8266\n  #define MAX_NUM_SEGMENTS  16\n  /* How much data bytes all segments combined may allocate */\n  #define MAX_SEGMENT_DATA  (6*1024) // 6k by default\n#elif defined(CONFIG_IDF_TARGET_ESP32S2)\n  #define MAX_NUM_SEGMENTS  32\n  #define MAX_SEGMENT_DATA  (20*1024) // 20k by default (S2 is short on free RAM), limit does not apply if PSRAM is available\n#else\n  #ifdef BOARD_HAS_PSRAM\n    #define MAX_NUM_SEGMENTS  64\n  #else\n    #define MAX_NUM_SEGMENTS  32\n  #endif\n  #define MAX_SEGMENT_DATA  (64*1024) // 64k by default, limit does not apply if PSRAM is available\n#endif\n\n/* How much data bytes each segment should max allocate to leave enough space for other segments,\n  assuming each segment uses the same amount of data. 256 for ESP8266, 640 for ESP32. */\n#define FAIR_DATA_PER_SEG (MAX_SEGMENT_DATA / MAX_NUM_SEGMENTS)\n\n#define MIN_SHOW_DELAY   (_frametime < 16 ? 8 : 15)\n\n#define NUM_COLORS       3 /* number of colors per segment */\n#define SEGMENT          (*strip._currentSegment)\n#define SEGENV           (*strip._currentSegment)\n#define SEGCOLOR(x)      Segment::getCurrentColor(x)\n#define SEGPALETTE       Segment::getCurrentPalette()\n#define SEGLEN           Segment::vLength()\n#define SEG_W            Segment::vWidth()\n#define SEG_H            Segment::vHeight()\n#define SPEED_FORMULA_L  (5U + (50U*(255U - SEGMENT.speed))/SEGLEN)\n\n// some common colors\n#define RED        (uint32_t)0xFF0000\n#define GREEN      (uint32_t)0x00FF00\n#define BLUE       (uint32_t)0x0000FF\n#define WHITE      (uint32_t)0xFFFFFF\n#define BLACK      (uint32_t)0x000000\n#define YELLOW     (uint32_t)0xFFFF00\n#define CYAN       (uint32_t)0x00FFFF\n#define MAGENTA    (uint32_t)0xFF00FF\n#define PURPLE     (uint32_t)0x400080\n#define ORANGE     (uint32_t)0xFF3000\n#define PINK       (uint32_t)0xFF1493\n#define GREY       (uint32_t)0x808080\n#define GRAY       GREY\n#define DARKGREY   (uint32_t)0x333333\n#define DARKGRAY   DARKGREY\n#define ULTRAWHITE (uint32_t)0xFFFFFFFF\n#define DARKSLATEGRAY (uint32_t)0x2F4F4F\n#define DARKSLATEGREY DARKSLATEGRAY\n\n// segment options\n#define NO_OPTIONS   (uint16_t)0x0000\n#define TRANSPOSED   (uint16_t)0x0100 // rotated 90deg & reversed\n#define MIRROR_Y_2D  (uint16_t)0x0080\n#define REVERSE_Y_2D (uint16_t)0x0040\n#define RESET_REQ    (uint16_t)0x0020\n#define FROZEN       (uint16_t)0x0010\n#define MIRROR       (uint16_t)0x0008\n#define SEGMENT_ON   (uint16_t)0x0004\n#define REVERSE      (uint16_t)0x0002\n#define SELECTED     (uint16_t)0x0001\n\n#define FX_MODE_STATIC                   0\n#define FX_MODE_BLINK                    1\n#define FX_MODE_BREATH                   2\n#define FX_MODE_COLOR_WIPE               3\n#define FX_MODE_COLOR_WIPE_RANDOM        4\n#define FX_MODE_RANDOM_COLOR             5\n#define FX_MODE_COLOR_SWEEP              6\n#define FX_MODE_DYNAMIC                  7\n#define FX_MODE_RAINBOW                  8\n#define FX_MODE_RAINBOW_CYCLE            9\n#define FX_MODE_SCAN                    10\n#define FX_MODE_DUAL_SCAN               11  // candidate for removal (use Scan)\n#define FX_MODE_FADE                    12\n#define FX_MODE_THEATER_CHASE           13\n#define FX_MODE_THEATER_CHASE_RAINBOW   14  // candidate for removal (use Theater)\n#define FX_MODE_RUNNING_LIGHTS          15\n#define FX_MODE_SAW                     16\n#define FX_MODE_TWINKLE                 17\n#define FX_MODE_DISSOLVE                18\n#define FX_MODE_DISSOLVE_RANDOM         19  // candidate for removal (use Dissolve with with check 3)\n#define FX_MODE_SPARKLE                 20\n#define FX_MODE_FLASH_SPARKLE           21\n#define FX_MODE_HYPER_SPARKLE           22\n#define FX_MODE_STROBE                  23\n#define FX_MODE_STROBE_RAINBOW          24\n#define FX_MODE_MULTI_STROBE            25\n#define FX_MODE_BLINK_RAINBOW           26\n#define FX_MODE_ANDROID                 27\n#define FX_MODE_CHASE_COLOR             28\n#define FX_MODE_CHASE_RANDOM            29\n#define FX_MODE_CHASE_RAINBOW           30\n#define FX_MODE_CHASE_FLASH             31\n#define FX_MODE_CHASE_FLASH_RANDOM      32\n#define FX_MODE_CHASE_RAINBOW_WHITE     33\n#define FX_MODE_COLORFUL                34\n#define FX_MODE_TRAFFIC_LIGHT           35\n#define FX_MODE_COLOR_SWEEP_RANDOM      36\n#define FX_MODE_RUNNING_COLOR           37  // candidate for removal (use Theater)\n#define FX_MODE_AURORA                  38\n#define FX_MODE_RUNNING_RANDOM          39\n#define FX_MODE_LARSON_SCANNER          40\n#define FX_MODE_COMET                   41\n#define FX_MODE_FIREWORKS               42\n#define FX_MODE_RAIN                    43\n#define FX_MODE_TETRIX                  44  //was Merry Christmas prior to 0.12.0 (use \"Chase 2\" with Red/Green)\n#define FX_MODE_FIRE_FLICKER            45\n#define FX_MODE_GRADIENT                46\n#define FX_MODE_LOADING                 47\n#define FX_MODE_ROLLINGBALLS            48  //was Police before 0.14\n#define FX_MODE_FAIRY                   49  //was Police All prior to 0.13.0-b6 (use \"Two Dots\" with Red/Blue and full intensity)\n#define FX_MODE_TWO_DOTS                50\n#define FX_MODE_FAIRYTWINKLE            51  //was Two Areas prior to 0.13.0-b6 (use \"Two Dots\" with full intensity)\n#define FX_MODE_RUNNING_DUAL            52  // candidate for removal (use Running)\n#define FX_MODE_IMAGE                   53\n#define FX_MODE_TRICOLOR_CHASE          54\n#define FX_MODE_TRICOLOR_WIPE           55\n#define FX_MODE_TRICOLOR_FADE           56\n#define FX_MODE_LIGHTNING               57\n#define FX_MODE_ICU                     58\n#define FX_MODE_MULTI_COMET             59\n#define FX_MODE_DUAL_LARSON_SCANNER     60  // candidate for removal (use Scanner with with check 1)\n#define FX_MODE_RANDOM_CHASE            61\n#define FX_MODE_OSCILLATE               62\n#define FX_MODE_PRIDE_2015              63\n#define FX_MODE_JUGGLE                  64\n#define FX_MODE_PALETTE                 65\n#define FX_MODE_FIRE_2012               66\n#define FX_MODE_COLORWAVES              67\n#define FX_MODE_BPM                     68\n#define FX_MODE_FILLNOISE8              69\n#define FX_MODE_NOISE16_1               70\n#define FX_MODE_NOISE16_2               71\n#define FX_MODE_NOISE16_3               72\n#define FX_MODE_NOISE16_4               73\n#define FX_MODE_COLORTWINKLE            74\n#define FX_MODE_LAKE                    75\n#define FX_MODE_METEOR                  76\n//#define FX_MODE_METEOR_SMOOTH           77  // replaced by Meteor\n#define FX_MODE_COPY                    77\n#define FX_MODE_RAILWAY                 78\n#define FX_MODE_RIPPLE                  79\n#define FX_MODE_TWINKLEFOX              80\n#define FX_MODE_TWINKLECAT              81\n#define FX_MODE_HALLOWEEN_EYES          82\n#define FX_MODE_STATIC_PATTERN          83\n#define FX_MODE_TRI_STATIC_PATTERN      84\n#define FX_MODE_SPOTS                   85\n#define FX_MODE_SPOTS_FADE              86\n#define FX_MODE_GLITTER                 87\n#define FX_MODE_CANDLE                  88\n#define FX_MODE_STARBURST               89\n#define FX_MODE_EXPLODING_FIREWORKS     90\n#define FX_MODE_BOUNCINGBALLS           91\n#define FX_MODE_SINELON                 92\n#define FX_MODE_SINELON_DUAL            93  // candidate for removal (use sinelon)\n#define FX_MODE_SINELON_RAINBOW         94  // candidate for removal (use sinelon)\n#define FX_MODE_POPCORN                 95\n#define FX_MODE_DRIP                    96\n#define FX_MODE_PLASMA                  97\n#define FX_MODE_PERCENT                 98\n#define FX_MODE_RIPPLE_RAINBOW          99  // candidate for removal (use ripple)\n#define FX_MODE_HEARTBEAT              100\n#define FX_MODE_PACIFICA               101\n#define FX_MODE_CANDLE_MULTI           102  // candidate for removal (use candle with multi select)\n#define FX_MODE_SOLID_GLITTER          103  // candidate for removal (use glitter)\n#define FX_MODE_SUNRISE                104\n#define FX_MODE_PHASED                 105\n#define FX_MODE_TWINKLEUP              106\n#define FX_MODE_NOISEPAL               107\n#define FX_MODE_SINEWAVE               108\n#define FX_MODE_PHASEDNOISE            109\n#define FX_MODE_FLOW                   110\n#define FX_MODE_CHUNCHUN               111\n#define FX_MODE_DANCING_SHADOWS        112\n#define FX_MODE_WASHING_MACHINE        113\n#define FX_MODE_2DPLASMAROTOZOOM       114 // was Candy Cane prior to 0.14 (use Chase 2)\n#define FX_MODE_BLENDS                 115\n#define FX_MODE_TV_SIMULATOR           116\n#define FX_MODE_DYNAMIC_SMOOTH         117 // candidate for removal (check3 in dynamic)\n\n// new 0.14 2D effects\n#define FX_MODE_2DSPACESHIPS           118 //gap fill\n#define FX_MODE_2DCRAZYBEES            119 //gap fill\n#define FX_MODE_2DGHOSTRIDER           120 //gap fill\n#define FX_MODE_2DBLOBS                121 //gap fill\n#define FX_MODE_2DSCROLLTEXT           122 //gap fill\n#define FX_MODE_2DDRIFTROSE            123 //gap fill\n#define FX_MODE_2DDISTORTIONWAVES      124 //gap fill\n#define FX_MODE_2DSOAP                 125 //gap fill\n#define FX_MODE_2DOCTOPUS              126 //gap fill\n#define FX_MODE_2DWAVINGCELL           127 //gap fill\n\n// WLED-SR effects (SR compatible IDs !!!)\n#define FX_MODE_PIXELS                 128\n#define FX_MODE_PIXELWAVE              129\n#define FX_MODE_JUGGLES                130\n#define FX_MODE_MATRIPIX               131\n#define FX_MODE_GRAVIMETER             132\n#define FX_MODE_PLASMOID               133\n#define FX_MODE_PUDDLES                134\n#define FX_MODE_MIDNOISE               135\n#define FX_MODE_NOISEMETER             136\n#define FX_MODE_FREQWAVE               137\n#define FX_MODE_FREQMATRIX             138\n#define FX_MODE_2DGEQ                  139\n#define FX_MODE_WATERFALL              140\n#define FX_MODE_FREQPIXELS             141\n#define FX_MODE_BINMAP                 142\n#define FX_MODE_NOISEFIRE              143\n#define FX_MODE_PUDDLEPEAK             144\n#define FX_MODE_NOISEMOVE              145\n#define FX_MODE_2DNOISE                146\n#define FX_MODE_PERLINMOVE             147\n#define FX_MODE_RIPPLEPEAK             148\n#define FX_MODE_2DFIRENOISE            149\n#define FX_MODE_2DSQUAREDSWIRL         150\n// #define FX_MODE_2DFIRE2012             151\n#define FX_MODE_PACMAN                 151 // gap fill (non-SR). Do NOT renumber; SR-ID range must remain stable.\n#define FX_MODE_2DDNA                  152\n#define FX_MODE_2DMATRIX               153\n#define FX_MODE_2DMETABALLS            154\n#define FX_MODE_FREQMAP                155\n#define FX_MODE_GRAVCENTER             156\n#define FX_MODE_GRAVCENTRIC            157\n#define FX_MODE_GRAVFREQ               158\n#define FX_MODE_DJLIGHT                159\n#define FX_MODE_2DFUNKYPLANK           160\n//#define FX_MODE_2DCENTERBARS           161\n#define FX_MODE_SHIMMER                161  // gap fill, non SR 1D effect\n#define FX_MODE_2DPULSER               162\n#define FX_MODE_BLURZ                  163\n#define FX_MODE_2DDRIFT                164\n#define FX_MODE_2DWAVERLY              165\n#define FX_MODE_2DSUNRADIATION         166\n#define FX_MODE_2DCOLOREDBURSTS        167\n#define FX_MODE_2DJULIA                168\n// #define FX_MODE_2DPOOLNOISE            169 //have been removed in WLED SR in the past because of low mem but should be added back\n// #define FX_MODE_2DTWISTER              170 //have been removed in WLED SR in the past because of low mem but should be added back\n// #define FX_MODE_2DCAELEMENTATY         171 //have been removed in WLED SR in the past because of low mem but should be added back\n#define FX_MODE_2DGAMEOFLIFE           172\n#define FX_MODE_2DTARTAN               173\n#define FX_MODE_2DPOLARLIGHTS          174\n#define FX_MODE_2DSWIRL                175\n#define FX_MODE_2DLISSAJOUS            176\n#define FX_MODE_2DFRIZZLES             177\n#define FX_MODE_2DPLASMABALL           178\n#define FX_MODE_FLOWSTRIPE             179\n#define FX_MODE_2DHIPHOTIC             180\n#define FX_MODE_2DSINDOTS              181\n#define FX_MODE_2DDNASPIRAL            182\n#define FX_MODE_2DBLACKHOLE            183\n#define FX_MODE_WAVESINS               184\n#define FX_MODE_ROCKTAVES              185\n#define FX_MODE_2DAKEMI                186\n\n#define FX_MODE_PARTICLEVOLCANO        187\n#define FX_MODE_PARTICLEFIRE           188\n#define FX_MODE_PARTICLEFIREWORKS      189\n#define FX_MODE_PARTICLEVORTEX         190\n#define FX_MODE_PARTICLEPERLIN         191\n#define FX_MODE_PARTICLEPIT            192\n#define FX_MODE_PARTICLEBOX            193\n#define FX_MODE_PARTICLEATTRACTOR      194\n#define FX_MODE_PARTICLEIMPACT         195\n#define FX_MODE_PARTICLEWATERFALL      196\n#define FX_MODE_PARTICLESPRAY          197\n#define FX_MODE_PARTICLESGEQ           198\n#define FX_MODE_PARTICLECENTERGEQ      199\n#define FX_MODE_PARTICLEGHOSTRIDER     200\n#define FX_MODE_PARTICLEBLOBS          201\n#define FX_MODE_PSDRIP                 202\n#define FX_MODE_PSPINBALL              203\n#define FX_MODE_PSDANCINGSHADOWS       204\n#define FX_MODE_PSFIREWORKS1D          205\n#define FX_MODE_PSSPARKLER             206\n#define FX_MODE_PSHOURGLASS            207\n#define FX_MODE_PS1DSPRAY              208\n#define FX_MODE_PSBALANCE              209\n#define FX_MODE_PSCHASE                210\n#define FX_MODE_PSSTARBURST            211\n#define FX_MODE_PS1DGEQ                212\n#define FX_MODE_PSFIRE1D               213\n#define FX_MODE_PS1DSONICSTREAM        214\n#define FX_MODE_PS1DSONICBOOM          215\n#define FX_MODE_PS1DSPRINGY            216\n#define FX_MODE_PARTICLEGALAXY         217\n#define FX_MODE_COLORCLOUDS            218\n#define MODE_COUNT                     219\n\n\n#define BLEND_STYLE_FADE            0x00  // universal\n#define BLEND_STYLE_FAIRY_DUST      0x01  // universal\n#define BLEND_STYLE_SWIPE_RIGHT     0x02  // 1D or 2D\n#define BLEND_STYLE_SWIPE_LEFT      0x03  // 1D or 2D\n#define BLEND_STYLE_OUTSIDE_IN      0x04  // 1D or 2D\n#define BLEND_STYLE_INSIDE_OUT      0x05  // 1D or 2D\n#define BLEND_STYLE_SWIPE_UP        0x06  // 2D\n#define BLEND_STYLE_SWIPE_DOWN      0x07  // 2D\n#define BLEND_STYLE_OPEN_H          0x08  // 2D\n#define BLEND_STYLE_OPEN_V          0x09  // 2D\n#define BLEND_STYLE_SWIPE_TL        0x0A  // 2D\n#define BLEND_STYLE_SWIPE_TR        0x0B  // 2D\n#define BLEND_STYLE_SWIPE_BR        0x0C  // 2D\n#define BLEND_STYLE_SWIPE_BL        0x0D  // 2D\n#define BLEND_STYLE_CIRCULAR_OUT    0x0E  // 2D\n#define BLEND_STYLE_CIRCULAR_IN     0x0F  // 2D\n// as there are many push variants to optimise if statements they are groupped together\n#define BLEND_STYLE_PUSH_RIGHT      0x10  // 1D or 2D (& 0b00010000)\n#define BLEND_STYLE_PUSH_LEFT       0x11  // 1D or 2D (& 0b00010000)\n#define BLEND_STYLE_PUSH_UP         0x12  // 2D (& 0b00010000)\n#define BLEND_STYLE_PUSH_DOWN       0x13  // 2D (& 0b00010000)\n#define BLEND_STYLE_PUSH_TL         0x14  // 2D (& 0b00010000)\n#define BLEND_STYLE_PUSH_TR         0x15  // 2D (& 0b00010000)\n#define BLEND_STYLE_PUSH_BR         0x16  // 2D (& 0b00010000)\n#define BLEND_STYLE_PUSH_BL         0x17  // 2D (& 0b00010000)\n#define BLEND_STYLE_PUSH_MASK       0x10\n#define BLEND_STYLE_COUNT           18\n\n\ntypedef enum mapping1D2D {\n  M12_Pixels = 0,\n  M12_pBar = 1,\n  M12_pArc = 2,\n  M12_pCorner = 3,\n  M12_sPinwheel = 4\n} mapping1D2D_t;\n\nclass WS2812FX;\n\n// segment, 76 bytes\nclass Segment {\n  public:\n    uint32_t colors[NUM_COLORS];\n    uint16_t start;   // start index / start X coordinate 2D (left)\n    uint16_t stop;    // stop index / stop X coordinate 2D (right); segment is invalid if stop == 0\n    uint16_t startY;  // start Y coodrinate 2D (top); there should be no more than 255 rows\n    uint16_t stopY;   // stop Y coordinate 2D (bottom); there should be no more than 255 rows\n    uint16_t offset;  // offset for 1D effects (effect will wrap around)\n    union {\n      mutable uint16_t options; //bit pattern: msb first: [transposed mirrorY reverseY] transitional (tbd) paused needspixelstate mirrored on reverse selected\n      struct {\n        mutable bool selected : 1;  //     0 : selected\n        bool    reverse       : 1;  //     1 : reversed\n        mutable bool on       : 1;  //     2 : is On\n        bool    mirror        : 1;  //     3 : mirrored\n        mutable bool freeze   : 1;  //     4 : paused/frozen\n        mutable bool reset    : 1;  //     5 : indicates that Segment runtime requires reset\n        bool    reverse_y     : 1;  //     6 : reversed Y (2D)\n        bool    mirror_y      : 1;  //     7 : mirrored Y (2D)\n        bool    transpose     : 1;  //     8 : transposed (2D, swapped X & Y)\n        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, ...)\n        uint8_t soundSim      : 2;  // 12-13 : 0-3 sound simulation types (\"soft\" & \"hard\" or \"on\"/\"off\")\n        mutable uint8_t set   : 2;  // 14-15 : 0-3 UI segment sets/groups\n      };\n    };\n    uint8_t  grouping, spacing;\n    uint8_t  opacity,  cct;       // 0==1900K, 255==10091K\n    // effect data\n    uint8_t  mode;\n    uint8_t  palette;\n    uint8_t  speed;\n    uint8_t  intensity;\n    uint8_t  custom1,  custom2;   // custom FX parameters/sliders\n    struct {\n      uint8_t custom3 : 5;        // reduced range slider (0-31)\n      bool    check1  : 1;        // checkmark 1\n      bool    check2  : 1;        // checkmark 2\n      bool    check3  : 1;        // checkmark 3\n      //uint8_t blendMode : 4;      // segment blending modes: top, bottom, add, subtract, difference, multiply, divide, lighten, darken, screen, overlay, hardlight, softlight, dodge, burn\n    };\n    uint8_t   blendMode;          // segment blending modes: top, bottom, add, subtract, difference, multiply, divide, lighten, darken, screen, overlay, hardlight, softlight, dodge, burn\n    char     *name;               // segment name\n\n    // runtime data\n    mutable uint32_t step;  // custom \"step\" var\n    mutable uint32_t call;  // call counter\n    mutable uint16_t aux0;  // custom var\n    mutable uint16_t aux1;  // custom var\n    byte     *data; // effect data pointer\n\n    static uint16_t maxWidth, maxHeight;  // these define matrix width & height (max. segment dimensions)\n\n  private:\n    uint32_t *pixels;                 // pixel data\n    unsigned _dataLen;\n    uint8_t  _default_palette;        // palette number that gets assigned to pal0\n    union {\n      mutable uint8_t _capabilities;  // determines segment capabilities in terms of what is available: RGB, W, CCT, manual W, etc.\n      struct {\n        bool    _isRGB    : 1;\n        bool    _hasW     : 1;\n        bool    _isCCT    : 1;\n        bool    _manualW  : 1;\n      };\n    };\n\n    // static variables are use to speed up effect calculations by stashing common pre-calculated values\n    static unsigned      _usedSegmentData;    // amount of data used by all segments\n    static unsigned      _vLength;            // 1D dimension used for current effect\n    static unsigned      _vWidth, _vHeight;   // 2D dimensions used for current effect\n    static uint32_t      _currentColors[NUM_COLORS]; // colors used for current effect (faster access from effect functions)\n    static CRGBPalette16 _currentPalette;     // palette used for current effect (includes transition, used in color_from_palette())\n    static CRGBPalette16 _randomPalette;      // actual random palette\n    static CRGBPalette16 _newRandomPalette;   // target random palette\n    static uint16_t      _lastPaletteChange;  // last random palette change time (in seconds)\n    static uint16_t      _nextPaletteBlend;   // next due time for random palette morph (in millis())\n    static bool          _modeBlend;          // mode/effect blending semaphore\n    // clipping rectangle used for blending\n    static uint16_t      _clipStart, _clipStop;\n    static uint8_t       _clipStartY, _clipStopY;\n\n    // transition data, holds values during transition (76 bytes/28 bytes)\n    struct Transition {\n      Segment      *_oldSegment;          // previous segment environment (may be nullptr if effect did not change)\n      unsigned long _start;               // must accommodate millis()\n      uint32_t      _colors[NUM_COLORS];  // current colors\n      #ifndef WLED_SAVE_RAM\n      CRGBPalette16 _palT;                // temporary palette (slowly being morphed from old to new)\n      #endif\n      uint16_t      _dur;                 // duration of transition in ms\n      uint16_t      _progress;            // transition progress (0-65535); pre-calculated from _start & _dur in updateTransitionProgress()\n      uint8_t       _prevPaletteBlends;   // number of previous palette blends (there are max 255 blends possible)\n      uint8_t       _palette, _bri, _cct; // palette ID, brightness and CCT at the start of transition (brightness will be 0 if segment was off)\n      Transition(uint16_t dur=750)\n      : _oldSegment(nullptr)\n      , _start(millis())\n      , _colors{0,0,0}\n      #ifndef WLED_SAVE_RAM\n      , _palT(CRGBPalette16(CRGB::Black))\n      #endif\n      , _dur(dur)\n      , _progress(0)\n      , _prevPaletteBlends(0)\n      , _palette(0)\n      , _bri(0)\n      , _cct(0)\n      {}\n      ~Transition() {\n        //DEBUGFX_PRINTF_P(PSTR(\"-- Destroying transition: %p\\n\"), this);\n        if (_oldSegment) delete _oldSegment;\n      }\n    } *_t;\n\n  protected:\n\n    inline static void     addUsedSegmentData(int len)     { Segment::_usedSegmentData += len; }\n\n    inline uint32_t *getPixels() const                              { return pixels; }\n    inline void     setPixelColorRaw(unsigned i, uint32_t c) const  { pixels[i] = c; }\n    inline uint32_t getPixelColorRaw(unsigned i) const              { return pixels[i]; };\n  #ifndef WLED_DISABLE_2D\n    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; }\n    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)]; };\n  #endif\n    void resetIfRequired();         // sets all SEGENV variables to 0 and clears data buffer\n    CRGBPalette16 &loadPalette(CRGBPalette16 &tgt, uint8_t pal);\n\n    // transition functions\n    void stopTransition();                  // ends transition mode by destroying transition structure (does nothing if not in transition)\n    void updateTransitionProgress() const;  // sets transition progress (0-65535) based on time passed since transition start\n    inline void handleTransition() {\n      updateTransitionProgress();\n      if (isInTransition() && progress() == 0xFFFFU) stopTransition();\n    }\n    inline uint16_t progress() const          { return isInTransition() ? _t->_progress : 0xFFFFU; } // relies on handleTransition()/updateTransitionProgress() to update progression variable\n    inline Segment *getOldSegment() const     { return isInTransition() ? _t->_oldSegment : nullptr; }\n\n    inline static void modeBlend(bool blend)  { Segment::_modeBlend = blend; }\n    inline static void setClippingRect(int startX, int stopX, int startY = 0, int stopY = 1) { _clipStart = startX; _clipStop = stopX; _clipStartY = startY; _clipStopY = stopY; };\n    inline static bool isPreviousMode()       { return Segment::_modeBlend; }    // needed for determining CCT/opacity during non-BLEND_STYLE_FADE transition\n\n    static void handleRandomPalette();\n\n  public:\n\n    Segment(uint16_t sStart=0, uint16_t sStop=30, uint16_t sStartY = 0, uint16_t sStopY = 1)\n    : colors{DEFAULT_COLOR,BLACK,BLACK}\n    , start(sStart)\n    , stop(sStop > sStart ? sStop : sStart+1) // minimum length is 1\n    , startY(sStartY)\n    , stopY(sStopY > sStartY ? sStopY : sStartY+1) // minimum height is 1\n    , offset(0)\n    , options(SELECTED | SEGMENT_ON)\n    , grouping(1)\n    , spacing(0)\n    , opacity(255)\n    , cct(127)\n    , mode(DEFAULT_MODE)\n    , palette(0)\n    , speed(DEFAULT_SPEED)\n    , intensity(DEFAULT_INTENSITY)\n    , custom1(DEFAULT_C1)\n    , custom2(DEFAULT_C2)\n    , custom3(DEFAULT_C3)\n    , check1(false)\n    , check2(false)\n    , check3(false)\n    , blendMode(0)\n    , name(nullptr)\n    , step(0)\n    , call(0)\n    , aux0(0)\n    , aux1(0)\n    , data(nullptr)\n    , _dataLen(0)\n    , _default_palette(6)\n    , _capabilities(0)\n    , _t(nullptr)\n    {\n      DEBUGFX_PRINTF_P(PSTR(\"-- Creating segment: %p [%d,%d:%d,%d]\\n\"), this, (int)start, (int)stop, (int)startY, (int)stopY);\n      // 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)\n      pixels = static_cast<uint32_t*>(allocate_buffer(length() * sizeof(uint32_t), BFRALLOC_PREFER_PSRAM | BFRALLOC_NOBYTEACCESS | BFRALLOC_CLEAR));\n      if (!pixels) {\n        DEBUGFX_PRINTLN(F(\"!!! Not enough RAM for pixel buffer !!!\"));\n        extern byte errorFlag;\n        errorFlag = ERR_NORAM_PX;\n        stop = 0; // mark segment as inactive/invalid\n      }\n    }\n\n    Segment(const Segment &orig); // copy constructor\n    Segment(Segment &&orig) noexcept; // move constructor\n\n    ~Segment() {\n      #ifdef WLED_DEBUG\n      DEBUGFX_PRINTF_P(PSTR(\"-- Destroying segment: %p [%d,%d:%d,%d]\"), this, (int)start, (int)stop, (int)startY, (int)stopY);\n      if (name) DEBUGFX_PRINTF_P(PSTR(\" %s (%p)\"), name, name);\n      if (data) DEBUGFX_PRINTF_P(PSTR(\" %u->(%p)\"), _dataLen, data);\n      DEBUGFX_PRINTF_P(PSTR(\" T[%p]\"), _t);\n      DEBUGFX_PRINTLN();\n      #endif\n      clearName();\n      #ifdef WLED_ENABLE_GIF\n      endImagePlayback(this);\n      #endif\n      deallocateData();\n      p_free(pixels);\n    }\n\n    Segment& operator= (const Segment &orig); // copy assignment\n    Segment& operator= (Segment &&orig) noexcept; // move assignment\n\n#ifdef WLED_DEBUG\n    size_t getSize() const { return sizeof(Segment) + (data?_dataLen:0) + (name?strlen(name):0) + (_t?sizeof(Transition):0) + (pixels?length()*sizeof(uint32_t):0); }\n#endif\n\n    inline bool     getOption(uint8_t n)   const { return ((options >> n) & 0x01); }\n    inline bool     isSelected()           const { return selected; }\n    inline bool     isInTransition()       const { return _t != nullptr; }\n    inline bool     isActive()             const { return stop > start && pixels; }\n    inline bool     hasRGB()               const { return _isRGB; }\n    inline bool     hasWhite()             const { return _hasW; }\n    inline bool     isCCT()                const { return _isCCT; }\n    inline uint16_t width()                const { return stop > start ? (stop - start) : 0; }// segment width in physical pixels (length if 1D)\n    inline uint16_t height()               const { return stopY - startY; }                   // segment height (if 2D) in physical pixels (it *is* always >=1)\n    inline uint16_t length()               const { return width() * height(); }               // segment length (count) in physical pixels\n    inline uint16_t groupLength()          const { return grouping + spacing; }\n    inline uint8_t  getLightCapabilities() const { return _capabilities; }\n    inline void     deactivate()                 { setGeometry(0,0); }\n    inline Segment &clearName()                  { p_free(name); name = nullptr; return *this; }\n    inline Segment &setName(const String &name)  { return setName(name.c_str()); }\n\n    inline static unsigned vLength()                       { return Segment::_vLength; }\n    inline static unsigned vWidth()                        { return Segment::_vWidth; }\n    inline static unsigned vHeight()                       { return Segment::_vHeight; }\n    inline static uint32_t getCurrentColor(unsigned i)     { return Segment::_currentColors[i<NUM_COLORS?i:0]; }\n    inline static const CRGBPalette16 &getCurrentPalette() { return Segment::_currentPalette; }\n\n    inline void setDrawDimensions() const { Segment::_vWidth = virtualWidth(); Segment::_vHeight = virtualHeight(); Segment::_vLength = virtualLength(); }\n\n    void    beginDraw(uint16_t prog = 0xFFFFU);         // set up parameters for current effect\n    void    setGeometry(uint16_t i1, uint16_t i2, uint8_t grp=1, uint8_t spc=0, uint16_t ofs=UINT16_MAX, uint16_t i1Y=0, uint16_t i2Y=1, uint8_t m12=0);\n    Segment &setColor(uint8_t slot, uint32_t c);\n    Segment &setCCT(uint16_t k);\n    Segment &setOpacity(uint8_t o);\n    Segment &setOption(uint8_t n, bool val);\n    Segment &setMode(uint8_t fx, bool loadDefaults = false);\n    Segment &setPalette(uint8_t pal);\n    Segment &setName(const char* name);\n    void    refreshLightCapabilities() const;\n\n    // runtime data functions\n    inline uint16_t dataSize() const { return _dataLen; }\n    bool allocateData(size_t len);  // allocates effect data buffer in heap and clears it\n    void deallocateData();          // deallocates (frees) effect data buffer from heap\n    inline static unsigned getUsedSegmentData()            { return Segment::_usedSegmentData; }\n    /**\n      * Flags that before the next effect is calculated,\n      * the internal segment state should be reset.\n      * Call resetIfRequired before calling the next effect function.\n      * Safe to call from interrupts and network requests.\n      */\n    inline Segment &markForReset() { reset = true; return *this; }  // setOption(SEG_OPTION_RESET, true)\n\n    void startTransition(uint16_t dur, bool segmentCopy = true);    // transition has to start before actual segment values change\n    uint8_t  currentCCT() const; // current segment's CCT (blended while in transition)\n    uint8_t  currentBri() const; // current segment's opacity/brightness (blended while in transition)\n\n    // 1D strip\n    uint16_t virtualLength() const;\n    uint16_t maxMappingLength() const;\n    [[gnu::hot]] void setPixelColor(int n, uint32_t c) const; // set relative pixel within segment with color\n    inline void setPixelColor(unsigned n, uint32_t c) const                    { setPixelColor(int(n), c); }\n    inline void setPixelColor(int n, byte r, byte g, byte b, byte w = 0) const { setPixelColor(n, RGBW32(r,g,b,w)); }\n    inline void setPixelColor(int n, CRGB c) const                             { setPixelColor(n, RGBW32(c.r,c.g,c.b,0)); }\n    void setRawPixelColor(int i, uint32_t col) const                           { if (i >= 0 && i < length()) setPixelColorRaw(i,col); }\n    #ifdef WLED_USE_AA_PIXELS\n    void setPixelColor(float i, uint32_t c, bool aa = true) const;\n    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); }\n    inline void setPixelColor(float i, CRGB c, bool aa = true) const                                         { setPixelColor(i, RGBW32(c.r,c.g,c.b,0), aa); }\n    #endif\n    [[gnu::hot]] bool isPixelClipped(int i) const;\n    [[gnu::hot]] uint32_t getPixelColor(int i) const;\n    // 1D support functions (some implement 2D as well)\n    void blur(uint8_t, bool smear = false) const;\n    void clear() const { fill(BLACK); } // clear segment\n    void fill(uint32_t c) const;\n    void fade_out(uint8_t r) const;\n    void fadeToSecondaryBy(uint8_t fadeBy) const;\n    void fadeToBlackBy(uint8_t fadeBy) const;\n    inline void blendPixelColor(int n, uint32_t color, uint8_t blend) const        { setPixelColor(n, color_blend(getPixelColor(n), color, blend)); }\n    inline void blendPixelColor(int n, CRGB c, uint8_t blend) const                { blendPixelColor(n, RGBW32(c.r,c.g,c.b,0), blend); }\n    inline void addPixelColor(int n, uint32_t color, bool preserveCR = true) const { setPixelColor(n, color_add(getPixelColor(n), color, preserveCR)); }\n    inline void addPixelColor(int n, byte r, byte g, byte b, byte w = 0, bool preserveCR = true) const\n                                                                                   { addPixelColor(n, RGBW32(r,g,b,w), preserveCR); }\n    inline void addPixelColor(int n, CRGB c, bool preserveCR = true) const         { addPixelColor(n, RGBW32(c.r,c.g,c.b,0), preserveCR); }\n    inline void fadePixelColor(uint16_t n, uint8_t fade) const                     { setPixelColor(n, color_fade(getPixelColor(n), fade, true)); }\n    [[gnu::hot]] uint32_t color_from_palette(uint16_t, bool mapping, bool moving, uint8_t mcol, uint8_t pbri = 255) const;\n    [[gnu::hot]] uint32_t color_wheel(uint8_t pos) const;\n    // 2D matrix\n    unsigned virtualWidth()  const;       // segment width in virtual pixels (accounts for groupping and spacing)\n    unsigned virtualHeight() const;       // segment height in virtual pixels (accounts for groupping and spacing)\n    inline unsigned nrOfVStrips() const { // returns number of virtual vertical strips in 2D matrix (used to expand 1D effects into 2D)\n    #ifndef WLED_DISABLE_2D\n      return (is2D() &&  map1D2D == M12_pBar) ? virtualWidth() : 1;\n    #else\n      return 1;\n    #endif\n    }\n    inline unsigned rawLength() const {   // returns length of used raw pixel buffer (eg. get/setPixelColorRaw())\n    #ifndef WLED_DISABLE_2D\n      if (is2D()) return virtualWidth() * virtualHeight();\n    #endif\n      return virtualLength();    \n    }\n\n  #ifndef WLED_DISABLE_2D\n    inline bool is2D() const                                                            { return (width()>1 && height()>1); }\n    [[gnu::hot]] void setPixelColorXY(int x, int y, uint32_t c) const; // set relative pixel within segment with color\n    inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) const               { setPixelColorXY(int(x), int(y), c); }\n    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)); }\n    inline void setPixelColorXY(int x, int y, CRGB c) const                             { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); }\n    inline void setPixelColorXY(unsigned x, unsigned y, CRGB c) const                   { setPixelColorXY(int(x), int(y), RGBW32(c.r,c.g,c.b,0)); }\n    #ifdef WLED_USE_AA_PIXELS\n    void setPixelColorXY(float x, float y, uint32_t c, bool aa = true) const;\n    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); }\n    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); }\n    #endif\n    [[gnu::hot]] bool isPixelXYClipped(int x, int y) const;\n    [[gnu::hot]] uint32_t getPixelColorXY(int x, int y) const;\n    // 2D support functions\n    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)); }\n    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); }\n    inline void addPixelColorXY(int x, int y, uint32_t color, bool preserveCR = true) const    { setPixelColorXY(x, y, color_add(getPixelColorXY(x,y), color, preserveCR)); }\n    inline void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool preserveCR = true)\n                                                                                               { addPixelColorXY(x, y, RGBW32(r,g,b,w), preserveCR); }\n    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); }\n    inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) const                   { setPixelColorXY(x, y, color_fade(getPixelColorXY(x,y), fade, true)); }\n    inline void blurCols(fract8 blur_amount, bool smear = false) const                         { blur2D(0, blur_amount, smear); } // blur all columns (50% faster than full 2D blur)\n    inline void blurRows(fract8 blur_amount, bool smear = false) const                         { blur2D(blur_amount, 0, smear); } // blur all rows (50% faster than full 2D blur)\n    //void box_blur(unsigned r = 1U, bool smear = false); // 2D box blur\n    void blur2D(uint8_t blur_x, uint8_t blur_y, bool smear = false) const;\n    void moveX(int delta, bool wrap = false) const;\n    void moveY(int delta, bool wrap = false) const;\n    void move(unsigned dir, unsigned delta, bool wrap = false) const;\n    void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) const;\n    void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) const;\n    void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft = false) const;\n    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;\n    void wu_pixel(uint32_t x, uint32_t y, CRGB c) const;\n    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); }\n    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); }\n    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\n    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\n    inline void fill_solid(CRGB c) const { fill(RGBW32(c.r,c.g,c.b,0)); }\n  #else\n    inline bool is2D() const                                                      { return false; }\n    inline void setPixelColorXY(int x, int y, uint32_t c) const                   { setPixelColor(x, c); }\n    inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) const         { setPixelColor(int(x), c); }\n    inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) const { setPixelColor(x, RGBW32(r,g,b,w)); }\n    inline void setPixelColorXY(int x, int y, CRGB c) const                       { setPixelColor(x, RGBW32(c.r,c.g,c.b,0)); }\n    inline void setPixelColorXY(unsigned x, unsigned y, CRGB c) const             { setPixelColor(int(x), RGBW32(c.r,c.g,c.b,0)); }\n    #ifdef WLED_USE_AA_PIXELS\n    inline void setPixelColorXY(float x, float y, uint32_t c, bool aa = true) const { setPixelColor(x, c, aa); }\n    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); }\n    inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) const     { setPixelColor(x, RGBW32(c.r,c.g,c.b,0), aa); }\n    #endif\n    inline bool isPixelXYClipped(int x, int y) const                                       { return isPixelClipped(x); }\n    inline uint32_t getPixelColorXY(int x, int y) const                                    { return getPixelColor(x); }\n    inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t c, uint8_t blend) const { blendPixelColor(x, c, blend); }\n    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); }\n    inline void addPixelColorXY(int x, int y, uint32_t color, bool saturate = false) const { addPixelColor(x, color, saturate); }\n    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); }\n    inline void addPixelColorXY(int x, int y, CRGB c, bool saturate = false) const         { addPixelColor(x, RGBW32(c.r,c.g,c.b,0), saturate); }\n    inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) const               { fadePixelColor(x, fade); }\n    //inline void box_blur(unsigned i, bool vertical, fract8 blur_amount) {}\n    inline void blur2D(uint8_t blur_x, uint8_t blur_y, bool smear = false) {}\n    inline void blurCols(fract8 blur_amount, bool smear = false) { blur(blur_amount, smear); } // blur all columns (50% faster than full 2D blur)\n    inline void blurRows(fract8 blur_amount, bool smear = false) {}\n    inline void moveX(int delta, bool wrap = false) {}\n    inline void moveY(int delta, bool wrap = false) {}\n    inline void move(uint8_t dir, uint8_t delta, bool wrap = false) {}\n    inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) {}\n    inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) {}\n    inline void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) {}\n    inline void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) {}\n    inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft = false) {}\n    inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c, bool soft = false) {}\n    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) {}\n    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) {}\n    inline void wu_pixel(uint32_t x, uint32_t y, CRGB c) {}\n  #endif\n  friend class WS2812FX;\n  friend class ParticleSystem2D;\n  friend class ParticleSystem1D;\n};\n\n// main \"strip\" class (108 bytes)\nclass WS2812FX {\n  typedef void (*mode_ptr)(); // pointer to mode function\n  typedef void (*show_callback)(); // pre show callback\n  typedef struct ModeData {\n    uint8_t     _id;   // mode (effect) id\n    mode_ptr    _fcn;  // mode (effect) function\n    const char *_data; // mode (effect) name and its UI control data\n    ModeData(uint8_t id, void (*fcn)(void), const char *data) : _id(id), _fcn(fcn), _data(data) {}\n  } mode_data_t;\n\n  public:\n\n    WS2812FX() :\n      now(millis()),\n      timebase(0),\n      isMatrix(false),\n#ifdef WLED_AUTOSEGMENTS\n      autoSegments(true),\n#else\n      autoSegments(false),\n#endif\n      correctWB(false),\n      cctFromRgb(false),\n      // true private variables\n      _pixels(nullptr),\n      _pixelCCT(nullptr),\n      _suspend(false),\n      _brightness(DEFAULT_BRIGHTNESS),\n      _length(DEFAULT_LED_COUNT),\n      _transitionDur(750),\n      _frametime(FRAMETIME_FIXED),\n      _cumulativeFps(WLED_FPS << FPS_CALC_SHIFT),\n      _targetFps(WLED_FPS),\n      _isServicing(false),\n      _isOffRefreshRequired(false),\n      _hasWhiteChannel(false),\n      _triggered(false),\n      _segment_index(0),\n      _mainSegment(0),\n      _modeCount(MODE_COUNT),\n      _callback(nullptr),\n      customMappingTable(nullptr),\n      customMappingSize(0),\n      _lastShow(0),\n      _lastServiceShow(0)\n    {\n      _mode.reserve(_modeCount);     // allocate memory to prevent initial fragmentation (does not increase size())\n      _modeData.reserve(_modeCount); // allocate memory to prevent initial fragmentation (does not increase size())\n      if (_mode.capacity() <= 1 || _modeData.capacity() <= 1) _modeCount = 1; // memory allocation failed only show Solid\n      else setupEffectData();\n    }\n\n    ~WS2812FX() {\n      p_free(_pixels);\n      p_free(_pixelCCT); // just in case\n      d_free(customMappingTable);\n      _mode.clear();\n      _modeData.clear();\n      _segments.clear();\n#ifndef WLED_DISABLE_2D\n      panel.clear();\n#endif\n    }\n\n    void\n#ifdef WLED_DEBUG\n      printSize(),                                // prints memory usage for strip components\n#endif\n      finalizeInit(),                             // initialises strip components\n      service(),                                  // executes effect functions when due and calls strip.show()\n      setCCT(uint16_t k),                         // sets global CCT (either in relative 0-255 value or in K)\n      setBrightness(uint8_t b, bool direct = false),    // sets strip brightness\n      setRange(uint16_t i, uint16_t i2, uint32_t col),  // used for clock overlay\n      purgeSegments(),                            // removes inactive segments from RAM (may incure penalty and memory fragmentation but reduces vector footprint)\n      setMainSegmentId(unsigned n = 0),\n      resetSegments(),                            // marks all segments for reset\n      makeAutoSegments(bool forceReset = false),  // will create segments based on configured outputs\n      fixInvalidSegments(),                       // fixes incorrect segment configuration\n      blendSegment(const Segment &topSegment) const,    // blends topSegment into pixels\n      show(),                                     // initiates LED output\n      setTargetFps(unsigned fps),\n      setupEffectData(),                          // add default effects to the list; defined in FX.cpp\n      waitForIt();                                // wait until frame is over (service() has finished or time for 1 frame has passed)\n\n    void setRealtimePixelColor(unsigned i, uint32_t c);\n    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\n    inline void resetTimebase()                               { timebase = 0UL - millis(); }\n    inline void setPixelColor(unsigned n, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) const\n                                                              { setPixelColor(n, RGBW32(r,g,b,w)); }\n    inline void setPixelColor(unsigned n, CRGB c) const       { setPixelColor(n, c.red, c.green, c.blue); }\n    inline void fill(uint32_t c) const                        { for (size_t i = 0; i < getLengthTotal(); i++) setPixelColor(i, c); } // fill whole strip with color (inline)\n    inline void trigger()                                     { _triggered = true; }  // Forces the next frame to be computed on all active segments.\n    inline void setShowCallback(show_callback cb)             { _callback = cb; }\n    inline void setTransition(uint16_t t)                     { _transitionDur = t; } // sets transition time (in ms)\n    inline void appendSegment(uint16_t sStart=0, uint16_t sStop=30, uint16_t sStartY = 0, uint16_t sStopY = 1)\n                                                              { if (_segments.size() < getMaxSegments()) _segments.emplace_back(sStart,sStop,sStartY,sStopY); }\n    inline void suspend()                                     { _suspend = true; }    // will suspend (and canacel) strip.service() execution\n    inline void resume()                                      { _suspend = false; }   // will resume strip.service() execution\n\n    void restartRuntime();\n    void setTransitionMode(bool t);\n\n    bool checkSegmentAlignment() const;\n    bool hasRGBWBus() const;\n    bool hasCCTBus() const;\n    bool deserializeMap(unsigned n = 0);\n\n    inline bool isUpdating() const           { return !BusManager::canAllShow(); } // return true if the strip is being sent pixel updates\n    inline bool isServicing() const          { return _isServicing; }           // returns true if strip.service() is executing\n    inline bool hasWhiteChannel() const      { return _hasWhiteChannel; }       // returns true if strip contains separate white chanel\n    inline bool isOffRefreshRequired() const { return _isOffRefreshRequired; }  // returns true if strip requires regular updates (i.e. TM1814 chipset)\n    inline bool isSuspended() const          { return _suspend; }               // returns true if strip.service() execution is suspended\n    inline bool needsUpdate() const          { return _triggered; }             // returns true if strip received a trigger() request\n\n    // uint8_t paletteBlend;  // obsolete - use global paletteBlend instead of strip.paletteBlend\n    uint8_t getActiveSegmentsNum() const;\n    uint8_t getFirstSelectedSegId() const;\n    uint8_t getLastActiveSegmentId() const;\n    uint8_t getActiveSegsLightCapabilities(bool selectedOnly = false) const;\n    uint8_t addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name);         // add effect to the list; defined in FX.cpp;\n\n    inline uint8_t getBrightness() const    { return _brightness; }       // returns current strip brightness\n    inline static constexpr unsigned getMaxSegments() { return MAX_NUM_SEGMENTS; }  // returns maximum number of supported segments (fixed value)\n    inline uint8_t getSegmentsNum() const   { return _segments.size(); }  // returns currently present segments\n    inline uint8_t getCurrSegmentId() const { return _segment_index; }    // returns current segment index (only valid while strip.isServicing())\n    inline uint8_t getMainSegmentId() const { return _mainSegment; }      // returns main segment index\n    inline uint8_t getTargetFps() const     { return _targetFps; }        // returns rough FPS value for las 2s interval\n    inline uint8_t getModeCount() const     { return _modeCount; }        // returns number of registered modes/effects\n\n    uint16_t getLengthPhysical() const;\n    uint16_t getLengthTotal() const; // will include virtual/nonexistent pixels in matrix\n\n    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)\n    inline uint16_t getFrameTime() const    { return _frametime; }        // returns amount of time a frame should take (in ms)\n    inline uint16_t getMinShowDelay() const { return MIN_FRAME_DELAY; }   // returns minimum amount of time strip.service() can be delayed (constant)\n    inline uint16_t getLength() const       { return _length; }           // returns actual amount of LEDs on a strip (2D matrix may have less LEDs than W*H)\n    inline uint16_t getTransition() const   { return _transitionDur; }    // returns currently set transition time (in ms)\n    inline uint16_t getMappedPixelIndex(uint16_t index) const {           // convert logical address to physical\n      if (index < customMappingSize && (realtimeMode == REALTIME_MODE_INACTIVE || realtimeRespectLedMaps)) index = customMappingTable[index];\n      return index;\n    };\n\n    unsigned long now, timebase;\n    inline uint32_t getPixelColor(unsigned n) const { return (getMappedPixelIndex(n) < getLengthTotal()) ? _pixels[n] : 0; } // returns color of pixel n, black if out of (mapped) bounds\n    inline uint32_t getPixelColorNoMap(unsigned n) const { return (n < getLengthTotal()) ? _pixels[n] : 0; } // ignores mapping table\n    inline uint32_t getLastShow() const             { return _lastShow; }                 // returns millis() timestamp of last strip.show() call\n\n    const char *getModeData(unsigned id = 0) const  { return (id && id < _modeCount) ? _modeData[id] : PSTR(\"Solid\"); }\n    inline const char **getModeDataSrc()            { return &(_modeData[0]); }           // vectors use arrays for underlying data\n\n    Segment&        getSegment(unsigned id);\n    inline Segment& getFirstSelectedSeg() { return _segments[getFirstSelectedSegId()]; }  // returns reference to first segment that is \"selected\"\n    inline Segment& getMainSegment()      { return _segments[getMainSegmentId()]; }       // returns reference to main segment\n    inline Segment* getSegments()         { return &(_segments[0]); }                     // returns pointer to segment vector structure (warning: use carefully)\n\n  // 2D support (panels)\n\n#ifndef WLED_DISABLE_2D\n    struct Panel {\n      uint16_t xOffset; // x offset relative to the top left of matrix in LEDs\n      uint16_t yOffset; // y offset relative to the top left of matrix in LEDs\n      uint8_t  width;   // width of the panel\n      uint8_t  height;  // height of the panel\n      union {\n        uint8_t options;\n        struct {\n          bool bottomStart : 1; // starts at bottom?\n          bool rightStart  : 1; // starts on right?\n          bool vertical    : 1; // is vertical?\n          bool serpentine  : 1; // is serpentine?\n        };\n      };\n      Panel()\n      : xOffset(0)\n      , yOffset(0)\n      , width(8)\n      , height(8)\n      , options(0)\n      {}\n    };\n    std::vector<Panel> panel;\n#endif\n\n    void setUpMatrix();     // sets up automatic matrix ledmap from panel configuration\n\n    inline void     setPixelColorXY(unsigned x, unsigned y, uint32_t c) const { setPixelColor(y * Segment::maxWidth + x, c); }\n    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)); }\n    inline void     setPixelColorXY(unsigned x, unsigned y, CRGB c) const     { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); }\n    inline uint32_t getPixelColorXY(unsigned x, unsigned y) const             { return getPixelColor(y * Segment::maxWidth + x); }\n\n  // end 2D support\n\n    bool isMatrix;\n    struct {\n      bool autoSegments : 1;\n      bool correctWB    : 1;\n      bool cctFromRgb   : 1;\n    };\n\n    Segment *_currentSegment;\n\n  private:\n    uint32_t *_pixels;\n    uint8_t  *_pixelCCT;\n    std::vector<Segment> _segments;\n\n    volatile bool _suspend;\n\n    uint8_t  _brightness;\n    uint16_t _length;\n    uint16_t _transitionDur;\n\n    uint16_t _frametime;\n    uint16_t _cumulativeFps;\n    uint8_t  _targetFps;\n\n    // will require only 1 byte\n    struct {\n      bool _isServicing          : 1;\n      bool _isOffRefreshRequired : 1; //periodic refresh is required for the strip to remain off.\n      bool _hasWhiteChannel      : 1;\n      bool _triggered            : 1;\n    };\n\n    uint8_t _segment_index;\n    uint8_t _mainSegment;\n\n    uint8_t                  _modeCount;\n    std::vector<mode_ptr>    _mode;     // SRAM footprint: 4 bytes per element\n    std::vector<const char*> _modeData; // mode (effect) name and its slider control data array\n\n    show_callback _callback;\n\n    uint16_t* customMappingTable;\n    uint16_t  customMappingSize;\n\n    unsigned long _lastShow;\n    unsigned long _lastServiceShow;\n\n    friend class Segment;\n};\n\nextern const char JSON_mode_names[];\nextern const char JSON_palette_names[];\n\n#endif\n"
  },
  {
    "path": "wled00/FX_2Dfcn.cpp",
    "content": "/*\n  FX_2Dfcn.cpp contains all 2D utility functions\n\n  Copyright (c) 2022  Blaz Kristan (https://blaz.at/home)\n  Licensed under the EUPL v. 1.2 or later\n  Adapted from code originally licensed under the MIT license\n\n  Parts of the code adapted from WLED Sound Reactive\n*/\n#include \"wled.h\"\n\n// setUpMatrix() - constructs ledmap array from matrix of panels with WxH pixels\n// this converts physical (possibly irregular) LED arrangement into well defined\n// array of logical pixels: fist entry corresponds to left-topmost logical pixel\n// followed by horizontal pixels, when Segment::maxWidth logical pixels are added they\n// are followed by next row (down) of Segment::maxWidth pixels (and so forth)\n// note: matrix may be comprised of multiple panels each with different orientation\n// but ledmap takes care of that. ledmap is constructed upon initialization\n// so matrix should disable regular ledmap processing\n// WARNING: effect drawing has to be suspended (strip.suspend()) or must be called from loop() context\nvoid WS2812FX::setUpMatrix() {\n#ifndef WLED_DISABLE_2D\n  // isMatrix is set in cfg.cpp or set.cpp\n  if (isMatrix) {\n    // calculate width dynamically because it may have gaps\n    Segment::maxWidth = 1;\n    Segment::maxHeight = 1;\n    for (const Panel &p : panel) {\n      if (p.xOffset + p.width > Segment::maxWidth) {\n        Segment::maxWidth = p.xOffset + p.width;\n      }\n      if (p.yOffset + p.height > Segment::maxHeight) {\n        Segment::maxHeight = p.yOffset + p.height;\n      }\n    }\n\n    // safety check\n    if (Segment::maxWidth * Segment::maxHeight > MAX_LEDS || Segment::maxWidth > 255 || Segment::maxHeight > 255 || Segment::maxWidth <= 1 || Segment::maxHeight <= 1) {\n      DEBUG_PRINTLN(F(\"2D Bounds error.\"));\n      isMatrix = false;\n      Segment::maxWidth = _length;\n      Segment::maxHeight = 1;\n      panel.clear(); // release memory allocated by panels\n      panel.shrink_to_fit(); // release memory if allocated\n      resetSegments();\n      return;\n    }\n\n    customMappingSize = 0; // prevent use of mapping if anything goes wrong\n\n    d_free(customMappingTable);\n    // Segment::maxWidth and Segment::maxHeight are set according to panel layout\n    // and the product will include at least all leds in matrix\n    // if actual LEDs are more, getLengthTotal() will return correct number of LEDs\n    customMappingTable = static_cast<uint16_t*>(d_malloc(sizeof(uint16_t)*getLengthTotal())); // prefer to not use SPI RAM\n\n    if (customMappingTable) {\n      customMappingSize = getLengthTotal();\n\n      // fill with empty in case we don't fill the entire matrix\n      unsigned matrixSize = Segment::maxWidth * Segment::maxHeight;\n      for (unsigned i = 0; i<matrixSize; i++) customMappingTable[i] = 0xFFFFU;\n      for (unsigned i = matrixSize; i<getLengthTotal(); i++) customMappingTable[i] = i; // trailing LEDs for ledmap (after matrix) if it exist\n\n      // we will try to load a \"gap\" array (a JSON file)\n      // the array has to have the same amount of values as mapping array (or larger)\n      // \"gap\" array is used while building ledmap (mapping array)\n      // and discarded afterwards as it has no meaning after the process\n      // content of the file is just raw JSON array in the form of [val1,val2,val3,...]\n      // there are no other \"key\":\"value\" pairs in it\n      // allowed values are: -1 (missing pixel/no LED attached), 0 (inactive/unused pixel), 1 (active/used pixel)\n      char    fileName[32]; strcpy_P(fileName, PSTR(\"/2d-gaps.json\"));\n      bool    isFile = WLED_FS.exists(fileName);\n      size_t  gapSize = 0;\n      int8_t *gapTable = nullptr;\n\n      if (isFile && requestJSONBufferLock(JSON_LOCK_LEDGAP)) {\n        DEBUG_PRINT(F(\"Reading LED gap from \"));\n        DEBUG_PRINTLN(fileName);\n        // read the array into global JSON buffer\n        if (readObjectFromFile(fileName, nullptr, pDoc)) {\n          // the array is similar to ledmap, except it has only 3 values:\n          // -1 ... missing pixel (do not increase pixel count)\n          //  0 ... inactive pixel (it does count, but should be mapped out (-1))\n          //  1 ... active pixel (it will count and will be mapped)\n          JsonArray map = pDoc->as<JsonArray>();\n          gapSize = map.size();\n          if (!map.isNull() && gapSize >= matrixSize) { // not an empty map\n            gapTable = static_cast<int8_t*>(p_malloc(gapSize));\n            if (gapTable) for (size_t i = 0; i < gapSize; i++) {\n              gapTable[i] = constrain(map[i], -1, 1);\n            }\n          }\n        }\n        DEBUG_PRINTLN(F(\"Gaps loaded.\"));\n        releaseJSONBufferLock();\n      }\n\n      unsigned x, y, pix=0; //pixel\n      for (const Panel &p : panel) {\n        unsigned h = p.vertical ? p.height : p.width;\n        unsigned v = p.vertical ? p.width  : p.height;\n        for (size_t j = 0; j < v; j++){\n          for(size_t i = 0; i < h; i++) {\n            y = (p.vertical?p.rightStart:p.bottomStart) ? v-j-1 : j;\n            x = (p.vertical?p.bottomStart:p.rightStart) ? h-i-1 : i;\n            x = p.serpentine && j%2 ? h-x-1 : x;\n            size_t index = (p.yOffset + (p.vertical?x:y)) * Segment::maxWidth + p.xOffset + (p.vertical?y:x);\n            if (!gapTable || (gapTable && gapTable[index] >  0)) customMappingTable[index] = pix; // a useful pixel (otherwise -1 is retained)\n            if (!gapTable || (gapTable && gapTable[index] >= 0)) pix++; // not a missing pixel\n          }\n        }\n      }\n\n      // delete gap array as we no longer need it\n      p_free(gapTable);\n\n      #ifdef WLED_DEBUG\n      DEBUG_PRINT(F(\"Matrix ledmap:\"));\n      for (unsigned i=0; i<customMappingSize; i++) {\n        if (!(i%Segment::maxWidth)) DEBUG_PRINTLN();\n        DEBUG_PRINTF_P(PSTR(\"%4d,\"), customMappingTable[i]);\n        #if defined(CONFIG_IDF_TARGET_ESP32S2)\n        delay(1); // on S2 the CDC output can crash without a delay\n        #endif\n      }\n      DEBUG_PRINTLN();\n      #endif\n    } else { // memory allocation error\n      DEBUG_PRINTLN(F(\"ERROR 2D LED map allocation error.\"));\n      isMatrix = false;\n      panel.clear();\n      Segment::maxWidth = _length;\n      Segment::maxHeight = 1;\n      resetSegments();\n    }\n  }\n#else\n  isMatrix = false; // no matter what config says\n#endif\n}\n\n\n///////////////////////////////////////////////////////////\n// Segment:: routines\n///////////////////////////////////////////////////////////\n\n#ifndef WLED_DISABLE_2D\n// pixel is clipped if it falls outside clipping range\n// if clipping start > stop the clipping range is inverted\nbool Segment::isPixelXYClipped(int x, int y) const {\n  if (blendingStyle != BLEND_STYLE_FADE && isInTransition() && _clipStart != _clipStop) {\n    const bool invertX = _clipStart  > _clipStop;\n    const bool invertY = _clipStartY > _clipStopY;\n    const int  cStartX = invertX ? _clipStop   : _clipStart;\n    const int  cStopX  = invertX ? _clipStart  : _clipStop;\n    const int  cStartY = invertY ? _clipStopY  : _clipStartY;\n    const int  cStopY  = invertY ? _clipStartY : _clipStopY;\n    if (blendingStyle == BLEND_STYLE_FAIRY_DUST) {\n      const unsigned width = cStopX - cStartX;          // assumes full segment width (faster than virtualWidth())\n      const unsigned len = width * (cStopY - cStartY);  // assumes full segment height (faster than virtualHeight())\n      if (len < 2) return false;\n      const unsigned shuffled = hashInt(x + y * width) % len;\n      const unsigned pos = (shuffled * 0xFFFFU) / len;\n      return progress() <= pos;\n    }\n    if (blendingStyle == BLEND_STYLE_CIRCULAR_IN || blendingStyle == BLEND_STYLE_CIRCULAR_OUT) {\n      const int cx   = (cStopX-cStartX+1) / 2;\n      const int cy   = (cStopY-cStartY+1) / 2;\n      const bool out = (blendingStyle == BLEND_STYLE_CIRCULAR_OUT);\n      const unsigned prog = out ? progress() : 0xFFFFU - progress();\n      int radius2    = max(cx, cy) * prog / 0xFFFF;\n      radius2 = 2 * radius2 * radius2;\n      if (radius2 == 0) return out;\n      const int dx = x - cx;\n      const int dy = y - cy;\n      const bool outside = dx * dx + dy * dy > radius2;\n      return out ? outside : !outside;\n    }\n    bool xInside = (x >= cStartX && x < cStopX); if (invertX) xInside = !xInside;\n    bool yInside = (y >= cStartY && y < cStopY); if (invertY) yInside = !yInside;\n    const bool clip = blendingStyle == BLEND_STYLE_OUTSIDE_IN ? xInside || yInside : xInside && yInside;\n    return !clip;\n  }\n  return false;\n}\n\nvoid IRAM_ATTR_YN Segment::setPixelColorXY(int x, int y, uint32_t col) const\n{\n  if (!isActive()) return; // not active\n  if ((unsigned)x >= vWidth() || (unsigned)y >= vHeight()) return;  // if pixel would fall out of virtual segment just exit\n  setPixelColorXYRaw(x, y, col);\n}\n\n#ifdef WLED_USE_AA_PIXELS\n// anti-aliased version of setPixelColorXY()\nvoid Segment::setPixelColorXY(float x, float y, uint32_t col, bool aa) const\n{\n  if (!isActive()) return; // not active\n  if (x<0.0f || x>1.0f || y<0.0f || y>1.0f) return; // not normalized\n\n  float fX = x * (vWidth()-1);\n  float fY = y * (vHeight()-1);\n  if (aa) {\n    unsigned xL = roundf(fX-0.49f);\n    unsigned xR = roundf(fX+0.49f);\n    unsigned yT = roundf(fY-0.49f);\n    unsigned yB = roundf(fY+0.49f);\n    float    dL = (fX - xL)*(fX - xL);\n    float    dR = (xR - fX)*(xR - fX);\n    float    dT = (fY - yT)*(fY - yT);\n    float    dB = (yB - fY)*(yB - fY);\n    uint32_t cXLYT = getPixelColorXY(xL, yT);\n    uint32_t cXRYT = getPixelColorXY(xR, yT);\n    uint32_t cXLYB = getPixelColorXY(xL, yB);\n    uint32_t cXRYB = getPixelColorXY(xR, yB);\n\n    if (xL!=xR && yT!=yB) {\n      setPixelColorXY(xL, yT, color_blend(col, cXLYT, uint8_t(sqrtf(dL*dT)*255.0f))); // blend TL pixel\n      setPixelColorXY(xR, yT, color_blend(col, cXRYT, uint8_t(sqrtf(dR*dT)*255.0f))); // blend TR pixel\n      setPixelColorXY(xL, yB, color_blend(col, cXLYB, uint8_t(sqrtf(dL*dB)*255.0f))); // blend BL pixel\n      setPixelColorXY(xR, yB, color_blend(col, cXRYB, uint8_t(sqrtf(dR*dB)*255.0f))); // blend BR pixel\n    } else if (xR!=xL && yT==yB) {\n      setPixelColorXY(xR, yT, color_blend(col, cXLYT, uint8_t(dL*255.0f))); // blend L pixel\n      setPixelColorXY(xR, yT, color_blend(col, cXRYT, uint8_t(dR*255.0f))); // blend R pixel\n    } else if (xR==xL && yT!=yB) {\n      setPixelColorXY(xR, yT, color_blend(col, cXLYT, uint8_t(dT*255.0f))); // blend T pixel\n      setPixelColorXY(xL, yB, color_blend(col, cXLYB, uint8_t(dB*255.0f))); // blend B pixel\n    } else {\n      setPixelColorXY(xL, yT, col); // exact match (x & y land on a pixel)\n    }\n  } else {\n    setPixelColorXY(uint16_t(roundf(fX)), uint16_t(roundf(fY)), col);\n  }\n}\n#endif\n\n// returns RGBW values of pixel\nuint32_t IRAM_ATTR_YN Segment::getPixelColorXY(int x, int y) const {\n  if (!isActive()) return 0; // not active\n  if ((unsigned)x >= vWidth() || (unsigned)y >= vHeight()) return 0;  // if pixel would fall out of virtual segment just exit\n  return getPixelColorXYRaw(x,y);\n}\n\n// 2D blurring, can be asymmetrical\nvoid Segment::blur2D(uint8_t blur_x, uint8_t blur_y, bool smear) const {\n  if (!isActive()) return; // not active\n  const unsigned cols = vWidth();\n  const unsigned rows = vHeight();\n  const auto XY = [&](unsigned x, unsigned y){ return x + y*cols; };\n  if (blur_x) {\n    const uint8_t keepx = smear ? 255 : 255 - blur_x;\n    const uint8_t seepx = blur_x >> 1;\n    for (unsigned row = 0; row < rows; row++) { // blur rows (x direction)\n      // handle first pixel in row to avoid conditional in loop (faster)\n      uint32_t cur = getPixelColorRaw(XY(0, row));\n      uint32_t carryover = fast_color_scale(cur, seepx);\n      setPixelColorRaw(XY(0, row), fast_color_scale(cur, keepx));\n      for (unsigned x = 1; x < cols; x++) {\n         cur = getPixelColorRaw(XY(x, row));\n        uint32_t part = fast_color_scale(cur, seepx);\n        cur = fast_color_scale(cur, keepx);\n        cur = color_add(cur, carryover);\n        setPixelColorRaw(XY(x - 1, row), color_add(getPixelColorRaw(XY(x-1, row)), part)); // previous pixel\n        setPixelColorRaw(XY(x, row), cur); // current pixel\n        carryover = part;\n      }\n    }\n  }\n  if (blur_y) {\n    const uint8_t keepy = smear ? 255 : 255 - blur_y;\n    const uint8_t seepy = blur_y >> 1;\n    for (unsigned col = 0; col < cols; col++) {\n      // handle first pixel in column\n      uint32_t cur = getPixelColorRaw(XY(col, 0));\n      uint32_t carryover = fast_color_scale(cur, seepy);\n      setPixelColorRaw(XY(col, 0), fast_color_scale(cur, keepy));\n      for (unsigned y = 1; y < rows; y++) {\n        cur = getPixelColorRaw(XY(col, y));\n        uint32_t part = fast_color_scale(cur, seepy);\n        cur = fast_color_scale(cur, keepy);\n        cur = color_add(cur, carryover);\n        setPixelColorRaw(XY(col, y - 1), color_add(getPixelColorRaw(XY(col, y-1)), part)); // previous pixel\n        setPixelColorRaw(XY(col, y), cur); // current pixel\n        carryover = part;\n      }\n    }\n  }\n}\n\n/*\n// 2D Box blur\nvoid Segment::box_blur(unsigned radius, bool smear) {\n  if (!isActive() || radius == 0) return; // not active\n  if (radius > 3) radius = 3;\n  const unsigned d = (1 + 2*radius) * (1 + 2*radius); // averaging divisor\n  const unsigned cols = vWidth();\n  const unsigned rows = vHeight();\n  uint16_t *tmpRSum = new uint16_t[cols*rows];\n  uint16_t *tmpGSum = new uint16_t[cols*rows];\n  uint16_t *tmpBSum = new uint16_t[cols*rows];\n  uint16_t *tmpWSum = new uint16_t[cols*rows];\n  // fill summed-area table (https://en.wikipedia.org/wiki/Summed-area_table)\n  for (unsigned x = 0; x < cols; x++) {\n    unsigned rS, gS, bS, wS;\n    unsigned index;\n    rS = gS = bS = wS = 0;\n    for (unsigned y = 0; y < rows; y++) {\n      index = x * cols + y;\n      if (x > 0) {\n        unsigned index2 = (x - 1) * cols + y;\n        tmpRSum[index] = tmpRSum[index2];\n        tmpGSum[index] = tmpGSum[index2];\n        tmpBSum[index] = tmpBSum[index2];\n        tmpWSum[index] = tmpWSum[index2];\n      } else {\n        tmpRSum[index] = 0;\n        tmpGSum[index] = 0;\n        tmpBSum[index] = 0;\n        tmpWSum[index] = 0;\n      }\n      uint32_t c = getPixelColorXY(x, y);\n      rS += R(c);\n      gS += G(c);\n      bS += B(c);\n      wS += W(c);\n      tmpRSum[index] += rS;\n      tmpGSum[index] += gS;\n      tmpBSum[index] += bS;\n      tmpWSum[index] += wS;\n    }\n  }\n  // do a box blur using pre-calculated sums\n  for (unsigned x = 0; x < cols; x++) {\n    for (unsigned y = 0; y < rows; y++) {\n      // sum = D + A - B - C where k = (x,y)\n      // +----+-+---- (x)\n      // |    | |\n      // +----A-B\n      // |    |k|\n      // +----C-D\n      // |\n      //(y)\n      unsigned x0 = x < radius ? 0 : x - radius;\n      unsigned y0 = y < radius ? 0 : y - radius;\n      unsigned x1 = x >= cols - radius ? cols - 1 : x + radius;\n      unsigned y1 = y >= rows - radius ? rows - 1 : y + radius;\n      unsigned A = x0 * cols + y0;\n      unsigned B = x1 * cols + y0;\n      unsigned C = x0 * cols + y1;\n      unsigned D = x1 * cols + y1;\n      unsigned r = tmpRSum[D] + tmpRSum[A] - tmpRSum[C] - tmpRSum[B];\n      unsigned g = tmpGSum[D] + tmpGSum[A] - tmpGSum[C] - tmpGSum[B];\n      unsigned b = tmpBSum[D] + tmpBSum[A] - tmpBSum[C] - tmpBSum[B];\n      unsigned w = tmpWSum[D] + tmpWSum[A] - tmpWSum[C] - tmpWSum[B];\n      setPixelColorXY(x, y, RGBW32(r/d, g/d, b/d, w/d));\n    }\n  }\n  delete[] tmpRSum;\n  delete[] tmpGSum;\n  delete[] tmpBSum;\n  delete[] tmpWSum;\n}\n*/\nvoid Segment::moveX(int delta, bool wrap) const {\n  if (!isActive() || !delta) return; // not active\n  const int vW = vWidth();   // segment width in logical pixels (can be 0 if segment is inactive)\n  const int vH = vHeight();  // segment height in logical pixels (is always >= 1)\n  const auto XY = [&](unsigned x, unsigned y){ return x + y*vW; };\n  int absDelta = abs(delta);\n  if (absDelta >= vW) return;\n  uint32_t newPxCol[vW];\n  int newDelta;\n  int stop = vW;\n  int start = 0;\n  if (wrap) newDelta = (delta + vW) % vW; // +cols in case delta < 0\n  else {\n    if (delta < 0) start = absDelta;\n    stop = vW - absDelta;\n    newDelta = delta > 0 ? delta : 0;\n  }\n  for (int y = 0; y < vH; y++) {\n    for (int x = 0; x < stop; x++) {\n      int srcX = x + newDelta;\n      if (wrap) srcX %= vW; // Wrap using modulo when `wrap` is true\n      newPxCol[x] = getPixelColorRaw(XY(srcX, y));\n    }\n    for (int x = 0; x < stop; x++) setPixelColorRaw(XY(x + start, y), newPxCol[x]);\n  }\n}\n\nvoid Segment::moveY(int delta, bool wrap) const {\n  if (!isActive() || !delta) return; // not active\n  const int vW = vWidth();   // segment width in logical pixels (can be 0 if segment is inactive)\n  const int vH = vHeight();  // segment height in logical pixels (is always >= 1)\n  const auto XY = [&](unsigned x, unsigned y){ return x + y*vW; };\n  int absDelta = abs(delta);\n  if (absDelta >= vH) return;\n  uint32_t newPxCol[vH];\n  int newDelta;\n  int stop = vH;\n  int start = 0;\n  if (wrap) newDelta = (delta + vH) % vH; // +rows in case delta < 0\n  else {\n    if (delta < 0) start = absDelta;\n    stop = vH - absDelta;\n    newDelta = delta > 0 ? delta : 0;\n  }\n  for (int x = 0; x < vW; x++) {\n    for (int y = 0; y < stop; y++) {\n      int srcY = y + newDelta;\n      if (wrap) srcY %= vH; // Wrap using modulo when `wrap` is true\n      newPxCol[y] = getPixelColorRaw(XY(x, srcY));\n    }\n    for (int y = 0; y < stop; y++) setPixelColorRaw(XY(x, y + start), newPxCol[y]);\n  }\n}\n\n// move() - move all pixels in desired direction delta number of pixels\n// @param dir direction: 0=left, 1=left-up, 2=up, 3=right-up, 4=right, 5=right-down, 6=down, 7=left-down\n// @param delta number of pixels to move\n// @param wrap around\nvoid Segment::move(unsigned dir, unsigned delta, bool wrap) const {\n  if (delta==0) return;\n  switch (dir) {\n    case 0: moveX( delta, wrap);                      break;\n    case 1: moveX( delta, wrap); moveY( delta, wrap); break;\n    case 2:                      moveY( delta, wrap); break;\n    case 3: moveX(-delta, wrap); moveY( delta, wrap); break;\n    case 4: moveX(-delta, wrap);                      break;\n    case 5: moveX(-delta, wrap); moveY(-delta, wrap); break;\n    case 6:                      moveY(-delta, wrap); break;\n    case 7: moveX( delta, wrap); moveY(-delta, wrap); break;\n  }\n}\n\nvoid Segment::drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, bool soft) const {\n  if (!isActive() || radius == 0) return; // not active\n  if (soft) {\n    // Xiaolin Wu’s algorithm\n    const int rsq = radius*radius;\n    int x = 0;\n    int y = radius;\n    unsigned oldFade = 0;\n    while (x < y) {\n      float yf = sqrtf(float(rsq - x*x)); // needs to be floating point\n      uint8_t fade = float(0xFF) * (ceilf(yf) - yf); // how much color to keep\n      if (oldFade > fade) y--;\n      oldFade = fade;\n      int px, py;\n      for (uint8_t i = 0; i < 16; i++) {\n          int swaps = (i & 0x4 ? 1 : 0); // 0,  0,  0,  0,  1,  1,  1,  1,  0,  0,  0,  0,  1,  1,  1,  1\n          int adj =  (i < 8) ? 0 : 1;    // 0,  0,  0,  0,  0,  0,  0,  0,  1,  1,  1,  1,  1,  1,  1,  1\n          int dx = (i & 1) ? -1 : 1;     // 1, -1,  1, -1,  1, -1,  1, -1,  1, -1,  1, -1,  1, -1,  1, -1\n          int dy = (i & 2) ? -1 : 1;     // 1,  1, -1, -1,  1,  1, -1, -1,  1,  1, -1, -1,  1,  1, -1, -1\n          if (swaps) {\n              px = cx + (y - adj) * dx;\n              py = cy + x * dy;\n          } else {\n              px = cx + x * dx;\n              py = cy + (y - adj) * dy;\n          }\n          uint32_t pixCol = getPixelColorXY(px, py);\n          setPixelColorXY(px, py, adj ?\n              color_blend(pixCol, col, fade) :\n              color_blend(col, pixCol, fade));\n      }\n      x++;\n    }\n  } else {\n    // Bresenham’s Algorithm\n    int d = 3 - (2*radius);\n    int y = radius, x = 0;\n    while (y >= x) {\n    for (int i = 0; i < 4; i++) {\n        int dx = (i & 1) ? -x : x;\n        int dy = (i & 2) ? -y : y;\n        setPixelColorXY(cx + dx, cy + dy, col);\n        setPixelColorXY(cx + dy, cy + dx, col);\n    }\n      x++;\n      if (d > 0) {\n        y--;\n        d += 4 * (x - y) + 10;\n      } else {\n        d += 4 * x + 6;\n      }\n    }\n  }\n}\n\n// by stepko, taken from https://editor.soulmatelights.com/gallery/573-blobs\nvoid Segment::fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, bool soft) const {\n  if (!isActive() || radius == 0) return; // not active\n  const int vW = vWidth();   // segment width in logical pixels (can be 0 if segment is inactive)\n  const int vH = vHeight();  // segment height in logical pixels (is always >= 1)\n  // draw soft bounding circle\n  if (soft) drawCircle(cx, cy, radius, col, soft);\n  // fill it\n  for (int y = -radius; y <= radius; y++) {\n    for (int x = -radius; x <= radius; x++) {\n      if (x * x + y * y <= radius * radius &&\n          int(cx)+x >= 0 && int(cy)+y >= 0 &&\n          int(cx)+x < vW && int(cy)+y < vH)\n        setPixelColorXY(cx + x, cy + y, col);\n    }\n  }\n}\n\n//line function\nvoid Segment::drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft) const {\n  if (!isActive()) return; // not active\n  const int vW = vWidth();   // segment width in logical pixels (can be 0 if segment is inactive)\n  const int vH = vHeight();  // segment height in logical pixels (is always >= 1)\n  if (x0 >= vW || x1 >= vW || y0 >= vH || y1 >= vH) return;\n\n  const int dx = abs(x1-x0), sx = x0<x1 ? 1 : -1; // x distance & step\n  const int dy = abs(y1-y0), sy = y0<y1 ? 1 : -1; // y distance & step\n\n  // single pixel (line length == 0)\n  if (dx+dy == 0) {\n    setPixelColorXY(x0, y0, c);\n    return;\n  }\n\n  if (soft) {\n    // Xiaolin Wu’s algorithm\n    const bool steep = dy > dx;\n    if (steep) {\n      // we need to go along longest dimension\n      std::swap(x0,y0);\n      std::swap(x1,y1);\n    }\n    if (x0 > x1) {\n      // we need to go in increasing fashion\n      std::swap(x0,x1);\n      std::swap(y0,y1);\n    }\n    float gradient = x1-x0 == 0 ? 1.0f : float(y1-y0) / float(x1-x0);\n    float intersectY = y0;\n    for (int x = x0; x <= x1; x++) {\n      uint8_t keep = float(0xFF) * (intersectY-int(intersectY)); // how much color to keep\n      uint8_t seep = 0xFF - keep; // how much background to keep\n      int y = int(intersectY);\n      if (steep) std::swap(x,y);  // temporaryly swap if steep\n      // pixel coverage is determined by fractional part of y co-ordinate\n      blendPixelColorXY(x, y, c, seep);\n      blendPixelColorXY(x+int(steep), y+int(!steep), c, keep);\n      intersectY += gradient;\n      if (steep) std::swap(x,y);  // restore if steep\n    }\n  } else {\n    // Bresenham's algorithm\n    int err = (dx>dy ? dx : -dy)/2;   // error direction\n    for (;;) {\n      setPixelColorXY(x0, y0, c);\n      if (x0==x1 && y0==y1) break;\n      int e2 = err;\n      if (e2 >-dx) { err -= dy; x0 += sx; }\n      if (e2 < dy) { err += dx; y0 += sy; }\n    }\n  }\n}\n\n#include \"src/font/console_font_4x6.h\"\n#include \"src/font/console_font_5x8.h\"\n#include \"src/font/console_font_5x12.h\"\n#include \"src/font/console_font_6x8.h\"\n#include \"src/font/console_font_7x9.h\"\n\n// draws a raster font character on canvas\n// only supports: 4x6=24, 5x8=40, 5x12=60, 6x8=48 and 7x9=63 fonts ATM\nvoid Segment::drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t col2, int8_t rotate) const {\n  if (!isActive()) return; // not active\n  if (chr < 32 || chr > 126) return; // only ASCII 32-126 supported\n  chr -= 32; // align with font table entries\n  const int font = w*h;\n\n  // if col2 == BLACK then use currently selected palette for gradient otherwise create gradient from color and col2\n  CRGBPalette16 grad = col2 ? CRGBPalette16(CRGB(color), CRGB(col2)) : SEGPALETTE; // selected palette as gradient\n\n  for (int i = 0; i<h; i++) { // character height\n    uint8_t bits = 0;\n    switch (font) {\n      case 24: bits = pgm_read_byte_near(&console_font_4x6[(chr * h) + i]); break;  // 4x6 font\n      case 40: bits = pgm_read_byte_near(&console_font_5x8[(chr * h) + i]); break;  // 5x8 font\n      case 48: bits = pgm_read_byte_near(&console_font_6x8[(chr * h) + i]); break;  // 6x8 font\n      case 63: bits = pgm_read_byte_near(&console_font_7x9[(chr * h) + i]); break;  // 7x9 font\n      case 60: bits = pgm_read_byte_near(&console_font_5x12[(chr * h) + i]); break; // 5x12 font\n      default: return;\n    }\n    CRGBW c = ColorFromPalette(grad, (i+1)*255/h, 255, LINEARBLEND_NOWRAP); // NOBLEND is faster\n    for (int j = 0; j<w; j++) { // character width\n      int x0, y0;\n      switch (rotate) {\n        case -1: x0 = x + (h-1) - i; y0 = y + (w-1) - j; break; // -90 deg\n        case -2:\n        case  2: x0 = x + j;         y0 = y + (h-1) - i; break; // 180 deg\n        case  1: x0 = x + i;         y0 = y + j;         break; // +90 deg\n        default: x0 = x + (w-1) - j; y0 = y + i;         break; // no rotation\n      }\n      if (x0 < 0 || x0 >= (int)vWidth() || y0 < 0 || y0 >= (int)vHeight()) continue; // drawing off-screen\n      if (((bits>>(j+(8-w))) & 0x01)) { // bit set\n        setPixelColorXYRaw(x0, y0, c.color32);\n      }\n    }\n  }\n}\n\n#define WU_WEIGHT(a,b) ((uint8_t) (((a)*(b)+(a)+(b))>>8))\nvoid Segment::wu_pixel(uint32_t x, uint32_t y, CRGB c) const {      //awesome wu_pixel procedure by reddit u/sutaburosu\n  if (!isActive()) return; // not active\n  // extract the fractional parts and derive their inverses\n  unsigned xx = x & 0xff, yy = y & 0xff, ix = 255 - xx, iy = 255 - yy;\n  // calculate the intensities for each affected pixel\n  uint8_t wu[4] = {WU_WEIGHT(ix, iy), WU_WEIGHT(xx, iy),\n                   WU_WEIGHT(ix, yy), WU_WEIGHT(xx, yy)};\n  // multiply the intensities by the colour, and saturating-add them to the pixels\n  for (int i = 0; i < 4; i++) {\n    int wu_x = (x >> 8) + (i & 1);        // precalculate x\n    int wu_y = (y >> 8) + ((i >> 1) & 1); // precalculate y\n    CRGB led = getPixelColorXY(wu_x, wu_y);\n    CRGB oldLed = led;\n    led.r = qadd8(led.r, c.r * wu[i] >> 8);\n    led.g = qadd8(led.g, c.g * wu[i] >> 8);\n    led.b = qadd8(led.b, c.b * wu[i] >> 8);\n    if (led != oldLed) setPixelColorXY(wu_x, wu_y, led); // don't repaint if same color\n  }\n}\n#undef WU_WEIGHT\n\n#endif // WLED_DISABLE_2D\n"
  },
  {
    "path": "wled00/FX_fcn.cpp",
    "content": "/*\n  WS2812FX_fcn.cpp contains all utility functions\n  Harm Aldick - 2016\n  www.aldick.org\n\n  Copyright (c) 2016  Harm Aldick\n  Licensed under the EUPL v. 1.2 or later\n  Adapted from code originally licensed under the MIT license\n\n  Modified heavily for WLED\n*/\n#include \"wled.h\"\n#include \"FXparticleSystem.h\"  // TODO: better define the required function (mem service) in FX.h?\n\n/*\n  Custom per-LED mapping has moved!\n\n  Create a file \"ledmap.json\" using the edit page.\n\n  this is just an example (30 LEDs). It will first set all even, then all uneven LEDs.\n  {\"map\":[\n  0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28,\n  1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29]}\n\n  another example. Switches direction every 5 LEDs.\n  {\"map\":[\n  0, 1, 2, 3, 4, 9, 8, 7, 6, 5, 10, 11, 12, 13, 14,\n  19, 18, 17, 16, 15, 20, 21, 22, 23, 24, 29, 28, 27, 26, 25]}\n*/\n\n#if MAX_NUM_SEGMENTS < WLED_MAX_BUSSES\n  #error \"Max segments must be at least max number of busses!\"\n#endif\n\n\n///////////////////////////////////////////////////////////////////////////////\n// Segment class implementation\n///////////////////////////////////////////////////////////////////////////////\nunsigned      Segment::_usedSegmentData   = 0U; // amount of RAM all segments use for their data[]\nuint16_t      Segment::maxWidth           = DEFAULT_LED_COUNT;\nuint16_t      Segment::maxHeight          = 1;\nunsigned      Segment::_vLength           = 0;\nunsigned      Segment::_vWidth            = 0;\nunsigned      Segment::_vHeight           = 0;\nuint32_t      Segment::_currentColors[NUM_COLORS] = {0,0,0};\nCRGBPalette16 Segment::_currentPalette    = CRGBPalette16(CRGB::Black);\nCRGBPalette16 Segment::_randomPalette     = generateRandomPalette();  // was CRGBPalette16(DEFAULT_COLOR);\nCRGBPalette16 Segment::_newRandomPalette  = generateRandomPalette();  // was CRGBPalette16(DEFAULT_COLOR);\nuint16_t      Segment::_lastPaletteChange = 0; // in seconds; perhaps it should be per segment\nuint16_t      Segment::_nextPaletteBlend  = 0; // in millis\n\nbool     Segment::_modeBlend = false;\nuint16_t Segment::_clipStart = 0;\nuint16_t Segment::_clipStop = 0;\nuint8_t  Segment::_clipStartY = 0;\nuint8_t  Segment::_clipStopY = 1;\n\n// copy constructor\nSegment::Segment(const Segment &orig) {\n  //DEBUG_PRINTF_P(PSTR(\"-- Copy segment constructor: %p -> %p\\n\"), &orig, this);\n  memcpy((void*)this, (void*)&orig, sizeof(Segment));\n  _t   = nullptr; // copied segment cannot be in transition\n  name = nullptr;\n  data = nullptr;\n  _dataLen = 0;\n  pixels = nullptr;\n  if (!stop) return;  // nothing to do if segment is inactive/invalid\n  if (orig.pixels) {\n    // allocate pixel buffer: prefer IRAM/PSRAM\n    pixels = static_cast<uint32_t*>(allocate_buffer(orig.length() * sizeof(uint32_t), BFRALLOC_PREFER_PSRAM | BFRALLOC_NOBYTEACCESS));\n    if (pixels) {\n      memcpy(pixels, orig.pixels, sizeof(uint32_t) * orig.length());\n      if (orig.name) { name = static_cast<char*>(allocate_buffer(strlen(orig.name)+1, BFRALLOC_PREFER_PSRAM)); if (name) strcpy(name, orig.name); }\n      if (orig.data) { if (allocateData(orig._dataLen)) memcpy(data, orig.data, orig._dataLen); }\n    } else {\n      DEBUGFX_PRINTLN(F(\"!!! Not enough RAM for pixel buffer !!!\"));\n      errorFlag = ERR_NORAM_PX;\n      stop = 0; // mark segment as inactive/invalid\n    }\n  } else stop = 0; // mark segment as inactive/invalid\n}\n\n// move constructor\nSegment::Segment(Segment &&orig) noexcept {\n  //DEBUG_PRINTF_P(PSTR(\"-- Move segment constructor: %p -> %p\\n\"), &orig, this);\n  memcpy((void*)this, (void*)&orig, sizeof(Segment));\n  orig._t   = nullptr; // old segment cannot be in transition any more\n  orig.name = nullptr;\n  orig.data = nullptr;\n  orig._dataLen = 0;\n  orig.pixels = nullptr;\n}\n\n// copy assignment\nSegment& Segment::operator= (const Segment &orig) {\n  //DEBUG_PRINTF_P(PSTR(\"-- Copying segment: %p -> %p\\n\"), &orig, this);\n  if (this != &orig) {\n    // clean destination\n    if (name) { p_free(name); name = nullptr; }\n    if (_t) stopTransition(); // also erases _t\n    deallocateData();\n    p_free(pixels);\n    pixels = nullptr;\n    // copy source\n    memcpy((void*)this, (void*)&orig, sizeof(Segment));\n    // erase pointers to allocated data\n    data = nullptr;\n    _dataLen = 0;\n    if (!stop) return *this;  // nothing to do if segment is inactive/invalid\n    // copy source data\n    if (orig.pixels) {\n      // allocate pixel buffer: prefer IRAM/PSRAM\n      pixels = static_cast<uint32_t*>(allocate_buffer(orig.length() * sizeof(uint32_t), BFRALLOC_PREFER_PSRAM | BFRALLOC_NOBYTEACCESS));\n      if (pixels) {\n        memcpy(pixels, orig.pixels, sizeof(uint32_t) * orig.length());\n        if (orig.name) { name = static_cast<char*>(allocate_buffer(strlen(orig.name)+1, BFRALLOC_PREFER_PSRAM)); if (name) strcpy(name, orig.name); }\n        if (orig.data) { if (allocateData(orig._dataLen)) memcpy(data, orig.data, orig._dataLen); }\n      } else {\n        DEBUG_PRINTLN(F(\"!!! Not enough RAM for pixel buffer !!!\"));\n        errorFlag = ERR_NORAM_PX;\n        stop = 0; // mark segment as inactive/invalid\n      }\n    } else stop = 0; // mark segment as inactive/invalid\n  }\n  return *this;\n}\n\n// move assignment\nSegment& Segment::operator= (Segment &&orig) noexcept {\n  //DEBUG_PRINTF_P(PSTR(\"-- Moving segment: %p -> %p\\n\"), &orig, this);\n  if (this != &orig) {\n    if (name) { p_free(name); name = nullptr; } // free old name\n    if (_t) stopTransition(); // also erases _t\n    deallocateData(); // free old runtime data\n    p_free(pixels);   // free old pixel buffer\n    // move source data\n    memcpy((void*)this, (void*)&orig, sizeof(Segment));\n    orig.name = nullptr;\n    orig.data = nullptr;\n    orig._dataLen = 0;\n    orig.pixels = nullptr;\n    orig._t = nullptr; // old segment cannot be in transition\n  }\n  return *this;\n}\n\n// allocates effect data buffer on heap and initialises (erases) it\nbool Segment::allocateData(size_t len) {\n  if (len == 0) return false;    // nothing to do\n  if (data && _dataLen >= len) { // already allocated enough (reduce fragmentation)\n    if (call == 0) {\n      if (_dataLen < FAIR_DATA_PER_SEG) { // segment data is small\n        //DEBUG_PRINTF_P(PSTR(\"--   Clearing data (%d): %p\\n\"), len, this);\n        memset(data, 0, len);  // erase buffer if called during effect initialisation\n        return true; // no need to reallocate\n      }\n    }\n    else\n      return true;\n  }\n  //DEBUG_PRINTF_P(PSTR(\"--   Allocating data (%d): %p\\n\"), len, this);\n  // limit to MAX_SEGMENT_DATA if there is no PSRAM, otherwise prefer functionality over speed\n  #ifndef BOARD_HAS_PSRAM\n  if (Segment::getUsedSegmentData() + len - _dataLen > MAX_SEGMENT_DATA) {\n    // not enough memory\n    DEBUG_PRINTF_P(PSTR(\"SegmentData limit reached: %d/%d\\n\"), len, Segment::getUsedSegmentData());\n    errorFlag = ERR_NORAM;\n    return false;\n  }\n  #endif\n\n  if (data) {\n    d_free(data); // free data and try to allocate again (segment buffer may be blocking contiguous heap)\n    Segment::addUsedSegmentData(-_dataLen); // subtract buffer size\n  }\n\n  data = static_cast<byte*>(allocate_buffer(len, BFRALLOC_PREFER_DRAM | BFRALLOC_CLEAR)); // prefer DRAM over PSRAM for speed\n\n  if (data) {\n    Segment::addUsedSegmentData(len);\n    _dataLen = len;\n    //DEBUG_PRINTF_P(PSTR(\"---  Allocated data (%p): %d/%d -> %p\\n\"), this, len, Segment::getUsedSegmentData(), data);\n    return true;\n  }\n  // allocation failed\n  DEBUG_PRINTLN(F(\"!!! Allocation failed. !!!\"));\n  errorFlag = ERR_NORAM;\n  return false;\n}\n\nvoid Segment::deallocateData() {\n  if (!data) { _dataLen = 0; return; }\n  if ((Segment::getUsedSegmentData() > 0) && (_dataLen > 0)) { // check that we don't have a dangling / inconsistent data pointer\n    //DEBUG_PRINTF_P(PSTR(\"---  Released data (%p): %d/%d -> %p\\n\"), this, _dataLen, Segment::getUsedSegmentData(), data);\n    d_free(data);\n  } else {\n    DEBUG_PRINTF_P(PSTR(\"---- Released data (%p): inconsistent UsedSegmentData (%d/%d), cowardly refusing to free nothing.\\n\"), this, _dataLen, Segment::getUsedSegmentData());\n  }\n  data = nullptr;\n  Segment::addUsedSegmentData(_dataLen <= Segment::getUsedSegmentData() ? -_dataLen : -Segment::getUsedSegmentData());\n  _dataLen = 0;\n}\n\n/**\n  * If reset of this segment was requested, clears runtime\n  * settings of this segment.\n  * Must not be called while an effect mode function is running\n  * because it could access the data buffer and this method\n  * may free that data buffer.\n  */\nvoid Segment::resetIfRequired() {\n  if (!reset || !isActive()) return;\n  //DEBUG_PRINTF_P(PSTR(\"-- Segment reset: %p\\n\"), this);\n  if (data && _dataLen > 0) {\n    if (_dataLen > FAIR_DATA_PER_SEG) deallocateData(); // do not keep large allocations\n    else memset(data, 0, _dataLen);  // can prevent heap fragmentation\n    DEBUG_PRINTF_P(PSTR(\"-- Segment %p reset, data cleared\\n\"), this);\n  }\n  if (pixels) for (size_t i = 0; i < length(); i++) pixels[i] = BLACK; // clear pixel buffer\n  step = 0; call = 0; aux0 = 0; aux1 = 0;\n  reset = false;\n  #ifdef WLED_ENABLE_GIF\n  endImagePlayback(this);\n  #endif\n}\n\nCRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) {\n  // there is one randomy generated palette (1) followed by 4 palettes created from segment colors (2-5)\n  // those are followed by 7 fastled palettes (6-12) and 59 gradient palettes (13-71)\n  // then come the custom palettes (255,254,...) growing downwards from 255 (255 being 1st custom palette)\n  // palette 0 is a varying palette depending on effect and may be replaced by segment's color if so\n  // instructed in color_from_palette()\n  if (pal >= FIXED_PALETTE_COUNT && pal <= 255-customPalettes.size()) pal = 0; // out of bounds palette\n  //default palette. Differs depending on effect\n  if (pal == 0) pal = _default_palette; // _default_palette is set in setMode()\n  switch (pal) {\n    case 0: //default palette. Exceptions for specific effects above\n      targetPalette = PartyColors_p;\n      break;\n    case 1: //randomly generated palette\n      targetPalette = _randomPalette; //random palette is generated at intervals in handleRandomPalette()\n      break;\n    case 2: {//primary color only\n      CRGB prim = colors[0];\n      targetPalette = CRGBPalette16(prim);\n      break;}\n    case 3: {//primary + secondary\n      CRGB prim = colors[0];\n      CRGB sec  = colors[1];\n      targetPalette = CRGBPalette16(prim,prim,sec,sec);\n      break;}\n    case 4: {//primary + secondary + tertiary\n      CRGB prim = colors[0];\n      CRGB sec  = colors[1];\n      CRGB ter  = colors[2];\n      targetPalette = CRGBPalette16(ter,sec,prim);\n      break;}\n    case 5: {//primary + secondary (+tertiary if not off), more distinct\n      CRGB prim = colors[0];\n      CRGB sec  = colors[1];\n      if (colors[2]) {\n        CRGB ter = colors[2];\n        targetPalette = CRGBPalette16(prim,prim,prim,prim,prim,sec,sec,sec,sec,sec,ter,ter,ter,ter,ter,prim);\n      } else {\n        targetPalette = CRGBPalette16(prim,prim,prim,prim,prim,prim,prim,prim,sec,sec,sec,sec,sec,sec,sec,sec);\n      }\n      break;}\n    default: //progmem palettes\n      if (pal > 255 - customPalettes.size()) {\n        targetPalette = customPalettes[255-pal]; // we checked bounds above\n      } else if (pal < DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT) { // palette 6 - 12, fastled palettes\n        targetPalette = *fastledPalettes[pal - DYNAMIC_PALETTE_COUNT];\n      } else {\n        byte tcp[72];\n        memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[pal - (DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT)])), sizeof(tcp));\n        targetPalette.loadDynamicGradientPalette(tcp);\n      }\n      break;\n  }\n  return targetPalette;\n}\n\n// starting a transition has to occur before change so we get current values 1st\nvoid Segment::startTransition(uint16_t dur, bool segmentCopy) {\n  if (dur == 0 || !isActive()) {\n    if (isInTransition()) _t->_dur = 0;\n    return;\n  }\n  if (isInTransition()) {\n    if (segmentCopy && !_t->_oldSegment) {\n      // already in transition but segment copy requested and not yet created\n      _t->_oldSegment = new(std::nothrow) Segment(*this); // store/copy current segment settings\n      _t->_start = millis();                              // restart countdown\n      _t->_dur   = dur;\n      _t->_prevPaletteBlends = 0;\n      if (_t->_oldSegment) {\n        _t->_oldSegment->palette = _t->_palette;          // restore original palette and colors (from start of transition)\n        for (unsigned i = 0; i < NUM_COLORS; i++) _t->_oldSegment->colors[i] = _t->_colors[i];\n        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);\n        if (!_t->_oldSegment->isActive()) stopTransition();\n      }\n    }\n    return;\n  }\n\n  // no previous transition running, start by allocating memory for segment copy\n  _t = new(std::nothrow) Transition(dur);\n  if (_t) {\n    _t->_bri = on ? opacity : 0;\n    _t->_cct = cct;\n    _t->_palette = palette;\n    #ifndef WLED_SAVE_RAM\n    loadPalette(_t->_palT, palette);\n    #endif\n    for (int i=0; i<NUM_COLORS; i++) _t->_colors[i] = colors[i];\n    if (segmentCopy) _t->_oldSegment = new(std::nothrow) Segment(*this); // store/copy current segment settings\n    if (_t->_oldSegment) {\n      DEBUGFX_PRINTF_P(PSTR(\"-- Started transition: S=%p T(%p) O[%p] OP[%p]\\n\"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels);\n      if (!_t->_oldSegment->isActive()) stopTransition();\n    } else {\n      DEBUGFX_PRINTF_P(PSTR(\"-- Started transition without old segment: S=%p T(%p)\\n\"), this, _t);\n    }\n  };\n}\n\nvoid Segment::stopTransition() {\n  DEBUG_PRINTF_P(PSTR(\"-- Stopping transition: S=%p T(%p) O[%p]\\n\"), this, _t, _t->_oldSegment);\n  delete _t;\n  _t = nullptr;\n}\n\n// sets transition progress variable (0-65535) based on time passed since transition start\nvoid Segment::updateTransitionProgress() const {\n  if (isInTransition()) {\n    _t->_progress = 0xFFFF;\n    unsigned diff = millis() - _t->_start;\n    if (_t->_dur > 0 && diff < _t->_dur) _t->_progress = diff * 0xFFFFU / _t->_dur;\n  }\n}\n\n// will return segment's CCT during a transition\n// isPreviousMode() is actually not implemented for CCT in strip.service() as WLED does not support per-pixel CCT\nuint8_t Segment::currentCCT() const {\n  unsigned prog = progress();\n  if (prog < 0xFFFFU) {\n    if (blendingStyle == BLEND_STYLE_FADE) return (cct * prog + (_t->_cct * (0xFFFFU - prog))) / 0xFFFFU;\n    //else                                   return Segment::isPreviousMode() ? _t->_cct : cct;\n  }\n  return cct;\n}\n\n// will return segment's opacity during a transition (blending it with old in case of FADE transition)\nuint8_t Segment::currentBri() const {\n  unsigned prog = progress();\n  unsigned curBri = on ? opacity : 0;\n  if (prog < 0xFFFFU) {\n    // this will blend opacity in new mode if style is FADE (single effect call)\n    if (blendingStyle == BLEND_STYLE_FADE) curBri = (prog * curBri + _t->_bri * (0xFFFFU - prog)) / 0xFFFFU;\n    else                                   curBri = Segment::isPreviousMode() ? _t->_bri : curBri;\n  }\n  return curBri;\n}\n\n// pre-calculate drawing parameters for faster access (based on the idea from @softhack007 from MM fork)\n// and blends colors and palettes if necessary\n// 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\n// which does not have transition structure\nvoid Segment::beginDraw(uint16_t prog) {\n  setDrawDimensions();\n  // load colors into _currentColors\n  for (unsigned i = 0; i < NUM_COLORS; i++) _currentColors[i] = colors[i];\n  // load palette into _currentPalette\n  loadPalette(Segment::_currentPalette, palette);\n  if (isInTransition() && prog < 0xFFFFU && blendingStyle == BLEND_STYLE_FADE) {\n    // blend colors\n    for (unsigned i = 0; i < NUM_COLORS; i++) _currentColors[i] = color_blend16(_t->_colors[i], colors[i], prog);\n    // blend palettes\n    // there are about 255 blend passes of 48 \"blends\" to completely blend two palettes (in _dur time)\n    // minimum blend time is 100ms maximum is 65535ms\n    #ifndef WLED_SAVE_RAM\n    unsigned noOfBlends = ((255U * prog) / 0xFFFFU) - _t->_prevPaletteBlends;\n    if(noOfBlends > 255) noOfBlends = 255; // safety check\n    for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, Segment::_currentPalette, 48);\n    Segment::_currentPalette = _t->_palT; // copy transitioning/temporary palette\n    #else\n    unsigned noOfBlends = ((255U * prog) / 0xFFFFU);\n    CRGBPalette16 tmpPalette;\n    loadPalette(tmpPalette, _t->_palette);\n    for (unsigned i = 0; i < noOfBlends; i++) nblendPaletteTowardPalette(tmpPalette, Segment::_currentPalette, 48);\n    Segment::_currentPalette = tmpPalette; // copy transitioning/temporary palette\n    #endif\n  }\n}\n\n// relies on WS2812FX::service() to call it for each frame\nvoid Segment::handleRandomPalette() {\n  unsigned long now = millis();\n  uint16_t now_s = now / 1000; // we only need seconds (and @dedehai hated shift >> 10)\n  now = (now_s)*1000 + (now % 1000); // ignore days (now is limited to 18 hours as now_s can only store 65535s ~ 18h 12min)\n  if (now_s < Segment::_lastPaletteChange) Segment::_lastPaletteChange = 0; // handle overflow (will cause 2*randomPaletteChangeTime glitch at most)\n  // is it time to generate a new palette?\n  if (now_s > Segment::_lastPaletteChange + randomPaletteChangeTime) {\n    Segment::_newRandomPalette  = useHarmonicRandomPalette ? generateHarmonicRandomPalette(Segment::_randomPalette) : generateRandomPalette();\n    Segment::_lastPaletteChange = now_s;\n    Segment::_nextPaletteBlend  = now; // starts blending immediately\n  }\n  // there are about 255 blend passes of 48 \"blends\" to completely blend two palettes (in strip.getTransition() time)\n  // if randomPaletteChangeTime is shorter than strip.getTransition() palette will never fully blend\n  unsigned frameTime = strip.getFrameTime();  // in ms [8-1000]\n  unsigned transitionTime = strip.getTransition(); // in ms [100-65535]\n  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\n  unsigned transitionFrames = frameTime > transitionTime ? 1 : transitionTime / frameTime; // i.e. 700ms/23ms = 30 or 20000ms/8ms = 2500 or 100ms/1000ms = 0 -> 1\n  unsigned noOfBlends = transitionFrames > 255 ? 1 : (255 + (transitionFrames>>1)) / transitionFrames;  // we do some rounding here\n  for (unsigned i = 0; i < noOfBlends; i++) nblendPaletteTowardPalette(Segment::_randomPalette, Segment::_newRandomPalette, 48);\n  Segment::_nextPaletteBlend = now + ((transitionFrames >> 8) * frameTime); // postpone next blend if necessary\n}\n\n// sets Segment geometry (length or width/height and grouping, spacing and offset as well as 2D mapping)\n// strip must be suspended (strip.suspend()) before calling this function\n// this function may call fill() to clear pixels if spacing or mapping changed (which requires setting _vWidth, _vHeight, _vLength or beginDraw())\nvoid 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) {\n  // return if neither bounds nor grouping have changed\n  bool boundsUnchanged = (start == i1 && stop == i2);\n  #ifndef WLED_DISABLE_2D\n  boundsUnchanged &= (startY == i1Y && stopY == i2Y); // 2D\n  #endif\n  boundsUnchanged &= (grouping == grp && spacing == spc); // changing grouping and/or spacing changes virtual segment length (painting dimensions)\n\n  if (stop && (spc > 0 || m12 != map1D2D)) clear();\n  if (grp) { // prevent assignment of 0\n    grouping = grp;\n    spacing = spc;\n  } else {\n    grouping = 1;\n    spacing = 0;\n  }\n  if (ofs < UINT16_MAX) offset = ofs;\n  map1D2D  = constrain(m12, 0, 7);\n\n  if (boundsUnchanged) return;\n\n  unsigned oldLength = length();\n\n  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);\n  markForReset();\n  if (_t) stopTransition(); // we can't use transition if segment dimensions changed\n  stateChanged = true;      // send UDP/WS broadcast\n\n  // apply change immediately\n  if (i2 <= i1) { //disable segment\n    #ifdef WLED_ENABLE_GIF\n    endImagePlayback(this);\n    #endif\n    deallocateData();\n    p_free(pixels);\n    pixels = nullptr;\n    stop = 0;\n    return;\n  }\n  if (i1 < Segment::maxWidth || (i1 >= Segment::maxWidth*Segment::maxHeight && i1 < strip.getLengthTotal())) start = i1; // Segment::maxWidth equals strip.getLengthTotal() for 1D\n  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\n  startY = 0;\n  stopY  = 1;\n  #ifndef WLED_DISABLE_2D\n  if (Segment::maxHeight>1) { // 2D\n    if (i1Y < Segment::maxHeight) startY = i1Y;\n    stopY = constrain(i2Y, 1, Segment::maxHeight);\n  }\n  #endif\n  // safety check\n  if (start >= stop || startY >= stopY) {\n    #ifdef WLED_ENABLE_GIF\n    endImagePlayback(this);\n    #endif\n    deallocateData();\n    p_free(pixels);\n    pixels = nullptr;\n    stop = 0;\n    return;\n  }\n  // allocate FX render buffer\n  if (length() != oldLength) {\n    // 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\n    p_free(pixels);\n    pixels = static_cast<uint32_t*>(allocate_buffer(length() * sizeof(uint32_t), BFRALLOC_PREFER_PSRAM | BFRALLOC_NOBYTEACCESS));\n    if (!pixels) {\n      DEBUGFX_PRINTLN(F(\"!!! Not enough RAM for pixel buffer !!!\"));\n      #ifdef WLED_ENABLE_GIF\n      endImagePlayback(this);\n      #endif\n      deallocateData();\n      errorFlag = ERR_NORAM_PX;\n      stop = 0;\n      return;\n    }\n\n  }\n  refreshLightCapabilities();\n}\n\n\nSegment &Segment::setColor(uint8_t slot, uint32_t c) {\n  if (slot >= NUM_COLORS || c == colors[slot]) return *this;\n  if (!_isRGB && !_hasW) {\n    if (slot == 0 && c == BLACK) return *this; // on/off segment cannot have primary color black\n    if (slot == 1 && c != BLACK) return *this; // on/off segment cannot have secondary color non black\n  }\n  //DEBUG_PRINTF_P(PSTR(\"- Starting color transition: %d [0x%X]\\n\"), slot, c);\n  startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change\n  colors[slot] = c;\n  stateChanged = true; // send UDP/WS broadcast\n  return *this;\n}\n\nSegment &Segment::setCCT(uint16_t k) {\n  if (k > 255) { //kelvin value, convert to 0-255\n    if (k < 1900)  k = 1900;\n    if (k > 10091) k = 10091;\n    k = (k - 1900) >> 5;\n  }\n  if (cct != k) {\n    //DEBUG_PRINTF_P(PSTR(\"- Starting CCT transition: %d\\n\"), k);\n    startTransition(strip.getTransition(), false); // start transition prior to change (no need to copy segment)\n    cct = k;\n    stateChanged = true; // send UDP/WS broadcast\n  }\n  return *this;\n}\n\nSegment &Segment::setOpacity(uint8_t o) {\n  if (opacity != o) {\n    //DEBUG_PRINTF_P(PSTR(\"- Starting opacity transition: %d\\n\"), o);\n    startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change\n    opacity = o;\n    stateChanged = true; // send UDP/WS broadcast\n  }\n  return *this;\n}\n\nSegment &Segment::setOption(uint8_t n, bool val) {\n  bool prev = (options >> n) & 0x01;\n  if (val == prev) return *this;\n  //DEBUG_PRINTF_P(PSTR(\"- Starting option transition: %d\\n\"), n);\n  if (n == SEG_OPTION_ON) startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change\n  if (val) options |=   0x01 << n;\n  else     options &= ~(0x01 << n);\n  stateChanged = true; // send UDP/WS broadcast\n  return *this;\n}\n\nSegment &Segment::setMode(uint8_t fx, bool loadDefaults) {\n  // skip reserved\n  while (fx < strip.getModeCount() && strncmp_P(\"RSVD\", strip.getModeData(fx), 4) == 0) fx++;\n  if (fx >= strip.getModeCount()) fx = 0; // set solid mode\n  // if we have a valid mode & is not reserved\n  if (fx != mode) {\n    startTransition(strip.getTransition(), true); // set effect transitions (must create segment copy)\n    mode = fx;\n    int sOpt;\n    // load default values from effect string\n    if (loadDefaults) {\n      sOpt = extractModeDefaults(fx, \"sx\");  speed     = (sOpt >= 0) ? sOpt : DEFAULT_SPEED;\n      sOpt = extractModeDefaults(fx, \"ix\");  intensity = (sOpt >= 0) ? sOpt : DEFAULT_INTENSITY;\n      sOpt = extractModeDefaults(fx, \"c1\");  custom1   = (sOpt >= 0) ? sOpt : DEFAULT_C1;\n      sOpt = extractModeDefaults(fx, \"c2\");  custom2   = (sOpt >= 0) ? sOpt : DEFAULT_C2;\n      sOpt = extractModeDefaults(fx, \"c3\");  custom3   = (sOpt >= 0) ? sOpt : DEFAULT_C3;\n      sOpt = extractModeDefaults(fx, \"o1\");  check1    = (sOpt >= 0) ? (bool)sOpt : false;\n      sOpt = extractModeDefaults(fx, \"o2\");  check2    = (sOpt >= 0) ? (bool)sOpt : false;\n      sOpt = extractModeDefaults(fx, \"o3\");  check3    = (sOpt >= 0) ? (bool)sOpt : false;\n      sOpt = extractModeDefaults(fx, \"m12\"); if (sOpt >= 0) map1D2D   = constrain(sOpt, 0, 7); else map1D2D = M12_Pixels;  // reset mapping if not defined (2D FX may not work)\n      sOpt = extractModeDefaults(fx, \"si\");  if (sOpt >= 0) soundSim  = constrain(sOpt, 0, 3);\n      sOpt = extractModeDefaults(fx, \"rev\"); if (sOpt >= 0) reverse   = (bool)sOpt;\n      sOpt = extractModeDefaults(fx, \"mi\");  if (sOpt >= 0) mirror    = (bool)sOpt; // NOTE: setting this option is a risky business\n      sOpt = extractModeDefaults(fx, \"rY\");  if (sOpt >= 0) reverse_y = (bool)sOpt;\n      sOpt = extractModeDefaults(fx, \"mY\");  if (sOpt >= 0) mirror_y  = (bool)sOpt; // NOTE: setting this option is a risky business\n    }\n    sOpt = extractModeDefaults(fx, \"pal\"); // always extract 'pal' to set _default_palette\n    if (sOpt >= 0 && loadDefaults) setPalette(sOpt);\n    if (sOpt <= 0) sOpt = 6; // partycolors if zero or not set\n    _default_palette = sOpt; // _deault_palette is loaded into pal0 in loadPalette() (if selected)\n    markForReset();\n    stateChanged = true; // send UDP/WS broadcast\n  }\n  return *this;\n}\n\nSegment &Segment::setPalette(uint8_t pal) {\n  if (pal <= 255-customPalettes.size() && pal > FIXED_PALETTE_COUNT) pal = 0; // not built in palette or custom palette\n  if (pal != palette) {\n    //DEBUG_PRINTF_P(PSTR(\"- Starting palette transition: %d\\n\"), pal);\n    startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change (no need to copy segment)\n    palette = pal;\n    stateChanged = true; // send UDP/WS broadcast\n  }\n  return *this;\n}\n\nSegment &Segment::setName(const char *newName) {\n  if (newName) {\n    const int newLen = min(strlen(newName), (size_t)WLED_MAX_SEGNAME_LEN);\n    if (newLen) {\n      if (name) p_free(name); // free old name\n      name = static_cast<char*>(allocate_buffer(newLen+1, BFRALLOC_PREFER_PSRAM));\n      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\n      if (name) strlcpy(name, newName, newLen+1);\n      return *this;\n    }\n  }\n  return clearName();\n}\n\n// 2D matrix\nunsigned Segment::virtualWidth() const {\n  unsigned groupLen = groupLength();\n  unsigned vWidth = ((transpose ? height() : width()) + groupLen - 1) / groupLen;\n  if (mirror) vWidth = (vWidth + 1) /2;  // divide by 2 if mirror, leave at least a single LED\n  return vWidth;\n}\n\nunsigned Segment::virtualHeight() const {\n  unsigned groupLen = groupLength();\n  unsigned vHeight = ((transpose ? width() : height()) + groupLen - 1) / groupLen;\n  if (mirror_y) vHeight = (vHeight + 1) /2;  // divide by 2 if mirror, leave at least a single LED\n  return vHeight;\n}\n\n// Constants for mapping mode \"Pinwheel\"\n#ifndef WLED_DISABLE_2D\nconstexpr int Fixed_Scale = 16384; // fixpoint scaling factor (14bit for fraction)\n// Pinwheel helper function: matrix dimensions to number of rays\nstatic int getPinwheelLength(int vW, int vH) {\n  // Returns multiple of 8, prevents over drawing\n  return (max(vW, vH) + 15) & ~7;\n}\nstatic void setPinwheelParameters(int i, int vW, int vH, int& startx, int& starty, int* cosVal, int* sinVal, bool getPixel = false) {\n  int steps = getPinwheelLength(vW, vH);\n  int baseAngle = ((0xFFFF + steps / 2) / steps);  // 360° / steps, in 16 bit scale round to nearest integer\n  int rotate = 0;\n  if (getPixel) rotate = baseAngle / 2; // rotate by half a ray width when reading pixel color\n  for (int k = 0; k < 2; k++) // angular steps for two consecutive rays\n  {\n    int angle = (i + k) * baseAngle + rotate;\n    cosVal[k] = (cos16_t(angle) * Fixed_Scale) >> 15; // step per pixel in fixed point, cos16 output is -0x7FFF to +0x7FFF\n    sinVal[k] = (sin16_t(angle) * Fixed_Scale) >> 15; // using explicit bit shifts as dividing negative numbers is not equivalent (rounding error is acceptable)\n  }\n  startx = (vW * Fixed_Scale) / 2; // + cosVal[0] / 4; // starting position = center + 1/4 pixel (in fixed point)\n  starty = (vH * Fixed_Scale) / 2; // + sinVal[0] / 4;\n}\n#endif\n\n// 1D strip\nuint16_t Segment::virtualLength() const {\n#ifndef WLED_DISABLE_2D\n  if (is2D()) {\n    unsigned vW = virtualWidth();\n    unsigned vH = virtualHeight();\n    unsigned vLen;\n    switch (map1D2D) {\n      case M12_pBar:\n        vLen = vH;\n        break;\n      case M12_pCorner:\n        vLen = max(vW,vH); // get the longest dimension\n        break;\n      case M12_pArc:\n        vLen = sqrt32_bw(vH*vH + vW*vW); // use diagonal\n        break;\n      case M12_sPinwheel:\n        vLen = getPinwheelLength(vW, vH);\n        break;\n      default:\n        vLen = vW * vH; // use all pixels from segment\n        break;\n    }\n    return vLen;\n  }\n#endif\n  unsigned groupLen = groupLength(); // is always >= 1\n  unsigned vLength = (length() + groupLen - 1) / groupLen;\n  if (mirror) vLength = (vLength + 1) /2;  // divide by 2 if mirror, leave at least a single LED\n  return vLength;\n}\n\n#ifndef WLED_DISABLE_2D\n// maximum length of a mapped 1D segment, used in PS for buffer allocation\nuint16_t Segment::maxMappingLength() const {\n  uint32_t vW = virtualWidth();\n  uint32_t vH = virtualHeight();\n  return max(sqrt32_bw(vH*vH + vW*vW), (uint32_t)getPinwheelLength(vW, vH)); // use diagonal\n}\n#endif\n// pixel is clipped if it falls outside clipping range\n// if clipping start > stop the clipping range is inverted\nbool Segment::isPixelClipped(int i) const {\n  if (blendingStyle != BLEND_STYLE_FADE && isInTransition() && _clipStart != _clipStop) {\n    bool invert = _clipStart > _clipStop;  // ineverted start & stop\n    int start = invert ? _clipStop : _clipStart;\n    int stop  = invert ? _clipStart : _clipStop;\n    if (blendingStyle == BLEND_STYLE_FAIRY_DUST) {\n      unsigned len = stop - start;\n      if (len < 2) return false;\n      unsigned shuffled = hashInt(i) % len;\n      unsigned pos = (shuffled * 0xFFFFU) / len;\n      return progress() <= pos;\n    }\n    const bool iInside = (i >= start && i < stop);\n    return !iInside ^ invert; // thanks @willmmiles (https://github.com/wled/WLED/pull/3877#discussion_r1554633876)\n  }\n  return false;\n}\n\nvoid WLED_O2_ATTR Segment::setPixelColor(int i, uint32_t col) const\n{\n  if (!isActive() || i < 0) return; // not active or invalid index\n#ifndef WLED_DISABLE_2D\n  int vStrip = 0;\n#endif\n  const int vL = vLength();\n  // if the 1D effect is using virtual strips \"i\" will have virtual strip id stored in upper 16 bits\n  // in such case \"i\" will be > virtualLength()\n  if (i >= vL) {\n    // check if this is a virtual strip\n    #ifndef WLED_DISABLE_2D\n    vStrip = i>>16; // hack to allow running on virtual strips (2D segment columns/rows)\n    #endif\n    i &= 0xFFFF;          // truncate vstrip index. note: vStrip index is 1 even in 1D, still need to truncate\n    if (i >= vL) return;  // if pixel would still fall out of segment just exit\n  }\n\n#ifndef WLED_DISABLE_2D\n  if (is2D()) {\n    const int vW = vWidth();   // segment width in logical pixels (can be 0 if segment is inactive)\n    const int vH = vHeight();  // segment height in logical pixels (is always >= 1)\n    const auto XY = [&](unsigned x, unsigned y){ return x + y*vW;};\n    switch (map1D2D) {\n      case M12_Pixels:\n        // use all available pixels as a long strip\n        setPixelColorRaw(XY(i % vW, i / vW), col);\n        break;\n      case M12_pBar:\n        // expand 1D effect vertically or have it play on virtual strips\n        if (vStrip > 0)                   setPixelColorRaw(XY(vStrip - 1, vH - i - 1), col);\n        else for (int x = 0; x < vW; x++) setPixelColorRaw(XY(x, vH - i - 1), col);\n        break;\n      case M12_pArc:\n        // expand in circular fashion from center\n        if (i == 0)\n          setPixelColorRaw(XY(0, 0), col);\n        else {\n          float r = i;\n          float step = HALF_PI / (2.8284f * r + 4); // we only need (PI/4)/(r/sqrt(2)+1) steps\n          for (float rad = 0.0f; rad <= (HALF_PI/2)+step/2; rad += step) {\n            int x = roundf(sin_t(rad) * r);\n            int y = roundf(cos_t(rad) * r);\n            // exploit symmetry\n            setPixelColorXY(x, y, col);\n            setPixelColorXY(y, x, col);\n          }\n          // Bresenham’s Algorithm (may not fill every pixel)\n          //int d = 3 - (2*i);\n          //int y = i, x = 0;\n          //while (y >= x) {\n          //  setPixelColorXY(x, y, col);\n          //  setPixelColorXY(y, x, col);\n          //  x++;\n          //  if (d > 0) {\n          //    y--;\n          //    d += 4 * (x - y) + 10;\n          //  } else {\n          //    d += 4 * x + 6;\n          //  }\n          //}\n        }\n        break;\n      case M12_pCorner:\n        for (int x = 0; x <= i; x++) setPixelColorXY(x, i, col); // note: <= to include i=0. Relies on overflow check in sPC()\n        for (int y = 0; y <  i; y++) setPixelColorXY(i, y, col);\n        break;\n      case M12_sPinwheel: {\n        // Uses Bresenham's algorithm to place coordinates of two lines in arrays then draws between them\n        int startX, startY, cosVal[2], sinVal[2]; // in fixed point scale\n        setPinwheelParameters(i, vW, vH, startX, startY, cosVal, sinVal);\n\n        unsigned maxLineLength = max(vW, vH) + 2; // pixels drawn is always smaller than dx or dy, +1 pair for rounding errors\n        uint16_t lineCoords[2][maxLineLength];    // uint16_t to save ram\n        int lineLength[2] = {0};\n\n        static int prevRays[2] = {INT_MAX, INT_MAX}; // previous two ray numbers\n        int closestEdgeIdx = INT_MAX; // index of the closest edge pixel\n\n        for (int lineNr = 0; lineNr < 2; lineNr++) {\n          int x0 = startX; // x, y coordinates in fixed scale\n          int y0 = startY;\n          int x1 = (startX + (cosVal[lineNr] << 9)); // outside of grid\n          int y1 = (startY + (sinVal[lineNr] << 9)); // outside of grid\n          const int dx =  abs(x1-x0), sx = x0<x1 ? 1 : -1; // x distance & step\n          const int dy = -abs(y1-y0), sy = y0<y1 ? 1 : -1; // y distance & step\n          uint16_t* coordinates = lineCoords[lineNr]; // 1D access is faster\n          int* length = &lineLength[lineNr];          // faster access\n          x0 /= Fixed_Scale; // convert to pixel coordinates\n          y0 /= Fixed_Scale;\n\n          // Bresenham's algorithm\n          int idx = 0;\n          int err = dx + dy;\n          while (true) {\n            if ((unsigned)x0 >= (unsigned)vW || (unsigned)y0 >= (unsigned)vH) {\n              closestEdgeIdx = min(closestEdgeIdx, idx-2);\n              break; // stop if outside of grid (exploit unsigned int overflow)\n            }\n            coordinates[idx++] = x0;\n            coordinates[idx++] = y0;\n            (*length)++;\n            // note: since endpoint is out of grid, no need to check if endpoint is reached\n            int e2 = 2 * err;\n            if (e2 >= dy) { err += dy; x0 += sx; }\n            if (e2 <= dx) { err += dx; y0 += sy; }\n          }\n        }\n\n        // fill up the shorter line with missing coordinates, so block filling works correctly and efficiently\n        int diff = lineLength[0] - lineLength[1];\n        int longLineIdx = (diff > 0) ? 0 : 1;\n        int shortLineIdx = longLineIdx ? 0 : 1;\n        if (diff != 0) {\n          int idx = (lineLength[shortLineIdx] - 1) * 2; // last valid coordinate index\n          int lastX = lineCoords[shortLineIdx][idx++];\n          int lastY = lineCoords[shortLineIdx][idx++];\n          bool keepX = lastX == 0 || lastX == vW - 1;\n          for (int d = 0; d < abs(diff); d++) {\n            lineCoords[shortLineIdx][idx] = keepX ? lastX :lineCoords[longLineIdx][idx];\n            idx++;\n            lineCoords[shortLineIdx][idx] =  keepX ? lineCoords[longLineIdx][idx] : lastY;\n            idx++;\n          }\n        }\n\n        // draw and block-fill the line coordinates. Note: block filling only efficient if angle between lines is small\n        closestEdgeIdx += 2;\n        int max_i = getPinwheelLength(vW, vH) - 1;\n        bool drawFirst = !(prevRays[0] == i - 1 || (i == 0 && prevRays[0] == max_i)); // draw first line if previous ray was not adjacent including wrap\n        bool drawLast  = !(prevRays[0] == i + 1 || (i == max_i && prevRays[0] == 0)); // same as above for last line\n        for (int idx = 0; idx < lineLength[longLineIdx] * 2;) { //!! should be long line idx!\n          int x1 = lineCoords[0][idx];\n          int x2 = lineCoords[1][idx++];\n          int y1 = lineCoords[0][idx];\n          int y2 = lineCoords[1][idx++];\n          int minX, maxX, minY, maxY;\n          (x1 < x2) ? (minX = x1, maxX = x2) : (minX = x2, maxX = x1);\n          (y1 < y2) ? (minY = y1, maxY = y2) : (minY = y2, maxY = y1);\n\n          // fill the block between the two x,y points\n          bool alwaysDraw = (drawFirst && drawLast) || // No adjacent rays, draw all pixels\n                            (idx > closestEdgeIdx)  || // Edge pixels on uneven lines are always drawn\n                            (i == 0 && idx == 2)    || // Center pixel special case\n                            (i == prevRays[1]);        // Effect drawing twice in 1 frame\n          for (int x = minX; x <= maxX; x++) {\n            for (int y = minY; y <= maxY; y++) {\n              bool onLine1 = x == x1 && y == y1;\n              bool onLine2 = x == x2 && y == y2;\n              if ((alwaysDraw) ||\n                  (!onLine1 && (!onLine2 || drawLast))  || // Middle pixels and line2 if drawLast\n                  (!onLine2 && (!onLine1 || drawFirst))    // Middle pixels and line1 if drawFirst\n                ) {\n                setPixelColorXY(x, y, col);\n              }\n            }\n          }\n        }\n        prevRays[1] = prevRays[0];\n        prevRays[0] = i;\n        break;\n      }\n    }\n    return;\n  } else if (Segment::maxHeight != 1 && (width() == 1 || height() == 1)) {\n    if (start < Segment::maxWidth*Segment::maxHeight) {\n      // we have a vertical or horizontal 1D segment (WARNING: virtual...() may be transposed)\n      int x = 0, y = 0;\n      if (vHeight() > 1) y = i;\n      if (vWidth()  > 1) x = i;\n      setPixelColorXY(x, y, col);\n      return;\n    }\n  }\n#endif\n  setPixelColorRaw(i, col);\n}\n\n#ifdef WLED_USE_AA_PIXELS\n// anti-aliased normalized version of setPixelColor()\nvoid Segment::setPixelColor(float i, uint32_t col, bool aa) const\n{\n  if (!isActive()) return; // not active\n  int vStrip = int(i/10.0f); // hack to allow running on virtual strips (2D segment columns/rows)\n  i -= int(i);\n\n  if (i<0.0f || i>1.0f) return; // not normalized\n\n  float fC = i * (virtualLength()-1);\n  if (aa) {\n    unsigned iL = roundf(fC-0.49f);\n    unsigned iR = roundf(fC+0.49f);\n    float    dL = (fC - iL)*(fC - iL);\n    float    dR = (iR - fC)*(iR - fC);\n    uint32_t cIL = getPixelColor(iL | (vStrip<<16));\n    uint32_t cIR = getPixelColor(iR | (vStrip<<16));\n    if (iR!=iL) {\n      // blend L pixel\n      cIL = color_blend(col, cIL, uint8_t(dL*255.0f));\n      setPixelColor(iL | (vStrip<<16), cIL);\n      // blend R pixel\n      cIR = color_blend(col, cIR, uint8_t(dR*255.0f));\n      setPixelColor(iR | (vStrip<<16), cIR);\n    } else {\n      // exact match (x & y land on a pixel)\n      setPixelColor(iL | (vStrip<<16), col);\n    }\n  } else {\n    setPixelColor(int(roundf(fC)) | (vStrip<<16), col);\n  }\n}\n#endif\n\nuint32_t WLED_O2_ATTR Segment::getPixelColor(int i) const\n{\n  if (!isActive() || i < 0) return 0; // not active or invalid index\n\n#ifndef WLED_DISABLE_2D\n  int vStrip = i>>16; // virtual strips are only relevant in Bar expansion mode\n  i &= 0xFFFF;\n#endif\n  if (i >= (int)vLength()) return 0;\n\n#ifndef WLED_DISABLE_2D\n  if (is2D()) {\n    const int vW = vWidth();   // segment width in logical pixels (can be 0 if segment is inactive)\n    const int vH = vHeight();  // segment height in logical pixels (is always >= 1)\n    int x = 0, y = 0;\n    switch (map1D2D) {\n      case M12_Pixels:\n        x = i % vW;\n        y = i / vW;\n        break;\n      case M12_pBar:\n        if (vStrip > 0) { x = vStrip - 1; y = vH - i - 1; }\n        else            { y = vH - i - 1; };\n        break;\n      case M12_pArc:\n        if (i > vW && i > vH) {\n          x = y = sqrt32_bw(i*i/2);\n          break; // use diagonal\n        }\n        // otherwise fallthrough\n      case M12_pCorner:\n        // use longest dimension\n        if (vW > vH) x = i;\n        else         y = i;\n        break;\n      case M12_sPinwheel: {\n        // not 100% accurate, returns pixel at outer edge\n        int cosVal[2], sinVal[2];\n        setPinwheelParameters(i, vW, vH, x, y, cosVal, sinVal, true);\n        int maxX = (vW-1) * Fixed_Scale;\n        int maxY = (vH-1) * Fixed_Scale;\n        // trace ray from center until we hit any edge - to avoid rounding problems, we use fixed point coordinates\n        while ((x < maxX)  && (y < maxY) && (x > Fixed_Scale) && (y > Fixed_Scale)) {\n          x += cosVal[0]; // advance to next position\n          y += sinVal[0];\n        }\n        x /= Fixed_Scale;\n        y /= Fixed_Scale;\n        break;\n      }\n    }\n    return getPixelColorXY(x, y);\n  }\n#endif\n  return getPixelColorRaw(i);\n}\n\nvoid Segment::refreshLightCapabilities() const {\n  unsigned capabilities = 0;\n\n  if (!isActive()) {\n    _capabilities = 0;\n    return;\n  }\n\n  // we must traverse each pixel in segment to determine its capabilities (as pixel may be mapped)\n  for (unsigned y = startY; y < stopY; y++) for (unsigned x = start; x < stop; x++) {\n    unsigned index = x + Segment::maxWidth * y;\n    index = strip.getMappedPixelIndex(index); // convert logical address to physical\n    if (index == 0xFFFF) continue;  // invalid/missing  pixel\n    for (unsigned b = 0; b < BusManager::getNumBusses(); b++) {\n      const Bus *bus = BusManager::getBus(b);\n      if (!bus || !bus->isOk()) break;\n      if (bus->containsPixel(index)) {\n        if (bus->hasRGB() || (strip.cctFromRgb && bus->hasCCT())) capabilities |= SEG_CAPABILITY_RGB;\n        if (!strip.cctFromRgb && bus->hasCCT())                   capabilities |= SEG_CAPABILITY_CCT;\n        if (strip.correctWB && (bus->hasRGB() || bus->hasCCT()))  capabilities |= SEG_CAPABILITY_CCT; //white balance correction (CCT slider)\n        if (bus->hasWhite()) {\n          unsigned aWM = Bus::getGlobalAWMode() == AW_GLOBAL_DISABLED ? bus->getAutoWhiteMode() : Bus::getGlobalAWMode();\n          bool whiteSlider = (aWM == RGBW_MODE_DUAL || aWM == RGBW_MODE_MANUAL_ONLY); // white slider allowed\n          // if auto white calculation from RGB is active (Accurate/Brighter), force RGB controls even if there are no RGB busses\n          if (!whiteSlider) capabilities |= SEG_CAPABILITY_RGB;\n          // if auto white calculation from RGB is disabled/optional (None/Dual), allow white channel adjustments\n          if ( whiteSlider) capabilities |= SEG_CAPABILITY_W;\n        }\n        break;\n      }\n    }\n  }\n  _capabilities = capabilities;\n}\n\n/*\n * Fills segment with color\n */\nvoid Segment::fill(uint32_t c) const {\n  if (!isActive()) return; // not active\n  for (unsigned i = 0; i < length(); i++) setPixelColorRaw(i,c); // always fill all pixels (blending will take care of grouping, spacing and clipping)\n}\n\n/*\n * fade out function, higher rate = quicker fade\n * fading is highly dependant on frame rate (higher frame rates, faster fading)\n * each frame will fade at max 9% or as little as 0.8%\n */\nvoid Segment::fade_out(uint8_t rate) const {\n  if (!isActive()) return; // not active\n  rate = (256-rate) >> 1;\n  const int mappedRate = 256 / (rate + 1);\n  const size_t rlength = rawLength();  // calculate only once\n  for (unsigned j = 0; j < rlength; j++) {\n    uint32_t color = getPixelColorRaw(j);\n    if (color == colors[1]) continue; // already at target color\n    for (int i = 0; i < 32; i += 8) {\n      uint8_t c2 = (colors[1]>>i);  // get background channel\n      uint8_t c1 = (color>>i);      // get foreground channel\n      // we can't use bitshift since we are using int\n      int delta = (c2 - c1) * mappedRate / 256;\n      // if fade isn't complete, make sure delta is at least 1 (fixes rounding issues)\n      if (delta == 0) delta += (c2 == c1) ? 0 : (c2 > c1) ? 1 : -1;\n      // stuff new value back into color\n      color &= ~(0xFF<<i);\n      color |= ((c1 + delta) & 0xFF) << i;\n    }\n    setPixelColorRaw(j, color);\n  }\n}\n\n// fades all pixels to secondary color\nvoid Segment::fadeToSecondaryBy(uint8_t fadeBy) const {\n  if (!isActive() || fadeBy == 0) return;   // optimization - no scaling to apply\n  const size_t rlength = rawLength();  // calculate only once\n  for (unsigned i = 0; i < rlength; i++) setPixelColorRaw(i, color_blend(getPixelColorRaw(i), colors[1], fadeBy));\n}\n\n// fades all pixels to black using nscale8()\nvoid Segment::fadeToBlackBy(uint8_t fadeBy) const {\n  if (!isActive() || fadeBy == 0) return;   // optimization - no scaling to apply\n  const size_t rlength = rawLength();  // calculate only once\n  for (unsigned i = 0; i < rlength; i++) setPixelColorRaw(i, fast_color_scale(getPixelColorRaw(i), 255-fadeBy));\n}\n\n/*\n * blurs segment content, source: FastLED colorutils.cpp\n * Note: for blur_amount > 215 this function does not work properly (creates alternating pattern)\n */\nvoid Segment::blur(uint8_t blur_amount, bool smear) const {\n  if (!isActive() || blur_amount == 0) return; // optimization: 0 means \"don't blur\"\n#ifndef WLED_DISABLE_2D\n  if (is2D()) {\n    // compatibility with 2D\n    blur2D(blur_amount, blur_amount, smear); // symmetrical 2D blur\n    //box_blur(map(blur_amount,1,255,1,3), smear);\n    return;\n  }\n#endif\n  uint8_t keep = smear ? 255 : 255 - blur_amount;\n  uint8_t seep = blur_amount >> 1;\n  unsigned vlength = vLength();\n  // handle first pixel to avoid conditional in loop (faster)\n  uint32_t cur = getPixelColorRaw(0);\n  uint32_t carryover = fast_color_scale(cur, seep);\n  setPixelColorRaw(0, fast_color_scale(cur, keep));\n  for (unsigned i = 1; i < vlength; i++) {\n    cur = getPixelColorRaw(i);\n    uint32_t part = fast_color_scale(cur, seep);\n    cur = fast_color_scale(cur, keep);\n    cur = color_add(cur, carryover);\n    setPixelColorRaw(i - 1, color_add(getPixelColorRaw(i - 1), part)); // previous pixel\n    setPixelColorRaw(i, cur); // current pixel\n    carryover = part;\n  }\n}\n\n/*\n * Put a value 0 to 255 in to get a color value.\n * The colours are a transition r -> g -> b -> back to r\n * Rotates the color in HSV space, where pos is H. (0=0deg, 256=360deg)\n */\nuint32_t Segment::color_wheel(uint8_t pos) const {\n  if (palette) return color_from_palette(pos, false, false, 0); // only wrap if \"always wrap\" is set\n  uint8_t w = W(getCurrentColor(0));\n  uint32_t rgb;\n  hsv2rgb(CHSV32(static_cast<uint16_t>(pos << 8), 255, 255), rgb);\n  return rgb | (w << 24); // add white channel\n}\n\n/*\n * Gets a single color from the currently selected palette.\n * @param i Palette Index (if mapping is true, the full palette will be _virtualSegmentLength long, if false, 255). Will wrap around automatically.\n * @param mapping if true, LED position in segment is considered for color\n * @param moving FastLED palettes will usually wrap back to the start smoothly. Set to true if effect has moving palette and you want wrap.\n * @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\n * @param pbri Value to scale the brightness of the returned color by. Default is 255. (no scaling)\n * @returns Single color from palette\n */\nuint32_t Segment::color_from_palette(uint16_t i, bool mapping, bool moving, uint8_t mcol, uint8_t pbri) const {\n  uint32_t color = getCurrentColor(mcol);\n  // default palette or no RGB support on segment\n  if ((palette == 0 && mcol < NUM_COLORS) || !_isRGB) {\n    return color_fade(color, pbri, true);\n  }\n\n  unsigned paletteIndex = i;\n  if (mapping) paletteIndex = min((i*255)/vLength(), 255U);\n  // paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined/no interpolation of palette entries)\n  // ColorFromPalette interpolations are: NOBLEND, LINEARBLEND, LINEARBLEND_NOWRAP\n  TBlendType blend = NOBLEND;\n  switch (paletteBlend) {\n    case 0: blend = moving ? LINEARBLEND : LINEARBLEND_NOWRAP; break;\n    case 1: blend = LINEARBLEND; break;\n    case 2: blend = LINEARBLEND_NOWRAP; break;\n  }\n  CRGBW palcol = ColorFromPalette(_currentPalette, paletteIndex, pbri, blend);\n  palcol.w = W(color);\n\n  return palcol.color32;\n}\n\n\n///////////////////////////////////////////////////////////////////////////////\n// WS2812FX class implementation\n///////////////////////////////////////////////////////////////////////////////\n\n//do not call this method from system context (network callback)\nvoid WS2812FX::finalizeInit() {\n  //reset segment runtimes\n  restartRuntime();\n\n  // for the lack of better place enumerate ledmaps here\n  // if we do it in json.cpp (serializeInfo()) we are getting flashes on LEDs\n  // unfortunately this means we do not get updates after uploads\n  // the other option is saving UI settings which will cause enumeration\n  enumerateLedmaps();\n\n  _hasWhiteChannel = _isOffRefreshRequired = false;\n  BusManager::removeAll();\n  // 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.\n  unsigned digitalCount = 0;\n  #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3)\n  // validate the bus config: count I2S buses and check if they meet requirements\n  unsigned i2sBusCount = 0;\n\n  for (const auto &bus : busConfigs) {\n    if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type)) {\n      digitalCount++;\n      if (bus.driverType == 1)\n        i2sBusCount++;\n    }\n  }\n  DEBUG_PRINTF_P(PSTR(\"Digital buses: %u, I2S buses: %u\\n\"), digitalCount, i2sBusCount);\n\n  // Determine parallel vs single I2S usage (used for memory calculation only)\n  bool useParallelI2S = false;\n  #if defined(CONFIG_IDF_TARGET_ESP32S3)\n  // ESP32-S3 always uses parallel LCD driver for I2S\n  if (i2sBusCount > 0) {\n    useParallelI2S = true;\n  }\n  #else\n  if (i2sBusCount > 1) {\n    useParallelI2S = true;\n  }\n  #endif\n  #endif\n\n  DEBUG_PRINTF_P(PSTR(\"Heap before buses: %d\\n\"), getFreeHeapSize());\n  // create buses/outputs\n  unsigned mem = 0; // memory estimation including DMA buffer for I2S and pixel buffers\n  unsigned I2SdmaMem = 0;\n  for (auto &bus : busConfigs) {\n    // assign bus types: call to getI() determines bus types/drivers, allocates and tracks polybus channels\n    // store the result in iType for later use during bus creation (getI() must only be called once per BusConfig)\n    // note: this needs to be determined for all buses prior to creating them as it also determines parallel I2S usage\n    bus.iType = BusManager::getI(bus.type, bus.pins, bus.driverType);\n  }\n  for (auto &bus : busConfigs) {\n    bool use_placeholder = false;\n    unsigned busMemUsage = bus.memUsage(); // does not include DMA/RMT buffer but includes pixel buffers (segment buffer + global buffer)\n    mem += busMemUsage;\n    // estimate maximum I2S memory usage (only relevant for digital non-2pin busses when I2S is enabled)\n    #if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(ESP8266)\n    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\n    if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) && usesI2S) {\n      #ifdef NPB_CONF_4STEP_CADENCE\n      constexpr unsigned stepFactor = 4; // 4 step cadence (4 bits per pixel bit)\n      #else\n      constexpr unsigned stepFactor = 3; // 3 step cadence (3 bits per pixel bit)\n      #endif\n      unsigned i2sCommonMem = (stepFactor * bus.count * (3*Bus::hasRGB(bus.type)+Bus::hasWhite(bus.type)+Bus::hasCCT(bus.type)) * (Bus::is16bit(bus.type)+1));\n      if (useParallelI2S) i2sCommonMem *= 8; // parallel I2S uses 8 channels, requiring 8x the DMA buffer size (common buffer shared between all parallel busses)\n      if (i2sCommonMem > I2SdmaMem) I2SdmaMem = i2sCommonMem;\n    }\n    #endif\n    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)\n      DEBUG_PRINTF_P(PSTR(\"Bus %d with %d LEDS memory usage exceeds limit\\n\"), (int)bus.type, bus.count);\n      errorFlag = ERR_NORAM; // alert UI  TODO: make this a distinct error: not enough memory for bus\n      use_placeholder = true;\n    }\n    if (BusManager::add(bus, use_placeholder) != -1) {\n      mem += BusManager::busses.back()->getBusSize();\n      if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) && BusManager::busses.back()->isPlaceholder()) digitalCount--; // remove placeholder from digital count\n    }\n  }\n  DEBUG_PRINTF_P(PSTR(\"Estimated buses + pixel-buffers size: %uB\\n\"), mem + I2SdmaMem);\n  busConfigs.clear();\n  busConfigs.shrink_to_fit();\n\n  _length = 0;\n  for (size_t i=0; i<BusManager::getNumBusses(); i++) {\n    Bus *bus = BusManager::getBus(i);\n    if (!bus || !bus->isOk() || bus->getStart() + bus->getLength() > MAX_LEDS) break;\n    //RGBW mode is enabled if at least one of the strips is RGBW\n    _hasWhiteChannel |= bus->hasWhite();\n    //refresh is required to remain off if at least one of the strips requires the refresh.\n    _isOffRefreshRequired |= bus->isOffRefreshRequired() && !bus->isPWM(); // use refresh bit for phase shift with analog\n    unsigned busEnd = bus->getStart() + bus->getLength();\n    if (busEnd > _length) _length = busEnd;\n    // This must be done after all buses have been created, as some kinds (parallel I2S) interact\n    bus->begin();\n    bus->setBrightness(scaledBri(bri));\n  }\n  BusManager::initializeABL(); // init brightness limiter\n  DEBUG_PRINTF_P(PSTR(\"Heap after buses: %d\\n\"), ESP.getFreeHeap());\n\n  Segment::maxWidth  = _length;\n  Segment::maxHeight = 1;\n\n  //segments are created in makeAutoSegments();\n  DEBUG_PRINTLN(F(\"Loading custom palettes\"));\n  loadCustomPalettes(); // (re)load all custom palettes\n  DEBUG_PRINTLN(F(\"Loading custom ledmaps\"));\n  deserializeMap();     // (re)load default ledmap (will also setUpMatrix() if ledmap does not exist)\n\n  // allocate frame buffer after matrix has been set up (gaps!)\n  p_free(_pixels); // using realloc on large buffers can cause additional fragmentation instead of reducing it\n  // use PSRAM if available: there is no measurable perfomance impact between PSRAM and DRAM on S2/S3 with QSPI PSRAM for this buffer\n  _pixels = static_cast<uint32_t*>(allocate_buffer(getLengthTotal() * sizeof(uint32_t), BFRALLOC_ENFORCE_PSRAM | BFRALLOC_NOBYTEACCESS | BFRALLOC_CLEAR));\n  DEBUG_PRINTF_P(PSTR(\"strip buffer size: %uB\\n\"), getLengthTotal() * sizeof(uint32_t));\n  DEBUG_PRINTF_P(PSTR(\"Heap after strip init: %uB\\n\"), getFreeHeapSize());\n}\n\nvoid WS2812FX::service() {\n  unsigned long nowUp = millis(); // Be aware, millis() rolls over every 49 days\n  now = nowUp + timebase;\n  unsigned long elapsed = nowUp - _lastServiceShow;\n  if (_suspend || elapsed <= MIN_FRAME_DELAY) return;   // keep wifi alive - no matter if triggered or unlimited\n  if (!_triggered && (_targetFps != FPS_UNLIMITED)) {   // unlimited mode = no frametime\n    if (elapsed < _frametime) return;                   // too early for service\n  }\n\n  bool doShow = false;\n\n  _isServicing = true;\n  _segment_index = 0;\n\n  for (Segment &seg : _segments) {\n    if (_suspend) break; // immediately stop processing segments if suspend requested during service()\n\n    // process transition (also pre-calculates progress value)\n    seg.handleTransition();\n    // reset the segment runtime data if needed\n    seg.resetIfRequired();\n\n    if (!seg.isActive()) continue;\n\n    // last condition ensures all solid segments are updated at the same time\n    if (nowUp > _lastServiceShow + _frametime || _triggered || (doShow && seg.mode == FX_MODE_STATIC))\n    {\n      doShow = true;\n\n      if (!seg.freeze) { //only run effect function if not frozen\n        // Effect blending\n        uint16_t prog = seg.progress();\n        seg.beginDraw(prog);                // set up parameters for get/setPixelColor() (will also blend colors and palette if blend style is FADE)\n        _currentSegment = &seg;             // set current segment for effect functions (SEGMENT & SEGENV)\n        // workaround for on/off transition to respect blending style\n        _mode[seg.mode]();                  // run new/current mode (needed for bri workaround)\n        seg.call++;\n        // if segment is in transition and no old segment exists we don't need to run the old mode\n        // (blendSegments() takes care of On/Off transitions and clipping)\n        Segment *segO = seg.getOldSegment();\n        if (segO && segO->isActive() && (seg.mode != segO->mode || blendingStyle != BLEND_STYLE_FADE ||\n            (segO->name != seg.name && segO->name && seg.name && strncmp(segO->name, seg.name, WLED_MAX_SEGNAME_LEN) != 0))) {\n          Segment::modeBlend(true);         // set semaphore for beginDraw() to blend colors and palette\n          segO->beginDraw(prog);            // set up palette & colors (also sets draw dimensions), parent segment has transition progress\n          _currentSegment = segO;           // set current segment\n          // workaround for on/off transition to respect blending style\n          _mode[segO->mode]();              // run old mode (needed for bri workaround; semaphore!!)\n          segO->call++;                     // increment old mode run counter\n          Segment::modeBlend(false);        // unset semaphore\n        }\n      }\n    }\n    _segment_index++;\n  }\n\n  #ifdef WLED_DEBUG\n  if ((_targetFps != FPS_UNLIMITED) && (millis() - nowUp > _frametime)) DEBUG_PRINTF_P(PSTR(\"Slow effects %u/%d.\\n\"), (unsigned)(millis()-nowUp), (int)_frametime);\n  #endif\n  if (doShow && !_suspend) {\n    yield();\n    Segment::handleRandomPalette(); // slowly transition random palette; move it into for loop when each segment has individual random palette\n    _lastServiceShow = nowUp; // update timestamp, for precise FPS control\n    show();\n  }\n  #ifdef WLED_DEBUG\n  if ((_targetFps != FPS_UNLIMITED) && (millis() - nowUp > _frametime)) DEBUG_PRINTF_P(PSTR(\"Slow strip %u/%d.\\n\"), (unsigned)(millis()-nowUp), (int)_frametime);\n  #endif\n\n  _triggered = false;\n  _isServicing = false;\n}\n\n// https://en.wikipedia.org/wiki/Blend_modes but using a for top layer & b for bottom layer\nstatic uint8_t _top       (uint8_t a, uint8_t b) { return a; }\nstatic uint8_t _bottom    (uint8_t a, uint8_t b) { return b; }\nstatic uint8_t _add       (uint8_t a, uint8_t b) { unsigned t = a + b; return t > 255 ? 255 : t; }\nstatic uint8_t _subtract  (uint8_t a, uint8_t b) { return b > a ? (b - a) : 0; }\nstatic uint8_t _difference(uint8_t a, uint8_t b) { return b > a ? (b - a) : (a - b); }\nstatic uint8_t _average   (uint8_t a, uint8_t b) { return (a + b) >> 1; }\n#if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32C3)\nstatic uint8_t _multiply  (uint8_t a, uint8_t b) { return ((a * b) + 255) >> 8; } // faster than division on C3 but slightly less accurate\n#else\nstatic uint8_t _multiply  (uint8_t a, uint8_t b) { return (a * b) / 255; } // origianl uses a & b in range [0,1]\n#endif\nstatic uint8_t _divide    (uint8_t a, uint8_t b) { return a > b ? (b * 255) / a : 255; }\nstatic uint8_t _lighten   (uint8_t a, uint8_t b) { return a > b ? a : b; }\nstatic uint8_t _darken    (uint8_t a, uint8_t b) { return a < b ? a : b; }\nstatic uint8_t _screen    (uint8_t a, uint8_t b) { return 255 - _multiply(~a,~b); } // 255 - (255-a)*(255-b)/255\nstatic uint8_t _overlay   (uint8_t a, uint8_t b) { return b < 128 ? 2 * _multiply(a,b) : (255 - 2 * _multiply(~a,~b)); }\nstatic uint8_t _hardlight (uint8_t a, uint8_t b) { return a < 128 ? 2 * _multiply(a,b) : (255 - 2 * _multiply(~a,~b)); }\n#if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32C3)\nstatic 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\n#else\nstatic 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\n#endif\nstatic uint8_t _dodge     (uint8_t a, uint8_t b) { return _divide(~a,b); }\nstatic uint8_t _burn      (uint8_t a, uint8_t b) { return ~_divide(a,~b); }\n\nvoid WS2812FX::blendSegment(const Segment &topSegment) const {\n\n  typedef uint8_t(*FuncType)(uint8_t, uint8_t);\n  FuncType funcs[] = {\n    _top, _bottom,\n    _add, _subtract, _difference, _average,\n    _multiply, _divide, _lighten, _darken, _screen, _overlay,\n    _hardlight, _softlight, _dodge, _burn\n  };\n\n  const size_t blendMode = topSegment.blendMode < (sizeof(funcs) / sizeof(FuncType)) ? topSegment.blendMode : 0;\n  const auto func  = funcs[blendMode]; // blendMode % (sizeof(funcs) / sizeof(FuncType))\n  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))); };\n\n  const int     length     = topSegment.length();     // physical segment length (counts all pixels in 2D segment)\n  const int     width      = topSegment.width();\n  const int     height     = topSegment.height();\n  const auto    XY         = [](int x, int y){ return x + y*Segment::maxWidth; };\n  const size_t  matrixSize = Segment::maxWidth * Segment::maxHeight;\n  const size_t  startIndx  = XY(topSegment.start, topSegment.startY);\n  const size_t  stopIndx   = startIndx + length;\n  const unsigned progress  = topSegment.progress();\n  const unsigned progInv   = 0xFFFFU - progress;\n  uint8_t       opacity    = topSegment.currentBri(); // returns transitioned opacity for style FADE\n  uint8_t       cct        = topSegment.currentCCT();\n  if (gammaCorrectCol) opacity = gamma8inv(opacity); // use inverse gamma on brightness for correct color scaling after gamma correction (see #5343 for details)\n\n  Segment::setClippingRect(0, 0);             // disable clipping by default\n\n  const unsigned dw = (blendingStyle==BLEND_STYLE_OUTSIDE_IN ? progInv : progress) * width / 0xFFFFU + 1;\n  const unsigned dh = (blendingStyle==BLEND_STYLE_OUTSIDE_IN ? progInv : progress) * height / 0xFFFFU + 1;\n  const unsigned orgBS = blendingStyle;\n  if (width*height == 1) blendingStyle = BLEND_STYLE_FADE; // disable style for single pixel segments (use fade instead)\n  switch (blendingStyle) {\n    case BLEND_STYLE_CIRCULAR_IN: // (must set entire segment, see isPixelXYClipped())\n    case BLEND_STYLE_CIRCULAR_OUT:// (must set entire segment, see isPixelXYClipped())\n    case BLEND_STYLE_FAIRY_DUST:  // fairy dust (must set entire segment, see isPixelXYClipped())\n      Segment::setClippingRect(0, width, 0, height);\n      break;\n    case BLEND_STYLE_SWIPE_RIGHT: // left-to-right\n    case BLEND_STYLE_PUSH_RIGHT:  // left-to-right\n      Segment::setClippingRect(0, dw, 0, height);\n      break;\n    case BLEND_STYLE_SWIPE_LEFT:  // right-to-left\n    case BLEND_STYLE_PUSH_LEFT:   // right-to-left\n      Segment::setClippingRect(width - dw, width, 0, height);\n      break;\n    case BLEND_STYLE_OUTSIDE_IN:   // corners\n      Segment::setClippingRect((width + dw)/2, (width - dw)/2, (height + dh)/2, (height - dh)/2); // inverted!!\n      break;\n    case BLEND_STYLE_INSIDE_OUT:  // outward\n      Segment::setClippingRect((width - dw)/2, (width + dw)/2, (height - dh)/2, (height + dh)/2);\n      break;\n    case BLEND_STYLE_SWIPE_DOWN:  // top-to-bottom (2D)\n    case BLEND_STYLE_PUSH_DOWN:   // top-to-bottom (2D)\n      Segment::setClippingRect(0, width, 0, dh);\n      break;\n    case BLEND_STYLE_SWIPE_UP:    // bottom-to-top (2D)\n    case BLEND_STYLE_PUSH_UP:     // bottom-to-top (2D)\n      Segment::setClippingRect(0, width, height - dh, height);\n      break;\n    case BLEND_STYLE_OPEN_H:      // horizontal-outward (2D) same look as INSIDE_OUT on 1D\n      Segment::setClippingRect((width - dw)/2, (width + dw)/2, 0, height);\n      break;\n    case BLEND_STYLE_OPEN_V:      // vertical-outward (2D)\n      Segment::setClippingRect(0, width, (height - dh)/2, (height + dh)/2);\n      break;\n    case BLEND_STYLE_SWIPE_TL:    // TL-to-BR (2D)\n    case BLEND_STYLE_PUSH_TL:     // TL-to-BR (2D)\n      Segment::setClippingRect(0, dw, 0, dh);\n      break;\n    case BLEND_STYLE_SWIPE_TR:    // TR-to-BL (2D)\n    case BLEND_STYLE_PUSH_TR:     // TR-to-BL (2D)\n      Segment::setClippingRect(width - dw, width, 0, dh);\n      break;\n    case BLEND_STYLE_SWIPE_BR:    // BR-to-TL (2D)\n    case BLEND_STYLE_PUSH_BR:     // BR-to-TL (2D)\n      Segment::setClippingRect(width - dw, width, height - dh, height);\n      break;\n    case BLEND_STYLE_SWIPE_BL:    // BL-to-TR (2D)\n    case BLEND_STYLE_PUSH_BL:     // BL-to-TR (2D)\n      Segment::setClippingRect(0, dw, height - dh, height);\n      break;\n  }\n\n  if (isMatrix && stopIndx <= matrixSize) {\n#ifndef WLED_DISABLE_2D\n    const int nCols = topSegment.virtualWidth();\n    const int nRows = topSegment.virtualHeight();\n    const Segment *segO = topSegment.getOldSegment();\n    const int oCols = segO ? segO->virtualWidth() : nCols;\n    const int oRows = segO ? segO->virtualHeight() : nRows;\n\n    const auto setMirroredPixel = [&](int x, int y, uint32_t c, uint8_t o) {\n      const int baseX = topSegment.start  + x;\n      const int baseY = topSegment.startY + y;\n      size_t indx = XY(baseX, baseY); // absolute address on strip\n      _pixels[indx] = color_blend(_pixels[indx], blend(c, _pixels[indx]), o);\n      if (_pixelCCT) _pixelCCT[indx] = cct;\n      // Apply mirroring\n      if (topSegment.mirror || topSegment.mirror_y) {\n        const int mirrorX = topSegment.start  + width  - x - 1;\n        const int mirrorY = topSegment.startY + height - y - 1;\n        const size_t idxMX = XY(topSegment.transpose ? baseX : mirrorX, topSegment.transpose ? mirrorY : baseY);\n        const size_t idxMY = XY(topSegment.transpose ? mirrorX : baseX, topSegment.transpose ? baseY : mirrorY);\n        const size_t idxMM = XY(mirrorX, mirrorY);\n        if (topSegment.mirror)                        _pixels[idxMX] = color_blend(_pixels[idxMX], blend(c, _pixels[idxMX]), o);\n        if (topSegment.mirror_y)                      _pixels[idxMY] = color_blend(_pixels[idxMY], blend(c, _pixels[idxMY]), o);\n        if (topSegment.mirror && topSegment.mirror_y) _pixels[idxMM] = color_blend(_pixels[idxMM], blend(c, _pixels[idxMM]), o);\n        if (_pixelCCT) {\n          if (topSegment.mirror)                        _pixelCCT[idxMX] = cct;\n          if (topSegment.mirror_y)                      _pixelCCT[idxMY] = cct;\n          if (topSegment.mirror && topSegment.mirror_y) _pixelCCT[idxMM] = cct;\n        }\n      }\n    };\n\n    // if we blend using \"push\" style we need to \"shift\" canvas to left/right/up/down\n    unsigned offsetX = (blendingStyle == BLEND_STYLE_PUSH_UP   || blendingStyle == BLEND_STYLE_PUSH_DOWN)  ? 0 : progInv * nCols / 0xFFFFU;\n    unsigned offsetY = (blendingStyle == BLEND_STYLE_PUSH_LEFT || blendingStyle == BLEND_STYLE_PUSH_RIGHT) ? 0 : progInv * nRows / 0xFFFFU;\n\n    // we only traverse new segment, not old one\n    for (int r = 0; r < nRows; r++) for (int c = 0; c < nCols; c++) {\n      const bool clipped = topSegment.isPixelXYClipped(c, r);\n      // if segment is in transition and pixel is clipped take old segment's pixel and opacity\n      const Segment *seg = clipped && segO ? segO : &topSegment;  // pixel is never clipped for FADE\n      int vCols = seg == segO ? oCols : nCols;         // old segment may have different dimensions\n      int vRows = seg == segO ? oRows : nRows;         // old segment may have different dimensions\n      int x = c;\n      int y = r;\n      // if we blend using \"push\" style we need to \"shift\" canvas to left/right/up/down\n      switch (blendingStyle) {\n        case BLEND_STYLE_PUSH_RIGHT: x = (x + offsetX) % nCols;         break;\n        case BLEND_STYLE_PUSH_LEFT:  x = (x - offsetX + nCols) % nCols; break;\n        case BLEND_STYLE_PUSH_DOWN:  y = (y + offsetY) % nRows;         break;\n        case BLEND_STYLE_PUSH_UP:    y = (y - offsetY + nRows) % nRows; break;\n      }\n      uint32_t c_a = BLACK;\n      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\n      if (segO && blendingStyle == BLEND_STYLE_FADE\n        && (topSegment.mode != segO->mode || (segO->name != topSegment.name && segO->name && topSegment.name && strncmp(segO->name, topSegment.name, WLED_MAX_SEGNAME_LEN) != 0))\n        && x < oCols && y < oRows) {\n        // we need to blend old segment using fade as pixels are not clipped\n        c_a = color_blend16(c_a, segO->getPixelColorRaw(x + y*oCols), progInv);\n      } else if (blendingStyle != BLEND_STYLE_FADE) {\n        // if we have global brightness change (not On/Off change) we will ignore transition style and just fade brightness (see led.cpp)\n        // workaround for On/Off transition\n        // (bri != briT) && !bri => from On to Off\n        // (bri != briT) &&  bri => from Off to On\n        if ((briOld == 0 || bri == 0) && ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri))) c_a = BLACK;\n      }\n      // map it into frame buffer\n      x = c;  // restore coordiates if we were PUSHing\n      y = r;\n      if (topSegment.reverse  ) x = nCols - x - 1;\n      if (topSegment.reverse_y) y = nRows - y - 1;\n      if (topSegment.transpose) std::swap(x,y); // swap X & Y if segment transposed\n      // expand pixel\n      const unsigned groupLen = topSegment.groupLength();\n      if (groupLen == 1) {\n        setMirroredPixel(x, y, c_a, opacity);\n      } else {\n        // handle grouping and spacing\n        x *= groupLen; // expand to physical pixels\n        y *= groupLen; // expand to physical pixels\n        const int maxX = std::min(x + topSegment.grouping, width);\n        const int maxY = std::min(y + topSegment.grouping, height);\n        while (y < maxY) {\n          int _x = x;\n          while (_x < maxX) setMirroredPixel(_x++, y, c_a, opacity);\n          y++;\n        }\n      }\n    }\n#endif\n  } else {\n    const int nLen = topSegment.virtualLength();\n    const Segment *segO = topSegment.getOldSegment();\n    const int oLen = segO ? segO->virtualLength() : nLen;\n\n    const auto setMirroredPixel = [&](int i, uint32_t c, uint8_t o) {\n      int indx = topSegment.start + i;\n      // Apply mirroring\n      if (topSegment.mirror) {\n        unsigned indxM = topSegment.stop - i - 1;\n        indxM += topSegment.offset; // offset/phase\n        if (indxM >= topSegment.stop) indxM -= length; // wrap\n        _pixels[indxM] = color_blend(_pixels[indxM], blend(c, _pixels[indxM]), o);\n        if (_pixelCCT) _pixelCCT[indxM] = cct;\n      }\n      indx += topSegment.offset; // offset/phase\n      if (indx >= topSegment.stop) indx -= length; // wrap\n      _pixels[indx] = color_blend(_pixels[indx], blend(c, _pixels[indx]), o);\n      if (_pixelCCT) _pixelCCT[indx] = cct;\n    };\n\n    // if we blend using \"push\" style we need to \"shift\" canvas to left/right/\n    unsigned offsetI = progInv * nLen / 0xFFFFU;\n\n    for (int k = 0; k < nLen; k++) {\n      const bool clipped = topSegment.isPixelClipped(k);\n      // if segment is in transition and pixel is clipped take old segment's pixel and opacity\n      const Segment *seg = clipped && segO ? segO : &topSegment;  // pixel is never clipped for FADE\n      const int vLen = seg == segO ? oLen : nLen;\n      int i = k;\n      // if we blend using \"push\" style we need to \"shift\" canvas to left or right\n      switch (blendingStyle) {\n        case BLEND_STYLE_PUSH_RIGHT: i = (i + offsetI) % nLen;        break;\n        case BLEND_STYLE_PUSH_LEFT:  i = (i - offsetI + nLen) % nLen; break;\n      }\n      uint32_t c_a = BLACK;\n      if (i < vLen) c_a = seg->getPixelColorRaw(i); // will get clipped pixel from old segment or unclipped pixel from new segment\n      if (segO && blendingStyle == BLEND_STYLE_FADE && topSegment.mode != segO->mode && i < oLen) {\n        // we need to blend old segment using fade as pixels are not clipped\n        c_a = color_blend16(c_a, segO->getPixelColorRaw(i), progInv);\n      } else if (blendingStyle != BLEND_STYLE_FADE) {\n        // if we have global brightness change (not On/Off change) we will ignore transition style and just fade brightness (see led.cpp)\n        // workaround for On/Off transition\n        // (bri != briT) && !bri => from On to Off\n        // (bri != briT) &&  bri => from Off to On\n        if ((briOld == 0 || bri == 0) && ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri))) c_a = BLACK;\n      }\n      // map into frame buffer\n      i = k; // restore index if we were PUSHing\n      if (topSegment.reverse) i = nLen - i - 1; // is segment reversed?\n      // expand pixel\n      i *= topSegment.groupLength();\n      // set all the pixels in the group\n      const int maxI = std::min(i + topSegment.grouping, length); // make sure to not go beyond physical length\n      while (i < maxI) setMirroredPixel(i++, c_a, opacity);\n    }\n  }\n\n  blendingStyle = orgBS;\n  Segment::setClippingRect(0, 0);             // disable clipping for overlays\n}\n\nvoid WS2812FX::show() {\n  if (!_pixels) {\n    DEBUGFX_PRINTLN(F(\"Error: no _pixels!\"));\n    errorFlag = ERR_NORAM;\n    return; // no pixels allocated, nothing to show\n  }\n\n  unsigned long showNow = millis();\n  size_t diff = showNow - _lastShow;\n\n  size_t totalLen = getLengthTotal();\n  // WARNING: as WLED doesn't handle CCT on pixel level but on Segment level instead\n  // we need to keep track of each pixel's CCT when blending segments (if CCT is present)\n  // and then set appropriate CCT from that pixel during paint (see below).\n  if ((hasCCTBus() || correctWB) && !cctFromRgb)\n    _pixelCCT = static_cast<uint8_t*>(allocate_buffer(totalLen * sizeof(uint8_t), BFRALLOC_PREFER_PSRAM)); // allocate CCT buffer if necessary, prefer PSRAM\n  if (_pixelCCT) memset(_pixelCCT, 127, totalLen); // set neutral (50:50) CCT\n\n  if (realtimeMode == REALTIME_MODE_INACTIVE || useMainSegmentOnly || realtimeOverride > REALTIME_OVERRIDE_NONE) {\n    // clear frame buffer\n    for (size_t i = 0; i < totalLen; i++) _pixels[i] = BLACK; // memset(_pixels, 0, sizeof(uint32_t) * getLengthTotal());\n    // blend all segments into (cleared) buffer\n    for (Segment &seg : _segments) if (seg.isActive() && (seg.on || seg.isInTransition())) {\n      blendSegment(seg);              // blend segment's buffer into frame buffer\n    }\n  }\n\n  // avoid race condition, capture _callback value\n  show_callback callback = _callback;\n  if (callback) callback(); // will call setPixelColor or setRealtimePixelColor\n\n  // paint actual pixels\n  int oldCCT = Bus::getCCT(); // store original CCT value (since it is global)\n  // when cctFromRgb is true we implicitly calculate WW and CW from RGB values (cct==-1)\n  if (cctFromRgb) BusManager::setSegmentCCT(-1);\n  for (size_t i = 0; i < totalLen; i++) {\n    // when correctWB is true setSegmentCCT() will convert CCT into K with which we can then\n    // correct/adjust RGB value according to desired CCT value, it will still affect actual WW/CW ratio\n    if (_pixelCCT) { // cctFromRgb already exluded at allocation\n      if (i == 0 || _pixelCCT[i-1] != _pixelCCT[i]) BusManager::setSegmentCCT(_pixelCCT[i], correctWB);\n    }\n\n    uint32_t c = _pixels[i]; // need a copy, do not modify _pixels directly (no byte access allowed on ESP32)\n    if(c > 0 && !(realtimeMode && arlsDisableGammaCorrection))\n        c = gamma32(c); // apply gamma correction if enabled note: applying gamma after brightness has too much color loss\n    BusManager::setPixelColor(getMappedPixelIndex(i), c);\n  }\n  Bus::setCCT(oldCCT);  // restore old CCT for ABL adjustments\n\n  p_free(_pixelCCT);\n  _pixelCCT = nullptr;\n\n  // some buses send asynchronously and this method will return before\n  // all of the data has been sent.\n  // See https://github.com/Makuna/NeoPixelBus/wiki/ESP32-NeoMethods#neoesp32rmt-methods\n  BusManager::show();\n\n  if (diff > 0) { // skip calculation if no time has passed\n    size_t fpsCurr = (1000 << FPS_CALC_SHIFT) / diff; // fixed point math\n    _cumulativeFps = (FPS_CALC_AVG * _cumulativeFps + fpsCurr + FPS_CALC_AVG / 2) / (FPS_CALC_AVG + 1);   // \"+FPS_CALC_AVG/2\" for proper rounding\n    _lastShow = showNow;\n  }\n}\n\nvoid WS2812FX::setRealtimePixelColor(unsigned i, uint32_t c) {\n  if (useMainSegmentOnly) {\n    const Segment &seg = getMainSegment();\n    if (seg.isActive() && i < seg.length()) seg.setPixelColorRaw(i, c);\n  } else {\n    setPixelColor(i, c);\n  }\n}\n\n// reset all segments\nvoid WS2812FX::restartRuntime() {\n  suspend();\n  waitForIt();\n  for (Segment &seg : _segments) seg.markForReset().resetIfRequired();\n  resume();\n}\n\n// start or stop transition for all segments\nvoid WS2812FX::setTransitionMode(bool t) {\n  suspend();\n  waitForIt();\n  for (Segment &seg : _segments) seg.startTransition(t ? _transitionDur : 0);\n  resume();\n}\n\n// wait until frame is over (service() has finished or time for 2 frames have passed; yield() crashes on 8266)\n// the latter may, in rare circumstances, lead to incorrectly assuming strip is done servicing but will not block\n// other processing \"indefinitely\"\n// rare circumstances are: setting FPS to high number (i.e. 120) and have very slow effect that will need more\n// time than 2 * _frametime (1000/FPS) to draw content\nvoid WS2812FX::waitForIt() {\n  unsigned long waitStart = millis();\n  unsigned long maxWait = 2*getFrameTime() + 100; // TODO: this needs a proper fix for timeout! see #4779\n  while (isServicing() && (millis() - waitStart < maxWait)) delay(1); // safe even when millis() rolls over\n  #ifdef WLED_DEBUG\n  if (millis()-waitStart >= maxWait) DEBUG_PRINTLN(F(\"Waited for strip to finish servicing.\"));\n  #endif\n};\n\nvoid WS2812FX::setTargetFps(unsigned fps) {\n  if (fps <= 250) _targetFps = fps;\n  if (_targetFps > 0) _frametime = 1000 / _targetFps;\n  else _frametime = MIN_FRAME_DELAY;     // unlimited mode\n}\n\nvoid WS2812FX::setCCT(uint16_t k) {\n  for (Segment &seg : _segments) {\n    if (seg.isActive() && seg.isSelected()) {\n      seg.setCCT(k);\n    }\n  }\n}\n\n// direct=true either expects the caller to call show() themselves (realtime modes) or be ok waiting for the next frame for the change to apply\n// direct=false immediately triggers an effect redraw\nvoid WS2812FX::setBrightness(uint8_t b, bool direct) {\n  if (gammaCorrectBri) b = gamma8(b);\n  if (_brightness == b) return;\n  _brightness = b;\n  if (_brightness == 0) { //unfreeze all segments on power off\n    for (const Segment &seg : _segments) seg.freeze = false; // freeze is mutable\n  }\n  BusManager::setBrightness(scaledBri(b));\n  if (!direct) {\n    unsigned long t = millis();\n    if (t - _lastShow > MIN_SHOW_DELAY) trigger(); //apply brightness change immediately if no refresh soon\n  }\n}\n\nuint8_t WS2812FX::getActiveSegsLightCapabilities(bool selectedOnly) const {\n  uint8_t totalLC = 0;\n  for (const Segment &seg : _segments) {\n    if (seg.isActive() && (!selectedOnly || seg.isSelected())) totalLC |= seg.getLightCapabilities();\n  }\n  return totalLC;\n}\n\nuint8_t WS2812FX::getFirstSelectedSegId() const {\n  size_t i = 0;\n  for (const Segment &seg : _segments) {\n    if (seg.isActive() && seg.isSelected()) return i;\n    i++;\n  }\n  // if none selected, use the main segment\n  return getMainSegmentId();\n}\n\nvoid WS2812FX::setMainSegmentId(unsigned n) {\n  _mainSegment = getLastActiveSegmentId();\n  if (n < _segments.size() && _segments[n].isActive()) {  // only set if segment is active\n    _mainSegment = n;\n  }\n  return;\n}\n\nuint8_t WS2812FX::getLastActiveSegmentId() const {\n  for (size_t i = _segments.size() -1; i > 0; i--) {\n    if (_segments[i].isActive()) return i;\n  }\n  return 0;\n}\n\nuint8_t WS2812FX::getActiveSegmentsNum() const {\n  unsigned c = 0;\n  for (const Segment &seg : _segments) if (seg.isActive()) c++;\n  return c;\n}\n\nuint16_t WS2812FX::getLengthTotal() const {\n  unsigned len = Segment::maxWidth * Segment::maxHeight; // will be _length for 1D (see finalizeInit()) but should cover whole matrix for 2D\n  if (isMatrix && _length > len) len = _length; // for 2D with trailing strip\n  return len;\n}\n\nuint16_t WS2812FX::getLengthPhysical() const {\n  return BusManager::getTotalLength(true);\n}\n\n//used for JSON API info.leds.rgbw. Little practical use, deprecate with info.leds.rgbw.\n//returns if there is an RGBW bus (supports RGB and White, not only white)\n//not influenced by auto-white mode, also true if white slider does not affect output white channel\nbool WS2812FX::hasRGBWBus() const {\n  for (size_t b = 0; b < BusManager::getNumBusses(); b++) {\n    const Bus *bus = BusManager::getBus(b);\n    if (!bus || !bus->isOk()) break;\n    if (bus->hasRGB() && bus->hasWhite()) return true;\n  }\n  return false;\n}\n\nbool WS2812FX::hasCCTBus() const {\n  if (cctFromRgb && !correctWB) return false;\n  for (size_t b = 0; b < BusManager::getNumBusses(); b++) {\n    const Bus *bus = BusManager::getBus(b);\n    if (!bus || !bus->isOk()) break;\n    if (bus->hasCCT()) return true;\n  }\n  return false;\n}\n\nvoid WS2812FX::purgeSegments() {\n  // remove all inactive segments (from the back)\n  int deleted = 0;\n  if (_segments.size() <= 1) return;\n  for (size_t i = _segments.size()-1; i > 0; i--)\n    if (_segments[i].stop == 0) {\n      deleted++;\n      _segments.erase(_segments.begin() + i);\n    }\n  if (deleted) {\n    _segments.shrink_to_fit();\n    setMainSegmentId(0);\n  }\n}\n\nSegment& WS2812FX::getSegment(unsigned id) {\n  return _segments[id >= _segments.size() ? getMainSegmentId() : id]; // vectors\n}\n\n// WARNING: resetSegments(), makeAutoSegments() and fixInvalidSegments() must not be called while\n// strip is being serviced (strip.service()), you must call suspend prior if changing segments outside\n// loop() context\nvoid WS2812FX::resetSegments() {\n  if (isServicing()) return;\n  _segments.clear();          // destructs all Segment as part of clearing\n  _segments.emplace_back(0, isMatrix ? Segment::maxWidth : _length, 0, isMatrix ? Segment::maxHeight : 1);\n  if(_segments.size() == 0) {\n    _segments.emplace_back(); // if out of heap, create a default segment\n    errorFlag = ERR_NORAM_PX;\n  }\n  _segments.shrink_to_fit();  // just in case ...\n  _mainSegment = 0;\n}\n\nvoid WS2812FX::makeAutoSegments(bool forceReset) {\n  if (isServicing()) return;\n  if (autoSegments) { //make one segment per bus\n    unsigned segStarts[MAX_NUM_SEGMENTS] = {0};\n    unsigned segStops [MAX_NUM_SEGMENTS] = {0};\n    size_t s = 0;\n\n    #ifndef WLED_DISABLE_2D\n    // 2D segment is the 1st one using entire matrix\n    if (isMatrix) {\n      segStarts[0] = 0;\n      segStops[0]  = Segment::maxWidth*Segment::maxHeight;\n      s++;\n    }\n    #endif\n\n    for (size_t i = s; i < BusManager::getNumBusses(); i++) {\n      const Bus *bus = BusManager::getBus(i);\n      if (!bus) break;\n\n      segStarts[s] = bus->getStart();\n      segStops[s]  = segStarts[s] + bus->getLength();\n\n      #ifndef WLED_DISABLE_2D\n      if (isMatrix && segStops[s] <= Segment::maxWidth*Segment::maxHeight) continue; // ignore buses comprising matrix\n      if (isMatrix && segStarts[s] < Segment::maxWidth*Segment::maxHeight) segStarts[s] = Segment::maxWidth*Segment::maxHeight;\n      #endif\n\n      //check for overlap with previous segments\n      for (size_t j = 0; j < s; j++) {\n        if (segStops[j] > segStarts[s] && segStarts[j] < segStops[s]) {\n          //segments overlap, merge\n          segStarts[j] = min(segStarts[s],segStarts[j]);\n          segStops [j] = max(segStops [s],segStops [j]); segStops[s] = 0;\n          s--;\n        }\n      }\n      s++;\n    }\n\n    _segments.clear();\n    _segments.reserve(s); // prevent reallocations\n    // there is always at least one segment (but we need to differentiate between 1D and 2D)\n    #ifndef WLED_DISABLE_2D\n    if (isMatrix)\n      _segments.emplace_back(0, Segment::maxWidth, 0, Segment::maxHeight);\n    else\n    #endif\n      _segments.emplace_back(segStarts[0], segStops[0]);\n    for (size_t i = 1; i < s; i++) {\n      _segments.emplace_back(segStarts[i], segStops[i]);\n    }\n    DEBUGFX_PRINTF_P(PSTR(\"%d auto segments created.\\n\"), _segments.size());\n\n  } else {\n\n    if (forceReset || getSegmentsNum() == 0) resetSegments();\n    //expand the main seg to the entire length, but only if there are no other segments, or reset is forced\n    else if (getActiveSegmentsNum() == 1) {\n      size_t i = getLastActiveSegmentId();\n      #ifndef WLED_DISABLE_2D\n      _segments[i].setGeometry(0, Segment::maxWidth, 1, 0, 0xFFFF, 0, Segment::maxHeight);\n      #else\n      _segments[i].setGeometry(0, _length);\n      #endif\n    }\n  }\n  _mainSegment = 0;\n\n  fixInvalidSegments();\n}\n\nvoid WS2812FX::fixInvalidSegments() {\n  if (isServicing()) return;\n  //make sure no segment is longer than total (sanity check)\n  for (size_t i = getSegmentsNum()-1; i > 0; i--) {\n    if (isMatrix) {\n    #ifndef WLED_DISABLE_2D\n      if (_segments[i].start >= Segment::maxWidth * Segment::maxHeight) {\n        // 1D segment at the end of matrix\n        if (_segments[i].start >= _length || _segments[i].startY > 0 || _segments[i].stopY > 1) { _segments.erase(_segments.begin()+i); continue; }\n        if (_segments[i].stop  >  _length) _segments[i].stop = _length;\n        continue;\n      }\n      if (_segments[i].start >= Segment::maxWidth || _segments[i].startY >= Segment::maxHeight) { _segments.erase(_segments.begin()+i); continue; }\n      if (_segments[i].stop  >  Segment::maxWidth)  _segments[i].stop  = Segment::maxWidth;\n      if (_segments[i].stopY >  Segment::maxHeight) _segments[i].stopY = Segment::maxHeight;\n    #endif\n    } else {\n      if (_segments[i].start >= _length) { _segments.erase(_segments.begin()+i); continue; }\n      if (_segments[i].stop  >  _length) _segments[i].stop = _length;\n    }\n  }\n  // if any segments were deleted free memory\n  purgeSegments();\n  // this is always called as the last step after finalizeInit(), update covered bus types\n  for (const Segment &seg : _segments)\n    seg.refreshLightCapabilities();\n}\n\n//true if all segments align with a bus, or if a segment covers the total length\n//irrelevant in 2D set-up\nbool WS2812FX::checkSegmentAlignment() const {\n  bool aligned = false;\n  for (const Segment &seg : _segments) {\n    for (unsigned b = 0; b<BusManager::getNumBusses(); b++) {\n      const Bus *bus = BusManager::getBus(b);\n      if (!bus || !bus->isOk()) break;\n      if (seg.start == bus->getStart() && seg.stop == bus->getStart() + bus->getLength()) aligned = true;\n    }\n    if (seg.start == 0 && seg.stop == _length) aligned = true;\n    if (!aligned) return false;\n  }\n  return true;\n}\n\n// used by analog clock overlay\nvoid WS2812FX::setRange(uint16_t i, uint16_t i2, uint32_t col) {\n  if (i2 < i) std::swap(i,i2);\n  for (unsigned x = i; x <= i2; x++) setPixelColor(x, col);\n}\n\n#ifdef WLED_DEBUG\nvoid WS2812FX::printSize() {\n  size_t size = 0;\n  for (const Segment &seg : _segments) size += seg.getSize();\n  DEBUG_PRINTF_P(PSTR(\"Segments: %d -> %u/%dB\\n\"), _segments.size(), size, Segment::getUsedSegmentData());\n  for (const Segment &seg : _segments) DEBUG_PRINTF_P(PSTR(\"  Seg: %d,%d [A=%d, 2D=%d, RGB=%d, W=%d, CCT=%d]\\n\"), seg.width(), seg.height(), seg.isActive(), seg.is2D(), seg.hasRGB(), seg.hasWhite(), seg.isCCT());\n  DEBUG_PRINTF_P(PSTR(\"Modes: %d*%d=%uB\\n\"), sizeof(mode_ptr), _mode.size(), (_mode.capacity()*sizeof(mode_ptr)));\n  DEBUG_PRINTF_P(PSTR(\"Data: %d*%d=%uB\\n\"), sizeof(const char *), _modeData.size(), (_modeData.capacity()*sizeof(const char *)));\n  DEBUG_PRINTF_P(PSTR(\"Map: %d*%d=%uB\\n\"), sizeof(uint16_t), (int)customMappingSize, customMappingSize*sizeof(uint16_t));\n}\n#endif\n\n// load custom mapping table from JSON file (called from finalizeInit() or deserializeState())\n// if this is a matrix set-up and default ledmap.json file does not exist, create mapping table using setUpMatrix() from panel information\n// WARNING: effect drawing has to be suspended (strip.suspend()) or must be called from loop() context\nbool WS2812FX::deserializeMap(unsigned n) {\n  char fileName[32];\n  strcpy_P(fileName, PSTR(\"/ledmap\"));\n  if (n) sprintf(fileName +7, \"%d\", n);\n  strcat_P(fileName, PSTR(\".json\"));\n  bool isFile = WLED_FS.exists(fileName);\n\n  customMappingSize = 0; // prevent use of mapping if anything goes wrong\n  currentLedmap = 0;\n  if (n == 0 || isFile) interfaceUpdateCallMode = CALL_MODE_WS_SEND; // schedule WS update (to inform UI)\n\n  if (!isFile && n==0 && isMatrix) {\n    // 2D panel support creates its own ledmap (on the fly) if a ledmap.json does not exist\n    setUpMatrix();\n    return false;\n  }\n\n  if (!isFile || !requestJSONBufferLock(JSON_LOCK_LEDMAP)) return false;\n\n  StaticJsonDocument<64> filter;\n  filter[F(\"width\")]  = true;\n  filter[F(\"height\")] = true;\n  if (!readObjectFromFile(fileName, nullptr, pDoc, &filter)) {\n    DEBUG_PRINTF_P(PSTR(\"ERROR Invalid ledmap in %s\\n\"), fileName);\n    releaseJSONBufferLock();\n    return false; // if file does not load properly then exit\n  } else\n    DEBUG_PRINTF_P(PSTR(\"Reading LED map from %s\\n\"), fileName);\n\n  JsonObject root = pDoc->as<JsonObject>();\n  // if we are loading default ledmap (at boot) set matrix width and height from the ledmap (compatible with WLED MM ledmaps)\n  if (n == 0 && (!root[F(\"width\")].isNull() || !root[F(\"height\")].isNull())) {\n    Segment::maxWidth  = min(max(root[F(\"width\")].as<int>(), 1), 255);\n    Segment::maxHeight = min(max(root[F(\"height\")].as<int>(), 1), 255);\n    isMatrix = true;\n    DEBUG_PRINTF_P(PSTR(\"LED map width=%d, height=%d\\n\"), Segment::maxWidth, Segment::maxHeight);\n  }\n\n  d_free(customMappingTable);\n  customMappingTable = static_cast<uint16_t*>(d_malloc(sizeof(uint16_t)*getLengthTotal())); // prefer DRAM for speed\n\n  if (customMappingTable) {\n    DEBUG_PRINTF_P(PSTR(\"ledmap allocated: %uB\\n\"), sizeof(uint16_t)*getLengthTotal());\n    File f = WLED_FS.open(fileName, \"r\");\n    f.find(\"\\\"map\\\":[\");\n    while (f.available()) { // f.position() < f.size() - 1\n      char number[32];\n      size_t numRead = f.readBytesUntil(',', number, sizeof(number)-1); // read a single number (may include array terminating \"]\" but not number separator ',')\n      number[numRead] = 0;\n      if (numRead > 0) {\n        char *end = strchr(number,']'); // we encountered end of array so stop processing if no digit found\n        bool foundDigit = (end == nullptr);\n        int i = 0;\n        if (end != nullptr) do {\n          if (number[i] >= '0' && number[i] <= '9') foundDigit = true;\n          if (foundDigit || &number[i++] == end) break;\n        } while (i < 32);\n        if (!foundDigit) break;\n        int index = atoi(number);\n        if (index < 0 || index > 65535) index = 0xFFFF; // prevent integer wrap around\n        customMappingTable[customMappingSize++] = index;\n        if (customMappingSize >= getLengthTotal()) break;\n      } else break; // there was nothing to read, stop\n    }\n    currentLedmap = n;\n    f.close();\n\n    #ifdef WLED_DEBUG\n    DEBUG_PRINT(F(\"Loaded ledmap:\"));\n    for (unsigned i=0; i<customMappingSize; i++) {\n      if (!(i%Segment::maxWidth)) DEBUG_PRINTLN();\n      DEBUG_PRINTF_P(PSTR(\"%4d,\"), customMappingTable[i] < 0xFFFFU ? customMappingTable[i] : -1);\n    }\n    DEBUG_PRINTLN();\n    #endif\n/*\n    JsonArray map = root[F(\"map\")];\n    if (!map.isNull() && map.size()) {  // not an empty map\n      customMappingSize = min((unsigned)map.size(), (unsigned)getLengthTotal());\n      for (unsigned i=0; i<customMappingSize; i++) customMappingTable[i] = (uint16_t) (map[i]<0 ? 0xFFFFU : map[i]);\n      currentLedmap = n;\n    }\n*/\n  } else {\n    DEBUG_PRINTLN(F(\"ERROR LED map allocation error.\"));\n  }\n\n  releaseJSONBufferLock();\n  return (customMappingSize > 0);\n}\n\n\nconst char JSON_mode_names[] PROGMEM = R\"=====([\"FX names moved\"])=====\";\nconst char JSON_palette_names[] PROGMEM = R\"=====([\n\"Default\",\"* Random Cycle\",\"* Color 1\",\"* Colors 1&2\",\"* Color Gradient\",\"* Colors Only\",\"Party\",\"Cloud\",\"Lava\",\"Ocean\",\n\"Forest\",\"Rainbow\",\"Rainbow Bands\",\"Sunset\",\"Rivendell\",\"Breeze\",\"Red & Blue\",\"Yellowout\",\"Analogous\",\"Splash\",\n\"Pastel\",\"Sunset 2\",\"Beach\",\"Vintage\",\"Departure\",\"Landscape\",\"Beech\",\"Sherbet\",\"Hult\",\"Hult 64\",\n\"Drywet\",\"Jul\",\"Grintage\",\"Rewhi\",\"Tertiary\",\"Fire\",\"Icefire\",\"Cyane\",\"Light Pink\",\"Autumn\",\n\"Magenta\",\"Magred\",\"Yelmag\",\"Yelblu\",\"Orange & Teal\",\"Tiamat\",\"April Night\",\"Orangery\",\"C9\",\"Sakura\",\n\"Aurora\",\"Atlantica\",\"C9 2\",\"C9 New\",\"Temperature\",\"Aurora 2\",\"Retro Clown\",\"Candy\",\"Toxy Reaf\",\"Fairy Reaf\",\n\"Semi Blue\",\"Pink Candy\",\"Red Reaf\",\"Aqua Flash\",\"Yelblu Hot\",\"Lite Light\",\"Red Flash\",\"Blink Red\",\"Red Shift\",\"Red Tide\",\n\"Candy2\",\"Traffic Light\"\n])=====\";\n"
  },
  {
    "path": "wled00/FXparticleSystem.cpp",
    "content": "/*\n  FXparticleSystem.cpp\n\n  Particle system with functions for particle generation, particle movement and particle rendering to RGB matrix.\n  by DedeHai (Damian Schneider) 2013-2024\n\n  Copyright (c) 2024  Damian Schneider\n  Licensed under the EUPL v. 1.2 or later\n*/\n\n#ifdef WLED_DISABLE_2D\n#define WLED_DISABLE_PARTICLESYSTEM2D\n#endif\n\n#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled\n#include \"FXparticleSystem.h\"\n// local shared functions (used both in 1D and 2D system)\nstatic int32_t calcForce_dv(const int8_t force, uint8_t &counter);\nstatic 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\nstatic 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)\n#endif\n\n#ifndef WLED_DISABLE_PARTICLESYSTEM2D\nParticleSystem2D::ParticleSystem2D(uint32_t width, uint32_t height, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced, bool sizecontrol) {\n  PSPRINTLN(\"\\n ParticleSystem2D constructor\");\n  numSources = numberofsources; // number of sources allocated in init\n  numParticles = numberofparticles; // number of particles allocated in init\n  usedParticles = numParticles; // use all particles by default\n  advPartProps = nullptr; //make sure we start out with null pointers (just in case memory was not cleared)\n  advPartSize = nullptr;\n  setMatrixSize(width, height);\n  updatePSpointers(isadvanced, sizecontrol); // set the particle and sources pointer (call this before accessing sprays or particles)\n  setWallHardness(255); // set default wall hardness to max\n  setWallRoughness(0); // smooth walls by default\n  setGravity(0); //gravity disabled by default\n  setParticleSize(1); // 2x2 rendering size by default (disables per particle size control by default)\n  motionBlur = 0; //no fading by default\n  smearBlur = 0; //no smearing by default\n  emitIndex = 0;\n  collisionStartIdx = 0;\n\n  //initialize some default non-zero values most FX use\n  for (uint32_t i = 0; i < numParticles; i++) {\n     particles[i].sat = 255; // full saturation\n  }\n  for (uint32_t i = 0; i < numSources; i++) {\n    sources[i].source.sat = 255; //set saturation to max by default\n    sources[i].source.ttl = 1; //set source alive\n    sources[i].sourceFlags.asByte = 0; // all flags disabled\n  }\n  perParticleSize = isadvanced; // enable per particle size by default if using advanced properties (FX can disable if needed)\n\n}\n\n// update function applies gravity, moves the particles, handles collisions and renders the particles\nvoid ParticleSystem2D::update(void) {\n  //apply gravity globally if enabled\n  if (particlesettings.useGravity)\n    applyGravity();\n\n  //update size settings before handling collisions\n  if (advPartSize != nullptr) {\n    for (uint32_t i = 0; i < usedParticles; i++) {\n      if (updateSize(&advPartProps[i], &advPartSize[i]) == false) { // if particle shrinks to 0 size\n        particles[i].ttl = 0; // kill particle\n      }\n    }\n  }\n\n  // 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)\n  if (particlesettings.useCollisions)\n    handleCollisions();\n\n  //move all particles\n  for (uint32_t i = 0; i < usedParticles; i++) {\n    particleMoveUpdate(particles[i], particleFlags[i], nullptr, advPartProps ? &advPartProps[i] : nullptr); // note: splitting this into two loops is slower and uses more flash\n  }\n\n  render();\n}\n\n// update function for fire animation\nvoid ParticleSystem2D::updateFire(const uint8_t intensity) {\n  fireParticleupdate();\n  fireIntesity = intensity > 0 ? intensity : 1; // minimum of 1, zero checking is used in render function\n  render();\n}\n\n// set percentage of used particles as uint8_t i.e 127 means 50% for example\nvoid ParticleSystem2D::setUsedParticles(uint8_t percentage) {\n  usedParticles = max((uint32_t)1, (numParticles * ((int)percentage+1)) >> 8); // number of particles to use (percentage is 0-255, 255 = 100%)\n  PSPRINT(\" SetUsedpaticles: allocated particles: \");\n  PSPRINT(numParticles);\n  PSPRINT(\" ,used particles: \");\n  PSPRINTLN(usedParticles);\n}\n\nvoid ParticleSystem2D::setWallHardness(uint8_t hardness) {\n  wallHardness = hardness;\n}\n\nvoid ParticleSystem2D::setWallRoughness(uint8_t roughness) {\n  wallRoughness = roughness;\n}\n\nvoid ParticleSystem2D::setCollisionHardness(uint8_t hardness) {\n  collisionHardness = (int)hardness + 1;\n}\n\nvoid ParticleSystem2D::setMatrixSize(uint32_t x, uint32_t y) {\n  maxXpixel = x - 1; // last physical pixel that can be drawn to\n  maxYpixel = y - 1;\n  maxX = x * PS_P_RADIUS - 1;  // particle system boundary for movements\n  maxY = y * PS_P_RADIUS - 1;  // this value is often needed (also by FX) to calculate positions\n}\n\nvoid ParticleSystem2D::setWrapX(bool enable) {\n  particlesettings.wrapX = enable;\n}\n\nvoid ParticleSystem2D::setWrapY(bool enable) {\n  particlesettings.wrapY = enable;\n}\n\nvoid ParticleSystem2D::setBounceX(bool enable) {\n  particlesettings.bounceX = enable;\n}\n\nvoid ParticleSystem2D::setBounceY(bool enable) {\n  particlesettings.bounceY = enable;\n}\n\nvoid ParticleSystem2D::setKillOutOfBounds(bool enable) {\n  particlesettings.killoutofbounds = enable;\n}\n\nvoid ParticleSystem2D::setColorByAge(bool enable) {\n  particlesettings.colorByAge = enable;\n}\n\nvoid ParticleSystem2D::setMotionBlur(uint8_t bluramount) {\n    motionBlur = bluramount;\n}\n\nvoid ParticleSystem2D::setSmearBlur(uint8_t bluramount) {\n  smearBlur = bluramount;\n}\n\n\n// set global particle size\nvoid ParticleSystem2D::setParticleSize(uint8_t size) {\n  particlesize = size;\n  particleHardRadius = PS_P_MINHARDRADIUS; // ~1 pixel\n  perParticleSize = false; // disable per particle size control if global size is set\n  if (particlesize > 1) {\n    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)\n  }\n  else if (particlesize == 0)\n    particleHardRadius = PS_P_MINHARDRADIUS >> 1; // single pixel particles have half the radius (i.e. 1/2 pixel)\n}\n\n// enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable\n// if enabled, gravity is applied to all particles in ParticleSystemUpdate()\n// 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)\nvoid ParticleSystem2D::setGravity(int8_t force) {\n  if (force) {\n    gforce = force;\n    particlesettings.useGravity = true;\n  } else {\n    particlesettings.useGravity = false;\n  }\n}\n\nvoid 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\n  particlesettings.useCollisions = enable;\n  collisionHardness = (int)hardness + 1;\n}\n\n// emit one particle with variation, returns index of emitted particle (or -1 if no particle emitted)\nint32_t ParticleSystem2D::sprayEmit(const PSsource &emitter) {\n  bool success = false;\n  for (uint32_t i = 0; i < usedParticles; i++) {\n    emitIndex++;\n    if (emitIndex >= usedParticles)\n      emitIndex = 0;\n    if (particles[emitIndex].ttl == 0) { // find a dead particle\n      success = true;\n      int32_t dx = hw_random16(emitter.var << 1) - emitter.var;\n      int32_t dy = hw_random16(emitter.var << 1) - emitter.var;\n      if (emitter.var > 5) { // use circular random distribution for large variance to generate nicer \"explosions\"\n        while (dx*dx + dy*dy > emitter.var*emitter.var) { // reject points outside circle\n            dx = hw_random16(emitter.var << 1) - emitter.var;\n            dy = hw_random16(emitter.var << 1) - emitter.var;\n        }\n      }\n      particles[emitIndex].vx = emitter.vx + dx;\n      particles[emitIndex].vy = emitter.vy + dy;\n      particles[emitIndex].x = emitter.source.x;\n      particles[emitIndex].y = emitter.source.y;\n      particles[emitIndex].hue = emitter.source.hue;\n      particles[emitIndex].sat = emitter.source.sat;\n      particleFlags[emitIndex].collide = emitter.sourceFlags.collide;\n      particles[emitIndex].ttl = hw_random16(emitter.minLife, emitter.maxLife);\n      if (advPartProps != nullptr)\n        advPartProps[emitIndex].size = emitter.size;\n      break;\n    }\n  }\n  if (success)\n    return emitIndex;\n  else\n    return -1;\n}\n\n// Spray emitter for particles used for flames (particle TTL depends on source TTL)\nvoid ParticleSystem2D::flameEmit(const PSsource &emitter) {\n  int emitIndex = sprayEmit(emitter);\n  if (emitIndex > 0)  particles[emitIndex].ttl += emitter.source.ttl;\n}\n\n// Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var\n// angle = 0 means in positive x-direction (i.e. to the right)\nint32_t ParticleSystem2D::angleEmit(PSsource &emitter, const uint16_t angle, const int32_t speed) {\n  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\n  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!\n  return sprayEmit(emitter);\n}\n\n// particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0\n// 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\nvoid ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSparticleFlags &partFlags, PSsettings2D *options, PSadvancedParticle *advancedproperties) {\n  if (options == nullptr)\n    options = &particlesettings; //use PS system settings by default\n\n  if (part.ttl > 0) {\n    if (!partFlags.perpetual)\n      part.ttl--; // age\n    if (options->colorByAge)\n      part.hue = min(part.ttl, (uint16_t)255); //set color to ttl\n\n    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\n    int32_t newX = part.x + (int32_t)part.vx;\n    int32_t newY = part.y + (int32_t)part.vy;\n    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\n\n    if (perParticleSize && advancedproperties != nullptr) { // using individual particle size\n      renderradius = PS_P_HALFRADIUS - 1 + advancedproperties->size; // note: single pixel particles should be zero but OOB checks in rendering function handle this\n      if (advancedproperties->size > 0)\n        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\")\n      else // single pixel particles use half the collision distance for walls\n        particleHardRadius = PS_P_MINHARDRADIUS >> 1;\n    }\n    // 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\n    if (options->bounceY) {\n      if ((newY < (int32_t)particleHardRadius) || ((newY > (int32_t)(maxY - particleHardRadius)) && !options->useGravity)) { // reached floor / ceiling\n         bounce(part.vy, part.vx, newY, maxY);\n      }\n    }\n\n    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\n      partFlags.outofbounds = true;\n      if (options->killoutofbounds) {\n        if (newY < 0) // if gravity is enabled, only kill particles below ground\n          part.ttl = 0;\n        else if (!options->useGravity)\n          part.ttl = 0;\n      }\n    }\n\n    if (part.ttl) { //check x direction only if still alive\n      if (options->bounceX) {\n        if ((newX < (int32_t)particleHardRadius) || (newX > (int32_t)(maxX - particleHardRadius))) // reached a wall\n          bounce(part.vx, part.vy, newX, maxX);\n      }\n      else if (!checkBoundsAndWrap(newX, maxX, renderradius, options->wrapX)) { // check out of bounds\n        partFlags.outofbounds = true;\n        if (options->killoutofbounds)\n          part.ttl = 0;\n      }\n    }\n\n    part.x = (int16_t)newX; // set new position\n    part.y = (int16_t)newY; // set new position\n  }\n}\n\n// move function for fire particles\nvoid ParticleSystem2D::fireParticleupdate() {\n  for (uint32_t i = 0; i < usedParticles; i++) {\n    if (particles[i].ttl > 0)\n    {\n      particles[i].ttl--; // age\n      int32_t newY = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter\n      int32_t newX = particles[i].x + (int32_t)particles[i].vx;\n      particleFlags[i].outofbounds = 0; // reset out of bounds flag  note: moving this to checks below is not faster but adds code\n      // check if particle is out of bounds, wrap x around to other side if wrapping is enabled\n      // 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\n      if (newY < -PS_P_HALFRADIUS)\n        particleFlags[i].outofbounds = 1;\n      else if (newY > int32_t(maxY + PS_P_HALFRADIUS)) // particle moved out at the top\n        particles[i].ttl = 0;\n      else // particle is in frame in y direction, also check x direction now Note: using checkBoundsAndWrap() is slower, only saves a few bytes\n      {\n        if ((newX < 0) || (newX > (int32_t)maxX)) { // handle out of bounds & wrap\n          if (particlesettings.wrapX) {\n            newX = newX % (maxX + 1);\n            if (newX < 0) // handle negative modulo\n              newX += maxX + 1;\n          }\n          else if ((newX < -PS_P_HALFRADIUS) || (newX > int32_t(maxX + PS_P_HALFRADIUS))) { //if fully out of view\n            particles[i].ttl = 0;\n          }\n        }\n        particles[i].x = newX;\n      }\n      particles[i].y = newY;\n    }\n  }\n}\n\n// update advanced particle size control, returns false if particle shrinks to 0 size\nbool ParticleSystem2D::updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize) {\n  if (advsize == nullptr) // safety check\n    return false;\n  // grow/shrink particle\n  int32_t newsize = advprops->size;\n  uint32_t counter = advsize->sizecounter;\n  uint32_t increment = 0;\n  // calculate grow speed using 0-8 for low speeds and 9-15 for higher speeds\n  if (advsize->grow) increment = advsize->growspeed;\n  else if (advsize->shrink) increment = advsize->shrinkspeed;\n  if (increment < 9) { // 8 means +1 every frame\n    counter += increment;\n    if (counter > 7) {\n      counter -= 8;\n      increment = 1;\n    } else\n      increment = 0;\n    advsize->sizecounter = counter;\n  } else {\n    increment = (increment - 8) << 1; // 9 means +2, 10 means +4 etc. 15 means +14\n  }\n\n  if (advsize->grow) {\n    if (newsize < advsize->maxsize) {\n      newsize += increment;\n      if (newsize >= advsize->maxsize) {\n        advsize->grow = false; // stop growing, shrink from now on if enabled\n        newsize = advsize->maxsize; // limit\n        if (advsize->pulsate) advsize->shrink = true;\n      }\n    }\n  } else if (advsize->shrink) {\n    if (newsize > advsize->minsize) {\n      newsize -= increment;\n      if (newsize <= advsize->minsize) {\n        if (advsize->minsize == 0)\n          return false; // particle shrunk to zero\n        advsize->shrink = false; // disable shrinking\n        newsize = advsize->minsize; // limit\n        if (advsize->pulsate) advsize->grow = true;\n      }\n    }\n  }\n  advprops->size = newsize;\n  // handle wobbling\n  if (advsize->wobble) {\n    advsize->asymdir += advsize->wobblespeed; // note: if need better wobblespeed control a counter is already in the struct\n  }\n  return true;\n}\n\n// calculate x and y size for asymmetrical particles (advanced size control)\nvoid ParticleSystem2D::getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize) {\n  if (advsize == nullptr) // if advsize is valid, also advanced properties pointer is valid (handled by updatePSpointers())\n    return;\n  int32_t size = advprops->size;\n  int32_t asymdir = advsize->asymdir;\n  int32_t deviation = ((uint32_t)size * (uint32_t)advsize->asymmetry + 255) >> 8; // deviation from symmetrical size\n  // Calculate x and y size based on deviation and direction (0 is symmetrical, 64 is x, 128 is symmetrical, 192 is y)\n  if (asymdir < 64) {\n    deviation = (asymdir * deviation) >> 6;\n  } else if (asymdir < 192) {\n    deviation = ((128 - asymdir) * deviation) >> 6;\n  } else {\n    deviation = ((asymdir - 255) * deviation) >> 6;\n  }\n  // Calculate x and y size based on deviation, limit to 255 (rendering function cannot handle larger sizes)\n  xsize = min((size - deviation), (int32_t)255);\n  ysize = min((size + deviation), (int32_t)255);;\n}\n\n// function to bounce a particle from a wall using set parameters (wallHardness and wallRoughness)\nvoid ParticleSystem2D::bounce(int8_t &incomingspeed, int8_t &parallelspeed, int32_t &position, const uint32_t maxposition) {\n  incomingspeed = -incomingspeed;\n  incomingspeed = (incomingspeed * wallHardness + 128) >> 8; // reduce speed as energy is lost on non-hard surface\n  if (position < (int32_t)particleHardRadius)\n    position = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better\n  else\n    position = maxposition - particleHardRadius;\n  if (wallRoughness) {\n    int32_t incomingspeed_abs = abs((int32_t)incomingspeed);\n    int32_t totalspeed = incomingspeed_abs + abs((int32_t)parallelspeed);\n    // transfer an amount of incomingspeed speed to parallel speed\n    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\n    parallelspeed = limitSpeed((int32_t)parallelspeed + donatespeed);\n    // give the remainder of the speed to perpendicular speed\n    donatespeed = int8_t(totalspeed - abs(parallelspeed)); // keep total speed the same\n    incomingspeed = incomingspeed > 0 ? donatespeed : -donatespeed;\n  }\n}\n\n// apply a force in x,y direction to individual particle\n// caller needs to provide a 8bit counter (for each particle) that holds its value between calls\n// 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)\nvoid ParticleSystem2D::applyForce(PSparticle &part, const int8_t xforce, const int8_t yforce, uint8_t &counter) {\n  // for small forces, need to use a delay counter\n  uint8_t xcounter = counter & 0x0F; // lower four bits\n  uint8_t ycounter = counter >> 4;   // upper four bits\n\n  // velocity increase\n  int32_t dvx = calcForce_dv(xforce, xcounter);\n  int32_t dvy = calcForce_dv(yforce, ycounter);\n\n  // save counter values back\n  counter = xcounter & 0x0F; // write lower four bits, make sure not to write more than 4 bits\n  counter |= (ycounter << 4) & 0xF0; // write upper four bits\n\n  // apply the force to particle\n  part.vx = limitSpeed((int32_t)part.vx + dvx);\n  part.vy = limitSpeed((int32_t)part.vy + dvy);\n}\n\n// apply a force in x,y direction to individual particle using advanced particle properties\nvoid ParticleSystem2D::applyForce(const uint32_t particleindex, const int8_t xforce, const int8_t yforce) {\n  if (advPartProps == nullptr)\n    return; // no advanced properties available\n  applyForce(particles[particleindex], xforce, yforce, advPartProps[particleindex].forcecounter);\n}\n\n// apply a force in x,y direction to all particles\n// force is in 3.4 fixed point notation (see above)\nvoid ParticleSystem2D::applyForce(const int8_t xforce, const int8_t yforce) {\n  // for small forces, need to use a delay counter\n  uint8_t tempcounter;\n  // note: this is not the most computationally efficient way to do this, but it saves on duplicate code and is fast enough\n  for (uint32_t i = 0; i < usedParticles; i++) {\n    tempcounter = forcecounter;\n    applyForce(particles[i], xforce, yforce, tempcounter);\n  }\n  forcecounter = tempcounter; // save value back\n}\n\n// apply a force in angular direction to single particle\n// caller needs to provide a 8bit counter that holds its value between calls (if using single particles, a counter for each particle is needed)\n// angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right)\n// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame (useful force range is +/- 127)\nvoid ParticleSystem2D::applyAngleForce(PSparticle &part, const int8_t force, const uint16_t angle, uint8_t &counter) {\n  int8_t xforce = ((int32_t)force * cos16_t(angle)) / 32767; // force is +/- 127\n  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!\n  applyForce(part, xforce, yforce, counter);\n}\n\nvoid ParticleSystem2D::applyAngleForce(const uint32_t particleindex, const int8_t force, const uint16_t angle) {\n  if (advPartProps == nullptr)\n    return; // no advanced properties available\n  applyAngleForce(particles[particleindex], force, angle, advPartProps[particleindex].forcecounter);\n}\n\n// apply a force in angular direction to all particles\n// angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right)\nvoid ParticleSystem2D::applyAngleForce(const int8_t force, const uint16_t angle) {\n  int8_t xforce = ((int32_t)force * cos16_t(angle)) / 32767; // force is +/- 127\n  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!\n  applyForce(xforce, yforce);\n}\n\n// apply gravity to all particles using PS global gforce setting\n// force is in 3.4 fixed point notation, see note above\n// note: faster than apply force since direction is always down and counter is fixed for all particles\nvoid ParticleSystem2D::applyGravity() {\n  int32_t dv = calcForce_dv(gforce, gforcecounter);\n  if (dv == 0) return;\n  for (uint32_t i = 0; i < usedParticles; i++) {\n    // Note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways\n    particles[i].vy = limitSpeed((int32_t)particles[i].vy - dv);\n  }\n}\n\n// apply gravity to single particle using system settings (use this for sources)\n// function does not increment gravity counter, if gravity setting is disabled, this cannot be used\nvoid ParticleSystem2D::applyGravity(PSparticle &part) {\n  uint32_t counterbkp = gforcecounter; // backup PS gravity counter\n  int32_t dv = calcForce_dv(gforce, gforcecounter);\n  gforcecounter = counterbkp; //save it back\n  part.vy = limitSpeed((int32_t)part.vy - dv);\n}\n\n// slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop)\n// 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\nvoid ParticleSystem2D::applyFriction(PSparticle &part, const int32_t coefficient) {\n  // note: not checking if particle is dead can be done by caller (or can be omitted)\n  #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster)\n  int32_t friction = 256 - coefficient;\n  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\n  part.vy = ((int32_t)part.vy * friction + (((int32_t)part.vy >> 31) & 0xFF)) >> 8;\n  #else // division is faster on ESP32, S2 and S3\n  int32_t friction = 255 - coefficient;\n  part.vx = ((int32_t)part.vx * friction) / 255;\n  part.vy = ((int32_t)part.vy * friction) / 255;\n  #endif\n}\n\n// apply friction to all particles\n// note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways\nvoid ParticleSystem2D::applyFriction(const int32_t coefficient) {\n  #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster)\n  int32_t friction = 256 - coefficient;\n  for (uint32_t i = 0; i < usedParticles; i++) {\n    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\n    particles[i].vy = ((int32_t)particles[i].vy * friction + (((int32_t)particles[i].vy >> 31) & 0xFF)) >> 8;\n  }\n  #else // division is faster on ESP32, S2 and S3\n  int32_t friction = 255 - coefficient;\n  for (uint32_t i = 0; i < usedParticles; i++) {\n    particles[i].vx = ((int32_t)particles[i].vx * friction) / 255;\n    particles[i].vy = ((int32_t)particles[i].vy * friction) / 255;\n  }\n  #endif\n}\n\n// attracts a particle to an attractor particle using the inverse square-law\nvoid ParticleSystem2D::pointAttractor(const uint32_t particleindex, PSparticle &attractor, const uint8_t strength, const bool swallow) {\n  if (advPartProps == nullptr)\n    return; // no advanced properties available\n\n  // Calculate the distance between the particle and the attractor\n  int32_t dx = attractor.x - particles[particleindex].x;\n  int32_t dy = attractor.y - particles[particleindex].y;\n\n  // Calculate the force based on inverse square law\n  int32_t distanceSquared = dx * dx + dy * dy;\n  if (distanceSquared < 8192) {\n    if (swallow) { // particle is close, age it fast so it fades out, do not attract further\n      if (particles[particleindex].ttl > 7)\n        particles[particleindex].ttl -= 8;\n      else {\n        particles[particleindex].ttl = 0;\n        return;\n      }\n    }\n    distanceSquared = 2 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance to avoid very high forces\n  }\n\n  int32_t force = ((int32_t)strength << 16) / distanceSquared;\n  int8_t xforce = (force * dx) / 1024; // scale to a lower value, found by experimenting\n  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!\n  applyForce(particleindex, xforce, yforce);\n}\n\n// render particles to the LED buffer (uses palette to render the 8bit particle color value)\n// if wrap is set, particles half out of bounds are rendered to the other side of the matrix\n// warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds\n// firemode is only used for PS Fire FX\nvoid ParticleSystem2D::render() {\n  if (framebuffer == nullptr) {\n    PSPRINTLN(F(\"PS render: no framebuffer!\"));\n    return;\n  }\n  CRGBW baseRGB;\n  uint32_t brightness; // particle brightness, fades if dying\n  TBlendType blend = LINEARBLEND; // default color rendering: wrap palette\n  if (particlesettings.colorByAge) {\n    blend = LINEARBLEND_NOWRAP;\n  }\n\n  if (motionBlur) { // motion-blurring active\n    for (int32_t y = 0; y <= maxYpixel; y++) {\n      int index = y * (maxXpixel + 1);\n      for (int32_t x = 0; x <= maxXpixel; x++) {\n        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\n        index++;\n      }\n    }\n  }\n  else { // no blurring: clear buffer\n    memset(framebuffer, 0, (maxXpixel+1) * (maxYpixel+1) * sizeof(CRGBW));\n  }\n\n  // go over particles and render them to the buffer\n  for (uint32_t i = 0; i < usedParticles; i++) {\n    if (particles[i].ttl == 0 || particleFlags[i].outofbounds)\n      continue;\n    // generate RGB values for particle\n    if (fireIntesity) { // fire mode\n      brightness = (uint32_t)particles[i].ttl * (3 + (fireIntesity >> 5)) + 5;\n      brightness = min(brightness, (uint32_t)255);\n      baseRGB = ColorFromPaletteWLED(SEGPALETTE, brightness, 255, LINEARBLEND_NOWRAP); // map hue to brightness for fire effect\n    }\n    else {\n      brightness = min((particles[i].ttl << 1), (int)255);\n      baseRGB = ColorFromPaletteWLED(SEGPALETTE, particles[i].hue, 255, blend);\n      if (particles[i].sat < 255) {\n        CHSV32 baseHSV;\n        rgb2hsv(baseRGB.color32, baseHSV); // convert to HSV\n        baseHSV.s = min(baseHSV.s, particles[i].sat); // set the saturation but don't increase it\n        hsv2rgb(baseHSV, baseRGB.color32); // convert back to RGB\n      }\n    }\n    if (gammaCorrectCol) brightness = gamma8(brightness); // apply gamma correction, used for gamma-inverted brightness distribution\n    renderParticle(i, brightness, baseRGB, particlesettings.wrapX, particlesettings.wrapY);\n  }\n\n  // apply 2D blur to rendered frame\n  if (smearBlur) {\n    SEGMENT.blur2D(smearBlur, smearBlur, true);\n  }\n}\n\n// calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer\nvoid WLED_O2_ATTR ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrapX, const bool wrapY) {\n  uint32_t size = particlesize;\n\n  if (perParticleSize && advPartProps != nullptr) // use advanced size properties\n    size = 1 + advPartProps[particleindex].size; // add 1 to avoid single pixel size particles (collisions do not support it)\n\n  if (size == 0) { // single pixel rendering\n    uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT;\n    uint32_t y = particles[particleindex].y >> PS_P_RADIUS_SHIFT;\n    if (x <= (uint32_t)maxXpixel && y <= (uint32_t)maxYpixel) {\n      uint32_t index = x + (maxYpixel - y) * (maxXpixel + 1); // flip y coordinate (0,0 is bottom left in PS but top left in framebuffer)\n      framebuffer[index] = fast_color_scaleAdd(framebuffer[index], color, brightness);\n    }\n    return;\n  }\n\n  if (size > 1) { // size > 1: render as ellipse\n    renderLargeParticle(size, particleindex, brightness, color, wrapX, wrapY); // larger size rendering\n    return;\n  }\n\n  // size = 1: standard 2x2 pixel rendering using bilinear interpolation (20% faster than ellipse rendering)\n  uint8_t pxlbrightness[4]; // brightness values for the four pixels representing a particle\n  struct {\n    int32_t x,y;\n  } 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)\n  bool pixelvalid[4] = {true, true, true, true}; // is set to false if pixel is out of bounds\n\n  // 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)\n  // if sub-pixel position is 0-PS_P_HALFRADIUS it will render to x>>PS_P_RADIUS_SHIFT as the right pixel\n  int32_t xoffset = particles[particleindex].x + PS_P_HALFRADIUS;\n  int32_t yoffset = particles[particleindex].y + PS_P_HALFRADIUS;\n  int32_t dx = xoffset & (PS_P_RADIUS - 1); // relativ particle position in subpixel space\n  int32_t dy = yoffset & (PS_P_RADIUS - 1); // modulo replaced with bitwise AND, as radius is always a power of 2\n  int32_t x = (xoffset >> PS_P_RADIUS_SHIFT); // divide by PS_P_RADIUS which is 64, so can bitshift (compiler can not optimize integer)\n  int32_t y = (yoffset >> PS_P_RADIUS_SHIFT);\n\n  // set the four raw pixel coordinates\n  pixco[1].x = pixco[2].x = x;  // bottom right & top right\n  pixco[2].y = pixco[3].y = y;  // top right & top left\n  x--; // shift by a full pixel here, this is skipped above to not do -1 and then +1\n  y--;\n  pixco[0].x = pixco[3].x = x;      // bottom left & top left\n  pixco[0].y = pixco[1].y = y;      // bottom left & bottom right\n\n  // calculate brightness values for all four pixels representing a particle using linear interpolation\n  // could check for out of frame pixels here but calculating them is faster (very few are out)\n  // precalculate values for speed optimization. Note: rounding is not perfect but close enough, some inaccuracy is traded for speed\n  int32_t precal1 = (int32_t)PS_P_RADIUS - dx;\n  int32_t precal2 = ((int32_t)PS_P_RADIUS - dy) * brightness;\n  int32_t precal3 = dy * brightness;\n  pxlbrightness[0] = (precal1 * precal2) >> PS_P_SURFACE; // bottom left value equal to ((PS_P_RADIUS - dx) * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE\n  pxlbrightness[1] = (dx * precal2) >> PS_P_SURFACE; // bottom right value equal to (dx * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE\n  pxlbrightness[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightness) >> PS_P_SURFACE\n  pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightness) >> PS_P_SURFACE\n  // adjust brightness such that distribution is linear after gamma correction:\n  // - scale brigthness with gamma correction (done in render())\n  // - apply inverse gamma correction to brightness values\n  // - gamma is applied again in show() -> the resulting brightness distribution is linear but gamma corrected in total\n  if (gammaCorrectCol) {\n    for (uint32_t i = 0; i < 4; i++) {\n      pxlbrightness[i] = gamma8inv(pxlbrightness[i]); // use look-up-table for invers gamma\n    }\n  }\n\n  // standard rendering (2x2 pixels)\n  // check for out of frame pixels and wrap them if required: x,y is bottom left pixel coordinate of the particle\n  if (pixco[0].x < 0) { // left pixels out of frame\n    if (wrapX) { // wrap x to the other side if required\n      pixco[0].x = pixco[3].x = maxXpixel;\n    } else {\n      pixelvalid[0] = pixelvalid[3] = false; // out of bounds\n      if (pixco[0].x < -1) return; // both left pixels out of bounds, no need to continue (safety check)\n    }\n  }\n  else if (pixco[1].x > (int32_t)maxXpixel) { // right pixels, only has to be checked if left pixel is in frame\n    if (wrapX) { // wrap y to the other side if required\n      pixco[1].x = pixco[2].x = 0;\n    } else {\n      pixelvalid[1] = pixelvalid[2] = false; // out of bounds\n      if (pixco[0].x > (int32_t)maxXpixel) return; // both pixels out of bounds, no need to continue (safety check)\n    }\n  }\n\n  if (pixco[0].y < 0) { // bottom pixels out of frame\n    if (wrapY) { // wrap y to the other side if required\n      pixco[0].y = pixco[1].y = maxYpixel;\n    } else {\n      pixelvalid[0] = pixelvalid[1] = false; // out of bounds\n      if (pixco[0].y < -1) return; // both bottom pixels out of bounds, no need to continue (safety check)\n    }\n  }\n  else if (pixco[2].y > maxYpixel) { // top pixels\n    if (wrapY) { // wrap y to the other side if required\n      pixco[2].y = pixco[3].y = 0;\n    } else {\n      pixelvalid[2] = pixelvalid[3] = false; // out of bounds\n      if (pixco[2].y > (int32_t)maxYpixel + 1) return; // both top pixels out of bounds, no need to continue (safety check)\n    }\n  }\n  for (uint32_t i = 0; i < 4; i++) {\n    if (pixelvalid[i]) {\n      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)\n      framebuffer[idx] = fast_color_scaleAdd(framebuffer[idx], color, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left\n    }\n  }\n}\n\n// render particle as ellipse/circle with linear brightness falloff and sub-pixel precision\nvoid 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) {\n  // particle position with sub-pixel precision\n  int32_t x_subcenter = particles[particleindex].x;\n  int32_t y_subcenter = particles[particleindex].y;\n\n  // example: for x = 128, a paticle is exacly between pixel 1 and 2, with a radius of 2 pixels, we draw pixels 0-3\n  // integer center jumps when x = 127 -> pixel 1 goes to x = 128 -> pixel 2\n  // 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:\n  // 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:\n  // dx = pixel_x * PS_P_RADIUS - x_subcenter + PS_P_HALFRADIUS\n\n  // sub-pixel offset (0-63)\n  int32_t x_offset = x_subcenter & (PS_P_RADIUS - 1); // same as modulo PS_P_RADIUS but faster\n  int32_t y_offset = y_subcenter & (PS_P_RADIUS - 1);\n  // integer pixel position, this is rounded down\n  int32_t x_center = (x_subcenter) >> PS_P_RADIUS_SHIFT;\n  int32_t y_center = (y_subcenter) >> PS_P_RADIUS_SHIFT;\n\n  // ellipse radii in pixels\n  uint32_t xsize = size;\n  uint32_t ysize = size;\n  if (advPartSize != nullptr && advPartSize[particleindex].asymmetry > 0) {\n    getParticleXYsize(&advPartProps[particleindex], &advPartSize[particleindex], xsize, ysize);\n  }\n\n  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)\n  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\n\n  // rendering bounding box in pixels\n  int32_t rx_pixels = (rx_subpixel >> PS_P_RADIUS_SHIFT);\n  int32_t ry_pixels = (ry_subpixel >> PS_P_RADIUS_SHIFT);\n\n  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\n  int32_t x_max = x_center + rx_pixels;\n  int32_t y_min = y_center - ry_pixels;\n  int32_t y_max = y_center + ry_pixels;\n\n  // cache for speed\n  uint32_t matrixX = maxXpixel + 1;\n  uint32_t matrixY = maxYpixel + 1;\n  uint32_t rx_sq = rx_subpixel * rx_subpixel;\n  uint32_t ry_sq = ry_subpixel * ry_subpixel;\n\n  // iterate over bounding box and render each pixel\n  for (int32_t py = y_min; py <= y_max; py++) {\n    for (int32_t px = x_min; px <= x_max; px++) {\n      // Check bounds and apply wrapping\n      int32_t render_x = px;\n      int32_t render_y = py;\n      if (render_x < 0) {\n        if (!wrapX) continue;\n        render_x += matrixX;\n      } else if (render_x > maxXpixel) {\n        if (!wrapX) continue;\n        render_x -= matrixX;\n      }\n\n      if (render_y < 0) {\n        if (!wrapY) continue;\n        render_y += matrixY;\n      } else if (render_y > maxYpixel) {\n        if (!wrapY) continue;\n        render_y -= matrixY;\n      }\n\n      // distance from particle center, explanation see above\n      int32_t dx_subpixel = (px << PS_P_RADIUS_SHIFT) - x_subcenter + PS_P_HALFRADIUS;\n      int32_t dy_subpixel = (py << PS_P_RADIUS_SHIFT) - y_subcenter + PS_P_HALFRADIUS;\n\n      // calculate brightness based on squared distance to ellipse center\n      uint8_t pixel_brightness = calculateEllipseBrightness(dx_subpixel, dy_subpixel, rx_sq, ry_sq, brightness);\n\n      if (pixel_brightness == 0) continue; // skip black pixels\n\n      // apply inverse gamma correction if needed, if this is skipped, particles flicker due to changing total brightness\n      if (gammaCorrectCol) {\n        pixel_brightness = gamma8inv(pixel_brightness); // invert brigthess so brightness distribution is linear after gamma correction\n      }\n\n      // Render pixel\n      uint32_t idx = render_x + (maxYpixel - render_y) * matrixX; // flip y coordinate (0,0 is bottom left in PS but top left in framebuffer)\n      framebuffer[idx] = fast_color_scaleAdd(framebuffer[idx], color, pixel_brightness);\n    }\n  }\n}\n\n// detect collisions in an array of particles and handle them\n// 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)\n// for code simplicity, no y slicing is done, making very tall matrix configurations less efficient\n// 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\nvoid ParticleSystem2D::handleCollisions() {\n  uint32_t collDistSq = particleHardRadius << 1; // distance is double the radius note: particleHardRadius is updated when setting global particle size\n  collDistSq = collDistSq * collDistSq; // square it for faster comparison (square is one operation)\n  // note: partices are binned in x-axis, assumption is that no more than half of the particles are in the same bin\n  // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions)\n  int binWidth = 6 * PS_P_RADIUS; // width of a bin in sub-pixels\n  int32_t overlap = particleHardRadius << 1; // overlap bins to include edge particles to neighbouring bins\n  if (perParticleSize && advPartProps != nullptr)\n    overlap = 512; // max overlap for collision detection if using per-particle size, enough to catch all particles even at max speed\n\n  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\n  uint32_t numBins = (maxX + (binWidth - 1)) / binWidth; // number of bins in x direction\n  if (usedParticles < maxBinParticles) {\n    numBins = 1; // use single bin for small number of particles\n    binWidth = maxX + 1;\n  }\n  uint16_t binIndices[maxBinParticles]; // creat array on stack for indices, 2kB max for 1024 particles (ESP32_MAXPARTICLES/2)\n  uint32_t binParticleCount; // number of particles in the current bin\n  uint32_t nextFrameStartIdx = hw_random16(usedParticles); // index of the first particle in the next frame (set to fixed value if bin overflow)\n  uint32_t pidx = collisionStartIdx; //start index in case a bin is full, process remaining particles next frame\n\n  // fill the binIndices array for this bin\n  for (uint32_t bin = 0; bin < numBins; bin++) {\n    binParticleCount = 0; // reset for this bin\n    int32_t binStart = bin * binWidth - overlap; // note: first bin will extend to negative, but that is ok as out of bounds particles are ignored\n    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;\n\n    // fill the binIndices array for this bin\n    for (uint32_t i = 0; i < usedParticles; i++) {\n      if (particles[pidx].ttl > 0) { // is alive\n        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)\n          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\n            if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame\n              nextFrameStartIdx = pidx; // bin overflow can only happen once as bin size is at least half of the particles (or half +1)\n              break;\n            }\n            binIndices[binParticleCount++] = pidx;\n          }\n        }\n      }\n      pidx++;\n      if (pidx >= usedParticles) pidx = 0; // wrap around\n    }\n\n    int32_t massratio1 = 0; // 0 means dont use mass ratio (equal mass)\n    int32_t massratio2 = 0; // TODO: if implementing \"fixed\" particles, set to 1 (fixed) and 255 (movable)\n    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\n      uint32_t idx_i = binIndices[i];\n      for (uint32_t j = i + 1; j < binParticleCount; j++) { // check against higher number particles\n        uint32_t idx_j = binIndices[j];\n        if (perParticleSize && advPartProps != nullptr) { // using individual particle size\n          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)\n          collDistSq = collDistSq * collDistSq; // square it for faster comparison\n          // calculate mass ratio for collision response\n          uint32_t mass1 = PS_P_RADIUS + advPartProps[idx_i].size;\n          uint32_t mass2 = PS_P_RADIUS + advPartProps[idx_j].size;\n          mass1 = mass1 * mass1; // mass proportional to area\n          mass2 = mass2 * mass2;\n          uint32_t totalmass = mass1 + mass2;\n          massratio1 = (mass2 << 8) / totalmass; // massratio 1 depends on mass of particle 2, i.e. if 2 is heavier -> higher velocity impact on 1\n          massratio2 = (mass1 << 8) / totalmass;\n        }\n        // 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\n        int32_t dx = (particles[idx_j].x + particles[idx_j].vx) - (particles[idx_i].x + particles[idx_i].vx); // distance with lookahead\n        if (dx * dx < collDistSq) { // check x direction, if close, check y direction (squaring is faster than abs() or dual compare)\n          int32_t dy = (particles[idx_j].y + particles[idx_j].vy)  - (particles[idx_i].y + particles[idx_i].vy); // distance with lookahead\n          if (dy * dy < collDistSq) // particles are close\n            collideParticles(particles[idx_i], particles[idx_j], dx, dy, collDistSq, massratio1, massratio2);\n        }\n      }\n    }\n  }\n  collisionStartIdx = nextFrameStartIdx; // set the start index for the next frame\n}\n\n// handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS\n// takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard)\nvoid 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) {\n  int32_t distanceSquared = dx * dx + dy * dy;\n  // 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\n  int32_t relativeVx = (int32_t)particle2.vx - (int32_t)particle1.vx;\n  int32_t relativeVy = (int32_t)particle2.vy - (int32_t)particle1.vy;\n\n  // 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)\n  if (distanceSquared == 0) {\n    // Adjust positions based on relative velocity direction\n    dx = -1;\n    if (relativeVx < 0) // if true, particle2 is on the right side\n      dx = 1;\n    else if (relativeVx == 0)\n      relativeVx = 1;\n\n    dy = -1;\n    if (relativeVy < 0)\n      dy = 1;\n    else if (relativeVy == 0)\n      relativeVy = 1;\n\n    distanceSquared = 2; // 1 + 1\n  }\n\n  // Calculate dot product of relative velocity and relative distance\n  int32_t dotProduct = (dx * relativeVx + dy * relativeVy); // is always negative if moving towards each other\n\n  if (dotProduct < 0) {// particles are moving towards each other\n    // integer math is much faster than using floats (float divisions are slow on all ESPs)\n    // 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\n    // 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\n    // Calculate new velocities after collision\n    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\n    int32_t impulse = (((((-dotProduct) << 15) / distanceSquared) * surfacehardness) >> 8); // note: inverting before bitshift corrects for asymmetry in right-shifts (is slightly faster)\n\n    #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster)\n    int32_t ximpulse = (impulse * dx + ((dx >> 31) & 0x7FFF)) >> 15; // note: extracting sign bit and adding rounding value to correct for asymmetry in right shifts\n    int32_t yimpulse = (impulse * dy + ((dy >> 31) & 0x7FFF)) >> 15;\n    #else\n    int32_t ximpulse = (impulse * dx) / 32767;\n    int32_t yimpulse = (impulse * dy) / 32767;\n    #endif\n    // if particles are not the same size, use a mass ratio. mass ratio is set to 0 if particles are the same size\n    if (massratio1) {\n      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\n      int32_t vy1 = (int32_t)particle1.vy - ((yimpulse * massratio1) >> 7);\n      int32_t vx2 = (int32_t)particle2.vx + ((ximpulse * massratio2) >> 7);\n      int32_t vy2 = (int32_t)particle2.vy + ((yimpulse * massratio2) >> 7);\n      // limit speeds to max speed (required if a lot of impulse is transferred from a large to a small particle)\n      particle1.vx = limitSpeed(vx1);\n      particle1.vy = limitSpeed(vy1);\n      particle2.vx = limitSpeed(vx2);\n      particle2.vy = limitSpeed(vy2);\n    }\n    else {\n      particle1.vx -= ximpulse; // note: impulse is inverted, so subtracting it\n      particle1.vy -= yimpulse;\n      particle2.vx += ximpulse;\n      particle2.vy += yimpulse;\n    }\n    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)\n      const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS);\n      // Note: could call applyFriction, but this is faster and speed is key here\n      #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster)\n      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\n      particle1.vy = ((int32_t)particle1.vy * coeff + (((int32_t)particle1.vy >> 31) & 0xFF)) >> 8;\n      particle2.vx = ((int32_t)particle2.vx * coeff + (((int32_t)particle2.vx >> 31) & 0xFF)) >> 8;\n      particle2.vy = ((int32_t)particle2.vy * coeff + (((int32_t)particle2.vy >> 31) & 0xFF)) >> 8;\n      #else // division is faster on ESP32, S2 and S3\n      particle1.vx = ((int32_t)particle1.vx * coeff) / 255;\n      particle1.vy = ((int32_t)particle1.vy * coeff) / 255;\n      particle2.vx = ((int32_t)particle2.vx * coeff) / 255;\n      particle2.vy = ((int32_t)particle2.vy * coeff) / 255;\n      #endif\n    }\n  }\n    // particles have volume, push particles apart if they are too close\n    // tried lots of configurations, what works best is to give one particle a little velocity. When adding hard pushing things tend to oscillate.\n    // when hard pushing by offsetting position without velocity, they tend to sink into each other under gravity.\n    // when using hard-pushing and velocity, there are some oscillations and softer particles do not pile nicely.\n    // oscillation get worse if pushing both particles so one is chosen somewhat randomly.\n    // 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\n\n    if (distanceSquared < collDistSq && (relativeVx*relativeVx + relativeVy*relativeVy < 50)) { // too close and also slow, push them apart\n      bool fairlyrandom = dotProduct & 0x01; //dotprouct LSB should be somewhat random, so no need to calculate a random number\n      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)\n      int8_t pushx = dx > 0 ? -pushamount : pushamount; // particle 1 is on the left\n      int8_t pushy = dy > 0 ? -pushamount : pushamount; // particle 1 is below particle 2\n\n      // if they are very soft, stop slow particles completely to make them stick to each other\n      if (collisionHardness < 5) {\n        if (fairlyrandom) { // do not stop them every frame to avoid groups of particles hanging mid-air\n          particle1.vx = 0;\n          particle1.vy = 0;\n          particle2.vx = 0;\n          particle2.vy = 0;\n          // hard-push particle 1 only: if both are pushed, this oscillates ever so slightly\n          particle1.x += pushx;\n          particle1.y += pushy;\n        }\n      }\n      else {\n        if (fairlyrandom) {\n          particle1.vx += pushx;\n          //particle1.x += pushx;\n          particle1.vy += pushy;\n          //particle1.y += pushy;\n        }\n        else {\n          particle2.vx -= pushx;\n          //particle2.x -= pushx;\n          particle2.vy -= pushy;\n          //particle2.y -= pushy;\n        }\n      }\n    }\n}\n\n// update size and pointers (memory location and size can change dynamically)\n// note: do not access the PS class in FX befor running this function (or it messes up SEGENV.data)\nvoid ParticleSystem2D::updateSystem(void) {\n  //PSPRINTLN(\"updateSystem2D\");\n  setMatrixSize(SEGMENT.vWidth(), SEGMENT.vHeight());\n  updatePSpointers(advPartProps != nullptr, advPartSize != nullptr); // update pointers to PS data, also updates availableParticles\n  //PSPRINTLN(\"\\n END update System2D, running FX...\");\n}\n\n// 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)\n// 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)\n// FX handles the PSsources, need to tell this function how many there are\nvoid ParticleSystem2D::updatePSpointers(bool isadvanced, bool sizecontrol) {\n  //PSPRINTLN(\"updatePSpointers\");\n  // Note on memory alignment:\n  // 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.\n  // 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.\n  // 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.\n  particles = reinterpret_cast<PSparticle *>(this + 1); // pointer to particles\n  particleFlags = reinterpret_cast<PSparticleFlags *>(particles + numParticles); // pointer to particle flags\n  sources = reinterpret_cast<PSsource *>(particleFlags + numParticles); // pointer to source(s) at data+sizeof(ParticleSystem2D)\n  framebuffer = SEGMENT.getPixels(); // pointer to framebuffer\n  PSdataEnd = reinterpret_cast<uint8_t *>(sources + numSources); // pointer to first available byte after the PS for FX additional data (already aligned to 4 byte boundary)\n  if (isadvanced) {\n    advPartProps = reinterpret_cast<PSadvancedParticle *>(PSdataEnd);\n    PSdataEnd = reinterpret_cast<uint8_t *>(advPartProps + numParticles);\n    if (sizecontrol) {\n      advPartSize = reinterpret_cast<PSsizeControl *>(PSdataEnd);\n      PSdataEnd = reinterpret_cast<uint8_t *>(advPartSize + numParticles);\n    }\n  }\n#ifdef DEBUG_PS\n  Serial.printf_P(PSTR(\" particles %p \"), particles);\n  Serial.printf_P(PSTR(\" sources %p \"), sources);\n  Serial.printf_P(PSTR(\" adv. props %p \"), advPartProps);\n  Serial.printf_P(PSTR(\" adv. ctrl %p \"), advPartSize);\n  Serial.printf_P(PSTR(\"end %p\\n\"), PSdataEnd);\n  #endif\n\n}\n\n//non class functions to use for initialization\nuint32_t calculateNumberOfParticles2D(uint32_t const pixels, const bool isadvanced, const bool sizecontrol) {\n  uint32_t numberofParticles = pixels;  // 1 particle per pixel (for example 512 particles on 32x16)\n  uint32_t particlelimit = MAXPARTICLES_2D; // maximum number of paticles allowed\n  numberofParticles = max((uint32_t)4, min(numberofParticles, particlelimit)); // limit to 4 - particlelimit\n  if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount\n    numberofParticles = (numberofParticles * sizeof(PSparticle)) / (sizeof(PSparticle) + sizeof(PSadvancedParticle));\n  if (sizecontrol) // advanced property array needs ram, reduce number of particles\n    numberofParticles /= 8; // if advanced size control is used, much fewer particles are needed note: if changing this number, adjust FX using this accordingly\n\n  //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes)\n  numberofParticles = (numberofParticles+3) & ~0x03;\n  return numberofParticles;\n}\n\nuint32_t calculateNumberOfSources2D(uint32_t pixels, uint32_t requestedsources) {\n  int numberofSources = min((pixels) / SOURCEREDUCTIONFACTOR, (uint32_t)requestedsources);\n  numberofSources = max(1, min(numberofSources, MAXSOURCES_2D)); // limit\n  // make sure it is a multiple of 4 for proper memory alignment\n  numberofSources = (numberofSources+3) & ~0x03;\n  return numberofSources;\n}\n\n//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?\nbool allocateParticleSystemMemory2D(uint32_t numparticles, uint32_t numsources, bool isadvanced, bool sizecontrol, uint32_t additionalbytes) {\n  PSPRINTLN(\"PS 2D alloc\");\n  PSPRINTLN(\"numparticles:\" + String(numparticles) + \" numsources:\" + String(numsources) + \" additionalbytes:\" + String(additionalbytes));\n  uint32_t requiredmemory = sizeof(ParticleSystem2D);\n  // functions above make sure numparticles is a multiple of 4 bytes (to avoid alignment issues)\n  requiredmemory += sizeof(PSparticleFlags) * numparticles;\n  requiredmemory += sizeof(PSparticle) * numparticles;\n  if (isadvanced)\n    requiredmemory += sizeof(PSadvancedParticle) * numparticles;\n  if (sizecontrol)\n    requiredmemory += sizeof(PSsizeControl) * numparticles;\n  requiredmemory += sizeof(PSsource) * numsources;\n  requiredmemory += additionalbytes;\n  return(SEGMENT.allocateData(requiredmemory));\n}\n\n// initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd)\nbool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, uint32_t additionalbytes, bool advanced, bool sizecontrol) {\n  PSPRINT(\"PS 2D init \");\n  if (!strip.isMatrix) {\n    errorFlag = ERR_NOT_IMPL; // TODO: need a better error code if more codes are added\n    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)\n    return false; // only for 2D\n  }\n  uint32_t cols = SEGMENT.virtualWidth();\n  uint32_t rows = SEGMENT.virtualHeight();\n  uint32_t pixels = cols * rows;\n  if (sizecontrol)\n    advanced = true; // size control needs advanced properties, prevent wrong usage\n\n  uint32_t numparticles = calculateNumberOfParticles2D(pixels, advanced, sizecontrol);\n  PSPRINT(\" segmentsize:\" + String(cols) + \" x \" + String(rows));\n  PSPRINTLN(\" request numparticles:\" + String(numparticles));\n  uint32_t numsources = calculateNumberOfSources2D(pixels, requestedsources);\n  bool allocsuccess = false;\n  while(numparticles >= 5) { // make sure we have at least 5 particles or quit\n    if (allocateParticleSystemMemory2D(numparticles, numsources, advanced, sizecontrol, additionalbytes)) {\n      PSPRINTLN(F(\"PS 2D alloc succeeded\"));\n      allocsuccess = true;\n      break; // allocation succeeded\n    }\n    numparticles = ((numparticles / 2) + 3) & ~0x03; // cut number of particles in half and try again, must be 4 byte aligned\n    PSPRINTLN(F(\"PS 2D alloc failed, trying with less particles...\"));\n  }\n  if (!allocsuccess) {\n    PSPRINTLN(F(\"PS 2D alloc failed, not enough memory!\"));\n    return false; // allocation failed\n  }\n\n  PartSys = new (SEGENV.data) ParticleSystem2D(cols, rows, numparticles, numsources, advanced, sizecontrol); // particle system constructor\n\n  PSPRINTLN(F(\"2D PS init done\"));\n  return true;\n}\n\n#endif // WLED_DISABLE_PARTICLESYSTEM2D\n\n\n////////////////////////\n// 1D Particle System //\n////////////////////////\n#ifndef WLED_DISABLE_PARTICLESYSTEM1D\n\nParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced) {\n  numSources = numberofsources;\n  numParticles = numberofparticles; // number of particles allocated in init\n  usedParticles = numParticles; // use all particles by default\n  advPartProps = nullptr; //make sure we start out with null pointers (just in case memory was not cleared)\n  //advPartSize = nullptr;\n  setSize(length);\n  updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles)\n  setWallHardness(255); // set default wall hardness to max\n  setGravity(0); //gravity disabled by default\n  setParticleSize(0); // 1 pixel size by default\n  motionBlur = 0; //no fading by default\n  smearBlur = 0; //no smearing by default\n  emitIndex = 0;\n  collisionStartIdx = 0;\n  // initialize some default non-zero values most FX use\n  for (uint32_t i = 0; i < numSources; i++) {\n    sources[i].source.ttl = 1; //set source alive\n    sources[i].sourceFlags.asByte = 0; // all flags disabled\n  }\n  perParticleSize = isadvanced; // enable per particle size by default so FX do not need to set this explicitly. FX can disable by setting global size.\n  if (isadvanced) {\n    for (uint32_t i = 0; i < numParticles; i++) {\n      advPartProps[i].sat = 255; // set full saturation\n    }\n  }\n}\n\n// update function applies gravity, moves the particles, handles collisions and renders the particles\nvoid ParticleSystem1D::update(void) {\n  //apply gravity globally if enabled\n  if (particlesettings.useGravity) //note: in 1D system, applying gravity after collisions also works but may be worse\n    applyGravity();\n\n  // 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)\n  if (particlesettings.useCollisions) {\n    handleCollisions();\n    if (perParticleSize)\n      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)\n  }\n\n  //move all particles\n  for (uint32_t i = 0; i < usedParticles; i++) {\n    particleMoveUpdate(particles[i], particleFlags[i], nullptr, advPartProps ? &advPartProps[i] : nullptr);\n  }\n\n  if (particlesettings.colorByPosition) {\n    uint32_t scale = (255 << 16) / maxX;\n    for (uint32_t i = 0; i < usedParticles; i++) {\n      particles[i].hue = (scale * particles[i].x) >> 16; // note: x is > 0 if not out of bounds\n    }\n  }\n\n  render();\n}\n\n// set percentage of used particles as uint8_t i.e 127 means 50% for example\nvoid ParticleSystem1D::setUsedParticles(const uint8_t percentage) {\n  usedParticles =  max((uint32_t)1, (numParticles * ((int)percentage+1)) >> 8); // number of particles to use (percentage is 0-255, 255 = 100%)\n  PSPRINT(\" SetUsedpaticles: allocated particles: \");\n  PSPRINT(numParticles);\n  PSPRINT(\" ,used particles: \");\n  PSPRINTLN(usedParticles);\n}\n\nvoid ParticleSystem1D::setWallHardness(const uint8_t hardness) {\n  wallHardness = hardness;\n}\n\nvoid ParticleSystem1D::setSize(const uint32_t x) {\n  maxXpixel = x - 1; // last physical pixel that can be drawn to\n  maxX = x * PS_P_RADIUS_1D - 1;  // particle system boundary for movements\n}\n\nvoid ParticleSystem1D::setWrap(const bool enable) {\n  particlesettings.wrap = enable;\n}\n\nvoid ParticleSystem1D::setBounce(const bool enable) {\n  particlesettings.bounce = enable;\n}\n\nvoid ParticleSystem1D::setKillOutOfBounds(const bool enable) {\n  particlesettings.killoutofbounds = enable;\n}\n\nvoid ParticleSystem1D::setColorByAge(const bool enable) {\n  particlesettings.colorByAge = enable;\n}\n\nvoid ParticleSystem1D::setColorByPosition(const bool enable) {\n  particlesettings.colorByPosition = enable;\n}\n\nvoid ParticleSystem1D::setMotionBlur(const uint8_t bluramount) {\n  motionBlur = bluramount;\n}\n\nvoid ParticleSystem1D::setSmearBlur(const uint8_t bluramount) {\n  smearBlur = bluramount;\n}\n\n// render size, 0 = 1 pixel, 1 = 2 pixel (interpolated), 255 = 18 pixel diameter\nvoid ParticleSystem1D::setParticleSize(const uint8_t size) {\n  particlesize = size;\n  particleHardRadius = PS_P_MINHARDRADIUS_1D; // ~1 pixel\n  perParticleSize = false; // disable per particle size control if global size is set\n  if (particlesize > 1) {\n    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)\n  }\n  else if (particlesize == 0)\n    particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; // single pixel particles have half the radius (i.e. 1/2 pixel)\n}\n\n// enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable\n// if enabled, gravity is applied to all particles in ParticleSystemUpdate()\n// 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)\nvoid ParticleSystem1D::setGravity(const int8_t force) {\n  if (force) {\n    gforce = force;\n    particlesettings.useGravity = true;\n  }\n  else\n    particlesettings.useGravity = false;\n}\n\nvoid ParticleSystem1D::enableParticleCollisions(const bool enable, const uint8_t hardness) {\n  particlesettings.useCollisions = enable;\n  collisionHardness = hardness;\n}\n\n// emit one particle with variation, returns index of last emitted particle (or -1 if no particle emitted)\nint32_t ParticleSystem1D::sprayEmit(const PSsource1D &emitter) {\n  for (uint32_t i = 0; i < usedParticles; i++) {\n    emitIndex++;\n    if (emitIndex >= usedParticles)\n      emitIndex = 0;\n    if (particles[emitIndex].ttl == 0) { // find a dead particle\n      particles[emitIndex].vx = emitter.v + hw_random16(emitter.var << 1) - emitter.var; // random(-var,var)\n      particles[emitIndex].x = emitter.source.x;\n      particles[emitIndex].hue = emitter.source.hue;\n      particles[emitIndex].ttl = hw_random16(emitter.minLife, emitter.maxLife);\n      particleFlags[emitIndex].collide = emitter.sourceFlags.collide; // TODO: could just set all flags (asByte) but need to check if that breaks any of the FX\n      particleFlags[emitIndex].reversegrav = emitter.sourceFlags.reversegrav;\n      particleFlags[emitIndex].perpetual = emitter.sourceFlags.perpetual;\n      if (advPartProps) {\n        advPartProps[emitIndex].sat = emitter.sat;\n        advPartProps[emitIndex].size = emitter.size;\n      }\n      return emitIndex;\n    }\n  }\n  return -1;\n}\n\n// particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0\n// 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\nvoid ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSparticleFlags1D &partFlags, PSsettings1D *options, PSadvancedParticle1D *advancedproperties) {\n  if (options == nullptr)\n    options = &particlesettings; // use PS system settings by default\n\n  if (part.ttl > 0) {\n    if (!partFlags.perpetual)\n      part.ttl--; // age\n    if (options->colorByAge)\n      part.hue = min(part.ttl, (uint16_t)255); // set color to ttl\n\n    int32_t renderradius = PS_P_HALFRADIUS_1D - 1 + particlesize; // used to check out of bounds, default for 2 pixel rendering\n    int32_t newX = part.x + (int32_t)part.vx;\n    partFlags.outofbounds = false; // reset out of bounds (in case particle was created outside the matrix and is now moving into view)\n\n    if (perParticleSize && advancedproperties != nullptr) { // using individual particle size?\n      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\n      if (advancedproperties->size > 1)\n        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)\n      else // single pixel particles use half the collision distance for walls\n        particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1;\n    }\n\n    // 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\n    if (options->bounce) {\n      if ((newX < (int32_t)particleHardRadius) || ((newX > (int32_t)(maxX - particleHardRadius)))) { // reached a wall\n        bool bouncethis = true;\n        if (options->useGravity) {\n          if (partFlags.reversegrav) { // skip bouncing at x = 0\n            if (newX < (int32_t)particleHardRadius)\n              bouncethis = false;\n          } else if (newX > (int32_t)particleHardRadius) { // skip bouncing at x = max\n            bouncethis = false;\n          }\n        }\n        if (bouncethis) {\n          part.vx = -part.vx; // invert speed\n          part.vx = ((int32_t)part.vx * (int32_t)wallHardness) / 255; // reduce speed as energy is lost on non-hard surface\n          if (newX < (int32_t)particleHardRadius)\n            newX = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better\n          else\n            newX = maxX - particleHardRadius;\n        }\n      }\n    }\n\n    if (!checkBoundsAndWrap(newX, maxX, renderradius, options->wrap)) { // check out of bounds note: this must not be skipped or it can lead to crashes\n      partFlags.outofbounds = true;\n      if (options->killoutofbounds) {\n        bool killthis = true;\n        if (options->useGravity) { // if gravity is used, only kill below 'floor level'\n          if (partFlags.reversegrav) { // skip at x = 0, do not skip far out of bounds\n            if (newX < 0 || newX > maxX << 2)\n              killthis = false;\n          } else { // skip at x = max, do not skip far out of bounds\n            if (newX > 0 &&  newX < maxX << 2)\n              killthis = false;\n          }\n        }\n        if (killthis)\n          part.ttl = 0;\n      }\n    }\n\n    if (!partFlags.fixed)\n      part.x = newX; // set new position\n    else\n      part.vx = 0; // set speed to zero. note: particle can get speed in collisions, if unfixed, it should not speed away\n  }\n}\n\n// apply a force in x direction to individual particle (or source)\n// caller needs to provide a 8bit counter (for each paticle) that holds its value between calls\n// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame\nvoid ParticleSystem1D::applyForce(PSparticle1D &part, const int8_t xforce, uint8_t &counter) {\n  int32_t dv = calcForce_dv(xforce, counter); // velocity increase\n  part.vx = limitSpeed((int32_t)part.vx + dv);   // apply the force to particle\n}\n\n// apply a force to all particles\n// force is in 3.4 fixed point notation (see above)\nvoid ParticleSystem1D::applyForce(const int8_t xforce) {\n  int32_t dv = calcForce_dv(xforce, forcecounter); // velocity increase\n  for (uint32_t i = 0; i < usedParticles; i++) {\n    particles[i].vx = limitSpeed((int32_t)particles[i].vx + dv);\n  }\n}\n\n// apply gravity to all particles using PS global gforce setting\n// gforce is in 3.4 fixed point notation, see note above\nvoid ParticleSystem1D::applyGravity() {\n  int32_t dv_raw = calcForce_dv(gforce, gforcecounter);\n  for (uint32_t i = 0; i < usedParticles; i++) {\n    int32_t dv = dv_raw;\n    if (particleFlags[i].reversegrav) dv = -dv_raw;\n    // note: not checking if particle is dead is omitted as most are usually alive and if few are alive, rendering is fast anyways\n    particles[i].vx = limitSpeed((int32_t)particles[i].vx - dv);\n  }\n}\n\n// apply gravity to single particle using system settings (use this for sources)\n// function does not increment gravity counter, if gravity setting is disabled, this cannot be used\nvoid ParticleSystem1D::applyGravity(PSparticle1D &part, PSparticleFlags1D &partFlags) {\n  uint32_t counterbkp = gforcecounter;\n  int32_t dv = calcForce_dv(gforce, gforcecounter);\n  if (partFlags.reversegrav) dv = -dv;\n  gforcecounter = counterbkp; //save it back\n  part.vx = limitSpeed((int32_t)part.vx - dv);\n}\n\n\n// slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop)\n// 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\nvoid ParticleSystem1D::applyFriction(int32_t coefficient) {\n  #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster)\n  int32_t friction = 256 - coefficient;\n  for (uint32_t i = 0; i < usedParticles; i++) {\n    if (particles[i].ttl)\n      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\n  }\n  #else // division is faster on ESP32, S2 and S3\n  int32_t friction = 255 - coefficient;\n  for (uint32_t i = 0; i < usedParticles; i++) {\n    if (particles[i].ttl)\n      particles[i].vx = ((int32_t)particles[i].vx * friction) / 255;\n  }\n  #endif\n}\n\n\n// render particles to the LED buffer (uses palette to render the 8bit particle color value)\n// if wrap is set, particles half out of bounds are rendered to the other side of the matrix\n// warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds\nvoid ParticleSystem1D::render() {\n  if (framebuffer == nullptr) {\n    PSPRINTLN(F(\"PS render: no framebuffer!\"));\n    return;\n  }\n  CRGBW baseRGB;\n  uint32_t brightness; // particle brightness, fades if dying\n  TBlendType blend = LINEARBLEND; // default color rendering: wrap palette\n  if (particlesettings.colorByAge || particlesettings.colorByPosition) {\n    blend = LINEARBLEND_NOWRAP;\n  }\n\n  if (motionBlur) { // blurring active\n    for (int32_t x = 0; x <= maxXpixel; x++) {\n      framebuffer[x] = fast_color_scale(framebuffer[x], motionBlur);\n    }\n  }\n  else { // no blurring: clear buffer\n    memset(framebuffer, 0, (maxXpixel+1) * sizeof(CRGBW));\n  }\n\n  // go over particles and render them to the buffer\n  for (uint32_t i = 0; i < usedParticles; i++) {\n    if ( particles[i].ttl == 0 || particleFlags[i].outofbounds)\n      continue;\n\n    // generate RGB values for particle\n    brightness = min(particles[i].ttl << 1, (int)255);\n    baseRGB = ColorFromPaletteWLED(SEGPALETTE, particles[i].hue, 255, blend);\n\n    if (advPartProps != nullptr) { //saturation is advanced property in 1D system\n      if (advPartProps[i].sat < 255) {\n        CHSV32 baseHSV;\n        rgb2hsv(baseRGB.color32, baseHSV); // convert to HSV\n        baseHSV.s = min(baseHSV.s, advPartProps[i].sat); // set the saturation but don't increase it\n        hsv2rgb(baseHSV, baseRGB.color32); // convert back to RGB\n      }\n    }\n    if (gammaCorrectCol) brightness = gamma8(brightness); // apply gamma correction, used for gamma-inverted brightness distribution\n    renderParticle(i, brightness, baseRGB, particlesettings.wrap);\n  }\n  // apply smear-blur to rendered frame\n  if (smearBlur) {\n    SEGMENT.blur(smearBlur, true);\n  }\n\n  // add background color\n  CRGBW bg_color = SEGCOLOR(1);\n  if (bg_color > 0) { //if not black\n    for (int32_t i = 0; i <= maxXpixel; i++) {\n      framebuffer[i] = fast_color_scaleAdd(framebuffer[i], bg_color);\n    }\n  }\n#ifndef WLED_DISABLE_2D\n  // transfer local buffer to segment if using 1D->2D mapping\n  if (SEGMENT.is2D() && SEGMENT.map1D2D) {\n    for (int x = 0; x <= maxXpixel; x++) {\n    //for (int x = 0; x < SEGMENT.vLength(); x++) {\n      SEGMENT.setPixelColor(x, framebuffer[x]); // this applies the mapping\n    }\n  }\n#endif\n}\n\n// calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer\nvoid WLED_O2_ATTR ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW &color, const bool wrap) {\n  uint32_t size = particlesize;\n  if (perParticleSize && advPartProps != nullptr) // use advanced size properties\n    size = 1 + advPartProps[particleindex].size; // add 1 to avoid single pixel size particles (collisions do not support it)\n\n  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)\n    uint32_t x =  particles[particleindex].x >> PS_P_RADIUS_SHIFT_1D;\n    if (x <= (uint32_t)maxXpixel) { //by making x unsigned there is no need to check < 0 as it will overflow\n      framebuffer[x] = fast_color_scaleAdd(framebuffer[x], color, brightness);\n    }\n    return;\n  }\n  //render larger particles\n  if (size > 1) { // size > 1: render as gradient line\n    renderLargeParticle(size, particleindex, brightness, color, wrap); // larger size rendering\n    return;\n  }\n\n  // standard rendering (2 pixels per particle)\n  bool pxlisinframe[2] = {true, true};\n  int32_t pxlbrightness[2];\n  int32_t pixco[2]; // physical pixel coordinates of the two pixels representing a particle\n\n  // 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)\n  int32_t xoffset = particles[particleindex].x + PS_P_HALFRADIUS_1D;\n  int32_t dx = xoffset & (PS_P_RADIUS_1D - 1); //relativ particle position in subpixel space,  modulo replaced with bitwise AND\n  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)\n\n  // set the raw pixel coordinates\n  pixco[1] = x;  // right pixel\n  x--; // shift by a full pixel here, this is skipped above to not do -1 and then +1\n  pixco[0] = x;  // left pixel\n\n  //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)\n  pxlbrightness[0] = (((int32_t)PS_P_RADIUS_1D - dx) * brightness) >> PS_P_SURFACE_1D;\n  pxlbrightness[1] = (dx * brightness) >> PS_P_SURFACE_1D;\n  // adjust brightness such that distribution is linear after gamma correction:\n  // - scale brigthness with gamma correction (done in render())\n  // - apply inverse gamma correction to brightness values\n  // - gamma is applied again in show() -> the resulting brightness distribution is linear but gamma corrected in total -> fixes brightness fluctuations\n  if (gammaCorrectCol) {\n    pxlbrightness[0] = gamma8inv(pxlbrightness[0]); // use look-up-table for invers gamma\n    pxlbrightness[1] = gamma8inv(pxlbrightness[1]);\n  }\n\n  // check if any pixels are out of frame\n  if (pixco[0] < 0) { // left pixels out of frame\n    if (wrap) // wrap x to the other side if required\n      pixco[0] = maxXpixel;\n    else {\n      pxlisinframe[0] = false; // pixel is out of matrix boundaries, do not render\n      if (pixco[0] < -1)\n        return; // both pixels out of frame (safety check)\n    }\n  }\n  else if (pixco[1] > (int32_t)maxXpixel) { // right pixel, only has to be checkt if left pixel did not overflow\n    if (wrap) // wrap y to the other side if required\n      pixco[1] = 0;\n    else {\n      pxlisinframe[1] = false;\n      if (pixco[0] > (int32_t)maxXpixel)\n        return; // both pixels out of frame (safety check)\n    }\n  }\n  for (uint32_t i = 0; i < 2; i++) {\n    if (pxlisinframe[i]) {\n      framebuffer[pixco[i]] = fast_color_scaleAdd(framebuffer[pixco[i]], color, pxlbrightness[i]);\n    }\n  }\n}\n\n// render particle as a line with linear brightness falloff and sub-pixel precision, size is in 0-255 (1-9 pixel radius)\nvoid WLED_O2_ATTR ParticleSystem1D::renderLargeParticle(const uint32_t size, const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrap) {\n  int32_t x_subcenter = particles[particleindex].x; // particle position in sub-pixel space\n\n  // sub-pixel offset (0-31)\n  int32_t x_offset = x_subcenter & (PS_P_RADIUS_1D - 1); // same as modulo PS_P_RADIUS but faster\n  int32_t x_center = x_subcenter >> PS_P_RADIUS_SHIFT_1D; // integer pixel position, this is rounded down\n\n  // particle radius in pixels, size = 1 means radius of just over 1 pixel\n  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)\n  // rendering bounding box in pixels\n  int32_t r_pixels = r_subpixel >> PS_P_RADIUS_SHIFT_1D;\n\n  int32_t x_min = x_center - r_pixels - 1; // extend by one for much smoother movement\n  int32_t x_max = x_center + r_pixels + 1;\n\n  // cache for speed\n  uint32_t matrixX = maxXpixel + 1;\n\n  // iterate over bounding box and render each pixel\n  for (int32_t px = x_min; px <= x_max; px++) {\n    // Check bounds and apply wrapping\n    int32_t render_x = px;\n    if (render_x < 0) {\n      if (!wrap) continue; // skip out of frame pixels\n      render_x += matrixX;\n    } else if (render_x > maxXpixel) {\n      if (!wrap) continue;\n      render_x -= matrixX;\n    }\n    // squared distance from particle center\n    int32_t dx_sq = ((px << PS_P_RADIUS_SHIFT_1D) - x_subcenter + PS_P_HALFRADIUS_1D); // explanation see 2D version\n    dx_sq = dx_sq * dx_sq;\n    int32_t rx_sq = r_subpixel * r_subpixel;\n    uint32_t dist_sq = (dx_sq << 8) / rx_sq; // normalized squared distance in fixed point (0-256)\n\n    // calculate brightness based on distance from particle center with linear falloff\n    uint8_t pixel_brightness = dist_sq >= 256 ? 0 : ((256 - dist_sq) * brightness) >> 8;\n    //if (pixel_brightness == 0) continue; // skip black pixels note: very few pixels will be black, skipping this is usually faster\n\n    // Render pixel\n    framebuffer[render_x] = fast_color_scaleAdd(framebuffer[render_x], color, pixel_brightness);\n  }\n}\n\n// detect collisions in an array of particles and handle them\nvoid ParticleSystem1D::handleCollisions() {\n  uint32_t collisiondistance = particleHardRadius << 1; // twice the radius is min distance between colliding particles\n  uint32_t checkDistSq = max(2 * PS_P_MAXSPEED, (int)collisiondistance);\n  if (perParticleSize && advPartProps != nullptr) // using individual particle size\n    checkDistSq = max(2 * PS_P_MAXSPEED, (512 * 52) >> 6); // max possible collision distance that catches all collisons\n  checkDistSq = checkDistSq * checkDistSq; // square it for distance comparison (faster than abs() )\n  // note: partices are binned by position, assumption is that no more than half of the particles are in the same bin\n  // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions)\n  int binWidth = 64 * PS_P_RADIUS_1D; // width of each bin, a compromise between speed and accuracy\n  int32_t overlap = collisiondistance + (2 * PS_P_MAXSPEED); // overlap bins to include edge particles to neighbouring bins (+ look-ahead of speed)\n  if (perParticleSize && advPartProps != nullptr) //may be using individual particle size\n    overlap = 512; // 2 * max radius, enough to catch all collisions even at full speed\n  uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 4); // do not bin small amounts, limit max to 1/4 of particles\n  uint32_t numBins = (maxX + (binWidth - 1)) / binWidth; // calculate number of bins\n  if (usedParticles < maxBinParticles) {\n    numBins = 1; // use single bin for small number of particles\n    binWidth = maxX + 1;\n  }\n  uint16_t binIndices[maxBinParticles]; // array to store indices of particles in a bin\n  uint32_t binParticleCount; // number of particles in the current bin\n  uint32_t nextFrameStartIdx = hw_random16(usedParticles); // index of the first particle in the next frame (set to fixed value if bin overflow)\n  uint32_t pidx = collisionStartIdx; //start index in case a bin is full, process remaining particles next frame\n  for (uint32_t bin = 0; bin < numBins; bin++) {\n    binParticleCount = 0; // reset for this bin\n    int32_t binStart = bin * binWidth - overlap; // note: first bin will extend to negative, but that is ok as out of bounds particles are ignored\n    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\n\n    // fill the binIndices array for this bin\n    for (uint32_t i = 0; i < usedParticles; i++) {\n      if (particles[pidx].ttl > 0) { // alivee\n        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)\n          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\n            if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame\n              nextFrameStartIdx = pidx; // bin overflow can only happen once as bin size is at least half of the particles (or half +1)\n              break;\n            }\n            binIndices[binParticleCount++] = pidx;\n          }\n        }\n      }\n      pidx++;\n      if (pidx >= usedParticles) pidx = 0; // wrap around\n    }\n\n    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\n      uint32_t idx_i = binIndices[i];\n      for (uint32_t j = i + 1; j < binParticleCount; j++) { // check against higher number particles\n        uint32_t idx_j = binIndices[j];\n        int32_t dx = particles[idx_j].x - particles[idx_i].x; // distance between particles\n        uint32_t dx_sq = dx * dx; // square distance (faster than abs() and works the same)\n        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\n          collideParticles(idx_i, idx_j, dx, collisiondistance); // handle the collision\n        }\n      }\n    }\n  }\n  collisionStartIdx = nextFrameStartIdx; // set the start index for the next frame\n}\n// handle a collision if close proximity is detected, i.e. dx smaller than 2*radius + speed look-ahead\nvoid WLED_O2_ATTR ParticleSystem1D::collideParticles(uint32_t partIdx1, uint32_t partIdx2, int32_t dx, uint32_t collisiondistance) {\n  int32_t massratio1 = 0; // 0 means dont use mass ratio (equal mass)\n  int32_t massratio2 = 0;\n  if (perParticleSize && advPartProps != nullptr) { // use advanced size properties, calculate collision distance and mass ratio\n    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)\n    // calculate mass ratio for collision response\n    uint32_t mass1 = PS_P_RADIUS_1D + advPartProps[partIdx1].size;\n    uint32_t mass2 = PS_P_RADIUS_1D + advPartProps[partIdx2].size;\n    uint32_t totalmass = mass1 + mass2 - 2; // -2 to account for rounding\n    massratio1 = (mass2 << 8) / totalmass; // massratio 1 depends on mass of particle 2, i.e. if 2 is heavier -> higher velocity impact on 1\n    massratio2 = (mass1 << 8) / totalmass;\n  }\n  int32_t dv = (int)particles[partIdx2].vx - (int)particles[partIdx1].vx;\n  int32_t absdv = abs(dv);\n  int32_t dotProduct = (dx * dv); // is always negative if moving towards each other\n  uint32_t dx_abs = abs(dx);\n\n  if (dotProduct < 0) { // particles are moving towards each other\n    uint32_t lookaheadDistance = collisiondistance + absdv; // add look-ahead: if reaching collisiondistance in this frame, collide\n    if (dx_abs <= lookaheadDistance) {\n      // 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\n      if (particleFlags[partIdx1].fixed) {\n        particles[partIdx2].vx = -(particles[partIdx2].vx * collisionHardness) / 255;\n        particles[partIdx2].x = particles[partIdx1].x + (dx < 0 ? -collisiondistance : collisiondistance); // dv < 0 means particle2.x < particle1.x\n        return;\n      }\n      else if (particleFlags[partIdx2].fixed) {\n        particles[partIdx1].vx = -(particles[partIdx1].vx * collisionHardness) / 255;\n        particles[partIdx1].x = particles[partIdx2].x + (dx < 0 ? collisiondistance : -collisiondistance);\n        return;\n      }\n      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\n      // Calculate new velocities after collision  note: not using dot product like in 2D as impulse is purely speed depnedent\n      #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster)\n      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\n      #else // division is faster on ESP32, S2 and S3\n      int32_t impulse = (dv * surfacehardness) / 255;\n      #endif\n\n      // if particles are not the same size, use a mass ratio. mass ratio is set to 0 if particles are the same size\n      if (massratio1) {\n        int vx1 = (int)particles[partIdx1].vx + ((impulse * massratio1) >> 7); // mass ratio is in fixed point 8bit\n        int vx2 = (int)particles[partIdx2].vx - ((impulse * massratio2) >> 7);\n        // limit speeds to max speed (required as a lot of impulse can be transferred from a large to a small particle)\n        particles[partIdx1].vx = limitSpeed(vx1);\n        particles[partIdx2].vx = limitSpeed(vx2);\n      }\n      else {\n        particles[partIdx1].vx += impulse;\n        particles[partIdx2].vx -= impulse;\n      }\n\n      if (collisionHardness < PS_P_MINSURFACEHARDNESS_1D && (SEGMENT.call & 0x07) == 0) { // if particles are soft, they become 'sticky' i.e. apply some friction\n        const uint32_t coeff = collisionHardness + (250 - PS_P_MINSURFACEHARDNESS_1D);\n        #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster)\n        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\n        particles[partIdx2].vx = ((int32_t)particles[partIdx2].vx * coeff + (((int32_t)particles[partIdx2].vx >> 31) & 0xFF)) >> 8;\n        #else // division is faster on ESP32, S2 and S3\n        particles[partIdx1].vx = ((int32_t)particles[partIdx1].vx * coeff) / 255;\n        particles[partIdx2].vx = ((int32_t)particles[partIdx2].vx * coeff) / 255;\n        #endif\n      }\n    } else {\n      return; // not close enough yet\n    }\n  }\n  // particles have volume, push particles apart if they are too close\n  // note: like in 2D, pushing by a distance makes softer piles collapse, giving particles speed prevents that and looks nicer\n\n  if (dx_abs < collisiondistance) { // too close, force push particles so they dont collapse\n    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\n    int32_t addspeed = 1;\n    if (dx < 0) {  // particle2.x < particle1.x\n      pushamount = -pushamount;\n      addspeed = -addspeed;\n    }\n    if (absdv < 4) { // low relative speed, add speed to help with the pushing (less collapsing piles)\n      particles[partIdx1].vx -= addspeed;\n      particles[partIdx2].vx += addspeed;\n    }\n    // push only one particle to avoid oscillations\n    bool fairlyrandom = dotProduct & 0x01;\n    if (fairlyrandom) {\n      particles[partIdx1].x -= pushamount;\n    }\n    else {\n      particles[partIdx2].x += pushamount;\n    }\n  }\n}\n\n// update size and pointers (memory location and size can change dynamically)\n// note: do not access the PS class in FX befor running this function (or it messes up SEGENV.data)\nvoid ParticleSystem1D::updateSystem(void) {\n  setSize(SEGMENT.vLength()); // update size\n  updatePSpointers(advPartProps != nullptr);\n}\n\n// 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)\n// 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)\n// FX handles the PSsources, need to tell this function how many there are\nvoid ParticleSystem1D::updatePSpointers(bool isadvanced) {\n  // Note on memory alignment:\n  // 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.\n  // 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.\n  // 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.\n  particles = reinterpret_cast<PSparticle1D *>(this + 1); // pointer to particles\n  particleFlags = reinterpret_cast<PSparticleFlags1D *>(particles + numParticles); // pointer to particle flags\n  sources = reinterpret_cast<PSsource1D *>(particleFlags + numParticles); // pointer to source(s)\n  PSdataEnd = reinterpret_cast<uint8_t *>(sources + numSources);   // pointer to first available byte after the PS for FX additional data (already aligned to 4 byte boundary)\n#ifndef WLED_DISABLE_2D\n  if (SEGMENT.is2D() && SEGMENT.map1D2D) {\n    framebuffer = reinterpret_cast<uint32_t *>(sources + numSources); // use local framebuffer for 1D->2D mapping\n    PSdataEnd = reinterpret_cast<uint8_t *>(framebuffer + SEGMENT.maxMappingLength()); // pointer to first available byte after the PS for FX additional data (still aligned to 4 byte boundary)\n  }\n  else\n#endif\n    framebuffer = SEGMENT.getPixels();  // use segment buffer for standard 1D rendering\n\n  if (isadvanced) {\n    advPartProps = reinterpret_cast<PSadvancedParticle1D *>(PSdataEnd);\n    PSdataEnd = reinterpret_cast<uint8_t *>(advPartProps + numParticles); // since numParticles is a multiple of 4, this is always aligned to 4 bytes. No need to add padding bytes here\n  }\n  #ifdef WLED_DEBUG_PS\n  PSPRINTLN(\" PS Pointers: \");\n  PSPRINT(\" PS : 0x\");\n  Serial.println((uintptr_t)this, HEX);\n  PSPRINT(\" Particleflags : 0x\");\n  Serial.println((uintptr_t)particleFlags, HEX);\n  PSPRINT(\" Particles : 0x\");\n  Serial.println((uintptr_t)particles, HEX);\n  PSPRINT(\" Sources : 0x\");\n  Serial.println((uintptr_t)sources, HEX);\n  #endif\n}\n\n//non class functions to use for initialization, fraction is uint8_t: 255 means 100%\nuint32_t calculateNumberOfParticles1D(const uint32_t fraction, const bool isadvanced) {\n  uint32_t numberofParticles = SEGMENT.virtualLength();  // one particle per pixel (if possible)\n  uint32_t particlelimit = MAXPARTICLES_1D; // maximum number of paticles allowed\n  numberofParticles = min(numberofParticles, particlelimit); // limit to particlelimit\n  if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount\n    numberofParticles = (numberofParticles * sizeof(PSparticle1D)) / (sizeof(PSparticle1D) + sizeof(PSadvancedParticle1D));\n  numberofParticles = (numberofParticles * (fraction + 1)) >> 8; // calculate fraction of particles\n  numberofParticles = numberofParticles < 10 ? 10 : numberofParticles; // 10 minimum\n  //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes)\n  numberofParticles = (numberofParticles+3) & ~0x03; // note: with a separate particle buffer, this is probably unnecessary\n  PSPRINTLN(\" calc numparticles:\" + String(numberofParticles));\n  return numberofParticles;\n}\n\nuint32_t calculateNumberOfSources1D(const uint32_t requestedsources) {\n  int numberofSources = max(1, min((int)requestedsources,MAXSOURCES_1D)); // limit\n  // make sure it is a multiple of 4 for proper memory alignment (so minimum is acutally 4)\n  numberofSources = (numberofSources+3) & ~0x03;\n  return numberofSources;\n}\n\n//allocate memory for particle system class, particles, sprays plus additional memory requested by FX\nbool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t numsources, const bool isadvanced, const uint32_t additionalbytes) {\n  uint32_t requiredmemory = sizeof(ParticleSystem1D);\n  // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues)\n  requiredmemory += sizeof(PSparticleFlags1D) * numparticles;\n  requiredmemory += sizeof(PSparticle1D) * numparticles;\n  requiredmemory += sizeof(PSsource1D) * numsources;\n#ifndef WLED_DISABLE_2D\n  if (SEGMENT.is2D())\n    requiredmemory += sizeof(uint32_t) * SEGMENT.maxMappingLength(); // need local buffer for mapped rendering\n#endif\n  requiredmemory += additionalbytes;\n  if (isadvanced)\n    requiredmemory += sizeof(PSadvancedParticle1D) * numparticles;\n  return(SEGMENT.allocateData(requiredmemory));\n}\n\n// initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd)\n// 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)\nbool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedsources, const uint8_t fractionofparticles, const uint32_t additionalbytes, const bool advanced) {\n  if (SEGLEN == 1) {\n    errorFlag = ERR_NOT_IMPL; // TODO: need a better error code if more codes are added\n    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)\n    return false; // single pixel not supported\n  }\n  uint32_t numparticles = calculateNumberOfParticles1D(fractionofparticles, advanced);\n  uint32_t numsources = calculateNumberOfSources1D(requestedsources);\n  bool allocsuccess = false;\n  while(numparticles >= 10) { // make sure we have at least 10 particles or quit\n    if (allocateParticleSystemMemory1D(numparticles, numsources, advanced, additionalbytes)) {\n      PSPRINT(F(\"PS 1D alloc succeeded\"));\n      allocsuccess = true;\n      break; // allocation succeeded\n    }\n    numparticles = ((numparticles / 2) + 3) & ~0x03; // cut number of particles in half and try again, must be 4 byte aligned\n    PSPRINTLN(F(\"PS 1D alloc failed, trying with less particles...\"));\n  }\n  if (!allocsuccess) {\n    PSPRINTLN(F(\"PS init failed: memory depleted\"));\n    return false; // allocation failed\n  }\n  PartSys = new (SEGENV.data) ParticleSystem1D(SEGMENT.virtualLength(), numparticles, numsources, advanced); // particle system constructor\n  return true;\n}\n#endif // WLED_DISABLE_PARTICLESYSTEM1D\n\n#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled\n\n//////////////////////////////\n// Shared Utility Functions //\n//////////////////////////////\n\n// calculate the delta speed (dV) value and update the counter for force calculation (is used several times, function saves on codesize)\n// force is in 3.4 fixedpoint notation, +/-127\nstatic int32_t calcForce_dv(const int8_t force, uint8_t &counter) {\n  if (force == 0)\n    return 0;\n  // for small forces, need to use a delay counter\n  int32_t force_abs = abs(force); // absolute value (faster than lots of if's only 7 instructions)\n  int32_t dv = 0;\n  // for small forces, need to use a delay counter, apply force only if it overflows\n  if (force_abs < 16) {\n    counter += force_abs;\n    if (counter > 15) {\n      counter -= 16;\n      dv = force < 0 ? -1 : 1; // force is either 1 or -1 if it is small (zero force is handled above)\n    }\n  }\n  else\n    dv = force / 16; // MSBs, note: cannot use bitshift as dv can be negative\n\n  return dv;\n}\n\n// check if particle is out of bounds and wrap it around if required, returns false if out of bounds\nstatic bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, const bool wrap) {\n  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)\n    if (wrap) {\n      position = position % (max + 1); // note: cannot optimize modulo, particles can be far out of bounds when wrap is enabled\n      if (position < 0)\n        position += max + 1;\n    }\n    else if (((position < -particleradius) || (position > max + particleradius))) // particle is leaving boundaries, out of bounds if it has fully left\n      return false; // out of bounds\n  }\n  return true; // particle is in bounds\n}\n\n// this is a fast version for RGB color adding ignoring white channel (PS does not handle white) including scaling of second color\n// note: function is mainly used to add scaled colors, so checking if one color is black is slower\nstatic uint32_t fast_color_scaleAdd(const uint32_t c1, const uint32_t c2, const uint8_t scale) {\n    constexpr uint32_t MASK_RB = 0x00FF00FF;  // red and blue mask\n    constexpr uint32_t MASK_G  = 0x0000FF00;  // green mask\n\n    uint32_t rb = c2 & MASK_RB; // 0x00RR00BB\n    uint32_t g  = c2 & MASK_G;  // 0x0000GG00\n    // scale second color\n    rb = ((rb * scale) >> 8) & MASK_RB;\n    g  = ((g  * scale) >> 8) & MASK_G;\n    // add colors\n    rb = (c1 & MASK_RB) + rb;\n    g = ((c1 & MASK_G)  + g);\n\n    // check for overflow by looking at the 9th bit of each channel\n    if ((rb | (g >> 8)) & 0x01000100) {\n        // find max among the three 16-bit values\n        g = g >> 8; // shift to get 0x000000GG\n        uint32_t max_val = (rb >> 16); // red\n        max_val = ((rb & 0xFFFF) > max_val) ? rb & 0xFFFF : max_val;  // blue\n        max_val = (g > max_val) ? g : max_val; // green\n        // scale down to avoid saturation\n        uint32_t scale_factor = (255 << 8) / max_val;\n        rb = ((rb * scale_factor) >> 8) & MASK_RB;\n        g = (g * scale_factor) & MASK_G;\n    }\n    return rb | g;\n}\n\n#endif  // !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D))\n"
  },
  {
    "path": "wled00/FXparticleSystem.h",
    "content": "/*\n  FXparticleSystem.cpp\n\n  Particle system with functions for particle generation, particle movement and particle rendering to RGB matrix.\n  by DedeHai (Damian Schneider) 2013-2024\n\n  Copyright (c) 2024  Damian Schneider\n  Licensed under the EUPL v. 1.2 or later\n*/\n\n#ifdef WLED_DISABLE_2D\n#define WLED_DISABLE_PARTICLESYSTEM2D\n#endif\n\n#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled\n\n#include <stdint.h>\n#include \"wled.h\"\n\n#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\n#define MAX_MEMIDLE 10 // max idle time (in frames) before memory is deallocated (if deallocated during an effect, it will crash!)\n\n//#define WLED_DEBUG_PS // note: enabling debug uses ~3k of flash\n\n#ifdef WLED_DEBUG_PS\n  #define PSPRINT(x) Serial.print(x)\n  #define PSPRINTLN(x) Serial.println(x)\n#else\n  #define PSPRINT(x)\n  #define PSPRINTLN(x)\n#endif\n\n// limit speed of particles (used in 1D and 2D)\nstatic inline int32_t limitSpeed(const int32_t speed) {\n  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\n}\n#endif\n\n#ifndef WLED_DISABLE_PARTICLESYSTEM2D\n// memory allocation (based on reasonable segment size and available FX memory)\n#ifdef ESP8266\n  #define MAXPARTICLES_2D 256\n  #define MAXSOURCES_2D 24\n  #define SOURCEREDUCTIONFACTOR 8\n#elif ARDUINO_ARCH_ESP32S2\n  #define MAXPARTICLES_2D 1024\n  #define MAXSOURCES_2D 64\n  #define SOURCEREDUCTIONFACTOR 6\n#else\n  #define MAXPARTICLES_2D 2048\n  #define MAXSOURCES_2D 128\n  #define SOURCEREDUCTIONFACTOR 4\n#endif\n\n// particle dimensions (subpixel division)\n#define PS_P_RADIUS 64 // subpixel size, each pixel is divided by this for particle movement (must be a power of 2)\n#define PS_P_HALFRADIUS (PS_P_RADIUS >> 1)\n#define PS_P_RADIUS_SHIFT 6 // shift for RADIUS\n#define PS_P_SURFACE 12 // shift: 2^PS_P_SURFACE = (PS_P_RADIUS)^2\n#define PS_P_MINHARDRADIUS 64 // minimum hard surface radius for collisions\n#define PS_P_MINSURFACEHARDNESS 128 // minimum hardness used in collision impulse calculation, below this hardness, particles become sticky\n\n// struct for PS settings (shared for 1D and 2D class)\ntypedef union {\n  struct{ // one byte bit field for 2D settings\n    bool wrapX : 1;\n    bool wrapY : 1;\n    bool bounceX : 1;\n    bool bounceY : 1;\n    bool killoutofbounds : 1; // if set, out of bound particles are killed immediately\n    bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top\n    bool useCollisions : 1;\n    bool colorByAge : 1; // if set, particle hue is set by ttl value in render function\n  };\n  byte asByte; // access as a byte, order is: LSB is first entry in the list above\n} PSsettings2D;\n\n//struct for a single particle\ntypedef struct { // 10 bytes\n  int16_t x;  // x position in particle system\n  int16_t y;  // y position in particle system\n  uint16_t ttl; // time to live in frames\n  int8_t vx;  // horizontal velocity\n  int8_t vy;  // vertical velocity\n  uint8_t hue;  // color hue\n  uint8_t sat; // particle color saturation\n} PSparticle;\n\n//struct for particle flags note: this is separate from the particle struct to save memory (ram alignment)\ntypedef union {\n  struct { // 1 byte\n    bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area\n    bool collide : 1; // if set, particle takes part in collisions\n    bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds)\n    bool custom1 : 1; // unused custom flags, can be used by FX to track particle states\n    bool custom2 : 1;\n    bool custom3 : 1;\n    bool custom4 : 1;\n    bool custom5 : 1;\n  };\n  byte asByte; // access as a byte, order is: LSB is first entry in the list above\n} PSparticleFlags;\n\n// struct for additional particle settings (option)\ntypedef struct { // 2 bytes\n  uint8_t size; // particle size, 255 means 10 pixels in diameter, set perParticleSize = true to enable\n  uint8_t forcecounter; // counter for applying forces to individual particles\n} PSadvancedParticle;\n\n// struct for advanced particle size control (option)\ntypedef struct { // 8 bytes\n  uint8_t asymmetry; // asymmetrical size (0=symmetrical, 255 fully asymmetric)\n  uint8_t asymdir; // direction of asymmetry, 64 is x, 192 is y (0 and 128 is symmetrical)\n  uint8_t maxsize; // target size for growing\n  uint8_t minsize; // target size for shrinking\n  uint8_t sizecounter : 4; // counters used for size contol (grow/shrink/wobble)\n  uint8_t wobblecounter : 4;\n  uint8_t growspeed : 4;\n  uint8_t shrinkspeed : 4;\n  uint8_t wobblespeed : 4;\n  bool grow : 1; // flags\n  bool shrink : 1;\n  bool pulsate : 1; // grows & shrinks & grows & ...\n  bool wobble : 1; // alternate x and y size\n} PSsizeControl;\n\n\n//struct for a particle source (20 bytes)\ntypedef struct {\n  uint16_t minLife; // minimum ttl of emittet particles\n  uint16_t maxLife; // maximum ttl of emitted particles\n  PSparticle source; // use a particle as the emitter source (speed, position, color)\n  PSparticleFlags sourceFlags; // flags for the source particle\n  int8_t var; // variation of emitted speed (adds random(+/- var) to speed)\n  int8_t vx; // emitting speed\n  int8_t vy;\n  uint8_t size; // particle size (advanced property), global size is added on top to this size\n} PSsource;\n\n// class uses approximately 60 bytes\nclass ParticleSystem2D {\npublic:\n  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\n  // note: memory is allcated in the FX function, no deconstructor needed\n  void update(void); //update the particles according to set options and render to the matrix\n  void updateFire(const uint8_t intensity); // update function for fire\n  void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions\n  void particleMoveUpdate(PSparticle &part, PSparticleFlags &partFlags, PSsettings2D *options = NULL, PSadvancedParticle *advancedproperties = NULL); // move function\n  // particle emitters\n  int32_t sprayEmit(const PSsource &emitter);\n  void flameEmit(const PSsource &emitter);\n  int32_t angleEmit(PSsource& emitter, const uint16_t angle, const int32_t speed);\n  //particle physics\n  void applyGravity(PSparticle &part); // applies gravity to single particle (use this for sources)\n  [[gnu::hot]] void applyForce(PSparticle &part, const int8_t xforce, const int8_t yforce, uint8_t &counter);\n  [[gnu::hot]] void applyForce(const uint32_t particleindex, const int8_t xforce, const int8_t yforce); // use this for advanced property particles\n  void applyForce(const int8_t xforce, const int8_t yforce); // apply a force to all particles\n  void applyAngleForce(PSparticle &part, const int8_t force, const uint16_t angle, uint8_t &counter);\n  void applyAngleForce(const uint32_t particleindex, const int8_t force, const uint16_t angle); // use this for advanced property particles\n  void applyAngleForce(const int8_t force, const uint16_t angle); // apply angular force to all particles\n  void applyFriction(PSparticle &part, const int32_t coefficient); // apply friction to specific particle\n  void applyFriction(const int32_t coefficient); // apply friction to all used particles\n  void pointAttractor(const uint32_t particleindex, PSparticle &attractor, const uint8_t strength, const bool swallow);\n  // set options  note: inlining the set function uses more flash so dont optimize\n  void setUsedParticles(const uint8_t percentage);  // set the percentage of particles used in the system, 255=100%\n  void setCollisionHardness(const uint8_t hardness); // hardness for particle collisions (255 means full hard)\n  void setWallHardness(const uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set\n  void setWallRoughness(const uint8_t roughness); // wall roughness randomizes wall collisions\n  void setMatrixSize(const uint32_t x, const uint32_t y);\n  void setWrapX(const bool enable);\n  void setWrapY(const bool enable);\n  void setBounceX(const bool enable);\n  void setBounceY(const bool enable);\n  void setKillOutOfBounds(const bool enable); // if enabled, particles outside of matrix instantly die\n  void setSaturation(const uint8_t sat); // set global color saturation\n  void setColorByAge(const bool enable);\n  void setMotionBlur(const uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero\n  void setSmearBlur(const uint8_t bluramount); // enable 2D smeared blurring of full frame\n  void setParticleSize(const uint8_t size);\n  void setGravity(const int8_t force = 8);\n  void enableParticleCollisions(const bool enable, const uint8_t hardness = 255);\n\n  PSparticle *particles; // pointer to particle array\n  PSparticleFlags *particleFlags; // pointer to particle flags array\n  PSsource *sources; // pointer to sources\n  PSadvancedParticle *advPartProps; // pointer to advanced particle properties (can be NULL)\n  PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL)\n  uint8_t* PSdataEnd; // points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data\n  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)\n  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\n  uint32_t numSources; // number of sources\n  uint32_t usedParticles; // number of particles used in animation, is relative to 'numParticles'\n  bool perParticleSize; // if true, uses individual particle sizes from advPartProps if available (disabled when calling setParticleSize())\n  //note: some variables are 32bit for speed and code size at the cost of ram\n\nprivate:\n  //rendering functions\n  void render();\n  [[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrapX, const bool wrapY);\n  void renderLargeParticle(const uint32_t size, const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrapX, const bool wrapY);\n  //paricle physics applied by system if flags are set\n  void applyGravity(); // applies gravity to all particles\n  void handleCollisions();\n  void collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy, const uint32_t collDistSq, int32_t massratio1, int32_t massratio2);\n  void fireParticleupdate();\n  //utility functions\n  void updatePSpointers(const bool isadvanced, const bool sizecontrol); // update the data pointers to current segment data space\n  bool updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control\n  void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize);\n  [[gnu::hot]] void bounce(int8_t &incomingspeed, int8_t &parallelspeed, int32_t &position, const uint32_t maxposition); // bounce on a wall\n  // note: variables that are accessed often are 32bit for speed\n  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\n  PSsettings2D particlesettings; // settings used when updating particles (can also used by FX to move sources), do not edit properties directly, use functions above\n  uint32_t numParticles;  // total number of particles allocated by this system\n  uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster\n  int32_t collisionHardness;\n  uint32_t wallHardness;\n  uint32_t wallRoughness; // randomizes wall collisions\n  uint32_t particleHardRadius; // hard surface radius of a particle, used for collision detection (32bit for speed)\n  uint16_t collisionStartIdx; // particle array start index for collision detection\n  uint8_t fireIntesity = 0; // fire intensity, used for fire mode (flash use optimization, better than passing an argument to render function)\n  uint8_t forcecounter; // counter for globally applied forces\n  uint8_t gforcecounter; // counter for global gravity\n  int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards)\n  // global particle properties for basic particles\n  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)\n  uint8_t motionBlur; // motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0\n  uint8_t smearBlur; // 2D smeared blurring of full frame\n};\n\n// initialization functions (not part of class)\nbool initParticleSystem2D(ParticleSystem2D *&PartSys, const uint32_t requestedsources, const uint32_t additionalbytes = 0, const bool advanced = false, const bool sizecontrol = false);\nuint32_t calculateNumberOfParticles2D(const uint32_t pixels, const bool advanced, const bool sizecontrol);\nuint32_t calculateNumberOfSources2D(const uint32_t pixels, const uint32_t requestedsources);\nbool allocateParticleSystemMemory2D(const uint32_t numparticles, const uint32_t numsources, const bool advanced, const bool sizecontrol, const uint32_t additionalbytes);\n\n// distance-based brightness for ellipse rendering, returns brightness (0-255) based on distance from ellipse center\ninline uint8_t calculateEllipseBrightness(int32_t dx, int32_t dy, int32_t rxsq, int32_t rysq, uint8_t maxBrightness) {\n  // square the distances\n  uint32_t dx_sq = dx * dx;\n  uint32_t dy_sq = dy * dy;\n\n  uint32_t dist_sq = ((dx_sq << 8) / rxsq) + ((dy_sq << 8) / rysq); // normalized squared distance in fixed point: (dx²/rx²) * 256 + (dy²/ry²) * 256\n\n  if (dist_sq >= 256) return 0;  // pixel is outside the ellipse, unit radius in fixed point: 256 = 1.0\n  //if (dist_sq <= 96) return maxBrightness; // core at full brightness\n  int32_t falloff = 256 - dist_sq;\n  return (maxBrightness * falloff) >> 8; // linear falloff\n  //return (maxBrightness * falloff * falloff) >> 16; // squared falloff for even softer edges\n}\n#endif // WLED_DISABLE_PARTICLESYSTEM2D\n\n////////////////////////\n// 1D Particle System //\n////////////////////////\n#ifndef WLED_DISABLE_PARTICLESYSTEM1D\n// memory allocation\n#ifdef ESP8266\n  #define MAXPARTICLES_1D 320\n  #define MAXSOURCES_1D 16\n#elif ARDUINO_ARCH_ESP32S2\n  #define MAXPARTICLES_1D 1300\n  #define MAXSOURCES_1D 32\n#else\n  #define MAXPARTICLES_1D 2600\n  #define MAXSOURCES_1D 64\n#endif\n\n\n\n// particle dimensions (subpixel division)\n#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)\n#define PS_P_HALFRADIUS_1D (PS_P_RADIUS_1D >> 1)\n#define PS_P_RADIUS_SHIFT_1D 5 // 1 << PS_P_RADIUS_SHIFT = PS_P_RADIUS\n#define PS_P_SURFACE_1D 5 // shift: 2^PS_P_SURFACE = PS_P_RADIUS_1D\n#define PS_P_MINHARDRADIUS_1D 32 // minimum hard surface radius note: do not change or hourglass effect will be broken\n#define PS_P_MINSURFACEHARDNESS_1D 120 // minimum hardness used in collision impulse calculation\n\n// struct for PS settings (shared for 1D and 2D class)\ntypedef union {\n  struct{\n  // one byte bit field for 1D settings\n  bool wrap : 1;\n  bool bounce : 1;\n  bool killoutofbounds : 1; // if set, out of bound particles are killed immediately\n  bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top\n  bool useCollisions : 1;\n  bool colorByAge : 1; // if set, particle hue is set by ttl value in render function\n  bool colorByPosition : 1; // if set, particle hue is set by its position in the strip segment\n  bool unused : 1;\n  };\n  byte asByte; // access as a byte, order is: LSB is first entry in the list above\n} PSsettings1D;\n\n//struct for a single particle (8 bytes)\ntypedef struct {\n  int32_t x;  // x position in particle system\n  uint16_t ttl; // time to live in frames\n  int8_t vx;  // horizontal velocity\n  uint8_t hue;  // color hue\n} PSparticle1D;\n\n//struct for particle flags\ntypedef union {\n  struct { // 1 byte\n    bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area\n    bool collide : 1; // if set, particle takes part in collisions\n    bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds)\n    bool reversegrav : 1; // if set, gravity is reversed on this particle\n    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\n    bool fixed : 1; // if set, particle does not move (and collisions make other particles revert direction),\n    bool custom1 : 1; // unused custom flags, can be used by FX to track particle states\n    bool custom2 : 1;\n  };\n  byte asByte; // access as a byte, order is: LSB is first entry in the list above\n} PSparticleFlags1D;\n\n// struct for additional particle settings (optional)\ntypedef struct {\n  uint8_t sat;  // color saturation\n  uint8_t size; // particle size, 255 means 10 pixels in diameter, this overrides global size setting\n  uint8_t forcecounter; // counter for applying forces to individual particles\n} PSadvancedParticle1D;\n\n//struct for a particle source (20 bytes)\ntypedef struct {\n  uint16_t minLife; // minimum ttl of emittet particles\n  uint16_t maxLife; // maximum ttl of emitted particles\n  PSparticle1D source; // use a particle as the emitter source (speed, position, color)\n  PSparticleFlags1D sourceFlags; // flags for the source particle\n  int8_t var; // variation of emitted speed (adds random(+/- var) to speed)\n  int8_t v; // emitting speed\n  uint8_t sat; // color saturation (advanced property)\n  uint8_t size; // particle size (advanced property)\n  // note: there is 3 bytes of padding added here\n} PSsource1D;\n\nclass ParticleSystem1D\n{\npublic:\n  ParticleSystem1D(const uint32_t length, const uint32_t numberofparticles, const uint32_t numberofsources, const bool isadvanced = false); // constructor\n  // note: memory is allcated in the FX function, no deconstructor needed\n  void update(void); //update the particles according to set options and render to the matrix\n  void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions\n  // particle emitters\n  int32_t sprayEmit(const PSsource1D &emitter);\n  void particleMoveUpdate(PSparticle1D &part, PSparticleFlags1D &partFlags, PSsettings1D *options = NULL, PSadvancedParticle1D *advancedproperties = NULL); // move function\n  //particle physics\n  [[gnu::hot]] void applyForce(PSparticle1D &part, const int8_t xforce, uint8_t &counter); //apply a force to a single particle\n  void applyForce(const int8_t xforce); // apply a force to all particles\n  void applyGravity(PSparticle1D &part, PSparticleFlags1D &partFlags); // applies gravity to single particle (use this for sources)\n  void applyFriction(const int32_t coefficient); // apply friction to all used particles\n  // set options\n  void setUsedParticles(const uint8_t percentage); // set the percentage of particles used in the system, 255=100%\n  void setWallHardness(const uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set\n  void setSize(const uint32_t x); //set particle system size (= strip length)\n  void setWrap(const bool enable);\n  void setBounce(const bool enable);\n  void setKillOutOfBounds(const bool enable); // if enabled, particles outside of matrix instantly die\n // void setSaturation(uint8_t sat); // set global color saturation\n  void setColorByAge(const bool enable);\n  void setColorByPosition(const bool enable);\n  void setMotionBlur(const uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero\n  void setSmearBlur(const uint8_t bluramount); // enable 1D smeared blurring of full frame\n  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\n  void setGravity(int8_t force = 8);\n  void enableParticleCollisions(bool enable, const uint8_t hardness = 255);\n\n\n  PSparticle1D *particles; // pointer to particle array\n  PSparticleFlags1D *particleFlags; // pointer to particle flags array\n  PSsource1D *sources; // pointer to sources\n  PSadvancedParticle1D *advPartProps; // pointer to advanced particle properties (can be NULL)\n  //PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL)\n  uint8_t* PSdataEnd; // points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data\n  int32_t maxX; // particle system size i.e. width-1, Note: all \"max\" variables must be signed to compare to coordinates (which are signed)\n  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\n  uint32_t numSources; // number of sources\n  uint32_t usedParticles; // number of particles used in animation, is relative to 'numParticles'\n  bool perParticleSize; // if true, uses individual particle sizes from advPartProps if available (disabled when calling setParticleSize())\n\nprivate:\n  //rendering functions\n  void render(void);\n  void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGBW &color, const bool wrap);\n  void renderLargeParticle(const uint32_t size, const uint32_t particleindex, const uint8_t brightness, const CRGBW& color, const bool wrap);\n\n  //paricle physics applied by system if flags are set\n  void applyGravity(); // applies gravity to all particles\n  void handleCollisions();\n  void collideParticles(uint32_t partIdx1, uint32_t partIdx2, int32_t dx, uint32_t collisiondistance);\n\n  //utility functions\n  void updatePSpointers(const bool isadvanced); // update the data pointers to current segment data space\n  //void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control\n  [[gnu::hot]] void bounce(int8_t &incomingspeed, int8_t &parallelspeed, int32_t &position, const uint32_t maxposition); // bounce on a wall\n  // note: variables that are accessed often are 32bit for speed\n  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\n  PSsettings1D particlesettings; // settings used when updating particles\n  uint32_t numParticles;  // total number of particles allocated by this system\n  uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster\n  int32_t collisionHardness;\n  uint32_t particleHardRadius; // hard surface radius of a particle, used for collision detection\n  uint32_t wallHardness;\n  uint8_t gforcecounter; // counter for global gravity\n  int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards)\n  uint8_t forcecounter; // counter for globally applied forces\n  uint16_t collisionStartIdx; // particle array start index for collision detection\n  //global particle properties for basic particles\n  uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels, is overruled by advanced particle size\n  uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations\n  uint8_t smearBlur; // smeared blurring of full frame\n};\n\nbool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedsources, const uint8_t fractionofparticles = 255, const uint32_t additionalbytes = 0, const bool advanced = false);\nuint32_t calculateNumberOfParticles1D(const uint32_t fraction, const bool isadvanced);\nuint32_t calculateNumberOfSources1D(const uint32_t requestedsources);\nbool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t numsources, const bool isadvanced, const uint32_t additionalbytes);\n\n#endif // WLED_DISABLE_PARTICLESYSTEM1D\n"
  },
  {
    "path": "wled00/NodeStruct.h",
    "content": "#ifndef WLED_NODESTRUCT_H\n#define WLED_NODESTRUCT_H\n\n/*********************************************************************************************\\\n* NodeStruct from the ESP Easy project (https://github.com/letscontrolit/ESPEasy)\n\\*********************************************************************************************/\n\n#include <map>\n#include <IPAddress.h>\n\n#define NODE_TYPE_ID_UNDEFINED        0\n#define NODE_TYPE_ID_ESP8266         82 // should be 1\n#define NODE_TYPE_ID_ESP32           32 // should be 2\n#define NODE_TYPE_ID_ESP32S2         33 // etc\n#define NODE_TYPE_ID_ESP32S3         34\n#define NODE_TYPE_ID_ESP32C3         35\n\n// updated node types from the ESP Easy project\n// https://github.com/letscontrolit/ESPEasy/blob/mega/src/src/DataTypes/NodeTypeID.h\n//#define NODE_TYPE_ID_ESP32        33\n//#define NODE_TYPE_ID_ESP32S2      34\n//#define NODE_TYPE_ID_ESP32C3      35\n//#define NODE_TYPE_ID_ESP32S3      36\n#define NODE_TYPE_ID_ESP32C2      37\n#define NODE_TYPE_ID_ESP32H2      38\n#define NODE_TYPE_ID_ESP32C6      39\n#define NODE_TYPE_ID_ESP32C61     40\n#define NODE_TYPE_ID_ESP32C5      41\n#define NODE_TYPE_ID_ESP32P4      42\n#define NODE_TYPE_ID_ESP32P4r3    45\n#define NODE_TYPE_ID_ESP32H21     43\n#define NODE_TYPE_ID_ESP32H4      44\n\n/*********************************************************************************************\\\n* NodeStruct\n\\*********************************************************************************************/\nstruct NodeStruct\n{\n  String    nodeName;\n  IPAddress ip;\n  uint8_t   age;\n  union {\n    uint8_t nodeType;   // a waste of space as we only have 5 types\n    struct {\n      uint8_t type : 7; // still a waste of space (4 bits would be enough and future-proof)\n      bool    on   : 1;\n    };\n  };\n  uint32_t  build;\n\n  NodeStruct() : age(0), nodeType(0), build(0)\n  {\n    for (unsigned i = 0; i < 4; ++i) { ip[i] = 0; }\n  }\n};\ntypedef std::map<uint8_t, NodeStruct> NodesMap;\n\n#endif // WLED_NODESTRUCT_H\n"
  },
  {
    "path": "wled00/alexa.cpp",
    "content": "#include \"wled.h\"\n\n/*\n * Alexa Voice On/Off/Brightness/Color Control. Emulates a Philips Hue bridge to Alexa.\n *\n * This was put together from these two excellent projects:\n * https://github.com/kakopappa/arduino-esp8266-alexa-wemo-switch\n * https://github.com/probonopd/ESP8266HueEmulator\n */\n#include \"src/dependencies/espalexa/EspalexaDevice.h\"\n\n#ifndef WLED_DISABLE_ALEXA\nvoid onAlexaChange(EspalexaDevice* dev);\n\nvoid alexaInit()\n{\n  if (!alexaEnabled || !WLED_CONNECTED) return;\n\n  espalexa.removeAllDevices();\n  // the original configured device for on/off or macros (added first, i.e. index 0)\n  espalexaDevice = new EspalexaDevice(alexaInvocationName, onAlexaChange, EspalexaDeviceType::extendedcolor);\n  espalexa.addDevice(espalexaDevice);\n\n  // up to 9 devices (added second, third, ... i.e. index 1 to 9) serve for switching on up to nine presets (preset IDs 1 to 9 in WLED),\n  // names are identical as the preset names, switching off can be done by switching off any of them\n  if (alexaNumPresets) {\n    String name = \"\";\n    for (unsigned presetIndex = 1; presetIndex <= alexaNumPresets; presetIndex++)\n    {\n      if (!getPresetName(presetIndex, name)) break; // no more presets\n      EspalexaDevice* dev = new EspalexaDevice(name.c_str(), onAlexaChange, EspalexaDeviceType::extendedcolor);\n      espalexa.addDevice(dev);\n    }\n  }\n  espalexa.begin(&server);\n}\n\nvoid handleAlexa()\n{\n  if (!alexaEnabled || !WLED_CONNECTED) return;\n  espalexa.loop();\n}\n\nvoid onAlexaChange(EspalexaDevice* dev)\n{\n  EspalexaDeviceProperty m = dev->getLastChangedProperty();\n\n  if (m == EspalexaDeviceProperty::on)\n  {\n    if (dev->getId() == 0) // Device 0 is for on/off or macros\n    {\n      if (!macroAlexaOn)\n      {\n        if (bri == 0)\n        {\n          bri = briLast;\n          stateUpdated(CALL_MODE_ALEXA);\n        }\n      } else\n      {\n        applyPreset(macroAlexaOn, CALL_MODE_ALEXA);\n        if (bri == 0) dev->setValue(briLast); //stop Alexa from complaining if macroAlexaOn does not actually turn on\n      }\n    } else // switch-on behavior for preset devices\n    {\n      // turn off other preset devices\n      for (unsigned i = 1; i < espalexa.getDeviceCount(); i++)\n      {\n        if (i == dev->getId()) continue;\n        espalexa.getDevice(i)->setValue(0); // turn off other presets\n      }\n\n      applyPreset(dev->getId(), CALL_MODE_ALEXA); // in alexaInit() preset 1 device was added second (index 1), preset 2 third (index 2) etc.\n    }\n  } else if (m == EspalexaDeviceProperty::off)\n  {\n    if (!macroAlexaOff)\n    {\n      if (bri > 0)\n      {\n        briLast = bri;\n        bri = 0;\n        stateUpdated(CALL_MODE_ALEXA);\n      }\n    } else\n    {\n      applyPreset(macroAlexaOff, CALL_MODE_ALEXA);\n      // below for loop stops Alexa from complaining if macroAlexaOff does not actually turn off\n    }\n    for (unsigned i = 0; i < espalexa.getDeviceCount(); i++)\n    {\n      espalexa.getDevice(i)->setValue(0);\n    }\n  } else if (m == EspalexaDeviceProperty::bri)\n  {\n    bri = dev->getValue();\n    stateUpdated(CALL_MODE_ALEXA);\n  } else //color\n  {\n    if (dev->getColorMode() == EspalexaColorMode::ct) //shade of white\n    {\n      byte rgbw[4];\n      uint16_t ct = dev->getCt();\n      if (!ct) return;\n      uint16_t k = 1000000 / ct; //mireds to kelvin\n\n      if (strip.hasCCTBus()) {\n        bool hasManualWhite = strip.getActiveSegsLightCapabilities(true) & SEG_CAPABILITY_W;\n\n        strip.setCCT(k);\n        if (hasManualWhite) {\n          rgbw[0] = 0; rgbw[1] = 0; rgbw[2] = 0; rgbw[3] = 255;\n        } else {\n          rgbw[0] = 255; rgbw[1] = 255; rgbw[2] = 255; rgbw[3] = 0;\n          dev->setValue(255);\n        }\n      } else if (strip.hasWhiteChannel()) {\n        switch (ct) { //these values empirically look good on RGBW\n          case 199: rgbw[0]=255; rgbw[1]=255; rgbw[2]=255; rgbw[3]=255; break;\n          case 234: rgbw[0]=127; rgbw[1]=127; rgbw[2]=127; rgbw[3]=255; break;\n          case 284: rgbw[0]=  0; rgbw[1]=  0; rgbw[2]=  0; rgbw[3]=255; break;\n          case 350: rgbw[0]=130; rgbw[1]= 90; rgbw[2]=  0; rgbw[3]=255; break;\n          case 383: rgbw[0]=255; rgbw[1]=153; rgbw[2]=  0; rgbw[3]=255; break;\n          default : colorKtoRGB(k, rgbw);\n        }\n      } else {\n        colorKtoRGB(k, rgbw);\n      }\n      strip.getMainSegment().setColor(0, RGBW32(rgbw[0], rgbw[1], rgbw[2], rgbw[3]));\n    } else {\n      uint32_t color = dev->getRGB();\n      strip.getMainSegment().setColor(0, color);\n    }\n    stateUpdated(CALL_MODE_ALEXA);\n  }\n}\n\n\n#else\n void alexaInit(){}\n void handleAlexa(){}\n#endif\n"
  },
  {
    "path": "wled00/bus_manager.cpp",
    "content": "/*\n * Class implementation for addressing various light types\n */\n\n#include <Arduino.h>\n#include <IPAddress.h>\n#ifdef ARDUINO_ARCH_ESP32\n#include <ESPmDNS.h>\n#include \"src/dependencies/network/Network.h\" // for isConnected() (& WiFi)\n#include \"driver/ledc.h\"\n#include \"soc/ledc_struct.h\"\n#endif\n#ifdef ESP8266\n#include \"core_esp8266_waveform.h\"\n#endif\n#include \"bus_manager.h\"\n#include \"bus_wrapper.h\"\n#include \"wled.h\"\n\n// functions to get/set bits in an array - based on functions created by Brandon for GOL\n//  toDo : make this a class that's completely defined in a header file\n// note: these functions are automatically inline by the compiler\nstatic inline bool getBitFromArray(const uint8_t* byteArray, size_t position) { // get bit value\n  size_t byteIndex = position >> 3;    // divide by 8\n  unsigned bitIndex = position & 0x07; // modulo 8\n  uint8_t byteValue = byteArray[byteIndex];\n  return (byteValue >> bitIndex) & 1;\n}\n\nstatic inline void setBitInArray(uint8_t* byteArray, size_t position, bool value) {  // set bit - with error handling for nullptr\n    //if (byteArray == nullptr) return;\n    size_t byteIndex = position >> 3;    // divide by 8\n    unsigned bitIndex = position & 0x07; // modulo 8\n    if (value)\n        byteArray[byteIndex] |= (1 << bitIndex);\n    else\n        byteArray[byteIndex] &= ~(1 << bitIndex);\n}\n\nstatic inline size_t getBitArrayBytes(size_t num_bits) { // number of bytes needed for an array with num_bits bits\n  return (num_bits + 7) >> 3;\n}\n\nstatic inline void setBitArray(uint8_t* byteArray, size_t numBits, bool value) {  // set all bits to same value\n  if (byteArray == nullptr) return;\n  size_t len =  getBitArrayBytes(numBits);\n  if (value) memset(byteArray, 0xFF, len);\n  else memset(byteArray, 0x00, len);\n}\n\nstatic ColorOrderMap _colorOrderMap = {};\n\nbool ColorOrderMap::add(uint16_t start, uint16_t len, uint8_t colorOrder) {\n  if (count() >= WLED_MAX_COLOR_ORDER_MAPPINGS || len == 0 || (colorOrder & 0x0F) > COL_ORDER_MAX) return false; // upper nibble contains W swap information\n  _mappings.push_back({start,len,colorOrder});\n  DEBUGBUS_PRINTF_P(PSTR(\"Bus: Add COM (%d,%d,%d)\\n\"), (int)start, (int)len, (int)colorOrder);\n  return true;\n}\n\nuint8_t IRAM_ATTR ColorOrderMap::getPixelColorOrder(uint16_t pix, uint8_t defaultColorOrder) const {\n  // upper nibble contains W swap information\n  // when ColorOrderMap's upper nibble contains value >0 then swap information is used from it, otherwise global swap is used\n  for (const auto& map : _mappings) {\n    if (pix >= map.start && pix < (map.start + map.len)) return map.colorOrder | ((map.colorOrder >> 4) ? 0 : (defaultColorOrder & 0xF0));\n  }\n  return defaultColorOrder;\n}\n\n\nvoid Bus::calculateCCT(uint32_t c, uint8_t &ww, uint8_t &cw) {\n  unsigned cct = 0; //0 - full warm white, 255 - full cold white\n  unsigned w = W(c);\n\n  if (_cct > -1) {                                    // using RGB?\n    if (_cct >= 1900)    cct = (_cct - 1900) >> 5;    // convert K in relative format\n    else if (_cct < 256) cct = _cct;                  // already relative\n  } else {\n    cct = (approximateKelvinFromRGB(c) - 1900) >> 5;  // convert K (from RGB value) to relative format\n  }\n\n  // CCT blending modes (_cctBlend):\n  // blend<0: ww: ▓▓▒░__  | blend=0: ww: ▓▒▒░░ |  blend>0 ww: ▓▓▓▒░\n  //          cw: __░▒▓▓  |          cw: ░░▒▒▓ |          cw: ░▒▓▓▓\n  int32_t ww_val, cw_val;\n  if (_cctBlend < 0) {\n    uint16_t range = 255 - 2 * (uint16_t)(-_cctBlend);\n    if (range > 255) range = 255; // prevent overflow\n    ww_val = range ? ((int32_t)(255 + _cctBlend - cct) * 255) / range : (cct < 128 ? 255 : 0); // exclusive blending\n    cw_val = 255 - ww_val;\n  } else {\n    ww_val = _cctBlend ? ((int32_t)(255 - cct) * 255) / (255 - _cctBlend) : 255 - cct; // additive blending\n    cw_val = _cctBlend ? ((int32_t) cct      * 255) / (255 - _cctBlend) : cct;\n  }\n  ww = (uint8_t)(ww_val < 0 ? 0 : ww_val > 255 ? 255 : ww_val);\n  cw = (uint8_t)(cw_val < 0 ? 0 : cw_val > 255 ? 255 : cw_val);\n\n  ww = (w * ww) / 255; //brightness scaling\n  cw = (w * cw) / 255;\n}\n\n// calculates white channel and CCT values based on given settings\nuint32_t Bus::autoWhiteCalc(uint32_t c, uint8_t &ww, uint8_t &cw) const {\n  unsigned aWM = _autoWhiteMode;\n  if (_gAWM < AW_GLOBAL_DISABLED) aWM = _gAWM;\n  CRGBW cIn = c; // save original color for CCT calculation\n  unsigned w = W(c);\n  if (aWM != RGBW_MODE_MANUAL_ONLY) {\n    unsigned r = R(c); // note: using uint8_t generates larger code\n    unsigned g = G(c);\n    unsigned b = B(c);\n    if (aWM == RGBW_MODE_DUAL && w > 0) {\n      //ignore auto-white calculation if w>0 and mode DUAL (DUAL behaves as BRIGHTER if w==0)\n    } else if (aWM == RGBW_MODE_MAX) {\n      w = r > g ? (r > b ? r : b) : (g > b ? g : b); // brightest RGB channel\n    } else {\n      w = r < g ? (r < b ? r : b) : (g < b ? g : b); // darkest RGB channel\n      if (aWM == RGBW_MODE_AUTO_ACCURATE) { r -= w; g -= w; b -= w; } //subtract w in ACCURATE mode\n    }\n    c = RGBW32(r, g, b, w);\n  }\n  if (_hasCCT) {\n    cIn.w = w; // need original rgb values in case CCT is derived from RGB\n    calculateCCT(cIn, ww, cw);\n  }\n  return c;\n}\n\n\nBusDigital::BusDigital(const BusConfig &bc)\n: Bus(bc.type, bc.start, bc.autoWhite, bc.count, bc.reversed, (bc.refreshReq || bc.type == TYPE_TM1814))\n, _skip(bc.skipAmount) //sacrificial pixels\n, _colorOrder(bc.colorOrder)\n, _milliAmpsPerLed(bc.milliAmpsPerLed)\n, _milliAmpsMax(bc.milliAmpsMax)\n, _driverType(bc.driverType) // Store driver preference (0=RMT, 1=I2S)\n{\n  DEBUGBUS_PRINTLN(F(\"Bus: Creating digital bus.\"));\n  if (!isDigital(bc.type) || !bc.count) { DEBUGBUS_PRINTLN(F(\"Not digial or empty bus!\")); return; }\n  _iType = bc.iType; // reuse the iType that was determined by polyBus in getI() in finalizeInit()\n  if (_iType == I_NONE) { DEBUGBUS_PRINTLN(F(\"Incorrect iType!\")); return; }\n\n  if (!PinManager::allocatePin(bc.pins[0], true, PinOwner::BusDigital)) { DEBUGBUS_PRINTLN(F(\"Pin 0 allocated!\")); return; }\n  _frequencykHz = 0U;\n  _colorSum = 0;\n  _pins[0] = bc.pins[0];\n  if (is2Pin(bc.type)) {\n    if (!PinManager::allocatePin(bc.pins[1], true, PinOwner::BusDigital)) {\n      cleanup();\n      DEBUGBUS_PRINTLN(F(\"Pin 1 allocated!\"));\n      return;\n    }\n    _pins[1] = bc.pins[1];\n    _frequencykHz = bc.frequency ? bc.frequency : 2000U; // 2MHz clock if undefined\n  }\n\n  _hasRgb = hasRGB(bc.type);\n  _hasWhite = hasWhite(bc.type);\n  _hasCCT = hasCCT(bc.type);\n  uint16_t lenToCreate = bc.count;\n  if (bc.type == TYPE_WS2812_1CH_X3) lenToCreate = NUM_ICS_WS2812_1CH_3X(bc.count); // only needs a third of \"RGB\" LEDs for NeoPixelBus\n  _busPtr = PolyBus::create(_iType, _pins, lenToCreate + _skip);\n  _valid = (_busPtr != nullptr) && bc.count > 0;\n  // fix for wled#4759\n  if (_valid) for (unsigned i = 0; i < _skip; i++) {\n    PolyBus::setPixelColor(_busPtr, _iType, i, 0, COL_ORDER_GRB); // set sacrificial pixels to black (CO does not matter here)\n  }\n  else {\n    cleanup();\n  }\n  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\"),\n    (int)bc.count,\n    (int)bc.type,\n    (int)_hasRgb, (int)_hasWhite, (int)_hasCCT,\n    (unsigned)_pins[0], is2Pin(bc.type)?(unsigned)_pins[1]:255U,\n    (unsigned)_iType,\n    isI2S() ? \"I2S\" : \"RMT\",\n    (int)_milliAmpsPerLed, (int)_milliAmpsMax,\n    _valid ? \" \" : \"FAILED\"\n  );\n}\n\n//DISCLAIMER\n//The following function attemps to calculate the current LED power usage,\n//and will limit the brightness to stay below a set amperage threshold.\n//It is NOT a measurement and NOT guaranteed to stay within the ablMilliampsMax margin.\n//Stay safe with high amperage and have a reasonable safety margin!\n//I am NOT to be held liable for burned down garages or houses!\n\n// note on ABL implementation:\n// ABL is set up in finalizeInit()\n// scaled color channels are summed in BusDigital::setPixelColor()\n// the used current is estimated and limited in BusManager::show()\n// if limit is set too low, brightness is limited to 1 to at least show some light\n// to disable brightness limiter for a bus, set LED current to 0\n\nvoid BusDigital::estimateCurrent() {\n  uint32_t actualMilliampsPerLed = _milliAmpsPerLed;\n  if (_milliAmpsPerLed == 255) {\n    // use wacky WS2815 power model, see WLED issue #549\n    _colorSum *= 3; // sum is sum of max value for each color, need to multiply by three to account for clrUnitsPerChannel being 3*255\n    actualMilliampsPerLed = 12; // from testing an actual strip\n  }\n  // _colorSum has all the values of color channels summed, max would be getLength()*(3*255 + (255 if hasWhite()): convert to milliAmps\n  uint32_t clrUnitsPerChannel = hasWhite() ? 4*255 : 3*255;\n  _milliAmpsTotal = ((uint64_t)_colorSum * actualMilliampsPerLed) / clrUnitsPerChannel + getLength(); // add 1mA standby current per LED to total (WS2812: ~0.7mA, WS2815: ~2mA)\n}\n\nvoid BusDigital::applyBriLimit(uint8_t newBri) {\n  // a newBri of 0 means calculate per-bus brightness limit\n  _NPBbri = 255; // reset, intermediate value is set below, final value is calculated in bus::show()\n  if (newBri == 0) {\n    if (_milliAmpsLimit == 0 || _milliAmpsTotal == 0) return; // ABL not used for this bus\n    newBri = 255;\n\n    if (_milliAmpsLimit > getLength()) { // each LED uses about 1mA in standby\n      if (_milliAmpsTotal > _milliAmpsLimit) {\n        // scale brightness down to stay in current limit\n        newBri = ((uint32_t)_milliAmpsLimit * 255) / _milliAmpsTotal + 1; // +1 to avoid 0 brightness\n        _milliAmpsTotal = _milliAmpsLimit;\n      }\n    } else {\n      newBri = 1; // limit too low, set brightness to 1, this will dim down all colors to minimum since we use video scaling\n      _milliAmpsTotal = getLength(); // estimate bus current as minimum\n    }\n  }\n\n  if (newBri < 255) {\n    _NPBbri = newBri; // store value so it can be updated in show() (must be updated even if ABL is not used)\n    uint16_t wwcw = 0;\n    unsigned hwLen = _len;\n    if (_type == TYPE_WS2812_1CH_X3) hwLen = NUM_ICS_WS2812_1CH_3X(_len); // only needs a third of \"RGB\" LEDs for NeoPixelBus\n    for (unsigned i = 0; i < hwLen; i++) {\n      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\n      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\n      if (hasCCT()) {\n        uint8_t cctWW, cctCW;\n        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)\n        wwcw = ((cctCW + 1) * newBri) & 0xFF00; // apply brightness to CCT (leave it in upper byte for 16bit NeoPixelBus value)\n        wwcw |= ((cctWW + 1) * newBri) >> 8;\n      }\n      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\n      PolyBus::setPixelColor(_busPtr, _iType, i, c, co, wwcw); // repaint all pixels with new brightness\n    }\n  }\n\n  _colorSum = 0; // reset for next frame\n}\n\nvoid BusDigital::show() {\n  if (!_valid) return;\n  _NPBbri = (_NPBbri * _bri) / 255;      // total applied brightness for use in restoreColorLossy (see applyBriLimit())\n  PolyBus::show(_busPtr, _iType, _skip); // faster if buffer consistency is not important (no skipped LEDs)\n}\n\nbool BusDigital::canShow() const {\n  if (!_valid) return true;\n  return PolyBus::canShow(_busPtr, _iType);\n}\n\n//If LEDs are skipped, it is possible to use the first as a status LED.\n//TODO only show if no new show due in the next 50ms\nvoid BusDigital::setStatusPixel(uint32_t c) {\n  if (_valid && _skip) {\n    PolyBus::setPixelColor(_busPtr, _iType, 0, c, _colorOrderMap.getPixelColorOrder(_start, _colorOrder));\n    if (canShow()) PolyBus::show(_busPtr, _iType);\n  }\n}\n\n// note: using WLED_O2_ATTR makes this function ~7% faster at the expense of 600 bytes of flash\nvoid IRAM_ATTR BusDigital::setPixelColor(unsigned pix, uint32_t c) {\n  if (!_valid) return;\n  if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT\n  uint8_t cctWW = 0, cctCW = 0;\n  uint16_t wwcw = 0;\n  if (hasWhite()) c = autoWhiteCalc(c, cctWW, cctCW);\n  c = color_fade(c, _bri, true); // apply brightness\n\n  if (hasCCT()) {\n    wwcw = ((cctCW + 1) * _bri) & 0xFF00; // apply brightness to CCT (store CW in upper byte)\n    wwcw |= ((cctWW + 1) * _bri) >> 8;\n    if (_type == TYPE_WS2812_WWA) c = RGBW32(wwcw, wwcw >> 8, 0, W(c)); // ww,cw, 0, w\n  }\n\n  if (BusManager::_useABL) {\n    // if using ABL, sum all color channels to estimate current and limit brightness in show()\n    uint8_t r = R(c), g = G(c), b = B(c);\n    if (_milliAmpsPerLed < 255) { // normal ABL\n      _colorSum += r + g + b + W(c);\n    } else { // wacky WS2815 power model, ignore white channel, use max of RGB (issue #549)\n      _colorSum += ((r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b));\n    }\n  }\n\n  if (_reversed) pix = _len - pix -1;\n  pix += _skip;\n  const uint8_t co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder);\n  if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs\n    unsigned pOld = pix;\n    pix = IC_INDEX_WS2812_1CH_3X(pix);\n    uint32_t cOld = PolyBus::getPixelColor(_busPtr, _iType, pix, co);\n    switch (pOld % 3) { // change only the single channel (TODO: this can cause loss because of get/set)\n      case 0: c = RGBW32(R(cOld), W(c)   , B(cOld), 0); break;\n      case 1: c = RGBW32(W(c)   , G(cOld), B(cOld), 0); break;\n      case 2: c = RGBW32(R(cOld), G(cOld), W(c)   , 0); break;\n    }\n  }\n\n  PolyBus::setPixelColor(_busPtr, _iType, pix, c, co, wwcw);\n}\n\n// returns lossly restored color from bus\nuint32_t IRAM_ATTR BusDigital::getPixelColor(unsigned pix) const {\n  if (!_valid) return 0;\n  if (_reversed) pix = _len - pix -1;\n  pix += _skip;\n  const uint8_t co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder);\n  uint32_t c = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, (_type==TYPE_WS2812_1CH_X3) ? IC_INDEX_WS2812_1CH_3X(pix) : pix, co),_NPBbri);\n  if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs\n    uint8_t r = R(c);\n    uint8_t g = _reversed ? B(c) : G(c); // should G and B be switched if _reversed?\n    uint8_t b = _reversed ? G(c) : B(c);\n    switch (pix % 3) { // get only the single channel\n      case 0: c = RGBW32(g, g, g, g); break;\n      case 1: c = RGBW32(r, r, r, r); break;\n      case 2: c = RGBW32(b, b, b, b); break;\n    }\n  }\n  if (_type == TYPE_WS2812_WWA) {\n    uint8_t w = R(c) | G(c);\n    c = RGBW32(w, w, 0, w);\n  }\n  return c;\n}\n\nsize_t BusDigital::getPins(uint8_t* pinArray) const {\n  unsigned numPins = is2Pin(_type) + 1;\n  if (pinArray) for (unsigned i = 0; i < numPins; i++) pinArray[i] = _pins[i];\n  return numPins;\n}\n\nsize_t BusDigital::getBusSize() const {\n  return sizeof(BusDigital) + (isOk() ? PolyBus::getDataSize(_busPtr, _iType) : 0); // does not include common I2S DMA buffer\n}\n\nvoid BusDigital::setColorOrder(uint8_t colorOrder) {\n  // upper nibble contains W swap information\n  if ((colorOrder & 0x0F) > 5) return;\n  _colorOrder = colorOrder;\n}\n\n// credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056\nstd::vector<LEDType> BusDigital::getLEDTypes() {\n  return {\n    {TYPE_WS2812_RGB,    \"D\",  PSTR(\"WS281x\")},\n    {TYPE_SK6812_RGBW,   \"D\",  PSTR(\"SK6812/WS2814 RGBW\")},\n    {TYPE_TM1814,        \"D\",  PSTR(\"TM1814\")},\n    {TYPE_WS2811_400KHZ, \"D\",  PSTR(\"400kHz\")},\n    {TYPE_TM1829,        \"D\",  PSTR(\"TM1829\")},\n    {TYPE_UCS8903,       \"D\",  PSTR(\"UCS8903\")},\n    {TYPE_APA106,        \"D\",  PSTR(\"APA106/PL9823\")},\n    {TYPE_TM1914,        \"D\",  PSTR(\"TM1914\")},\n    {TYPE_FW1906,        \"D\",  PSTR(\"FW1906 GRBCW\")},\n    {TYPE_UCS8904,       \"D\",  PSTR(\"UCS8904 RGBW\")},\n    {TYPE_WS2805,        \"D\",  PSTR(\"WS2805 RGBCW\")},\n    {TYPE_SM16825,       \"D\",  PSTR(\"SM16825 RGBCW\")},\n    {TYPE_WS2812_1CH_X3, \"D\",  PSTR(\"WS2811 White\")},\n    //{TYPE_WS2812_2CH_X3, \"D\",  PSTR(\"WS281x CCT\")}, // not implemented\n    {TYPE_WS2812_WWA,    \"D\",  PSTR(\"WS281x WWA\")}, // amber ignored\n    {TYPE_WS2801,        \"2P\", PSTR(\"WS2801\")},\n    {TYPE_APA102,        \"2P\", PSTR(\"APA102\")},\n    {TYPE_LPD8806,       \"2P\", PSTR(\"LPD8806\")},\n    {TYPE_LPD6803,       \"2P\", PSTR(\"LPD6803\")},\n    {TYPE_P9813,         \"2P\", PSTR(\"PP9813\")},\n  };\n}\n\nbool BusDigital::isI2S() {\n  return (_iType & 0x01) == 0; // I2S types have even iType values\n}\n\nvoid BusDigital::begin() {\n  if (!_valid) return;\n  PolyBus::begin(_busPtr, _iType, _pins, _frequencykHz);\n}\n\nvoid BusDigital::cleanup() {\n  DEBUGBUS_PRINTLN(F(\"Digital Cleanup.\"));\n  PolyBus::cleanup(_busPtr, _iType);\n  _iType = I_NONE;\n  _valid = false;\n  _busPtr = nullptr;\n  PinManager::deallocatePin(_pins[1], PinOwner::BusDigital);\n  PinManager::deallocatePin(_pins[0], PinOwner::BusDigital);\n}\n\n\n#ifdef ESP8266\n  // 1 MHz clock\n  #define CLOCK_FREQUENCY 1000000UL\n#else\n  // Use XTAL clock if possible to avoid timer frequency error when setting APB clock < 80 Mhz\n  // https://github.com/espressif/arduino-esp32/blob/2.0.2/cores/esp32/esp32-hal-ledc.c\n  #ifdef SOC_LEDC_SUPPORT_XTAL_CLOCK\n    #define CLOCK_FREQUENCY 40000000UL\n  #else\n    #define CLOCK_FREQUENCY 80000000UL\n  #endif\n#endif\n\n#ifdef ESP8266\n  #define MAX_BIT_WIDTH 10\n#else\n  #ifdef SOC_LEDC_TIMER_BIT_WIDE_NUM\n    // C6/H2/P4: 20 bit, S2/S3/C2/C3: 14 bit\n    #define MAX_BIT_WIDTH SOC_LEDC_TIMER_BIT_WIDE_NUM\n  #else\n    // ESP32: 20 bit (but in reality we would never go beyond 16 bit as the frequency would be to low)\n    #define MAX_BIT_WIDTH 14\n  #endif\n#endif\n\nBusPwm::BusPwm(const BusConfig &bc)\n: Bus(bc.type, bc.start, bc.autoWhite, 1, bc.reversed, bc.refreshReq) // hijack Off refresh flag to indicate usage of dithering\n{\n  if (!isPWM(bc.type)) return;\n  const unsigned numPins = numPWMPins(bc.type);\n  [[maybe_unused]] const bool dithering = _needsRefresh;\n  _frequency = bc.frequency ? bc.frequency : WLED_PWM_FREQ;\n  // duty cycle resolution (_depth) can be extracted from this formula: CLOCK_FREQUENCY > _frequency * 2^_depth\n  for (_depth = MAX_BIT_WIDTH; _depth > 8; _depth--) if (((CLOCK_FREQUENCY/_frequency) >> _depth) > 0) break;\n\n  managed_pin_type pins[numPins];\n  for (unsigned i = 0; i < numPins; i++) pins[i] = {(int8_t)bc.pins[i], true};\n  if (PinManager::allocateMultiplePins(pins, numPins, PinOwner::BusPwm)) {\n    #ifdef ESP8266\n    analogWriteRange((1<<_depth)-1);\n    analogWriteFreq(_frequency);\n    #else\n    // for 2 pin PWM CCT strip pinManager will make sure both LEDC channels are in the same speed group and sharing the same timer\n    _ledcStart = PinManager::allocateLedc(numPins);\n    if (_ledcStart == 255) { //no more free LEDC channels\n      PinManager::deallocateMultiplePins(pins, numPins, PinOwner::BusPwm);\n      DEBUGBUS_PRINTLN(F(\"No more free LEDC channels!\"));\n      return;\n    }\n    // if _needsRefresh is true (UI hack) we are using dithering (credit @dedehai & @zalatnaicsongor)\n    if (dithering) _depth = 12; // fixed 8 bit depth PWM with 4 bit dithering (ESP8266 has no hardware to support dithering)\n    #endif\n\n    for (unsigned i = 0; i < numPins; i++) {\n      _pins[i] = bc.pins[i]; // store only after allocateMultiplePins() succeeded\n      #ifdef ESP8266\n      pinMode(_pins[i], OUTPUT);\n      #else\n      unsigned channel = _ledcStart + i;\n      ledcSetup(channel, _frequency, _depth - (dithering*4)); // with dithering _frequency doesn't really matter as resolution is 8 bit\n      ledcAttachPin(_pins[i], channel);\n      // LEDC timer reset credit @dedehai\n      uint8_t group = (channel / 8), timer = ((channel / 2) % 4); // same fromula as in ledcSetup()\n      ledc_timer_rst((ledc_mode_t)group, (ledc_timer_t)timer); // reset timer so all timers are almost in sync (for phase shift)\n      #endif\n    }\n    _hasRgb = hasRGB(bc.type);\n    _hasWhite = hasWhite(bc.type);\n    _hasCCT = hasCCT(bc.type);\n    _valid = true;\n  }\n  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]);\n}\n\nvoid BusPwm::setPixelColor(unsigned pix, uint32_t c) {\n  if (pix != 0 || !_valid) return; //only react to first pixel\n  if (Bus::_cct >= 1900 && (_type == TYPE_ANALOG_3CH || _type == TYPE_ANALOG_4CH)) {\n    c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT\n  }\n  uint8_t cctWW, cctCW;\n  if (_type != TYPE_ANALOG_3CH) c = autoWhiteCalc(c, cctWW, cctCW);\n  uint8_t r = R(c), g = G(c), b = B(c), w = W(c);\n  // note: no color scaling, brightness is applied in show()\n\n  switch (_type) {\n    case TYPE_ANALOG_1CH: //one channel (white), relies on auto white calculation\n      _data[0] = w;\n      break;\n    case TYPE_ANALOG_2CH: //warm white + cold white\n      if (cctICused) {\n        _data[0] = w;\n        _data[1] = Bus::_cct < 0 || Bus::_cct > 255 ? 127 : Bus::_cct;\n      } else {\n        _data[0] = cctWW;\n        _data[1] = cctCW;\n      }\n      break;\n    case TYPE_ANALOG_5CH: //RGB + warm white + cold white\n      if (cctICused)\n        _data[4] = Bus::_cct < 0 || Bus::_cct > 255 ? 127 : Bus::_cct;\n      else {\n        w = cctWW;\n        _data[4] = cctCW;\n      }\n      // fall through to set RGBW channels\n    case TYPE_ANALOG_4CH: //RGBW\n      _data[3] = w;\n    case TYPE_ANALOG_3CH: //standard dumb RGB\n      _data[0] = r; _data[1] = g; _data[2] = b;\n      break;\n  }\n}\n\n//does no index check\nuint32_t BusPwm::getPixelColor(unsigned pix) const {\n  if (!_valid) return 0;\n  // TODO getting the reverse from CCT is involved (a quick approximation when CCT blending is ste to 0 implemented)\n  switch (_type) {\n    case TYPE_ANALOG_1CH: //one channel (white), relies on auto white calculation\n      return RGBW32(0, 0, 0, _data[0]);\n    case TYPE_ANALOG_2CH: //warm white + cold white\n      if (cctICused) return RGBW32(0, 0, 0, _data[0]);\n      else           return RGBW32(0, 0, 0, _data[0] + _data[1]);\n    case TYPE_ANALOG_5CH: //RGB + warm white + cold white\n      if (cctICused) return RGBW32(_data[0], _data[1], _data[2], _data[3]);\n      else           return RGBW32(_data[0], _data[1], _data[2], _data[3] + _data[4]);\n    case TYPE_ANALOG_4CH: //RGBW\n      return RGBW32(_data[0], _data[1], _data[2], _data[3]);\n    case TYPE_ANALOG_3CH: //standard dumb RGB\n      return RGBW32(_data[0], _data[1], _data[2], 0);\n  }\n  return RGBW32(_data[0], _data[0], _data[0], _data[0]);\n}\n\nvoid BusPwm::show() {\n  if (!_valid) return;\n  const size_t   numPins = getPins();\n#ifdef ESP8266\n   const unsigned analogPeriod = F_CPU / _frequency;\n   const unsigned maxBri = analogPeriod;  // compute to clock cycle accuracy\n   constexpr bool dithering = false;\n   constexpr unsigned bitShift = 8;  // 256 clocks for dead time, ~3us at 80MHz\n#else\n  // if _needsRefresh is true (UI hack) we are using dithering (credit @dedehai & @zalatnaicsongor)\n  // https://github.com/wled/WLED/pull/4115 and https://github.com/zalatnaicsongor/WLED/pull/1)\n  const bool     dithering = _needsRefresh; // avoid working with bitfield\n  const unsigned maxBri = (1<<_depth);      // possible values: 16384 (14), 8192 (13), 4096 (12), 2048 (11), 1024 (10), 512 (9) and 256 (8)\n  const unsigned bitShift = dithering * 4;  // if dithering, _depth is 12 bit but LEDC channel is set to 8 bit (using 4 fractional bits)\n#endif\n  // use CIE brightness formula (linear + cubic) to approximate human eye perceived brightness\n  // see: https://en.wikipedia.org/wiki/Lightness\n  unsigned pwmBri = _bri;\n  if (pwmBri < 21) {                                   // linear response for values [0-20]\n    pwmBri = (pwmBri * maxBri + 2300 / 2) / 2300 ;     // adding '0.5' before division for correct rounding, 2300 gives a good match to CIE curve\n  } else {                                             // cubic response for values [21-255]\n    float temp = float(pwmBri + 41) / float(255 + 41); // 41 is to match offset & slope to linear part\n    temp = temp * temp * temp * (float)maxBri;\n    pwmBri = (unsigned)temp;                           // pwmBri is in range [0-maxBri] C\n  }\n\n  [[maybe_unused]] unsigned hPoint = 0;  // phase shift (0 - maxBri)\n  // we will be phase shifting every channel by previous pulse length (plus dead time if required)\n  // phase shifting is only mandatory when using H-bridge to drive reverse-polarity PWM CCT (2 wire) LED type\n  // CCT additive blending must be 0 (WW & CW will not overlap) otherwise signals *will* overlap\n  // for all other cases it will just try to \"spread\" the load on PSU\n  // Phase shifting requires that LEDC timers are synchronised (see setup()). For PWM CCT (and H-bridge) it is\n  // also mandatory that both channels use the same timer (pinManager takes care of that).\n  for (unsigned i = 0; i < numPins; i++) {\n    unsigned duty = (_data[i] * pwmBri) / 255;\n    unsigned deadTime = 0;\n\n    if (_type == TYPE_ANALOG_2CH && Bus::_cctBlend <= 0) {\n      // add dead time between signals (when using dithering, two full 8bit pulses are required)\n      deadTime = (1+dithering) << bitShift;\n      // we only need to take care of shortening the signal at (almost) full brightness otherwise pulses may overlap\n      if (_bri >= 254 && duty >= maxBri / 2 && duty < maxBri) {\n        duty -= deadTime << 1; // shorten duty of larger signal except if full on\n      }\n    }\n    if (_reversed) {\n      if (i) hPoint += duty;  // align start at time zero\n      duty = maxBri - duty;\n    }\n    #ifdef ESP8266\n    //stopWaveform(_pins[i]);  // can cause the waveform to miss a cycle. instead we risk crossovers.\n    startWaveformClockCycles(_pins[i], duty, analogPeriod - duty, 0, i ? _pins[0] : -1, hPoint, false);\n    #else\n    unsigned channel = _ledcStart + i;\n    unsigned gr = channel/8;  // high/low speed group\n    unsigned ch = channel%8;  // group channel\n    // directly write to LEDC struct as there is no HAL exposed function for dithering\n    // duty has 20 bit resolution with 4 fractional bits (24 bits in total)\n    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\n    LEDC.channel_group[gr].channel[ch].hpoint.hpoint = hPoint >> bitShift;    // hPoint is at _depth resolution (needs shifting if dithering)\n    ledc_update_duty((ledc_mode_t)gr, (ledc_channel_t)ch);\n    #endif\n\n    if (!_reversed) hPoint += duty;\n    hPoint += deadTime;        // offset to cascade the signals\n    if (hPoint >= maxBri) hPoint -= maxBri; // offset is out of bounds, reset\n  }\n}\n\nsize_t BusPwm::getPins(uint8_t* pinArray) const {\n  if (!_valid) return 0;\n  unsigned numPins = numPWMPins(_type);\n  if (pinArray) for (unsigned i = 0; i < numPins; i++) pinArray[i] = _pins[i];\n  return numPins;\n}\n\n// credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056\nstd::vector<LEDType> BusPwm::getLEDTypes() {\n  return {\n    {TYPE_ANALOG_1CH, \"A\",      PSTR(\"PWM White\")},\n    {TYPE_ANALOG_2CH, \"AA\",     PSTR(\"PWM CCT\")},\n    {TYPE_ANALOG_3CH, \"AAA\",    PSTR(\"PWM RGB\")},\n    {TYPE_ANALOG_4CH, \"AAAA\",   PSTR(\"PWM RGBW\")},\n    {TYPE_ANALOG_5CH, \"AAAAA\",  PSTR(\"PWM RGB+CCT\")},\n    //{TYPE_ANALOG_6CH, \"AAAAAA\", PSTR(\"PWM RGB+DCCT\")}, // unimplementable ATM\n  };\n}\n\nvoid BusPwm::deallocatePins() {\n  size_t numPins = getPins();\n  for (unsigned i = 0; i < numPins; i++) {\n    PinManager::deallocatePin(_pins[i], PinOwner::BusPwm);\n    if (!PinManager::isPinOk(_pins[i])) continue;\n    #ifdef ESP8266\n    digitalWrite(_pins[i], LOW); //turn off PWM interrupt\n    #else\n    if (_ledcStart < WLED_MAX_ANALOG_CHANNELS) ledcDetachPin(_pins[i]);\n    #endif\n  }\n  #ifdef ARDUINO_ARCH_ESP32\n  PinManager::deallocateLedc(_ledcStart, numPins);\n  #endif\n}\n\n\nBusOnOff::BusOnOff(const BusConfig &bc)\n: Bus(bc.type, bc.start, bc.autoWhite, 1, bc.reversed)\n, _data(0)\n{\n  if (!Bus::isOnOff(bc.type)) return;\n\n  uint8_t currentPin = bc.pins[0];\n  if (!PinManager::allocatePin(currentPin, true, PinOwner::BusOnOff)) {\n    return;\n  }\n  _pin = currentPin; //store only after allocatePin() succeeds\n  pinMode(_pin, OUTPUT);\n  _hasRgb = false;\n  _hasWhite = false;\n  _hasCCT = false;\n  _valid = true;\n  DEBUGBUS_PRINTF_P(PSTR(\"%successfully inited On/Off strip with pin %u\\n\"), _valid?\"S\":\"Uns\", _pin);\n}\n\nvoid BusOnOff::setPixelColor(unsigned pix, uint32_t c) {\n  if (pix != 0 || !_valid) return; //only react to first pixel\n  _data = (c > 0) && bool(_bri) ? 0xFF : 0; // if any color channel is on and brightness is not zero, set to on\n}\n\nuint32_t BusOnOff::getPixelColor(unsigned pix) const {\n  if (!_valid) return 0;\n  return RGBW32(_data, _data, _data, _data);\n}\n\nvoid BusOnOff::show() {\n  if (!_valid) return;\n  digitalWrite(_pin, _reversed ? !(bool)_data : (bool)_data);\n}\n\nsize_t BusOnOff::getPins(uint8_t* pinArray) const {\n  if (!_valid) return 0;\n  if (pinArray) pinArray[0] = _pin;\n  return 1;\n}\n\n// credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056\nstd::vector<LEDType> BusOnOff::getLEDTypes() {\n  return {\n    {TYPE_ONOFF, \"\", PSTR(\"On/Off\")},\n  };\n}\n\nBusNetwork::BusNetwork(const BusConfig &bc)\n: Bus(bc.type, bc.start, bc.autoWhite, bc.count)\n, _broadcastLock(false)\n{\n  switch (bc.type) {\n    case TYPE_NET_ARTNET_RGB:\n      _UDPtype = 2;\n      break;\n    case TYPE_NET_ARTNET_RGBW:\n      _UDPtype = 2;\n      break;\n    case TYPE_NET_E131_RGB:\n      _UDPtype = 1;\n      break;\n    default: // TYPE_NET_DDP_RGB / TYPE_NET_DDP_RGBW\n      _UDPtype = 0;\n      break;\n  }\n  _hasRgb = hasRGB(bc.type);\n  _hasWhite = hasWhite(bc.type);\n  _hasCCT = false;\n  _UDPchannels = _hasWhite + 3;\n  _client = IPAddress(bc.pins[0],bc.pins[1],bc.pins[2],bc.pins[3]);\n  #ifdef ARDUINO_ARCH_ESP32\n  _hostname = bc.text;\n  resolveHostname(); // resolve hostname to IP address if needed\n  #endif\n  _data = (uint8_t*)d_calloc(_len, _UDPchannels);\n  _valid = (_data != nullptr);\n  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]);\n}\n\nvoid BusNetwork::setPixelColor(unsigned pix, uint32_t c) {\n  if (!_valid || pix >= _len) return;\n  uint8_t ww, cw; // dummy, unused\n  if (_hasWhite) c = autoWhiteCalc(c, ww, cw);\n  if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT\n  unsigned offset = pix * _UDPchannels;\n  _data[offset]   = R(c);\n  _data[offset+1] = G(c);\n  _data[offset+2] = B(c);\n  if (_hasWhite) _data[offset+3] = W(c);\n}\n\nuint32_t BusNetwork::getPixelColor(unsigned pix) const {\n  if (!_valid || pix >= _len) return 0;\n  unsigned offset = pix * _UDPchannels;\n  return RGBW32(_data[offset], _data[offset+1], _data[offset+2], (hasWhite() ? _data[offset+3] : 0));\n}\n\nvoid BusNetwork::show() {\n  if (!_valid || !canShow()) return;\n  _broadcastLock = true;\n  realtimeBroadcast(_UDPtype, _client, _len, _data, _bri, hasWhite());\n  _broadcastLock = false;\n}\n\nsize_t BusNetwork::getPins(uint8_t* pinArray) const {\n  if (pinArray) for (unsigned i = 0; i < 4; i++) pinArray[i] = _client[i];\n  return 4;\n}\n\n#ifdef ARDUINO_ARCH_ESP32\nvoid BusNetwork::resolveHostname() {\n  static unsigned long nextResolve = 0;\n  if (Network.isConnected() && millis() > nextResolve && _hostname.length() > 0) {\n    nextResolve = millis() + 600000; // resolve only every 10 minutes\n    IPAddress clnt;\n    if (strlen(cmDNS) > 0) clnt = MDNS.queryHost(_hostname);\n    else WiFi.hostByName(_hostname.c_str(), clnt);\n    if (clnt != IPAddress()) _client = clnt;\n  }\n}\n#endif\n\n// credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056\nstd::vector<LEDType> BusNetwork::getLEDTypes() {\n  return {\n    {TYPE_NET_DDP_RGB,     \"N\",     PSTR(\"DDP RGB (network)\")},      // should be \"NNNN\" to determine 4 \"pin\" fields\n    {TYPE_NET_ARTNET_RGB,  \"N\",     PSTR(\"Art-Net RGB (network)\")},\n    {TYPE_NET_DDP_RGBW,    \"N\",     PSTR(\"DDP RGBW (network)\")},\n    {TYPE_NET_ARTNET_RGBW, \"N\",     PSTR(\"Art-Net RGBW (network)\")},\n    // hypothetical extensions\n    //{TYPE_VIRTUAL_I2C_W,   \"V\",     PSTR(\"I2C White (virtual)\")}, // allows setting I2C address in _pin[0]\n    //{TYPE_VIRTUAL_I2C_CCT, \"V\",     PSTR(\"I2C CCT (virtual)\")}, // allows setting I2C address in _pin[0]\n    //{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]\n    //{TYPE_USERMOD,         \"VVVVV\", PSTR(\"Usermod (virtual)\")}, // 5 data fields (see https://github.com/wled/WLED/pull/4123)\n  };\n}\n\nvoid BusNetwork::cleanup() {\n  DEBUGBUS_PRINTLN(F(\"Virtual Cleanup.\"));\n  d_free(_data);\n  _data = nullptr;\n  _type = I_NONE;\n  _valid = false;\n}\n\n// ***************************************************************************\n\n#ifdef WLED_ENABLE_HUB75MATRIX\n#warning \"HUB75 driver enabled (experimental)\"\n#ifdef ESP8266\n#error ESP8266 does not support HUB75\n#endif\n\nBusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite) {\n  size_t lastHeap = ESP.getFreeHeap();\n  _valid = false;\n  _hasRgb = true;\n  _hasWhite = false;\n  virtualDisp = nullptr; // todo: this should be solved properly, can cause memory leak (if omitted here, nothing seems to work)\n  // aliases for easier reading\n  uint8_t panelWidth  = bc.pins[0];\n  uint8_t panelHeight = bc.pins[1];\n  uint8_t chainLength = bc.pins[2];\n  _rows = bc.pins[3];\n  _cols = bc.pins[4];\n\n  mxconfig.double_buff = false; // Use our own memory-optimised buffer rather than the driver's own double-buffer\n\n  // mxconfig.driver = HUB75_I2S_CFG::ICN2038S;  // experimental - use specific shift register driver\n  // mxconfig.driver = HUB75_I2S_CFG::FM6124;    // try this driver in case you panel stays dark, or when colors look too pastel\n\n  // mxconfig.latch_blanking = 3;\n  // mxconfig.i2sspeed = HUB75_I2S_CFG::HZ_10M;  // experimental - 5MHZ should be enugh, but colours looks slightly better at 10MHz\n  // mxconfig.min_refresh_rate = 90;\n  // mxconfig.min_refresh_rate = 120;\n\n  mxconfig.clkphase = bc.reversed;\n  // allow chain length up to 4, limit to prevent bad data from preventing boot due to low memory\n  mxconfig.chain_length = max((uint8_t) 1, min(chainLength, (uint8_t) 4));\n\n  if (mxconfig.mx_height >= 64 && (mxconfig.chain_length > 1)) {\n    DEBUGBUS_PRINTLN(F(\"WARNING, only single panel can be used of 64 pixel boards due to memory\"));\n    mxconfig.chain_length = 1;\n  }\n\n  if (bc.type == TYPE_HUB75MATRIX_HS) {\n      mxconfig.mx_width = min((uint8_t) 64, panelWidth); // TODO: UI limit is 128, this limits to 64\n      mxconfig.mx_height = min((uint8_t) 64, panelHeight);\n  } else if (bc.type == TYPE_HUB75MATRIX_QS) {\n      _isVirtual = true;\n      mxconfig.mx_width = min((uint8_t) 64, panelWidth) * 2;\n      mxconfig.mx_height = min((uint8_t) 64, panelHeight) / 2;\n  } else {\n    DEBUGBUS_PRINTLN(\"Unknown type\");\n    return;\n  }\n\n#if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2)// classic esp32, or esp32-s2: reduce bitdepth for large panels\n  if (mxconfig.mx_height >= 64) {\n    if (mxconfig.chain_length * mxconfig.mx_width > 192) mxconfig.setPixelColorDepthBits(3);\n    else if (mxconfig.chain_length * mxconfig.mx_width > 64)  mxconfig.setPixelColorDepthBits(4);\n    else mxconfig.setPixelColorDepthBits(8);\n  } else mxconfig.setPixelColorDepthBits(8);\n#endif\n\n\n\n//  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};\n\n#if defined(ARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3) // MatrixPortal ESP32-S3\n\n  // https://www.adafruit.com/product/5778\n  DEBUGBUS_PRINTLN(\"MatrixPanel_I2S_DMA - Matrix Portal S3 config\");\n  mxconfig.gpio = { 42, 41, 40, 38, 39, 37,  45, 36, 48, 35, 21, 47, 14, 2 };\n\n#elif defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM)// ESP32-S3 with PSRAM\n\n#if defined(MOONHUB_S3_PINOUT)\n  DEBUGBUS_PRINTLN(\"MatrixPanel_I2S_DMA - T7 S3 with PSRAM, MOONHUB pinout\");\n\n  // 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};\n  mxconfig.gpio = { 1, 5, 6, 7, 13, 9, 16, 48, 47, 21, 38, 8, 4, 18 };\n\n#else\n  DEBUGBUS_PRINTLN(\"MatrixPanel_I2S_DMA - S3 with PSRAM\");\n  // 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};\n  mxconfig.gpio = {1, 2, 42, 41, 40, 39, 45, 48, 47, 21, 38, 8, 3, 18};\n#endif\n#elif defined(ESP32_FORUM_PINOUT) // Common format for boards designed for SmartMatrix\n\n  DEBUGBUS_PRINTLN(\"MatrixPanel_I2S_DMA - ESP32_FORUM_PINOUT\");\n/*\n    ESP32 with SmartMatrix's default pinout - ESP32_FORUM_PINOUT\n    https://github.com/pixelmatix/SmartMatrix/blob/teensylc/src/MatrixHardware_ESP32_V0.h\n    Can use a board like https://github.com/rorosaurus/esp32-hub75-driver\n*/\n\n mxconfig.gpio = { 2, 15, 4, 16, 27, 17, 5, 18, 19, 21, 12, 26, 25, 22 };\n\n#else\n  DEBUGBUS_PRINTLN(\"MatrixPanel_I2S_DMA - Default pins\");\n  /*\n   https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA?tab=readme-ov-file\n\n   Boards\n\n   https://esp32trinity.com/\n   https://www.electrodragon.com/product/rgb-matrix-panel-drive-interface-board-for-esp32-dma/\n\n  */\n mxconfig.gpio = { 25, 26, 27, 14, 12, 13, 23, 19, 5, 17, 18, 4, 15, 16 };\n\n#endif\n\n  int8_t pins[PIN_COUNT];\n  memcpy(pins, &mxconfig.gpio, sizeof(mxconfig.gpio));\n  if (!PinManager::allocateMultiplePins(pins, PIN_COUNT, PinOwner::HUB75, true)) {\n    DEBUGBUS_PRINTLN(\"Failed to allocate pins for HUB75\");\n    return;\n  }\n\n  if (bc.colorOrder == COL_ORDER_RGB) {\n    DEBUGBUS_PRINTLN(\"MatrixPanel_I2S_DMA = Default color order (RGB)\");\n  } else if (bc.colorOrder == COL_ORDER_BGR) {\n    DEBUGBUS_PRINTLN(\"MatrixPanel_I2S_DMA = color order BGR\");\n    int8_t tmpPin;\n    tmpPin = mxconfig.gpio.r1;\n    mxconfig.gpio.r1 = mxconfig.gpio.b1;\n    mxconfig.gpio.b1 = tmpPin;\n    tmpPin = mxconfig.gpio.r2;\n    mxconfig.gpio.r2 = mxconfig.gpio.b2;\n    mxconfig.gpio.b2 = tmpPin;\n  }\n  else {\n    DEBUGBUS_PRINTF(\"MatrixPanel_I2S_DMA = unsupported color order %u\\n\", bc.colorOrder);\n  }\n\n  DEBUGBUS_PRINTF(\"MatrixPanel_I2S_DMA config - %ux%u length: %u\\n\", mxconfig.mx_width, mxconfig.mx_height, mxconfig.chain_length);\n  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\",\n                mxconfig.gpio.r1, mxconfig.gpio.g1, mxconfig.gpio.b1, mxconfig.gpio.r2, mxconfig.gpio.g2, mxconfig.gpio.b2,\n                mxconfig.gpio.a, mxconfig.gpio.b, mxconfig.gpio.c, mxconfig.gpio.d, mxconfig.gpio.e, mxconfig.gpio.lat, mxconfig.gpio.oe, mxconfig.gpio.clk);\n\n  // OK, now we can create our matrix object\n  display = new MatrixPanel_I2S_DMA(mxconfig);\n  if (display == nullptr) {\n      DEBUGBUS_PRINTLN(\"****** MatrixPanel_I2S_DMA !KABOOM! driver allocation failed ***********\");\n      DEBUGBUS_PRINT(F(\"heap usage: \")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap());\n      return;\n  }\n\n  this->_len = (display->width() * display->height()); // note: this returns correct number of pixels but incorrect dimensions if using virtual display (updated below)\n\n  DEBUGBUS_PRINTF(\"Length: %u\\n\", _len);\n  if (this->_len >= MAX_LEDS) {\n    DEBUGBUS_PRINTLN(\"MatrixPanel_I2S_DMA Too many LEDS - playing safe\");\n    return;\n  }\n\n  DEBUGBUS_PRINTLN(\"MatrixPanel_I2S_DMA created\");\n\n  // as noted in HUB75_I2S_DMA library, some panels can show ghosting if set higher than 239, so let users override at compile time\n  #ifndef WLED_HUB75_MAX_BRIGHTNESS\n  #define WLED_HUB75_MAX_BRIGHTNESS 255\n  #endif\n  // let's adjust default brightness (128), brightness scaling is handled by WLED\n  //display->setBrightness8(WLED_HUB75_MAX_BRIGHTNESS); // range is 0-255, 0 - 0%, 255 - 100%\n\n  delay(24); // experimental\n  DEBUGBUS_PRINT(F(\"heap usage: \")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap());\n  // Allocate memory and start DMA display\n  if (!display->begin() ) {\n      DEBUGBUS_PRINTLN(\"****** MatrixPanel_I2S_DMA !KABOOM! I2S memory allocation failed ***********\");\n      DEBUGBUS_PRINT(F(\"heap usage: \")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap());\n      return;\n  }\n  else {\n    DEBUGBUS_PRINTLN(\"MatrixPanel_I2S_DMA begin ok\");\n    DEBUGBUS_PRINT(F(\"heap usage: \")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap());\n    delay(18);   // experiment - give the driver a moment (~ one full frame @ 60hz) to settle\n    _valid = true;\n    display->clearScreen();   // initially clear the screen buffer\n    DEBUGBUS_PRINTLN(\"MatrixPanel_I2S_DMA clear ok\");\n\n    if (_ledBuffer) d_free(_ledBuffer);                 // should not happen\n    if (_ledsDirty) d_free(_ledsDirty);                 // should not happen\n    DEBUGBUS_PRINTLN(\"MatrixPanel_I2S_DMA allocate memory\");\n    _ledsDirty = (byte*) d_malloc(getBitArrayBytes(_len));  // create LEDs dirty bits\n    DEBUGBUS_PRINTLN(\"MatrixPanel_I2S_DMA allocate memory ok\");\n\n    if (_ledsDirty == nullptr) {\n      display->stopDMAoutput();\n      delete display; display = nullptr;\n      _valid = false;\n      DEBUGBUS_PRINTLN(F(\"MatrixPanel_I2S_DMA not started - not enough memory for dirty bits!\"));\n      DEBUGBUS_PRINT(F(\"heap usage: \")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap());\n      return;  //  fail is we cannot get memory for the buffer\n    }\n    setBitArray(_ledsDirty, _len, false);             // reset dirty bits\n\n    if (mxconfig.double_buff == false) {\n      // 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)\n      _ledBuffer = static_cast<CRGB*>(allocate_buffer(_len * sizeof(CRGB), BFRALLOC_PREFER_DRAM | BFRALLOC_CLEAR));\n    }\n  }\n\n  PANEL_CHAIN_TYPE chainType = CHAIN_NONE; // default for quarter-scan panels that do not use chaining\n  // chained panels with cols and rows define need the virtual display driver, so do quarter-scan panels\n  if (chainLength > 1 && (_rows > 1 || _cols > 1) || bc.type == TYPE_HUB75MATRIX_QS) {\n    _isVirtual = true;\n    chainType = CHAIN_BOTTOM_LEFT_UP; // TODO: is there any need to support other chaining types?\n    DEBUGBUS_PRINTF_P(PSTR(\"Using virtual matrix: %ux%u panels of %ux%u pixels\\n\"), _cols, _rows, mxconfig.mx_width, mxconfig.mx_height);\n  }\n  else {\n    _isVirtual = false;\n  }\n\n  if (_isVirtual) {\n    virtualDisp = new VirtualMatrixPanel((*display), _rows, _cols, mxconfig.mx_width, mxconfig.mx_height, chainType);\n    virtualDisp->setRotation(0);\n    if (bc.type == TYPE_HUB75MATRIX_QS) {\n      switch(panelHeight) {\n        case 16:\n          virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_16PX_HIGH);\n          break;\n        case 32:\n          virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_32PX_HIGH);\n          break;\n        case 64:\n          virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_64PX_HIGH);\n          break;\n        default:\n          DEBUGBUS_PRINTLN(\"Unsupported height\");\n          cleanup();\n          return;\n      }\n    }\n  }\n\n  if (_valid) {\n    _panelWidth = virtualDisp ? virtualDisp->width() : display->width();  // cache width - it will never change\n  }\n\n  DEBUGBUS_PRINT(F(\"MatrixPanel_I2S_DMA \"));\n  DEBUGBUS_PRINTF_P(PSTR(\"%sstarted, width=%u, %u pixels.\\n\"), _valid? \"\":\"not \", _panelWidth, _len);\n\n  if (_ledBuffer != nullptr) DEBUGBUS_PRINTLN(F(\"MatrixPanel_I2S_DMA LEDS buffer enabled.\"));\n  if (_ledsDirty != nullptr) DEBUGBUS_PRINTLN(F(\"MatrixPanel_I2S_DMA LEDS dirty bit optimization enabled.\"));\n  if ((_ledBuffer != nullptr) || (_ledsDirty != nullptr)) {\n    DEBUGBUS_PRINT(F(\"MatrixPanel_I2S_DMA LEDS buffer uses \"));\n    DEBUGBUS_PRINT((_ledBuffer? _len*sizeof(CRGB) :0) + (_ledsDirty? getBitArrayBytes(_len) :0));\n    DEBUGBUS_PRINTLN(F(\" bytes.\"));\n  }\n}\n\nvoid IRAM_ATTR BusHub75Matrix::setPixelColor(unsigned pix, uint32_t c) {\n  if (!_valid) return; // note: no need to check pix >= _len as that is checked in containsPixel()\n  // if (_cct >= 1900) c = colorBalanceFromKelvin(_cct, c); //color correction from CCT\n\n  if (_ledBuffer) {\n    CRGB fastled_col = CRGB(c);\n    if (_ledBuffer[pix] != fastled_col) {\n      _ledBuffer[pix] = fastled_col;\n      setBitInArray(_ledsDirty, pix, true);  // flag pixel as \"dirty\"\n    }\n  }\n  else {\n    if ((c == IS_BLACK) && (getBitFromArray(_ledsDirty, pix) == false)) return; // ignore black if pixel is already black\n    setBitInArray(_ledsDirty, pix, c != IS_BLACK);                              // dirty = true means \"color is not BLACK\"\n\n    uint8_t r = R(c);\n    uint8_t g = G(c);\n    uint8_t b = B(c);\n\n    if (virtualDisp != nullptr) {\n      int x = pix % _panelWidth; // TODO: check if using & and shift would be faster here, it limits to power-of-2 widths though\n      int y = pix / _panelWidth;\n      virtualDisp->drawPixelRGB888(int16_t(x), int16_t(y), r, g, b);\n    } else {\n      int x = pix % _panelWidth;\n      int y = pix / _panelWidth;\n      display->drawPixelRGB888(int16_t(x), int16_t(y), r, g, b);\n    }\n  }\n}\n\nuint32_t BusHub75Matrix::getPixelColor(unsigned pix) const {\n  if (!_valid) return IS_BLACK; // note: no need to check pix >= _len as that is checked in containsPixel()\n  if (_ledBuffer)\n    return uint32_t(_ledBuffer[pix]);\n  else\n    return getBitFromArray(_ledsDirty, pix) ? IS_DARKGREY: IS_BLACK;   // just a hack - we only know if the pixel is black or not\n}\n\nvoid BusHub75Matrix::setBrightness(uint8_t b) {\n  _bri = b;\n  display->setBrightness(_bri); \n}\n\nvoid BusHub75Matrix::show(void) {\n  if (!_valid) return;\n  if (_ledBuffer) {\n    // write out buffered LEDs\n    unsigned height = _isVirtual ? virtualDisp->height() : display->height();\n    unsigned width = _panelWidth;\n\n    //while(!previousBufferFree) delay(1);   // experimental - Wait before we allow any writing to the buffer. Stop flicker.\n    size_t pix = 0; // running pixel index\n    for (int y=0; y<height; y++) for (int x=0; x<width; x++) {\n      if (getBitFromArray(_ledsDirty, pix) == true) {        // only repaint the \"dirty\"  pixels\n        CRGB c = _ledBuffer[pix];\n        //c.nscale8_video(_bri); // apply brightness\n        if (_isVirtual) virtualDisp->drawPixelRGB888(int16_t(x), int16_t(y), c.r, c.g, c.b);\n        else                display->drawPixelRGB888(int16_t(x), int16_t(y), c.r, c.g, c.b);\n      }\n      pix++;\n    }\n    setBitArray(_ledsDirty, _len, false);  // buffer shown - reset all dirty bits\n  }\n}\n\nvoid BusHub75Matrix::cleanup() {\n  if (display && _valid) display->stopDMAoutput();  // terminate DMA driver (display goes black)\n  _valid = false;\n  delay(30); // give some time to finish DMA\n  _panelWidth = 0;\n  deallocatePins();\n  DEBUGBUS_PRINTLN(F(\"HUB75 output ended.\"));\n  #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\n  if (virtualDisp != nullptr) delete virtualDisp; // note: in MM there is a warning to not do this but if using \"NO_GFX\" this is safe\n  if (display != nullptr) delete display;\n  display = nullptr;\n  virtualDisp = nullptr; // note: when not using \"NO_GFX\" this causes a memory leak\n  #endif\n  if (_ledBuffer != nullptr) d_free(_ledBuffer); _ledBuffer = nullptr;\n  if (_ledsDirty != nullptr) d_free(_ledsDirty); _ledsDirty = nullptr;\n}\n\nvoid BusHub75Matrix::deallocatePins() {\n  uint8_t pins[PIN_COUNT];\n  memcpy(pins, &mxconfig.gpio, sizeof(mxconfig.gpio));\n  PinManager::deallocateMultiplePins(pins, PIN_COUNT, PinOwner::HUB75);\n}\n\nstd::vector<LEDType> BusHub75Matrix::getLEDTypes() {\n  return {\n    {TYPE_HUB75MATRIX_HS,     \"H\",     PSTR(\"HUB75 (Half Scan)\")},\n    {TYPE_HUB75MATRIX_QS,     \"H\",     PSTR(\"HUB75 (Quarter Scan)\")},\n  };\n}\n\nsize_t BusHub75Matrix::getPins(uint8_t* pinArray) const {\n  if (pinArray) {\n    pinArray[0] = mxconfig.mx_width;\n    pinArray[1] = mxconfig.mx_height;\n    pinArray[2] = mxconfig.chain_length;\n    pinArray[3] = _rows;\n    pinArray[4] = _cols;\n  }\n  return 5;\n}\n\n#endif\n// ***************************************************************************\n\nBusPlaceholder::BusPlaceholder(const BusConfig &bc)\n: Bus(bc.type, bc.start, bc.autoWhite, bc.count, bc.reversed, bc.refreshReq)\n, _colorOrder(bc.colorOrder)\n, _skipAmount(bc.skipAmount)\n, _driverType(bc.driverType)\n, _frequency(bc.frequency)\n, _milliAmpsPerLed(bc.milliAmpsPerLed)\n, _milliAmpsMax(bc.milliAmpsMax)\n, _text(bc.text)\n{\n  memcpy(_pins, bc.pins, sizeof(_pins));\n}\n\nsize_t BusPlaceholder::getPins(uint8_t* pinArray) const {\n  size_t nPins = Bus::getNumberOfPins(_type);\n  if (pinArray) {\n    for (size_t i = 0; i < nPins; i++) pinArray[i] = _pins[i];\n  }\n  return nPins;\n}\n\n//utility to get the approx. memory usage of a given BusConfig inclduding segmentbuffer and global buffer (4 bytes per pixel)\nsize_t BusConfig::memUsage() const {\n  size_t mem = (count + skipAmount) * 8; // 8 bytes per pixel for segment + global buffer\n  if (Bus::isVirtual(type)) {\n    mem += sizeof(BusNetwork) + (count * Bus::getNumberOfChannels(type)); // note: getNumberOfChannels() includes CCT channel if applicable but virtual buses do not use CCT channel buffer\n  } else if (Bus::isDigital(type)) {\n    // if any of digital buses uses I2S, there is additional common I2S DMA buffer not accounted for here\n    mem += sizeof(BusDigital) + PolyBus::memUsage(count + skipAmount, iType);\n  } else if (Bus::isOnOff(type)) {\n    mem += sizeof(BusOnOff);\n  } else {\n    mem += sizeof(BusPwm);\n  }\n  return mem;\n}\n\nint BusManager::add(const BusConfig &bc, bool placeholder) {\n  DEBUGBUS_PRINTF_P(PSTR(\"Bus: Adding bus (p:%d v:%d)\\n\"), getNumBusses(), getNumVirtualBusses());\n  unsigned digital = 0;\n  unsigned analog  = 0;\n  unsigned twoPin  = 0;\n  for (const auto &bus : busses) {\n    if (bus->isPWM()) analog += bus->getPins(); // number of analog channels used\n    if (bus->isDigital() && !bus->is2Pin()) digital++;\n    if (bus->is2Pin()) twoPin++;\n  }\n  digital += (Bus::isDigital(bc.type) && !Bus::is2Pin(bc.type));\n  analog  += (Bus::isPWM(bc.type) ? Bus::numPWMPins(bc.type) : 0);\n  if (digital > WLED_MAX_DIGITAL_CHANNELS || analog > WLED_MAX_ANALOG_CHANNELS) placeholder = true; // TODO: add errorFlag here\n  if (placeholder) {\n    busses.push_back(make_unique<BusPlaceholder>(bc));\n  } else if (Bus::isVirtual(bc.type)) {\n    busses.push_back(make_unique<BusNetwork>(bc));\n#ifdef WLED_ENABLE_HUB75MATRIX\n  } else if (Bus::isHub75(bc.type)) {\n    busses.push_back(make_unique<BusHub75Matrix>(bc));\n#endif\n  } else if (Bus::isDigital(bc.type)) {\n    busses.push_back(make_unique<BusDigital>(bc));\n  } else if (Bus::isOnOff(bc.type)) {\n    busses.push_back(make_unique<BusOnOff>(bc));\n  } else {\n    busses.push_back(make_unique<BusPwm>(bc));\n  }\n  return busses.size();\n}\n\n// credit @willmmiles\nstatic String LEDTypesToJson(const std::vector<LEDType>& types) {\n  String json;\n  for (const auto &type : types) {\n    // capabilities follows similar pattern as JSON API\n    int capabilities = Bus::hasRGB(type.id) | Bus::hasWhite(type.id)<<1 | Bus::hasCCT(type.id)<<2 | Bus::is16bit(type.id)<<4 | Bus::mustRefresh(type.id)<<5;\n    char str[256];\n    sprintf_P(str, PSTR(\"{i:%d,c:%d,t:\\\"%s\\\",n:\\\"%s\\\"},\"), type.id, capabilities, type.type, type.name);\n    json += str;\n  }\n  return json;\n}\n\n// credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056\nString BusManager::getLEDTypesJSONString() {\n  String json = \"[\";\n  json += LEDTypesToJson(BusDigital::getLEDTypes());\n  json += LEDTypesToJson(BusOnOff::getLEDTypes());\n  json += LEDTypesToJson(BusPwm::getLEDTypes());\n  json += LEDTypesToJson(BusNetwork::getLEDTypes());\n  //json += LEDTypesToJson(BusVirtual::getLEDTypes());\n  #ifdef WLED_ENABLE_HUB75MATRIX\n  json += LEDTypesToJson(BusHub75Matrix::getLEDTypes());\n  #endif\n\n  json.setCharAt(json.length()-1, ']'); // replace last comma with bracket\n  return json;\n}\n\nuint8_t BusManager::getI(uint8_t busType, const uint8_t* pins, uint8_t driverPreference) {\n  return PolyBus::getI(busType, pins, driverPreference);\n}\n//do not call this method from system context (network callback)\nvoid BusManager::removeAll() {\n  DEBUGBUS_PRINTLN(F(\"Removing all.\"));\n  //prevents crashes due to deleting busses while in use.\n  while (!canAllShow()) yield();\n  busses.clear();\n  #ifndef ESP8266\n  // Reset channel tracking for fresh allocation\n  PolyBus::resetChannelTracking();\n  #endif\n}\n\n#ifdef ESP32_DATA_IDLE_HIGH\n// #2478\n// If enabled, RMT idle level is set to HIGH when off\n// to prevent leakage current when using an N-channel MOSFET to toggle LED power\n// since I2S outputs are known only during config of buses, lets just assume RMT is used for digital buses\n// unused RMT channels should have no effect\nvoid BusManager::esp32RMTInvertIdle() {\n  bool idle_out;\n  unsigned rmt = 0;\n  unsigned u = 0;\n  for (auto &bus : busses) {\n    if (bus->getLength()==0 || !bus->isDigital() || bus->is2Pin()) continue;\n    if (static_cast<BusDigital*>(bus.get())->isI2S()) continue;\n    if (u >= WLED_MAX_RMT_CHANNELS) return;\n    //assumes that bus number to rmt channel mapping stays 1:1\n    rmt_channel_t ch = static_cast<rmt_channel_t>(rmt);\n    rmt_idle_level_t lvl;\n    rmt_get_idle_level(ch, &idle_out, &lvl);\n    if (lvl == RMT_IDLE_LEVEL_HIGH) lvl = RMT_IDLE_LEVEL_LOW;\n    else if (lvl == RMT_IDLE_LEVEL_LOW) lvl = RMT_IDLE_LEVEL_HIGH;\n    else continue;\n    rmt_set_idle_level(ch, idle_out, lvl);\n    u++;\n  }\n}\n#endif\n\nvoid BusManager::on() {\n  #ifdef ESP8266\n  //Fix for turning off onboard LED breaking bus\n  if (PinManager::getPinOwner(LED_BUILTIN) == PinOwner::BusDigital) {\n    for (auto &bus : busses) {\n      uint8_t pins[2] = {255,255};\n      if (bus->isDigital() && bus->getPins(pins) && bus->isOk()) {\n        if (pins[0] == LED_BUILTIN || pins[1] == LED_BUILTIN) {\n          BusDigital &b = static_cast<BusDigital&>(*bus);\n          b.begin();\n          break;\n        }\n      }\n    }\n  }\n  #else\n  for (auto &bus : busses) if (bus->isVirtual()) {\n    // virtual/network bus should check for IP change if hostname is specified\n    // otherwise there are no endpoints to force DNS resolution\n    BusNetwork &b = static_cast<BusNetwork&>(*bus);\n    b.resolveHostname();\n  }\n  #endif\n  #ifdef ESP32_DATA_IDLE_HIGH\n  esp32RMTInvertIdle();\n  #endif\n}\n\nvoid BusManager::off() {\n  #ifdef ESP8266\n  // turn off built-in LED if strip is turned off\n  // this will break digital bus so will need to be re-initialised on On\n  if (PinManager::getPinOwner(LED_BUILTIN) == PinOwner::BusDigital) {\n    for (const auto &bus : busses) if (bus->isOffRefreshRequired()) return;\n    pinMode(LED_BUILTIN, OUTPUT);\n    digitalWrite(LED_BUILTIN, HIGH);\n  }\n  #endif\n  #ifdef ESP32_DATA_IDLE_HIGH\n  esp32RMTInvertIdle();\n  #endif\n  _gMilliAmpsUsed = 0; // reset, assume no LED idle current if relay is off\n}\n\nvoid BusManager::show() {\n  applyABL(); // apply brightness limit, updates _gMilliAmpsUsed\n  for (auto &bus : busses) {\n    bus->show();\n  }\n}\n\nvoid IRAM_ATTR BusManager::setPixelColor(unsigned pix, uint32_t c) {\n  for (auto &bus : busses) {\n    if (!bus->containsPixel(pix)) continue;\n    bus->setPixelColor(pix - bus->getStart(), c);\n  }\n}\n\nvoid BusManager::setSegmentCCT(int16_t cct, bool allowWBCorrection) {\n  if (cct > 255) cct = 255;\n  if (cct >= 0) {\n    //if white balance correction allowed, save as kelvin value instead of 0-255\n    if (allowWBCorrection) cct = 1900 + (cct << 5);\n  } else cct = -1; // will use kelvin approximation from RGB\n  Bus::setCCT(cct);\n}\n\nuint32_t BusManager::getPixelColor(unsigned pix) {\n  for (auto &bus : busses) {\n    if (!bus->containsPixel(pix)) continue;\n    return bus->getPixelColor(pix - bus->getStart());\n  }\n  return 0;\n}\n\nbool BusManager::canAllShow() {\n  for (const auto &bus : busses) if (!bus->canShow()) return false;\n  return true;\n}\n\nvoid BusManager::initializeABL() {\n  _useABL = false; // reset\n  if (_gMilliAmpsMax > 0) {\n    // check global brightness limit\n    for (auto &bus : busses) {\n      if (bus->isDigital() && bus->getLEDCurrent() > 0) {\n        _useABL = true; // at least one bus has valid LED current\n        return;\n      }\n    }\n  } else {\n    // check per bus brightness limit\n    unsigned numABLbuses = 0;\n    for (auto &bus : busses) {\n      if (bus->isDigital() && bus->getLEDCurrent() > 0 && bus->getMaxCurrent() > 0)\n        numABLbuses++; // count ABL enabled buses\n    }\n    if (numABLbuses > 0) {\n      _useABL = true; // at least one bus has ABL set\n      uint32_t ESPshare = MA_FOR_ESP / numABLbuses; // share of ESP current per ABL bus\n      for (auto &bus : busses) {\n        if (bus->isDigital() && bus->isOk()) {\n          BusDigital &busd = static_cast<BusDigital&>(*bus);\n          uint32_t busLength = busd.getLength();\n          uint32_t busDemand = busLength * busd.getLEDCurrent();\n          uint32_t busMax    = busd.getMaxCurrent();\n          if (busMax > ESPshare)  busMax -= ESPshare;\n          if (busMax < busLength) busMax  = busLength; // give each LED 1mA, ABL will dim down to minimum\n          if (busDemand == 0) busMax = 0; // no LED current set, disable ABL for this bus\n          busd.setCurrentLimit(busMax);\n        }\n      }\n    }\n  }\n}\n\nvoid BusManager::applyABL() {\n  if (_useABL) {\n    unsigned milliAmpsSum = 0; // use temporary variable to always return a valid _gMilliAmpsUsed to UI\n    unsigned totalLEDs = 0;\n    for (auto &bus : busses) {\n      if (bus->isDigital() && bus->isOk()) {\n        BusDigital &busd = static_cast<BusDigital&>(*bus);\n        busd.estimateCurrent(); // sets _milliAmpsTotal, current is estimated for all buses even if they have the limit set to 0\n        if (_gMilliAmpsMax == 0)\n          busd.applyBriLimit(0); // apply per bus ABL limit, updates _milliAmpsTotal if limit reached\n        milliAmpsSum += busd.getUsedCurrent();\n        totalLEDs += busd.getLength(); // sum total number of LEDs for global Limit\n      }\n    }\n    // check global current limit and apply global ABL limit, total current is summed above\n    if (_gMilliAmpsMax > 0) {\n      uint8_t  newBri = 255;\n      uint32_t globalMax = _gMilliAmpsMax > MA_FOR_ESP ? _gMilliAmpsMax - MA_FOR_ESP : 1; // subtract ESP current consumption, fully limit if too low\n      if (globalMax > totalLEDs) { // check if budget is larger than standby current\n        if (milliAmpsSum > globalMax) {\n          newBri = globalMax * 255 / milliAmpsSum + 1; // scale brightness down to stay in current limit, +1 to avoid 0 brightness\n          milliAmpsSum = globalMax; // update total used current\n        }\n      } else {\n        newBri = 1; // limit too low, set brightness to minimum\n        milliAmpsSum = totalLEDs; // estimate total used current as minimum\n      }\n\n      // apply brightness limit to each bus, if its 255 it will only reset _colorSum\n      for (auto &bus : busses) {\n        if (bus->isDigital() && bus->isOk()) {\n          BusDigital &busd = static_cast<BusDigital&>(*bus);\n          if (busd.getLEDCurrent() > 0)  // skip buses with LED current set to 0\n            busd.applyBriLimit(newBri);\n        }\n      }\n    }\n    _gMilliAmpsUsed = milliAmpsSum;\n  }\n  else\n    _gMilliAmpsUsed = 0; // reset, we have no current estimation without ABL\n}\n\nColorOrderMap& BusManager::getColorOrderMap() { return _colorOrderMap; }\n\n#ifndef ESP8266\n// PolyBus channel tracking for dynamic allocation\nbool PolyBus::_useParallelI2S = false;\nuint8_t PolyBus::_rmtChannelsAssigned = 0; // number of RMT channels assigned durig getI() check\nuint8_t PolyBus::_rmtChannel = 0;     // number of RMT channels actually used during bus creation in create()\nuint8_t PolyBus::_i2sChannelsAssigned = 0; // number of I2S channels assigned durig getI() check\nuint8_t PolyBus::_parallelBusItype = 0;    // type I_NONE\nuint8_t PolyBus::_2PchannelsAssigned = 0;\n#endif\n// Bus static member definition\nint16_t Bus::_cct = -1;     // -1 means use approximateKelvinFromRGB(), 0-255 is standard, >1900 use colorBalanceFromKelvin()\nint8_t  Bus::_cctBlend = 0; // -128 to +127\nuint8_t Bus::_gAWM = 255;\n\nuint16_t BusDigital::_milliAmpsTotal = 0;\n\nstd::vector<std::unique_ptr<Bus>> BusManager::busses;\nuint16_t BusManager::_gMilliAmpsUsed = 0;\nuint16_t BusManager::_gMilliAmpsMax = ABL_MILLIAMPS_DEFAULT;\nbool BusManager::_useABL = false;\n"
  },
  {
    "path": "wled00/bus_manager.h",
    "content": "#pragma once\n#ifndef BusManager_h\n#define BusManager_h\n\n#ifdef WLED_ENABLE_HUB75MATRIX\n\n#include <ESP32-HUB75-MatrixPanel-I2S-DMA.h>\n#include <ESP32-VirtualMatrixPanel-I2S-DMA.h>\n#include <FastLED.h>\n\n#endif\n/*\n * Class for addressing various light types\n */\n\n#include \"const.h\"\n#include \"pin_manager.h\"\n#include <vector>\n#include <memory>\n\n#if __cplusplus >= 201402L\nusing std::make_unique;\n#else\n// Really simple C++11 shim for non-array case; implementation from cppreference.com\ntemplate<class T, class... Args>\nstd::unique_ptr<T>\nmake_unique(Args&&... args)\n{\n    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));\n}\n#endif\n\n// enable additional debug output\n#if defined(WLED_DEBUG_HOST)\n  #include \"net_debug.h\"\n  #define DEBUGOUT NetDebug\n#else\n  #define DEBUGOUT Serial\n#endif\n\n#ifdef WLED_DEBUG_BUS\n  #ifndef ESP8266\n  #include <rom/rtc.h>\n  #endif\n  #define DEBUGBUS_PRINT(x) DEBUGOUT.print(x)\n  #define DEBUGBUS_PRINTLN(x) DEBUGOUT.println(x)\n  #define DEBUGBUS_PRINTF(x...) DEBUGOUT.printf(x)\n  #define DEBUGBUS_PRINTF_P(x...) DEBUGOUT.printf_P(x)\n#else\n  #define DEBUGBUS_PRINT(x)\n  #define DEBUGBUS_PRINTLN(x)\n  #define DEBUGBUS_PRINTF(x...)\n  #define DEBUGBUS_PRINTF_P(x...)\n#endif\n\n//colors.cpp\nuint16_t approximateKelvinFromRGB(uint32_t rgb);\n\n#define GET_BIT(var,bit)    (((var)>>(bit))&0x01)\n#define SET_BIT(var,bit)    ((var)|=(uint16_t)(0x0001<<(bit)))\n#define UNSET_BIT(var,bit)  ((var)&=(~(uint16_t)(0x0001<<(bit))))\n\n#define NUM_ICS_WS2812_1CH_3X(len) (((len)+2)/3)   // 1 WS2811 IC controls 3 zones (each zone has 1 LED, W)\n#define IC_INDEX_WS2812_1CH_3X(i)  ((i)/3)\n\n#define NUM_ICS_WS2812_2CH_3X(len) (((len)+1)*2/3) // 2 WS2811 ICs control 3 zones (each zone has 2 LEDs, CW and WW)\n#define IC_INDEX_WS2812_2CH_3X(i)  ((i)*2/3)\n#define WS2812_2CH_3X_SPANS_2_ICS(i) ((i)&0x01)    // every other LED zone is on two different ICs\n\nstruct BusConfig; // forward declaration\n\n// Defines an LED Strip and its color ordering.\ntypedef struct {\n  uint16_t start;\n  uint16_t len;\n  uint8_t colorOrder;\n} ColorOrderMapEntry;\n\nstruct ColorOrderMap {\n    bool add(uint16_t start, uint16_t len, uint8_t colorOrder);\n\n    inline uint8_t count() const { return _mappings.size(); }\n    inline void reserve(size_t num) { _mappings.reserve(num); }\n\n    void reset() {\n      _mappings.clear();\n      _mappings.shrink_to_fit();\n    }\n\n    const ColorOrderMapEntry* get(uint8_t n) const {\n      if (n >= count()) return nullptr;\n      return &(_mappings[n]);\n    }\n\n    [[gnu::hot]] uint8_t getPixelColorOrder(uint16_t pix, uint8_t defaultColorOrder) const;\n\n  private:\n    std::vector<ColorOrderMapEntry> _mappings;\n};\n\n\ntypedef struct {\n  uint8_t id;\n  const char *type;\n  const char *name;\n} LEDType;\n\n\n//parent class of BusDigital, BusPwm, and BusNetwork\nclass Bus {\n  public:\n    Bus(uint8_t type, uint16_t start, uint8_t aw, uint16_t len = 1, bool reversed = false, bool refresh = false)\n    : _type(type)\n    , _bri(255)\n    , _NPBbri(255)\n    , _start(start)\n    , _len(std::max(len,(uint16_t)1))\n    , _reversed(reversed)\n    , _valid(false)\n    , _needsRefresh(refresh)\n    {\n      _autoWhiteMode = Bus::hasWhite(type) ? aw : RGBW_MODE_MANUAL_ONLY;\n    };\n\n    virtual ~Bus() {} //throw the bus under the bus\n\n    virtual void     begin()                                    {};\n    virtual void     show()                                     = 0;\n    virtual bool     canShow() const                            { return true; }\n    virtual void     setStatusPixel(uint32_t c)                 {}\n    virtual void     setPixelColor(unsigned pix, uint32_t c)    = 0;\n    virtual void     setBrightness(uint8_t b)                   { _bri = b; };\n    virtual void     setColorOrder(uint8_t co)                  {}\n    virtual uint32_t getPixelColor(unsigned pix) const          { return 0; }\n    virtual size_t   getPins(uint8_t* pinArray = nullptr) const { return 0; }\n    virtual uint16_t getLength() const                          { return _len; }\n    virtual uint8_t  getColorOrder() const                      { return COL_ORDER_RGB; }\n    virtual unsigned skippedLeds() const                        { return 0; }\n    virtual uint16_t getFrequency() const                       { return 0U; }\n    virtual uint16_t getLEDCurrent() const                      { return 0; }\n    virtual uint16_t getUsedCurrent() const                     { return 0; }\n    virtual uint16_t getMaxCurrent() const                      { return 0; }\n    virtual uint8_t  getDriverType() const                      { return 0; } // Default to RMT (0) for non-digital buses\n    virtual size_t   getBusSize() const                         { return sizeof(Bus); } // currently unused\n    virtual const String getCustomText() const                  { return String(); }\n\n    inline  bool     hasRGB() const                             { return _hasRgb; }\n    inline  bool     hasWhite() const                           { return _hasWhite; }\n    inline  bool     hasCCT() const                             { return _hasCCT; }\n    inline  bool     isDigital() const                          { return isDigital(_type); }\n    inline  bool     is2Pin() const                             { return is2Pin(_type); }\n    inline  bool     isOnOff() const                            { return isOnOff(_type); }\n    inline  bool     isPWM() const                              { return isPWM(_type); }\n    inline  bool     isVirtual() const                          { return isVirtual(_type); }\n    inline  bool     is16bit() const                            { return is16bit(_type); }\n    virtual bool     isPlaceholder() const                      { return false; }\n    inline  bool     mustRefresh() const                        { return mustRefresh(_type); }\n    inline  void     setReversed(bool reversed)                 { _reversed = reversed; }\n    inline  void     setStart(uint16_t start)                   { _start = start; }\n    inline  void     setAutoWhiteMode(uint8_t m)                { if (m < 5) _autoWhiteMode = m; }\n    inline  uint8_t  getAutoWhiteMode() const                   { return _autoWhiteMode; }\n    inline  size_t   getNumberOfChannels() const                { return hasWhite() + 3*hasRGB() + hasCCT(); }\n    inline  uint16_t getStart() const                           { return _start; }\n    inline  uint8_t  getType() const                            { return _type; }\n    inline  bool     isOk() const                               { return _valid; }\n    inline  bool     isReversed() const                         { return _reversed; }\n    inline  bool     isOffRefreshRequired() const               { return _needsRefresh; }\n    inline  bool     containsPixel(uint16_t pix) const          { return pix >= _start && pix < _start + _len; }\n\n    static inline std::vector<LEDType> getLEDTypes()            { return {{TYPE_NONE, \"\", PSTR(\"None\")}}; } // not used. just for reference for derived classes\n    static constexpr size_t   getNumberOfPins(uint8_t type)     { return isVirtual(type) ? 4 : isPWM(type) ? numPWMPins(type) : isHub75(type) ? 5 : is2Pin(type) + 1; } // credit @PaoloTK\n    static constexpr size_t   getNumberOfChannels(uint8_t type) { return hasWhite(type) + 3*hasRGB(type) + hasCCT(type); }\n    static constexpr bool hasRGB(uint8_t type) {\n      return !((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || type == TYPE_ANALOG_1CH || type == TYPE_ANALOG_2CH || type == TYPE_ONOFF);\n    }\n    static constexpr bool hasWhite(uint8_t type) {\n      return  (type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) ||\n              type == TYPE_SK6812_RGBW || type == TYPE_TM1814 || type == TYPE_UCS8904 ||\n              type == TYPE_FW1906 || type == TYPE_WS2805 || type == TYPE_SM16825 ||        // digital types with white channel\n              (type > TYPE_ONOFF && type <= TYPE_ANALOG_5CH && type != TYPE_ANALOG_3CH) || // analog types with white channel\n              type == TYPE_NET_DDP_RGBW || type == TYPE_NET_ARTNET_RGBW;                   // network types with white channel\n    }\n    static constexpr bool hasCCT(uint8_t type) {\n      return  type == TYPE_WS2812_2CH_X3 || type == TYPE_WS2812_WWA ||\n              type == TYPE_ANALOG_2CH    || type == TYPE_ANALOG_5CH ||\n              type == TYPE_FW1906        || type == TYPE_WS2805     ||\n              type == TYPE_SM16825;\n    }\n    static constexpr bool  isTypeValid(uint8_t type)  { return (type > 15 && type < 128); }\n    static constexpr bool  isDigital(uint8_t type)    { return (type >= TYPE_DIGITAL_MIN && type <= TYPE_DIGITAL_MAX) || is2Pin(type); }\n    static constexpr bool  is2Pin(uint8_t type)       { return (type >= TYPE_2PIN_MIN && type <= TYPE_2PIN_MAX); }\n    static constexpr bool  isOnOff(uint8_t type)      { return (type == TYPE_ONOFF); }\n    static constexpr bool  isPWM(uint8_t type)        { return (type >= TYPE_ANALOG_MIN && type <= TYPE_ANALOG_MAX); }\n    static constexpr bool  isVirtual(uint8_t type)    { return (type >= TYPE_VIRTUAL_MIN && type <= TYPE_VIRTUAL_MAX); }\n    static constexpr bool  isHub75(uint8_t type)      { return (type >= TYPE_HUB75MATRIX_MIN && type <= TYPE_HUB75MATRIX_MAX); }\n    static constexpr bool  is16bit(uint8_t type)      { return type == TYPE_UCS8903 || type == TYPE_UCS8904 || type == TYPE_SM16825; }\n    static constexpr bool  mustRefresh(uint8_t type)  { return type == TYPE_TM1814; }\n    static constexpr int   numPWMPins(uint8_t type)   { return (type - 40); }\n\n    static inline int16_t  getCCT()                   { return _cct; }\n    static inline void     setGlobalAWMode(uint8_t m) { if (m < 5) _gAWM = m; else _gAWM = AW_GLOBAL_DISABLED; }\n    static inline uint8_t  getGlobalAWMode()          { return _gAWM; }\n    static inline void     setCCT(int16_t cct)        { _cct = cct; }\n    static inline int8_t   getCCTBlend()              { return (_cctBlend * 100 + (_cctBlend >= 0 ? 64 : -64)) / 127; } // returns -100 to +100, +/-100% = +/-127. +/-64 for rounding \n    static inline void     setCCTBlend(int8_t b) {    // input is -100 to +100\n      _cctBlend = (std::max(-100, std::min(100, (int)b)) * 127 + (b >= 0 ? 50 : -50)) / 100; // +/-50 for rounding, b=+/-100% -> +/-127\n      //compile-time limiter for hardware that can't power both white channels at max\n      #ifdef WLED_MAX_CCT_BLEND\n        if (_cctBlend > WLED_MAX_CCT_BLEND) _cctBlend = WLED_MAX_CCT_BLEND;\n      #endif\n    }\n    static void calculateCCT(uint32_t c, uint8_t &ww, uint8_t &cw);\n\n  protected:\n    uint8_t  _type;\n    uint8_t  _bri;    // bus brightness\n    uint8_t  _NPBbri; // total brightness applied to colors in NPB buffer (_bri + ABL)\n    uint8_t  _autoWhiteMode; // global Auto White Calculation override\n    uint16_t _start;\n    uint16_t _len;\n    //struct { //using bitfield struct adds abour 250 bytes to binary size\n      bool _reversed;//     : 1;\n      bool _valid;//        : 1;\n      bool _needsRefresh;// : 1;\n      bool _hasRgb;//       : 1;\n      bool _hasWhite;//     : 1;\n      bool _hasCCT;//       : 1;\n    //} __attribute__ ((packed));\n    static uint8_t _gAWM;\n    // _cct has the following meanings (see calculateCCT() & BusManager::setSegmentCCT()):\n    //    -1 means to extract approximate CCT value in K from RGB (in calcualteCCT())\n    //    [0,255] is the exact CCT value where 0 means warm and 255 cold\n    //    [1900,10060] only for color correction expressed in K (colorBalanceFromKelvin())\n    static int16_t _cct;\n    // _cctBlend determines WW/CW blending, see calculateCCT()\n    //  < 0 - linear blending in center, single white at both ends, single white zone extends with decreased value (-127 min)\n    //    0 - linear (CCT 127 => 50% warm, 50% cold)\n    //   63 - semi additive/nonlinear (CCT 127 => 66% warm, 66% cold)\n    //  127 - additive CCT blending (CCT 127 => 100% warm, 100% cold)\n    static int8_t _cctBlend;\n\n    uint32_t autoWhiteCalc(uint32_t c, uint8_t &ww, uint8_t &cw) const;\n};\n\n\nclass BusDigital : public Bus {\n  public:\n    BusDigital(const BusConfig &bc);\n    ~BusDigital() { cleanup(); }\n\n    void show() override;\n    bool canShow() const override;\n    void setStatusPixel(uint32_t c) override;\n    [[gnu::hot]] void setPixelColor(unsigned pix, uint32_t c) override;\n    void setColorOrder(uint8_t colorOrder) override;\n    [[gnu::hot]] uint32_t getPixelColor(unsigned pix) const override;\n    uint8_t  getColorOrder() const override  { return _colorOrder; }\n    size_t   getPins(uint8_t* pinArray = nullptr) const override;\n    unsigned skippedLeds() const override    { return _skip; }\n    uint16_t getFrequency() const override   { return _frequencykHz; }\n    uint16_t getLEDCurrent() const override  { return _milliAmpsPerLed; }\n    uint16_t getUsedCurrent() const override { return _milliAmpsTotal; }\n    uint16_t getMaxCurrent() const override  { return _milliAmpsMax; }\n    uint8_t  getDriverType() const override  { return _driverType; }\n    void     setCurrentLimit(uint16_t milliAmps) { _milliAmpsLimit = milliAmps; }\n    void     estimateCurrent(); // estimate used current from summed colors\n    void     applyBriLimit(uint8_t newBri);\n    size_t   getBusSize() const override;\n    bool isI2S(); // true if this bus uses I2S driver\n    void begin() override;\n    void cleanup();\n\n    static std::vector<LEDType> getLEDTypes();\n\n  private:\n    uint8_t  _skip;\n    uint8_t  _colorOrder;\n    uint8_t  _pins[2];\n    uint8_t  _iType;\n    uint8_t  _driverType; // 0=RMT (default), 1=I2S\n    uint16_t _frequencykHz;\n    uint16_t _milliAmpsMax;\n    uint8_t  _milliAmpsPerLed;\n    uint16_t _milliAmpsLimit;\n    uint32_t _colorSum; // total color value for the bus, updated in setPixelColor(), used to estimate current\n    void    *_busPtr;\n\n    static uint16_t _milliAmpsTotal; // is overwitten/recalculated on each show()\n\n    inline uint32_t restoreColorLossy(uint32_t c, uint8_t restoreBri) const {\n      if (restoreBri < 255) {\n        uint8_t* chan = (uint8_t*) &c;\n        for (uint_fast8_t i=0; i<4; i++) {\n          uint_fast16_t val = chan[i];\n          chan[i] = ((val << 8) + restoreBri) / (restoreBri + 1); //adding _bri slightly improves recovery / stops degradation on re-scale\n        }\n      }\n      return c;\n    }\n};\n\n\nclass BusPwm : public Bus {\n  public:\n    BusPwm(const BusConfig &bc);\n    ~BusPwm() { cleanup(); }\n\n    void setPixelColor(unsigned pix, uint32_t c) override;\n    uint32_t getPixelColor(unsigned pix) const override; //does no index check\n    size_t   getPins(uint8_t* pinArray = nullptr) const override;\n    uint16_t getFrequency() const override { return _frequency; }\n    size_t   getBusSize() const override   { return sizeof(BusPwm); }\n    void show() override;\n    inline void cleanup() { deallocatePins(); }\n\n    static std::vector<LEDType> getLEDTypes();\n\n  private:\n    uint8_t _pins[OUTPUT_MAX_PINS];\n    uint8_t _data[OUTPUT_MAX_PINS];\n    #ifdef ARDUINO_ARCH_ESP32\n    uint8_t _ledcStart;\n    #endif\n    uint8_t _depth;\n    uint16_t _frequency;\n\n    void deallocatePins();\n};\n\n\nclass BusOnOff : public Bus {\n  public:\n    BusOnOff(const BusConfig &bc);\n    ~BusOnOff() { cleanup(); }\n\n    void setPixelColor(unsigned pix, uint32_t c) override;\n    uint32_t getPixelColor(unsigned pix) const override;\n    size_t   getPins(uint8_t* pinArray) const override;\n    size_t   getBusSize() const override { return sizeof(BusOnOff); }\n    void show() override;\n    inline void cleanup() { PinManager::deallocatePin(_pin, PinOwner::BusOnOff); }\n\n    static std::vector<LEDType> getLEDTypes();\n\n  private:\n    uint8_t _pin;\n    uint8_t _data;\n};\n\n\nclass BusNetwork : public Bus {\n  public:\n    BusNetwork(const BusConfig &bc);\n    ~BusNetwork() { cleanup(); }\n\n    bool canShow() const override  { return !_broadcastLock; } // this should be a return value from UDP routine if it is still sending data out\n    [[gnu::hot]] void setPixelColor(unsigned pix, uint32_t c) override;\n    [[gnu::hot]] uint32_t getPixelColor(unsigned pix) const override;\n    size_t getPins(uint8_t* pinArray = nullptr) const override;\n    size_t getBusSize() const override  { return sizeof(BusNetwork) + (isOk() ? _len * _UDPchannels : 0); }\n    void   show() override;\n    void   cleanup();\n    #ifdef ARDUINO_ARCH_ESP32\n    void   resolveHostname();\n    const String getCustomText() const override { return _hostname; }\n    #endif\n\n    static std::vector<LEDType> getLEDTypes();\n\n  private:\n    IPAddress _client;\n    uint8_t   _UDPtype;\n    uint8_t   _UDPchannels;\n    bool      _broadcastLock;\n    uint8_t   *_data;\n    #ifdef ARDUINO_ARCH_ESP32\n    String    _hostname;\n    #endif\n};\n\n// Placeholder for buses that we can't construct due to resource limitations\n// This preserves the configuration so it can be read back to the settings pages\n// Function calls \"mimic\" the replaced bus, isPlaceholder() can be used to identify a placeholder\nclass BusPlaceholder : public Bus {\n  public:\n    BusPlaceholder(const BusConfig &bc);\n\n    // Actual calls are stubbed out\n    void setPixelColor(unsigned pix, uint32_t c) override {};\n    void show() override {};\n\n    // Accessors\n    uint8_t  getColorOrder() const override  { return _colorOrder; }\n    size_t   getPins(uint8_t* pinArray) const override;\n    unsigned skippedLeds() const override    { return _skipAmount; }\n    uint16_t getFrequency() const override   { return _frequency; }\n    uint16_t getLEDCurrent() const override  { return _milliAmpsPerLed; }\n    uint16_t getMaxCurrent() const override  { return _milliAmpsMax; }\n    uint8_t  getDriverType() const override  { return _driverType; }\n    const String getCustomText() const override { return _text; }\n    bool     isPlaceholder() const override  { return true; }\n\n    size_t   getBusSize() const override   { return sizeof(BusPlaceholder); }\n\n  private:\n    uint8_t _colorOrder;\n    uint8_t _skipAmount;\n    uint8_t _pins[OUTPUT_MAX_PINS];\n    uint8_t _driverType;\n    uint16_t _frequency;\n    uint8_t _milliAmpsPerLed;\n    uint16_t _milliAmpsMax;\n    String _text;\n};\n\n#ifdef WLED_ENABLE_HUB75MATRIX\nclass BusHub75Matrix : public Bus {\n  public:\n    BusHub75Matrix(const BusConfig &bc);\n    [[gnu::hot]] void setPixelColor(unsigned pix, uint32_t c) override;\n    [[gnu::hot]] uint32_t getPixelColor(unsigned pix) const override;\n    void show() override;\n    void setBrightness(uint8_t b) override;\n    size_t getPins(uint8_t* pinArray = nullptr) const override;\n    void deallocatePins();\n    void cleanup();\n\n    ~BusHub75Matrix() {\n      cleanup();\n    }\n\n    static std::vector<LEDType> getLEDTypes(void);\n\n  private:\n    MatrixPanel_I2S_DMA *display = nullptr;\n    VirtualMatrixPanel  *virtualDisp = nullptr;\n    HUB75_I2S_CFG mxconfig;\n    unsigned _panelWidth = 0;\n    uint8_t _rows = 1; // panels per row\n    uint8_t _cols = 1; // panels per column\n    bool _isVirtual = false; // note: this is not strictly needed but there are padding bytes here anyway\n    CRGB *_ledBuffer = nullptr; // note: using uint32_t buffer is only 2% faster and not worth the extra RAM\n    byte *_ledsDirty = nullptr;\n    // workaround for missing constants on include path for non-MM\n    static constexpr uint32_t IS_BLACK = 0x000000u;\n    static constexpr uint32_t IS_DARKGREY = 0x333333u;\n    static constexpr int PIN_COUNT = 14;\n};\n#endif\n\n//temporary struct for passing bus configuration to bus\nstruct BusConfig {\n  uint8_t type;\n  uint16_t count;\n  uint16_t start;\n  uint8_t colorOrder;\n  bool reversed;\n  uint8_t skipAmount;\n  bool refreshReq;\n  uint8_t autoWhite;\n  uint8_t pins[OUTPUT_MAX_PINS] = {255, 255, 255, 255, 255};\n  uint16_t frequency;\n  uint8_t milliAmpsPerLed;\n  uint16_t milliAmpsMax;\n  uint8_t driverType; // 0=RMT (default), 1=I2S\n  uint8_t iType; // internal bus type (I_*) determined during memory estimation, used for bus creation\n  String text;\n\n  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 = \"\")\n  : count(std::max(len,(uint16_t)1))\n  , start(pstart)\n  , colorOrder(pcolorOrder)\n  , reversed(rev)\n  , skipAmount(skip)\n  , autoWhite(aw)\n  , frequency(clock_kHz)\n  , milliAmpsPerLed(maPerLed)\n  , milliAmpsMax(maMax)\n  , driverType(driver)\n  , iType(0) // default to I_NONE\n  , text(sometext)\n  {\n    refreshReq = (bool) GET_BIT(busType,7);\n    type = busType & 0x7F;  // bit 7 may be/is hacked to include refresh info (1=refresh in off state, 0=no refresh)\n    size_t nPins = Bus::getNumberOfPins(type);\n    for (size_t i = 0; i < nPins; i++) pins[i] = ppins[i];\n    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\"),\n      (int)start, (int)(start+len),\n      (int)type,\n      (int)colorOrder,\n      (int)reversed,\n      (int)skipAmount,\n      (int)autoWhite,\n      (int)frequency,\n      (int)milliAmpsPerLed, (int)milliAmpsMax,\n      driverType == 0 ? \"RMT\" : \"I2S\"\n    );\n  }\n\n  //validates start and length and extends total if needed\n  bool adjustBounds(uint16_t& total) {\n    if (!count) count = 1;\n    if (!Bus::isVirtual(type) && count > MAX_LEDS_PER_BUS) count = MAX_LEDS_PER_BUS;\n    if (start >= MAX_LEDS) return false;\n    //limit length of strip if it would exceed total permissible LEDs\n    if (start + count > MAX_LEDS) count = MAX_LEDS - start;\n    //extend total count accordingly\n    if (start + count > total) total = start + count;\n    return true;\n  }\n\n  size_t memUsage() const;\n};\n\n\n// milliamps used by ESP (for power estimation)\n// you can set it to 0 if the ESP is powered by USB and the LEDs by external\n#ifndef MA_FOR_ESP\n  #ifdef ESP8266\n    #define MA_FOR_ESP         80 //how much mA does the ESP use (Wemos D1 about 80mA)\n  #else\n    #define MA_FOR_ESP        120 //how much mA does the ESP use (ESP32 about 120mA)\n  #endif\n#endif\n\nnamespace BusManager {\n\n  extern std::vector<std::unique_ptr<Bus>> busses;\n  //extern std::vector<Bus*> busses;\n  extern uint16_t _gMilliAmpsUsed;\n  extern uint16_t _gMilliAmpsMax;\n  extern bool     _useABL;\n\n  #ifdef ESP32_DATA_IDLE_HIGH\n  void    esp32RMTInvertIdle() ;\n  #endif\n  inline size_t   getNumVirtualBusses() {\n    size_t j = 0;\n    for (const auto &bus : busses) j += bus->isVirtual();\n    return j;\n  }\n\n  inline uint16_t currentMilliamps()            { return _gMilliAmpsUsed + MA_FOR_ESP; }\n  //inline uint16_t ablMilliampsMax()             { unsigned sum = 0; for (auto &bus : busses) sum += bus->getMaxCurrent(); return sum; }\n  inline uint16_t ablMilliampsMax()             { return _gMilliAmpsMax; }  // used for compatibility reasons (and enabling virtual global ABL)\n  inline void     setMilliampsMax(uint16_t max) { _gMilliAmpsMax = max;}\n  void            initializeABL();              // setup automatic brightness limiter parameters, call once after buses are initialized\n  void            applyABL();                   // apply automatic brightness limiter, global or per bus\n\n  uint8_t getI(uint8_t busType, const uint8_t* pins, uint8_t driverPreference); // workaround for access to PolyBus function from FX_fcn.cpp\n\n  //do not call this method from system context (network callback)\n  void removeAll();\n  int  add(const BusConfig &bc, bool placeholder);\n\n  void on();\n  void off();\n\n  [[gnu::hot]] void     setPixelColor(unsigned pix, uint32_t c);\n  [[gnu::hot]] uint32_t getPixelColor(unsigned pix);\n  void        show();\n  bool        canAllShow();\n  inline void setStatusPixel(uint32_t c) { for (auto &bus : busses) bus->setStatusPixel(c);}\n  inline void setBrightness(uint8_t b)   { for (auto &bus : busses) bus->setBrightness(b); }\n  // for setSegmentCCT(), cct can only be in [-1,255] range; allowWBCorrection will convert it to K\n  // WARNING: setSegmentCCT() is a misleading name!!! much better would be setGlobalCCT() or just setCCT()\n  void           setSegmentCCT(int16_t cct, bool allowWBCorrection = false);\n  inline int16_t getSegmentCCT()         { return Bus::getCCT(); }\n  inline Bus*    getBus(size_t busNr)    { return busNr < busses.size() ? busses[busNr].get() : nullptr; }\n  inline size_t  getNumBusses()          { return busses.size(); }\n\n  //semi-duplicate of strip.getLengthTotal() (though that just returns strip._length, calculated in finalizeInit())\n  inline uint16_t getTotalLength(bool onlyPhysical = false) {\n    unsigned len = 0;\n    for (const auto &bus : busses) if (!(bus->isVirtual() && onlyPhysical)) len += bus->getLength();\n    return len;\n  }\n  String         getLEDTypesJSONString();\n  ColorOrderMap& getColorOrderMap();\n};\n#endif\n"
  },
  {
    "path": "wled00/bus_wrapper.h",
    "content": "#pragma once\n#ifndef BusWrapper_h\n#define BusWrapper_h\n\n//#define NPB_CONF_4STEP_CADENCE\n#include \"NeoPixelBus.h\"\n\n//Hardware SPI Pins\n#define P_8266_HS_MOSI 13\n#define P_8266_HS_CLK  14\n#define P_32_HS_MOSI   13\n#define P_32_HS_CLK    14\n#define P_32_VS_MOSI   23\n#define P_32_VS_CLK    18\n\n//The dirty list of possible bus types. Quite a lot...\n#define I_NONE 0\n//ESP8266 RGB\n#define I_8266_U0_NEO_3 1\n#define I_8266_U1_NEO_3 2\n#define I_8266_DM_NEO_3 3\n#define I_8266_BB_NEO_3 4\n//RGBW\n#define I_8266_U0_NEO_4 5\n#define I_8266_U1_NEO_4 6\n#define I_8266_DM_NEO_4 7\n#define I_8266_BB_NEO_4 8\n//400Kbps\n#define I_8266_U0_400_3 9\n#define I_8266_U1_400_3 10\n#define I_8266_DM_400_3 11\n#define I_8266_BB_400_3 12\n//TM1814 (RGBW)\n#define I_8266_U0_TM1_4 13\n#define I_8266_U1_TM1_4 14\n#define I_8266_DM_TM1_4 15\n#define I_8266_BB_TM1_4 16\n//TM1829 (RGB)\n#define I_8266_U0_TM2_3 17\n#define I_8266_U1_TM2_3 18\n#define I_8266_DM_TM2_3 19\n#define I_8266_BB_TM2_3 20\n//UCS8903 (RGB)\n#define I_8266_U0_UCS_3 21\n#define I_8266_U1_UCS_3 22\n#define I_8266_DM_UCS_3 23\n#define I_8266_BB_UCS_3 24\n//UCS8904 (RGBW)\n#define I_8266_U0_UCS_4 25\n#define I_8266_U1_UCS_4 26\n#define I_8266_DM_UCS_4 27\n#define I_8266_BB_UCS_4 28\n//FW1906 GRBCW\n#define I_8266_U0_FW6_5 29\n#define I_8266_U1_FW6_5 30\n#define I_8266_DM_FW6_5 31\n#define I_8266_BB_FW6_5 32\n//ESP8266 APA106\n#define I_8266_U0_APA106_3 33\n#define I_8266_U1_APA106_3 34\n#define I_8266_DM_APA106_3 35\n#define I_8266_BB_APA106_3 36\n//WS2805 (RGBCW)\n#define I_8266_U0_2805_5 37\n#define I_8266_U1_2805_5 38\n#define I_8266_DM_2805_5 39\n#define I_8266_BB_2805_5 40\n//TM1914 (RGB)\n#define I_8266_U0_TM1914_3 41\n#define I_8266_U1_TM1914_3 42\n#define I_8266_DM_TM1914_3 43\n#define I_8266_BB_TM1914_3 44\n//SM16825 (RGBCW)\n#define I_8266_U0_SM16825_5 45\n#define I_8266_U1_SM16825_5 46\n#define I_8266_DM_SM16825_5 47\n#define I_8266_BB_SM16825_5 48\n\n/*** ESP32 Neopixel methods ***/\n//RGB\n#define I_32_RN_NEO_3 1\n#define I_32_I2_NEO_3 2\n//RGBW\n#define I_32_RN_NEO_4 5\n#define I_32_I2_NEO_4 6\n//400Kbps\n#define I_32_RN_400_3 9\n#define I_32_I2_400_3 10\n//TM1814 (RGBW)\n#define I_32_RN_TM1_4 13\n#define I_32_I2_TM1_4 14\n//TM1829 (RGB)\n#define I_32_RN_TM2_3 17\n#define I_32_I2_TM2_3 18\n//UCS8903 (RGB)\n#define I_32_RN_UCS_3 21\n#define I_32_I2_UCS_3 22\n//UCS8904 (RGBW)\n#define I_32_RN_UCS_4 25\n#define I_32_I2_UCS_4 26\n//FW1906 GRBCW\n#define I_32_RN_FW6_5 29\n#define I_32_I2_FW6_5 30\n//APA106\n#define I_32_RN_APA106_3 33\n#define I_32_I2_APA106_3 34\n//WS2805 (RGBCW)\n#define I_32_RN_2805_5 37\n#define I_32_I2_2805_5 38\n//TM1914 (RGB)\n#define I_32_RN_TM1914_3 41\n#define I_32_I2_TM1914_3 42\n//SM16825 (RGBCW)\n#define I_32_RN_SM16825_5 45\n#define I_32_I2_SM16825_5 46\n\n//APA102\n#define I_HS_DOT_3 101 //hardware SPI\n#define I_SS_DOT_3 102 //soft SPI\n\n//LPD8806\n#define I_HS_LPD_3 103\n#define I_SS_LPD_3 104\n\n//WS2801\n#define I_HS_WS1_3 105\n#define I_SS_WS1_3 106\n\n//P9813\n#define I_HS_P98_3 107\n#define I_SS_P98_3 108\n\n//LPD6803\n#define I_HS_LPO_3 109\n#define I_SS_LPO_3 110\n\n\n// In the following NeoGammaNullMethod can be replaced with NeoGammaWLEDMethod to perform Gamma correction implicitly\n// unfortunately that may apply Gamma correction to pre-calculated palettes which is undesired\n\n/*** ESP8266 Neopixel methods ***/\n#ifdef ESP8266\n//RGB\n#define B_8266_U0_NEO_3 NeoPixelBus<NeoGrbFeature, NeoEsp8266Uart0Ws2813Method> //3 chan, esp8266, gpio1\n#define B_8266_U1_NEO_3 NeoPixelBus<NeoGrbFeature, NeoEsp8266Uart1Ws2813Method> //3 chan, esp8266, gpio2\n#define B_8266_DM_NEO_3 NeoPixelBus<NeoGrbFeature, NeoEsp8266Dma800KbpsMethod>  //3 chan, esp8266, gpio3\n#define B_8266_BB_NEO_3 NeoPixelBus<NeoGrbFeature, NeoEsp8266BitBang800KbpsMethod> //3 chan, esp8266, bb (any pin but 16)\n//RGBW\n#define B_8266_U0_NEO_4 NeoPixelBus<NeoGrbwFeature, NeoEsp8266Uart0Ws2813Method>   //4 chan, esp8266, gpio1\n#define B_8266_U1_NEO_4 NeoPixelBus<NeoGrbwFeature, NeoEsp8266Uart1Ws2813Method>   //4 chan, esp8266, gpio2\n#define B_8266_DM_NEO_4 NeoPixelBus<NeoGrbwFeature, NeoEsp8266Dma800KbpsMethod>    //4 chan, esp8266, gpio3\n#define B_8266_BB_NEO_4 NeoPixelBus<NeoGrbwFeature, NeoEsp8266BitBang800KbpsMethod> //4 chan, esp8266, bb (any pin)\n//400Kbps\n#define B_8266_U0_400_3 NeoPixelBus<NeoGrbFeature, NeoEsp8266Uart0400KbpsMethod>   //3 chan, esp8266, gpio1\n#define B_8266_U1_400_3 NeoPixelBus<NeoGrbFeature, NeoEsp8266Uart1400KbpsMethod>   //3 chan, esp8266, gpio2\n#define B_8266_DM_400_3 NeoPixelBus<NeoGrbFeature, NeoEsp8266Dma400KbpsMethod>     //3 chan, esp8266, gpio3\n#define B_8266_BB_400_3 NeoPixelBus<NeoGrbFeature, NeoEsp8266BitBang400KbpsMethod> //3 chan, esp8266, bb (any pin)\n//TM1814 (RGBW)\n#define B_8266_U0_TM1_4 NeoPixelBus<NeoWrgbTm1814Feature, NeoEsp8266Uart0Tm1814Method>\n#define B_8266_U1_TM1_4 NeoPixelBus<NeoWrgbTm1814Feature, NeoEsp8266Uart1Tm1814Method>\n#define B_8266_DM_TM1_4 NeoPixelBus<NeoWrgbTm1814Feature, NeoEsp8266DmaTm1814Method>\n#define B_8266_BB_TM1_4 NeoPixelBus<NeoWrgbTm1814Feature, NeoEsp8266BitBangTm1814Method>\n//TM1829 (RGB)\n#define B_8266_U0_TM2_3 NeoPixelBus<NeoBrgFeature, NeoEsp8266Uart0Tm1829Method>\n#define B_8266_U1_TM2_3 NeoPixelBus<NeoBrgFeature, NeoEsp8266Uart1Tm1829Method>\n#define B_8266_DM_TM2_3 NeoPixelBus<NeoBrgFeature, NeoEsp8266DmaTm1829Method>\n#define B_8266_BB_TM2_3 NeoPixelBus<NeoBrgFeature, NeoEsp8266BitBangTm1829Method>\n//UCS8903\n#define B_8266_U0_UCS_3 NeoPixelBus<NeoRgbUcs8903Feature, NeoEsp8266Uart0Ws2813Method> //3 chan, esp8266, gpio1\n#define B_8266_U1_UCS_3 NeoPixelBus<NeoRgbUcs8903Feature, NeoEsp8266Uart1Ws2813Method> //3 chan, esp8266, gpio2\n#define B_8266_DM_UCS_3 NeoPixelBus<NeoRgbUcs8903Feature, NeoEsp8266Dma800KbpsMethod>  //3 chan, esp8266, gpio3\n#define B_8266_BB_UCS_3 NeoPixelBus<NeoRgbUcs8903Feature, NeoEsp8266BitBang800KbpsMethod> //3 chan, esp8266, bb (any pin but 16)\n//UCS8904 RGBW\n#define B_8266_U0_UCS_4 NeoPixelBus<NeoRgbwUcs8904Feature, NeoEsp8266Uart0Ws2813Method>   //4 chan, esp8266, gpio1\n#define B_8266_U1_UCS_4 NeoPixelBus<NeoRgbwUcs8904Feature, NeoEsp8266Uart1Ws2813Method>   //4 chan, esp8266, gpio2\n#define B_8266_DM_UCS_4 NeoPixelBus<NeoRgbwUcs8904Feature, NeoEsp8266Dma800KbpsMethod>    //4 chan, esp8266, gpio3\n#define B_8266_BB_UCS_4 NeoPixelBus<NeoRgbwUcs8904Feature, NeoEsp8266BitBang800KbpsMethod> //4 chan, esp8266, bb (any pin)\n//APA106\n#define B_8266_U0_APA106_3 NeoPixelBus<NeoRbgFeature, NeoEsp8266Uart0Apa106Method> //3 chan, esp8266, gpio1\n#define B_8266_U1_APA106_3 NeoPixelBus<NeoRbgFeature, NeoEsp8266Uart1Apa106Method> //3 chan, esp8266, gpio2\n#define B_8266_DM_APA106_3 NeoPixelBus<NeoGrbFeature, NeoEsp8266DmaApa106Method>  //3 chan, esp8266, gpio3\n#define B_8266_BB_APA106_3 NeoPixelBus<NeoGrbFeature, NeoEsp8266BitBangApa106Method> //3 chan, esp8266, bb (any pin but 16)\n//FW1906 GRBCW\n#define B_8266_U0_FW6_5 NeoPixelBus<NeoGrbcwxFeature, NeoEsp8266Uart0Ws2813Method>   //esp8266, gpio1\n#define B_8266_U1_FW6_5 NeoPixelBus<NeoGrbcwxFeature, NeoEsp8266Uart1Ws2813Method>   //esp8266, gpio2\n#define B_8266_DM_FW6_5 NeoPixelBus<NeoGrbcwxFeature, NeoEsp8266Dma800KbpsMethod>   //esp8266, gpio3\n#define B_8266_BB_FW6_5 NeoPixelBus<NeoGrbcwxFeature, NeoEsp8266BitBang800KbpsMethod>   //esp8266, bb\n//WS2805 GRBCW\n#define B_8266_U0_2805_5 NeoPixelBus<NeoGrbwwFeature, NeoEsp8266Uart0Ws2805Method> //esp8266, gpio1\n#define B_8266_U1_2805_5 NeoPixelBus<NeoGrbwwFeature, NeoEsp8266Uart1Ws2805Method> //esp8266, gpio2\n#define B_8266_DM_2805_5 NeoPixelBus<NeoGrbwwFeature, NeoEsp8266DmaWs2805Method> //esp8266, gpio3\n#define B_8266_BB_2805_5 NeoPixelBus<NeoGrbwwFeature, NeoEsp8266BitBangWs2805Method> //esp8266, bb\n//TM1914 (RGB)\n#define B_8266_U0_TM1914_3 NeoPixelBus<NeoRgbTm1914Feature, NeoEsp8266Uart0Tm1914Method>\n#define B_8266_U1_TM1914_3 NeoPixelBus<NeoRgbTm1914Feature, NeoEsp8266Uart1Tm1914Method>\n#define B_8266_DM_TM1914_3 NeoPixelBus<NeoRgbTm1914Feature, NeoEsp8266DmaTm1914Method>\n#define B_8266_BB_TM1914_3 NeoPixelBus<NeoRgbTm1914Feature, NeoEsp8266BitBangTm1914Method>\n//Sm16825 (RGBWC)\n#define B_8266_U0_SM16825_5 NeoPixelBus<NeoRgbwcSm16825eFeature, NeoEsp8266Uart0Ws2813Method>\n#define B_8266_U1_SM16825_5 NeoPixelBus<NeoRgbwcSm16825eFeature, NeoEsp8266Uart1Ws2813Method>\n#define B_8266_DM_SM16825_5 NeoPixelBus<NeoRgbwcSm16825eFeature, NeoEsp8266Dma800KbpsMethod>\n#define B_8266_BB_SM16825_5 NeoPixelBus<NeoRgbwcSm16825eFeature, NeoEsp8266BitBangWs2813Method>\n#endif\n\n/*** ESP32 Neopixel methods ***/\n#ifdef ARDUINO_ARCH_ESP32\n// C3: I2S0 and I2S1 methods not supported (has one I2S bus)\n// S2: I2S0 methods supported (single & parallel), I2S1 methods not supported (has one I2S bus)\n// S3: I2S0 methods not supported, I2S1 supports LCD parallel methods (has two I2S buses)\n// https://github.com/Makuna/NeoPixelBus/blob/b32f719e95ef3c35c46da5c99538017ef925c026/src/internal/Esp32_i2s.h#L4\n// https://github.com/Makuna/NeoPixelBus/blob/b32f719e95ef3c35c46da5c99538017ef925c026/src/internal/NeoEsp32RmtMethod.h#L857\n#if defined(CONFIG_IDF_TARGET_ESP32S3)\n  // S3 will always use LCD parallel output\n  typedef X8Ws2812xMethod X1Ws2812xMethod;\n  typedef X8Sk6812Method X1Sk6812Method;\n  typedef X8400KbpsMethod X1400KbpsMethod;\n  typedef X8800KbpsMethod X1800KbpsMethod;\n  typedef X8Tm1814Method X1Tm1814Method;\n  typedef X8Tm1829Method X1Tm1829Method;\n  typedef X8Apa106Method X1Apa106Method;\n  typedef X8Ws2805Method X1Ws2805Method;\n  typedef X8Tm1914Method X1Tm1914Method;\n#elif defined(CONFIG_IDF_TARGET_ESP32S2)\n  // S2 will use I2S0\n  typedef NeoEsp32I2s0Ws2812xMethod X1Ws2812xMethod;\n  typedef NeoEsp32I2s0Sk6812Method X1Sk6812Method;\n  typedef NeoEsp32I2s0400KbpsMethod X1400KbpsMethod;\n  typedef NeoEsp32I2s0800KbpsMethod X1800KbpsMethod;\n  typedef NeoEsp32I2s0Tm1814Method X1Tm1814Method;\n  typedef NeoEsp32I2s0Tm1829Method X1Tm1829Method;\n  typedef NeoEsp32I2s0Apa106Method X1Apa106Method;\n  typedef NeoEsp32I2s0Ws2805Method X1Ws2805Method;\n  typedef NeoEsp32I2s0Tm1914Method X1Tm1914Method;\n#elif !defined(CONFIG_IDF_TARGET_ESP32C3)\n  // regular ESP32 will use I2S1\n  typedef NeoEsp32I2s1Ws2812xMethod X1Ws2812xMethod;\n  typedef NeoEsp32I2s1Sk6812Method X1Sk6812Method;\n  typedef NeoEsp32I2s1400KbpsMethod X1400KbpsMethod;\n  typedef NeoEsp32I2s1800KbpsMethod X1800KbpsMethod;\n  typedef NeoEsp32I2s1Tm1814Method X1Tm1814Method;\n  typedef NeoEsp32I2s1Tm1829Method X1Tm1829Method;\n  typedef NeoEsp32I2s1Apa106Method X1Apa106Method;\n  typedef NeoEsp32I2s1Ws2805Method X1Ws2805Method;\n  typedef NeoEsp32I2s1Tm1914Method X1Tm1914Method;\n#endif\n\n// RMT driver selection\n#if !defined(WLED_USE_SHARED_RMT)  && !defined(__riscv)\n#include <NeoEsp32RmtHIMethod.h>\n#define NeoEsp32RmtMethod(x) NeoEsp32RmtHIN ## x ## Method\n#else\n#define NeoEsp32RmtMethod(x) NeoEsp32RmtN ## x ## Method\n#endif\n\n//RGB\n#define B_32_RN_NEO_3 NeoPixelBus<NeoGrbFeature, NeoEsp32RmtMethod(Ws2812x)> // ESP32, S2, S3, C3\n//#define B_32_IN_NEO_3 NeoPixelBus<NeoGrbFeature, NeoEsp32I2sNWs2812xMethod> // ESP32 (dynamic I2S selection)\n#define B_32_I2_NEO_3 NeoPixelBus<NeoGrbFeature, X1Ws2812xMethod> // ESP32, S2, S3 (automatic I2S selection, see typedef above)\n#define B_32_IP_NEO_3 NeoPixelBus<NeoGrbFeature, X8Ws2812xMethod> // parallel I2S (ESP32, S2, S3)\n//RGBW\n#define B_32_RN_NEO_4 NeoPixelBus<NeoGrbwFeature, NeoEsp32RmtMethod(Sk6812)>\n#define B_32_I2_NEO_4 NeoPixelBus<NeoGrbwFeature, X1Sk6812Method>\n#define B_32_IP_NEO_4 NeoPixelBus<NeoGrbwFeature, X8Sk6812Method> // parallel I2S\n//400Kbps\n#define B_32_RN_400_3 NeoPixelBus<NeoGrbFeature, NeoEsp32RmtMethod(400Kbps)>\n#define B_32_I2_400_3 NeoPixelBus<NeoGrbFeature, X1400KbpsMethod>\n#define B_32_IP_400_3 NeoPixelBus<NeoGrbFeature, X8400KbpsMethod> // parallel I2S\n//TM1814 (RGBW)\n#define B_32_RN_TM1_4 NeoPixelBus<NeoWrgbTm1814Feature, NeoEsp32RmtMethod(Tm1814)>\n#define B_32_I2_TM1_4 NeoPixelBus<NeoWrgbTm1814Feature, X1Tm1814Method>\n#define B_32_IP_TM1_4 NeoPixelBus<NeoWrgbTm1814Feature, X8Tm1814Method> // parallel I2S\n//TM1829 (RGB)\n#define B_32_RN_TM2_3 NeoPixelBus<NeoBrgFeature, NeoEsp32RmtMethod(Tm1829)>\n#define B_32_I2_TM2_3 NeoPixelBus<NeoBrgFeature, X1Tm1829Method>\n#define B_32_IP_TM2_3 NeoPixelBus<NeoBrgFeature, X8Tm1829Method> // parallel I2S\n//UCS8903\n#define B_32_RN_UCS_3 NeoPixelBus<NeoRgbUcs8903Feature, NeoEsp32RmtMethod(Ws2812x)>\n#define B_32_I2_UCS_3 NeoPixelBus<NeoRgbUcs8903Feature, X1800KbpsMethod>\n#define B_32_IP_UCS_3 NeoPixelBus<NeoRgbUcs8903Feature, X8800KbpsMethod> // parallel I2S\n//UCS8904\n#define B_32_RN_UCS_4 NeoPixelBus<NeoRgbwUcs8904Feature, NeoEsp32RmtMethod(Ws2812x)>\n#define B_32_I2_UCS_4 NeoPixelBus<NeoRgbwUcs8904Feature, X1800KbpsMethod>\n#define B_32_IP_UCS_4 NeoPixelBus<NeoRgbwUcs8904Feature, X8800KbpsMethod>// parallel I2S\n//APA106\n#define B_32_RN_APA106_3 NeoPixelBus<NeoGrbFeature, NeoEsp32RmtMethod(Apa106)>\n#define B_32_I2_APA106_3 NeoPixelBus<NeoGrbFeature, X1Apa106Method>\n#define B_32_IP_APA106_3 NeoPixelBus<NeoGrbFeature, X8Apa106Method> // parallel I2S\n//FW1906 GRBCW\n#define B_32_RN_FW6_5 NeoPixelBus<NeoGrbcwxFeature, NeoEsp32RmtMethod(Ws2812x)>\n#define B_32_I2_FW6_5 NeoPixelBus<NeoGrbcwxFeature, X1800KbpsMethod>\n#define B_32_IP_FW6_5 NeoPixelBus<NeoGrbcwxFeature, X8800KbpsMethod> // parallel I2S\n//WS2805 RGBWC\n#define B_32_RN_2805_5 NeoPixelBus<NeoGrbwwFeature, NeoEsp32RmtMethod(Ws2805)>\n#define B_32_I2_2805_5 NeoPixelBus<NeoGrbwwFeature, X1Ws2805Method>\n#define B_32_IP_2805_5 NeoPixelBus<NeoGrbwwFeature, X8Ws2805Method> // parallel I2S\n//TM1914 (RGB)\n#define B_32_RN_TM1914_3 NeoPixelBus<NeoGrbTm1914Feature, NeoEsp32RmtMethod(Tm1914)>\n#define B_32_I2_TM1914_3 NeoPixelBus<NeoGrbTm1914Feature, X1Tm1914Method>\n#define B_32_IP_TM1914_3 NeoPixelBus<NeoGrbTm1914Feature, X8Tm1914Method> // parallel I2S\n//Sm16825 (RGBWC)\n#define B_32_RN_SM16825_5 NeoPixelBus<NeoRgbcwSm16825eFeature, NeoEsp32RmtMethod(Ws2812x)>\n#define B_32_I2_SM16825_5 NeoPixelBus<NeoRgbcwSm16825eFeature, X1Ws2812xMethod>\n#define B_32_IP_SM16825_5 NeoPixelBus<NeoRgbcwSm16825eFeature, X8Ws2812xMethod> // parallel I2S\n#endif\n\n//APA102\n#ifdef WLED_USE_ETHERNET\n// fix for #2542 (by @BlackBird77)\n#define B_HS_DOT_3 NeoPixelBus<DotStarBgrFeature, DotStarEsp32HspiHzMethod> //hardware HSPI (was DotStarEsp32DmaHspi5MhzMethod in NPB @ 2.6.9)\n#else\n#define B_HS_DOT_3 NeoPixelBus<DotStarBgrFeature, DotStarSpiHzMethod> //hardware VSPI\n#endif\n#define B_SS_DOT_3 NeoPixelBus<DotStarBgrFeature, DotStarMethod>    //soft SPI\n\n//LPD8806\n#define B_HS_LPD_3 NeoPixelBus<Lpd8806GrbFeature, Lpd8806SpiHzMethod>\n#define B_SS_LPD_3 NeoPixelBus<Lpd8806GrbFeature, Lpd8806Method>\n\n//LPD6803\n#define B_HS_LPO_3 NeoPixelBus<Lpd6803GrbFeature, Lpd6803SpiHzMethod>\n#define B_SS_LPO_3 NeoPixelBus<Lpd6803GrbFeature, Lpd6803Method>\n\n//WS2801\n#ifdef WLED_USE_ETHERNET\n#define B_HS_WS1_3 NeoPixelBus<NeoRbgFeature, Ws2801MethodBase<TwoWireHspiImple<SpiSpeedHz>>>\n#else\n#define B_HS_WS1_3 NeoPixelBus<NeoRbgFeature, Ws2801SpiHzMethod>\n#endif\n#define B_SS_WS1_3 NeoPixelBus<NeoRbgFeature, Ws2801Method>\n\n//P9813\n#define B_HS_P98_3 NeoPixelBus<P9813BgrFeature, P9813SpiHzMethod>\n#define B_SS_P98_3 NeoPixelBus<P9813BgrFeature, P9813Method>\n\n// 48bit & 64bit to 24bit & 32bit RGB(W) conversion\n#define toRGBW32(c) (RGBW32((c>>40)&0xFF, (c>>24)&0xFF, (c>>8)&0xFF, (c>>56)&0xFF))\n#define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b))))\n\n//handles pointer type conversion for all possible bus types\nclass PolyBus {\n  private:\n  #ifndef ESP8266\n    static bool _useParallelI2S;          // use parallel I2S/LCD (8 channels)\n    static uint8_t _rmtChannelsAssigned;  // RMT channel tracking for dynamic allocation\n    static uint8_t _rmtChannel;           // physical RMT channel to use during bus creation\n    static uint8_t _i2sChannelsAssigned;  // I2S channel tracking for dynamic allocation\n    static uint8_t _parallelBusItype;     // parallel output does not allow mixed LED types, track I_Type\n    static uint8_t _2PchannelsAssigned;  // 2-Pin (SPI) channel assigned: first one gets the hardware SPI, others use bit-banged SPI\n    // 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.\n    // 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.\n  #endif\n  public:\n\n  // initialize SPI bus speed for DotStar methods\n  template <class T>\n  static void beginDotStar(void* busPtr, int8_t sck, int8_t miso, int8_t mosi, int8_t ss, uint16_t clock_kHz /* 0 == use default */) {\n    T dotStar_strip = static_cast<T>(busPtr);\n    #ifdef ESP8266\n    dotStar_strip->Begin();\n    #else\n    if (sck == -1 && mosi == -1) dotStar_strip->Begin();\n    else                         dotStar_strip->Begin(sck, miso, mosi, ss);\n    #endif\n    if (clock_kHz) dotStar_strip->SetMethodSettings(NeoSpiSettings((uint32_t)clock_kHz*1000));\n  }\n\n  // Begin & initialize the PixelSettings for TM1814 strips.\n  template <class T>\n  static void beginTM1814(void* busPtr) {\n    T tm1814_strip = static_cast<T>(busPtr);\n    tm1814_strip->Begin();\n    // Max current for each LED (22.5 mA).\n    tm1814_strip->SetPixelSettings(NeoTm1814Settings(/*R*/225, /*G*/225, /*B*/225, /*W*/225));\n  }\n\n  template <class T>\n  static void beginTM1914(void* busPtr) {\n    T tm1914_strip = static_cast<T>(busPtr);\n    tm1914_strip->Begin();\n    tm1914_strip->SetPixelSettings(NeoTm1914Settings());  //NeoTm1914_Mode_DinFdinAutoSwitch, NeoTm1914_Mode_DinOnly, NeoTm1914_Mode_FdinOnly \n  }\n\n  static void begin(void* busPtr, uint8_t busType, uint8_t* pins, uint16_t clock_kHz /* only used by DotStar */) {\n    switch (busType) {\n      case I_NONE: break;\n    #ifdef ESP8266\n      case I_8266_U0_NEO_3: (static_cast<B_8266_U0_NEO_3*>(busPtr))->Begin(); break;\n      case I_8266_U1_NEO_3: (static_cast<B_8266_U1_NEO_3*>(busPtr))->Begin(); break;\n      case I_8266_DM_NEO_3: (static_cast<B_8266_DM_NEO_3*>(busPtr))->Begin(); break;\n      case I_8266_BB_NEO_3: (static_cast<B_8266_BB_NEO_3*>(busPtr))->Begin(); break;\n      case I_8266_U0_NEO_4: (static_cast<B_8266_U0_NEO_4*>(busPtr))->Begin(); break;\n      case I_8266_U1_NEO_4: (static_cast<B_8266_U1_NEO_4*>(busPtr))->Begin(); break;\n      case I_8266_DM_NEO_4: (static_cast<B_8266_DM_NEO_4*>(busPtr))->Begin(); break;\n      case I_8266_BB_NEO_4: (static_cast<B_8266_BB_NEO_4*>(busPtr))->Begin(); break;\n      case I_8266_U0_400_3: (static_cast<B_8266_U0_400_3*>(busPtr))->Begin(); break;\n      case I_8266_U1_400_3: (static_cast<B_8266_U1_400_3*>(busPtr))->Begin(); break;\n      case I_8266_DM_400_3: (static_cast<B_8266_DM_400_3*>(busPtr))->Begin(); break;\n      case I_8266_BB_400_3: (static_cast<B_8266_BB_400_3*>(busPtr))->Begin(); break;\n      case I_8266_U0_TM1_4: beginTM1814<B_8266_U0_TM1_4*>(busPtr); break;\n      case I_8266_U1_TM1_4: beginTM1814<B_8266_U1_TM1_4*>(busPtr); break;\n      case I_8266_DM_TM1_4: beginTM1814<B_8266_DM_TM1_4*>(busPtr); break;\n      case I_8266_BB_TM1_4: beginTM1814<B_8266_BB_TM1_4*>(busPtr); break;\n      case I_8266_U0_TM2_3: (static_cast<B_8266_U0_TM2_3*>(busPtr))->Begin(); break;\n      case I_8266_U1_TM2_3: (static_cast<B_8266_U1_TM2_3*>(busPtr))->Begin(); break;\n      case I_8266_DM_TM2_3: (static_cast<B_8266_DM_TM2_3*>(busPtr))->Begin(); break;\n      case I_8266_BB_TM2_3: (static_cast<B_8266_BB_TM2_3*>(busPtr))->Begin(); break;\n      case I_HS_DOT_3: beginDotStar<B_HS_DOT_3*>(busPtr, -1, -1, -1, -1, clock_kHz); break;\n      case I_HS_LPD_3: beginDotStar<B_HS_LPD_3*>(busPtr, -1, -1, -1, -1, clock_kHz); break;\n      case I_HS_LPO_3: beginDotStar<B_HS_LPO_3*>(busPtr, -1, -1, -1, -1, clock_kHz); break;\n      case I_HS_WS1_3: beginDotStar<B_HS_WS1_3*>(busPtr, -1, -1, -1, -1, clock_kHz); break;\n      case I_HS_P98_3: beginDotStar<B_HS_P98_3*>(busPtr, -1, -1, -1, -1, clock_kHz); break;\n      case I_8266_U0_UCS_3: (static_cast<B_8266_U0_UCS_3*>(busPtr))->Begin(); break;\n      case I_8266_U1_UCS_3: (static_cast<B_8266_U1_UCS_3*>(busPtr))->Begin(); break;\n      case I_8266_DM_UCS_3: (static_cast<B_8266_DM_UCS_3*>(busPtr))->Begin(); break;\n      case I_8266_BB_UCS_3: (static_cast<B_8266_BB_UCS_3*>(busPtr))->Begin(); break;\n      case I_8266_U0_UCS_4: (static_cast<B_8266_U0_UCS_4*>(busPtr))->Begin(); break;\n      case I_8266_U1_UCS_4: (static_cast<B_8266_U1_UCS_4*>(busPtr))->Begin(); break;\n      case I_8266_DM_UCS_4: (static_cast<B_8266_DM_UCS_4*>(busPtr))->Begin(); break;\n      case I_8266_BB_UCS_4: (static_cast<B_8266_BB_UCS_4*>(busPtr))->Begin(); break;\n      case I_8266_U0_APA106_3: (static_cast<B_8266_U0_APA106_3*>(busPtr))->Begin(); break;\n      case I_8266_U1_APA106_3: (static_cast<B_8266_U1_APA106_3*>(busPtr))->Begin(); break;\n      case I_8266_DM_APA106_3: (static_cast<B_8266_DM_APA106_3*>(busPtr))->Begin(); break;\n      case I_8266_BB_APA106_3: (static_cast<B_8266_BB_APA106_3*>(busPtr))->Begin(); break;\n      case I_8266_U0_FW6_5: (static_cast<B_8266_U0_FW6_5*>(busPtr))->Begin(); break;\n      case I_8266_U1_FW6_5: (static_cast<B_8266_U1_FW6_5*>(busPtr))->Begin(); break;\n      case I_8266_DM_FW6_5: (static_cast<B_8266_DM_FW6_5*>(busPtr))->Begin(); break;\n      case I_8266_BB_FW6_5: (static_cast<B_8266_BB_FW6_5*>(busPtr))->Begin(); break;\n      case I_8266_U0_2805_5: (static_cast<B_8266_U0_2805_5*>(busPtr))->Begin(); break;\n      case I_8266_U1_2805_5: (static_cast<B_8266_U1_2805_5*>(busPtr))->Begin(); break;\n      case I_8266_DM_2805_5: (static_cast<B_8266_DM_2805_5*>(busPtr))->Begin(); break;\n      case I_8266_BB_2805_5: (static_cast<B_8266_BB_2805_5*>(busPtr))->Begin(); break;\n      case I_8266_U0_TM1914_3: beginTM1914<B_8266_U0_TM1914_3*>(busPtr); break;\n      case I_8266_U1_TM1914_3: beginTM1914<B_8266_U1_TM1914_3*>(busPtr); break;\n      case I_8266_DM_TM1914_3: beginTM1914<B_8266_DM_TM1914_3*>(busPtr); break;\n      case I_8266_BB_TM1914_3: beginTM1914<B_8266_BB_TM1914_3*>(busPtr); break;\n      case I_8266_U0_SM16825_5: (static_cast<B_8266_U0_SM16825_5*>(busPtr))->Begin(); break;\n      case I_8266_U1_SM16825_5: (static_cast<B_8266_U1_SM16825_5*>(busPtr))->Begin(); break;\n      case I_8266_DM_SM16825_5: (static_cast<B_8266_DM_SM16825_5*>(busPtr))->Begin(); break;\n      case I_8266_BB_SM16825_5: (static_cast<B_8266_BB_SM16825_5*>(busPtr))->Begin(); break;\n    #endif\n    #ifdef ARDUINO_ARCH_ESP32\n      // RMT buses\n      case I_32_RN_NEO_3: (static_cast<B_32_RN_NEO_3*>(busPtr))->Begin(); break;\n      case I_32_RN_NEO_4: (static_cast<B_32_RN_NEO_4*>(busPtr))->Begin(); break;\n      case I_32_RN_400_3: (static_cast<B_32_RN_400_3*>(busPtr))->Begin(); break;\n      case I_32_RN_TM1_4: beginTM1814<B_32_RN_TM1_4*>(busPtr); break;\n      case I_32_RN_TM2_3: (static_cast<B_32_RN_TM2_3*>(busPtr))->Begin(); break;\n      case I_32_RN_UCS_3: (static_cast<B_32_RN_UCS_3*>(busPtr))->Begin(); break;\n      case I_32_RN_UCS_4: (static_cast<B_32_RN_UCS_4*>(busPtr))->Begin(); break;\n      case I_32_RN_FW6_5: (static_cast<B_32_RN_FW6_5*>(busPtr))->Begin(); break;\n      case I_32_RN_APA106_3: (static_cast<B_32_RN_APA106_3*>(busPtr))->Begin(); break;\n      case I_32_RN_2805_5: (static_cast<B_32_RN_2805_5*>(busPtr))->Begin(); break;\n      case I_32_RN_TM1914_3: beginTM1914<B_32_RN_TM1914_3*>(busPtr); break;\n      case I_32_RN_SM16825_5: (static_cast<B_32_RN_SM16825_5*>(busPtr))->Begin(); break;\n      // I2S1 bus or parellel buses\n      #ifndef CONFIG_IDF_TARGET_ESP32C3\n      case I_32_I2_NEO_3: if (_useParallelI2S) (static_cast<B_32_IP_NEO_3*>(busPtr))->Begin(); else (static_cast<B_32_I2_NEO_3*>(busPtr))->Begin(); break;\n      case I_32_I2_NEO_4: if (_useParallelI2S) (static_cast<B_32_IP_NEO_4*>(busPtr))->Begin(); else (static_cast<B_32_I2_NEO_4*>(busPtr))->Begin(); break;\n      case I_32_I2_400_3: if (_useParallelI2S) (static_cast<B_32_IP_400_3*>(busPtr))->Begin(); else (static_cast<B_32_I2_400_3*>(busPtr))->Begin(); break;\n      case I_32_I2_TM1_4: if (_useParallelI2S) beginTM1814<B_32_IP_TM1_4*>(busPtr); else beginTM1814<B_32_I2_TM1_4*>(busPtr); break;\n      case I_32_I2_TM2_3: if (_useParallelI2S) (static_cast<B_32_IP_TM2_3*>(busPtr))->Begin(); else (static_cast<B_32_I2_TM2_3*>(busPtr))->Begin(); break;\n      case I_32_I2_UCS_3: if (_useParallelI2S) (static_cast<B_32_IP_UCS_3*>(busPtr))->Begin(); else (static_cast<B_32_I2_UCS_3*>(busPtr))->Begin(); break;\n      case I_32_I2_UCS_4: if (_useParallelI2S) (static_cast<B_32_IP_UCS_4*>(busPtr))->Begin(); else (static_cast<B_32_I2_UCS_4*>(busPtr))->Begin(); break;\n      case I_32_I2_FW6_5: if (_useParallelI2S) (static_cast<B_32_IP_FW6_5*>(busPtr))->Begin(); else (static_cast<B_32_I2_FW6_5*>(busPtr))->Begin(); break;\n      case I_32_I2_APA106_3: if (_useParallelI2S) (static_cast<B_32_IP_APA106_3*>(busPtr))->Begin(); else (static_cast<B_32_I2_APA106_3*>(busPtr))->Begin(); break;\n      case I_32_I2_2805_5: if (_useParallelI2S) (static_cast<B_32_IP_2805_5*>(busPtr))->Begin(); else (static_cast<B_32_I2_2805_5*>(busPtr))->Begin(); break;\n      case I_32_I2_TM1914_3: if (_useParallelI2S) beginTM1914<B_32_IP_TM1914_3*>(busPtr); else beginTM1914<B_32_I2_TM1914_3*>(busPtr); break;\n      case I_32_I2_SM16825_5: if (_useParallelI2S) (static_cast<B_32_IP_SM16825_5*>(busPtr))->Begin(); else (static_cast<B_32_I2_SM16825_5*>(busPtr))->Begin(); break;\n      #endif\n      // ESP32 can (and should, to avoid inadvertantly driving the chip select signal) specify the pins used for SPI, but only in begin()\n      case I_HS_DOT_3: beginDotStar<B_HS_DOT_3*>(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break;\n      case I_HS_LPD_3: beginDotStar<B_HS_LPD_3*>(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break;\n      case I_HS_LPO_3: beginDotStar<B_HS_LPO_3*>(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break;\n      case I_HS_WS1_3: beginDotStar<B_HS_WS1_3*>(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break;\n      case I_HS_P98_3: beginDotStar<B_HS_P98_3*>(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break;\n    #endif\n      case I_SS_DOT_3: (static_cast<B_SS_DOT_3*>(busPtr))->Begin(); break;\n      case I_SS_LPD_3: (static_cast<B_SS_LPD_3*>(busPtr))->Begin(); break;\n      case I_SS_LPO_3: (static_cast<B_SS_LPO_3*>(busPtr))->Begin(); break;\n      case I_SS_WS1_3: (static_cast<B_SS_WS1_3*>(busPtr))->Begin(); break;\n      case I_SS_P98_3: (static_cast<B_SS_P98_3*>(busPtr))->Begin(); break;\n    }\n  }\n\n  static void* create(uint8_t busType, uint8_t* pins, uint16_t len) {\n    void* busPtr = nullptr;\n    switch (busType) {\n      case I_NONE: break;\n    #ifdef ESP8266\n      case I_8266_U0_NEO_3: busPtr = new B_8266_U0_NEO_3(len, pins[0]); break;\n      case I_8266_U1_NEO_3: busPtr = new B_8266_U1_NEO_3(len, pins[0]); break;\n      case I_8266_DM_NEO_3: busPtr = new B_8266_DM_NEO_3(len, pins[0]); break;\n      case I_8266_BB_NEO_3: busPtr = new B_8266_BB_NEO_3(len, pins[0]); break;\n      case I_8266_U0_NEO_4: busPtr = new B_8266_U0_NEO_4(len, pins[0]); break;\n      case I_8266_U1_NEO_4: busPtr = new B_8266_U1_NEO_4(len, pins[0]); break;\n      case I_8266_DM_NEO_4: busPtr = new B_8266_DM_NEO_4(len, pins[0]); break;\n      case I_8266_BB_NEO_4: busPtr = new B_8266_BB_NEO_4(len, pins[0]); break;\n      case I_8266_U0_400_3: busPtr = new B_8266_U0_400_3(len, pins[0]); break;\n      case I_8266_U1_400_3: busPtr = new B_8266_U1_400_3(len, pins[0]); break;\n      case I_8266_DM_400_3: busPtr = new B_8266_DM_400_3(len, pins[0]); break;\n      case I_8266_BB_400_3: busPtr = new B_8266_BB_400_3(len, pins[0]); break;\n      case I_8266_U0_TM1_4: busPtr = new B_8266_U0_TM1_4(len, pins[0]); break;\n      case I_8266_U1_TM1_4: busPtr = new B_8266_U1_TM1_4(len, pins[0]); break;\n      case I_8266_DM_TM1_4: busPtr = new B_8266_DM_TM1_4(len, pins[0]); break;\n      case I_8266_BB_TM1_4: busPtr = new B_8266_BB_TM1_4(len, pins[0]); break;\n      case I_8266_U0_TM2_3: busPtr = new B_8266_U0_TM2_3(len, pins[0]); break;\n      case I_8266_U1_TM2_3: busPtr = new B_8266_U1_TM2_3(len, pins[0]); break;\n      case I_8266_DM_TM2_3: busPtr = new B_8266_DM_TM2_3(len, pins[0]); break;\n      case I_8266_BB_TM2_3: busPtr = new B_8266_BB_TM2_3(len, pins[0]); break;\n      case I_8266_U0_UCS_3: busPtr = new B_8266_U0_UCS_3(len, pins[0]); break;\n      case I_8266_U1_UCS_3: busPtr = new B_8266_U1_UCS_3(len, pins[0]); break;\n      case I_8266_DM_UCS_3: busPtr = new B_8266_DM_UCS_3(len, pins[0]); break;\n      case I_8266_BB_UCS_3: busPtr = new B_8266_BB_UCS_3(len, pins[0]); break;\n      case I_8266_U0_UCS_4: busPtr = new B_8266_U0_UCS_4(len, pins[0]); break;\n      case I_8266_U1_UCS_4: busPtr = new B_8266_U1_UCS_4(len, pins[0]); break;\n      case I_8266_DM_UCS_4: busPtr = new B_8266_DM_UCS_4(len, pins[0]); break;\n      case I_8266_BB_UCS_4: busPtr = new B_8266_BB_UCS_4(len, pins[0]); break;\n      case I_8266_U0_APA106_3: busPtr = new B_8266_U0_APA106_3(len, pins[0]); break;\n      case I_8266_U1_APA106_3: busPtr = new B_8266_U1_APA106_3(len, pins[0]); break;\n      case I_8266_DM_APA106_3: busPtr = new B_8266_DM_APA106_3(len, pins[0]); break;\n      case I_8266_BB_APA106_3: busPtr = new B_8266_BB_APA106_3(len, pins[0]); break;\n      case I_8266_U0_FW6_5: busPtr = new B_8266_U0_FW6_5(len, pins[0]); break;\n      case I_8266_U1_FW6_5: busPtr = new B_8266_U1_FW6_5(len, pins[0]); break;\n      case I_8266_DM_FW6_5: busPtr = new B_8266_DM_FW6_5(len, pins[0]); break;\n      case I_8266_BB_FW6_5: busPtr = new B_8266_BB_FW6_5(len, pins[0]); break;\n      case I_8266_U0_2805_5: busPtr = new B_8266_U0_2805_5(len, pins[0]); break;\n      case I_8266_U1_2805_5: busPtr = new B_8266_U1_2805_5(len, pins[0]); break;\n      case I_8266_DM_2805_5: busPtr = new B_8266_DM_2805_5(len, pins[0]); break;\n      case I_8266_BB_2805_5: busPtr = new B_8266_BB_2805_5(len, pins[0]); break;\n      case I_8266_U0_TM1914_3: busPtr = new B_8266_U0_TM1914_3(len, pins[0]); break;\n      case I_8266_U1_TM1914_3: busPtr = new B_8266_U1_TM1914_3(len, pins[0]); break;\n      case I_8266_DM_TM1914_3: busPtr = new B_8266_DM_TM1914_3(len, pins[0]); break;\n      case I_8266_BB_TM1914_3: busPtr = new B_8266_BB_TM1914_3(len, pins[0]); break;\n      case I_8266_U0_SM16825_5: busPtr = new B_8266_U0_SM16825_5(len, pins[0]); break;\n      case I_8266_U1_SM16825_5: busPtr = new B_8266_U1_SM16825_5(len, pins[0]); break;\n      case I_8266_DM_SM16825_5: busPtr = new B_8266_DM_SM16825_5(len, pins[0]); break;\n      case I_8266_BB_SM16825_5: busPtr = new B_8266_BB_SM16825_5(len, pins[0]); break;\n    #endif\n    #ifdef ARDUINO_ARCH_ESP32\n      // RMT buses\n      case I_32_RN_NEO_3: busPtr = new B_32_RN_NEO_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break;\n      case I_32_RN_NEO_4: busPtr = new B_32_RN_NEO_4(len, pins[0], (NeoBusChannel)_rmtChannel++); break;\n      case I_32_RN_400_3: busPtr = new B_32_RN_400_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break;\n      case I_32_RN_TM1_4: busPtr = new B_32_RN_TM1_4(len, pins[0], (NeoBusChannel)_rmtChannel++); break;\n      case I_32_RN_TM2_3: busPtr = new B_32_RN_TM2_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break;\n      case I_32_RN_UCS_3: busPtr = new B_32_RN_UCS_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break;\n      case I_32_RN_UCS_4: busPtr = new B_32_RN_UCS_4(len, pins[0], (NeoBusChannel)_rmtChannel++); break;\n      case I_32_RN_APA106_3: busPtr = new B_32_RN_APA106_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break;\n      case I_32_RN_FW6_5: busPtr = new B_32_RN_FW6_5(len, pins[0], (NeoBusChannel)_rmtChannel++); break;\n      case I_32_RN_2805_5: busPtr = new B_32_RN_2805_5(len, pins[0], (NeoBusChannel)_rmtChannel++); break;\n      case I_32_RN_TM1914_3: busPtr = new B_32_RN_TM1914_3(len, pins[0], (NeoBusChannel)_rmtChannel++); break;\n      case I_32_RN_SM16825_5: busPtr = new B_32_RN_SM16825_5(len, pins[0], (NeoBusChannel)_rmtChannel++); break;\n      // I2S1 bus or paralell buses\n      #ifndef CONFIG_IDF_TARGET_ESP32C3\n      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;\n      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;\n      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;\n      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;\n      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;\n      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;\n      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;\n      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;\n      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;\n      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;\n      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;\n      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;\n      #endif\n    #endif\n      // for 2-wire: pins[1] is clk, pins[0] is dat.  begin expects (len, clk, dat)\n      case I_HS_DOT_3: busPtr = new B_HS_DOT_3(len, pins[1], pins[0]); break;\n      case I_SS_DOT_3: busPtr = new B_SS_DOT_3(len, pins[1], pins[0]); break;\n      case I_HS_LPD_3: busPtr = new B_HS_LPD_3(len, pins[1], pins[0]); break;\n      case I_SS_LPD_3: busPtr = new B_SS_LPD_3(len, pins[1], pins[0]); break;\n      case I_HS_LPO_3: busPtr = new B_HS_LPO_3(len, pins[1], pins[0]); break;\n      case I_SS_LPO_3: busPtr = new B_SS_LPO_3(len, pins[1], pins[0]); break;\n      case I_HS_WS1_3: busPtr = new B_HS_WS1_3(len, pins[1], pins[0]); break;\n      case I_SS_WS1_3: busPtr = new B_SS_WS1_3(len, pins[1], pins[0]); break;\n      case I_HS_P98_3: busPtr = new B_HS_P98_3(len, pins[1], pins[0]); break;\n      case I_SS_P98_3: busPtr = new B_SS_P98_3(len, pins[1], pins[0]); break;\n    }\n\n    return busPtr;\n  }\n\n  static void show(void* busPtr, uint8_t busType, bool consistent = true) {\n    switch (busType) {\n      case I_NONE: break;\n    #ifdef ESP8266\n      case I_8266_U0_NEO_3: (static_cast<B_8266_U0_NEO_3*>(busPtr))->Show(consistent); break;\n      case I_8266_U1_NEO_3: (static_cast<B_8266_U1_NEO_3*>(busPtr))->Show(consistent); break;\n      case I_8266_DM_NEO_3: (static_cast<B_8266_DM_NEO_3*>(busPtr))->Show(consistent); break;\n      case I_8266_BB_NEO_3: (static_cast<B_8266_BB_NEO_3*>(busPtr))->Show(consistent); break;\n      case I_8266_U0_NEO_4: (static_cast<B_8266_U0_NEO_4*>(busPtr))->Show(consistent); break;\n      case I_8266_U1_NEO_4: (static_cast<B_8266_U1_NEO_4*>(busPtr))->Show(consistent); break;\n      case I_8266_DM_NEO_4: (static_cast<B_8266_DM_NEO_4*>(busPtr))->Show(consistent); break;\n      case I_8266_BB_NEO_4: (static_cast<B_8266_BB_NEO_4*>(busPtr))->Show(consistent); break;\n      case I_8266_U0_400_3: (static_cast<B_8266_U0_400_3*>(busPtr))->Show(consistent); break;\n      case I_8266_U1_400_3: (static_cast<B_8266_U1_400_3*>(busPtr))->Show(consistent); break;\n      case I_8266_DM_400_3: (static_cast<B_8266_DM_400_3*>(busPtr))->Show(consistent); break;\n      case I_8266_BB_400_3: (static_cast<B_8266_BB_400_3*>(busPtr))->Show(consistent); break;\n      case I_8266_U0_TM1_4: (static_cast<B_8266_U0_TM1_4*>(busPtr))->Show(consistent); break;\n      case I_8266_U1_TM1_4: (static_cast<B_8266_U1_TM1_4*>(busPtr))->Show(consistent); break;\n      case I_8266_DM_TM1_4: (static_cast<B_8266_DM_TM1_4*>(busPtr))->Show(consistent); break;\n      case I_8266_BB_TM1_4: (static_cast<B_8266_BB_TM1_4*>(busPtr))->Show(consistent); break;\n      case I_8266_U0_TM2_3: (static_cast<B_8266_U0_TM2_3*>(busPtr))->Show(consistent); break;\n      case I_8266_U1_TM2_3: (static_cast<B_8266_U1_TM2_3*>(busPtr))->Show(consistent); break;\n      case I_8266_DM_TM2_3: (static_cast<B_8266_DM_TM2_3*>(busPtr))->Show(consistent); break;\n      case I_8266_BB_TM2_3: (static_cast<B_8266_BB_TM2_3*>(busPtr))->Show(consistent); break;\n      case I_8266_U0_UCS_3: (static_cast<B_8266_U0_UCS_3*>(busPtr))->Show(consistent); break;\n      case I_8266_U1_UCS_3: (static_cast<B_8266_U1_UCS_3*>(busPtr))->Show(consistent); break;\n      case I_8266_DM_UCS_3: (static_cast<B_8266_DM_UCS_3*>(busPtr))->Show(consistent); break;\n      case I_8266_BB_UCS_3: (static_cast<B_8266_BB_UCS_3*>(busPtr))->Show(consistent); break;\n      case I_8266_U0_UCS_4: (static_cast<B_8266_U0_UCS_4*>(busPtr))->Show(consistent); break;\n      case I_8266_U1_UCS_4: (static_cast<B_8266_U1_UCS_4*>(busPtr))->Show(consistent); break;\n      case I_8266_DM_UCS_4: (static_cast<B_8266_DM_UCS_4*>(busPtr))->Show(consistent); break;\n      case I_8266_BB_UCS_4: (static_cast<B_8266_BB_UCS_4*>(busPtr))->Show(consistent); break;\n      case I_8266_U0_APA106_3: (static_cast<B_8266_U0_APA106_3*>(busPtr))->Show(consistent); break;\n      case I_8266_U1_APA106_3: (static_cast<B_8266_U1_APA106_3*>(busPtr))->Show(consistent); break;\n      case I_8266_DM_APA106_3: (static_cast<B_8266_DM_APA106_3*>(busPtr))->Show(consistent); break;\n      case I_8266_BB_APA106_3: (static_cast<B_8266_BB_APA106_3*>(busPtr))->Show(consistent); break;\n      case I_8266_U0_FW6_5: (static_cast<B_8266_U0_FW6_5*>(busPtr))->Show(consistent); break;\n      case I_8266_U1_FW6_5: (static_cast<B_8266_U1_FW6_5*>(busPtr))->Show(consistent); break;\n      case I_8266_DM_FW6_5: (static_cast<B_8266_DM_FW6_5*>(busPtr))->Show(consistent); break;\n      case I_8266_BB_FW6_5: (static_cast<B_8266_BB_FW6_5*>(busPtr))->Show(consistent); break;\n      case I_8266_U0_2805_5: (static_cast<B_8266_U0_2805_5*>(busPtr))->Show(consistent); break;\n      case I_8266_U1_2805_5: (static_cast<B_8266_U1_2805_5*>(busPtr))->Show(consistent); break;\n      case I_8266_DM_2805_5: (static_cast<B_8266_DM_2805_5*>(busPtr))->Show(consistent); break;\n      case I_8266_BB_2805_5: (static_cast<B_8266_BB_2805_5*>(busPtr))->Show(consistent); break;\n      case I_8266_U0_TM1914_3: (static_cast<B_8266_U0_TM1914_3*>(busPtr))->Show(consistent); break;\n      case I_8266_U1_TM1914_3: (static_cast<B_8266_U1_TM1914_3*>(busPtr))->Show(consistent); break;\n      case I_8266_DM_TM1914_3: (static_cast<B_8266_DM_TM1914_3*>(busPtr))->Show(consistent); break;\n      case I_8266_BB_TM1914_3: (static_cast<B_8266_BB_TM1914_3*>(busPtr))->Show(consistent); break;\n      case I_8266_U0_SM16825_5: (static_cast<B_8266_U0_SM16825_5*>(busPtr))->Show(consistent); break;\n      case I_8266_U1_SM16825_5: (static_cast<B_8266_U1_SM16825_5*>(busPtr))->Show(consistent); break;\n      case I_8266_DM_SM16825_5: (static_cast<B_8266_DM_SM16825_5*>(busPtr))->Show(consistent); break;\n      case I_8266_BB_SM16825_5: (static_cast<B_8266_BB_SM16825_5*>(busPtr))->Show(consistent); break;\n    #endif\n    #ifdef ARDUINO_ARCH_ESP32\n      // RMT buses\n      case I_32_RN_NEO_3: (static_cast<B_32_RN_NEO_3*>(busPtr))->Show(consistent); break;\n      case I_32_RN_NEO_4: (static_cast<B_32_RN_NEO_4*>(busPtr))->Show(consistent); break;\n      case I_32_RN_400_3: (static_cast<B_32_RN_400_3*>(busPtr))->Show(consistent); break;\n      case I_32_RN_TM1_4: (static_cast<B_32_RN_TM1_4*>(busPtr))->Show(consistent); break;\n      case I_32_RN_TM2_3: (static_cast<B_32_RN_TM2_3*>(busPtr))->Show(consistent); break;\n      case I_32_RN_UCS_3: (static_cast<B_32_RN_UCS_3*>(busPtr))->Show(consistent); break;\n      case I_32_RN_UCS_4: (static_cast<B_32_RN_UCS_4*>(busPtr))->Show(consistent); break;\n      case I_32_RN_APA106_3: (static_cast<B_32_RN_APA106_3*>(busPtr))->Show(consistent); break;\n      case I_32_RN_FW6_5: (static_cast<B_32_RN_FW6_5*>(busPtr))->Show(consistent); break;\n      case I_32_RN_2805_5: (static_cast<B_32_RN_2805_5*>(busPtr))->Show(consistent); break;\n      case I_32_RN_TM1914_3: (static_cast<B_32_RN_TM1914_3*>(busPtr))->Show(consistent); break;\n      case I_32_RN_SM16825_5: (static_cast<B_32_RN_SM16825_5*>(busPtr))->Show(consistent); break;\n      // I2S1 bus or paralell buses\n      #ifndef CONFIG_IDF_TARGET_ESP32C3\n      case I_32_I2_NEO_3: if (_useParallelI2S) (static_cast<B_32_IP_NEO_3*>(busPtr))->Show(consistent); else (static_cast<B_32_I2_NEO_3*>(busPtr))->Show(consistent); break;\n      case I_32_I2_NEO_4: if (_useParallelI2S) (static_cast<B_32_IP_NEO_4*>(busPtr))->Show(consistent); else (static_cast<B_32_I2_NEO_4*>(busPtr))->Show(consistent); break;\n      case I_32_I2_400_3: if (_useParallelI2S) (static_cast<B_32_IP_400_3*>(busPtr))->Show(consistent); else (static_cast<B_32_I2_400_3*>(busPtr))->Show(consistent); break;\n      case I_32_I2_TM1_4: if (_useParallelI2S) (static_cast<B_32_IP_TM1_4*>(busPtr))->Show(consistent); else (static_cast<B_32_I2_TM1_4*>(busPtr))->Show(consistent); break;\n      case I_32_I2_TM2_3: if (_useParallelI2S) (static_cast<B_32_IP_TM2_3*>(busPtr))->Show(consistent); else (static_cast<B_32_I2_TM2_3*>(busPtr))->Show(consistent); break;\n      case I_32_I2_UCS_3: if (_useParallelI2S) (static_cast<B_32_IP_UCS_3*>(busPtr))->Show(consistent); else (static_cast<B_32_I2_UCS_3*>(busPtr))->Show(consistent); break;\n      case I_32_I2_UCS_4: if (_useParallelI2S) (static_cast<B_32_IP_UCS_4*>(busPtr))->Show(consistent); else (static_cast<B_32_I2_UCS_4*>(busPtr))->Show(consistent); break;\n      case I_32_I2_APA106_3: if (_useParallelI2S) (static_cast<B_32_IP_APA106_3*>(busPtr))->Show(consistent); else (static_cast<B_32_I2_APA106_3*>(busPtr))->Show(consistent); break;\n      case I_32_I2_FW6_5: if (_useParallelI2S) (static_cast<B_32_IP_FW6_5*>(busPtr))->Show(consistent); else (static_cast<B_32_I2_FW6_5*>(busPtr))->Show(consistent); break;\n      case I_32_I2_2805_5: if (_useParallelI2S) (static_cast<B_32_IP_2805_5*>(busPtr))->Show(consistent); else (static_cast<B_32_I2_2805_5*>(busPtr))->Show(consistent); break;\n      case I_32_I2_TM1914_3: if (_useParallelI2S) (static_cast<B_32_IP_TM1914_3*>(busPtr))->Show(consistent); else (static_cast<B_32_I2_TM1914_3*>(busPtr))->Show(consistent); break;\n      case I_32_I2_SM16825_5: if (_useParallelI2S) (static_cast<B_32_IP_SM16825_5*>(busPtr))->Show(consistent); else (static_cast<B_32_I2_SM16825_5*>(busPtr))->Show(consistent); break;\n      #endif\n    #endif\n      case I_HS_DOT_3: (static_cast<B_HS_DOT_3*>(busPtr))->Show(consistent); break;\n      case I_SS_DOT_3: (static_cast<B_SS_DOT_3*>(busPtr))->Show(consistent); break;\n      case I_HS_LPD_3: (static_cast<B_HS_LPD_3*>(busPtr))->Show(consistent); break;\n      case I_SS_LPD_3: (static_cast<B_SS_LPD_3*>(busPtr))->Show(consistent); break;\n      case I_HS_LPO_3: (static_cast<B_HS_LPO_3*>(busPtr))->Show(consistent); break;\n      case I_SS_LPO_3: (static_cast<B_SS_LPO_3*>(busPtr))->Show(consistent); break;\n      case I_HS_WS1_3: (static_cast<B_HS_WS1_3*>(busPtr))->Show(consistent); break;\n      case I_SS_WS1_3: (static_cast<B_SS_WS1_3*>(busPtr))->Show(consistent); break;\n      case I_HS_P98_3: (static_cast<B_HS_P98_3*>(busPtr))->Show(consistent); break;\n      case I_SS_P98_3: (static_cast<B_SS_P98_3*>(busPtr))->Show(consistent); break;\n    }\n  }\n\n  static bool canShow(void* busPtr, uint8_t busType) {\n    switch (busType) {\n      case I_NONE: return true;\n    #ifdef ESP8266\n      case I_8266_U0_NEO_3: return (static_cast<B_8266_U0_NEO_3*>(busPtr))->CanShow(); break;\n      case I_8266_U1_NEO_3: return (static_cast<B_8266_U1_NEO_3*>(busPtr))->CanShow(); break;\n      case I_8266_DM_NEO_3: return (static_cast<B_8266_DM_NEO_3*>(busPtr))->CanShow(); break;\n      case I_8266_BB_NEO_3: return (static_cast<B_8266_BB_NEO_3*>(busPtr))->CanShow(); break;\n      case I_8266_U0_NEO_4: return (static_cast<B_8266_U0_NEO_4*>(busPtr))->CanShow(); break;\n      case I_8266_U1_NEO_4: return (static_cast<B_8266_U1_NEO_4*>(busPtr))->CanShow(); break;\n      case I_8266_DM_NEO_4: return (static_cast<B_8266_DM_NEO_4*>(busPtr))->CanShow(); break;\n      case I_8266_BB_NEO_4: return (static_cast<B_8266_BB_NEO_4*>(busPtr))->CanShow(); break;\n      case I_8266_U0_400_3: return (static_cast<B_8266_U0_400_3*>(busPtr))->CanShow(); break;\n      case I_8266_U1_400_3: return (static_cast<B_8266_U1_400_3*>(busPtr))->CanShow(); break;\n      case I_8266_DM_400_3: return (static_cast<B_8266_DM_400_3*>(busPtr))->CanShow(); break;\n      case I_8266_BB_400_3: return (static_cast<B_8266_BB_400_3*>(busPtr))->CanShow(); break;\n      case I_8266_U0_TM1_4: return (static_cast<B_8266_U0_TM1_4*>(busPtr))->CanShow(); break;\n      case I_8266_U1_TM1_4: return (static_cast<B_8266_U1_TM1_4*>(busPtr))->CanShow(); break;\n      case I_8266_DM_TM1_4: return (static_cast<B_8266_DM_TM1_4*>(busPtr))->CanShow(); break;\n      case I_8266_BB_TM1_4: return (static_cast<B_8266_BB_TM1_4*>(busPtr))->CanShow(); break;\n      case I_8266_U0_TM2_3: return (static_cast<B_8266_U0_TM2_3*>(busPtr))->CanShow(); break;\n      case I_8266_U1_TM2_3: return (static_cast<B_8266_U1_TM2_3*>(busPtr))->CanShow(); break;\n      case I_8266_DM_TM2_3: return (static_cast<B_8266_DM_TM2_3*>(busPtr))->CanShow(); break;\n      case I_8266_BB_TM2_3: return (static_cast<B_8266_BB_TM2_3*>(busPtr))->CanShow(); break;\n      case I_8266_U0_UCS_3: return (static_cast<B_8266_U0_UCS_3*>(busPtr))->CanShow(); break;\n      case I_8266_U1_UCS_3: return (static_cast<B_8266_U1_UCS_3*>(busPtr))->CanShow(); break;\n      case I_8266_DM_UCS_3: return (static_cast<B_8266_DM_UCS_3*>(busPtr))->CanShow(); break;\n      case I_8266_BB_UCS_3: return (static_cast<B_8266_BB_UCS_3*>(busPtr))->CanShow(); break;\n      case I_8266_U0_UCS_4: return (static_cast<B_8266_U0_UCS_4*>(busPtr))->CanShow(); break;\n      case I_8266_U1_UCS_4: return (static_cast<B_8266_U1_UCS_4*>(busPtr))->CanShow(); break;\n      case I_8266_DM_UCS_4: return (static_cast<B_8266_DM_UCS_4*>(busPtr))->CanShow(); break;\n      case I_8266_BB_UCS_4: return (static_cast<B_8266_BB_UCS_4*>(busPtr))->CanShow(); break;\n      case I_8266_U0_APA106_3: return (static_cast<B_8266_U0_APA106_3*>(busPtr))->CanShow(); break;\n      case I_8266_U1_APA106_3: return (static_cast<B_8266_U1_APA106_3*>(busPtr))->CanShow(); break;\n      case I_8266_DM_APA106_3: return (static_cast<B_8266_DM_APA106_3*>(busPtr))->CanShow(); break;\n      case I_8266_BB_APA106_3: return (static_cast<B_8266_BB_APA106_3*>(busPtr))->CanShow(); break;\n      case I_8266_U0_FW6_5: return (static_cast<B_8266_U0_FW6_5*>(busPtr))->CanShow(); break;\n      case I_8266_U1_FW6_5: return (static_cast<B_8266_U1_FW6_5*>(busPtr))->CanShow(); break;\n      case I_8266_DM_FW6_5: return (static_cast<B_8266_DM_FW6_5*>(busPtr))->CanShow(); break;\n      case I_8266_BB_FW6_5: return (static_cast<B_8266_BB_FW6_5*>(busPtr))->CanShow(); break;\n      case I_8266_U0_2805_5: return (static_cast<B_8266_U0_2805_5*>(busPtr))->CanShow(); break;\n      case I_8266_U1_2805_5: return (static_cast<B_8266_U1_2805_5*>(busPtr))->CanShow(); break;\n      case I_8266_DM_2805_5: return (static_cast<B_8266_DM_2805_5*>(busPtr))->CanShow(); break;\n      case I_8266_BB_2805_5: return (static_cast<B_8266_BB_2805_5*>(busPtr))->CanShow(); break;\n      case I_8266_U0_TM1914_3: return (static_cast<B_8266_U0_TM1914_3*>(busPtr))->CanShow(); break;\n      case I_8266_U1_TM1914_3: return (static_cast<B_8266_U1_TM1914_3*>(busPtr))->CanShow(); break;\n      case I_8266_DM_TM1914_3: return (static_cast<B_8266_DM_TM1914_3*>(busPtr))->CanShow(); break;\n      case I_8266_BB_TM1914_3: return (static_cast<B_8266_BB_TM1914_3*>(busPtr))->CanShow(); break;\n      case I_8266_U0_SM16825_5: return (static_cast<B_8266_U0_SM16825_5*>(busPtr))->CanShow(); break;\n      case I_8266_U1_SM16825_5: return (static_cast<B_8266_U1_SM16825_5*>(busPtr))->CanShow(); break;\n      case I_8266_DM_SM16825_5: return (static_cast<B_8266_DM_SM16825_5*>(busPtr))->CanShow(); break;\n      case I_8266_BB_SM16825_5: return (static_cast<B_8266_BB_SM16825_5*>(busPtr))->CanShow(); break;\n    #endif\n    #ifdef ARDUINO_ARCH_ESP32\n      // RMT buses\n      case I_32_RN_NEO_3: return (static_cast<B_32_RN_NEO_3*>(busPtr))->CanShow(); break;\n      case I_32_RN_NEO_4: return (static_cast<B_32_RN_NEO_4*>(busPtr))->CanShow(); break;\n      case I_32_RN_400_3: return (static_cast<B_32_RN_400_3*>(busPtr))->CanShow(); break;\n      case I_32_RN_TM1_4: return (static_cast<B_32_RN_TM1_4*>(busPtr))->CanShow(); break;\n      case I_32_RN_TM2_3: return (static_cast<B_32_RN_TM2_3*>(busPtr))->CanShow(); break;\n      case I_32_RN_UCS_3: return (static_cast<B_32_RN_UCS_3*>(busPtr))->CanShow(); break;\n      case I_32_RN_UCS_4: return (static_cast<B_32_RN_UCS_4*>(busPtr))->CanShow(); break;\n      case I_32_RN_APA106_3: return (static_cast<B_32_RN_APA106_3*>(busPtr))->CanShow(); break;\n      case I_32_RN_FW6_5: return (static_cast<B_32_RN_FW6_5*>(busPtr))->CanShow(); break;\n      case I_32_RN_2805_5: return (static_cast<B_32_RN_2805_5*>(busPtr))->CanShow(); break;\n      case I_32_RN_TM1914_3: return (static_cast<B_32_RN_TM1914_3*>(busPtr))->CanShow(); break;\n      case I_32_RN_SM16825_5: return (static_cast<B_32_RN_SM16825_5*>(busPtr))->CanShow(); break;\n      // I2S1 bus or paralell buses\n      #ifndef CONFIG_IDF_TARGET_ESP32C3\n      case I_32_I2_NEO_3: if (_useParallelI2S) return (static_cast<B_32_IP_NEO_3*>(busPtr))->CanShow(); else return (static_cast<B_32_I2_NEO_3*>(busPtr))->CanShow(); break;\n      case I_32_I2_NEO_4: if (_useParallelI2S) return (static_cast<B_32_IP_NEO_4*>(busPtr))->CanShow(); else return (static_cast<B_32_I2_NEO_4*>(busPtr))->CanShow(); break;\n      case I_32_I2_400_3: if (_useParallelI2S) return (static_cast<B_32_IP_400_3*>(busPtr))->CanShow(); else return (static_cast<B_32_I2_400_3*>(busPtr))->CanShow(); break;\n      case I_32_I2_TM1_4: if (_useParallelI2S) return (static_cast<B_32_IP_TM1_4*>(busPtr))->CanShow(); else return (static_cast<B_32_I2_TM1_4*>(busPtr))->CanShow(); break;\n      case I_32_I2_TM2_3: if (_useParallelI2S) return (static_cast<B_32_IP_TM2_3*>(busPtr))->CanShow(); else return (static_cast<B_32_I2_TM2_3*>(busPtr))->CanShow(); break;\n      case I_32_I2_UCS_3: if (_useParallelI2S) return (static_cast<B_32_IP_UCS_3*>(busPtr))->CanShow(); else return (static_cast<B_32_I2_UCS_3*>(busPtr))->CanShow(); break;\n      case I_32_I2_UCS_4: if (_useParallelI2S) return (static_cast<B_32_IP_UCS_4*>(busPtr))->CanShow(); else return (static_cast<B_32_I2_UCS_4*>(busPtr))->CanShow(); break;\n      case I_32_I2_APA106_3: if (_useParallelI2S) return (static_cast<B_32_IP_APA106_3*>(busPtr))->CanShow(); else return (static_cast<B_32_I2_APA106_3*>(busPtr))->CanShow(); break;\n      case I_32_I2_FW6_5: if (_useParallelI2S) return (static_cast<B_32_IP_FW6_5*>(busPtr))->CanShow(); else return (static_cast<B_32_I2_FW6_5*>(busPtr))->CanShow(); break;\n      case I_32_I2_2805_5: if (_useParallelI2S) return (static_cast<B_32_IP_2805_5*>(busPtr))->CanShow(); else return (static_cast<B_32_I2_2805_5*>(busPtr))->CanShow(); break;\n      case I_32_I2_TM1914_3: if (_useParallelI2S) return (static_cast<B_32_IP_TM1914_3*>(busPtr))->CanShow(); else return (static_cast<B_32_I2_TM1914_3*>(busPtr))->CanShow(); break;\n      case I_32_I2_SM16825_5: if (_useParallelI2S) return (static_cast<B_32_IP_SM16825_5*>(busPtr))->CanShow(); else return (static_cast<B_32_I2_SM16825_5*>(busPtr))->CanShow(); break;\n      #endif\n    #endif\n      case I_HS_DOT_3: return (static_cast<B_HS_DOT_3*>(busPtr))->CanShow(); break;\n      case I_SS_DOT_3: return (static_cast<B_SS_DOT_3*>(busPtr))->CanShow(); break;\n      case I_HS_LPD_3: return (static_cast<B_HS_LPD_3*>(busPtr))->CanShow(); break;\n      case I_SS_LPD_3: return (static_cast<B_SS_LPD_3*>(busPtr))->CanShow(); break;\n      case I_HS_LPO_3: return (static_cast<B_HS_LPO_3*>(busPtr))->CanShow(); break;\n      case I_SS_LPO_3: return (static_cast<B_SS_LPO_3*>(busPtr))->CanShow(); break;\n      case I_HS_WS1_3: return (static_cast<B_HS_WS1_3*>(busPtr))->CanShow(); break;\n      case I_SS_WS1_3: return (static_cast<B_SS_WS1_3*>(busPtr))->CanShow(); break;\n      case I_HS_P98_3: return (static_cast<B_HS_P98_3*>(busPtr))->CanShow(); break;\n      case I_SS_P98_3: return (static_cast<B_SS_P98_3*>(busPtr))->CanShow(); break;\n    }\n    return true;\n  }\n\n  [[gnu::hot]] static void setPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint32_t c, uint8_t co, uint16_t wwcw = 0) {\n    uint8_t r = c >> 16;\n    uint8_t g = c >> 8;\n    uint8_t b = c >> 0;\n    uint8_t w = c >> 24;\n    RgbwColor col;\n    uint8_t cctWW = wwcw & 0xFF, cctCW = (wwcw>>8) & 0xFF;\n\n    // reorder channels to selected order\n    switch (co & 0x0F) {\n      default: col.G = g; col.R = r; col.B = b; break; //0 = GRB, default\n      case  1: col.G = r; col.R = g; col.B = b; break; //1 = RGB, common for WS2811\n      case  2: col.G = b; col.R = r; col.B = g; break; //2 = BRG\n      case  3: col.G = r; col.R = b; col.B = g; break; //3 = RBG\n      case  4: col.G = b; col.R = g; col.B = r; break; //4 = BGR\n      case  5: col.G = g; col.R = b; col.B = r; break; //5 = GBR\n    }\n    // upper nibble contains W swap information\n    switch (co >> 4) {\n      default: col.W = w;                break; // no swapping\n      case  1: col.W = col.B; col.B = w; break; // swap W & B\n      case  2: col.W = col.G; col.G = w; break; // swap W & G\n      case  3: col.W = col.R; col.R = w; break; // swap W & R\n      case  4: std::swap(cctWW, cctCW);  break; // swap WW & CW\n    }\n\n    switch (busType) {\n      case I_NONE: break;\n    #ifdef ESP8266\n      case I_8266_U0_NEO_3: (static_cast<B_8266_U0_NEO_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_8266_U1_NEO_3: (static_cast<B_8266_U1_NEO_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_8266_DM_NEO_3: (static_cast<B_8266_DM_NEO_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_8266_BB_NEO_3: (static_cast<B_8266_BB_NEO_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_8266_U0_NEO_4: (static_cast<B_8266_U0_NEO_4*>(busPtr))->SetPixelColor(pix, col); break;\n      case I_8266_U1_NEO_4: (static_cast<B_8266_U1_NEO_4*>(busPtr))->SetPixelColor(pix, col); break;\n      case I_8266_DM_NEO_4: (static_cast<B_8266_DM_NEO_4*>(busPtr))->SetPixelColor(pix, col); break;\n      case I_8266_BB_NEO_4: (static_cast<B_8266_BB_NEO_4*>(busPtr))->SetPixelColor(pix, col); break;\n      case I_8266_U0_400_3: (static_cast<B_8266_U0_400_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_8266_U1_400_3: (static_cast<B_8266_U1_400_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_8266_DM_400_3: (static_cast<B_8266_DM_400_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_8266_BB_400_3: (static_cast<B_8266_BB_400_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_8266_U0_TM1_4: (static_cast<B_8266_U0_TM1_4*>(busPtr))->SetPixelColor(pix, col); break;\n      case I_8266_U1_TM1_4: (static_cast<B_8266_U1_TM1_4*>(busPtr))->SetPixelColor(pix, col); break;\n      case I_8266_DM_TM1_4: (static_cast<B_8266_DM_TM1_4*>(busPtr))->SetPixelColor(pix, col); break;\n      case I_8266_BB_TM1_4: (static_cast<B_8266_BB_TM1_4*>(busPtr))->SetPixelColor(pix, col); break;\n      case I_8266_U0_TM2_3: (static_cast<B_8266_U0_TM2_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_8266_U1_TM2_3: (static_cast<B_8266_U1_TM2_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_8266_DM_TM2_3: (static_cast<B_8266_DM_TM2_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_8266_BB_TM2_3: (static_cast<B_8266_BB_TM2_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_8266_U0_UCS_3: (static_cast<B_8266_U0_UCS_3*>(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break;\n      case I_8266_U1_UCS_3: (static_cast<B_8266_U1_UCS_3*>(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break;\n      case I_8266_DM_UCS_3: (static_cast<B_8266_DM_UCS_3*>(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break;\n      case I_8266_BB_UCS_3: (static_cast<B_8266_BB_UCS_3*>(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break;\n      case I_8266_U0_UCS_4: (static_cast<B_8266_U0_UCS_4*>(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break;\n      case I_8266_U1_UCS_4: (static_cast<B_8266_U1_UCS_4*>(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break;\n      case I_8266_DM_UCS_4: (static_cast<B_8266_DM_UCS_4*>(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break;\n      case I_8266_BB_UCS_4: (static_cast<B_8266_BB_UCS_4*>(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break;\n      case I_8266_U0_APA106_3: (static_cast<B_8266_U0_APA106_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_8266_U1_APA106_3: (static_cast<B_8266_U1_APA106_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_8266_DM_APA106_3: (static_cast<B_8266_DM_APA106_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_8266_BB_APA106_3: (static_cast<B_8266_BB_APA106_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_8266_U0_FW6_5: (static_cast<B_8266_U0_FW6_5*>(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break;\n      case I_8266_U1_FW6_5: (static_cast<B_8266_U1_FW6_5*>(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break;\n      case I_8266_DM_FW6_5: (static_cast<B_8266_DM_FW6_5*>(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break;\n      case I_8266_BB_FW6_5: (static_cast<B_8266_BB_FW6_5*>(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break;\n      case I_8266_U0_2805_5: (static_cast<B_8266_U0_2805_5*>(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break;\n      case I_8266_U1_2805_5: (static_cast<B_8266_U1_2805_5*>(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break;\n      case I_8266_DM_2805_5: (static_cast<B_8266_DM_2805_5*>(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break;\n      case I_8266_BB_2805_5: (static_cast<B_8266_BB_2805_5*>(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break;\n      case I_8266_U0_TM1914_3: (static_cast<B_8266_U0_TM1914_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_8266_U1_TM1914_3: (static_cast<B_8266_U1_TM1914_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_8266_DM_TM1914_3: (static_cast<B_8266_DM_TM1914_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_8266_BB_TM1914_3: (static_cast<B_8266_BB_TM1914_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_8266_U0_SM16825_5: (static_cast<B_8266_U0_SM16825_5*>(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break;\n      case I_8266_U1_SM16825_5: (static_cast<B_8266_U1_SM16825_5*>(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break;\n      case I_8266_DM_SM16825_5: (static_cast<B_8266_DM_SM16825_5*>(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break;\n      case I_8266_BB_SM16825_5: (static_cast<B_8266_BB_SM16825_5*>(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break;\n    #endif\n    #ifdef ARDUINO_ARCH_ESP32\n      // RMT buses\n      case I_32_RN_NEO_3: (static_cast<B_32_RN_NEO_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_32_RN_NEO_4: (static_cast<B_32_RN_NEO_4*>(busPtr))->SetPixelColor(pix, col); break;\n      case I_32_RN_400_3: (static_cast<B_32_RN_400_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_32_RN_TM1_4: (static_cast<B_32_RN_TM1_4*>(busPtr))->SetPixelColor(pix, col); break;\n      case I_32_RN_TM2_3: (static_cast<B_32_RN_TM2_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_32_RN_UCS_3: (static_cast<B_32_RN_UCS_3*>(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break;\n      case I_32_RN_UCS_4: (static_cast<B_32_RN_UCS_4*>(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break;\n      case I_32_RN_APA106_3: (static_cast<B_32_RN_APA106_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_32_RN_FW6_5: (static_cast<B_32_RN_FW6_5*>(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break;\n      case I_32_RN_2805_5: (static_cast<B_32_RN_2805_5*>(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break;\n      case I_32_RN_TM1914_3: (static_cast<B_32_RN_TM1914_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_32_RN_SM16825_5: (static_cast<B_32_RN_SM16825_5*>(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break;\n      // I2S1 bus or paralell buses\n      #ifndef CONFIG_IDF_TARGET_ESP32C3\n      case I_32_I2_NEO_3: if (_useParallelI2S) (static_cast<B_32_IP_NEO_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast<B_32_I2_NEO_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_32_I2_NEO_4: if (_useParallelI2S) (static_cast<B_32_IP_NEO_4*>(busPtr))->SetPixelColor(pix, col); else (static_cast<B_32_I2_NEO_4*>(busPtr))->SetPixelColor(pix, col); break;\n      case I_32_I2_400_3: if (_useParallelI2S) (static_cast<B_32_IP_400_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast<B_32_I2_400_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_32_I2_TM1_4: if (_useParallelI2S) (static_cast<B_32_IP_TM1_4*>(busPtr))->SetPixelColor(pix, col); else (static_cast<B_32_I2_TM1_4*>(busPtr))->SetPixelColor(pix, col); break;\n      case I_32_I2_TM2_3: if (_useParallelI2S) (static_cast<B_32_IP_TM2_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast<B_32_I2_TM2_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_32_I2_UCS_3: if (_useParallelI2S) (static_cast<B_32_IP_UCS_3*>(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); else (static_cast<B_32_I2_UCS_3*>(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break;\n      case I_32_I2_UCS_4: if (_useParallelI2S) (static_cast<B_32_IP_UCS_4*>(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); else (static_cast<B_32_I2_UCS_4*>(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break;\n      case I_32_I2_APA106_3: if (_useParallelI2S) (static_cast<B_32_IP_APA106_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast<B_32_I2_APA106_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_32_I2_FW6_5: if (_useParallelI2S) (static_cast<B_32_IP_FW6_5*>(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); else (static_cast<B_32_I2_FW6_5*>(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break;\n      case I_32_I2_2805_5: if (_useParallelI2S) (static_cast<B_32_IP_2805_5*>(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); else (static_cast<B_32_I2_2805_5*>(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break;\n      case I_32_I2_TM1914_3: if (_useParallelI2S) (static_cast<B_32_IP_TM1914_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast<B_32_I2_TM1914_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_32_I2_SM16825_5: if (_useParallelI2S) (static_cast<B_32_IP_SM16825_5*>(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); else (static_cast<B_32_I2_SM16825_5*>(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break;\n      #endif\n    #endif\n      case I_HS_DOT_3: (static_cast<B_HS_DOT_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_SS_DOT_3: (static_cast<B_SS_DOT_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_HS_LPD_3: (static_cast<B_HS_LPD_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_SS_LPD_3: (static_cast<B_SS_LPD_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_HS_LPO_3: (static_cast<B_HS_LPO_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_SS_LPO_3: (static_cast<B_SS_LPO_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_HS_WS1_3: (static_cast<B_HS_WS1_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_SS_WS1_3: (static_cast<B_SS_WS1_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_HS_P98_3: (static_cast<B_HS_P98_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n      case I_SS_P98_3: (static_cast<B_SS_P98_3*>(busPtr))->SetPixelColor(pix, RgbColor(col)); break;\n    }\n  }\n\n  [[gnu::hot]] static uint32_t getPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint8_t co) {\n    RgbwColor col(0,0,0,0);\n    switch (busType) {\n      case I_NONE: break;\n    #ifdef ESP8266\n      case I_8266_U0_NEO_3: col = (static_cast<B_8266_U0_NEO_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_U1_NEO_3: col = (static_cast<B_8266_U1_NEO_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_DM_NEO_3: col = (static_cast<B_8266_DM_NEO_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_BB_NEO_3: col = (static_cast<B_8266_BB_NEO_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_U0_NEO_4: col = (static_cast<B_8266_U0_NEO_4*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_U1_NEO_4: col = (static_cast<B_8266_U1_NEO_4*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_DM_NEO_4: col = (static_cast<B_8266_DM_NEO_4*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_BB_NEO_4: col = (static_cast<B_8266_BB_NEO_4*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_U0_400_3: col = (static_cast<B_8266_U0_400_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_U1_400_3: col = (static_cast<B_8266_U1_400_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_DM_400_3: col = (static_cast<B_8266_DM_400_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_BB_400_3: col = (static_cast<B_8266_BB_400_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_U0_TM1_4: col = (static_cast<B_8266_U0_TM1_4*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_U1_TM1_4: col = (static_cast<B_8266_U1_TM1_4*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_DM_TM1_4: col = (static_cast<B_8266_DM_TM1_4*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_BB_TM1_4: col = (static_cast<B_8266_BB_TM1_4*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_U0_TM2_3: col = (static_cast<B_8266_U0_TM2_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_U1_TM2_3: col = (static_cast<B_8266_U1_TM2_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_DM_TM2_3: col = (static_cast<B_8266_DM_TM2_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_BB_TM2_3: col = (static_cast<B_8266_BB_TM2_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_U0_UCS_3: { Rgb48Color c = (static_cast<B_8266_U0_UCS_3*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break;\n      case I_8266_U1_UCS_3: { Rgb48Color c = (static_cast<B_8266_U1_UCS_3*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break;\n      case I_8266_DM_UCS_3: { Rgb48Color c = (static_cast<B_8266_DM_UCS_3*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break;\n      case I_8266_BB_UCS_3: { Rgb48Color c = (static_cast<B_8266_BB_UCS_3*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break;\n      case I_8266_U0_UCS_4: { Rgbw64Color c = (static_cast<B_8266_U0_UCS_4*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break;\n      case I_8266_U1_UCS_4: { Rgbw64Color c = (static_cast<B_8266_U1_UCS_4*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break;\n      case I_8266_DM_UCS_4: { Rgbw64Color c = (static_cast<B_8266_DM_UCS_4*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break;\n      case I_8266_BB_UCS_4: { Rgbw64Color c = (static_cast<B_8266_BB_UCS_4*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break;\n      case I_8266_U0_APA106_3: col = (static_cast<B_8266_U0_APA106_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_U1_APA106_3: col = (static_cast<B_8266_U1_APA106_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_DM_APA106_3: col = (static_cast<B_8266_DM_APA106_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_BB_APA106_3: col = (static_cast<B_8266_BB_APA106_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_U0_FW6_5: { RgbwwColor c = (static_cast<B_8266_U0_FW6_5*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W\n      case I_8266_U1_FW6_5: { RgbwwColor c = (static_cast<B_8266_U1_FW6_5*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W\n      case I_8266_DM_FW6_5: { RgbwwColor c = (static_cast<B_8266_DM_FW6_5*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W\n      case I_8266_BB_FW6_5: { RgbwwColor c = (static_cast<B_8266_BB_FW6_5*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W\n      case I_8266_U0_2805_5: { RgbwwColor c = (static_cast<B_8266_U0_2805_5*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W\n      case I_8266_U1_2805_5: { RgbwwColor c = (static_cast<B_8266_U1_2805_5*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W\n      case I_8266_DM_2805_5: { RgbwwColor c = (static_cast<B_8266_DM_2805_5*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W\n      case I_8266_BB_2805_5: { RgbwwColor c = (static_cast<B_8266_BB_2805_5*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W\n      case I_8266_U0_TM1914_3: col = (static_cast<B_8266_U0_TM1914_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_U1_TM1914_3: col = (static_cast<B_8266_U1_TM1914_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_DM_TM1914_3: col = (static_cast<B_8266_DM_TM1914_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_BB_TM1914_3: col = (static_cast<B_8266_BB_TM1914_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_8266_U0_SM16825_5: { Rgbww80Color c = (static_cast<B_8266_U0_SM16825_5*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W\n      case I_8266_U1_SM16825_5: { Rgbww80Color c = (static_cast<B_8266_U1_SM16825_5*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W\n      case I_8266_DM_SM16825_5: { Rgbww80Color c = (static_cast<B_8266_DM_SM16825_5*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W\n      case I_8266_BB_SM16825_5: { Rgbww80Color c = (static_cast<B_8266_BB_SM16825_5*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W\n    #endif\n    #ifdef ARDUINO_ARCH_ESP32\n      // RMT buses\n      case I_32_RN_NEO_3: col = (static_cast<B_32_RN_NEO_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_32_RN_NEO_4: col = (static_cast<B_32_RN_NEO_4*>(busPtr))->GetPixelColor(pix); break;\n      case I_32_RN_400_3: col = (static_cast<B_32_RN_400_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_32_RN_TM1_4: col = (static_cast<B_32_RN_TM1_4*>(busPtr))->GetPixelColor(pix); break;\n      case I_32_RN_TM2_3: col = (static_cast<B_32_RN_TM2_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_32_RN_UCS_3: { Rgb48Color c = (static_cast<B_32_RN_UCS_3*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break;\n      case I_32_RN_UCS_4: { Rgbw64Color c = (static_cast<B_32_RN_UCS_4*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break;\n      case I_32_RN_APA106_3: col = (static_cast<B_32_RN_APA106_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_32_RN_FW6_5: { RgbwwColor c = (static_cast<B_32_RN_FW6_5*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W\n      case I_32_RN_2805_5: { RgbwwColor c = (static_cast<B_32_RN_2805_5*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W\n      case I_32_RN_TM1914_3: col = (static_cast<B_32_RN_TM1914_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_32_RN_SM16825_5: { Rgbww80Color c = (static_cast<B_32_RN_SM16825_5*>(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\n      // I2S1 bus or paralell buses\n      #ifndef CONFIG_IDF_TARGET_ESP32C3\n      case I_32_I2_NEO_3: col = (_useParallelI2S) ? (static_cast<B_32_IP_NEO_3*>(busPtr))->GetPixelColor(pix) : (static_cast<B_32_I2_NEO_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_32_I2_NEO_4: col = (_useParallelI2S) ? (static_cast<B_32_IP_NEO_4*>(busPtr))->GetPixelColor(pix) : (static_cast<B_32_I2_NEO_4*>(busPtr))->GetPixelColor(pix); break;\n      case I_32_I2_400_3: col = (_useParallelI2S) ? (static_cast<B_32_IP_400_3*>(busPtr))->GetPixelColor(pix) : (static_cast<B_32_I2_400_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_32_I2_TM1_4: col = (_useParallelI2S) ? (static_cast<B_32_IP_TM1_4*>(busPtr))->GetPixelColor(pix) : (static_cast<B_32_I2_TM1_4*>(busPtr))->GetPixelColor(pix); break;\n      case I_32_I2_TM2_3: col = (_useParallelI2S) ? (static_cast<B_32_IP_TM2_3*>(busPtr))->GetPixelColor(pix) : (static_cast<B_32_I2_TM2_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_32_I2_UCS_3: { Rgb48Color c = (_useParallelI2S) ? (static_cast<B_32_IP_UCS_3*>(busPtr))->GetPixelColor(pix) : (static_cast<B_32_I2_UCS_3*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,0); } break;\n      case I_32_I2_UCS_4: { Rgbw64Color c = (_useParallelI2S) ? (static_cast<B_32_IP_UCS_4*>(busPtr))->GetPixelColor(pix) : (static_cast<B_32_I2_UCS_4*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,c.W/257); } break;\n      case I_32_I2_APA106_3: col = (_useParallelI2S) ? (static_cast<B_32_IP_APA106_3*>(busPtr))->GetPixelColor(pix) : (static_cast<B_32_I2_APA106_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_32_I2_FW6_5: { RgbwwColor c = (_useParallelI2S) ? (static_cast<B_32_IP_FW6_5*>(busPtr))->GetPixelColor(pix) : (static_cast<B_32_I2_FW6_5*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W\n      case I_32_I2_2805_5: { RgbwwColor c = (_useParallelI2S) ? (static_cast<B_32_IP_2805_5*>(busPtr))->GetPixelColor(pix) : (static_cast<B_32_I2_2805_5*>(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W\n      case I_32_I2_TM1914_3: col = (_useParallelI2S) ? (static_cast<B_32_IP_TM1914_3*>(busPtr))->GetPixelColor(pix) : (static_cast<B_32_I2_TM1914_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_32_I2_SM16825_5: { Rgbww80Color c = (_useParallelI2S) ? (static_cast<B_32_IP_SM16825_5*>(busPtr))->GetPixelColor(pix) : (static_cast<B_32_I2_SM16825_5*>(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\n      #endif\n    #endif\n      case I_HS_DOT_3: col = (static_cast<B_HS_DOT_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_SS_DOT_3: col = (static_cast<B_SS_DOT_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_HS_LPD_3: col = (static_cast<B_HS_LPD_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_SS_LPD_3: col = (static_cast<B_SS_LPD_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_HS_LPO_3: col = (static_cast<B_HS_LPO_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_SS_LPO_3: col = (static_cast<B_SS_LPO_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_HS_WS1_3: col = (static_cast<B_HS_WS1_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_SS_WS1_3: col = (static_cast<B_SS_WS1_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_HS_P98_3: col = (static_cast<B_HS_P98_3*>(busPtr))->GetPixelColor(pix); break;\n      case I_SS_P98_3: col = (static_cast<B_SS_P98_3*>(busPtr))->GetPixelColor(pix); break;\n    }\n\n    // upper nibble contains W swap information\n    uint8_t w = col.W;\n    switch (co >> 4) {\n      case 1: col.W = col.B; col.B = w; break; // swap W & B\n      case 2: col.W = col.G; col.G = w; break; // swap W & G\n      case 3: col.W = col.R; col.R = w; break; // swap W & R\n    }\n    switch (co & 0x0F) {\n      //                    W               G              R               B\n      default: return ((col.W << 24) | (col.G << 8) | (col.R << 16) | (col.B)); //0 = GRB, default\n      case  1: return ((col.W << 24) | (col.R << 8) | (col.G << 16) | (col.B)); //1 = RGB, common for WS2811\n      case  2: return ((col.W << 24) | (col.B << 8) | (col.R << 16) | (col.G)); //2 = BRG\n      case  3: return ((col.W << 24) | (col.B << 8) | (col.G << 16) | (col.R)); //3 = RBG\n      case  4: return ((col.W << 24) | (col.R << 8) | (col.B << 16) | (col.G)); //4 = BGR\n      case  5: return ((col.W << 24) | (col.G << 8) | (col.B << 16) | (col.R)); //5 = GBR\n    }\n    return 0;\n  }\n\n  static void cleanup(void* busPtr, uint8_t busType) {\n    if (busPtr == nullptr) return;\n    switch (busType) {\n      case I_NONE: break;\n    #ifdef ESP8266\n      case I_8266_U0_NEO_3: delete (static_cast<B_8266_U0_NEO_3*>(busPtr)); break;\n      case I_8266_U1_NEO_3: delete (static_cast<B_8266_U1_NEO_3*>(busPtr)); break;\n      case I_8266_DM_NEO_3: delete (static_cast<B_8266_DM_NEO_3*>(busPtr)); break;\n      case I_8266_BB_NEO_3: delete (static_cast<B_8266_BB_NEO_3*>(busPtr)); break;\n      case I_8266_U0_NEO_4: delete (static_cast<B_8266_U0_NEO_4*>(busPtr)); break;\n      case I_8266_U1_NEO_4: delete (static_cast<B_8266_U1_NEO_4*>(busPtr)); break;\n      case I_8266_DM_NEO_4: delete (static_cast<B_8266_DM_NEO_4*>(busPtr)); break;\n      case I_8266_BB_NEO_4: delete (static_cast<B_8266_BB_NEO_4*>(busPtr)); break;\n      case I_8266_U0_400_3: delete (static_cast<B_8266_U0_400_3*>(busPtr)); break;\n      case I_8266_U1_400_3: delete (static_cast<B_8266_U1_400_3*>(busPtr)); break;\n      case I_8266_DM_400_3: delete (static_cast<B_8266_DM_400_3*>(busPtr)); break;\n      case I_8266_BB_400_3: delete (static_cast<B_8266_BB_400_3*>(busPtr)); break;\n      case I_8266_U0_TM1_4: delete (static_cast<B_8266_U0_TM1_4*>(busPtr)); break;\n      case I_8266_U1_TM1_4: delete (static_cast<B_8266_U1_TM1_4*>(busPtr)); break;\n      case I_8266_DM_TM1_4: delete (static_cast<B_8266_DM_TM1_4*>(busPtr)); break;\n      case I_8266_BB_TM1_4: delete (static_cast<B_8266_BB_TM1_4*>(busPtr)); break;\n      case I_8266_U0_TM2_3: delete (static_cast<B_8266_U0_TM2_3*>(busPtr)); break;\n      case I_8266_U1_TM2_3: delete (static_cast<B_8266_U1_TM2_3*>(busPtr)); break;\n      case I_8266_DM_TM2_3: delete (static_cast<B_8266_DM_TM2_3*>(busPtr)); break;\n      case I_8266_BB_TM2_3: delete (static_cast<B_8266_BB_TM2_3*>(busPtr)); break;\n      case I_8266_U0_UCS_3: delete (static_cast<B_8266_U0_UCS_3*>(busPtr)); break;\n      case I_8266_U1_UCS_3: delete (static_cast<B_8266_U1_UCS_3*>(busPtr)); break;\n      case I_8266_DM_UCS_3: delete (static_cast<B_8266_DM_UCS_3*>(busPtr)); break;\n      case I_8266_BB_UCS_3: delete (static_cast<B_8266_BB_UCS_3*>(busPtr)); break;\n      case I_8266_U0_UCS_4: delete (static_cast<B_8266_U0_UCS_4*>(busPtr)); break;\n      case I_8266_U1_UCS_4: delete (static_cast<B_8266_U1_UCS_4*>(busPtr)); break;\n      case I_8266_DM_UCS_4: delete (static_cast<B_8266_DM_UCS_4*>(busPtr)); break;\n      case I_8266_BB_UCS_4: delete (static_cast<B_8266_BB_UCS_4*>(busPtr)); break;\n      case I_8266_U0_APA106_3: delete (static_cast<B_8266_U0_APA106_3*>(busPtr)); break;\n      case I_8266_U1_APA106_3: delete (static_cast<B_8266_U1_APA106_3*>(busPtr)); break;\n      case I_8266_DM_APA106_3: delete (static_cast<B_8266_DM_APA106_3*>(busPtr)); break;\n      case I_8266_BB_APA106_3: delete (static_cast<B_8266_BB_APA106_3*>(busPtr)); break;\n      case I_8266_U0_FW6_5: delete (static_cast<B_8266_U0_FW6_5*>(busPtr)); break;\n      case I_8266_U1_FW6_5: delete (static_cast<B_8266_U1_FW6_5*>(busPtr)); break;\n      case I_8266_DM_FW6_5: delete (static_cast<B_8266_DM_FW6_5*>(busPtr)); break;\n      case I_8266_BB_FW6_5: delete (static_cast<B_8266_BB_FW6_5*>(busPtr)); break;\n      case I_8266_U0_2805_5: delete (static_cast<B_8266_U0_2805_5*>(busPtr)); break;\n      case I_8266_U1_2805_5: delete (static_cast<B_8266_U1_2805_5*>(busPtr)); break;\n      case I_8266_DM_2805_5: delete (static_cast<B_8266_DM_2805_5*>(busPtr)); break;\n      case I_8266_BB_2805_5: delete (static_cast<B_8266_BB_2805_5*>(busPtr)); break;\n      case I_8266_U0_TM1914_3: delete (static_cast<B_8266_U0_TM1914_3*>(busPtr)); break;\n      case I_8266_U1_TM1914_3: delete (static_cast<B_8266_U1_TM1914_3*>(busPtr)); break;\n      case I_8266_DM_TM1914_3: delete (static_cast<B_8266_DM_TM1914_3*>(busPtr)); break;\n      case I_8266_BB_TM1914_3: delete (static_cast<B_8266_BB_TM1914_3*>(busPtr)); break;\n      case I_8266_U0_SM16825_5: delete (static_cast<B_8266_U0_SM16825_5*>(busPtr)); break;\n      case I_8266_U1_SM16825_5: delete (static_cast<B_8266_U1_SM16825_5*>(busPtr)); break;\n      case I_8266_DM_SM16825_5: delete (static_cast<B_8266_DM_SM16825_5*>(busPtr)); break;\n      case I_8266_BB_SM16825_5: delete (static_cast<B_8266_BB_SM16825_5*>(busPtr)); break;\n    #endif\n    #ifdef ARDUINO_ARCH_ESP32\n      // RMT buses\n      case I_32_RN_NEO_3: delete (static_cast<B_32_RN_NEO_3*>(busPtr)); break;\n      case I_32_RN_NEO_4: delete (static_cast<B_32_RN_NEO_4*>(busPtr)); break;\n      case I_32_RN_400_3: delete (static_cast<B_32_RN_400_3*>(busPtr)); break;\n      case I_32_RN_TM1_4: delete (static_cast<B_32_RN_TM1_4*>(busPtr)); break;\n      case I_32_RN_TM2_3: delete (static_cast<B_32_RN_TM2_3*>(busPtr)); break;\n      case I_32_RN_UCS_3: delete (static_cast<B_32_RN_UCS_3*>(busPtr)); break;\n      case I_32_RN_UCS_4: delete (static_cast<B_32_RN_UCS_4*>(busPtr)); break;\n      case I_32_RN_APA106_3: delete (static_cast<B_32_RN_APA106_3*>(busPtr)); break;\n      case I_32_RN_FW6_5: delete (static_cast<B_32_RN_FW6_5*>(busPtr)); break;\n      case I_32_RN_2805_5: delete (static_cast<B_32_RN_2805_5*>(busPtr)); break;\n      case I_32_RN_TM1914_3: delete (static_cast<B_32_RN_TM1914_3*>(busPtr)); break;\n      case I_32_RN_SM16825_5: delete (static_cast<B_32_RN_SM16825_5*>(busPtr)); break;\n      // I2S1 bus or paralell buses\n      #ifndef CONFIG_IDF_TARGET_ESP32C3\n      case I_32_I2_NEO_3: if (_useParallelI2S) delete (static_cast<B_32_IP_NEO_3*>(busPtr)); else delete (static_cast<B_32_I2_NEO_3*>(busPtr)); break;\n      case I_32_I2_NEO_4: if (_useParallelI2S) delete (static_cast<B_32_IP_NEO_4*>(busPtr)); else delete (static_cast<B_32_I2_NEO_4*>(busPtr)); break;\n      case I_32_I2_400_3: if (_useParallelI2S) delete (static_cast<B_32_IP_400_3*>(busPtr)); else delete (static_cast<B_32_I2_400_3*>(busPtr)); break;\n      case I_32_I2_TM1_4: if (_useParallelI2S) delete (static_cast<B_32_IP_TM1_4*>(busPtr)); else delete (static_cast<B_32_I2_TM1_4*>(busPtr)); break;\n      case I_32_I2_TM2_3: if (_useParallelI2S) delete (static_cast<B_32_IP_TM2_3*>(busPtr)); else delete (static_cast<B_32_I2_TM2_3*>(busPtr)); break;\n      case I_32_I2_UCS_3: if (_useParallelI2S) delete (static_cast<B_32_IP_UCS_3*>(busPtr)); else delete (static_cast<B_32_I2_UCS_3*>(busPtr)); break;\n      case I_32_I2_UCS_4: if (_useParallelI2S) delete (static_cast<B_32_IP_UCS_4*>(busPtr)); else delete (static_cast<B_32_I2_UCS_4*>(busPtr)); break;\n      case I_32_I2_APA106_3: if (_useParallelI2S) delete (static_cast<B_32_IP_APA106_3*>(busPtr)); else delete (static_cast<B_32_I2_APA106_3*>(busPtr)); break;\n      case I_32_I2_FW6_5: if (_useParallelI2S) delete (static_cast<B_32_IP_FW6_5*>(busPtr)); else delete (static_cast<B_32_I2_FW6_5*>(busPtr)); break;\n      case I_32_I2_2805_5: if (_useParallelI2S) delete (static_cast<B_32_IP_2805_5*>(busPtr)); else delete (static_cast<B_32_I2_2805_5*>(busPtr)); break;\n      case I_32_I2_TM1914_3: if (_useParallelI2S) delete (static_cast<B_32_IP_TM1914_3*>(busPtr)); else delete (static_cast<B_32_I2_TM1914_3*>(busPtr)); break;\n      case I_32_I2_SM16825_5: if (_useParallelI2S) delete (static_cast<B_32_IP_SM16825_5*>(busPtr)); else delete (static_cast<B_32_I2_SM16825_5*>(busPtr)); break;\n      #endif\n    #endif\n      case I_HS_DOT_3: delete (static_cast<B_HS_DOT_3*>(busPtr)); break;\n      case I_SS_DOT_3: delete (static_cast<B_SS_DOT_3*>(busPtr)); break;\n      case I_HS_LPD_3: delete (static_cast<B_HS_LPD_3*>(busPtr)); break;\n      case I_SS_LPD_3: delete (static_cast<B_SS_LPD_3*>(busPtr)); break;\n      case I_HS_LPO_3: delete (static_cast<B_HS_LPO_3*>(busPtr)); break;\n      case I_SS_LPO_3: delete (static_cast<B_SS_LPO_3*>(busPtr)); break;\n      case I_HS_WS1_3: delete (static_cast<B_HS_WS1_3*>(busPtr)); break;\n      case I_SS_WS1_3: delete (static_cast<B_SS_WS1_3*>(busPtr)); break;\n      case I_HS_P98_3: delete (static_cast<B_HS_P98_3*>(busPtr)); break;\n      case I_SS_P98_3: delete (static_cast<B_SS_P98_3*>(busPtr)); break;\n    }\n  }\n\n  static unsigned getDataSize(void* busPtr, uint8_t busType) {\n    unsigned size = 0;\n    #ifdef ARDUINO_ARCH_ESP32\n    size = 100; // ~100bytes for NPB internal structures (measured for both I2S and RMT, much smaller and more variable on ESP8266)\n    #endif\n    switch (busType) {\n      case I_NONE: break;\n    #ifdef ESP8266\n      case I_8266_U0_NEO_3: size = (static_cast<B_8266_U0_NEO_3*>(busPtr))->PixelsSize(); break;\n      case I_8266_U1_NEO_3: size = (static_cast<B_8266_U1_NEO_3*>(busPtr))->PixelsSize(); break;\n      case I_8266_DM_NEO_3: size = (static_cast<B_8266_DM_NEO_3*>(busPtr))->PixelsSize()*5; break;\n      case I_8266_BB_NEO_3: size = (static_cast<B_8266_BB_NEO_3*>(busPtr))->PixelsSize(); break;\n      case I_8266_U0_NEO_4: size = (static_cast<B_8266_U0_NEO_4*>(busPtr))->PixelsSize(); break;\n      case I_8266_U1_NEO_4: size = (static_cast<B_8266_U1_NEO_4*>(busPtr))->PixelsSize(); break;\n      case I_8266_DM_NEO_4: size = (static_cast<B_8266_DM_NEO_4*>(busPtr))->PixelsSize()*5; break;\n      case I_8266_BB_NEO_4: size = (static_cast<B_8266_BB_NEO_4*>(busPtr))->PixelsSize(); break;\n      case I_8266_U0_400_3: size = (static_cast<B_8266_U0_400_3*>(busPtr))->PixelsSize(); break;\n      case I_8266_U1_400_3: size = (static_cast<B_8266_U1_400_3*>(busPtr))->PixelsSize(); break;\n      case I_8266_DM_400_3: size = (static_cast<B_8266_DM_400_3*>(busPtr))->PixelsSize()*5; break;\n      case I_8266_BB_400_3: size = (static_cast<B_8266_BB_400_3*>(busPtr))->PixelsSize(); break;\n      case I_8266_U0_TM1_4: size = (static_cast<B_8266_U0_TM1_4*>(busPtr))->PixelsSize(); break;\n      case I_8266_U1_TM1_4: size = (static_cast<B_8266_U1_TM1_4*>(busPtr))->PixelsSize(); break;\n      case I_8266_DM_TM1_4: size = (static_cast<B_8266_DM_TM1_4*>(busPtr))->PixelsSize()*5; break;\n      case I_8266_BB_TM1_4: size = (static_cast<B_8266_BB_TM1_4*>(busPtr))->PixelsSize(); break;\n      case I_8266_U0_TM2_3: size = (static_cast<B_8266_U0_TM2_3*>(busPtr))->PixelsSize(); break;\n      case I_8266_U1_TM2_3: size = (static_cast<B_8266_U1_TM2_3*>(busPtr))->PixelsSize(); break;\n      case I_8266_DM_TM2_3: size = (static_cast<B_8266_DM_TM2_3*>(busPtr))->PixelsSize()*5; break;\n      case I_8266_BB_TM2_3: size = (static_cast<B_8266_BB_TM2_3*>(busPtr))->PixelsSize(); break;\n      case I_8266_U0_UCS_3: size = (static_cast<B_8266_U0_UCS_3*>(busPtr))->PixelsSize(); break;\n      case I_8266_U1_UCS_3: size = (static_cast<B_8266_U1_UCS_3*>(busPtr))->PixelsSize(); break;\n      case I_8266_DM_UCS_3: size = (static_cast<B_8266_DM_UCS_3*>(busPtr))->PixelsSize()*5; break;\n      case I_8266_BB_UCS_3: size = (static_cast<B_8266_BB_UCS_3*>(busPtr))->PixelsSize(); break;\n      case I_8266_U0_UCS_4: size = (static_cast<B_8266_U0_UCS_4*>(busPtr))->PixelsSize(); break;\n      case I_8266_U1_UCS_4: size = (static_cast<B_8266_U1_UCS_4*>(busPtr))->PixelsSize(); break;\n      case I_8266_DM_UCS_4: size = (static_cast<B_8266_DM_UCS_4*>(busPtr))->PixelsSize()*5; break;\n      case I_8266_BB_UCS_4: size = (static_cast<B_8266_BB_UCS_4*>(busPtr))->PixelsSize(); break;\n      case I_8266_U0_APA106_3: size = (static_cast<B_8266_U0_APA106_3*>(busPtr))->PixelsSize(); break;\n      case I_8266_U1_APA106_3: size = (static_cast<B_8266_U1_APA106_3*>(busPtr))->PixelsSize(); break;\n      case I_8266_DM_APA106_3: size = (static_cast<B_8266_DM_APA106_3*>(busPtr))->PixelsSize()*5; break;\n      case I_8266_BB_APA106_3: size = (static_cast<B_8266_BB_APA106_3*>(busPtr))->PixelsSize(); break;\n      case I_8266_U0_FW6_5: size = (static_cast<B_8266_U0_FW6_5*>(busPtr))->PixelsSize(); break;\n      case I_8266_U1_FW6_5: size = (static_cast<B_8266_U1_FW6_5*>(busPtr))->PixelsSize(); break;\n      case I_8266_DM_FW6_5: size = (static_cast<B_8266_DM_FW6_5*>(busPtr))->PixelsSize()*5; break;\n      case I_8266_BB_FW6_5: size = (static_cast<B_8266_BB_FW6_5*>(busPtr))->PixelsSize(); break;\n      case I_8266_U0_2805_5: size = (static_cast<B_8266_U0_2805_5*>(busPtr))->PixelsSize(); break;\n      case I_8266_U1_2805_5: size = (static_cast<B_8266_U1_2805_5*>(busPtr))->PixelsSize(); break;\n      case I_8266_DM_2805_5: size = (static_cast<B_8266_DM_2805_5*>(busPtr))->PixelsSize()*5; break;\n      case I_8266_BB_2805_5: size = (static_cast<B_8266_BB_2805_5*>(busPtr))->PixelsSize(); break;\n      case I_8266_U0_TM1914_3: size = (static_cast<B_8266_U0_TM1914_3*>(busPtr))->PixelsSize(); break;\n      case I_8266_U1_TM1914_3: size = (static_cast<B_8266_U1_TM1914_3*>(busPtr))->PixelsSize(); break;\n      case I_8266_DM_TM1914_3: size = (static_cast<B_8266_DM_TM1914_3*>(busPtr))->PixelsSize()*5; break;\n      case I_8266_BB_TM1914_3: size = (static_cast<B_8266_BB_TM1914_3*>(busPtr))->PixelsSize(); break;\n      case I_8266_U0_SM16825_5: size = (static_cast<B_8266_U0_SM16825_5*>(busPtr))->PixelsSize(); break;\n      case I_8266_U1_SM16825_5: size = (static_cast<B_8266_U1_SM16825_5*>(busPtr))->PixelsSize(); break;\n      case I_8266_DM_SM16825_5: size = (static_cast<B_8266_DM_SM16825_5*>(busPtr))->PixelsSize()*5; break;\n      case I_8266_BB_SM16825_5: size = (static_cast<B_8266_BB_SM16825_5*>(busPtr))->PixelsSize(); break;\n    #endif\n    #ifdef ARDUINO_ARCH_ESP32\n      // RMT buses (front + back + small system managed RMT)\n      case I_32_RN_NEO_3: size += (static_cast<B_32_RN_NEO_3*>(busPtr))->PixelsSize()*2; break;\n      case I_32_RN_NEO_4: size += (static_cast<B_32_RN_NEO_4*>(busPtr))->PixelsSize()*2; break;\n      case I_32_RN_400_3: size += (static_cast<B_32_RN_400_3*>(busPtr))->PixelsSize()*2; break;\n      case I_32_RN_TM1_4: size += (static_cast<B_32_RN_TM1_4*>(busPtr))->PixelsSize()*2; break;\n      case I_32_RN_TM2_3: size += (static_cast<B_32_RN_TM2_3*>(busPtr))->PixelsSize()*2; break;\n      case I_32_RN_UCS_3: size += (static_cast<B_32_RN_UCS_3*>(busPtr))->PixelsSize()*2; break;\n      case I_32_RN_UCS_4: size += (static_cast<B_32_RN_UCS_4*>(busPtr))->PixelsSize()*2; break;\n      case I_32_RN_APA106_3: size += (static_cast<B_32_RN_APA106_3*>(busPtr))->PixelsSize()*2; break;\n      case I_32_RN_FW6_5: size += (static_cast<B_32_RN_FW6_5*>(busPtr))->PixelsSize()*2; break;\n      case I_32_RN_2805_5: size += (static_cast<B_32_RN_2805_5*>(busPtr))->PixelsSize()*2; break;\n      case I_32_RN_TM1914_3: size += (static_cast<B_32_RN_TM1914_3*>(busPtr))->PixelsSize()*2; break;\n      case I_32_RN_SM16825_5: size += (static_cast<B_32_RN_SM16825_5*>(busPtr))->PixelsSize()*2; break;\n      // 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\n      #ifndef CONFIG_IDF_TARGET_ESP32C3\n      case I_32_I2_NEO_3: size += (_useParallelI2S) ? (static_cast<B_32_IP_NEO_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_NEO_3*>(busPtr))->PixelsSize()*4; break;\n      case I_32_I2_NEO_4: size += (_useParallelI2S) ? (static_cast<B_32_IP_NEO_4*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_NEO_4*>(busPtr))->PixelsSize()*4; break;\n      case I_32_I2_400_3: size += (_useParallelI2S) ? (static_cast<B_32_IP_400_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_400_3*>(busPtr))->PixelsSize()*4; break;\n      case I_32_I2_TM1_4: size += (_useParallelI2S) ? (static_cast<B_32_IP_TM1_4*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_TM1_4*>(busPtr))->PixelsSize()*4; break;\n      case I_32_I2_TM2_3: size += (_useParallelI2S) ? (static_cast<B_32_IP_TM2_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_TM2_3*>(busPtr))->PixelsSize()*4; break;\n      case I_32_I2_UCS_3: size += (_useParallelI2S) ? (static_cast<B_32_IP_UCS_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_UCS_3*>(busPtr))->PixelsSize()*4; break;\n      case I_32_I2_UCS_4: size += (_useParallelI2S) ? (static_cast<B_32_IP_UCS_4*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_UCS_4*>(busPtr))->PixelsSize()*4; break;\n      case I_32_I2_APA106_3: size += (_useParallelI2S) ? (static_cast<B_32_IP_APA106_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_APA106_3*>(busPtr))->PixelsSize()*4; break;\n      case I_32_I2_FW6_5: size += (_useParallelI2S) ? (static_cast<B_32_IP_FW6_5*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_FW6_5*>(busPtr))->PixelsSize()*4; break;\n      case I_32_I2_2805_5: size += (_useParallelI2S) ? (static_cast<B_32_IP_2805_5*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_2805_5*>(busPtr))->PixelsSize()*4; break;\n      case I_32_I2_TM1914_3: size += (_useParallelI2S) ? (static_cast<B_32_IP_TM1914_3*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_TM1914_3*>(busPtr))->PixelsSize()*4; break;\n      case I_32_I2_SM16825_5: size += (_useParallelI2S) ? (static_cast<B_32_IP_SM16825_5*>(busPtr))->PixelsSize()*4 : (static_cast<B_32_I2_SM16825_5*>(busPtr))->PixelsSize()*4; break;\n      #endif\n    #endif\n      case I_HS_DOT_3: size = (static_cast<B_HS_DOT_3*>(busPtr))->PixelsSize()*2; break;\n      case I_SS_DOT_3: size = (static_cast<B_SS_DOT_3*>(busPtr))->PixelsSize()*2; break;\n      case I_HS_LPD_3: size = (static_cast<B_HS_LPD_3*>(busPtr))->PixelsSize()*2; break;\n      case I_SS_LPD_3: size = (static_cast<B_SS_LPD_3*>(busPtr))->PixelsSize()*2; break;\n      case I_HS_LPO_3: size = (static_cast<B_HS_LPO_3*>(busPtr))->PixelsSize()*2; break;\n      case I_SS_LPO_3: size = (static_cast<B_SS_LPO_3*>(busPtr))->PixelsSize()*2; break;\n      case I_HS_WS1_3: size = (static_cast<B_HS_WS1_3*>(busPtr))->PixelsSize()*2; break;\n      case I_SS_WS1_3: size = (static_cast<B_SS_WS1_3*>(busPtr))->PixelsSize()*2; break;\n      case I_HS_P98_3: size = (static_cast<B_HS_P98_3*>(busPtr))->PixelsSize()*2; break;\n      case I_SS_P98_3: size = (static_cast<B_SS_P98_3*>(busPtr))->PixelsSize()*2; break;\n    }\n    return size;\n  }\n\n  static unsigned memUsage(unsigned count, unsigned busType) {\n    unsigned size = count*3;  // let's assume 3 channels, we will add count or 2*count below for 4 channels or 5 channels\n    switch (busType) {\n      case I_NONE: size = 0; break;\n    #ifdef ESP8266\n      // UART methods have front + back buffers + small UART\n      case I_8266_U0_NEO_4    : // fallthrough\n      case I_8266_U1_NEO_4    : // fallthrough\n      case I_8266_BB_NEO_4    : // fallthrough\n      case I_8266_U0_TM1_4    : // fallthrough\n      case I_8266_U1_TM1_4    : // fallthrough\n      case I_8266_BB_TM1_4    : size = (size + count);       break; // 4 channels\n      case I_8266_U0_UCS_3    : // fallthrough\n      case I_8266_U1_UCS_3    : // fallthrough\n      case I_8266_BB_UCS_3    : size *= 2;                   break; // 16 bit\n      case I_8266_U0_UCS_4    : // fallthrough\n      case I_8266_U1_UCS_4    : // fallthrough\n      case I_8266_BB_UCS_4    : size = (size + count)*2;     break; // 16 bit 4 channels\n      case I_8266_U0_FW6_5    : // fallthrough\n      case I_8266_U1_FW6_5    : // fallthrough\n      case I_8266_BB_FW6_5    : // fallthrough\n      case I_8266_U0_2805_5   : // fallthrough\n      case I_8266_U1_2805_5   : // fallthrough\n      case I_8266_BB_2805_5   : size = (size + 2*count);     break; // 5 channels\n      case I_8266_U0_SM16825_5: // fallthrough\n      case I_8266_U1_SM16825_5: // fallthrough\n      case I_8266_BB_SM16825_5: size = (size + 2*count)*2;   break; // 16 bit 5 channels\n      // DMA methods have front + DMA buffer = ((1+(3+1)) * channels; exact value is a bit of mistery - needs a dig into NPB)\n      case I_8266_DM_NEO_3    : // fallthrough\n      case I_8266_DM_400_3    : // fallthrough\n      case I_8266_DM_TM2_3    : // fallthrough\n      case I_8266_DM_APA106_3 : // fallthrough\n      case I_8266_DM_TM1914_3 : size *= 5;                   break;\n      case I_8266_DM_NEO_4    : // fallthrough\n      case I_8266_DM_TM1_4    : size = (size + count)*5;     break;\n      case I_8266_DM_UCS_3    : size *= 2*5;                 break;\n      case I_8266_DM_UCS_4    : size = (size + count)*2*5;   break;\n      case I_8266_DM_FW6_5    : // fallthrough\n      case I_8266_DM_2805_5   : size = (size + 2*count)*5;   break;\n      case I_8266_DM_SM16825_5: size = (size + 2*count)*2*5; break;\n    #else\n      // note: RMT and I2S buses use ~100 bytes of internal NPB memory each, not included here for simplicity\n      // RMT buses (1x front and 1x back buffer, does not include small RMT buffer)\n      case I_32_RN_NEO_4    : // fallthrough\n      case I_32_RN_TM1_4    : size = (size + count)*2;     break; // 4 channels\n      case I_32_RN_UCS_3    : size *= 2*2;                 break; // 16bit\n      case I_32_RN_UCS_4    : size = (size + count)*2*2;   break; // 16bit, 4 channels\n      case I_32_RN_FW6_5    : // fallthrough\n      case I_32_RN_2805_5   : size = (size + 2*count)*2;   break; // 5 channels\n      case I_32_RN_SM16825_5: size = (size + 2*count)*2*2; break; // 16bit, 5 channels\n      // I2S bus or paralell I2S buses (1x front, does not include DMA buffer which is front*cadence, a bit(?) more for LCD)\n      #ifndef CONFIG_IDF_TARGET_ESP32C3\n      case I_32_I2_NEO_3    : // fallthrough\n      case I_32_I2_400_3    : // fallthrough\n      case I_32_I2_TM2_3    : // fallthrough\n      case I_32_I2_APA106_3 :                              break; // do nothing, I2S uses single buffer + DMA buffer\n      case I_32_I2_NEO_4    : // fallthrough\n      case I_32_I2_TM1_4    : size = (size + count);       break; // 4 channels\n      case I_32_I2_UCS_3    : size *= 2;                   break; // 16 bit\n      case I_32_I2_UCS_4    : size = (size + count)*2;     break; // 16 bit, 4 channels\n      case I_32_I2_FW6_5    : // fallthrough\n      case I_32_I2_2805_5   : size = (size + 2*count);     break; // 5 channels\n      case I_32_I2_SM16825_5: size = (size + 2*count)*2;   break; // 16 bit, 5 channels\n      #endif\n      default               : size *= 2;                   break; // everything else uses 2 buffers\n    #endif\n    }\n    return size;\n  }\n#ifndef ESP8266\n  // Reset channel tracking (call before adding buses)\n  static void resetChannelTracking() {\n    _useParallelI2S = false;\n    _rmtChannelsAssigned = 0;\n    _rmtChannel = 0;\n    _i2sChannelsAssigned = 0;\n    _parallelBusItype = I_NONE;\n    _2PchannelsAssigned = 0;\n  }\n#endif\n  // reserves and gives back the internal type index (I_XX_XXX_X above) for the input based on bus type and pins\n  static uint8_t getI(uint8_t busType, const uint8_t* pins, uint8_t driverPreference) {\n    if (!Bus::isDigital(busType)) return I_NONE;\n    uint8_t t = I_NONE;\n    if (Bus::is2Pin(busType)) { //SPI LED chips\n      bool isHSPI = false;\n      #ifdef ESP8266\n      if (pins[0] == P_8266_HS_MOSI && pins[1] == P_8266_HS_CLK) isHSPI = true;\n      #else\n      if (_2PchannelsAssigned == 0) isHSPI = true; // first 2-pin channel uses hardware SPI\n      _2PchannelsAssigned++;\n      #endif\n      switch (busType) {\n        case TYPE_APA102:  t = I_SS_DOT_3; break;\n        case TYPE_LPD8806: t = I_SS_LPD_3; break;\n        case TYPE_LPD6803: t = I_SS_LPO_3; break;\n        case TYPE_WS2801:  t = I_SS_WS1_3; break;\n        case TYPE_P9813:   t = I_SS_P98_3; break;\n      }\n      if (t > I_NONE && isHSPI) t--; //hardware SPI has one smaller ID than software\n    } else {\n      #ifdef ESP8266\n      uint8_t offset = pins[0] -1; //for driver: 0 = uart0, 1 = uart1, 2 = dma, 3 = bitbang\n      if (offset > 3) offset = 3;\n      switch (busType) {\n        case TYPE_WS2812_1CH_X3:\n        case TYPE_WS2812_2CH_X3:\n        case TYPE_WS2812_RGB:\n        case TYPE_WS2812_WWA:\n          t = I_8266_U0_NEO_3 + offset; break;\n        case TYPE_SK6812_RGBW:\n          t = I_8266_U0_NEO_4 + offset; break;\n        case TYPE_WS2811_400KHZ:\n          t = I_8266_U0_400_3 + offset; break;\n        case TYPE_TM1814:\n          t = I_8266_U0_TM1_4 + offset; break;\n        case TYPE_TM1829:\n          t = I_8266_U0_TM2_3 + offset; break;\n        case TYPE_UCS8903:\n          t = I_8266_U0_UCS_3 + offset; break;\n        case TYPE_UCS8904:\n          t = I_8266_U0_UCS_4 + offset; break;\n        case TYPE_APA106:\n          t = I_8266_U0_APA106_3 + offset; break;\n        case TYPE_FW1906:\n          t = I_8266_U0_FW6_5 + offset; break;\n        case TYPE_WS2805:\n          t = I_8266_U0_2805_5 + offset; break;\n        case TYPE_TM1914:\n          t = I_8266_U0_TM1914_3 + offset; break;\n        case TYPE_SM16825:\n          t = I_8266_U0_SM16825_5 + offset; break;\n      }\n      #else //ESP32\n      // dynamic channel allocation based on driver preference\n      // 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)\n      uint8_t offset = 0; // 0 = RMT, 1 = I2S/LCD\n      if (driverPreference == 0 && _rmtChannelsAssigned < WLED_MAX_RMT_CHANNELS) {\n        _rmtChannelsAssigned++;\n      } else if (_i2sChannelsAssigned < WLED_MAX_I2S_CHANNELS) {\n        offset = 1; // I2S requested or RMT full\n        _i2sChannelsAssigned++;\n      } else {\n        return I_NONE; // No channels available\n      }\n\n      // Now determine actual bus type with the chosen offset\n      switch (busType) {\n        case TYPE_WS2812_1CH_X3:\n        case TYPE_WS2812_2CH_X3:\n        case TYPE_WS2812_RGB:\n        case TYPE_WS2812_WWA:\n          t = I_32_RN_NEO_3 + offset; break;\n        case TYPE_SK6812_RGBW:\n          t = I_32_RN_NEO_4 + offset; break;\n        case TYPE_WS2811_400KHZ:\n          t = I_32_RN_400_3 + offset; break;\n        case TYPE_TM1814:\n          t = I_32_RN_TM1_4 + offset; break;\n        case TYPE_TM1829:\n          t = I_32_RN_TM2_3 + offset; break;\n        case TYPE_UCS8903:\n          t = I_32_RN_UCS_3 + offset; break;\n        case TYPE_UCS8904:\n          t = I_32_RN_UCS_4 + offset; break;\n        case TYPE_APA106:\n          t = I_32_RN_APA106_3 + offset; break;\n        case TYPE_FW1906:\n          t = I_32_RN_FW6_5 + offset; break;\n        case TYPE_WS2805:\n          t = I_32_RN_2805_5 + offset; break;\n        case TYPE_TM1914:\n          t = I_32_RN_TM1914_3 + offset; break;\n        case TYPE_SM16825:\n          t = I_32_RN_SM16825_5 + offset; break;\n      }\n      // If using parallel I2S, set the type accordingly\n      if (_i2sChannelsAssigned == 1 && offset == 1) { // first I2S channel request, lock the type\n        _parallelBusItype = t;\n        #ifdef CONFIG_IDF_TARGET_ESP32S3\n        _useParallelI2S = true; // ESP32-S3 always uses parallel I2S (LCD method)\n        #endif\n      }\n      else if (offset == 1) { // not first I2S channel, use locked type and enable parallel flag\n        _useParallelI2S = true;\n        t = _parallelBusItype;\n      }\n      #endif\n    }\n    return t;\n  }\n};\n#endif\n"
  },
  {
    "path": "wled00/button.cpp",
    "content": "#include \"wled.h\"\n\n/*\n * Physical IO\n */\n\n#define WLED_DEBOUNCE_THRESHOLD      50 // only consider button input of at least 50ms as valid (debouncing)\n#define WLED_LONG_PRESS             600 // long press if button is released after held for at least 600ms\n#define WLED_DOUBLE_PRESS           350 // double press if another press within 350ms after a short press\n#define WLED_LONG_REPEATED_ACTION   400 // how often a repeated action (e.g. dimming) is fired on long press on button IDs >0\n#define WLED_LONG_AP               5000 // how long button 0 needs to be held to activate WLED-AP\n#define WLED_LONG_FACTORY_RESET   10000 // how long button 0 needs to be held to trigger a factory reset\n#define WLED_LONG_BRI_STEPS          16 // how much to increase/decrease the brightness with each long press repetition\n\nstatic const char _mqtt_topic_button[] PROGMEM = \"%s/button/%d\";  // optimize flash usage\nstatic bool buttonBriDirection = false; // true: increase brightness, false: decrease brightness\n\nvoid shortPressAction(uint8_t b)\n{\n  if (!buttons[b].macroButton) {\n    switch (b) {\n      case 0: toggleOnOff(); stateUpdated(CALL_MODE_BUTTON); break;\n      case 1: ++effectCurrent %= strip.getModeCount(); stateChanged = true; colorUpdated(CALL_MODE_BUTTON); break;\n    }\n  } else {\n    applyPreset(buttons[b].macroButton, CALL_MODE_BUTTON_PRESET);\n  }\n\n#ifndef WLED_DISABLE_MQTT\n  // publish MQTT message\n  if (buttonPublishMqtt && WLED_MQTT_CONNECTED) {\n    char subuf[MQTT_MAX_TOPIC_LEN + 32];\n    sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b);\n    mqtt->publish(subuf, 0, false, \"short\");\n  }\n#endif\n}\n\nvoid longPressAction(uint8_t b)\n{\n  if (!buttons[b].macroLongPress) {\n    switch (b) {\n      case 0: setRandomColor(colPri); colorUpdated(CALL_MODE_BUTTON); break;\n      case 1: \n        if(buttonBriDirection) {\n          if (bri == 255) break; // avoid unnecessary updates to brightness\n          if (bri >= 255 - WLED_LONG_BRI_STEPS) bri = 255;\n          else bri += WLED_LONG_BRI_STEPS;\n        } else {\n          if (bri == 1) break; // avoid unnecessary updates to brightness\n          if (bri <= WLED_LONG_BRI_STEPS) bri = 1;\n          else bri -= WLED_LONG_BRI_STEPS;\n        }\n        stateUpdated(CALL_MODE_BUTTON); \n        buttons[b].pressedTime = millis();         \n        break; // repeatable action\n    }\n  } else {\n    applyPreset(buttons[b].macroLongPress, CALL_MODE_BUTTON_PRESET);\n  }\n\n#ifndef WLED_DISABLE_MQTT\n  // publish MQTT message\n  if (buttonPublishMqtt && WLED_MQTT_CONNECTED) {\n    char subuf[MQTT_MAX_TOPIC_LEN + 32];\n    sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b);\n    mqtt->publish(subuf, 0, false, \"long\");\n  }\n#endif\n}\n\nvoid doublePressAction(uint8_t b)\n{\n  if (!buttons[b].macroDoublePress) {\n    switch (b) {\n      //case 0: toggleOnOff(); colorUpdated(CALL_MODE_BUTTON); break; //instant short press on button 0 if no macro set\n      case 1: ++effectPalette %= getPaletteCount(); colorUpdated(CALL_MODE_BUTTON); break;\n    }\n  } else {\n    applyPreset(buttons[b].macroDoublePress, CALL_MODE_BUTTON_PRESET);\n  }\n\n#ifndef WLED_DISABLE_MQTT\n  // publish MQTT message\n  if (buttonPublishMqtt && WLED_MQTT_CONNECTED) {\n    char subuf[MQTT_MAX_TOPIC_LEN + 32];\n    sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b);\n    mqtt->publish(subuf, 0, false, \"double\");\n  }\n#endif\n}\n\nbool isButtonPressed(uint8_t b)\n{\n  if (buttons[b].pin < 0) return false;\n  unsigned pin = buttons[b].pin;\n\n  switch (buttons[b].type) {\n    case BTN_TYPE_NONE:\n    case BTN_TYPE_RESERVED:\n      break;\n    case BTN_TYPE_PUSH:\n    case BTN_TYPE_SWITCH:\n      if (digitalRead(pin) == LOW) return true;\n      break;\n    case BTN_TYPE_PUSH_ACT_HIGH:\n    case BTN_TYPE_PIR_SENSOR:\n      if (digitalRead(pin) == HIGH) return true;\n      break;\n    case BTN_TYPE_TOUCH:\n    case BTN_TYPE_TOUCH_SWITCH:\n      #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3)\n        #ifdef SOC_TOUCH_VERSION_2 //ESP32 S2 and S3 provide a function to check touch state (state is updated in interrupt)\n        if (touchInterruptGetLastStatus(pin)) return true;\n        #else\n        if (digitalPinToTouchChannel(pin) >= 0 && touchRead(pin) <= touchThreshold) return true;\n        #endif\n      #endif\n     break;\n  }\n  return false;\n}\n\nvoid handleSwitch(uint8_t b)\n{\n  // isButtonPressed() handles inverted/noninverted logic\n  if (buttons[b].pressedBefore != isButtonPressed(b)) {\n    DEBUG_PRINTF_P(PSTR(\"Switch: State changed %u\\n\"), b);\n    buttons[b].pressedTime = millis();\n    buttons[b].pressedBefore = !buttons[b].pressedBefore; // toggle pressed state\n  }\n\n  if (buttons[b].longPressed == buttons[b].pressedBefore) return;\n\n  if (millis() - buttons[b].pressedTime > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce)\n    DEBUG_PRINTF_P(PSTR(\"Switch: Activating  %u\\n\"), b);\n    if (!buttons[b].pressedBefore) { // on -> off\n      DEBUG_PRINTF_P(PSTR(\"Switch: On -> Off (%u)\\n\"), b);\n      if (buttons[b].macroButton) applyPreset(buttons[b].macroButton, CALL_MODE_BUTTON_PRESET);\n      else { //turn on\n        if (!bri) {toggleOnOff(); stateUpdated(CALL_MODE_BUTTON);}\n      }\n    } else {  // off -> on\n      DEBUG_PRINTF_P(PSTR(\"Switch: Off -> On (%u)\\n\"), b);\n      if (buttons[b].macroLongPress) applyPreset(buttons[b].macroLongPress, CALL_MODE_BUTTON_PRESET);\n      else { //turn off\n        if (bri) {toggleOnOff(); stateUpdated(CALL_MODE_BUTTON);}\n      }\n    }\n\n#ifndef WLED_DISABLE_MQTT\n    // publish MQTT message\n    if (buttonPublishMqtt && WLED_MQTT_CONNECTED) {\n      char subuf[MQTT_MAX_TOPIC_LEN + 32];\n      if (buttons[b].type == BTN_TYPE_PIR_SENSOR) sprintf_P(subuf, PSTR(\"%s/motion/%d\"), mqttDeviceTopic, (int)b);\n      else sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b);\n      mqtt->publish(subuf, 0, false, !buttons[b].pressedBefore ? \"off\" : \"on\");\n    }\n#endif\n\n    buttons[b].longPressed = buttons[b].pressedBefore; //save the last \"long term\" switch state\n  }\n}\n\n#define ANALOG_BTN_READ_CYCLE 250   // min time between two analog reading cycles\n#define STRIP_WAIT_TIME 6           // max wait time in case of strip.isUpdating()\n#define POT_SMOOTHING 0.25f         // smoothing factor for raw potentiometer readings\n#define POT_SENSITIVITY 4           // changes below this amount are noise (POT scratching, or ADC noise)\n\nvoid handleAnalog(uint8_t b)\n{\n  static uint8_t oldRead[WLED_MAX_BUTTONS] = {0};\n  static float filteredReading[WLED_MAX_BUTTONS] = {0.0f};\n  unsigned rawReading;    // raw value from analogRead, scaled to 12bit\n\n  DEBUG_PRINTF_P(PSTR(\"Analog: Reading button %u\\n\"), b);\n\n  #ifdef ESP8266\n  rawReading = analogRead(A0) << 2;   // convert 10bit read to 12bit\n  #else\n  if ((buttons[b].pin < 0) /*|| (digitalPinToAnalogChannel(buttons[b].pin) < 0)*/) return; // pin must support analog ADC - newer esp32 frameworks throw lots of warnings otherwise\n  rawReading = analogRead(buttons[b].pin); // collect at full 12bit resolution\n  #endif\n  yield();                            // keep WiFi task running - analog read may take several millis on ESP8266\n\n  filteredReading[b] += POT_SMOOTHING * ((float(rawReading) / 16.0f) - filteredReading[b]); // filter raw input, and scale to [0..255]\n  unsigned aRead = max(min(int(filteredReading[b]), 255), 0);                               // squash into 8bit\n  if (aRead <= POT_SENSITIVITY) aRead = 0;                                                   // make sure that 0 and 255 are used\n  if (aRead >= 255-POT_SENSITIVITY) aRead = 255;\n\n  if (buttons[b].type == BTN_TYPE_ANALOG_INVERTED) aRead = 255 - aRead;\n\n  // remove noise & reduce frequency of UI updates\n  if (abs(int(aRead) - int(oldRead[b])) <= POT_SENSITIVITY) return;  // no significant change in reading\n\n  DEBUG_PRINTF_P(PSTR(\"Analog: Raw = %u\\n\"), rawReading);\n  DEBUG_PRINTF_P(PSTR(\" Filtered = %u\\n\"), aRead);\n\n  // Unomment the next lines if you still see flickering related to potentiometer\n  // This waits until strip finishes updating (why: strip was not updating at the start of handleButton() but may have started during analogRead()?)\n  //unsigned long wait_started = millis();\n  //while(strip.isUpdating() && (millis() - wait_started < STRIP_WAIT_TIME)) {\n  //  delay(1);\n  //}\n\n  oldRead[b] = aRead;\n\n  // if no macro for \"short press\" and \"long press\" is defined use brightness control\n  if (!buttons[b].macroButton && !buttons[b].macroLongPress) {\n    DEBUG_PRINTF_P(PSTR(\"Analog: Action = %u\\n\"), buttons[b].macroDoublePress);\n    // if \"double press\" macro defines which option to change\n    if (buttons[b].macroDoublePress >= 250) {\n      // global brightness\n      if (aRead == 0) {\n        briLast = bri;\n        bri = 0;\n      } else {\n        if (bri == 0) strip.restartRuntime();\n        bri = aRead;\n      }\n    } else if (buttons[b].macroDoublePress == 249) {\n      // effect speed\n      effectSpeed = aRead;\n    } else if (buttons[b].macroDoublePress == 248) {\n      // effect intensity\n      effectIntensity = aRead;\n    } else if (buttons[b].macroDoublePress == 247) {\n      // selected palette\n      effectPalette = map(aRead, 0, 252, 0, getPaletteCount()-1);\n      effectPalette = constrain(effectPalette, 0, getPaletteCount()-1);  // map is allowed to \"overshoot\", so we need to contrain the result\n    } else if (buttons[b].macroDoublePress == 200) {\n      // primary color, hue, full saturation\n      colorHStoRGB(aRead*256, 255, colPri);\n    } else {\n      // otherwise use \"double press\" for segment selection\n      Segment& seg = strip.getSegment(buttons[b].macroDoublePress);\n      if (aRead == 0) {\n        seg.on = false; // do not use transition\n        //seg.setOption(SEG_OPTION_ON, false); // off (use transition)\n      } else {\n        seg.opacity = aRead; // set brightness (opacity) of segment\n        seg.on = true;\n        //seg.setOpacity(aRead);\n        //seg.setOption(SEG_OPTION_ON, true); // on (use transition)\n      }\n      // this will notify clients of update (websockets,mqtt,etc)\n      updateInterfaces(CALL_MODE_BUTTON);\n    }\n  } else {\n    DEBUG_PRINTLN(F(\"Analog: No action\"));\n    //TODO:\n    // we can either trigger a preset depending on the level (between short and long entries)\n    // or use it for RGBW direct control\n  }\n  colorUpdated(CALL_MODE_BUTTON);\n}\n\nvoid handleButton()\n{\n  static unsigned long lastAnalogRead = 0UL;\n  static unsigned long lastRun = 0UL;\n  unsigned long now = millis();\n\n  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)\n  lastRun = now;\n\n  for (unsigned b = 0; b < buttons.size(); b++) {\n    #ifdef ESP8266\n    if ((buttons[b].pin < 0 && !(buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED)) || buttons[b].type == BTN_TYPE_NONE) continue;\n    #else\n    if (buttons[b].pin < 0 || buttons[b].type == BTN_TYPE_NONE) continue;\n    #endif\n\n    if (UsermodManager::handleButton(b)) continue; // did usermod handle buttons\n\n    if (buttons[b].type == BTN_TYPE_ANALOG || buttons[b].type == BTN_TYPE_ANALOG_INVERTED) { // button is not a button but a potentiometer\n      if (now - lastAnalogRead > ANALOG_BTN_READ_CYCLE) {\n        handleAnalog(b);\n      }\n      continue;\n    }\n\n    // button is not momentary, but switch. This is only suitable on pins whose on-boot state does not matter (NOT gpio0)\n    if (buttons[b].type == BTN_TYPE_SWITCH || buttons[b].type == BTN_TYPE_TOUCH_SWITCH || buttons[b].type == BTN_TYPE_PIR_SENSOR) {\n      handleSwitch(b);\n      continue;\n    }\n\n    // momentary button logic\n    if (isButtonPressed(b)) { // pressed\n\n      // if all macros are the same, fire action immediately on rising edge\n      if (buttons[b].macroButton && buttons[b].macroButton == buttons[b].macroLongPress && buttons[b].macroButton == buttons[b].macroDoublePress) {\n        if (!buttons[b].pressedBefore) shortPressAction(b);\n        buttons[b].pressedBefore = true;\n        buttons[b].pressedTime = now; // continually update (for debouncing to work in release handler)\n        continue;\n      }\n\n      if (!buttons[b].pressedBefore) buttons[b].pressedTime = now;\n      buttons[b].pressedBefore = true;\n\n      if (now - buttons[b].pressedTime > WLED_LONG_PRESS) { //long press\n        if (!buttons[b].longPressed) {\n          buttonBriDirection = !buttonBriDirection; //toggle brightness direction on long press\n          longPressAction(b);\n        } else if (b) { //repeatable action (~5 times per s) on button > 0\n          longPressAction(b);\n          buttons[b].pressedTime = now - WLED_LONG_REPEATED_ACTION; //200ms\n        }\n        buttons[b].longPressed = true;\n      }\n\n    } else if (buttons[b].pressedBefore) { //released\n      long dur = now - buttons[b].pressedTime;\n\n      // released after rising-edge short press action\n      if (buttons[b].macroButton && buttons[b].macroButton == buttons[b].macroLongPress && buttons[b].macroButton == buttons[b].macroDoublePress) {\n        if (dur > WLED_DEBOUNCE_THRESHOLD) buttons[b].pressedBefore = false; // debounce, blocks button for 50 ms once it has been released\n        continue;\n      }\n\n      if (dur < WLED_DEBOUNCE_THRESHOLD) {buttons[b].pressedBefore = false; continue;} // too short \"press\", debounce\n      bool doublePress = buttons[b].waitTime; //did we have a short press before?\n      buttons[b].waitTime = 0;\n\n      if (b == 0 && dur > WLED_LONG_AP) { // long press on button 0 (when released)\n        if (dur > WLED_LONG_FACTORY_RESET) { // factory reset if pressed > 10 seconds\n          WLED_FS.format();\n          doReboot = true;\n        } else {\n          WLED::instance().initAP(true);\n        }\n      } else if (!buttons[b].longPressed) { //short press\n        //NOTE: this interferes with double click handling in usermods so usermod needs to implement full button handling\n        if (b != 1 && !buttons[b].macroDoublePress) { //don't wait for double press on buttons without a default action if no double press macro set\n          shortPressAction(b);\n        } else { //double press if less than 350 ms between current press and previous short press release (buttonWaitTime!=0)\n          if (doublePress) {\n            doublePressAction(b);\n          } else {\n            buttons[b].waitTime = now;\n          }\n        }\n      }\n      buttons[b].pressedBefore = false;\n      buttons[b].longPressed = false;\n    }\n\n    //if 350ms elapsed since last short press release it is a short press\n    if (buttons[b].waitTime && now - buttons[b].waitTime > WLED_DOUBLE_PRESS && !buttons[b].pressedBefore) {\n      buttons[b].waitTime = 0;\n      shortPressAction(b);\n    }\n  }\n  if (now - lastAnalogRead > ANALOG_BTN_READ_CYCLE) {\n    lastAnalogRead = now;\n  }\n}\n\n// handleIO() happens *after* handleTransitions() (see wled.cpp) which may change bri/briT but *before* strip.service()\n// where actual LED painting occurrs\n// this is important for relay control and in the event of turning off on-board LED\nvoid handleIO()\n{\n  handleButton();\n\n  // 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\n  // next loop() cycle\n  handleOnOff();\n}\n\nvoid handleOnOff(bool forceOff)\n{\n  if (strip.getBrightness() && !forceOff) {\n    lastOnTime = millis();\n    if (offMode) {\n      BusManager::on();\n      if (rlyPin>=0) {\n        // note: pinMode is set in first call to handleOnOff(true) in beginStrip()\n        digitalWrite(rlyPin, rlyMde); // set to on state\n        delay(RELAY_DELAY); // let power stabilize before sending LED data (#346 #812 #3581 #3955)\n      }\n      offMode = false;\n    }\n  } else if ((millis() - lastOnTime > 600 && !strip.needsUpdate()) || forceOff) {\n    // for turning LED or relay off we need to wait until strip no longer needs updates (strip.trigger())\n    if (!offMode) {\n      BusManager::off();\n      if (rlyPin>=0) {\n        digitalWrite(rlyPin, !rlyMde); // set output before disabling high-z state to avoid output glitches\n        pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT);\n      }\n      offMode = true;\n    }\n  }\n}\n\nvoid IRAM_ATTR touchButtonISR()\n{\n  // used for ESP32 S2 and S3: nothing to do, ISR is just used to update registers of HAL driver\n}\n"
  },
  {
    "path": "wled00/cfg.cpp",
    "content": "#include \"wled.h\"\n#include \"wled_ethernet.h\"\n\n/*\n * Serializes and parses the cfg.json and wsec.json settings files, stored in internal FS.\n * The structure of the JSON is not to be considered an official API and may change without notice.\n */\n\n#ifndef PIXEL_COUNTS\n  #define PIXEL_COUNTS DEFAULT_LED_COUNT\n#endif\n\n#ifndef DATA_PINS\n  #define DATA_PINS DEFAULT_LED_PIN\n#endif\n\n#ifndef LED_TYPES\n  #define LED_TYPES DEFAULT_LED_TYPE\n#endif\n\n#ifndef DEFAULT_LED_COLOR_ORDER\n  #define DEFAULT_LED_COLOR_ORDER COL_ORDER_GRB  //default to GRB\n#endif\n\nstatic constexpr unsigned sumPinsRequired(const unsigned* current, size_t count) {\n  return (count > 0) ? (Bus::getNumberOfPins(*current) + sumPinsRequired(current+1,count-1)) : 0;\n}\n\nstatic constexpr bool validatePinsAndTypes(const unsigned* types, unsigned numTypes, unsigned numPins ) {\n  // Pins provided < pins required -> always invalid\n  // Pins provided = pins required -> always valid\n  // Pins provided > pins required -> valid if excess pins are a product of last type pins since it will be repeated\n  return (sumPinsRequired(types, numTypes) > numPins) ? false :\n          (numPins - sumPinsRequired(types, numTypes)) % Bus::getNumberOfPins(types[numTypes-1]) == 0;\n}\n\n\n//simple macro for ArduinoJSON's or syntax\n#define CJSON(a,b) a = b | a\n\nstatic inline void getStringFromJson(char* dest, const char* src, size_t len) {\n  if (src != nullptr) strlcpy(dest, src, len);\n}\n\nbool deserializeConfig(JsonObject doc, bool fromFS) {\n  bool needsSave = false;\n  //int rev_major = doc[\"rev\"][0]; // 1\n  //int rev_minor = doc[\"rev\"][1]; // 0\n\n  //long vid = doc[F(\"vid\")]; // 2010020\n\n  JsonObject id = doc[\"id\"];\n  getStringFromJson(cmDNS, id[F(\"mdns\")], 33);\n  getStringFromJson(serverDescription, id[F(\"name\")], 33);\n#ifndef WLED_DISABLE_ALEXA\n  getStringFromJson(alexaInvocationName, id[F(\"inv\")], 33);\n#endif\n  CJSON(simplifiedUI, id[F(\"sui\")]);\n\n  JsonObject nw = doc[\"nw\"];\n#ifndef WLED_DISABLE_ESPNOW\n  CJSON(enableESPNow, nw[F(\"espnow\")]);\n  linked_remotes.clear();\n  JsonVariant lrem = nw[F(\"linked_remote\")];\n  if (!lrem.isNull()) {\n     if (lrem.is<JsonArray>()) {\n      for (size_t i = 0; i < lrem.size(); i++) {\n        std::array<char, 13> entry{};\n        getStringFromJson(entry.data(), lrem[i], 13);\n        entry[12] = '\\0';\n        linked_remotes.emplace_back(entry);\n      }\n    }\n    else { // legacy support for single MAC address in config\n      std::array<char, 13> entry{};\n      getStringFromJson(entry.data(), lrem, 13);\n      entry[12] = '\\0';\n      linked_remotes.emplace_back(entry);\n    }\n  }\n#endif\n\n  size_t n = 0;\n  JsonArray nw_ins = nw[\"ins\"];\n  if (!nw_ins.isNull()) {\n    // as password are stored separately in wsec.json when reading configuration vector resize happens there, but for dynamic config we need to resize if necessary\n    if (nw_ins.size() > 1 && nw_ins.size() > multiWiFi.size()) multiWiFi.resize(nw_ins.size()); // resize constructs objects while resizing\n    for (JsonObject wifi : nw_ins) {\n      JsonArray ip = wifi[\"ip\"];\n      JsonArray gw = wifi[\"gw\"];\n      JsonArray sn = wifi[\"sn\"];\n      char ssid[33] = \"\";\n      char pass[65] = \"\";\n      char bssid[13] = \"\";\n      IPAddress nIP = (uint32_t)0U, nGW = (uint32_t)0U, nSN = (uint32_t)0x00FFFFFF; // little endian\n      getStringFromJson(ssid, wifi[F(\"ssid\")], 33);\n      getStringFromJson(pass, wifi[\"psk\"], 65); // password is not normally present but if it is, use it\n      getStringFromJson(bssid, wifi[F(\"bssid\")], 13);\n      for (size_t i = 0; i < 4; i++) {\n        CJSON(nIP[i], ip[i]);\n        CJSON(nGW[i], gw[i]);\n        CJSON(nSN[i], sn[i]);\n      }\n      if (strlen(ssid) > 0) strlcpy(multiWiFi[n].clientSSID, ssid, 33); // this will keep old SSID intact if not present in JSON\n      if (strlen(pass) > 0) strlcpy(multiWiFi[n].clientPass, pass, 65); // this will keep old password intact if not present in JSON\n      if (strlen(bssid) > 0) fillStr2MAC(multiWiFi[n].bssid, bssid);\n      multiWiFi[n].staticIP = nIP;\n      multiWiFi[n].staticGW = nGW;\n      multiWiFi[n].staticSN = nSN;\n#ifdef WLED_ENABLE_WPA_ENTERPRISE\n      byte encType = WIFI_ENCRYPTION_TYPE_PSK;\n      char anonIdent[65] = \"\";\n      char ident[65] = \"\";\n      CJSON(encType, wifi[F(\"enc_type\")]);\n      getStringFromJson(anonIdent, wifi[\"e_anon_ident\"], 65);\n      getStringFromJson(ident, wifi[\"e_ident\"], 65);\n      multiWiFi[n].encryptionType = encType;\n      strlcpy(multiWiFi[n].enterpriseAnonIdentity, anonIdent, 65);\n      strlcpy(multiWiFi[n].enterpriseIdentity, ident, 65);\n#endif\n      if (++n >= WLED_MAX_WIFI_COUNT) break;\n    }\n  }\n\n  JsonArray dns = nw[F(\"dns\")];\n  if (!dns.isNull()) {\n    for (size_t i = 0; i < 4; i++) {\n      CJSON(dnsAddress[i], dns[i]);\n    }\n  }\n\n  // https://github.com/wled/WLED/issues/5247\n#ifdef WLED_USE_ETHERNET\n  JsonObject ethernet = doc[F(\"eth\")];\n  CJSON(ethernetType, ethernet[\"type\"]);\n  // NOTE: Ethernet configuration takes priority over other use of pins\n  initEthernet();\n#endif\n\n  JsonObject ap = doc[\"ap\"];\n  getStringFromJson(apSSID, ap[F(\"ssid\")], 33);\n  getStringFromJson(apPass, ap[\"psk\"] , 65); //normally not present due to security\n  //int ap_pskl = ap[F(\"pskl\")];\n  CJSON(apChannel, ap[F(\"chan\")]);\n  if (apChannel > 13 || apChannel < 1) apChannel = 1;\n  CJSON(apHide, ap[F(\"hide\")]);\n  if (apHide > 1) apHide = 1;\n  CJSON(apBehavior, ap[F(\"behav\")]);\n  /*\n  JsonArray ap_ip = ap[\"ip\"];\n  for (unsigned i = 0; i < 4; i++) {\n    apIP[i] = ap_ip;\n  }\n  */\n\n  JsonObject wifi = doc[F(\"wifi\")];\n  noWifiSleep = !(wifi[F(\"sleep\")] | !noWifiSleep); // inverted\n  //noWifiSleep = !noWifiSleep;\n  CJSON(force802_3g, wifi[F(\"phy\")]); //force phy mode g?\n#ifdef ARDUINO_ARCH_ESP32\n  CJSON(txPower, wifi[F(\"txpwr\")]);\n  txPower = min(max((int)txPower, (int)WIFI_POWER_2dBm), (int)WIFI_POWER_19_5dBm);\n#endif\n\n  JsonObject hw = doc[F(\"hw\")];\n\n  // initialize LED pins and lengths prior to other HW (except for ethernet)\n  JsonObject hw_led = hw[\"led\"];\n\n  uint16_t total = hw_led[F(\"total\")] | strip.getLengthTotal();\n  uint16_t ablMilliampsMax = hw_led[F(\"maxpwr\")] | BusManager::ablMilliampsMax();\n  BusManager::setMilliampsMax(ablMilliampsMax);\n  Bus::setGlobalAWMode(hw_led[F(\"rgbwm\")] | AW_GLOBAL_DISABLED);\n  CJSON(strip.correctWB, hw_led[\"cct\"]);\n  CJSON(strip.cctFromRgb, hw_led[F(\"cr\")]);\n  CJSON(cctICused, hw_led[F(\"ic\")]);\n  uint8_t cctBlending = hw_led[F(\"cb\")] | Bus::getCCTBlend();\n  Bus::setCCTBlend(cctBlending);\n  unsigned targetFPS = hw_led[\"fps\"] | WLED_FPS;\n  strip.setTargetFps(targetFPS); //unlimited if 0, default 42 FPS\n\n  #ifndef WLED_DISABLE_2D\n  // 2D Matrix Settings\n  JsonObject matrix = hw_led[F(\"matrix\")];\n  if (!matrix.isNull()) {\n    strip.isMatrix = true;\n    unsigned numPanels = matrix[F(\"mpc\")] | 1;\n    numPanels = constrain(numPanels, 1, WLED_MAX_PANELS);\n    strip.panel.clear();\n    JsonArray panels = matrix[F(\"panels\")];\n    unsigned s = 0;\n    if (!panels.isNull()) {\n      strip.panel.reserve(numPanels);  // pre-allocate default 8x8 panels\n      for (JsonObject pnl : panels) {\n        WS2812FX::Panel p;\n        CJSON(p.bottomStart, pnl[\"b\"]);\n        CJSON(p.rightStart,  pnl[\"r\"]);\n        CJSON(p.vertical,    pnl[\"v\"]);\n        CJSON(p.serpentine,  pnl[\"s\"]);\n        CJSON(p.xOffset,     pnl[\"x\"]);\n        CJSON(p.yOffset,     pnl[\"y\"]);\n        CJSON(p.height,      pnl[\"h\"]);\n        CJSON(p.width,       pnl[\"w\"]);\n        strip.panel.push_back(p);\n        if (++s >= numPanels) break; // max panels reached\n      }\n    }\n    strip.panel.shrink_to_fit();  // release unused memory (just in case)\n    // cannot call strip.deserializeLedmap()/strip.setUpMatrix() here due to already locked JSON buffer\n    //if (!fromFS) doInit2D = true; // if called at boot (fromFS==true), WLED::beginStrip() will take care of setting up matrix\n  }\n  #endif\n\n  DEBUG_PRINTF_P(PSTR(\"Heap before buses: %d\\n\"), getFreeHeapSize());\n  JsonArray ins = hw_led[\"ins\"];\n  if (!ins.isNull()) {\n    int s = 0;  // bus iterator\n    for (JsonObject elm : ins) {\n      if (s >= WLED_MAX_BUSSES) break; // only counts physical buses\n      uint8_t pins[OUTPUT_MAX_PINS] = {255, 255, 255, 255, 255};\n      JsonArray pinArr = elm[\"pin\"];\n      if (pinArr.size() == 0) continue;\n      //pins[0] = pinArr[0];\n      unsigned i = 0;\n      for (int p : pinArr) {\n        pins[i++] = p;\n        if (i>4) break;\n      }\n      uint16_t length = elm[\"len\"] | 1;\n      uint8_t colorOrder = (int)elm[F(\"order\")]; // contains white channel swap option in upper nibble\n      uint8_t skipFirst = elm[F(\"skip\")];\n      uint16_t start = elm[\"start\"] | 0;\n      if (length==0 || start + length > MAX_LEDS) continue; // zero length or we reached max. number of LEDs, just stop\n      uint8_t ledType = elm[\"type\"] | TYPE_WS2812_RGB;\n      bool reversed = elm[\"rev\"];\n      bool refresh = elm[\"ref\"] | false;\n      uint16_t freqkHz = elm[F(\"freq\")] | 0;  // will be in kHz for DotStar and Hz for PWM\n      uint8_t AWmode = elm[F(\"rgbwm\")] | RGBW_MODE_MANUAL_ONLY;\n      uint8_t maPerLed = elm[F(\"ledma\")] | LED_MILLIAMPS_DEFAULT;\n      uint16_t maMax = elm[F(\"maxpwr\")] | (ablMilliampsMax * length) / total; // rough (incorrect?) per strip ABL calculation when no config exists\n      // To disable brightness limiter we either set output max current to 0 or single LED current to 0 (we choose output max current)\n      if (Bus::isPWM(ledType) || Bus::isOnOff(ledType) || Bus::isVirtual(ledType)) { // analog and virtual\n        maPerLed = 0;\n        maMax = 0;\n      }\n      ledType |= refresh << 7; // hack bit 7 to indicate strip requires off refresh\n      uint8_t driverType = elm[F(\"drv\")] | 0; // 0=RMT (default), 1=I2S note: polybus may override this if driver is not available\n\n      String host = elm[F(\"text\")] | String();\n      busConfigs.emplace_back(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, maPerLed, maMax, driverType, host);\n      doInitBusses = true;  // finalization done in beginStrip()\n      if (!Bus::isVirtual(ledType)) s++; // have as many virtual buses as you want\n    }\n  } else if (fromFS) {\n    //if busses failed to load, add default (fresh install, FS issue, ...)\n    BusManager::removeAll();\n    busConfigs.clear();\n\n    DEBUG_PRINTLN(F(\"No busses, init default\"));\n    constexpr unsigned defDataTypes[] = {LED_TYPES};\n    constexpr unsigned defDataPins[] = {DATA_PINS};\n    constexpr unsigned defCounts[] = {PIXEL_COUNTS};\n    constexpr unsigned defNumTypes = (sizeof(defDataTypes) / sizeof(defDataTypes[0]));\n    constexpr unsigned defNumPins = (sizeof(defDataPins) / sizeof(defDataPins[0]));\n    constexpr unsigned defNumCounts = (sizeof(defCounts) / sizeof(defCounts[0]));\n\n    static_assert(validatePinsAndTypes(defDataTypes, defNumTypes, defNumPins),\n                  \"The default pin list defined in DATA_PINS does not match the pin requirements for the default buses defined in LED_TYPES\");\n\n    unsigned pinsIndex = 0;\n    for (unsigned i = 0; i < WLED_MAX_BUSSES; i++) {\n      uint8_t defPin[OUTPUT_MAX_PINS];\n      // if we have less types than requested outputs and they do not align, use last known type to set current type\n      unsigned dataType = defDataTypes[(i < defNumTypes) ? i : defNumTypes -1];\n      unsigned busPins = Bus::getNumberOfPins(dataType);\n\n      // if we need more pins than available all outputs have been configured\n      if (pinsIndex + busPins > defNumPins) break;\n\n      // Assign all pins first so we can check for conflicts on this bus\n      for (unsigned j = 0; j < busPins && j < OUTPUT_MAX_PINS; j++) defPin[j] = defDataPins[pinsIndex + j];\n\n      for (unsigned j = 0; j < busPins && j < OUTPUT_MAX_PINS; j++) {\n        bool validPin = true;\n        // When booting without config (1st boot) we need to make sure GPIOs defined for LED output don't clash with hardware\n        // i.e. DEBUG (GPIO1), DMX (2), SPI RAM/FLASH (16&17 on ESP32-WROVER/PICO), read/only pins, etc.\n        // Pin should not be already allocated, read/only or defined for current bus\n        while (PinManager::isPinAllocated(defPin[j]) || !PinManager::isPinOk(defPin[j],true)) {\n          if (validPin) {\n            DEBUG_PRINTLN(F(\"Some of the provided pins cannot be used to configure this LED output.\"));\n            defPin[j] = 1; // start with GPIO1 and work upwards\n            validPin = false;\n          } else if (defPin[j] < WLED_NUM_PINS) {\n            defPin[j]++;\n          } else {\n            DEBUG_PRINTLN(F(\"No available pins left! Can't configure output.\"));\n            break;\n          }\n          // is the newly assigned pin already defined or used previously?\n          // try next in line until there are no clashes or we run out of pins\n          bool clash;\n          do {\n            clash = false;\n            // check for conflicts on current bus\n            for (const auto &pin : defPin) {\n              if (&pin != &defPin[j] && pin == defPin[j]) {\n                clash = true;\n                break;\n              }\n            }\n            // We already have a clash on current bus, no point checking next buses\n            if (!clash) {\n              // check for conflicts in defined pins\n              for (const auto &pin : defDataPins) {\n                if (pin == defPin[j]) {\n                  clash = true;\n                  break;\n                }\n              }\n            }\n            if (clash) defPin[j]++;\n            if (defPin[j] >= WLED_NUM_PINS) break;\n          } while (clash);\n        }\n      }\n      pinsIndex += busPins;\n\n      // if we have less counts than pins and they do not align, use last known count to set current count\n      unsigned count = defCounts[(i < defNumCounts) ? i : defNumCounts -1];\n      unsigned start = 0;\n      // analog always has length 1\n      if (Bus::isPWM(dataType) || Bus::isOnOff(dataType)) count = 1;\n      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)\n      doInitBusses = true;  // finalization done in beginStrip()\n    }\n  }\n  if (hw_led[\"rev\"] && BusManager::getNumBusses()) BusManager::getBus(0)->setReversed(true); //set 0.11 global reversed setting for first bus\n\n  // read color order map configuration\n  JsonArray hw_com = hw[F(\"com\")];\n  if (!hw_com.isNull()) {\n    BusManager::getColorOrderMap().reserve(std::min(hw_com.size(), (size_t)WLED_MAX_COLOR_ORDER_MAPPINGS));\n    for (JsonObject entry : hw_com) {\n      uint16_t start = entry[\"start\"] | 0;\n      uint16_t len = entry[\"len\"] | 0;\n      uint8_t colorOrder = (int)entry[F(\"order\")];\n      if (!BusManager::getColorOrderMap().add(start, len, colorOrder)) break;\n    }\n  }\n\n  // read multiple button configuration\n  JsonObject btn_obj = hw[\"btn\"];\n  CJSON(touchThreshold, btn_obj[F(\"tt\")]);\n  bool pull = btn_obj[F(\"pull\")] | (!disablePullUp); // if true, pullup is enabled\n  disablePullUp = !pull;\n  JsonArray hw_btn_ins = btn_obj[\"ins\"];\n  if (!hw_btn_ins.isNull()) {\n    // deallocate existing button pins\n    for (const auto &button : buttons) PinManager::deallocatePin(button.pin, PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button\n    buttons.clear(); // clear existing buttons\n    unsigned s = 0;\n    for (JsonObject btn : hw_btn_ins) {\n      uint8_t type = btn[\"type\"] | BTN_TYPE_NONE;\n      int8_t  pin  = btn[\"pin\"][0] | -1;\n      if (pin > -1 && PinManager::allocatePin(pin, false, PinOwner::Button)) {\n        #ifdef ARDUINO_ARCH_ESP32\n        // ESP32 only: check that analog button pin is a valid ADC gpio\n        if ((type == BTN_TYPE_ANALOG) || (type == BTN_TYPE_ANALOG_INVERTED)) {\n          if (digitalPinToAnalogChannel(pin) < 0) {\n            // not an ADC analog pin\n            DEBUG_PRINTF_P(PSTR(\"PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\\n\"), pin, s);\n            PinManager::deallocatePin(pin, PinOwner::Button);\n            pin = -1;\n            continue;\n          } else {\n            analogReadResolution(12); // see #4040\n          }\n        } else if ((type == BTN_TYPE_TOUCH || type == BTN_TYPE_TOUCH_SWITCH)) {\n          if (digitalPinToTouchChannel(pin) < 0) {\n            // not a touch pin\n            DEBUG_PRINTF_P(PSTR(\"PIN ALLOC error: GPIO%d for touch button #%d is not a touch pin!\\n\"), pin, s);\n            PinManager::deallocatePin(pin, PinOwner::Button);\n            pin = -1;\n            continue;\n          }          \n          //if touch pin, enable the touch interrupt on ESP32 S2 & S3\n          #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\n          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)\n          #endif\n        } else\n        #endif\n        {\n          // regular buttons and switches\n          if (disablePullUp) {\n            pinMode(pin, INPUT);\n          } else {\n            #ifdef ESP32\n            pinMode(pin, type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);\n            #else\n            pinMode(pin, INPUT_PULLUP);\n            #endif\n          }\n        }\n        JsonArray hw_btn_ins_0_macros = btn[\"macros\"];\n        uint8_t press       = hw_btn_ins_0_macros[0] | 0;\n        uint8_t longPress   = hw_btn_ins_0_macros[1] | 0;\n        uint8_t doublePress = hw_btn_ins_0_macros[2] | 0;\n        buttons.emplace_back(pin, type, press, longPress, doublePress); // add button to vector\n      }\n      if (++s >= WLED_MAX_BUTTONS) break; // max buttons reached\n    }\n  } else if (fromFS) {\n    // new install/missing configuration (button 0 has defaults)\n    // relies upon only being called once with fromFS == true, which is currently true.\n    constexpr uint8_t  defTypes[] = {BTNTYPE};\n    constexpr int8_t   defPins[]  = {BTNPIN};\n    constexpr unsigned numTypes   = (sizeof(defTypes) / sizeof(defTypes[0]));\n    constexpr unsigned numPins    = (sizeof(defPins) / sizeof(defPins[0]));\n    // check if the number of pins and types are valid; count of pins must be greater than or equal to types\n    static_assert(numTypes <= numPins, \"The default button pins defined in BTNPIN do not match the button types defined in BTNTYPE\");\n\n    uint8_t type = BTN_TYPE_NONE;\n    buttons.clear(); // clear existing buttons (just in case)\n    for (size_t s = 0; s < WLED_MAX_BUTTONS && s < numPins; s++) {\n      type = defTypes[s < numTypes ? s : numTypes - 1]; // use last known type to set current type if types less than pins\n      if (type == BTN_TYPE_NONE || defPins[s] < 0 || !PinManager::allocatePin(defPins[s], false, PinOwner::Button)) {\n        if (buttons.size() == 0) buttons.emplace_back(-1, BTN_TYPE_NONE); // add disabled button to vector (so we have at least one button defined)\n        continue; // pin not available or invalid, skip configuring this GPIO\n      }\n      if (disablePullUp) {\n        pinMode(defPins[s], INPUT);\n      } else {\n        #ifdef ESP32\n        pinMode(defPins[s], type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);\n        #else\n        pinMode(defPins[s], INPUT_PULLUP);\n        #endif\n      }\n      buttons.emplace_back(defPins[s], type); // add button to vector\n    }\n  }\n\n  CJSON(buttonPublishMqtt, btn_obj[\"mqtt\"]);\n\n  #ifndef WLED_DISABLE_INFRARED\n  int hw_ir_pin = hw[\"ir\"][\"pin\"] | -2; // 4\n  if (hw_ir_pin > -2) {\n    PinManager::deallocatePin(irPin, PinOwner::IR);\n    if (PinManager::allocatePin(hw_ir_pin, false, PinOwner::IR)) {\n      irPin = hw_ir_pin;\n    } else {\n      irPin = -1;\n    }\n  }\n  CJSON(irEnabled, hw[\"ir\"][\"type\"]);\n  #endif\n  CJSON(irApplyToAllSelected, hw[\"ir\"][\"sel\"]);\n\n  JsonObject relay = hw[F(\"relay\")];\n\n  rlyOpenDrain  = relay[F(\"odrain\")] | rlyOpenDrain;\n  int hw_relay_pin = relay[\"pin\"] | -2;\n  if (hw_relay_pin > -2) {\n    PinManager::deallocatePin(rlyPin, PinOwner::Relay);\n    if (PinManager::allocatePin(hw_relay_pin,true, PinOwner::Relay)) {\n      rlyPin = hw_relay_pin;\n      pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT);\n    } else {\n      rlyPin = -1;\n    }\n  }\n  if (relay.containsKey(\"rev\")) {\n    rlyMde = !relay[\"rev\"];\n  }\n\n  CJSON(serialBaud, hw[F(\"baud\")]);\n  if (serialBaud < 96 || serialBaud > 15000) serialBaud = 1152;\n  updateBaudRate(serialBaud *100);\n\n  JsonArray hw_if_i2c = hw[F(\"if\")][F(\"i2c-pin\")];\n  CJSON(i2c_sda, hw_if_i2c[0]);\n  CJSON(i2c_scl, hw_if_i2c[1]);\n  PinManagerPinType i2c[2] = { { i2c_sda, true }, { i2c_scl, true } };\n  if (i2c_scl >= 0 && i2c_sda >= 0 && PinManager::allocateMultiplePins(i2c, 2, PinOwner::HW_I2C)) {\n    #ifdef ESP32\n    if (!Wire.setPins(i2c_sda, i2c_scl)) { i2c_scl = i2c_sda = -1; } // this will fail if Wire is initialised (Wire.begin() called prior)\n    else Wire.begin();\n    #else\n    Wire.begin(i2c_sda, i2c_scl);\n    #endif\n  } else {\n    i2c_sda = -1;\n    i2c_scl = -1;\n  }\n  JsonArray hw_if_spi = hw[F(\"if\")][F(\"spi-pin\")];\n  CJSON(spi_mosi, hw_if_spi[0]);\n  CJSON(spi_sclk, hw_if_spi[1]);\n  CJSON(spi_miso, hw_if_spi[2]);\n  PinManagerPinType spi[3] = { { spi_mosi, true }, { spi_miso, true }, { spi_sclk, true } };\n  if (spi_mosi >= 0 && spi_sclk >= 0 && PinManager::allocateMultiplePins(spi, 3, PinOwner::HW_SPI)) {\n    #ifdef ESP32\n    SPI.begin(spi_sclk, spi_miso, spi_mosi);  // SPI global uses VSPI on ESP32 and FSPI on C3, S3\n    #else\n    SPI.begin();\n    #endif\n  } else {\n    spi_mosi = -1;\n    spi_miso = -1;\n    spi_sclk = -1;\n  }\n\n  //int hw_status_pin = hw[F(\"status\")][\"pin\"]; // -1\n\n  JsonObject light = doc[F(\"light\")];\n  CJSON(briMultiplier, light[F(\"scale-bri\")]);\n  CJSON(paletteBlend, light[F(\"pal-mode\")]);\n  CJSON(strip.autoSegments, light[F(\"aseg\")]);\n\n  CJSON(gammaCorrectVal, light[\"gc\"][\"val\"]); // default 2.2\n  float light_gc_bri = light[\"gc\"][\"bri\"] | 1.0f; // default to 1.0 (false)\n  float light_gc_col = light[\"gc\"][\"col\"] | gammaCorrectVal; // default to gammaCorrectVal (true)\n  if (light_gc_bri != 1.0f) gammaCorrectBri = true;\n  else                      gammaCorrectBri = false;\n  if (light_gc_col != 1.0f) gammaCorrectCol = true;\n  else                      gammaCorrectCol = false;\n  if (gammaCorrectVal < 0.1f || gammaCorrectVal > 3) {\n    gammaCorrectVal = 1.0f; // no gamma correction\n    gammaCorrectBri = false;\n    gammaCorrectCol = false;\n  }\n  NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); // fill look-up tables\n\n  JsonObject light_tr = light[\"tr\"];\n  int tdd = light_tr[\"dur\"] | -1;\n  if (tdd >= 0) transitionDelay = transitionDelayDefault = tdd * 100;\n  strip.setTransition(transitionDelayDefault);\n  CJSON(randomPaletteChangeTime, light_tr[F(\"rpc\")]);\n  CJSON(useHarmonicRandomPalette, light_tr[F(\"hrp\")]);\n\n  JsonObject light_nl = light[\"nl\"];\n  CJSON(nightlightMode, light_nl[\"mode\"]);\n  byte prev = nightlightDelayMinsDefault;\n  CJSON(nightlightDelayMinsDefault, light_nl[\"dur\"]);\n  if (nightlightDelayMinsDefault != prev) nightlightDelayMins = nightlightDelayMinsDefault;\n\n  CJSON(nightlightTargetBri, light_nl[F(\"tbri\")]);\n  CJSON(macroNl, light_nl[\"macro\"]);\n\n  JsonObject def = doc[\"def\"];\n  CJSON(bootPreset, def[\"ps\"]);\n  CJSON(turnOnAtBoot, def[\"on\"]); // true\n  CJSON(briS, def[\"bri\"]); // 128\n\n  JsonObject interfaces = doc[\"if\"];\n\n  JsonObject if_sync = interfaces[\"sync\"];\n  CJSON(udpPort, if_sync[F(\"port0\")]); // 21324\n  CJSON(udpPort2, if_sync[F(\"port1\")]); // 65506\n\n#ifndef WLED_DISABLE_ESPNOW\n  CJSON(useESPNowSync, if_sync[F(\"espnow\")]);\n#endif\n\n  JsonObject if_sync_recv = if_sync[F(\"recv\")];\n  CJSON(receiveNotificationBrightness, if_sync_recv[\"bri\"]);\n  CJSON(receiveNotificationColor, if_sync_recv[\"col\"]);\n  CJSON(receiveNotificationEffects, if_sync_recv[\"fx\"]);\n  CJSON(receiveNotificationPalette, if_sync_recv[\"pal\"]);\n  CJSON(receiveGroups, if_sync_recv[\"grp\"]);\n  CJSON(receiveSegmentOptions, if_sync_recv[\"seg\"]);\n  CJSON(receiveSegmentBounds, if_sync_recv[\"sb\"]);\n\n  JsonObject if_sync_send = if_sync[F(\"send\")];\n  CJSON(sendNotifications, if_sync_send[\"en\"]);\n  sendNotificationsRT = sendNotifications;\n  CJSON(notifyDirect, if_sync_send[F(\"dir\")]);\n  CJSON(notifyButton, if_sync_send[\"btn\"]);\n  CJSON(notifyAlexa, if_sync_send[\"va\"]);\n  CJSON(notifyHue, if_sync_send[\"hue\"]);\n  CJSON(syncGroups, if_sync_send[\"grp\"]);\n  if (if_sync_send[F(\"twice\")]) udpNumRetries = 1; // import setting from 0.13 and earlier\n  CJSON(udpNumRetries, if_sync_send[\"ret\"]);\n\n  JsonObject if_nodes = interfaces[\"nodes\"];\n  CJSON(nodeListEnabled, if_nodes[F(\"list\")]);\n  CJSON(nodeBroadcastEnabled, if_nodes[F(\"bcast\")]);\n\n  JsonObject if_live = interfaces[\"live\"];\n  CJSON(receiveDirect, if_live[\"en\"]);  // UDP/Hyperion realtime\n  CJSON(useMainSegmentOnly, if_live[F(\"mso\")]);\n  CJSON(realtimeRespectLedMaps, if_live[F(\"rlm\")]);\n  CJSON(e131Port, if_live[\"port\"]); // 5568\n  if (e131Port == DDP_DEFAULT_PORT) e131Port = E131_DEFAULT_PORT; // prevent double DDP port allocation\n  CJSON(e131Multicast, if_live[F(\"mc\")]);\n\n  JsonObject if_live_dmx = if_live[\"dmx\"];\n  CJSON(e131Universe, if_live_dmx[F(\"uni\")]);\n  CJSON(e131SkipOutOfSequence, if_live_dmx[F(\"seqskip\")]);\n  CJSON(DMXAddress, if_live_dmx[F(\"addr\")]);\n  if (!DMXAddress || DMXAddress > 510) DMXAddress = 1;\n  CJSON(DMXSegmentSpacing, if_live_dmx[F(\"dss\")]);\n  if (DMXSegmentSpacing > 150) DMXSegmentSpacing = 0;\n  CJSON(e131Priority, if_live_dmx[F(\"e131prio\")]);\n  if (e131Priority > 200) e131Priority = 200;\n  CJSON(DMXMode, if_live_dmx[\"mode\"]);\n\n  tdd = if_live[F(\"timeout\")] | -1;\n  if (tdd >= 0) realtimeTimeoutMs = tdd * 100;\n\n  #ifdef WLED_ENABLE_DMX_INPUT\n    CJSON(dmxInputTransmitPin, if_live_dmx[F(\"inputRxPin\")]);\n    CJSON(dmxInputReceivePin, if_live_dmx[F(\"inputTxPin\")]);\n    CJSON(dmxInputEnablePin, if_live_dmx[F(\"inputEnablePin\")]);\n    CJSON(dmxInputPort, if_live_dmx[F(\"dmxInputPort\")]);\n  #endif\n\n  CJSON(arlsForceMaxBri, if_live[F(\"maxbri\")]);\n  CJSON(arlsDisableGammaCorrection, if_live[F(\"no-gc\")]); // false\n  CJSON(arlsOffset, if_live[F(\"offset\")]); // 0\n\n#ifndef WLED_DISABLE_ALEXA\n  CJSON(alexaEnabled, interfaces[\"va\"][F(\"alexa\")]); // false\n  CJSON(macroAlexaOn, interfaces[\"va\"][\"macros\"][0]);\n  CJSON(macroAlexaOff, interfaces[\"va\"][\"macros\"][1]);\n  CJSON(alexaNumPresets, interfaces[\"va\"][\"p\"]);\n#endif\n\n#ifndef WLED_DISABLE_MQTT\n  JsonObject if_mqtt = interfaces[\"mqtt\"];\n  CJSON(mqttEnabled, if_mqtt[\"en\"]);\n  getStringFromJson(mqttServer, if_mqtt[F(\"broker\")], MQTT_MAX_SERVER_LEN+1);\n  CJSON(mqttPort, if_mqtt[\"port\"]); // 1883\n  getStringFromJson(mqttUser, if_mqtt[F(\"user\")], 41);\n  getStringFromJson(mqttPass, if_mqtt[\"psk\"], 65); //normally not present due to security\n  getStringFromJson(mqttClientID, if_mqtt[F(\"cid\")], 41);\n\n  getStringFromJson(mqttDeviceTopic, if_mqtt[F(\"topics\")][F(\"device\")], MQTT_MAX_TOPIC_LEN+1); // \"wled/test\"\n  getStringFromJson(mqttGroupTopic, if_mqtt[F(\"topics\")][F(\"group\")], MQTT_MAX_TOPIC_LEN+1); // \"\"\n  CJSON(retainMqttMsg, if_mqtt[F(\"rtn\")]);\n#endif\n\n#ifndef WLED_DISABLE_HUESYNC\n  JsonObject if_hue = interfaces[\"hue\"];\n  CJSON(huePollingEnabled, if_hue[\"en\"]);\n  CJSON(huePollLightId, if_hue[\"id\"]);\n  tdd = if_hue[F(\"iv\")] | -1;\n  if (tdd >= 2) huePollIntervalMs = tdd * 100;\n\n  JsonObject if_hue_recv = if_hue[\"recv\"];\n  CJSON(hueApplyOnOff, if_hue_recv[\"on\"]);\n  CJSON(hueApplyBri, if_hue_recv[\"bri\"]);\n  CJSON(hueApplyColor, if_hue_recv[\"col\"]);\n\n  JsonArray if_hue_ip = if_hue[\"ip\"];\n\n  for (unsigned i = 0; i < 4; i++)\n    CJSON(hueIP[i], if_hue_ip[i]);\n#endif\n\n  JsonObject if_ntp = interfaces[F(\"ntp\")];\n  CJSON(ntpEnabled, if_ntp[\"en\"]);\n  getStringFromJson(ntpServerName, if_ntp[F(\"host\")], 33); // \"1.wled.pool.ntp.org\"\n  CJSON(currentTimezone, if_ntp[F(\"tz\")]);\n  CJSON(utcOffsetSecs, if_ntp[F(\"offset\")]);\n  CJSON(useAMPM, if_ntp[F(\"ampm\")]);\n  CJSON(longitude, if_ntp[F(\"ln\")]);\n  CJSON(latitude, if_ntp[F(\"lt\")]);\n\n  JsonObject ol = doc[F(\"ol\")];\n  CJSON(overlayCurrent ,ol[F(\"clock\")]); // 0\n  CJSON(countdownMode, ol[F(\"cntdwn\")]);\n\n  CJSON(overlayMin, ol[\"min\"]);\n  CJSON(overlayMax, ol[F(\"max\")]);\n  CJSON(analogClock12pixel, ol[F(\"o12pix\")]);\n  CJSON(analogClock5MinuteMarks, ol[F(\"o5m\")]);\n  CJSON(analogClockSecondsTrail, ol[F(\"osec\")]);\n  CJSON(analogClockSolidBlack, ol[F(\"osb\")]);\n\n  //timed macro rules\n  JsonObject tm = doc[F(\"timers\")];\n  JsonObject cntdwn = tm[F(\"cntdwn\")];\n  JsonArray cntdwn_goal = cntdwn[F(\"goal\")];\n  CJSON(countdownYear,  cntdwn_goal[0]);\n  CJSON(countdownMonth, cntdwn_goal[1]);\n  CJSON(countdownDay,   cntdwn_goal[2]);\n  CJSON(countdownHour,  cntdwn_goal[3]);\n  CJSON(countdownMin,   cntdwn_goal[4]);\n  CJSON(countdownSec,   cntdwn_goal[5]);\n  CJSON(macroCountdown, cntdwn[\"macro\"]);\n  setCountdown();\n\n  JsonArray timers = tm[\"ins\"];\n  uint8_t it = 0;\n  for (JsonObject timer : timers) {\n    if (it > 9) break;\n    if (it<8 && timer[F(\"hour\")]==255) it=8;  // hour==255 -> sunrise/sunset\n    CJSON(timerHours[it], timer[F(\"hour\")]);\n    CJSON(timerMinutes[it], timer[\"min\"]);\n    CJSON(timerMacro[it], timer[\"macro\"]);\n\n    byte dowPrev = timerWeekday[it];\n    //note: act is currently only 0 or 1.\n    //the reason we are not using bool is that the on-disk type in 0.11.0 was already int\n    int actPrev = timerWeekday[it] & 0x01;\n    CJSON(timerWeekday[it], timer[F(\"dow\")]);\n    if (timerWeekday[it] != dowPrev) { //present in JSON\n      timerWeekday[it] <<= 1; //add active bit\n      int act = timer[\"en\"] | actPrev;\n      if (act) timerWeekday[it]++;\n    }\n    if (it<8) {\n      JsonObject start = timer[\"start\"];\n      byte startm = start[\"mon\"];\n      if (startm) timerMonth[it] = (startm << 4);\n      CJSON(timerDay[it], start[\"day\"]);\n      JsonObject end = timer[\"end\"];\n      CJSON(timerDayEnd[it], end[\"day\"]);\n      byte endm = end[\"mon\"];\n      if (startm) timerMonth[it] += endm & 0x0F;\n      if (!(timerMonth[it] & 0x0F)) timerMonth[it] += 12; //default end month to 12\n    }\n    it++;\n  }\n\n  JsonObject ota = doc[\"ota\"];\n  const char* pwd = ota[\"psk\"]; //normally not present due to security\n\n  bool pwdCorrect = !otaLock; //always allow access if ota not locked\n  if (pwd != nullptr && strncmp(otaPass, pwd, 33) == 0) pwdCorrect = true;\n\n  if (pwdCorrect) { //only accept these values from cfg.json if ota is unlocked (else from wsec.json)\n    CJSON(otaLock, ota[F(\"lock\")]);\n    CJSON(wifiLock, ota[F(\"lock-wifi\")]);\n    #ifndef WLED_DISABLE_OTA\n    CJSON(aOtaEnabled, ota[F(\"aota\")]);\n    #endif\n    getStringFromJson(otaPass, pwd, 33); //normally not present due to security\n    CJSON(otaSameSubnet, ota[F(\"same-subnet\")]);\n  }\n\n  #ifdef WLED_ENABLE_DMX\n  JsonObject dmx = doc[\"dmx\"];\n  CJSON(DMXChannels, dmx[F(\"chan\")]);\n  CJSON(DMXGap,dmx[F(\"gap\")]);\n  CJSON(DMXStart, dmx[\"start\"]);\n  CJSON(DMXStartLED,dmx[F(\"start-led\")]);\n\n  JsonArray dmx_fixmap = dmx[F(\"fixmap\")];\n  for (int i = 0; i < dmx_fixmap.size(); i++) {\n    if (i > 14) break;\n    CJSON(DMXFixtureMap[i],dmx_fixmap[i]);\n  }\n\n  CJSON(e131ProxyUniverse, dmx[F(\"e131proxy\")]);\n  #endif\n\n  DEBUG_PRINTLN(F(\"Starting usermod config.\"));\n  JsonObject usermods_settings = doc[\"um\"];\n  if (!usermods_settings.isNull()) {\n    needsSave = !UsermodManager::readFromConfig(usermods_settings);\n  }\n\n  if (fromFS) return needsSave;\n  // if from /json/cfg\n  doReboot = doc[F(\"rb\")] | doReboot;\n  if (doInitBusses) return false; // no save needed, will do after bus init in wled.cpp loop\n  return (doc[\"sv\"] | true);\n}\n\nstatic const char s_cfg_json[] PROGMEM = \"/cfg.json\";\n\nbool backupConfig() {\n  return backupFile(s_cfg_json);\n}\n\nbool restoreConfig() {\n  return restoreFile(s_cfg_json);\n}\n\nbool verifyConfig() {\n  return validateJsonFile(s_cfg_json);\n}\n\nbool configBackupExists() {\n  return checkBackupExists(s_cfg_json);\n}\n\n// rename config file and reboot\n// if the cfg file doesn't exist, such as after a reset, do nothing\nvoid resetConfig() {\n  if (WLED_FS.exists(s_cfg_json)) {\n    DEBUG_PRINTLN(F(\"Reset config\"));\n    char backupname[32];\n    snprintf_P(backupname, sizeof(backupname), PSTR(\"/rst.%s\"), &s_cfg_json[1]);\n    WLED_FS.rename(s_cfg_json, backupname);\n    doReboot = true;\n  }\n}\n\nbool deserializeConfigFromFS() {\n  [[maybe_unused]] bool success = deserializeConfigSec();\n\n  if (!requestJSONBufferLock(JSON_LOCK_CFG_DES)) return false;\n\n  DEBUG_PRINTLN(F(\"Reading settings from /cfg.json...\"));\n\n  success = readObjectFromFile(s_cfg_json, nullptr, pDoc);\n\n  // NOTE: This routine deserializes *and* applies the configuration\n  //       Therefore, must also initialize ethernet from this function\n  JsonObject root = pDoc->as<JsonObject>();\n  bool needsSave = deserializeConfig(root, true);\n  releaseJSONBufferLock();\n\n  return needsSave;\n}\n\nvoid serializeConfigToFS() {\n  serializeConfigSec();\n  backupConfig(); // backup before writing new config\n\n  DEBUG_PRINTLN(F(\"Writing settings to /cfg.json...\"));\n\n  if (!requestJSONBufferLock(JSON_LOCK_CFG_SER)) return;\n\n  JsonObject root = pDoc->to<JsonObject>();\n\n  serializeConfig(root);\n\n  File f = WLED_FS.open(FPSTR(s_cfg_json), \"w\");\n  if (f) serializeJson(root, f);\n  f.close();\n  releaseJSONBufferLock();\n\n  configNeedsWrite = false;\n}\n\nvoid serializeConfig(JsonObject root) {\n  JsonArray rev = root.createNestedArray(\"rev\");\n  rev.add(1); //major settings revision\n  rev.add(0); //minor settings revision\n\n  root[F(\"vid\")] = VERSION;\n\n  JsonObject id = root.createNestedObject(\"id\");\n  id[F(\"mdns\")] = cmDNS;\n  id[F(\"name\")] = serverDescription;\n#ifndef WLED_DISABLE_ALEXA\n  id[F(\"inv\")] = alexaInvocationName;\n#endif\n  id[F(\"sui\")] = simplifiedUI;\n\n  JsonObject nw = root.createNestedObject(\"nw\");\n#ifndef WLED_DISABLE_ESPNOW\n  nw[F(\"espnow\")] = enableESPNow;\n  JsonArray lrem = nw.createNestedArray(F(\"linked_remote\"));\n  for (size_t i = 0; i < linked_remotes.size(); i++) {\n    lrem.add(linked_remotes[i].data());\n  }\n#endif\n\n  JsonArray nw_ins = nw.createNestedArray(\"ins\");\n  for (size_t n = 0; n < multiWiFi.size(); n++) {\n    JsonObject wifi = nw_ins.createNestedObject();\n    wifi[F(\"ssid\")] = multiWiFi[n].clientSSID;\n    wifi[F(\"pskl\")] = strlen(multiWiFi[n].clientPass);\n    char bssid[13];\n    fillMAC2Str(bssid, multiWiFi[n].bssid);\n    wifi[F(\"bssid\")] = bssid;\n    JsonArray wifi_ip = wifi.createNestedArray(\"ip\");\n    JsonArray wifi_gw = wifi.createNestedArray(\"gw\");\n    JsonArray wifi_sn = wifi.createNestedArray(\"sn\");\n    for (size_t i = 0; i < 4; i++) {\n      wifi_ip.add(multiWiFi[n].staticIP[i]);\n      wifi_gw.add(multiWiFi[n].staticGW[i]);\n      wifi_sn.add(multiWiFi[n].staticSN[i]);\n    }\n#ifdef WLED_ENABLE_WPA_ENTERPRISE\n    wifi[F(\"enc_type\")] = multiWiFi[n].encryptionType;\n    if (multiWiFi[n].encryptionType == WIFI_ENCRYPTION_TYPE_ENTERPRISE) {\n      wifi[F(\"e_anon_ident\")] = multiWiFi[n].enterpriseAnonIdentity;\n      wifi[F(\"e_ident\")] = multiWiFi[n].enterpriseIdentity;\n    }\n#endif\n  }\n\n  JsonArray dns = nw.createNestedArray(F(\"dns\"));\n  for (size_t i = 0; i < 4; i++) {\n    dns.add(dnsAddress[i]);\n  }\n\n  JsonObject ap = root.createNestedObject(\"ap\");\n  ap[F(\"ssid\")] = apSSID;\n  ap[F(\"pskl\")] = strlen(apPass);\n  ap[F(\"chan\")] = apChannel;\n  ap[F(\"hide\")] = apHide;\n  ap[F(\"behav\")] = apBehavior;\n\n  JsonArray ap_ip = ap.createNestedArray(\"ip\");\n  ap_ip.add(4);\n  ap_ip.add(3);\n  ap_ip.add(2);\n  ap_ip.add(1);\n\n  JsonObject wifi = root.createNestedObject(F(\"wifi\"));\n  wifi[F(\"sleep\")] = !noWifiSleep;\n  wifi[F(\"phy\")] = force802_3g;\n#ifdef ARDUINO_ARCH_ESP32\n  wifi[F(\"txpwr\")] = txPower;\n#endif\n\n#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET)\n  JsonObject ethernet = root.createNestedObject(\"eth\");\n  ethernet[\"type\"] = ethernetType;\n  if (ethernetType != WLED_ETH_NONE && ethernetType < WLED_NUM_ETH_TYPES) {\n    JsonArray pins = ethernet.createNestedArray(\"pin\");\n    for (unsigned p=0; p<WLED_ETH_RSVD_PINS_COUNT; p++) pins.add(esp32_nonconfigurable_ethernet_pins[p].pin);\n    if (ethernetBoards[ethernetType].eth_power>=0)     pins.add(ethernetBoards[ethernetType].eth_power);\n    if (ethernetBoards[ethernetType].eth_mdc>=0)       pins.add(ethernetBoards[ethernetType].eth_mdc);\n    if (ethernetBoards[ethernetType].eth_mdio>=0)      pins.add(ethernetBoards[ethernetType].eth_mdio);\n    switch (ethernetBoards[ethernetType].eth_clk_mode) {\n      case ETH_CLOCK_GPIO0_IN:\n      case ETH_CLOCK_GPIO0_OUT:\n        pins.add(0);\n        break;\n      case ETH_CLOCK_GPIO16_OUT:\n        pins.add(16);\n        break;\n      case ETH_CLOCK_GPIO17_OUT:\n        pins.add(17);\n        break;\n    }\n  }\n#endif\n\n  JsonObject hw = root.createNestedObject(F(\"hw\"));\n\n  JsonObject hw_led = hw.createNestedObject(\"led\");\n  hw_led[F(\"total\")] = strip.getLengthTotal(); //provided for compatibility on downgrade and per-output ABL\n  hw_led[F(\"maxpwr\")] = BusManager::ablMilliampsMax();\n//  hw_led[F(\"ledma\")] = 0; // no longer used\n  hw_led[\"cct\"] = strip.correctWB;\n  hw_led[F(\"cr\")] = strip.cctFromRgb;\n  hw_led[F(\"ic\")] = cctICused;\n  hw_led[F(\"cb\")] = Bus::getCCTBlend();\n  hw_led[\"fps\"] = strip.getTargetFps();\n  hw_led[F(\"rgbwm\")] = Bus::getGlobalAWMode(); // global auto white mode override\n\n  #ifndef WLED_DISABLE_2D\n  // 2D Matrix Settings\n  if (strip.isMatrix) {\n    JsonObject matrix = hw_led.createNestedObject(F(\"matrix\"));\n    matrix[F(\"mpc\")] = strip.panel.size();\n    JsonArray panels = matrix.createNestedArray(F(\"panels\"));\n    for (size_t i = 0; i < strip.panel.size(); i++) {\n      JsonObject pnl = panels.createNestedObject();\n      pnl[\"b\"] = strip.panel[i].bottomStart;\n      pnl[\"r\"] = strip.panel[i].rightStart;\n      pnl[\"v\"] = strip.panel[i].vertical;\n      pnl[\"s\"] = strip.panel[i].serpentine;\n      pnl[\"x\"] = strip.panel[i].xOffset;\n      pnl[\"y\"] = strip.panel[i].yOffset;\n      pnl[\"h\"] = strip.panel[i].height;\n      pnl[\"w\"] = strip.panel[i].width;\n    }\n  }\n  #endif\n\n  JsonArray hw_led_ins = hw_led.createNestedArray(\"ins\");\n\n  for (size_t s = 0; s < BusManager::getNumBusses(); s++) {\n    DEBUG_PRINTF_P(PSTR(\"Cfg: Saving bus #%u\\n\"), s);\n    const Bus *bus = BusManager::getBus(s);\n    if (!bus) break;  // Memory corruption, iterator invalid\n    DEBUG_PRINTF_P(PSTR(\"  (%d-%d, type:%d, CO:%d, rev:%d, skip:%d, AW:%d kHz:%d, mA:%d/%d)\\n\"),\n      (int)bus->getStart(), (int)(bus->getStart()+bus->getLength()),\n      (int)(bus->getType() & 0x7F),\n      (int)bus->getColorOrder(),\n      (int)bus->isReversed(),\n      (int)bus->skippedLeds(),\n      (int)bus->getAutoWhiteMode(),\n      (int)bus->getFrequency(),\n      (int)bus->getLEDCurrent(), (int)bus->getMaxCurrent()\n    );\n    JsonObject ins = hw_led_ins.createNestedObject();\n    ins[\"start\"] = bus->getStart();\n    ins[\"len\"]   = bus->getLength();\n    JsonArray ins_pin = ins.createNestedArray(\"pin\");\n    uint8_t pins[5];\n    uint8_t nPins = bus->getPins(pins);\n    for (int i = 0; i < nPins; i++) ins_pin.add(pins[i]);\n    ins[F(\"order\")]  = bus->getColorOrder();\n    ins[\"rev\"]       = bus->isReversed();\n    ins[F(\"skip\")]   = bus->skippedLeds();\n    ins[\"type\"]      = bus->getType() & 0x7F;\n    ins[\"ref\"]       = bus->isOffRefreshRequired();\n    ins[F(\"rgbwm\")]  = bus->getAutoWhiteMode();\n    ins[F(\"freq\")]   = bus->getFrequency();\n    ins[F(\"maxpwr\")] = bus->getMaxCurrent();\n    ins[F(\"ledma\")]  = bus->getLEDCurrent();\n    ins[F(\"drv\")]    = bus->getDriverType();\n    ins[F(\"text\")]   = bus->getCustomText();\n  }\n\n  JsonArray hw_com = hw.createNestedArray(F(\"com\"));\n  const ColorOrderMap& com = BusManager::getColorOrderMap();\n  for (size_t s = 0; s < com.count(); s++) {\n    const ColorOrderMapEntry *entry = com.get(s);\n    if (!entry || !entry->len) break;\n    JsonObject co = hw_com.createNestedObject();\n    co[\"start\"] = entry->start;\n    co[\"len\"] = entry->len;\n    co[F(\"order\")] = entry->colorOrder;\n  }\n\n  // button(s)\n  JsonObject hw_btn = hw.createNestedObject(\"btn\");\n  hw_btn[\"max\"] = WLED_MAX_BUTTONS; // just information about max number of buttons (not actually used)\n  hw_btn[F(\"pull\")] = !disablePullUp;\n  JsonArray hw_btn_ins = hw_btn.createNestedArray(\"ins\");\n\n  // configuration for all buttons\n  for (const auto &button : buttons) {\n    JsonObject hw_btn_ins_0 = hw_btn_ins.createNestedObject();\n    hw_btn_ins_0[\"type\"] = button.type;\n    JsonArray hw_btn_ins_0_pin = hw_btn_ins_0.createNestedArray(\"pin\");\n    hw_btn_ins_0_pin.add(button.pin);\n    JsonArray hw_btn_ins_0_macros = hw_btn_ins_0.createNestedArray(\"macros\");\n    hw_btn_ins_0_macros.add(button.macroButton);\n    hw_btn_ins_0_macros.add(button.macroLongPress);\n    hw_btn_ins_0_macros.add(button.macroDoublePress);\n  }\n\n  hw_btn[F(\"tt\")] = touchThreshold;\n  hw_btn[\"mqtt\"] = buttonPublishMqtt;\n\n  JsonObject hw_ir = hw.createNestedObject(\"ir\");\n  #ifndef WLED_DISABLE_INFRARED\n  hw_ir[\"pin\"] = irPin;\n  hw_ir[\"type\"] = irEnabled;  // the byte 'irEnabled' does contain the IR-Remote Type ( 0=disabled )\n  #endif\n  hw_ir[\"sel\"] = irApplyToAllSelected;\n\n  JsonObject hw_relay = hw.createNestedObject(F(\"relay\"));\n  hw_relay[\"pin\"] = rlyPin;\n  hw_relay[\"rev\"] = !rlyMde;\n  hw_relay[F(\"odrain\")] = rlyOpenDrain;\n\n  hw[F(\"baud\")] = serialBaud;\n\n  JsonObject hw_if = hw.createNestedObject(F(\"if\"));\n  JsonArray hw_if_i2c = hw_if.createNestedArray(\"i2c-pin\");\n  hw_if_i2c.add(i2c_sda);\n  hw_if_i2c.add(i2c_scl);\n  JsonArray hw_if_spi = hw_if.createNestedArray(\"spi-pin\");\n  hw_if_spi.add(spi_mosi);\n  hw_if_spi.add(spi_sclk);\n  hw_if_spi.add(spi_miso);\n\n  //JsonObject hw_status = hw.createNestedObject(\"status\");\n  //hw_status[\"pin\"] = -1;\n\n  JsonObject light = root.createNestedObject(F(\"light\"));\n  light[F(\"scale-bri\")] = briMultiplier;\n  light[F(\"pal-mode\")] = paletteBlend;\n  light[F(\"aseg\")] = strip.autoSegments;\n\n  JsonObject light_gc = light.createNestedObject(\"gc\");\n  light_gc[\"bri\"] = (gammaCorrectBri) ? gammaCorrectVal : 1.0f;  // keep compatibility\n  light_gc[\"col\"] = (gammaCorrectCol) ? gammaCorrectVal : 1.0f;  // keep compatibility\n  light_gc[\"val\"] = gammaCorrectVal;\n\n  JsonObject light_tr = light.createNestedObject(\"tr\");\n  light_tr[\"dur\"] = transitionDelayDefault / 100;\n  light_tr[F(\"rpc\")] = randomPaletteChangeTime;\n  light_tr[F(\"hrp\")] = useHarmonicRandomPalette;\n\n  JsonObject light_nl = light.createNestedObject(\"nl\");\n  light_nl[\"mode\"] = nightlightMode;\n  light_nl[\"dur\"] = nightlightDelayMinsDefault;\n  light_nl[F(\"tbri\")] = nightlightTargetBri;\n  light_nl[\"macro\"] = macroNl;\n\n  JsonObject def = root.createNestedObject(\"def\");\n  def[\"ps\"] = bootPreset;\n  def[\"on\"] = turnOnAtBoot;\n  def[\"bri\"] = briS;\n\n  JsonObject interfaces = root.createNestedObject(\"if\");\n\n  JsonObject if_sync = interfaces.createNestedObject(\"sync\");\n  if_sync[F(\"port0\")] = udpPort;\n  if_sync[F(\"port1\")] = udpPort2;\n\n#ifndef WLED_DISABLE_ESPNOW\n  if_sync[F(\"espnow\")] = useESPNowSync;\n#endif\n\n  JsonObject if_sync_recv = if_sync.createNestedObject(F(\"recv\"));\n  if_sync_recv[\"bri\"] = receiveNotificationBrightness;\n  if_sync_recv[\"col\"] = receiveNotificationColor;\n  if_sync_recv[\"fx\"]  = receiveNotificationEffects;\n  if_sync_recv[\"pal\"] = receiveNotificationPalette;\n  if_sync_recv[\"grp\"] = receiveGroups;\n  if_sync_recv[\"seg\"] = receiveSegmentOptions;\n  if_sync_recv[\"sb\"]  = receiveSegmentBounds;\n\n  JsonObject if_sync_send = if_sync.createNestedObject(F(\"send\"));\n  if_sync_send[\"en\"] = sendNotifications;\n  if_sync_send[F(\"dir\")] = notifyDirect;\n  if_sync_send[\"btn\"] = notifyButton;\n  if_sync_send[\"va\"] = notifyAlexa;\n  if_sync_send[\"hue\"] = notifyHue;\n  if_sync_send[\"grp\"] = syncGroups;\n  if_sync_send[\"ret\"] = udpNumRetries;\n\n  JsonObject if_nodes = interfaces.createNestedObject(\"nodes\");\n  if_nodes[F(\"list\")] = nodeListEnabled;\n  if_nodes[F(\"bcast\")] = nodeBroadcastEnabled;\n\n  JsonObject if_live = interfaces.createNestedObject(\"live\");\n  if_live[\"en\"] = receiveDirect; // UDP/Hyperion realtime\n  if_live[F(\"mso\")] = useMainSegmentOnly;\n  if_live[F(\"rlm\")] = realtimeRespectLedMaps;\n  if_live[\"port\"] = e131Port;\n  if_live[F(\"mc\")] = e131Multicast;\n\n  JsonObject if_live_dmx = if_live.createNestedObject(\"dmx\");\n  if_live_dmx[F(\"uni\")] = e131Universe;\n  if_live_dmx[F(\"seqskip\")] = e131SkipOutOfSequence;\n  if_live_dmx[F(\"e131prio\")] = e131Priority;\n  if_live_dmx[F(\"addr\")] = DMXAddress;\n  if_live_dmx[F(\"dss\")] = DMXSegmentSpacing;\n  if_live_dmx[\"mode\"] = DMXMode;\n  #ifdef WLED_ENABLE_DMX_INPUT\n    if_live_dmx[F(\"inputRxPin\")] = dmxInputTransmitPin;\n    if_live_dmx[F(\"inputTxPin\")] = dmxInputReceivePin;\n    if_live_dmx[F(\"inputEnablePin\")] = dmxInputEnablePin;\n    if_live_dmx[F(\"dmxInputPort\")] = dmxInputPort;\n  #endif\n\n  if_live[F(\"timeout\")] = realtimeTimeoutMs / 100;\n  if_live[F(\"maxbri\")] = arlsForceMaxBri;\n  if_live[F(\"no-gc\")] = arlsDisableGammaCorrection;\n  if_live[F(\"offset\")] = arlsOffset;\n\n#ifndef WLED_DISABLE_ALEXA\n  JsonObject if_va = interfaces.createNestedObject(\"va\");\n  if_va[F(\"alexa\")] = alexaEnabled;\n\n  JsonArray if_va_macros = if_va.createNestedArray(\"macros\");\n  if_va_macros.add(macroAlexaOn);\n  if_va_macros.add(macroAlexaOff);\n\n  if_va[\"p\"] = alexaNumPresets;\n#endif\n\n#ifndef WLED_DISABLE_MQTT\n  JsonObject if_mqtt = interfaces.createNestedObject(\"mqtt\");\n  if_mqtt[\"en\"] = mqttEnabled;\n  if_mqtt[F(\"broker\")] = mqttServer;\n  if_mqtt[\"port\"] = mqttPort;\n  if_mqtt[F(\"user\")] = mqttUser;\n  if_mqtt[F(\"pskl\")] = strlen(mqttPass);\n  if_mqtt[F(\"cid\")] = mqttClientID;\n  if_mqtt[F(\"rtn\")] = retainMqttMsg;\n\n  JsonObject if_mqtt_topics = if_mqtt.createNestedObject(F(\"topics\"));\n  if_mqtt_topics[F(\"device\")] = mqttDeviceTopic;\n  if_mqtt_topics[F(\"group\")] = mqttGroupTopic;\n#endif\n\n#ifndef WLED_DISABLE_HUESYNC\n  JsonObject if_hue = interfaces.createNestedObject(\"hue\");\n  if_hue[\"en\"] = huePollingEnabled;\n  if_hue[\"id\"] = huePollLightId;\n  if_hue[F(\"iv\")] = huePollIntervalMs / 100;\n\n  JsonObject if_hue_recv = if_hue.createNestedObject(F(\"recv\"));\n  if_hue_recv[\"on\"] = hueApplyOnOff;\n  if_hue_recv[\"bri\"] = hueApplyBri;\n  if_hue_recv[\"col\"] = hueApplyColor;\n\n  JsonArray if_hue_ip = if_hue.createNestedArray(\"ip\");\n  for (unsigned i = 0; i < 4; i++) {\n    if_hue_ip.add(hueIP[i]);\n  }\n#endif\n\n  JsonObject if_ntp = interfaces.createNestedObject(\"ntp\");\n  if_ntp[\"en\"] = ntpEnabled;\n  if_ntp[F(\"host\")] = ntpServerName;\n  if_ntp[F(\"tz\")] = currentTimezone;\n  if_ntp[F(\"offset\")] = utcOffsetSecs;\n  if_ntp[F(\"ampm\")] = useAMPM;\n  if_ntp[F(\"ln\")] = longitude;\n  if_ntp[F(\"lt\")] = latitude;\n\n  JsonObject ol = root.createNestedObject(\"ol\");\n  ol[F(\"clock\")] = overlayCurrent;\n  ol[F(\"cntdwn\")] = countdownMode;\n\n  ol[\"min\"] = overlayMin;\n  ol[F(\"max\")] = overlayMax;\n  ol[F(\"o12pix\")] = analogClock12pixel;\n  ol[F(\"o5m\")] = analogClock5MinuteMarks;\n  ol[F(\"osec\")] = analogClockSecondsTrail;\n  ol[F(\"osb\")] = analogClockSolidBlack;\n\n  JsonObject timers = root.createNestedObject(F(\"timers\"));\n\n  JsonObject cntdwn = timers.createNestedObject(F(\"cntdwn\"));\n  JsonArray goal = cntdwn.createNestedArray(F(\"goal\"));\n  goal.add(countdownYear); goal.add(countdownMonth); goal.add(countdownDay);\n  goal.add(countdownHour); goal.add(countdownMin); goal.add(countdownSec);\n  cntdwn[\"macro\"] = macroCountdown;\n\n  JsonArray timers_ins = timers.createNestedArray(\"ins\");\n\n  for (unsigned i = 0; i < 10; i++) {\n    if (timerMacro[i] == 0 && timerHours[i] == 0 && timerMinutes[i] == 0) continue; // sunrise/sunset get saved always (timerHours=255)\n    JsonObject timers_ins0 = timers_ins.createNestedObject();\n    timers_ins0[\"en\"] = (timerWeekday[i] & 0x01);\n    timers_ins0[F(\"hour\")] = timerHours[i];\n    timers_ins0[\"min\"] = timerMinutes[i];\n    timers_ins0[\"macro\"] = timerMacro[i];\n    timers_ins0[F(\"dow\")] = timerWeekday[i] >> 1;\n    if (i<8) {\n      JsonObject start = timers_ins0.createNestedObject(\"start\");\n      start[\"mon\"] = (timerMonth[i] >> 4) & 0xF;\n      start[\"day\"] = timerDay[i];\n      JsonObject end = timers_ins0.createNestedObject(\"end\");\n      end[\"mon\"] = timerMonth[i] & 0xF;\n      end[\"day\"] = timerDayEnd[i];\n    }\n  }\n\n  JsonObject ota = root.createNestedObject(\"ota\");\n  ota[F(\"lock\")] = otaLock;\n  ota[F(\"lock-wifi\")] = wifiLock;\n  ota[F(\"pskl\")] = strlen(otaPass);\n  #ifndef WLED_DISABLE_OTA\n  ota[F(\"aota\")] = aOtaEnabled;\n  #endif\n  ota[F(\"same-subnet\")] = otaSameSubnet;\n\n  #ifdef WLED_ENABLE_DMX\n  JsonObject dmx = root.createNestedObject(\"dmx\");\n  dmx[F(\"chan\")] = DMXChannels;\n  dmx[F(\"gap\")] = DMXGap;\n  dmx[\"start\"] = DMXStart;\n  dmx[F(\"start-led\")] = DMXStartLED;\n\n  JsonArray dmx_fixmap = dmx.createNestedArray(F(\"fixmap\"));\n  for (unsigned i = 0; i < 15; i++) {\n    dmx_fixmap.add(DMXFixtureMap[i]);\n  }\n\n  dmx[F(\"e131proxy\")] = e131ProxyUniverse;\n  #endif\n\n  JsonObject usermods_settings = root.createNestedObject(\"um\");\n  UsermodManager::addToConfig(usermods_settings);\n}\n\n\nstatic const char s_wsec_json[] PROGMEM = \"/wsec.json\";\n\n//settings in /wsec.json, not accessible via webserver, for passwords and tokens\nbool deserializeConfigSec() {\n  DEBUG_PRINTLN(F(\"Reading settings from /wsec.json...\"));\n\n  if (!requestJSONBufferLock(JSON_LOCK_CFG_SEC_DES)) return false;\n\n  bool success = readObjectFromFile(s_wsec_json, nullptr, pDoc);\n  if (!success) {\n    releaseJSONBufferLock();\n    return false;\n  }\n\n  JsonObject root = pDoc->as<JsonObject>();\n\n  size_t n = 0;\n  JsonArray nw_ins = root[\"nw\"][\"ins\"];\n  if (!nw_ins.isNull()) {\n    if (nw_ins.size() > 1 && nw_ins.size() > multiWiFi.size()) multiWiFi.resize(nw_ins.size()); // resize constructs objects while resizing\n    for (JsonObject wifi : nw_ins) {\n      char pw[65] = \"\";\n      getStringFromJson(pw, wifi[\"psk\"], 65);\n      strlcpy(multiWiFi[n].clientPass, pw, 65);\n      if (++n >= WLED_MAX_WIFI_COUNT) break;\n    }\n  }\n\n  JsonObject ap = root[\"ap\"];\n  getStringFromJson(apPass, ap[\"psk\"] , 65);\n\n  [[maybe_unused]] JsonObject interfaces = root[\"if\"];\n\n#ifndef WLED_DISABLE_MQTT\n  JsonObject if_mqtt = interfaces[\"mqtt\"];\n  getStringFromJson(mqttPass, if_mqtt[\"psk\"], 65);\n#endif\n\n#ifndef WLED_DISABLE_HUESYNC\n  getStringFromJson(hueApiKey, interfaces[\"hue\"][F(\"key\")], 47);\n#endif\n\n  getStringFromJson(settingsPIN, root[\"pin\"], 5);\n  correctPIN = !strlen(settingsPIN);\n\n  JsonObject ota = root[\"ota\"];\n  getStringFromJson(otaPass, ota[F(\"pwd\")], 33);\n  CJSON(otaLock, ota[F(\"lock\")]);\n  CJSON(wifiLock, ota[F(\"lock-wifi\")]);\n  #ifndef WLED_DISABLE_OTA\n  CJSON(aOtaEnabled, ota[F(\"aota\")]);\n  #endif\n\n  releaseJSONBufferLock();\n  return true;\n}\n\nvoid serializeConfigSec() {\n  DEBUG_PRINTLN(F(\"Writing settings to /wsec.json...\"));\n\n  if (!requestJSONBufferLock(JSON_LOCK_CFG_SEC_SER)) return;\n\n  JsonObject root = pDoc->to<JsonObject>();\n\n  JsonObject nw = root.createNestedObject(\"nw\");\n\n  JsonArray nw_ins = nw.createNestedArray(\"ins\");\n  for (size_t i = 0; i < multiWiFi.size(); i++) {\n    JsonObject wifi = nw_ins.createNestedObject();\n    wifi[F(\"psk\")] = multiWiFi[i].clientPass;\n  }\n\n  JsonObject ap = root.createNestedObject(\"ap\");\n  ap[\"psk\"] = apPass;\n\n  [[maybe_unused]] JsonObject interfaces = root.createNestedObject(\"if\");\n#ifndef WLED_DISABLE_MQTT\n  JsonObject if_mqtt = interfaces.createNestedObject(\"mqtt\");\n  if_mqtt[\"psk\"] = mqttPass;\n#endif\n#ifndef WLED_DISABLE_HUESYNC\n  JsonObject if_hue = interfaces.createNestedObject(\"hue\");\n  if_hue[F(\"key\")] = hueApiKey;\n#endif\n\n  root[\"pin\"] = settingsPIN;\n\n  JsonObject ota = root.createNestedObject(\"ota\");\n  ota[F(\"pwd\")] = otaPass;\n  ota[F(\"lock\")] = otaLock;\n  ota[F(\"lock-wifi\")] = wifiLock;\n  #ifndef WLED_DISABLE_OTA\n  ota[F(\"aota\")] = aOtaEnabled;\n  #endif\n\n  File f = WLED_FS.open(FPSTR(s_wsec_json), \"w\");\n  if (f) serializeJson(root, f);\n  f.close();\n  releaseJSONBufferLock();\n}\n"
  },
  {
    "path": "wled00/colors.cpp",
    "content": "#include \"wled.h\"\n\n/*\n * Color conversion & utility methods\n */\n\n/*\n * color blend function, based on FastLED blend function\n * the calculation for each color is: result = (A*(amountOfA) + A + B*(amountOfB) + B) / 256 with amountOfA = 255 - amountOfB\n */\nuint32_t WLED_O2_ATTR IRAM_ATTR color_blend(uint32_t color1, uint32_t color2, uint8_t blend) {\n  // min / max blend checking is omitted: calls with 0 or 255 are rare, checking lowers overall performance\n  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)\n  uint32_t rb1 =  color1       & TWO_CHANNEL_MASK;  // extract R & B channels from color1\n  uint32_t wg1 = (color1 >> 8) & TWO_CHANNEL_MASK;  // extract W & G channels from color1 (shifted for multiplication later)\n  uint32_t rb2 =  color2       & TWO_CHANNEL_MASK;  // extract R & B channels from color2\n  uint32_t wg2 = (color2 >> 8) & TWO_CHANNEL_MASK;  // extract W & G channels from color2 (shifted for multiplication later)\n  uint32_t rb3 = ((((rb1 << 8) | rb2) + (rb2 * blend) - (rb1 * blend)) >> 8) &  TWO_CHANNEL_MASK; // blend red and blue\n  uint32_t wg3 = ((((wg1 << 8) | wg2) + (wg2 * blend) - (wg1 * blend)))      & ~TWO_CHANNEL_MASK; // negated mask for white and green\n  return rb3 | wg3;\n}\n\n/*\n * color add function that preserves ratio\n * original idea: https://github.com/wled-dev/WLED/pull/2465 by https://github.com/Proto-molecule\n * speed optimisations by @dedehai\n */\nuint32_t WLED_O2_ATTR color_add(uint32_t c1, uint32_t c2, bool preserveCR) //1212558 | 1212598 | 1212576 | 1212530\n{\n  if (c1 == BLACK) return c2;\n  if (c2 == BLACK) return c1;\n  const uint32_t TWO_CHANNEL_MASK = 0x00FF00FF; // mask for R and B channels or W and G if negated\n  uint32_t rb = ( c1     & TWO_CHANNEL_MASK) + ( c2     & TWO_CHANNEL_MASK); // mask and add two colors at once\n  uint32_t wg = ((c1>>8) & TWO_CHANNEL_MASK) + ((c2>>8) & TWO_CHANNEL_MASK);\n\n  if (preserveCR) { // preserve color ratios\n    uint32_t overflow = (rb | wg) & 0x01000100; // detect overflow by checking 9th bit\n    if (overflow) {\n      uint32_t r = rb >> 16; // extract single color values\n      uint32_t b = rb & 0xFFFF;\n      uint32_t w = wg >> 16;\n      uint32_t g = wg & 0xFFFF;\n      uint32_t max = std::max(r,g);\n      max = std::max(max,b);\n      max = std::max(max,w);\n      const uint32_t scale = (uint32_t(255)<<8) / max; // division of two 8bit (shifted) values does not work -> use bit shifts and multiplaction instead\n      rb = ((rb * scale) >> 8) &  TWO_CHANNEL_MASK;\n      wg =  (wg * scale)       & ~TWO_CHANNEL_MASK;\n    } else wg <<= 8; //shift white and green back to correct position\n  } else {\n    // branchless per-channel saturation to 255 (extract 9th bit, subtract 1 if it is set, mask with 0xFF, input is 0xFF+0xFF=0x1EF max)\n    // example with overflow: input: 0x01EF01EF -> (0x0100100 - 0x00010001) = 0x00FF00FF -> input|0x00FF00FF = 0x00FF00FF (saturate)\n    // example without overflow: input: 0x007F007F -> (0x00000000 - 0x00000000) = 0x00000000 -> input|0x00000000 = input  (no change)\n    rb |= ((rb & 0x01000100) - ((rb >> 8) & 0x00010001)) & 0x00FF00FF;\n    wg |= ((wg & 0x01000100) - ((wg >> 8) & 0x00010001)) & 0x00FF00FF;\n    wg <<= 8; // restore WG position\n  }\n  return rb | wg;\n}\n\n/*\n * fades color toward black\n * if using \"video\" method the resulting color will never become black unless it is already black\n */\nuint32_t IRAM_ATTR color_fade(uint32_t c1, uint8_t amount, bool video) {\n  if (c1 == BLACK || amount == 0) return 0; // black or full fade\n  if (amount == 255) return c1;             // no change\n  uint32_t addRemains = 0;\n\n  if (!video) amount++; // add one for correct scaling using bitshifts\n  else {\n    // video scaling: make sure colors do not dim to zero if they started non-zero unless they distort the hue\n    uint8_t r = byte(c1>>16), g = byte(c1>>8), b = byte(c1), w = byte(c1>>24); // extract r, g, b, w channels\n    uint8_t maxc = (r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b); // determine dominant channel for hue preservation\n    addRemains  = r && (r<<5) > maxc ? 0x00010000 : 0; // note: setting color preservation threshold too high results in flickering and\n    addRemains |= g && (g<<5) > maxc ? 0x00000100 : 0; // jumping colors in low brightness gradients. Multiplying the color preserves\n    addRemains |= b && (b<<5) > maxc ? 0x00000001 : 0; // better accuracy than dividing the maxc. Shifting by 5 is a good compromise \n    addRemains |= w ? 0x01000000 : 0;                  // i.e. remove color channel if <13% of max\n  }\n  const uint32_t TWO_CHANNEL_MASK = 0x00FF00FF;\n  uint32_t rb = (((c1 & TWO_CHANNEL_MASK) * amount) >> 8) &  TWO_CHANNEL_MASK; // scale red and blue\n  uint32_t wg = (((c1 >> 8) & TWO_CHANNEL_MASK) * amount) & ~TWO_CHANNEL_MASK; // scale white and green\n  return (rb | wg) + addRemains;\n}\n\n/*\n * color adjustment in HSV color space (converts RGB to HSV and back), color conversions are not 100% accurate!\n   shifts hue, increase brightness, decreases saturation (if not black)\n   note: inputs are 32bit to speed up the function, useful input value ranges are 0-255\n */\nuint32_t adjust_color(uint32_t rgb, uint32_t hueShift, uint32_t lighten, uint32_t brighten) {\n  if (rgb == 0 || hueShift + lighten + brighten == 0) return rgb; // black or no change\n  CHSV32 hsv;\n  rgb2hsv(rgb, hsv); //convert to HSV\n  hsv.h += (hueShift << 8); // shift hue (hue is 16 bits)\n  hsv.s =  max((int32_t)0, (int32_t)hsv.s - (int32_t)lighten); // desaturate\n  hsv.v =  min((uint32_t)255, (uint32_t)hsv.v + brighten); // increase brightness\n  uint32_t rgb_adjusted;\n  hsv2rgb(hsv, rgb_adjusted); // convert back to RGB TODO: make this into 16 bit conversion\n  return rgb_adjusted;\n}\n\n// 1:1 replacement of fastled function optimized for ESP, slightly faster, more accurate and uses less flash (~ -200bytes)\nuint32_t ColorFromPaletteWLED(const CRGBPalette16& pal, unsigned index, uint8_t brightness, TBlendType blendType) {\n  if (blendType == LINEARBLEND_NOWRAP) {\n    index = (index * 0xF0) >> 8; // Blend range is affected by lo4 blend of values, remap to avoid wrapping\n  }\n  unsigned hi4 = byte(index) >> 4;\n  unsigned lo4 = (index & 0x0F);\n  const CRGB* entry = (CRGB*)&(pal[0]) + hi4;\n  unsigned red1   = entry->r;\n  unsigned green1 = entry->g;\n  unsigned blue1  = entry->b;\n  if (lo4 && blendType != NOBLEND) {\n    if (hi4 == 15) entry = &(pal[0]);\n    else ++entry;\n    unsigned f2 = (lo4 << 4);\n    unsigned f1 = 256 - f2;\n    red1   = (red1   * f1 + (unsigned)entry->r * f2) >> 8; // note: using color_blend() is slower\n    green1 = (green1 * f1 + (unsigned)entry->g * f2) >> 8;\n    blue1  = (blue1  * f1 + (unsigned)entry->b * f2) >> 8;\n  }\n  if (brightness < 255) { // note: zero checking could be done to return black but that is hardly ever used so it is omitted\n    // actually same as color_fade(), using color_fade() is slower\n    uint32_t scale = brightness + 1; // adjust for rounding (bitshift)\n    red1   = (red1   * scale) >> 8;\n    green1 = (green1 * scale) >> 8;\n    blue1  = (blue1  * scale) >> 8;\n  }\n  return RGBW32(red1,green1,blue1,0);\n}\n\nvoid setRandomColor(byte* rgb)\n{\n  lastRandomIndex = get_random_wheel_index(lastRandomIndex);\n  colorHStoRGB(lastRandomIndex*256,255,rgb);\n}\n\n/*\n * generates a random palette based on harmonic color theory\n * takes a base palette as the input, it will choose one color of the base palette and keep it\n */\nCRGBPalette16 generateHarmonicRandomPalette(const CRGBPalette16 &basepalette)\n{\n  CHSV palettecolors[4]; // array of colors for the new palette\n  uint8_t keepcolorposition = hw_random8(4); // color position of current random palette to keep\n  palettecolors[keepcolorposition] = rgb2hsv(basepalette.entries[keepcolorposition*5]); // read one of the base colors of the current palette\n  palettecolors[keepcolorposition].hue += hw_random8(10)-5; // +/- 5 randomness of base color\n  // generate 4 saturation and brightness value numbers\n  // only one saturation is allowed to be below 200 creating mostly vibrant colors\n  // only one brightness value number is allowed below 200, creating mostly bright palettes\n\n  for (int i = 0; i < 3; i++) { // generate three high values\n    palettecolors[i].saturation = hw_random8(200,255);\n    palettecolors[i].value = hw_random8(220,255);\n  }\n  // allow one to be lower\n  palettecolors[3].saturation = hw_random8(20,255);\n  palettecolors[3].value = hw_random8(80,255);\n\n  // shuffle the arrays\n  for (int i = 3; i > 0; i--) {\n    std::swap(palettecolors[i].saturation, palettecolors[hw_random8(i + 1)].saturation);\n    std::swap(palettecolors[i].value, palettecolors[hw_random8(i + 1)].value);\n  }\n\n  // now generate three new hues based off of the hue of the chosen current color\n  uint8_t basehue = palettecolors[keepcolorposition].hue;\n  uint8_t harmonics[3]; // hues that are harmonic but still a little random\n  uint8_t type = hw_random8(5); // choose a harmony type\n\n  switch (type) {\n    case 0: // analogous\n      harmonics[0] = basehue + hw_random8(30, 50);\n      harmonics[1] = basehue + hw_random8(10, 30);\n      harmonics[2] = basehue - hw_random8(10, 30);\n      break;\n\n    case 1: // triadic\n      harmonics[0] = basehue + 113 + hw_random8(15);\n      harmonics[1] = basehue + 233 + hw_random8(15);\n      harmonics[2] = basehue -   7 + hw_random8(15);\n      break;\n\n    case 2: // split-complementary\n      harmonics[0] = basehue + 145 + hw_random8(10);\n      harmonics[1] = basehue + 205 + hw_random8(10);\n      harmonics[2] = basehue -   5 + hw_random8(10);\n      break;\n\n    case 3: // square\n      harmonics[0] = basehue +  85 + hw_random8(10);\n      harmonics[1] = basehue + 175 + hw_random8(10);\n      harmonics[2] = basehue + 265 + hw_random8(10);\n     break;\n\n    case 4: // tetradic\n      harmonics[0] = basehue +  80 + hw_random8(20);\n      harmonics[1] = basehue + 170 + hw_random8(20);\n      harmonics[2] = basehue -  15 + hw_random8(30);\n     break;\n  }\n\n  if (hw_random8() < 128) {\n    // 50:50 chance of shuffling hues or keep the color order\n    for (int i = 2; i > 0; i--) {\n      std::swap(harmonics[i], harmonics[hw_random8(i + 1)]);\n    }\n  }\n\n  // now set the hues\n  int j = 0;\n  for (int i = 0; i < 4; i++) {\n    if (i==keepcolorposition) continue; // skip the base color\n    palettecolors[i].hue = harmonics[j];\n    j++;\n  }\n\n  bool makepastelpalette = false;\n  if (hw_random8() < 25) { // ~10% chance of desaturated 'pastel' colors\n    makepastelpalette = true;\n  }\n\n  // apply saturation\n  CRGB RGBpalettecolors[4];\n  for (int i = 0; i < 4; i++) {\n    if (makepastelpalette && palettecolors[i].saturation > 180) {\n      palettecolors[i].saturation -= 160; //desaturate all four colors\n    }\n    RGBpalettecolors[i] = (CRGB)palettecolors[i]; //convert to RGB\n    RGBpalettecolors[i] = ((uint32_t)RGBpalettecolors[i]) & 0x00FFFFFFU; //strip alpha from CRGB\n  }\n\n  return CRGBPalette16(RGBpalettecolors[0],\n                       RGBpalettecolors[1],\n                       RGBpalettecolors[2],\n                       RGBpalettecolors[3]);\n}\n\nCRGBPalette16 generateRandomPalette()  // generate fully random palette\n{\n  return CRGBPalette16(CHSV(hw_random8(), hw_random8(160, 255), hw_random8(128, 255)),\n                       CHSV(hw_random8(), hw_random8(160, 255), hw_random8(128, 255)),\n                       CHSV(hw_random8(), hw_random8(160, 255), hw_random8(128, 255)),\n                       CHSV(hw_random8(), hw_random8(160, 255), hw_random8(128, 255)));\n}\n\nvoid loadCustomPalettes() {\n  byte tcp[72]; //support gradient palettes with up to 18 entries\n  CRGBPalette16 targetPalette;\n  customPalettes.clear(); // start fresh\n  StaticJsonDocument<1536> pDoc; // barely enough to fit 72 numbers -> TODO: current format uses 214 bytes max per palette, why is this buffer so large?\n  unsigned emptyPaletteGap = 0; // count gaps in palette files to stop looking for more (each exists() call takes ~5ms)\n  for (int index = 0; index < WLED_MAX_CUSTOM_PALETTES; index++) {\n    char fileName[32];\n    sprintf_P(fileName, PSTR(\"/palette%d.json\"), index);\n    if (WLED_FS.exists(fileName)) {\n      emptyPaletteGap = 0; // reset gap counter if file exists\n      DEBUGFX_PRINTF_P(PSTR(\"Reading palette from %s\\n\"), fileName);\n      if (readObjectFromFile(fileName, nullptr, &pDoc)) {\n        JsonArray pal = pDoc[F(\"palette\")];\n        if (!pal.isNull() && pal.size()>3) { // not an empty palette (at least 2 entries)\n          memset(tcp, 255, sizeof(tcp));\n          if (pal[0].is<int>() && pal[1].is<const char *>()) {\n            // we have an array of index & hex strings\n            size_t palSize = MIN(pal.size(), 36);\n            palSize -= palSize % 2; // make sure size is multiple of 2\n            for (size_t i=0, j=0; i<palSize && pal[i].as<int>()<256; i+=2) {\n              uint8_t rgbw[] = {0,0,0,0};\n              if (colorFromHexString(rgbw, pal[i+1].as<const char *>())) { // will catch non-string entires\n                tcp[ j ] = (uint8_t) pal[ i ].as<int>(); // index\n                for (size_t c=0; c<3; c++) tcp[j+1+c] = rgbw[c]; // only use RGB component\n                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]));\n                j += 4;\n              }\n            }\n          } else {\n            size_t palSize = MIN(pal.size(), 72);\n            palSize -= palSize % 4; // make sure size is multiple of 4\n            for (size_t i=0; i<palSize && pal[i].as<int>()<256; i+=4) {\n              tcp[ i ] = (uint8_t) pal[ i ].as<int>(); // index\n              for (size_t c=0; c<3; c++) tcp[i+1+c] = (uint8_t) pal[i+1+c].as<int>();\n              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]));\n            }\n          }\n          customPalettes.push_back(targetPalette.loadDynamicGradientPalette(tcp));\n        } else {\n          DEBUGFX_PRINTLN(F(\"Wrong palette format.\"));\n        }\n      }\n    } else {\n      emptyPaletteGap++;\n      if (emptyPaletteGap > WLED_MAX_CUSTOM_PALETTE_GAP) break; // stop looking for more palettes\n    }\n  }\n}\n\nvoid hsv2rgb(const CHSV32& hsv, uint32_t& rgb) // convert HSV (16bit hue) to RGB (32bit with white = 0)\n{\n  unsigned int remainder, region, p, q, t;\n  unsigned int h = hsv.h;\n  unsigned int s = hsv.s;\n  unsigned int v = hsv.v;\n  if (s == 0) {\n      rgb = v << 16 | v << 8 | v;\n      return;\n  }\n  region = h / 10923;  // 65536 / 6 = 10923\n  remainder = (h - (region * 10923)) * 6;\n  p = (v * (255 - s)) >> 8;\n  q = (v * (255 - ((s * remainder) >> 16))) >> 8;\n  t = (v * (255 - ((s * (65535 - remainder)) >> 16))) >> 8;\n  switch (region) {\n    case 0:\n      rgb = v << 16 | t << 8 | p; break;\n    case 1:\n      rgb = q << 16 | v << 8 | p; break;\n    case 2:\n      rgb = p << 16 | v << 8 | t; break;\n    case 3:\n      rgb = p << 16 | q << 8 | v; break;\n    case 4:\n      rgb = t << 16 | p << 8 | v; break;\n    default:\n      rgb = v << 16 | p << 8 | q; break;\n  }\n}\n\nvoid rgb2hsv(const uint32_t rgb, CHSV32& hsv) // convert RGB to HSV (16bit hue), much more accurate and faster than fastled version\n{\n    hsv.raw = 0;\n    int32_t r = (rgb>>16)&0xFF;\n    int32_t g = (rgb>>8)&0xFF;\n    int32_t b = rgb&0xFF;\n    int32_t minval, maxval, delta;\n    minval = min(r, g);\n    minval = min(minval, b);\n    maxval = max(r, g);\n    maxval = max(maxval, b);\n    if (maxval == 0)  return; // black\n    hsv.v = maxval;\n    delta = maxval - minval;\n    hsv.s = (255 * delta) / maxval;\n    if (hsv.s == 0)  return; // gray value\n    if (maxval == r) hsv.h = (10923 * (g - b)) / delta;\n    else if (maxval == g)  hsv.h = 21845 + (10923 * (b - r)) / delta;\n    else hsv.h = 43690 + (10923 * (r - g)) / delta;\n}\n\nvoid colorHStoRGB(uint16_t hue, byte sat, byte* rgb) { //hue, sat to rgb\n  uint32_t crgb;\n  hsv2rgb(CHSV32(hue, sat, 255), crgb);\n  rgb[0] = byte((crgb) >> 16);\n  rgb[1] = byte((crgb) >> 8);\n  rgb[2] = byte(crgb);\n}\n\n//get RGB values from color temperature in K (https://tannerhelland.com/2012/09/18/convert-temperature-rgb-algorithm-code.html)\nvoid colorKtoRGB(uint16_t kelvin, byte* rgb) //white spectrum to rgb, calc\n{\n  int r = 0, g = 0, b = 0;\n  float temp = kelvin / 100.0f;\n  if (temp <= 66.0f) {\n    r = 255;\n    g = roundf(99.4708025861f * logf(temp) - 161.1195681661f);\n    if (temp <= 19.0f) {\n      b = 0;\n    } else {\n      b = roundf(138.5177312231f * logf((temp - 10.0f)) - 305.0447927307f);\n    }\n  } else {\n    r = roundf(329.698727446f * powf((temp - 60.0f), -0.1332047592f));\n    g = roundf(288.1221695283f * powf((temp - 60.0f), -0.0755148492f));\n    b = 255;\n  }\n  //g += 12; //mod by Aircoookie, a bit less accurate but visibly less pinkish\n  rgb[0] = (uint8_t) constrain(r, 0, 255);\n  rgb[1] = (uint8_t) constrain(g, 0, 255);\n  rgb[2] = (uint8_t) constrain(b, 0, 255);\n  rgb[3] = 0;\n}\n\nvoid colorCTtoRGB(uint16_t mired, byte* rgb) //white spectrum to rgb, bins\n{\n  //this is only an approximation using WS2812B with gamma correction enabled\n  if (mired > 475) {\n    rgb[0]=255;rgb[1]=199;rgb[2]=92;//500\n  } else if (mired > 425) {\n    rgb[0]=255;rgb[1]=213;rgb[2]=118;//450\n  } else if (mired > 375) {\n    rgb[0]=255;rgb[1]=216;rgb[2]=118;//400\n  } else if (mired > 325) {\n    rgb[0]=255;rgb[1]=234;rgb[2]=140;//350\n  } else if (mired > 275) {\n    rgb[0]=255;rgb[1]=243;rgb[2]=160;//300\n  } else if (mired > 225) {\n    rgb[0]=250;rgb[1]=255;rgb[2]=188;//250\n  } else if (mired > 175) {\n    rgb[0]=247;rgb[1]=255;rgb[2]=215;//200\n  } else {\n    rgb[0]=237;rgb[1]=255;rgb[2]=239;//150\n  }\n}\n\n#ifndef WLED_DISABLE_HUESYNC\nvoid colorXYtoRGB(float x, float y, byte* rgb) //coordinates to rgb (https://www.developers.meethue.com/documentation/color-conversions-rgb-xy)\n{\n  float z = 1.0f - x - y;\n  float X = (1.0f / y) * x;\n  float Z = (1.0f / y) * z;\n  float r = (int)255*(X * 1.656492f - 0.354851f - Z * 0.255038f);\n  float g = (int)255*(-X * 0.707196f + 1.655397f + Z * 0.036152f);\n  float b = (int)255*(X * 0.051713f - 0.121364f + Z * 1.011530f);\n  if (r > b && r > g && r > 1.0f) {\n    // red is too big\n    g = g / r;\n    b = b / r;\n    r = 1.0f;\n  } else if (g > b && g > r && g > 1.0f) {\n    // green is too big\n    r = r / g;\n    b = b / g;\n    g = 1.0f;\n  } else if (b > r && b > g && b > 1.0f) {\n    // blue is too big\n    r = r / b;\n    g = g / b;\n    b = 1.0f;\n  }\n  // Apply gamma correction\n  r = r <= 0.0031308f ? 12.92f * r : (1.0f + 0.055f) * powf(r, (1.0f / 2.4f)) - 0.055f;\n  g = g <= 0.0031308f ? 12.92f * g : (1.0f + 0.055f) * powf(g, (1.0f / 2.4f)) - 0.055f;\n  b = b <= 0.0031308f ? 12.92f * b : (1.0f + 0.055f) * powf(b, (1.0f / 2.4f)) - 0.055f;\n\n  if (r > b && r > g) {\n    // red is biggest\n    if (r > 1.0f) {\n      g = g / r;\n      b = b / r;\n      r = 1.0f;\n    }\n  } else if (g > b && g > r) {\n    // green is biggest\n    if (g > 1.0f) {\n      r = r / g;\n      b = b / g;\n      g = 1.0f;\n    }\n  } else if (b > r && b > g) {\n    // blue is biggest\n    if (b > 1.0f) {\n      r = r / b;\n      g = g / b;\n      b = 1.0f;\n    }\n  }\n  rgb[0] = byte(255.0f*r);\n  rgb[1] = byte(255.0f*g);\n  rgb[2] = byte(255.0f*b);\n}\n\nvoid colorRGBtoXY(const byte* rgb, float* xy) //rgb to coordinates (https://www.developers.meethue.com/documentation/color-conversions-rgb-xy)\n{\n  float X = rgb[0] * 0.664511f + rgb[1] * 0.154324f + rgb[2] * 0.162028f;\n  float Y = rgb[0] * 0.283881f + rgb[1] * 0.668433f + rgb[2] * 0.047685f;\n  float Z = rgb[0] * 0.000088f + rgb[1] * 0.072310f + rgb[2] * 0.986039f;\n  xy[0] = X / (X + Y + Z);\n  xy[1] = Y / (X + Y + Z);\n}\n#endif // WLED_DISABLE_HUESYNC\n\n//RRGGBB / WWRRGGBB order for hex\nvoid colorFromDecOrHexString(byte* rgb, const char* in)\n{\n  if (in[0] == 0) return;\n  char first = in[0];\n  uint32_t c = 0;\n\n  if (first == '#' || first == 'h' || first == 'H') //is HEX encoded\n  {\n    c = strtoul(in +1, NULL, 16);\n  } else\n  {\n    c = strtoul(in, NULL, 10);\n  }\n\n  rgb[0] = R(c);\n  rgb[1] = G(c);\n  rgb[2] = B(c);\n  rgb[3] = W(c);\n}\n\n//contrary to the colorFromDecOrHexString() function, this uses the more standard RRGGBB / RRGGBBWW order\nbool colorFromHexString(byte* rgb, const char* in) {\n  if (in == nullptr) return false;\n  size_t inputSize = strnlen(in, 9);\n  if (inputSize != 6 && inputSize != 8) return false;\n\n  uint32_t c = strtoul(in, NULL, 16);\n\n  if (inputSize == 6) {\n    rgb[0] = (c >> 16);\n    rgb[1] = (c >>  8);\n    rgb[2] =  c       ;\n  } else {\n    rgb[0] = (c >> 24);\n    rgb[1] = (c >> 16);\n    rgb[2] = (c >>  8);\n    rgb[3] =  c       ;\n  }\n  return true;\n}\n\nstatic inline float minf(float v, float w)\n{\n  if (w > v) return v;\n  return w;\n}\n\nstatic inline float maxf(float v, float w)\n{\n  if (w > v) return w;\n  return v;\n}\n\n// adjust RGB values based on color temperature in K (range [2800-10200]) (https://en.wikipedia.org/wiki/Color_balance)\n// called from bus manager when color correction is enabled!\nuint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb)\n{\n  //remember so that slow colorKtoRGB() doesn't have to run for every setPixelColor()\n  static byte correctionRGB[4] = {0,0,0,0};\n  static uint16_t lastKelvin = 0;\n  if (lastKelvin != kelvin) colorKtoRGB(kelvin, correctionRGB);  // convert Kelvin to RGB\n  lastKelvin = kelvin;\n  byte rgbw[4];\n  rgbw[0] = ((uint16_t) correctionRGB[0] * R(rgb)) /255; // correct R\n  rgbw[1] = ((uint16_t) correctionRGB[1] * G(rgb)) /255; // correct G\n  rgbw[2] = ((uint16_t) correctionRGB[2] * B(rgb)) /255; // correct B\n  rgbw[3] =                                W(rgb);\n  return RGBW32(rgbw[0],rgbw[1],rgbw[2],rgbw[3]);\n}\n\n//approximates a Kelvin color temperature from an RGB color.\n//this does no check for the \"whiteness\" of the color,\n//so should be used combined with a saturation check (as done by auto-white)\n//values from http://www.vendian.org/mncharity/dir3/blackbody/UnstableURLs/bbr_color.html (10deg)\n//equation spreadsheet at https://bit.ly/30RkHaN\n//accuracy +-50K from 1900K up to 8000K\n//minimum returned: 1900K, maximum returned: 10091K (range of 8192)\nuint16_t approximateKelvinFromRGB(uint32_t rgb) {\n  //if not either red or blue is 255, color is dimmed. Scale up\n  uint8_t r = R(rgb), b = B(rgb);\n  if (r == b) return 6550; //red == blue at about 6600K (also can't go further if both R and B are 0)\n\n  if (r > b) {\n    //scale blue up as if red was at 255\n    uint16_t scale = 0xFFFF / r; //get scale factor (range 257-65535)\n    b = ((uint16_t)b * scale) >> 8;\n    //For all temps K<6600 R is bigger than B (for full bri colors R=255)\n    //-> Use 9 linear approximations for blackbody radiation blue values from 2000-6600K (blue is always 0 below 2000K)\n    if (b < 33)  return 1900 + b       *6;\n    if (b < 72)  return 2100 + (b-33)  *10;\n    if (b < 101) return 2492 + (b-72)  *14;\n    if (b < 132) return 2900 + (b-101) *16;\n    if (b < 159) return 3398 + (b-132) *19;\n    if (b < 186) return 3906 + (b-159) *22;\n    if (b < 210) return 4500 + (b-186) *25;\n    if (b < 230) return 5100 + (b-210) *30;\n                 return 5700 + (b-230) *34;\n  } else {\n    //scale red up as if blue was at 255\n    uint16_t scale = 0xFFFF / b; //get scale factor (range 257-65535)\n    r = ((uint16_t)r * scale) >> 8;\n    //For all temps K>6600 B is bigger than R (for full bri colors B=255)\n    //-> Use 2 linear approximations for blackbody radiation red values from 6600-10091K (blue is always 0 below 2000K)\n    if (r > 225) return 6600 + (254-r) *50;\n    uint16_t k = 8080 + (225-r) *86;\n    return (k > 10091) ? 10091 : k;\n  }\n}\n\n// gamma lookup tables used for color correction (filled on 1st use (cfg.cpp & set.cpp))\nuint8_t NeoGammaWLEDMethod::gammaT[256];\nuint8_t NeoGammaWLEDMethod::gammaT_inv[256];\n\n// re-calculates & fills gamma tables\nvoid NeoGammaWLEDMethod::calcGammaTable(float gamma)\n{\n  float gamma_inv = 1.0f / gamma; // inverse gamma\n  for (size_t i = 1; i < 256; i++) {\n    gammaT[i] = (int)(powf((float)i / 255.0f, gamma) * 255.0f + 0.5f);\n    gammaT_inv[i] = (int)(powf(((float)i - 0.5f) / 255.0f, gamma_inv) * 255.0f + 0.5f);\n    //DEBUG_PRINTF_P(PSTR(\"gammaT[%d] = %d gammaT_inv[%d] = %d\\n\"), i, gammaT[i], i, gammaT_inv[i]);\n  }\n  gammaT[0] = 0;\n  gammaT_inv[0] = 0;\n}\n\nuint8_t NeoGammaWLEDMethod::Correct(uint8_t value)\n{\n  if (!gammaCorrectCol) return value;\n  return gammaT[value];\n}\n\nuint32_t NeoGammaWLEDMethod::inverseGamma32(uint32_t color)\n{\n  if (!gammaCorrectCol) return color;\n  uint8_t w = W(color);\n  uint8_t r = R(color);\n  uint8_t g = G(color);\n  uint8_t b = B(color);\n  w = gammaT_inv[w];\n  r = gammaT_inv[r];\n  g = gammaT_inv[g];\n  b = gammaT_inv[b];\n  return RGBW32(r, g, b, w);\n}\n"
  },
  {
    "path": "wled00/colors.h",
    "content": "#pragma once\n#ifndef WLED_COLORS_H\n#define WLED_COLORS_H\n\n/*\n * Color structs and color utility functions\n */\n#include <vector>\n#include \"FastLED.h\"\n\n#define ColorFromPalette ColorFromPaletteWLED // override fastled version\n\n// 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\n// 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\n// it can be useful to avoid back and forth conversions between uint32_t and fastled CRGB\nstruct CRGBW {\n    union {\n        uint32_t color32; // Access as a 32-bit value (0xWWRRGGBB)\n        struct {\n            uint8_t b;\n            uint8_t g;\n            uint8_t r;\n            uint8_t w;\n        };\n        uint8_t raw[4];   // Access as an array in the order B, G, R, W\n    };\n\n    // Default constructor\n    inline CRGBW() __attribute__((always_inline)) = default;\n\n    // Constructor from a 32-bit color (0xWWRRGGBB)\n    constexpr CRGBW(uint32_t color) __attribute__((always_inline)) : color32(color) {}\n\n    // Constructor with r, g, b, w values\n    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) {}\n\n    // Constructor from CRGB\n    constexpr CRGBW(CRGB rgb) __attribute__((always_inline)) : b(rgb.b), g(rgb.g), r(rgb.r), w(0) {}\n\n    // Access as an array\n    inline const uint8_t& operator[] (uint8_t x) const __attribute__((always_inline)) { return raw[x]; }\n\n    // Assignment from 32-bit color\n    inline CRGBW& operator=(uint32_t color) __attribute__((always_inline)) { color32 = color; return *this; }\n\n    // Assignment from r, g, b, w\n    inline CRGBW& operator=(const CRGB& rgb) __attribute__((always_inline)) { b = rgb.b; g = rgb.g; r = rgb.r; w = 0; return *this; }\n\n    // Conversion operator to uint32_t\n    inline operator uint32_t() const __attribute__((always_inline)) {\n      return color32;\n    }\n    /*\n    // Conversion operator to CRGB\n    inline operator CRGB() const __attribute__((always_inline)) {\n      return CRGB(r, g, b);\n    }\n\n    CRGBW& scale32 (uint8_t scaledown) // 32bit math\n    {\n      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\n      uint32_t scale = scaledown + 1;\n      uint32_t rb = (((color32 & 0x00FF00FF) * scale) >> 8) & 0x00FF00FF; // scale red and blue\n      uint32_t wg = (((color32 & 0xFF00FF00) >> 8) * scale) & 0xFF00FF00; // scale white and green\n          color32 =  rb | wg;\n      return *this;\n    }*/\n\n};\n\nstruct CHSV32 { // 32bit HSV color with 16bit hue for more accurate conversions\n  union {\n    struct {\n        uint16_t h;  // hue\n        uint8_t s;   // saturation\n        uint8_t v;   // value\n    };\n    uint32_t raw;    // 32bit access\n  };\n  inline CHSV32() __attribute__((always_inline)) = default; // default constructor\n\n    /// Allow construction from hue, saturation, and value\n    /// @param ih input hue\n    /// @param is input saturation\n    /// @param iv input value\n  inline CHSV32(uint16_t ih, uint8_t is, uint8_t iv) __attribute__((always_inline)) // constructor from 16bit h, s, v\n        : h(ih), s(is), v(iv) {}\n  inline CHSV32(uint8_t ih, uint8_t is, uint8_t iv) __attribute__((always_inline)) // constructor from 8bit h, s, v\n        : h((uint16_t)ih << 8), s(is), v(iv) {}\n  inline CHSV32(const CHSV& chsv) __attribute__((always_inline))  // constructor from CHSV\n    : h((uint16_t)chsv.h << 8), s(chsv.s), v(chsv.v) {}\n  inline operator CHSV() const { return CHSV((uint8_t)(h >> 8), s, v); } // typecast to CHSV\n};\nextern bool gammaCorrectCol;\n// similar to NeoPixelBus NeoGammaTableMethod but allows dynamic changes (superseded by NPB::NeoGammaDynamicTableMethod)\nclass NeoGammaWLEDMethod {\n  public:\n    [[gnu::hot]] static uint8_t Correct(uint8_t value);             // apply Gamma to single channel\n    [[gnu::hot]] static uint32_t inverseGamma32(uint32_t color);    // apply inverse Gamma to RGBW32 color\n    static void calcGammaTable(float gamma);                        // re-calculates & fills gamma tables\n    static inline uint8_t rawGamma8(uint8_t val) { return gammaT[val]; }  // get value from Gamma table (WLED specific, not used by NPB)\n    static inline uint8_t rawInverseGamma8(uint8_t val) { return gammaT_inv[val]; }  // get value from inverse Gamma table (WLED specific, not used by NPB)\n    static inline uint32_t Correct32(uint32_t color) { // apply Gamma to RGBW32 color (WLED specific, not used by NPB)\n      if (!gammaCorrectCol) return color; // no gamma correction\n      uint8_t  w = byte(color>>24), r = byte(color>>16), g = byte(color>>8), b = byte(color); // extract r, g, b, w channels\n      w = gammaT[w]; r = gammaT[r]; g = gammaT[g]; b = gammaT[b];\n      return (uint32_t(w) << 24) | (uint32_t(r) << 16) | (uint32_t(g) << 8) | uint32_t(b);\n    }\n  private:\n    static uint8_t gammaT[];\n    static uint8_t gammaT_inv[];\n};\n#define gamma32(c) NeoGammaWLEDMethod::Correct32(c)\n#define gamma8(c)  NeoGammaWLEDMethod::rawGamma8(c)\n#define gamma32inv(c) NeoGammaWLEDMethod::inverseGamma32(c)\n#define gamma8inv(c)  NeoGammaWLEDMethod::rawInverseGamma8(c)\n[[gnu::hot, gnu::pure]] uint32_t color_blend(uint32_t c1, uint32_t c2 , uint8_t blend);\ninline uint32_t color_blend16(uint32_t c1, uint32_t c2, uint16_t b) { return color_blend(c1, c2, b >> 8); };\n[[gnu::hot, gnu::pure]] uint32_t color_add(uint32_t, uint32_t, bool preserveCR = false);\n[[gnu::hot, gnu::pure]] uint32_t color_fade(uint32_t c1, uint8_t amount, bool video = false);\n[[gnu::hot, gnu::pure]] uint32_t adjust_color(uint32_t rgb, uint32_t hueShift, uint32_t lighten, uint32_t brighten);\n[[gnu::hot, gnu::pure]] uint32_t ColorFromPaletteWLED(const CRGBPalette16 &pal, unsigned index, uint8_t brightness = (uint8_t)255U, TBlendType blendType = LINEARBLEND);\nCRGBPalette16 generateHarmonicRandomPalette(const CRGBPalette16 &basepalette);\nCRGBPalette16 generateRandomPalette();\nvoid loadCustomPalettes();\nextern std::vector<CRGBPalette16> customPalettes;\ninline size_t getPaletteCount() { return FIXED_PALETTE_COUNT + customPalettes.size(); }\ninline uint32_t colorFromRgbw(byte* rgbw) { return uint32_t((byte(rgbw[3]) << 24) | (byte(rgbw[0]) << 16) | (byte(rgbw[1]) << 8) | (byte(rgbw[2]))); }\nvoid hsv2rgb(const CHSV32& hsv, uint32_t& rgb);\nvoid colorHStoRGB(uint16_t hue, byte sat, byte* rgb);\nvoid rgb2hsv(const uint32_t rgb, CHSV32& hsv);\ninline 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\nvoid colorKtoRGB(uint16_t kelvin, byte* rgb);\nvoid colorCTtoRGB(uint16_t mired, byte* rgb); //white spectrum to rgb\nvoid colorXYtoRGB(float x, float y, byte* rgb); // only defined if huesync disabled TODO\nvoid colorRGBtoXY(const byte* rgb, float* xy); // only defined if huesync disabled TODO\nvoid colorFromDecOrHexString(byte* rgb, const char* in);\nbool colorFromHexString(byte* rgb, const char* in);\nuint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb);\nuint16_t approximateKelvinFromRGB(uint32_t rgb);\nvoid setRandomColor(byte* rgb);\n\n// fast scaling function for colors, performs color*scale/256 for all four channels, speed over accuracy\n// note: inlining uses less code than actual function calls\nstatic inline uint32_t fast_color_scale(const uint32_t c, const uint8_t scale) {\n  uint32_t rb = (((c     & 0x00FF00FF) * scale) >> 8) &  0x00FF00FF;\n  uint32_t wg = (((c>>8) & 0x00FF00FF) * scale)       & ~0x00FF00FF;\n  return rb | wg;\n}\n\n// palettes\nextern const TProgmemRGBPalette16* const fastledPalettes[];\nextern const uint8_t* const gGradientPalettes[];\n#endif\n\n"
  },
  {
    "path": "wled00/const.h",
    "content": "#pragma once\n#ifndef WLED_CONST_H\n#define WLED_CONST_H\n\n/*\n * Readability defines and their associated numerical values + compile-time constants\n */\n\nconstexpr size_t FASTLED_PALETTE_COUNT = 7;   //  6-12 = sizeof(fastledPalettes) / sizeof(fastledPalettes[0]);\nconstexpr size_t GRADIENT_PALETTE_COUNT = 59; // 13-72 = sizeof(gGradientPalettes) / sizeof(gGradientPalettes[0]);\nconstexpr 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)\nconstexpr size_t FIXED_PALETTE_COUNT = DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT + GRADIENT_PALETTE_COUNT; // total number of fixed palettes\n#ifndef ESP8266\n  #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\n#else\n  #define WLED_MAX_CUSTOM_PALETTES 10 // ESP8266: limit custom palettes to 10\n#endif\n#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)\n\n// You can define custom product info from build flags.\n// This is useful to allow API consumer to identify what type of WLED version\n// they are interacting with. Be aware that changing this might cause some third\n// party API consumers to consider this as a non-WLED device since the values\n// returned by the API and by MQTT will no longer be default. However, most\n// third party only uses mDNS to validate, so this is generally fine to change.\n// For example, Home Assistant will still work fine even with this value changed.\n// Use like this:\n// -D WLED_BRAND=\"\\\"Custom Brand\\\"\"\n// -D WLED_PRODUCT_NAME=\"\\\"Custom Product\\\"\"\n#ifndef WLED_BRAND\n  #define WLED_BRAND \"WLED\"\n#endif\n#ifndef WLED_PRODUCT_NAME\n  #define WLED_PRODUCT_NAME \"FOSS\"\n#endif\n\n//Defaults\n#define DEFAULT_CLIENT_SSID \"Your_Network\"\n#define DEFAULT_AP_SSID     WLED_BRAND \"-AP\"\n#define DEFAULT_AP_PASS     \"wled1234\"\n#define DEFAULT_OTA_PASS    \"wledota\"\n#define DEFAULT_MDNS_NAME   \"x\"\n\n//increase if you need more\n#ifndef WLED_MAX_WIFI_COUNT\n  #define WLED_MAX_WIFI_COUNT 3\n#endif\n\n#ifndef WLED_MAX_USERMODS\n  #if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32S2)\n    #define WLED_MAX_USERMODS 4\n  #else\n    #define WLED_MAX_USERMODS 6\n  #endif\n#endif\n\n#ifdef ESP8266\n  #define WLED_MAX_DIGITAL_CHANNELS 3\n  #define WLED_MAX_RMT_CHANNELS 0           // ESP8266 does not have RMT nor I2S\n  #define WLED_MAX_I2S_CHANNELS 0\n  #define WLED_MAX_ANALOG_CHANNELS 5\n  #define WLED_PLATFORM_ID 0         // used in UI to distinguish ESP types, needs a proper fix!\n#else\n  #if !defined(LEDC_CHANNEL_MAX) || !defined(LEDC_SPEED_MODE_MAX)\n    #include \"driver/ledc.h\" // needed for analog/LEDC channel counts\n  #endif\n  #define WLED_MAX_ANALOG_CHANNELS (LEDC_CHANNEL_MAX*LEDC_SPEED_MODE_MAX)\n  #if defined(CONFIG_IDF_TARGET_ESP32C3)    // 2 RMT, 6 LEDC, only has 1 I2S but NPB does not support it ATM\n    #define WLED_MAX_RMT_CHANNELS 2         // ESP32-C3 has 2 RMT output channels\n    #define WLED_MAX_I2S_CHANNELS 0         // I2S not supported by NPB\n    //#define WLED_MAX_ANALOG_CHANNELS 6\n    #define WLED_PLATFORM_ID 1       // used in UI to distinguish ESP types, needs a proper fix!\n  #elif defined(CONFIG_IDF_TARGET_ESP32S2)  // 4 RMT, 8 LEDC, only has 1 I2S bus, supported in NPB\n    #define WLED_MAX_RMT_CHANNELS 4         // ESP32-S2 has 4 RMT output channels\n    #define WLED_MAX_I2S_CHANNELS 8         // I2S parallel output supported by NPB\n    //#define WLED_MAX_ANALOG_CHANNELS 8\n    #define WLED_PLATFORM_ID 2       // used in UI to distinguish ESP type in UI\n  #elif defined(CONFIG_IDF_TARGET_ESP32S3)  // 4 RMT, 8 LEDC, has 2 I2S but NPB supports parallel x8 LCD on I2S1\n    #define WLED_MAX_RMT_CHANNELS 4         // ESP32-S3 has 4 RMT output channels\n    #define WLED_MAX_I2S_CHANNELS 8         // uses LCD parallel output not I2S\n    //#define WLED_MAX_ANALOG_CHANNELS 8\n    #define WLED_PLATFORM_ID 3       // used in UI to distinguish ESP type in UI, needs a proper fix!\n  #else\n    #define WLED_MAX_RMT_CHANNELS 8         // ESP32 has 8 RMT output channels\n    #define WLED_MAX_I2S_CHANNELS 8         // I2S parallel output supported by NPB\n    //#define WLED_MAX_ANALOG_CHANNELS 16\n    #define WLED_PLATFORM_ID 4       // used in UI to distinguish ESP type in UI, needs a proper fix!\n  #endif\n  #define WLED_MAX_DIGITAL_CHANNELS (WLED_MAX_RMT_CHANNELS + WLED_MAX_I2S_CHANNELS)\n#endif\n// WLED_MAX_BUSSES was used to define the size of busses[] array which is no longer needed\n// instead it will help determine max number of buses that can be defined at compile time\n#ifdef WLED_MAX_BUSSES\n  #undef WLED_MAX_BUSSES\n#endif\n#define WLED_MAX_BUSSES (WLED_MAX_DIGITAL_CHANNELS+WLED_MAX_ANALOG_CHANNELS)\nstatic_assert(WLED_MAX_BUSSES <= 32, \"WLED_MAX_BUSSES exceeds hard limit\");\n\n// Maximum number of pins per output. 5 for RGBCCT analog LEDs.\n#define OUTPUT_MAX_PINS 5\n\n// for pin manager\n#ifdef ESP8266\n#define WLED_NUM_PINS (GPIO_PIN_COUNT+1) // somehow they forgot GPIO 16 (0-16==17)\n#else\n#define WLED_NUM_PINS (GPIO_PIN_COUNT)\n#endif\n\n#ifndef WLED_MAX_BUTTONS\n  #ifdef ESP8266\n    #define WLED_MAX_BUTTONS 10\n  #else\n    #define WLED_MAX_BUTTONS 32\n  #endif\n#else\n  #if WLED_MAX_BUTTONS < 2\n    #undef WLED_MAX_BUTTONS\n    #define WLED_MAX_BUTTONS 2\n  #endif\n#endif\n\n#define RELAY_DELAY 50 // delay in ms between switching on relay and sending data to LEDs\n\n#if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32S2)\n#define WLED_MAX_COLOR_ORDER_MAPPINGS 5\n#else\n#define WLED_MAX_COLOR_ORDER_MAPPINGS 10\n#endif\n\n#if defined(WLED_MAX_LEDMAPS) && (WLED_MAX_LEDMAPS > 32 || WLED_MAX_LEDMAPS < 10)\n  #undef WLED_MAX_LEDMAPS\n#endif\n#ifndef WLED_MAX_LEDMAPS\n  #if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32S2)\n    #define WLED_MAX_LEDMAPS 10\n  #else\n    #define WLED_MAX_LEDMAPS 16\n  #endif\n#endif\n\n#ifndef WLED_MAX_SEGNAME_LEN\n  #ifdef ESP8266\n    #define WLED_MAX_SEGNAME_LEN 32\n  #else\n    #define WLED_MAX_SEGNAME_LEN 64\n  #endif\n#else\n  #if WLED_MAX_SEGNAME_LEN<32\n    #undef WLED_MAX_SEGNAME_LEN\n    #define WLED_MAX_SEGNAME_LEN 32\n  #else\n    #warning WLED UI does not support modified maximum segment name length!\n  #endif\n#endif\n\n#define WLED_MAX_PANELS 18                      // must not be more than 32\n\n//Usermod IDs\n#define USERMOD_ID_RESERVED               0     //Unused. Might indicate no usermod present\n#define USERMOD_ID_UNSPECIFIED            1     //Default value for a general user mod that does not specify a custom ID\n#define USERMOD_ID_EXAMPLE                2     //Usermod \"usermod_v2_example.h\"\n#define USERMOD_ID_TEMPERATURE            3     //Usermod \"usermod_temperature.h\"\n#define USERMOD_ID_FIXNETSERVICES         4     //Usermod \"usermod_Fix_unreachable_netservices.h\"\n#define USERMOD_ID_PIRSWITCH              5     //Usermod \"usermod_PIR_sensor_switch.h\"\n#define USERMOD_ID_IMU                    6     //Usermod \"usermod_mpu6050_imu.h\"\n#define USERMOD_ID_FOUR_LINE_DISP         7     //Usermod \"usermod_v2_four_line_display.h\n#define USERMOD_ID_ROTARY_ENC_UI          8     //Usermod \"usermod_v2_rotary_encoder_ui.h\"\n#define USERMOD_ID_AUTO_SAVE              9     //Usermod \"usermod_v2_auto_save.h\"\n#define USERMOD_ID_DHT                   10     //Usermod \"usermod_dht.h\"\n#define USERMOD_ID_MODE_SORT             11     //Usermod \"usermod_v2_mode_sort.h\"\n#define USERMOD_ID_VL53L0X               12     //Usermod \"usermod_vl53l0x_gestures.h\"\n#define USERMOD_ID_MULTI_RELAY           13     //Usermod \"usermod_multi_relay.h\"\n#define USERMOD_ID_ANIMATED_STAIRCASE    14     //Usermod \"Animated_Staircase.h\"\n#define USERMOD_ID_RTC                   15     //Usermod \"usermod_rtc.h\"\n#define USERMOD_ID_ELEKSTUBE_IPS         16     //Usermod \"usermod_elekstube_ips.h\"\n#define USERMOD_ID_SN_PHOTORESISTOR      17     //Usermod \"usermod_sn_photoresistor.h\"\n#define USERMOD_ID_BATTERY               18     //Usermod \"usermod_v2_battery.h\"\n#define USERMOD_ID_PWM_FAN               19     //Usermod \"usermod_PWM_fan.h\"\n#define USERMOD_ID_BH1750                20     //Usermod \"usermod_bh1750.h\"\n#define USERMOD_ID_SEVEN_SEGMENT_DISPLAY 21     //Usermod \"usermod_v2_seven_segment_display.h\"\n#define USERMOD_RGB_ROTARY_ENCODER       22     //Usermod \"rgb-rotary-encoder.h\"\n#define USERMOD_ID_QUINLED_AN_PENTA      23     //Usermod \"quinled-an-penta.h\"\n#define USERMOD_ID_SSDR                  24     //Usermod \"usermod_v2_seven_segment_display_reloaded.h\"\n#define USERMOD_ID_CRONIXIE              25     //Usermod \"usermod_cronixie.h\"\n#define USERMOD_ID_WIZLIGHTS             26     //Usermod \"wizlights.h\"\n#define USERMOD_ID_WORDCLOCK             27     //Usermod \"usermod_v2_word_clock.h\"\n#define USERMOD_ID_MY9291                28     //Usermod \"usermod_MY9291.h\"\n#define USERMOD_ID_SI7021_MQTT_HA        29     //Usermod \"usermod_si7021_mqtt_ha.h\"\n#define USERMOD_ID_BME280                30     //Usermod \"usermod_bme280.h\n#define USERMOD_ID_SMARTNEST             31     //Usermod \"usermod_smartnest.h\"\n#define USERMOD_ID_AUDIOREACTIVE         32     //Usermod \"audioreactive.h\"\n#define USERMOD_ID_ANALOG_CLOCK          33     //Usermod \"Analog_Clock.h\"\n#define USERMOD_ID_PING_PONG_CLOCK       34     //Usermod \"usermod_v2_ping_pong_clock.h\"\n#define USERMOD_ID_ADS1115               35     //Usermod \"usermod_ads1115.h\"\n#define USERMOD_ID_BOBLIGHT              36     //Usermod \"boblight.h\"\n#define USERMOD_ID_SD_CARD               37     //Usermod \"usermod_sd_card.h\"\n#define USERMOD_ID_PWM_OUTPUTS           38     //Usermod \"usermod_pwm_outputs.h\n#define USERMOD_ID_SHT                   39     //Usermod \"usermod_sht.h\n#define USERMOD_ID_KLIPPER               40     //Usermod Klipper percentage\n#define USERMOD_ID_WIREGUARD             41     //Usermod \"wireguard.h\"\n#define USERMOD_ID_INTERNAL_TEMPERATURE  42     //Usermod \"usermod_internal_temperature.h\"\n#define USERMOD_ID_LDR_DUSK_DAWN         43     //Usermod \"usermod_LDR_Dusk_Dawn_v2.h\"\n#define USERMOD_ID_STAIRWAY_WIPE         44     //Usermod \"stairway-wipe-usermod-v2.h\"\n#define USERMOD_ID_ANIMARTRIX            45     //Usermod \"usermod_v2_animartrix.h\"\n#define USERMOD_ID_HTTP_PULL_LIGHT_CONTROL 46   //usermod \"usermod_v2_HttpPullLightControl.h\"\n#define USERMOD_ID_TETRISAI              47     //Usermod \"usermod_v2_tetris.h\"\n#define USERMOD_ID_MAX17048              48     //Usermod \"usermod_max17048.h\"\n#define USERMOD_ID_BME68X                49     //Usermod \"usermod_bme68x.h\n#define USERMOD_ID_INA226                50     //Usermod \"usermod_ina226.h\"\n#define USERMOD_ID_AHT10                 51     //Usermod \"usermod_aht10.h\"\n#define USERMOD_ID_LD2410                52     //Usermod \"usermod_ld2410.h\"\n#define USERMOD_ID_POV_DISPLAY           53     //Usermod \"usermod_pov_display.h\"\n#define USERMOD_ID_PIXELS_DICE_TRAY      54     //Usermod \"pixels_dice_tray.h\"\n#define USERMOD_ID_DEEP_SLEEP            55     //Usermod \"usermod_deep_sleep.h\"\n#define USERMOD_ID_RF433                 56     //Usermod \"usermod_v2_RF433.h\"\n#define USERMOD_ID_BRIGHTNESS_FOLLOW_SUN 57     //Usermod \"usermod_v2_brightness_follow_sun.h\"\n#define USERMOD_ID_USER_FX               58     //Usermod \"user_fx\"\n\n//Wifi encryption type\n#ifdef WLED_ENABLE_WPA_ENTERPRISE\n  #define WIFI_ENCRYPTION_TYPE_PSK          0     //None/WPA/WPA2\n  #define WIFI_ENCRYPTION_TYPE_ENTERPRISE   1     //WPA/WPA2-Enterprise\n#endif\n\n//Access point behavior\n#define AP_BEHAVIOR_BOOT_NO_CONN          0     //Open AP when no connection after boot\n#define AP_BEHAVIOR_NO_CONN               1     //Open when no connection (either after boot or if connection is lost)\n#define AP_BEHAVIOR_ALWAYS                2     //Always open\n#define AP_BEHAVIOR_BUTTON_ONLY           3     //Only when button pressed for 6 sec\n#define AP_BEHAVIOR_TEMPORARY             4     //Open AP when no connection after boot but only temporary\n#ifndef WLED_AP_TIMEOUT\n  #define WLED_AP_TIMEOUT            300000     //Temporary AP timeout\n#endif\n\n//Notifier callMode\n#define CALL_MODE_INIT           0     //no updates on init, can be used to disable updates\n#define CALL_MODE_DIRECT_CHANGE  1\n#define CALL_MODE_BUTTON         2     //default button actions applied to selected segments\n#define CALL_MODE_NOTIFICATION   3     //caused by incoming notification (UDP or DMX preset)\n#define CALL_MODE_NIGHTLIGHT     4     //nightlight progress\n#define CALL_MODE_NO_NOTIFY      5     //change state but do not send notifications (UDP)\n#define CALL_MODE_FX_CHANGED     6     //no longer used\n#define CALL_MODE_HUE            7\n#define CALL_MODE_PRESET_CYCLE   8     //no longer used\n#define CALL_MODE_BLYNK          9     //no longer used\n#define CALL_MODE_ALEXA         10\n#define CALL_MODE_WS_SEND       11     //special call mode, not for notifier, updates websocket only\n#define CALL_MODE_BUTTON_PRESET 12     //button/IR JSON preset/macro\n\n//RGB to RGBW conversion mode\n#define RGBW_MODE_MANUAL_ONLY     0    // No automatic white channel calculation. Manual white channel slider\n#define RGBW_MODE_AUTO_BRIGHTER   1    // New algorithm. Adds as much white as the darkest RGBW channel\n#define RGBW_MODE_AUTO_ACCURATE   2    // New algorithm. Adds as much white as the darkest RGBW channel and subtracts this amount from each RGB channel\n#define RGBW_MODE_DUAL            3    // Manual slider + auto calculation. Automatically calculates only if manual slider is set to off (0)\n#define RGBW_MODE_MAX             4    // Sets white to the value of the brightest RGB channel (good for white-only LEDs without any RGB)\n//#define RGBW_MODE_LEGACY        4    // Old floating algorithm. Too slow for realtime and palette support (unused)\n#define AW_GLOBAL_DISABLED      255    // Global auto white mode override disabled. Per-bus setting is used\n\n//realtime modes\n#define REALTIME_MODE_INACTIVE    0\n#define REALTIME_MODE_GENERIC     1\n#define REALTIME_MODE_UDP         2\n#define REALTIME_MODE_HYPERION    3\n#define REALTIME_MODE_E131        4\n#define REALTIME_MODE_ADALIGHT    5\n#define REALTIME_MODE_ARTNET      6\n#define REALTIME_MODE_TPM2NET     7\n#define REALTIME_MODE_DDP         8\n#define REALTIME_MODE_DMX         9\n\n//realtime override modes\n#define REALTIME_OVERRIDE_NONE    0\n#define REALTIME_OVERRIDE_ONCE    1\n#define REALTIME_OVERRIDE_ALWAYS  2\n\n//E1.31 DMX modes\n#define DMX_MODE_DISABLED         0            //not used\n#define DMX_MODE_SINGLE_RGB       1            //all LEDs same RGB color (3 channels)\n#define DMX_MODE_SINGLE_DRGB      2            //all LEDs same RGB color and master dimmer (4 channels)\n#define DMX_MODE_EFFECT           3            //trigger standalone effects of WLED (15 channels)\n#define DMX_MODE_EFFECT_W         7            //trigger standalone effects of WLED (18 channels)\n#define DMX_MODE_MULTIPLE_RGB     4            //every LED is addressed with its own RGB (ledCount * 3 channels)\n#define DMX_MODE_MULTIPLE_DRGB    5            //every LED is addressed with its own RGB and share a master dimmer (ledCount * 3 + 1 channels)\n#define DMX_MODE_MULTIPLE_RGBW    6            //every LED is addressed with its own RGBW (ledCount * 4 channels)\n#define DMX_MODE_EFFECT_SEGMENT   8            //trigger standalone effects of WLED (15 channels per segment)\n#define DMX_MODE_EFFECT_SEGMENT_W 9            //trigger standalone effects of WLED (18 channels per segment)\n#define DMX_MODE_PRESET           10           //apply presets (1 channel)\n\n//Light capability byte (unused) 0bRCCCTTTT\n//bits 0/1/2/3: specifies a type of LED driver. A single \"driver\" may have different chip models but must have the same protocol/behavior\n//bits 4/5/6: specifies the class of LED driver - 0b000 (dec. 0-15)  unconfigured/reserved\n//                                              - 0b001 (dec. 16-31) digital (data pin only)\n//                                              - 0b010 (dec. 32-47) analog (PWM)\n//                                              - 0b011 (dec. 48-63) digital (data + clock / SPI)\n//                                              - 0b100 (dec. 64-79) unused/reserved\n//                                              - 0b101 (dec. 80-95) virtual network busses\n//                                              - 0b110 (dec. 96-111) unused/reserved\n//                                              - 0b111 (dec. 112-127) unused/reserved\n//bit 7 is reserved and set to 0\n\n#define TYPE_NONE                 0            //light is not configured\n#define TYPE_RESERVED             1            //unused. Might indicate a \"virtual\" light\n//Digital types (data pin only) (16-39)\n#define TYPE_DIGITAL_MIN         16            // first usable digital type\n#define TYPE_WS2812_1CH          18            //white-only chips (1 channel per IC) (unused)\n#define TYPE_WS2812_1CH_X3       19            //white-only chips (3 channels per IC)\n#define TYPE_WS2812_2CH_X3       20            //CCT chips (1st IC controls WW + CW of 1st zone and CW of 2nd zone, 2nd IC controls WW of 2nd zone and WW + CW of 3rd zone)\n#define TYPE_WS2812_WWA          21            //amber + warm + cold white\n#define TYPE_WS2812_RGB          22\n#define TYPE_GS8608              23            //same driver as WS2812, but will require signal 2x per second (else displays test pattern)\n#define TYPE_WS2811_400KHZ       24            //half-speed WS2812 protocol, used by very old WS2811 units\n#define TYPE_TM1829              25\n#define TYPE_UCS8903             26\n#define TYPE_APA106              27\n#define TYPE_FW1906              28            //RGB + CW + WW + unused channel (6 channels per IC)\n#define TYPE_UCS8904             29            //first RGBW digital type (hardcoded in busmanager.cpp)\n#define TYPE_SK6812_RGBW         30\n#define TYPE_TM1814              31\n#define TYPE_WS2805              32            //RGB + WW + CW\n#define TYPE_TM1914              33            //RGB\n#define TYPE_SM16825             34            //RGB + WW + CW\n#define TYPE_DIGITAL_MAX         39            // last usable digital type\n//\"Analog\" types (40-47)\n#define TYPE_ONOFF               40            //binary output (relays etc.; NOT PWM)\n#define TYPE_ANALOG_MIN          41            // first usable analog type\n#define TYPE_ANALOG_1CH          41            //single channel PWM. Uses value of brightest RGBW channel\n#define TYPE_ANALOG_2CH          42            //analog WW + CW\n#define TYPE_ANALOG_3CH          43            //analog RGB\n#define TYPE_ANALOG_4CH          44            //analog RGBW\n#define TYPE_ANALOG_5CH          45            //analog RGB + WW + CW\n#define TYPE_ANALOG_6CH          46            //analog RGB + A + WW + CW\n#define TYPE_ANALOG_MAX          47            // last usable analog type\n//Digital types (data + clock / SPI) (48-63)\n#define TYPE_2PIN_MIN            48\n#define TYPE_WS2801              50\n#define TYPE_APA102              51\n#define TYPE_LPD8806             52\n#define TYPE_P9813               53\n#define TYPE_LPD6803             54\n#define TYPE_2PIN_MAX            63\n\n#define TYPE_HUB75MATRIX_MIN     64\n#define TYPE_HUB75MATRIX_HS      65\n#define TYPE_HUB75MATRIX_QS      66\n#define TYPE_HUB75MATRIX_MAX     71\n\n//Network types (master broadcast) (80-95)\n#define TYPE_VIRTUAL_MIN         80\n#define TYPE_NET_DDP_RGB         80            //network DDP RGB bus (master broadcast bus)\n#define TYPE_NET_E131_RGB        81            //network E131 RGB bus (master broadcast bus, unused)\n#define TYPE_NET_ARTNET_RGB      82            //network ArtNet RGB bus (master broadcast bus, unused)\n#define TYPE_NET_DDP_RGBW        88            //network DDP RGBW bus (master broadcast bus)\n#define TYPE_NET_ARTNET_RGBW     89            //network ArtNet RGB bus (master broadcast bus, unused)\n#define TYPE_VIRTUAL_MAX         95\n\n//Color orders\n#define COL_ORDER_GRB             0           //GRB(w),defaut\n#define COL_ORDER_RGB             1           //common for WS2811\n#define COL_ORDER_BRG             2\n#define COL_ORDER_RBG             3\n#define COL_ORDER_BGR             4\n#define COL_ORDER_GBR             5\n#define COL_ORDER_MAX             5\n\n//ESP-NOW\n#define ESP_NOW_STATE_UNINIT       0\n#define ESP_NOW_STATE_ON           1\n#define ESP_NOW_STATE_ERROR        2\n\n//Button type\n#define BTN_TYPE_NONE             0\n#define BTN_TYPE_RESERVED         1\n#define BTN_TYPE_PUSH             2\n#define BTN_TYPE_PUSH_ACT_HIGH    3\n#define BTN_TYPE_SWITCH           4\n#define BTN_TYPE_PIR_SENSOR       5\n#define BTN_TYPE_TOUCH            6\n#define BTN_TYPE_ANALOG           7\n#define BTN_TYPE_ANALOG_INVERTED  8\n#define BTN_TYPE_TOUCH_SWITCH     9\n\n//Ethernet board types\n#define WLED_NUM_ETH_TYPES        14\n\n\n#define WLED_ETH_NONE              0\n#define WLED_ETH_WT32_ETH01        1\n#define WLED_ETH_ESP32_POE         2\n#define WLED_ETH_WESP32            3\n#define WLED_ETH_QUINLED           4\n#define WLED_ETH_TWILIGHTLORD      5\n#define WLED_ETH_ESP32DEUX         6\n#define WLED_ETH_ESP32ETHKITVE     7\n#define WLED_ETH_QUINLED_OCTA      8\n#define WLED_ETH_ABCWLEDV43ETH     9\n#define WLED_ETH_SERG74           10\n#define WLED_ETH_ESP32_POE_WROVER 11\n#define WLED_ETH_LILYGO_T_POE_PRO 12\n#define WLED_ETH_GLEDOPTO         13\n\n//Hue error codes\n#define HUE_ERROR_INACTIVE        0\n#define HUE_ERROR_UNAUTHORIZED    1\n#define HUE_ERROR_LIGHTID         3\n#define HUE_ERROR_PUSHLINK      101\n#define HUE_ERROR_JSON_PARSING  250\n#define HUE_ERROR_TIMEOUT       251\n#define HUE_ERROR_ACTIVE        255\n\n//Segment option byte bits\n#define SEG_OPTION_SELECTED       0\n#define SEG_OPTION_REVERSED       1\n#define SEG_OPTION_ON             2\n#define SEG_OPTION_MIRROR         3            //Indicates that the effect will be mirrored within the segment\n#define SEG_OPTION_FREEZE         4            //Segment contents will not be refreshed\n#define SEG_OPTION_RESET          5            //Segment runtime requires reset\n#define SEG_OPTION_REVERSED_Y     6\n#define SEG_OPTION_MIRROR_Y       7\n#define SEG_OPTION_TRANSPOSED     8\n\n//Segment differs return byte\n#define SEG_DIFFERS_BRI        0x01 // opacity\n#define SEG_DIFFERS_OPT        0x02 // all segment options except: selected, reset & transitional\n#define SEG_DIFFERS_COL        0x04 // colors\n#define SEG_DIFFERS_FX         0x08 // effect/mode parameters\n#define SEG_DIFFERS_BOUNDS     0x10 // segment start/stop bounds\n#define SEG_DIFFERS_GSO        0x20 // grouping, spacing & offset\n#define SEG_DIFFERS_SEL        0x80 // selected\n\n//Playlist option byte\n#define PL_OPTION_SHUFFLE      0x01\n#define PL_OPTION_RESTORE      0x02\n\n// Segment capability byte\n#define SEG_CAPABILITY_RGB     0x01\n#define SEG_CAPABILITY_W       0x02\n#define SEG_CAPABILITY_CCT     0x04\n\n// WLED Error modes\n#define ERR_NONE         0  // All good :)\n#define ERR_DENIED       1  // Permission denied\n#define ERR_CONCURRENCY  2  // Conurrency (client active)\n#define ERR_NOBUF        3  // JSON buffer was not released in time, request cannot be handled at this time\n#define ERR_NOT_IMPL     4  // Not implemented\n#define ERR_NORAM_PX     7  // not enough RAM for pixels\n#define ERR_NORAM        8  // effect RAM depleted\n#define ERR_JSON         9  // JSON parsing failed (input too large?)\n#define ERR_FS_BEGIN    10  // Could not init filesystem (no partition?)\n#define ERR_FS_QUOTA    11  // The FS is full or the maximum file size is reached\n#define ERR_FS_PLOAD    12  // It was attempted to load a preset that does not exist\n#define ERR_FS_IRLOAD   13  // It was attempted to load an IR JSON cmd, but the \"ir.json\" file does not exist\n#define ERR_FS_RMLOAD   14  // It was attempted to load an remote JSON cmd, but the \"remote.json\" file does not exist\n#define ERR_FS_GENERAL  19  // A general unspecified filesystem error occurred\n#define ERR_OVERTEMP    30  // An attached temperature sensor has measured above threshold temperature (not implemented)\n#define ERR_OVERCURRENT 31  // An attached current sensor has measured a current above the threshold (not implemented)\n#define ERR_UNDERVOLT   32  // An attached voltmeter has measured a voltage below the threshold (not implemented)\n\n// JSON buffer lock owners\n#define JSON_LOCK_UNKNOWN        255\n#define JSON_LOCK_CFG_DES          1\n#define JSON_LOCK_CFG_SER          2\n#define JSON_LOCK_CFG_SEC_DES      3\n#define JSON_LOCK_CFG_SEC_SER      4\n#define JSON_LOCK_SETTINGS         5\n#define JSON_LOCK_XML              6\n#define JSON_LOCK_LEDMAP           7\n// unused                          8\n#define JSON_LOCK_PRESET_LOAD      9\n#define JSON_LOCK_PRESET_SAVE     10\n#define JSON_LOCK_WS_RECEIVE      11\n#define JSON_LOCK_WS_SEND         12\n#define JSON_LOCK_IR              13\n#define JSON_LOCK_SERVER          14\n#define JSON_LOCK_MQTT            15\n#define JSON_LOCK_SERIAL          16\n#define JSON_LOCK_SERVEJSON       17\n#define JSON_LOCK_NOTIFY          18\n#define JSON_LOCK_PRESET_NAME     19\n#define JSON_LOCK_LEDGAP          20\n#define JSON_LOCK_LEDMAP_ENUM     21\n#define JSON_LOCK_REMOTE          22\n\n// Timer mode types\n#define NL_MODE_SET               0            //After nightlight time elapsed, set to target brightness\n#define NL_MODE_FADE              1            //Fade to target brightness gradually\n#define NL_MODE_COLORFADE         2            //Fade to target brightness and secondary color gradually\n#define NL_MODE_SUN               3            //Sunrise/sunset. Target brightness is set immediately, then Sunrise effect is started. Max 60 min.\n\n// Settings sub page IDs\n#define SUBPAGE_MENU              0\n#define SUBPAGE_WIFI              1\n#define SUBPAGE_LEDS              2\n#define SUBPAGE_UI                3\n#define SUBPAGE_SYNC              4\n#define SUBPAGE_TIME              5\n#define SUBPAGE_SEC               6\n#define SUBPAGE_DMX               7\n#define SUBPAGE_UM                8\n#define SUBPAGE_UPDATE            9\n#define SUBPAGE_2D               10\n#define SUBPAGE_PINS             11\n#define SUBPAGE_LAST             SUBPAGE_PINS\n#define SUBPAGE_LOCK            251\n#define SUBPAGE_PINREQ          252\n#define SUBPAGE_CSS             253\n#define SUBPAGE_JS              254\n#define SUBPAGE_WELCOME         255\n\n#define NTP_PACKET_SIZE 48       // size of NTP receive buffer\n#define NTP_MIN_PACKET_SIZE 48   // min expected size - NTP v4 allows for \"extended information\" appended to the standard fields\n\n//maximum number of rendered LEDs - this does not have to match max. physical LEDs, e.g. if there are virtual busses\n#ifndef MAX_LEDS\n  #ifdef ESP8266\n    #define MAX_LEDS 1536 //can't rely on memory limit to limit this to 1536 LEDs\n  #elif defined(CONFIG_IDF_TARGET_ESP32S2)\n    #define MAX_LEDS 2048 //due to memory constraints S2\n  #else\n    #define MAX_LEDS 16384\n  #endif\n#endif\n\n// maximum total memory that can be used for bus-buffers and pixel buffers\n#ifndef MAX_LED_MEMORY\n  #ifdef ESP8266\n    #define MAX_LED_MEMORY (8*1024)\n  #else\n    #if defined(CONFIG_IDF_TARGET_ESP32S2)\n      #ifndef BOARD_HAS_PSRAM\n        #define MAX_LED_MEMORY (28*1024)  // S2 has ~170k of free heap after boot, using 28k is the absolute limit to keep WLED functional\n      #else\n        #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)\n      #endif\n    #elif defined(CONFIG_IDF_TARGET_ESP32S3)\n      #define MAX_LED_MEMORY (192*1024) // S3 has ~330k of free heap after boot\n    #elif defined(CONFIG_IDF_TARGET_ESP32C3)\n      #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\n    #else\n      #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\n    #endif\n  #endif\n#endif\n\n#ifndef MAX_LEDS_PER_BUS\n#define MAX_LEDS_PER_BUS 2048   // may not be enough for fast LEDs (i.e. APA102)\n#endif\n\n// string temp buffer (now stored in stack locally)\n#ifdef ESP8266\n#define SETTINGS_STACK_BUF_SIZE 2560\n#else\n#define SETTINGS_STACK_BUF_SIZE 3840  // warning: quite a large value for stack (640 * WLED_MAX_USERMODS)\n#endif\n\n#ifdef WLED_USE_ETHERNET\n  #define E131_MAX_UNIVERSE_COUNT 20\n#else\n  #ifdef ESP8266\n    #define E131_MAX_UNIVERSE_COUNT 9\n  #else\n    #define E131_MAX_UNIVERSE_COUNT 12\n  #endif\n#endif\n\n#ifndef ABL_MILLIAMPS_DEFAULT\n  #define ABL_MILLIAMPS_DEFAULT 850   // auto lower brightness to stay close to milliampere limit\n#else\n  #if ABL_MILLIAMPS_DEFAULT == 0      // disable ABL\n  #elif ABL_MILLIAMPS_DEFAULT < 250   // make sure value is at least 250\n   #warning \"make sure value is at least 250\"\n   #define ABL_MILLIAMPS_DEFAULT 250\n  #endif\n#endif\n\n#ifndef LED_MILLIAMPS_DEFAULT\n  #define LED_MILLIAMPS_DEFAULT 55    // common WS2812B\n#else\n  #if LED_MILLIAMPS_DEFAULT < 1 || LED_MILLIAMPS_DEFAULT > 100\n   #warning \"Unusual LED mA current, overriding with default value.\"\n   #undef LED_MILLIAMPS_DEFAULT\n   #define LED_MILLIAMPS_DEFAULT 55\n  #endif\n#endif\n\n// PWM settings\n#ifndef WLED_PWM_FREQ\n#ifdef ESP8266\n  #define WLED_PWM_FREQ    880 //PWM frequency proven as good for LEDs\n#else\n  #ifdef SOC_LEDC_SUPPORT_XTAL_CLOCK\n    #define WLED_PWM_FREQ 9765    // XTAL clock is 40MHz (this will allow 12 bit resolution)\n  #else\n    #define WLED_PWM_FREQ  19531  // APB clock is 80MHz\n  #endif\n#endif\n#endif\n\n#define TOUCH_THRESHOLD 32 // limit to recognize a touch, higher value means more sensitive\n\n// Size of buffer for API JSON object (increase for more segments)\n#ifdef ESP8266\n  #define JSON_BUFFER_SIZE 10240\n#else\n  #if defined(CONFIG_IDF_TARGET_ESP32S2)\n    #define JSON_BUFFER_SIZE 24576\n  #else\n    #define JSON_BUFFER_SIZE 32767\n  #endif\n#endif\n\n// minimum heap size required to process web requests: try to keep free heap above this value\n#ifdef ESP8266\n  #define MIN_HEAP_SIZE (9*1024)\n#else\n  #define MIN_HEAP_SIZE (15*1024) // WLED allocation functions (util.cpp) try to keep this much contiguous heap free for other tasks\n#endif\n// threshold for PSRAM use: if heap is running low, requests to allocate_buffer(prefer DRAM) above PSRAM_THRESHOLD may be put in PSRAM\n// if heap is depleted, PSRAM will be used regardless of threshold\n#if defined(CONFIG_IDF_TARGET_ESP32S3)\n  #define PSRAM_THRESHOLD (12*1024) // S3 has plenty of DRAM\n#elif defined(CONFIG_IDF_TARGET_ESP32)\n  #define PSRAM_THRESHOLD (5*1024)\n#else\n  #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\n#endif\n\n// Web server limits\n#ifdef ESP8266\n// Minimum heap to consider handling a request\n#define WLED_REQUEST_MIN_HEAP (8*1024)\n// Estimated maximum heap required by any one request\n#define WLED_REQUEST_HEAP_USAGE (6*1024)\n#else\n// ESP32 TCP stack needs much more RAM than ESP8266\n// Minimum heap remaining before queuing a request\n#define WLED_REQUEST_MIN_HEAP (12*1024)\n// Estimated maximum heap required by any one request\n#define WLED_REQUEST_HEAP_USAGE (12*1024)\n#endif\n// Maximum number of requests in queue; absolute cap on web server resource usage.\n// Websockets do not count against this limit.\n#define WLED_REQUEST_MAX_QUEUE 6\n\n// Maximum size of node map (list of other WLED instances)\n#ifdef ESP8266\n  #define WLED_MAX_NODES 24\n#else\n  #define WLED_MAX_NODES 150\n#endif\n\n// Defaults pins, type and counts to configure LED output\n#if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32C3)\n  #ifdef WLED_ENABLE_DMX\n    #define DEFAULT_LED_PIN 1\n    #warning \"Compiling with DMX. The default LED pin has been changed to pin 1.\"\n  #else\n    #define DEFAULT_LED_PIN 2    // GPIO2 (D4) on Wemos D1 mini compatible boards, safe to use on any board\n  #endif\n#else\n  #if defined(WLED_USE_ETHERNET)\n    #define DEFAULT_LED_PIN 4    // GPIO4 seems to be a \"safe bet\" for all known ethernet boards (issue #5155)\n    //#warning \"Compiling with Ethernet support. The default LED pin has been changed to pin 4.\"\n  #else\n    #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())\n  #endif\n#endif\n#define DEFAULT_LED_TYPE TYPE_WS2812_RGB\n#define DEFAULT_LED_COUNT 30\n\n#define INTERFACE_UPDATE_COOLDOWN 1000 // time in ms to wait between websockets, alexa, and MQTT updates\n\n#define PIN_RETRY_COOLDOWN   3000 // time in ms after an incorrect attempt PIN and OTA pass will be rejected even if correct\n#define PIN_TIMEOUT        900000 // time in ms after which the PIN will be required again, 15 minutes\n\n// HW_PIN_SCL & HW_PIN_SDA are used for information in usermods settings page and usermods themselves\n// which GPIO pins are actually used in a hardware layout (controller board)\n#if defined(I2CSCLPIN) && !defined(HW_PIN_SCL)\n  #define HW_PIN_SCL I2CSCLPIN\n#endif\n#if defined(I2CSDAPIN) && !defined(HW_PIN_SDA)\n  #define HW_PIN_SDA I2CSDAPIN\n#endif\n// you cannot change HW I2C pins on 8266\n#if defined(ESP8266) && defined(HW_PIN_SCL)\n  #undef HW_PIN_SCL\n#endif\n#if defined(ESP8266) && defined(HW_PIN_SDA)\n  #undef HW_PIN_SDA\n#endif\n// defaults for 1st I2C on ESP32 (Wire global)\n#ifndef HW_PIN_SCL\n  #define HW_PIN_SCL SCL\n#endif\n#ifndef HW_PIN_SDA\n  #define HW_PIN_SDA SDA\n#endif\n\n// HW_PIN_SCLKSPI & HW_PIN_MOSISPI & HW_PIN_MISOSPI are used for information in usermods settings page and usermods themselves\n// which GPIO pins are actually used in a hardware layout (controller board)\n#if defined(SPISCLKPIN) && !defined(HW_PIN_CLOCKSPI)\n  #define HW_PIN_CLOCKSPI SPISCLKPIN\n#endif\n#if defined(SPIMOSIPIN) && !defined(HW_PIN_MOSISPI)\n  #define HW_PIN_MOSISPI SPIMOSIPIN\n#endif\n#if defined(SPIMISOPIN) && !defined(HW_PIN_MISOSPI)\n  #define HW_PIN_MISOSPI SPIMISOPIN\n#endif\n// you cannot change HW SPI pins on 8266\n#if defined(ESP8266) && defined(HW_PIN_CLOCKSPI)\n  #undef HW_PIN_CLOCKSPI\n#endif\n#if defined(ESP8266) && defined(HW_PIN_DATASPI)\n  #undef HW_PIN_DATASPI\n#endif\n#if defined(ESP8266) && defined(HW_PIN_MISOSPI)\n  #undef HW_PIN_MISOSPI\n#endif\n// defaults for VSPI on ESP32 (SPI global, SPI.cpp) as HSPI is used by WLED (bus_wrapper.h)\n#ifndef HW_PIN_CLOCKSPI\n  #define HW_PIN_CLOCKSPI SCK\n#endif\n#ifndef HW_PIN_DATASPI\n  #define HW_PIN_DATASPI MOSI\n#endif\n#ifndef HW_PIN_MISOSPI\n  #define HW_PIN_MISOSPI MISO\n#endif\n\n// IRAM_ATTR for 8266 with 32Kb IRAM causes error: section `.text1' will not fit in region `iram1_0_seg'\n// this hack removes the IRAM flag for some 1D/2D functions - somewhat slower, but it solves problems with some older 8266 chips\n#ifdef WLED_SAVE_IRAM\n  #define IRAM_ATTR_YN\n#else\n  #define IRAM_ATTR_YN IRAM_ATTR\n#endif\n\n#define WLED_O2_ATTR __attribute__((optimize(\"O2\")))\n\n#endif\n"
  },
  {
    "path": "wled00/data/404.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<meta content='width=device-width' name='viewport'>\n\t\t<meta name=\"theme-color\" content=\"#222222\">\n\t\t<title>Not found</title>\n\t\t<style>\n\t\t\tbody {\n\t\t\t\tfont-family: Verdana, Helvetica, sans-serif;\n\t\t\t\ttext-align: center;\n\t\t\t\tbackground-color: #222;\n\t\t\t\tmargin: 0;\n\t\t\t\tcolor: #fff;\n\t\t\t}\n\n\t\t\timg {\n\t\t\t\twidth: 400px;\n\t\t\t\tmax-width: 50%;\n\t\t\t\timage-rendering: pixelated;\n\t\t\t\timage-rendering: crisp-edges;\n\t\t\t\tmargin: 25px 0 -10px 0;\n\t\t\t}\n\t\t\n\t\t\tbutton {\n\t\t\t\toutline: none;\n\t\t\t\tcursor: pointer;\n\t\t\t\tpadding: 8px;\n\t\t\t\tmargin: 10px;\n\t\t\t\twidth: 230px;\n\t\t\t\ttext-transform: uppercase;\n\t\t\t\tfont-family: helvetica;\n\t\t\t\tfont-size: 19px;\n\t\t\t\tbackground-color: #333;\n\t\t\t\tcolor: white;\n\t\t\t\tborder: 0px solid white;\n\t\t\t\tborder-radius: 25px;\n\t\t\t}\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<img alt=\"\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAbUExURQAAAAB81gCU/zKq/////9bW1oCAgGhoaAAAAGPLX8AAAAAJdFJOU///////////AFNPeBIAAAAJcEhZcwAADsAAAA7AAWrWiQkAAACdSURBVDhPxc9bDoUgEANQebP/FUuHMjBGY/B+3EYR7RH0qC/ZBc6HwCljgHO+xZIVSI2sYgHaG7EBWh8jWoxTrCBFdDJ+BD4lbIHxAcz8APAVLTsrZE4eQD5qzt3cAFTYokC4YCN9Gybgu4yAQtBFLQXHuHABA7JMeOEC/E0W5uy9gv4vo5QHK2i7yq2C8UABM4HmL+CSTXCTF1DrCX6+Gp9zB5dsAAAAAElFTkSuQmCC\">\n\t\t<h1>404 Not Found</h1>\n\t\t<b>Akemi does not know where you are headed...</b><br><br>\n\t\t<button onclick=\"window.location.href='../?sliders'\">Back to controls</button>\n\t</body>\n</html>"
  },
  {
    "path": "wled00/data/common.js",
    "content": "var d=document;\nvar loc = false, locip, locproto = \"http:\";\n\nfunction H(pg=\"\")   { window.open(\"https://kno.wled.ge/\"+pg); }\nfunction GH()       { window.open(\"https://github.com/wled-dev/WLED\"); }\nfunction gId(c)     { return d.getElementById(c); } // getElementById\nfunction cE(e)      { return d.createElement(e); } // createElement\nfunction gEBCN(c)   { return d.getElementsByClassName(c); } // getElementsByClassName\nfunction gN(s)      { return d.getElementsByName(s)[0]; } // getElementsByName\nfunction isE(o)     { return Object.keys(o).length === 0; } // isEmpty\nfunction isO(i)     { return (i && typeof i === 'object' && !Array.isArray(i)); } // isObject\nfunction isN(n)     { return !isNaN(parseFloat(n)) && isFinite(n); } // isNumber\n// https://stackoverflow.com/questions/3885817/how-do-i-check-that-a-number-is-float-or-integer\nfunction isF(n)     { return n === +n && n !== (n|0); } // isFloat\nfunction isI(n)     { return n === +n && n === (n|0); } // isInteger\nfunction toggle(el) { gId(el).classList.toggle(\"hide\"); let n = gId('No'+el); if (n) n.classList.toggle(\"hide\"); }\nfunction tooltip(cont=null) {\n\td.querySelectorAll((cont?cont+\" \":\"\")+\"[title]\").forEach((element)=>{\n\t\telement.addEventListener(\"pointerover\", ()=>{\n\t\t\t// save title\n\t\t\telement.setAttribute(\"data-title\", element.getAttribute(\"title\"));\n\t\t\tconst tooltip = d.createElement(\"span\");\n\t\t\ttooltip.className = \"tooltip\";\n\t\t\ttooltip.textContent = element.getAttribute(\"title\");\n\n\t\t\t// prevent default title popup\n\t\t\telement.removeAttribute(\"title\");\n\n\t\t\tlet { top, left, width } = element.getBoundingClientRect();\n\n\t\t\td.body.appendChild(tooltip);\n\n\t\t\tconst { offsetHeight, offsetWidth } = tooltip;\n\n\t\t\tconst offset = element.classList.contains(\"sliderwrap\") ? 4 : 10;\n\t\t\ttop -= offsetHeight + offset;\n\t\t\tleft += (width - offsetWidth) / 2;\n\n\t\t\ttooltip.style.top = top + \"px\";\n\t\t\ttooltip.style.left = left + \"px\";\n\t\t\ttooltip.classList.add(\"visible\");\n\t\t});\n\n\t\telement.addEventListener(\"pointerout\", ()=>{\n\t\t\td.querySelectorAll('.tooltip').forEach((tooltip)=>{\n\t\t\t\ttooltip.classList.remove(\"visible\");\n\t\t\t\td.body.removeChild(tooltip);\n\t\t\t});\n\t\t\t// restore title\n\t\t\telement.setAttribute(\"title\", element.getAttribute(\"data-title\"));\n\t\t});\n\t});\n};\n// sequential loading of external resources (JS or CSS) with retry, calls init() when done\nfunction loadResources(files, init) {\n\tlet i = 0;\n\tconst loadNext = () => {\n\t\tif (i >= files.length) {\n\t\t\tif (init) {\n\t\t\t\td.documentElement.style.visibility = 'visible'; // make page visible after all files are loaded if it was hidden (prevent ugly display)\n\t\t\t\td.readyState === 'complete' ? init() : window.addEventListener('load', init);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tconst file = files[i++];\n\t\tconst isCSS = file.endsWith('.css');\n\t\tconst el = d.createElement(isCSS ? 'link' : 'script');\n\t\tif (isCSS) {\n\t\t\tel.rel = 'stylesheet';\n\t\t\tel.href = file;\n\t\t\tconst st = d.head.querySelector('style');\n\t\t\tif (st) d.head.insertBefore(el, st); // insert before any <style> to allow overrides\n\t\t\telse d.head.appendChild(el);\n\t\t} else {\n\t\t\tel.src = file;\n\t\t\td.head.appendChild(el);\n\t\t}\n\t\tel.onload = () => {\tloadNext(); };\n\t\tel.onerror = () => {\n\t\t\ti--; // load this file again\n\t\t\tsetTimeout(loadNext, 100);\n\t\t};\n\t};\n\tloadNext();\n}\n// https://www.educative.io/edpresso/how-to-dynamically-load-a-js-file-in-javascript\nfunction loadJS(FILE_URL, async = true, preGetV = undefined, postGetV = undefined) {\n\tlet scE = d.createElement(\"script\");\n\tscE.setAttribute(\"src\", FILE_URL);\n\tscE.setAttribute(\"type\", \"text/javascript\");\n\tscE.setAttribute(\"async\", async);\n\td.body.appendChild(scE);\n\t// success event \n\tscE.addEventListener(\"load\", () => {\n\t\t//console.log(\"File loaded\");\n\t\tif (preGetV) preGetV();\n\t\tGetV();\n\t\tif (postGetV) postGetV();\n\t});\n\t// error event\n\tscE.addEventListener(\"error\", (ev) => {\n\t\tconsole.log(\"Error on loading file\", ev);\n\t\talert(\"Loading of configuration script failed.\\nIncomplete page data!\");\n\t});\n}\nfunction getLoc() {\n\tlet l = window.location;\n\tif (l.protocol == \"file:\") {\n\t\tloc = true;\n\t\tlocip = localStorage.getItem('locIp');\n\t\tif (!locip) {\n\t\t\tlocip = prompt(\"File Mode. Please enter WLED IP!\");\n\t\t\tlocalStorage.setItem('locIp', locip);\n\t\t}\n\t} else {\n\t\t// detect reverse proxy\n\t\tlet path = l.pathname;\n\t\tlet paths = path.slice(1,path.endsWith('/')?-1:undefined).split(\"/\");\n\t\tif (paths.length > 1) paths.pop(); // remove subpage (or \"settings\")\n\t\tif (paths.length > 0 && paths[paths.length-1]==\"settings\") paths.pop(); // remove \"settings\"\n\t\tif (paths.length > 1) {\n\t\t\tlocproto = l.protocol;\n\t\t\tloc = true;\n\t\t\tlocip = l.hostname + (l.port ? \":\" + l.port : \"\") + \"/\" + paths.join('/');\n\t\t}\n\t}\n}\nfunction getURL(path) { return (loc ? locproto + \"//\" + locip : \"\") + path; }\nfunction B()          { window.open(getURL(\"/settings\"),\"_self\"); }\nvar timeout;\nfunction showToast(text, error = false) {\n\tvar x = gId(\"toast\");\n\tif (!x) return;\n\tx.innerHTML = text;\n\tx.className = error ? \"error\":\"show\";\n\tclearTimeout(timeout);\n\tx.style.animation = 'none';\n\ttimeout = setTimeout(function(){ x.className = x.className.replace(\"show\", \"\"); }, 2900);\n}\nasync function uploadFile(fileObj, name, callback) {\n\tlet file = fileObj.files?.[0]; // get first file, \"?\"\" = optional chaining in case no file is selected\n  if (!file) { callback?.(false); return; }\n\tif (/\\.json$/i.test(name)) { // same as name.toLowerCase().endsWith('.json')\n    try {\n      const minified = JSON.stringify(JSON.parse(await file.text())); // validate and minify JSON\n      file = new Blob([minified], { type: file.type || \"application/json\" });\n    } catch (err) {\n      if (!confirm(\"JSON invalid. Continue?\")) { callback?.(false); return; }\n      // proceed with original file if invalid but user confirms\n    }\n  }\n\tvar req = new XMLHttpRequest();\n\treq.addEventListener('load', function(){showToast(this.responseText,this.status >= 400); if(callback) callback(this.status < 400);});\n\treq.addEventListener('error', function(e){showToast(\"Upload failed\",true); if(callback) callback(false);});\n\treq.open(\"POST\", \"/upload\");\n\tvar formData = new FormData();\n\tformData.append(\"data\", file, name);\n\treq.send(formData);\n\tfileObj.value = '';\n}\n// connect to WebSocket, use parent WS or open new, callback function gets passed the new WS object\nfunction connectWs(onOpen) {\n\tlet ws;\n\ttry {\tws = top.window.ws;} catch (e) {}\n\t// reuse if open\n\tif (ws && ws.readyState === WebSocket.OPEN) {\n\t\tif (onOpen) onOpen(ws);\n\t} else {\n\t\t// create new ws connection\n\t\tgetLoc(); // ensure globals are up to date\n\t\tlet url = loc ? getURL('/ws').replace(\"http\", \"ws\")\n\t\t\t\t\t\t\t\t\t: \"ws://\" + window.location.hostname + \"/ws\";\n\t\tws = new WebSocket(url);\n\t\tws.binaryType = \"arraybuffer\";\n\t\tif (onOpen) ws.onopen = () => onOpen(ws);\n\t}\n\treturn ws;\n}\n\n// send LED colors to ESP using WebSocket and DDP protocol (RGB)\n// ws: WebSocket object\n// start: start pixel index\n// len: number of pixels to send\n// colors: Uint8Array with RGB values (3*len bytes)\nfunction sendDDP(ws, start, len, colors) {\n\tif (!colors || colors.length < len * 3) return false; // not enough color data\n\tlet maxDDPpx = 472; // must fit into one WebSocket frame of 1428 bytes, DDP header is 10+1 bytes -> 472 RGB pixels\n\t//let maxDDPpx = 172; // ESP8266: must fit into one WebSocket frame of 528 bytes -> 172 RGB pixels TODO: add support for ESP8266?\n\tif (!ws || ws.readyState !== WebSocket.OPEN) return false;\n\t// send in chunks of maxDDPpx\n\tfor (let i = 0; i < len; i += maxDDPpx) {\n\t\tlet cnt = Math.min(maxDDPpx, len - i);\n\t\tlet off = (start + i) * 3; // DDP pixel offset in bytes\n\t\tlet dLen = cnt * 3;\n\t\tlet cOff = i * 3; // offset in color buffer\n\t\tlet pkt = new Uint8Array(11 + dLen); // DDP header is 10 bytes, plus 1 byte for WLED websocket protocol indicator\n\t\tpkt[0] = 0x02; // DDP protocol indicator for WLED websocket. Note: below DDP protocol bytes are offset by 1\n\t\tpkt[1] = 0x40; // flags: 0x40 = no push, 0x41 = push (i.e. render), note: this is DDP protocol byte 0\n\t\tpkt[2] = 0x00; // reserved\n\t\tpkt[3] = 0x01; // 1 = RGB (currently only supported mode)\n\t\tpkt[4] = 0x01; // destination id (not used but 0x01 is default output)\n\t\tpkt[5] = (off >> 24) & 255; // DDP protocol 4-7 is offset\n\t\tpkt[6] = (off >> 16) & 255;\n\t\tpkt[7] = (off >> 8) & 255;\n\t\tpkt[8] = off & 255;\n\t\tpkt[9] = (dLen >> 8) & 255; // DDP protocol 8-9 is data length\n\t\tpkt[10] = dLen & 255;\n\t\tpkt.set(colors.subarray(cOff, cOff + dLen), 11);\n\t\tif(i + cnt >= len) {\n\t\t\tpkt[1] = 0x41;  //if this is last packet, set the \"push\" flag to render the frame\n\t\t}\n\t\ttry {\n\t\t\tws.send(pkt.buffer);\n\t\t} catch (e) {\n\t\t\tconsole.error(e);\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n"
  },
  {
    "path": "wled00/data/cpal/cpal.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, minimum-scale=1\">\n\t<title>WLED Palette Editor</title>\n\t<!--* <link rel=\"stylesheet\" href=\"style.css\">\n\t<script src=\"common.js\"></script>\n\t<script src=\"iro.js\"></script> \n\t<link rel=\"icon\" href=\"data:,\"> *-->\n</head>\n<body>\n\t<div class=\"ctr\">\n\t\t<header><h1>WLED Palette Editor</h1></header>\n\n\t\t<div id=\"pickerWrap\">\n\t\t\t<div id=\"picker\"></div>\n\t\t\t<div class=\"rgbi\">\n\t\t\t\t<label>R</label><input type=\"number\" id=\"rInput\" min=\"0\" max=\"255\" value=\"0\">\n\t\t\t\t<label>G</label><input type=\"number\" id=\"gInput\" min=\"0\" max=\"255\" value=\"0\">\n\t\t\t\t<label>B</label><input type=\"number\" id=\"bInput\" min=\"0\" max=\"255\" value=\"0\">\n\t\t\t</div>\n\t\t</div>\n\n\t\t<div class=\"bar\">\n\t\t\t\t<div class=\"bar\">\n\t\t\t\t\t\t<button id=\"btnNew\" class=\"sml\">Generate</button>\n\t\t\t\t\t\t<button id=\"btnDist\" class=\"sml\">Distribute</button>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"bar\">\n\t\t\t\t\t\t<button id=\"btnCopy\" class=\"sml\">Copy</button>\n\t\t\t\t\t\t<button id=\"btnPaste\" class=\"sml\">Paste</button>\n\t\t\t\t\t\t<button id=\"btnDel\" class=\"sml\">Delete</button>\n\t\t\t\t</div>\n\t\t</div>\n\n\t\t<div id=\"editor\">\n\t\t\t<div id=\"gradWrap\"><div id=\"grad\"></div></div>\n\t\t</div>\n\n\t\t<label>\n\t\t\t<input type=\"checkbox\" id=\"chkPreview\">\n\t\t\t<span>preview on selected segments</span>\n\t\t</label>\n\n\t\t<div class=\"tbl\" id=\"empty\"></div>\n\n\t\t<section>\n\t\t\t<span id=\"memWarn\">Warning: Adding many custom palettes might cause stability issues, create <a href=\"/settings/sec#backup\">backups</a></span>\n\t\t\t<div class=\"tbl\"><div id=\"custom\" class=\"lst\"></div></div>\n\t\t</section>\n\n\t\t<section><div id=\"allCats\" class=\"cats\"></div></section>\n\n\t\t<div>\n\t\t\t<button id=\"btnFetchExt\" class=\"btn\">Download more palettes</button>\n\t\t</div>\n\t\t<div style=\"margin-bottom: 24px;\">\n\t\t\t  <button class=\"btn\" onclick=\"window.location.href = getURL('/');\">Back to the controls</button>\n\t\t</div>\n\n\t\t<div style=\"font-size:12px;color:#666;\">by @dedehai</div>\n\t</div>\n\n\t<script>\n\t\t// State\n\t\tlet gr, wr, rc, w = 0, sc = 1, pl = 16;\n\t\tlet sel = null, pk = null;\n\t\tlet cpc = 0, cpm = 10, pnm = [], cpal = [], spal = [];  // custom palette count, custom palette max, names, custom palettes, static palettes\n\t\tlet prvTmr = null, prvEn = false, palCache = [];\n\t\tlet isDragging = false;\n\t\tlet copyColor = '#000';\n\t\tlet ws = null;\n\t\tlet maxCol; // max colors to send out in one chunk, ESP8266 is limited to ~50 (500 bytes), ESP32 can do ~128 (1340 bytes)\n\n\t\t// load external resources in sequence to avoid 503 errors if heap is low, repeats indefinitely until loaded\n\t\t(function loadFiles() {\n\t\t\tconst s = document.createElement('script');\n\t\t\ts.src = 'common.js';\n\t\t\ts.onerror = () => setTimeout(loadFiles, 100);\n\t\t\ts.onload = () => {\n\t\t\t\tloadResources(['style.css', 'iro.js'], init);\n\t\t\t};\n\t\t\tdocument.head.appendChild(s);\n\t\t})();\n\n\t\t// main init function, called when all resources are loaded\n\t\tfunction init() {\n\t\t\t// init iro color picker\n\t\t\tpk = new iro.ColorPicker('#picker', {\n\t\t\t\twidth: 240,\n\t\t\t\twheelAngle: 270,\n\t\t\t\twheelDirection: 'clockwise',\n\t\t\t\tlayout: [\n\t\t\t\t\t{component: iro.ui.Wheel},\n\t\t\t\t\t{component: iro.ui.Slider, options: {sliderType: 'value'}}\n\t\t\t\t]\n\t\t\t});\n\t\t\t// update color when picker changes\n\t\t\tpk.on('color:change', (c) => { setCol(c.hexString); updRGB(c.hexString); });\n\n\t\t\tconst updFromRGB = () => {\n\t\t\t\tconst r = clamp(parseInt(gId('rInput').value) || 0, 0, 255);\n\t\t\t\tconst g = clamp(parseInt(gId('gInput').value) || 0, 0, 255);\n\t\t\t\tconst b = clamp(parseInt(gId('bInput').value) || 0, 0, 255);\n\t\t\t\tpk.color.rgb = {r, g, b};\n\t\t\t};\n\t\t\tgId('rInput').addEventListener('change', updFromRGB);\n\t\t\tgId('gInput').addEventListener('change', updFromRGB);\n\t\t\tgId('bInput').addEventListener('change', updFromRGB);\n\n\t\t\tgId('btnDist').onclick = dist;\n\t\t\tgId('btnDel').onclick = deleteMarker;\n\t\t\tgId('btnNew').onclick = rndPal;\n\t\t\tgId('btnCopy').onclick = () => copypasteColor(0);\n\t\t\tgId('btnPaste').onclick = () => copypasteColor(1);\n\t\t\tgId('btnFetchExt').onclick = fetchExt;\n\t\t\tgId('chkPreview').addEventListener('change', (e) => {\n\t\t\t\tprvEn = e.target.checked;\n\t\t\t\tif (prvEn) applyLED();\n\t\t\t\telse requestJson({seg:{frz:false}});\n\t\t\t});\n\n\t\t\tgr = gId('grad');\n  \t\twr = gId('gradWrap');\n\t\t\twr.addEventListener('pointerdown', (e) => {\n\t\t\t\tconst m = e.target.closest('.mk');\n\t\t\t\tif (m) {\n\t\t\t\t\tisDragging = false;\n\t\t\t\t\tselMk(m);\n\t\t\t\t\tif (m.dataset.lock === '1') return;\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\tconst startT = +m.dataset.t;\n\t\t\t\t\tconst mv = (ev) => {\n\t\t\t\t\t\tisDragging = true;\n\t\t\t\t\t\tconst minT = (startT === 0) ? 0 : 1;\n\t\t\t\t\t\tconst maxT = (startT === 255) ? 255 : 254;\n\t\t\t\t\t\tconst x = clamp(Math.round((ev.clientX - (rc.left + pl)) / sc), minT, maxT);\n\t\t\t\t\t\tm.dataset.t = x; m.style.left = (pl + (x * sc)) + 'px'; draw();\n\t\t\t\t\t};\n\t\t\t\t\td.addEventListener('pointermove', mv);\n\t\t\t\t\td.addEventListener('pointerup', () => d.removeEventListener('pointermove', mv), {once:1});\n\t\t\t\t} else if (e.target === wr || e.target === gr) {\n\t\t\t\t\tconst t = clamp(Math.round((e.clientX - (rc.left + pl)) / sc), 1, 254);\n\t\t\t\t\tif (canAdd() && t !== 0 && t !== 255) {\n\t\t\t\t\t\taddMk(t, '#' + (palCache[t] || '000'));\n\t\t\t\t\t\t// trigger drag immediately\n\t\t\t\t\t\tconst newM = sel;\n\t\t\t\t\t\tif (newM) {\n\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\tconst mv = (ev) => {\n\t\t\t\t\t\t\t\tisDragging = true;\n\t\t\t\t\t\t\t\tconst x = clamp(Math.round((ev.clientX - (rc.left + pl)) / sc), 1, 254);\n\t\t\t\t\t\t\t\tnewM.dataset.t = x; newM.style.left = (pl + (x * sc)) + 'px'; draw();\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\td.addEventListener('pointermove', mv);\n\t\t\t\t\t\t\td.addEventListener('pointerup', () => d.removeEventListener('pointermove', mv), {once:1});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t\t// keyboard nudge for selected marker (note: uses about 100 bytes of code)\n\t\t\td.addEventListener('keydown', (e) => {\n\t\t\t\tif (!sel || sel.dataset.lock === '1') return;\n\t\t\t\tlet t = +sel.dataset.t;\n\t\t\t\tif (e.key === 'ArrowLeft') t = Math.max(1, t - (e.shiftKey ? 8 : 1));\n\t\t\t\telse if (e.key === 'ArrowRight') t = Math.min(254, t + (e.shiftKey ? 8 : 1));\n\t\t\t\telse return;\n\t\t\t\tsel.dataset.t = t; sel.style.left = (pl + (t * sc)) + 'px'; draw(); e.preventDefault();\n\t\t\t});\n\n\t\t\tgetLoc(); // set base URL\n\t\t\tws = connectWs();\n\n\t\t\tlet extGrp = {};\n\t\t\tconst cached = tryCache();\n\t\t\tif (!cached) gId('btnFetchExt').style.display = '';\n\t\t\t// fetch info + palnames\n\t\t\tPromise.all([fetch(getURL('/json/info')).then(r=>r.json()), fetch(getURL('/json/pal')).then(r=>r.json())])\n\t\t\t\t.then(([inf, nm]) => {\n\t\t\t\t\tpnm = nm; cpc = inf.cpalcount; cpm = inf.cpalmax;\n\t\t\t\t\tfetchC(cpc);\n\t\t\t\t\tif (inf.arch === 'esp8266') maxCol = 50; // TODO: test if this works.\n\t\t\t\t\telse maxCol = 128;\n\n\t\t\t\t\t// Extract WLED palettes from wledPalx cache from main UI\n\t\t\t\t\tlet cache; try { cache = JSON.parse(localStorage.getItem('wledPalx')); } catch {}\n\t\t\t\t\tif (cache?.p) {\n\t\t\t\t\t\tfor (const k in cache.p) {\n\t\t\t\t\t\t\tif (+k > 255 - cpm || !Array.isArray(cache.p[k])) { delete cache.p[k]; continue; }\n\t\t\t\t\t\t\tconst a = cache.p[k];\n\t\t\t\t\t\t\tif (a[a.length-1][0] !== 255) a.push([255, ...a[a.length-1].slice(1)]);\n\t\t\t\t\t\t\tcache.p[k].name = pnm[k];\n\t\t\t\t\t\t}\n\t\t\t\t\t\tspal = Object.entries(cache.p).filter(([k]) => +k >= 8).map(([k, v]) => ({[k]:v.flat(), name:v.name}));\n\t\t\t\t\t}\n\t\t\t\t\ttry { const raw = localStorage.getItem('wledCptCityJson'); if (raw) extGrp = grpExt(JSON.parse(raw)); } catch(e) {}\n\t\t\t\t\tbldCat(extGrp);\n\t\t\t\t})\n\t\t\t\t.catch(()=>{});\n\n\t\t\trecalc();\n\t\t\trndPal();\n\t\t}\n\n\t\t// Utils\n\t\tfunction clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }\n\t\tfunction rndHex() { return '#' + (Math.random() * 0xFFFFFF << 0).toString(16).padStart(6, '0'); }\n\t\tfunction h2rgb(h) { h = h.replace('#', ''); const n = parseInt(h, 16); return [(n>>16)&255, (n>>8)&255, n&255]; }\n\t\tfunction rgb2h(r, g, b) { return ((1<<24) + (r<<16) + (g<<8) + b).toString(16).slice(1); }\n\t\tfunction lerp(a, b, t) { return a + (b-a)*t; }\n\t\tfunction updRGB(h) { const [r,g,b] = h2rgb(h); gId('rInput').value=r; gId('gInput').value=g; gId('bInput').value=b; }\n\t\tfunction isEmpty(a) { return Array.isArray(a) && a.length === 1 && a[0] === 255; }\n\n\t\t// copy / paste color\n\t\tfunction copypasteColor(paste = false) {\n\t\t\tif (!sel) return;\n\t\t\tif (paste) {\n\t\t\t\tsetCol(copyColor);\n\t\t\t} else {\n\t\t\t\tcopyColor = sel.dataset.c;\n\t\t\t\tgId('btnPaste').style.borderColor = copyColor;\n\t\t\t}\n\t\t}\n\n\t\t// Geometry\n\t\tfunction recalc() {\n\t\t\trc = wr.getBoundingClientRect();\n\t\t\tconst cs = getComputedStyle(wr);\n\t\t\tpl = parseInt(cs.paddingLeft || 16, 10);\n\t\t\tw = rc.width - pl - parseInt(cs.paddingRight || 16, 10);\n\t\t\tsc = w / 255;\n\t\t\tlayout();\n\t\t\tdraw();\n\t\t}\n\n\t\tfunction layout() {\n\t\t\t[...gr.querySelectorAll('.mk')].forEach(m => {\n\t\t\t\tm.style.left = (pl + (+m.dataset.t * sc)) + 'px';\n\t\t\t});\n\t\t}\n\n\t\tfunction stops() {\n\t\t\treturn [...gr.querySelectorAll('.mk')]\n\t\t\t\t.map(m => ({ t: +m.dataset.t, c: m.dataset.c, lock: m.dataset.lock === '1' }))\n\t\t\t\t.sort((a, b) => a.t - b.t);\n\t\t}\n\n\t\t/* Build a 256-entry cache of palette RGB hex (without #). This is used for previewing and sending to LEDs */\n\t\tfunction bldCache() {\n\t\t\tconst s = stops();\n\t\t\tpalCache = new Array(256);\n\t\t\tif (!s.length) { palCache.fill('000000'); return; }\n\n\t\t\tfor (let t = 0; t <= 255; t++) {\n\t\t\t\tif (t <= s[0].t) { palCache[t] = s[0].c.slice(1); continue; }\n\t\t\t\tif (t >= s[s.length - 1].t) { palCache[t] = s[s.length - 1].c.slice(1); continue; }\n\n\t\t\t\tfor (let i = 0; i < s.length - 1; i++) {\n\t\t\t\t\tconst a = s[i], b = s[i+1];\n\t\t\t\t\tif (t >= a.t && t <= b.t) {\n\t\t\t\t\t\tconst f = (b.t === a.t) ? 0 : ((t - a.t) / (b.t - a.t));\n\t\t\t\t\t\tconst [ar,ag,ab] = h2rgb(a.c), [br,bg,bb] = h2rgb(b.c);\n\t\t\t\t\t\tpalCache[t] = rgb2h(Math.round(lerp(ar,br,f)), Math.round(lerp(ag,bg,f)), Math.round(lerp(ab,bb,f)));\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfunction draw() {\n\t\t\tconst s = stops();\n\t\t\tgr.style.background = 'linear-gradient(to right,' + s.map(x => x.c + ' ' + Math.round(x.t * sc) + 'px').join(',') + ')';\n\t\t\tbldCache();\n\t\t\tif (prvEn && !prvTmr) {\n\t\t\t\tlet d = (ws && ws.readyState == 1) ? 50 : 500; // slower updates if using HTTP to not overwhelm the ESP\n\t\t\t\tprvTmr = setTimeout(() => { prvTmr = null; applyLED(); }, d);\n\t\t\t}\n\t\t}\n\n\t\t// Markers\n\t\tfunction selMk(m) {\n\t\t\tif (sel) sel.classList.remove('sel');\n\t\t\tsel = m || null;\n\t\t\tif (sel) {\n\t\t\t\tsel.classList.add('sel');\n\t\t\t\tif (pk) { pk.color.hexString = sel.dataset.c; updRGB(sel.dataset.c); }\n\t\t\t}\n\t\t}\n\n\t\tfunction canAdd() { return gr.querySelectorAll('.mk').length < 16; }\n\n\t\tfunction addMk(t, c, lock) {\n\t\t\t// keep start and end markers at 0/255 locked\n\t\t\tif (t < 0 || t > 255 || !canAdd() || gr.querySelector('.mk[data-t=\"' + t + '\"]')) return;\n\t\t\tconst m = cE('div');\n\t\t\tm.className = 'mk';\n\t\t\tm.dataset.t = t;\n\t\t\tm.dataset.c = c || rndHex();\n\t\t\tm.dataset.lock = lock ? '1' : '0';\n\t\t\tm.style.left = (pl + (t * sc)) + 'px';\n\t\t\tm.style.background = m.dataset.c;\n\t\t\tgr.appendChild(m);\n\t\t\tselMk(m);\n\t\t\tdraw();\n\t\t}\n\n\t\tfunction deleteMarker() {\n\t\t\tif (!sel || sel.dataset.lock === '1') return;\n\t\t\tsel.remove();\n\t\t\tselMk(null);\n\t\t\tdraw();\n\t\t}\n\n\t\tfunction setCol(h) {\n\t\t\tif (!sel) return;\n\t\t\tsel.dataset.c = h;\n\t\t\tsel.style.background = h;\n\t\t\tdraw();\n\t\t}\n\n\t\tfunction dist() {\n\t\t\tconst s = stops();\n\t\t\tif (s.length < 3) return;\n\t\t\tconst inner = s.slice(1, -1), step = Math.round(255 / (inner.length + 1));\n\t\t\tinner.forEach((p, i) => {\n\t\t\t\t// find node corresponding to this pos (skips locked)\n\t\t\t\tconst m = [...gr.querySelectorAll('.mk')].find(x => +x.dataset.t === p.t && x.dataset.lock !== '1');\n\t\t\t\tif (m) { m.dataset.t = step * (i + 1); m.style.left = (pl + (m.dataset.t * sc)) + 'px'; }\n\t\t\t});\n\t\t\tdraw();\n\t\t}\n/*\n\t\tfunction rndPal() {\n\t\t\tgr.innerHTML = '';\n\t\t\taddMk(0, rndHex(), 1);\n\t\t\tconst cnt = Math.floor(Math.random() * 3) + 2, pos = new Set();\n\t\t\twhile (pos.size < cnt) pos.add(Math.floor(Math.random() * 254) + 1);\n\t\t\t[...pos].sort((a,b)=>a-b).forEach(t => addMk(t, rndHex()));\n\t\t\taddMk(255, rndHex(), 1);\n\t\t}*/\n\n\t\t\n\t\t// convert hsl to hex using canvas\n\t\tfunction hslToHex(h, s, l) {\n\t\t\tlet ctx = cE(\"canvas\").getContext(\"2d\");\n\t\t\tctx.fillStyle = `hsl(${h},${s}%,${l}%)`;\n\t\t\treturn ctx.fillStyle;\n\t\t}\n\t\t// random or harmonic palette (uses 100 bytes extra compared to simple random palette)\n\t\tfunction rndPal() {\n\t\t\tgr.innerHTML = '';\n\t\t\tconst mode = Math.floor(Math.random() * 3); // 33% chance for random, 67% for harmonic\n\t\t\tconst cnt = Math.floor(Math.random() * 4) + 1; // 1-4 colors (+ start/end)\n\t\t\tlet pos = new Set();\n\t\t\twhile (pos.size < cnt) pos.add(Math.floor(Math.random() * 254) + 1);\n\t\t\tconst markers = [0, ...[...pos].sort((a,b)=>a-b), 255];\n\t\t\tlet colors;\n\n\t\t\tif (mode === 0) { // random\n\t\t\t\t//console.log('random');\n\t\t\t\tcolors = markers.map(() => hslToHex(\n\t\t\t\t\tMath.random() * 360,\n\t\t\t\t\tMath.random() * 100,\n\t\t\t\t\tMath.random() * 60 + 10\n\t\t\t\t));\n\t\t\t}\n\t\t\telse { // harmonic triadic/tetradic\n\t\t\t\t//console.log('harmonic');\n\t\t\t\tconst hcount = Math.random() < 0.5 ? 3 : 4;\n\t\t\t\tconst base = Math.random() * 360;\n\t\t\t\tcolors = markers.map((_, i) => hslToHex(\n\t\t\t\t\t(base + 360 * (i % hcount) / hcount) % 360,\n\t\t\t\t\t100,\n\t\t\t\t\tMath.random() * 60 + 25 // chance for pastel colors\n\t\t\t\t));\n\t\t\t}\n\n\t\t\tmarkers.forEach((t, i) =>\n\t\t\t\taddMk(t, colors[i], (i === 0 || i === markers.length - 1) ? 1 : 0)\n\t\t\t);\n\t\t}\n\t\t// Data\n\t\tfunction toJSON() { return JSON.stringify({ palette: stops().flatMap(s => [s.t, s.c.slice(1)]) }); }\n\n\t\t// convert wled palette array to CSS gradient string\n\t\tfunction cssArr(a) {\n\t\t\tlet out = [];\n\t\t\tfor (let i = 0; i < a.length; i += 2) {\n\t\t\t\tconst t = a[i], v = a[i + 1];\n\t\t\t\tout.push(typeof v === 'string' ? `#${v} ${t/255*100}%` : `rgba(${v},${a[i+2]},${a[i+3]},1) ${t/255*100}%`);\n\t\t\t\tif (typeof v !== 'string') i += 2;\n\t\t\t}\n\t\t\treturn 'linear-gradient(to right,' + out.join(',') + ')';\n\t\t}\n\n\t\t// accepts stops array: [pos, color, pos, color, ...] where color can be 'rrggbb' or [r,g,b]\n\t\tfunction loadArr(a) {\n\t\t\tgr.innerHTML = '';\n\t\t\tfor (let i = 0; i < a.length; i += 2) {\n\t\t\t\tconst t = a[i], v = a[i + 1];\n\t\t\t\tconst h = typeof v === 'string' ? '#' + v : '#' + ((v << 16) | (a[i + 2] << 8) | a[i + 3]).toString(16).padStart(6, '0');\n\t\t\t\tif (typeof v !== 'string') i += 2;\n\t\t\t\taddMk(t, h, (t === 0 || t === 255));\n\t\t\t}\n\t\t}\n\n\t\tfunction normCat(k) { const n = (k || '').toLowerCase(); return n === 'themed' ? 'thematic' : n; }\n\n\t\t// group external palettes into categories\n\t\tfunction grpExt(d) {\n\t\t\tconst g = {colorful:[], thematic:[], pastel:[], striped:[], gradient:[], monochrome:[]};\n\t\t\tconst add = (p) => { const k = normCat(p.category || p.class); if (g[k]) g[k].push(p); };\n\t\t\tif (d?.categories && isO(d.categories)) {\n\t\t\t\tfor (const [k, l] of Object.entries(d.categories)) {\n\t\t\t\t\tconst nk = normCat(k);\n\t\t\t\t\tif (Array.isArray(l)) l.forEach(p => { if (g[nk]) g[nk].push(p); });\n\t\t\t\t}\n\t\t\t} else if (Array.isArray(d?.palettes)) d.palettes.forEach(add);\n\t\t\telse if (Array.isArray(d)) d.forEach(add);\n\t\t\treturn g;\n\t\t}\n\n\t\t// UI\n\t\tfunction mkPalItem(name, author, css, arr, save, remove) {\n\t\t\tconst itm = cE('div'), nam = cE('div'), pvw = cE('div'), prow = cE('div'), pv = cE('div');\n\t\t\titm.className = 'pal';\n\t\t\tnam.className = 'nam';\n\t\t\tnam.textContent = name;\n\t\t\tif (author) {\n\t\t\t\tconst a = cE('span');\n\t\t\t\ta.className = 'by';\n\t\t\t\ta.textContent = ' by ' + author; \n\t\t\t\tnam.appendChild(a);\n\t\t\t}\n\n\t\t\tpvw.style.alignItems = 'center';\n\t\t\tpvw.style.flex = '1';\n\t\t\tprow.className = 'prow';\n\t\t\tpv.className = 'prv';\n\t\t\tpv.style.background = css;\n\t\t\tpv.title = 'Click to load';\n\t\t\tpv.onclick = () => loadArr(arr);\n\t\t\tpvw.append(nam, pv);\n\t\t\tprow.appendChild(pvw);\n\t\t\tif (save) prow.appendChild(save);\n\t\t\tif (remove) prow.appendChild(remove);\n\t\t\titm.appendChild(prow);\n\t\t\treturn itm;\n\t\t}\n\n\t\t// Build the custom palette list (custom slots + empty slot handling)\n\t\t// input: slots larger than \"loaded\" are set to 50% opacity to indicate they are not refreshed yet\n\t\tfunction bldLst(loaded) {\n\t\t\tconst cd = gId('custom'), fc = d.createDocumentFragment();\n\t\t\tcd.innerHTML = '';\n\t\t\tlet emptyslot = gId('empty');\n\t\t\temptyslot.innerHTML = '';\n\t\t\tlet foundEmpty = false;\n\n\t\t\tcpal.forEach((p, i) => {\n\t\t\t\tconst sv = cE('button');\n\t\t\t\tsv.className = 'sml';\n\t\t\t\tsv.innerHTML = '&#8678;'; // alternative arrows: '&#8617;'  '&#8592;'\n\t\t\t\tsv.title = 'Save to slot ' + i;\n\t\t\t\tsv.onclick = () => upload(i);\n\n\t\t\t\tconst rm = cE('button');\n\t\t\t\trm.className = 'sml';\n\t\t\t\trm.title = 'Delete palette';\n\t\t\t\trm.innerHTML = '&#10006;';\n\t\t\t\trm.onclick = () => { requestJson({rmcpal:i}); setTimeout(refr, 500); };\n\n\t\t\t\tconst name = isEmpty(p.palette) ? 'Empty slot' : 'Custom' + i;\n\t\t\t\tconst css = isEmpty(p.palette) ? '#666' : cssArr(p.palette);\n\t\t\t\tconst item = mkPalItem(name, null, css, p.palette, sv, rm);\n\t\t\t\tconst prv = item.querySelector('.prv');\n\t\t\t\tprv.style.opacity = i > loaded ? 0.5 : 1; // set opacity of palette preview\n\n\t\t\t\t// disable loading for empty palettes\n\t\t\t\tif (isEmpty(p.palette) && !foundEmpty) {\n\t\t\t\t\tfoundEmpty = true;\n\t\t\t\t\tprv.style.cursor = 'not-allowed';\n\t\t\t\t\tprv.onclick = null;\n\t\t\t\t\titem.querySelector('.nam').style.transform = 'translateY(100%)'; // move name down to preview center\n\t\t\t\t\temptyslot.appendChild(item);\n\t\t\t\t} else if (!isEmpty(p.palette)) {\n\t\t\t\t\tfc.appendChild(item);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tcd.appendChild(fc);\n\t\t\tgId('memWarn').style.display = (cpc > 10) ? 'block' : 'none'; // show warning if 10 or more custom palettes\n\t\t}\n\n\t\t// build categories UI from grouped palettes\n\t\tfunction bldCat(extGrp) {\n\t\t\tconst h = gId('allCats');\n\t\t\tif (!h) return;\n\t\t\th.innerHTML = '';\n\t\t\tconst cats = [\n\t\t\t\t{k:'wled', lbl:'WLED Palettes', items:spal},\n\t\t\t\t{k:'colorful', lbl:'Colorful', items:extGrp.colorful||[]},\n\t\t\t\t{k:'thematic', lbl:'Thematic', items:extGrp.thematic||[]},\n\t\t\t\t{k:'pastel', lbl:'Pastel', items:extGrp.pastel||[]},\n\t\t\t\t{k:'striped', lbl:'Striped', items:extGrp.striped||[]},\n\t\t\t\t{k:'gradient', lbl:'Gradient', items:extGrp.gradient||[]},\n\t\t\t\t{k:'monochrome', lbl:'Monochrome', items:extGrp.monochrome||[]}\n\t\t\t];\n\t\t\tcats.forEach(({k, lbl, items}) => {\n\t\t\t\tif (!items.length && k !== 'wled') return; // skip external categories if empty\n\t\t\t\tconst det = cE('details'), sum = cE('summary'), body = cE('div');\n\t\t\t\tdet.className = 'cat';\n\t\t\t\tsum.innerHTML = `&#9656; ${lbl} (${items.length})`;\n\t\t\t\tdet.appendChild(sum);\n\t\t\t\tbody.className = 'cbdy';\n\n\t\t\t\titems.forEach(p => {\n\t\t\t\tif (k === 'wled') {\n\t\t\t\t\t\tconst key = Object.keys(p)[0];\n\t\t\t\t\t\tbody.appendChild(mkPalItem(p.name || ' ', null, cssArr(p[key]), p[key]));\n\t\t\t\t} else {\n\t\t\t\t\t\tbody.appendChild(mkPalItem(p.name || ' ', p.author, cssArr(p.colors), p.colors));\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tdet.appendChild(body);\n\t\t\t\th.appendChild(det);\n\t\t\t\tdet.addEventListener('toggle', () => {\n\t\t\t\t\tsum.innerHTML = det.open ? `&#9662; ${lbl} (${items.length})` : `&#9656; ${lbl} (${items.length})`;\n\t\t\t\t});\n\t\t\t});\n\t\t}\n\n\t\t// Network\n\t\tfunction upload(i) {\n\t\t\tconst b = new Blob([toJSON()], {type:'application/json'});\n\t\t\tconst fakeFileObj = { files: [b] };\n\t\t\tuploadFile(fakeFileObj, '/palette' + i + '.json');\n\t\t\tlocalStorage.removeItem('wledPalx'); // invalidate main UI cache\n\t\t\tsetTimeout(refr, 300);\n\t\t}\n\n\t\tasync function refr() {\n\t\t\ttry {\n\t\t\t\tconst inf = await fetch(getURL('/json/info'), {cache:'no-store'}).then(r=>r.json());\n\t\t\t\tcpc = inf.cpalcount; cpm = inf.cpalmax;\n\t\t\t\tawait fetchC(cpc);\n\t\t\t} catch(e) {}\n\t\t}\n\n\t\t// load custom palettes, loads from cache and updates in the background as palettes are coming in\n\t\tasync function fetchC(n) {\n\t\t\ttry {\n\t\t\t\tconst cached = localStorage.getItem('wledCustomPal');\n\t\t\t\tif (cached) cpal = JSON.parse(cached);\n\t\t\t\tif (cpal.length > n) cpal = cpal.slice(0, n);\n\t\t\t} catch(e) {}\n\n\t\t\twhile (cpal.length < n) cpal.push({palette:[255]});\n\t\t\tif (cpal.length < cpm) cpal.push({palette:[255]});\n\t\t\tbldLst(0); // initial build, all previews at 50% opacity\n\n\t\t\t// fetch and replace palettes as they load\n\t\t\tfor (let i = 0; i < n; i++) {\n\t\t\t\ttry {\n\t\t\t\t\tconst res = await fetch(getURL('/palette' + i + '.json'), {cache:'no-store'});\n\t\t\t\t\tif (res.ok) {\n\t\t\t\t\t\tcpal[i] = await res.json();\n\t\t\t\t\t\tbldLst(i); // rebuild list, set opacity of this (and previous slots) to 100%\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcpal[i] = {palette:[255]};\n\t\t\t\t\t}\n\t\t\t\t} catch(e) {\n\t\t\t\t\tcpal[i] = {palette:[255]};\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tlocalStorage.setItem('wledCustomPal', JSON.stringify(cpal));\n\t\t\t} catch(e) {}\n\t\t}\n\n\t\tfunction tryCache() {\n\t\t\ttry {\n\t\t\t\tconst raw = localStorage.getItem('wledCptCityJson');\n\t\t\t\tif (!raw) return false;\n\t\t\t\tbldCat(grpExt(JSON.parse(raw)));\n\t\t\t\tgId('btnFetchExt').style.display = 'none';\n\t\t\t\treturn true;\n\t\t\t} catch (e) { localStorage.removeItem('wledCptCityJson'); return false; }\n\t\t}\n\n\t\t// download external palettes, these were hand picked from cpt-city (http://seaviewsensing.com/pub/cpt-city/)\n\t\t// all palettes are licensed \"free to use\", converted to WLED JSON format by @dedehai\n\t\tfunction fetchExt() {\n\t\t\tfetch('https://dedehai.github.io/cpt_city_selection.json')\n\t\t\t\t.then(r => { if (!r.ok) throw new Error(); return r.json(); })\n\t\t\t\t.then(data => {\n\t\t\t\t\ttry { localStorage.setItem('wledCptCityJson', JSON.stringify(data)); } catch(e) {}\n\t\t\t\t\tbldCat(grpExt(data));\n\t\t\t\t\tgId('btnFetchExt').style.display = 'none';\n\t\t\t\t})\n\t\t\t\t.catch(() => { alert('Download failed'); });\n\t\t}\n\n\t\t// connect to WebSocket, use parent WS or open new\n\t\tfunction connectWs(onOpen) {\n\t\t\ttry {\n\t\t\t\tif (top.window.ws && top.window.ws.readyState === WebSocket.OPEN) {\n\t\t\t\t\tif (onOpen) onOpen();\n\t\t\t\t\treturn top.window.ws;\n\t\t\t\t}\n\t\t\t} catch (e) {}\n\t\t\tlet url = loc ? getURL('/ws').replace(\"http\",\"ws\") : \"ws://\"+window.location.hostname+\"/ws\";\n\t\t\tlet w = new WebSocket(url);\n\t\t\tw.binaryType = \"arraybuffer\";\n\t\t\tif (onOpen) w.addEventListener('open', onOpen);\n\t\t\tw.addEventListener('close', () => { ws = null; });\n\t\t\tw.addEventListener('error', () => { ws = null; });\n\t\t\treturn w;\n\t\t}\n\n\t\tasync function requestJson(cmd)\n\t\t{\n\t\t\tif (ws && ws.readyState == 1) {\n\t\t\t\ttry {\n\t\t\t\t\tws.send(JSON.stringify(cmd));\n\t\t\t\t\treturn 1;\n\t\t\t\t} catch (e) {}\n\t\t\t}\n\n\t\t\tif (!window._httpQueue) {\n\t\t\t\twindow._httpQueue = [];\n\t\t\t\twindow._httpRun = 0;\n\t\t\t}\n\t\t\tif (_httpQueue.length >= 5) {\n\t\t\t\treturn Promise.resolve(-1); // reject if too many queued requests\n\t\t\t}\n\n\t\t\treturn new Promise(resolve => {\n\t\t\t\t_httpQueue.push({ cmd, resolve });\n\t\t\t\t(async function run() {\n\t\t\t\t\tif (_httpRun) return;\n\t\t\t\t\t_httpRun = 1;\n\t\t\t\t\twhile (_httpQueue.length) {\n\t\t\t\t\t\tlet q = _httpQueue.shift();\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait fetch(getURL('/json'), {\n\t\t\t\t\t\t\t\tmethod: 'post',\n\t\t\t\t\t\t\t\tbody: JSON.stringify(q.cmd),\n\t\t\t\t\t\t\t\tcache: 'no-store'\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t} catch (e) {}\n\t\t\t\t\t\tawait new Promise(r => setTimeout(r, 120));\n\t\t\t\t\t\tq.resolve(0);\n\t\t\t\t\t}\n\t\t\t\t\t_httpRun = 0;\n\t\t\t\t})();\n\t\t\t});\n\t\t}\n\n\t\t// apply palette preview to selected segments\n\t\tasync function applyLED()\n\t\t{\n\t\t\tif (!palCache.length) return;\n\t\t\ttry {\n\t\t\t\tlet st = await (await fetch(getURL('/json/state'), { cache: 'no-store' })).json();\n\t\t\t\tif (!st.seg || !st.seg.length) return;\n\n\t\t\t\t// get selected segments, use main segment if none selected\n\t\t\t\tlet segs = st.seg.filter(s => s.sel);\n\t\t\t\tif (!segs.length) {\n\t\t\t\t\tconst mainSeg = st.seg.find(s => s.id === (st.mainseg || 0));\n\t\t\t\t\tif (mainSeg) segs.push(mainSeg);\n\t\t\t\t}\n\t\t\t\t// show palette on each selected segment, 2D are treated as 1D to show better gradient\n\t\t\t\tfor (let s of segs) {\n\t\t\t\t\tlet len = (s.stop - s.start) * ((s.stopY - s.startY) || 1);\n\t\t\t\t\tlet arr = [];\n\t\t\t\t\tfor (let i = 0; i < len; i++)\n\t\t\t\t\t\tarr.push(palCache[len > 1 ? Math.round(i * 255 / (len - 1)) : 0]);\n\t\t\t\t\t// send colors in chunks\n\t\t\t\t\tfor (let j = 0; j < arr.length; j += maxCol) {\n\t\t\t\t\t\tlet chunk = [s.start + j, ...arr.slice(j, j + maxCol)];\n\t\t\t\t\t\tawait requestJson({ seg: { id: s.id, i: chunk } });\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (e) {}\n\t\t}\n\n\t\t// Events\n\t\twindow.addEventListener('resize', recalc);\n\t\t// Unfreeze segments when page is unloaded\n\t\twindow.addEventListener('beforeunload', () => {\n\t\t\tif (prvEn) requestJson({seg:{frz:false}});\n\t\t});\n\n\t</script>\n\t<style>\n\t\t:root {\n\t\t\t--pad:16px;\n\t\t\t--maxw:820px;\n\t\t}\n\t\tbody {\n\t\t\tbackground: #111;\n\t\t\tmin-width: 320px; /* prevent layout breakdown */\n\t\t}\n\t\t.ctr {\n\t\t\tmax-width: calc(var(--maxw) + 2*var(--pad));\n\t\t\tmargin: 20px auto;\n\t\t\tpadding: 0 var(--pad);\n\t\t\tdisplay: flex; flex-direction: column; align-items: center;\n\t\t}\n\n\t\theader {\n\t\t\ttext-align: center;\n\t\t\tmargin-bottom: 10px;\n\t\t\twidth: 100%;\n\t\t}\n\t\theader h1 {\n\t\t\tmargin: 0;\n\t\t\tfont-size: 20px;\n\t\t\tfont-weight: 600;\n\t\t}\n\t\t#pickerWrap {\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: column;\n\t\t\talign-items: center;\n\t\t\tmargin: 8px 0 12px;\n\t\t\twidth: 100%;\n\t\t}\n\t\t.bar {\n\t\t\t\tdisplay: flex;\n\t\t\t\tgap: 8px;\n\t\t\t\tflex-wrap: wrap;\n\t\t\t\tjustify-content: center;\n\t\t\t\talign-items: center;\n\t\t\t\tmargin: 8px 0;\n\t\t\t\twidth: 100%;\n\t\t}\n\n\t\tbutton.sml { min-width: 70px; }\n\n\t\t#editor {\n\t\t\twidth: 100%;\n\t\t\tdisplay: flex;\n\t\t\tjustify-content: center;\n\t\t\tposition: sticky;\n\t\t\ttop: 0;\n\t\t\tz-index: 5;\n\t\t\tpadding: 6px 0;\n\t\t\tbackground: #111;\n\t\t}\n\t\t#gradWrap {\n\t\t\tposition: relative;\n\t\t\theight: 34px;\n\t\t\twidth: 100%;\n\t\t\tmax-width: var(--maxw);\n\t\t\ttouch-action: none; /* fixes click&drag on touch devices */\n\t\t}\n\t\t#grad {\n\t\t\theight: 100%;\n\t\t\tborder-radius: 5px;\n\t\t}\n\n\t\t#empty {\n\t\t\tposition: sticky;\n\t\t\ttop: 15px;\n\t\t\tz-index: 4;\n\t\t\tpadding: 4px 0;\n\t\t}\n\n\t\t.mk {\n\t\t\tposition: absolute;\n\t\t\ttop: 50%;\n\t\t\ttransform: translate(-50%, -50%);\n\t\t\twidth: 8px;\n\t\t\theight: 34px;\n\t\t\tborder: 2px solid #5557;\n\t\t\tborder-radius: 5px;\n\t\t\ttouch-action: none;\n\t\t}\n\t\t.mk.sel {\n\t\t\toutline: 2px solid #0bf;\n\t\t\tborder-color: #000;\n\t\t}\n\t\t@media (pointer: coarse) {\n\t\t\t#gradWrap {\n\t\t\t\theight: 60px; /* taller gradient for touch devices */\n\t\t\t}\n\t\t\t.mk {\n\t\t\t\theight: 60px;\n\t\t\t\twidth: 20px; /* wider markers for touch devices */\n\t\t\t}\n\t\t\t#empty {\n\t\t\t\ttop: 40px; /* adjust for taller gradient */\n\t\t\t}\n\t\t}\n\n\t\tsection {\n\t\t\tmargin-top: 18px;\n\t\t\twidth: 100%;\n\t\t}\n\t\t.tbl {\n\t\t\tbackground: #111;\n\t\t\twidth: 100%;\n\t\t}\n\t\t.lst {\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: column;\n\t\t\twidth: 100%;\n\t\t}\n\n\t\t.pal {\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: column;\n\t\t\tposition: relative;\n\t\t\tmargin-bottom: 5px;\n\t\t}\n\t\t.pal .nam {\n\t\t\ttext-align: center;\n\t\t}\n\t\t.pal .nam .by {\n\t\t\tfont-size: 11px;\n\t\t\tmargin-left: 4px;\n\t\t}\n\t\t.pal .prow {\n\t\t\tdisplay: flex;\n\t\t\talign-items: flex-end;\n\t\t\tgap: 10px;\n\t\t}\n\t\t.pal .prv {\n\t\t\tflex: 1;\n\t\t\tborder-radius: 26px;\n\t\t\tpadding: 17px;\n\t\t\tcursor: pointer;\n\t\t}\n\t\t.pal .sml {\n\t\t\tmin-width: 32px;\n\t\t\theight: 32px;\n\t\t\tpadding: 0;\n\t\t\tfont-weight: 600;\n\t\t}\n\n\t\t.cats {\n\t\t\twidth: 100%;\n\t\t}\n\t\tdetails.cat {\n\t\t\tborder: 1px solid #444;\n\t\t\tborder-radius: 8px;\n\t\t\tmargin: 10px 0;\n\t\t\tbackground: #222;\n\t\t}\n\t\tdetails.cat > summary {\n\t\t\tcursor: pointer;\n\t\t\tpadding: 8px 10px;\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tgap: 8px;\n\t\t\tcolor: #fff;\n\t\t\tlist-style: none;\n\t\t\tfont-size: 18px;\n\t\t\tfont-weight: 500;\n\t\t}\n\t\tdetails.cat > summary::-webkit-details-marker {\n\t\t\tdisplay: none;\n\t\t}\n\t\t.cbdy {\n\t\t\tpadding: 8px 10px 12px;\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: column;\n\t\t\tgap: 6px;\n\t\t}\n\n\t\t.rgbi {\n\t\t\tdisplay: flex;\n\t\t\tgap: 6px;\n\t\t\tmargin-top: 8px;\n\t\t\talign-items: center;\n\t\t}\n\t\t.rgbi input {\n\t\t\twidth: 60px;\n\t\t\tborder-radius: 6px;\n\t\t}\n\n\t\t#memWarn {\n\t\t\tdisplay: none;\n\t\t\tcolor: #ff9900;\n\t\t\tfont-size: 16px;\n\t\t\tmargin-bottom: 8px;\n\t\t\ttext-align: center;\n\t\t}\n\t\t#memWarn a { color: #ffcc00; text-decoration: underline; }\n\t</style>\n</body>\n</html>"
  },
  {
    "path": "wled00/data/dmxmap.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\"><head><meta content='width=device-width' name='viewport'>\n<title>DMX Map</title>\n<script>function B(){window.history.back()};function RS(){window.location = \"/settings\";}function RP(){top.location.href=\"/\";}function FM() {\n  var dmxlabels = [\"SET 0\",\"RED\",\"GREEN\",\"BLUE\",\"WHITE\",\"SHUTTER\",\"SET 255\", \"DISABLED\"];\n  var dmxchans = [];\n  for (i=0;i<512;i++) {\n    dmxchans.push(7); // set all to DISABLED\n  }\n  for (i=0;i<LC;i++) {\n    FS = CS + (CG * i);\n    for (j=0;j<CN;j++) {\n      DA=FS+j;\n      dmxchans[DA-1] = CH[j];\n    }\n  }\n  DMXMap = \"\";\n  for (i=0;i<512;i++) {\n    DMXMap += \"<div class=\\\"anytype type\" + dmxchans[i] + \"\\\">\" + String(i+1) + \"<br />\" + dmxlabels[dmxchans[i]] + \"</div>\";\n  }\n  document.getElementById(\"map\").innerHTML = DMXMap;\n}</script>\n<style>.anytype{border: 1px solid white; margin: 1px; float: left; width: 100px; height: 100px;}.S { margin: 0px; border: 2px solid white;} .type7{color: #888; border: 1px dotted grey;}.type6{color: #FFF;}.type4{color: #FFF; font-weight: bold; }.type3{color: #00F; font-weight: bold; }.type2{color: #0F0; font-weight: bold; }.type1{color: #F00; font-weight: bold; } .bt{background:#333;color:#fff;font-family:Verdana,sans-serif;border:.3ch solid #333;display:inline-block;font-size:20px;margin:8px;margin-top:12px}body{font-family:Verdana,sans-serif;text-align:center;background:#222;color:#fff;line-height:200%%;margin:0}</style></head>\n<body onload=\"FM();\"><div id=\"map\">...</div></body></html>"
  },
  {
    "path": "wled00/data/edit.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<title>WLED File Editor</title>\n<meta name=\"author\" content=\"DedeHai, based on editor by Me-No-Dev\">\n<meta content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\" name=\"viewport\"><!-- prevent too much scaling on mobile -->\n<link rel=\"shortcut icon\" href=\"favicon.ico\">\n<style>\n/* Editor-specific styles */\nbody {\n\tdisplay: flex;\n\tflex-direction: column;\n\tmin-height: 100vh;\n\tmargin: 0;\n\tmin-width: 850px; /* prevent layout breakdown on small screens */\n}\n#top {\n\tfont-size: 20px;\n\tfont-weight: bold;\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tbackground: #222;\n\tpadding: 10px 10px;\n}\n#top .center {\n\tflex: 1;\n\ttext-align: center;\n}\n#top .right {\n\tmargin-left: auto;\n}\n#top input[type=\"text\"] {\n\tbackground: #555;\n\tborder: 2px solid #555;\n\tborder-radius: 8px;\n\tpadding: 6px 8px;\n\tmin-width: 200px;\n}\n#tree {\n\tposition: absolute;\n\ttop: 0;\n\tbottom: 0;\n\tleft: 0;\n\twidth: 300px;\n\tbackground: #222;\n\toverflow-y: auto;\n\tpadding: 8px 8px 100px;  /* extra space on bottom so context menu always fits */\n\ttext-align: left;\n}\n#editor, #preview {\n\tposition: absolute;\n\ttop: 0;\n\tbottom: 0;\n\tleft: 300px;\n\tright: 0;\n\tbackground: #333;\n}\n#editor {\n\tdisplay: flex;\n\tflex-direction: column;\n}\n#editor textarea {\n\tflex: 1;\n\tbackground: #333;\n\tcolor: #fff;\n\tborder: 2px solid #333;\n\tpadding: 8px;\n\tfont: 13px monospace;\n\tresize: none;\n\toutline: none;\n}\n#ace-editor {\n\tflex: 1;\n}\n#preview {\n\tdisplay: none;\n\tpadding: 10px;\n\ttext-align: center;\n}\n#preview img {\n\timage-rendering: pixelated;\n\twidth: 40%;\n\theight: auto;\n}\n#loader {\n\tposition: fixed;\n\ttop: 50%;\n\tleft: 50%;\n\ttransform: translate(-50%, -50%);\n\tdisplay: none;\n}\n.loader {\n\twidth: 60px;\n\theight: 60px;\n\tborder: 6px solid #444;\n\tborder-top-color: #28f;\n\tborder-radius: 50%;\n\tanimation: spin 1s linear infinite;\n}\n/* Ace editor colors to match WLED style*/\n.ace-monokai .ace_string { color: #4c4 !important; }\n.ace-monokai .ace_constant.ace_numeric { color: #fa0 !important; }\n.ace-monokai .ace_constant.ace_language { color: #f84 !important; }\n.ace-monokai .ace_variable { color: #28f !important; }\n.ace_editor { font: 13px monospace !important; }\n@keyframes spin { to { transform: rotate(360deg); } }\n</style>\n<script>\n// load common.js with retry on error, then load Ace editor from CDN with retries, then load style.css\nfunction loadFiles(u, r, cb) {\n\tvar s = document.createElement('script'),\n\t\t\t// set 1s timeout if retry count is less than 10 (i.e. for Ace scripts)\n\t\t\ttm = r < 10 ? setTimeout(() => { s.onload=s.onerror=null; cb(); }, 1000) : 0;\n\ts.src = u;\n\ts.onload = () => { clearTimeout(tm); cb(); };\n\ts.onerror = () => {\n\t\tclearTimeout(tm);\n\t\tif (r !== 0) setTimeout(() => loadFiles(u, r < 0 ? r : r - 1, cb), 100); // if r is -1 or > 0 try again\n\t\telse cb();\n\t};\n\tdocument.head.appendChild(s);\n}\n\nloadFiles('common.js', -1, () => {\n\tconst cdn = \"https://cdnjs.cloudflare.com/ajax/libs/ace/1.23.4/\";\n\t// load external scripts in sequence (3 retries each)\n\tloadFiles(cdn + 'ace.min.js', 3, () => {\n\t\tloadFiles(cdn + 'mode-json.min.js', 3, () => {\n\t\t\tloadFiles(cdn + 'theme-monokai.min.js', 3, () => {\n\t\t\t\t// all scripts loaded (or skipped) load style.css\n\t\t\t\tif (window.loadResources) loadResources(['style.css'], S);\n\t\t\t});\n\t\t});\n\t});\n});\n\nvar QueuedRequester = function(){ this.q=[]; this.r=false; this.x=null; }\nQueuedRequester.prototype = {\n\t_request: function(req){\n\t\tthis.r = true;\n\t\tvar that = this;\n\t\tfunction cb(x,d){ return function(){\n\t\t\tif (x.readyState==4){\n\t\t\t\tgId(\"loader\").style.display=\"none\";\n\t\t\t\td.callback(x.status,x.responseText);\n\t\t\t\tif (that.q.length===0) that.r=false;\n\t\t\t\tif (that.r) that._request(that.q.shift());\n\t\t\t}\n\t\t}}\n\t\tgId(\"loader\").style.display=\"block\";\n\t\tvar p=\"\";\n\t\tif (req.params instanceof FormData) p=req.params;\n\t\telse if (req.params instanceof Object){\n\t\t\tfor(var key in req.params){\n\t\t\t\tp+=(p===\"\"?(req.method===\"GET\"?\"?\":\"\"):\"&\")+encodeURIComponent(key)+\"=\"+encodeURIComponent(req.params[key]);\n\t\t\t}\n\t\t}\n\t\tthis.x=new XMLHttpRequest();\n\t\tthis.x.onreadystatechange=cb(this.x,req);\n\t\tif (req.method===\"GET\"){ \n\t\t\tthis.x.open(req.method, req.url+p, true);\n\t\t\tthis.x.send();\n\t\t}\n\t\telse{ \n\t\t\tthis.x.open(req.method, req.url, true);\n\t\t\tif (typeof p === \"string\") this.x.setRequestHeader(\"Content-type\",\"application/x-www-form-urlencoded\");\n\t\t\tthis.x.send(p);\n\t\t}\n\t},\n\tadd: function(method,url,params,cb){\n\t\tthis.q.push({url:url,method:method,params:params,callback:cb});\n\t\tif (!this.r) this._request(this.q.shift());\n\t}\n};\nvar req=new QueuedRequester();\nvar globalTree; // Single global reference for tree refresh\n\nfunction loadPreview(filename, editor) {\n\tvar pathField = gId(\"filepath\");\n\tpathField.value = filename;\n\tif (/\\.(png|jpg|jpeg|gif|bmp|webp)$/i.test(filename)) {\n\t\tgId(\"editor\").style.display=\"none\";\n\t\tgId(\"preview\").style.display=\"block\";\n\t\tgId(\"preview\").innerHTML = '<img src=\"/edit?func=edit&path=' + encodeURIComponent(filename) + '&_cb=' + Date.now() + '\">';\n\t} else {\n\t\teditor.loadText(filename);\n\t}\n}\n\nfunction refreshTree() {\n\tif (globalTree) globalTree.refreshPath(\"/\");\n}\n\nfunction createTop(element, editor){\n\tvar input = cE(\"input\");\n\tinput.type = \"file\";\n\tinput.style.display = \"none\"; // Hide the default file input\n\n\t// Create container structure\n\tvar leftDiv = cE(\"div\");\n\tleftDiv.className = \"left\";\n\tvar centerDiv = cE(\"div\");\n\tcenterDiv.className = \"center\";\n\tvar rightDiv = cE(\"div\");\n\trightDiv.className = \"right\";\n\n\t// Single text field for filename\n\tvar path = cE(\"input\");\n\tpath.id = \"filepath\";\n\tpath.type = \"text\";\n\tpath.maxLength = \"31\"; // limit filename length\n\n\tvar uploadBtn = cE(\"button\"); uploadBtn.className = \"sml\"; uploadBtn.innerHTML = \"Upload File\";\n\tvar clearBtn = cE(\"button\"); clearBtn.className = \"sml\"; clearBtn.innerHTML = \"Clear\";\n\tvar saveBtn = cE(\"button\"); saveBtn.className = \"sml\"; saveBtn.innerHTML = \"Save\";\n\tvar backBtn = cE(\"button\"); backBtn.className = \"sml\"; backBtn.innerHTML = \"Back to the controls\"; backBtn.onclick = function(){ window.location.href = getURL(\"/\"); };\n\n\t// Add elements to Top\n\tleftDiv.appendChild(path);\n\tleftDiv.appendChild(clearBtn);\n\tleftDiv.appendChild(saveBtn);\n\tleftDiv.appendChild(uploadBtn);\n\tcenterDiv.innerHTML = \"WLED File Editor\";\n\trightDiv.appendChild(backBtn);\n\tgId(element).appendChild(input);\n\tgId(element).appendChild(leftDiv);\n\tgId(element).appendChild(centerDiv);\n\tgId(element).appendChild(rightDiv);\n\n\teditor.clearEditor();\n\n\tuploadBtn.onclick = function() { input.click(); }; // invokes file selector\n\n\tfunction httpPostCb(st,resp){\n\t\tif (st!=200) alert(\"ERROR \"+st+\": \"+resp);\n\t\telse {\n\t\t\tshowToast(\"Upload successful!\");\n\t\t\trefreshTree();\n\t\t}\n\t}\n\n\t// Clear button - clears the editor\n\tclearBtn.onclick = function(){\n\t\teditor.clearEditor(); // Clear editor, file will be created on save\n\t\tinput.value = \"\"; // Clear the file selection\n\t};\n\n\tsaveBtn.onclick = function(){ editor.save(); };\n\n\t// Handle file selection and upload\n\tinput.onchange = function(){\n\t\tif (input.files.length == 0) return;\n\t\tvar file = input.files[0];\n\t\tvar fd = new FormData();\n\t\tfd.append(\"file\", file, file.name);\n\t\tconsole.log(\"Uploading:\", file.name);\n\t\treq.add(\"POST\", \"/upload\", fd, function(st, resp) {\n\t\t\thttpPostCb(st, resp);\n\t\t\tif(st == 200) { loadPreview(file.name, editor); }\n\t\t});\n\t\tinput.value = \"\"; // Clear the file selection\n\t};\n}\n\nfunction createTree(element, editor){\n\tvar treeRoot=cE(\"div\");\n\tgId(element).appendChild(treeRoot);\n\tvar menuCleanup = null; // Track menu cleanup function\n\n\tfunction downloadFile(p){\n\t\twindow.open(getURL(\"/edit\") + \"?func=download&path=\" + encodeURIComponent(p), \"_blank\");\n\t}\n\n\tfunction deleteFile(p) {\n\t\tif (!confirm(\"Delete \" + p + \"?\")) return;\n\t\treq.add(\"GET\", getURL(\"/edit\"), { func:\"delete\", path:p }, function(st, resp) {\n\t\t\tif (st != 200) alert(\"ERROR \" + st + \": \" + resp);\n\t\t\telse refreshTree();\n\t\t});\n\t}\n\n\tfunction createLeaf(path,name,size){\n\t\tvar leaf=cE(\"div\");\n\t\tleaf.style.cssText=\"cursor:pointer;padding:2px 4px;border-radius:2px;position:relative\";\n\t\tleaf.textContent=name;\n\t\tvar span = cE(\"span\");\n\t\tspan.style.cssText = \"font-size: 14px; color: #aaa; margin-left: 8px;\";\n\t\tspan.textContent =  (size > 0 ? Math.max(0.1, (size / 1024)).toFixed(1) : 0) + \"KB\"; // show size in KB, minimum 0.1 to not show 0KB for small files\n\t\tleaf.appendChild(span);\n\t\tleaf.onmouseover=function(){ leaf.style.background=\"#333\"; };\n\t\tleaf.onmouseout=function(){ leaf.style.background=\"\"; };\n\t\tleaf.onclick=function(){ loadPreview(name, editor); };\n\n\t\t// Right-click context menu\n\t\tleaf.oncontextmenu = function(e) {\n\t\t\te.preventDefault();\n\t\t\t// Clean up previous menu\n\t\t\tif (menuCleanup) menuCleanup();\n\t\t\tvar menu = cE(\"div\");\n\t\t\tmenu.id = \"context-menu\";\n\t\t\tmenu.style.cssText = \"position:fixed;left:\"+e.clientX+\"px;top:\"+e.clientY+\"px;background:#333;border:1px solid #666;border-radius:4px;z-index:1000;box-shadow:2px 2px 8px rgba(0,0,0,0.5)\";\n\n\t\t\tfunction createOption(text, color, handler) {\n\t\t\t\tvar opt = cE(\"div\");\n\t\t\t\topt.textContent = text;\n\t\t\t\topt.style.cssText = \"padding:8px 12px;cursor:pointer;color:\" + color;\n\t\t\t\topt.onmouseover = function() { this.style.background = \"#555\"; };\n\t\t\t\topt.onmouseout = function() { this.style.background = \"\"; };\n\t\t\t\topt.onclick = function() { handler(); cleanup(); };\n\t\t\t\treturn opt;\n\t\t\t}\n\n\t\t\tmenu.appendChild(createOption(\"Download\", \"#fff\", function(){ downloadFile(path); }));\n\t\t\tmenu.appendChild(createOption(\"Delete\", \"#f66\", function(){ deleteFile(path); }));\n\t\t\td.body.appendChild(menu);\n\n\t\t\tfunction cleanup() {\n\t\t\t\tif (menu.parentNode) menu.remove();\n\t\t\t\td.onclick = null;\n\t\t\t\tmenuCleanup = null;\n\t\t\t}\n\n\t\t\tmenuCleanup = cleanup;\n\t\t\tsetTimeout(function() { d.onclick = cleanup; }, 100);\n\t\t};\n\n\t\ttreeRoot.appendChild(leaf);\n\t}\n\n\tfunction addList(path,items){\n\t\tfor(var i=0;i<items.length;i++){\n\t\t\tif (items[i].type===\"file\" && items[i].name !== \"wsec.json\") { // hide wsec.json, this is redundant on purpose (done in C code too), just in case...\n\t\t\t\tvar fullPath = path === \"/\" ? \"/\" + items[i].name : path + \"/\" + items[i].name;\n\t\t\t\tcreateLeaf(fullPath, items[i].name, items[i].size);\n\t\t\t}\n\t\t}\n\t\tfsMem();\n\t}\n\n\t// add file system memory info and credits at the bottom of the tree\n\tasync function fsMem(){\n\t\ttry{\n\t\t\tconst r=await fetch(getURL(\"/json/info\"));\n\t\t\tconst info=await r.json();\n\t\t\tvar c=cE(\"div\");\n\t\t  c.style.cssText=\"font-size:12px;color:#aaa;margin-top:40px\";\n\t\t\tc.textContent=\"by @dedehai\";\n\t\t\tif(info&&info.fs){\n\t\t\t\tc.textContent=`by @dedehai | Memory: ${info.fs.u} KB / ${info.fs.t} KB`;\n\t\t\t}\n\t\t\ttreeRoot.appendChild(c);\n\t\t}catch(e){console.error(e);}\n\t}\n\n\tfunction getCb(p){\n\t\treturn function(st,resp){\n\t\t\tif (st==200) {\n\t\t\t\ttry {\n\t\t\t\t\taddList(p, JSON.parse(resp));\n\t\t\t\t} catch(e) {\n\t\t\t\t\tconsole.error(\"Error parsing file list:\", e);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconsole.error(\"Error loading file list:\", st, resp);\n\t\t\t}\n\t\t};\n\t}\n\n\tfunction httpGet(p){\n\t\treq.add(\"GET\", \"/edit\", { func:\"list\", path:p }, getCb(p));\n\t}\n\n\tthis.refreshPath=function(p){\n\t\ttreeRoot.innerHTML=\"\";\n\t\thttpGet(\"/\");\n\t};\n\n\thttpGet(\"/\");\n\treturn this;\n}\n\n// Pretty-print ledmapX.json: print 2D maps in aligned columns, print 1D maps as single line\nfunction prettyLedmap(json){\n\ttry {\n\t\tlet obj = JSON.parse(json);\n\t\tif (!obj.map || !Array.isArray(obj.map)) return JSON.stringify(obj, null, 2);\n\t\tlet width = obj.width || obj.map.length;\n\t\tlet maxLen = Math.max(...obj.map.map(n => String(n).length)); // max length of numbers for padding\n\n\t\tfunction pad(num) {\n\t\t\tlet s = String(num);\n\t\t\twhile (s.length < maxLen) s = \" \" + s;\n\t\t\treturn s;\n\t\t}\n\n\t\tlet rows = [];\n\t\tfor (let i = 0; i < obj.map.length; i += width) {\n\t\t\trows.push(\"    \" + obj.map.slice(i, i + width).map(pad).join(\", \"));\n\t\t}\n\n\t\tlet pretty = \"{\\n\";\n\t\tfor (let k of Object.keys(obj)) {\n\t\t\tif (k !== \"map\") {\n\t\t\t\tpretty += \" \\\"\" + k + \"\\\": \" + JSON.stringify(obj[k]) + \",\\n\"; // print all keys first (speeds up loading)\n\t\t\t}\n\t\t}\n\t\tpretty += \" \\\"map\\\": [\\n\" + rows.join(\",\\n\") + \"\\n  ]\\n}\";\n\t\treturn pretty;\n\t} catch (e) {\n\t\treturn json;\n\t}\n}\n\nfunction createEditor(element,file){\n\tif (!file) file=\"\";\n\n\tvar ta = cE(\"textarea\");\n\tvar editorDiv = cE(\"div\");\n\teditorDiv.id = \"ace-editor\";\n\teditorDiv.style.display = \"none\";\n\n\tgId(element).appendChild(ta);\n\tgId(element).appendChild(editorDiv);\n\n\tvar currentFile = file;\n\tvar aceEditor = null;\n\tvar useAce = false;\n\n\tfunction updateEditorMode() {\n\t\tif (!useAce || !aceEditor) return;\n\n\t\t// Check filename from text field or current file\n\t\tvar pathField = gId(\"filepath\");\n\t\tvar filename = (pathField && pathField.value) ? pathField.value : currentFile;\n\t\taceEditor.session.setMode(filename && (/\\.json$/i.test(filename)) ? \"ace/mode/json\" : \"ace/mode/text\"); // same as filename.toLowerCase().endsWith('.json')\n\t}\n\n\t// Try to initialize Ace editor if available\n\tfunction initAce(){\n\t\tif (useAce || typeof ace === 'undefined') return;\n\t\ttry {\n\t\t\taceEditor = ace.edit(editorDiv);\n\t\t\taceEditor.setTheme(\"ace/theme/monokai\");\n\t\t\taceEditor.session.setMode(\"ace/mode/text\");\n\t\t\taceEditor.setOptions({ fontSize:\"13px\", fontFamily:\"monospace\", showPrintMargin:false, wrap:true });\n\t\t\tuseAce = true;\n\t\t\t//console.log(\"Use Ace editor\");\n\t\t\tswitchToAce();\n\t\t\tupdateEditorMode();\n\n\t\t\t// Monitor filename input for JSON highlighting (prevent duplicate listeners)\n\t\t\tvar pathField = gId(\"filepath\");\n\t\t\tif (pathField && !pathField.jsonListener) {\n\t\t\t\tpathField.oninput = updateEditorMode;\n\t\t\t\tpathField.jsonListener = true;\n\t\t\t}\n\t\t} catch(e) {\n\t\t\t//console.log(\"Ace load failed:\", e);\n\t\t\tuseAce = false;\n\t\t}\n\t}\n\t// Try now and on window load as a fallback\n\tsetTimeout(initAce, 100);\n\twindow.addEventListener('load', initAce);\n\n\tfunction switchToAce() {\n\t\tif (useAce && aceEditor) {\n\t\t\tta.style.display = \"none\";\n\t\t\teditorDiv.style.display = \"block\";\n\t\t\teditorDiv.style.flex = \"1\";\n\t\t\taceEditor.setValue(ta.value, -1);\n\t\t\taceEditor.resize();\n\t\t}\n\t}\n\n\tfunction getContent() {\n\t\treturn (useAce && aceEditor && editorDiv.style.display !== \"none\") ? aceEditor.getValue() : ta.value;\n\t}\n\n\tfunction setContent(content) {\n\t\tta.value = content;\n\t\tif (useAce && aceEditor) aceEditor.setValue(content, -1);\n\t}\n\n\t// Live JSON validation for textarea\n\tta.oninput = function() {\n\t\tvar pathField = gId(\"filepath\");\n\t\tvar filename = pathField ? pathField.value : currentFile;\n\t\tvar border = \"2px solid #333\";\n\n\t\tif (filename && (/\\.json$/i.test(filename))) { // same as filename.toLowerCase().endsWith('.json')\n\t\t\ttry {\n\t\t\t\tJSON.parse(ta.value);\n\t\t\t} catch(e) {\n\t\t\t\tborder = \"2px solid #f00\";\n\t\t\t}\n\t\t}\n\t\tta.style.border = border;\n\t};\n\n\tfunction saveFile(filename,data){\n\t\tvar outdata = data;\n\t\tif (/\\.json$/i.test(filename)) { // same as filename.toLowerCase().endsWith('.json')\n\t\t\ttry {\n\t\t\t\toutdata = JSON.stringify(JSON.parse(data)); // validate and minify\n\t\t\t} catch(e) {\n\t\t\t\talert(\"Invalid JSON! Please fix.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tuploadFile({files: [new Blob([outdata], {type:\"text/plain\"})]}, filename, function(s) {\n\t\t\tif(s) {\n\t\t\t\trefreshTree();\n\t\t\t\tloadFile(filename); // (re)load if saved successfully to update formating or show file content\n\t\t\t}\n\t\t});\n\t}\n\n\tfunction loadFile(filename){\n\t\tif (!filename) return;\n\t\treq.add(\"GET\", \"/edit\", { func:\"edit\", path:filename }, function(st, resp) {\n\t\t\tgId(\"preview\").style.display=\"none\";\n\t\t\tgId(\"editor\").style.display=\"flex\";\n\t\t\tif (st==200) {\n\t\t\t\tif ((/\\.json$/i.test(filename))) { // same as filename.toLowerCase().endsWith('.json')\n\t\t\t\t\ttry {\n\t\t\t\t\t\tsetContent(/ledmap/i.test(filename) ? prettyLedmap(resp) : JSON.stringify(JSON.parse(resp), null, 2)); // pretty-print ledmap files (i.e. if file name includes \"ledmap\" case-insensitive)\n\t\t\t\t\t} catch(e) {\n\t\t\t\t\t\tsetContent(resp);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tsetContent(resp);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tsetContent(\"\");\n\t\t\t}\n\t\t\tcurrentFile = filename;\n\t\t\tupdateEditorMode();\n\t\t});\n\t}\n\n\tif (file) loadFile(file);\n\n\treturn {\n\t\tsave:function(){\n\t\t\tvar pathField = gId(\"filepath\");\n\t\t\tvar fn = pathField ? pathField.value : \"\";\n\t\t\tif (!fn) {\n\t\t\t\talert(\"Please enter a filename!\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!fn.startsWith(\"/\")) fn = \"/\" + fn;\n\t\t\tcurrentFile = fn; // Update current file\n\t\t\tsaveFile(fn, getContent()) \n\t\t},\n\t\tloadText:function(fn){\n\t\t\tcurrentFile=fn;\n\t\t\tvar pathField = gId(\"filepath\");\n\t\t\tif (pathField && fn) {\n\t\t\t\tpathField.value = fn.startsWith(\"/\") ? fn.substring(1) : fn;\n\t\t\t}\n\t\t\tloadFile(fn);\n\t\t},\n\t\tclearEditor:function(){\n\t\t\tgId(\"preview\").style.display=\"none\";\n\t\t\tgId(\"editor\").style.display=\"flex\";\n\t\t\t// Update filename in text field\n\t\t\tsetContent(\"\");\n\t\t\tvar pathField = gId(\"filepath\");\n\t\t\tpathField.value = \"\";\n\t\t\tpathField.placeholder = \"Filename to save\";\n\t\t\tupdateEditorMode();\n\t\t}\n\t};\n}\n\nfunction S(){\n\tvar vars={};\n\twindow.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi,function(m,k,v){\n\t\tvars[decodeURIComponent(k)]=decodeURIComponent(v);\n\t});\n\n\tvar editor=createEditor(\"editor\",vars.file);\n\tglobalTree=createTree(\"tree\",editor);\n\tcreateTop(\"top\",editor);\n\t// Add Ctrl+S / Cmd+S override to save the file\n\tdocument.addEventListener('keydown', function(e) {\n\t\tif ((e.ctrlKey || e.metaKey) && e.key === 's') {\n\t\t\te.preventDefault();\n\t\t\teditor.save();\n\t\t}\n\t});\n}\n</script>\n</head>\n<body>\n\t<div id=\"toast\"></div>\n\t<div id=\"loader\"><div class=\"loader\"></div></div>\n\t<div id=\"top\"></div>\n\t<div style=\"flex:1;position:relative\">\n\t\t<div id=\"tree\">\n\t\t</div>\n\t\t<div id=\"editor\"></div>\n\t\t<div id=\"preview\"></div>\n\t</div>\n</body>\n</html>"
  },
  {
    "path": "wled00/data/icons-ui/HowTo_AddNewIcons.txt",
    "content": "To edit the current font, this is the workflow:\n\ngo to https://icomoon.io/\nIn the menu, go to manage projects and import the json file from this folder and load it\nAdd new icons or exchange existing ones: if changing existing one, make sure the unicode stays the same (can be edited before exporting)\nGo to \"Generate SVG & More\" and check the size of new icons (clicking on icons brings up the editor) -> scale new icons to match the size of existing ones\nGo to \"Generate font\" tab, check unicodes are correct (can use any unicode, range > e900 is \"custom range\" and now preferred)\nDownload the font package and replace the files in this folder with new files\nUsing an online converter, convert the *.woff font into woff2 format (about half the file size)\nUsing another online converter, convert the woff2 font to base64 encoding for CSS\nin index.css, replace the font string at the top, keep the \"data:font/woff2;charset=utf-8;\" and dont use octet-stream (browser compatibility).\n\nenjoy your new icons in the UI :)\n\n"
  },
  {
    "path": "wled00/data/icons-ui/Read Me.txt",
    "content": "Open *demo.html* to see a list of all the glyphs in your font along with their codes/ligatures.\n\nTo use the generated font in desktop programs, you can install the TTF font. In order to copy the character associated with each icon, refer to the text box at the bottom right corner of each glyph in demo.html. The character inside this text box may be invisible; but it can still be copied. See this guide for more info: https://icomoon.io/docs#install\n\nYou won't need any of the files located under the *demo-files* directory when including the generated font in your own projects.\n\nYou can import *selection.json* back to the IcoMoon app using the *Import Icons* button (or via Main Menu → Manage Projects) to retrieve your icon selection.\n"
  },
  {
    "path": "wled00/data/icons-ui/demo-files/demo.css",
    "content": "body {\n  padding: 0;\n  margin: 0;\n  font-family: sans-serif;\n  font-size: 1em;\n  line-height: 1.5;\n  color: #555;\n  background: #fff;\n}\nh1 {\n  font-size: 1.5em;\n  font-weight: normal;\n}\nsmall {\n  font-size: .66666667em;\n}\na {\n  color: #e74c3c;\n  text-decoration: none;\n}\na:hover, a:focus {\n  box-shadow: 0 1px #e74c3c;\n}\n.bshadow0, input {\n  box-shadow: inset 0 -2px #e7e7e7;\n}\ninput:hover {\n  box-shadow: inset 0 -2px #ccc;\n}\ninput, fieldset {\n  font-family: sans-serif;\n  font-size: 1em;\n  margin: 0;\n  padding: 0;\n  border: 0;\n}\ninput {\n  color: inherit;\n  line-height: 1.5;\n  height: 1.5em;\n  padding: .25em 0;\n}\ninput:focus {\n  outline: none;\n  box-shadow: inset 0 -2px #449fdb;\n}\n.glyph {\n  font-size: 16px;\n  width: 15em;\n  padding-bottom: 1em;\n  margin-right: 4em;\n  margin-bottom: 1em;\n  float: left;\n  overflow: hidden;\n}\n.liga {\n  width: 80%;\n  width: calc(100% - 2.5em);\n}\n.talign-right {\n  text-align: right;\n}\n.talign-center {\n  text-align: center;\n}\n.bgc1 {\n  background: #f1f1f1;\n}\n.fgc1 {\n  color: #999;\n}\n.fgc0 {\n  color: #000;\n}\np {\n  margin-top: 1em;\n  margin-bottom: 1em;\n}\n.mvm {\n  margin-top: .75em;\n  margin-bottom: .75em;\n}\n.mtn {\n  margin-top: 0;\n}\n.mtl, .mal {\n  margin-top: 1.5em;\n}\n.mbl, .mal {\n  margin-bottom: 1.5em;\n}\n.mal, .mhl {\n  margin-left: 1.5em;\n  margin-right: 1.5em;\n}\n.mhmm {\n  margin-left: 1em;\n  margin-right: 1em;\n}\n.mls {\n  margin-left: .25em;\n}\n.ptl {\n  padding-top: 1.5em;\n}\n.pbs, .pvs {\n  padding-bottom: .25em;\n}\n.pvs, .pts {\n  padding-top: .25em;\n}\n.unit {\n  float: left;\n}\n.unitRight {\n  float: right;\n}\n.size1of2 {\n  width: 50%;\n}\n.size1of1 {\n  width: 100%;\n}\n.clearfix:before, .clearfix:after {\n  content: \" \";\n  display: table;\n}\n.clearfix:after {\n  clear: both;\n}\n.hidden-true {\n  display: none;\n}\n.textbox0 {\n  width: 3em;\n  background: #f1f1f1;\n  padding: .25em .5em;\n  line-height: 1.5;\n  height: 1.5em;\n}\n#testDrive {\n  display: block;\n  padding-top: 24px;\n  line-height: 1.5;\n}\n.fs0 {\n  font-size: 16px;\n}\n.fs1 {\n  font-size: 48px;\n}\n.fs2 {\n  font-size: 28px;\n}\n.fs3 {\n  font-size: 32px;\n}\n\n"
  },
  {
    "path": "wled00/data/icons-ui/demo-files/demo.js",
    "content": "if (!('boxShadow' in document.body.style)) {\n    document.body.setAttribute('class', 'noBoxShadow');\n}\n\ndocument.body.addEventListener(\"click\", function(e) {\n    var target = e.target;\n    if (target.tagName === \"INPUT\" &&\n        target.getAttribute('class').indexOf('liga') === -1) {\n        target.select();\n    }\n});\n\n(function() {\n    var fontSize = document.getElementById('fontSize'),\n        testDrive = document.getElementById('testDrive'),\n        testText = document.getElementById('testText');\n    function updateTest() {\n        testDrive.innerHTML = testText.value || String.fromCharCode(160);\n        if (window.icomoonLiga) {\n            window.icomoonLiga(testDrive);\n        }\n    }\n    function updateSize() {\n        testDrive.style.fontSize = fontSize.value + 'px';\n    }\n    fontSize.addEventListener('change', updateSize, false);\n    testText.addEventListener('input', updateTest, false);\n    testText.addEventListener('change', updateTest, false);\n    updateSize();\n}());\n"
  },
  {
    "path": "wled00/data/icons-ui/demo.html",
    "content": "<!doctype html>\n<html>\n<head>\n    <meta charset=\"utf-8\">\n    <title>IcoMoon Demo</title>\n    <meta name=\"description\" content=\"An Icon Font Generated By IcoMoon.io\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <link rel=\"stylesheet\" href=\"demo-files/demo.css\">\n    <link rel=\"stylesheet\" href=\"style.css\"></head>\n<body>\n    <div class=\"bgc1 clearfix\">\n        <h1 class=\"mhmm mvm\"><span class=\"fgc1\">Font Name:</span> wled122 <small class=\"fgc1\">(Glyphs:&nbsp;25)</small></h1>\n    </div>\n    <div class=\"clearfix mhl ptl\">\n        <h1 class=\"mvm mtn fgc1\">Grid Size: 16</h1>\n        <div class=\"glyph fs1\">\n            <div class=\"clearfix bshadow0 pbs\">\n                <span class=\"i-pixelforge\"></span>\n                <span class=\"mls\"> i-pixelforge</span>\n            </div>\n            <fieldset class=\"fs0 size1of1 clearfix hidden-false\">\n                <input type=\"text\" readonly value=\"e900\" class=\"unit size1of2\" />\n                <input type=\"text\" maxlength=\"1\" readonly value=\"&#xe900;\" class=\"unitRight size1of2 talign-right\" />\n            </fieldset>\n            <div class=\"fs0 bshadow0 clearfix hidden-true\">\n                <span class=\"unit pvs fgc1\">liga: </span>\n                <input type=\"text\" readonly value=\"\" class=\"liga unitRight\" />\n            </div>\n        </div>\n    </div>\n    <div class=\"clearfix mhl ptl\">\n        <h1 class=\"mvm mtn fgc1\">Grid Size: 14</h1>\n        <div class=\"glyph fs2\">\n            <div class=\"clearfix bshadow0 pbs\">\n                <span class=\"i-editor\"></span>\n                <span class=\"mls\"> i-editor</span>\n            </div>\n            <fieldset class=\"fs0 size1of1 clearfix hidden-false\">\n                <input type=\"text\" readonly value=\"e901\" class=\"unit size1of2\" />\n                <input type=\"text\" maxlength=\"1\" readonly value=\"&#xe901;\" class=\"unitRight size1of2 talign-right\" />\n            </fieldset>\n            <div class=\"fs0 bshadow0 clearfix hidden-true\">\n                <span class=\"unit pvs fgc1\">liga: </span>\n                <input type=\"text\" readonly value=\"\" class=\"liga unitRight\" />\n            </div>\n        </div>\n    </div>\n    <div class=\"clearfix mhl ptl\">\n        <h1 class=\"mvm mtn fgc1\">Grid Size: Unknown</h1>\n        <div class=\"glyph fs3\">\n            <div class=\"clearfix bshadow0 pbs\">\n                <span class=\"i-pattern\"></span>\n                <span class=\"mls\"> i-pattern</span>\n            </div>\n            <fieldset class=\"fs0 size1of1 clearfix hidden-false\">\n                <input type=\"text\" readonly value=\"e23d\" class=\"unit size1of2\" />\n                <input type=\"text\" maxlength=\"1\" readonly value=\"&#xe23d;\" class=\"unitRight size1of2 talign-right\" />\n            </fieldset>\n            <div class=\"fs0 bshadow0 clearfix hidden-true\">\n                <span class=\"unit pvs fgc1\">liga: </span>\n                <input type=\"text\" readonly value=\"\" class=\"liga unitRight\" />\n            </div>\n        </div>\n        <div class=\"glyph fs3\">\n            <div class=\"clearfix bshadow0 pbs\">\n                <span class=\"i-segments\"></span>\n                <span class=\"mls\"> i-segments</span>\n            </div>\n            <fieldset class=\"fs0 size1of1 clearfix hidden-false\">\n                <input type=\"text\" readonly value=\"e34b\" class=\"unit size1of2\" />\n                <input type=\"text\" maxlength=\"1\" readonly value=\"&#xe34b;\" class=\"unitRight size1of2 talign-right\" />\n            </fieldset>\n            <div class=\"fs0 bshadow0 clearfix hidden-true\">\n                <span class=\"unit pvs fgc1\">liga: </span>\n                <input type=\"text\" readonly value=\"\" class=\"liga unitRight\" />\n            </div>\n        </div>\n        <div class=\"glyph fs3\">\n            <div class=\"clearfix bshadow0 pbs\">\n                <span class=\"i-sun\"></span>\n                <span class=\"mls\"> i-sun</span>\n            </div>\n            <fieldset class=\"fs0 size1of1 clearfix hidden-false\">\n                <input type=\"text\" readonly value=\"e333\" class=\"unit size1of2\" />\n                <input type=\"text\" maxlength=\"1\" readonly value=\"&#xe333;\" class=\"unitRight size1of2 talign-right\" />\n            </fieldset>\n            <div class=\"fs0 bshadow0 clearfix hidden-true\">\n                <span class=\"unit pvs fgc1\">liga: </span>\n                <input type=\"text\" readonly value=\"\" class=\"liga unitRight\" />\n            </div>\n        </div>\n        <div class=\"glyph fs3\">\n            <div class=\"clearfix bshadow0 pbs\">\n                <span class=\"i-palette\"></span>\n                <span class=\"mls\"> i-palette</span>\n            </div>\n            <fieldset class=\"fs0 size1of1 clearfix hidden-false\">\n                <input type=\"text\" readonly value=\"e2b3\" class=\"unit size1of2\" />\n                <input type=\"text\" maxlength=\"1\" readonly value=\"&#xe2b3;\" class=\"unitRight size1of2 talign-right\" />\n            </fieldset>\n            <div class=\"fs0 bshadow0 clearfix hidden-true\">\n                <span class=\"unit pvs fgc1\">liga: </span>\n                <input type=\"text\" readonly value=\"\" class=\"liga unitRight\" />\n            </div>\n        </div>\n        <div class=\"glyph fs3\">\n            <div class=\"clearfix bshadow0 pbs\">\n                <span class=\"i-eye\"></span>\n                <span class=\"mls\"> i-eye</span>\n            </div>\n            <fieldset class=\"fs0 size1of1 clearfix hidden-false\">\n                <input type=\"text\" readonly value=\"e0e8\" class=\"unit size1of2\" />\n                <input type=\"text\" maxlength=\"1\" readonly value=\"&#xe0e8;\" class=\"unitRight size1of2 talign-right\" />\n            </fieldset>\n            <div class=\"fs0 bshadow0 clearfix hidden-true\">\n                <span class=\"unit pvs fgc1\">liga: </span>\n                <input type=\"text\" readonly value=\"\" class=\"liga unitRight\" />\n            </div>\n        </div>\n        <div class=\"glyph fs3\">\n            <div class=\"clearfix bshadow0 pbs\">\n                <span class=\"i-speed\"></span>\n                <span class=\"mls\"> i-speed</span>\n            </div>\n            <fieldset class=\"fs0 size1of1 clearfix hidden-false\">\n                <input type=\"text\" readonly value=\"e325\" class=\"unit size1of2\" />\n                <input type=\"text\" maxlength=\"1\" readonly value=\"&#xe325;\" class=\"unitRight size1of2 talign-right\" />\n            </fieldset>\n            <div class=\"fs0 bshadow0 clearfix hidden-true\">\n                <span class=\"unit pvs fgc1\">liga: </span>\n                <input type=\"text\" readonly value=\"\" class=\"liga unitRight\" />\n            </div>\n        </div>\n        <div class=\"glyph fs3\">\n            <div class=\"clearfix bshadow0 pbs\">\n                <span class=\"i-expand\"></span>\n                <span class=\"mls\"> i-expand</span>\n            </div>\n            <fieldset class=\"fs0 size1of1 clearfix hidden-false\">\n                <input type=\"text\" readonly value=\"e395\" class=\"unit size1of2\" />\n                <input type=\"text\" maxlength=\"1\" readonly value=\"&#xe395;\" class=\"unitRight size1of2 talign-right\" />\n            </fieldset>\n            <div class=\"fs0 bshadow0 clearfix hidden-true\">\n                <span class=\"unit pvs fgc1\">liga: </span>\n                <input type=\"text\" readonly value=\"\" class=\"liga unitRight\" />\n            </div>\n        </div>\n        <div class=\"glyph fs3\">\n            <div class=\"clearfix bshadow0 pbs\">\n                <span class=\"i-power\"></span>\n                <span class=\"mls\"> i-power</span>\n            </div>\n            <fieldset class=\"fs0 size1of1 clearfix hidden-false\">\n                <input type=\"text\" readonly value=\"e08f\" class=\"unit size1of2\" />\n                <input type=\"text\" maxlength=\"1\" readonly value=\"&#xe08f;\" class=\"unitRight size1of2 talign-right\" />\n            </fieldset>\n            <div class=\"fs0 bshadow0 clearfix hidden-true\">\n                <span class=\"unit pvs fgc1\">liga: </span>\n                <input type=\"text\" readonly value=\"\" class=\"liga unitRight\" />\n            </div>\n        </div>\n        <div class=\"glyph fs3\">\n            <div class=\"clearfix bshadow0 pbs\">\n                <span class=\"i-settings\"></span>\n                <span class=\"mls\"> i-settings</span>\n            </div>\n            <fieldset class=\"fs0 size1of1 clearfix hidden-false\">\n                <input type=\"text\" readonly value=\"e0a2\" class=\"unit size1of2\" />\n                <input type=\"text\" maxlength=\"1\" readonly value=\"&#xe0a2;\" class=\"unitRight size1of2 talign-right\" />\n            </fieldset>\n            <div class=\"fs0 bshadow0 clearfix hidden-true\">\n                <span class=\"unit pvs fgc1\">liga: </span>\n                <input type=\"text\" readonly value=\"\" class=\"liga unitRight\" />\n            </div>\n        </div>\n        <div class=\"glyph fs3\">\n            <div class=\"clearfix bshadow0 pbs\">\n                <span class=\"i-playlist\"></span>\n                <span class=\"mls\"> i-playlist</span>\n            </div>\n            <fieldset class=\"fs0 size1of1 clearfix hidden-false\">\n                <input type=\"text\" readonly value=\"e139\" class=\"unit size1of2\" />\n                <input type=\"text\" maxlength=\"1\" readonly value=\"&#xe139;\" class=\"unitRight size1of2 talign-right\" />\n            </fieldset>\n            <div class=\"fs0 bshadow0 clearfix hidden-true\">\n                <span class=\"unit pvs fgc1\">liga: </span>\n                <input type=\"text\" readonly value=\"\" class=\"liga unitRight\" />\n            </div>\n        </div>\n        <div class=\"glyph fs3\">\n            <div class=\"clearfix bshadow0 pbs\">\n                <span class=\"i-night\"></span>\n                <span class=\"mls\"> i-night</span>\n            </div>\n            <fieldset class=\"fs0 size1of1 clearfix hidden-false\">\n                <input type=\"text\" readonly value=\"e2a2\" class=\"unit size1of2\" />\n                <input type=\"text\" maxlength=\"1\" readonly value=\"&#xe2a2;\" class=\"unitRight size1of2 talign-right\" />\n            </fieldset>\n            <div class=\"fs0 bshadow0 clearfix hidden-true\">\n                <span class=\"unit pvs fgc1\">liga: </span>\n                <input type=\"text\" readonly value=\"\" class=\"liga unitRight\" />\n            </div>\n        </div>\n        <div class=\"glyph fs3\">\n            <div class=\"clearfix bshadow0 pbs\">\n                <span class=\"i-cancel\"></span>\n                <span class=\"mls\"> i-cancel</span>\n            </div>\n            <fieldset class=\"fs0 size1of1 clearfix hidden-false\">\n                <input type=\"text\" readonly value=\"e38f\" class=\"unit size1of2\" />\n                <input type=\"text\" maxlength=\"1\" readonly value=\"&#xe38f;\" class=\"unitRight size1of2 talign-right\" />\n            </fieldset>\n            <div class=\"fs0 bshadow0 clearfix hidden-true\">\n                <span class=\"unit pvs fgc1\">liga: </span>\n                <input type=\"text\" readonly value=\"\" class=\"liga unitRight\" />\n            </div>\n        </div>\n        <div class=\"glyph fs3\">\n            <div class=\"clearfix bshadow0 pbs\">\n                <span class=\"i-sync\"></span>\n                <span class=\"mls\"> i-sync</span>\n            </div>\n            <fieldset class=\"fs0 size1of1 clearfix hidden-false\">\n                <input type=\"text\" readonly value=\"e116\" class=\"unit size1of2\" />\n                <input type=\"text\" maxlength=\"1\" readonly value=\"&#xe116;\" class=\"unitRight size1of2 talign-right\" />\n            </fieldset>\n            <div class=\"fs0 bshadow0 clearfix hidden-true\">\n                <span class=\"unit pvs fgc1\">liga: </span>\n                <input type=\"text\" readonly value=\"\" class=\"liga unitRight\" />\n            </div>\n        </div>\n        <div class=\"glyph fs3\">\n            <div class=\"clearfix bshadow0 pbs\">\n                <span class=\"i-confirm\"></span>\n                <span class=\"mls\"> i-confirm</span>\n            </div>\n            <fieldset class=\"fs0 size1of1 clearfix hidden-false\">\n                <input type=\"text\" readonly value=\"e390\" class=\"unit size1of2\" />\n                <input type=\"text\" maxlength=\"1\" readonly value=\"&#xe390;\" class=\"unitRight size1of2 talign-right\" />\n            </fieldset>\n            <div class=\"fs0 bshadow0 clearfix hidden-true\">\n                <span class=\"unit pvs fgc1\">liga: </span>\n                <input type=\"text\" readonly value=\"\" class=\"liga unitRight\" />\n            </div>\n        </div>\n        <div class=\"glyph fs3\">\n            <div class=\"clearfix bshadow0 pbs\">\n                <span class=\"i-brightness\"></span>\n                <span class=\"mls\"> i-brightness</span>\n            </div>\n            <fieldset class=\"fs0 size1of1 clearfix hidden-false\">\n                <input type=\"text\" readonly value=\"e2a6\" class=\"unit size1of2\" />\n                <input type=\"text\" maxlength=\"1\" readonly value=\"&#xe2a6;\" class=\"unitRight size1of2 talign-right\" />\n            </fieldset>\n            <div class=\"fs0 bshadow0 clearfix hidden-true\">\n                <span class=\"unit pvs fgc1\">liga: </span>\n                <input type=\"text\" readonly value=\"\" class=\"liga unitRight\" />\n            </div>\n        </div>\n        <div class=\"glyph fs3\">\n            <div class=\"clearfix bshadow0 pbs\">\n                <span class=\"i-nodes\"></span>\n                <span class=\"mls\"> i-nodes</span>\n            </div>\n            <fieldset class=\"fs0 size1of1 clearfix hidden-false\">\n                <input type=\"text\" readonly value=\"e22d\" class=\"unit size1of2\" />\n                <input type=\"text\" maxlength=\"1\" readonly value=\"&#xe22d;\" class=\"unitRight size1of2 talign-right\" />\n            </fieldset>\n            <div class=\"fs0 bshadow0 clearfix hidden-true\">\n                <span class=\"unit pvs fgc1\">liga: </span>\n                <input type=\"text\" readonly value=\"\" class=\"liga unitRight\" />\n            </div>\n        </div>\n        <div class=\"glyph fs3\">\n            <div class=\"clearfix bshadow0 pbs\">\n                <span class=\"i-add\"></span>\n                <span class=\"mls\"> i-add</span>\n            </div>\n            <fieldset class=\"fs0 size1of1 clearfix hidden-false\">\n                <input type=\"text\" readonly value=\"e18a\" class=\"unit size1of2\" />\n                <input type=\"text\" maxlength=\"1\" readonly value=\"&#xe18a;\" class=\"unitRight size1of2 talign-right\" />\n            </fieldset>\n            <div class=\"fs0 bshadow0 clearfix hidden-true\">\n                <span class=\"unit pvs fgc1\">liga: </span>\n                <input type=\"text\" readonly value=\"\" class=\"liga unitRight\" />\n            </div>\n        </div>\n        <div class=\"glyph fs3\">\n            <div class=\"clearfix bshadow0 pbs\">\n                <span class=\"i-edit\"></span>\n                <span class=\"mls\"> i-edit</span>\n            </div>\n            <fieldset class=\"fs0 size1of1 clearfix hidden-false\">\n                <input type=\"text\" readonly value=\"e2c6\" class=\"unit size1of2\" />\n                <input type=\"text\" maxlength=\"1\" readonly value=\"&#xe2c6;\" class=\"unitRight size1of2 talign-right\" />\n            </fieldset>\n            <div class=\"fs0 bshadow0 clearfix hidden-true\">\n                <span class=\"unit pvs fgc1\">liga: </span>\n                <input type=\"text\" readonly value=\"\" class=\"liga unitRight\" />\n            </div>\n        </div>\n        <div class=\"glyph fs3\">\n            <div class=\"clearfix bshadow0 pbs\">\n                <span class=\"i-intensity\"></span>\n                <span class=\"mls\"> i-intensity</span>\n            </div>\n            <fieldset class=\"fs0 size1of1 clearfix hidden-false\">\n                <input type=\"text\" readonly value=\"e409\" class=\"unit size1of2\" />\n                <input type=\"text\" maxlength=\"1\" readonly value=\"&#xe409;\" class=\"unitRight size1of2 talign-right\" />\n            </fieldset>\n            <div class=\"fs0 bshadow0 clearfix hidden-true\">\n                <span class=\"unit pvs fgc1\">liga: </span>\n                <input type=\"text\" readonly value=\"\" class=\"liga unitRight\" />\n            </div>\n        </div>\n        <div class=\"glyph fs3\">\n            <div class=\"clearfix bshadow0 pbs\">\n                <span class=\"i-star\"></span>\n                <span class=\"mls\"> i-star</span>\n            </div>\n            <fieldset class=\"fs0 size1of1 clearfix hidden-false\">\n                <input type=\"text\" readonly value=\"e410\" class=\"unit size1of2\" />\n                <input type=\"text\" maxlength=\"1\" readonly value=\"&#xe410;\" class=\"unitRight size1of2 talign-right\" />\n            </fieldset>\n            <div class=\"fs0 bshadow0 clearfix hidden-true\">\n                <span class=\"unit pvs fgc1\">liga: </span>\n                <input type=\"text\" readonly value=\"\" class=\"liga unitRight\" />\n            </div>\n        </div>\n        <div class=\"glyph fs3\">\n            <div class=\"clearfix bshadow0 pbs\">\n                <span class=\"i-info\"></span>\n                <span class=\"mls\"> i-info</span>\n            </div>\n            <fieldset class=\"fs0 size1of1 clearfix hidden-false\">\n                <input type=\"text\" readonly value=\"e066\" class=\"unit size1of2\" />\n                <input type=\"text\" maxlength=\"1\" readonly value=\"&#xe066;\" class=\"unitRight size1of2 talign-right\" />\n            </fieldset>\n            <div class=\"fs0 bshadow0 clearfix hidden-true\">\n                <span class=\"unit pvs fgc1\">liga: </span>\n                <input type=\"text\" readonly value=\"\" class=\"liga unitRight\" />\n            </div>\n        </div>\n        <div class=\"glyph fs3\">\n            <div class=\"clearfix bshadow0 pbs\">\n                <span class=\"i-del\"></span>\n                <span class=\"mls\"> i-del</span>\n            </div>\n            <fieldset class=\"fs0 size1of1 clearfix hidden-false\">\n                <input type=\"text\" readonly value=\"e037\" class=\"unit size1of2\" />\n                <input type=\"text\" maxlength=\"1\" readonly value=\"&#xe037;\" class=\"unitRight size1of2 talign-right\" />\n            </fieldset>\n            <div class=\"fs0 bshadow0 clearfix hidden-true\">\n                <span class=\"unit pvs fgc1\">liga: </span>\n                <input type=\"text\" readonly value=\"\" class=\"liga unitRight\" />\n            </div>\n        </div>\n        <div class=\"glyph fs3\">\n            <div class=\"clearfix bshadow0 pbs\">\n                <span class=\"i-presets\"></span>\n                <span class=\"mls\"> i-presets</span>\n            </div>\n            <fieldset class=\"fs0 size1of1 clearfix hidden-false\">\n                <input type=\"text\" readonly value=\"e04c\" class=\"unit size1of2\" />\n                <input type=\"text\" maxlength=\"1\" readonly value=\"&#xe04c;\" class=\"unitRight size1of2 talign-right\" />\n            </fieldset>\n            <div class=\"fs0 bshadow0 clearfix hidden-true\">\n                <span class=\"unit pvs fgc1\">liga: </span>\n                <input type=\"text\" readonly value=\"\" class=\"liga unitRight\" />\n            </div>\n        </div>\n    </div>\n\n    <!--[if gt IE 8]><!-->\n    <div class=\"mhl clearfix mbl\">\n        <h1>Font Test Drive</h1>\n        <label>\n            Font Size: <input id=\"fontSize\" type=\"number\" class=\"textbox0 mbm\"\n            min=\"8\" value=\"48\" />\n            px\n        </label>\n        <input id=\"testText\" type=\"text\" class=\"phl size1of1 mvl\"\n        placeholder=\"Type some text to test...\" value=\"\"/>\n        <div id=\"testDrive\" class=\"i-\" style=\"font-family: wled122\">&nbsp;\n        </div>\n    </div>\n    <!--<![endif]-->\n    <div class=\"bgc1 clearfix\">\n        <p class=\"mhl\">Generated by <a href=\"https://icomoon.io/app\">IcoMoon</a></p>\n    </div>\n\n    <script src=\"demo-files/demo.js\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "wled00/data/icons-ui/selection.json",
    "content": "{\"IcoMoonType\":\"selection\",\"icons\":[{\"icon\":{\"paths\":[\"M910.398 765.581l-241.236-241.236c-14.934-14.934-39.371-14.934-54.306 0l-18.102 18.102-147.2-147.2 241.646-241.648h-256.001l-113.645 113.645-11.249-11.247h-54.306v54.306l11.247 11.247-164.848 164.849 127.999 127.999 164.848-164.848 147.2 147.2-18.102 18.102c-14.934 14.934-14.934 39.371 0 54.306l241.236 241.236c14.934 14.934 39.371 14.934 54.306 0l90.509-90.509c14.935-14.934 14.935-39.371 0.002-54.306z\"],\"attrs\":[{}],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"hammer\",\"tool\",\"fix\",\"make\",\"generate\",\"work\",\"build\"],\"grid\":16},\"attrs\":[{}],\"properties\":{\"order\":1,\"id\":0,\"name\":\"pixelforge\",\"prevSize\":48,\"code\":59648},\"setIdx\":0,\"setId\":3,\"iconIdx\":0},{\"icon\":{\"paths\":[\"M976.272 538.191c0 11.223-7.016 22.448-14.5 30.867l-157.14 185.202c-27.126 31.802-82.311 57.055-123.469 57.055h-508.837c-16.837 0-40.688-5.146-40.688-26.191 0-11.223 7.016-22.448 14.5-30.867l157.14-185.202c27.126-31.802 82.311-57.055 123.469-57.055h508.837c16.837 0 40.688 5.146 40.688 26.191zM815.856 377.307v74.828h-389.112c-58.461 0-130.952 33.208-168.835 78.104l-159.949 188.009c0-3.74-0.467-7.951-0.467-11.691v-448.977c0-57.523 47.233-104.761 104.761-104.761h149.66c57.523 0 104.761 47.233 104.761 104.761v14.968h254.418c57.523 0 104.761 47.233 104.761 104.761z\"],\"attrs\":[{}],\"width\":1074,\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"folder-open\"],\"grid\":14},\"attrs\":[{}],\"properties\":{\"order\":1,\"id\":1,\"prevSize\":28,\"name\":\"editor\",\"code\":59649},\"setIdx\":1,\"setId\":2,\"iconIdx\":0},{\"icon\":{\"paths\":[\"M511.573 85.333c235.947 0 427.094 191.147 427.094 426.667s-191.147 426.667-427.094 426.667c-235.52 0-426.24-191.147-426.24-426.667s190.72-426.667 426.24-426.667zM512 853.333c188.587 0 341.333-152.746 341.333-341.333s-152.746-341.333-341.333-341.333-341.333 152.746-341.333 341.333 152.746 341.333 341.333 341.333zM661.333 469.333c-35.413 0-64-28.586-64-64s28.587-64 64-64c35.414 0 64 28.587 64 64s-28.586 64-64 64zM362.667 469.333c-35.414 0-64-28.586-64-64s28.586-64 64-64c35.413 0 64 28.587 64 64s-28.587 64-64 64zM512 746.667c-99.413 0-183.893-62.294-218.027-149.334h436.054c-34.134 87.040-118.614 149.334-218.027 149.334z\"],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"uniE23D\"],\"defaultCode\":57917,\"grid\":0,\"attrs\":[]},\"attrs\":[],\"properties\":{\"id\":11,\"order\":26,\"prevSize\":32,\"code\":57917,\"name\":\"pattern\"},\"setIdx\":2,\"setId\":1,\"iconIdx\":0},{\"icon\":{\"paths\":[\"M511.573 791.040l314.88-244.907 69.547 54.187-384 298.667-384-298.667 69.12-53.76zM512 682.667l-384-298.667 384-298.667 384 298.667-69.973 54.187z\"],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"uniE34B\"],\"defaultCode\":58187,\"grid\":0,\"attrs\":[]},\"attrs\":[],\"properties\":{\"id\":14,\"order\":35,\"ligatures\":\"\",\"prevSize\":32,\"code\":58187,\"name\":\"segments\"},\"setIdx\":2,\"setId\":1,\"iconIdx\":1},{\"icon\":{\"paths\":[\"M288.427 206.507l-60.587 60.16-76.373-76.374 60.16-60.16zM170.667 448v85.333h-128v-85.333h128zM554.667 23.467v125.866h-85.334v-125.866h85.334zM872.533 190.293l-76.373 76.374-60.16-60.16 76.373-76.374zM735.573 774.827l59.734-59.734 76.8 76.374-60.16 60.16zM853.333 448h128v85.333h-128v-85.333zM512 234.667c141.227 0 256 114.773 256 256 0 141.226-114.773 256-256 256s-256-114.774-256-256c0-141.227 114.773-256 256-256zM469.333 957.867v-125.867h85.334v125.867h-85.334zM151.467 791.040l76.373-76.8 60.16 60.16-76.373 76.8z\"],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"uniE333\"],\"defaultCode\":58163,\"grid\":0,\"attrs\":[]},\"attrs\":[],\"properties\":{\"id\":40,\"order\":73,\"ligatures\":\"\",\"prevSize\":32,\"code\":58163,\"name\":\"sun\"},\"setIdx\":2,\"setId\":1,\"iconIdx\":2},{\"icon\":{\"paths\":[\"M512 128c212.053 0 384 152.747 384 341.333 0 117.76-95.573 213.334-213.333 213.334h-75.52c-35.414 0-64 28.586-64 64 0 16.213 6.4 31.146 16.213 42.24 10.24 11.52 16.64 26.453 16.64 43.093 0 35.413-28.587 64-64 64-212.053 0-384-171.947-384-384s171.947-384 384-384zM277.333 512c35.414 0 64-28.587 64-64s-28.586-64-64-64c-35.413 0-64 28.587-64 64s28.587 64 64 64zM405.333 341.333c35.414 0 64-28.586 64-64 0-35.413-28.586-64-64-64-35.413 0-64 28.587-64 64 0 35.414 28.587 64 64 64zM618.667 341.333c35.413 0 64-28.586 64-64 0-35.413-28.587-64-64-64-35.414 0-64 28.587-64 64 0 35.414 28.586 64 64 64zM746.667 512c35.413 0 64-28.587 64-64s-28.587-64-64-64c-35.414 0-64 28.587-64 64s28.586 64 64 64z\"],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"uniE2B3\"],\"defaultCode\":58035,\"grid\":0,\"attrs\":[]},\"attrs\":[],\"properties\":{\"id\":75,\"order\":75,\"prevSize\":32,\"code\":58035,\"name\":\"palette\"},\"setIdx\":2,\"setId\":1,\"iconIdx\":3},{\"icon\":{\"paths\":[\"M512 192c213.333 0 395.52 132.693 469.333 320-73.813 187.307-256 320-469.333 320s-395.52-132.693-469.333-320c73.813-187.307 256-320 469.333-320zM512 725.333c117.76 0 213.333-95.573 213.333-213.333s-95.573-213.333-213.333-213.333-213.333 95.573-213.333 213.333 95.573 213.333 213.333 213.333zM512 384c70.827 0 128 57.173 128 128s-57.173 128-128 128-128-57.173-128-128 57.173-128 128-128z\"],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"uniE0E8\"],\"defaultCode\":57576,\"grid\":0,\"attrs\":[]},\"attrs\":[],\"properties\":{\"id\":172,\"order\":74,\"prevSize\":32,\"code\":57576,\"name\":\"eye\"},\"setIdx\":2,\"setId\":1,\"iconIdx\":4},{\"icon\":{\"paths\":[\"M640 42.667v85.333h-256v-85.333h256zM469.333 597.333v-256h85.334v256h-85.334zM811.947 315.307c52.48 65.706 84.053 148.906 84.053 239.36 0 212.053-171.52 384-384 384s-384-171.947-384-384c0-212.054 171.947-384 384-384 90.453 0 173.653 31.573 239.787 84.48l60.586-60.587c21.76 17.92 41.814 38.4 60.16 60.16zM512 853.333c165.12 0 298.667-133.546 298.667-298.666s-133.547-298.667-298.667-298.667-298.667 133.547-298.667 298.667 133.547 298.666 298.667 298.666z\"],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"uniE325\"],\"defaultCode\":58149,\"grid\":0,\"attrs\":[]},\"attrs\":[],\"properties\":{\"id\":370,\"order\":21,\"ligatures\":\"\",\"prevSize\":32,\"code\":58149,\"name\":\"speed\"},\"setIdx\":2,\"setId\":1,\"iconIdx\":5},{\"icon\":{\"paths\":[\"M707.84 366.507l60.16 60.16-256 256-256-256 60.16-60.16 195.84 195.413z\"],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"uniE395\"],\"defaultCode\":58261,\"grid\":0,\"attrs\":[]},\"attrs\":[],\"properties\":{\"id\":549,\"order\":69,\"ligatures\":\"\",\"prevSize\":32,\"code\":58261,\"name\":\"expand\"},\"setIdx\":2,\"setId\":1,\"iconIdx\":6},{\"icon\":{\"paths\":[\"M554.667 128v426.667h-85.334v-426.667h85.334zM760.747 220.587c82.773 70.4 135.253 174.506 135.253 291.413 0 212.053-171.947 384-384 384s-384-171.947-384-384c0-116.907 52.48-221.013 135.253-291.413l60.16 60.16c-66.986 54.613-110.080 137.813-110.080 231.253 0 165.12 133.547 298.667 298.667 298.667s298.667-133.547 298.667-298.667c0-93.44-43.094-176.64-110.507-230.827z\"],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"uniE08F\"],\"defaultCode\":57487,\"grid\":0,\"attrs\":[]},\"attrs\":[],\"properties\":{\"id\":557,\"order\":19,\"ligatures\":\"\",\"prevSize\":32,\"code\":57487,\"name\":\"power\"},\"setIdx\":2,\"setId\":1,\"iconIdx\":7},{\"icon\":{\"paths\":[\"M816.64 551.936l85.504 67.584c8.192 6.144 10.24 16.896 5.12 26.112l-81.92 141.824c-5.12 9.216-15.872 12.8-25.088 9.216l-101.888-40.96c-20.992 15.872-44.032 29.696-69.12 39.936l-15.36 108.544c-1.024 10.24-9.728 17.408-19.968 17.408h-163.84c-10.24 0-18.432-7.168-20.48-17.408l-15.36-108.544c-25.088-10.24-47.616-23.552-69.12-39.936l-101.888 40.96c-9.216 3.072-19.968 0-25.088-9.216l-81.92-141.824c-4.608-8.704-2.56-19.968 5.12-26.112l86.528-67.584c-2.048-12.8-3.072-26.624-3.072-39.936s1.536-27.136 3.584-39.936l-86.528-67.584c-8.192-6.144-10.24-16.896-5.12-26.112l81.92-141.824c5.12-9.216 15.872-12.8 25.088-9.216l101.888 40.96c20.992-15.872 44.032-29.696 69.12-39.936l15.36-108.544c1.536-10.24 9.728-17.408 19.968-17.408h163.84c10.24 0 18.944 7.168 20.48 17.408l15.36 108.544c25.088 10.24 47.616 23.552 69.12 39.936l101.888-40.96c9.216-3.072 19.968 0 25.088 9.216l81.92 141.824c4.608 8.704 2.56 19.968-5.12 26.112l-86.528 67.584c2.048 12.8 3.072 26.112 3.072 39.936s-1.024 27.136-2.56 39.936zM512 665.6c84.48 0 153.6-69.12 153.6-153.6s-69.12-153.6-153.6-153.6-153.6 69.12-153.6 153.6 69.12 153.6 153.6 153.6z\"],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"uniE0A2\"],\"defaultCode\":57506,\"grid\":0,\"attrs\":[]},\"attrs\":[],\"properties\":{\"id\":562,\"order\":29,\"ligatures\":\"\",\"prevSize\":32,\"code\":57506,\"name\":\"settings\"},\"setIdx\":2,\"setId\":1,\"iconIdx\":8},{\"icon\":{\"paths\":[\"M556.8 417.707l125.867 94.293-256 192v-384zM556.8 417.707l125.867 94.293-256 192v-384zM556.8 417.707l-130.133-97.707v384l256-192zM469.333 173.653c-62.293 7.68-119.040 32.427-166.4 69.12l-60.586-61.013c63.146-51.627 141.226-85.76 226.986-94.293v86.186zM242.773 302.933c-36.693 47.36-61.44 104.107-69.12 166.4h-86.186c8.533-85.76 42.666-163.84 94.293-226.986zM173.653 554.667c7.68 62.293 32.427 119.040 69.12 165.973l-61.013 61.013c-51.627-63.146-85.76-141.226-94.293-226.986h86.186zM242.347 842.24l60.586-61.013c47.36 36.693 104.107 61.44 166.4 69.12v86.186c-85.333-8.533-163.84-42.666-226.986-94.293zM938.667 512c0 220.16-167.254 401.92-381.867 424.533v-86.186c167.253-22.187 296.533-165.547 296.533-338.347s-129.28-316.16-296.533-338.347v-86.186c214.613 22.613 381.867 204.373 381.867 424.533z\"],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"uniE139\"],\"defaultCode\":57657,\"grid\":0,\"attrs\":[]},\"attrs\":[],\"properties\":{\"id\":595,\"order\":46,\"ligatures\":\"\",\"prevSize\":32,\"code\":57657,\"name\":\"playlist\"},\"setIdx\":2,\"setId\":1,\"iconIdx\":9},{\"icon\":{\"paths\":[\"M386.4 93.333c231.104 0 418.667 187.563 418.667 418.667s-187.563 418.667-418.667 418.667c-43.96 0-85.827-6.699-125.6-19.259 169.979-53.171 293.067-211.845 293.067-399.408s-123.088-346.237-293.067-399.408c39.773-12.56 81.64-19.259 125.6-19.259z\"],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"uniE2A2\"],\"defaultCode\":58018,\"grid\":0,\"attrs\":[]},\"attrs\":[],\"properties\":{\"id\":607,\"order\":34,\"ligatures\":\"\",\"prevSize\":32,\"code\":58018,\"name\":\"night\"},\"setIdx\":2,\"setId\":1,\"iconIdx\":10},{\"icon\":{\"paths\":[\"M512 85.333c235.947 0 426.667 190.72 426.667 426.667s-190.72 426.667-426.667 426.667-426.667-190.72-426.667-426.667 190.72-426.667 426.667-426.667zM725.333 665.173l-153.173-153.173 153.173-153.173-60.16-60.16-153.173 153.173-153.173-153.173-60.16 60.16 153.173 153.173-153.173 153.173 60.16 60.16 153.173-153.173 153.173 153.173z\"],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"uniE38F\"],\"defaultCode\":58255,\"grid\":0,\"attrs\":[]},\"attrs\":[],\"properties\":{\"id\":662,\"order\":50,\"ligatures\":\"\",\"prevSize\":32,\"code\":58255,\"name\":\"cancel\"},\"setIdx\":2,\"setId\":1,\"iconIdx\":11},{\"icon\":{\"paths\":[\"M512 170.667c188.587 0 341.333 152.746 341.333 341.333 0 66.987-19.626 129.28-52.906 181.76l-62.294-62.293c19.2-35.414 29.867-76.374 29.867-119.467 0-141.227-114.773-256-256-256v128l-170.667-170.667 170.667-170.666v128zM512 768v-128l170.667 170.667-170.667 170.666v-128c-188.587 0-341.333-152.746-341.333-341.333 0-66.987 19.626-129.28 52.906-181.76l62.294 62.293c-19.2 35.414-29.867 76.374-29.867 119.467 0 141.227 114.773 256 256 256z\"],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"uniE116\"],\"defaultCode\":57622,\"grid\":0,\"attrs\":[]},\"attrs\":[],\"properties\":{\"id\":709,\"order\":17,\"ligatures\":\"\",\"prevSize\":32,\"code\":57622,\"name\":\"sync\"},\"setIdx\":2,\"setId\":1,\"iconIdx\":12},{\"icon\":{\"paths\":[\"M384 689.92l451.84-451.413 60.16 60.16-512 512-238.507-238.507 60.587-60.16z\"],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"uniE390\"],\"defaultCode\":58256,\"grid\":0,\"attrs\":[]},\"attrs\":[],\"properties\":{\"id\":733,\"order\":56,\"ligatures\":\"\",\"prevSize\":32,\"code\":58256,\"name\":\"confirm\"},\"setIdx\":2,\"setId\":1,\"iconIdx\":13},{\"icon\":{\"paths\":[\"M853.333 370.773l141.227 141.227-141.227 141.227v200.106h-200.106l-141.227 141.227-141.227-141.227h-200.106v-200.106l-141.227-141.227 141.227-141.227v-200.106h200.106l141.227-141.227 141.227 141.227h200.106v200.106zM512 768c141.227 0 256-114.773 256-256s-114.773-256-256-256-256 114.773-256 256 114.773 256 256 256zM512 341.333c94.293 0 170.667 76.374 170.667 170.667s-76.374 170.667-170.667 170.667-170.667-76.374-170.667-170.667 76.374-170.667 170.667-170.667z\"],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"uniE2A6\"],\"defaultCode\":58022,\"grid\":0,\"attrs\":[]},\"attrs\":[],\"properties\":{\"id\":785,\"order\":15,\"ligatures\":\"\",\"prevSize\":32,\"code\":58022,\"name\":\"brightness\"},\"setIdx\":2,\"setId\":1,\"iconIdx\":14},{\"icon\":{\"paths\":[\"M85.333 725.333v-42.666h128v170.666h-128v-42.666h85.334v-21.334h-42.667v-42.666h42.667v-21.334h-85.334zM128 341.333v-128h-42.667v-42.666h85.334v170.666h-42.667zM85.333 469.333v-42.666h128v38.4l-76.8 89.6h76.8v42.666h-128v-38.4l76.8-89.6h-76.8zM298.667 213.333h597.333v85.334h-597.333v-85.334zM298.667 810.667v-85.334h597.333v85.334h-597.333zM298.667 554.667v-85.334h597.333v85.334h-597.333z\"],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"uniE22D\"],\"defaultCode\":57901,\"grid\":0,\"attrs\":[]},\"attrs\":[],\"properties\":{\"id\":797,\"order\":58,\"ligatures\":\"\",\"prevSize\":32,\"code\":57901,\"name\":\"nodes\"},\"setIdx\":2,\"setId\":1,\"iconIdx\":15},{\"icon\":{\"paths\":[\"M810.667 554.667h-256v256h-85.334v-256h-256v-85.334h256v-256h85.334v256h256v85.334z\"],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"uniE18A\"],\"defaultCode\":57738,\"grid\":0,\"attrs\":[]},\"attrs\":[],\"properties\":{\"id\":803,\"order\":59,\"ligatures\":\"\",\"prevSize\":32,\"code\":57738,\"name\":\"add\"},\"setIdx\":2,\"setId\":1,\"iconIdx\":16},{\"icon\":{\"paths\":[\"M128 736l471.893-471.893 160 160-471.893 471.893h-160v-160zM883.627 300.373l-78.080 78.080-160-160 78.080-78.080c16.64-16.64 43.52-16.64 60.16 0l99.84 99.84c16.64 16.64 16.64 43.52 0 60.16z\"],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"uniE2C6\"],\"defaultCode\":58054,\"grid\":0,\"attrs\":[]},\"attrs\":[],\"properties\":{\"id\":834,\"order\":72,\"ligatures\":\"\",\"prevSize\":32,\"code\":58054,\"name\":\"edit\"},\"setIdx\":2,\"setId\":1,\"iconIdx\":17},{\"icon\":{\"paths\":[\"M576 28.587c166.827 133.546 277.333 338.773 277.333 568.746 0 188.587-152.746 341.334-341.333 341.334s-341.333-152.747-341.333-341.334c0-144.213 51.626-276.906 137.813-379.306l-1.28 15.36c0 87.893 66.56 159.146 154.88 159.146 87.893 0 145.493-71.253 145.493-159.146 0-91.734-31.573-204.8-31.573-204.8zM499.627 810.667c113.066 0 204.8-91.734 204.8-204.8 0-59.307-8.534-117.334-25.174-172.374-43.52 58.454-121.6 94.72-197.12 110.080-75.093 15.36-119.893 64-119.893 133.12 0 74.24 61.44 133.974 137.387 133.974z\"],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"uniE409\"],\"defaultCode\":58377,\"grid\":0,\"attrs\":[]},\"attrs\":[],\"properties\":{\"id\":871,\"order\":10,\"ligatures\":\"\",\"prevSize\":32,\"code\":58377,\"name\":\"intensity\"},\"setIdx\":2,\"setId\":1,\"iconIdx\":18},{\"icon\":{\"paths\":[\"M938.667 394.24l-232.534 201.813 69.547 299.947-263.68-159.147-263.68 159.147 69.973-299.947-232.96-201.813 306.774-26.027 119.893-282.88 119.893 282.454zM512 657.067l160.853 97.28-42.666-182.614 141.653-122.88-186.88-16.213-72.96-172.373-72.533 171.946-186.88 16.214 141.653 122.88-42.667 182.613z\"],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"uniE410\"],\"defaultCode\":58384,\"grid\":0,\"attrs\":[]},\"attrs\":[],\"properties\":{\"id\":927,\"order\":9,\"ligatures\":\"\",\"prevSize\":32,\"code\":58384,\"name\":\"star\"},\"setIdx\":2,\"setId\":1,\"iconIdx\":19},{\"icon\":{\"paths\":[\"M512 85.333c235.52 0 426.667 191.147 426.667 426.667s-191.147 426.667-426.667 426.667-426.667-191.147-426.667-426.667 191.147-426.667 426.667-426.667zM554.667 725.333v-256h-85.334v256h85.334zM554.667 384v-85.333h-85.334v85.333h85.334z\"],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"uniE066\"],\"defaultCode\":57446,\"grid\":0,\"attrs\":[]},\"attrs\":[],\"properties\":{\"id\":952,\"order\":62,\"ligatures\":\"\",\"prevSize\":32,\"code\":57446,\"name\":\"info\"},\"setIdx\":2,\"setId\":1,\"iconIdx\":20},{\"icon\":{\"paths\":[\"M256 810.667v-512h512v512c0 46.933-38.4 85.333-85.333 85.333h-341.334c-46.933 0-85.333-38.4-85.333-85.333zM810.667 170.667v85.333h-597.334v-85.333h149.334l42.666-42.667h213.334l42.666 42.667h149.334z\"],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"uniE037\"],\"defaultCode\":57399,\"grid\":0,\"attrs\":[]},\"attrs\":[],\"properties\":{\"id\":969,\"order\":55,\"ligatures\":\"\",\"prevSize\":32,\"code\":57399,\"name\":\"del\"},\"setIdx\":2,\"setId\":1,\"iconIdx\":21},{\"icon\":{\"paths\":[\"M704 128c131.413 0 234.667 103.253 234.667 234.667 0 161.28-145.067 292.693-364.8 491.946l-61.867 56.32-61.867-55.893c-219.733-199.68-364.8-331.093-364.8-492.373 0-131.414 103.254-234.667 234.667-234.667 74.24 0 145.493 34.56 192 89.173 46.507-54.613 117.76-89.173 192-89.173zM516.267 791.467c203.093-183.894 337.066-305.494 337.066-428.8 0-85.334-64-149.334-149.333-149.334-65.707 0-129.707 42.24-151.893 100.694h-79.787c-22.613-58.454-86.613-100.694-152.32-100.694-85.333 0-149.333 64-149.333 149.334 0 123.306 133.973 244.906 337.066 428.8l4.267 4.266z\"],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"uniE04C\"],\"defaultCode\":57420,\"grid\":0,\"attrs\":[]},\"attrs\":[],\"properties\":{\"id\":1034,\"order\":61,\"ligatures\":\"\",\"prevSize\":32,\"code\":57420,\"name\":\"presets\"},\"setIdx\":2,\"setId\":1,\"iconIdx\":22}],\"height\":1024,\"metadata\":{\"name\":\"wled122\"},\"preferences\":{\"showGlyphs\":true,\"showCodes\":true,\"showQuickUse\":true,\"showQuickUse2\":true,\"showSVGs\":true,\"fontPref\":{\"prefix\":\"i-\",\"metadata\":{\"fontFamily\":\"wled122\",\"majorVersion\":1,\"minorVersion\":7},\"metrics\":{\"emSize\":1024,\"baseline\":20,\"whitespace\":0},\"embed\":false,\"autoHost\":true,\"noie8\":true,\"ie7\":false,\"showSelector\":false,\"showMetrics\":false,\"showMetadata\":false,\"showVersion\":true},\"imagePref\":{\"prefix\":\"icon-\",\"png\":true,\"useClassSelector\":true,\"color\":0,\"bgColor\":16777215,\"name\":\"icomoon\",\"classSelector\":\".icon\"},\"historySize\":50,\"quickUsageToken\":{\"wled122\":\"MDA1MGUxOTY0MyMxNzczNTY5NTMxI0ljcTJCSm9WQnFUUUFOdUZHQzlpaFZ0QkZaOGRFTXFRWFJoU1VjN2VqbmFn\"},\"showLiga\":false,\"gridSize\":16}}"
  },
  {
    "path": "wled00/data/icons-ui/style.css",
    "content": "@font-face {\n  font-family: 'wled122';\n  src:\n    url('fonts/wled122.ttf?yzxblb') format('truetype'),\n    url('fonts/wled122.woff?yzxblb') format('woff'),\n    url('fonts/wled122.svg?yzxblb#wled122') format('svg');\n  font-weight: normal;\n  font-style: normal;\n  font-display: block;\n}\n\n[class^=\"i-\"], [class*=\" i-\"] {\n  /* use !important to prevent issues with browser extensions that change fonts */\n  font-family: 'wled122' !important;\n  speak: never;\n  font-style: normal;\n  font-weight: normal;\n  font-variant: normal;\n  text-transform: none;\n  line-height: 1;\n\n  /* Better Font Rendering =========== */\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\n.i-pixelforge:before {\n  content: \"\\e900\";\n}\n.i-editor:before {\n  content: \"\\e901\";\n}\n.i-pattern:before {\n  content: \"\\e23d\";\n}\n.i-segments:before {\n  content: \"\\e34b\";\n}\n.i-sun:before {\n  content: \"\\e333\";\n}\n.i-palette:before {\n  content: \"\\e2b3\";\n}\n.i-eye:before {\n  content: \"\\e0e8\";\n}\n.i-speed:before {\n  content: \"\\e325\";\n}\n.i-expand:before {\n  content: \"\\e395\";\n}\n.i-power:before {\n  content: \"\\e08f\";\n}\n.i-settings:before {\n  content: \"\\e0a2\";\n}\n.i-playlist:before {\n  content: \"\\e139\";\n}\n.i-night:before {\n  content: \"\\e2a2\";\n}\n.i-cancel:before {\n  content: \"\\e38f\";\n}\n.i-sync:before {\n  content: \"\\e116\";\n}\n.i-confirm:before {\n  content: \"\\e390\";\n}\n.i-brightness:before {\n  content: \"\\e2a6\";\n}\n.i-nodes:before {\n  content: \"\\e22d\";\n}\n.i-add:before {\n  content: \"\\e18a\";\n}\n.i-edit:before {\n  content: \"\\e2c6\";\n}\n.i-intensity:before {\n  content: \"\\e409\";\n}\n.i-star:before {\n  content: \"\\e410\";\n}\n.i-info:before {\n  content: \"\\e066\";\n}\n.i-del:before {\n  content: \"\\e037\";\n}\n.i-presets:before {\n  content: \"\\e04c\";\n}\n"
  },
  {
    "path": "wled00/data/index.css",
    "content": "@font-face {\n\tfont-family: \"WIcons\";\n  src: url(data:font/woff2;charset=utf-8;base64,d09GMgABAAAAAAsAAA0AAAAAFlgAAAqqAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP0ZGVE0cGhgGYACEGhEICp08lnkLPgABNgIkA3gEIAWDGweCNhvxEVGUcFI3wBeFsYOmlCFXadeSCl4PGhMTwyMh0q9d2MXuDaeszCMkmT3Abd0Eu2ijAIMUa1IDbaQmRj/wndtnJB+d8BHN/+ZKv+zJJpUCCAsMA5IcArBbtlteAg6ToYi3nPp6KxH97fd9OQgssMYTSymghAPMMODmLNpvv/P8BPzeodosVKppyCRNZE0QEqlTCp0SqP9T4O4gAMzzFuTJg2RPa6/23s/f4IYKREKfr6tTc/cLu7dh2JTwmhJdUiSLQqZVQFvmy6mScazQAwlZ7apjDAOl7l8dYEyN5azo7xRYCTCz7gCAzIa7hoI38uBn9/NfQMIrA5RCyCOfOtya0oEneAKP2+M8AEzujgX5QIQYkXEhC5nk4BVC6f6L4cmN4YazURxLPmVQjD4XkFWhNcfmv38+EMNisJkOyOKfgx6n/2z9efLjZY9fPol6EvJEdaY7I5y1zu3Ok64kl58r7bcprplPfZ+GvELuPwEAiGmvZJPj8ErdT9kXF+1jV7AvsG3seNY31uuFw5m/LLgKwNzGLGd8mO+cfw6A8S5jCsM/9wfEH8iWrJEYBLUxMHfsLJpcHQqzOuDEFhQjM1otoVvVg94O/zMIoCJtI1ACwThSfr8yQL1KvQ5rAApCJOJJKBSl4cdB3IwhcY7A5i3/hNDuIJq7NmfVLJNq2Z1hACMTkEpSDwPzGMtL6Qj7EFl8BemVw4zAppSRHW5ZhSxVZIZwKIDXduoxP57T0cgYeukZbC1afoHHq6/OwUSERJEC0lcLGXjp0QKyd7tOLYzdaXLTFHYixavtddgQ0YyI6xbZbLleW+DKSDGxqrvjTWIRtNNgOF6yGYc0ZhihH0R1vR9WuWn2Q4pkWdcmW0QsbEIYzglYJKxhzbvPBSWhn9uiMsuraZ3jiQ75dBgpD4mW9tgSdSHFzLzEcnLiNDvb59n9lVxzrObWObWDviOG3Dwt5RZCKdLLyl04L0+xvKG6aEG0nJFTM6AcuXROdpzmFJCcH9+uWfmohYxDH0Nxk+nRN4ZT3uJW3O32b9GChl57lSFlYeur0F6s+ve/cC8GeUHLy5CeTZoB7XGeFaxDWspDQ9CBaXdnUZU9hGerGTqIgUtgQxhFauojOOdYXo78csyahwycYlRk/FVxQdrYrQc7r1tJQJv1+Xi5FbW+xPCwj5pLicU1YATAPRM9hVc9RfdxWc2300x9lIgM3K/9xgtYHI8miESYICECeMSQt3EtAdq7jhUlLE2CiYgNqUeZNrzc9nLTTg+EeckP9Kz28vnwTeoolOtCGyF5WOonuVZNPkHX/RKff2/l48rnCUbIfJZad59cYhSwkWPEJUQkRvZrYUMVbAKCS6jB/bp7M2ItABfEMpgBinhBFLgze5jkAlW62xjORdV67XqlRsPsObLU7cI+/4ss97HdGJ2iXMrTFMuRTzAe2SISYd9NlE6rZmS4ahqS+8GKTA7ZuWs9YGQfYGQHdUqbXcy+iQC2aiEDhkdLTkhvpoYOmp6tTc6yvgVbEIGdkoPu2sV275V27N23h7awKFxyUm5n1CGxXfscu7nrlINyF7v00vEyotuwG5If4LpYtazK+s3xmi4bpC2UoPNVnRa9JubCZj3+jg4Zl+iGnds38V2bNqxnXOKcUkYv8Vw1vppL4+lMDDMok9jqbFmxHE1LeYp/Sc6O03odj1droeRpqckiE7Qa4jB+nO5OlVyIymtCtJdACJKcTKe3Kct4DL+2knGWW/gpzKXr191XULH0Ay1NmD9ndUMvJaoqrCq5dStqFaosxPyr8/N902gfWD5BcFtmaqreo8wxq1T1+g6+d8iQDLnRJBeYZP4jf/MEBpHR0Lj1zmvSecXw/+vqjLhyTs+enLSoujoiRy3LDbIhvmtxCTAzTZPZBNzr683+Pi7U/TOZjE+Z8yHfzlQzMbsdS4t1ulIwTTJN6/hj5nBM5GevHDFhfTVob+tnthVHUVyu6o1q8GeQCn5TYowqQ4a0asLK33fsSX2zLCVo473WZ4XPWu/gTUr4n1nSfH38mHmqzKpYCucxNo9yXJO1toU1NYX8GuAm7EXRRVH9ja9f0zCPBxUQoNvXeb64MoLftWmu23d39+9eBU1d+UObPKOkpETCw4F7hvbO3brNG0u0Qnrt6B9fveVI0AIMu+aSkOtc3VrSJG5IwMsAv2Rwvfs6ObS5xyXIGfFGlW5cxjv/b4+s7/gTclsCLce7ZvXo6i3rJxi2P9ln4irW+XW89OtSmD3FBmYRo9jaDUvEEip98Bf1mytr7BaFwmJXXVf/AsfRQx8c8MBdywDCjkgAM7s2GDeXXEdyeRSPy4viEmSqzesYgTclp1nKvv50S/kNN+Me01EF0wbWprFZyoBXWACDKu3Cljz17p1WbIZ7xFwjnWai0bGQqsZQK2xf3jggsrSXIVaxQ5EaS2GoE0/jlHG6deccNaU4PqGWZWrG4+588wUzl9saGzWaiLzKjH1B/XEQ4LgwcYIvPn16iYkW1K9gpBLXayyhAJWUWWu2o4jXaVtbtfzXgfuQTk3DaPbaBw817l7OvamJX0Yz0gPOtn4jx9N79MYQbCTF84i+sxz6kXTj3MYcbvkx56XzGsMoWng/EOvWrcWLo2/Jki/by8srSCjHsse7du1fBqtFNQfTTAMOYnfw+6srmZgvttlWFUunU+SoXWtJpU4qtduaqVndnxftCHhw2c5Qs43pa9cbRfu0y1Nt5oTN6hPvfS+w6LgjvexcaoGJZO76IeYh02unz5FWVjqiKer9q78ieyU0Da60eLSoAM296/BJHbMKCIXs4Xs17vLgTs35ikkIrh9BLc4dTXAxNvU5UvV1Vb7bhkO4BhD/9lGHO+/fn4NjlwtHhQO8BSSK5a9HRtGUqfZwnbmeTb2rTbpb764lHTY8Ydt87VtQbHW9UlkZn5WaPRqobxB3qLN+/cb18J+f+dNROn5AISbO1lVAbseul2ewdd4vjwdVkzC2L02fKWdJE3fnxAH7JhVtSF4/EDxhQNoukP0c++bTOk5j6JfTPn6EbndfYOD6FcsJIgKUob1Inz6u5zRZLPsWD0IB4t4DWzCg1XLY/wIGg30NHTTauPsJKVtSOtJ9O2/rYgfF03zzHqybNYqD/yx4tforP6Ld9vAr2ybl/3yIRTcdrwzuetFFSSMAH0LMxI2+fkDdCcDYJyA3ipitETmBOLi8EZmJSOpOPFq/DTxMGhrE3JLs83kymayp5Uh8Ms2xDiHtOJqBLNjEdz8eyLwgrYDkpX6syTp5sNVEYdFEZesHeyLOS68ey57lZy682pmLOqOJ4wcS2GwSmTfZWPLgMWbCdumm7N32YP5QDH110k4bAfiCL0Df065NIHyl/q626c2Y16wHeIviHYE4G+iT5oGtK/bUXlddcGyeJwQBPKxxgIKM7PhKE0/2uuQ+juqSmmzG3PDQFXfqjwMpWpmyPLpjTQbA8zda3OddU9za9W/xDBTYht7SfiikklBMEosFGw5ceGBX1J+TRABBhBBGBFHEEIeCD40EkkghjYx77NI+y02QY4JeWJYom4tVXCrlMg1XCDMwWSeBQMpFORkyRSehUM1EmQphXMqVogyVtJNIKOEiERcruUyTmZOVJOkkzsrJEGRl5WR5AgA=);\n}\n\n:root {\n\t--c-1: #111;\n\t--c-f: #fff;\n\t--c-2: #222;\n\t--c-3: #333;\n\t--c-4: #444;\n\t--c-5: #555;\n\t--c-6: #666;\n\t--c-8: #888;\n\t--c-b: #bbb;\n\t--c-c: #ccc;\n\t--c-e: #eee;\n\t--c-d: #ddd;\n\t--c-r: #c32;\n\t--c-g: #2c1;\n\t--c-l: #48a;\n\t--c-y: #a90;\n\t--t-b: .5;\n\t--c-o: rgba(34, 34, 34, .9);\n\t--c-tb : rgba(34, 34, 34, var(--t-b));\n\t--c-tba: rgba(102, 102, 102, var(--t-b));\n\t--c-tbh: rgba(51, 51, 51, var(--t-b));\n\t/*following are internal*/\n\t--th: 70px;\n\t--tp: 70px;\n\t--bh: 63px;\n\t--tbp: 14px 14px 10px 14px;\n\t--bbp: 9px 0 7px 0;\n\t--bhd: none;\n\t--sgp: \"block\";\n\t--bmt: 0;\n\t--sti: 42px;\n\t--stp: 42px;\n}\n\nhtml {\n\ttouch-action: manipulation;\n}\n\nbody {\n\tmargin: 0;\n\tbackground-color: var(--c-1);\n\tfont-family: Helvetica, Verdana, sans-serif;\n\tfont-size: 17px;\n\tcolor: var(--c-f);\n\ttext-align: center;\n\t-webkit-touch-callout: none;\n\t-webkit-user-select: none;\n\t-moz-user-select: none;\n\t-ms-user-select: none;\n\tuser-select: none;\n\t-webkit-tap-highlight-color: transparent;\n\tscrollbar-width: 6px;\n\tscrollbar-color: var(--c-sb) transparent;\n}\n\nhtml,\nbody {\n\theight: 100%;\n\twidth: 100%;\n\tposition: fixed;\n\toverscroll-behavior: none;\n}\n\n#bg {\n\theight: 100vh;\n\twidth: 100vw;\n\tposition: fixed;\n\tz-index: -10;\n\tbackground-position: center;\n\tbackground-repeat: no-repeat;\n\tbackground-size: cover;\n\topacity: 0;\n\ttransition: opacity 2s;\n}\n\np {\n\tmargin: 10px 0 2px 0;\n}\na, p, a:visited {\n\tcolor: var(--c-d);\n}\na, a:visited {\n\ttext-decoration: none;\n}\n\nbutton {\n\toutline: 0;\n\tcursor: pointer;\n}\n\n.iconlabel{\n\tfont-size:12px;\n\tmargin-top:2px;\n}\n\n.labels {\n\tmargin: 0;\n\tpadding: 8px 0 2px 0;\n\tfont-size: 19px;\n}\n\n#namelabel {\n\tposition: fixed;\n\tbottom: calc(var(--bh) + 6px);\n\tright: 6px;\n\tcolor: var(--c-8); /* set bright (--c-d) with dark text shadow (see below) to be legible on gray background (in image) */\n\tcursor: pointer;\n\twriting-mode: vertical-rl;\n\t/* transform: rotate(180deg); */\n}\n\n.bri {\n\tpadding: 4px;\n}\n\n.wrapper {\n\tposition: fixed;\n\ttop: 0;\n\tleft: 0;\n\tright: 0;\n\tbackground: var(--c-tb);\n\tz-index: 1;\n}\n\n.icons {\n\tfont-family: 'WIcons';\n\tfont-style: normal;\n\tfont-size: 24px !important;\n\tline-height: 1 !important;\n\tdisplay: inline-block;\n}\n\n.on {\n\tcolor: var(--c-g) !important;\n}\n\n.off {\n\tcolor: var(--c-6) !important;\n\t/* cursor: default !important; */\n}\n\n.top .icons, .bot .icons {\n\tmargin: -2px 0 4px 0;\n}\n\n.huge {\n\tfont-size: 60px !important;\n}\n\n.segt, .plentry TABLE {\n\ttable-layout: fixed;\n\twidth: 100%;\n}\n\n.segt TD {\n\tpadding: 2px 0 !important;\n\ttext-align: center;\n\t/*text-transform: uppercase;*/\n}\n.segt TD, .plentry TD {\n\tfont-size: 13px;\n\tpadding: 0;\n\tvertical-align: middle;\n}\n\n.keytd {\n\ttext-align: left;\n}\n\n.valtd {\n\ttext-align: right;\n}\n\n.valtd i {\n\tfont-size: small;\n}\n\n.slider-icon {\n\tposition: absolute;\n\tleft: 8px;\n\tbottom: 5px;\n}\n\n.sel-icon {\n\ttransform: translateX(3px);\n}\n\n.e-icon, .g-icon, .sel-icon, .slider-icon {\n\tcursor: pointer;\n\tcolor: var(--c-d);\n}\n\n.g-icon {\n\tfont-style: normal;\n\tposition: absolute;\n\ttop: 8px;\n\tright: 8px;\n}\n\n/* pop-up container */\n.pop {\n\tposition: absolute;\n\tdisplay: inline-block;\n\ttop: 0;\n\tright: 0;\n}\n\n/* pop-up content (segment sets) */\n.pop-c {\n\tposition: absolute;\n\tbackground-color: var(--c-2);\n\tborder: 1px solid var(--c-8);\n\tborder-radius: 20px;\n\tz-index: 1;\n\ttop: 3px;\n\tright: 35px;\n\tpadding: 3px 8px 1px;\n\tfont-size: 24px;\n\tline-height: 24px;\n}\n.pop-c span {\n\tpadding: 2px 6px;\n}\n\n.search-icon {\n\tposition: absolute;\n\ttop: 8px;\n\tleft: 12px;\n\twidth: 24px;\n\theight: 24px;\n}\n\n.clear-icon {\n\tposition: absolute;\n\ttop: 8px;\n\tright: 9px;\n\tcursor: pointer;\n}\n\n.flr {\n\tcolor: var(--c-f);\n\ttransform: rotate(0deg);\n\ttransition: transform .3s;\n\tposition: absolute;\n\ttop: 0;\n\tright: 0;\n\tpadding: 8px;\n}\n\n.expanded .flr,\n.exp {\n\ttransform: rotate(180deg);\n}\n\n.il {\n\tdisplay: inline-block;\n\tvertical-align: middle;\n}\n\n#liveview {\n\theight: 4px;\n\twidth: 100%;\n\tborder: 0;\n}\n\n#liveview2D {\n\theight: 90%;\n\twidth: 90%;\n\tborder: 0;\n\tposition: absolute;\n\ttop: 50%;\n\tleft: 50%;\n\ttransform: translate(-50%,-50%);\n}\n\n.tab {\n\tbackground-color: transparent;\n\tcolor: var(--c-d);\n}\n\n.bot {\n\tposition: fixed;\n\tbottom: 0;\n\tleft: 0;\n\twidth: 100%;\n\tbackground-color: var(--c-tb);\n}\n\n.tab button {\n\tbackground-color: transparent;\n\tfloat: left;\n\tborder: 0;\n\ttransition: color .3s, background-color .3s;\n\tfont-size: 17px;\n\tcolor: var(--c-c);\n\tmin-width: 44px;\n}\n\n.top button {\n\tpadding: var(--tbp);\n\tmargin: 0;\n}\n\n.bot button {\n\tpadding: var(--bbp);\n\twidth: 25%;\n\tmargin: 0;\n}\n\n.tab button:hover {\n\tbackground-color: var(--c-tbh);\n\tcolor: var(--c-e);\n}\n\n.tab button.active {\n\tbackground-color: var(--c-tba) !important;\n\tcolor: var(--c-f);\n}\n\n.active {\n\tbackground-color: var(--c-6) !important;\n\tcolor: var(--c-f);\n}\n\n.container {\n\t--n: 1;\n\twidth: 100%;\n\twidth: calc(var(--n)*100%);\n\theight: calc(100% - var(--tp) - var(--bh));\n\tmargin-top: var(--tp);\n\ttransform: translate(calc(var(--i, 0)/var(--n)*-100%));\n\toverscroll-behavior: none;\n}\n\n.tabcontent {\n\tfloat: left;\n\tposition: relative;\n\twidth: 100%;\n\twidth: calc(100%/var(--n));\n\tbox-sizing: border-box;\n\tborder: 0;\n\toverflow-y: auto;\n\toverflow-x: hidden;\n\theight: 100%;\n\toverscroll-behavior: none;\n\tpadding: 0 4px;\n\t-webkit-overflow-scrolling: touch;\n}\n\n#Segments, #Presets, #Effects, #Colors {\n\tfont-size: 19px;\n\tpadding: 4px 0 0;\n}\n\n#segutil, #segutil2, #segcont, #putil, #pcont, #pql, #fx, #palw, #bsp,\n.fnd {\n\tmax-width: 280px;\n}\n\n#putil, #segutil, #segutil2, #bsp {\n\tmin-height: 42px;\n\tmargin: 0 auto;\n}\n\n#segutil .segin {\n\tpadding-top: 12px;\n}\n\n#fx, #pql, #segcont, #pcont, #sliders, #qcs-w, #hexw, #pall, #ledmap,\n.slider, .filter, .option, .segname, .pname, .fnd {\n\tmargin: 0 auto;\n}\n\n#putil {\n\tpadding: 5px 0 0;\n}\n\n/* Quick load magin for simplified UI */\n.simplified #pql, .simplified #palw, .simplified #fx {\n\tmargin-bottom: 8px;\n}\n\n.smooth { transition: transform\tcalc(var(--f, 1)*.5s) ease-out }\n\n.tab-label {\n\tmargin: 0 0 -5px 0;\n\tpadding-bottom: 4px;\n\tdisplay: var(--bhd);\n}\n\n.overlay {\n\tposition: fixed;\n\theight: 100%;\n\twidth: 100%;\n\ttop: 0;\n\tleft: 0;\n\tbackground-color: var(--c-3);\n\tfont-size: 24px;\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tz-index: 11;\n\topacity: .95;\n\ttransition: .7s;\n\tpointer-events: none;\n}\n\n.staytop, .staybot {\n\tdisplay: block;\n\tposition: -webkit-sticky;\n\tposition: sticky !important;\n\ttop: 0;\n\tz-index: 2;\n\tmargin: 0 auto auto;\n}\n\n.staybot {\n\tbottom: 5px;\n}\n\n#sliders {\n\tposition: -webkit-sticky;\n\tposition: sticky;\n\tbottom: 0;\n\tmax-width: 300px;\n\tz-index: 2;\n}\n\n#sliders .labels {\n\tpadding-top: 3px;\n\tfont-size: small;\n}\n\n.slider {\n\t/*max-width: 300px;*/\n\t/* margin: 5px auto; add 5px; if you want some vertical space but looks ugly */\n\tborder-radius: 24px;\n\tposition: relative;\n\tpadding-bottom: 2px;\n}\n\n/* Slider wrapper div */\n.sliderwrap {\n\theight: 30px;\n\twidth: 230px;\n\tmax-width: 230px;\n\tposition: relative;\n\tz-index: 0;\n}\n\n#sliders .slider {\n\tpadding-right: 64px; /* offset for bubble */\n}\n\n#sliders .slider, #info .slider {\n\tbackground-color: var(--c-2);\n}\n\n#sliders .sliderwrap, .sbs .sliderwrap {\n\tleft: 32px; /* offset for icon */\n}\n\n.filter, .option {\n\tbackground-color: var(--c-4);\n\tborder-radius: 26px;\n\theight: 26px;\n\tmax-width: 300px;\n\t/* margin: 0 auto 4px; add 4-8px if you want space at the bottom */\n\tpadding: 4px 2px;\n\tposition: relative;\n\topacity: 1;\n\ttransition: opacity .25s linear, height .2s, transform .2s;\n}\n\n.filter {\n\tz-index: 1;\n\t/*overflow: visible;*/\n\tborder-radius: 0 0 16px 16px;\n\tmax-width: 220px;\n\theight: 54px;\n\tline-height: 1.5;\n\tpadding-bottom: 8px;\n\tpointer-events: none;\n}\n\n/* New tooltip */\n.tooltip {\n\tposition: absolute;\n\topacity: 0;\n\tvisibility: hidden;\n\ttransition: opacity .25s ease, visibility .25s ease;\n\tbackground-color: var(--c-5);\n\tbox-shadow: 4px 4px 10px 4px var(--c-1);\n\tcolor: var(--c-f);\n\ttext-align: center;\n\tpadding: 8px 16px;\n\tborder-radius: 6px;\n\tz-index: 1;\n\tpointer-events: none;\n}\n\n .tooltip::after {\n\tcontent: \"\";\n\tposition: absolute;\n\tborder: 8px solid;\n\tborder-color: var(--c-5) transparent transparent transparent;\n\ttop: 100%;\n\tleft: calc(50% - 8px);\n\tz-index: 0;\n }\n\n.tooltip.visible {\n\topacity: 1;\n\tvisibility: visible;\n}\n\n.fade {\n\tvisibility: hidden; /* hide it */\n\topacity: 0; /* make it transparent */\n\ttransform: scaleY(0); /* shrink content */\n\theight: 0; /* force other elements to move */\n\tpadding: 0; /* remove empty space */\n}\n\n.first {\n\tmargin-top: 10px;\n}\n\n#toast {\n\topacity: 0;\n\tbackground-color: var(--c-5);\n\tborder: 1px solid var(--c-2);\n\tmax-width: 90%;\n\tcolor: var(--c-f);\n\ttext-align: center;\n\tborder-radius: 5px;\n\tpadding: 22px;\n\tposition: fixed;\n\tz-index: 5;\n\tleft: 50%;\n\ttransform: translateX(-50%);\n\tbottom: calc(var(--bh) + 22px);\n\tfont-size: 17px;\n\tpointer-events: none;\n}\n\n#toast.show {\n\topacity: 1;\n\tanimation: fadein .5s, fadein .5s 2.5s reverse;\n}\n\n#toast.error {\n\topacity: 1;\n\tbackground-color: #b21;\n\tanimation: fadein .5s;\n}\n\n.modal {\n\tposition: fixed;\n\tleft: 0;\n\tbottom: 0;\n\tright: 0;\n\ttop: calc(var(--th) - 1px);\n\tbackground-color: var(--c-o);\n\ttransform: translateY(100%);\n\ttransition: transform .4s;\n\tpadding: 8px;\n\tfont-size: 20px;\n\toverflow: auto;\n}\n\n.close {\n\tposition: -webkit-sticky;\n\tposition: sticky;\n\ttop: 0;\n\tfloat: right;\n}\n\n#info, #nodes {\n\tz-index: 4;\n}\n\n#rover {\n\tz-index: 3;\n}\n\n#rover .ibtn {\n\tmargin: 5px;\n}\n\n#ndlt {\n\tmargin: 12px 0;\n}\n\n#roverstar {\n\tposition: fixed;\n\ttop: calc(var(--th) + 5px);\n\tleft: 1px;\n\tcursor: pointer;\n}\n\n#connind {\n\tposition: fixed;\n\tbottom: calc(var(--bh) + 5px);\n\tleft: 4px;\n\tpadding: 5px;\n\tborder-radius: 5px;\n\tbackground-color: #a90;\n\tz-index: -2;\n}\n\n#info .slider {\n\tmax-width: 200px;\n\tmin-width: 145px;\n\tfloat: right;\n\tmargin: 0;\n}\n#info .sliderwrap {\n\twidth: 200px;\n}\n\n#info table, #nodes table {\n\ttable-layout: fixed;\n\twidth: 100%;\n}\n\n#info td, #nodes td {\n  padding-bottom: 8px;\n}\n\n#info .ibtn {\n\tmargin: 5px;\n}\n\n#info div, #nodes div {\n\tmax-width: 490px;\n\tmargin: 0 auto;\n}\n\n#info #imgw {\n\tmargin: 8px auto;\n}\n\n#lv {\n\tmax-width: 600px;\n\tdisplay: inline-block;\n}\n\n#heart {\n\ttransition: color .9s;\n\tfont-size: 16px;\n\tcolor: #f00;\n}\n\nimg {\n\tmax-width: 100%;\n\tmax-height: 100%;\n}\n\n.wi {\n\timage-rendering: pixelated;\n\timage-rendering: crisp-edges;\n\twidth: 210px;\n}\n\n@keyframes fadein {\n\tfrom {bottom: 0; opacity: 0;}\n\tto {bottom: calc(var(--bh) + 22px); opacity: 1;}\n}\n\n.sliderdisplay {\n\tcontent:'';\n\tposition: absolute;\n\ttop: 12px; left: 8px; right: 8px;\n\theight: 5px;\n\tbackground: var(--c-4);\n\tborder-radius: 16px;\n\tpointer-events: none;\n\tz-index: -1;\n\t--bg: var(--c-f);\n}\n\n#rwrap .sliderdisplay { --bg: none; background: linear-gradient(90deg, #000 -15%, #f00); } /* -15% since #000 is too dark */\n#gwrap .sliderdisplay { --bg: none; background: linear-gradient(90deg, #000 -15%, #0f0); } /* -15% since #000 is too dark */\n#bwrap .sliderdisplay { --bg: none; background: linear-gradient(90deg, #000 -15%, #00f); } /* -15% since #000 is too dark */\n#wwrap .sliderdisplay { --bg: none; background: linear-gradient(90deg, #000 -15%, #fff); } /* -15% since #000 is too dark */\n#kwrap .sliderdisplay,\n#wbal  .sliderdisplay { background: linear-gradient(90deg, #ff8f1f 0%, #fff 50%, #cbdbff); }\n\n/* wrapper divs hidden by default */\n#liveview, #liveview2D, #roverstar, #pql\n#rgbwrap, #swrap, #hwrap, #kwrap, #wwrap, #wbal, #qcs-w, #hexw,\n.clear-icon, .edit-icon, .ptxt {\n\tdisplay: none;\n}\n\n.sliderbubble {\n\twidth: 24px;\n\tposition: absolute;\n\tdisplay: inline-block;\n\tborder-radius: 16px;\n\tbackground: var(--c-3);\n\tcolor: var(--c-f);\n\tpadding: 4px;\n\tfont-size: 14px;\n\tright: 6px;\n\ttransition: visibility .25s ease,opacity .25s ease;\n\topacity: 0;\n\tvisibility: hidden;\n\t/* left: 8px; */\n\ttop: 4px;\n}\n\noutput.sliderbubbleshow {\n\tvisibility: visible;\n\topacity: 1;\n}\n\ninput[type=range] {\n\t-webkit-appearance: none;\n\twidth: 100%;\n\tpadding: 0;\n\tmargin: 0;\n\tbackground-color: transparent;\n\tcursor: pointer;\n}\n\ninput[type=range]:focus {\n\toutline: 0;\n}\ninput[type=range]::-webkit-slider-runnable-track {\n\twidth: 100%;\n\theight: 30px;\n\tcursor: pointer;\n\tbackground: transparent;\n}\ninput[type=range]::-webkit-slider-thumb {\n\theight: 16px;\n\twidth: 16px;\n\tborder-radius: 50%;\n\tbackground: var(--c-f);\n\tcursor: pointer;\n\t-webkit-appearance: none;\n\tmargin-top: 7px;\n}\ninput[type=range]::-moz-range-track {\n\twidth: 100%;\n\theight: 30px;\n\tbackground-color: rgba(0, 0, 0, 0);\n}\ninput[type=range]::-moz-range-thumb {\n\tborder: 0 solid rgba(0, 0, 0, 0);\n\theight: 16px;\n\twidth: 16px;\n\tborder-radius: 50%;\n\tbackground: var(--c-f);\n\ttransform: translateY(5px);\n}\n#Colors input[type=range]::-webkit-slider-thumb {\n\theight: 18px;\n\twidth: 18px;\n\tborder: 2px solid var(--c-1);\n\tmargin-top: 5px;\n}\n#Colors input[type=range]::-moz-range-thumb {\n\tborder: 2px solid var(--c-1);\n}\n\n#Colors .sliderwrap {\n\tmargin: 2px 0 0;\n}\n\n/* Dynamically hide labels */\n.hd {\n\tdisplay: var(--bhd);\n}\n/* Do not hide quick load label in simplified mode on small screen widths */\n.simplified #pql .hd {\n\tdisplay: var(--bhd) !important;\n}\n\n#briwrap {\n\tmin-width: 300px;\n\tfloat: right;\n\tmargin-top: var(--bmt);\n}\n\n#picker {\n\tmargin: 4px auto 0 !important;\n\tmax-width: max-content;\n}\n\n/* buttons */\n.btn {\n\tpadding: 8px;\n\tmargin: 10px 4px;\n\twidth: 230px;\n\tfont-size: 19px;\n\tcolor: var(--c-d);\n\tcursor: pointer;\n\tborder-radius: 25px;\n\ttransition-duration: .3s;\n\t-webkit-backface-visibility: hidden;\n\t-webkit-transform: translate3d(0,0,0);\n\tbackface-visibility: hidden;\n\ttransform: translate3d(0,0,0);\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\tborder: 1px solid var(--c-3);\n\tbackground-color: var(--c-3);\n}\n#segutil .btn-s:hover,\n#segutil2 .btn-s:hover,\n#putil .btn-s:hover,\n.btn:hover {\n\tborder: 1px solid var(--c-5) /*!important*/;\n\tbackground-color: var(--c-5) /*!important*/;\n}\n.btn-s {\n\twidth: 100%;\n\tmargin: 0;\n}\n.btn-icon {\n\tmargin: -4px 4px -1px 0;\n\tvertical-align: middle;\n\tdisplay: inline-block;\n}\n.btn-n {\n\twidth: 230px;\n\tmargin: 0 8px 0 0;\n}\n.btn-p {\n\twidth: 120px;\n\tmargin: 5px 0;\n}\n.btn-xs, .btn-pl-del, .btn-pl-add {\n\twidth: 42px !important;\n\theight: 42px !important;\n\ttext-overflow: clip;\n}\n.btn-xs {\n\tmargin: 0;\n}\n#info .btn-xs {\n\tborder: 1px solid var(--c-4);\n}\n#btns .btn-xs {\n\tmargin: 0 4px;\n}\n\n#putil .btn-s {\n\twidth: 135px;\n}\n\n#nodes .ibtn {\n\tmargin: 0;\n}\n\n#segutil .btn-s, #segutil2 .btn-s, #putil .btn-s {\n\tbackground-color: var(--c-3);\n\tborder: 1px solid var(--c-3);\n}\n\n.btn-pl-del, .btn-pl-add {\n\tmargin: 0;\n\twhite-space: nowrap;\n}\na.btn {\n\tdisplay: block;\n\twhite-space: nowrap;\n\ttext-align: center;\n\tpadding: 9px 32px 7px 24px;\n\tposition: relative;\n\tbox-sizing: border-box;\n\tline-height: 24px;\n}\n\n/* Quick color select wrapper div */\n#qcs-w {\n\tmargin-top: 10px;\n}\n\n/* Quick color select buttons */\n.qcs {\n\tmargin: 2px;\n\tborder-radius: 14px;\n\tdisplay: inline-block;\n\twidth: 28px;\n\theight: 28px;\n\tline-height: 28px;\n}\n\n/* Quick color select Black and White button (has white/black border, depending on the theme) */\n.qcsb, .qcsw {\n\twidth: 26px;\n\theight: 26px;\n\tline-height: 26px;\n\tborder: 1px solid var(--c-f);\n}\n\n/* Hex color input wrapper div */\n#hexw {\n\tmargin-top: 5px;\n}\n\nselect {\n\tpadding: 4px 8px;\n\tmargin: 0;\n\tfont-size: 19px;\n\tbackground-color: var(--c-3);\n\tcolor: var(--c-d);\n\tcursor: pointer;\n\tborder: 0 solid var(--c-2);\n\tborder-radius: 20px;\n\ttransition-duration: .5s;\n\t-webkit-backface-visibility: hidden;\n\t-webkit-transform: translate3d(0,0,0);\n\t-webkit-appearance: none;\n\t-moz-appearance: none;\n\tbackface-visibility: hidden;\n\ttransform: translate3d(0,0,0);\n\ttext-overflow: ellipsis;\n}\n#tt {\n\ttext-align: center;\n}\nselect.sel-p, select.sel-pl, select.sel-ple {\n\tmargin: 5px 0;\n\twidth: 100%;\n\theight: 40px;\n\tpadding: 0 20px 0 8px;\n}\ndiv.sel-p {\n\tposition: relative;\n}\ndiv.sel-p:after {\n\tcontent: \"\";\n\tposition: absolute;\n\tright: 10px;\n\ttop: 22px;\n\twidth: 0;\n\theight: 0;\n\tborder-left: 8px solid transparent;\n\tborder-right: 8px solid transparent;\n\tborder-top: 8px solid var(--c-f);\n}\nselect.sel-ple {\n\ttext-align: center;\n}\nselect.sel-sg {\n\tmargin: 5px 0;\n\theight: 40px;\n}\noption {\n\tbackground-color: var(--c-3);\n\tcolor: var(--c-f);\n}\ninput[type=number],\ninput[type=text] {\n\tbackground: var(--c-3);\n\tcolor: var(--c-f);\n\tborder: 0 solid var(--c-2);\n\tborder-radius: 10px;\n\tpadding: 8px;\n\t/*margin: 6px 6px 6px 0;*/\n\tfont-size: 19px;\n\ttransition: background-color .2s;\n\toutline: 0;\n\t-webkit-appearance: textfield;\n\t-moz-appearance: textfield;\n\tappearance: textfield;\n}\n\ninput[type=number] {\n\ttext-align: right;\n\twidth: 50px;\n}\n\ninput[type=text] {\n\ttext-align: center;\n}\n\ninput[type=number]:focus,\ninput[type=text]:focus {\n\tbackground: var(--c-6);\n}\n\ninput[type=number]::-webkit-inner-spin-button,\ninput[type=number]::-webkit-outer-spin-button {\n\t-webkit-appearance: none;\n}\n\n#hexw input[type=text] {\n\twidth: 6em;\n}\n\ninput[type=text].ptxt {\n\twidth: calc(100% - 24px);\n}\n\ntextarea {\n\tbackground: var(--c-2);\n\tcolor: var(--c-f);\n\twidth: calc(100% - 14px); /* +padding=260px */\n\theight: 90px;\n\tborder-radius: 5px;\n\tborder: 2px solid var(--c-5);\n\toutline: 0;\n\tresize: none;\n\tfont-size: 19px;\n\tpadding: 5px;\n}\n\n.apitxt {\n\theight: 7em;\n}\n\n::selection {\n\tbackground: var(--c-b);\n}\n\n.ptxt {\n  \tmargin: -1px 4px 8px !important;\n}\n\n.stxt {\n\twidth: 50px !important;\n}\n\n.segname, .pname {\n\twhite-space: nowrap;\n\ttext-align: center;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\tline-height: 24px;\n\tpadding: 8px 24px;\n\tmax-width: 170px;\n\tposition: relative;\n}\n\n.segname .flr, .pname .flr {\n\ttransform: rotate(0deg);\n\t/*right: -6px;*/\n}\n\n/* segment power wrapper */\n.sbs {\n\t/*padding: 1px 0 1px 20px;*/\n\tdisplay: var(--sgp);\n\twidth: 100%;\n\tposition: relative;\n}\n\n.pname {\n\ttop: 1px;\n}\n.plname {\n\ttop: 0;\n}\n\n/* preset id number */\n.pid {\n\tposition: absolute;\n\ttop: 8px;\n\tleft: 12px;\n\tfont-size: 16px;\n\ttext-align: center;\n\tcolor: var(--c-b);\n}\n\n.newseg {\n\tcursor: default;\n}\n/*\n.ic {\n\tpadding: 6px 0 0 0;\n}\n*/\n/* color selector */\n#csl button {\n\twidth: 44px;\n\theight: 44px;\n\tmargin: 5px;\n\tborder: 2px solid var(--c-d) !important;\n\tbackground-color: #000;\n}\n/* selected color selector */\n#csl .sl {\n\tmargin: 2px;\n\twidth: 50px;\n\theight: 50px;\n\tborder-width: 5px !important;\n}\n\n.qcs, #namelabel { /* text shadow for name to be legible on grey backround */\n\ttext-shadow: -1px -1px 0 var(--c-1), 1px -1px 0 var(--c-1), -1px 1px 0 var(--c-1), 1px 1px 0 var(--c-1);\n}\n\n.psts {\n\tcolor: var(--c-f);\n\tmargin: 4px;\n}\n\n.pwr {\n\tcolor: var(--c-6);\n\tcursor: pointer;\n}\n\n.act {\n\tcolor: var(--c-f);\n}\n\n.del {\n\tposition: absolute;\n\tbottom: 8px;\n\tright: 8px;\n}\n\n.frz {\n\tleft: 10px;\n\tposition: absolute;\n\ttop: 8px;\n\tcursor: pointer;\n\tz-index: 1;\n}\n\n/* radiobuttons and checkmarks */\n.check, .radio {\n\tdisplay: block;\n\tposition: relative;\n\tcursor: pointer;\n}\n\n.revchkl {\n\tpadding: 4px 0 0 35px;\n\tmargin-bottom: 0;\n\tmargin-top: 8px;\n}\n\nTD .revchkl {\n\tpadding: 0 0 0 32px;\n\tmargin-top: 0;\n}\n\n.check input, .radio input {\n\tposition: absolute;\n\topacity: 0;\n\tcursor: pointer;\n\theight: 0;\n\twidth: 0;\n}\n\n.checkmark, .radiomark {\n\tposition: absolute;\n\theight: 24px;\n\twidth: 24px;\n\ttop: 0;\n\tbottom: 0;\n\tleft: 0;\n\tbackground-color: var(--c-3);\n\tborder: 1px solid var(--c-2);\n}\n\n.radiomark {\n\ttop: 8px;\n\tleft: 8px;\n\theight: 22px;\n\twidth: 22px;\n\tborder-radius: 50%;\n\tbackground-color: transparent;\n}\n\n.checkmark {\n\tborder-radius: 10px;\n}\n\n.radio:hover input ~ .radiomark,\n.check:hover input ~ .checkmark {\n\tbackground-color: var(--c-5);\n}\n\n.checkmark:after, .radiomark:after {\n\tcontent: \"\";\n\tposition: absolute;\n\tdisplay: none;\n}\n\n.check .checkmark:after {\n\tleft: 9px;\n\ttop: 4px;\n\twidth: 5px;\n\theight: 10px;\n\tborder: solid var(--c-f);\n\tborder-width: 0 3px 3px 0;\n}\n\n.rot45,\n.check .checkmark:after {\n\t-webkit-transform: rotate(45deg);\n\t-ms-transform: rotate(45deg);\n\ttransform: rotate(45deg);\n}\n\n.radio .radiomark:after {\n\twidth: 14px;\n\theight: 14px;\n\ttop: 50%;\n\tleft: 50%;\n\tmargin: -7px;\n\tborder-radius: 50%;\n\tbackground: var(--c-f);\n}\n\nTD .checkmark, TD .radiomark {\n\ttop: -6px;\n}\n\n.h {\n\tfont-size: 13px;\n\ttext-align: center;\n\tcolor: var(--c-b);\n}\n\n.bp {\n\tmargin-bottom: 8px;\n}\n\n/* segment & preset wrapper */\n.seg, .pres {\n\tbackground-color: var(--c-2);\n\t/*color: var(--c-f);*/ /* seems to affect only the Add segment button, which should be same color as reset segments */\n\tborder: 0 solid var(--c-f);\n\ttext-align: left;\n\ttransition: background-color .5s;\n\tborder-radius: 21px;\n}\n\n.seg {\n\ttop: auto !important;\t/* prevent sticky */\n\tbottom: auto !important;\n}\n/* checkmark labels */\n.seg .schkl {\n\tposition: absolute;\n\ttop: 7px;\n\tleft: 9px;\n}\n/* checkmark labels */\n.filter .fchkl, .option .ochkl {\n\tdisplay: inline-block;\n\tmin-width: .7em;\n\tpadding: 1px 4px 1px 32px;\n\ttext-align: left;\n\tline-height: 24px;\n\tvertical-align: middle;\n\t-webkit-filter: grayscale(100%); /* Safari 6.0 - 9.0 */\n\tfilter: grayscale(100%);\n}\n.filter .fchkl {\n\tmargin: 0 4px;\n\tmin-width: 20px;\n\tpointer-events: auto;\n}\n\n.lbl-l {\n\tfont-size: 13px;\n\ttext-align: center;\n\tpadding: 4px 0;\n}\n\n.lbl-s {\n\tdisplay: inline-block;\n\tmargin-top: 6px;\n\tfont-size: 13px;\n\twidth: 48%;\n\ttext-align: center;\n}\n\n/* list wrapper */\n.list {\n\tposition: relative;\n\ttransition: background-color .5s;\n\tmargin: auto auto 10px;\n\tline-height: 24px;\n}\n\n/* list item */\n.lstI {\n\talign-items: center;\n\tcursor: pointer;\n\tbackground-color: var(--c-2);\n\toverflow: hidden;\n\tposition: -webkit-sticky;\n\tposition: sticky;\n\tborder-radius: 21px;\n\tmargin: 0 auto 12px;\n\tmin-height: 40px;\n\tborder: 1px solid var(--c-2);\n\twidth: 100%;\n}\n\n#segutil {\n\tmargin-bottom: 12px;\n}\n\n#segcont > div:first-child, #fxFind  {\n\tmargin-top: 4px;\n}\n\n/* Simplify segments */\n.simplified #segcont .lstI {\n\tmargin-top: 4px;\n\tmin-height: unset;\n}\n\n/* selected item/element */\n.selected { /* has to be after .lstI since !important is not ok */\n\tbackground: var(--c-4);\n}\n\n#segcont .seg:hover:not([class*=\"expanded\"]),\n.lstI:hover:not([class*=\"expanded\"]) {\n\tbackground: var(--c-5);\n}\n\n.selected .checkmark,\n.selected .radiomark,\n.selected input[type=number],\n.selected input[type=text] {\n\tbackground-color: var(--c-3);\n}\n\n/* selected list item */\n.lstI.selected {\n\ttop: 0;\n\tbottom: 0;\n\tborder: 1px solid var(--c-4);\n}\n\n.lstI.sticky,\n.lstI.selected {\n\tz-index: 1;\n\tbox-shadow: 0 0 10px 4px var(--c-1);\n}\n\n.lstI .flr:hover {\n    background: var(--c-6);\n    border-radius: 100%;\n}\n\n#pcont .selected:not([class*=\"expanded\"]) {\n\tbottom: 52px;\n\ttop: 42px;\n}\n\n#fxlist .lstI.selected {\n\ttop: calc(var(--sti) + 42px);\n}\n#pallist .lstI.selected {\n\ttop: calc(var(--stp) + 42px);\n}\n\ndialog::backdrop {\n\tbackdrop-filter: blur(10px);\n\t-webkit-backdrop-filter: blur(10px);\n}\ndialog {\n\tmax-height: 70%;\n\tborder: 0;\n\tborder-radius: 10px;\n\tbackground: linear-gradient(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.1)), var(--c-3);\n\tbox-shadow: 4px 4px 10px 4px var(--c-1);\n\tcolor: var(--c-f);\n}\n\n#fxlist .lstI.sticky {\n\ttop: var(--sti);\n}\n#pallist .lstI.sticky {\n\ttop: var(--stp);\n}\n\n/* list item content */\n.lstIcontent {\n\tpadding: 9px 0 7px;\n\tposition: relative;\n}\n\n/* list item name (for sorting) */\n.lstIname {\n\twhite-space: nowrap;\n\ttext-overflow: ellipsis;\n\t-webkit-filter: grayscale(100%); /* Safari 6.0 - 9.0 */\n\tfilter: grayscale(100%);\n}\n\n/* list item palette preview */\n.lstIprev {\n\twidth: 100%;\n\theight: 6px;\n\tposition: absolute;\n\tbottom: 0;\n\tleft: 0;\n\tz-index: -1;\n}\n\n/* find/search element */\n.fnd {\n\tposition: relative;\n}\n\n.fnd input[type=\"text\"] {\n\tdisplay: block;\n\twidth: 100%;\n\tbox-sizing: border-box;\n\tpadding: 8px 40px 8px 44px;\n\tmargin: 4px auto 12px;\n\ttext-align: left;\n\tborder-radius: 21px;\n\tbackground: var(--c-2);\n\tborder: 1px solid var(--c-3);\n\t-webkit-filter: grayscale(100%); /* Safari 6.0 - 9.0 */\n\tfilter: grayscale(100%);\n}\n\n.fnd input[type=\"text\"]:focus {\n\tbackground-color: var(--c-4);\n}\n\n.fnd input[type=\"text\"]:not(:placeholder-shown),\n.fnd input[type=\"text\"]:hover {\n\tbackground-color: var(--c-3);\n}\n\n#fxFind.fnd input[type=\"text\"] {\n\tmargin-bottom: 0;\n}\n#fxFind {\n\tmargin-bottom: 12px;\n}\n\n/* segment & preset inner/expanded content */\n.segin,\n.presin {\n\tpadding: 8px;\n\tposition: relative;\n}\n\n.presin {\n\twidth: 100%; \n\tbox-sizing: border-box;\n}\n\n.btn-s,\n.btn-n {\n\tborder: 1px solid var(--c-2);\n\tbackground-color: var(--c-2);\n}\n.modal .btn:hover,\n.segin .btn:hover {\n\tborder: 1px solid var(--c-5) /*!important*/;\n\tbackground-color: var(--c-5) /*!important*/;\n}\n\n/* hidden list items, must be after .expanded */\n.pres .lstIcontent, .segin {\n\tdisplay: none;\n}\n\n.check input:checked ~ .checkmark:after,\n.radio input:checked ~ .radiomark:after,\n.show,\n.expanded .edit-icon,\n.expanded .segin, .expanded .presin, .expanded .sbs,\n.expanded {\n\tdisplay: inline-block !important;\n}\n.hide, .expanded .segin.hide, .expanded .presin.hide, .expanded .sbs.hide, .expanded .g-icon {\n\tdisplay: none !important;\n}\n\n.m6 {\n\tmargin: 6px 0;\n}\n\n.c {\n\ttext-align: center;\n}\n\n.po2 {\n\tdisplay: none;\n\tmargin-top: 8px;\n}\n\n.pwarn {\n\tcolor: red;\n}\n\n/* horizontal divider (playlist entries) */\n.hrz {\n\twidth: auto;\n\theight: 2px;\n\tbackground-color: var(--c-b);\n\tmargin: 3px 0;\n}\n\n::-webkit-scrollbar {\n\twidth: 6px;\n}\n::-webkit-scrollbar-track {\n\tbackground: transparent;\n}\n::-webkit-scrollbar-thumb {\n\tbackground: var(--c-sb);\n\topacity: .2;\n\tborder-radius: 5px;\n}\n::-webkit-scrollbar-thumb:hover {\n\tbackground: var(--c-sbh);\n}\n\n@media not all and (hover: none) {\n\t.sliderwrap:hover + output.sliderbubble {\n\t\tvisibility: visible;\n\t\topacity: 1;\n\t}\n}\n\n@media all and (max-width: 1023px) {\n\t.top button {\n\t\twidth: 8%;\n\t\tpadding: 10px 0 8px 0;\n\t}\n\t#buttonPcm {\n\t\tdisplay: none;\n\t}\n}\n\n@media all and (max-width: 335px) {\n\t.sliderbubble {\n\t\tdisplay: none;\n  \t}\n}\n\n@media all and (max-width: 550px) and (min-width: 374px) {\n\t#info table .btn, #nodes table .btn {\n\t\twidth: 200px;\n\t}\n\t#info .ibtn, #nodes .ibtn {\n\t\twidth: 145px;\n\t}\n\t#info div, #nodes div, #nodes a.btn {\n\t\tmax-width: 320px;\n\t}\n}\n\n@media all and (max-width: 420px) {\n\t#buttonNodes {\n\t\tdisplay: none;\n\t}\n}\n\n@media all and (max-width: 639px) {\n\t.top button {\n\t\twidth: 16.6%;\n\t\tpadding: 8px 0 4px 0;\n\t}\n\t#briwrap {\n\t\tmargin: 0 auto !important;\n\t\tfloat: none;\n\t\tdisplay: inline-block;\n\t}\n\t.hd {\n\t\tdisplay: none !important;\n\t}\n}\n\n@media all and (min-width: 420px) and (max-width: 639px) {\n\t.top button {\n\t\twidth: 14.28%;\n\t\tpadding: 8px 0 4px 0;\n\t}\n}\n\n@media all and (min-width: 640px) and (max-width: 767px) {\n\t#buttonNodes {\n\t\tdisplay: none;\n\t}\n}\n\n/* small screen & tablet \"PC mode\" support */\n@media all and (min-width: 1024px) and (max-width: 1249px) {\n\t#segutil, #segutil2, #segcont, #putil, #pcont, #pql, #fx, #palw, #psFind, #sliders {\n\t\twidth: 100%;\n\t\tmax-width: 280px;\n\t\tfont-size: 18px;\n\t}\n\t#putil .btn-s {\n\t\twidth: 114px;\n\t}\n\t#sliders .sliderbubble {\n\t\tdisplay: none;\n\t}\n\t#sliders .sliderwrap, .sbs .sliderwrap {\n\t\twidth: calc(100% - 42px);\n\t}\n\t#sliders .slider {\n\t\tpadding-right: 0;\n\t}\n\t#sliders .sliderwrap {\n\t\tleft: 12px;\n\t}\n\t.segname {\n\t\tmax-width: calc(100% - 110px);\n\t}\n\t.segt TD {\n\t\tpadding: 0 !important;\n\t}\n\tinput[type=\"number\"], input[type=text], select, textarea {\n\t\tfont-size: 18px;\n\t}\n\tinput[type=\"number\"] {\n\t\twidth: 32px;\n\t}\n\t.lstIcontent {\n\t\tpadding-left: 8px;\n\t}\n\t.revchkl {\n\t\tmax-width: 183px;\n\t\ttext-overflow: ellipsis;\n\t\toverflow-x: clip;\n\t}\n}\n"
  },
  {
    "path": "wled00/data/index.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, minimum-scale=1\">\n\t<meta charset=\"utf-8\">\n\t<meta name=\"theme-color\" content=\"#222222\">\n\t<meta content=\"yes\" name=\"apple-mobile-web-app-capable\">\n\t<link rel=\"shortcut icon\" href=\"data:image/x-icon;base64,AAABAAEAEBAAAAEAGACGAAAAFgAAAIlQTkcNChoKAAAADUlIRFIAAAAQAAAAEAgGAAAAH/P/YQAAAE1JREFUOI1j/P//PwOxgNGeAUMxE9G6cQCKDWAhpADZ2f8PMjBS3QW08QK20KaZC2gfC9hCnqouoNgARgY7zMxAyNlUdQHlXiAlO2MDAD63EVqNHAe0AAAAAElFTkSuQmCC\"/>\n\t<link rel=\"apple-touch-icon\" href=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAMAAAAKE/YAAAAANlBMVEUAAAAAKacAPv8APv8APv8APv8APv8AKacAQv8AJZsAKacAKacAPv8APv8AL8MAPv8APv8AQv/mTD7uAAAAEHRSTlMAf1mvpgzzka9/Vy3wnX9ic6GVaQAAAQhJREFUeNrt3EtuwkAQRdGGYAyYT9j/ZqN4GNkv9gSV4ZwVXJU8cVerGwAAAAAAUNTl9LXY6dJq6J/fiz37VsNuTfSu1SB6hmjRgWjRgWjRgWjRgWjRgehf5y1G70r/2Ha3w5T7foWhvVg3Pbxzq6w7lv5MRa8iWnQgWnQgWnQgWnQgWnQgenQtHT3spzxqT7p/+/MX0aJFj0SLDkSLDkSLDkSLDkSL/s/n7OhXRR8fNXb0q6KvrbSZSXetMtEzRIsORIsORIsORIsORIsORIdr/i+PHvYr3A9Tbsui7egn+2pf83//ky7RokWPRIsORIsORIsORIsORG/2gbhNPsUHAAAAAAD89QMg+PZb+jO0tAAAAABJRU5ErkJggg==\" sizes=\"180x180\"/>\n\t<title>WLED</title>\n\t<link rel=\"stylesheet\" href=\"index.css\">\n</head>\n<body>\n\n<div id=\"cv\" class=\"overlay\">Loading WLED UI...</div>\n<noscript><div class=\"overlay\" style=\"opacity:1;\">Sorry, WLED UI needs JavaScript!</div></noscript>\n<div id=\"bg\"></div>\n\n<div class=\"wrapper\" id=\"top\">\n\t<div class=\"tab top\">\n\t\t<div class=\"btnwrap\">\n\t\t\t<button id=\"buttonPower\" onclick=\"togglePower()\" class=\"tgl\"><i class=\"icons\">&#xe08f;</i><p class=\"tab-label\">Power</p></button>\n\t\t\t<button id=\"buttonNl\" onclick=\"toggleNl()\"><i class=\"icons\">&#xe2a2;</i><p class=\"tab-label\">Timer</p></button>\n\t\t\t<button id=\"buttonSync\" onclick=\"toggleSync()\"><i class=\"icons\">&#xe116;</i><p class=\"tab-label\">Sync</p></button>\n\t\t\t<button id=\"buttonSr\" onclick=\"toggleLiveview()\"><i class=\"icons\">&#xe410;</i><p class=\"tab-label\">Peek</p></button>\n\t\t\t<button id=\"buttonI\" onclick=\"toggleInfo()\"><i class=\"icons\">&#xe066;</i><p class=\"tab-label\">Info</p></button>\n\t\t\t<button id=\"buttonNodes\" onclick=\"toggleNodes()\"><i class=\"icons\">&#xe22d;</i><p class=\"tab-label\">Nodes</p></button>\n\t\t\t<button onclick=\"window.location.href=getURL('/settings');\"><i class=\"icons\">&#xe0a2;</i><p class=\"tab-label\">Config</p></button>\n\t\t\t<button id=\"buttonPcm\" onclick=\"togglePcMode(true)\"><i class=\"icons\">&#xe23d;</i><p class=\"tab-label\">PC Mode</p></button>\n\t\t</div>\n\t\t<div id=\"briwrap\">\n\t\t\t<p class=\"hd\">Brightness</p>\n\t\t\t<div class=\"slider\">\n\t\t\t\t<i class=\"icons slider-icon\" onclick=\"tglTheme()\" style=\"transform: translateY(2px);\">&#xe2a6;</i>\n\t\t\t\t<div class=\"sliderwrap il\">\n\t\t\t\t\t<input id=\"sliderBri\" onchange=\"setBri()\" oninput=\"updateTrail(this)\" max=\"255\" min=\"1\" type=\"range\" value=\"128\" />\n\t\t\t\t\t<div class=\"sliderdisplay\"></div>\n\t\t\t\t</div>\n\t\t\t\t<output class=\"sliderbubble\"></output>\n\t\t\t</div>\n\t\t</div>\n\t\t<iframe id=\"liveview\" src=\"about:blank\"></iframe>\n\t</div>\n</div>\n\n<div class =\"container\">\n\t<div id=\"Colors\" class=\"tabcontent\">\n\t\t<div id=\"picker\" class=\"noslide\"></div>\n\t\t<div id=\"hwrap\" class=\"slider\">\n\t\t\t<div title=\"Hue\" class=\"sliderwrap il\">\n\t\t\t\t<input id=\"sliderH\" class=\"noslide\" oninput=\"fromH()\" onchange=\"setColor(0)\" max=\"359\" min=\"0\" type=\"range\" value=\"0\" step=\"any\">\n\t\t\t\t<div class=\"sliderdisplay\" style=\"background: linear-gradient(90deg, #f00 2%, #ff0 19%, #0f0 35%, #0ff 52%, #00f 68%, #f0f 85%, #f00)\"></div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div id=\"swrap\" class=\"slider\">\n\t\t\t<div title=\"Saturation\" class=\"sliderwrap il\">\n\t\t\t\t<input id=\"sliderS\" class=\"noslide\" oninput=\"fromS()\" onchange=\"setColor(0)\" max=\"100\" min=\"0\" type=\"range\" value=\"100\" step=\"any\">\n\t\t\t\t<div class=\"sliderdisplay\" style=\"background: linear-gradient(90deg, #aaa 0%, #f00)\"></div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div id=\"vwrap\" class=\"slider\">\n\t\t\t<div title=\"Value/Brightness\" class=\"sliderwrap il\">\n\t\t\t\t<input id=\"sliderV\" class=\"noslide\" oninput=\"fromV()\" onchange=\"setColor(0)\" max=\"100\" min=\"0\" type=\"range\" value=\"100\" step=\"any\" />\n\t\t\t\t<div class=\"sliderdisplay\"></div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div id=\"kwrap\" class=\"slider\">\n\t\t\t<div title=\"Kelvin/Temperature\" class=\"sliderwrap il\">\n\t\t\t\t<input id=\"sliderK\" class=\"noslide\" oninput=\"fromK()\" onchange=\"setColor(0)\" max=\"10091\" min=\"1900\" type=\"range\" value=\"6550\" />\n\t\t\t\t<div class=\"sliderdisplay\"></div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div id=\"rgbwrap\">\n\t\t\t<div id=\"rwrap\" class=\"slider\">\n\t\t\t\t<div title=\"Red channel\" class=\"sliderwrap il\">\n\t\t\t\t\t<input id=\"sliderR\" class=\"noslide\" oninput=\"fromRgb()\" onchange=\"setColor(0)\" max=\"255\" min=\"0\" type=\"range\" value=\"128\" />\n\t\t\t\t\t<div class=\"sliderdisplay\"></div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div id=\"gwrap\" class=\"slider\">\n\t\t\t\t<div title=\"Green channel\" class=\"sliderwrap il\">\n\t\t\t\t\t<input id=\"sliderG\" class=\"noslide\" oninput=\"fromRgb()\" onchange=\"setColor(0)\" max=\"255\" min=\"0\" type=\"range\" value=\"128\" />\n\t\t\t\t\t<div class=\"sliderdisplay\"></div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div id=\"bwrap\" class=\"slider\">\n\t\t\t\t<div title=\"Blue channel\" class=\"sliderwrap il\">\n\t\t\t\t\t<input id=\"sliderB\" class=\"noslide\" oninput=\"fromRgb()\" onchange=\"setColor(0)\" max=\"255\" min=\"0\" type=\"range\" value=\"128\" />\n\t\t\t\t\t<div class=\"sliderdisplay\"></div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div id=\"wwrap\" class=\"slider\">\n\t\t\t<div id=\"whibri\" title=\"White channel\" class=\"sliderwrap il\">\n\t\t\t\t<input id=\"sliderW\" class=\"noslide\" oninput=\"fromW()\" onchange=\"setColor(0)\" max=\"255\" min=\"0\" type=\"range\" value=\"128\" />\n\t\t\t\t<div class=\"sliderdisplay\"></div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div id=\"wbal\" class=\"slider\">\n\t\t\t<div title=\"White balance\" class=\"sliderwrap il\">\n\t\t\t\t<input id=\"sliderA\" class=\"noslide\" onchange=\"setBalance(this.value)\" max=\"255\" min=\"0\" type=\"range\" value=\"128\" />\n\t\t\t\t<div class=\"sliderdisplay\"></div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div id=\"qcs-w\">\n\t\t\t<div class=\"qcs\" onclick=\"pC('#ff0000');\" style=\"background-color:#ff0000;\"></div>\n\t\t\t<div class=\"qcs\" onclick=\"pC('#ffa000');\" style=\"background-color:#ffa000;\"></div>\n\t\t\t<div class=\"qcs\" onclick=\"pC('#ffc800');\" style=\"background-color:#ffc800;\"></div>\n\t\t\t<div class=\"qcs\" onclick=\"pC('#ffe0a0');\" style=\"background-color:#ffe0a0;\"></div>\n\t\t\t<div class=\"qcs qcsw\" onclick=\"pC('#ffffff');\" style=\"background-color:#ffffff;\"></div>\n\t\t\t<div class=\"qcs qcsb\" onclick=\"pC('#000000');\" style=\"background-color:#000000;\"></div><br>\n\t\t\t<div class=\"qcs\" onclick=\"pC('#ff00ff');\" style=\"background-color:#ff00ff;\"></div>\n\t\t\t<div class=\"qcs\" onclick=\"pC('#0000ff');\" style=\"background-color:#0000ff;\"></div>\n\t\t\t<div class=\"qcs\" onclick=\"pC('#00ffc8');\" style=\"background-color:#00ffc8;\"></div>\n\t\t\t<div class=\"qcs\" onclick=\"pC('#08ff00');\" style=\"background-color:#08ff00;\"></div>\n\t\t\t<div class=\"qcs\" onclick=\"pC('rnd');\" title=\"Random\" style=\"background:linear-gradient(to right, red, orange, yellow, green, blue, purple);transform: translateY(-11px);\">R</div>\n\t\t</div>\n\t\t<div id=\"csl\">\n\t\t\t<button id=\"csl0\" title=\"Select slot\" class=\"btn\" onclick=\"selectSlot(0);\" data-r=\"0\" data-g=\"0\" data-b=\"0\" data-w=\"0\">1</button>\n\t\t\t<button id=\"csl1\" title=\"Select slot\" class=\"btn\" onclick=\"selectSlot(1);\" data-r=\"0\" data-g=\"0\" data-b=\"0\" data-w=\"0\">2</button>\n\t\t\t<button id=\"csl2\" title=\"Select slot\" class=\"btn\" onclick=\"selectSlot(2);\" data-r=\"0\" data-g=\"0\" data-b=\"0\" data-w=\"0\">3</button>\n\t\t</div>\n\t\t<p class=\"labels h\" id=\"cslLabel\"></p>\n\t\t<div id=\"hexw\">\n\t\t\t<i class=\"icons sel-icon\" onclick=\"tglRgb()\">&#xe22d;</i>\n\t\t\t<input id=\"hexc\" title=\"Hex RGB\" type=\"text\" class=\"noslide\" onkeydown=\"hexEnter()\" autocomplete=\"off\" maxlength=\"8\" />\n\t\t\t<button id=\"hexcnf\" class=\"btn btn-xs\" onclick=\"fromHex();\"><i class=\"icons btn-icon\">&#xe390;</i></button>\n\t\t</div>\n\n\t\t<div style=\"padding: 12px 0; display:flex; justify-content:center; gap:16px;\" id=\"btns\">\n\t\t\t<div>\n\t\t\t\t<button class=\"btn btn-xs\" title=\"File editor\" type=\"button\" onclick=\"window.location.href=getURL('/edit')\">\n\t\t\t\t<i class=\"icons btn-icon\">&#xe901;</i>\n\t\t\t\t</button>\n\t\t\t\t<div class=\"iconlabel\">Files</div>\n\t\t\t</div>\n\t\t\t<div>\n\t\t\t\t<button class=\"btn btn-xs\" title=\"PixelForge\" type=\"button\" onclick=\"window.location.href=getURL('/pixelforge.htm')\">\n\t\t\t\t<i class=\"icons btn-icon\">&#xe900;</i>\n\t\t\t\t</button>\n\t\t\t\t<div class=\"iconlabel\">PixelForge</div>\n\t\t\t</div>\n\t\t\t<div>\n\t\t\t\t<button class=\"btn btn-xs\" title=\"Custom palettes\" type=\"button\" onclick=\"window.location.href=getURL('/cpal.htm')\">\n\t\t\t\t<i class=\"icons btn-icon\">&#xe2b3;</i>\n\t\t\t\t</button>\n\t\t\t\t<div class=\"iconlabel\">Palettes</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<p class=\"labels hd\" id=\"pall\"><i class=\"icons sel-icon\" onclick=\"tglHex()\">&#xe2b3;</i> Color palette</p>\n\t\t<div id=\"palw\" class=\"il\">\n\t\t\t<div class=\"staytop fnd\">\n\t\t\t\t<input type=\"text\" placeholder=\"Search\" oninput=\"search(this,'pallist')\" onfocus=\"search(this,'pallist')\" />\n\t\t\t\t<i class=\"icons clear-icon\" onclick=\"clean(this)\">&#xe38f;</i>\n\t\t\t\t<i class=\"icons search-icon\">&#xe0a1;</i>\n\t\t\t</div>\n\t\t\t<div id=\"pallist\" class=\"list\">\n\t\t\t\t<div class=\"lstI\">\n\t\t\t\t\t<label class=\"radio schkl\" onclick=\"loadPalettes()\">\n\t\t\t\t\t\t<div class=\"lstIcontent\">\n\t\t\t\t\t\t\t<span class=\"lstIname\">\n\t\t\t\t\t\t\t\tLoading...\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</label>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<div id=\"Effects\" class=\"tabcontent\">\n\t\t<div id=\"fx\">\n\t\t\t<p class=\"labels hd\" id=\"modeLabel\">Effect mode</p>\n\t\t\t<div class=\"staytop fnd\" id=\"fxFind\" onmousedown=\"preventBlur(event);\">\n\t\t\t\t<input type=\"text\" placeholder=\"Search\" oninput=\"search(this,'fxlist')\" onfocus=\"filterFocus(event);search(this,'fxlist');\" onblur=\"filterFocus(event);\">\n\t\t\t\t<i class=\"icons clear-icon\" onclick=\"clean(this);\">&#xe38f;</i>\n\t\t\t\t<i class=\"icons search-icon\" style=\"cursor:pointer;\">&#xe0a1;</i>\n\t\t\t\t<div id=\"filters\" class=\"filter fade\">\n\t\t\t\t\t<label id=\"filterPal\" title=\"Uses palette\" class=\"check fchkl\">&#x1F3A8;\n\t\t\t\t\t\t<input type=\"checkbox\" data-flt=\"&#x1F3A8;\" onchange=\"filterFx();\">\n\t\t\t\t\t\t<span class=\"checkmark\"></span>\n\t\t\t\t\t</label>\n\t\t\t\t\t<label id=\"filter0D\" title=\"Single pixel\" class=\"check fchkl\">&#8226;\n\t\t\t\t\t\t<input type=\"checkbox\" data-flt=\"&#8226;\" onchange=\"filterFx();\">\n\t\t\t\t\t\t<span class=\"checkmark\"></span>\n\t\t\t\t\t</label>\n\t\t\t\t\t<label id=\"filter1D\" title=\"1D\" class=\"check fchkl\">&#8942;\n\t\t\t\t\t\t<input type=\"checkbox\" data-flt=\"&#8942;\" onchange=\"filterFx();\">\n\t\t\t\t\t\t<span class=\"checkmark\"></span>\n\t\t\t\t\t</label>\n\t\t\t\t\t<label id=\"filter2D\" title=\"2D\" class=\"check fchkl\">&#9638;\n\t\t\t\t\t\t<input type=\"checkbox\" data-flt=\"&#9638;\" onchange=\"filterFx();\">\n\t\t\t\t\t\t<span class=\"checkmark\"></span>\n\t\t\t\t\t</label>\n\t\t\t\t\t<label id=\"filterVol\" title=\"Volume\" class=\"check fchkl\">&#9834;\n\t\t\t\t\t\t<input type=\"checkbox\" data-flt=\"&#9834;\" onchange=\"filterFx();\">\n\t\t\t\t\t\t<span class=\"checkmark\"></span>\n\t\t\t\t\t</label>\n\t\t\t\t\t<label id=\"filterFreq\" title=\"Frequency\" class=\"check fchkl\">&#9835;\n\t\t\t\t\t\t<input type=\"checkbox\" data-flt=\"&#9835;\" onchange=\"filterFx();\">\n\t\t\t\t\t\t<span class=\"checkmark\"></span>\n\t\t\t\t\t</label>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div id=\"fxlist\" class=\"list\">\n\t\t\t\t<div class=\"lstI\">\n\t\t\t\t\t<label class=\"radio schkl\" onclick=\"loadFX()\">\n\t\t\t\t\t\t<div class=\"lstIcontent\">\n\t\t\t\t\t\t\t<span class=\"lstIname\">\n\t\t\t\t\t\t\t\tLoading...\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</label>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div id=\"sliders\">\n\t\t\t<div id=\"slider0\" class=\"slider\">\n\t\t\t\t<i class=\"icons slider-icon\" title=\"Freeze\" onclick=\"tglFreeze()\">&#xe325;</i>\n\t\t\t\t<div title=\"Effect speed\" class=\"sliderwrap il\">\n\t\t\t\t\t<input id=\"sliderSpeed\" class=\"noslide\" onchange=\"setSpeed()\" oninput=\"updateTrail(this)\" max=\"255\" min=\"0\" type=\"range\" value=\"128\" />\n\t\t\t\t\t<div class=\"sliderdisplay\"></div>\n\t\t\t\t</div>\n\t\t\t\t<output class=\"sliderbubble\"></output>\n\t\t\t</div>\n\t\t\t<div id=\"slider1\" class=\"slider\">\n\t\t\t\t<i class=\"icons slider-icon\" title=\"Toggle labels\" onclick=\"tglLabels()\">&#xe409;</i>\n\t\t\t\t<div title=\"Effect intensity\" class=\"sliderwrap il\">\n\t\t\t\t\t<input id=\"sliderIntensity\" class=\"noslide\" onchange=\"setIntensity()\" oninput=\"updateTrail(this)\" max=\"255\" min=\"0\" type=\"range\" value=\"128\" />\n\t\t\t\t\t<div class=\"sliderdisplay\"></div>\n\t\t\t\t</div>\n\t\t\t\t<output class=\"sliderbubble\"></output>\n\t\t\t</div>\n\t\t\t<div id=\"slider2\" class=\"slider hide\">\n\t\t\t\t<i class=\"icons slider-icon\">&#xe410;</i>\n\t\t\t\t<div title=\"Custom 1\" class=\"sliderwrap il\">\n\t\t\t\t\t<input id=\"sliderC1\" class=\"noslide\" onchange=\"setCustom(1)\" oninput=\"updateTrail(this)\" max=\"255\" min=\"0\" type=\"range\" value=\"0\" />\n\t\t\t\t\t<div class=\"sliderdisplay\"></div>\n\t\t\t\t</div>\n\t\t\t\t<output class=\"sliderbubble\"></output>\n\t\t\t</div>\n\t\t\t<div id=\"slider3\" class=\"slider hide\">\n\t\t\t\t<i class=\"icons slider-icon\">&#xe0a2;</i>\n\t\t\t\t<div title=\"Custom 2\" class=\"sliderwrap il\">\n\t\t\t\t\t<input id=\"sliderC2\" class=\"noslide\" onchange=\"setCustom(2)\" oninput=\"updateTrail(this)\" max=\"255\" min=\"0\" type=\"range\" value=\"0\" />\n\t\t\t\t\t<div class=\"sliderdisplay\"></div>\n\t\t\t\t</div>\n\t\t\t\t<output class=\"sliderbubble\"></output>\n\t\t\t</div>\n\t\t\t<div id=\"slider4\" class=\"slider hide\">\n\t\t\t\t<i class=\"icons slider-icon\">&#xe0e8;</i>\n\t\t\t\t<div title=\"Custom 3\" class=\"sliderwrap il\">\n\t\t\t\t\t<input id=\"sliderC3\" class=\"noslide\" onchange=\"setCustom(3)\" oninput=\"updateTrail(this)\" max=\"31\" min=\"0\" type=\"range\" value=\"0\" />\n\t\t\t\t\t<div class=\"sliderdisplay\"></div>\n\t\t\t\t</div>\n\t\t\t\t<output class=\"sliderbubble\"></output>\n\t\t\t</div>\n\t\t\t<div id=\"fxopt\" class=\"option fade\">\n\t\t\t\t<label id=\"opt0\" title=\"Check 1\" class=\"check ochkl hide\"><i class=\"icons\">&#xe2b3;</i>\n\t\t\t\t\t<input id=\"checkO1\" type=\"checkbox\" onchange=\"setOption(1, this.checked)\">\n\t\t\t\t\t<span class=\"checkmark\"></span>\n\t\t\t\t</label>\n\t\t\t\t<label id=\"opt1\" title=\"Check 2\" class=\"check ochkl hide\"><i class=\"icons\">&#xe34b;</i>\n\t\t\t\t\t<input id=\"checkO2\" type=\"checkbox\" onchange=\"setOption(2, this.checked)\">\n\t\t\t\t\t<span class=\"checkmark\"></span>\n\t\t\t\t</label>\n\t\t\t\t<label id=\"opt2\" title=\"Check 3\" class=\"check ochkl hide\"><i class=\"icons\">&#xe04c;</i>\n\t\t\t\t\t<input id=\"checkO3\" type=\"checkbox\" onchange=\"setOption(3, this.checked)\">\n\t\t\t\t\t<span class=\"checkmark\"></span>\n\t\t\t\t</label>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<div id=\"Segments\" class=\"tabcontent\">\n\t\t<p class=\"labels hd\" id=\"segLabel\">Segments</p>\n\t\t<div id=\"segcont\">\n\t\t\tLoading...\n\t\t</div>\n\t\t<div id=\"segutil\" class=\"staybot\">\n\t\t</div>\n\t\t<div id=\"segutil2\">\n\t\t\t<button class=\"btn btn-s\" id=\"rsbtn\" onclick=\"rSegs()\">Reset segments</button>\n\t\t</div>\n\t\t<p>Transition: <input id=\"tt\" type=\"number\" min=\"0\" max=\"65.5\" step=\"0.1\" value=\"0.7\" onchange=\"parseFloat(this.value)===0?gId('bsp').classList.add('hide'):gId('bsp').classList.remove('hide');\">&nbsp;s</p>\n\t\t<div id=\"bsp\" class=\"sel-p\"><select id=\"bs\" class=\"sel-ple\" onchange=\"requestJson({'bs':parseInt(this.value)})\">\n\t\t\t<option value=\"0\">Fade</option>\n\t\t\t<option value=\"1\">Fairy Dust</option>\n\t\t\t<option value=\"2\">Swipe right</option>\n\t\t\t<option value=\"3\">Swipe left</option>\n\t\t\t<option value=\"16\">Push right</option>\n\t\t\t<option value=\"17\">Push left</option>\n\t\t\t<option value=\"4\">Outside-in</option>\n\t\t\t<option value=\"5\">Inside-out</option>\n\t\t\t<option value=\"6\" data-type=\"2D\">Swipe up</option>\n\t\t\t<option value=\"7\" data-type=\"2D\">Swipe down</option>\n\t\t\t<option value=\"8\" data-type=\"2D\">Open H</option>\n\t\t\t<option value=\"9\" data-type=\"2D\">Open V</option>\n\t\t\t<option value=\"18\" data-type=\"2D\">Push up</option>\n\t\t\t<option value=\"19\" data-type=\"2D\">Push down</option>\n\t\t\t<option value=\"10\" data-type=\"2D\">Swipe TL</option>\n\t\t\t<option value=\"11\" data-type=\"2D\">Swipe TR</option>\n\t\t\t<option value=\"12\" data-type=\"2D\">Swipe BR</option>\n\t\t\t<option value=\"13\" data-type=\"2D\">Swipe BL</option>\n\t\t\t<option value=\"14\" data-type=\"2D\">Circular Out</option>\n\t\t\t<option value=\"15\" data-type=\"2D\">Circular In</option>\n\t\t</select></div>\n\t\t<p id=\"ledmap\" class=\"hide\"></p>\n\t</div>\n\n\t<div id=\"Presets\" class=\"tabcontent\">\n\t\t<div id=\"pql\">\n\t\t</div>\n\t\t<p class=\"labels hd\">Presets</p>\n\t\t<div class=\"staytop fnd\" id=\"psFind\">\n\t\t\t<input type=\"text\" placeholder=\"Search\" oninput=\"search(this,'pcont')\" onfocus=\"search(this,'pcont')\" />\n\t\t\t<i class=\"icons clear-icon\" onclick=\"clean(this);\">&#xe38f;</i>\n\t\t\t<i class=\"icons search-icon\">&#xe0a1;</i>\n\t\t</div>\n\t\t<div id=\"pcont\" class=\"list\">\n\t\t\t<span onclick=\"loadPresets()\">Loading...</span>\n\t\t</div>\n\t\t<div id=\"putil\" class=\"staybot\">\n\t\t</div>\n\t</div>\n</div>\n\n<div class=\"tab bot\" id=\"bot\">\n\t<button class=\"tablinks\" onclick=\"openTab(0)\"><i class=\"icons\">&#xe2b3;</i><p class=\"tab-label\">Colors</p></button>\n\t<button class=\"tablinks\" onclick=\"openTab(1)\"><i class=\"icons\">&#xe23d;</i><p class=\"tab-label\">Effects</p></button>\n\t<button class=\"tablinks\" onclick=\"openTab(2)\"><i class=\"icons\">&#xe34b;</i><p class=\"tab-label\">Segments</p></button>\n\t<button class=\"tablinks\" onclick=\"openTab(3)\"><i class=\"icons\">&#xe04c;</i><p class=\"tab-label\">Presets</p></button>\n</div>\n\n<div id=\"connind\"></div>\n<div id=\"toast\" onclick=\"clearErrorToast(100);\"></div>\n<div id=\"namelabel\" onclick=\"toggleNodes()\"></div>\n<div id=\"info\" class=\"modal\">\n\t<button class=\"btn btn-xs close\" onclick=\"toggleInfo()\"><i class=\"icons rot45\">&#xe18a;</i></button>\n\t<div id=\"imgw\">\n\t\t<img class=\"wi\" alt=\"\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAFCAYAAAC5Fuf5AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABbSURBVChTlY9bDoAwDMNW7n9nwCipytQN4Z8tbrTHmDmF4oPzyldwRqp1SSdnV/NuZuzqerAByxXznBw3igkeFEfXyUuhK/yFM0CxJfyqXZEOc6/Sr9/bf7uIC5Nwd7orMvAPAAAAAElFTkSuQmCC\" />\n\t</div>\n\t<div id=\"kv\">Loading...</div><br>\n\t<div>\n\t\t<button class=\"btn ibtn\" onclick=\"requestJson()\">Refresh</button>\n\t\t<button class=\"btn ibtn\" onclick=\"toggleNodes()\">Instance List</button>\n\t\t<button class=\"btn ibtn\" onclick=\"window.open(getURL('/update'),'_self');\" id=\"updBt\">Update WLED</button>\n\t\t<button class=\"btn ibtn\" id=\"resetbtn\" onclick=\"cnfReset()\">Reboot WLED</button>\n\t</div>\n\t<br>\n\t<span class=\"h\">Made with&#32;<span id=\"heart\">&#10084;&#xFE0E;</span>&#32;by&#32;<a href=\"https://github.com/Aircoookie/\" target=\"_blank\">Aircoookie</a>&#32;and the&#32;<a href=\"https://wled.discourse.group/\" target=\"_blank\">WLED community</a></span>\n</div>\n\n<div id=\"nodes\" class=\"modal\">\n\t<button class=\"btn btn-xs close\" onclick=\"toggleNodes()\"><i class=\"icons rot45\">&#xe18a;</i></button>\n\t<div id=\"ndlt\">WLED instances</div>\n\t<div id=\"kn\">Loading...</div>\n\t<div style=\"position:sticky;bottom:0;\">\n\t\t<button class=\"btn ibtn\" onclick=\"loadNodes()\">Refresh</button>\n\t</div>\n</div>\n\n<div id=\"mlv2D\" class=\"modal\">\n\t<div id=\"klv2D\" style=\"width:100%; height:100%\">Loading...</div>\n</div>\n\n<div id=\"rover\" class=\"modal\">\n\t<i class=\"icons huge\">&#xe410;</i><br>\n\t<div id=\"lv\">?</div><br><br>\n\tTo use built-in effects, use an override button below.<br>\n\tYou can return to realtime mode by pressing the star in the top left corner.<br>\n\t<button class=\"btn ibtn\" onclick=\"setLor(1)\">Override once</button>\n\t<button class=\"btn ibtn\" onclick=\"setLor(2)\">Override until reboot</button><br>\n\t<span class=\"h\">For best performance, it is recommended to turn off the streaming source when not in use.</span>\n</div>\n\n<i id=\"roverstar\" class=\"icons huge\" onclick=\"setLor(0)\">&#xe410;</i><br>\n\n<!-- \n\tIf you want to load iro.js and rangetouch.js as consecutive requests, you can do it like it was done in 0.14.0:\n\thttps://github.com/wled/WLED/blob/v0.14.0/wled00/data/index.htm\n\tA more compact approach is implemented to load iro.js at the beginning of index.js.\n-->\n<!--  <script src=\"iro.js\"></script> NOTE: iro.js is loaded at the beginning of index.js -->\n<script src=\"rangetouch.js\"></script>\n<script src=\"index.js\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "wled00/data/index.js",
    "content": "//page js\nvar loc = false, locip, locproto = \"http:\";\nvar isOn = false, nlA = false, isLv = false, isInfo = false, isNodes = false, syncSend = false/*, syncTglRecv = true*/;\nvar hasWhite = false, hasRGB = false, hasCCT = false, has2D = false;\nvar nlDur = 60, nlTar = 0;\nvar nlMode = false;\nvar segLmax = 0; // size (in pixels) of largest selected segment\nvar selectedFx = 0;\nvar selectedPal = 0;\nvar csel = 0; // selected color slot (0-2)\nvar cpick; // iro color picker\nvar currentPreset = -1;\nvar lastUpdate = 0;\nvar segCount = 0, ledCount = 0, lowestUnused = 0, maxSeg = 0, lSeg = 0;\nvar pcMode = false, pcModeA = false, lastw = 0, wW;\nvar simplifiedUI = false;\nvar tr = 7;\nvar d = document;\nconst ranges = RangeTouch.setup('input[type=\"range\"]', {});\nvar palettesData;\nvar fxdata = [];\nvar pJson = {}, eJson = {}, lJson = {};\nvar plJson = {}; // array of playlists\nvar pN = \"\", pI = 0, pNum = 0;\nvar pmt = 1, pmtLS = 0;\nvar lastinfo = {};\nvar isM = false, mw = 0, mh=0;\nvar ws, wsRpt=0;\nvar cfg = {\n\ttheme:{base:\"dark\", bg:{url:\"\", rnd: false, rndGrayscale: false, rndBlur: false}, alpha:{bg:0.6,tab:0.8}, color:{bg:\"\"}},\n\tcomp :{colors:{picker: true, rgb: false, quick: true, hex: false},\n\t\t  labels:true, pcmbot:false, pid:true, seglen:false, segpwr:false, segexp:false,\n\t\t  css:true, hdays:false, fxdef:true, on:0, off:0, idsort: false}\n};\n// [year, month (0 -> January, 11 -> December), day, duration in days, image url]\nvar hol = [\n\t[0, 11, 24, 4, \"https://aircoookie.github.io/xmas.png\"],\t\t// christmas\n\t[0, 2, 17, 1, \"https://images.alphacoders.com/491/491123.jpg\"],\t// st. Patrick's day\n\t[2026, 3, 5, 2, \"https://aircoookie.github.io/easter.png\"],\t\t// easter 2026\n\t[2027, 2, 28, 2, \"https://aircoookie.github.io/easter.png\"],\t// easter 2027\n\t//[2028, 3, 16, 2, \"https://aircoookie.github.io/easter.png\"],\t// easter 2028\n\t[0, 6, 4, 1, \"https://images.alphacoders.com/516/516792.jpg\"],\t// 4th of July\n\t[0, 0, 1, 1, \"https://images.alphacoders.com/119/1198800.jpg\"]\t// new year\n];\n\n// load iro.js sequentially to avoid 503 errors, retries until successful\n(function loadIro() {\n\tconst l = d.createElement('script');\n\tl.src = 'iro.js';\n\tl.onload = () => {\n\t\tcpick = new iro.ColorPicker(\"#picker\", {\n\t\t\twidth: 260,\n\t\t\twheelLightness: false,\n\t\t\twheelAngle: 270,\n\t\t\twheelDirection: \"clockwise\",\n\t\t\tlayout: [{component: iro.ui.Wheel, options: {}}]\n\t\t});\n\t\td.readyState === 'complete' ? onLoad() : window.addEventListener('load', onLoad);\n\t};\n\tl.onerror = () => setTimeout(loadIro, 100);\n\tdocument.head.appendChild(l);\n})();\n\n\nfunction handleVisibilityChange() {if (!d.hidden && new Date () - lastUpdate > 3000) requestJson();}\nfunction sCol(na, col) {d.documentElement.style.setProperty(na, col);}\nfunction gId(c) {return d.getElementById(c);}\nfunction gEBCN(c) {return d.getElementsByClassName(c);}\nfunction isEmpty(o) {for (const i in o) return false; return true;}\nfunction isObj(i) {return (i && typeof i === 'object' && !Array.isArray(i));}\nfunction isNumeric(n) {return !isNaN(parseFloat(n)) && isFinite(n);}\n\n// returns true if dataset R, G & B values are 0\nfunction isRgbBlack(a) {return (parseInt(a.r) == 0 && parseInt(a.g) == 0 && parseInt(a.b) == 0);}\n\n// returns RGB color from a given dataset\nfunction rgbStr(a) {return \"rgb(\" + a.r + \",\" + a.g + \",\" + a.b + \")\";}\n\n// brightness approximation for selecting white as text color if background bri < 127, and black if higher\nfunction rgbBri(a) {return 0.2126*parseInt(a.r) + 0.7152*parseInt(a.g) + 0.0722*parseInt(a.b);}\n\n// sets background of color slot selectors\nfunction setCSL(cs)\n{\n\tlet w = cs.dataset.w ? parseInt(cs.dataset.w) : 0;\n\tlet hasShadow = getComputedStyle(cs).textShadow !== \"none\";\n\tif (hasRGB && !isRgbBlack(cs.dataset)) {\n\t\tif (!hasShadow) cs.style.color = rgbBri(cs.dataset) > 127 ? \"#000\":\"#fff\"; // if text has no CSS \"shadow\"\n\t\tcs.style.background = (hasWhite && w > 0) ? `linear-gradient(180deg, ${rgbStr(cs.dataset)} 30%, rgb(${w},${w},${w}))` : rgbStr(cs.dataset);\n\t} else {\n\t\tif (hasRGB && !hasWhite) w = 0;\n\t\tcs.style.background = `rgb(${w},${w},${w})`;\n\t\tif (!hasShadow) cs.style.color = w > 127 ? \"#000\":\"#fff\";\n\t}\n}\n\nfunction applyCfg()\n{\n\tcTheme(cfg.theme.base === \"light\");\n\tvar bg = cfg.theme.color.bg;\n\tif (bg) sCol('--c-1', bg);\n\tvar l = cfg.comp.labels;\n\tsCol('--tbp', l ? \"14px 14px 10px 14px\":\"10px 22px 4px 22px\");\n\tsCol('--bbp', l ? \"9px 0 7px 0\":\"10px 0 4px 0\");\n\tsCol('--bhd', l ? \"block\":\"none\"); // show/hide labels\n\tsCol('--bmt', l ? \"0px\":\"5px\");\n\tsCol('--t-b', cfg.theme.alpha.tab);\n\tsCol('--sgp', !cfg.comp.segpwr ? \"block\":\"none\"); // show/hide segment power\n\tsize();\n\tlocalStorage.setItem('wledUiCfg', JSON.stringify(cfg));\n\tif (lastinfo.leds) updateUI(); // update component visibility\n}\n\nfunction tglHex()\n{\n\tcfg.comp.colors.hex = !cfg.comp.colors.hex;\n\tapplyCfg();\n}\n\nfunction tglTheme()\n{\n\tcfg.theme.base = (cfg.theme.base === \"light\") ? \"dark\":\"light\";\n\tapplyCfg();\n}\n\nfunction tglLabels()\n{\n\tcfg.comp.labels = !cfg.comp.labels;\n\tapplyCfg();\n}\n\nfunction tglRgb()\n{\n\tcfg.comp.colors.rgb = !cfg.comp.colors.rgb;\n\tcfg.comp.colors.picker = !cfg.comp.colors.picker;\n\tapplyCfg();\n}\n\nfunction cTheme(light) {\n\tif (light) {\n\tsCol('--c-1','#eee');\n\tsCol('--c-f','#000');\n\tsCol('--c-2','#ddd');\n\tsCol('--c-3','#bbb');\n\tsCol('--c-4','#aaa');\n\tsCol('--c-5','#999');\n\tsCol('--c-6','#999');\n\tsCol('--c-8','#888');\n\tsCol('--c-b','#444');\n\tsCol('--c-c','#333');\n\tsCol('--c-e','#111');\n\tsCol('--c-d','#222');\n\tsCol('--c-r','#a21');\n\tsCol('--c-g','#2a1');\n\tsCol('--c-l','#26c');\n\tsCol('--c-o','rgba(204, 204, 204, 0.9)');\n\tsCol('--c-sb','#0003'); sCol('--c-sbh','#0006');\n\tsCol('--c-tb','rgba(204, 204, 204, var(--t-b))');\n\tsCol('--c-tba','rgba(170, 170, 170, var(--t-b))');\n\tsCol('--c-tbh','rgba(204, 204, 204, var(--t-b))');\n\tgId('imgw').style.filter = \"invert(0.8)\";\n\t} else {\n\tsCol('--c-1','#111');\n\tsCol('--c-f','#fff');\n\tsCol('--c-2','#222');\n\tsCol('--c-3','#333');\n\tsCol('--c-4','#444');\n\tsCol('--c-5','#555');\n\tsCol('--c-6','#666');\n\tsCol('--c-8','#888');\n\tsCol('--c-b','#bbb');\n\tsCol('--c-c','#ccc');\n\tsCol('--c-e','#eee');\n\tsCol('--c-d','#ddd');\n\tsCol('--c-r','#e42');\n\tsCol('--c-g','#4e2');\n\tsCol('--c-l','#48a');\n\tsCol('--c-o','rgba(34, 34, 34, 0.9)');\n\tsCol('--c-sb','#fff3'); sCol('--c-sbh','#fff5');\n\tsCol('--c-tb','rgba(34, 34, 34, var(--t-b))');\n\tsCol('--c-tba','rgba(102, 102, 102, var(--t-b))');\n\tsCol('--c-tbh','rgba(51, 51, 51, var(--t-b))');\n\tgId('imgw').style.filter = \"unset\";\n\t}\n}\n\nfunction loadBg() {\n\tconst { url: iUrl, rnd: iRnd } = cfg.theme.bg;\n\tconst bg = gId('bg');\n\tconst img = d.createElement(\"img\");\n\timg.src = iUrl;\n\tif (!iUrl || iRnd) {\n\t\tconst today = new Date();\n\t\tfor (const holiday of (hol || [])) {\n\t\t\tconst year = holiday[0] == 0 ? today.getFullYear() : holiday[0];\n\t\t\tconst holidayStart = new Date(year, holiday[1], holiday[2]);\n\t\t\tconst holidayEnd = new Date(holidayStart);\n\t\t\tholidayEnd.setDate(holidayEnd.getDate() + holiday[3]);\n\t\t\tif (today >= holidayStart && today <= holidayEnd) img.src = holiday[4];\n\t\t}\n\t}\n\timg.addEventListener('load', (e) => {\n\t\tvar a = parseFloat(cfg.theme.alpha.bg);\n\t\tif (isNaN(a)) a = 0.6;\n\t\tbg.style.opacity = a;\n\t\tbg.style.backgroundImage = `url(${img.src})`;\n\t\tgId('namelabel').style.color = \"var(--c-c)\"; // improve namelabel legibility on background image\n\t});\n}\n\nfunction loadSkinCSS(cId) {\n\treturn new Promise((resolve, reject) => {\n\t\tif (gId(cId)) return resolve();\n\t\tconst l = d.createElement('link');\n\t\tl.id = cId;\n\t\tl.rel = 'stylesheet';\n\t\tl.href = getURL('/skin.css');\n\t\tl.onload = resolve;\n\t\tl.onerror = reject;\n\t\td.head.appendChild(l);\n\t});\n}\n\nfunction getURL(path) {\n\treturn (loc ? locproto + \"//\" + locip : \"\") + path;\n}\nfunction onLoad()\n{\n\tlet l = window.location;\n\tif (l.protocol == \"file:\") {\n\t\tloc = true;\n\t\tlocip = localStorage.getItem('locIp');\n\t\tif (!locip) {\n\t\t\tlocip = prompt(\"File Mode. Please enter WLED IP!\");\n\t\t\tlocalStorage.setItem('locIp', locip);\n\t\t}\n\t} else {\n\t\t// detect reverse proxy and/or HTTPS\n\t\tlet pathn = l.pathname;\n\t\tlet paths = pathn.slice(1,pathn.endsWith('/')?-1:undefined).split(\"/\");\n\t\tlocproto = l.protocol;\n\t\tlocip = l.hostname + (l.port ? \":\" + l.port : \"\");\n\t\tif (paths.length > 0 && paths[0]!==\"\") {\n\t\t\tloc = true;\n\t\t\tlocip +=  \"/\" + paths.join('/');\n\t\t} else if (locproto===\"https:\") {\n\t\t\tloc = true;\n\t\t}\n\t}\n\tvar sett = localStorage.getItem('wledUiCfg');\n\tif (sett) cfg = mergeDeep(cfg, JSON.parse(sett));\n\n\ttooltip();\n\tresetPUtil();\n\tinitFilters();\n\n\tif (localStorage.getItem('pcm') == \"true\" || (!/Mobi/.test(navigator.userAgent) && localStorage.getItem('pcm') == null)) togglePcMode(true);\n\tapplyCfg();\n\tif (cfg.comp.hdays) { //load custom holiday list\n\t\tfetch(getURL(\"/holidays.json\"), {\t// may be loaded from external source\n\t\t\tmethod: 'get'\n\t\t})\n\t\t.then((res)=>{\n\t\t\t//if (!res.ok) showErrorToast();\n\t\t\treturn res.json();\n\t\t})\n\t\t.then((json)=>{\n\t\t\tif (Array.isArray(json)) hol = json;\n\t\t\t//TODO: do some parsing first\n\t\t})\n\t\t.catch((e)=>{\n\t\t\tconsole.log(\"No array of holidays in holidays.json. Defaults loaded.\");\n\t\t})\n\t\t.finally(()=>{\n\t\t\tloadBg();\n\t\t});\n\t} else\n\t\tloadBg();\n\n\tselectSlot(0);\n\tupdateTablinks(0);\n\thandleLocationHash();\n\tcpick.on(\"input:end\", () => {setColor(1);});\n\tcpick.on(\"color:change\", () => {updatePSliders()});\n\tpmtLS = localStorage.getItem('wledPmt');\n\n\t// Load initial data sequentially, no parallel requests to avoid \"503\" errors when heap is low (slower but much more reliable)\n\t(async ()=>{\n\t\ttry {\n\t\t\tawait loadPalettes();        // loads base palettes and builds #pallist (safe first)\n\t\t\tawait loadFXData();          // loads fx data\n\t\t\tawait loadFX();              // populates effect list\n\t\t\tawait requestJson();         // updates info variables\n\t\t\tawait loadPalettesData();    // fills palettesData[] for previews\n\t\t\tpopulatePalettes();          // repopulate with custom palettes now that cpalcount is known\n\t\t\tif(pmt == pmtLS) populatePresets(true); // load presets from localStorage if signature matches (i.e. no device reboot)\n\t\t\telse await loadPresets();    // load and populate presets\n\t\t\tif (cfg.comp.css) await loadSkinCSS('skinCss');\n\t\t\tif (!ws) makeWS();\n\t\t} catch(e) {\n\t\t\tshowToast(\"Init failed: \" + e, true);\n\t\t}\n\t})();\n\tresetUtil();\n\n\td.addEventListener(\"visibilitychange\", handleVisibilityChange, false);\n\t//size();\n\tgId(\"cv\").style.opacity=0;\n\td.querySelectorAll('input[type=\"range\"]').forEach((sl)=>{\n\t\tsl.addEventListener('touchstart', toggleBubble);\n\t\tsl.addEventListener('touchend', toggleBubble);\n\t});\n}\n\nfunction updateTablinks(tabI)\n{\n\tvar tablinks = gEBCN(\"tablinks\");\n\tfor (var i of tablinks) i.classList.remove('active');\n\ttablinks[tabI].classList.add('active');\n}\n\nfunction openTab(tabI, force = false)\n{\n\tif (pcMode && !force) return;\n\tiSlide = tabI;\n\t_C.classList.toggle('smooth', false);\n\t_C.style.setProperty('--i', iSlide);\n\tupdateTablinks(tabI);\n\tswitch (tabI) {\n\t\tcase 0: window.location.hash = \"Colors\"; break;\n\t\tcase 1: window.location.hash = \"Effects\"; break;\n\t\tcase 2: window.location.hash = \"Segments\"; break;\n\t\tcase 3: window.location.hash = \"Presets\"; break;\n\t}\n}\n\nfunction handleLocationHash() {\n\tswitch (window.location.hash) {\n\t\tcase \"#Colors\": openTab(0); break;\n\t\tcase \"#Effects\": openTab(1); break;\n\t\tcase \"#Segments\": openTab(2); break;\n\t\tcase \"#Presets\": openTab(3); break;\n\t}\n}\n\nvar timeout;\nfunction showToast(text, error = false)\n{\n\tvar x = gId('toast');\n\t//if (error) text += '<i class=\"icons btn-icon\" style=\"transform:rotate(45deg);position:absolute;top:10px;right:0px;\" onclick=\"clearErrorToast(100);\">&#xe18a;</i>';\n\tx.innerHTML = text;\n\tx.classList.add(error ? 'error':'show');\n\tclearTimeout(timeout);\n\tx.style.animation = 'none';\n\ttimeout = setTimeout(()=>{ x.classList.remove('show'); }, 2900);\n\tif (error) console.log(text);\n}\n\nfunction showErrorToast()\n{\n\tgId('connind').style.backgroundColor = \"var(--c-r)\";\n\tshowToast('Connection to light failed!', true);\n}\n\nfunction clearErrorToast(n=5000)\n{\n\tvar x = gId('toast');\n\tif (x.classList.contains('error')) {\n\t\tclearTimeout(timeout);\n\t\ttimeout = setTimeout(()=>{\n\t\t\tx.classList.remove('show');\n\t\t\tx.classList.remove('error');\n\t\t}, n);\n\t}\n}\n\nfunction getRuntimeStr(rt)\n{\n\tvar t = parseInt(rt);\n\tvar days = Math.floor(t/86400);\n\tvar hrs = Math.floor((t - days*86400)/3600);\n\tvar mins = Math.floor((t - days*86400 - hrs*3600)/60);\n\tvar str = days ? (days + \" \" + (days == 1 ? \"day\" : \"days\") + \", \") : \"\";\n\tstr += (hrs || days) ? (hrs + \" \" + (hrs == 1 ? \"hour\" : \"hours\")) : \"\";\n\tif (!days && hrs) str += \", \";\n\tif (t > 59 && !days) str += mins + \" min\";\n\tif (t < 3600 && t > 59) str += \", \";\n\tif (t < 3600) str += (t - mins*60) + \" sec\";\n\treturn str;\n}\n\nfunction inforow(key, val, unit = \"\")\n{\n\treturn `<tr><td class=\"keytd\">${key}</td><td class=\"valtd\">${val}${unit}</td></tr>`;\n}\n\nfunction getLowestUnusedP()\n{\n\tvar l = 1;\n\tfor (var key in pJson) if (key == l) l++;\n\tif (l > 250) l = 250;\n\treturn l;\n}\n\nfunction checkUsed(i)\n{\n\tvar id = gId(`p${i}id`).value;\n\tif (pJson[id] && (i == 0 || id != i))\n\t\tgId(`p${i}warn`).innerHTML = `&#9888; Overwriting ${pName(id)}!`;\n\telse\n\t\tgId(`p${i}warn`).innerHTML = id>250?\"&#9888; ID must be 250 or less.\":\"\";\n}\n\nfunction pName(i)\n{\n\tvar n = \"Preset \" + i;\n\tif (pJson && pJson[i] && pJson[i].n) n = pJson[i].n;\n\treturn n;\n}\n\nfunction isPlaylist(i)\n{\n\tif (isNumeric(i)) return pJson[i].playlist && pJson[i].playlist.ps;\n\tif (isObj(i)) return i.playlist && i.playlist.ps;\n\treturn false;\n}\n\nfunction papiVal(i)\n{\n\tif (!pJson || !pJson[i]) return \"\";\n\tvar o = Object.assign({},pJson[i]);\n\tif (o.win) return o.win;\n\tdelete o.n; delete o.p; delete o.ql;\n\treturn JSON.stringify(o);\n}\n\nfunction qlName(i)\n{\n\tif (!pJson || !pJson[i] || !pJson[i].ql) return \"\";\n\treturn pJson[i].ql;\n}\n\nfunction cpBck()\n{\n\tvar copyText = gId(\"bck\");\n\n\tcopyText.select();\n\tcopyText.setSelectionRange(0, 999999);\n\td.execCommand(\"copy\");\n\tshowToast(\"Copied to clipboard!\");\n}\n\nfunction presetError(empty)\n{\n\tvar hasBackup = false; var bckstr = \"\";\n\ttry {\n\t\tbckstr = localStorage.getItem(\"wledP\");\n\t\tif (bckstr.length > 10) hasBackup = true;\n\t} catch (e) {}\n\n\tvar cn = `<div class=\"pres c\" style=\"padding:8px;margin-bottom:8px;${empty?'':'cursor:pointer;'}\" ${empty?'':'onclick=\"loadPresets();\"'}>`;\n\tif (empty)\n\t\tcn += `You have no presets yet!`;\n\telse\n\t\tcn += `Sorry, there was an issue loading your presets!`;\n\n\tif (hasBackup) {\n\t\tcn += `<br><br>`;\n\t\tif (empty)\n\t\t\tcn += `However, there is backup preset data of a previous installation available.<br>(Saving a preset will hide this and overwrite the backup)`;\n\t\telse\n\t\t\tcn += `Here is a backup of the last known good state:`;\n\t\tcn += `<textarea id=\"bck\"></textarea><br><button class=\"btn\" style=\"margin-top:12px;\" onclick=\"cpBck()\">Copy to clipboard</button>`;\n\t\tcn += `<br><button type=\"button\" class=\"btn\" style=\"margin-top:12px;\" onclick=\"restore(gId('bck').value)\">Restore</button>`;\n\t}\n\tcn += `</div>`;\n\tgId('pcont').innerHTML = cn;\n\tif (hasBackup) gId('bck').value = bckstr;\n}\n\nfunction restore(txt) {\n\tvar req = new XMLHttpRequest();\n\treq.addEventListener('load', function(){showToast(this.responseText,this.status >= 400)});\n\treq.addEventListener('error', function(e){showToast(e.stack,true);});\n\treq.open(\"POST\", getURL(\"/upload\"));\n\tvar formData = new FormData();\n\tvar b = new Blob([txt], {type: \"application/json\"});\n\tformData.append(\"data\", b, '/presets.json');\n\treq.send(formData);\n\tsetTimeout(loadPresets, 2000);\n\treturn false;\n}\n\nasync function loadPresets() {\n\treturn new Promise((resolve) => {\n\t\tfetch(getURL('/presets.json'), {method: 'get'})\n\t\t.then(res => res.status==\"404\" ? {\"0\":{}} : res.json())\n\t\t.then(json => {\n\t\t\tpJson = json;\n\t\t\tpopulatePresets();\n\t\t\tresolve();\n\t\t})\n\t\t.catch(() => {\n\t\t\tpresetError(false);\n\t\t\tresolve();\n\t\t})\n\t});\n}\n\nasync function loadPalettes(retry=0) {\n\treturn new Promise((resolve) => {\n\t\tfetch(getURL('/json/palettes'), {method: 'get'})\n\t\t.then(res => res.ok ? res.json() : Promise.reject())\n\t\t.then(json => {\n\t\t\tlJson = Object.entries(json);\n\t\t\tpopulatePalettes();\n\t\t\tresolve();\n\t\t})\n\t\t.catch((e) => {\n\t\t\tif (retry<5) {\n\t\t\t\tsetTimeout(() => loadPalettes(retry+1).then(resolve), 100);\n\t\t\t} else {\n\t\t\t\tshowToast(e, true);\n\t\t\t\tresolve();\n\t\t\t}\n\t\t});\n\t});\n}\n\nasync function loadFX(retry=0) {\n\treturn new Promise((resolve) => {\n\t\tfetch(getURL('/json/effects'), {method: 'get'})\n\t\t.then(res => res.ok ? res.json() : Promise.reject())\n\t\t.then(json => {\n\t\t\teJson = Object.entries(json);\n\t\t\tpopulateEffects();\n\t\t\tresolve();\n\t\t})\n\t\t.catch((e) => {\n\t\t\tif (retry<5) {\n\t\t\t\tsetTimeout(() => loadFX(retry+1).then(resolve), 100);\n\t\t\t} else {\n\t\t\t\tshowToast(e, true);\n\t\t\t\tresolve();\n\t\t\t}\n\t\t});\n\t});\n}\n\nasync function loadFXData(retry=0) {\n\treturn new Promise((resolve) => {\n\t\tfetch(getURL('/json/fxdata'), {method: 'get'})\n\t\t.then(res => res.ok ? res.json() : Promise.reject())\n\t\t.then(json => {\n\t\t\tfxdata = json||[];\n\t\t\tfxdata.shift();\n\t\t\tfxdata.unshift(\";!;\");\n\t\t\tresolve();\n\t\t})\n\t\t.catch((e) => {\n\t\t\tfxdata = [];\n\t\t\tif (retry<5) {\n\t\t\t\tsetTimeout(() => loadFXData(retry+1).then(resolve), 100);\n\t\t\t} else {\n\t\t\t\tshowToast(e, true);\n\t\t\t\tresolve();\n\t\t\t}\n\t\t});\n\t});\n}\n\nvar pQL = [];\nfunction populateQL()\n{\n\tvar cn = \"\";\n\tif (pQL.length > 0) {\n\t\tpQL.sort((a,b) => (a[0]>b[0]));\n\t\tcn += `<p class=\"labels hd\">Quick load</p>`;\n\t\tfor (var key of (pQL||[])) {\n\t\t\tcn += `<button class=\"btn btn-xs psts\" id=\"p${key[0]}qlb\" title=\"${key[2]?key[2]:''}\" onclick=\"setPreset(${key[0]});\">${key[1]}</button>`;\n\t\t}\n\t\tgId('pql').classList.add('expanded');\n\t} else gId('pql').classList.remove('expanded');\n\tgId('pql').innerHTML = cn;\n}\n\nfunction populatePresets(fromls)\n{\n\tif (fromls) pJson = JSON.parse(localStorage.getItem(\"wledP\"));\n\tif (!pJson) {loadPresets(); return;} // note: no await as this is a fallback that should not be needed as init function fetches pJson\n\tdelete pJson[\"0\"];\n\tvar cn = \"\";\n\tvar arr = Object.entries(pJson).sort(cmpP);\n\tpQL = [];\n\tvar is = [];\n\tpNum = 0;\n\tfor (var key of (arr||[]))\n\t{\n\t\tif (!isObj(key[1])) continue;\n\t\tlet i = parseInt(key[0]);\n\t\tvar qll = key[1].ql;\n\t\tif (qll) pQL.push([i, qll, pName(i)]);\n\t\tis.push(i);\n\n\t\tcn += `<div class=\"pres lstI\" id=\"p${i}o\">`;\n\t\tif (cfg.comp.pid) cn += `<div class=\"pid\">${i}</div>`;\n\t\tcn += `<div class=\"pname lstIname\" onclick=\"setPreset(${i})\">${i==lastinfo.leds.bootps?\"<i class='icons btn-icon'>&#xe410;</i>\":\"\"}${isPlaylist(i)?\"<i class='icons btn-icon'>&#xe139;</i>\":\"\"}${pName(i)}\n\t<i class=\"icons edit-icon flr\" id=\"p${i}nedit\" onclick=\"tglSegn(${i+100})\">&#xe2c6;</i></div>\n\t<i class=\"icons e-icon flr\" id=\"sege${i+100}\" onclick=\"expand(${i+100})\">&#xe395;</i>\n\t<div class=\"presin lstIcontent\" id=\"seg${i+100}\"></div>\n</div>`;\n\t\tpNum++;\n\t}\n\n\tgId('pcont').innerHTML = cn;\n\tif (pNum > 0) {\n\t\tif (pmtLS != pmt && pmt != 0) {\n\t\t\tlocalStorage.setItem(\"wledPmt\", pmt);\n\t\t\tpJson[\"0\"] = {};\n\t\t\tlocalStorage.setItem(\"wledP\", JSON.stringify(pJson));\n\t\t}\n\t\tpmtLS = pmt;\n\t} else { presetError(true); }\n\tupdatePA();\n\tpopulateQL();\n}\n\nfunction parseInfo(i) {\n\tlastinfo = i;\n\tvar name = i.name;\n\tgId('namelabel').innerHTML = name;\n\tif (!name.match(/[\\u3040-\\u30ff\\u3400-\\u4dbf\\u4e00-\\u9fff\\uf900-\\ufaff\\uff66-\\uff9f\\u3131-\\uD79D]/))\n\t\tgId('namelabel').style.transform = \"rotate(180deg)\"; // rotate if no CJK characters\n\tif (name === \"Dinnerbone\") d.documentElement.style.transform = \"rotate(180deg)\"; // Minecraft easter egg\n\tif (i.live) name = \"(Live) \" + name;\n\tif (loc)    name = \"(L) \" + name;\n\td.title      = name;\n\tsimplifiedUI = i.simplifiedui;\n\tledCount     = i.leds.count;\n\t//syncTglRecv   = i.str;\n\tmaxSeg       = i.leds.maxseg;\n\tpmt          = i.fs.pmt;\n\tgId('buttonNodes').style.display = lastinfo.ndc > 0 ? null:\"none\";\n\t// do we have a matrix set-up\n\tmw = i.leds.matrix ? i.leds.matrix.w : 0;\n\tmh = i.leds.matrix ? i.leds.matrix.h : 0;\n\tisM = mw>0 && mh>0;\n\tif (!isM) {\n\t\tgId(\"filter2D\").classList.add('hide');\n\t\tgId('bs').querySelectorAll('option[data-type=\"2D\"]').forEach((o,i)=>{o.style.display='none';});\n\t} else {\n\t\tgId(\"filter2D\").classList.remove('hide');\n\t\tgId('bs').querySelectorAll('option[data-type=\"2D\"]').forEach((o,i)=>{o.style.display='';});\n\t}\n\tgId(\"updBt\").style.display = (i.opt & 1) ? '':'none';\n//\tif (i.noaudio) {\n//\t\tgId(\"filterVol\").classList.add(\"hide\");\n//\t\tgId(\"filterFreq\").classList.add(\"hide\");\n//\t}\n//\tif (!i.u || !i.u.AudioReactive) {\n//\t\tgId(\"filterVol\").classList.add(\"hide\"); hideModes(\" ♪\"); // hide volume reactive effects\n//\t\tgId(\"filterFreq\").classList.add(\"hide\"); hideModes(\" ♫\"); // hide frequency reactive effects\n//\t}\n\t// Check for version upgrades on page load\n\tcheckVersionUpgrade(i);\n}\n\n//https://stackoverflow.com/questions/2592092/executing-script-elements-inserted-with-innerhtml\n//var setInnerHTML = function(elm, html) {\n//\telm.innerHTML = html;\n//\tArray.from(elm.querySelectorAll(\"script\")).forEach( oldScript => {\n//\t  const newScript = d.createElement(\"script\");\n//\t  Array.from(oldScript.attributes)\n//\t\t.forEach( attr => newScript.setAttribute(attr.name, attr.value) );\n//\t  newScript.appendChild(d.createTextNode(oldScript.innerHTML));\n//\t  oldScript.parentNode.replaceChild(newScript, oldScript);\n//\t});\n//}\n//setInnerHTML(obj, html);\n\nfunction populateInfo(i)\n{\n\tvar cn=\"\";\n\tvar pwr = i.leds.pwr;\n\tvar pwru = \"Not calculated\";\n\tif (pwr > 1000) {pwr /= 1000; pwr = pwr.toFixed((pwr > 10) ? 0 : 1); pwru = pwr + \" A\";}\n\telse if (pwr > 0) {pwr = 50 * Math.round(pwr/50); pwru = pwr + \" mA\";}\n\tvar urows=\"\";\n\tif (i.u) {\n\t\tfor (const [k, val] of Object.entries(i.u)) {\n\t\t\tif (val[1])\n\t\t\t\turows += inforow(k,val[0],val[1]);\n\t\t\telse\n\t\t\t\turows += inforow(k,val);\n\t\t}\n\t}\n\tvar vcn = \"Kuuhaku\";\n\tif (i.cn) vcn = i.cn;\n\n\tcn += `v${i.ver} <i>\"${vcn}\"</i>${i.release ? '<br>('+i.release+')' : ''}<br><br><table>\n${urows}\n${urows===\"\"?'':'<tr><td colspan=2><hr style=\"height:1px;border-width:0;color:gray;background-color:gray\"></td></tr>'}\n${i.opt&0x100?inforow(\"Debug\",\"<button class=\\\"btn btn-xs\\\" onclick=\\\"requestJson({'debug':\"+(i.opt&0x0080?\"false\":\"true\")+\"});\\\"><i class=\\\"icons \"+(i.opt&0x0080?\"on\":\"off\")+\"\\\">&#xe08f;</i></button>\"):''}\n${inforow(\"Build\",i.vid)}\n${inforow(\"Signal strength\",i.wifi.signal +\"% (\"+ i.wifi.rssi, \" dBm)\")}\n${inforow(\"Uptime\",getRuntimeStr(i.uptime))}\n${inforow(\"Time\",i.time)}\n${inforow(\"Free heap\",(i.freeheap/1024).toFixed(1),\" kB\")}\n${i.psram?inforow(\"Free PSRAM\",(i.psram/1024).toFixed(1),\" kB\"):\"\"}\n<tr><td colspan=2><hr class=\"sml\"></td></tr>\n${i.leds.count?inforow(\"Total LEDs\",i.leds.count):\"\"}\n${inforow(\"Estimated current\",pwru)}\n${inforow(\"Average FPS\",i.leds.fps)}\n<tr><td colspan=2><hr class=\"sml\"></td></tr>\n${inforow(\"MAC address\",i.mac)}\n${inforow(\"CPU clock\",i.clock,\" MHz\")}\n${inforow(\"Flash size\",i.flash,\" MB\")}\n${inforow(\"Filesystem\",i.fs.u + \"/\" + i.fs.t + \" kB (\" +Math.round(i.fs.u*100/i.fs.t) + \"%)\")}\n${inforow(\"Environment\",i.arch + \" \" + i.core + ( i.lwip ? \" (\" + i.lwip + \")\" : \"\"))}\n${i.repo?inforow(\"GitHub\",\"<a href=\\\"https://github.com/\"+i.repo+\"\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">\" + i.repo + \"</a>\"):\"\"}\n</table>`;\n\tgId('kv').innerHTML = cn;\n\t//  update all sliders in Info\n\td.querySelectorAll('#kv .sliderdisplay').forEach((sd,i) => {\n\t\tlet s = sd.previousElementSibling;\n\t\tif (s) updateTrail(s);\n\t});\n}\n\nfunction populateSegments(s)\n{\n\tvar cn = \"\";\n\tlet li = lastinfo;\n\tsegCount = 0; lowestUnused = 0; lSeg = 0;\n\n\tfor (var inst of (s.seg||[])) {\n\t\tsegCount++;\n\n\t\tlet i = parseInt(inst.id);\n\t\tif (i == lowestUnused) lowestUnused = i+1;\n\t\tif (i > lSeg) lSeg = i;\n\n\t\tlet sg = gId(`seg${i}`);\n\t\tlet exp = sg ? (sg.classList.contains('expanded') || (i===0 && cfg.comp.segexp)) : false;\n\n\t\t// segment set icon color\n\t\tlet cG = \"var(--c-b)\";\n\t\tswitch (inst.set) {\n\t\t\tcase 1: cG = \"var(--c-r)\"; break;\n\t\t\tcase 2: cG = \"var(--c-g)\"; break;\n\t\t\tcase 3: cG = \"var(--c-l)\"; break;\n\t\t}\n\n\t\tlet segp = `<div id=\"segp${i}\" class=\"sbs\">`+\n\t\t\t\t\t\t`<i class=\"icons slider-icon pwr ${inst.on ? \"act\":\"\"}\" id=\"seg${i}pwr\" title=\"Power\" onclick=\"setSegPwr(${i})\">&#xe08f;</i>`+\n\t\t\t\t\t\t`<div class=\"sliderwrap il\" title=\"Opacity/Brightness\">`+\n\t\t\t\t\t\t\t`<input id=\"seg${i}bri\" class=\"noslide\" onchange=\"setSegBri(${i})\" oninput=\"updateTrail(this)\" max=\"255\" min=\"1\" type=\"range\" value=\"${inst.bri}\" />`+\n\t\t\t\t\t\t\t`<div class=\"sliderdisplay\"></div>`+\n\t\t\t\t\t\t`</div>`+\n\t\t\t\t\t`</div>`;\n\t\tlet staX = inst.start;\n\t\tlet stoX = inst.stop;\n\t\tlet staY = inst.startY;\n\t\tlet stoY = inst.stopY;\n\t\tlet isMSeg = isM && staX<mw*mh; // 2D matrix segment\n\t\tlet rvXck = `<label class=\"check revchkl\">Reverse ${isM?'':'direction'}<input type=\"checkbox\" id=\"seg${i}rev\" onchange=\"setRev(${i})\" ${inst.rev?\"checked\":\"\"}><span class=\"checkmark\"></span></label>`;\n\t\tlet miXck = `<label class=\"check revchkl\">Mirror<input type=\"checkbox\" id=\"seg${i}mi\" onchange=\"setMi(${i})\" ${inst.mi?\"checked\":\"\"}><span class=\"checkmark\"></span></label>`;\n\t\tlet rvYck = \"\", miYck =\"\";\n\t\tlet smpl = simplifiedUI ? 'hide' : '';\n\t\tif (isMSeg) {\n\t\t\trvYck = `<label class=\"check revchkl\">Reverse<input type=\"checkbox\" id=\"seg${i}rY\" onchange=\"setRevY(${i})\" ${inst.rY?\"checked\":\"\"}><span class=\"checkmark\"></span></label>`;\n\t\t\tmiYck = `<label class=\"check revchkl\">Mirror<input type=\"checkbox\" id=\"seg${i}mY\" onchange=\"setMiY(${i})\" ${inst.mY?\"checked\":\"\"}><span class=\"checkmark\"></span></label>`;\n\t\t}\n\t\tlet map2D = `<div id=\"seg${i}map2D\" data-map=\"map2D\" class=\"lbl-s hide\">Expand 1D FX<br>`+\n\t\t\t\t\t\t`<div class=\"sel-p\"><select class=\"sel-p\" id=\"seg${i}m12\" onchange=\"setM12(${i})\">`+\n\t\t\t\t\t\t\t`<option value=\"0\" ${inst.m12==0?' selected':''}>Pixels</option>`+\n\t\t\t\t\t\t\t`<option value=\"1\" ${inst.m12==1?' selected':''}>Bar</option>`+\n\t\t\t\t\t\t\t`<option value=\"2\" ${inst.m12==2?' selected':''}>Arc</option>`+\n\t\t\t\t\t\t\t`<option value=\"3\" ${inst.m12==3?' selected':''}>Corner</option>`+\n\t\t\t\t\t\t\t`<option value=\"4\" ${inst.m12==4?' selected':''}>Pinwheel</option>`+\n\t\t\t\t\t\t`</select></div>`+\n\t\t\t\t\t`</div>`;\n\t\tlet blend = `<div class=\"lbl-l\">Blend mode<br>`+\n\t\t\t\t\t\t`<div class=\"sel-p\"><select class=\"sel-ple\" id=\"seg${i}bm\" onchange=\"setBm(${i})\">`+\n\t\t\t\t\t\t\t`<option value=\"0\" ${inst.bm==0?' selected':''}>Top/Default</option>`+\n\t\t\t\t\t\t\t`<option value=\"1\" ${inst.bm==1?' selected':''}>Bottom/None</option>`+\n\t\t\t\t\t\t\t`<option value=\"2\" ${inst.bm==2?' selected':''}>Add</option>`+\n\t\t\t\t\t\t\t`<option value=\"3\" ${inst.bm==3?' selected':''}>Subtract</option>`+\n\t\t\t\t\t\t\t`<option value=\"4\" ${inst.bm==4?' selected':''}>Difference</option>`+\n\t\t\t\t\t\t\t`<option value=\"5\" ${inst.bm==5?' selected':''}>Average</option>`+\n\t\t\t\t\t\t\t`<option value=\"6\" ${inst.bm==6?' selected':''}>Multiply</option>`+\n\t\t\t\t\t\t\t`<option value=\"7\" ${inst.bm==7?' selected':''}>Divide</option>`+\n\t\t\t\t\t\t\t`<option value=\"8\" ${inst.bm==8?' selected':''}>Lighten</option>`+\n\t\t\t\t\t\t\t`<option value=\"9\" ${inst.bm==9?' selected':''}>Darken</option>`+\n\t\t\t\t\t\t\t`<option value=\"10\" ${inst.bm==10?' selected':''}>Screen</option>`+\n\t\t\t\t\t\t\t`<option value=\"11\" ${inst.bm==11?' selected':''}>Overlay</option>`+\n\t\t\t\t\t\t\t`<option value=\"12\" ${inst.bm==12?' selected':''}>Hard Light</option>`+\n\t\t\t\t\t\t\t`<option value=\"13\" ${inst.bm==13?' selected':''}>Soft Light</option>`+\n\t\t\t\t\t\t\t`<option value=\"14\" ${inst.bm==14?' selected':''}>Dodge</option>`+\n\t\t\t\t\t\t\t`<option value=\"15\" ${inst.bm==15?' selected':''}>Burn</option>`+\n\t\t\t\t\t\t`</select></div>`+\n\t\t\t\t\t`</div>`;\n\t\tlet sndSim = `<div data-snd=\"si\" class=\"lbl-s hide\">Sound sim<br>`+\n\t\t\t\t\t\t`<div class=\"sel-p\"><select class=\"sel-p\" id=\"seg${i}si\" onchange=\"setSi(${i})\">`+\n\t\t\t\t\t\t\t`<option value=\"0\" ${inst.si==0?' selected':''}>BeatSin</option>`+\n\t\t\t\t\t\t\t`<option value=\"1\" ${inst.si==1?' selected':''}>WeWillRockYou</option>`+\n\t\t\t\t\t\t\t`<option value=\"2\" ${inst.si==2?' selected':''}>10/13</option>`+\n\t\t\t\t\t\t\t`<option value=\"3\" ${inst.si==3?' selected':''}>14/3</option>`+\n\t\t\t\t\t\t`</select></div>`+\n\t\t\t\t\t`</div>`;\n\t\tcn += `<div class=\"seg lstI ${i==s.mainseg && !simplifiedUI ? 'selected' : ''} ${exp ? \"expanded\":\"\"}\" id=\"seg${i}\" data-set=\"${inst.set}\">`+\n\t\t\t\t`<label class=\"check schkl ${smpl}\">`+\n\t\t\t\t\t`<input type=\"checkbox\" id=\"seg${i}sel\" onchange=\"selSeg(${i})\" ${inst.sel ? \"checked\":\"\"}>`+\n\t\t\t\t\t`<span class=\"checkmark\" title=\"Select\"></span>`+\n\t\t\t\t`</label>`+\n\t\t\t\t`<div class=\"segname ${smpl}\" onclick=\"selSegEx(${i})\">`+\n\t\t\t\t\t`<i class=\"icons e-icon frz\" id=\"seg${i}frz\" title=\"(un)Freeze\" onclick=\"event.preventDefault();tglFreeze(${i});\">&#x${inst.frz ? (li.live && li.liveseg==i?'e410':'e0e8') : 'e325'};</i>`+\n\t\t\t\t\t(inst.n ? inst.n : \"Segment \"+i) +\n\t\t\t\t\t`<div class=\"pop hide\" onclick=\"event.preventDefault();event.stopPropagation();\">`+\n\t\t\t\t\t\t`<i class=\"icons g-icon\" title=\"Set group\" style=\"color:${cG};\" onclick=\"this.nextElementSibling.classList.toggle('hide');\">&#x278${String.fromCharCode(inst.set+\"A\".charCodeAt(0))};</i>`+\n\t\t\t\t\t\t`<div class=\"pop-c hide\"><span style=\"color:var(--c-f);\" onclick=\"setGrp(${i},0);\">&#x278A;</span><span style=\"color:var(--c-r);\" onclick=\"setGrp(${i},1);\">&#x278B;</span><span style=\"color:var(--c-g);\" onclick=\"setGrp(${i},2);\">&#x278C;</span><span style=\"color:var(--c-l);\" onclick=\"setGrp(${i},3);\">&#x278D;</span></div>`+\n\t\t\t\t\t`</div> `+\n\t\t\t\t\t`<i class=\"icons edit-icon flr ${smpl}\" id=\"seg${i}nedit\" title=\"Edit\" onclick=\"tglSegn(${i})\">&#xe2c6;</i>`+\n\t\t\t\t`</div>`+\n\t\t\t\t`<i class=\"icons e-icon flr ${smpl}\" id=\"sege${i}\" onclick=\"expand(${i})\">&#xe395;</i>`+\n\t\t\t\t(cfg.comp.segpwr ? segp : '') +\n\t\t\t\t`<div class=\"segin ${smpl}\" id=\"seg${i}in\">`+\n\t\t\t\t\t`<input type=\"text\" class=\"ptxt\" id=\"seg${i}t\" autocomplete=\"off\" maxlength=${li.arch==\"esp8266\"?32:64} value=\"${inst.n?inst.n:\"\"}\" placeholder=\"Enter name...\"/>`+\n\t\t\t\t\t`<table class=\"infot segt\">`+\n\t\t\t\t\t`<tr>`+\n\t\t\t\t\t\t`<td>${isMSeg?'Start X':'Start LED'}</td>`+\n\t\t\t\t\t\t`<td>${isMSeg?(cfg.comp.seglen?\"Width\":\"Stop X\"):(cfg.comp.seglen?\"LED count\":\"Stop LED\")}</td>`+\n\t\t\t\t\t\t`<td>${isMSeg?'':'Offset'}</td>`+\n\t\t\t\t\t`</tr>`+\n\t\t\t\t\t`<tr>`+\n\t\t\t\t\t\t`<td><input class=\"segn\" id=\"seg${i}s\" type=\"number\" min=\"0\" max=\"${(isMSeg?mw:ledCount)-1}\" value=\"${staX}\" oninput=\"updateLen(${i})\" onkeydown=\"segEnter(${i})\"></td>`+\n\t\t\t\t\t\t`<td><input class=\"segn\" id=\"seg${i}e\" type=\"number\" min=\"0\" max=\"${(isMSeg?mw:ledCount)}\" value=\"${stoX-(cfg.comp.seglen?staX:0)}\" oninput=\"updateLen(${i})\" onkeydown=\"segEnter(${i})\"></td>`+\n\t\t\t\t\t\t`<td ${isMSeg?'style=\"text-align:revert;\"':''}>${isMSeg?miXck+'<br>'+rvXck:''}<input class=\"segn ${isMSeg?'hide':''}\" id=\"seg${i}of\" type=\"number\" value=\"${inst.of}\" oninput=\"updateLen(${i})\"></td>`+\n\t\t\t\t\t`</tr>`+\n\t\t\t\t\t(isMSeg ? '<tr><td>Start Y</td><td>'+(cfg.comp.seglen?'Height':'Stop Y')+'</td><td></td></tr>'+\n\t\t\t\t\t'<tr>'+\n\t\t\t\t\t\t'<td><input class=\"segn\" id=\"seg'+i+'sY\" type=\"number\" min=\"0\" max=\"'+(mh-1)+'\" value=\"'+staY+'\" oninput=\"updateLen('+i+')\" onkeydown=\"segEnter('+i+')\"></td>'+\n\t\t\t\t\t\t'<td><input class=\"segn\" id=\"seg'+i+'eY\" type=\"number\" min=\"0\" max=\"'+mh+'\" value=\"'+(stoY-(cfg.comp.seglen?staY:0))+'\" oninput=\"updateLen('+i+')\" onkeydown=\"segEnter('+i+')\"></td>'+\n\t\t\t\t\t\t'<td style=\"text-align:revert;\">'+miYck+'<br>'+rvYck+'</td>'+\n\t\t\t\t\t'</tr>' : '') +\n\t\t\t\t\t`<tr>`+\n\t\t\t\t\t\t`<td>Grouping</td>`+\n\t\t\t\t\t\t`<td>Spacing</td>`+\n\t\t\t\t\t\t`<td></td>`+\n\t\t\t\t\t`</tr>`+\n\t\t\t\t\t`<tr>`+\n\t\t\t\t\t\t`<td><input class=\"segn\" id=\"seg${i}grp\" type=\"number\" min=\"1\" max=\"255\" value=\"${inst.grp}\" oninput=\"updateLen(${i})\" onkeydown=\"segEnter(${i})\"></td>`+\n\t\t\t\t\t\t`<td><input class=\"segn\" id=\"seg${i}spc\" type=\"number\" min=\"0\" max=\"255\" value=\"${inst.spc}\" oninput=\"updateLen(${i})\" onkeydown=\"segEnter(${i})\"></td>`+\n\t\t\t\t\t\t`<td><button class=\"btn btn-xs\" title=\"Update\" onclick=\"setSeg(${i})\"><i class=\"icons btn-icon\" id=\"segc${i}\">&#xe390;</i></button></td>`+\n\t\t\t\t\t`</tr>`+\n\t\t\t\t\t`</table>`+\n\t\t\t\t\t`<div class=\"h bp\" id=\"seg${i}len\"></div>`+\n\t\t\t\t\tblend +\n\t\t\t\t\t(!isMSeg ? rvXck : '') +\n\t\t\t\t\t(isMSeg&&stoY-staY>1&&stoX-staX>1 ? map2D : '') +\n\t\t\t\t\t(s.AudioReactive && s.AudioReactive.on ? \"\" : sndSim) +\n\t\t\t\t\t`<label class=\"check revchkl\" id=\"seg${i}lbtm\">`+\n\t\t\t\t\t\t(isMSeg?'Transpose':'Mirror effect') + (isMSeg ?\n\t\t\t\t\t\t'<input type=\"checkbox\" id=\"seg'+i+'tp\" onchange=\"setTp('+i+')\" '+(inst.tp?\"checked\":\"\")+'>':\n\t\t\t\t\t\t'<input type=\"checkbox\" id=\"seg'+i+'mi\" onchange=\"setMi('+i+')\" '+(inst.mi?\"checked\":\"\")+'>') +\n\t\t\t\t\t\t`<span class=\"checkmark\"></span>`+\n\t\t\t\t\t`</label>`+\n\t\t\t\t\t`<div class=\"del\">`+\n\t\t\t\t\t\t`<button class=\"btn btn-xs\" id=\"segr${i}\" title=\"Repeat until end\" onclick=\"rptSeg(${i})\"><i class=\"icons btn-icon\">&#xe22d;</i></button>`+\n\t\t\t\t\t\t`<button class=\"btn btn-xs\" id=\"segd${i}\" title=\"Delete\" onclick=\"delSeg(${i})\"><i class=\"icons btn-icon\">&#xe037;</i></button>`+\n\t\t\t\t\t`</div>`+\n\t\t\t\t`</div>`+\n\t\t\t\t(cfg.comp.segpwr ? '' : segp) +\n\t\t\t`</div>`;\n\t}\n\n\tgId('segcont').innerHTML = cn;\n\tgId(\"segcont\").classList.remove(\"hide\");\n\tlet noNewSegs = (lowestUnused >= maxSeg);\n\tresetUtil(noNewSegs);\n\tif (segCount === 0) return; // no segments to populate\n\tfor (var i = 0; i <= lSeg; i++) {\n\t\tif (!gId(`seg${i}`)) continue;\n\t\tupdateLen(i);\n\t\tupdateTrail(gId(`seg${i}bri`));\n\t\tgId(`segr${i}`).classList.add(\"hide\");\n\t}\n\tif (segCount < 2) {\n\t\tgId(`segd${lSeg}`).classList.add(\"hide\"); // hide delete if only one segment\n\t\tif (parseInt(gId(\"seg0bri\").value)==255) gId(`segp0`).classList.add(\"hide\");\n\t\t// hide segment controls if there is only one segment in simplified UI\n\t\tif (simplifiedUI) gId(\"segcont\").classList.add(\"hide\");\n\t}\n\tif (!isM && !noNewSegs && (cfg.comp.seglen?parseInt(gId(`seg${lSeg}s`).value):0)+parseInt(gId(`seg${lSeg}e`).value)<ledCount) gId(`segr${lSeg}`).classList.remove(\"hide\");\n\tgId('segutil2').style.display = (segCount > 1) ? \"block\":\"none\"; // rsbtn parent\n\n\tif (Array.isArray(li.maps) && li.maps.length>1) {\n\t\tlet cont = `Ledmap:&nbsp;<select class=\"sel-sg\" onchange=\"requestJson({'ledmap':parseInt(this.value)})\">`;\n\t\tfor (const k of li.maps) cont += `<option ${s.ledmap===k.id?\"selected\":\"\"} value=\"${k.id}\">${k.id==0?'Default':(k.n?k.n:'ledmap'+k.id+'.json')}</option>`;\n\t\tcont += \"</select></div>\";\n\t\tgId(\"ledmap\").innerHTML = cont;\n\t\tgId(\"ledmap\").classList.remove('hide');\n\t} else {\n\t\tgId(\"ledmap\").classList.add('hide');\n\t}\n\ttooltip(\"#Segments\");\n}\n\nfunction populateEffects()\n{\n\tvar effects = eJson;\n\tvar html = \"\";\n\n\teffects.shift(); // temporary remove solid\n\tfor (let i = 0; i < effects.length; i++) {\n\t\teffects[i] = {\n\t\t\tid: effects[i][0],\n\t\t\tname:effects[i][1]\n\t\t};\n\t}\n\teffects.sort((a,b) => (a.name).localeCompare(b.name));\n\teffects.unshift({\n\t\t\"id\": 0,\n\t\t\"name\": \"Solid\"\n\t});\n\n\tfor (let ef of effects) {\n\t\t// add slider and color control to setFX (used by requestjson)\n\t\tlet id = ef.id;\n\t\tlet nm = ef.name+\" \";\n\t\tlet fd = \"\";\n\t\tif (ef.name.indexOf(\"RSVD\") < 0) {\n\t\t\tif (Array.isArray(fxdata) && fxdata.length>id) {\n\t\t\t\tif (fxdata[id].length==0) fd = \";;!;1\"\n\t\t\t\telse fd = fxdata[id];\n\t\t\t\tlet eP = (fd == '')?[]:fd.split(\";\"); // effect parameters\n\t\t\t\tlet p = (eP.length<3 || eP[2]==='')?[]:eP[2].split(\",\"); // palette data\n\t\t\t\tif (p.length>0 && (p[0] !== \"\" && !isNumeric(p[0]))) nm += \"&#x1F3A8;\";\t// effects using palette\n\t\t\t\tlet m = (eP.length<4 || eP[3]==='')?'1':eP[3]; // flags\n\t\t\t\tif (id == 0) m = ''; // solid has no flags\n\t\t\t\tif (m.length>0) {\n\t\t\t\t\tif (m.includes('0')) nm += \"&#8226;\"; // 0D effects (PWM & On/Off)\n\t\t\t\t\tif (m.includes('1')) nm += \"&#8942;\"; // 1D effects\n\t\t\t\t\tif (m.includes('2')) nm += \"&#9638;\"; // 2D effects\n\t\t\t\t\tif (m.includes('v')) nm += \"&#9834;\"; // volume effects\n\t\t\t\t\tif (m.includes('f')) nm += \"&#9835;\"; // frequency effects\n\t\t\t\t}\n\t\t\t}\n\t\t\thtml += generateListItemHtml('fx',id,nm,'setFX','',fd);\n\t\t}\n\t}\n\n\tgId('fxlist').innerHTML=html;\n}\n\nfunction populatePalettes()\n{\n\tlJson.shift(); // temporary remove default\n\tlJson.sort((a,b) => (a[1]).localeCompare(b[1]));\n\tlJson.unshift([0,\"Default\"]);\n\n\tvar html = \"\";\n\tfor (let pa of lJson) {\n\t\thtml += generateListItemHtml(\n\t\t\t'palette',\n\t\t\tpa[0],\n\t\t\tpa[1],\n\t\t\t'setPalette',\n\t\t\t`<div class=\"lstIprev\" style=\"${genPalPrevCss(pa[0])}\"></div>`\n\t\t);\n\t}\n\tgId('pallist').innerHTML=html;\n\t// append custom palettes (when loading for the 1st time)\n\tlet li = lastinfo;\n\tif (!isEmpty(li) && li.cpalcount) {\n\t\tfor (let j = 0; j<li.cpalcount; j++) {\n\t\t\tlet div = d.createElement(\"div\");\n\t\t\tgId('pallist').appendChild(div);\n\t\t\tdiv.outerHTML = generateListItemHtml(\n\t\t\t\t'palette',\n\t\t\t\t255-j,\n\t\t\t\t'~ Custom '+j+' ~',\n\t\t\t\t'setPalette',\n\t\t\t\t`<div class=\"lstIprev\" style=\"${genPalPrevCss(255-j)}\"></div>`\n\t\t\t);\n\t\t}\n\t}\n}\n\nfunction redrawPalPrev()\n{\n\td.querySelectorAll('#pallist .lstI').forEach((pal,i) =>{\n\t\tlet lP = pal.querySelector('.lstIprev');\n\t\tif (lP) {\n\t\t\tlP.style = genPalPrevCss(pal.dataset.id);\n\t\t}\n\t});\n}\n\nfunction genPalPrevCss(id)\n{\n\tif (!palettesData) return;\n\n\tvar paletteData = palettesData[id];\n\n\tif (!paletteData) return 'display: none';\n\n\t// We need at least two colors for a gradient\n\tif (paletteData.length == 1) {\n\t\tpaletteData[1] = paletteData[0];\n\t\tif (Array.isArray(paletteData[1])) {\n\t\t\tpaletteData[1][0] = 255;\n\t\t}\n\t}\n\n\tvar gradient = [];\n\tpaletteData.forEach((e,j) => {\n\t\tlet r, g, b;\n\t\tlet index = false;\n\t\tif (Array.isArray(e)) {\n\t\t\tindex = Math.round(e[0]/255*100);\n\t\t\tr = e[1];\n\t\t\tg = e[2];\n\t\t\tb = e[3];\n\t\t} else if (e == 'r') {\n\t\t\tr = Math.random() * 255;\n\t\t\tg = Math.random() * 255;\n\t\t\tb = Math.random() * 255;\n\t\t} else {\n\t\t\tlet i = e[1] - 1;\n\t\t\tvar cd = gId('csl').children;\n\t\t\tr = parseInt(cd[i].dataset.r);\n\t\t\tg = parseInt(cd[i].dataset.g);\n\t\t\tb = parseInt(cd[i].dataset.b);\n\t\t}\n\t\tif (index === false) {\n\t\t\tindex = Math.round(j / paletteData.length * 100);\n\t\t}\n\t\tgradient.push(`rgb(${r},${g},${b}) ${index}%`);\n\t});\n\n\treturn `background: linear-gradient(to right,${gradient.join()});`;\n}\n\nfunction generateListItemHtml(listName, id, name, clickAction, extraHtml = '', effectPar = '')\n{\n\treturn `<div class=\"lstI${id==0?' sticky':''}\" data-id=\"${id}\" ${effectPar===''?'':'data-opt=\"'+effectPar+'\" '}onClick=\"${clickAction}(${id})\">`+\n\t\t`<label title=\"(${id})\" class=\"radio schkl\" onclick=\"event.preventDefault()\">`+ // (#1984)\n\t\t\t`<input type=\"radio\" value=\"${id}\" name=\"${listName}\">`+\n\t\t\t`<span class=\"radiomark\"></span>`+\n\t\t\t`<div class=\"lstIcontent\">`+\n\t\t\t\t`<span class=\"lstIname\">${name}</span>`+\n\t\t\t`</div>`+\n\t\t`</label>`+\n\t\textraHtml +\n\t`</div>`;\n}\n\nfunction btype(b)\n{\n\tswitch (b) {\n\t\tcase 2:\n\t\tcase 32: return \"ESP32\";\n\t\tcase 3:\n\t\tcase 33: return \"ESP32-S2\";\n\t\tcase 4:\n\t\tcase 34: return \"ESP32-S3\";\n\t\tcase 5:\n\t\tcase 35: return \"ESP32-C3\";\n\t\tcase 39: return \"ESP32-C6\";\n\t\tcase 40: return \"ESP32-C61\";\n\t\tcase 41: return \"ESP32-C5\";\n\t\tcase 42:\n\t\tcase 43: return \"ESP32-P4\";\n\t\tcase 1:\n\t\tcase 82: return \"ESP8266\";\n\t}\n\treturn \"?\";\n}\n\nfunction bname(o)\n{\n\tif (o.name==\"WLED\") return o.ip;\n\treturn o.name;\n}\n\nfunction populateNodes(i,n)\n{\n\tvar cn=\"\";\n\tvar urows=\"\";\n\tvar nnodes = 0;\n\tif (n.nodes) {\n\t\tn.nodes.sort((a,b) => (a.name).localeCompare(b.name));\n\t\tfor (var o of n.nodes) {\n\t\t\tif (o.name) {\n\t\t\t\tlet onoff = `<i class=\"icons e-icon flr ${o.type&0x80?'':'off'}\" onclick=\"rmtTgl('${o.ip}',this);\"\">&#xe08f;</i>`;\n\t\t\t\tvar url = `<a class=\"btn\" title=\"${o.ip}\" href=\"http://${o.ip}\">${bname(o)}${o.vid<2307130?'':onoff}</a>`;\n\t\t\t\turows += inforow(url,`${btype(o.type&0x7F)}<br><i>${o.vid==0?\"N/A\":o.vid}</i>`);\n\t\t\t\tnnodes++;\n\t\t\t}\n\t\t}\n\t}\n\tif (i.ndc < 0) cn += `Instance List is disabled.`;\n\telse if (nnodes == 0) cn += `No other instances found.`;\n\tcn += `<table>\n\t${inforow(\"Current instance:\",i.name)}\n\t${urows}\n\t</table>`;\n\tgId('kn').innerHTML = cn;\n}\n\nfunction loadNodes()\n{\n\tfetch(getURL('/json/nodes'), {\n\t\tmethod: 'get'\n\t})\n\t.then((res)=>{\n\t\tif (!res.ok) showToast('Could not load Node list!', true);\n\t\treturn res.json();\n\t})\n\t.then((json)=>{\n\t\tclearErrorToast(100);\n\t\tpopulateNodes(lastinfo, json);\n\t})\n\t.catch((e)=>{\n\t\tshowToast(e, true);\n\t});\n}\n\n// update the 'sliderdisplay' background div of a slider for a visual indication of slider position\nfunction updateTrail(e)\n{\n\tif (e==null) return;\n\tlet sd = e.parentNode.getElementsByClassName('sliderdisplay')[0];\n\tif (sd && getComputedStyle(sd).getPropertyValue(\"--bg\").trim() !== \"none\") { // trim() for Safari\n\t\tvar max = e.hasAttribute('max') ? e.attributes.max.value : 255;\n\t\tvar perc = Math.round(e.value * 100 / max);\n\t\tif (perc < 50) perc += 2;\n\t\tvar val = `linear-gradient(90deg, var(--bg) ${perc}%, var(--c-6) ${perc}%)`;\n\t\tsd.style.backgroundImage = val;\n\t}\n\tvar b = e.parentNode.parentNode.getElementsByTagName('output')[0];\n\tif (b) b.innerHTML = e.value;\n}\n\n// rangetouch slider function\nfunction toggleBubble(e)\n{\n\tvar b = e.target.parentNode.parentNode.getElementsByTagName('output')[0];\n\tb.classList.toggle('sliderbubbleshow');\n}\n\n// updates segment length upon input of segment values\nfunction updateLen(s)\n{\n\tif (!gId(`seg${s}s`)) return;\n\tvar start = parseInt(gId(`seg${s}s`).value);\n\tvar stop = parseInt(gId(`seg${s}e`).value) + (cfg.comp.seglen?start:0);\n\tvar len = stop - start;\n\tlet sY = gId(`seg${s}sY`);\n\tlet eY = gId(`seg${s}eY`);\n\tlet sX = gId(`seg${s}s`);\n\tlet eX = gId(`seg${s}e`);\n\tlet of = gId(`seg${s}of`);\n\tlet mySH = gId(\"mkSYH\");\n\tlet mySD = gId(\"mkSYD\");\n\tif (isM) {\n\t\t// do we have 1D segment *after* the matrix?\n\t\tif (start >= mw*mh) {\n\t\t\tif (sY) { sY.value = 0; sY.max = 0; sY.min = 0; }\n\t\t\tif (eY) { eY.value = 1; eY.max = 1; eY.min = 0; }\n\t\t\tsX.min = mw*mh; sX.max = ledCount-1;\n\t\t\teX.min = mw*mh+1; eX.max = ledCount;\n\t\t\tif (mySH) mySH.classList.add(\"hide\");\n\t\t\tif (mySD) mySD.classList.add(\"hide\");\n\t\t\tif (of) of.classList.remove(\"hide\");\n\t\t} else {\n\t\t\t// matrix setup\n\t\t\tif (mySH) mySH.classList.remove(\"hide\");\n\t\t\tif (mySD) mySD.classList.remove(\"hide\");\n\t\t\tif (of) of.classList.add(\"hide\");\n\t\t\tlet startY = parseInt(sY.value);\n\t\t\tlet stopY = parseInt(eY.value) + (cfg.comp.seglen?startY:0);\n\t\t\tlen *= (stopY-startY);\n\t\t\tlet tPL = gId(`seg${s}lbtm`);\n\t\t\tif (stop-start>1 && stopY-startY>1) {\n\t\t\t\t// 2D segment\n\t\t\t\tif (tPL) tPL.classList.remove('hide'); // unhide transpose checkbox\n\t\t\t\tlet sE = gId('fxlist').querySelector(`.lstI[data-id=\"${selectedFx}\"]`);\n\t\t\t\tif (sE) {\n\t\t\t\t\tlet sN = sE.querySelector(\".lstIname\").innerText;\n\t\t\t\t\tlet seg = gId(`seg${s}map2D`);\n\t\t\t\t\tif (seg) {\n\t\t\t\t\t\tif(sN.indexOf(\"\\u25A6\")<0) seg.classList.remove('hide'); // unhide mapping for 1D effects (| in name)\n\t\t\t\t\t\telse seg.classList.add('hide');\t// hide mapping otherwise\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// 1D segment in 2D set-up\n\t\t\t\tif (tPL) {\n\t\t\t\t\ttPL.classList.add('hide'); // hide transpose checkbox\n\t\t\t\t\tgId(`seg${s}tp`).checked = false;\t// and uncheck it\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tvar out = \"(delete)\";\n\tif (len > 1) {\n\t\tout = `${len} LEDs`;\n\t} else if (len == 1) {\n\t\tout = \"1 LED\";\n\t}\n\n\tif (gId(`seg${s}grp`) != null)\n\t{\n\t\tvar grp = parseInt(gId(`seg${s}grp`).value);\n\t\tvar spc = parseInt(gId(`seg${s}spc`).value);\n\t\tif (grp == 0) grp = 1;\n\t\tvar virt = Math.ceil(len/(grp + spc));\n\t\tif (!isNaN(virt) && (grp > 1 || spc > 0)) out += ` (${virt} virtual)`;\n\t}\n\tif (isM && start >= mw*mh) out += \" [strip]\";\n\n\tgId(`seg${s}len`).innerHTML = out;\n}\n\n// updates background color of currently selected preset\nfunction updatePA()\n{\n\tlet ps;\n\tps = gEBCN(\"pres\"); for (let p of ps) p.classList.remove('selected');\n\tps = gEBCN(\"psts\"); for (let p of ps) p.classList.remove('selected');\n\tif (currentPreset > 0) {\n\t\tvar acv = gId(`p${currentPreset}o`);\n\t\tif (acv /*&& !acv.classList.contains('expanded')*/) {\n\t\t\tacv.classList.add('selected');\n\t\t\t/*\n\t\t\t// scroll selected preset into view (on WS refresh)\n\t\t\tacv.scrollIntoView({\n\t\t\t\tbehavior: 'smooth',\n\t\t\t\tblock: 'center'\n\t\t\t});\n\t\t\t*/\n\t\t}\n\t\tacv = gId(`p${currentPreset}qlb`);\n\t\tif (acv) acv.classList.add('selected');\n\t}\n}\n\nfunction updateUI()\n{\n\tgId('buttonPower').className = (isOn) ? 'active':'';\n\tgId('buttonNl').className = (nlA) ? 'active':'';\n\tgId('buttonSync').className = (syncSend) ? 'active':'';\n\n\tupdateSelectedFx();\n\tupdateSelectedPalette(selectedPal); // must be after updateSelectedFx() to un-hide color slots for * palettes\n\n\tupdateTrail(gId('sliderBri'));\n\tupdateTrail(gId('sliderSpeed'));\n\tupdateTrail(gId('sliderIntensity'));\n\n\tupdateTrail(gId('sliderC1'));\n\tupdateTrail(gId('sliderC2'));\n\tupdateTrail(gId('sliderC3'));\n\n\tif (hasRGB) {\n\t\tupdateTrail(gId('sliderR'));\n\t\tupdateTrail(gId('sliderG'));\n\t\tupdateTrail(gId('sliderB'));\n\t}\n\tif (hasWhite) updateTrail(gId('sliderW'));\n\n\tvar ccfg = cfg.comp.colors;\n\tgId('wwrap').style.display   = (hasWhite) ? \"block\":\"none\";               // white channel\n\tgId('wbal').style.display    = (hasCCT) ? \"block\":\"none\";                 // white balance\n\tgId('hexw').style.display    = (ccfg.hex) ? \"block\":\"none\";               // HEX input\n\tgId('picker').style.display  = (hasRGB && ccfg.picker) ? \"block\":\"none\";  // color picker wheel\n\tgId('hwrap').style.display   = (hasRGB && !ccfg.picker) ? \"block\":\"none\"; // hue slider\n\tgId('swrap').style.display   = (hasRGB && !ccfg.picker) ? \"block\":\"none\"; // saturation slider\n\tgId('vwrap').style.display   = (hasRGB) ? \"block\":\"none\";                 // brightness (value) slider\n\tgId('kwrap').style.display   = (hasRGB && !hasCCT) ? \"block\":\"none\";      // Kelvin slider\n\tgId('rgbwrap').style.display = (hasRGB && ccfg.rgb) ? \"block\":\"none\";     // RGB sliders\n\tgId('qcs-w').style.display   = (hasRGB && ccfg.quick) ? \"block\":\"none\";   // quick selection\n\t//gId('csl').style.display     = (hasRGB || hasWhite) ? \"block\":\"none\";     // color selectors (hide for On/Off bus)\n\t//gId('palw').style.display    = (hasRGB) ? \"inline-block\":\"none\";          // palettes are shown/hidden in setEffectParameters()\n\n\tupdatePA();\n\tupdatePSliders();\n}\n\nfunction updateSelectedPalette(s)\n{\n\tvar parent = gId('pallist');\n\tvar selPaletteInput = parent.querySelector(`input[name=\"palette\"][value=\"${s}\"]`);\n\tif (selPaletteInput) selPaletteInput.checked = true;\n\n\tvar selElement = parent.querySelector('.selected');\n\tif (selElement) selElement.classList.remove('selected');\n\n\tvar selectedPalette = parent.querySelector(`.lstI[data-id=\"${s}\"]`);\n\tif (!selectedPalette) return; // palette not yet loaded (custom palette on initial load)\n\tselectedPalette.classList.add('selected');\n\n\t// Display selected palette name on button in simplified UI\n\tlet selectedName = selectedPalette.querySelector(\".lstIname\").innerText;\n\tif (simplifiedUI) {\n\t\tgId(\"palwbtn\").innerText = \"Palette: \" + selectedName;\n\t}\n\n\t// in case of special palettes (* Colors...), force show color selectors (if hidden by effect data)\n\tlet cd = gId('csl').children; // color selectors\n\tif (s > 1 && s < 6) {\n\t\tcd[0].classList.remove('hide'); // * Color 1\n\t\tif (s > 2) cd[1].classList.remove('hide'); // * Color 1 & 2\n\t\tif (s > 3) cd[2].classList.remove('hide'); // all colors\n\t} else {\n\t\tfor (let i of cd) if (i.dataset.hide == '1') i.classList.add('hide');\n\t}\n}\n\nfunction updateSelectedFx()\n{\n\tvar parent = gId('fxlist');\n\tvar selEffectInput = parent.querySelector(`input[name=\"fx\"][value=\"${selectedFx}\"]`);\n\tif (selEffectInput) selEffectInput.checked = true;\n\n\tvar selElement = parent.querySelector('.selected');\n\tif (selElement) {\n\t\tselElement.classList.remove('selected');\n\t\tselElement.style.bottom = null; // remove element style added in slider handling\n\t}\n\n\tvar selectedEffect = parent.querySelector(`.lstI[data-id=\"${selectedFx}\"]`);\n\tif (selectedEffect) {\n\t\tselectedEffect.classList.add('selected');\n\t\tsetEffectParameters(selectedFx);\n\t\t// hide non-0D effects if segment only has 1 pixel (0D)\n\t\tparent.querySelectorAll('.lstI').forEach((fx)=>{\n\t\t\tlet ds = fx.dataset;\n\t\t\tif (ds.opt) {\n\t\t\t\tlet opts = ds.opt.split(\";\");\n\t\t\t\tif (ds.id>0) {\n\t\t\t\t\tif (segLmax==0) fx.classList.add('hide'); // none of the segments selected (hide all effects)\n\t\t\t\t\telse {\n\t\t\t\t\t\tif ((segLmax==1 && (!opts[3] || opts[3].indexOf(\"0\")<0)) || (!has2D && opts[3] && ((opts[3].indexOf(\"2\")>=0 && opts[3].indexOf(\"1\")<0)))) fx.classList.add('hide');\n\t\t\t\t\t\telse fx.classList.remove('hide');\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tvar selectedName = selectedEffect.querySelector(\".lstIname\").innerText;\n\n\t\t// Display selected effect name on button in simplified UI\n\t\tlet selectedNameOnlyAscii = selectedName.replace(/[^\\x00-\\x7F]/g, \"\");\n\t\tif (simplifiedUI) {\n\t\t\tgId(\"fxbtn\").innerText = \"Effect: \" + selectedNameOnlyAscii;\n\t\t}\n\n\t\t// hide 2D mapping and/or sound simulation options\n\t\tgId(\"segcont\").querySelectorAll(`div[data-map=\"map2D\"]`).forEach((seg)=>{\n\t\t\tif (selectedName.indexOf(\"\\u25A6\")<0) seg.classList.remove('hide'); else seg.classList.add('hide');\n\t\t});\n\t\tgId(\"segcont\").querySelectorAll(`div[data-snd=\"si\"]`).forEach((seg)=>{\n\t\t\tif (selectedName.indexOf(\"\\u266A\")<0 && selectedName.indexOf(\"\\u266B\")<0) seg.classList.add('hide'); else seg.classList.remove('hide'); // also \"♫ \"?\n\t\t});\n\t}\n}\n\nfunction displayRover(i,s)\n{\n\tgId('rover').style.transform = (i.live && s.lor == 0 && i.liveseg<0) ? \"translateY(0px)\":\"translateY(100%)\";\n\tvar sour = i.lip ? i.lip:\"\"; if (sour.length > 2) sour = \" from \" + sour;\n\tgId('lv').innerHTML = `WLED is receiving live ${i.lm} data${sour}`;\n\tgId('roverstar').style.display = (i.live && s.lor) ? \"block\":\"none\";\n}\n\nfunction cmpP(a, b)\n{\n\tif (cfg.comp.idsort || !a[1].n) return (parseInt(a[0]) > parseInt(b[0]));\n\t// sort playlists first, followed by presets with characters and last presets with special 1st character\n\tconst c = a[1].n.charCodeAt(0);\n\tconst d = b[1].n.charCodeAt(0);\n\tif ((c>47 && c<58) || (c>64 && c<91) || (c>96 && c<123) || c>255) x = '='; else x = '>';\n\tif ((d>47 && d<58) || (d>64 && d<91) || (d>96 && d<123) || d>255) y = '='; else y = '>';\n\tconst n = (a[1].playlist ? '<' : x) + a[1].n;\n\treturn n.localeCompare((b[1].playlist ? '<' : y) + b[1].n, undefined, {numeric: true});\n}\n\nfunction makeWS() {\n\tif (ws || lastinfo.ws < 0) return;\n\tlet url = loc ? getURL('/ws').replace(\"http\",\"ws\") : \"ws://\"+window.location.hostname+\"/ws\";\n\tws = new WebSocket(url);\n\tws.binaryType = \"arraybuffer\";\n\tws.onmessage = (e)=>{\n\t\tif (e.data instanceof ArrayBuffer) return; // liveview packet\n\t\tvar json = JSON.parse(e.data);\n\t\tif (json.leds) return; // JSON liveview packet\n\t\tclearTimeout(jsonTimeout);\n\t\tjsonTimeout = null;\n\t\tlastUpdate = new Date();\n\t\tclearErrorToast();\n\t\tgId('connind').style.backgroundColor = \"var(--c-l)\";\n\t\t// json object should contain json.info AND json.state (but may not)\n\t\tvar i = json.info;\n\t\tif (i) {\n\t\t\tparseInfo(i);\n\t\t\tif (isInfo) populateInfo(i);\n\t\t} else\n\t\t\ti = lastinfo;\n\t\tvar s = json.state ? json.state : json;\n\t\tdisplayRover(i, s);\n\t\treadState(s);\n\t};\n\tws.onclose = (e)=>{\n\t\tgId('connind').style.backgroundColor = \"var(--c-r)\";\n\t\tif (wsRpt++ < 10) setTimeout(makeWS,wsRpt * 200); // retry WS connection\n\t\tws = null;\n\t}\n\tws.onopen = (e)=>{\n\t\t//ws.send(\"{'v':true}\"); // unnecessary (https://github.com/wled/WLED/blob/master/wled00/ws.cpp#L18)\n\t\twsRpt = 0;\n\t\treqsLegal = true;\n\t}\n}\n\nfunction readState(s,command=false)\n{\n\tif (!s) return false;\n\tif (s.success) return true; // no data to process\n\n\tisOn = s.on;\n\tgId('sliderBri').value = s.bri;\n\tnlA = s.nl.on;\n\tnlDur = s.nl.dur;\n\tnlTar = s.nl.tbri;\n\tnlFade = s.nl.fade;\n\tsyncSend = s.udpn.send;\n\tif (s.pl<0)\tcurrentPreset = s.ps;\n\telse currentPreset = s.pl;\n\n\ttr = s.transition;\n\tgId('tt').value = tr/10;\n\tgId('bs').value = s.bs || 0;\n\tif (tr===0) gId('bsp').classList.add('hide')\n\telse gId('bsp').classList.remove('hide')\n\n\tpopulateSegments(s);\n\thasRGB = hasWhite = hasCCT = has2D = false;\n\tsegLmax = 0; // reset max selected segment length\n\tlet i = {};\n\t// determine light capabilities from selected segments\n\tfor (let seg of (s.seg||[])) {\n\t\tlet w  = (seg.stop - seg.start);\n\t\tlet h  = seg.stopY ? (seg.stopY - seg.startY) : 1;\n\t\tlet lc = seg.lc;\n\t\tif (w*h > segLmax) segLmax = w*h;\n\t\tif (seg.sel) {\n\t\t\tif (isEmpty(i) || (i.id == s.mainseg && !i.sel)) i = seg; // get first selected segment (and replace mainseg if it is not selected)\n\t\t\thasRGB   |= !!(lc & 0x01);\n\t\t\thasWhite |= !!(lc & 0x02);\n\t\t\thasCCT   |= !!(lc & 0x04);\n\t\t\thas2D    |= w > 1 && h > 1;\n\t\t} else if (isEmpty(i) && seg.id == s.mainseg) i = seg; // assign mainseg if no segments are selected\n\t}\n\tif (isEmpty(i)) {\n\t\tshowToast('No segments!', true);\n\t\tupdateUI();\n\t\treturn true;\n\t} else if (i.id == s.mainseg) {\n\t\t// fallback if no segments are selected but we have mainseg\n\t\thasRGB   |= !!(i.lc & 0x01);\n\t\thasWhite |= !!(i.lc & 0x02);\n\t\thasCCT   |= !!(i.lc & 0x04);\n\t\thas2D    |= (i.stop - i.start) > 1 && (i.stopY ? (i.stopY - i.startY) : 1) > 1;\n\t}\n\n\tvar cd = gId('csl').querySelectorAll(\"button\");\n\tfor (let e = cd.length-1; e >= 0; e--) {\n\t\tcd[e].dataset.r = i.col[e][0];\n\t\tcd[e].dataset.g = i.col[e][1];\n\t\tcd[e].dataset.b = i.col[e][2];\n\t\tif (hasWhite || (!hasRGB && !hasWhite)) { cd[e].dataset.w = i.col[e][3]; }\n\t\tsetCSL(cd[e]);\n\t}\n\tselectSlot(csel);\n\tif (i.cct != null && i.cct>=0) gId(\"sliderA\").value = i.cct;\n\n\tgId('sliderSpeed').value = i.sx;\n\tgId('sliderIntensity').value = i.ix;\n\tgId('sliderC1').value  = i.c1 ? i.c1 : 0;\n\tgId('sliderC2').value  = i.c2 ? i.c2 : 0;\n\tgId('sliderC3').value  = i.c3 ? i.c3 : 0;\n\tgId('checkO1').checked = !(!i.o1);\n\tgId('checkO2').checked = !(!i.o2);\n\tgId('checkO3').checked = !(!i.o3);\n\n\tif (s.error && s.error != 0) {\n\t\tvar errstr = \"\";\n\t\tswitch (s.error) {\n\t\t\tcase  1:\n\t\t\t\terrstr = \"Denied!\";\n\t\t\t\tbreak;\n\t\t\tcase  3:\n\t\t\t\terrstr = \"Buffer locked!\";\n\t\t\t\tbreak;\n\t\t\tcase  7:\n\t\t\t\terrstr = \"No RAM for buffer!\";\n\t\t\t\tbreak;\n\t\t\tcase  8:\n\t\t\t\terrstr = \"Effect RAM depleted!\";\n\t\t\t\tbreak;\n\t\t\tcase  9:\n\t\t\t\terrstr = \"JSON parsing error!\";\n\t\t\t\tbreak;\n\t\t\tcase 10:\n\t\t\t\terrstr = \"Could not mount filesystem!\";\n\t\t\t\tbreak;\n\t\t\tcase 11:\n\t\t\t\terrstr = \"Not enough space to save preset!\";\n\t\t\t\tbreak;\n\t\t\tcase 12:\n\t\t\t\terrstr = \"Preset not found.\";\n\t\t\t\tbreak;\n\t\t\tcase 13:\n\t\t\t\terrstr = \"Missing ir.json.\";\n\t\t\t\tbreak;\n\t\t\tcase 19:\n\t\t\t\terrstr = \"A filesystem error has occured.\";\n\t\t\t\tbreak;\n\t\t}\n\t\tshowToast('Error ' + s.error + \": \" + errstr, true);\n\t}\n\n\tselectedPal = i.pal;\n\tselectedFx = i.fx;\n\tredrawPalPrev(); // if any color changed (random palette did at least)\n\tupdateUI();\n\treturn true;\n}\n\n// control HTML elements for Slider and Color Control (original ported form WLED-SR)\n// Technical notes\n// ===============\n// If an effect name is followed by an @, slider and color control is effective.\n// If not effective then:\n//      - For AC effects (id<128) 2 sliders and 3 colors and the palette will be shown\n//      - For SR effects (id>128) 5 sliders and 3 colors and the palette will be shown\n// If effective (@)\n//      - a ; separates slider controls (left) from color controls (middle) and palette control (right)\n//      - if left, middle or right is empty no controls are shown\n//      - a , separates slider controls (max 5) or color controls (max 3). Palette has only one value\n//      - a ! means that the default is used.\n//             - For sliders: Effect speeds, Effect intensity, Custom 1, Custom 2, Custom 3\n//             - For colors: Fx color, Background color, Custom\n//             - For palette: prompt for color palette OR palette ID if numeric (will hide palette selection)\n//\n// Note: If palette is on and no colors are specified 1,2 and 3 is shown in each color circle.\n//       If a color is specified, the 1,2 or 3 is replaced by that specification.\n// Note: Effects can override default pattern behaviour\n//       - FadeToBlack can override the background setting\n//       - Defining SEGCOL(<i>) can override a specific palette using these values (e.g. Color Gradient)\nfunction setEffectParameters(idx)\n{\n\tif (!(Array.isArray(fxdata) && fxdata.length>idx)) return;\n\tvar controlDefined = fxdata[idx].length;\n\tvar effectPar = fxdata[idx];\n\tvar effectPars = (effectPar == '')?[]:effectPar.split(\";\");\n\tvar slOnOff = (effectPars.length==0 || effectPars[0]=='')?[]:effectPars[0].split(\",\");\n\tvar coOnOff = (effectPars.length<2  || effectPars[1]=='')?[]:effectPars[1].split(\",\");\n\tvar paOnOff = (effectPars.length<3  || effectPars[2]=='')?[]:effectPars[2].split(\",\");\n\n\t// set html slider items on/off\n\td.querySelectorAll(\"#sliders .sliderwrap\").forEach((slider, i)=>{\n\t\tlet text = slider.getAttribute(\"title\");\n\t\tif ((!controlDefined && i<((idx<128)?2:nSliders)) || (slOnOff.length>i && slOnOff[i]!=\"\")) {\n\t\t\tif (slOnOff.length>i && slOnOff[i]!=\"!\") text = slOnOff[i];\n\t\t\t// restore overwritten default tooltips\n\t\t\tif (i<2 && slOnOff[i]===\"!\") text = i==0 ? \"Effect speed\" : \"Effect intensity\";\n\t\t\tslider.setAttribute(\"title\", text);\n\t\t\tslider.parentElement.classList.remove('hide');\n\t\t} else\n\t\t\tslider.parentElement.classList.add('hide');\n\t});\n\n\tif (slOnOff.length > 5) { // up to 3 checkboxes\n\t\tgId('fxopt').classList.remove('fade');\n\t\td.querySelectorAll(\"#sliders .ochkl\").forEach((check, i)=>{\n\t\t\tlet text = check.getAttribute(\"title\");\n\t\t\tif (5+i<slOnOff.length && slOnOff[5+i]!=='') {\n\t\t\t\tif (slOnOff.length>5+i && slOnOff[5+i]!=\"!\") text = slOnOff[5+i];\n\t\t\t\tcheck.setAttribute(\"title\", text);\n\t\t\t\tcheck.classList.remove('hide');\n\t\t\t} else\n\t\t\t\tcheck.classList.add('hide');\n\t\t});\n\t} else gId('fxopt').classList.add('fade');\n\n\t// set the bottom position of selected effect (sticky) as the top of sliders div\n\tfunction setSelectedEffectPosition() {\n\t\tif (simplifiedUI) return;\n\t\tlet top = parseInt(getComputedStyle(gId(\"sliders\")).height);\n\t\ttop += 5;\n\t\tlet sel = d.querySelector('#fxlist .selected');\n\t\tif (sel) sel.style.bottom = top + \"px\"; // we will need to remove this when unselected (in setFX())\n\t}\n\n\tsetSelectedEffectPosition();\n\tsetInterval(setSelectedEffectPosition,750);\n\t// set html color items on/off\n\tvar cslLabel = '';\n\tvar sep = '';\n\tvar cslCnt = 0, oCsel = csel;\n\td.querySelectorAll(\"#csl button\").forEach((e,i)=>{\n\t\tvar btn = gId(\"csl\" + i);\n\t\t// if no controlDefined or coOnOff has a value\n\t\tif (coOnOff.length>i && coOnOff[i] != \"\") {\n\t\t\tbtn.classList.remove('hide');\n\t\t\tbtn.dataset.hide = 0;\n\t\t\tif (coOnOff[i] != \"!\") {\n\t\t\t\tvar abbreviation = coOnOff[i].substr(0,2);\n\t\t\t\tbtn.innerHTML = abbreviation;\n\t\t\t\tif (abbreviation != coOnOff[i]) {\n\t\t\t\t\tcslLabel += sep + abbreviation + '=' + coOnOff[i];\n\t\t\t\t\tsep = ', ';\n\t\t\t\t}\n\t\t\t}\n\t\t\telse if (i==0) btn.innerHTML = \"Fx\";\n\t\t\telse if (i==1) btn.innerHTML = \"Bg\";\n\t\t\telse btn.innerHTML = \"Cs\";\n\t\t\tif (!cslCnt || oCsel==i) selectSlot(i); // select 1st displayed slot or old one\n\t\t\tcslCnt++;\n\t\t} else if (!controlDefined) { // if no controls then all buttons should be shown for color 1..3\n\t\t\tbtn.classList.remove('hide');\n\t\t\tbtn.dataset.hide = 0;\n\t\t\tbtn.innerHTML = `${i+1}`;\n\t\t\tif (!cslCnt || oCsel==i) selectSlot(i); // select 1st displayed slot or old one\n\t\t\tcslCnt++;\n\t\t} else {\n\t\t\tbtn.classList.add('hide');\n\t\t\tbtn.dataset.hide = 1;\n\t\t\tbtn.innerHTML = `${i+1}`; // name hidden buttons 1..3 for * palettes\n\t\t}\n\t});\n\tgId(\"cslLabel\").innerHTML = cslLabel;\n\tif (cslLabel!==\"\") gId(\"cslLabel\").classList.remove(\"hide\");\n\telse               gId(\"cslLabel\").classList.add(\"hide\");\n\n\t// set palette on/off\n\tvar palw = gId(\"palw\"); // wrapper\n\tvar pall = gId(\"pall\");\t// label\n\tvar icon = '<i class=\"icons sel-icon\" onclick=\"tglHex()\">&#xe2b3;</i> ';\n\tvar text = 'Color palette';\n\t// if not controlDefined or palette has a value\n\tif (hasRGB && ((!controlDefined) || (paOnOff.length>0 && paOnOff[0]!=\"\" && isNaN(paOnOff[0])))) {\n\t\tpalw.style.display = \"inline-block\";\n\t\tif (paOnOff.length>0 && paOnOff[0].indexOf(\"=\")>0) {\n\t\t\t// embeded default values\n\t\t\tvar dPos = paOnOff[0].indexOf(\"=\");\n\t\t\tvar v = Math.max(0,Math.min(255,parseInt(paOnOff[0].substr(dPos+1))));\n\t\t\tpaOnOff[0] = paOnOff[0].substring(0,dPos);\n\t\t}\n\t\tif (paOnOff.length>0 && paOnOff[0] != \"!\") text = paOnOff[0];\n\t} else {\n\t\t// disable palette list\n\t\ttext += ' not used';\n\t\tpalw.style.display = \"none\";\n\t\t// Close palette dialog if not available\n\t\tif (palw.lastElementChild.tagName == \"DIALOG\") {\n\t\t\tpalw.lastElementChild.close();\n\t\t}\n\t}\n\tpall.innerHTML = icon + text;\n\t// not all color selectors shown, hide palettes created from color selectors\n\t// NOTE: this will disallow user to select \"* Color ...\" palettes which may be undesirable in some cases or for some users\n\t//for (let e of (gId('pallist').querySelectorAll('.lstI')||[])) {\n\t//\tlet fltr = \"* C\";\n\t//\tif (cslCnt==1 && csel==0) fltr = \"* Colors\";\n\t//\telse if (cslCnt==2) fltr = \"* Colors Only\";\n\t//\tif (cslCnt < 3 && e.querySelector('.lstIname').innerText.indexOf(fltr)>=0) e.classList.add('hide'); else e.classList.remove('hide');\n\t//}\n}\n\nvar jsonTimeout;\nvar reqsLegal = false;\nasync function requestJson(command=null, retry=0) {\n\treturn new Promise((resolve, reject) => {\n\t\tgId('connind').style.backgroundColor = \"var(--c-y)\";\n\t\tif (command && !reqsLegal) {resolve(); return;}\n\t\tif (!jsonTimeout) jsonTimeout = setTimeout(()=>{if (ws) ws.close(); ws=null; showErrorToast()}, 3000);\n\n\t\tvar useWs = (ws && ws.readyState === WebSocket.OPEN);\n\t\tvar req = null;\n\t\tif (command) {\n\t\t\tcommand.v = true;\n\t\t\tcommand.time = Math.floor(Date.now() / 1000);\n\t\t\tvar t = gId('tt');\n\t\t\tif (t && t.validity.valid && command.transition==null) {\n\t\t\t\tvar tn = parseInt(t.value*10);\n\t\t\t\tif (tn != tr) command.transition = tn;\n\t\t\t}\n\t\t\treq = JSON.stringify(command);\n\t\t\tif (req.length > 1340) useWs = false;\n\t\t\tif (req.length > 500 && lastinfo && lastinfo.arch == \"esp8266\") useWs = false;\n\t\t}\n\n\t\tif (useWs) {\n\t\t\tws.send(req?req:'{\"v\":true}');\n\t\t\tresolve();\n\t\t\treturn;\n\t\t}\n\n\t\tfetch(getURL('/json/si'), {\n\t\t\tmethod: command ? 'post' : 'get',\n\t\t\theaders: {\"Content-Type\": \"application/json; charset=UTF-8\"},\n\t\t\tbody: req\n\t\t})\n\t\t.then(res => {\n\t\t\tclearTimeout(jsonTimeout);\n\t\t\tjsonTimeout = null;\n\t\t\treturn res.ok ? res.json() : Promise.reject();\n\t\t})\n\t\t.then(json => {\n\t\t\tlastUpdate = new Date();\n\t\t\tclearErrorToast(3000);\n\t\t\tgId('connind').style.backgroundColor = \"var(--c-g)\";\n\t\t\tif (!json) { showToast('Empty response', true); resolve(); return; }\n\t\t\tif (json.success) {resolve(); return;}\n\t\t\tif (json.info) {\n\t\t\t\tparseInfo(json.info);\n\t\t\t\tif (isInfo) populateInfo(json.info);\n\t\t\t\tif (simplifiedUI) simplifyUI();\n\t\t\t}\n\t\t\tvar s = json.state ? json.state : json;\n\t\t\treadState(s);\n\n\t\t\treqsLegal = true;\n\t\t\tresolve();\n\t\t})\n\t\t.catch((e)=>{\n\t\t\tif (retry<10) {\n\t\t\t\tsetTimeout(() => requestJson(command,retry+1).then(resolve).catch(reject), retry*50);\n\t\t\t} else {\n\t\t\t\tshowToast(e, true);\n\t\t\t\tresolve();\n\t\t\t}\n\t\t});\n\t});\n}\n\nfunction togglePower()\n{\n\tisOn = !isOn;\n\tvar obj = {\"on\": isOn};\n\tif (isOn && lastinfo && lastinfo.live && lastinfo.liveseg>=0) {\n\t\tobj.live = false;\n\t\tobj.seg = [];\n\t\tobj.seg[0] = {\"id\": lastinfo.liveseg, \"frz\": false};\n\t}\n\tif (cfg.comp.on >0 &&  isOn) obj = {\"ps\": cfg.comp.on }; // don't use setPreset()\n\tif (cfg.comp.off>0 && !isOn) obj = {\"ps\": cfg.comp.off}; // don't use setPreset()\n\trequestJson(obj);\n}\n\nfunction toggleNl()\n{\n\tnlA = !nlA;\n\tif (nlA)\n\t{\n\t\tshowToast(`Timer active. Your light will turn ${nlTar > 0 ? \"on\":\"off\"} ${nlMode ? \"over\":\"after\"} ${nlDur} minutes.`);\n\t} else {\n\t\tshowToast('Timer deactivated.');\n\t}\n\tvar obj = {\"nl\": {\"on\": nlA}};\n\trequestJson(obj);\n}\n\nfunction toggleSync()\n{\n\tsyncSend = !syncSend;\n\tif (syncSend) showToast('Other lights in the network will now sync to this one.');\n\telse showToast('This light and other lights in the network will no longer sync.');\n\tvar obj = {\"udpn\": {\"send\": syncSend}};\n\t//if (syncTglRecv) obj.udpn.recv = syncSend;\n\trequestJson(obj);\n}\n\nfunction toggleLiveview()\n{\n\tif (isInfo && isM) toggleInfo();\n\tif (isNodes && isM) toggleNodes();\n\tisLv = !isLv;\n\tlet wsOn = ws && ws.readyState === WebSocket.OPEN;\n\n\tvar lvID = \"liveview\";\n\tif (isM && wsOn) {\n\t\tlvID += \"2D\";\n\t\tif (isLv) gId('klv2D').innerHTML = `<iframe id=\"${lvID}\" src=\"about:blank\"></iframe>`;\n\t\tgId('mlv2D').style.transform = (isLv) ? \"translateY(0px)\":\"translateY(100%)\";\n\t}\n\n\tgId(lvID).style.display = (isLv) ? \"block\":\"none\";\n\tgId(lvID).src = (isLv) ? getURL(\"/\" + lvID + ((wsOn) ? \"?ws\":\"\")):\"about:blank\";\n\tgId('buttonSr').classList.toggle(\"active\");\n\tif (!isLv && wsOn) ws.send('{\"lv\":false}');\n\tsize();\n}\n\nfunction toggleInfo()\n{\n\tif (isNodes) toggleNodes();\n\tif (isLv && isM) toggleLiveview();\n\tisInfo = !isInfo;\n\tif (isInfo) requestJson();\n\tgId('info').style.transform = (isInfo) ? \"translateY(0px)\":\"translateY(100%)\";\n\tgId('buttonI').className = (isInfo) ? \"active\":\"\";\n}\n\nfunction toggleNodes()\n{\n\tif (isInfo) toggleInfo();\n\tif (isLv && isM) toggleLiveview();\n\tisNodes = !isNodes;\n\tif (isNodes) loadNodes();\n\tgId('nodes').style.transform = (isNodes) ? \"translateY(0px)\":\"translateY(100%)\";\n\tgId('buttonNodes').className = (isNodes) ? \"active\":\"\";\n}\n\nfunction makeSeg()\n{\n\tvar ns = 0, ct = isM ? mw : ledCount;\n\tvar lu = lowestUnused;\n\tlet li = lastinfo;\n\tif (lu > 0) {\n\t\tlet xend = parseInt(gId(`seg${lu -1}e`).value,10) + (cfg.comp.seglen?parseInt(gId(`seg${lu -1}s`).value,10):0);\n\t\tif (isM) {\n\t\t\tns = 0;\n\t\t} else {\n\t\t\tif (xend < ledCount) ns = xend;\n\t\t\tct -= cfg.comp.seglen?ns:0;\n\t\t}\n\t}\n\tgId('segutil').scrollIntoView({\n\t\tbehavior: 'smooth',\n\t\tblock: 'start',\n\t});\n\tvar cn = `<div class=\"seg lstI expanded\">`+\n\t\t`<div class=\"segin\">`+\n\t\t\t`<input class=\"ptxt show\" type=\"text\" id=\"seg${lu}t\" autocomplete=\"off\" maxlength=32 value=\"\" placeholder=\"New segment ${lu}\"/>`+\n\t\t\t`<table class=\"segt\">`+\n\t\t\t\t`<tr>`+\n\t\t\t\t\t`<td width=\"38%\">${isM?'Start X':'Start LED'}</td>`+\n\t\t\t\t\t`<td width=\"38%\">${isM?(cfg.comp.seglen?\"Width\":\"Stop X\"):(cfg.comp.seglen?\"LED count\":\"Stop LED\")}</td>`+\n\t\t\t\t`</tr>`+\n\t\t\t\t`<tr>`+\n\t\t\t\t\t`<td><input class=\"segn\" id=\"seg${lu}s\" type=\"number\" min=\"0\" max=\"${isM?mw-1:ledCount-1}\" value=\"${ns}\" oninput=\"updateLen(${lu})\" onkeydown=\"segEnter(${lu})\"></td>`+\n\t\t\t\t\t`<td><input class=\"segn\" id=\"seg${lu}e\" type=\"number\" min=\"0\" max=\"${ct}\" value=\"${ct}\" oninput=\"updateLen(${lu})\" onkeydown=\"segEnter(${lu})\"></td>`+\n\t\t\t\t\t`<td><button class=\"btn btn-xs\" onclick=\"setSeg(${lu});\"><i class=\"icons bth-icon\" id=\"segc${lu}\">&#xe390;</i></button></td>`+\n\t\t\t\t`</tr>`+\n\t\t\t\t`<tr id=\"mkSYH\" class=\"${isM?\"\":\"hide\"}\"><td>Start Y</td><td>${cfg.comp.seglen?'Height':'Stop Y'}</td></tr>`+\n\t\t\t\t`<tr id=\"mkSYD\" class=\"${isM?\"\":\"hide\"}\">`+\n\t\t\t\t\t`<td><input class=\"segn\" id=\"seg${lu}sY\" type=\"number\" min=\"0\" max=\"${mh-1}\" value=\"0\" oninput=\"updateLen(${lu})\" onkeydown=\"segEnter(${lu})\"></td>`+\n\t\t\t\t\t`<td><input class=\"segn\" id=\"seg${lu}eY\" type=\"number\" min=\"0\" max=\"${mh}\" value=\"${isM?mh:1}\" oninput=\"updateLen(${lu})\" onkeydown=\"segEnter(${lu})\"></td>`+\n\t\t\t\t`</tr>`+\n\t\t\t`</table>`+\n\t\t\t`<div class=\"h\" id=\"seg${lu}len\">${ledCount - ns} LEDs</div>`+\n\t\t\t`<div class=\"c\"><button class=\"btn btn-p\" onclick=\"resetUtil()\">Cancel</button></div>`+\n\t\t`</div>`+\n\t`</div>`;\n\tgId('segutil').innerHTML = cn;\n}\n\nfunction resetUtil(off=false)\n{\n\tgId('segutil').innerHTML = `<div class=\"seg btn btn-s${off?' off':''}\" style=\"padding:0;margin-bottom:12px;\">`\n\t+ '<label class=\"check schkl\"><input type=\"checkbox\" id=\"selall\" onchange=\"selSegAll(this)\"><span class=\"checkmark\" title=\"Select all\"></span></label>'\n\t+ `<div class=\"segname\" ${off?'':'onclick=\"makeSeg()\"'}><i class=\"icons btn-icon\">&#xe18a;</i>Add segment</div>`\n\t+ '<div class=\"pop hide\" onclick=\"event.stopPropagation();\">'\n\t+ `<i class=\"icons g-icon\" title=\"Select group\" onclick=\"this.nextElementSibling.classList.toggle('hide');\">&#xE34B;</i>`\n\t+ '<div class=\"pop-c hide\"><span style=\"color:var(--c-f);\" onclick=\"selGrp(0);\">&#x278A;</span><span style=\"color:var(--c-r);\" onclick=\"selGrp(1);\">&#x278B;</span><span style=\"color:var(--c-g);\" onclick=\"selGrp(2);\">&#x278C;</span><span style=\"color:var(--c-l);\" onclick=\"selGrp(3);\">&#x278D;</span></div>'\n\t+ '</div></div>';\n\tgId('selall').checked = true;\n\tfor (var i = 0; i <= lSeg; i++) {\n\t\tif (!gId(`seg${i}`)) continue;\n\t\tif (!gId(`seg${i}sel`).checked) gId('selall').checked = false; // uncheck if at least one is unselected.\n\t}\n\tif (lSeg>2) d.querySelectorAll(\"#Segments .pop\").forEach((e)=>{e.classList.remove(\"hide\");});\n}\n\nfunction makePlSel(p, el)\n{\n\tvar plSelContent = \"\";\n\tdelete pJson[\"0\"];\t// remove filler preset\n\tObject.entries(pJson).sort(cmpP).forEach((a)=>{\n\t\tvar n = a[1].n ? a[1].n : \"Preset \" + a[0];\n\t\tif (isPlaylist(a[1])) n += ' &#9654;'; // mark playlist\n\t\tif (cfg.comp.idsort) n = a[0] + ' ' + n;\n\t\t// skip endless playlists and itself\n\t\tif (!isPlaylist(a[1]) || (a[1].playlist.repeat > 0 && a[0]!=p)) plSelContent += `<option value=\"${a[0]}\" ${a[0]==el?\"selected\":\"\"}>${n}</option>`;\n\t});\n\treturn plSelContent;\n}\n\nfunction refreshPlE(p)\n{\n\tvar plEDiv = gId(`ple${p}`);\n\tif (!plEDiv) return;\n\tvar content = \"<div class=\\\"first c\\\">Playlist entries</div>\";\n\tplJson[p].ps.forEach((e,i)=>{content += makePlEntry(p,i);});\n\n\tcontent += `<div class=\"hrz\"></div>`;\n\tplEDiv.innerHTML = content;\n\tvar dels = plEDiv.getElementsByClassName(\"btn-pl-del\");\n\tif (dels.length < 2) dels[0].style.display = \"none\";\n\n\td.querySelectorAll(`#seg${p+100} .sel`).forEach((i)=>{\n\t\tif (i.dataset.val) {\n\t\t\tif (parseInt(i.dataset.val) > 0) i.value = i.dataset.val;\n\t\t\telse plJson[p].ps[i.dataset.index] = parseInt(i.value);\n\t\t}\n\t});\n}\n\n// p: preset ID, i: playlist item index\nfunction addPl(p,i)\n{\n\tconst pl = plJson[p];\n\tpl.ps.splice(i+1,0,1);\n\tpl.dur.splice(i+1,0,pl.dur[i]);\n\tpl.transition.splice(i+1,0,pl.transition[i]);\n\trefreshPlE(p);\n}\n\nfunction delPl(p,i)\n{\n\tconst pl = plJson[p];\n\tif (pl.ps.length < 2) return;\n\tpl.ps.splice(i,1);\n\tpl.dur.splice(i,1);\n\tpl.transition.splice(i,1);\n\trefreshPlE(p);\n}\n\nfunction plePs(p,i,field)\n{\n\tplJson[p].ps[i] = parseInt(field.value);\n}\n\nfunction pleDur(p,i,field)\n{\n\tif (field.validity.valid)\n\t\tplJson[p].dur[i] = Math.floor(field.value*10);\n}\n\nfunction pleTr(p,i,field)\n{\n\tconst du = gId(`pl${p}du${i}`);\n\tconst dv = parseFloat(du.value);\n\tif (dv > 0) {\n\t\tfield.max = dv;\n\t\tif (parseFloat(field.value) > dv)\n\t\t\tfield.value = du.value;\n\t}\n\tif (field.validity.valid)\n\t\tplJson[p].transition[i] = Math.floor(field.value*10);\n}\n\nfunction plR(p)\n{\n\tvar pl = plJson[p];\n\tpl.r = gId(`pl${p}rtgl`).checked;\n\tif (gId(`pl${p}rptgl`).checked) { // infinite\n\t\tpl.repeat = 0;\n\t\tdelete pl.end;\n\t\tgId(`pl${p}o1`).style.display = \"none\";\n\t} else {\n\t\tpl.repeat = parseInt(gId(`pl${p}rp`).value);\n\t\tpl.end = parseInt(gId(`pl${p}selEnd`).value);\n\t\tgId(`pl${p}o1`).style.display = \"block\";\n\t}\n}\n\nfunction plM(p)\n{\n\tconst man = gId(`pl${p}manual`).checked;\n\tplJson[p].dur.forEach((e,i)=>{\n\t\tconst d = gId(`pl${p}du${i}`);\n\t\tplJson[p].dur[i] = e = man ? 0 : 100;\n\t\td.value = e/10; // 10s default \n\t\td.readOnly = man;\n\t});\n}\n\nfunction makeP(i,pl)\n{\n\tvar content = \"\";\n\tconst bps = lastinfo.leds.bootps;\n\tif (pl) {\n\t\tif (i===0) plJson[0] = {\n\t\t\tps: [1],\n\t\t\tdur: [100],\n\t\t\ttransition: [tr],\n\t\t\trepeat: 0,\n\t\t\tr: false,\n\t\t\tend: 0\n\t\t};\n\t\tconst rep = plJson[i].repeat ? plJson[i].repeat : 0;\n\t\tconst man = plJson[i].dur == 0;\n\t\tcontent =\n`<div id=\"ple${i}\" style=\"margin-top:10px;\"></div><label class=\"check revchkl\">Shuffle\n\t<input type=\"checkbox\" id=\"pl${i}rtgl\" onchange=\"plR(${i})\" ${plJson[i].r||rep<0?\"checked\":\"\"}>\n\t<span class=\"checkmark\"></span>\n</label>\n<label class=\"check revchkl\">Manual advance\n\t<input type=\"checkbox\" id=\"pl${i}manual\" onchange=\"plM(${i})\" ${man?\"checked\":\"\"}>\n\t<span class=\"checkmark\"></span>\n</label>\n<label class=\"check revchkl\">Repeat indefinitely\n\t<input type=\"checkbox\" id=\"pl${i}rptgl\" onchange=\"plR(${i})\" ${rep>0?\"\":\"checked\"}>\n\t<span class=\"checkmark\"></span>\n</label>\n<div id=\"pl${i}o1\" style=\"display:${rep>0?\"block\":\"none\"}\">\n<div class=\"c\">Repeat <input type=\"number\" id=\"pl${i}rp\" oninput=\"plR(${i})\" max=127 min=0 value=${rep>0?rep:1}> times</div>\n<div class=\"sel\">End preset:<br>\n<div class=\"sel-p\"><select class=\"sel-ple\" id=\"pl${i}selEnd\" onchange=\"plR(${i})\" data-val=${plJson[i].end?plJson[i].end:0}>\n<option value=\"0\">None</option>\n<option value=\"255\" ${plJson[i].end && plJson[i].end==255?\"selected\":\"\"}>Restore preset</option>\n${makePlSel(i, plJson[i].end?plJson[i].end:0)}\n</select></div></div>\n</div>\n<div class=\"c\"><button class=\"btn btn-p\" onclick=\"testPl(${i}, this)\"><i class='icons btn-icon'>&#xe139;</i>Test</button></div>`;\n\t} else {\n\t\tcontent =\n`<label class=\"check revchkl\">\n\t<span class=\"lstIname\">Include brightness</span>\n\t<input type=\"checkbox\" id=\"p${i}ibtgl\" checked>\n\t<span class=\"checkmark\"></span>\n</label>\n<label class=\"check revchkl\">\n\t<span class=\"lstIname\">Save segment bounds</span>\n\t<input type=\"checkbox\" id=\"p${i}sbtgl\" checked>\n\t<span class=\"checkmark\"></span>\n</label>\n<label class=\"check revchkl\">\n\t<span class=\"lstIname\">Checked segments only</span>\n\t<input type=\"checkbox\" id=\"p${i}sbchk\">\n\t<span class=\"checkmark\"></span>\n</label>`;\n\t\tif (Array.isArray(lastinfo.maps) && lastinfo.maps.length>1) {\n\t\t\tcontent += `<div class=\"lbl-l\">Ledmap:&nbsp;<div class=\"sel-p\"><select class=\"sel-p\" id=\"p${i}lmp\"><option value=\"\">Unchanged</option>`;\n\t\t\tfor (const k of lastinfo.maps) content += `<option value=\"${k.id}\"${(i>0 && pJson[i].ledmap==k.id)?\" selected\":\"\"}>${k.id==0?'Default':(k.n?k.n:'ledmap'+k.id+'.json')}</option>`;\n\t\t\tcontent += \"</select></div></div>\";\n\t\t}\n\t}\n\n\treturn `<input type=\"text\" class=\"ptxt ${i==0?'show':''}\" id=\"p${i}txt\" autocomplete=\"off\" maxlength=32 value=\"${(i>0)?pName(i):\"\"}\" placeholder=\"Enter name...\"/>\n<div class=\"c\">Quick load label: <input type=\"text\" class=\"stxt\" maxlength=2 value=\"${qlName(i)}\" id=\"p${i}ql\" autocomplete=\"off\"/></div>\n<div class=\"h\">(leave empty for no Quick load button)</div>\n<div ${pl&&i==0?\"style='display:none'\":\"\"}>\n<label class=\"check revchkl\">\n\t<span class=\"lstIname\">${pl?\"Show playlist editor\":(i>0)?\"Overwrite with state\":\"Use current state\"}</span>\n\t<input type=\"checkbox\" id=\"p${i}cstgl\" onchange=\"tglCs(${i})\" ${(i==0||pl)?\"checked\":\"\"}>\n\t<span class=\"checkmark\"></span>\n</label>\n</div>\n<div class=\"po2\" id=\"p${i}o2\">API command<br><textarea class=\"apitxt\" id=\"p${i}api\"></textarea></div>\n<div class=\"po1\" id=\"p${i}o1\">${content}</div>\n<label class=\"check revchkl\">\n\t<span class=\"lstIname\">Apply at boot</span>\n\t<input type=\"checkbox\" id=\"p${i}bps\" ${i==bps?\"checked\":\"\"}>\n\t<span class=\"checkmark\"></span>\n</label>\n<div class=\"c m6\">Save to ID <input id=\"p${i}id\" type=\"number\" oninput=\"checkUsed(${i})\" max=250 min=1 value=${(i>0)?i:getLowestUnusedP()}></div>\n<div class=\"c\">\n\t<button class=\"btn btn-p\" onclick=\"saveP(${i},${pl})\"><i class=\"icons btn-icon\">&#xe390;</i>Save</button>\n\t${(i>0)?'<button class=\"btn btn-p\" id=\"p'+i+'del\" onclick=\"delP('+i+')\"><i class=\"icons btn-icon\">&#xe037;</i>Delete':'<button class=\"btn btn-p\" onclick=\"resetPUtil()\">Cancel'}</button>\n</div>\n<div class=\"pwarn ${(i>0)?\"bp\":\"\"} c\" id=\"p${i}warn\"></div>\n${(i>0)? ('<div class=\"h\">ID ' +i+ '</div>'):\"\"}`;\n}\n\nfunction makePUtil()\n{\n\tlet p = gId('putil');\n\tp.classList.remove('staybot');\n\tp.classList.add('pres');\n\tp.innerHTML = `<div class=\"presin expanded\">${makeP(0)}</div>`;\n\tlet pTx = gId('p0txt');\n\tpTx.focus();\n\tpTx.value = eJson.find((o)=>{return o.id==selectedFx}).name;\n\tpTx.select();\n\tp.scrollIntoView({\n\t\tbehavior: 'smooth',\n\t\tblock: 'center'\n\t});\n\tgId('psFind').classList.remove('staytop');\n}\n\nfunction makePlEntry(p,i)\n{\n\tconst man = gId(`pl${p}manual`).checked;\n\treturn `<div class=\"plentry\">\n\t<div class=\"hrz\"></div>\n\t<table>\n\t<tr>\n\t\t<td width=\"80%\" colspan=2>\n\t\t\t<div class=\"sel-p\"><select class=\"sel-pl\" onchange=\"plePs(${p},${i},this)\" data-val=\"${plJson[p].ps[i]}\" data-index=\"${i}\">\n\t\t\t${makePlSel(p, plJson[p].ps[i])}\n\t\t\t</select></div>\n\t\t</td>\n\t\t<td class=\"c\"><button class=\"btn btn-pl-add\" onclick=\"addPl(${p},${i})\"><i class=\"icons btn-icon\">&#xe18a;</i></button></td>\n\t</tr>\n\t<tr>\n\t\t<td class=\"c\">Duration <i style=\"font-size:70%;\">(0=inf.)</i></td>\n\t\t<td class=\"c\">Transition</td>\n\t\t<td class=\"c\">#${i+1}</td>\n\t</tr>\n\t<tr>\n\t\t<td class=\"c\" width=\"40%\"><input class=\"segn\" type=\"number\" placeholder=\"Duration\" max=6553.0 min=0.0 step=0.1 oninput=\"pleDur(${p},${i},this)\" value=\"${plJson[p].dur[i]/10.0}\" id=\"pl${p}du${i}\" ${man?\"readonly\":\"\"}>s</td>\n\t\t<td class=\"c\" width=\"40%\"><input class=\"segn\" type=\"number\" placeholder=\"Transition\" max=65.0 min=0.0 step=0.1 oninput=\"pleTr(${p},${i},this)\" onfocus=\"pleTr(${p},${i},this)\" value=\"${plJson[p].transition[i]/10.0}\">s</td>\n\t\t<td class=\"c\"><button class=\"btn btn-pl-del\" onclick=\"delPl(${p},${i})\"><i class=\"icons btn-icon\">&#xe037;</i></button></div></td>\n\t</tr>\n\t</table>\n</div>`;\n}\n\nfunction makePlUtil()\n{\n\tif (pNum < 2) {\n\t\tshowToast(\"You need at least 2 presets to make a playlist!\"); //return;\n\t}\n\tlet p = gId('putil');\n\tp.classList.remove('staybot');\n\tp.classList.add('pres');\n\tp.innerHTML = `<div class=\"presin expanded\" id=\"seg100\">${makeP(0,true)}</div></div>`;\n\trefreshPlE(0);\n\tgId('p0txt').focus();\n\tp.scrollIntoView({\n\t\tbehavior: 'smooth',\n\t\tblock: 'center'\n\t});\n\tgId('psFind').classList.remove('staytop');\n}\n\nfunction resetPUtil()\n{\n\tgId('psFind').classList.add('staytop');\n\tlet p = gId('putil');\n\tp.classList.add('staybot');\n\tp.classList.remove('pres');\n\tp.innerHTML = `<button class=\"btn btn-s\" onclick=\"makePUtil()\" style=\"float:left;\"><i class=\"icons btn-icon\">&#xe18a;</i>Preset</button>`\n\t+ `<button class=\"btn btn-s\" onclick=\"makePlUtil()\" style=\"float:right;\"><i class=\"icons btn-icon\">&#xe18a;</i>Playlist</button>`;\n}\n\nfunction tglCs(i)\n{\n\tvar pss = gId(`p${i}cstgl`).checked;\n\tgId(`p${i}o1`).style.display = pss? \"block\" : \"none\";\n\tgId(`p${i}o2`).style.display = !pss? \"block\" : \"none\";\n}\n\nfunction tglSegn(s)\n{\n\tlet t = gId(s<100?`seg${s}t`:`p${s-100}txt`);\n\tif (t) {\n\t\tt.classList.toggle('show');\n\t\tt.focus();\n\t\tt.select();\n\t}\n\tevent.preventDefault();\n\tevent.stopPropagation();\n}\n\nfunction selSegAll(o)\n{\n\tvar obj = {\"seg\":[]};\n\tfor (let i=0; i<=lSeg; i++) if (gId(`seg${i}`)) obj.seg.push({\"id\":i,\"sel\":o.checked});\n\trequestJson(obj);\n}\n\nfunction selSegEx(s)\n{\n\tvar obj = {\"seg\":[]};\n\tfor (let i=0; i<=lSeg; i++) if (gId(`seg${i}`)) obj.seg.push({\"id\":i,\"sel\":(i==s)});\n\tobj.mainseg = s;\n\trequestJson(obj);\n}\n\nfunction selSeg(s)\n{\n\tvar sel = gId(`seg${s}sel`).checked;\n\tvar obj = {\"seg\": {\"id\": s, \"sel\": sel}};\n\trequestJson(obj);\n}\n\nfunction selGrp(g)\n{\n\tevent.preventDefault();\n\tevent.stopPropagation();\n\tvar obj = {\"seg\":[]};\n\tfor (let i=0; i<=lSeg; i++) if (gId(`seg${i}`)) obj.seg.push({\"id\":i,\"sel\":false});\n\tgId(`segcont`).querySelectorAll(`div[data-set=\"${g}\"]`).forEach((s)=>{\n\t\tlet i = parseInt(s.id.substring(3));\n\t\tobj.seg[i] = {\"id\":i,\"sel\":true};\n\t});\n\tif (obj.seg.length) requestJson(obj);\n}\n\nfunction rptSeg(s)\n{\n\t//TODO: 2D support\n\tvar name = gId(`seg${s}t`).value;\n\tvar start = parseInt(gId(`seg${s}s`).value);\n\tvar stop = parseInt(gId(`seg${s}e`).value);\n\tif (stop == 0) {return;}\n\tvar rev = gId(`seg${s}rev`).checked;\n\tvar mi = gId(`seg${s}mi`).checked;\n\tvar sel = gId(`seg${s}sel`).checked;\n\tvar pwr = gId(`seg${s}pwr`).classList.contains('act');\n\tvar obj = {\"seg\": {\"id\": s, \"n\": name, \"start\": start, \"stop\": (cfg.comp.seglen?start:0)+stop, \"rev\": rev, \"mi\": mi, \"on\": pwr, \"bri\": parseInt(gId(`seg${s}bri`).value), \"sel\": sel}};\n\tif (gId(`seg${s}grp`)) {\n\t\tvar grp = parseInt(gId(`seg${s}grp`).value);\n\t\tvar spc = parseInt(gId(`seg${s}spc`).value);\n\t\tvar ofs = parseInt(gId(`seg${s}of` ).value);\n\t\tobj.seg.grp = grp;\n\t\tobj.seg.spc = spc;\n\t\tobj.seg.of  = ofs;\n\t}\n\tobj.seg.rpt = true;\n\texpand(s);\n\trequestJson(obj);\n}\n\nfunction setSeg(s)\n{\n\tvar name = gId(`seg${s}t`).value;\n\tlet sX = gId(`seg${s}s`);\n\tlet eX = gId(`seg${s}e`);\n\tvar start = parseInt(sX.value);\n\tvar stop = parseInt(eX.value) + (cfg.comp.seglen?start:0);\n\tif (start<sX.min || start>sX.max) {sX.value=sX.min; return;} // prevent out of bounds\n\tif (stop<eX.min || stop-(cfg.comp.seglen?start:0)>eX.max) {eX.value=eX.max; return;} // prevent out of bounds\n\tif ((cfg.comp.seglen && stop == 0) || (!cfg.comp.seglen && stop <= start)) {delSeg(s); return;}\n\tvar obj = {\"seg\": {\"id\": s, \"n\": name, \"start\": start, \"stop\": stop}};\n\tif (isM && start<mw*mh) {\n\t\tlet sY = gId(`seg${s}sY`);\n\t\tlet eY = gId(`seg${s}eY`);\n\t\tvar startY = parseInt(sY.value);\n\t\tvar stopY = parseInt(eY.value) + (cfg.comp.seglen?startY:0);\n\t\tif (startY<sY.min || startY>sY.max) {sY.value=sY.min; return;} // prevent out of bounds\n\t\tif (stopY<eY.min || stopY>eY.max) {eY.value=eY.max; return;} // prevent out of bounds\n\t\tobj.seg.startY = startY;\n\t\tobj.seg.stopY = stopY;\n\t}\n\tlet g = gId(`seg${s}grp`);\n\tif (g) { // advanced options, not present in new segment dialog (makeSeg())\n\t\tlet grp = parseInt(g.value);\n\t\tlet spc = parseInt(gId(`seg${s}spc`).value);\n\t\tlet ofs = parseInt(gId(`seg${s}of` ).value);\n\t\tobj.seg.grp = grp;\n\t\tobj.seg.spc = spc;\n\t\tobj.seg.of  = ofs;\n\t\tif (isM && gId(`seg${s}tp`)) obj.seg.tp = gId(`seg${s}tp`).checked;\n\t}\n\tresetUtil(); // close add segment dialog just in case\n\trequestJson(obj);\n}\n\nfunction delSeg(s)\n{\n\tif (segCount < 2) {\n\t\tshowToast(\"You need to have multiple segments to delete one!\");\n\t\treturn;\n\t}\n\tsegCount--;\n\tvar obj = {\"seg\": {\"id\": s, \"stop\": 0}};\n\trequestJson(obj);\n}\n\nfunction setRev(s)\n{\n\tvar rev = gId(`seg${s}rev`).checked;\n\tvar obj = {\"seg\": {\"id\": s, \"rev\": rev}};\n\trequestJson(obj);\n}\n\nfunction setRevY(s)\n{\n\tvar rev = gId(`seg${s}rY`).checked;\n\tvar obj = {\"seg\": {\"id\": s, \"rY\": rev}};\n\trequestJson(obj);\n}\n\nfunction setMi(s)\n{\n\tvar mi = gId(`seg${s}mi`).checked;\n\tvar obj = {\"seg\": {\"id\": s, \"mi\": mi}};\n\trequestJson(obj);\n}\n\nfunction setMiY(s)\n{\n\tvar mi = gId(`seg${s}mY`).checked;\n\tvar obj = {\"seg\": {\"id\": s, \"mY\": mi}};\n\trequestJson(obj);\n}\n\nfunction setM12(s)\n{\n\tvar value = gId(`seg${s}m12`).selectedIndex;\n\tvar obj = {\"seg\": {\"id\": s, \"m12\": value}};\n\trequestJson(obj);\n}\n\nfunction setSi(s)\n{\n\tvar value = gId(`seg${s}si`).selectedIndex;\n\tvar obj = {\"seg\": {\"id\": s, \"si\": value}};\n\trequestJson(obj);\n}\n\nfunction setBm(s)\n{\n\tvar value = gId(`seg${s}bm`).selectedIndex;\n\tvar obj = {\"seg\": {\"id\": s, \"bm\": value}};\n\trequestJson(obj);\n}\n\nfunction setTp(s)\n{\n\tvar tp = gId(`seg${s}tp`).checked;\n\tvar obj = {\"seg\": {\"id\": s, \"tp\": tp}};\n\trequestJson(obj);\n}\n\nfunction setGrp(s, g)\n{\n\tevent.preventDefault();\n\tevent.stopPropagation();\n\tvar obj = {\"seg\": {\"id\": s, \"set\": g}};\n\trequestJson(obj);\n}\n\nfunction setSegPwr(s)\n{\n\tvar pwr = gId(`seg${s}pwr`).classList.contains('act');\n\tvar obj = {\"seg\": {\"id\": s, \"on\": !pwr}};\n\trequestJson(obj);\n}\n\nfunction setSegBri(s)\n{\n\tvar obj = {\"seg\": {\"id\": s, \"bri\": parseInt(gId(`seg${s}bri`).value)}};\n\trequestJson(obj);\n}\n\nfunction tglFreeze(s=null)\n{\n\tvar obj = {\"seg\": {\"frz\": \"t\"}}; // toggle\n\tif (s!==null) {\n\t\tobj.seg.id = s;\n\t\t// if live segment, enter live override (which also unfreezes)\n\t\tif (lastinfo && s==lastinfo.liveseg && lastinfo.live) obj = {\"lor\":1};\n\t}\n\trequestJson(obj);\n}\n\nfunction setFX(ind = null)\n{\n\tif (ind === null) {\n\t\tind = parseInt(d.querySelector('#fxlist input[name=\"fx\"]:checked').value);\n\t} else {\n\t\td.querySelector(`#fxlist input[name=\"fx\"][value=\"${ind}\"]`).checked = true;\n\t}\n\n\t// Close effect dialog in simplified UI\n\tif (simplifiedUI) {\n\t\tgId(\"fx\").lastElementChild.close();\n\t}\n\n\tvar obj = {\"seg\": {\"fx\": parseInt(ind), \"fxdef\": cfg.comp.fxdef}}; // fxdef sets effect parameters to default values\n\trequestJson(obj);\n}\n\nfunction setPalette(paletteId = null)\n{\n\tif (paletteId === null) {\n\t\tpaletteId = parseInt(d.querySelector('#pallist input[name=\"palette\"]:checked').value);\n\t} else {\n\t\td.querySelector(`#pallist input[name=\"palette\"][value=\"${paletteId}\"]`).checked = true;\n\t}\n\n\t// Close palette dialog in simplified UI\n\tif (simplifiedUI) {\n\t\tgId(\"palw\").lastElementChild.close();\n\t}\n\n\tvar obj = {\"seg\": {\"pal\": paletteId}};\n\trequestJson(obj);\n}\n\nfunction setBri()\n{\n\tvar obj = {\"bri\": parseInt(gId('sliderBri').value)};\n\trequestJson(obj);\n}\n\nfunction setSpeed()\n{\n\tvar obj = {\"seg\": {\"sx\": parseInt(gId('sliderSpeed').value)}};\n\trequestJson(obj);\n}\n\nfunction setIntensity()\n{\n\tvar obj = {\"seg\": {\"ix\": parseInt(gId('sliderIntensity').value)}};\n\trequestJson(obj);\n}\n\nfunction setCustom(i=1)\n{\n\tif (i<1 || i>3) return;\n\tvar obj = {\"seg\": {}};\n\tvar val = parseInt(gId(`sliderC${i}`).value);\n\tif      (i===3) obj.seg.c3 = val;\n\telse if (i===2) obj.seg.c2 = val;\n\telse            obj.seg.c1 = val;\n\trequestJson(obj);\n}\n\nfunction setOption(i=1, v=false)\n{\n\tif (i<1 || i>3) return;\n\tvar obj = {\"seg\": {}};\n\tif      (i===3) obj.seg.o3 = !(!v); //make sure it is bool\n\telse if (i===2) obj.seg.o2 = !(!v); //make sure it is bool\n\telse            obj.seg.o1 = !(!v); //make sure it is bool\n\trequestJson(obj);\n}\n\nfunction setLor(i)\n{\n\tvar obj = {\"lor\": i};\n\trequestJson(obj);\n}\n\nfunction setPreset(i)\n{\n\tvar obj = {\"ps\":i};\n\tif (!isPlaylist(i) && pJson && pJson[i] && (!pJson[i].win || pJson[i].win.indexOf(\"Please\") <= 0)) {\n\t\t// we will send the complete preset content as to avoid delay introduced by\n\t\t// async nature of applyPreset() and having to read the preset from file system.\n\t\tobj = {\"pd\":i}; // use \"pd\" instead of \"ps\" to indicate that we are sending the preset content directly\n\t\tObject.assign(obj, pJson[i]);\n\t\tdelete obj.ql; // no need for quick load\n\t\tdelete obj.n;  // no need for name\n\t}\n\tif (isPlaylist(i)) obj.on = true; // force on\n\tshowToast(\"Loading preset \" + pName(i) +\" (\" + i + \")\");\n\trequestJson(obj);\n}\n\nfunction saveP(i,pl)\n{\n\tpI = parseInt(gId(`p${i}id`).value);\n\tif (!pI || pI < 1) pI = (i>0) ? i : getLowestUnusedP();\n\tif (pI > 250) {alert(\"Preset ID must be 250 or less.\"); return;}\n\tpN = gId(`p${i}txt`).value;\n\tif (pN == \"\") pN = (pl?\"Playlist \":\"Preset \") + pI;\n\tvar obj = {};\n\tif (!gId(`p${i}cstgl`).checked) {\n\t\tvar raw = gId(`p${i}api`).value;\n\t\ttry {\n\t\t\tobj = JSON.parse(raw);\n\t\t} catch (e) {\n\t\t\tobj.win = raw;\n\t\t\tif (raw.length < 2) {\n\t\t\t\tgId(`p${i}warn`).innerHTML = \"&#9888; Please enter your API command first\";\n\t\t\t\treturn;\n\t\t\t} else if (raw.indexOf('{') > -1) {\n\t\t\t\tgId(`p${i}warn`).innerHTML = \"&#9888; Syntax error in custom JSON API command\";\n\t\t\t\treturn;\n\t\t\t} else if (raw.indexOf(\"Please\") == 0) {\n\t\t\t\tgId(`p${i}warn`).innerHTML = \"&#9888; Please refresh the page before modifying this preset\";\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tobj.o = true;\n\t} else {\n\t\tif (pl) {\n\t\t\tobj.playlist = plJson[i];\n\t\t\tobj.on = true;\n\t\t\tobj.o = true;\n\t\t} else {\n\t\t\tobj.ib = gId(`p${i}ibtgl`).checked;\n\t\t\tobj.sb = gId(`p${i}sbtgl`).checked;\n\t\t\tobj.sc = gId(`p${i}sbchk`).checked;\n\t\t\tif (gId(`p${i}lmp`) && gId(`p${i}lmp`).value!==\"\") obj.ledmap = parseInt(gId(`p${i}lmp`).value);\n\t\t}\n\t}\n\tif (gId(`p${i}bps`).checked) obj.bootps = pI;\n\tobj.psave = pI;\n\tobj.n = pN;\n\tvar pQN = gId(`p${i}ql`).value;\n\tif (pQN.length > 0) obj.ql = pQN;\n\n\tshowToast(\"Saving \" + pN +\" (\" + pI + \")\");\n\trequestJson(obj);\n\tif (obj.o) {\n\t\tpJson[pI] = obj;\n\t\tdelete pJson[pI].psave;\n\t\tdelete pJson[pI].o;\n\t\tdelete pJson[pI].v;\n\t\tdelete pJson[pI].time;\n\t} else {\n\t\tpJson[pI] = {\"n\":pN, \"win\":\"Please refresh the page to see this newly saved command.\"};\n\t\tif (obj.win) pJson[pI].win = obj.win;\n\t\tif (obj.ql)  pJson[pI].ql = obj.ql;\n\t}\n\tpopulatePresets();\n\tresetPUtil();\n\tsetTimeout(()=>{loadPresets();}, 750); // force reloading of presets\n}\n\nfunction testPl(i,bt) {\n\tif (bt.dataset.test == 1) {\n\t\tbt.dataset.test = 0;\n\t\tbt.innerHTML = \"<i class='icons btn-icon'>&#xe139;</i>Test\";\n\t\tstopPl();\n\t\treturn;\n\t}\n\tbt.dataset.test = 1;\n\tbt.innerHTML = \"<i class='icons btn-icon'>&#xe38f;</i>Stop\";\n\tvar obj = {};\n\tobj.playlist = plJson[i];\n\tobj.on = true;\n\trequestJson(obj);\n}\n\nfunction stopPl() {\n\trequestJson({playlist:{}})\n}\n\nfunction delP(i) {\n\tvar bt = gId(`p${i}del`);\n\tif (bt.dataset.cnf == 1) {\n\t\tvar obj = {\"pdel\": i};\n\t\trequestJson(obj);\n\t\tdelete pJson[i];\n\t\tpopulatePresets();\n\t\tgId('putil').classList.add('staybot');\n\t} else {\n\t\tbt.style.color = \"var(--c-r)\";\n\t\tbt.innerHTML = \"<i class='icons btn-icon'>&#xe037;</i>Delete!\";\n\t\tbt.dataset.cnf = 1;\n\t}\n}\n\nfunction selectSlot(b)\n{\n\tcsel = b;\n\tvar cd = gId('csl').children;\n\tfor (let i of cd) i.classList.remove('sl');\n\tcd[b].classList.add('sl');\n\tsetPicker(rgbStr(cd[b].dataset));\n\t// force slider update on initial load (picker \"color:change\" not fired if black)\n\tif (cpick.color.value == 0) updatePSliders();\n\tgId('sliderW').value = parseInt(cd[b].dataset.w);\n\tupdateTrail(gId('sliderW'));\n\tredrawPalPrev();\n}\n\n// set the color from a hex string. Used by quick color selectors\nvar lasth = 0;\nfunction pC(col)\n{\n\tif (col == \"rnd\") {\n\t\tcol = {h: 0, s: 0, v: 100};\n\t\tcol.s = Math.floor((Math.random() * 50) + 50);\n\t\tdo {\n\t\t\tcol.h = Math.floor(Math.random() * 360);\n\t\t} while (Math.abs(col.h - lasth) < 50);\n\t\tlasth = col.h;\n\t}\n\tsetPicker(col);\n\tsetColor(0);\n}\n\nfunction updatePSliders() {\n\t// update RGB sliders\n\tvar col = cpick.color.rgb;\n\tgId('sliderR').value = col.r;\n\tgId('sliderG').value = col.g;\n\tgId('sliderB').value = col.b;\n\n\t// update hex field\n\tvar str = cpick.color.hexString.substring(1);\n\tvar w = parseInt(gId(\"csl\").children[csel].dataset.w);\n\tif (w > 0) str += w.toString(16);\n\tgId('hexc').value = str;\n\tgId('hexcnf').style.backgroundColor = \"var(--c-3)\";\n\n\t// update HSV sliders\n\tvar c;\n\tlet h = cpick.color.hue;\n\tlet s = cpick.color.saturation;\n\tlet v = cpick.color.value;\n\n\tgId(\"sliderH\").value = h;\n\tgId(\"sliderS\").value = s;\n\tgId('sliderV').value = v;\n\n\tc = iro.Color.hsvToRgb({\"h\":h,\"s\":100,\"v\":100});\n\tgId(\"sliderS\").nextElementSibling.style.backgroundImage = 'linear-gradient(90deg, #aaa -15%, rgb('+c.r+','+c.g+','+c.b+'))';\n\n\tc = iro.Color.hsvToRgb({\"h\":h,\"s\":s,\"v\":100});\n\tgId('sliderV').nextElementSibling.style.backgroundImage = 'linear-gradient(90deg, #000 -15%, rgb('+c.r+','+c.g+','+c.b+'))';\n\n\t// update Kelvin slider\n\tgId('sliderK').value = cpick.color.kelvin;\n}\n\nfunction hexEnter()\n{\n\tif(event.keyCode == 13) fromHex();\n}\n\nfunction segEnter(s) {\n\tif(event.keyCode == 13) setSeg(s);\n}\n\nfunction fromHex()\n{\n\tvar str = gId('hexc').value;\n\tlet w = parseInt(str.substring(6), 16);\n\ttry {\n\t\tsetPicker(\"#\" + str.substring(0,6));\n\t} catch (e) {\n\t\tsetPicker(\"#ffaa00\");\n\t}\n\tgId(\"csl\").children[csel].dataset.w = isNaN(w) ? 0 : w;\n\tsetColor(2);\n}\n\nfunction setPicker(rgb) {\n\tvar c = new iro.Color(rgb);\n\tif (c.value > 0) cpick.color.set(c);\n\telse cpick.color.setChannel('hsv', 'v', 0);\n\tupdateTrail(gId('sliderR'));\n\tupdateTrail(gId('sliderG'));\n\tupdateTrail(gId('sliderB'));\n}\n\nfunction fromH()\n{\n\tcpick.color.setChannel('hsv', 'h', gId('sliderH').value);\n}\n\nfunction fromS()\n{\n\tcpick.color.setChannel('hsv', 's', gId('sliderS').value);\n}\n\nfunction fromV()\n{\n\tcpick.color.setChannel('hsv', 'v', gId('sliderV').value);\n}\n\nfunction fromK()\n{\n\tcpick.color.set({ kelvin: gId('sliderK').value });\n}\n\nfunction fromRgb()\n{\n\tvar r = gId('sliderR').value;\n\tvar g = gId('sliderG').value;\n\tvar b = gId('sliderB').value;\n\tsetPicker(`rgb(${r},${g},${b})`);\n\tlet cd = gId('csl').children[csel]; // color slots\n\tcd.dataset.r = r;\n\tcd.dataset.g = g;\n\tcd.dataset.b = b;\n\tsetCSL(cd);\n}\n\nfunction fromW()\n{\n\tlet w = gId('sliderW');\n\tlet cd = gId('csl').children[csel]; // color slots\n\tcd.dataset.w = w.value;\n\tsetCSL(cd);\n\tupdateTrail(w);\n}\n\n// sr 0: from RGB sliders, 1: from picker, 2: from hex\nfunction setColor(sr)\n{\n\tvar cd = gId('csl').children[csel]; // color slots\n\tlet cdd = cd.dataset;\n\tlet w = parseInt(cdd.w), r = parseInt(cdd.r), g = parseInt(cdd.g), b = parseInt(cdd.b);\n\tif (sr == 1 && isRgbBlack(cdd)) cpick.color.setChannel('hsv', 'v', 100);\n\tif (sr != 2 && hasWhite) w = parseInt(gId('sliderW').value);\n\tvar col = cpick.color.rgb;\n\tcdd.r = r = hasRGB ? col.r : w;\n\tcdd.g = g = hasRGB ? col.g : w;\n\tcdd.b = b = hasRGB ? col.b : w;\n\tcdd.w = w;\n\tsetCSL(cd);\n\tvar obj = {\"seg\": {\"col\": [[],[],[]]}};\n\tobj.seg.col[csel] = [r, g, b, w];\n\trequestJson(obj);\n}\n\nfunction setBalance(b)\n{\n\tvar obj = {\"seg\": {\"cct\": parseInt(b)}};\n\trequestJson(obj);\n}\n\nfunction rmtTgl(ip,i) {\n\tevent.preventDefault();\n\tevent.stopPropagation();\n\tfetch(`http://${ip}/win&T=2`, {\n\t\tmethod: 'get'\n\t})\n\t.then((r)=>{\n\t\treturn r.text();\n\t})\n\t.then((t)=>{\n\t\tlet c = (new window.DOMParser()).parseFromString(t, \"text/xml\");\n\t\t// perhaps just i.classList.toggle(\"off\"); would be enough\n\t\tif (c.getElementsByTagName('ac')[0].textContent === \"0\") {\n\t\t\ti.classList.add(\"off\");\n\t\t} else {\n\t\t\ti.classList.remove(\"off\");\n\t\t}\n\t});\n}\n\nvar hc = 0;\nsetInterval(()=>{\n\tif (!isInfo) return;\n\thc+=18;\n\tif (hc>300) hc=0;\n\tif (hc>200)hc=306;\n\tif (hc==144) hc+=36;\n\tif (hc==108) hc+=18;\n\tgId('heart').style.color = `hsl(${hc}, 100%, 50%)`;\n}, 910);\n\nfunction openGH() { window.open(\"https://github.com/wled/WLED/wiki\"); }\n\nvar cnfr = false;\nfunction cnfReset()\n{\n\tif (!cnfr) {\n\t\tvar bt = gId('resetbtn');\n\t\tbt.style.color = \"var(--c-r)\";\n\t\tbt.innerHTML = \"Confirm Reboot\";\n\t\tcnfr = true; return;\n\t}\n\twindow.location.href = getURL(\"/reset\");\n}\n\nvar cnfrS = false;\nfunction rSegs()\n{\n\tvar bt = gId('rsbtn');\n\tif (!cnfrS) {\n\t\tbt.style.color = \"var(--c-r)\";\n\t\tbt.innerHTML = \"Confirm reset\";\n\t\tcnfrS = true; return;\n\t}\n\tcnfrS = false;\n\tbt.style.color = \"var(--c-f)\";\n\tbt.innerHTML = \"Reset segments\";\n\tvar obj = {\"seg\":[{\"start\":0,\"stop\":ledCount,\"sel\":true}]};\n\tif (isM) {\n\t\tobj.seg[0].stop = mw;\n\t\tobj.seg[0].startX = 0;\n\t\tobj.seg[0].stopY = mh;\n\t}\n\tfor (let i=1; i<=lSeg; i++) obj.seg.push({\"stop\":0});\n\trequestJson(obj);\n}\n\nfunction loadPalettesData() {\n\treturn new Promise((resolve) => {\n\t\tif (palettesData) return resolve(); // already loaded\n\t\tvar lsPalData = localStorage.getItem(\"wledPalx\");\n\t\tif (lsPalData) {\n\t\t\ttry {\n\t\t\t\tvar d = JSON.parse(lsPalData);\n\t\t\t\tif (d && d.vid == lastinfo.vid) {\n\t\t\t\t\tpalettesData = d.p;\n\t\t\t\t\tredrawPalPrev();\n\t\t\t\t\treturn resolve();\n\t\t\t\t}\n\t\t\t} catch (e) {}\n\t\t}\n\n\t\tpalettesData = {};\n\t\tgetPalettesData(0, () => {\n\t\t\tlocalStorage.setItem(\"wledPalx\", JSON.stringify({\n\t\t\t\tp: palettesData,\n\t\t\t\tvid: lastinfo.vid\n\t\t\t}));\n\t\t\tredrawPalPrev();\n\t\t\tsetTimeout(resolve, 99); // delay optional\n\t\t});\n\t});\n}\n\nfunction getPalettesData(page, callback, retry=0) {\n\tfetch(getURL(`/json/palx?page=${page}`), {method: 'get'})\n\t.then(res => res.ok ? res.json() : Promise.reject())\n\t.then(json => {\n\t\tpalettesData = Object.assign({}, palettesData, json.p);\n\t\tif (page < json.m) setTimeout(()=>{ getPalettesData(page + 1, callback); }, 75);\n\t\telse callback();\n\t})\n\t.catch((error)=>{\n\t\tif (retry<5) {\n\t\t\tsetTimeout(()=>{getPalettesData(page,callback,retry+1);}, 100);\n\t\t} else {\n\t\t\tshowToast(error, true);\n\t\t\tcallback();\n\t\t}\n\t});\n}\n\n/*\nfunction hideModes(txt)\n{\n\tfor (let e of (gId('fxlist').querySelectorAll('.lstI')||[])) {\n\t\tlet iT = e.querySelector('.lstIname').innerText;\n\t\tlet f = false;\n\t\tif (txt===\"2D\") f = iT.indexOf(\"\\u25A6\") >= 0 && iT.indexOf(\"\\u22EE\") < 0; // 2D && !1D\n\t\telse f = iT.indexOf(txt) >= 0;\n\t\tif (f) e.classList.add('hide'); //else e.classList.remove('hide');\n\t}\n}\n*/\nfunction search(field, listId = null) {\n\tfield.nextElementSibling.style.display = (field.value !== '') ? 'block' : 'none';\n\tif (!listId) return;\n\n\tconst search = field.value !== '';\n\n\t// restore default preset sorting if no search term is entered\n\tif (!search) {\n\t\tif (listId === 'pcont') { populatePresets(); return; }\n\t\tif (listId === 'pallist') {\n\t\t\tlet id = parseInt(d.querySelector('#pallist input[name=\"palette\"]:checked').value); // preserve selected palette\n\t\t\tpopulatePalettes();\n\t\t\tupdateSelectedPalette(id);\n\t\t\treturn;\n\t\t}\n\t}\n\n\t// clear filter if searching in fxlist\n\tif (listId === 'fxlist' && search) {\n\t\tgId(\"filters\").querySelectorAll(\"input[type=checkbox]\").forEach((e) => { e.checked = false; });\n\t}\n\n\t// do not search if filter is active\n\tif (gId(\"filters\").querySelectorAll(\"input[type=checkbox]:checked\").length) return;\n\n\t// filter list items but leave (Default & Solid) always visible\n\tconst listItems = gId(listId).querySelectorAll('.lstI');\n\tlistItems.forEach((listItem, i) => {\n\t\tif (listId !== 'pcont' && i === 0) return;\n\t\tconst listItemName = listItem.querySelector('.lstIname').innerText.toUpperCase();\n\t\tconst searchIndex = listItemName.indexOf(field.value.toUpperCase());\n\t\tif (searchIndex < 0) {\n\t\t\tlistItem.dataset.searchIndex = Number.MAX_SAFE_INTEGER;\n\t\t} else {\n\t\t\tlistItem.dataset.searchIndex = searchIndex;\n\t\t}\n\t\tlistItem.style.display = (searchIndex < 0) && !listItem.classList.contains(\"selected\") ? 'none' : '';\n\t});\n\n\t// sort list items by search index and name\n\tconst sortedListItems = Array.from(listItems).sort((a, b) => {\n\t\tconst aSearchIndex = parseInt(a.dataset.searchIndex);\n\t\tconst bSearchIndex = parseInt(b.dataset.searchIndex);\n\n\t\tif (aSearchIndex !== bSearchIndex) {\n\t\t\treturn aSearchIndex - bSearchIndex;\n\t\t}\n\n\t\tconst aName = a.querySelector('.lstIname').innerText.toUpperCase();\n\t\tconst bName = b.querySelector('.lstIname').innerText.toUpperCase();\n\n\t\treturn aName.localeCompare(bName);\n\t});\n\tsortedListItems.forEach(item => {\n\t\tgId(listId).append(item);\n\t});\n\n\t// scroll to first search result\n\tconst firstVisibleItem = sortedListItems.find(item => item.style.display !== 'none' && !item.classList.contains('sticky') && !item.classList.contains('selected'));\n\tif (firstVisibleItem && search) {\n\t\tfirstVisibleItem.scrollIntoView({ behavior: \"instant\", block: \"center\" });\n\t}\n}\n\nfunction clean(clearButton) {\n\tclearButton.style.display = 'none';\n\tconst inputField = clearButton.previousElementSibling;\n\tinputField.value = '';\n\tsearch(inputField, clearButton.parentElement.nextElementSibling.id);\n}\n\nfunction initFilters() {\n\tgId(\"filters\").querySelectorAll(\"input[type=checkbox]\").forEach((e) => { e.checked = false; });\n}\n\nfunction filterFocus(e) {\n\tconst f = gId(\"filters\");\n\tconst c = !!f.querySelectorAll(\"input[type=checkbox]:checked\").length;\n\tconst h = f.offsetHeight;\n\tconst sti = parseInt(getComputedStyle(d.documentElement).getPropertyValue('--sti'));\n\tif (e.type === \"focus\") {\n\t\t// compute sticky top (with delay for transition)\n\t\tif (!h) setTimeout(() => {\n\t\t\tsCol('--sti', (sti+f.offsetHeight) + \"px\"); // has an unpleasant consequence on palette offset\n\t\t}, 255);\n\t\tf.classList.remove('fade');\t// immediately show (still has transition)\n\t}\n\tif (e.type === \"blur\") {\n\t\tsetTimeout(() => {\n\t\t\tif (e.target === d.activeElement && d.hasFocus()) return;\n\t\t\t// do not hide if filter is active\n\t\t\tif (!c) {\n\t\t\t\t// compute sticky top\n\t\t\t\tsCol('--sti', (sti-h) + \"px\"); // has an unpleasant consequence on palette offset\n\t\t\t\tf.classList.add('fade');\n\t\t\t}\n\t\t}, 255);\t// wait with hiding\n\t}\n}\n\nfunction filterFx() {\n\tconst inputField = gId('fxFind').children[0];\n\tinputField.value = '';\n\tinputField.focus();\n\tclean(inputField.nextElementSibling);\n\tgId(\"fxlist\").querySelectorAll('.lstI').forEach((listItem, i) => {\n\t\tconst listItemName = listItem.querySelector('.lstIname').innerText;\n\t\tlet hide = false;\n\t\tgId(\"filters\").querySelectorAll(\"input[type=checkbox]\").forEach((e) => { if (e.checked && !listItemName.includes(e.dataset.flt)) hide = i > 0 /*true*/; });\n\t\tlistItem.style.display = hide && !listItem.classList.contains(\"selected\") ? 'none' : '';\n\t});\n}\n\nfunction preventBlur(e) {\n\tif (e.target === gId(\"fxFind\").children[0] || e.target === gId(\"filters\")) return;\n\te.preventDefault();\n}\n\n// make sure \"dur\" and \"transition\" are arrays with at least the length of \"ps\"\nfunction formatArr(pl) {\n\tvar l = pl.ps.length;\n\tif (!Array.isArray(pl.dur)) {\n\t\tvar v = pl.dur;\n\t\tif (isNaN(v)) v = 100;\n\t\tpl.dur = [v];\n\t}\n\tvar l2 = pl.dur.length;\n\tif (l2 < l)\n\t{\n\t\tfor (var i = 0; i < l - l2; i++)\n\t\t\tpl.dur.push(pl.dur[l2-1]);\n\t}\n\n\tif (!Array.isArray(pl.transition)) {\n\t\tvar v = pl.transition;\n\t\tif (isNaN(v)) v = tr;\n\t\tpl.transition = [v];\n\t}\n\tvar l2 = pl.transition.length;\n\tif (l2 < l)\n\t{\n\t\tfor (var i = 0; i < l - l2; i++)\n\t\t\tpl.transition.push(pl.transition[l2-1]);\n\t}\n}\n\nfunction expand(i)\n{\n\tvar seg = i<100 ? gId('seg' +i) : gId(`p${i-100}o`);\n\tlet ps = gId(\"pcont\").children; // preset wrapper\n\tif (i>100) for (let p of ps) { p.classList.remove('selected'); if (p!==seg) p.classList.remove('expanded'); } // collapse all other presets & remove selected\n\n\tseg.classList.toggle('expanded');\n\n\t// presets\n\tif (i >= 100) {\n\t\tvar p = i-100;\n\t\tif (seg.classList.contains('expanded')) {\n\t\t\tif (isPlaylist(p)) {\n\t\t\t\tplJson[p] = pJson[p].playlist;\n\t\t\t\t// make sure all keys are present in plJson[p]\n\t\t\t\tformatArr(plJson[p]);\n\t\t\t\tif (isNaN(plJson[p].repeat)) plJson[p].repeat = 0;\n\t\t\t\tif (!plJson[p].r) plJson[p].r = false;\n\t\t\t\tif (isNaN(plJson[p].end)) plJson[p].end = 0;\n\t\t\t\tgId('seg' +i).innerHTML = makeP(p,true);\n\t\t\t\trefreshPlE(p);\n\t\t\t} else {\n\t\t\t\tgId('seg' +i).innerHTML = makeP(p);\n\t\t\t}\n\t\t\tvar papi = papiVal(p);\n\t\t\tgId(`p${p}api`).value = papi;\n\t\t\tif (papi.indexOf(\"Please\") == 0) gId(`p${p}cstgl`).checked = false;\n\t\t\ttglCs(p);\n\t\t\tgId('putil').classList.remove('staybot');\n\t\t} else {\n\t\t\tupdatePA();\n\t\t\tgId('seg' +i).innerHTML = \"\";\n\t\t\tgId('putil').classList.add('staybot');\n\t\t}\n\t}\n\n\tseg.scrollIntoView({\n\t\tbehavior: 'smooth',\n\t\tblock: 'center'\n\t});\n}\n\nfunction unfocusSliders()\n{\n\tgId(\"sliderBri\").blur();\n\tgId(\"sliderSpeed\").blur();\n\tgId(\"sliderIntensity\").blur();\n}\n\n// sliding UI\nconst _C = d.querySelector('.container'), N = 4;\n\nlet iSlide = 0, x0 = null, scrollS = 0, locked = false;\n\nfunction unify(e) {\treturn e.changedTouches ? e.changedTouches[0] : e; }\n\nfunction hasIroClass(classList)\n{\n\tlet found = false;\n\tclassList.forEach((e)=>{ if (e.startsWith('Iro')) found = true; });\n\treturn found;\n}\n//required by rangetouch.js\nfunction lock(e)\n{\n\tif (pcMode || simplifiedUI) return;\n\tvar l = e.target.classList;\n\tvar pl = e.target.parentElement.classList;\n\n\tif (l.contains('noslide') || hasIroClass(l) || hasIroClass(pl)) return;\n\n\tx0 = unify(e).clientX;\n\tscrollS = gEBCN(\"tabcontent\")[iSlide].scrollTop;\n\n\t_C.classList.toggle('smooth', !(locked = true));\n}\n//required by rangetouch.js\nfunction move(e)\n{\n\tif(!locked || pcMode || simplifiedUI) return;\n\tvar clientX = unify(e).clientX;\n\tvar dx = clientX - x0;\n\tvar s = Math.sign(dx);\n\tvar f = +(s*dx/wW).toFixed(2);\n\n\tif((clientX != 0) &&\n\t\t(iSlide > 0 || s < 0) && (iSlide < N - 1 || s > 0) &&\n\t\tf > 0.12 &&\n\t\tgEBCN(\"tabcontent\")[iSlide].scrollTop == scrollS)\n\t{\n\t\t_C.style.setProperty('--i', iSlide -= s);\n\t\tf = 1 - f;\n\t\tupdateTablinks(iSlide);\n\t}\n\t_C.style.setProperty('--f', f);\n\t_C.classList.toggle('smooth', !(locked = false));\n\tx0 = null;\n}\n\nfunction size()\n{\n\twW = window.innerWidth;\n\tvar h = gId('top').clientHeight;\n\tsCol('--th', h + \"px\");\n\tsCol('--bh', gId('bot').clientHeight + \"px\");\n\tif (isLv) h -= 4;\n\tsCol('--tp', h + \"px\");\n\ttogglePcMode();\n\tlastw = wW;\n}\n\nfunction togglePcMode(fromB = false)\n{\n\tlet ap = (fromB && !lastinfo) || (lastinfo && lastinfo.wifi && lastinfo.wifi.ap);\n\tif (fromB) {\n\t\tpcModeA = !pcModeA;\n\t\tlocalStorage.setItem('pcm', pcModeA);\n\t}\n\tpcMode = (wW >= 1024) && pcModeA;\n\tif (cpick) cpick.resize(pcMode && wW>1023 && wW<1250 ? 230 : 260); // for tablet in landscape\n\tif (!fromB && ((wW < 1024 && lastw < 1024) || (wW >= 1024 && lastw >= 1024))) return; // no change in size and called from size()\n\tif (pcMode) openTab(0, true);\n\tgId('buttonPcm').className = (pcMode) ? \"active\":\"\";\n\tgId('bot').style.height = (pcMode && !cfg.comp.pcmbot) ? \"0\":\"auto\";\n\tsCol('--bh', gId('bot').clientHeight + \"px\");\n\t_C.style.width = (pcMode || simplifiedUI)?'100%':'400%';\n}\n\nfunction mergeDeep(target, ...sources)\n{\n\tif (!sources.length) return target;\n\tconst source = sources.shift();\n\n\tif (isObj(target) && isObj(source)) {\n\t\tfor (const key in source) {\n\t\t\tif (isObj(source[key])) {\n\t\t\t\tif (!target[key]) Object.assign(target, { [key]: {} });\n\t\t\t\tmergeDeep(target[key], source[key]);\n\t\t\t} else {\n\t\t\t\tObject.assign(target, { [key]: source[key] });\n\t\t\t}\n\t\t}\n\t}\n\treturn mergeDeep(target, ...sources);\n}\n\nfunction tooltip(cont=null)\n{\n\td.querySelectorAll((cont?cont+\" \":\"\")+\"[title]\").forEach((element)=>{\n\t\telement.addEventListener(\"pointerover\", ()=>{\n\t\t\t// save title\n\t\t\telement.setAttribute(\"data-title\", element.getAttribute(\"title\"));\n\t\t\tconst tooltip = d.createElement(\"span\");\n\t\t\ttooltip.className = \"tooltip\";\n\t\t\ttooltip.textContent = element.getAttribute(\"title\");\n\n\t\t\t// prevent default title popup\n\t\t\telement.removeAttribute(\"title\");\n\n\t\t\tlet { top, left, width } = element.getBoundingClientRect();\n\n\t\t\td.body.appendChild(tooltip);\n\n\t\t\tconst { offsetHeight, offsetWidth } = tooltip;\n\n\t\t\tconst offset = element.classList.contains(\"sliderwrap\") ? 4 : 10;\n\t\t\ttop -= offsetHeight + offset;\n\t\t\tleft += (width - offsetWidth) / 2;\n\n\t\t\ttooltip.style.top = top + \"px\";\n\t\t\ttooltip.style.left = left + \"px\";\n\t\t\ttooltip.classList.add(\"visible\");\n\t\t});\n\n\t\telement.addEventListener(\"pointerout\", ()=>{\n\t\t\td.querySelectorAll('.tooltip').forEach((tooltip)=>{\n\t\t\t\ttooltip.classList.remove(\"visible\");\n\t\t\t\td.body.removeChild(tooltip);\n\t\t\t});\n\t\t\t// restore title\n\t\t\telement.setAttribute(\"title\", element.getAttribute(\"data-title\"));\n\t\t});\n\t});\n};\n\n// Transforms the default UI into the simple UI\nfunction simplifyUI() {\n\t// Create dropdown dialog\n\tfunction createDropdown(id, buttonText, dialogElements = null) {\n\t\t// Create dropdown dialog\n\t\tconst dialog = d.createElement(\"dialog\");\n\t\t// Move every dialogElement to the dropdown dialog or if none are given, move all children of the element with the given id\n\t\tif (dialogElements) {\n\t\t\tdialogElements.forEach((e) => {\n\t\t\t\tdialog.appendChild(e);\n\t\t\t});\n\t\t} else {\n\t\t\twhile (gId(id).firstChild) {\n\t\t\t\tdialog.appendChild(gId(id).firstChild);\n\t\t\t}\n\t\t}\n\n\t\t// Create button for the dropdown\n\t\tconst btn = d.createElement(\"button\");\n\t\tbtn.id = id + \"btn\";\n\t\tbtn.classList.add(\"btn\");\n\t\tbtn.innerText = buttonText;\n\t\tfunction toggleDialog(e) {\n\t\t\tif (e.target != btn && e.target != dialog) return;\n\t\t\tif (dialog.open) {\n\t\t\t\tdialog.close();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Prevent autofocus on dialog open\n\t\t\tdialog.inert = true;\n\t\t\tdialog.showModal();\n\t\t\tdialog.inert = false;\n\t\t\tclean(dialog.firstElementChild.children[1]);\n\t\t\tdialog.scrollTop = 0;\n\t\t};\n\t\tbtn.addEventListener(\"click\", toggleDialog);\n\t\tdialog.addEventListener(\"click\", toggleDialog);\n\n\t\t// Add the dialog and button to the element with the given id\n\t\tgId(id).append(btn);\n\t\tgId(id).append(dialog);\n\t}\n\n\t// Check if the UI was already simplified\n\tif (gId(\"Colors\").classList.contains(\"simplified\")) return;\n\n\t// Disable PC Mode as it does not exist in simple UI\n\tif (pcMode) togglePcMode(true);\n\t_C.style.width = '100%'\n\t_C.style.setProperty('--n', 1);\n\n\tgId(\"Colors\").classList.add(\"simplified\");\n\t// Put effects below palett list\n\tgId(\"Colors\").append(gId(\"fx\"));\n\tgId(\"Colors\").append(gId(\"sliders\"));\n\t// Put segments before palette list\n\tgId(\"Colors\").insertBefore(gId(\"segcont\"), gId(\"pall\"));\n\t// Put preset quick load before palette list and segemts\n\tgId(\"Colors\").insertBefore(gId(\"pql\"), gId(\"pall\"));\n\n\t// Create dropdown for palette list\n\tcreateDropdown(\"palw\", \"Change palette\");\n\tcreateDropdown(\"fx\", \"Change effect\", [gId(\"fxFind\"), gId(\"fxlist\")]);\n\n\t// Hide palette label\n\tgId(\"pall\").style.display = \"none\";\n\tgId(\"Colors\").insertBefore(d.createElement(\"br\"), gId(\"pall\"));\n\t// Hide effect label\n\tgId(\"modeLabel\").style.display = \"none\";\n\n\t// Hide buttons in top bar\n\tgId(\"buttonNl\").style.display = \"none\";\n\tgId(\"buttonSync\").style.display = \"none\";\n\tgId(\"buttonSr\").style.display = \"none\";\n\tgId(\"buttonPcm\").style.display = \"none\";\n\n\t// Hide bottom bar \n\tgId(\"bot\").style.display = \"none\";\n\td.documentElement.style.setProperty('--bh', '0px');\n\n\t// Hide other tabs\n\tgId(\"Effects\").style.display = \"none\";\n\tgId(\"Segments\").style.display = \"none\";\n\tgId(\"Presets\").style.display = \"none\";\n\n\t// Hide filter options\n\tgId(\"filters\").style.display = \"none\";\n\n\t// Hide buttons for pixel art and custom palettes (add / delete)\n\tgId(\"btns\").style.display = \"none\";\n}\n\n// Version reporting feature\nvar versionCheckDone = false;\n\nfunction checkVersionUpgrade(info) {\n\t// Only check once per page load\n\tif (versionCheckDone) return;\n\tversionCheckDone = true;\n\n\t// Suppress feature if in AP mode (no internet connection available)\n\tif (info.wifi && info.wifi.ap) return;\n\n\t// Fetch version-info.json using existing /edit endpoint\n\tfetch(getURL('/edit?func=edit&path=/version-info.json'), {\n\t\tmethod: 'get'\n\t})\n\t\t.then(res => {\n\t\t\tif (res.status === 404) {\n\t\t\t\t// File doesn't exist - first install, show install prompt\n\t\t\t\tshowVersionUpgradePrompt(info, null, info.ver);\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tif (!res.ok) {\n\t\t\t\tthrow new Error('Failed to fetch version-info.json');\n\t\t\t}\n\t\t\treturn res.json();\n\t\t})\n\t\t.then(versionInfo => {\n\t\t\tif (!versionInfo) return; // 404 case already handled\n\n\t\t\t// Check if user opted out\n\t\t\tif (versionInfo.neverAsk) return;\n\n\t\t\t// Check if version has changed\n\t\t\tconst currentVersion = info.ver;\n\t\t\tconst storedVersion = versionInfo.version || '';\n\n\t\t\tif (storedVersion && storedVersion !== currentVersion) {\n\t\t\t\t// Version has changed\n\t\t\t\tif (versionInfo.alwaysReport) {\n\t\t\t\t\t// Automatically report if user opted in for always reporting\n\t\t\t\t\treportUpgradeEvent(info, storedVersion, true);\n\t\t\t\t} else {\n\t\t\t\t\t// Show upgrade prompt\n\t\t\t\t\tshowVersionUpgradePrompt(info, storedVersion, currentVersion);\n\t\t\t\t}\n\t\t\t} else if (!storedVersion) {\n\t\t\t\t// Empty version in file, show install prompt\n\t\t\t\tshowVersionUpgradePrompt(info, null, currentVersion);\n\t\t\t}\n\t\t})\n\t\t.catch(e => {\n\t\t\tconsole.log('Failed to load version-info.json', e);\n\t\t\t// On error, save current version for next time\n\t\t\tif (info && info.ver) {\n\t\t\t\tupdateVersionInfo(info.ver, false, false);\n\t\t\t}\n\t\t});\n}\n\nfunction showVersionUpgradePrompt(info, oldVersion, newVersion) {\n\t// Determine if this is an install or upgrade\n\tconst isInstall = !oldVersion;\n\n\t// Create overlay and dialog\n\tconst overlay = d.createElement('div');\n\toverlay.id = 'versionUpgradeOverlay';\n\toverlay.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;';\n\n\tconst dialog = d.createElement('div');\n\tdialog.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);';\n\n\t// Build contextual message based on install vs upgrade\n\tconst title = isInstall\n\t\t? '🎉 Thank you for installing WLED!'\n\t\t: '🎉 WLED Upgrade Detected!';\n\n\tconst description = isInstall\n\t\t? `You are now running WLED <strong style=\"text-wrap: nowrap\">${newVersion}</strong>.`\n\t\t: `Your WLED has been upgraded from <strong style=\"text-wrap: nowrap\">${oldVersion}</strong> to <strong style=\"text-wrap: nowrap\">${newVersion}</strong>.`;\n\n\tconst question = 'Help make WLED better by sharing hardware details like chip type and LED count? This helps us understand how WLED is used and prioritize features — we never collect personal data or your activities.'\n\n\tdialog.innerHTML = `\n\t\t<h2 style=\"margin-top:0;color:var(--c-f);\">${title}</h2>\n\t\t<p style=\"color:var(--c-f);\">${description}</p>\n\t\t<p style=\"color:var(--c-f);\">${question}</p>\n\t\t<p style=\"color:var(--c-f);font-size:0.9em;\">\n\t\t\t<a href=\"https://kno.wled.ge/about/privacy-policy/\" target=\"_blank\" style=\"color:var(--c-6);\">Learn more about what data is collected and why</a>\n\t\t</p>\n\t\t<div style=\"margin-top:15px;margin-bottom:15px;\">\n\t\t\t<label style=\"display:flex;align-items:center;gap:8px;color:var(--c-f);cursor:pointer;\">\n\t\t\t\t<input type=\"checkbox\" id=\"versionSaveChoice\" style=\"cursor:pointer;\">\n\t\t\t\t<span>Save my choice for future updates</span>\n\t\t\t</label>\n\t\t</div>\n\t\t<div style=\"margin-top:20px;display:flex;flex-wrap:wrap;gap:8px;\">\n\t\t\t<button id=\"versionReportYes\" class=\"btn\">Report update</button>\n\t\t\t<button id=\"versionReportNo\" class=\"btn\">Skip reporting</button>\n\t\t</div>\n\t`;\n\n\toverlay.appendChild(dialog);\n\td.body.appendChild(overlay);\n\n\t// Add event listeners\n\tgId('versionReportYes').addEventListener('click', () => {\n\t\tconst saveChoice = gId('versionSaveChoice').checked;\n\t\td.body.removeChild(overlay);\n\t\t// Pass saveChoice as alwaysReport parameter\n\t\treportUpgradeEvent(info, oldVersion, saveChoice);\n\t});\n\n\tgId('versionReportNo').addEventListener('click', () => {\n\t\tconst saveChoice = gId('versionSaveChoice').checked;\n\t\td.body.removeChild(overlay);\n\t\tif (saveChoice) {\n\t\t\t// Save \"never ask\" preference\n\t\t\tupdateVersionInfo(newVersion, true, false);\n\t\t\tshowToast('You will not be asked again.');\n\t\t} else {\n\t\t\t// Save current version to prevent re-prompting until version changes\n\t\t\tupdateVersionInfo(newVersion, false, false);\n\t\t}\n\t});\n}\n\nfunction reportUpgradeEvent(info, oldVersion, alwaysReport) {\n\tshowToast('Reporting upgrade...');\n\n\t// Fetch fresh data from /json/info endpoint as requested\n\tfetch(getURL('/json/info'), {\n\t\tmethod: 'get'\n\t})\n\t\t.then(res => res.json())\n\t\t.then(infoData => {\n\t\t\t// Map to UpgradeEventRequest structure per OpenAPI spec\n\t\t\t// Required fields: deviceId, version, previousVersion, releaseName, chip, ledCount, isMatrix, bootloaderSHA256\n\t\t\tconst upgradeData = {\n\t\t\t\tdeviceId: infoData.deviceId,                     // Use anonymous unique device ID\n\t\t\t\tversion: infoData.ver || '',                     // Current version string\n\t\t\t\tpreviousVersion: oldVersion || '',               // Previous version from version-info.json\n\t\t\t\treleaseName: infoData.release || '',             // Release name (e.g., \"WLED 0.15.0\")\n\t\t\t\tchip: infoData.arch || '',                       // Chip architecture (esp32, esp8266, etc)\n\t\t\t\tledCount: infoData.leds ? infoData.leds.count : 0,  // Number of LEDs\n\t\t\t\tisMatrix: !!(infoData.leds && infoData.leds.matrix),  // Whether it's a 2D matrix setup\n\t\t\t\tbootloaderSHA256: infoData.bootloaderSHA256 || '',   // Bootloader SHA256 hash\n\t\t\t\tbrand: infoData.brand,                           // Device brand (always present)\n\t\t\t\tproduct: infoData.product,                       // Product name (always present)\n\t\t\t\tflashSize: infoData.flash,                       // Flash size (always present)\n\t\t\t\trepo: infoData.repo                              // GitHub repository (always present)\n\t\t\t};\n\n\t\t\t// Add optional fields if available\n\t\t\tif (infoData.psrSz !== undefined) upgradeData.psramSize = infoData.psrSz;  // Total PSRAM size in MB; can be 0\n\n\t\t\t// Note: partitionSizes not currently available in /json/info endpoint\n\n\t\t\t// Make AJAX call to postUpgradeEvent API\n\t\t\treturn fetch('https://usage.wled.me/api/usage/upgrade', {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify(upgradeData)\n\t\t\t});\n\t\t})\n\t\t.then(res => {\n\t\t\tif (res.ok) {\n\t\t\t\tif (alwaysReport) {\n\t\t\t\t\tshowToast('Thank you! Future upgrades will be reported automatically.');\n\t\t\t\t} else {\n\t\t\t\t\tshowToast('Thank you for reporting!');\n\t\t\t\t}\n\t\t\t\tupdateVersionInfo(info.ver, false, !!alwaysReport);\n\t\t\t} else {\n\t\t\t\tshowToast('Report failed. Please try again later.', true);\n\t\t\t\t// Do NOT update version info on failure - user will be prompted again\n\t\t\t}\n\t\t})\n\t\t.catch(e => {\n\t\t\tconsole.log('Failed to report upgrade', e);\n\t\t\tshowToast('Report failed. Please try again later.', true);\n\t\t\t// Do NOT update version info on error - user will be prompted again\n\t\t});\n}\n\nfunction updateVersionInfo(version, neverAsk, alwaysReport) {\n\tconst versionInfo = {\n\t\tversion: version,\n\t\tneverAsk: neverAsk,\n\t\talwaysReport: !!alwaysReport\n\t};\n\n\t// Create a Blob with JSON content and use /upload endpoint\n\tconst blob = new Blob([JSON.stringify(versionInfo)], {type: 'application/json'});\n\tconst formData = new FormData();\n\tformData.append('data', blob, 'version-info.json');\n\n\tfetch(getURL('/upload'), {\n\t\tmethod: 'POST',\n\t\tbody: formData\n\t})\n\t\t.then(res => res.text())\n\t\t.then(data => {\n\t\t\tconsole.log('Version info updated', data);\n\t\t})\n\t\t.catch(e => {\n\t\t\tconsole.log('Failed to update version-info.json', e);\n\t\t});\n}\n\nsize();\n_C.style.setProperty('--n', N);\n\nwindow.addEventListener('resize', size, true);\nwindow.addEventListener('hashchange', handleLocationHash);\n\n_C.addEventListener('mousedown', lock, false);\n_C.addEventListener('touchstart', lock, false);\n\n_C.addEventListener('mouseout', move, false);\n_C.addEventListener('mouseup', move, false);\n_C.addEventListener('touchend', move, false);"
  },
  {
    "path": "wled00/data/iro.js",
    "content": "/*!\n * iro.js v5.5.2\n * 2016-2021 James Daniel\n * Licensed under MPL 2.0\n * github.com/jaames/iro.js\n */\n!function(t,n){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=n():\"function\"==typeof define&&define.amd?define(n):(t=t||self).iro=n()}(this,function(){\"use strict\";var m,s,n,i,o,x={},j=[],r=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|^--/i;function M(t,n){for(var i in n)t[i]=n[i];return t}function y(t){var n=t.parentNode;n&&n.removeChild(t)}function h(t,n,i){var r,e,u,o,l=arguments;if(n=M({},n),3<arguments.length)for(i=[i],r=3;r<arguments.length;r++)i.push(l[r]);if(null!=i&&(n.children=i),null!=t&&null!=t.defaultProps)for(e in t.defaultProps)void 0===n[e]&&(n[e]=t.defaultProps[e]);return o=n.key,null!=(u=n.ref)&&delete n.ref,null!=o&&delete n.key,c(t,n,o,u)}function c(t,n,i,r){var e={type:t,props:n,key:i,ref:r,n:null,i:null,e:0,o:null,l:null,c:null,constructor:void 0};return m.vnode&&m.vnode(e),e}function O(t){return t.children}function I(t,n){this.props=t,this.context=n}function w(t,n){if(null==n)return t.i?w(t.i,t.i.n.indexOf(t)+1):null;for(var i;n<t.n.length;n++)if(null!=(i=t.n[n])&&null!=i.o)return i.o;return\"function\"==typeof t.type?w(t):null}function a(t){var n,i;if(null!=(t=t.i)&&null!=t.c){for(t.o=t.c.base=null,n=0;n<t.n.length;n++)if(null!=(i=t.n[n])&&null!=i.o){t.o=t.c.base=i.o;break}return a(t)}}function e(t){(!t.f&&(t.f=!0)&&1===s.push(t)||i!==m.debounceRendering)&&(i=m.debounceRendering,(m.debounceRendering||n)(u))}function u(){var t,n,i,r,e,u,o,l;for(s.sort(function(t,n){return n.d.e-t.d.e});t=s.pop();)t.f&&(r=i=void 0,u=(e=(n=t).d).o,o=n.p,l=n.u,n.u=!1,o&&(i=[],r=k(o,e,M({},e),n.w,void 0!==o.ownerSVGElement,null,i,l,null==u?w(e):u),d(i,e),r!=u&&a(e)))}function S(n,i,t,r,e,u,o,l,s){var c,a,f,h,v,d,g,b=t&&t.n||j,p=b.length;if(l==x&&(l=null!=u?u[0]:p?w(t,0):null),c=0,i.n=A(i.n,function(t){if(null!=t){if(t.i=i,t.e=i.e+1,null===(f=b[c])||f&&t.key==f.key&&t.type===f.type)b[c]=void 0;else for(a=0;a<p;a++){if((f=b[a])&&t.key==f.key&&t.type===f.type){b[a]=void 0;break}f=null}if(h=k(n,t,f=f||x,r,e,u,o,null,l,s),(a=t.ref)&&f.ref!=a&&(g=g||[]).push(a,t.c||h,t),null!=h){if(null==d&&(d=h),null!=t.l)h=t.l,t.l=null;else if(u==f||h!=l||null==h.parentNode){t:if(null==l||l.parentNode!==n)n.appendChild(h);else{for(v=l,a=0;(v=v.nextSibling)&&a<p;a+=2)if(v==h)break t;n.insertBefore(h,l)}\"option\"==i.type&&(n.value=\"\")}l=h.nextSibling,\"function\"==typeof i.type&&(i.l=h)}}return c++,t}),i.o=d,null!=u&&\"function\"!=typeof i.type)for(c=u.length;c--;)null!=u[c]&&y(u[c]);for(c=p;c--;)null!=b[c]&&N(b[c],b[c]);if(g)for(c=0;c<g.length;c++)E(g[c],g[++c],g[++c])}function A(t,n,i){if(null==i&&(i=[]),null==t||\"boolean\"==typeof t)n&&i.push(n(null));else if(Array.isArray(t))for(var r=0;r<t.length;r++)A(t[r],n,i);else i.push(n?n(function(t){if(null==t||\"boolean\"==typeof t)return null;if(\"string\"==typeof t||\"number\"==typeof t)return c(null,t,null,null);if(null==t.o&&null==t.c)return t;var n=c(t.type,t.props,t.key,null);return n.o=t.o,n}(t)):t);return i}function f(t,n,i){\"-\"===n[0]?t.setProperty(n,i):t[n]=\"number\"==typeof i&&!1===r.test(n)?i+\"px\":null==i?\"\":i}function R(t,n,i,r,e){var u,o,l,s,c;if(\"key\"===(n=e?\"className\"===n?\"class\":n:\"class\"===n?\"className\":n)||\"children\"===n);else if(\"style\"===n)if(u=t.style,\"string\"==typeof i)u.cssText=i;else{if(\"string\"==typeof r&&(u.cssText=\"\",r=null),r)for(o in r)i&&o in i||f(u,o,\"\");if(i)for(l in i)r&&i[l]===r[l]||f(u,l,i[l])}else\"o\"===n[0]&&\"n\"===n[1]?(s=n!==(n=n.replace(/Capture$/,\"\")),n=((c=n.toLowerCase())in t?c:n).slice(2),i?(r||t.addEventListener(n,v,s),(t.t||(t.t={}))[n]=i):t.removeEventListener(n,v,s)):\"list\"!==n&&\"tagName\"!==n&&\"form\"!==n&&!e&&n in t?t[n]=null==i?\"\":i:\"function\"!=typeof i&&\"dangerouslySetInnerHTML\"!==n&&(n!==(n=n.replace(/^xlink:?/,\"\"))?null==i||!1===i?t.removeAttributeNS(\"http://www.w3.org/1999/xlink\",n.toLowerCase()):t.setAttributeNS(\"http://www.w3.org/1999/xlink\",n.toLowerCase(),i):null==i||!1===i?t.removeAttribute(n):t.setAttribute(n,i))}function v(t){return this.t[t.type](m.event?m.event(t):t)}function k(t,n,i,r,e,u,o,l,s,c){var a,f,h,v,d,g,b,p,y,w,k=n.type;if(void 0!==n.constructor)return null;(a=m.e)&&a(n);try{t:if(\"function\"==typeof k){if(p=n.props,y=(a=k.contextType)&&r[a.c],w=a?y?y.props.value:a.i:r,i.c?b=(f=n.c=i.c).i=f.k:(\"prototype\"in k&&k.prototype.render?n.c=f=new k(p,w):(n.c=f=new I(p,w),f.constructor=k,f.render=z),y&&y.sub(f),f.props=p,f.state||(f.state={}),f.context=w,f.w=r,h=f.f=!0,f.m=[]),null==f.j&&(f.j=f.state),null!=k.getDerivedStateFromProps&&M(f.j==f.state?f.j=M({},f.j):f.j,k.getDerivedStateFromProps(p,f.j)),h)null==k.getDerivedStateFromProps&&null!=f.componentWillMount&&f.componentWillMount(),null!=f.componentDidMount&&o.push(f);else{if(null==k.getDerivedStateFromProps&&null==l&&null!=f.componentWillReceiveProps&&f.componentWillReceiveProps(p,w),!l&&null!=f.shouldComponentUpdate&&!1===f.shouldComponentUpdate(p,f.j,w)){for(f.props=p,f.state=f.j,f.f=!1,(f.d=n).o=null!=s?s!==i.o?s:i.o:null,n.n=i.n,a=0;a<n.n.length;a++)n.n[a]&&(n.n[a].i=n);break t}null!=f.componentWillUpdate&&f.componentWillUpdate(p,f.j,w)}for(v=f.props,d=f.state,f.context=w,f.props=p,f.state=f.j,(a=m.M)&&a(n),f.f=!1,f.d=n,f.p=t,a=f.render(f.props,f.state,f.context),n.n=A(null!=a&&a.type==O&&null==a.key?a.props.children:a),null!=f.getChildContext&&(r=M(M({},r),f.getChildContext())),h||null==f.getSnapshotBeforeUpdate||(g=f.getSnapshotBeforeUpdate(v,d)),S(t,n,i,r,e,u,o,s,c),f.base=n.o;a=f.m.pop();)f.j&&(f.state=f.j),a.call(f);h||null==v||null==f.componentDidUpdate||f.componentDidUpdate(v,d,g),b&&(f.k=f.i=null)}else n.o=function(t,n,i,r,e,u,o,l){var s,c,a,f,h=i.props,v=n.props;if(e=\"svg\"===n.type||e,null==t&&null!=u)for(s=0;s<u.length;s++)if(null!=(c=u[s])&&(null===n.type?3===c.nodeType:c.localName===n.type)){t=c,u[s]=null;break}if(null==t){if(null===n.type)return document.createTextNode(v);t=e?document.createElementNS(\"http://www.w3.org/2000/svg\",n.type):document.createElement(n.type),u=null}return null===n.type?h!==v&&(null!=u&&(u[u.indexOf(t)]=null),t.data=v):n!==i&&(null!=u&&(u=j.slice.call(t.childNodes)),a=(h=i.props||x).dangerouslySetInnerHTML,f=v.dangerouslySetInnerHTML,l||(f||a)&&(f&&a&&f.O==a.O||(t.innerHTML=f&&f.O||\"\")),function(t,n,i,r,e){var u;for(u in i)u in n||R(t,u,null,i[u],r);for(u in n)e&&\"function\"!=typeof n[u]||\"value\"===u||\"checked\"===u||i[u]===n[u]||R(t,u,n[u],i[u],r)}(t,v,h,e,l),n.n=n.props.children,f||S(t,n,i,r,\"foreignObject\"!==n.type&&e,u,o,x,l),l||(\"value\"in v&&void 0!==v.value&&v.value!==t.value&&(t.value=null==v.value?\"\":v.value),\"checked\"in v&&void 0!==v.checked&&v.checked!==t.checked&&(t.checked=v.checked))),t}(i.o,n,i,r,e,u,o,c);(a=m.diffed)&&a(n)}catch(t){m.o(t,n,i)}return n.o}function d(t,n){for(var i;i=t.pop();)try{i.componentDidMount()}catch(t){m.o(t,i.d)}m.c&&m.c(n)}function E(t,n,i){try{\"function\"==typeof t?t(n):t.current=n}catch(t){m.o(t,i)}}function N(t,n,i){var r,e,u;if(m.unmount&&m.unmount(t),(r=t.ref)&&E(r,null,n),i||\"function\"==typeof t.type||(i=null!=(e=t.o)),t.o=t.l=null,null!=(r=t.c)){if(r.componentWillUnmount)try{r.componentWillUnmount()}catch(t){m.o(t,n)}r.base=r.p=null}if(r=t.n)for(u=0;u<r.length;u++)r[u]&&N(r[u],n,i);null!=e&&y(e)}function z(t,n,i){return this.constructor(t,i)}function g(t,n){for(var i=0;i<n.length;i++){var r=n[i];r.enumerable=r.enumerable||!1,r.configurable=!0,\"value\"in r&&(r.writable=!0),Object.defineProperty(t,r.key,r)}}function b(){return(b=Object.assign||function(t){for(var n=arguments,i=1;i<arguments.length;i++){var r=n[i];for(var e in r)Object.prototype.hasOwnProperty.call(r,e)&&(t[e]=r[e])}return t}).apply(this,arguments)}m={},I.prototype.setState=function(t,n){var i=this.j!==this.state&&this.j||(this.j=M({},this.state));\"function\"==typeof t&&!(t=t(i,this.props))||M(i,t),null!=t&&this.d&&(this.u=!1,n&&this.m.push(n),e(this))},I.prototype.forceUpdate=function(t){this.d&&(t&&this.m.push(t),this.u=!0,e(this))},I.prototype.render=O,s=[],n=\"function\"==typeof Promise?Promise.prototype.then.bind(Promise.resolve()):setTimeout,i=m.debounceRendering,m.o=function(t,n,i){for(var r;n=n.i;)if((r=n.c)&&!r.i)try{if(r.constructor&&null!=r.constructor.getDerivedStateFromError)r.setState(r.constructor.getDerivedStateFromError(t));else{if(null==r.componentDidCatch)continue;r.componentDidCatch(t)}return e(r.k=r)}catch(n){t=n}throw t},o=x;var t=\"(?:[-\\\\+]?\\\\d*\\\\.\\\\d+%?)|(?:[-\\\\+]?\\\\d+%?)\",l=\"[\\\\s|\\\\(]+(\"+t+\")[,|\\\\s]+(\"+t+\")[,|\\\\s]+(\"+t+\")\\\\s*\\\\)?\",p=\"[\\\\s|\\\\(]+(\"+t+\")[,|\\\\s]+(\"+t+\")[,|\\\\s]+(\"+t+\")[,|\\\\s]+(\"+t+\")\\\\s*\\\\)?\",_=new RegExp(\"rgb\"+l),H=new RegExp(\"rgba\"+p),P=new RegExp(\"hsl\"+l),$=new RegExp(\"hsla\"+p),T=\"^(?:#?|0x?)\",W=\"([0-9a-fA-F]{1})\",C=\"([0-9a-fA-F]{2})\",D=new RegExp(T+W+W+W+\"$\"),F=new RegExp(T+W+W+W+W+\"$\"),L=new RegExp(T+C+C+C+\"$\"),B=new RegExp(T+C+C+C+C+\"$\"),q=Math.log,G=Math.round,Z=Math.floor;function J(t,n,i){return Math.min(Math.max(t,n),i)}function K(t,n){var i=-1<t.indexOf(\"%\"),r=parseFloat(t);return i?n/100*r:r}function Q(t){return parseInt(t,16)}function U(t){return t.toString(16).padStart(2,\"0\")}var V=function(){function l(t,n){this.$={h:0,s:0,v:0,a:1},t&&this.set(t),this.onChange=n,this.initialValue=b({},this.$)}var t=l.prototype;return t.set=function(t){if(\"string\"==typeof t)/^(?:#?|0x?)[0-9a-fA-F]{3,8}$/.test(t)?this.hexString=t:/^rgba?/.test(t)?this.rgbString=t:/^hsla?/.test(t)&&(this.hslString=t);else{if(\"object\"!=typeof t)throw new Error(\"Invalid color value\");t instanceof l?this.hsva=t.hsva:\"r\"in t&&\"g\"in t&&\"b\"in t?this.rgb=t:\"h\"in t&&\"s\"in t&&\"v\"in t?this.hsv=t:\"h\"in t&&\"s\"in t&&\"l\"in t?this.hsl=t:\"kelvin\"in t&&(this.kelvin=t.kelvin)}},t.setChannel=function(t,n,i){var r;this[t]=b({},this[t],((r={})[n]=i,r))},t.reset=function(){this.hsva=this.initialValue},t.clone=function(){return new l(this)},t.unbind=function(){this.onChange=void 0},l.hsvToRgb=function(t){var n=t.h/60,i=t.s/100,r=t.v/100,e=Z(n),u=n-e,o=r*(1-i),l=r*(1-u*i),s=r*(1-(1-u)*i),c=e%6,a=[s,r,r,l,o,o][c],f=[o,o,s,r,r,l][c];return{r:J(255*[r,l,o,o,s,r][c],0,255),g:J(255*a,0,255),b:J(255*f,0,255)}},l.rgbToHsv=function(t){var n=t.r/255,i=t.g/255,r=t.b/255,e=Math.max(n,i,r),u=Math.min(n,i,r),o=e-u,l=0,s=e,c=0===e?0:o/e;switch(e){case u:l=0;break;case n:l=(i-r)/o+(i<r?6:0);break;case i:l=(r-n)/o+2;break;case r:l=(n-i)/o+4}return{h:60*l%360,s:J(100*c,0,100),v:J(100*s,0,100)}},l.hsvToHsl=function(t){var n=t.s/100,i=t.v/100,r=(2-n)*i,e=r<=1?r:2-r,u=e<1e-9?0:n*i/e;return{h:t.h,s:J(100*u,0,100),l:J(50*r,0,100)}},l.hslToHsv=function(t){var n=2*t.l,i=t.s*(n<=100?n:200-n)/100,r=n+i<1e-9?0:2*i/(n+i);return{h:t.h,s:J(100*r,0,100),v:J((n+i)/2,0,100)}},l.kelvinToRgb=function(t){var n,i,r,e=t/100;return r=e<66?(n=255,i=-155.25485562709179-.44596950469579133*(i=e-2)+104.49216199393888*q(i),e<20?0:.8274096064007395*(r=e-10)-254.76935184120902+115.67994401066147*q(r)):(n=351.97690566805693+.114206453784165*(n=e-55)-40.25366309332127*q(n),i=325.4494125711974+.07943456536662342*(i=e-50)-28.0852963507957*q(i),255),{r:J(Z(n),0,255),g:J(Z(i),0,255),b:J(Z(r),0,255)}},l.rgbToKelvin=function(t){for(var n,i=t.r,r=t.b,e=2e3,u=4e4;.4<u-e;){var o=l.kelvinToRgb(n=.5*(u+e));o.b/o.r>=r/i?u=n:e=n}return n},function(t,n,i){n&&g(t.prototype,n),i&&g(t,i)}(l,[{key:\"hsv\",get:function(){var t=this.$;return{h:t.h,s:t.s,v:t.v}},set:function(t){var n=this.$;if(t=b({},n,t),this.onChange){var i={h:!1,v:!1,s:!1,a:!1};for(var r in n)i[r]=t[r]!=n[r];this.$=t,(i.h||i.s||i.v||i.a)&&this.onChange(this,i)}else this.$=t}},{key:\"hsva\",get:function(){return b({},this.$)},set:function(t){this.hsv=t}},{key:\"hue\",get:function(){return this.$.h},set:function(t){this.hsv={h:t}}},{key:\"saturation\",get:function(){return this.$.s},set:function(t){this.hsv={s:t}}},{key:\"value\",get:function(){return this.$.v},set:function(t){this.hsv={v:t}}},{key:\"alpha\",get:function(){return this.$.a},set:function(t){this.hsv=b({},this.hsv,{a:t})}},{key:\"kelvin\",get:function(){return l.rgbToKelvin(this.rgb)},set:function(t){this.rgb=l.kelvinToRgb(t)}},{key:\"red\",get:function(){return this.rgb.r},set:function(t){this.rgb=b({},this.rgb,{r:t})}},{key:\"green\",get:function(){return this.rgb.g},set:function(t){this.rgb=b({},this.rgb,{g:t})}},{key:\"blue\",get:function(){return this.rgb.b},set:function(t){this.rgb=b({},this.rgb,{b:t})}},{key:\"rgb\",get:function(){var t=l.hsvToRgb(this.$),n=t.r,i=t.g,r=t.b;return{r:G(n),g:G(i),b:G(r)}},set:function(t){this.hsv=b({},l.rgbToHsv(t),{a:void 0===t.a?1:t.a})}},{key:\"rgba\",get:function(){return b({},this.rgb,{a:this.alpha})},set:function(t){this.rgb=t}},{key:\"hsl\",get:function(){var t=l.hsvToHsl(this.$),n=t.h,i=t.s,r=t.l;return{h:G(n),s:G(i),l:G(r)}},set:function(t){this.hsv=b({},l.hslToHsv(t),{a:void 0===t.a?1:t.a})}},{key:\"hsla\",get:function(){return b({},this.hsl,{a:this.alpha})},set:function(t){this.hsl=t}},{key:\"rgbString\",get:function(){var t=this.rgb;return\"rgb(\"+t.r+\", \"+t.g+\", \"+t.b+\")\"},set:function(t){var n,i,r,e,u=1;if((n=_.exec(t))?(i=K(n[1],255),r=K(n[2],255),e=K(n[3],255)):(n=H.exec(t))&&(i=K(n[1],255),r=K(n[2],255),e=K(n[3],255),u=K(n[4],1)),!n)throw new Error(\"Invalid rgb string\");this.rgb={r:i,g:r,b:e,a:u}}},{key:\"rgbaString\",get:function(){var t=this.rgba;return\"rgba(\"+t.r+\", \"+t.g+\", \"+t.b+\", \"+t.a+\")\"},set:function(t){this.rgbString=t}},{key:\"hexString\",get:function(){var t=this.rgb;return\"#\"+U(t.r)+U(t.g)+U(t.b)},set:function(t){var n,i,r,e,u=255;if((n=D.exec(t))?(i=17*Q(n[1]),r=17*Q(n[2]),e=17*Q(n[3])):(n=F.exec(t))?(i=17*Q(n[1]),r=17*Q(n[2]),e=17*Q(n[3]),u=17*Q(n[4])):(n=L.exec(t))?(i=Q(n[1]),r=Q(n[2]),e=Q(n[3])):(n=B.exec(t))&&(i=Q(n[1]),r=Q(n[2]),e=Q(n[3]),u=Q(n[4])),!n)throw new Error(\"Invalid hex string\");this.rgb={r:i,g:r,b:e,a:u/255}}},{key:\"hex8String\",get:function(){var t=this.rgba;return\"#\"+U(t.r)+U(t.g)+U(t.b)+U(Z(255*t.a))},set:function(t){this.hexString=t}},{key:\"hslString\",get:function(){var t=this.hsl;return\"hsl(\"+t.h+\", \"+t.s+\"%, \"+t.l+\"%)\"},set:function(t){var n,i,r,e,u=1;if((n=P.exec(t))?(i=K(n[1],360),r=K(n[2],100),e=K(n[3],100)):(n=$.exec(t))&&(i=K(n[1],360),r=K(n[2],100),e=K(n[3],100),u=K(n[4],1)),!n)throw new Error(\"Invalid hsl string\");this.hsl={h:i,s:r,l:e,a:u}}},{key:\"hslaString\",get:function(){var t=this.hsla;return\"hsla(\"+t.h+\", \"+t.s+\"%, \"+t.l+\"%, \"+t.a+\")\"},set:function(t){this.hslString=t}}]),l}();function X(t){var n,i=t.width,r=t.sliderSize,e=t.borderWidth,u=t.handleRadius,o=t.padding,l=t.sliderShape,s=\"horizontal\"===t.layoutDirection;return r=null!=(n=r)?n:2*o+2*u,\"circle\"===l?{handleStart:t.padding+t.handleRadius,handleRange:i-2*o-2*u,width:i,height:i,cx:i/2,cy:i/2,radius:i/2-e/2}:{handleStart:r/2,handleRange:i-r,radius:r/2,x:0,y:0,width:s?r:i,height:s?i:r}}function Y(t,n){var i=X(t),r=i.width,e=i.height,u=i.handleRange,o=i.handleStart,l=\"horizontal\"===t.layoutDirection,s=l?r/2:e/2,c=o+function(t,n){var i=n.hsva,r=n.rgb;switch(t.sliderType){case\"red\":return r.r/2.55;case\"green\":return r.g/2.55;case\"blue\":return r.b/2.55;case\"alpha\":return 100*i.a;case\"kelvin\":var e=t.minTemperature,u=t.maxTemperature-e,o=(n.kelvin-e)/u*100;return Math.max(0,Math.min(o,100));case\"hue\":return i.h/=3.6;case\"saturation\":return i.s;case\"value\":default:return i.v}}(t,n)/100*u;return l&&(c=-1*c+u+2*o),{x:l?s:c,y:l?c:s}}var tt,nt=2*Math.PI,it=function(t,n){return(t%n+n)%n},rt=function(t,n){return Math.sqrt(t*t+n*n)};function et(t){return t.width/2-t.padding-t.handleRadius-t.borderWidth}function ut(t){var n=t.width/2;return{width:t.width,radius:n-t.borderWidth,cx:n,cy:n}}function ot(t,n,i){var r=t.wheelAngle,e=t.wheelDirection;return i&&\"clockwise\"===e?n=r+n:\"clockwise\"===e?n=360-r+n:i&&\"anticlockwise\"===e?n=r+180-n:\"anticlockwise\"===e&&(n=r-n),it(n,360)}function lt(t,n,i){var r=ut(t),e=r.cx,u=r.cy,o=et(t);n=e-n,i=u-i;var l=ot(t,Math.atan2(-i,-n)*(360/nt)),s=Math.min(rt(n,i),o);return{h:Math.round(l),s:Math.round(100/o*s)}}function st(t){var n=t.width,i=t.boxHeight;return{width:n,height:null!=i?i:n,radius:t.padding+t.handleRadius}}function ct(t,n,i){var r=st(t),e=r.width,u=r.height,o=r.radius,l=(n-o)/(e-2*o)*100,s=(i-o)/(u-2*o)*100;return{s:Math.max(0,Math.min(l,100)),v:Math.max(0,Math.min(100-s,100))}}function at(t,n,i,r){for(var e=0;e<r.length;e++){var u=r[e].x-n,o=r[e].y-i;if(Math.sqrt(u*u+o*o)<t.handleRadius)return e}return null}function ft(t){return{boxSizing:\"border-box\",border:t.borderWidth+\"px solid \"+t.borderColor}}function ht(t,n,i){return t+\"-gradient(\"+n+\", \"+i.map(function(t){var n=t[0];return t[1]+\" \"+n+\"%\"}).join(\",\")+\")\"}function vt(t){return\"string\"==typeof t?t:t+\"px\"}var dt=[\"mousemove\",\"touchmove\",\"mouseup\",\"touchend\"],gt=function(n){function t(t){n.call(this,t),this.uid=(Math.random()+1).toString(36).substring(5)}return n&&(t.__proto__=n),((t.prototype=Object.create(n&&n.prototype)).constructor=t).prototype.render=function(t){var n=this.handleEvent.bind(this),i={onMouseDown:n,ontouchstart:n},r=\"horizontal\"===t.layoutDirection,e=null===t.margin?t.sliderMargin:t.margin,u={overflow:\"visible\",display:r?\"inline-block\":\"block\"};return 0<t.index&&(u[r?\"marginLeft\":\"marginTop\"]=e),h(O,null,t.children(this.uid,i,u))},t.prototype.handleEvent=function(t){var n=this,i=this.props.onInput,r=this.base.getBoundingClientRect();t.preventDefault();var e=t.touches?t.changedTouches[0]:t,u=e.clientX-r.left,o=e.clientY-r.top;switch(t.type){case\"mousedown\":case\"touchstart\":!1!==i(u,o,0)&&dt.forEach(function(t){document.addEventListener(t,n,{passive:!1})});break;case\"mousemove\":case\"touchmove\":i(u,o,1);break;case\"mouseup\":case\"touchend\":i(u,o,2),dt.forEach(function(t){document.removeEventListener(t,n,{passive:!1})})}},t}(I);function bt(t){var n=t.r,i=t.url,r=n,e=n;return h(\"svg\",{className:\"IroHandle IroHandle--\"+t.index+\" \"+(t.isActive?\"IroHandle--isActive\":\"\"),style:{\"-webkit-tap-highlight-color\":\"rgba(0, 0, 0, 0);\",transform:\"translate(\"+vt(t.x)+\", \"+vt(t.y)+\")\",willChange:\"transform\",top:vt(-n),left:vt(-n),width:vt(2*n),height:vt(2*n),position:\"absolute\",overflow:\"visible\"}},i&&h(\"use\",Object.assign({xlinkHref:function(t){tt=tt||document.getElementsByTagName(\"base\");var n=window.navigator.userAgent,i=/^((?!chrome|android).)*safari/i.test(n),r=/iPhone|iPod|iPad/i.test(n),e=window.location;return(i||r)&&0<tt.length?e.protocol+\"//\"+e.host+e.pathname+e.search+t:t}(i)},t.props)),!i&&h(\"circle\",{cx:r,cy:e,r:n,fill:\"none\",\"stroke-width\":2,stroke:\"#000\"}),!i&&h(\"circle\",{cx:r,cy:e,r:n-2,fill:t.fill,\"stroke-width\":2,stroke:\"#fff\"}))}function pt(e){var t=e.activeIndex,u=void 0!==t&&t<e.colors.length?e.colors[t]:e.color,n=X(e),r=n.width,o=n.height,l=n.radius,s=Y(e,u),c=function(t,n){var i=n.hsv,r=n.rgb;switch(t.sliderType){case\"red\":return[[0,\"rgb(0,\"+r.g+\",\"+r.b+\")\"],[100,\"rgb(255,\"+r.g+\",\"+r.b+\")\"]];case\"green\":return[[0,\"rgb(\"+r.r+\",0,\"+r.b+\")\"],[100,\"rgb(\"+r.r+\",255,\"+r.b+\")\"]];case\"blue\":return[[0,\"rgb(\"+r.r+\",\"+r.g+\",0)\"],[100,\"rgb(\"+r.r+\",\"+r.g+\",255)\"]];case\"alpha\":return[[0,\"rgba(\"+r.r+\",\"+r.g+\",\"+r.b+\",0)\"],[100,\"rgb(\"+r.r+\",\"+r.g+\",\"+r.b+\")\"]];case\"kelvin\":for(var e=[],u=t.minTemperature,o=t.maxTemperature,l=o-u,s=u,c=0;s<o;s+=l/8,c+=1){var a=V.kelvinToRgb(s),f=a.r,h=a.g,v=a.b;e.push([12.5*c,\"rgb(\"+f+\",\"+h+\",\"+v+\")\"])}return e;case\"hue\":return[[0,\"#f00\"],[16.666,\"#ff0\"],[33.333,\"#0f0\"],[50,\"#0ff\"],[66.666,\"#00f\"],[83.333,\"#f0f\"],[100,\"#f00\"]];case\"saturation\":var d=V.hsvToHsl({h:i.h,s:0,v:i.v}),g=V.hsvToHsl({h:i.h,s:100,v:i.v});return[[0,\"hsl(\"+d.h+\",\"+d.s+\"%,\"+d.l+\"%)\"],[100,\"hsl(\"+g.h+\",\"+g.s+\"%,\"+g.l+\"%)\"]];case\"value\":default:var b=V.hsvToHsl({h:i.h,s:i.s,v:100});return[[0,\"#000\"],[100,\"hsl(\"+b.h+\",\"+b.s+\"%,\"+b.l+\"%)\"]]}}(e,u);return h(gt,Object.assign({},e,{onInput:function(t,n,i){var r=function(t,n,i){var r,e=X(t),u=e.handleRange,o=e.handleStart;r=\"horizontal\"===t.layoutDirection?-1*i+u+o:n-o,r=Math.max(Math.min(r,u),0);var l=Math.round(100/u*r);switch(t.sliderType){case\"kelvin\":var s=t.minTemperature;return s+l/100*(t.maxTemperature-s);case\"alpha\":return l/100;case\"hue\":return 3.6*l;case\"red\":case\"blue\":case\"green\":return 2.55*l;default:return l}}(e,t,n);e.parent.inputActive=!0,u[e.sliderType]=r,e.onInput(i,e.id)}}),function(t,n,i){return h(\"div\",Object.assign({},n,{className:\"IroSlider\",style:Object.assign({},{position:\"relative\",width:vt(r),height:vt(o),borderRadius:vt(l),background:\"conic-gradient(#ccc 25%, #fff 0 50%, #ccc 0 75%, #fff 0)\",backgroundSize:\"8px 8px\"},i)}),h(\"div\",{className:\"IroSliderGradient\",style:Object.assign({},{position:\"absolute\",top:0,left:0,width:\"100%\",height:\"100%\",borderRadius:vt(l),background:ht(\"linear\",\"horizontal\"===e.layoutDirection?\"to top\":\"to right\",c)},ft(e))}),h(bt,{isActive:!0,index:u.index,r:e.handleRadius,url:e.handleSvg,props:e.handleProps,x:s.x,y:s.y}))})}function yt(e){var t=st(e),r=t.width,u=t.height,o=t.radius,l=e.colors,s=e.parent,n=e.activeIndex,c=void 0!==n&&n<e.colors.length?e.colors[n]:e.color,a=function(t,n){return[[[0,\"#fff\"],[100,\"hsl(\"+n.hue+\",100%,50%)\"]],[[0,\"rgba(0,0,0,0)\"],[100,\"#000\"]]]}(0,c),f=l.map(function(t){return function(t,n){var i=st(t),r=i.width,e=i.height,u=i.radius,o=n.hsv,l=u,s=r-2*u,c=e-2*u;return{x:l+o.s/100*s,y:l+(c-o.v/100*c)}}(e,t)});return h(gt,Object.assign({},e,{onInput:function(t,n,i){if(0===i){var r=at(e,t,n,f);null!==r?s.setActiveColor(r):(s.inputActive=!0,c.hsv=ct(e,t,n),e.onInput(i,e.id))}else 1===i&&(s.inputActive=!0,c.hsv=ct(e,t,n));e.onInput(i,e.id)}}),function(t,n,i){return h(\"div\",Object.assign({},n,{className:\"IroBox\",style:Object.assign({},{width:vt(r),height:vt(u),position:\"relative\"},i)}),h(\"div\",{className:\"IroBox\",style:Object.assign({},{width:\"100%\",height:\"100%\",borderRadius:vt(o)},ft(e),{background:ht(\"linear\",\"to bottom\",a[1])+\",\"+ht(\"linear\",\"to right\",a[0])})}),l.filter(function(t){return t!==c}).map(function(t){return h(bt,{isActive:!1,index:t.index,fill:t.hslString,r:e.handleRadius,url:e.handleSvg,props:e.handleProps,x:f[t.index].x,y:f[t.index].y})}),h(bt,{isActive:!0,index:c.index,fill:c.hslString,r:e.activeHandleRadius||e.handleRadius,url:e.handleSvg,props:e.handleProps,x:f[c.index].x,y:f[c.index].y}))})}bt.defaultProps={fill:\"none\",x:0,y:0,r:8,url:null,props:{x:0,y:0}},pt.defaultProps=Object.assign({},{sliderShape:\"bar\",sliderType:\"value\",minTemperature:2200,maxTemperature:11e3});function wt(e){var r=ut(e).width,u=e.colors,o=(e.borderWidth,e.parent),l=e.color,s=l.hsv,c=u.map(function(t){return function(t,n){var i=n.hsv,r=ut(t),e=r.cx,u=r.cy,o=et(t),l=(180+ot(t,i.h,!0))*(nt/360),s=i.s/100*o,c=\"clockwise\"===t.wheelDirection?-1:1;return{x:e+s*Math.cos(l)*c,y:u+s*Math.sin(l)*c}}(e,t)}),a={position:\"absolute\",top:0,left:0,width:\"100%\",height:\"100%\",borderRadius:\"50%\",boxSizing:\"border-box\"};return h(gt,Object.assign({},e,{onInput:function(t,n,i){if(0===i){if(!function(t,n,i){var r=ut(t),e=r.cx,u=r.cy,o=t.width/2;return rt(e-n,u-i)<o}(e,t,n))return!1;var r=at(e,t,n,c);null!==r?o.setActiveColor(r):(o.inputActive=!0,l.hsv=lt(e,t,n),e.onInput(i,e.id))}else 1===i&&(o.inputActive=!0,l.hsv=lt(e,t,n));e.onInput(i,e.id)}}),function(t,n,i){return h(\"div\",Object.assign({},n,{className:\"IroWheel\",style:Object.assign({},{width:vt(r),height:vt(r),position:\"relative\"},i)}),h(\"div\",{className:\"IroWheelHue\",style:Object.assign({},a,{transform:\"rotateZ(\"+(e.wheelAngle+90)+\"deg)\",background:\"clockwise\"===e.wheelDirection?\"conic-gradient(red, yellow, lime, aqua, blue, magenta, red)\":\"conic-gradient(red, magenta, blue, aqua, lime, yellow, red)\"})}),h(\"div\",{className:\"IroWheelSaturation\",style:Object.assign({},a,{background:\"radial-gradient(circle closest-side, #fff, transparent)\"})}),e.wheelLightness&&h(\"div\",{className:\"IroWheelLightness\",style:Object.assign({},a,{background:\"#000\",opacity:1-s.v/100})}),h(\"div\",{className:\"IroWheelBorder\",style:Object.assign({},a,ft(e))}),u.filter(function(t){return t!==l}).map(function(t){return h(bt,{isActive:!1,index:t.index,fill:t.hslString,r:e.handleRadius,url:e.handleSvg,props:e.handleProps,x:c[t.index].x,y:c[t.index].y})}),h(bt,{isActive:!0,index:l.index,fill:l.hslString,r:e.activeHandleRadius||e.handleRadius,url:e.handleSvg,props:e.handleProps,x:c[l.index].x,y:c[l.index].y}))})}var kt=function(i){function t(t){var n=this;i.call(this,t),this.colors=[],this.inputActive=!1,this.events={},this.activeEvents={},this.deferredEvents={},this.id=t.id,(0<t.colors.length?t.colors:[t.color]).forEach(function(t){return n.addColor(t)}),this.setActiveColor(0),this.state=Object.assign({},t,{color:this.color,colors:this.colors,layout:t.layout})}return i&&(t.__proto__=i),((t.prototype=Object.create(i&&i.prototype)).constructor=t).prototype.addColor=function(t,n){void 0===n&&(n=this.colors.length);var i=new V(t,this.onColorChange.bind(this));this.colors.splice(n,0,i),this.colors.forEach(function(t,n){return t.index=n}),this.state&&this.setState({colors:this.colors}),this.deferredEmit(\"color:init\",i)},t.prototype.removeColor=function(t){var n=this.colors.splice(t,1)[0];n.unbind(),this.colors.forEach(function(t,n){return t.index=n}),this.state&&this.setState({colors:this.colors}),n.index===this.color.index&&this.setActiveColor(0),this.emit(\"color:remove\",n)},t.prototype.setActiveColor=function(t){this.color=this.colors[t],this.state&&this.setState({color:this.color}),this.emit(\"color:setActive\",this.color)},t.prototype.setColors=function(t,n){var i=this;void 0===n&&(n=0),this.colors.forEach(function(t){return t.unbind()}),this.colors=[],t.forEach(function(t){return i.addColor(t)}),this.setActiveColor(n),this.emit(\"color:setAll\",this.colors)},t.prototype.on=function(t,n){var i=this,r=this.events;(Array.isArray(t)?t:[t]).forEach(function(t){(r[t]||(r[t]=[])).push(n),i.deferredEvents[t]&&(i.deferredEvents[t].forEach(function(t){n.apply(null,t)}),i.deferredEvents[t]=[])})},t.prototype.off=function(t,i){var r=this;(Array.isArray(t)?t:[t]).forEach(function(t){var n=r.events[t];n&&n.splice(n.indexOf(i),1)})},t.prototype.emit=function(t){for(var n=this,i=[],r=arguments.length-1;0<r--;)i[r]=arguments[r+1];var e=this.activeEvents;!!e.hasOwnProperty(t)&&e[t]||(e[t]=!0,(this.events[t]||[]).forEach(function(t){return t.apply(n,i)}),e[t]=!1)},t.prototype.deferredEmit=function(t){for(var n,i=[],r=arguments.length-1;0<r--;)i[r]=arguments[r+1];var e=this.deferredEvents;(n=this).emit.apply(n,[t].concat(i)),(e[t]||(e[t]=[])).push(i)},t.prototype.setOptions=function(t){this.setState(t)},t.prototype.resize=function(t){this.setOptions({width:t})},t.prototype.reset=function(){this.colors.forEach(function(t){return t.reset()}),this.setState({colors:this.colors})},t.prototype.onMount=function(t){this.el=t,this.deferredEmit(\"mount\",this)},t.prototype.onColorChange=function(t,n){this.setState({color:this.color}),this.inputActive&&(this.inputActive=!1,this.emit(\"input:change\",t,n)),this.emit(\"color:change\",t,n)},t.prototype.emitInputEvent=function(t,n){0===t?this.emit(\"input:start\",this.color,n):1===t?this.emit(\"input:move\",this.color,n):2===t&&this.emit(\"input:end\",this.color,n)},t.prototype.render=function(t,e){var u=this,n=e.layout;return Array.isArray(n)||(n=[{component:wt},{component:pt}],e.transparency&&n.push({component:pt,options:{sliderType:\"alpha\"}})),h(\"div\",{class:\"IroColorPicker\",id:e.id,style:{display:e.display}},n.map(function(t,n){var i=t.component,r=t.options;return h(i,Object.assign({},e,r,{ref:void 0,onInput:u.emitInputEvent.bind(u),parent:u,index:n}))}))},t}(I);kt.defaultProps=Object.assign({},{width:300,height:300,color:\"#fff\",colors:[],padding:6,layoutDirection:\"vertical\",borderColor:\"#fff\",borderWidth:0,handleRadius:8,activeHandleRadius:null,handleSvg:null,handleProps:{x:0,y:0},wheelLightness:!0,wheelAngle:0,wheelDirection:\"anticlockwise\",sliderSize:null,sliderMargin:12,boxHeight:null},{colors:[],display:\"block\",id:null,layout:\"default\",margin:null});var mt,xt,jt,Mt,Ot=(It.prototype=(mt=kt).prototype,Object.assign(It,mt),It.I=mt,It);function It(n,t){var i,r=document.createElement(\"div\");function e(){var t=n instanceof Element?n:document.querySelector(n);t.appendChild(i.base),i.onMount(t)}return function(t,n,i){var r,e,u;m.i&&m.i(t,n),e=(r=i===o)?null:i&&i.n||n.n,t=h(O,null,[t]),u=[],k(n,r?n.n=t:(i||n).n=t,e||x,x,void 0!==n.ownerSVGElement,i&&!r?[i]:e?null:j.slice.call(n.childNodes),u,!1,i||x,r),d(u,t)}(h(mt,Object.assign({},{ref:function(t){return i=t}},t)),r),\"loading\"!==document.readyState?e():document.addEventListener(\"DOMContentLoaded\",e),i}return(jt=xt=xt||{}).version=\"5.5.2\",jt.Color=V,jt.ColorPicker=Ot,(Mt=jt.ui||(jt.ui={})).h=h,Mt.ComponentBase=gt,Mt.Handle=bt,Mt.Slider=pt,Mt.Wheel=wt,Mt.Box=yt,xt});"
  },
  {
    "path": "wled00/data/liveview.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, minimum-scale=1\">\n  <meta charset=\"utf-8\">\n  <meta name=\"theme-color\" content=\"#222222\">\n  <title>WLED Live Preview</title>\n  <style>\n    body {\n      margin: 0;\n    }\n    #canv {\n      background: black;\n      filter: brightness(175%);\n      width: 100%;\n      height: 100%;\n      position: absolute;\n    }\n  </style>\n  <script>\n    // load common.js with retry on error\n    (function common() {\n      const l = document.createElement('script');\n      l.src = 'common.js';\n      l.onload = () => S();\n      l.onerror = () => setTimeout(common, 100);\n      document.head.appendChild(l);\n    })();\n    var ws;\n    var tmout = null;\n    var c;\n    var ctx;\n    function draw(start, skip, leds, fill) {\n      c.width = d.documentElement.clientWidth;\n      let w = (c.width * skip) / (leds.length - start);\n      for (let i = start; i < leds.length; i += skip) {\n        ctx.fillStyle = fill(leds,i);\n        ctx.fillRect(Math.round((i - start) * w / skip), 0, Math.ceil(w), c.height);\n      }\n    }\n    function update(retry=0) { // via HTTP (/json/live)\n      if (d.hidden) {\n        clearTimeout(tmout);\n        tmout = setTimeout(update, 250);\n        return;\n      }\n      fetch('./json/live')\n      .then(res => {\n        if (!res.ok) {\n          clearTimeout(tmout);\n          tmout = setTimeout(update, 2500);\n        }\n        return res.json();\n      })\n      .then(json => {\n        draw(0, 1, json.leds, (a,i) => \"#\" + ((a[i].length > 6) ? a[i].substring(2) : a[i]));\n        clearTimeout(tmout);\n        tmout = setTimeout(update, 40);\n      })\n      .catch((error)=>{\n        //console.error(\"Peek HTTP error:\",error);\n        clearTimeout(tmout);\n        if (retry<5) tmout = setTimeout(() => update(retry+1), 2500); // stop endlessly bugging the ESP if resource is not available\n      })\n    }\n    function S() { // Startup function (onload)\n      c = d.getElementById('canv');\n      ctx = c.getContext('2d');\n      if (window.location.href.indexOf(\"?ws\") == -1) {update(); return;}\n\n      // Initialize WebSocket connection\n      ws = connectWs(ws => ws.send('{\"lv\":true}'));\n      ws.addEventListener('message', (e) => {\n        try {\n          if (toString.call(e.data) === '[object ArrayBuffer]') {\n            let leds = new Uint8Array(e.data);\n            if (leds[0] != 76) return; //'L'\n            // leds[1] = 1: 1D; leds[1] = 2: 1D/2D (leds[2]=w, leds[3]=h)\n            draw(leds[1]==2 ? 4 : 2, 3, leds, (a,i) => `rgb(${a[i]},${a[i+1]},${a[i+2]})`);\n          }\n        } catch (err) {\n          console.error(\"Peek WS error:\",err);\n        } \n      });\n    }\n  </script>\n</head>\n<body>\n  <canvas id=\"canv\"></canvas>\n</body>\n</html>"
  },
  {
    "path": "wled00/data/liveviewws2D.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, minimum-scale=1\">\n\t<meta charset=\"utf-8\">\n\t<meta name=\"theme-color\" content=\"#222222\">\n\t<title>WLED Live Preview</title>\n\t<style>\n\tbody {\n\t\tmargin: 0;\n\t}\n\t</style>\n</head>\n<body>\n\t<canvas id=\"canv\"></canvas>\n\t<script>\n\t\t// load common.js with retry on error\n\t\t(function common() {\n\t\t\tconst l = document.createElement('script');\n\t\t\tl.src = 'common.js';\n\t\t\tl.onload = () => S();\n\t\t\tl.onerror = () => setTimeout(common, 100);\n\t\t\tdocument.head.appendChild(l);\n\t\t})();\n\t\tvar c = document.getElementById('canv');\n\t\tvar leds = \"\";\n\t\tvar throttled = false;\n\t\tfunction setCanvas() {\n\t\t\tc.width  = window.innerWidth * 0.98; //remove scroll bars\n\t\t\tc.height = window.innerHeight * 0.98; //remove scroll bars\n\t\t}\n\t\tfunction S() { // Startup function (onload)\n\t\t\tsetCanvas();\n\t\t\t// Check for canvas support\n\t\t\tvar ctx = c.getContext('2d');\n\t\t\tif (ctx) { // Access the rendering context\n\t\t\t\tws = connectWs(ws => ws.send('{\"lv\":true}')); // use parent WS or open new\n\t\t\t\tws.addEventListener('message',(e)=>{\n\t\t\t\t\ttry {\n\t\t\t\t\t\tif (toString.call(e.data) === '[object ArrayBuffer]') {\n\t\t\t\t\t\t\tlet leds = new Uint8Array(e.data);\n\t\t\t\t\t\t\tif (leds[0] != 76 || leds[1] != 2 || !ctx) return; //'L', set in ws.cpp\n\t\t\t\t\t\t\tlet mW = leds[2]; // matrix width\n\t\t\t\t\t\t\tlet mH = leds[3]; // matrix height\n\t\t\t\t\t\t\tlet pPL = Math.min(c.width / mW, c.height / mH); // pixels per LED (width of circle)\n\t\t\t\t\t\t\tlet lOf = Math.floor((c.width - pPL*mW)/2); //left offset (to center matrix)\n\t\t\t\t\t\t\tvar i = 4;\n\t\t\t\t\t\t\tfor (y=0.5;y<mH;y++) for (x=0.5; x<mW; x++) {\n\t\t\t\t\t\t\t\tctx.fillStyle = `rgb(${leds[i]},${leds[i+1]},${leds[i+2]})`;\n\t\t\t\t\t\t\t\tctx.beginPath();\n\t\t\t\t\t\t\t\tctx.arc(x*pPL+lOf, y*pPL, pPL*0.4, 0, 2 * Math.PI);\n\t\t\t\t\t\t\t\tctx.fill();\n\t\t\t\t\t\t\t\ti+=3;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tconsole.error(\"Peek WS error:\",err);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\t// window.resize event listener\n\t\twindow.addEventListener('resize', (e)=>{\n\t\t\tif (!throttled) {     // only run if we're not throttled\n\t\t\t\tsetCanvas();      // actual callback action\n\t\t\t\tthrottled = true; // we're throttled!\n\t\t\t\tsetTimeout(()=>{  // set a timeout to un-throttle\n\t\t\t\t\tthrottled = false;\n\t\t\t\t}, 250);\n\t\t\t}\n\t\t});\n\t</script>\n</body>\n</html>"
  },
  {
    "path": "wled00/data/msg.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n\t<meta content='width=device-width' name='viewport'>\n\t<title>WLED Message</title>\n\t<script>\n\t\tfunction B() { window.history.back() };\n\t\tfunction RS() { window.location = \"../settings\"; }\n\t\tfunction RP() { top.location.href = \"../\"; }\n\t</script>\n\t<style>\n\t\t@import url(\"style.css\");\n\t</style>\n</head>\n\n<body>\n\t<h2>Sample Message.</h2>\n\tSample Detail.\n</body>\n\n</html>"
  },
  {
    "path": "wled00/data/pixart/boxdraw.js",
    "content": "function drawBoxes(inputPixelArray, widthPixels, heightPixels) {\n \n    var w = window;\n\n    // Get the canvas context\n    var ctx = canvas.getContext('2d', { willReadFrequently: true });\n\n    // Set the width and height of the canvas\n    if (w.innerHeight < w.innerWidth) {\n        canvas.width = Math.floor(w.innerHeight * 0.98);\n    }\n    else{\n        canvas.width = Math.floor(w.innerWidth * 0.98);\n    }\n    //canvas.height = w.innerWidth;\n\n    let pixelSize = Math.floor(canvas.width/widthPixels);\n\n    let xOffset = (w.innerWidth - (widthPixels * pixelSize))/2\n\n    //Set the canvas height to fit the right number of pixelrows\n    canvas.height = (pixelSize * heightPixels) + 10\n    \n    //Iterate through the matrix\n    for (let y = 0; y < heightPixels; y++) {\n        for (let x = 0; x < widthPixels; x++) {\n\n            // Calculate the index of the current pixel\n            let i = (y*widthPixels) + x;\n            \n            //Gets the RGB of the current pixel\n            let pixel = inputPixelArray[i];\n\n            let pixelColor = 'rgb(' + pixel[0] + ', ' + pixel[1] + ', ' + pixel[2] + ')';\n\n            let textColor = 'rgb(128,128,128)';\n\n            // Set the fill style to the pixel color\n            ctx.fillStyle = pixelColor;\n\n            //Draw the rectangle\n            ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);\n\n            // Draw a border on the box\n            ctx.strokeStyle = '#888888';\n            ctx.lineWidth = 1;\n            ctx.strokeRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);\n\n            //Write text to box\n            ctx.font = \"10px Arial\";\n            ctx.fillStyle = textColor;\n            ctx.textAlign = \"center\";\n            ctx.textBaseline = 'middle';\n            ctx.fillText((pixel[4] + 1), (x * pixelSize) + (pixelSize /2), (y * pixelSize) + (pixelSize /2));\n        }\n    }\n    var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);\n    ctx.clearRect(0, 0, canvas.width, canvas.height);\n    canvas.width = w.innerWidth;\n    ctx.putImageData(imageData, xOffset, 0);\n}\n\n"
  },
  {
    "path": "wled00/data/pixart/getPixelValues.js",
    "content": "function getPixelRGBValues(base64Image) {\n  httpArray = [];\n  fileJSON = `{\"on\":true,\"bri\":${brgh.value},\"seg\":{\"id\":${tSg.value},\"i\":[`;\n\n  //Which object holds the secret to the segment ID\n\n  let segID = 0;\n  if(tSg.style.display == \"flex\"){\n    segID = tSg.value\n  } else {\n    segID = sID.value;\n  }\n  \n\n  //const copyJSONledbutton = gId('copyJSONledbutton');\n  const maxNoOfColorsInCommandSting = parseInt(cLN.value);\n  \n  let hybridAddressing = false;\n  let selectedIndex = -1;\n\n  selectedIndex = frm.selectedIndex;\n  const formatSelection = frm.options[selectedIndex].value;\n\n  \n  selectedIndex = lSS.selectedIndex;\n  const ledSetupSelection = lSS.options[selectedIndex].value;\n\n  selectedIndex = cFS.selectedIndex;\n  let hexValueCheck = true;\n  if (cFS.options[selectedIndex].value == 'dec'){\n    hexValueCheck = false\n  }\n\n  selectedIndex = aS.selectedIndex;\n  let segmentValueCheck = true; //If Range or Hybrid\n  if (aS.options[selectedIndex].value == 'single'){\n    segmentValueCheck = false\n  } else if (aS.options[selectedIndex].value == 'hybrid'){\n    hybridAddressing = true;\n  }\n\n  let curlString = ''\n  let haString = ''\n\n  let colorSeparatorStart = '\"';\n  let colorSeparatorEnd = '\"';\n  if (!hexValueCheck){\n    colorSeparatorStart = '[';\n    colorSeparatorEnd = ']';\n  }\n  // Warnings\n  let hasTransparency = false; //If alpha < 255 is detected on any pixel, this is set to true in code below\n  let imageInfo = '';\n  \n  // Create an off-screen canvas\n  var canvas = cE('canvas');\n  var context = canvas.getContext('2d', { willReadFrequently: true });\n\n  // Create an image element and set its src to the base64 image\n  var image = new Image();\n  image.src = base64Image;\n\n  // Wait for the image to load before drawing it onto the canvas\n  image.onload = function() {\n    \n    let scalePath = scDiv.children[0].children[0];\n    let color = scalePath.getAttribute(\"fill\");\n    let sizeX = szX.value;\n    let sizeY = szY.value;\n\n    if (color != accentColor || sizeX < 1 || sizeY < 1){\n      //image will not be resized Set desired size to original size\n      sizeX = image.width;\n      sizeY = image.height;\n      //failsafe for not generating huge images automatically\n      if (image.width > 512 || image.height > 512)\n      {\n        sizeX = 16;\n        sizeY = 16;\n      }\n    }\n\n    // Set the canvas size to the same as the desired image size\n    canvas.width = sizeX;\n    canvas.height = sizeY;\n\n    imageInfo = '<p>Width: ' + sizeX + ', Height: ' + sizeY + ' (make sure this matches your led matrix setup)</p>'\n\n    // Draw the image onto the canvas\n    context.drawImage(image, 0, 0, sizeX, sizeY);\n\n    // Get the pixel data from the canvas\n    var pixelData = context.getImageData(0, 0, sizeX, sizeY).data;\n  \n    // Create an array to hold the RGB values of each pixel\n    var pixelRGBValues = [];\n\n    // If the first row of the led matrix is right -> left\n    let right2leftAdjust = 1;\n          \n    if (ledSetupSelection == 'l2r'){\n      right2leftAdjust = 0;\n    }\n\n    // Loop through the pixel data and get the RGB values of each pixel\n    for (var i = 0; i < pixelData.length; i += 4) {\n      var r = pixelData[i];\n      var g = pixelData[i + 1];\n      var b = pixelData[i + 2];\n      var a = pixelData[i + 3];\n\n      let pixel = i/4\n      let row = Math.floor(pixel/sizeX);\n      let led = pixel;\n      if (ledSetupSelection == 'matrix'){\n          //Do nothing, the matrix is set upp like the index in the image\n          //Every row starts from the left, i.e. no zigzagging\n      }\n      else if ((row + right2leftAdjust) % 2 === 0) {\n          //Setup is traditional zigzag\n          //right2leftAdjust basically flips the row order if = 1\n          //Row is left to right\n          //Leave led index as pixel index\n        \n      } else {\n          //Setup is traditional zigzag\n          //Row is right to left\n          //Invert index of row for led\n          let indexOnRow = led - (row * sizeX);\n          let maxIndexOnRow = sizeX - 1;\n          let reversedIndexOnRow = maxIndexOnRow - indexOnRow;\n          led = (row * sizeX) + reversedIndexOnRow;\n      }\n\n      // Add the RGB values to the pixel RGB values array\n      pixelRGBValues.push([r, g, b, a, led, pixel, row]);\n    }\n    \n    pixelRGBValues.sort((a, b) => a[5] - b[5]);\n\n    //Copy the values to a new array for resorting\n    let ledRGBValues = [... pixelRGBValues];\n    \n    //Sort the array based on led index\n    ledRGBValues.sort((a, b) => a[4] - b[4]);\n    \n    //Generate JSON in WLED format\n    let JSONledString = '';\n\n    //Set starting values for the segment check to something that is no color\n    let segmentStart = -1;\n    let maxi = ledRGBValues.length;\n    let curentColorIndex = 0\n    let commandArray = [];\n\n    //For every pixel in the LED array\n    for (let i = 0; i < maxi; i++) {\n      let pixel = ledRGBValues[i];\n      let r = pixel[0];\n      let g = pixel[1];\n      let b = pixel[2];\n      let a = pixel[3];\n      let segmentString = '';\n      let segmentEnd = -1;\n\n      if(segmentValueCheck){\n        if (segmentStart < 0){\n          //This is the first led of a new segment\n          segmentStart = i;\n        } //Else we allready have a start index\n        \n        if (i < maxi - 1){ \n          \n          let iNext = i + 1;\n          let nextPixel = ledRGBValues[iNext];\n\n          if (nextPixel[0] != r || nextPixel[1] != g || nextPixel[2] != b ){\n            //Next pixel has new color\n            //The current segment ends with this pixel\n            segmentEnd = i + 1 //WLED wants the NEXT LED as the stop led...\n            if (segmentStart == i && hybridAddressing){\n              //If only one led/pixel, no segment info needed\n              if (JSONledString == ''){\n                //If addressing is single, we need to start every command with a starting possition\n                segmentString = '' + i + ',';\n                //Fixed to b2\n              } else{\n                segmentString = ''\n              }\n            }\n            else {\n              segmentString = segmentStart + ',' + segmentEnd + ',';\n            }\n          }\n\n        } else {\n          //This is the last pixel, so the segment must end\n          segmentEnd = i + 1;\n\n          if (segmentStart + 1 == segmentEnd && hybridAddressing){\n            //If only one led/pixel, no segment info needed\n            if (JSONledString == ''){\n              //If addressing is single, we need to start every command with a starting possition\n              segmentString = '' + i + ',';\n              //Fixed to b2\n            } else{\n              segmentString = ''\n            }\n          }\n          else {\n            segmentString = segmentStart + ',' + segmentEnd + ','; \n          }\n        }\n      } else{\n        //Write every pixel\n        if (JSONledString == ''){\n          //If addressing is single, we need to start every command with a starting possition\n          JSONledString = i\n          //Fixed to b2\n        }\n\n        segmentStart = i\n        segmentEnd = i   \n        //Segment string should be empty for when addressing single. So no need to set it again.       \n      }\n\n      if (a < 255){\n        hasTransparency = true; //If ANY pixel has alpha < 255 then this is set to true to warn the user\n      }\n\n      if (segmentEnd > -1){\n        //This is the last pixel in the segment, write to the JSONledString\n        //Return color value in selected format\n        let colorValueString = r + ',' + g + ',' + b ;\n\n        if (hexValueCheck){\n          const [red, green, blue] = [r, g, b];\n          colorValueString = `${[red, green, blue].map(x => x.toString(16).padStart(2, '0')).join('')}`;\n        } else{\n          //do nothing, allready set\n        }\n\n        // Check if start and end is the same, in which case remove\n\n        JSONledString += segmentString + colorSeparatorStart + colorValueString + colorSeparatorEnd;\n        fileJSON = JSONledString + segmentString + colorSeparatorStart + colorValueString + colorSeparatorEnd;\n\n        curentColorIndex = curentColorIndex + 1; // We've just added a new color to the string so up the count with one\n\n        if (curentColorIndex % maxNoOfColorsInCommandSting === 0 || i == maxi - 1) { \n\n          //If we have accumulated the max number of colors to send in a single command or if this is the last pixel, we should write the current colorstring to the array\n          commandArray.push(JSONledString);\n          JSONledString = ''; //Start on an new command string\n        } else\n        {\n          //Add a comma to continue the command string\n          JSONledString = JSONledString + ','\n        }\n        //Reset segment values\n        segmentStart = - 1;\n      }\n    }\n    \n    JSONledString = ''\n\n    //For every commandString in the array\n    for (let i = 0; i < commandArray.length; i++) {\n      let thisJSONledString = `{\"on\":true,\"bri\":${brgh.value},\"seg\":{\"id\":${segID},\"i\":[${commandArray[i]}]}}`;\n      httpArray.push(thisJSONledString);\n\n      let thiscurlString = `curl -X POST \"http://${gurl.value}/json/state\" -d \\'${thisJSONledString}\\' -H \"Content-Type: application/json\"`;\n      \n      //Aggregated Strings That should be returned to the user\n      if (i > 0){\n        JSONledString = JSONledString + '\\n<NEXT COMMAND (multiple commands not supported in API/preset setup)>\\n';\n        curlString = curlString + ' && ';\n      }\n      JSONledString += thisJSONledString;\n      curlString += thiscurlString;\n    }\n\n    \n    haString = `#Uncomment if you don\\'t allready have these defined in your switch section of your configuration.yaml\n#- platform: command_line\n  #switches:\n    ${haIDe.value}\n      friendly_name: ${haNe.value}\n      unique_id: ${haUe.value}\n      command_on: >\n        ${curlString}\n      command_off: >\n        curl -X POST \"http://${gurl.value}/json/state\" -d \\'{\"on\":false}\\' -H \"Content-Type: application/json\"`;\n\n    if (formatSelection == 'wled'){\n      JLD.value = JSONledString;\n    } else if (formatSelection == 'curl'){\n      JLD.value = curlString;\n    } else if (formatSelection == 'ha'){\n      JLD.value = haString;\n    } else {\n      JLD.value = 'ERROR!/n' + formatSelection + ' is an unknown format.'\n    }\n    \n    fileJSON += ']}}';\n\n    let infoDiv = imin;\n    let canvasDiv = imin;\n    if (hasTransparency){\n      imageInfo = imageInfo + '<p><b>WARNING!</b> Transparency info detected in image. Transparency (alpha) has been ignored. To ensure you get the result you desire, use only solid colors in your image.</p>'\n    }\n    \n    infoDiv.innerHTML = imageInfo;\n    canvasDiv.style.display = \"block\"\n\n\n    //Drawing the image\n    drawBoxes(pixelRGBValues, sizeX, sizeY);\n  }\n}"
  },
  {
    "path": "wled00/data/pixart/pixart.css",
    "content": "\n.box {\n  border: 2px solid #fff;\n}\nbody {\n  font-family: Arial, sans-serif;\n  background-color: #111;\n}\n\n.top-part {\n  width: 600px;\n  margin: 0 auto;\n}\n.container {\n  max-width: 100% -40px;\n  border-radius: 0px;\n  padding: 20px;\n  text-align: center;\n}\nh1 {\n  font-size: 2.3em;\n  color: #ddd;\n  margin: 1px 0;\n  font-family: Arial, sans-serif;\n  line-height: 0.5;\n  /*text-align: center;*/\n}\nh2 {\n  font-size: 1.1em;\n  color: rgba(221, 221, 221, 0.61);\n  margin: 1px 0;\n  font-family: Arial, sans-serif;\n  line-height: 0.5;\n  text-align: center;\n}\nh3 {\n  font-size: 0.7em;\n  color: rgba(221, 221, 221, 0.61);\n  margin: 1px 0;\n  font-family: Arial, sans-serif;\n  line-height: 1.4;\n  text-align: center;\n  align-items: center;\n  justify-content: center;\n  display: flex;\n}\n\np {\n  font-size: 1em;\n  color: #777;\n  line-height: 1.5;\n  font-family: Arial, sans-serif;\n}\n\n#fieldTable {\n  font-size: 1  em;\n  color: #777;\n  line-height: 1;\n  font-family: Arial, sans-serif;\n}\n\n#scaleTable {\n  font-size: 1  em;\n  color: #777;\n  line-height: 1;\n  font-family: Arial, sans-serif;\n}\n\n#drop-zone {\n  display: block;\n  width: 100%-40px;\n  border: 3px dashed #ddd;\n  border-radius: 0px;\n  text-align: center;\n  padding: 20px;\n  margin: 0px;\n  cursor: pointer;\n  font-family: Arial, sans-serif;\n  font-size: 15px;\n  color: #777;\n}\n\n#file-picker {\n  display: none;\n}\n.adaptiveTD{\n  display: flex;\n  flex-direction: row;\n  flex-wrap: nowrap;\n  align-items: center;\n\n}\n\n.mainSelector {\n  background-color: #222;\n  color: #ddd;\n  border: 1px solid #333;\n  margin-top: 4px;\n  margin-bottom: 4px;\n  padding: 0 8px;\n  height: 28px;\n  font-size: 15px;\n  border-radius: 7px;\n  flex-grow: 1;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.adaptiveSelector {\n  background-color: #222;\n  color: #ddd;\n  border: 1px solid #333;\n  margin-top: 4px;\n  margin-bottom: 4px;\n  padding: 0 8px;\n  height: 28px;\n  font-size: 15px;\n  border-radius: 7px;\n  flex-grow: 1;\n  display: none;\n}\n\n.segmentsDiv{\n  width: 36px;\n  padding-left: 5px;\n}\n\n* input[type=range] {\n\tappearance: none;\n  -moz-appearance: none;\n  -webkit-appearance: none;\n\tflex-grow: 1;\n\tpadding: 0;\n\tmargin: 4px 8px 4px 0;\n\tbackground-color: transparent;\n\tcursor: pointer;\n  background: linear-gradient(to right, #bbb 50%, #333 50%);\n  border-radius: 7px;\n}\n\ninput[type=range]:focus {\n\toutline: none;\n}\ninput[type=range]::-webkit-slider-runnable-track {\n\theight: 28px;\n\tcursor: pointer;\n\tbackground: transparent;\n  border-radius: 7px;\n}\ninput[type=range]::-webkit-slider-thumb {\n\theight: 16px;\n\twidth: 16px;\n\tborder-radius: 50%;\n\tbackground: #fff;\n\tcursor: pointer;\n\t-webkit-appearance: none;\n\tmargin-top: 4px;\n  border-radius: 7px;\n}\ninput[type=range]::-moz-range-track {\n\theight: 28px;\n\tbackground-color: rgba(0, 0, 0, 0);\n  border-radius: 7px;\n}\ninput[type=range]::-moz-range-thumb {\n\tborder: 0px solid rgba(0, 0, 0, 0);\n\theight: 16px;\n\twidth: 16px;\n\tborder-radius: 7px;\n\tbackground: #fff;\n}\n\n.rangeNumber{\n  width: 20px;\n  vertical-align: middle;\n}\n\n.fullTextField[type=text] {\n  background-color: #222;\n  border: 1px solid #333;\n  padding-inline-start: 5px;\n  margin-top: 4px;\n  margin-bottom: 4px;\n  height: 24px;\n  border-radius: 0px;\n  font-family: Arial, sans-serif;\n  font-size: 15px;\n  color: #ddd;\n  border-radius: 7px;\n  flex-grow: 1;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n.flxTFld{\n  background-color: #222;\n  border: 1px solid #333;\n  padding-inline-start: 5px;\n  height: 24px;\n  border-radius: 0px;\n  font-family: Arial, sans-serif;\n  font-size: 15px;\n  color: #ddd;\n  border-radius: 7px;\n  flex-grow: 1;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n* input[type=submit] {\n  background-color: #222;\n  border: 1px solid #333;\n  padding: 0.5em;\n  width: 100%;\n  border-radius: 24px;\n  font-family: Arial, sans-serif;\n  font-size: 1.3em;\n  color: #ddd;\n}\n\n* button {\n  background-color: #222;\n  border: 1px solid #333;\n  padding-inline: 5px;\n  width: 100%;\n  border-radius: 24px;\n  font-family: Arial, sans-serif;\n  font-size: 1em;\n  color: #ddd;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  cursor: pointer;\n}\n\n#scaleDiv {\n  display: flex;\n  align-items: center;\n  vertical-align: middle;\n}\n\ntextarea {\n  grid-row: 1 / 2;\n  width: 100%;\n  height: 200px;\n  background-color: #222;\n  border: 1px solid #333;\n  color: #ddd;\n}\n.hide {\n    display: none;\n}\n\n.svg-icon {\n  vertical-align: middle;\n}\n#image-container {\n  display: grid;\n  grid-template-rows: 1fr 1fr;\n}\n#button-container {\n  display: flex;\n  padding-bottom: 10px;\n  padding-top: 10px;\n}\n\n.buttonclass {\n  flex: 1;\n  padding-top: 5px;\n  padding-bottom: 5px;\n}\n\n.gap {\n  width: 10px;\n}\n\n#submitConvert::before {\n  content: \"\";\n  display: inline-block;\n  background-image: url('data:image/svg+xml;utf8, <svg style=\"width:24px;height:24px\" viewBox=\"0 0 24 24\" <path fill=\"currentColor\" d=\"M12,6V9L16,5L12,1V4A8,8 0 0,0 4,12C4,13.57 4.46,15.03 5.24,16.26L6.7,14.8C6.25,13.97 6,13 6,12A6,6 0 0,1 12,6M18.76,7.74L17.3,9.2C17.74,10.04 18,11 18,12A6,6 0 0,1 12,18V15L8,19L12,23V20A8,8 0 0,0 20,12C20,10.43 19.54,8.97 18.76,7.74Z\" /></svg>');\n  width: 36px;\n  height: 36px;\n}\n\n#sizeDiv * {\n  display: inline-block;\n}\n.sizeInputFields{\n  width: 50px;\n  background-color: #222;\n  border: 1px solid #333;\n  padding-inline-start: 5px;\n  margin-top: -5px;\n  height: 24px;\n  border-radius: 7px;\n  font-family: Arial, sans-serif;\n  font-size: 15px;\n  color: #ddd;\n}\na:link {\n  color: rgba(221, 221, 221, 0.61);\n  background-color: transparent;\n  text-decoration: none;\n}\n\na:visited {\n  color: rgba(221, 221, 221, 0.61);\n  background-color: transparent;\n  text-decoration: none;\n}\n\na:hover {\n  color: #ddd;\n  background-color: transparent;\n  text-decoration: none;\n}\n\na:active {\n  color: rgba(221, 221, 221, 0.61);\n  background-color: transparent;\n  text-decoration: none;\n}"
  },
  {
    "path": "wled00/data/pixart/pixart.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta http-equiv=\"Cache-Control\" content=\"no-cache, no-store, must-revalidate\">\n    <meta http-equiv=\"Pragma\" content=\"no-cache\">\n    <meta http-equiv=\"Expires\" content=\"0\">\n    <title>WLED Pixel Art Converter</title>\n    <link rel=\"stylesheet\" href=\"pixart.css\">\n    <link rel=\"shortcut icon\" href=\"favicon-16x16.png\">\n    <script type=\"text/javascript\">\n      var d = document;\n      function gId(e) {return d.getElementById(e);}\n      function cE(e) {return d.createElement(e);}\n    </script>\n  </head>\n  <body>\n    <body>\n      <div class=\"top-part\" >\n        <div style=\"display: flex; justify-content: center;\">\n          <h1 style=\"display: flex; align-items: center;\">\n            <svg style=\"width:36px;height:36px;margin-right:6px;\" viewBox=\"0 0 32 32\">\n              <rect style=\"fill:#003FFF\" x=\"6\" y=\"22\" width=\"8\" height=\"4\"/>\n              <rect style=\"fill:#003FFF\" x=\"14\" y=\"14\" width=\"4\" height=\"8\"/>\n              <rect style=\"fill:#003FFF\" x=\"18\" y=\"10\" width=\"4\" height=\"8\"/>\n              <rect style=\"fill:#003FFF\" x=\"22\" y=\"6\" width=\"8\" height=\"4\"/>\n            </svg>\n            WLED Pixel Art Converter\n          </h1>\n        </div>\n        <h2>Convert image to WLED JSON (pixel art on WLED matrix)</h2>\n        <p>\n          <table id=\"fieldTable\"  style=\"width: 100%; table-layout: fixed; align-content: center;\">\n            <tr>\n              <td style=\"vertical-align: middle;\">\n                <label for=\"ledSetupSelector\">Led setup:</label>\n              </td>\n              <td class=\"adaptiveTD\">\n                <select id=\"ledSetupSelector\" class=\"mainSelector\">\n                  <option value=\"matrix\" selected>2D Matrix</option>\n                  <option value=\"r2l\">Serpentine, first row right to left &lt;-</option>\n                  <option value=\"l2r\">Serpentine, first row left to right -&gt;</option>\n                </select>\n              </td>\n            </tr>        \n            <tr>\n              <td style=\"vertical-align: middle;\">\n                <label for=\"formatSelector\">Output format:</label>\n              </td>\n              <td class=\"adaptiveTD\">\n                <select id=\"formatSelector\" class=\"mainSelector\">\n                  <option value=\"wled\" selected>WLED JSON</option>\n                  <option value=\"curl\">CURL</option>\n                  <option value=\"ha\">Home Assistant YAML</option>\n                </select>\n              </td>\n            </tr>        \n            <tr>\n              <td style=\"vertical-align: middle;\">\n                <label for=\"colorFormatSelector\">Color code format:</label>\n              </td>\n              <td class=\"adaptiveTD\">\n                <select id=\"colorFormatSelector\" class=\"mainSelector\">\n                  <option value=\"hex\" selected>HEX (&quot;f4f4f4&quot;)</option>\n                  <option value=\"dec\">DEC (244,244,244)</option>\n                </select>\n            </td>\n            </tr>\n            <tr>\n              <td style=\"vertical-align: middle;\">\n                <label for=\"addressingSelector\">Addressing:</label>\n              </td>\n              <td class=\"adaptiveTD\">\n                <select id=\"addressingSelector\" class=\"mainSelector\">\n                  <option value=\"hybrid\" selected>Hybrid (&quot;f0f0f0&quot;,10, 17, &quot;f4f4f4&quot;)</option>\n                  <option value=\"range\">Range (10, 17, &quot;f4f4f4&quot;)</option>\n                  <option value=\"single\">Single (&quot;f4f4f4&quot;)</option>\n                </select>\n              </td>\n            </tr>\n            <tr>\n              <td style=\"vertical-align: middle;\">\n                <label for=\"brightnessNumber\">Brightness:</label>\n              </td>\n              <td style=\"vertical-align: middle; display: flex; align-items: center;\">\n                <input type=\"range\" id=\"brightnessNumber\" min=\"1\" max=\"255\" value=\"128\">\n                <span id=\"brightnessValue\">128</span>\n              </td>\n            </tr>\n            <tr>\n              <td style=\"vertical-align: middle;\">\n                <label for=\"colorLimitNumber\">Max no of colors/JSON:</label>\n              </td>\n              <td style=\"vertical-align: middle; display: flex; align-items: center;\">\n                <input type=\"range\" id=\"colorLimitNumber\" min=\"1\" max=\"512\" value=\"256\">\n                <span id=\"colorLimitValue\" >256</span>\n              </td>\n            </tr>\n            <tr class=\"ha-hide\">\n              <td style=\"vertical-align: middle;\">\n                <label for=\"haID\">HA Device ID:</label>\n              </td>\n              <td class=\"adaptiveTD\">\n                <input class=\"fullTextField\" type=\"text\" id=\"haID\" value=\"pixel_art_controller_001\">\n              </td>\n            </tr>\n            <tr class=\"ha-hide\">\n              <td style=\"vertical-align: middle;\">\n                <label for=\"haUID\">HA Device Unique ID:</label>\n              </td>\n              <td class=\"adaptiveTD\">\n                <input class=\"fullTextField\" type=\"text\" id=\"haUID\" value=\"pixel_art_controller_001a\">\n              </td>\n            </tr>\n            <tr class=\"ha-hide\">\n              <td style=\"vertical-align: middle;\">\n                <label for=\"haName\">HA Device Name:</label>\n              </td>\n              <td class=\"adaptiveTD\">\n                <input class=\"fullTextField\" type=\"text\" id=\"haName\" value=\"Pixel Art Kitchen\">\n              </td>\n            </tr>\n            <tr>\n              <td style=\"vertical-align: middle;\">\n                <label for=\"curlUrl\">Device IP/host name:</label>\n              </td>\n              <td class=\"adaptiveTD\">\n                <input class=\"fullTextField\" type=\"text\" id=\"curlUrl\" value=\"\">\n              </td>\n            </tr>\n            <tr>\n              <td style=\"vertical-align: middle;\">\n                <label for=\"targetSegment\">Target segment id:</label>\n              </td>\n              <td class=\"adaptiveTD\">\n                <input class=\"flxTFld\" type=\"number\" id=\"segID\" value=\"0\" min=\"0\" max=\"63\">\n                <select id=\"targetSegment\" class=\"adaptiveSelector\">\n                </select>\n                <div id=\"getSegmentsDiv\" class=\"segmentsDiv\"></div>\n              </td>\n            </tr>\n          </table>\n          <table  class= \"scaleTableClass\" id=\"scaleTable\"  style=\"width: 100%; table-layout: fixed; align-content: center;\">\n            <tr>\n              <td style=\"vertical-align: middle;\">\n                <div id=\"scaleDiv\">\n                  <svg id=\"scaleToggle\" style=\"width:36px;height:36px; cursor: pointer;\" viewBox=\"0 0 24 24\" onclick=\"switchScale()\">\n                    <path id=\"scaleTogglePath\" fill=\"currentColor\" d=\"M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M7,15A3,3 0 0,1 4,12A3,3 0 0,1 7,9A3,3 0 0,1 10,12A3,3 0 0,1 7,15Z\" />\n                  </svg>\n                   &nbsp;Scale image\n                </div>\n              </td>\n              <td style=\"vertical-align: middle;\">\n                <div id=\"sizeDiv\" style=\"display: none;\">\n                  <label for=\"sizeX\">W : </label> &nbsp;<input class=\"sizeInputFields\" type=\"number\" id=\"sizeX\" min=\"1\" value=\"16\">\n                  &nbsp;&nbsp;&nbsp;\n                  <label for=\"sizeY\">H : </label> &nbsp;<input class=\"sizeInputFields\" type=\"number\" id=\"sizeY\" min=\"1\" value=\"16\">\n                </div>\n              </td>\n            </tr>\n          </table>\n        </p>\n  \n        <p>\n          <label for=\"file-picker\">\n            <div id=\"drop-zone\">    \n                Drop image here <br>or <br>\n                Click to select a file\n            </div>\n          </label>\n        </p>\n        \n        <p>\n          <input type=\"file\" id=\"file-picker\" style=\"display: none;\">\n          <div style=\"width: 100%; text-align: center;\">\n            <img id=\"preview\" style=\"display: none; margin: 0 auto;\">\n          </div>\n          <!--\n          <div id=\"submitConvertDiv\" style=\"display: none;\">\n            <button id=\"convertbutton\" class=\"buttonclass\"></button>\n          </div>\n          -->\n          <div id=\"raw-image-container\" style=\"display: none\">\n            <img id=\"image\" src=\"\" alt=\"RawImage image\">\n          </div>\n          \n        </p>\n        \n        <div id=\"image-container\" style=\"display: none;\">\n          <div id=\"image-info\" style=\"display: none\"></div>\n          <textarea id=\"JSONled\" readonly></textarea>\n        </div>\n  \n        <div id=\"button-container\" style=\"display: none;\">\n          <button id=\"copyJSONledbutton\" class=\"buttonclass\"></button>\n          <div id=\"gap1\" class=\"gap\"></div>\n          <button id=\"sendJSONledbutton\" class=\"buttonclass\"></button>\n        </div>\n        <div>\n          <h3><div id=\"version\">Version 1.0.8</div>&nbsp;-&nbsp; <a href=\"https://github.com/werkstrom/WLED-PixelArtConverter/blob/main/README.md\" target=\"_blank\">Help/About</a></h3>\n        </div>\n      </div>\n      <div id=bottom-part style=\"display: none\" class=bottom-part></div>\n            <canvas id=\"pixelCanvas\"></canvas>\n      </div>\n      <script src=\"statics.js\" type=\"text/javascript\"></script>\n      <script src=\"getPixelValues.js\" type=\"text/javascript\"></script>\n      <script src=\"boxdraw.js\" type=\"text/javascript\"></script>\n      <script src=\"pixart.js\" type=\"text/javascript\"></script>\n  </body>\n</html>"
  },
  {
    "path": "wled00/data/pixart/pixart.js",
    "content": "//Start up code\n//if (window.location.protocol == \"file:\") {\n//  let locip = prompt(\"File Mode. Please enter WLED IP!\");\n//  gId('curlUrl').value = locip;\n//} else\n//\n//Start up code\nlet devMode =  false; //Remove\ngurl.value = location.host;\n\nconst urlParams = new URLSearchParams(window.location.search);\nif (gurl.value.length < 1){\n  gurl.value = \"Missing_Host\";\n}\n\nfunction gen(){\n  //Generate image if enough info is in place\n  //Is host non empty\n  //Is image loaded\n  //is scale > 0\n  if (((szX.value > 0 && szY.value > 0) || szDiv.style.display == 'none') && gurl.value.length > 0 && prw.style.display != 'none'){\n    //regenerate\n    let base64Image = prw.src;\n    if (isValidBase64Gif(base64Image)) {\n      im.src = base64Image;\n      getPixelRGBValues(base64Image);\n      imcn.style.display = \"block\";\n      bcn.style.display = \"\";\n    } else {\n      let imageInfo = '<p><b>WARNING!</b> File does not appear to be a valid image</p>';\n      imin.innerHTML = imageInfo;\n      imin.style.display = \"block\";\n      imcn.style.display = \"none\";\n      JLD.value = '';\n      if (devMode) console.log(\"The string '\" + base64Image + \"' is not a valid base64 image.\");\n    }\n  }\n  \n  if(gurl.value.length > 0){\n    gId(\"sSg\").setAttribute(\"fill\", accentColor);\n  } else{\n    gId(\"sSg\").setAttribute(\"fill\", accentTextColor);\n    let ts = tSg;\n    ts.style.display = \"none\";\n    ts.innerHTML = \"\";\n    sID.style.display = \"flex\";\n  }\n}\n\n\n// Code for copying the generated string to clipboard\n\ncjb.addEventListener('click', async () => {\n  let JSONled = JLD;\n  JSONled.select();\n  try {\n    await navigator.clipboard.writeText(JSONled.value);\n  } catch (err) {\n    try {\n      await d.execCommand(\"copy\");\n    } catch (err) {\n      console.error('Failed to copy text: ', err);\n    }\n  }\n});\n\n// Event listeners =======================\n\nlSS.addEventListener(\"change\", gen);\nszY.addEventListener(\"change\", gen);\nszX.addEventListener(\"change\", gen);\ncFS.addEventListener(\"change\", gen);\naS.addEventListener(\"change\", gen);\nbrgh.addEventListener(\"change\", gen);\ncLN.addEventListener(\"change\", gen);\nhaIDe.addEventListener(\"change\", gen);\nhaUe.addEventListener(\"change\", gen);\nhaNe.addEventListener(\"change\", gen);\ngurl.addEventListener(\"change\", gen);\nsID.addEventListener(\"change\", gen);\nprw.addEventListener(\"load\", gen);\n//gId(\"convertbutton\").addEventListener(\"click\", gen);\n\ntSg.addEventListener(\"change\", () => {\n  sop = tSg.options[tSg.selectedIndex];\n  szX.value = sop.dataset.x;\n  szY.value = sop.dataset.y;\n  gen();\n});\n\ngId(\"sendJSONledbutton\").addEventListener('click', async () => {\n  if (window.location.protocol === \"https:\") {\n    alert('Will only be available when served over http (or WLED is run over https)');\n  } else {\n    postPixels();\n  }\n});\n\nbrgh.oninput = () => {\n  brgV.textContent = brgh.value;\n  let perc = parseInt(brgh.value)*100/255;\n  var val = `linear-gradient(90deg, #bbb ${perc}%, #333 ${perc}%)`;\n  brgh.style.backgroundImage = val;\n}\n\ncLN.oninput = () => {\n  let cln = cLN;\n  cLV.textContent = cln.value;\n  let perc = parseInt(cln.value)*100/512;\n  var val = `linear-gradient(90deg, #bbb ${perc}%, #333 ${perc}%)`;\n  cln.style.backgroundImage = val;\n}\n\nfrm.addEventListener(\"change\", () => {\n  for (var i = 0; i < hideableRows.length; i++) {\n    hideableRows[i].classList.toggle(\"hide\", frm.value !== \"ha\");\n    gen();\n  }\n});\n\nasync function postPixels() {\n  let ss = gId(\"sendSvgP\");\n  ss.setAttribute(\"fill\", prsCol);\n  let er = false;\n  for (let i of httpArray) {\n    try {\n      if (devMode) console.log(i);\n      if (devMode) console.log(i.length);\n      const response = await fetch('http://'+gId('curlUrl').value+'/json/state', {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json'\n          //'Content-Type': 'text/html; charset=UTF-8'\n        },\n        body: i\n      });\n      const data = await response.json();\n      if (devMode) console.log(data);\n    } catch (error) {\n      console.error(error);\n      er = true;\n    }\n  }\n  if(er){\n    //Something went wrong\n    ss.setAttribute(\"fill\", redColor);\n    setTimeout(function(){ \n      ss.setAttribute(\"fill\", accentTextColor);\n    }, 1000);\n  } else {\n    // A, OK\n    ss.setAttribute(\"fill\", greenColor);\n    setTimeout(function(){ \n      ss.setAttribute(\"fill\", accentColor);\n    }, 1000);\n  }\n}\n\n//File uploader code\nconst dropZone = gId('drop-zone');\nconst filePicker = gId('file-picker');\nconst preview = prw;\n\n// Listen for dragenter, dragover, and drop events\ndropZone.addEventListener('dragenter', dragEnter);\ndropZone.addEventListener('dragover', dragOver);\ndropZone.addEventListener('drop', dropped);\ndropZone.addEventListener('click', zoneClicked);\n\n// Listen for change event on file picker\nfilePicker.addEventListener('change', filePicked);\n\n// Handle zone click\nfunction zoneClicked(e) {\n  e.preventDefault();\n  //this.classList.add('drag-over');\n  //alert('Hej');\n  filePicker.click();\n}\n\n// Handle dragenter\nfunction dragEnter(e) {\n  e.preventDefault();\n  this.classList.add('drag-over');\n}\n\n// Handle dragover\nfunction dragOver(e) {\n  e.preventDefault();\n}\n\n// Handle drop\nfunction dropped(e) {\n  e.preventDefault();\n  this.classList.remove('drag-over');\n\n  // Get the dropped file\n  const file = e.dataTransfer.files[0];\n  updatePreview(file)\n}\n\n// Handle file picked\nfunction filePicked(e) {\n  // Get the picked file\n  const file = e.target.files[0];\n  updatePreview(file)\n}\n\n// Update the preview image\nfunction updatePreview(file) {\n  // Use FileReader to read the file\n  const reader = new FileReader();\n  reader.onload = () => {\n    // Update the preview image\n    preview.src = reader.result;\n    //gId(\"submitConvertDiv\").style.display = \"\";\n    prw.style.display = \"\";\n  };\n  reader.readAsDataURL(file);\n}\n\nfunction isValidBase64Gif(string) {\n  // Use a regular expression to check that the string is a valid base64 string\n  /*\n  const base64gifPattern = /^data:image\\/gif;base64,([A-Za-z0-9+/:]{4})*([A-Za-z0-9+/:]{3}=|[A-Za-z0-9+/:]{2}==)?$/;\n  const base64pngPattern = /^data:image\\/png;base64,([A-Za-z0-9+/:]{4})*([A-Za-z0-9+/:]{3}=|[A-Za-z0-9+/:]{2}==)?$/;\n  const base64jpgPattern = /^data:image\\/jpg;base64,([A-Za-z0-9+/:]{4})*([A-Za-z0-9+/:]{3}=|[A-Za-z0-9+/:]{2}==)?$/;\n  const base64webpPattern = /^data:image\\/webp;base64,([A-Za-z0-9+/:]{4})*([A-Za-z0-9+/:]{3}=|[A-Za-z0-9+/:]{2}==)?$/;\n  */\n  //REMOVED, Any image appear to work as long as it can be drawn to the canvas. Leaving code in for future use, possibly\n  if (1==1 || base64gifPattern.test(string) || base64pngPattern.test(string) || base64jpgPattern.test(string) || base64webpPattern.test(string)) {\n    return true;\n  } else {\n    //Not OK\n    return false;\n  }\n}\n\nvar hideableRows = d.querySelectorAll(\".ha-hide\");\nfor (var i = 0; i < hideableRows.length; i++) {\n  hideableRows[i].classList.add(\"hide\");\n}\nfrm.addEventListener(\"change\", () => {\n  for (var i = 0; i < hideableRows.length; i++) {\n    hideableRows[i].classList.toggle(\"hide\", frm.value !== \"ha\");\n  }\n});\n\nfunction switchScale() {\n  //let scalePath = gId(\"scaleDiv\").children[1].children[0]\n  let scaleTogglePath = scDiv.children[0].children[0]\n  let color = scaleTogglePath.getAttribute(\"fill\");\n  let d = '';\n  if (color === accentColor) {\n    color = accentTextColor;\n    d = scaleToggleOffd;\n    szDiv.style.display = \"none\";\n    // Set values to actual XY of image, if possible\n  } else {\n    color = accentColor;\n    d = scaleToggleOnd;\n    szDiv.style.display = \"\";\n  }\n  //scalePath.setAttribute(\"fill\", color);\n  scaleTogglePath.setAttribute(\"fill\", color);\n  scaleTogglePath.setAttribute(\"d\", d);\n  gen();\n}\n\nfunction generateSegmentOptions(array) {\n  //This function is prepared for a name property on each segment for easier selection\n  //Currently the name is generated generically based on index\n  tSg.innerHTML = \"\";\n  for (var i = 0; i < array.length; i++) {\n    var option = cE(\"option\");\n    option.value = array[i].value;\n    option.text = array[i].text;\n    option.dataset.x = array[i].x;\n    option.dataset.y = array[i].y;\n    tSg.appendChild(option);\n    if(i === 0) {\n      option.selected = true;\n      szX.value = option.dataset.x;\n      szY.value = option.dataset.y;\n    }\n  }\n}\n\n// Get segments from device\nasync function getSegments() {\n  cv = gurl.value;\n  if (cv.length > 0 ){\n    try {\n      var arr = [];\n      const response = await fetch('http://'+cv+'/json/state');\n      const json = await response.json();\n      let ids = json.seg.map(sg => ({id: sg.id, n: sg.n, xs: sg.start, xe: sg.stop, ys: sg.startY, ye: sg.stopY}));\n      for (var i = 0; i < ids.length; i++) {\n        arr.push({\n            value: ids[i][\"id\"],\n            text: ids[i][\"n\"] + ' (index: ' + ids[i][\"id\"] + ')',\n            x: ids[i][\"xe\"] - ids[i][\"xs\"],\n            y: ids[i][\"ye\"] - ids[i][\"ys\"]\n        });\n      }\n      generateSegmentOptions(arr);\n      tSg.style.display = \"flex\";\n      sID.style.display = \"none\";\n      gId(\"sSg\").setAttribute(\"fill\", greenColor);\n      setTimeout(function(){ \n        gId(\"sSg\").setAttribute(\"fill\", accentColor);\n      }, 1000);\n\n    } catch (error) {\n      console.error(error);\n      gId(\"sSg\").setAttribute(\"fill\", redColor);\n      setTimeout(function(){ \n        gId(\"sSg\").setAttribute(\"fill\", accentColor);\n      }, 1000);\n      tSg.style.display = \"none\";\n      sID.style.display = \"flex\";\n    }\n  } else{\n    gId(\"sSg\").setAttribute(\"fill\", redColor);\n    setTimeout(function(){ \n      gId(\"sSg\").setAttribute(\"fill\", accentTextColor);\n    }, 1000);\n    tSg.style.display = \"none\";\n    sID.style.display = \"flex\";\n  }\n}\n\n//Initial population of segment selection\nfunction generateSegmentArray(noOfSegments) {\n  var arr = [];\n  for (var i = 0; i < noOfSegments; i++) {\n    arr.push({\n      value: i,\n      text: \"Segment index \" + i\n    });\n  }\n  return arr;\n}\n\nvar segmentData = generateSegmentArray(10);\n\ngenerateSegmentOptions(segmentData);\n\nseDiv.innerHTML =\n'<svg id=getSegmentsSVG style=\"width:36px;height:36px;cursor:pointer\" viewBox=\"0 0 24 24\" onclick=\"getSegments()\"><path id=sSg fill=\"currentColor\" d=\"M6.5 20Q4.22 20 2.61 18.43 1 16.85 1 14.58 1 12.63 2.17 11.1 3.35 9.57 5.25 9.15 5.68 7.35 7.38 5.73 9.07 4.1 11 4.1 11.83 4.1 12.41 4.69 13 5.28 13 6.1V12.15L14.6 10.6L16 12L12 16L8 12L9.4 10.6L11 12.15V6.1Q9.1 6.45 8.05 7.94 7 9.43 7 11H6.5Q5.05 11 4.03 12.03 3 13.05 3 14.5 3 15.95 4.03 17 5.05 18 6.5 18H18.5Q19.55 18 20.27 17.27 21 16.55 21 15.5 21 14.45 20.27 13.73 19.55 13 18.5 13H17V11Q17 9.8 16.45 8.76 15.9 7.73 15 7V4.68Q16.85 5.55 17.93 7.26 19 9 19 11 20.73 11.2 21.86 12.5 23 13.78 23 15.5 23 17.38 21.69 18.69 20.38 20 18.5 20M12 11.05Z\" /></svg>'\n/*gId(\"convertbutton\").innerHTML = \n'<svg style=\"width:36px;height:36px\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M12,6V9L16,5L12,1V4A8,8 0 0,0 4,12C4,13.57 4.46,15.03 5.24,16.26L6.7,14.8C6.25,13.97 6,13 6,12A6,6 0 0,1 12,6M18.76,7.74L17.3,9.2C17.74,10.04 18,11 18,12A6,6 0 0,1 12,18V15L8,19L12,23V20A8,8 0 0,0 20,12C20,10.43 19.54,8.97 18.76,7.74Z\" /> </svg>&nbsp; Convert to WLED JSON '; \n*/\ncjb.innerHTML = \n'<svg class=\"svg-icon\" style=\"width:36px;height:36px\" viewBox=\"0 0 24 24\"> <path fill=\"currentColor\" d=\"M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z\" /> </svg>&nbsp; Copy to clipboard'; \ngId(\"sendJSONledbutton\").innerHTML = \n'<svg class=\"svg-icon\" style=\"width:36px;height:36px\" viewBox=\"0 0 24 24\"> <path id=sendSvgP fill=\"currentColor\" d=\"M6.5 20Q4.22 20 2.61 18.43 1 16.85 1 14.58 1 12.63 2.17 11.1 3.35 9.57 5.25 9.15 5.88 6.85 7.75 5.43 9.63 4 12 4 14.93 4 16.96 6.04 19 8.07 19 11 20.73 11.2 21.86 12.5 23 13.78 23 15.5 23 17.38 21.69 18.69 20.38 20 18.5 20H13Q12.18 20 11.59 19.41 11 18.83 11 18V12.85L9.4 14.4L8 13L12 9L16 13L14.6 14.4L13 12.85V18H18.5Q19.55 18 20.27 17.27 21 16.55 21 15.5 21 14.45 20.27 13.73 19.55 13 18.5 13H17V11Q17 8.93 15.54 7.46 14.08 6 12 6 9.93 6 8.46 7.46 7 8.93 7 11H6.5Q5.05 11 4.03 12.03 3 13.05 3 14.5 3 15.95 4.03 17 5.05 18 6.5 18H9V20M12 13Z\" /> </svg>&nbsp; Send to device';\n\n//After everything is loaded, check if we have a possible IP/host\n\nif(gurl.value.length > 0){\n  // Needs to be addressed directly here so the object actually exists\n  gId(\"sSg\").setAttribute(\"fill\", accentColor);\n}\n"
  },
  {
    "path": "wled00/data/pixart/site.webmanifest",
    "content": "{\n    \"name\": \"WLED Pixel Art Convertor\",\n    \"short_name\": \"ledconv\",\n    \"icons\": [\n       {\n          \"src\": \"/favicon-32x32.png\",\n          \"sizes\": \"32x322\",\n          \"type\": \"image/png\"\n       },\n       {\n          \"src\": \"/favicon-32x32.png\",\n          \"sizes\": \"32x32\",\n          \"type\": \"image/png\"\n       }\n    ],\n    \"theme_color\": \"#ffffff\",\n    \"background_color\": \"#ffffff\",\n    \"display\": \"standalone\"\n }"
  },
  {
    "path": "wled00/data/pixart/statics.js",
    "content": "//elements\nvar gurl = gId('curlUrl'); \nvar szX = gId(\"sizeX\"); \nvar szY = gId(\"sizeY\");\nvar szDiv = gId(\"sizeDiv\"); \nvar prw = gId(\"preview\");\nvar sID = gId('segID');\nvar JLD = gId('JSONled');\nvar tSg = gId('targetSegment');\nvar brgh = gId(\"brightnessNumber\");\n\nvar seDiv = gId(\"getSegmentsDiv\")\nvar cjb = gId(\"copyJSONledbutton\");\nvar frm = gId(\"formatSelector\");\nvar cLN = gId(\"colorLimitNumber\");\nvar haIDe = gId(\"haID\");\nvar haUe = gId(\"haUID\");\nvar haNe = gId(\"haName\");\nvar aS = gId(\"addressingSelector\");\nvar cFS = gId(\"colorFormatSelector\");\nvar lSS  = gId(\"ledSetupSelector\");\nvar imin = gId('image-info');\nvar imcn = gId('image-container');\nvar bcn = gId(\"button-container\");\nvar im = gId('image');\n//var ss = gId(\"sendSvgP\");\nvar scDiv = gId(\"scaleDiv\");\nvar w = window;\nvar canvas = gId('pixelCanvas');\nvar brgV = gId(\"brightnessValue\");\nvar cLV = gId(\"colorLimitValue\")\n\n//vars\nvar httpArray = [];\nvar fileJSON = '';\n\nvar hideableRows = d.querySelectorAll(\".ha-hide\");\nfor (var i = 0; i < hideableRows.length; i++) {\n  hideableRows[i].classList.add(\"hide\");\n}\n\nvar accentColor = '#eee';\nvar accentTextColor = '#777';\nvar prsCol = '#ccc';\nvar greenColor = '#056b0a';\nvar redColor = '#6b050c';\n\nvar scaleToggleOffd = \"M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M7,15A3,3 0 0,1 4,12A3,3 0 0,1 7,9A3,3 0 0,1 10,12A3,3 0 0,1 7,15Z\";\nvar scaleToggleOnd = \"M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z\";\n\nvar sSg = gId(\"getSegmentsSVGpath\");"
  },
  {
    "path": "wled00/data/pixelforge/omggif.js",
    "content": "// (c) Dean McNamee <dean@gmail.com>, 2013.\n//\n// https://github.com/deanm/omggif\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to\n// deal in the Software without restriction, including without limitation the\n// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n// sell copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in\n// all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n// IN THE SOFTWARE.\n//\n// omggif is a JavaScript implementation of a GIF 89a encoder and decoder,\n// including animation and compression.  It does not rely on any specific\n// underlying system, so should run in the browser, Node, or Plask.\n\n\"use strict\";\n\nfunction GifWriter(buf, width, height, gopts) {\n  var p = 0;\n\n  var gopts = gopts === undefined ? { } : gopts;\n  var loop_count = gopts.loop === undefined ? null : gopts.loop;\n  var global_palette = gopts.palette === undefined ? null : gopts.palette;\n\n  if (width <= 0 || height <= 0 || width > 65535 || height > 65535)\n    throw new Error(\"Width/Height invalid.\");\n\n  function check_palette_and_num_colors(palette) {\n    var num_colors = palette.length;\n    if (num_colors < 2 || num_colors > 256 ||  num_colors & (num_colors-1)) {\n      throw new Error(\n          \"Invalid code/color length, must be power of 2 and 2 .. 256.\");\n    }\n    return num_colors;\n  }\n\n  // - Header.\n  buf[p++] = 0x47; buf[p++] = 0x49; buf[p++] = 0x46;  // GIF\n  buf[p++] = 0x38; buf[p++] = 0x39; buf[p++] = 0x61;  // 89a\n\n  // Handling of Global Color Table (palette) and background index.\n  var gp_num_colors_pow2 = 0;\n  var background = 0;\n  if (global_palette !== null) {\n    var gp_num_colors = check_palette_and_num_colors(global_palette);\n    while (gp_num_colors >>= 1) ++gp_num_colors_pow2;\n    gp_num_colors = 1 << gp_num_colors_pow2;\n    --gp_num_colors_pow2;\n    if (gopts.background !== undefined) {\n      background = gopts.background;\n      if (background >= gp_num_colors)\n        throw new Error(\"Background index out of range.\");\n      // The GIF spec states that a background index of 0 should be ignored, so\n      // this is probably a mistake and you really want to set it to another\n      // slot in the palette.  But actually in the end most browsers, etc end\n      // up ignoring this almost completely (including for dispose background).\n      if (background === 0)\n        throw new Error(\"Background index explicitly passed as 0.\");\n    }\n  }\n\n  // - Logical Screen Descriptor.\n  // NOTE(deanm): w/h apparently ignored by implementations, but set anyway.\n  buf[p++] = width & 0xff; buf[p++] = width >> 8 & 0xff;\n  buf[p++] = height & 0xff; buf[p++] = height >> 8 & 0xff;\n  // NOTE: Indicates 0-bpp original color resolution (unused?).\n  buf[p++] = (global_palette !== null ? 0x80 : 0) |  // Global Color Table Flag.\n             gp_num_colors_pow2;  // NOTE: No sort flag (unused?).\n  buf[p++] = background;  // Background Color Index.\n  buf[p++] = 0;  // Pixel aspect ratio (unused?).\n\n  // - Global Color Table\n  if (global_palette !== null) {\n    for (var i = 0, il = global_palette.length; i < il; ++i) {\n      var rgb = global_palette[i];\n      buf[p++] = rgb >> 16 & 0xff;\n      buf[p++] = rgb >> 8 & 0xff;\n      buf[p++] = rgb & 0xff;\n    }\n  }\n\n  if (loop_count !== null) {  // Netscape block for looping.\n    if (loop_count < 0 || loop_count > 65535)\n      throw new Error(\"Loop count invalid.\")\n    // Extension code, label, and length.\n    buf[p++] = 0x21; buf[p++] = 0xff; buf[p++] = 0x0b;\n    // NETSCAPE2.0\n    buf[p++] = 0x4e; buf[p++] = 0x45; buf[p++] = 0x54; buf[p++] = 0x53;\n    buf[p++] = 0x43; buf[p++] = 0x41; buf[p++] = 0x50; buf[p++] = 0x45;\n    buf[p++] = 0x32; buf[p++] = 0x2e; buf[p++] = 0x30;\n    // Sub-block\n    buf[p++] = 0x03; buf[p++] = 0x01;\n    buf[p++] = loop_count & 0xff; buf[p++] = loop_count >> 8 & 0xff;\n    buf[p++] = 0x00;  // Terminator.\n  }\n\n\n  var ended = false;\n\n  this.addFrame = function(x, y, w, h, indexed_pixels, opts) {\n    if (ended === true) { --p; ended = false; }  // Un-end.\n\n    opts = opts === undefined ? { } : opts;\n\n    // TODO(deanm): Bounds check x, y.  Do they need to be within the virtual\n    // canvas width/height, I imagine?\n    if (x < 0 || y < 0 || x > 65535 || y > 65535)\n      throw new Error(\"x/y invalid.\")\n\n    if (w <= 0 || h <= 0 || w > 65535 || h > 65535)\n      throw new Error(\"Width/Height invalid.\")\n\n    if (indexed_pixels.length < w * h)\n      throw new Error(\"Not enough pixels for the frame size.\");\n\n    var using_local_palette = true;\n    var palette = opts.palette;\n    if (palette === undefined || palette === null) {\n      using_local_palette = false;\n      palette = global_palette;\n    }\n\n    if (palette === undefined || palette === null)\n      throw new Error(\"Must supply either a local or global palette.\");\n\n    var num_colors = check_palette_and_num_colors(palette);\n\n    // Compute the min_code_size (power of 2), destroying num_colors.\n    var min_code_size = 0;\n    while (num_colors >>= 1) ++min_code_size;\n    num_colors = 1 << min_code_size;  // Now we can easily get it back.\n\n    var delay = opts.delay === undefined ? 0 : opts.delay;\n\n    // From the spec:\n    //     0 -   No disposal specified. The decoder is\n    //           not required to take any action.\n    //     1 -   Do not dispose. The graphic is to be left\n    //           in place.\n    //     2 -   Restore to background color. The area used by the\n    //           graphic must be restored to the background color.\n    //     3 -   Restore to previous. The decoder is required to\n    //           restore the area overwritten by the graphic with\n    //           what was there prior to rendering the graphic.\n    //  4-7 -    To be defined.\n    // NOTE(deanm): Dispose background doesn't really work, apparently most\n    // browsers ignore the background palette index and clear to transparency.\n    var disposal = opts.disposal === undefined ? 0 : opts.disposal;\n    if (disposal < 0 || disposal > 3)  // 4-7 is reserved.\n      throw new Error(\"Disposal out of range.\");\n\n    var use_transparency = false;\n    var transparent_index = 0;\n    if (opts.transparent !== undefined && opts.transparent !== null) {\n      use_transparency = true;\n      transparent_index = opts.transparent;\n      if (transparent_index < 0 || transparent_index >= num_colors)\n        throw new Error(\"Transparent color index.\");\n    }\n\n    if (disposal !== 0 || use_transparency || delay !== 0) {\n      // - Graphics Control Extension\n      buf[p++] = 0x21; buf[p++] = 0xf9;  // Extension / Label.\n      buf[p++] = 4;  // Byte size.\n\n      buf[p++] = disposal << 2 | (use_transparency === true ? 1 : 0);\n      buf[p++] = delay & 0xff; buf[p++] = delay >> 8 & 0xff;\n      buf[p++] = transparent_index;  // Transparent color index.\n      buf[p++] = 0;  // Block Terminator.\n    }\n\n    // - Image Descriptor\n    buf[p++] = 0x2c;  // Image Seperator.\n    buf[p++] = x & 0xff; buf[p++] = x >> 8 & 0xff;  // Left.\n    buf[p++] = y & 0xff; buf[p++] = y >> 8 & 0xff;  // Top.\n    buf[p++] = w & 0xff; buf[p++] = w >> 8 & 0xff;\n    buf[p++] = h & 0xff; buf[p++] = h >> 8 & 0xff;\n    // NOTE: No sort flag (unused?).\n    // TODO(deanm): Support interlace.\n    buf[p++] = using_local_palette === true ? (0x80 | (min_code_size-1)) : 0;\n\n    // - Local Color Table\n    if (using_local_palette === true) {\n      for (var i = 0, il = palette.length; i < il; ++i) {\n        var rgb = palette[i];\n        buf[p++] = rgb >> 16 & 0xff;\n        buf[p++] = rgb >> 8 & 0xff;\n        buf[p++] = rgb & 0xff;\n      }\n    }\n\n    p = GifWriterOutputLZWCodeStream(\n            buf, p, min_code_size < 2 ? 2 : min_code_size, indexed_pixels);\n\n    return p;\n  };\n\n  this.end = function() {\n    if (ended === false) {\n      buf[p++] = 0x3b;  // Trailer.\n      ended = true;\n    }\n    return p;\n  };\n\n  this.getOutputBuffer = function() { return buf; };\n  this.setOutputBuffer = function(v) { buf = v; };\n  this.getOutputBufferPosition = function() { return p; };\n  this.setOutputBufferPosition = function(v) { p = v; };\n}\n\n// Main compression routine, palette indexes -> LZW code stream.\n// |index_stream| must have at least one entry.\nfunction GifWriterOutputLZWCodeStream(buf, p, min_code_size, index_stream) {\n  buf[p++] = min_code_size;\n  var cur_subblock = p++;  // Pointing at the length field.\n\n  var clear_code = 1 << min_code_size;\n  var code_mask = clear_code - 1;\n  var eoi_code = clear_code + 1;\n  var next_code = eoi_code + 1;\n\n  var cur_code_size = min_code_size + 1;  // Number of bits per code.\n  var cur_shift = 0;\n  // We have at most 12-bit codes, so we should have to hold a max of 19\n  // bits here (and then we would write out).\n  var cur = 0;\n\n  function emit_bytes_to_buffer(bit_block_size) {\n    while (cur_shift >= bit_block_size) {\n      buf[p++] = cur & 0xff;\n      cur >>= 8; cur_shift -= 8;\n      if (p === cur_subblock + 256) {  // Finished a subblock.\n        buf[cur_subblock] = 255;\n        cur_subblock = p++;\n      }\n    }\n  }\n\n  function emit_code(c) {\n    cur |= c << cur_shift;\n    cur_shift += cur_code_size;\n    emit_bytes_to_buffer(8);\n  }\n\n  // I am not an expert on the topic, and I don't want to write a thesis.\n  // However, it is good to outline here the basic algorithm and the few data\n  // structures and optimizations here that make this implementation fast.\n  // The basic idea behind LZW is to build a table of previously seen runs\n  // addressed by a short id (herein called output code).  All data is\n  // referenced by a code, which represents one or more values from the\n  // original input stream.  All input bytes can be referenced as the same\n  // value as an output code.  So if you didn't want any compression, you\n  // could more or less just output the original bytes as codes (there are\n  // some details to this, but it is the idea).  In order to achieve\n  // compression, values greater then the input range (codes can be up to\n  // 12-bit while input only 8-bit) represent a sequence of previously seen\n  // inputs.  The decompressor is able to build the same mapping while\n  // decoding, so there is always a shared common knowledge between the\n  // encoding and decoder, which is also important for \"timing\" aspects like\n  // how to handle variable bit width code encoding.\n  //\n  // One obvious but very important consequence of the table system is there\n  // is always a unique id (at most 12-bits) to map the runs.  'A' might be\n  // 4, then 'AA' might be 10, 'AAA' 11, 'AAAA' 12, etc.  This relationship\n  // can be used for an effecient lookup strategy for the code mapping.  We\n  // need to know if a run has been seen before, and be able to map that run\n  // to the output code.  Since we start with known unique ids (input bytes),\n  // and then from those build more unique ids (table entries), we can\n  // continue this chain (almost like a linked list) to always have small\n  // integer values that represent the current byte chains in the encoder.\n  // This means instead of tracking the input bytes (AAAABCD) to know our\n  // current state, we can track the table entry for AAAABC (it is guaranteed\n  // to exist by the nature of the algorithm) and the next character D.\n  // Therefor the tuple of (table_entry, byte) is guaranteed to also be\n  // unique.  This allows us to create a simple lookup key for mapping input\n  // sequences to codes (table indices) without having to store or search\n  // any of the code sequences.  So if 'AAAA' has a table entry of 12, the\n  // tuple of ('AAAA', K) for any input byte K will be unique, and can be our\n  // key.  This leads to a integer value at most 20-bits, which can always\n  // fit in an SMI value and be used as a fast sparse array / object key.\n\n  // Output code for the current contents of the index buffer.\n  var ib_code = index_stream[0] & code_mask;  // Load first input index.\n  var code_table = { };  // Key'd on our 20-bit \"tuple\".\n\n  emit_code(clear_code);  // Spec says first code should be a clear code.\n\n  // First index already loaded, process the rest of the stream.\n  for (var i = 1, il = index_stream.length; i < il; ++i) {\n    var k = index_stream[i] & code_mask;\n    var cur_key = ib_code << 8 | k;  // (prev, k) unique tuple.\n    var cur_code = code_table[cur_key];  // buffer + k.\n\n    // Check if we have to create a new code table entry.\n    if (cur_code === undefined) {  // We don't have buffer + k.\n      // Emit index buffer (without k).\n      // This is an inline version of emit_code, because this is the core\n      // writing routine of the compressor (and V8 cannot inline emit_code\n      // because it is a closure here in a different context).  Additionally\n      // we can call emit_byte_to_buffer less often, because we can have\n      // 30-bits (from our 31-bit signed SMI), and we know our codes will only\n      // be 12-bits, so can safely have 18-bits there without overflow.\n      // emit_code(ib_code);\n      cur |= ib_code << cur_shift;\n      cur_shift += cur_code_size;\n      while (cur_shift >= 8) {\n        buf[p++] = cur & 0xff;\n        cur >>= 8; cur_shift -= 8;\n        if (p === cur_subblock + 256) {  // Finished a subblock.\n          buf[cur_subblock] = 255;\n          cur_subblock = p++;\n        }\n      }\n\n      if (next_code === 4096) {  // Table full, need a clear.\n        emit_code(clear_code);\n        next_code = eoi_code + 1;\n        cur_code_size = min_code_size + 1;\n        code_table = { };\n      } else {  // Table not full, insert a new entry.\n        // Increase our variable bit code sizes if necessary.  This is a bit\n        // tricky as it is based on \"timing\" between the encoding and\n        // decoder.  From the encoders perspective this should happen after\n        // we've already emitted the index buffer and are about to create the\n        // first table entry that would overflow our current code bit size.\n        if (next_code >= (1 << cur_code_size)) ++cur_code_size;\n        code_table[cur_key] = next_code++;  // Insert into code table.\n      }\n\n      ib_code = k;  // Index buffer to single input k.\n    } else {\n      ib_code = cur_code;  // Index buffer to sequence in code table.\n    }\n  }\n\n  emit_code(ib_code);  // There will still be something in the index buffer.\n  emit_code(eoi_code);  // End Of Information.\n\n  // Flush / finalize the sub-blocks stream to the buffer.\n  emit_bytes_to_buffer(1);\n\n  // Finish the sub-blocks, writing out any unfinished lengths and\n  // terminating with a sub-block of length 0.  If we have already started\n  // but not yet used a sub-block it can just become the terminator.\n  if (cur_subblock + 1 === p) {  // Started but unused.\n    buf[cur_subblock] = 0;\n  } else {  // Started and used, write length and additional terminator block.\n    buf[cur_subblock] = p - cur_subblock - 1;\n    buf[p++] = 0;\n  }\n  return p;\n}\n\nfunction GifReader(buf) {\n  var p = 0;\n\n  // - Header (GIF87a or GIF89a).\n  if (buf[p++] !== 0x47 ||            buf[p++] !== 0x49 || buf[p++] !== 0x46 ||\n      buf[p++] !== 0x38 || (buf[p++]+1 & 0xfd) !== 0x38 || buf[p++] !== 0x61) {\n    throw new Error(\"Invalid GIF 87a/89a header.\");\n  }\n\n  // - Logical Screen Descriptor.\n  var width = buf[p++] | buf[p++] << 8;\n  var height = buf[p++] | buf[p++] << 8;\n  var pf0 = buf[p++];  // <Packed Fields>.\n  var global_palette_flag = pf0 >> 7;\n  var num_global_colors_pow2 = pf0 & 0x7;\n  var num_global_colors = 1 << (num_global_colors_pow2 + 1);\n  var background = buf[p++];\n  buf[p++];  // Pixel aspect ratio (unused?).\n\n  var global_palette_offset = null;\n  var global_palette_size   = null;\n\n  if (global_palette_flag) {\n    global_palette_offset = p;\n    global_palette_size = num_global_colors;\n    p += num_global_colors * 3;  // Seek past palette.\n  }\n\n  var no_eof = true;\n\n  var frames = [ ];\n\n  var delay = 0;\n  var transparent_index = null;\n  var disposal = 0;  // 0 - No disposal specified.\n  var loop_count = null;\n\n  this.width = width;\n  this.height = height;\n\n  while (no_eof && p < buf.length) {\n    switch (buf[p++]) {\n      case 0x21:  // Graphics Control Extension Block\n        switch (buf[p++]) {\n          case 0xff:  // Application specific block\n            // Try if it's a Netscape block (with animation loop counter).\n            if (buf[p   ] !== 0x0b ||  // 21 FF already read, check block size.\n                // NETSCAPE2.0\n                buf[p+1 ] == 0x4e && buf[p+2 ] == 0x45 && buf[p+3 ] == 0x54 &&\n                buf[p+4 ] == 0x53 && buf[p+5 ] == 0x43 && buf[p+6 ] == 0x41 &&\n                buf[p+7 ] == 0x50 && buf[p+8 ] == 0x45 && buf[p+9 ] == 0x32 &&\n                buf[p+10] == 0x2e && buf[p+11] == 0x30 &&\n                // Sub-block\n                buf[p+12] == 0x03 && buf[p+13] == 0x01 && buf[p+16] == 0) {\n              p += 14;\n              loop_count = buf[p++] | buf[p++] << 8;\n              p++;  // Skip terminator.\n            } else {  // We don't know what it is, just try to get past it.\n              p += 12;\n              while (true) {  // Seek through subblocks.\n                var block_size = buf[p++];\n                // Bad block size (ex: undefined from an out of bounds read).\n                if (!(block_size >= 0)) throw Error(\"Invalid block size\");\n                if (block_size === 0) break;  // 0 size is terminator\n                p += block_size;\n              }\n            }\n            break;\n\n          case 0xf9:  // Graphics Control Extension\n            if (buf[p++] !== 0x4 || buf[p+4] !== 0)\n              throw new Error(\"Invalid graphics extension block.\");\n            var pf1 = buf[p++];\n            delay = buf[p++] | buf[p++] << 8;\n            transparent_index = buf[p++];\n            if ((pf1 & 1) === 0) transparent_index = null;\n            disposal = pf1 >> 2 & 0x7;\n            p++;  // Skip terminator.\n            break;\n\n          case 0xfe:  // Comment Extension.\n            while (true) {  // Seek through subblocks.\n              var block_size = buf[p++];\n              // Bad block size (ex: undefined from an out of bounds read).\n              if (!(block_size >= 0)) throw Error(\"Invalid block size\");\n              if (block_size === 0) break;  // 0 size is terminator\n              // console.log(buf.slice(p, p+block_size).toString('ascii'));\n              p += block_size;\n            }\n            break;\n\n          default:\n            throw new Error(\n                \"Unknown graphic control label: 0x\" + buf[p-1].toString(16));\n        }\n        break;\n\n      case 0x2c:  // Image Descriptor.\n        var x = buf[p++] | buf[p++] << 8;\n        var y = buf[p++] | buf[p++] << 8;\n        var w = buf[p++] | buf[p++] << 8;\n        var h = buf[p++] | buf[p++] << 8;\n        var pf2 = buf[p++];\n        var local_palette_flag = pf2 >> 7;\n        var interlace_flag = pf2 >> 6 & 1;\n        var num_local_colors_pow2 = pf2 & 0x7;\n        var num_local_colors = 1 << (num_local_colors_pow2 + 1);\n        var palette_offset = global_palette_offset;\n        var palette_size = global_palette_size;\n        var has_local_palette = false;\n        if (local_palette_flag) {\n          var has_local_palette = true;\n          palette_offset = p;  // Override with local palette.\n          palette_size = num_local_colors;\n          p += num_local_colors * 3;  // Seek past palette.\n        }\n\n        var data_offset = p;\n\n        p++;  // codesize\n        while (true) {\n          var block_size = buf[p++];\n          // Bad block size (ex: undefined from an out of bounds read).\n          if (!(block_size >= 0)) throw Error(\"Invalid block size\");\n          if (block_size === 0) break;  // 0 size is terminator\n          p += block_size;\n        }\n\n        frames.push({x: x, y: y, width: w, height: h,\n                     has_local_palette: has_local_palette,\n                     palette_offset: palette_offset,\n                     palette_size: palette_size,\n                     data_offset: data_offset,\n                     data_length: p - data_offset,\n                     transparent_index: transparent_index,\n                     interlaced: !!interlace_flag,\n                     delay: delay,\n                     disposal: disposal});\n        break;\n\n      case 0x3b:  // Trailer Marker (end of file).\n        no_eof = false;\n        break;\n\n      default:\n        throw new Error(\"Unknown gif block: 0x\" + buf[p-1].toString(16));\n        break;\n    }\n  }\n\n  this.numFrames = function() {\n    return frames.length;\n  };\n\n  this.loopCount = function() {\n    return loop_count;\n  };\n\n  this.frameInfo = function(frame_num) {\n    if (frame_num < 0 || frame_num >= frames.length)\n      throw new Error(\"Frame index out of range.\");\n    return frames[frame_num];\n  }\n\n  this.decodeAndBlitFrameBGRA = function(frame_num, pixels) {\n    var frame = this.frameInfo(frame_num);\n    var num_pixels = frame.width * frame.height;\n    var index_stream = new Uint8Array(num_pixels);  // At most 8-bit indices.\n    GifReaderLZWOutputIndexStream(\n        buf, frame.data_offset, index_stream, num_pixels);\n    var palette_offset = frame.palette_offset;\n\n    // NOTE(deanm): It seems to be much faster to compare index to 256 than\n    // to === null.  Not sure why, but CompareStub_EQ_STRICT shows up high in\n    // the profile, not sure if it's related to using a Uint8Array.\n    var trans = frame.transparent_index;\n    if (trans === null) trans = 256;\n\n    // We are possibly just blitting to a portion of the entire frame.\n    // That is a subrect within the framerect, so the additional pixels\n    // must be skipped over after we finished a scanline.\n    var framewidth  = frame.width;\n    var framestride = width - framewidth;\n    var xleft       = framewidth;  // Number of subrect pixels left in scanline.\n\n    // Output indicies of the top left and bottom right corners of the subrect.\n    var opbeg = ((frame.y * width) + frame.x) * 4;\n    var opend = ((frame.y + frame.height) * width + frame.x) * 4;\n    var op    = opbeg;\n\n    var scanstride = framestride * 4;\n\n    // Use scanstride to skip past the rows when interlacing.  This is skipping\n    // 7 rows for the first two passes, then 3 then 1.\n    if (frame.interlaced === true) {\n      scanstride += width * 4 * 7;  // Pass 1.\n    }\n\n    var interlaceskip = 8;  // Tracking the row interval in the current pass.\n\n    for (var i = 0, il = index_stream.length; i < il; ++i) {\n      var index = index_stream[i];\n\n      if (xleft === 0) {  // Beginning of new scan line\n        op += scanstride;\n        xleft = framewidth;\n        if (op >= opend) { // Catch the wrap to switch passes when interlacing.\n          scanstride = framestride * 4 + width * 4 * (interlaceskip-1);\n          // interlaceskip / 2 * 4 is interlaceskip << 1.\n          op = opbeg + (framewidth + framestride) * (interlaceskip << 1);\n          interlaceskip >>= 1;\n        }\n      }\n\n      if (index === trans) {\n        op += 4;\n      } else {\n        var r = buf[palette_offset + index * 3];\n        var g = buf[palette_offset + index * 3 + 1];\n        var b = buf[palette_offset + index * 3 + 2];\n        pixels[op++] = b;\n        pixels[op++] = g;\n        pixels[op++] = r;\n        pixels[op++] = 255;\n      }\n      --xleft;\n    }\n  };\n\n  // I will go to copy and paste hell one day...\n  this.decodeAndBlitFrameRGBA = function(frame_num, pixels) {\n    var frame = this.frameInfo(frame_num);\n    var num_pixels = frame.width * frame.height;\n    var index_stream = new Uint8Array(num_pixels);  // At most 8-bit indices.\n    GifReaderLZWOutputIndexStream(\n        buf, frame.data_offset, index_stream, num_pixels);\n    var palette_offset = frame.palette_offset;\n\n    // NOTE(deanm): It seems to be much faster to compare index to 256 than\n    // to === null.  Not sure why, but CompareStub_EQ_STRICT shows up high in\n    // the profile, not sure if it's related to using a Uint8Array.\n    var trans = frame.transparent_index;\n    if (trans === null) trans = 256;\n\n    // We are possibly just blitting to a portion of the entire frame.\n    // That is a subrect within the framerect, so the additional pixels\n    // must be skipped over after we finished a scanline.\n    var framewidth  = frame.width;\n    var framestride = width - framewidth;\n    var xleft       = framewidth;  // Number of subrect pixels left in scanline.\n\n    // Output indicies of the top left and bottom right corners of the subrect.\n    var opbeg = ((frame.y * width) + frame.x) * 4;\n    var opend = ((frame.y + frame.height) * width + frame.x) * 4;\n    var op    = opbeg;\n\n    var scanstride = framestride * 4;\n\n    // Use scanstride to skip past the rows when interlacing.  This is skipping\n    // 7 rows for the first two passes, then 3 then 1.\n    if (frame.interlaced === true) {\n      scanstride += width * 4 * 7;  // Pass 1.\n    }\n\n    var interlaceskip = 8;  // Tracking the row interval in the current pass.\n\n    for (var i = 0, il = index_stream.length; i < il; ++i) {\n      var index = index_stream[i];\n\n      if (xleft === 0) {  // Beginning of new scan line\n        op += scanstride;\n        xleft = framewidth;\n        if (op >= opend) { // Catch the wrap to switch passes when interlacing.\n          scanstride = framestride * 4 + width * 4 * (interlaceskip-1);\n          // interlaceskip / 2 * 4 is interlaceskip << 1.\n          op = opbeg + (framewidth + framestride) * (interlaceskip << 1);\n          interlaceskip >>= 1;\n        }\n      }\n\n      if (index === trans) {\n        op += 4;\n      } else {\n        var r = buf[palette_offset + index * 3];\n        var g = buf[palette_offset + index * 3 + 1];\n        var b = buf[palette_offset + index * 3 + 2];\n        pixels[op++] = r;\n        pixels[op++] = g;\n        pixels[op++] = b;\n        pixels[op++] = 255;\n      }\n      --xleft;\n    }\n  };\n}\n\nfunction GifReaderLZWOutputIndexStream(code_stream, p, output, output_length) {\n  var min_code_size = code_stream[p++];\n\n  var clear_code = 1 << min_code_size;\n  var eoi_code = clear_code + 1;\n  var next_code = eoi_code + 1;\n\n  var cur_code_size = min_code_size + 1;  // Number of bits per code.\n  // NOTE: This shares the same name as the encoder, but has a different\n  // meaning here.  Here this masks each code coming from the code stream.\n  var code_mask = (1 << cur_code_size) - 1;\n  var cur_shift = 0;\n  var cur = 0;\n\n  var op = 0;  // Output pointer.\n\n  var subblock_size = code_stream[p++];\n\n  // TODO(deanm): Would using a TypedArray be any faster?  At least it would\n  // solve the fast mode / backing store uncertainty.\n  // var code_table = Array(4096);\n  var code_table = new Int32Array(4096);  // Can be signed, we only use 20 bits.\n\n  var prev_code = null;  // Track code-1.\n\n  while (true) {\n    // Read up to two bytes, making sure we always 12-bits for max sized code.\n    while (cur_shift < 16) {\n      if (subblock_size === 0) break;  // No more data to be read.\n\n      cur |= code_stream[p++] << cur_shift;\n      cur_shift += 8;\n\n      if (subblock_size === 1) {  // Never let it get to 0 to hold logic above.\n        subblock_size = code_stream[p++];  // Next subblock.\n      } else {\n        --subblock_size;\n      }\n    }\n\n    // TODO(deanm): We should never really get here, we should have received\n    // and EOI.\n    if (cur_shift < cur_code_size)\n      break;\n\n    var code = cur & code_mask;\n    cur >>= cur_code_size;\n    cur_shift -= cur_code_size;\n\n    // TODO(deanm): Maybe should check that the first code was a clear code,\n    // at least this is what you're supposed to do.  But actually our encoder\n    // now doesn't emit a clear code first anyway.\n    if (code === clear_code) {\n      // We don't actually have to clear the table.  This could be a good idea\n      // for greater error checking, but we don't really do any anyway.  We\n      // will just track it with next_code and overwrite old entries.\n\n      next_code = eoi_code + 1;\n      cur_code_size = min_code_size + 1;\n      code_mask = (1 << cur_code_size) - 1;\n\n      // Don't update prev_code ?\n      prev_code = null;\n      continue;\n    } else if (code === eoi_code) {\n      break;\n    }\n\n    // We have a similar situation as the decoder, where we want to store\n    // variable length entries (code table entries), but we want to do in a\n    // faster manner than an array of arrays.  The code below stores sort of a\n    // linked list within the code table, and then \"chases\" through it to\n    // construct the dictionary entries.  When a new entry is created, just the\n    // last byte is stored, and the rest (prefix) of the entry is only\n    // referenced by its table entry.  Then the code chases through the\n    // prefixes until it reaches a single byte code.  We have to chase twice,\n    // first to compute the length, and then to actually copy the data to the\n    // output (backwards, since we know the length).  The alternative would be\n    // storing something in an intermediate stack, but that doesn't make any\n    // more sense.  I implemented an approach where it also stored the length\n    // in the code table, although it's a bit tricky because you run out of\n    // bits (12 + 12 + 8), but I didn't measure much improvements (the table\n    // entries are generally not the long).  Even when I created benchmarks for\n    // very long table entries the complexity did not seem worth it.\n    // The code table stores the prefix entry in 12 bits and then the suffix\n    // byte in 8 bits, so each entry is 20 bits.\n\n    var chase_code = code < next_code ? code : prev_code;\n\n    // Chase what we will output, either {CODE} or {CODE-1}.\n    var chase_length = 0;\n    var chase = chase_code;\n    while (chase > clear_code) {\n      chase = code_table[chase] >> 8;\n      ++chase_length;\n    }\n\n    var k = chase;\n\n    var op_end = op + chase_length + (chase_code !== code ? 1 : 0);\n    if (op_end > output_length) {\n      console.log(\"Warning, gif stream longer than expected.\");\n      return;\n    }\n\n    // Already have the first byte from the chase, might as well write it fast.\n    output[op++] = k;\n\n    op += chase_length;\n    var b = op;  // Track pointer, writing backwards.\n\n    if (chase_code !== code)  // The case of emitting {CODE-1} + k.\n      output[op++] = k;\n\n    chase = chase_code;\n    while (chase_length--) {\n      chase = code_table[chase];\n      output[--b] = chase & 0xff;  // Write backwards.\n      chase >>= 8;  // Pull down to the prefix code.\n    }\n\n    if (prev_code !== null && next_code < 4096) {\n      code_table[next_code++] = prev_code << 8 | k;\n      // TODO(deanm): Figure out this clearing vs code growth logic better.  I\n      // have an feeling that it should just happen somewhere else, for now it\n      // is awkward between when we grow past the max and then hit a clear code.\n      // For now just check if we hit the max 12-bits (then a clear code should\n      // follow, also of course encoded in 12-bits).\n      if (next_code >= code_mask+1 && cur_code_size < 12) {\n        ++cur_code_size;\n        code_mask = code_mask << 1 | 1;\n      }\n    }\n\n    prev_code = code;\n  }\n\n  if (op !== output_length) {\n    console.log(\"Warning, gif stream shorter than expected.\");\n  }\n\n  return output;\n}\n\n\n// CommonJS.\n//try { exports.GifWriter = GifWriter; exports.GifReader = GifReader } catch(e) {}\ntry { exports.GifWriter = GifWriter; } catch(e) {}\n"
  },
  {
    "path": "wled00/data/pixelforge/pixelforge.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<meta name=\"author\" content=\"@dedehai\" />\n<link rel=\"shortcut icon\" href=\"favicon.ico\">\n<title>WLED PixelForge</title>\n<style>\nbody {\n\tmax-width: 800px;\n\tmargin: 0 auto;\n}\n/* Header styles */\n.title {\n\tfont-size: 32px;\n\tfont-weight: bold;\n\tcolor: #fff;\n\tpadding-top: 20px;\n}\nh3 {\n\tmargin-bottom: 0;\n}\n/* shimmer text animation */\n.title .sh {\n\tbackground: linear-gradient(90deg,\n\t\t#7b47db 0%, #ff6b6b 20%, #feca57 40%, #48dbfb 60%, #7b47db 100%);\n\tbackground-size: 200% 100%;\n\t-webkit-background-clip: text;\n\t-webkit-text-fill-color: transparent;\n\tanimation: shimmer 4s ease-in-out 5;\n\tfont-size: 36px;\n}\n@keyframes shimmer { 50% { background-position: 600% 0; } }\n\n/* image grid */\n.g {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(100px, 1fr));\n\tgap: 10px;\n\tmargin: 20px 0;\n\tpadding: 0 5px;\n}\n\n/* shared border styles */\n.it, #cv, #pv, .dp, button, .btn {\n\tborder: 2px solid #555;\n}\n.it:hover, .dp:hover, button:hover, .btn:hover {\n\tborder-color: #48a;\n}\n\n/* shared transitions */\n.it, .dp {\n\ttransition: all 0.3s ease;\n}\n\n.it {\n\taspect-ratio: 1;\n\tborder-radius: 4px;\n\tbackground-size: 100% 100%;\n\tbackground-position: center;\n\tcursor: pointer;\n\timage-rendering: pixelated;\n}\n\n/* shared flex centering */\n.it.loading, .crw, .cs {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n}\n.it.loading {\n\tbackground: #222;\n}\n.it.loading::before {\n\tcontent: \"Loading...\";\n}\n\n/* context menu */\n.cm {\n\tposition: fixed;\n}\n.cm button {\n\tdisplay: block;\n\twidth: 100%;\n\ttext-align: left;\n\tcursor: pointer;\n\tborder-radius: 1px;\n\tfont-size: 14px;\n\tmargin: 0;\n}\n.cm button:hover {\n\tbackground: #555;\n}\n.cm button.danger {\n\tcolor: #f44;\n}\n\n/* Editor styles */\n.ed {\n\tdisplay: none;\n\tmargin: 20px 0;\n\tpadding: 20px;\n\tbackground: #222;\n}\n.ed.active {\n\tdisplay: block;\n}\ninput[type=\"color\"] {\n\tborder: 0;\n}\n\n/* canvas wrap */\n.cw {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 5px;\n\talign-items: center;\n\twidth: 100%;\n}\n\n/* shared canvas styles */\n#cv, #pv {\n\tbackground: #333;\n}\n#cv {\n\tcursor: crosshair;\n\tmax-width: 100%;\n\tborder-radius: 8px;\n}\n#pv {\n\timage-rendering: pixelated;\n\tborder-radius: 4px;\n}\n\n/* toast */\n.t {\n\tposition: fixed;\n\ttop: 20px;\n\tright: 20px;\n\tbackground: #555;\n\tcolor: #fff;\n\tpadding: 12px 20px;\n\tborder-radius: 8px;\n\tfont-size: 14px;\n\tz-index: 888;\n\tborder: 2px solid #888;\n}\n\n/* drop zone */\n.dp {\n\tborder: 2px dashed #555;\n\tborder-radius: 8px;\n\tpadding: 40px 20px;\n\tbackground: #222;\n\tcursor: pointer;\n\tmargin: 20px auto;\n\tmax-width: 80%;\n}\n.dp:hover {\n\tbackground: #333;\n}\n\n/* buttons */\nbutton, .btn {\n\tborder: 2px solid #333;\n}\n\n/* sliders */\n.slc {\n\ttext-align: center;\n\tmargin-bottom: 5px;\n}\n.slc label {\n\tdisplay: block;\n\tmargin-bottom: 5px;\n}\n.sl {\n\twidth: 100%;\n\tmax-width: 500px;\n}\n\n/* controls row */\n.crw {\n\tgap: 8px;\n\tflex-wrap: wrap;\n\tmargin: 15px 0;\n}\n\n/* tabs */\n.tabc {\n\tdisplay: none;\n}\n.tabc.active {\n\tdisplay: block;\n}\n.tb {\n\tdisplay: flex;\n\tgap: 4px;\n\tborder-bottom: 2px solid #555;\n\tmargin: 20px 0 0 0;\n\tpadding: 0 20px;\n}\n.tb button {\n\tflex: 1;\n\tbackground: #111;\n\tborder: none;\n\tborder-radius: 8px 8px 0 0;\n\tpadding: 12px 24px;\n\tmargin: 0;\n\tcolor: #888;\n}\n.tb button:hover {\n\tbackground: #222;\n\tcolor: #aaa;\n}\n.tb button.active {\n\tbackground: #333;\n\tcolor: #fff;\n\tborder-bottom: 2px solid #333;\n}\n\n/* text tool */\n.cs {\n\tflex-direction: column;\n\tgap: 0;\n}\n.fr {\n\tdisplay: grid;\n\tgrid-template-columns: 100px 1fr;\n\talign-items: center;\n\ttext-align: left;\n\tgap: 8px;\n\tmax-width: 500px;\n}\n.tk {\n\tcolor: #8cf;\n\ttext-decoration: underline;\n\tcursor: pointer;\n}\n</style>\n</head>\n<body>\n<div class=\"cont\">\n\t<div class=\"title\">WLED<span class=\"sh\">PixelForge</span></div>\n\n\t<div class=\"tb\">\n\t\t<button class=\"active\" id=\"tImg\">Image Tool</button>\n\t\t<button id=\"tTxt\">Scrolling Text</button>\n\t\t<button id=\"tOth\">Other Tools</button>\n\t</div>\n\n\t<div id=\"iTab\" class=\"tabc active\">\n\t\t<h3 style=\"margin-top:20px;\">Target Segment</h3>\n\t\t<select id=\"seg\"></select>\n\n\t\t<h3>Images on Device</h3>\n\t\t<div class=\"g\" id=\"gr\"></div>\n\n\t\t<h3>Upload New Image</h3>\n\t\t<div id=\"drop\" class=\"dp\">\n\t\t\t<p>Drop image or click to select</p>\n\t\t</div>\n\t\t<input type=\"file\" id=\"src\" accept=\"image/*\" style=\"display:none\">\n\n\t\t<div class=\"ed\" id=\"ed\">\n\t\t\t<h3 style=\"margin-top:0;padding-top:0;border-top:0\">Crop & Adjust Image</h3>\n\n\t\t\t<div class=\"crw\">\n\t\t\t\t<button class=\"sml\" id=\"matchAspect\">Match Aspect Ratio</button>\n\t\t\t\t<button class=\"sml\" id=\"matchSize\">Match Size (1:1)</button>\n\t\t\t\t<button class=\"sml\" id=\"fullSize\">Full Size</button>\n\t\t\t\t<button class=\"sml\" id=\"resetCrop\">Reset</button>\n\t\t\t</div>\n\n\t\t\t<div class=\"cw\">\n\t\t\t\t<div style=\"width:100%\">\n\t\t\t\t\t<div class=\"slc\">\n\t\t\t\t\t\t<label>Rotation:  <span id=\"rotVal\">0</span>° <input type=\"checkbox\" id=\"snap\">snap</label>\n\t\t\t\t\t\t<input type=\"range\" id=\"rotSl\" min=\"0\" max=\"359\" value=\"0\" class=\"sl\">\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"slc\">\n\t\t\t\t\t\t<label>Zoom: </label>\n\t\t\t\t\t\t<input type=\"range\" id=\"zoom\" min=\"0\" max=\"100\" value=\"0\" class=\"sl\">\n\t\t\t\t\t</div>\n\t\t\t\t\t<canvas id=\"cv\" width=\"500\" height=\"500\"></canvas>\n\t\t\t\t</div>\n\n\t\t\t\t<small>Preview at target resolution</small>\n\n\t\t\t\t<canvas id=\"pv\"></canvas>\n\n\t\t\t\t<div class=\"slc\">\n\t\t\t\t\t<label>Dark Pixel Cutoff</label>\n\t\t\t\t\t<input type=\"range\" id=\"bt\" min=\"0\" max=\"255\" value=\"0\" class=\"sl\">\n\t\t\t\t\t<div style=\"display:flex;align-items:center;justify-content:center;margin-bottom:15px\">\n\t\t\t\t\t\t<label for=\"bg\" style=\"margin-right:10px\">Background Color</label>\n\t\t\t\t\t\t<input type=\"color\" id=\"bg\" value=\"#000000\">\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div style=\"display:none\" id=\"sz\">\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<label>Output size:</label>\n\t\t\t\t\t\t<input type=\"number\" id=\"w\" value=\"16\" min=\"1\" size=\"5\">\n\t\t\t\t\t\tx\n\t\t\t\t\t\t<input type=\"number\" id=\"h\" value=\"16\" min=\"1\" size=\"5\">\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t</div>\n\t\t</div>\n\n\t\t<div class=\"row\" style=\"margin-top:20px\">\n\t\t\t<div class=\"col\">\n\t\t\t\t<label for=\"fn\">Filename</label>\n\t\t\t\t<input type=\"text\" id=\"fn\" placeholder=\"image\" maxlength=\"26\">\n\t\t\t\t<small>.gif will be added</small>\n\t\t\t</div>\n\t\t</div>\n\t\t<button class=\"btn\" id=\"up\">Convert & Upload to WLED</button>\n\t</div>\n</div>\n\n<div id=\"xTab\" class=\"tabc\">\n\t<h3 style=\"margin-top:20px;\">Target Segment</h3>\n\t<select id=\"segT\"></select>\n\t<div id=\"ti\">\n\t\t<h3>Text to show</h3>\n\t\t<div class=\"col\" style=\"display:flex;gap:10px;align-items:center;justify-content:center;flex-wrap:wrap\">\n\t\t\t<input type=\"text\" id=\"txt\" placeholder=\"Enter text\" maxlength=\"64\"  style=\"margin-left:15px;flex:1;min-width:300px;\">\n\t\t\t<button class=\"btn\" id=\"aTxt\">✓</button>\n\t\t</div>\n\n\t\t<h3>Settings</h3>\n\t\t<div class=\"cs\">\n\t\t\t<div class=\"fr\">\n\t\t\t\tSpeed\t<input type=\"range\" id=\"sx\" min=\"0\" max=\"255\">\n\t\t\t</div>\n\t\t\t<div class=\"fr\">\n\t\t\t\tY Offset\t<input type=\"range\" id=\"ix\" min=\"0\" max=\"255\">\n\t\t\t</div>\n\t\t\t<div class=\"fr\">\n\t\t\t\tTrail\t<input type=\"range\" id=\"c1\" min=\"0\" max=\"255\">\n\t\t\t</div>\n\t\t\t<div class=\"fr\">\n\t\t\t\tFont Size\t<input type=\"range\" id=\"c2\" min=\"0\" max=\"255\">\n\t\t\t</div>\n\t\t\t<div class=\"fr\">\n\t\t\t\tRotate <input type=\"range\" id=\"c3\" min=\"0\" max=\"31\">\n\t\t\t</div>\n\t\t</div>\n\n\t\t<div class=\"col\" style=\"display:flex;gap:20px;justify-content:center;\">\n\t\t\t<label style=\"display:flex;align-items:center;gap:5px\">\n\t\t\t\t<input type=\"checkbox\" id=\"o1\"> Gradient\n\t\t\t</label>\n\t\t\t<label style=\"display:flex;align-items:center;gap:5px\">\n\t\t\t\t<input type=\"checkbox\" id=\"o3\"> Reverse\n\t\t\t</label>\n\t\t</div>\n\n\t\t<h3>Available Tokens</h3>\n\t\t<div style=\"padding:15px;\">\n\t\t\t<div style=\"display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px;text-align:left\">\n\t\t\t\t<div><a href=\"#\" class=\"tk\" data-t=\"#TIME\">#TIME</a> - HH:MM AM/PM</div>\n\t\t\t\t<div><a href=\"#\" class=\"tk\" data-t=\"#HHMM\">#HHMM</a> - HH:MM</div>\n\t\t\t\t<div><a href=\"#\" class=\"tk\" data-t=\"#DATE\">#DATE</a> - DD.MM.YYYY</div>\n\t\t\t\t<div><a href=\"#\" class=\"tk\" data-t=\"#DDMM\">#DDMM</a> - Day.Month</div>\n\t\t\t\t<div><a href=\"#\" class=\"tk\" data-t=\"#MMDD\">#MMDD</a> - Month/Day</div>\n\t\t\t\t<div><a href=\"#\" class=\"tk\" data-t=\"#YYYY\">#YYYY</a> - Year</div>\n\t\t\t\t<div><a href=\"#\" class=\"tk\" data-t=\"#YY\">#YY</a> - Year 2-digit</div>\n\t\t\t\t<div><a href=\"#\" class=\"tk\" data-t=\"#HH\">#HH</a> - Hours</div>\n\t\t\t\t<div><a href=\"#\" class=\"tk\" data-t=\"#MM\">#MM</a> - Minutes</div>\n\t\t\t\t<div><a href=\"#\" class=\"tk\" data-t=\"#SS\">#SS</a> - Seconds</div>\n\t\t\t\t<div><a href=\"#\" class=\"tk\" data-t=\"#MO\">#MO</a> - Month number</div>\n\t\t\t\t<div><a href=\"#\" class=\"tk\" data-t=\"#DD\">#DD</a> - Day number</div>\n\t\t\t\t<div><a href=\"#\" class=\"tk\" data-t=\"#MON\">#MON</a> - Month (Jan)</div>\n\t\t\t\t<div><a href=\"#\" class=\"tk\" data-t=\"#MONL\">#MONL</a> - Month (January)</div>\n\t\t\t\t<div><a href=\"#\" class=\"tk\" data-t=\"#DAY\">#DAY</a> - Weekday (Mon)</div>\n\t\t\t\t<div><a href=\"#\" class=\"tk\" data-t=\"#DDDD\">#DDDD</a> - Weekday (Monday)</div>\n\t\t\t</div>\n\t\t\t<div style=\"margin:10px;padding-top:10px;border-top:1px solid #444\">\n\t\t\t\t<strong>Tips:</strong></small><br>\n\t\t\t\t• Mix text and tokens: \"It's #HHMM O'Clock\" or \"#HH:#MM:#SS\"<br>\n\t\t\t\t• Add '0' suffix for leading zeros: #TIME0, #HH0, etc.\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\t<div id=\"ti1D\" style=\"display:none;\">Not available in 1D</div>\n</div>\n<div id=\"oTab\" class=\"tabc\">\n\t<div class=\"ed active\">\n\t\t<div>\n\t\t\t<h3>Pixel Paint</h3>\n\t\t\t<div><small>Interactive painting tool</small></div>\n\t\t\t<button class=\"btn\" id=\"t1\" style=\"display:none\"></button>\n\t\t</div>\n\t</div>\n\t<hr>\n\t<div class=\"ed active\">\n\t\t<div>\n\t\t\t<h3>Video Lab</h3>\n\t\t\t<div><small>Stream video and generate animated GIFs (beta)</small></div>\n\t\t\t<button class=\"btn\" id=\"t2\" style=\"display:none\"></button>\n\t\t</div>\n\t</div>\n\t<hr>\n\t<div class=\"ed active\">\n\t\t<div>\n\t\t\t<h3>PIXEL MAGIC Tool</h3>\n\t\t\t<div><small>Legacy pixel art editor</small></div>\n\t\t\t<button class=\"btn\" id=\"t3\" style=\"display:none\"></button>\n\t\t</div>\n\t</div>\n\t<hr>\n</div>\n\n<div style=\"margin:20px 0\">\n\t<button class=\"btn\" onclick=\"window.location.href=getURL('/')\">Back to the controls</button>\n</div>\n\n<div id=\"ov\"></div>\n<div id=\"mem\" style=\"display:none;font-size:12px;color:#aaa;\"></div>\n\n<script>\nconst imgageFX = 53; // image effect number\nconst txtFX = 122;   // scrolling text effect number\nconst getId = (i) => document.getElementById(i); // getId() is defined in common.js, but needed before it is loaded\n\n/* canvases */\nconst cv=getId('cv'),cx=cv.getContext('2d',{willReadFrequently:true});\nconst pv=getId('pv'),pvx=pv.getContext('2d',{willReadFrequently:true});\nlet rv, rvc; // off screen canvas for drawing resized & rotated image (created in init())\n\n\n/* globals */\nlet sI=null,sF=null,cI=null,bS=1,iS=1,pX=0,pY=0,rot=0;\nlet cr={x:50,y:50,w:200,h:150},drag=false,dH=null,oX=0,oY=0;\nlet pan=false,psX=0,psY=0,poX=0,poY=0;\nlet iL=[]; // image list\nlet gF=null,gI=null,aT=null;\nlet fL; // file list\n\n// load external resources in sequence to avoid 503 errors if heap is low, repeats indefinitely until loaded\n(function loadFiles() {\n\tconst s = document.createElement('script');\n\ts.src = 'common.js';\n\ts.onerror = () => setTimeout(loadFiles, 100);\n\ts.onload = () => {\n\t\tloadResources(['style.css','omggif.js'], init); // load omggif.js then call init()\n\t};\n\tdocument.head.appendChild(s);\n})();\n\n/* init */\nasync function init() {\n\tgetLoc();\n\t// create off screen canvas\n\trv = cE('canvas');\n\trvc = rv.getContext('2d',{willReadFrequently:true}); \n\trv.width = cv.width; rv.height = cv.height;\n\n\tawait segLoad(); // load available segments\n\tawait flU(); // update file list\n\ttoolChk('pixelpaint.htm','t1'); // update buttons of additional tools\n\ttoolChk('videolab.htm','t2');\n\ttoolChk('pxmagic.htm','t3');\n\tawait fsMem(); // show file system memory info\n}\n\n/* update file list */\nasync function flU(){\n\ttry{\n\t\tconst r = await fetch(getURL('/edit?list=/'));\n\t\tfL = await r.json();\n\t}catch(e){console.error(e);}\n}\n\n/* toast */\nfunction msg(m,t=''){\n\tconst el=cE('div');el.className='t';el.textContent=m;\n\tif(t==='err')el.style.background='#a00';\n\td.body.appendChild(el);setTimeout(()=>el.remove(),3000);\n}\n/* \"loading\" overlay */\nfunction ovShow(){getId('ov').classList.add('loading');getId('ov').style.display='block';}\nfunction ovHide(){getId('ov').classList.remove('loading');getId('ov').style.display='none';}\n\n/* segments */\nfunction segLoad(){\n\tconst s1=getId('seg'),v1=s1.value,s2=getId('segT'),v2=s2.value;\n\tfetch(getURL('/json/state')).then(r=>r.json()).then(j=>{\n\t\ts1.innerHTML=''; s2.innerHTML='';\n\t\tif(j.seg&&j.seg.length){\n\t\t\tj.seg.forEach(({id,n,start,stop,startY,stopY,fx})=>{\n\t\t\t\tconst w=stop-start,h=(stopY-startY)||1;\n\t\t\t\tconst t = (n || `Segment ${id}`) + (h>1 ? ` (${w}x${h})` : ` (${w}px)`) + (fx===imgageFX ? ' [Image]' : (fx===txtFX ? ' [Scrolling Text]' : ''));\n\t\t\t\tconst o=new Option(t,id);\n\t\t\t\to.dataset.w=w; o.dataset.h=h; o.dataset.fx=fx||0;\n\t\t\t\ts1.add(o); // gif tool\n\t\t\t\ts2.add(o.cloneNode(true)); // scrolling text tool\n\t\t\t});\n\t\t}else{\n\t\t\tconst o=new Option('Segment 0',0);\n\t\t\ts1.add(o); s2.add(o.cloneNode(true));\n\t\t}\n\t\tif(v1) s1.value=v1; if(v2) s2.value=v2;\n\t\ts2.onchange(); // trigger on load to toggle show/hide of text tool\n\t\tconst o=s1.options[s1.selectedIndex];\n\t\tif(o){ getId('w').value=o.dataset.w||16; getId('h').value=o.dataset.h||16; }\n\t}).catch(console.error);\n}\n\n/* which seg is showing image fx 53 */\nfunction curImgSeg(){\n\tconst sel=getId('seg');\n\tfor(let i=0;i<sel.options.length;i++){\n\t\tif(parseInt(sel.options[i].dataset.fx)===imgageFX) return parseInt(sel.options[i].value);\n\t}\n\treturn null;\n}\n\n/* seg change -> update target size */\ngetId('seg').onchange = () =>{\n\tconst o=getId('seg').selectedOptions[0];\n\tgetId('w').value=o.dataset.w;\n\tgetId('h').value=o.dataset.h;\n\tif(cI) crDraw();\n};\n\ngetId('segT').onchange = () => {\n\tconst is2D = (getId('segT').selectedOptions[0].dataset.h || 1) > 1;\n\tgetId('ti').style.display = is2D ? 'block' : 'none';\n\tgetId('ti1D').style.display = is2D ? 'none' : 'block';\n};\n\n/* image list */\nasync function imgLoad(){\n\ttry{\n\t\tawait flU(); // update file list\n\t\tconst grid=getId('gr');\n\t\tconst types=['gif','png','jpg','jpeg','bmp'];\n\t\tconst imgs=fL.filter(f=>types.includes(f.name.split('.').pop()?.toLowerCase()));\n\t\tconst newList=imgs.map(f=>f.name.replace('/',''));\n\t\tconst miss=newList.filter(n=>!iL.includes(n));\n\n\t\tif(iL.length===0){\n\t\t\tgrid.innerHTML='';\n\t\t\tiL=[...newList];\n\t\t\tif(!imgs.length){\n\t\t\t\tgrid.innerHTML='<div style=\"grid-column:1/-1;text-align:center;color:#aaa;padding:20px\">No images</div>';\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait imgLoad2(imgs);\n\t\t}else if(miss.length>0){\n\t\t\tconst missData=imgs.filter(f=>miss.includes(f.name.replace('/','')));\n\t\t\tiL=[...newList];\n\t\t\tawait imgLoad2(missData);\n\t\t}\n\t}catch(e){console.error(e);}\n}\n\n/* load images into grid TODO: when switching tabs, it can throw 503 and have unloaded images, tried to fix it but all my attempts failed*/\nasync function imgLoad2(imgs){\n\tconst grid=getId('gr');\n\tfor(const f of imgs){\n\t\tconst name=f.name.replace('/',''),url=getURL(`/${name}`);\n\t\tconst isGif=name.toLowerCase().endsWith('.gif');\n\t\tconst it=cE('div');it.className='it loading';\n\t\tit.dataset.name=name;it.dataset.url=url;\n\t\tit.onclick=()=>{ if(isGif) imgPlay(url,name); else unsup(url,name); };\n\t\tit.oncontextmenu=e=>{e.preventDefault();sI={name,url};menuShow(e.pageX,e.pageY);};\n\t\tgrid.appendChild(it);\n\t\tawait new Promise(res=>{\n\t\t\tconst im=new Image();\n\t\t\tim.onload=()=>{\n\t\t\t\tit.style.backgroundImage=`url(${url}?cb=${Date.now()})`;\n\t\t\t\tif(!isGif) it.style.border=\"5px solid red\";\n\t\t\t\tit.classList.remove('loading'); res();\n\t\t\t\tconst kb=Math.round(f.size/1024);\n\t\t\t\tit.title=`${name}\\n${im.width}x${im.height}\\n${kb} KB`;\n\t\t\t};\n\t\t\tim.onerror=()=>{it.classList.remove('loading');it.style.background='#222';res();};\n\t\t\tim.src=url+'?cb='+Date.now();\n\t\t});\n\t}\n}\n\nfunction imgRm(nm){\n\tiL=iL.filter(n=>n!==nm);\n\tconst grid=getId('gr');\n\tgrid.querySelectorAll('.it').forEach(it=>{ if(it.dataset.name===nm) it.remove(); });\n\t//if(iL.length===0){\n\t//\tgrid.innerHTML='<div style=\"grid-column:1/-1;text-align:center;color:#aaa;padding:20px\">No images found</div>';\n\t//}\n}\n\n/* additional tools: check if present, install if not */\nfunction toolChk(file, btnId) {\n\ttry {\n\t\tconst has = fL.some(f => f.name.includes(file));\n\t\tconst b = getId(btnId);\n\t\tb.style.display = 'block';\n\t\tb.style.margin = '10px auto';\n\t\tif (has) {\n\t\t\tb.textContent = 'Open';\n\t\t\tb.onclick = () => window.open(getURL(`/${file}`), '_blank'); // open tool: remove gz to not trigger download\n\t\t} else {\n\t\t\tb.textContent = 'Download';\n\t\t\tb.onclick = async () => {\n\t\t\t\tconst fileGz = file + '.gz'; // use gz version\n\t\t\t\tconst url = `https://dedehai.github.io/${fileGz}`; // always download gz version\n\t\t\t\tif (!confirm(`Download ${url}?`)) return;\n\t\t\t\ttry {\n\t\t\t\t\tconst f = await fetch(url);\n\t\t\t\t\tif (!f.ok) throw new Error(\"Download failed \" + f.status);\n\t\t\t\t\tconst blob = await f.blob(), fd = new FormData();\n\t\t\t\t\tfd.append(\"data\", blob, fileGz);\n\t\t\t\t\tconst u = await fetch(getURL(\"/upload\"), { method: \"POST\", body: fd });\n\t\t\t\t\talert(u.ok ? \"Tool installed!\" : \"Upload failed\");\n\t\t\t\t\tawait flU(); // update file list\n\t\t\t\t\ttoolChk(file, btnId); // re-check and update button (must pass non-gz file name)\n\t\t\t\t} catch (e) { alert(\"Error \" + e.message); }\n\t\t\t};\n\t\t}\n\t} catch (e) { console.error(e); }\n}\n\n/* fs/mem info */\nasync function fsMem(){\n\ttry{\n\t\tconst r=await fetch(getURL('/json/info'));\n\t\tconst info=await r.json();\n\t\tif(info&&info.fs){\n\t\t\tgetId(\"mem\").textContent=`by @dedehai | Memory: ${info.fs.u} KB / ${info.fs.t} KB`;\n\t\t\tgetId(\"mem\").style.display=\"block\";\n\t\t}\n\t}catch(e){console.error(e);}\n}\n\n/* drag-drop + file input */\ngetId('drop').onclick=()=>{getId('src').value='';getId('src').click();};\ngetId('drop').ondragover=e=>{e.preventDefault();getId('drop').classList.add('active');};\ngetId('drop').ondragleave=()=>getId('drop').classList.remove('active');\ngetId('drop').ondrop=e=>{e.preventDefault();getId('drop').classList.remove('active');getId('src').files=e.dataTransfer.files;fileHandle();};\ngetId('src').onchange=fileHandle;\n\n/* file handler */\nfunction fileHandle() {\n\tconst file = getId('src').files[0];\n\tif (!file) return;\n\tsF = file; gI = null; gF = [];\n\tgetId('sz').style.display = 'block';\n\n\tconst isGif = file.type === 'image/gif';\n\tconst rdr = new FileReader();\n\n\trdr.onload = e => {\n\t\tif (isGif) {\n\t\t\ttry {\n\t\t\t\tconst arr = new Uint8Array(e.target.result);\n\t\t\t\tconst gif = new GifReader(arr);\n\t\t\t\tgI = { width: gif.width, height: gif.height, numFrames: gif.numFrames() };\n\t\t\t\tconst ac = cE('canvas'); ac.width = gif.width; ac.height = gif.height;\n\t\t\t\tconst acx = ac.getContext('2d', { willReadFrequently: true });\n\t\t\t\tlet saved = null;\n\t\t\t\tfor (let i = 0; i < gI.numFrames; i++) {\n\t\t\t\t\tconst fi = gif.frameInfo(i), disp = fi.disposal || 0;\n\t\t\t\t\tif (disp === 3) saved = acx.getImageData(0, 0, gif.width, gif.height);\n\t\t\t\t\tconst tp = new Uint8Array(gif.width * gif.height * 4);\n\t\t\t\t\tgif.decodeAndBlitFrameRGBA(i, tp);\n\t\t\t\t\tconst tc = cE('canvas'); tc.width = gif.width; tc.height = gif.height;\n\t\t\t\t\tconst tctx = tc.getContext('2d');\n\t\t\t\t\tconst tid = new ImageData(new Uint8ClampedArray(tp), gif.width, gif.height);\n\t\t\t\t\ttctx.putImageData(tid, 0, 0);\n\t\t\t\t\tacx.drawImage(tc, 0, 0);\n\t\t\t\t\tconst full = acx.getImageData(0, 0, gif.width, gif.height);\n\t\t\t\t\tgF.push({ pixels: new Uint8Array(full.data), delay: fi.delay || 10 });\n\t\t\t\t\tif (disp === 2) acx.clearRect(0, 0, gif.width, gif.height);\n\t\t\t\t\telse if (disp === 3 && saved) acx.putImageData(saved, 0, 0);\n\t\t\t\t}\n\t\t\t\tconst tc = cE('canvas'); tc.width = gI.width; tc.height = gI.height;\n\t\t\t\tconst tctx = tc.getContext('2d');\n\t\t\t\ttctx.putImageData(new ImageData(new Uint8ClampedArray(gF[0].pixels), gI.width, gI.height), 0, 0);\n\t\t\t\timgShow(tc.toDataURL(), file.name);\n\t\t\t} catch (err) {\n\t\t\t\tmsg('GIF load failed', 'err');\n\t\t\t\tconsole.error(err);\n\t\t\t}\n\t\t} else {\n\t\t\t// static image → treat as single-frame GIF\n\t\t\tconst im = new Image();\n\t\t\tim.onload = () => {\n\t\t\t\tconst c = cE('canvas');\n\t\t\t\tc.width = im.width; c.height = im.height;\n\t\t\t\tconst ctx = c.getContext('2d');\n\t\t\t\tctx.drawImage(im, 0, 0);\n\t\t\t\tconst id = ctx.getImageData(0, 0, im.width, im.height);\n\t\t\t\tgF = [{ pixels: new Uint8Array(id.data), delay: 0 }];\n\t\t\t\tgI = { width: im.width, height: im.height, numFrames: 1 };\n\t\t\t\timgShow(c.toDataURL(), file.name);\n\t\t\t};\n\t\t\tim.src = e.target.result;\n\t\t}\n\t};\n\n\tisGif ? rdr.readAsArrayBuffer(file) : rdr.readAsDataURL(file);\n}\n\n/* display image on canvas */\nfunction imgShow(src, name) {\n\tcI = new Image();\n\tcI.onload = () => {\n\t\tgetId('ed').classList.add('active');\n\t\tgetId('drop').innerHTML = `<p>Image loaded: ${name}<br><small>Drop another to replace</small></p>`;\n\t\tgetId('fn').value = name.split('.')[0].substring(0, 16);\n\t\tviewReset();\n\t\tcr.w = cv.width * 0.8; cr.h = cv.height * 0.8;\n\t\tcr.x = (cv.width - cr.w) / 2; cr.y = (cv.height - cr.h) / 2;\n\t\tcrClamp();\n\t\tgifStart(); // handles both single- and multi-frame\n\t};\n\tcI.src = src;\n}\n\nfunction gifStart() {\n\tif (aT) clearInterval(aT);\n\tif (!gF || !gI || gF.length === 0) return crDraw();\n\n\tlet idx = 0;\n\tconst tc = cE('canvas');\n\ttc.width = gI.width;\n\ttc.height = gI.height;\n\tconst tctx = tc.getContext('2d');\n\n\tconst step = () => {\n\t\tconst id = new ImageData(new Uint8ClampedArray(gF[idx].pixels), gI.width, gI.height);\n\t\ttctx.putImageData(id, 0, 0);\n\t\tcI.src = tc.toDataURL();\n\t\tidx = (idx + 1) % gF.length;\n\t};\n\n\tcI.onload = () => crDraw();\n\tstep();\n\n\tif (gF.length > 1) {\n\t\tconst avg = gF.reduce((s, f) => s + f.delay, 0) / gF.length;\n\t\taT = setInterval(step, Math.max(avg * 10, 50));\n\t}\n}\nfunction gifStop(){ if(aT){ clearInterval(aT); aT=null; } }\n\n/* formats not supported by WLED */\nfunction unsup(url, name) {\n\talert(`Image format not supported.\\nPlease convert to GIF or use PIXEL MAGIC TOOL`);\n\tfetch(url).then(r => r.blob()).then(b => {\n\t\tconst f = new File([b], name, { type: b.type });\n\t\tconst dt = new DataTransfer();\n\t\tdt.items.add(f);\n\t\tgetId('src').files = dt.files;\n\t\tfileHandle();\n\t}).catch(() => msg('Failed to load image', 'err'));\n}\n\n/* size change -> redraw */\ngetId('w').oninput=()=>{if(cI)crDraw();};\ngetId('h').oninput=()=>{if(cI)crDraw();};\n\n/* crop helpers */\nfunction crClamp(){\n\tcr.w=Math.max(30,Math.min(cr.w,cv.width));\n\tcr.h=Math.max(30,Math.min(cr.h,cv.height));\n\tcr.x=Math.max(0,Math.min(cr.x,cv.width-cr.w));\n\tcr.y=Math.max(0,Math.min(cr.y,cv.height-cr.h));\n}\nfunction viewReset(){\n\tbS=Math.min(cv.width/cI.width,cv.height/cI.height);\n\tiS=bS;\n\tpX=(cv.width-cI.width*iS)/2;\n\tpY=(cv.height-cI.height*iS)/2;\n}\n\n/* zoom */\ngetId('zoom').oninput=()=>{\n\tif(!cI)return;\n\tconst t=getId('zoom').value/100,ns=bS*Math.pow(40,t);\n\tconst cxm=cv.width/2,cym=cv.height/2;\n\tconst dx=cxm-pX,dy=cym-pY,f=ns/iS;\n\tpX=cxm-dx*f; pY=cym-dy*f; iS=ns;\n\tcrClamp(); crDraw();\n};\n\n/* rotation */\nfunction rotUpd(v){\n\tif(getId('snap').checked) v = Math.round(v/15)*15 % 360; // snap to multiples of 15°\n\trot = v;\n\tgetId('rotVal').textContent = v;\n\tif(cI) crDraw();\n}\ngetId('rotSl').oninput = ()=> rotUpd(+getId('rotSl').value);\n\n\n/* color change */\ngetId('bg').oninput=crDraw;\n\n/* quick controls */\ngetId('matchAspect').onclick=e=>{\n\te.preventDefault();\n\tconst r=+getId('w').value/+getId('h').value;\n\tcr.h=cr.w/r; crClamp(); crDraw();\n};\ngetId('matchSize').onclick=e=>{\n\te.preventDefault();\n\tif(!cI)return;\n\tcr.w=+getId('w').value*iS; cr.h=+getId('h').value*iS;\n\tcrClamp(); crDraw();\n};\ngetId('fullSize').onclick=e=>{\n\te.preventDefault();\n\tif(!cI)return;\n\tcr.x=0; cr.y=0; cr.w=cv.width; cr.h=cv.height;\n\tcrClamp(); crDraw();\n};\ngetId('resetCrop').onclick=e=>{\n\te.preventDefault();\n\tif(!cI)return;\n\tcr.w=cv.width*0.8; cr.h=cv.height*0.8;\n\tcr.x=(cv.width-cr.w)/2; cr.y=(cv.height-cr.h)/2;\n\tcrClamp(); crDraw();\n};\n\n/* crop handles */\nfunction crHandles(r){\n\tconst s=40,o=s/2,ox=r.x-o,oy=r.y-o,ow=r.w+s,oh=r.h+s;\n\treturn{\n\t\tnw:{x:ox,y:oy,w:s,h:s}, ne:{x:ox+ow-s,y:oy,w:s,h:s},\n\t\tsw:{x:ox,y:oy+oh-s,w:s,h:s}, se:{x:ox+ow-s,y:oy+oh-s,w:s,h:s},\n\t\tn:{x:ox+s/2,y:oy,w:ow-s,h:s}, s:{x:ox+s/2,y:oy+oh-s,w:ow-s,h:s},\n\t\tw:{x:ox,y:oy+s/2,w:s,h:oh-s}, e:{x:ox+ow-s,y:oy+s/2,w:s,h:oh-s}\n\t};\n}\nfunction crHit(mx,my){\n\tconst h=crHandles(cr);\n\tfor(const k in h){let r=h[k];if(mx>=r.x&&mx<=r.x+r.w&&my>=r.y&&my<=r.y+r.h)return k;}\n\treturn null;\n}\n\n/* event coord */\nfunction posGet(e){\n\tconst r=cv.getBoundingClientRect();\n\tconst sx=cv.width/r.width,sy=cv.height/r.height;\n\tif(e.touches){\n\t\treturn {x:(e.touches[0].clientX-r.left)*sx,y:(e.touches[0].clientY-r.top)*sy};\n\t}else{\n\t\treturn {x:(e.clientX-r.left)*sx,y:(e.clientY-r.top)*sy};\n\t}\n}\n\n/* actions */\nfunction actStart(mx,my){\n\tdH=crHit(mx,my);\n\tif(dH){drag=true;return;}\n\tif(mx>cr.x&&mx<cr.x+cr.w&&my>cr.y&&my<cr.y+cr.h){\n\t\tdrag=true;dH=\"move\";oX=mx-cr.x;oY=my-cr.y;\n\t}else{\n\t\tpan=true;psX=mx;psY=my;poX=pX;poY=pY;\n\t}\n}\nfunction actMove(mx,my){\n\tif(drag){\n\t\tswitch(dH){\n\t\t\tcase \"move\":cr.x=mx-oX;cr.y=my-oY;break;\n\t\t\tcase \"nw\":cr.w+=(cr.x-mx);cr.h+=(cr.y-my);cr.x=mx;cr.y=my;break;\n\t\t\tcase \"ne\":cr.w=mx-cr.x;cr.h+=(cr.y-my);cr.y=my;break;\n\t\t\tcase \"sw\":cr.w+=(cr.x-mx);cr.x=mx;cr.h=my-cr.y;break;\n\t\t\tcase \"se\":cr.w=mx-cr.x;cr.h=my-cr.y;break;\n\t\t\tcase \"n\":cr.h+=(cr.y-my);cr.y=my;break;\n\t\t\tcase \"s\":cr.h=my-cr.y;break;\n\t\t\tcase \"w\":cr.w+=(cr.x-mx);cr.x=mx;break;\n\t\t\tcase \"e\":cr.w=mx-cr.x;break;\n\t\t}\n\t\tcrClamp(); crDraw();\n\t}else if(pan){\n\t\tpX=poX+(mx-psX); pY=poY+(my-psY);\n\t\tcrClamp(); crDraw();\n\t}\n}\nfunction actEnd(){ drag=false; dH=null; pan=false; }\n\n/* mouse */\ncv.onmousedown=e=>{if(!cI)return;const {x,y}=posGet(e);actStart(x,y);};\ncv.onmousemove=e=>{if(!cI)return;const {x,y}=posGet(e);actMove(x,y);};\ncv.onmouseup=actEnd;\n\n/* touch */\ncv.ontouchstart=e=>{\n\tif(!cI||e.touches.length!==1)return;\n\te.preventDefault();\n\tconst {x,y}=posGet(e); actStart(x,y);\n};\ncv.ontouchmove=e=>{\n\tif(!cI||e.touches.length!==1)return;\n\te.preventDefault();\n\tconst {x,y}=posGet(e); actMove(x,y);\n};\ncv.ontouchend=e=>{e.preventDefault();actEnd();};\ncv.ontouchcancel=e=>{e.preventDefault();actEnd();};\n\n/* draw + preview */\nfunction crDraw(){\n\tif(!cI) return;\n\n\t// render rotated image to offscreen\n\trvc.clearRect(0,0,rv.width,rv.height);\n\trvc.fillStyle = getId('bg').value;\n\trvc.fillRect(0,0,rv.width,rv.height);\n\trvc.imageSmoothingEnabled = false;\n\trvc.save();\n\tconst dw = cI.width * iS, dh = cI.height * iS;\n\trvc.translate(pX + dw/2, pY + dh/2);\n\trvc.rotate(rot * Math.PI / 180);\n\trvc.drawImage(cI, -dw/2, -dh/2, dw, dh);\n\trvc.restore();\n\n\t// copy offscreen to visible\n\tcx.clearRect(0,0,cv.width,cv.height);\n\tcx.drawImage(rv, 0, 0);\n\n\t// overlay crop frame (only on visible)\n\tcx.lineWidth=3; cx.setLineDash([6,4]); cx.shadowColor=\"#000\"; cx.shadowBlur=2;\n\tcx.strokeStyle=\"#FFF\"; cx.beginPath(); cx.roundRect(cr.x,cr.y,cr.w,cr.h,6); cx.stroke();\n\tcx.shadowColor=\"#000F\";\n\tprevUpd();\n}\n\ngetId('bt').addEventListener('input',()=>{prevUpd();});\nfunction blackTh(c){\n\tlet t=+getId('bt').value,\n\t\tdt=c.getImageData(0,0,c.canvas.width,c.canvas.height),\n\t\tb=getId('bg').value.match(/\\w\\w/g).map(x=>parseInt(x,16));\n\tfor(let i=0;i<dt.data.length;i+=4)\n\t\tif(dt.data[i]<t&&dt.data[i+1]<t&&dt.data[i+2]<t)\n\t\t\tdt.data[i]=b[0],dt.data[i+1]=b[1],dt.data[i+2]=b[2];\n\tc.putImageData(dt,0,0);\n}\n\nfunction prevUpd(){\n\tif(!cI)return;\n\tlet w=+getId('w').value,h=+getId('h').value;\n\t// Temporary canvas at target size\n\tconst tc = cE('canvas'); tc.width = w; tc.height = h;\n\tconst tcx = tc.getContext('2d');\n\ttcx.fillStyle=getId('bg').value;\n\ttcx.fillRect(0,0,w,h); // fill background (for transparent images)\n\ttcx.imageSmoothingEnabled = false;\n\ttcx.drawImage(rv, cr.x, cr.y, cr.w, cr.h, 0, 0, w, h); // sample cropped area from off screen canvas\n\tblackTh(tcx);\n\t// scale/stretch to preview canvas, limit to 256px in largest dimension but keep aspect ratio\n\tconst ratio = h/w;\n\tif(ratio > 1) {\n\t\tpv.height = 256;\n\t\tpv.width = Math.max(4, Math.round(256 / ratio)); // min 4px width for better visibility\n\t} else {\n\t\tpv.width = 256;\n\t\tpv.height = Math.max(4, Math.round(256 * ratio));\n\t}\n\tpvx.imageSmoothingEnabled=false;\n\tpvx.drawImage(tc,0,0,w,h,0,0,pv.width,pv.height);\n}\n\n// generate gif palette using median-cut algorithm, palette is padded to power of 2 size (gif standard)\nfunction genPal(pix){\n\tconst map=new Map();\n\tfor(let i=0;i<pix.length;i+=4){\n\t\tconst c=(pix[i]<<16)|(pix[i+1]<<8)|pix[i+2];\n\t\tmap.set(c,(map.get(c)||0)+1);\n\t}\n\tlet buckets=[Array.from(map,([rgb,count])=>({r:rgb>>16&255,g:rgb>>8&255,b:rgb&255,count}))];\n\twhile(buckets.length<256&&buckets.some(b=>b.length>1)){\n\t\tbuckets.sort((a,b)=>b.length-a.length);\n\t\tconst b=buckets.shift();\n\t\tconst ch=['r','g','b'].map(k=>({k,range:Math.max(...b.map(c=>c[k]))-Math.min(...b.map(c=>c[k]))}))\n\t\t\t.reduce((a,b)=>a.range>b.range?a:b).k;\n\t\tb.sort((a,c)=>a[ch]-c[ch]);\n\t\tconst m=b.length>>1;\n\t\tbuckets.push(b.slice(0,m),b.slice(m));\n\t}\n\tconst pal=buckets.map(b=>{\n\t\tconst t=b.reduce((s,c)=>s+c.count,0);\n\t\tconst r=Math.round(b.reduce((s,c)=>s+c.r*c.count,0)/t);\n\t\tconst g=Math.round(b.reduce((s,c)=>s+c.g*c.count,0)/t);\n\t\tconst bl=Math.round(b.reduce((s,c)=>s+c.b*c.count,0)/t);\n\t\treturn(r<<16)|(g<<8)|bl;\n\t});\n\tlet p2=1;\n\twhile(p2<pal.length)p2<<=1; // make it power of 2\n\tfor(let i=pal.length;i<p2;i++)pal[i]=pal[pal.length-1];\n\tconst idx=new Uint8Array(pix.length/4);\n\tfor(let i=0,j=0;i<pix.length;i+=4,j++){\n\t\tlet r=pix[i],g=pix[i+1],b=pix[i+2],best=0,min=1e9;\n\t\tfor(let k=0;k<buckets.length;k++){\n\t\t\tconst p=pal[k],pr=p>>16&255,pg=p>>8&255,pb=p&255;\n\t\t\tconst d=(r-pr)**2+(g-pg)**2+(b-pb)**2;\n\t\t\tif(d<min){min=d;best=k;}\n\t\t}\n\t\tidx[j]=best;\n\t}\n\treturn{indexed:idx,palette:pal};\n}\n\n// calculate optimal grid dimensions for 1D pixel data (gif is restricted to 320x320)\nfunction grid(length) {\n\tif (length <= 320) return { w: length, h: 1 };\n\tlet best = [1, length], waste = length;\n\t// find best matching width/height with least wasted pixels (should never be more than 1)\n\tfor (let w = 320; w >= 2; w--) {\n\t\tconst h = Math.ceil(length / w);\n\t\tconst wst = w * h - length;\n\t\tif (wst < waste) {\n\t\t\tbest = [w, h];\n\t\t\twaste = wst;\n\t\t\tif (!waste) break;\n\t\t}\n\t}\n\treturn { w: best[0], h: best[1] };\n}\n\n/* create GIF and upload */\ngetId('up').onclick = async () => {\n\tif (!gF || !gI) return; // no image\n\tconst w = +getId('w').value, h = +getId('h').value, fn = getId('fn').value.trim() || 'image';\n\tconst filename = `${fn}.gif`;\n\tconst repl = iL.includes(filename);\n\tif (repl && !confirm(`${filename} already exists. Overwrite?`)) return;\n\n\tovShow();\n\ttry {\n\t\tconst tc = cE('canvas'); tc.width = gI.width; tc.height = gI.height;\n\t\tconst tctx = tc.getContext('2d');\n\t\tconst cc = cE('canvas'); cc.width = w; cc.height = h;\n\t\tconst cctx = cc.getContext('2d');\n\t\tcctx.imageSmoothingEnabled = false;\n\n\t\tconst frames = [];\n\t\tfor (let i = 0; i < gF.length; i++) {\n\t\t\t// put current GIF frame into tc\n\t\t\tconst id = new ImageData(new Uint8ClampedArray(gF[i].pixels), gI.width, gI.height);\n\t\t\ttctx.putImageData(id, 0, 0);\n\n\t\t\t// render this frame into the offscreen rotated canvas (no overlay)\n\t\t\trvc.clearRect(0, 0, rv.width, rv.height);\n\t\t\trvc.fillStyle = getId('bg').value;\n\t\t\trvc.fillRect(0, 0, rv.width, rv.height);\n\t\t\trvc.imageSmoothingEnabled = false;\n\t\t\trvc.save();\n\t\t\tconst dw = gI.width * iS, dh = gI.height * iS;\n\t\t\trvc.translate(pX + dw / 2, pY + dh / 2);\n\t\t\trvc.rotate(rot * Math.PI / 180);\n\t\t\trvc.drawImage(tc, -dw / 2, -dh / 2, dw, dh);\n\t\t\trvc.restore();\n\n\t\t\t// sample the crop from the offscreen (already rotated) canvas into output size\n\t\t\tcctx.fillStyle = getId('bg').value;\n\t\t\tcctx.fillRect(0, 0, w, h);\n\t\t\tcctx.imageSmoothingEnabled = false;\n\t\t\tcctx.drawImage(rv, cr.x, cr.y, cr.w, cr.h, 0, 0, w, h);\n\n\t\t\tblackTh(cctx);\n\t\t\tconst fd = cctx.getImageData(0, 0, w, h);\n\t\t\tframes.push({ data: fd.data, delay: gF[i].delay });\n\t\t}\n\n\t\tconst g = grid(w); // calculate optimal grid size: 1D image is saved as \"2D gif\" if w > 320 (WLED gif size limit), 2D can not be larger than 256x256 so is unchange\n\t\tlet gw = g.w, gh = Math.max(g.h, h); // gif width and height\n\t\tconst all = new Uint8Array(frames.length * gw * gh * 4);\n\t\t// concat all frames to single array to create palette from all colors\n\t\tframes.forEach((f, i) => {\n\t\t\tconst off = i * gw * gh * 4;\n\t\t\tfor (let j = 0; j < gw * gh; j++) {\n\t\t\t\tconst src = (j < w*h ? j : w*h-1) * 4; // pad with last pixel of source frame if out of bounds (for 1D images)\n\t\t\t\tall[off + j*4] = f.data[src]; all[off + j*4 + 1] = f.data[src + 1]; all[off + j*4 + 2] = f.data[src + 2]; all[off + j*4 + 3] = 255;\n\t\t\t}\n\t\t});\n\t\tconst { indexed, palette } = genPal(all);\n\t\tconst gifData = [];\n\t\tconst wr = new GifWriter(gifData, gw, gh, { palette, loop: 0 });\n\t\tfor (let i = 0; i < frames.length; i++) {\n\t\t\tconst framePixels = indexed.slice(i * gw * gh, (i + 1) * gw * gh); // slice global array back into per-frame data\n\t\t\twr.addFrame(0, 0, gw, gh, framePixels, { delay: frames[i].delay, disposal: 2 });\n\t\t}\n\t\twr.end();\n\n\t\tconst fU = new File([new Uint8Array(gifData)], filename, { type: 'image/gif' });\n\t\tconst fd = new FormData();\n\t\tfd.append('file', fU, filename);\n\t\tconst r = await fetch(getURL('/upload'), { method: 'POST', body: fd });\n\n\t\tif (r.ok) {\n\t\t\tmsg(`${filename} uploaded`);\n\t\t\tif (repl) imgRm(filename);\n\t\t\tawait imgLoad();\n\t\t\tgetId('src').value = '';\n\t\t\tgetId('drop').innerHTML = '<p>Drop image or click to select</p>';\n\t\t} else msg('Upload failed', 'err');\n\t} catch (e) {\n\t\tmsg(`Error: ${e.message}`, 'err');\n\t} finally {\n\t\tovHide();\n\t}\n};\n\n/* play on device */\nasync function imgPlay(url,name){\n\tconst tgt=+getId('seg').value,cur=curImgSeg();\n\tif(cur!==null && cur!==tgt){\n\t\tif(!confirm(`Segment ${cur} is currently displaying an image. Switch image display to segment ${tgt}?`))return;\n\t}\n\tovShow();\n\ttry{\n\t\tconst j={\n\t\t\ton:true,\n\t\t\tseg: cur!==null && cur!==tgt\n\t\t\t\t? [{id:cur,fx:0,n:\"\"},{id:tgt,fx:53,frz:false,sx:128,n:name}]\n\t\t\t\t: {id:tgt,fx:53,frz:false,sx:128,n:name}\n\t\t};\n\t\tconst r=await fetch(getURL('/json/state'),{method:'POST',body:JSON.stringify(j)});\n\t\tconst out=await r.json();\n\t\tif(out.success){\n\t\t\tmsg(`Playing ${name}`);\n\t\t\tawait segLoad();\n\t\t}else msg('Failed to play','err');\n\t}catch(e){msg(`Error: ${e.message}`,'err');}\n\tfinally{ovHide();}\n}\n\n/* ctx menu */\nfunction menuShow(x,y){\n\tmenuClose();\n\tconst m=cE('div');\n\tm.className='cm';\n\tm.style.left=x+'px'; m.style.top=y+'px';\n\tm.innerHTML=`\n\t\t<button onclick=\"imgDl()\">Download</button>\n\t\t<button class=\"danger\" onclick=\"imgDel()\">Delete</button>`;\n\td.body.appendChild(m);\n\tsetTimeout(()=>{\n\t\tconst h=e=>{\n\t\t\tif(!e.target.closest('.cm')){menuClose();d.removeEventListener('click',h);}\n\t\t};\n\t\td.addEventListener('click',h);\n\t},100);\n}\nfunction menuClose(){d.querySelectorAll('.cm').forEach(m=>m.remove());}\n\nasync function imgDl(){\n\ttry{\n\t\tconst r=await fetch(sI.url),b=await r.blob();\n\t\tconst u=URL.createObjectURL(b),a=cE('a');\n\t\ta.href=u;a.download=sI.name;a.click();\n\t\tURL.revokeObjectURL(u); msg('Downloaded');\n\t}catch(e){msg('Download failed','err');}\n\tmenuClose();\n}\nasync function imgDel(){\n\tif(!confirm(`Delete ${sI.name}?`))return;\n\tovShow();\n\ttry{\n\t\tconst r = await fetch(getURL(`/edit?func=delete&path=/${sI.name}`));\n\t\tif(r.ok){ msg('Deleted'); imgRm(sI.name); }\n\t\telse msg('Delete failed! File in use?','err');\n\t}catch(e){msg('Delete failed','err');}\n\tfinally{ovHide();}\n\tmenuClose();\n}\n\n/* tab select and additional tools */\nfunction tabSw(tab) {\n\t'iTab,xTab,oTab,tImg,tTxt,tOth'.split(',').forEach((id,i)=>{\n\t\tgetId(id).classList.toggle('active', tab===['img','txt','oth'][i%3]);\n\t});\n\tlocalStorage.tab=tab;\n\t({txt:txtSegLoad,img:imgLoad}[tab]||(()=>{}))(); // functions to execute on tab switch (currently none for oth)\n}\n'Img,Txt,Oth'.split(',').forEach((s,i)=>{\n\tgetId('t'+s).onclick=()=>tabSw(['img','txt','oth'][i]);\n});\ntabSw(localStorage.tab||'img');\n\n/* tokens insert */\nfunction txtIns(el,t){\n\tconst s=el.selectionStart??el.value.length,e=el.selectionEnd??el.value.length,v=el.value;\n\tconst nv=(v.slice(0,s)+t+v.slice(e)).slice(0,64);\n\tconst p=Math.min(s+t.length,nv.length);\n\tel.value=nv;el.focus();el.selectionStart=el.selectionEnd=p;\n}\ndocument.addEventListener('click',e=>{\n\tconst a=e.target.closest('.tk'); if(!a) return;\n\te.preventDefault(); txtIns(getId('txt'),a.dataset.t);\n\t// txtUp();\n});\n\n/* load seg settings into text UI */\nasync function txtSegLoad(){\n\tconst id=+getId('segT').value;\n\ttry{\n\n\t\tconst r=await fetch(getURL('/json/state')),j=await r.json();\n\t\tif(j.seg&&j.seg[id]){\n\t\t\tconst s=j.seg[id];\n\t\t\tgetId('txt').value=s.n||'';\n\t\t\tgetId('sx').value=s.sx||128;\n\t\t\tgetId('ix').value=s.ix||128;\n\t\t\tgetId('c1').value=s.c1||0;\n\t\t\tgetId('c2').value=s.c2||0;\n\t\t\tgetId('c3').value=s.c3||0;\n\t\t\tgetId('o1').checked=!(!s.o1);\n\t\t\tgetId('o3').checked=!(!s.o3);\n\t\t}\n\t}catch(e){console.error(e);}\n}\n\n/* auto apply on change */\n['sx','ix','c1','c2','c3','o1','o3'].forEach(id=>{ getId(id).onchange=txtUp; });\n\n/* send text settings */\nfunction txtUp(){\n\tconst id=+getId('segT').value,txt=getId('txt').value.trim().slice(0,64);\n\tconst j={on:true,seg:{id,fx:122,n:txt,sx:+getId('sx').value,ix:+getId('ix').value,c1:+getId('c1').value,c2:+getId('c2').value,c3:+getId('c3').value,o1:getId('o1').checked,o3:getId('o3').checked}};\n\tfetch(getURL('/json/state'),{method:'POST',body:JSON.stringify(j)})\n\t\t.then(r => { if(r.ok) segLoad(); })\n\t\t.catch(console.error);\n}\n\n/* apply button */\ngetId('aTxt').onclick=async()=>{\n\tconst id=+getId('segT').value,txt=getId('txt').value.trim();\n\tovShow();\n\ttry{\n\t\tconst j={on:true,seg:{id,fx:122,n:txt,sx:+getId('sx').value,ix:+getId('ix').value,c1:+getId('c1').value,c2:+getId('c2').value,c3:+getId('c3').value,o1:getId('o1').checked,o3:getId('o3').checked}};\n\t\tconst r=await fetch(getURL('/json/state'),{method:'POST',body:JSON.stringify(j)});\n\t\tconst out=await r.json();\n\t\tif(out.success!==false){ msg(`Applied to Segment ${id}`); await segLoad(); }\n\t\telse msg('Failed to apply','err');\n\t}catch(e){ msg(`Error: ${e.message}`,'err'); }\n\tfinally{ ovHide(); }\n};\n</script>\n</body>\n</html>"
  },
  {
    "path": "wled00/data/pxmagic/pxmagic.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <meta name=\"author\" content=\"@ajotanc\" />\n\n    <title>Pixel Magic Tool</title>\n\n    <style>\n      :root {\n        --s-thumb: #0006;\n        --s-background: #0003;\n        --overlay: rgba(0, 0, 0, 0.5);\n        --background: #111;\n        --text: #bbb;\n        --gray-dark: #222;\n        --gray-medium: #333;\n        --gray-light: #eeeeee;\n        --blue-dark: #284352;\n        --blue-medium: #3b708b;\n        --blue-light: #48a;\n        --success-dark: #03440c;\n        --success-medium: #548c2f;\n        --success-light: #8ac926;\n        --error-dark: #a80a0a;\n        --error-medium: #c9262b;\n        --error-light: #ff595e;\n        --warning-dark: #dc2f02;\n        --warning-medium: #e85d04;\n        --warning-light: #f48c06;\n      }\n\n      ::-webkit-scrollbar {\n        width: 6px;\n      }\n\n      ::-webkit-scrollbar-track {\n        background: transparent;\n      }\n\n      ::-webkit-scrollbar-thumb {\n        background: var(--s-thumb);\n        opacity: 0.2;\n        border-radius: 5px;\n      }\n\n      ::-webkit-scrollbar-thumb:hover {\n        background: var(--s-background);\n      }\n\n      ::selection {\n        background: var(--blue-light);\n      }\n\n      * {\n        font-family: \"Helvetica\", Verdana, sans-serif;\n        box-sizing: border-box;\n        margin: 0;\n        padding: 0;\n      }\n\n      body {\n        margin: 0;\n        display: flex;\n        justify-content: center;\n        align-items: center;\n        min-height: 100vh;\n        background: var(--background);\n      }\n\n      canvas {\n        width: 100%;\n      }\n\n      small {\n        display: block;\n        font-weight: 400;\n        margin: 2px 0 5px;\n        color: var(--gray-light);\n        font-size: 0.75rem;\n      }\n\n      footer {\n        display: flex;\n        justify-content: center;\n        margin-block: 20px;\n      }\n\n      a {\n        text-decoration: none;\n        color: var(--blue-light);\n        font-size: 0.75rem;\n        font-weight: 600;\n      }\n\n      a:is(:hover, :focus, :active) {\n        color: var(--blue-medium);\n      }\n\n      #wledEdit {\n        padding: 1.5px 8px;\n        background: var(--blue-light);\n        margin-left: 6px;\n        border-radius: 4px;\n        color: var(--gray-light);\n      }\n\n      .container {\n        width: 100%;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        flex-direction: column;\n      }\n\n      .content {\n        width: min(768px, calc(100% - 40px));\n        margin-inline: 20px;\n      }\n\n      .row {\n        display: flex;\n        flex-wrap: nowrap;\n        justify-content: space-between;\n        margin-top: 20px;\n      }\n\n      .col {\n        flex-basis: calc(50% - 10px);\n        position: relative;\n        padding-inline: 5px;\n      }\n\n      .col-full {\n        flex-basis: 100%;\n        position: relative;\n        padding-inline: 5px;\n      }\n\n      .col-small {\n        flex-basis: 33.3333%;\n        position: relative;\n        padding-inline: 5px;\n      }\n\n      .header {\n        display: flex;\n        flex-direction: column;\n        padding-block: 20px;\n      }\n\n      .header .title {\n        font-size: 2.5rem;\n        line-height: 2.5rem;\n        font-weight: 800;\n        color: var(--gray-light);\n        padding-bottom: 5px;\n      }\n\n      .header .subtitle {\n        font-size: 0.75rem;\n        color: var(--text);\n      }\n\n      .header .rainbow {\n        background: linear-gradient(\n          to right,\n          #ef5350,\n          #f48fb1,\n          #7e57c2,\n          #2196f3,\n          #26c6da,\n          #43a047,\n          #eeff41,\n          #f9a825,\n          #ff5722\n        );\n        -webkit-background-clip: text;\n        -webkit-text-fill-color: transparent;\n        background-size: 200% 200%;\n        animation: rainbow 5s linear infinite;\n        transition: background-position 0.5s ease;\n      }\n\n      label {\n        display: flex;\n        margin-bottom: 5px;\n        font-weight: bold;\n        color: var(--text);\n        align-items: center;\n      }\n\n      input[type=\"text\"],\n      input[type=\"number\"],\n      select,\n      textarea {\n        width: 100%;\n        padding: 10px;\n        border-radius: 50px;\n        background-color: var(--gray-medium);\n        border: 1px solid var(--gray-medium);\n        outline: none;\n        color: var(--gray-light);\n        font-size: 0.875rem;\n      }\n\n      .input-group {\n        display: flex;\n        justify-content: center;\n        align-items: center;\n      }\n\n      .input-group input:not([type=\"range\"]) {\n        border-radius: 50px 0 0 50px;\n      }\n\n      .input-group .input-description {\n        width: 100%;\n        max-width: 38px;\n        height: 38px;\n        display: flex;\n        justify-content: center;\n        align-items: center;\n        color: var(--gray-dark);\n        background: var(--gray-light);\n        border-radius: 0px 50px 50px 0;\n        border: 1px solid var(--gray-light);\n        border-left: 0;\n        font-size: 0.875rem;\n        line-height: 1rem;\n      }\n\n      .input-group .square {\n        border-radius: 50px !important;\n        margin-left: 10px;\n      }\n\n      .input-group .square input {\n        text-align: center;\n        background: none;\n        padding: 0;\n        border: 0;\n        color: var(--gray-dark);\n      }\n\n      textarea {\n        resize: none;\n        border-radius: 8px;\n      }\n\n      .custom-select select {\n        appearance: none;\n        -webkit-appearance: none;\n        -moz-appearance: none;\n        background-image: none;\n        padding-right: 39px;\n        cursor: pointer;\n      }\n\n      .custom-select label::after {\n        content: \"\";\n        position: absolute;\n        top: calc(50% + 6px);\n        right: 21px;\n        transform: rotate(135deg);\n        width: 6px;\n        height: 6px;\n        border-top: 2px solid var(--gray-light);\n        border-right: 2px solid var(--gray-light);\n        pointer-events: none;\n      }\n\n      .dropzone {\n        width: 100%;\n        border: 1px dashed var(--gray-light);\n        background-color: var(--gray-dark);\n        color: var(--gray-light);\n        text-align: center;\n        padding: 40px 10px;\n        border-radius: 8px;\n        margin-top: 20px;\n        transition: all 0.5s ease-in-out;\n      }\n\n      .dropzone:hover {\n        cursor: pointer;\n        color: var(--gray-dark);\n        background-color: var(--gray-light);\n        border-color: var(--gray-dark);\n      }\n\n      .dropzone.dragover {\n        background-color: var(--gray-medium);\n      }\n\n      .range-slider {\n        appearance: none;\n        background-color: var(--gray-light);\n        height: 8px;\n        width: 100%;\n        border-radius: 10px;\n        outline: none;\n        margin-block: 15px;\n      }\n\n      .range-slider::-webkit-slider-thumb,\n      .range-slider::-moz-range-thumb {\n        appearance: none;\n        height: 16px;\n        width: 16px;\n        background-color: var(--blue-light);\n        border-radius: 50%;\n        cursor: pointer;\n        border: 0;\n      }\n\n      .switch {\n        position: relative;\n        display: inline-block;\n        width: 38px;\n        height: 20px;\n      }\n\n      .switch input {\n        outline: none;\n        display: none;\n      }\n\n      .switch-slider {\n        position: absolute;\n        cursor: pointer;\n        top: 0;\n        left: 0;\n        right: 0;\n        bottom: 0;\n        background-color: var(--gray-medium);\n        border-radius: 34px;\n        transition: 0.4s;\n      }\n\n      .switch-slider:before {\n        position: absolute;\n        content: \"\";\n        height: 14px;\n        width: 14px;\n        left: 3px;\n        bottom: 3px;\n        background-color: white;\n        border-radius: 50%;\n        transition: 0.4s;\n      }\n\n      input:checked + .switch-slider {\n        background-color: var(--blue-light);\n      }\n\n      input:focus + .switch-slider {\n        box-shadow: 0 0 1px var(--blue-light);\n      }\n\n      input:checked + .switch-slider:before {\n        transform: translateX(18px);\n      }\n\n      #toast-container {\n        position: fixed;\n        bottom: 20px;\n        right: 20px;\n        z-index: 9999;\n      }\n\n      .toast {\n        display: flex;\n        align-items: center;\n        width: auto;\n        padding: 6px 12px;\n        margin-top: 10px;\n        border-radius: 8px;\n        transform: translateY(30px);\n        opacity: 0;\n        visibility: hidden;\n      }\n\n      .toast .toast-body {\n        padding-block: 8px;\n        font-weight: 600;\n        color: var(--text);\n        letter-spacing: 0.5px;\n      }\n\n      .toast.success {\n        background-color: var(--success-medium);\n      }\n\n      .toast.error {\n        background-color: var(--error-light);\n      }\n\n      .toast.warning {\n        background-color: var(--warning-light);\n      }\n\n      .toast-progress {\n        position: absolute;\n        left: 4px;\n        bottom: 4px;\n        width: calc(100% - 8px);\n        height: 3px;\n        transform: scaleX(0);\n        transform-origin: left;\n        border-radius: 8px;\n      }\n\n      .toast.success .toast-progress {\n        background: linear-gradient(\n          to right,\n          var(--success-light),\n          var(--success-medium)\n        );\n      }\n\n      .toast.error .toast-progress {\n        background: linear-gradient(\n          to right,\n          var(--error-light),\n          var(--error-medium)\n        );\n      }\n\n      .toast.warning .toast-progress {\n        background: linear-gradient(\n          to right,\n          var(--warning-light),\n          var(--warning-medium)\n        );\n      }\n\n      .carousel {\n        display: flex;\n        height: 100%;\n        width: 100%;\n        cursor: pointer;\n      }\n\n      .button {\n        width: 100%;\n        border: 0;\n        padding: 10px 18px;\n        border-radius: 50px;\n        color: var(--text);\n        cursor: pointer;\n        margin-bottom: 10px;\n        background: var(--gray-medium);\n        border: 1px solid var(--gray-dark);\n        transition: all 0.5s ease-in-out;\n        font-size: 0.875rem;\n        font-weight: 600;\n      }\n\n      .button:hover {\n        background: var(--gray-dark);\n        border: 1px solid var(--gray-medium);\n      }\n\n      .button:last-child {\n        margin-bottom: 0;\n      }\n\n      .buttons {\n        display: flex;\n        flex-direction: column;\n        width: 100%;\n      }\n\n      #overlay {\n        position: fixed;\n        top: 0;\n        left: 0;\n        width: 100%;\n        height: 100%;\n        background-color: var(--overlay);\n        display: none;\n      }\n\n      #overlay.loading::after {\n        content: \"\";\n        display: block;\n        position: absolute;\n        top: calc(50% - 19px);\n        left: calc(50% - 19px);\n        width: 26px;\n        height: 26px;\n        border: 6px solid var(--gray-light);\n        border-top-color: var(--gray-dark);\n        border-radius: 50%;\n        animation: spin 1s linear infinite;\n      }\n\n      #recreatedImage {\n        margin-block: 20px;\n      }\n\n      .invalid {\n        border: 1px solid var(--error-dark) !important;\n      }\n\n      .error-message {\n        display: block;\n        color: var(--error-dark);\n        padding-block: 4px;\n        font-weight: 600;\n        font-size: 0.75rem;\n      }\n\n      @media (max-width: 767px) {\n        .row {\n          flex-wrap: wrap;\n          flex-direction: column;\n          margin: 0;\n        }\n\n        .col,\n        .col-full,\n        .col-small {\n          flex-basis: 100%;\n          margin-top: 20px;\n          padding: 0;\n        }\n      }\n\n      @keyframes spin {\n        to {\n          transform: rotate(360deg);\n        }\n      }\n\n      @keyframes progress {\n        to {\n          transform: scaleX(1);\n        }\n      }\n\n      @keyframes fadeIn {\n        5% {\n          opacity: 1;\n          visibility: visible;\n          transform: translateY(0);\n        }\n\n        95% {\n          opacity: 1;\n          transform: translateY(0);\n        }\n      }\n\n      @keyframes rainbow {\n        0% {\n          background-position: 0% 0;\n        }\n        100% {\n          background-position: 200% 0;\n        }\n      }\n    </style>\n  </head>\n  <body>\n    <div class=\"container\">\n      <div class=\"content\">\n        <form id=\"formGenerate\" novalidate>\n          <div class=\"header\">\n            <span class=\"title\"\n              >PIXEL <span class=\"rainbow\">MAGIC</span> TOOL</span\n            >\n            <span class=\"subtitle\"\n              >It is a tool that converts any image into code in\n              <strong>JSON WLED</strong> format for\n              <strong>2D Matrix</strong> panels</span\n            >\n          </div>\n          <div class=\"row\">\n            <div class=\"col\" validate>\n              <label for=\"hostname\">Hostname</label>\n              <input type=\"text\" name=\"hostname\" id=\"hostname\" required />\n            </div>\n            <div class=\"col\" validate>\n              <label for=\"name\">Preset Name</label>\n              <input\n                type=\"text\"\n                name=\"name\"\n                id=\"name\"\n                value=\"New Preset\"\n                required />\n            </div>\n          </div>\n          <div class=\"row\">\n            <div class=\"col\" validate>\n              <div class=\"custom-select\">\n                <label for=\"pattern\">Pattern</label>\n                <select name=\"pattern\" id=\"pattern\" required>\n                  <option value=\"1\" title=\"['ffffff']\">Individual</option>\n                  <option value=\"2\" title=\"[0, 'ffffff']\">Index</option>\n                  <option value=\"3\" title=\"[0, 5, 'ffffff']\" selected>\n                    Range\n                  </option>\n                </select>\n              </div>\n            </div>\n            <div class=\"col\" validate>\n              <div class=\"custom-select\">\n                <label for=\"output\">Output</label>\n                <select name=\"output\" id=\"output\" required>\n                  <option value=\"json\" selected>WLED JSON</option>\n                  <option value=\"ha\">Home Assistant</option>\n                  <option value=\"curl\">CURL</option>\n                </select>\n              </div>\n            </div>\n          </div>\n          <div class=\"row output\" style=\"display: none\">\n            <div class=\"col\" validate>\n              <label for=\"device\">Device</label>\n              <input type=\"text\" name=\"device\" id=\"device\" required />\n            </div>\n            <div class=\"col\" validate>\n              <label for=\"uniqueId\">Unique Id</label>\n              <input type=\"text\" name=\"uniqueId\" id=\"uniqueId\" required />\n            </div>\n            <div class=\"col\" validate>\n              <label for=\"friendlyName\">Friendly Name</label>\n              <input\n                type=\"text\"\n                name=\"friendlyName\"\n                id=\"friendlyName\"\n                required />\n            </div>\n          </div>\n          <div class=\"row\">\n            <div class=\"col\" validate>\n              <div class=\"custom-select\">\n                <label for=\"segments\">Segment Id</label>\n                <select name=\"segments\" id=\"segments\">\n                  <option value=\"0\" data-width=\"16\" data-height=\"16\">\n                    Segment 0\n                  </option>\n                </select>\n              </div>\n            </div>\n            <div class=\"col\" validate>\n              <label for=\"brightness\">Brightness</label>\n              <div class=\"input-group\">\n                <input\n                  type=\"range\"\n                  name=\"brightness\"\n                  id=\"brightness\"\n                  min=\"0\"\n                  max=\"255\"\n                  value=\"128\"\n                  class=\"range-slider\" />\n                <div class=\"input-description square\">\n                  <input\n                    type=\"text\"\n                    name=\"brightnessValue\"\n                    id=\"brightnessValue\"\n                    value=\"128\" />\n                </div>\n              </div>\n            </div>\n          </div>\n          <div class=\"row\">\n            <div class=\"col\" validate>\n              <label for=\"animation\">Animation</label>\n              <label class=\"switch\">\n                <input\n                  type=\"checkbox\"\n                  name=\"animation\"\n                  id=\"animation\"\n                  data-parent=\"animation\" />\n                <span class=\"switch-slider\"></span>\n              </label>\n            </div>\n            <div class=\"col\" validate>\n              <label for=\"transparentImage\">Transparent Image</label>\n              <label class=\"switch\">\n                <input\n                  type=\"checkbox\"\n                  name=\"transparentImage\"\n                  id=\"transparentImage\"\n                  data-parent=\"transparentImage\" />\n                <span class=\"switch-slider\"></span>\n              </label>\n            </div>\n            <div class=\"col\" validate>\n              <label for=\"resizeImage\">Resize Image</label>\n              <label class=\"switch\">\n                <input\n                  type=\"checkbox\"\n                  name=\"resizeImage\"\n                  id=\"resizeImage\"\n                  data-parent=\"resizeImage\"\n                  checked />\n                <span class=\"switch-slider\"></span>\n              </label>\n            </div>\n          </div>\n          <div class=\"row resizeImage\">\n            <div class=\"col\" validate>\n              <label for=\"width\">Width</label>\n              <input type=\"number\" name=\"width\" id=\"width\" value=\"16\" />\n            </div>\n            <div class=\"col\" validate>\n              <label for=\"height\">Height</label>\n              <input type=\"number\" name=\"height\" id=\"height\" value=\"16\" />\n            </div>\n          </div>\n          <div class=\"row animation\" style=\"display: none\">\n            <div class=\"col\" validate>\n              <label for=\"frames\">Frames</label>\n              <input type=\"number\" name=\"frames\" id=\"frames\" value=\"4\" />\n            </div>\n            <div class=\"col\" validate>\n              <label for=\"duration\">Duration</label>\n              <div class=\"input-group\">\n                <input\n                  type=\"number\"\n                  name=\"duration\"\n                  id=\"duration\"\n                  value=\"0.5\"\n                  required=\"required\"\n                  min=\"0\"\n                  step=\"0.1\"\n                  inputmode=\"numeric\" />\n                <div class=\"input-description\">\n                  <span>sec</span>\n                </div>\n              </div>\n            </div>\n            <div class=\"col\" validate>\n              <label for=\"transition\">Transition</label>\n              <div class=\"input-group\">\n                <input\n                  type=\"number\"\n                  name=\"transition\"\n                  id=\"transition\"\n                  value=\"0.2\"\n                  required=\"required\"\n                  min=\"0\"\n                  step=\"0.1\"\n                  inputmode=\"numeric\" />\n                <div class=\"input-description\">\n                  <span>sec</span>\n                </div>\n              </div>\n            </div>\n          </div>\n          <div class=\"row transparentImage\" style=\"display: none\">\n            <div class=\"col-full\" validate>\n              <label for=\"color\">Choose a color</label>\n              <small>\n                Color that will replace the\n                <strong>transparent pixels</strong> in the image\n              </small>\n              <input type=\"color\" name=\"color\" id=\"color\" value=\"#00BFFF\" />\n            </div>\n          </div>\n          <div class=\"row\">\n            <div class=\"col-full\" validate>\n              <div class=\"custom-select\">\n                <label for=\"images\">\n                  <span>Images upload to WLED</span>\n                  <a id=\"wledEdit\" href=\"http://[wled-ip]/edit\" target=\"_blank\">\n                    upload\n                  </a>\n                </label>\n                <select name=\"images\" id=\"images\">\n                  <option value=\"\">Select image</option>\n                </select>\n              </div>\n            </div>\n          </div>\n          <div id=\"dropzone\" class=\"dropzone\" validate>\n            <p id=\"dropzoneLabel\">\n              Drag and drop a file here or click to select a local file\n            </p>\n            <input\n              type=\"file\"\n              name=\"source\"\n              id=\"source\"\n              accept=\"image/*\"\n              style=\"display: none\" />\n          </div>\n          <div class=\"row\">\n            <div class=\"col-full\">\n              <button type=\"button\" class=\"button\" id=\"btnGenerate\">\n                Generate\n              </button>\n            </div>\n            <div class=\"col-small\" id=\"gbth\" style=\"display: none\">\n              <button\n                type=\"button\"\n                class=\"button\"\n                onclick=\"window.location.href = WLED_URL;\">\n                Back\n              </button>\n            </div>\n          </div>\n        </form>\n        <div id=\"preview\" style=\"display: none\">\n          <div id=\"recreatedImage\"></div>\n          <textarea name=\"response\" id=\"response\" rows=\"8\" readonly=\"readonly\">\n          </textarea>\n          <div class=\"buttons\">\n            <div class=\"row\">\n              <div class=\"col\">\n                <button type=\"button\" class=\"button\" id=\"btnCopyToClipboard\">\n                  Copy to Clipboard\n                </button>\n              </div>\n              <div class=\"col\">\n                <button type=\"button\" class=\"button\" id=\"btnSave\">Save</button>\n              </div>\n              <div class=\"col\">\n                <button type=\"button\" class=\"button\" id=\"btnDownloadPreset\">\n                  Download\n                </button>\n              </div>\n            </div>\n            <div class=\"row\" id=\"simulate\" style=\"display: none\">\n              <div class=\"col-full\">\n                <button type=\"button\" class=\"button\" id=\"btnSimulatePreset\">\n                  Simulate\n                </button>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n      <footer>\n        <a href=\"https://github.com/ajotanc/PixelMagicTool\" target=\"_blank\">\n          Github &copy; Pixel Magic Tool\n        </a>\n      </footer>\n    </div>\n    <div id=\"toast-container\"></div>\n    <div id=\"overlay\"></div>\n  </body>\n  <script>\n    const d = document;\n    const params = new URLSearchParams(window.location.search);\n    const host = params.get(\"hn\")\n      ? params.get(\"hn\")\n      : window.location.host\n      ? window.location.host\n      : \"0.0.0.0\";\n    const protocol =\n      window.location.protocol === \"file:\" ? \"http:\" : window.location.protocol;\n\n    let WLED_URL = `${protocol}//${host}`;\n\n    const hostname = gId(\"hostname\");\n    hostname.value = host;\n\n    hostname.addEventListener(\"blur\", async () => {\n      WLED_URL = `${protocol}//${hostname.value}`;\n\n      await segments();\n      await images();\n\n      hostnameLabel();\n    });\n\n    gId(\"gbth\").style.display =\n      window.location.protocol === \"http:\" ? \"flex\" : \"none\";\n\n    let jsonSaveWLED = [];\n    let jsonSendWLED = {};\n\n    (async function () {\n      await segments();\n      await images();\n\n      hostnameLabel();\n    })();\n\n    function gId(e) {return d.getElementById(e);}\n    function cE(e) {return d.createElement(e);}\n    function hostnameLabel() {\n      const link = gId(\"wledEdit\");\n      link.href = WLED_URL + \"/edit\";\n    }\n\n    async function playlist() {\n      const { value: duration } = gId(\"duration\");\n      const { value: transition } = gId(\"transition\");\n      const { value: name } = gId(\"name\");\n\n      const urlPreset = `${WLED_URL}/presets.json`;\n      const url = `${WLED_URL}/json`;\n\n      try {\n        const response = await fetch(urlPreset);\n        const data = await response.json();\n        const items = Object.keys(data);\n\n        const ps = items.filter(\n          (key) =>\n            typeof data[key] === \"object\" &&\n            !data[key].playlist &&\n            data[key].n &&\n            data[key].n.startsWith(name)\n        );\n\n        const id =\n          items.find(\n            (key) =>\n              typeof data[key] === \"object\" &&\n              data[key].playlist &&\n              data[key].n &&\n              data[key].n === name\n          ) || parseInt(items.pop()) + 1;\n\n        const body = {\n          psave: parseInt(id),\n          n: name,\n          on: true,\n          o: false,\n          playlist: {\n            ps,\n            dur: Array.from({ length: ps.length }, () => duration * 10),\n            transition: Array.from(\n              { length: ps.length },\n              () => transition * 10\n            ),\n            repeat: 0,\n            end: 0,\n          },\n        };\n\n        const options = {\n          method: \"POST\",\n          body: JSON.stringify(body),\n        };\n\n        try {\n          const response = await fetch(url, options);\n          const { success } = await response.json();\n\n          if (success) {\n            toast(`Playlist \"${name}\" save successfully`);\n          }\n        } catch (error) {\n          toast(`Error saving preset: ${error}`, \"error\");\n        }\n      } catch (error) {\n        toast(error, \"error\");\n      }\n    }\n\n    async function insert(data, isAnimated = false, delay = 5000) {\n      const urlPreset = `${WLED_URL}/presets.json`;\n      const url = `${WLED_URL}/json`;\n\n      let requestsCompleted = 0;\n\n      show();\n\n      const promises = data.map(async (item) => {\n        return new Promise((resolve, reject) => {\n          setTimeout(async () => {\n            try {\n              const response = await fetch(urlPreset);\n              const data = await response.json();\n              const items = Object.keys(data);\n\n              const id =\n                items.find(\n                  (key) =>\n                    typeof data[key] === \"object\" &&\n                    !data[key].playlist &&\n                    data[key].n === item.n\n                ) || parseInt(items.pop()) + 1;\n\n              const body = Object.assign(item, { psave: parseInt(id) });\n              const options = {\n                method: \"POST\",\n                body: JSON.stringify(body),\n              };\n\n              try {\n                const response = await fetch(url, options);\n                const { success } = await response.json();\n\n                if (success) {\n                  toast(`Preset \"${item.n}\" save successfully`);\n                  window.parent.postMessage(\"loadPresets\", WLED_URL);\n                }\n              } catch (error) {\n                toast(`Error saving preset: ${error}`, \"error\");\n              }\n            } catch (error) {\n              toast(error, \"error\");\n            } finally {\n              resolve();\n            }\n          }, delay * requestsCompleted++);\n        });\n      });\n\n      await Promise.all(promises);\n\n      if (isAnimated) {\n        setTimeout(async () => {\n          await playlist()\n            .then(() => {\n              hide();\n            })\n            .finally(() => {\n              hide();\n            });\n        }, delay);\n      } else {\n        hide();\n      }\n    }\n\n    async function images() {\n      const url = `${WLED_URL}/edit?list=/`;\n      const select = gId(\"images\");\n\n      show();\n\n      try {\n        const response = await fetch(url);\n        const data = await response.json();\n        const mimeTypes = [\"jpeg\", \"jpg\", \"png\", \"gif\"];\n\n        const images = data.filter((file) => {\n          const { name } = file;\n          const [filename, mimetype] = name.split(\".\");\n          file.name = name.replace(\"/\", \"\");\n          return mimeTypes.includes(mimetype);\n        });\n\n        const options = [{ text: \"Select image\", value: \"\" }];\n\n        if (images.length > 0) {\n          options.push(\n            ...images.map(({ name }) => ({\n              text: name,\n              value: `${WLED_URL}/${name}`,\n            }))\n          );\n\n          select.innerHTML = \"\";\n\n          options.forEach(({ text, value }) => {\n            const option = new Option(text, value);\n\n            if (index === 0) {\n              option.selected = true;\n            }\n\n            select.appendChild(option);\n          });\n        }\n      } catch (error) {\n        toast(error, \"error\");\n      } finally {\n        hide();\n      }\n    }\n\n    async function segments() {\n      const select = gId(\"segments\");\n      const width = gId(\"width\");\n      const height = gId(\"height\");\n\n      show();\n\n      try {\n        const url = `${WLED_URL}/json/state`;\n        const response = await fetch(url);\n        const { seg: segments } = await response.json();\n\n        const options = [\n          { text: \"Segment Default\", value: 0, width: 16, height: 16 },\n        ];\n\n        if (segments) {\n          options.splice(0);\n          options.push(\n            ...segments.map(({ id, n, start, stop, startY, stopY }) => ({\n              text: n ? n : `Segment ${id}`,\n              value: id,\n              width: stop - start,\n              height: stopY - startY,\n            }))\n          );\n        }\n\n        select.innerHTML = \"\";\n\n        options.forEach(({ text, value, width: w, height: h }, index) => {\n          const option = new Option(text, value);\n          option.dataset.width = w;\n          option.dataset.height = h;\n\n          if (index === 0) {\n            option.selected = true;\n            width.value = w;\n            height.value = h;\n          }\n\n          select.add(option);\n        });\n      } catch (error) {\n        toast(error, \"error\");\n      } finally {\n        hide();\n      }\n    }\n\n    gId(\"dropzone\").addEventListener(\"dragover\", (e) => {\n      e.preventDefault();\n    });\n\n    gId(\"dropzone\").addEventListener(\"drop\", (e) => {\n      e.preventDefault();\n\n      source.files = e.dataTransfer.files;\n\n      const { name } = source.files[0];\n      gId(\"dropzoneLabel\").textContent = `Image ${name} selected!`;\n\n      validate(e);\n    });\n\n    gId(\"dropzone\").addEventListener(\"click\", () => {\n      source.click();\n    });\n\n    gId(\"source\").addEventListener(\"change\", (e) => {\n      const dropzoneLabel = gId(\"dropzoneLabel\");\n      const { value } = e.target;\n\n      if (value) {\n        const { name } = e.target.files[0];\n        dropzoneLabel.textContent = `Image ${name} selected!`;\n      } else {\n        dropzoneLabel.textContent =\n          \"Drag and drop a file here or click to select a file\";\n      }\n\n      validate(e);\n    });\n\n    gId(\"btnSimulatePreset\").addEventListener(\"click\", async () => {\n      const url = `${WLED_URL}/json/state`;\n\n      const options = {\n        method: \"POST\",\n        body: JSON.stringify(jsonSendWLED),\n      };\n\n      show();\n\n      try {\n        const response = await fetch(url, options);\n        const { success } = await response.json();\n\n        if (success) {\n          toast(\"Successfully simulated preset\");\n        }\n      } catch (error) {\n        toast(error, \"error\");\n      } finally {\n        hide();\n      }\n    });\n\n    gId(\"btnSave\").addEventListener(\"click\", async () => {\n      const { checked } = gId(\"animation\");\n      const { value: name } = gId(\"name\");\n\n      if (checked) {\n        await insert(jsonSaveWLED, true);\n      } else {\n        jsonSaveWLED.splice(0);\n\n        jsonSaveWLED.push(\n          Object.assign({}, jsonSendWLED, { n: name, o: false })\n        );\n\n        await insert(jsonSaveWLED);\n      }\n    });\n\n    gId(\"btnCopyToClipboard\").addEventListener(\"click\", async () => {\n      const response = gId(\"response\");\n\n      response.select();\n\n      try {\n        await navigator.clipboard.writeText(response.value);\n        toast(\"Text copied to clipboard\");\n      } catch (error) {\n        try {\n          await d.execCommand(\"copy\");\n          toast(\"Text copied to clipboard\");\n        } catch (error) {\n          toast(error, \"error\");\n        }\n      }\n    });\n\n    gId(\"btnDownloadPreset\").addEventListener(\"click\", () => {\n      const { value: response } = gId(\"response\");\n      const { value: output } = gId(\"output\");\n\n      const timestamp = new Date().getTime();\n      const filename = `WLED_${timestamp}`;\n\n      downloadFile(response, filename, output);\n    });\n\n    gId(\"segments\").addEventListener(\"change\", (e) => {\n      const { width, height } = e.target.selectedOptions[0].dataset;\n\n      gId(\"width\").value = w;\n      gId(\"height\").value = h;\n    });\n\n    gId(\"output\").addEventListener(\"change\", (e) => {\n      const { value } = e.target.selectedOptions[0];\n\n      d.querySelector(\".output\").style.display =\n        value === \"ha\" ? \"flex\" : \"none\";\n    });\n\n    gId(\"brightnessValue\").addEventListener(\"input\", (e) => {\n      const { value } = e.target;\n      const bri = value <= 255 ? value : 255;\n\n      gId(\"brightness\").value = bri;\n      e.target.value = bri;\n    });\n\n    gId(\"brightness\").addEventListener(\"input\", (e) => {\n      const { value } = e.target;\n\n      gId(\"brightnessValue\").value = value;\n    });\n\n    gId(\"images\").addEventListener(\"change\", (e) => {\n      const dropzone = gId(\"dropzone\");\n      const { value } = e.target.selectedOptions[0];\n\n      if (!value) {\n        const source = gId(\"source\");\n\n        gId(\"dropzoneLabel\").textContent =\n          \"Drag and drop a file here or click to select a local file\";\n        source.value = \"\";\n\n        dropzone.style.display = \"block\";\n      } else {\n        dropzone.style.display = \"none\";\n      }\n    });\n\n    gId(\"transparentImage\").addEventListener(\"change\", (e) => {\n      const { checked } = e.target;\n\n      d.querySelector(\".transparentImage\").style.display = checked\n        ? \"flex\"\n        : \"none\";\n    });\n\n    gId(\"resizeImage\").addEventListener(\"change\", (e) => {\n      const { checked } = e.target;\n\n      d.querySelector(\".resizeImage\").style.display = checked ? \"flex\" : \"none\";\n    });\n\n    gId(\"animation\").addEventListener(\"change\", (e) => {\n      const animation = d.querySelector(\".animation\");\n      const source = gId(\"source\");\n\n      const { checked } = e.target;\n\n      if (checked) {\n        toast(\n          'If you want all frames in the image, set it to \"0\"',\n          \"warning\",\n          5000\n        );\n\n        source.setAttribute(\"accept\", \"image/gif\");\n        animation.style.display = \"flex\";\n      } else {\n        source.setAttribute(\"accept\", \"image/*\");\n        animation.style.display = \"none\";\n      }\n    });\n\n    gId(\"btnGenerate\").addEventListener(\"click\", async (event) => {\n      const { checked } = gId(\"animation\");\n\n      if (validate(event)) {\n        jsonSaveWLED.splice(0);\n\n        gId(\"preview\").style.display = \"block\";\n        gId(\"recreatedImage\").innerHTML = \"\";\n\n        const simulate = gId(\"simulate\");\n\n        if (checked) {\n          simulate.style.display = \"none\";\n          await generateAnimation();\n        } else {\n          simulate.style.display = \"flex\";\n          await generate();\n        }\n      }\n    });\n\n    async function createObjectURL(url) {\n      return fetch(url)\n        .then((response) => response.arrayBuffer())\n        .then((buffer) => {\n          const binaryData = new Uint8Array(buffer);\n          const base64 = btoa(String.fromCharCode(...binaryData));\n          return `data:image/png;base64,${base64}`;\n        });\n    }\n\n    function loadImage(url) {\n      return new Promise((resolve, reject) => {\n        const img = new Image();\n        img.onload = () => resolve(img);\n        img.onerror = (error) => reject(error);\n        img.src = url;\n      });\n    }\n\n    async function generate() {\n      const file = gId(\"source\").files[0];\n\n      const { value: images } = gId(\"images\");\n      const { value: output } = gId(\"output\");\n\n      show();\n\n      try {\n        const response = gId(\"response\");\n        const recreatedImage = gId(\"recreatedImage\");\n\n        const urlImage = !images\n          ? URL.createObjectURL(file)\n          : await createObjectURL(images);\n\t\t\t\t\t\n        const image = await loadImage(urlImage);\n        const { canvas, bri, id, i } = recreate(image);\n\n        await new Promise((resolve) => {\n          Object.assign(jsonSendWLED, {\n            on: true,\n            bri,\n            seg: {\n              id,\n              i,\n            },\n          });\n\n          resolve();\n        });\n\n        const jsonData = JSON.stringify(jsonSendWLED);\n        const dataResponse = formatOutput(output, jsonData);\n\n        response.value = dataResponse;\n        recreatedImage.appendChild(canvas);\n      } catch (error) {\n        toast(error, \"error\");\n      } finally {\n        hide();\n      }\n    }\n\n    async function generateAnimation() {\n      const file = gId(\"source\").files[0];\n      const images = gId(\"images\");\n      const response = gId(\"response\");\n\n      const { value: presetName } = gId(\"name\");\n      const { value: amount } = gId(\"frames\");\n      const { value: output } = gId(\"output\");\n      const { value: duration } = gId(\"duration\");\n\n      const { text: imageName, value: imageValue } = images.selectedOptions[0];\n\n      show();\n\n      try {\n        const body = new FormData();\n\n        if (!imageValue) {\n          body.append(\"image\", file);\n        } else {\n          const responseImage = await fetch(imageValue);\n          const blob = await responseImage.blob();\n\n          const file = new File([blob], imageName, { type: blob.type });\n\n          body.append(\"image\", file);\n        }\n\n        const responseFrames = await fetch(\n          `https://pixelmagictool.vercel.app/api/wled/frames?amount=${amount}`,\n          {\n            method: \"POST\",\n            body,\n          }\n        );\n\n        const { message, frames } = await responseFrames.json();\n\n        if (!responseFrames.ok) {\n          toast(message, \"error\");\n          return;\n        }\n\n        jsonSaveWLED = [];\n\n        const texteares = [];\n        const canvases = [];\n\n        const delay = duration * 1000;\n\n        const promises = frames.map(async (frame, index) => {\n          const image = await loadImage(frame);\n          const { canvas, bri, id, i } = recreate(image);\n          const jsonData = {\n            on: true,\n            bri,\n            seg: { id, i },\n          };\n\n          const n = `${presetName} ${index + 1}`;\n\n          texteares.push(`${n}|${JSON.stringify(jsonData)}`);\n          canvases.push(canvas);\n\n          jsonSaveWLED.push(Object.assign(jsonData, { n, o: true }));\n        });\n\n        await Promise.all(promises);\n\n        const dataResponse = texteares.map((item) => {\n          const [presetName, json] = item.split(\"|\");\n          const outputFormatted = formatOutput(output, json);\n          const comment = [\"ha\", \"curl\"].includes(output) ? \"#\" : \"//\";\n\n          return `${comment} ${presetName}\\n${outputFormatted}`;\n        });\n\n        response.value = dataResponse.join(\"\\n\");\n        toast(message);\n        carousel(\"recreatedImage\", canvases, delay);\n      } catch (error) {\n        toast(error, \"error\");\n      } finally {\n        hide();\n      }\n    }\n\n    function recreate(image) {\n      const { value: pattern } = gId(\"pattern\");\n      const { value: segmentId } = gId(\"segments\");\n      const { value: brightness } = gId(\"brightness\");\n      const { value: inputWidth } = gId(\"width\");\n      const { value: inputHeight } = gId(\"height\");\n      const { checked: resizeImage } = gId(\"resizeImage\");\n\n      const resizeWidth = parseInt(inputWidth);\n      const resizeHeight = parseInt(inputHeight);\n\n      const { width: dataWidth, height: dataHeight } =\n        gId(\"segments\").selectedOptions[0].dataset;\n\n      const segmentWidth = parseInt(dataWidth);\n      const segmentHeight = parseInt(dataHeight);\n\n      const imgWidth = image.naturalWidth;\n      const imgHeight = image.naturalHeight;\n\n      const width = resizeImage ? resizeWidth : imgWidth;\n      const height = resizeImage ? resizeHeight : imgHeight;\n\n      const overallWidth = resizeImage ? segmentWidth : width;\n      const overallHeight = resizeImage ? segmentHeight : height;\n\n      const startX = Math.floor((segmentWidth - width) / 2);\n      const startY = Math.floor((segmentHeight - height) / 2);\n\n      const pixelsRef = 48;\n\n      const canvasWidth = overallWidth * pixelsRef;\n      const canvasHeight = overallHeight * pixelsRef;\n\n      const fontSize = canvasWidth <= 768 ? 14 : 18;\n\n      const colors = [];\n\n      const { ctx: reference } = createCanvas(width, height);\n\n      reference.drawImage(image, 0, 0, width, height);\n\n      const imageData = reference.getImageData(0, 0, width, height);\n      const pixels = imageData.data;\n\n      const { canvas, ctx } = createCanvas(canvasWidth, canvasHeight);\n\n      ctx.fillStyle = \"#000000\";\n      ctx.fillRect(0, 0, canvasWidth, canvasHeight);\n\n      for (let h = 0; h < overallHeight; h++) {\n        for (let w = 0; w < overallWidth; w++) {\n          let pixelId = h * overallWidth + w + 1;\n\n          let coordinateX = w * pixelsRef;\n          let coordinateY = h * pixelsRef;\n\n          let pixelX = w - startX;\n          let pixelY = h - startY;\n\n          let hex = \"000000\";\n\n          if (\n            (resizeImage &&\n              pixelX >= 0 &&\n              pixelX < width &&\n              pixelY >= 0 &&\n              pixelY < height) ||\n            (!resizeImage && w < width && h < height)\n          ) {\n            let index = resizeImage ? pixelY * width + pixelX : h * width + w;\n\n            let red = pixels[index * 4];\n            let green = pixels[index * 4 + 1];\n            let blue = pixels[index * 4 + 2];\n            let alpha = pixels[index * 4 + 3];\n\n            hex = pixelColor(red, green, blue, alpha);\n            let { r, g, b } = hexToRgb(hex);\n\n            colors.push(hex);\n\n            ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;\n            ctx.fillRect(coordinateX, coordinateY, pixelsRef, pixelsRef);\n          } else {\n            colors.push(\"000000\");\n          }\n\n          ctx.lineWidth = 1;\n          ctx.strokeStyle = \"#111111\";\n          ctx.strokeRect(coordinateX, coordinateY, pixelsRef, pixelsRef);\n\n          let offsetX = coordinateX + pixelsRef / 2;\n          let offsetY = coordinateY + pixelsRef / 2;\n\n          ctx.font = `${fontSize}px Arial`;\n          ctx.fillStyle = hex === \"000000\" ? \"#eeeeee\" : \"#111111\";\n          ctx.textAlign = \"center\";\n          ctx.textBaseline = \"middle\";\n          ctx.fillText(pixelId, offsetX, offsetY);\n        }\n      }\n\n      switch (pattern) {\n        case \"1\":\n          i = colors;\n          break;\n        case \"2\":\n          i = index(colors);\n          break;\n        case \"3\":\n          i = range(colors);\n          break;\n      }\n\n      return {\n        canvas,\n        bri: parseInt(brightness),\n        id: parseInt(segmentId),\n        i,\n      };\n    }\n\n    function range(colors) {\n      let startIndex = 0;\n      let endIndex = 0;\n      let currentColor = colors[0];\n      let pattern = [];\n\n      colors.forEach((color, index) => {\n        if (color !== currentColor) {\n          endIndex = index;\n          const repetitions = endIndex - startIndex;\n\n          if (repetitions == 1) {\n            pattern.push(currentColor);\n          } else {\n            pattern.push(startIndex, endIndex, currentColor);\n          }\n          startIndex = index;\n        }\n        currentColor = color;\n      });\n\n      const lastRepetition = colors.length - startIndex;\n\n      if (lastRepetition === 1) {\n        pattern.push(currentColor);\n      } else {\n        pattern.push(startIndex, colors.length, currentColor);\n      }\n\n      return pattern;\n    }\n\n    function pixelColor(r, g, b, a) {\n      const { checked } = gId(\"transparentImage\");\n      const { value } = gId(\"color\");\n\n      if (a === 0) {\n        if (checked) {\n          return value.replace(\"#\", \"\").toLowerCase();\n        }\n\n        return rgbToHex(255, 255, 255);\n      }\n\n      return rgbToHex(r, g, b);\n    }\n\n    function hexToRgb(hex) {\n      hex = hex.replace(\"#\", \"\");\n\n      if (hex.length === 3) {\n        hex = hex.replace(/(.)/g, \"$1$1\");\n      }\n\n      const [r, g, b] = hex\n        .match(/.{2}/g)\n        .map((component) => parseInt(component, 16));\n\n      return { r, g, b };\n    }\n\n    function rgbToHex(r, g, b) {\n      const hex = ((r << 16) | (g << 8) | b).toString(16);\n      return (\"000000\" + hex).slice(-6);\n    }\n\n    function index(colors) {\n      const pattern = [];\n      colors.forEach((led, index) => {\n        pattern.push(index, led);\n      });\n\n      return pattern;\n    }\n\n    function createCanvas(width, height) {\n      const canvas = cE(\"canvas\");\n\n      canvas.width = width;\n      canvas.height = height;\n\n      const ctx = canvas.getContext(\"2d\", { willReadFrequently: true });\n\n      ctx.imageSmoothingQuality = \"high\";\n      ctx.imageSmoothingEnabled = false;\n\n      return { canvas, ctx };\n    }\n\n    function formatOutput(output, json) {\n      return output === \"ha\"\n        ? yaml(json)\n        : output === \"curl\"\n        ? curl(json)\n        : json;\n    }\n\n    function downloadFile(text, filename, output) {\n      let mimeType;\n      let fileExtension;\n      let response;\n\n      switch (output) {\n        case \"json\":\n          mimeType = \"application/json\";\n          fileExtension = \"json\";\n          break;\n        case \"ha\":\n          mimeType = \"application/x-yaml\";\n          fileExtension = \"yaml\";\n          break;\n        case \"curl\":\n          mimeType = \"text/plain\";\n          fileExtension = \"txt\";\n          break;\n      }\n\n      const blob = new Blob([text], { type: mimeType });\n      const url = URL.createObjectURL(blob);\n\n      const anchorElement = cE(\"a\");\n      anchorElement.href = url;\n      anchorElement.download = `${filename}.${fileExtension}`;\n\n      anchorElement.click();\n\n      URL.revokeObjectURL(url);\n    }\n\n    function yaml(jsonData) {\n      const { value: device } = gId(\"device\");\n      const { value: friendly_name } = gId(\"friendlyName\");\n      const { value: unique_id } = gId(\"uniqueId\");\n      const { value: hostname } = gId(\"hostname\");\n\n      if (device) {\n        const yamlData = {\n          switches: {\n            [device]: {\n              friendly_name,\n              unique_id,\n              command_on: `curl -X POST \"http://${hostname}/json/state\" -d '${jsonData}' -H \"Content-Type: application/json\"`,\n              command_off: `curl -X POST \"http://${hostname}/json/state\" -d '{\"on\":false}' -H \"Content-Type: application/json\"`,\n            },\n          },\n        };\n\n        return convertToYaml(yamlData);\n      }\n    }\n\n    function curl(jsonData) {\n      const { value: hostname } = gId(\"hostname\");\n      return `curl -X POST \"http://${hostname}/json/state\" -d '${jsonData}' -H \"Content-Type: application/json\"`;\n    }\n\n    function convertToYaml(obj) {\n      function processValue(value, indentationLevel) {\n        if (typeof value === \"object\" && !Array.isArray(value)) {\n          return processObject(value, indentationLevel + 1);\n        } else {\n          return value;\n        }\n      }\n\n      function processObject(obj, indentationLevel = 0) {\n        const indent = \"  \".repeat(indentationLevel);\n        const lines = Object.entries(obj).map(([key, value]) => {\n          if (typeof value === \"object\" && !Array.isArray(value)) {\n            return `${indent}${key}:\\n${processObject(\n              value,\n              indentationLevel + 1\n            )}`;\n          } else {\n            return `${indent}${key}: ${processValue(value, indentationLevel)}`;\n          }\n        });\n\n        return lines.join(\"\\n\");\n      }\n\n      return processObject(obj);\n    }\n\n    function toast(\n      message,\n      type = \"success\",\n      duration = 2000,\n      hideElement = \"preview\"\n    ) {\n      const hide = gId(hideElement);\n      const toast = cE(\"div\");\n      const wait = 100;\n\n      toast.style.animation = \"fadeIn\";\n      toast.style.animationDuration = `${duration + wait}ms`;\n      toast.style.animationTimingFunction = \"linear\";\n\n      toast.classList.add(\"toast\", type);\n\n      const body = cE(\"span\");\n      body.classList.add(\"toast-body\");\n\n      body.textContent = message;\n\n      toast.appendChild(body);\n\n      const progress = cE(\"div\");\n      progress.classList.add(\"toast-progress\");\n\n      progress.style.animation = \"progress\";\n      progress.style.animationDuration = `${duration + wait}ms`;\n      progress.style.animationDelay = \"0s\";\n\n      toast.appendChild(progress);\n\n      gId(\"toast-container\").appendChild(toast);\n\n      setTimeout(() => {\n        toast.style.opacity = 0;\n        setTimeout(() => {\n          toast.remove();\n        }, wait);\n      }, duration);\n\n      if (type === \"error\") {\n        hide.style.display = \"none\";\n      }\n    }\n\n    function carousel(id, images, delay = 3000) {\n      let index = 0;\n      const carousel = cE(\"div\");\n      carousel.classList.add(\"carousel\");\n\n      images.forEach((canvas, i) => {\n        if (i === index) {\n          carousel.appendChild(canvas);\n        } else {\n          canvas.style.display = \"none\";\n          carousel.appendChild(canvas);\n        }\n      });\n\n      const container = gId(id);\n      container.innerHTML = \"\";\n      container.appendChild(carousel);\n\n      function next() {\n        images[index].style.display = \"none\";\n        index++;\n        if (index >= images.length) {\n          index = 0;\n        }\n\n        images[index].style.display = \"block\";\n        loop();\n      }\n\n      function previous() {\n        images[index].style.display = \"none\";\n        index--;\n        if (index < 0) {\n          index = images.length - 1;\n        }\n\n        images[index].style.display = \"block\";\n        loop();\n      }\n\n      let timeoutId;\n      function loop() {\n        clearTimeout(timeoutId);\n        timeoutId = setTimeout(() => {\n          next();\n          if (index === 0) {\n            carousel.scrollTo(0, 0);\n          }\n        }, delay);\n      }\n      loop();\n\n      carousel.addEventListener(\"mouseover\", () => {\n        clearTimeout(timeoutId);\n      });\n\n      carousel.addEventListener(\"mouseout\", () => {\n        loop();\n      });\n    }\n\n    function show() {\n      const overlay = gId(\"overlay\");\n      overlay.classList.add(\"loading\");\n      overlay.style.display = \"block\";\n      overlay.style.cursor = \"not-allowed\";\n\n      d.body.style.overflow = \"hidden\";\n    }\n\n    function hide() {\n      const overlay = gId(\"overlay\");\n      overlay.classList.remove(\"loading\");\n      overlay.style.display = \"none\";\n      overlay.style.cursor = \"default\";\n\n      d.body.style.overflow = \"auto\";\n    }\n\n    function validate(event) {\n      event.preventDefault();\n\n      const form = gId(\"formGenerate\");\n      const inputs = form.querySelectorAll(\"input, select, textarea\");\n\n      let isValid = true;\n\n      const browserLanguage = navigator.language || navigator.userLanguage;\n      const messageRequired =\n        browserLanguage === \"pt-br\"\n          ? \"Este campo é obrigatório\"\n          : \"This field is required\";\n\n      inputs.forEach((input) => {\n        const parent = input.closest(\"[validate]\");\n\n        if (!parent) {\n          return;\n        }\n\n        let isVisible = true;\n\n        let tempParent = parent;\n        while (tempParent !== d.body) {\n          const parentStyles = window.getComputedStyle(tempParent);\n          if (\n            parentStyles.display === \"none\" ||\n            parentStyles.display === \"hidden\"\n          ) {\n            isVisible = false;\n            break;\n          }\n          tempParent = tempParent.parentNode;\n        }\n\n        if (\n          isVisible &&\n          (!input.checkValidity() ||\n            (input.type === \"file\" && input.files.length === 0))\n        ) {\n          input.classList.add(\"invalid\");\n\n          const errorMessage =\n            input.dataset.errorMessage ||\n            input.validationMessage ||\n            messageRequired;\n\n          let errorElement = parent.querySelector(\".error-message\");\n\n          if (!errorElement) {\n            errorElement = cE(\"div\");\n            errorElement.classList.add(\"error-message\");\n            parent.appendChild(errorElement);\n          }\n\n          errorElement.innerText = errorMessage;\n          isValid = false;\n        } else {\n          input.classList.remove(\"invalid\");\n          const errorElement = parent.querySelector(\".error-message\");\n\n          if (errorElement) {\n            parent.removeChild(errorElement);\n          }\n        }\n      });\n\n      return isValid;\n    }\n  </script>\n</html>"
  },
  {
    "path": "wled00/data/rangetouch.js",
    "content": "// ==========================================================================\n// rangetouch.js v2.0.1\n// Making <input type=\"range\"> work on touch devices\n// https://github.com/sampotts/rangetouch\n// License: The MIT License (MIT)\n// ==========================================================================\n!function(e,t){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=t():\"function\"==typeof define&&define.amd?define(\"RangeTouch\",t):(e=e||self).RangeTouch=t()}(this,(function(){\"use strict\";function e(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,\"value\"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function t(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function n(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function r(e){for(var r=1;r<arguments.length;r++){var i=null!=arguments[r]?arguments[r]:{};r%2?n(Object(i),!0).forEach((function(n){t(e,n,i[n])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(i)):n(Object(i)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(i,t))}))}return e}var i={addCSS:!0,thumbWidth:15,watch:!0};function u(e,t){return function(){return Array.from(document.querySelectorAll(t)).includes(this)}.call(e,t)}var o=function(e){return null!=e?e.constructor:null},c=function(e,t){return!!(e&&t&&e instanceof t)},l=function(e){return null==e},a=function(e){return o(e)===Object},s=function(e){return o(e)===String},f=function(e){return Array.isArray(e)},h=function(e){return c(e,NodeList)},d=s,y=f,b=h,m=function(e){return c(e,Element)},g=function(e){return c(e,Event)},p=function(e){return l(e)||(s(e)||f(e)||h(e))&&!e.length||a(e)&&!Object.keys(e).length};function v(e,t){if(1>t){var n=function(e){var t=\"\".concat(e).match(/(?:\\.(\\d+))?(?:[eE]([+-]?\\d+))?$/);return t?Math.max(0,(t[1]?t[1].length:0)-(t[2]?+t[2]:0)):0}(t);return parseFloat(e.toFixed(n))}return Math.round(e/t)*t}return function(){function t(e,n){(function(e,t){if(!(e instanceof t))throw new TypeError(\"Cannot call a class as a function\")})(this,t),m(e)?this.element=e:d(e)&&(this.element=document.querySelector(e)),m(this.element)&&p(this.element.rangeTouch)&&(this.config=r({},i,{},n),this.init())}return n=t,c=[{key:\"setup\",value:function(e){var n=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},o=null;if(p(e)||d(e)?o=Array.from(document.querySelectorAll(d(e)?e:'input[type=\"range\"]')):m(e)?o=[e]:b(e)?o=Array.from(e):y(e)&&(o=e.filter(m)),p(o))return null;var c=r({},i,{},n);if(d(e)&&c.watch){var l=new MutationObserver((function(n){Array.from(n).forEach((function(n){Array.from(n.addedNodes).forEach((function(n){m(n)&&u(n,e)&&new t(n,c)}))}))}));l.observe(document.body,{childList:!0,subtree:!0})}return o.map((function(e){return new t(e,n)}))}},{key:\"enabled\",get:function(){return\"ontouchstart\"in document.documentElement}}],(o=[{key:\"init\",value:function(){t.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\"none\",this.element.style.webKitUserSelect=\"none\",this.element.style.touchAction=\"manipulation\"),this.listeners(!0),this.element.rangeTouch=this)}},{key:\"destroy\",value:function(){t.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\"\",this.element.style.webKitUserSelect=\"\",this.element.style.touchAction=\"\"),this.listeners(!1),this.element.rangeTouch=null)}},{key:\"listeners\",value:function(e){var t=this,n=e?\"addEventListener\":\"removeEventListener\";[\"touchstart\",\"touchmove\",\"touchend\"].forEach((function(e){t.element[n](e,(function(e){return t.set(e)}),!1)}))}},{key:\"get\",value:function(e){if(!t.enabled||!g(e))return null;var n,r=e.target,i=e.changedTouches[0],u=parseFloat(r.getAttribute(\"min\"))||0,o=parseFloat(r.getAttribute(\"max\"))||100,c=parseFloat(r.getAttribute(\"step\"))||1,l=r.getBoundingClientRect(),a=100/l.width*(this.config.thumbWidth/2)/100;return 0>(n=100/l.width*(i.clientX-l.left))?n=0:100<n&&(n=100),50>n?n-=(100-2*n)*a:50<n&&(n+=2*(n-50)*a),u+v(n/100*(o-u),c)}},{key:\"set\",value:function(e){t.enabled&&g(e)&&!e.target.disabled&&(e.preventDefault(),e.target.value=this.get(e),function(e,t){if(e&&t){var n=new Event(t,{bubbles:!0});e.dispatchEvent(n)}}(e.target,\"touchend\"===e.type?\"change\":\"input\"))}}])&&e(n.prototype,o),c&&e(n,c),t;var n,o,c}()}));\n//# sourceMappingURL=rangetouch.js.map\n"
  },
  {
    "path": "wled00/data/settings.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"UTF-8\">\n\t<meta content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\" name=\"viewport\">\n\t<title>WLED Settings</title>\n\t<script>\n\t\t// load common.js with retry on error\n\t\t(function loadFiles() {\n\t\t\tconst l = document.createElement('script');\n\t\t\tl.src = 'common.js';\n\t\t\t// load style.css then initialize\n\t\t\tl.onload = () => loadResources(['style.css'], () => {\n\t\t\t\tgetLoc();\n\t\t\t\tloadJS(getURL('/settings/s.js?p=0'), false);\n\t\t\t});\n\t\t\tl.onerror = () => setTimeout(loadFiles, 100);\n\t\t\tdocument.head.appendChild(l);\n\t\t})();\n\t</script>\n\t<style>\n\t  @import url(\"style.css\");\n\t\tbody {\n\t\t\theight: 100px;\n\t\t\tmargin: 0;\n\t\t}\n\t\thtml {\n\t\t\t--h: 9vh;\n\t\t}\n\t\tbutton {\n\t\t\tdisplay: block;\n\t\t\tborder-radius: var(--h);\n\t\t\tfont-size: 6vmin;\n\t\t\theight: var(--h);\n\t\t\twidth: calc(100% - 40px);\n\t\t\tmargin: 2vh auto 0;\n\t\t\tpadding: 0;\n\t\t}\n\t</style>\n</head>\n<body>\n<button type=submit id=\"b\" onclick=\"window.location=getURL('/')\">Back</button>\n<button type=\"submit\" onclick=\"window.location=getURL('/settings/wifi')\">WiFi & Network</button>\n<button type=\"submit\" onclick=\"window.location=getURL('/settings/leds')\">LED & Hardware</button>\n<button type=\"submit\" onclick=\"window.location=getURL('/settings/pins')\">Pin Info</button>\n<button id=\"2dbtn\" type=\"submit\" onclick=\"window.location=getURL('/settings/2D')\">2D Configuration</button>\n<button type=\"submit\" onclick=\"window.location=getURL('/settings/ui')\">User Interface</button>\n<button id=\"dmxbtn\" style=\"display:none;\" type=\"submit\" onclick=\"window.location=getURL('/settings/dmx')\">DMX Output</button>\n<button type=\"submit\" onclick=\"window.location=getURL('/settings/sync')\">Sync Interfaces</button>\n<button type=\"submit\" onclick=\"window.location=getURL('/settings/time')\">Time & Macros</button>\n<button type=\"submit\" onclick=\"window.location=getURL('/settings/um')\">Usermods</button>\n<button type=\"submit\" onclick=\"window.location=getURL('/settings/sec')\">Security & Updates</button>\n</body>\n</html>"
  },
  {
    "path": "wled00/data/settings_2D.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"utf-8\">\n\t<meta content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\" name=\"viewport\">\n\t<title>2D Set-up</title>\n\t<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->\n\t<script>\n\tvar maxPanels=64;\n\tvar ctx = null;\n\tfunction fS(){d.Sf.submit();} // <button type=submit> sometimes didn't work\n\t// load common.js with retry on error\n\t(function loadFiles() {\n\t\tconst l = document.createElement('script');\n\t\tl.src = 'common.js';\n\t\tl.onload = () => loadResources(['style.css'], S); // load style.css then call S()\n\t\tl.onerror = () => setTimeout(loadFiles, 100);\n\t\tdocument.head.appendChild(l);\n\t})();\n\n\tfunction S() {\n\t\tgetLoc();\n\t\tloadJS(getURL('/settings/s.js?p=10'), false, undefined, ()=>{\n\t\t\tUI();\n\t\t\tSf.MPC.setAttribute(\"max\",maxPanels);\n\t\t});\t// If we set async false, file is loaded and executed, then next statement is processed\n\t\tif (loc) d.Sf.action = getURL('/settings/2D');\n\t}\n\n\tfunction UI() {\n\t\tif (gId(\"somp\").value === \"0\") {\n\t\t\tgId(\"mpdiv\").style.display = \"none\";\n\t\t\tresetPanels();\n\t\t\treturn;\n\t\t}\n\t\tgId(\"mpdiv\").style.display = \"block\";\n\t\tdraw();\n\t}\n\n\tfunction addPanels() {\n\t\tlet c = parseInt(d.Sf.MPC.value);\n\t\tlet i = gId(\"panels\").children.length;\n\t\tif (i<c) for (let j=i; j<c; j++) addPanel(j);\n\t\tif (i>c) for (let j=i; j>c; j--) remPanel();\n\t}\n\n\tfunction addPanel(i=0) {\n\t\tlet p = gId(\"panels\");\n\t\tif (p.children.length >= maxPanels) return;\n\t\tvar pw = parseInt(d.Sf.PW.value);\n\t\tvar ph = parseInt(d.Sf.PH.value);\n\t\tlet b = `<div id=\"pnl${i}\"><b>Panel ${i}</b><br>\n1<sup>st</sup> LED: <select name=\"P${i}B\" oninput=\"UI()\">\n\t<option value=\"0\">Top</option>\n\t<option value=\"1\">Bottom</option>\n</select><select name=\"P${i}R\" oninput=\"UI()\">\n\t<option value=\"0\">Left</option>\n\t<option value=\"1\">Right</option>\n</select><br>\nOrientation: <select name=\"P${i}V\" oninput=\"UI()\">\n\t<option value=\"0\">Horizontal</option>\n\t<option value=\"1\">Vertical</option>\n</select><br>\nSerpentine: <input type=\"checkbox\" name=\"P${i}S\" oninput=\"UI()\"><br>\nDimensions (WxH): <input name=\"P${i}W\" type=\"number\" min=\"1\" max=\"255\" value=\"${pw}\" oninput=\"UI()\"> x <input name=\"P${i}H\" type=\"number\" min=\"1\" max=\"255\" value=\"${ph}\" oninput=\"UI()\"><br>\nOffset X: <input name=\"P${i}X\" type=\"number\" min=\"0\" max=\"255\" value=\"0\" oninput=\"UI()\">\nY: <input name=\"P${i}Y\" type=\"number\" min=\"0\" max=\"255\" value=\"0\" oninput=\"UI()\"><br><i>(offset from top-left corner in # LEDs)</i>\n<br><br></div>`;\n\t\tp.insertAdjacentHTML(\"beforeend\", b);\n\t}\n\n\tfunction remPanel() {\n\t\tlet p = gId(\"panels\").children;\n\t  \tvar i = p.length;\n\t  \tif (i <= 1) return;\n\t  \tp[i-1].remove();\n\t}\n\n\tfunction resetPanels() {\n\t\td.Sf.MPC.value = 1;\n\t\tlet e = gId(\"panels\").children\n\t\tfor (let i = e.length; i>0; i--) e[i-1].remove();\n\t}\n/*\n\tfunction btnPanel(i) {\n\t\tgId(\"pnl_add\").style.display = (i<maxPanels) ? \"inline\":\"none\";\n\t\tgId(\"pnl_rem\").style.display = (i>1) ? \"inline\":\"none\";\n\t}\n*/\n\tfunction gen() {\n\t\tresetPanels();\n\n\t\tvar pansH = parseInt(Sf.MPH.value);\n\t\tvar pansV = parseInt(Sf.MPV.value);\n\t\tvar c = pansH*pansV;\n\t\tSf.MPC.value = c; // number of panels\n\n\t\tvar ps = Sf.PS.checked;\n\t\tvar pv = Sf.PV.value===\"1\";\n\t\tvar pb = Sf.PB.value===\"1\";\n\t\tvar pr = Sf.PR.value===\"1\";\n\t\tvar pw = parseInt(Sf.PW.value);\n\t\tvar ph = parseInt(Sf.PH.value);\n\n\t\tvar h = pv ? pansV : pansH;\n\t\tvar v = pv ? pansH : pansV;\n\t\tfor (let j = 0, p = 0; j < v; j++) {\n\t\t\tfor (let i = 0; i < h; i++, p++) {\n\t\t\t\tif (j*i < maxPanels) addPanel(p);\n\t\t\t\tvar y = (pv?pr:pb) ? v-j-1: j;\n\t\t\t\tvar x = (pv?pb:pr) ? h-i-1 : i;\n\t\t\t\tx = ps && j%2 ? h-x-1 : x;\n\t\t\t\tSf[`P${p}X`].value = (pv?y:x) * pw;\n\t\t\t\tSf[`P${p}Y`].value = (pv?x:y) * ph\n\t\t\t\tSf[`P${p}W`].value = pw;\n\t\t\t\tSf[`P${p}H`].value = ph;\n\t\t\t}\n\t\t}\n\t\tUI(); // Update the preview after generating panels\n\t}\n\n\tfunction expand(o,i)\n\t{\n\t\ti.style.display = i.style.display!==\"none\" ? \"none\" : \"\";\n\t\to.style.rotate = i.style.display===\"none\" ? \"none\" : \"90deg\";\n\t}\n\n\tfunction draw() {\n\n\t\tif (!ctx) {\n\t\t\t//WLEDMM: add canvas, initialize and set UI\n\t\t\tvar canvas = gId(\"canvas\");\n\t\t\tcanvas.width  = window.innerWidth > 640?640:400; //Mobile gets 400, pc 640\n\t\t\tcanvas.height = canvas.width;\n\t\t\tctx = canvas.getContext('2d');\n\n\t\t\t// window.requestAnimationFrame(animate);\n\t\t}\n\n\t\t//calc max height and width\n\t\tvar maxWidth = 0;\n\t\tvar maxHeight = 0;\n\t\tfor (let p=0; p<gId(\"panels\").children.length; p++) {\n\t\t\tvar px = parseInt(Sf[`P${p}X`].value); //first led x\n\t\t\tvar py = parseInt(Sf[`P${p}Y`].value); //first led y\n\t\t\tvar pw = parseInt(Sf[`P${p}W`].value); //width\n\t\t\tvar ph = parseInt(Sf[`P${p}H`].value); //height\n\t\t\tmaxWidth = Math.max(maxWidth, px + pw);\n\t\t\tmaxHeight = Math.max(maxHeight, py + ph);\n\t\t}\n\n\t\tctx.canvas.height = ctx.canvas.width / maxWidth * maxHeight;\n\t\tctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);\n\t\tvar space=0; // space between panels + margin\n\t\tvar ppL = (ctx.canvas.width  - space * 2) / maxWidth; //pixels per led\n\n\t\tctx.lineWidth = 1;\n\t\tctx.strokeStyle=\"yellow\";\n\t\tctx.strokeRect(0, 0, ctx.canvas.width, ctx.canvas.height); // add space between panels\n\n\t\tfor (let p=0; p<gId(\"panels\").children.length; p++) {\n\t\t\tvar px = parseInt(Sf[`P${p}X`].value); //first led x\n\t\t\tvar py = parseInt(Sf[`P${p}Y`].value); //first led y\n\t\t\tvar pw = parseInt(Sf[`P${p}W`].value); //width\n\t\t\tvar ph = parseInt(Sf[`P${p}H`].value); //height\n\n\t\t\tvar pb = Sf[`P${p}B`].value == \"1\"; //bottom\n\t\t\tvar pr = Sf[`P${p}R`].value == \"1\"; //right\n\t\t\tvar pv = Sf[`P${p}V`].value == \"1\"; //vertical\n\t\t\tvar ps = Sf[`P${p}S`].checked; //serpentine\n\n\t\t\tvar topLeftX = px*ppL + space; //left margin\n\t\t\tvar topLeftY = py*ppL + space; //top margin\n\n\t\t\tctx.lineWidth = 3;\n\t\t\tctx.strokeStyle=\"white\";\n\t\t\tctx.strokeRect(topLeftX, topLeftY, pw*ppL, ph*ppL); // add space between panels\n\n\t\t\tvar lnX;\n\t\t\tvar lnY;\n\n\t\t\t//find start led\n\t\t\tif (pb) //bottom\n\t\t\t\tlnY = topLeftY + ph*ppL - ppL/2;\n\t\t\telse //top\n\t\t\t\tlnY = topLeftY + ppL/2;\n\t\t\tif (pr) //right\n\t\t\t\tlnX = topLeftX + pw*ppL - ppL/2;\n\t\t\telse //left\n\t\t\t\tlnX = topLeftX + ppL/2;\n\n\t\t\t//first led\n\t\t\tctx.fillStyle = \"green\";\n\t\t\tctx.beginPath();\n\t\t\tctx.arc(lnX, lnY, ppL*0.5, 0, 2 * Math.PI);\n\t\t\tctx.fill();\n\n\t\t\t//start line\n\t\t\tctx.lineWidth = 1;\n\t\t\tctx.beginPath();\n\t\t\tctx.moveTo(lnX, lnY);\n\n\t\t\tvar longLineLength = (pv?ph:pw)*ppL - ppL;\n\t\t\tfor (let ln=0; ln<(pv?pw:ph); ln++) { //loop over panelwidth (or height of vertical?)\n\n\t\t\t\tvar serpLine = ps && ln%2!=0; //serp: turn around if even line\n\n\t\t\t\tif (pv) //if vertical\n\t\t\t\t\tlnY += (pb?-1:1) * longLineLength * (serpLine?-1:1); //if vertical change the Y\n\t\t\t\telse\n\t\t\t\t\tlnX += (pr?-1:1) * longLineLength * (serpLine?-1:1); //if horizontal change the X\n\n\t\t\t\tctx.lineTo(lnX, lnY); //draw the long line\n\n\t\t\t\tif (ln<(pv?pw:ph)-1) { //not the last\n\t\t\t\t\t//find the small line end point\n\t\t\t\t\tif (pv) //vertical\n\t\t\t\t\t\tlnX += (pr?-1:1) * ppL;\n\t\t\t\t\telse //horizontal\n\t\t\t\t\t\tlnY += (pb?-1:1) * ppL;\n\n\t\t\t\t\t//if serpentine go next else go down\n\t\t\t\t\tif (ps) { //serpentine\n\t\t\t\t\t\tctx.lineTo(lnX, lnY); //draw the serpentine line\n\t\t\t\t\t} else { \n\t\t\t\t\t\t//find the other end of the long line\n\t\t\t\t\t\tif (pv) //vertical\n\t\t\t\t\t\t\tlnY += (pb?1:-1) * longLineLength * (serpLine?-1:1); //min as we go back\n\t\t\t\t\t\telse //horizontal\n\t\t\t\t\t\t\tlnX += (pr?1:-1) * longLineLength * (serpLine?-1:1);\n\t\t\t\t\t\tctx.moveTo(lnX, lnY); //move to the start point of the next long line\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tctx.stroke();\n\n\t\t\t//last led\n\t\t\tctx.fillStyle = \"red\";\n\t\t\tctx.beginPath();\n\t\t\tctx.arc(lnX, lnY, ppL*0.5, 0, 2 * Math.PI);\n\t\t\tctx.fill();\n\n\t\t\tctx.font = '40px Arial'; \n\t\t\tctx.fillStyle = \"orange\";\n\t\t\tctx.fillText(p, topLeftX + pw/2*ppL - 10, topLeftY + ph/2*ppL + 10);\n\t\t}\n\t\tgId(\"MD\").innerHTML = \"Matrix Dimensions (W*H=LC): \" + maxWidth + \" x \" + maxHeight + \" = \" + maxWidth * maxHeight;\n\t}\n\t</script>\n</head>\n<body>\n\t<form id=\"form_s\" name=\"Sf\" method=\"post\">\n\t<div class=\"toprow\">\n\t\t<div class=\"helpB\"><button type=\"button\" onclick=\"H('features/2D')\">?</button></div>\n\t\t<button type=\"button\" onclick=\"B()\">Back</button><button type=\"button\" onclick=\"fS()\">Save</button><hr>\n\t</div>\n\t<h2>2D setup</h2>\n\t<div class=\"sec\">\n\tStrip or panel:\n\t<select id=\"somp\" name=\"SOMP\" onchange=\"resetPanels();addPanels();UI();\" >\n\t\t<option value=\"0\">1D Strip</option>\n\t\t<option value=\"1\">2D Matrix</option>\n\t</select>\n\t</div>\n\t<div id=\"mpdiv\" style=\"display:none;\">\n\t<div class=\"sec\">\n\t\t<h3>Matrix Generator <button type=\"button\" id=\"expGen\" onclick=\"expand(this,gId('mxGen'));\">&gt;</button></h3>\n\t\t<div id=\"mxGen\" style=\"display:none;\">\n\t\t\tPanel dimensions (WxH): <input name=\"PW\" type=\"number\" min=\"1\" max=\"128\" value=\"8\"> x <input name=\"PH\" type=\"number\" min=\"1\" max=\"128\" value=\"8\"><br>\n\t\t\tHorizontal panels: <input name=\"MPH\" type=\"number\" min=\"1\" max=\"8\" value=\"1\">\n\t\t\tVertical panels: <input name=\"MPV\" type=\"number\" min=\"1\" max=\"8\" value=\"1\"><br>\n\t\t\t1<sup>st</sup> panel: <select name=\"PB\">\n\t\t\t\t<option value=\"0\">Top</option>\n\t\t\t\t<option value=\"1\">Bottom</option>\n\t\t\t</select><select name=\"PR\">\n\t\t\t\t<option value=\"0\">Left</option>\n\t\t\t\t<option value=\"1\">Right</option>\n\t\t\t</select><br>\n\t\t\tOrientation: <select name=\"PV\">\n\t\t\t\t<option value=\"0\">Horizontal</option>\n\t\t\t\t<option value=\"1\">Vertical</option>\n\t\t\t</select><br>\n\t\t\tSerpentine: <input type=\"checkbox\" name=\"PS\"><br>\n\t\t\t<i  class=\"warn\">Pressing Populate will create LED panel layout with pre-arranged matrix.<br>Values above <i>will not</i> affect final layout.<br>\n\t\t\t\tWARNING: You may need to update each panel parameters after they are generated.</i><br>\n\t\t\t<button type=\"button\" onclick=\"gen();expand(gId('expGen'),gId('mxGen'));\">Populate</button>\n\t\t</div>\n\t</div>\n\t<div class=\"sec\">\n\t\t<h3>Panel set-up</h3>\n\t\tNumber of panels: <input name=\"MPC\" type=\"number\" min=\"1\" max=\"64\" value=\"1\" oninput=\"addPanels();UI();\"><br>\n\t\t<i>A matrix is made of 1 or more physical LED panels.<br>\n\t\tEach panel can be of different size and/or have different LED orientation and/or starting point and/or layout.</i><br>\n\t</div>\n\t<div class=\"sec\">\n\t\t<h3>LED panel layout</h3>\n\t\t<div id=\"panels\">\n\t\t</div>\n\t</div>\n\t<div id=\"MD\"></div>\n\t<canvas id=\"canvas\"></canvas>\n\t<div class=\"sec\">\n\t\t<div id=\"json\" >Gap file: <input type=\"file\" name=\"data\" accept=\".json\"><button type=\"button\" class=\"sml\" onclick=\"uploadFile(d.Sf.data,'/2d-gaps.json')\">Upload</button></div>\n\t\t<i>Note: Gap file is a <b>.json</b> file containing an array with number of elements equal to the matrix size.<br>\n\t\t\tA value of -1 means that pixel at that position is missing, a value of 0 means never paint that pixel, and 1 means regular pixel.</i>\n\t</div>\n\t</div>\n\t<hr>\n\t<button type=\"button\" onclick=\"B()\">Back</button><button type=\"button\" onclick=\"fS()\">Save</button>\n\t</form>\n\t<div id=\"toast\"></div>\n</body>\n</html>\n"
  },
  {
    "path": "wled00/data/settings_dmx.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\" name=\"viewport\">\n\t<meta charset=\"utf-8\">\n\t<title>DMX Settings</title>\n\t<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->\n\t<script>\n\t// load common.js with retry on error\n\t(function loadFiles() {\n\t\tconst l = document.createElement('script');\n\t\tl.src = 'common.js';\n\t\tl.onload = () => loadResources(['style.css'], S); // load style.css then call S()\n\t\tl.onerror = () => setTimeout(loadFiles, 100);\n\t\tdocument.head.appendChild(l);\n\t})();\n\tfunction HW(){window.open(\"https://kno.wled.ge/interfaces/dmx-output/\");}\n\tfunction GCH(num) {\n\t\tgId('dmxchannels').innerHTML += \"\";\n\t\tfor (i=0;i<num;i++) {\n\t\t\tgId('dmxchannels').innerHTML += \"<span id=CH\" + (i+1) + \"s >Channel \" + (i+1) + \": <select name=CH\" + (i+1) + \" id=\\\"CH\" + (i+1) + \"\\\"><option value=0>Set to 0</option><option value=1>Red</option><option value=2>Green</option><option value=3>Blue</option><option value=4>White</option><option value=5>Shutter (Brightness)</option><option value=6>Set to 255</option></select></span><br />\\n\";\n\t\t}\n\t}\n\tfunction mMap(){\n\t\tnumCh=document.Sf.CN.value;\n\t\tnumGap=document.Sf.CG.value;\n\t\tif (parseInt(numCh)>parseInt(numGap)) {\n\t\t\tgId(\"gapwarning\").style.display=\"block\";\n\t\t} else {\n\t\t\tgId(\"gapwarning\").style.display=\"none\";\n\t\t}\n\t\tfor (i=0;i<15;i++) {\n\t\t\tif (i>=numCh) {\n\t\t\t\tgId(\"CH\"+(i+1) + \"s\").style.opacity = \"0.5\";\n\t\t\t\tgId(\"CH\"+(i+1)).disabled = true;\n\t\t\t\t\n\t\t\t} else {\n\t\t\t\tgId(\"CH\"+(i+1) + \"s\").style.opacity = \"1\";\n\t\t\t\tgId(\"CH\"+(i+1)).disabled = false;\n\t\t\t}\n\t\t}\n\t}\n\tfunction S(){\n\t\tgetLoc();\n\t\tloadJS(getURL('/settings/s.js?p=7'), false, ()=>{GCH(15);}, ()=>{mMap();}); // If we set async false, file is loaded and executed, then next statement is processed\n\t\tif (loc) d.Sf.action = getURL('/settings/dmx');\n\t}\n\t</script>\n</head>\n<body>\n<form id=\"form_s\" name=\"Sf\" method=\"post\">\n<div class=\"toprow\">\n<div class=\"helpB\"><button type=\"button\" onclick=\"HW()\">?</button></div>\n<button type=\"button\" onclick=\"B()\">Back</button><button type=\"submit\">Save</button><hr>\n</div>\n<h2>Imma firin ma lazer (if it has DMX support)</h2><!-- TODO: Change to something less-meme-related //-->\n\nProxy Universe <input name=PU type=number min=0 max=63999 required> from E1.31 to DMX (0=disabled)<br>\n<i>This will disable the LED data output to DMX configurable below</i><br><br>\n<i>Number of fixtures is taken from LED config page</i><br>\n\nChannels per fixture (15 max): <input type=\"number\" min=\"1\" max=\"15\" name=\"CN\" maxlength=\"2\" onchange=\"mMap();\"><br />\nStart channel: <input type=\"number\" min=\"1\" max=\"512\" name=\"CS\" maxlength=\"2\"><br />\nSpacing between start channels: <input type=\"number\" min=\"1\" max=\"512\" name=\"CG\" maxlength=\"2\" onchange=\"mMap();\"> [ <a href=\"javascript:alert('if set to 10, first fixture will start at 10,\\nsecond will start at 20 etc.\\nRegardless of the channel count.\\nMakes memorizing channel numbers easier.');\">info</a> ]<br>\n<div id=\"gapwarning\" style=\"color: orange; display: none;\">WARNING: Channel gap is lower than channels per fixture.<br />This will cause overlap.</div>\n<button type=\"button\" onclick=\"location.href='/dmxmap';\">DMX Map</button><br>\nDMX fixtures start LED: <input type=\"number\" min=\"0\" max=\"1500\" name=\"SL\">\n<h3>Channel functions</h3>\n<div id=\"dmxchannels\"></div>\n<hr><button type=\"button\" onclick=\"B()\">Back</button><button type=\"submit\">Save</button>\n</form>\n</body>\n</html>\n"
  },
  {
    "path": "wled00/data/settings_leds.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"utf-8\">\n\t<meta content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\" name=\"viewport\">\n\t<title>LED Settings</title>\n\t<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->\n\t<script>\n\t\tvar maxD=1,maxI2S=0,maxRMT=0,maxA=1,chipID=0,maxM=4000,maxPB=2048,maxL=1664,maxCO=5,maxBT=4; //maximum bytes for LED allocation: 4kB for 8266, 32kB for 32\n\t\tvar customStarts=false,startsDirty=[];\n\t\tfunction off(n)    { gN(n).value = -1;}\n\t\t// these functions correspond to C macros found in const.h\n\t\tfunction gT(t)     { for (let type of d.ledTypes) if (t == type.i) return type; } // getType from available ledTypes\n\t\tfunction isPWM(t)  { return gT(t).t.charAt(0) === \"A\"; }    // is PWM type\n\t\tfunction isAna(t)  { return gT(t).t === \"\" || isPWM(t); }   // is analog type\n\t\tfunction isDig(t)  { return gT(t).t === \"D\" || isD2P(t); }  // is digital type\n\t\tfunction isD2P(t)  { return gT(t).t === \"2P\"; }             // is digital 2 pin type\n\t\tfunction isNet(t)  { return gT(t).t === \"N\"; }              // is network type\n\t\tfunction isVir(t)  { return gT(t).t === \"V\" || isNet(t); }  // is virtual type\n\t\tfunction isHub75(t){ return gT(t).t === \"H\"; }              // is HUB75 type\n\t\tfunction hasRGB(t) { return !!(gT(t).c & 0x01); }           // has RGB\n\t\tfunction hasW(t)   { return !!(gT(t).c & 0x02); }           // has white channel\n\t\tfunction hasCCT(t) { return !!(gT(t).c & 0x04); }           // is white CCT enabled\n\t\tfunction is16b(t)  { return !!(gT(t).c & 0x10); }           // is digital 16 bit type\n\t\tfunction mustR(t)  { return !!(gT(t).c & 0x20); }           // Off refresh is mandatory\n\t\tfunction numPins(t){ return Math.max(gT(t).t.length, 1); }  // type length determines number of GPIO pins\n\t\tfunction chrID(x)  { return String.fromCharCode((x<10?48:55)+x); }\n\t\tfunction toNum(c)  { let n=c.charCodeAt(0); return (n>=48 && n<=57)?n-48:(n>=65 && n<=90)?n-55:0; } // convert char (0-9A-Z) to number (0-35)\n\t\t// load common.js with retry on error\n\t\t(function loadFiles() {\n\t\t\tconst l = document.createElement('script');\n\t\t\tl.src = 'common.js';\n\t\t\tl.onload = () => loadResources(['style.css'], S); // load style.css then call S()\n\t\t\tl.onerror = () => setTimeout(loadFiles, 100);\n\t\t\tdocument.head.appendChild(l);\n\t\t})();\n\n\t\tfunction S() {\n\t\t\tgetLoc();\n\t\t\tif(localStorage.getItem('ASc')==='true') d.Sf.AS.checked=true;\n\t\t\tloadJS(getURL('/settings/s.js?p=2'), false, ()=>{\n\t\t\t\td.ledTypes = [/*{i:22,c:1,t:\"D\",n:\"WS2812\"},{i:42,c:6,t:\"AA\",n:\"PWM CCT\"}*/]; // filled from GetV()\n\t\t\t\td.um_p = [];\n\t\t\t\td.rsvd = [];\n\t\t\t\td.ro_gpio = [];\n\t\t\t\td.max_gpio = 50;\n\t\t\t}, ()=>{\n\t\t\t\tcheckSi();\n\t\t\t\tsetABL();\n\t\t\t\td.Sf.addEventListener(\"submit\", trySubmit);\n\t\t\t\tif (d.um_p[0]==-1) d.um_p.shift();\n\t\t\t\tpinDropdowns();\n\t\t\t});\t// If we set async false, file is loaded and executed, then next statement is processed\n\t\t\tif (loc) d.Sf.action = getURL('/settings/leds');\n\t\t}\n\t\tfunction bLimits(c,p,m,l,o,di,r,i,a,n) {\n\t\t\tchipID = c;\t\t// chip/platformID - 0 = ESP8266, 1 = C3, 2 = S2, 3 = S3, 4 = ESP32\n\t\t\tmaxPB = p;\t\t// maxPB - max LEDs per bus\n\t\t\tmaxM  = m;\t\t// maxM - max LED memory\n\t\t\tmaxL  = l;\t\t// maxL - max LEDs (will serve to determine ESP >1664 == ESP32)\n\t\t\tmaxCO = o;\t\t// maxCO - max Color Order mappings\n\t\t\tmaxD  = di;\t\t// maxD - max digital channels (can be changed if using ESP32 parallel I2S): 16 - ESP32, 12 - S3/S2, 2 - C3, 3 - 8266\n\t\t\tmaxRMT = r;\t\t// maxRMT - max RMT channels: 8 - ESP32, 4 - S2/S3, 2 - C3, 0 - 8266\n\t\t\tmaxI2S = i;\t\t// maxI2S - max I2S/LCD channels: 8 - ESP32/S2/S3, 0 - C3/8266\n\t\t\tmaxA  = a;\t\t// maxA - max analog channels: 16 - ESP32, 8 - S3/S2, 6 - C3, 5 - 8266\n\t\t\tmaxBT = n;\t\t// maxBT - max buttons\n\t\t}\n\t\tfunction is8266() { return chipID == 0; } // NOTE: see const.h: WLED_PLATFORM_ID (TODO: use info json lookup instead)\n\t\tfunction isC3()   { return chipID == 1; }\n\t\tfunction isS2()   { return chipID == 2; }\n\t\tfunction isS3()   { return chipID == 3; }\n\t\tfunction is32()   { return chipID == 4; }\n\t\tfunction pinsOK() {\n\t\t\tvar ok = true;\n\t\t\tvar nList = d.Sf.querySelectorAll(\"#mLC input[name^=L]\");\n\t\t\tnList.forEach((LC,i)=>{\n\t\t\t\tif (!ok) return; // prevent iteration after conflict\n\t\t\t\tlet nm = LC.name.substring(0,2);  // field name : /L./\n\t\t\t\tif (nm.search(/^L[0-4]/) < 0) return; // not pin fields\n\t\t\t\tlet n = LC.name.substring(2,3);   // bus number (0-Z)\n\t\t\t\tlet t = parseInt(d.Sf[\"LT\"+n].value, 10); // LED type SELECT\n\t\t\t\tif(isHub75(t)) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// ignore IP address\n\t\t\t  if (isNet(t)) return;\n\t\t\t\t//check for pin conflicts\n\t\t\t\tif (LC.value!=\"\" && LC.value!=\"-1\") {\n\t\t\t\t\tlet p = d.rsvd.concat(d.um_p); // used pin array\n\t\t\t\t\td.Sf.querySelectorAll(\"select.pin\").forEach((e)=>{if(e.value>-1)p.push(parseInt(e.value));}) // buttons, IR & relay\n\t\t\t\t\tif (p.some((e)=>e==parseInt(LC.value))) {\n\t\t\t\t\t\talert(`Sorry, pins ${JSON.stringify(p)} can't be used.`);\n\t\t\t\t\t\tLC.value=\"\";\n\t\t\t\t\t\tLC.focus();\n\t\t\t\t\t\tok = false;\n\t\t\t\t\t\treturn;\n\t\t\t\t\t} else if (d.ro_gpio.some((e)=>e==parseInt(LC.value))) {\n\t\t\t\t\t\talert(`Sorry, pins ${JSON.stringify(d.ro_gpio)} are input only.`);\n\t\t\t\t\t\tLC.value=\"\";\n\t\t\t\t\t\tLC.focus();\n\t\t\t\t\t\tok = false;\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tfor (j=i+1; j<nList.length; j++) {\n\t\t\t\t\t\tlet n2 = nList[j].name.substring(0,2); // field name /L./\n\t\t\t\t\t\tif (n2.search(/^L[0-4]/) == 0) { // pin fields\n\t\t\t\t\t\t\tlet m  = nList[j].name.substring(2,3); // bus number (0-Z)\n\t\t\t\t\t\t\tlet t2 = parseInt(gN(\"LT\"+m).value, 10);\n\t\t\t\t\t\t\tif (isVir(t2)) continue;\n\t\t\t\t\t\t\tif (nList[j].value!=\"\" && nList[i].value==nList[j].value) {\n\t\t\t\t\t\t\t\talert(`Pin conflict between ${LC.name}/${nList[j].name}!`);\n\t\t\t\t\t\t\t\tnList[j].value=\"\";\n\t\t\t\t\t\t\t\tnList[j].focus();\n\t\t\t\t\t\t\t\tok = false;\n\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t});\n\t\t\treturn ok;\n\t\t}\n\t\tfunction trySubmit(e) {\n\t\t\td.Sf.data.value = '';\n\t\t\te.preventDefault();\n\t\t\tif (!pinsOK()) {e.stopPropagation();return false;} // Prevent form submission and contact with server\n\t\t\tlet usage = getDuse(), invalidBus = false;\n\t\t\td.Sf.querySelectorAll(\"#mLC select[name^=LT]\").forEach(s=>{\n\t\t\t\tlet n = s.name.substring(2,3);\n\t\t\t\tif (!isBCok(n, usage)) invalidBus = true;\n\t\t\t});\n\t\t\tif (invalidBus) { alert(\"Invalid Bus-config\"); e.stopPropagation(); return false; }\n\t\t\t// validate HUB75 panel config\n\t\t\tlet LTs = d.Sf.querySelectorAll(\"#mLC select[name^=LT]\");\n\t\t\tfor (let i=0; i<LTs.length; i++) {\n    \t\tlet n = chrID(i);\n    \t\tlet t = parseInt(LTs[i].value);\n\t\t\t\tif (isHub75(t)) {\n\t\t\t\t\tlet p = parseInt(d.Sf[\"L2\"+n].value)||1, r = parseInt(d.Sf[\"L3\"+n].value)||1, c = parseInt(d.Sf[\"L4\"+n].value)||1, h = parseInt(d.Sf[\"L1\"+n].value)||1;\n\t\t\t\t\tif (r*c !== p) {alert(`HUB75 error: panels≠rows×cols`); e.stopPropagation(); return false;}\n\t\t\t\t\tif (h >= 64 && p > 1) {alert(`HUB75 error: height >= 64, only single panel allowed`); e.stopPropagation(); return false;}\n\t\t\t\t\tif(isS3()) {\n\t\t\t\t\t  alert(\"HUB75 changes require a reboot\"); // TODO: only throw this if panel config changed?\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\t\t\tif (bquot > 100) {var msg = \"Too many LEDs! Can't handle that!\"; alert(msg); e.stopPropagation(); return false;}\n\t\t\telse {\n\t\t\t\tif (bquot > 80) {var msg = \"Memory usage is high, reboot recommended!\\n\\rSet transitions to 0 to save memory.\";\n\t\t\t\tif (bquot > 100) msg += \"\\n\\rToo many LEDs for me to handle properly!\"; if (maxM < 10000) msg += \"\\n\\rConsider using an ESP32.\"; alert(msg);}\n\t\t\t\tif (!d.Sf.ABL.checked || d.Sf.PPL.checked) d.Sf.MA.value = 0; // submit 0 as ABL (PPL will handle it)\n\t\t\t\tif (d.Sf.checkValidity()) {\n\t\t\t\t\td.Sf.querySelectorAll(\"#mLC select[name^=LT]\").forEach((s)=>{s.disabled=false;}); // just in case\n\t\t\t\t\td.Sf.submit(); //https://stackoverflow.com/q/37323914\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfunction enABL()\n\t\t{\n\t\t\tvar en = d.Sf.ABL.checked;\n\t\t\tgId('abl').style.display = (en) ? 'inline':'none';\n\t\t\tgId('psu2').style.display = (en) ? 'inline':'none';\n\t\t\tif (!en) {\n\t\t\t\t// limiter disabled\n\t\t\t\td.Sf.PPL.checked = false;\n//\t\t\t\td.Sf.querySelectorAll(\"#mLC select[name^=LAsel]\").forEach((e)=>{e.selectedIndex = 0;}); // select default LED mA\n//\t\t\t\td.Sf.querySelectorAll(\"#mLC input[name^=LA]\").forEach((e)=>{e.min = 0; e.value = 0;}); // set min & value to 0\n\t\t\t}\n\t\t\tUI();\n\t\t}\n\t\t// enable per port limiter and calculate current\n\t\tfunction enPPL(sDI=0)\n\t\t{\n\t\t\tconst abl = d.Sf.ABL.checked;\n\t\t\tconst ppl = d.Sf.PPL.checked;\n\t\t\tlet sumMA = 0;\n\t\t\td.Sf.MA.readonly = ppl;\n\t\t\td.Sf.MA.min = abl && !ppl ? 250 : 0;\n\t\t\tgId(\"psuMA\").style.display = ppl ? 'none' : 'inline';\n\t\t\tgId(\"ppldis\").style.display = ppl ? 'inline' : 'none';\n\t\t\t// set PPL minimum value and clear actual PPL limit if ABL is disabled\n\t\t\td.Sf.querySelectorAll(\"#mLC input[name^=MA]\").forEach((i,x)=>{\n\t\t\t\tvar n = chrID(x);\n\t\t\t\tgId(\"PSU\"+n).style.display = ppl ? \"inline\" : \"none\";\n\t\t\t\tconst t = parseInt(d.Sf[\"LT\"+n].value); // LED type SELECT\n\t\t\t\tconst c = parseInt(d.Sf[\"LC\"+n].value); //get LED count\n\t\t\t\ti.min = ppl && isDig(t) ? 250 : 0;\n\t\t\t\tif (!abl || !isDig(t)) i.value = 0;\n\t\t\t\telse if (ppl) sumMA += parseInt(i.value,10);\n\t\t\t\telse if (sDI) i.value = Math.round(parseInt(d.Sf.MA.value,10)*c/sDI);\n\t\t\t});\n\t\t\tif (ppl) d.Sf.MA.value = sumMA; // populate UI ABL value if PPL used\n\t\t}\n\t\t// enable and update LED Amps\n\t\tfunction enLA(s,n)\n\t\t{\n\t\t\tconst abl = d.Sf.ABL.checked;\n\t\t\tconst t = parseInt(d.Sf[\"LT\"+n].value); // LED type SELECT\n\t\t\tgId('LAdis'+n).style.display = s.selectedIndex==5 ? \"inline\" : \"none\"; // show/hide custom mA field\n\t\t\tif (s.value!==\"0\") d.Sf[\"LA\"+n].value = s.value; // set value from select object\n\t\t\td.Sf[\"LA\"+n].min = (!isDig(t) || !abl) ? 0 : 1; // set minimum value for validation\n\t\t}\n\t\tfunction setABL()\n\t\t{\n\t\t\tlet en = parseInt(d.Sf.MA.value) > 0;\n\t\t\t// check if ABL is enabled (max mA entered per output)\n\t\t\td.Sf.querySelectorAll(\"#mLC input[name^=MA]\").forEach((i,n)=>{\n\t\t\t\tif (parseInt(i.value) > 0) en = true;\n\t\t\t});\n\t\t\td.Sf.ABL.checked = en;\n\t\t\t// select appropriate LED current\n\t\t\td.Sf.querySelectorAll(\"#mLC select[name^=LAsel]\").forEach((sel,x)=>{\n\t\t\t\tsel.value = 0; // set custom\n\t\t\t\tvar n = chrID(x);\n\t\t\t\tif (en)\n\t\t\t\t\tswitch (parseInt(d.Sf[\"LA\"+n].value)) {\n\t\t\t\t\t\tcase 0: break; // disable ABL\n\t\t\t\t\t\tcase 15: sel.value = 15; break;\n\t\t\t\t\t\tcase 30: sel.value = 30; break;\n\t\t\t\t\t\tcase 35: sel.value = 35; break;\n\t\t\t\t\t\tcase 55: sel.value = 55; break;\n\t\t\t\t\t\tcase 255: sel.value = 255; break;\n\t\t\t\t\t}\n\t\t\t\telse sel.value = 0;\n\t\t\t\tenLA(sel,n); // configure individual limiter\n\t\t\t});\n\t\t\tenABL();\n\t\t\tgId('m1').innerHTML = maxM;\n\t\t}\n\t\t//returns mem usage for buses including two pixel buffers (segment buffer + global buffer)\n\t\tfunction getMem(t, n) {\n\t\t\tif (isAna(t)) return 5;\t// analog\n\t\t\tlet len = parseInt(d.Sf[\"LC\"+n].value);\n\t\t\tlen += parseInt(d.Sf[\"SL\"+n].value); // skipped LEDs are allocated too\n\t\t\tlet dma = 0; // DMA memory for this bus (only for I2S)\n\t\t\tlet pbfr = len * 8; // pixel buffers: global buffer + segment buffer (at least one segment buffer is required)\n\t\t\tlet ch = 3*hasRGB(t) + hasW(t) + hasCCT(t);\n\t\t\tlet mul = 1;\n\t\t\tif (isDig(t)) {\n\t\t\t\tif (is16b(t)) len *= 2; // 16 bit LEDs\n\t\t\t\tif (is8266() && d.Sf[\"L0\"+n].value == 3) { //8266 DMA uses 5x the mem\n\t\t\t\t\tmul = 5;\n\t\t\t\t}\n\t\t\t\tif (!is8266() && !isD2P(t)) {\n\t\t\t\t\tmul = 2; // default to double buffer (RMT, 2-pin digital)\n\t\t\t\t\tlet driverPref = d.Sf[\"LD\"+n]?.value | 0; // driver preference selection: 0=RMT, 1=I2S\n\t\t\t\t\tif (driverPref == 1) { // I2S to be used\n\t\t\t\t\t\tmul = 1; // NPB uses single pixel buffer for I2S, DMA buffer serves as second buffer\n\t\t\t\t\t\tlet usage = getDuse();\n\t\t\t\t\t\tdma = usage.I2Smem; // DMA buffer for I2S/LCD, getDuse() returns the average per I2S bus so it can be distributed and summed\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t//console.log(`LED mem for bus ${n} (NPB buffers, DMA buffer per bus, WLED pixel buffers): ${len * ch * mul} + ${dma} + ${pbfr}`);\n\t\t\t}\n\t\t\treturn len * ch * mul + dma + pbfr;\n\t\t}\n\n\t\t// check if bus configuration is valid\n\t\tfunction isBCok(n, usage) {\n\t\t\tif (is8266()) return true; // no special bus limits on ESP8266 other than digital bus count, checked in updateTypeDropdowns()\n\t\t\tlet t = parseInt(d.Sf[\"LT\"+n].value);\n\t\t\tif (!isDig(t) || isD2P(t)) return true; // only digital non-2pin types need bus check\n\t\t\tlet drv = d.Sf[\"LD\"+n]?.value | 0; // driver preference selection: 0=RMT, 1=I2S\n\t\t\tif (drv==1 && usage.I2SType!==null && t!==usage.I2SType) return false; // mismatched type in parallel I2S\n\t\t\tif (drv==0 && usage.rmtUsed > maxRMT) return false; // too many RMT buses\n\t\t\tif (drv==1 && usage.i2sUsed > maxI2S) return false; // too many I2S buses\n\t\t\treturn true;\n\t\t}\n\n\t\tfunction UI(change=false)\n\t\t{\n\t\t\tlet gRGBW = false, memu = 0;\n\t\t\tlet busMA = 0;\n\t\t\tlet sLC = 0, sPC = 0, sDI = 0, maxLC = 0;\n\t\t\tconst abl = d.Sf.ABL.checked;\n\t\t\tlet setPinConfig = (n,t) => {\n\t\t\t\tlet p0d = \"GPIO:\";\n\t\t\t\tlet p1d = \"\";\n\t\t\t\tlet off = \"Off Refresh\";\n\t\t\t\tswitch (gT(t).t.charAt(0)) {\n\t\t\t\t\tcase '2': // 2 pin digital\n\t\t\t\t\t\tp1d = \"Clock \"+p0d;\n\t\t\t\t\t\t// fallthrough\n\t\t\t\t\tcase 'D': // digital\n\t\t\t\t\t\tp0d = \"Data \"+p0d;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'A': // PWM analog\n\t\t\t\t\t\tif (numPins(t) > 1) p0d = \"GPIOs:\";\n\t\t\t\t\t\toff = \"Dithering\";\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'N': // network\n\t\t\t\t\t\tp0d = \"IP address:\";\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'V': // virtual/non-GPIO based\n\t\t\t\t\t\tp0d = \"Config:\";\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 'H': // HUB75\n\t\t\t\t\t\tp0d = \"Panel (width x height):\";\n\t\t\t\t\t\tgId(\"p2d\"+n).innerHTML = \"<br>No. of Panels:\";\n\t\t\t\t\t\tgId(\"p3d\"+n).innerText = \"rows x cols:\";\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tgId(\"p0d\"+n).innerText = p0d;\n\t\t\t\tgId(\"p1d\"+n).innerText = p1d;\n\t\t\t\tgId(\"off\"+n).innerText = off;\n\t\t\t\t// secondary pins show/hide (type string length is equivalent to number of pins used; except for network and on/off)\n\t\t\t\tlet pins = Math.max(gT(t).t.length,1) + 3*isNet(t) + 4*isHub75(t); // fixes network pins to 4\n\t\t\t\tfor (let p=1; p<5; p++) {\n\t\t\t\t\tvar LK = d.Sf[\"L\"+p+n];\n\t\t\t\t\tif (!LK) continue;\n\t\t\t\t\tLK.style.display = (p < pins) ? \"inline\" : \"none\";\n\t\t\t\t\tLK.required = (p < pins);\n\t\t\t\t\tif (p >= pins) LK.value=\"\";\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// enable/disable LED fields\n\t\t\tupdateTypeDropdowns(); // restrict bus types in dropdowns to max allowed digital/analog buses\n\t\t\tlet LTs = d.Sf.querySelectorAll(\"#mLC select[name^=LT]\");\n\t\t\tLTs.forEach((s,i)=>{\n\t\t\t\t// is the field a LED type?\n\t\t\t\tvar n = s.name.substring(2,3); // bus number (0-Z)\n\t\t\t\tvar t = parseInt(s.value);\n\t\t\t\tmemu += getMem(t, n); // calc memory\n\t\t\t\tsetPinConfig(n,t);\n\t\t\t\tgId(\"abl\"+n).style.display = (!abl || !isDig(t)) ? \"none\" : \"inline\"; // show/hide individual ABL settings\n\t\t\t\tif (change) { // did we change LED type?\n\t\t\t\t\tgId(\"rf\"+n).checked = (gId(\"rf\"+n).checked || t == 31); // LEDs require data in off state (mandatory for TM1814)\n\t\t\t\t\tif (isAna(t)) d.Sf[\"LC\"+n].value = 1;                   // for sanity change analog count just to 1 LED\n\t\t\t\t\td.Sf[\"LA\"+n].min = (!isDig(t) || !abl) ? 0 : 1;         // set minimum value for LED mA\n\t\t\t\t\td.Sf[\"MA\"+n].min = (!isDig(t)) ? 0 : 250;               // set minimum value for PSU mA\n\t\t\t\t}\n\t\t\t\tgId(\"rf\"+n).onclick = mustR(t) ? (()=>{return false}) : (()=>{});           // prevent change change of \"Refresh\" checkmark when mandatory\n\t\t\t\tgRGBW |= hasW(t);                                                           // RGBW checkbox\n\t\t\t\tgId(\"co\"+n).style.display = (isVir(t) || isAna(t) || isHub75(t)) ? \"none\":\"inline\";       // hide color order for PWM\n\t\t\t\tgId(\"dig\"+n+\"w\").style.display = (isDig(t) && hasW(t)) ? \"inline\":\"none\";   // show swap channels dropdown\n\t\t\t\tgId(\"dig\"+n+\"w\").querySelector(\"[data-opt=CCT]\").disabled = !hasCCT(t);     // disable WW/CW swapping\n\t\t\t\tif (!(isDig(t) && hasW(t))) d.Sf[\"WO\"+n].value = 0;                         // reset swapping\n\t\t\t\tgId(\"dig\"+n+\"c\").style.display = (isAna(t) || isHub75(t)) ? \"none\":\"inline\";              // hide count for analog\n\t\t\t\tgId(\"dig\"+n+\"r\").style.display = (isVir(t)) ? \"none\":\"inline\";              // hide reversed for virtual\n\t\t\t\tgId(\"dig\"+n+\"s\").style.display = (isVir(t) || isAna(t) || isHub75(t)) ? \"none\":\"inline\";  // hide skip 1st for virtual & analog\n\t\t\t\tgId(\"dig\"+n+\"f\").style.display = (isDig(t) || (isPWM(t) && maxL>2048)) ? \"inline\":\"none\"; // hide refresh (PWM hijacks reffresh for dithering on ESP32)\n\t\t\t\tgId(\"dig\"+n+\"a\").style.display = (hasW(t)) ? \"inline\":\"none\";               // auto calculate white\n\t\t\t\tgId(\"dig\"+n+\"l\").style.display = (isD2P(t) || isPWM(t)) ? \"inline\":\"none\";  // bus clock speed / PWM speed (relative) (not On/Off)\n\t\t\t\tgId(\"rev\"+n).innerHTML = isAna(t) ? \"Inverted output\":\"Reversed\";           // change reverse text for analog else (rotated 180°)\n\t\t\t\t//gId(\"psd\"+n).innerHTML = isAna(t) ? \"Index:\":\"Start:\";                      // change analog start description\n\t\t\t\tgId(\"net\"+n+\"h\").style.display = isNet(t) && !is8266() ? \"block\" : \"none\";  // show host field for network types except on ESP8266\n\t\t\t\tif (!isNet(t) || is8266()) d.Sf[\"HS\"+n].value = \"\";                         // cleart host field if not network type or ESP8266\n\t\t\t});\n\t\t\t// display global white channel overrides\n\t\t\tgId(\"wc\").style.display = (gRGBW) ? 'inline':'none';\n\t\t\tif (!gRGBW) {\n\t\t\t\td.Sf.AW.selectedIndex = 0;\n\t\t\t\td.Sf.CR.checked = false;\n\t\t\t}\n\t\t\t// update start indexes, max values, calculate current, etc\n\t\t\tlet sameType = 0;\n\t\t\tvar nList = d.Sf.querySelectorAll(\"#mLC input[name^=L]\");\n\t\t\tnList.forEach((LC,i)=>{\n\t\t\t\tlet nm = LC.name.substring(0,2);     // field name : /L./\n\t\t\t\tlet n  = LC.name.substring(2,3);     // bus number (0-Z)\n\t\t\t\tlet t  = parseInt(d.Sf[\"LT\"+n].value); // LED type SELECT\n\t\t\t\tif (isDig(t) && !isD2P(t)) {\n\t\t\t\t\tif (sameType == 0) sameType = t; // first bus type\n\t\t\t\t}\n\t\t\t\t// do we have a led count field\n\t\t\t\tif (nm==\"LC\") {\n\t\t\t\t\tif (!isHub75(t)) {\n\t\t\t\t\t\tLC.max = isAna(t) ? 1 : (isDig(t) ? maxPB : 16384); // set max value\n\t\t\t\t\t} else {\n\t\t\t\t\t\tLC.min = undefined;\n\t\t\t\t\t\tLC.max = undefined;\n\t\t\t\t\t}\n\t\t\t\t\tlet c = parseInt(LC.value,10); //get LED count\n\t\t\t\t\tif (!customStarts || !startsDirty[toNum(n)]) gId(\"ls\"+n).value = sLC; //update start value\n\t\t\t\t\tgId(\"ls\"+n).disabled = !customStarts; //enable/disable field editing\n\t\t\t\t\tif (c) {\n\t\t\t\t\t\tlet s = parseInt(gId(\"ls\"+n).value); //start value\n\t\t\t\t\t\tif (s+c > sLC) sLC = s+c; //update total count\n\t\t\t\t\t\tif (!isVir(t)) sPC += c; //virtual out busses do not count towards physical LEDs\n\t\t\t\t\t\tif (isDig(t)) {\n\t\t\t\t\t\t\tif (c > maxLC) maxLC = c; //max per output\n\t\t\t\t\t\t\tsDI += c; // summarize digital LED count\n\t\t\t\t\t\t\tlet maPL = parseInt(d.Sf[\"LA\"+n].value);\n\t\t\t\t\t\t\tif (maPL == 255) maPL = 12; // wacky WS2815 mode (255 == 12mA per LED)\n\t\t\t\t\t\t\tbusMA += maPL*c; // summarize maximum bus current (calculated)\n\t\t\t\t\t\t}\n\t\t\t\t\t} // increase led count\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// ignore IP address (stored in pins for virtual busses)\n\t\t\t\tif (nm.search(/^L[0-3]/) == 0) { // pin fields\n\t\t\t\t\tif (isVir(t)) {\n\t\t\t\t\t\tLC.max = 255;\n\t\t\t\t\t\tLC.min = 0;\n\t\t\t\t\t\tLC.style.color=\"#fff\";\n\t\t\t\t\t\treturn; // do not check conflicts\n\t\t\t\t\t} else {\n\t\t\t\t\t\tLC.max = d.max_gpio-1;\n\t\t\t\t\t\tLC.min = -1;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (isHub75(t) && (nm==\"L0\" || nm==\"L1\")) {\n\t\t\t\t\t// Matrix width and height\n\t\t\t\t\tLC.max = 128;\n\t\t\t\t\tLC.min = 16;\n\t\t\t\t\tLC.style.color=\"#fff\";\n\t\t\t\t\treturn; // do not check conflicts\n\t\t\t\t}\n\t\t\t\telse if (isHub75(t) && (nm==\"L2\" || nm==\"L3\" || nm==\"L4\")) {\n\t\t\t\t\t// chain length aka panel count (L2), cols(L3), rows(L4)\n\t\t\t\t\tLC.max = 4;\n\t\t\t\t\tLC.min = 1;\n\t\t\t\t\tif (LC.value === \"\") LC.value = 1; // default to 1\n\t\t\t\t\tLC.style.color=\"#fff\";\n\t\t\t\t\treturn; // do not check conflicts\n\t\t\t\t}\n\t\t\t\t// check for pin conflicts & color fields\n\t\t\t\tif (nm.search(/^L[0-4]/) == 0) // pin fields\n\t\t\t\t\tif (LC.value!=\"\" && LC.value!=\"-1\") {\n\t\t\t\t\t\tlet p = d.rsvd.concat(d.um_p); // used pin array\n\t\t\t\t\t\td.Sf.querySelectorAll(\"select.pin\").forEach((e)=>{if(e.value>-1)p.push(parseInt(e.value));}) // buttons, IR & relay\n\t\t\t\t\t\tfor (j=0; j<nList.length; j++) {\n\t\t\t\t\t\t\tif (i==j) continue;\n\t\t\t\t\t\t\tlet n2 = nList[j].name.substring(0,2); // field name : /L./\n\t\t\t\t\t\t\tif (n2.search(/^L[0-4]/) == 0) { // pin fields\n\t\t\t\t\t\t\t\tlet m  = nList[j].name.substring(2,3); // bus number (0-Z)\n\t\t\t\t\t\t\t\tlet t2 = parseInt(gN(\"LT\"+m).value, 10);\n\t\t\t\t\t\t\t\tif (isVir(t2)) continue;\n\t\t\t\t\t\t\t\tif (nList[j].value!=\"\" && nList[j].value!=\"-1\") p.push(parseInt(nList[j].value,10));  // add current pin\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// now check for conflicts\n\t\t\t\t\t\tif (p.some((e)=>e==parseInt(LC.value))) LC.style.color = \"red\";\n\t\t\t\t\t\telse LC.style.color = d.ro_gpio.some((e)=>e==parseInt(LC.value)) ? \"orange\" : \"#fff\";\n\t\t\t\t\t} else LC.style.color = \"#fff\";\n\t\t\t});\n\n\t\t\t// Use helper function to calculate channel usage\n\t\t\tlet usage = getDuse();\n\n\t\t\td.Sf.querySelectorAll(\"#mLC select[name^=LT]\").forEach((s)=>{\n\t\t\t\tlet n = s.name.substring(2,3);\n\t\t\t\tlet t = parseInt(s.value);\n\t\t\t\tlet drvsel = gId(\"drvsel\"+n); // driver selection dropdown\n\t\t\t\tif (drvsel) {\n\t\t\t\t\tdrvsel.style.display = \"none\"; // hide by default\n\t\t\t\t\tdrvsel.style.color = \"#fff\"; // reset color\n\t\t\t\t}\n\t\t\t\ts.style.color = \"#fff\"; // reset\n\n\t\t\t\tif (isDig(t) && !isD2P(t)) {\n\t\t\t\t\t// Update I2S/RMT driver info/dropdown for ESP32 digital buses, C3 only supports RMT\n\t\t\t\t\tif (!is8266() && !isC3()) {\n\t\t\t\t\t\t// Show driver selection dropdown when I2S is enabled, mark red if invalid\n\t\t\t\t\t\tif (drvsel) {\n\t\t\t\t\t\t\tif (d.Sf.AS.checked) drvsel.style.display = \"inline\"; // only show when advanced settings enabled\n\t\t\t\t\t\t\td.Sf[\"LD\"+n].value = d.Sf[\"LD\"+n].value | 0; // default to RMT\n\t\t\t\t\t\t\tif (!isBCok(n, usage)) drvsel.style.color = \"red\"; else drvsel.style.color = \"#fff\";\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t\tupdateTypeDropdowns(); // update type dropdowns to disable unavailable digital/analog types (I2S/RMT bus count may have changed due to memory usage change)\n\t\t\t// note: do not remvoe this second call to updateTypeDropdowns() as it also updates the available LED types based on the current bus configuration, not just the driver options\n\n\t\t\t// Show channel usage warning\n\t\t\tlet chanuse = gId('chanuse');\n\t\t\tlet channelMsg = gId('chanusemsg');\n\t\t\tif (chanuse && channelMsg && !is8266()) {\n\t\t\t\tchanuse.style.display = 'inline';\n\t\t\t\tchanuse.style.color = '#ccc';\n\t\t\t\tchannelMsg.textContent = `Hardware channels used: RMT ${usage.rmtUsed}/${maxRMT}, I2S ${usage.i2sUsed}/${maxI2S}`;\n\t\t\t\tif (usage.rmtUsed > maxRMT || usage.i2sUsed > maxI2S) {\n\t\t\t\t\tchanuse.style.color = 'red';\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// distribute ABL current if not using PPL\n\t\t\tenPPL(sDI);\n\n\t\t\t// update total led count\n\t\t\tgId(\"lc\").textContent = sLC;\n\t\t\tgId(\"pc\").textContent = (sLC == sPC) ? \"\":\"(\" + sPC + \" physical)\";\n\n\t\t\t// memory usage and warnings\n\t\t\tgId('m0').innerHTML = memu;\n\t\t\tbquot = memu / maxM * 100;\n\t\t\tgId('dbar').style.background = `linear-gradient(90deg, ${bquot > 60 ? (bquot > 90 ? \"red\":\"orange\"):\"#ccc\"} 0 ${bquot}%, #444 ${bquot}% 100%)`;\n\t\t\tgId('ledwarning').style.display = (maxLC > Math.min(maxPB,800) || bquot > 80) ? 'inline':'none';\n\t\t\tgId('ledwarning').style.color = (maxLC > Math.max(maxPB,800) || bquot > 100) ? 'red':'orange';\n\t\t\tgId('wreason').innerHTML = (bquot > 80) ? \"80% of max LED memory\" +(bquot>100 ? ` (<b>WARNING: using over ${maxM}B!</b>)` : \"\") : \"800 LEDs per output\";\n\t\t\t// calculate power\n\t\t\tgId('ampwarning').style.display = (parseInt(d.Sf.MA.value,10) > 7200) ? 'inline':'none';\n\t\t\tvar val = Math.ceil((100 + busMA)/500)/2;\n\t\t\tval = (val > 5) ? Math.ceil(val) : val;\n\t\t\tvar s = \"A power supply with total of \";\n\t\t\ts += val;\n\t\t\ts += \"A is required.\";\n\t\t\tvar val2 = Math.ceil((100 + busMA)/1500)/2;\n\t\t\tval2 = (val2 > 5) ? Math.ceil(val2) : val2;\n\t\t\tvar s2 = \"(for most effects, ~\";\n\t\t\ts2 += val2;\n\t\t\ts2 += \"A is enough)<br>\";\n\t\t\tgId('psu').innerHTML = s;\n\t\t\tgId('psu2').innerHTML = s2;\n\t\t\tgId(\"json\").style.display = d.Sf.IT.value==8 ? \"\" : \"none\";\n\n\t\t\t// show/hide FPS warning messages\n\t\t\tgId('fpsNone').style.display = (d.Sf.FR.value == 0) ? 'block':'none';\n\t\t\tgId('fpsWarn').style.display = (d.Sf.FR.value == 0) || (d.Sf.FR.value >= 80) ? 'block':'none';\n\t\t\tgId('fpsHigh').style.display = (d.Sf.FR.value >= 80) ? 'block':'none';\n\t\t}\n\n\t\tfunction lastEnd(i) {\n\t\t\tif (i-- < 1) return 0;\n\t\t\tvar s = chrID(i);\n\t\t\tv = parseInt(d.getElementsByName(\"LS\"+s)[0].value) + parseInt(d.getElementsByName(\"LC\"+s)[0].value);\n\t\t\tvar t = parseInt(d.getElementsByName(\"LT\"+s)[0].value);\n\t\t\tif (isPWM(t)) v = 1; //PWM busses\n\t\t\treturn isNaN(v) ? 0 : v;\n\t\t}\n\n\t\tfunction addLEDs(n,init=true)\n\t\t{\n\t\t\tvar o = gEBCN(\"iST\");\n\t\t\tvar i = o.length;\n\n\t\t\tvar f = gId(\"mLC\");\n\n\t\t\tif ((n==1 && i>=36) || (n==-1 && i==0)) return; // used to be i>=maxB+maxV when virtual buses were limited (now :\"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n\t\t\tvar s = chrID(i);\n\n\t\t\tif (n==1) {\n// npm run build has trouble minimizing spaces inside string\n\t\t\t\tvar cn = `<div class=\"iST\">\n<hr class=\"sml\">\n${i+1}:\n<select name=\"LT${s}\" onchange=\"UI(true)\"></select><br>\n<div id=\"abl${s}\">\nmA/LED: <select name=\"LAsel${s}\" onchange=\"enLA(this,'${s}');UI();\">\n<option value=\"55\" selected>55mA (typ. 5V WS281x)</option>\n<option value=\"35\">35mA (eco WS2812)</option>\n<option value=\"30\">30mA (typ. 12V)</option>\n<option value=\"255\">12mA (WS2815)</option>\n<option value=\"15\">15mA (seed/fairy pixels)</option>\n<option value=\"0\">Custom</option>\n</select><br>\n<div id=\"LAdis${s}\" style=\"display: none;\">max. mA/LED: <input name=\"LA${s}\" type=\"number\" min=\"1\" max=\"255\" oninput=\"UI()\"> mA<br></div>\n<div id=\"PSU${s}\">PSU: <input name=\"MA${s}\" type=\"number\" class=\"xl\" min=\"250\" max=\"65000\" oninput=\"UI()\" value=\"250\"> mA<br></div>\n</div>\n<div id=\"co${s}\" style=\"display:inline\">Color Order:\n<select name=\"CO${s}\">\n<option value=\"0\">GRB</option>\n<option value=\"1\">RGB</option>\n<option value=\"2\">BRG</option>\n<option value=\"3\">RBG</option>\n<option value=\"4\">BGR</option>\n<option value=\"5\">GBR</option>\n</select></div>\n<div id=\"dig${s}w\" style=\"display:none\">Swap: <select name=\"WO${s}\"><option value=\"0\">None</option><option value=\"1\">W & B</option><option value=\"2\">W & G</option><option value=\"3\">W & R</option><option data-opt=\"CCT\" value=\"4\">WW & CW</option></select></div>\n<div id=\"dig${s}l\" style=\"display:none\">Clock: <select name=\"SP${s}\"><option value=\"0\">Slowest</option><option value=\"1\">Slow</option><option value=\"2\">Normal</option><option value=\"3\">Fast</option><option value=\"4\">Fastest</option></select></div>\n<div>\n<span id=\"psd${s}\">Start:</span> <input type=\"number\" name=\"LS${s}\" id=\"ls${s}\" class=\"l starts\" min=\"0\" max=\"8191\" value=\"${lastEnd(i)}\" oninput=\"startsDirty[${i}]=true;UI();\" required />&nbsp;\n<div id=\"dig${s}c\" style=\"display:inline\">Length: <input type=\"number\" name=\"LC${s}\" class=\"l\" min=\"1\" max=\"${maxPB}\" value=\"1\" required oninput=\"UI();\" /></div><br>\n</div>\n<span id=\"p0d${s}\">GPIO:</span><input type=\"number\" name=\"L0${s}\" required class=\"s\" onchange=\"UI();pinUpd(this);\"/>\n<span id=\"p1d${s}\"></span><input type=\"number\" name=\"L1${s}\" class=\"s\" onchange=\"UI();pinUpd(this);\"/>\n<span id=\"p2d${s}\"></span><input type=\"number\" name=\"L2${s}\" class=\"s\" onchange=\"UI();pinUpd(this);\"/>\n<span id=\"p3d${s}\"></span><input type=\"number\" name=\"L3${s}\" class=\"s\" onchange=\"UI();pinUpd(this);\"/>\n<span id=\"p4d${s}\"></span><input type=\"number\" name=\"L4${s}\" class=\"s\" onchange=\"UI();pinUpd(this);\"/>\n<div id=\"drvsel${s}\" style=\"display:none\">\nDriver: <select name=\"LD${s}\" onchange=\"UI()\">\n<option value=\"0\">RMT</option>\n<option value=\"1\">I2S</option>\n</select>\n</div>\n<div id=\"net${s}h\" class=\"hide\">Host: <input type=\"text\" name=\"HS${s}\" maxlength=\"32\" pattern=\"[a-zA-Z0-9_\\\\-]*\" onchange=\"UI()\"/>.local</div>\n<div id=\"dig${s}r\" style=\"display:inline\"><br><span id=\"rev${s}\">Reversed</span>: <input type=\"checkbox\" name=\"CV${s}\"></div>\n<div id=\"dig${s}s\" style=\"display:inline\"><br>Skip first LEDs: <input type=\"number\" name=\"SL${s}\" min=\"0\" max=\"255\" value=\"0\" oninput=\"UI()\"></div>\n<div id=\"dig${s}f\" style=\"display:inline\"><br><span id=\"off${s}\">Off Refresh</span>: <input id=\"rf${s}\" type=\"checkbox\" name=\"RF${s}\"></div>\n<div id=\"dig${s}a\" style=\"display:inline\"><br>Auto-calculate W channel from RGB:<br><select name=\"AW${s}\"><option value=0>None</option><option value=1>Brighter</option><option value=2>Accurate</option><option value=3>Dual</option><option value=4>Max</option></select>&nbsp;</div>\n</div>`;\n\t\t\t\tf.insertAdjacentHTML(\"beforeend\", cn);\n\t\t\t\t// fill led types (credit @netmindz)\n\t\t\t\tf.querySelectorAll(\"select[name^=LT]\").forEach((sel,n)=>{\n\t\t\t\t\tif (sel.length == 0) { // ignore already updated\n\t\t\t\t\t\tfor (let type of d.ledTypes) {\n\t\t\t\t\t\t\tlet opt = cE(\"option\");\n\t\t\t\t\t\t\topt.value = type.i;\n\t\t\t\t\t\t\topt.text = type.n;\n\t\t\t\t\t\t\tif (type.t != undefined && type.t != \"\") {\n\t\t\t\t\t\t\t\topt.setAttribute('data-type', type.t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tsel.appendChild(opt);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tenLA(d.Sf[\"LAsel\"+s],s); // update LED mA\n\t\t\t\t// temporarily set to virtual (network) type to avoid \"same type\" exception during dropdown update\n\t\t\t\tlet sel = d.getElementsByName(\"LT\"+s)[0];\n\t\t\t\tsel.value = sel.querySelector('option[data-type=\"N\"]').value;\n\t\t\t\tupdateTypeDropdowns(); // update valid bus options including this new one\n\t\t\t\tsel.selectedIndex = sel.querySelector('option:not(:disabled)').index;\n\t\t\t\tupdateTypeDropdowns(); // update again for the newly selected type\n\t\t\t}\n\t\t\tif (n==-1) {\n\t\t\t\to[--i].remove();--i;\n\t\t\t}\n\n\t\t\tgId(\"+\").style.display = (i<35) ? \"inline\":\"none\"; // was maxB+maxV-1 when virtual buses were limited (now :\"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n\t\t\tgId(\"-\").style.display = (i>0) ? \"inline\":\"none\";\n\n\t\t\tif (!init) {\n\t\t\t\tUI();\n\t\t\t}\n\t\t}\n\n\t\tfunction addCOM(start=0,len=1,co=0) {\n\t\t\tvar i = gEBCN(\"com_entry\").length;\n\t\t\tif (i >= maxCO) return;\n\t\t\tvar s = chrID(i);\n\t\t\tvar b = `<div class=\"com_entry\">\n<hr class=\"sml\">\n${i+1}: Start: <input type=\"number\" name=\"XS${s}\" id=\"xs${s}\" class=\"l starts\" min=\"0\" max=\"65535\" value=\"${start}\" oninput=\"UI();\" required=\"\">&nbsp;\nLength: <input type=\"number\" name=\"XC${s}\" id=\"xc${s}\" class=\"l\" min=\"1\" max=\"65535\" value=\"${len}\" required=\"\" oninput=\"UI()\">\n<div>Color Order:\n<select id=\"xo${s}\" name=\"XO${s}\">\n<option value=\"0\">GRB</option>\n<option value=\"1\">RGB</option>\n<option value=\"2\">BRG</option>\n<option value=\"3\">RBG</option>\n<option value=\"4\">BGR</option>\n<option value=\"5\">GBR</option>\n</select>\nSwap: <select id=\"xw${s}\" name=\"XW${s}\">\n<option value=\"0\">Use global</option>\n<option value=\"1\">W & B</option>\n<option value=\"2\">W & G</option>\n<option value=\"3\">W & R</option>\n</select>\n</div></div>`;\n\t\t\tgId(\"com_entries\").insertAdjacentHTML(\"beforeend\", b);\n\t\t\tgId(\"xo\"+s).value = co & 0x0F;\n\t\t\tgId(\"xw\"+s).value = co >> 4;\n\t\t\tbtnCOM(i+1);\n\t\t\tUI();\n\t\t}\n\n\t\tfunction remCOM() {\n\t\t\tvar entries = gEBCN(\"com_entry\");\n\t\t\tvar i = entries.length;\n\t\t\tif (i === 0) return;\n\t\t\tentries[i-1].remove();\n\t\t\tbtnCOM(i-1);\n\t\t\tUI();\n\t\t}\n\n\t\tfunction resetCOM(_newMaxCOOverrides=undefined) {\n\t\t\tif (_newMaxCOOverrides) {\n\t\t\t\tmaxCO = _newMaxCOOverrides;\n\t\t\t}\n\t\t\tfor (let e of gEBCN(\"com_entry\")) {\n\t\t\t\te.remove();\n\t\t\t}\n\t\t\tbtnCOM(0);\n\t\t}\n\n\t\tfunction btnCOM(i) {\n\t\t\tgId(\"com_add\").style.display = (i<maxCO) ? \"inline\":\"none\";\n\t\t\tgId(\"com_rem\").style.display = (i>0) ? \"inline\":\"none\";\n\t\t}\n\n\t\tfunction addBtn(i,p,t) {\n\t\t\tvar b = gId(\"btns\");\n\t\t\tvar s = chrID(i);\n\t\t\tvar c = `<div id=\"btn${i}\">#${i} GPIO: <input type=\"number\" name=\"BT${s}\" onchange=\"UI()\" min=\"-1\" max=\"${d.max_gpio}\" class=\"xs\" value=\"${p}\">`;\n\t\t\tc += `&nbsp;<select name=\"BE${s}\">`\n\t\t\tc += `<option value=\"0\" ${t==0?\"selected\":\"\"}>Disabled</option>`;\n\t\t\tc += `<option value=\"2\" ${t==2?\"selected\":\"\"}>Pushbutton</option>`;\n\t\t\tc += `<option value=\"3\" ${t==3?\"selected\":\"\"}>Push inverted</option>`;\n\t\t\tc += `<option value=\"4\" ${t==4?\"selected\":\"\"}>Switch</option>`;\n\t\t\tc += `<option value=\"5\" ${t==5?\"selected\":\"\"}>PIR sensor</option>`;\n\t\t\tc += `<option value=\"6\" ${t==6?\"selected\":\"\"}>Touch</option>`;\n\t\t\tc += `<option value=\"7\" ${t==7?\"selected\":\"\"}>Analog</option>`;\n\t\t\tc += `<option value=\"8\" ${t==8?\"selected\":\"\"}>Analog inverted</option>`;\n\t\t\tc += `<option value=\"9\" ${t==9?\"selected\":\"\"}>Touch (switch)</option>`;\n\t\t\tc += `</select>`;\n\t\t\tc += `<span style=\"cursor: pointer;\" onclick=\"off('BT${s}')\">&nbsp;&#x2715;</span><br></div>`;\n\t\t\tb.insertAdjacentHTML(\"beforeend\", c);\n\t\t\tbtnBtn();\n\t\t\tpinDropdowns();\n\t\t\tUI();\n\t\t}\n\t\tfunction remBtn() {\n\t\t\tvar b = gId(\"btns\");\n\t\t\tif (b.children.length <= 1) return;\n\t\t\tb.lastElementChild.remove();\n\t\t\tbtnBtn();\n\t\t\tpinDropdowns();\n\t\t\tUI();\n\t\t}\n\t\tfunction btnBtn() {\n\t\t\tvar b = gId(\"btns\");\n\t\t\tgId(\"btn_rem\").style.display = (b.children.length > 1) ? \"inline\" : \"none\";\n\t\t\tgId(\"btn_add\").style.display = (b.children.length < maxBT) ? \"inline\" : \"none\";\n\t\t}\n\t\tfunction tglSi(cs) {\n\t\t\tcustomStarts = cs;\n\t\t\tif (!customStarts) startsDirty = []; //set all starts to clean\n\t\t\tUI();\n\t\t}\n\t\tfunction checkSi() { //on load, checks whether there are custom start fields\n\t\t\tvar cs = false;\n\t\t\tfor (var i=1; i < gEBCN(\"iST\").length; i++) {\n\t\t\t\tvar s = chrID(i);\n\t\t\t\tvar p = chrID(i-1); // cover edge case 'A' previous char being '9'\n\t\t\t\tvar v = parseInt(gId(\"ls\"+p).value) + parseInt(gN(\"LC\"+p).value);\n\t\t\t\tif (v != parseInt(gId(\"ls\"+s).value)) {cs = true; startsDirty[i] = true;}\n\t\t\t}\n\t\t\tif (gId(\"ls0\") && parseInt(gId(\"ls0\").value) != 0) {cs = true; startsDirty[0] = true;}\n\t\t\tgId(\"si\").checked = cs;\n\t\t\ttglSi(cs);\n\t\t}\n\t\t// https://stackoverflow.com/questions/7346563/loading-local-json-file\n\t\tfunction loadCfg(o) {\n\t\t\tvar f, fr;\n\n\t\t\tif (typeof window.FileReader !== 'function') {\n\t\t\t\talert(\"The file API isn't supported on this browser yet.\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!o.files) {\n\t\t\t\talert(\"This browser doesn't support the `files` property of file inputs.\");\n\t\t\t} else if (!o.files[0]) {\n\t\t\t\talert(\"Please select a JSON file first!\");\n\t\t\t} else {\n\t\t\t\tf = o.files[0];\n\t\t\t\tfr = new FileReader();\n\t\t\t\tfr.onload = receivedText;\n\t\t\t\tfr.readAsText(f);\n\t\t\t}\n\t\t\to.value = '';\n\n\t\t\tfunction receivedText(e) {\n\t\t\t\tlet lines = e.target.result;\n\t\t\t\tlet c = JSON.parse(lines);\n\t\t\t\tif (c.hw) {\n\t\t\t\t\tif (c.hw.led) {\n\t\t\t\t\t\t// remove all existing outputs\n\t\t\t\t\t\tfor (const i=0; i<36; i++) addLEDs(-1); // was i<maxb+maxV when number of virtual buses was limited (now :\"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n\t\t\t\t\t\tlet l = c.hw.led;\n\t\t\t\t\t\tl.ins.forEach((v,i,a)=>{\n\t\t\t\t\t\t\taddLEDs(1);\n\t\t\t\t\t\t\tfor (var j=0; j<v.pin.length; j++) d.getElementsByName(`L${j}${i}`)[0].value = v.pin[j];\n\t\t\t\t\t\t\td.getElementsByName(\"LT\"+i)[0].value   = v.type;\n\t\t\t\t\t\t\td.getElementsByName(\"LD\"+i)[0].value   = v.drv | 0; // output driver type (RMT or I2S, default to RMT if not set)\n\t\t\t\t\t\t\td.getElementsByName(\"LS\"+i)[0].value   = v.start;\n\t\t\t\t\t\t\td.getElementsByName(\"LC\"+i)[0].value   = v.len;\n\t\t\t\t\t\t\td.getElementsByName(\"CO\"+i)[0].value   = v.order & 0x0F;\n\t\t\t\t\t\t\td.getElementsByName(\"SL\"+i)[0].value   = v.skip;\n\t\t\t\t\t\t\td.getElementsByName(\"RF\"+i)[0].checked = v.ref;\n\t\t\t\t\t\t\td.getElementsByName(\"CV\"+i)[0].checked = v.rev;\n\t\t\t\t\t\t\td.getElementsByName(\"AW\"+i)[0].value   = v.rgbwm;\n\t\t\t\t\t\t\td.getElementsByName(\"WO\"+i)[0].value   = (v.order>>4) & 0x0F;\n\t\t\t\t\t\t\td.getElementsByName(\"SP\"+i)[0].value   = v.freq;\n\t\t\t\t\t\t\td.getElementsByName(\"LA\"+i)[0].value   = v.ledma;\n\t\t\t\t\t\t\td.getElementsByName(\"MA\"+i)[0].value   = v.maxpwr;\n\t\t\t\t\t\t});\n\t\t\t\t\t\td.getElementsByName(\"MA\")[0].value    = l.maxpwr;\n\t\t\t\t\t\td.getElementsByName(\"ABL\")[0].checked = l.maxpwr > 0;\n\t\t\t\t\t}\n\t\t\t\t\tif(c.hw.com) {\n\t\t\t\t\t\tresetCOM();\n\t\t\t\t\t\tc.hw.com.forEach(e => {\n\t\t\t\t\t\t\taddCOM(e.start, e.len, e.order);\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tlet b = c.hw.btn;\n\t\t\t\t\tif (b) {\n\t\t\t\t\t\tif (Array.isArray(b.ins)) gId(\"btns\").innerHTML = \"\";\n\t\t\t\t\t\tb.ins.forEach((v,i,a)=>{\n\t\t\t\t\t\t\taddBtn(i,v.pin[0],v.type);\n\t\t\t\t\t\t});\n\t\t\t\t\t\td.getElementsByName(\"TT\")[0].value = b.tt;\n\t\t\t\t\t}\n\t\t\t\t\tlet ir = c.hw.ir;\n\t\t\t\t\tif (ir) {\n\t\t\t\t\t\td.getElementsByName(\"IR\")[0].value = ir.pin;\n\t\t\t\t\t\td.getElementsByName(\"IT\")[0].value = ir.type;\n\t\t\t\t\t}\n\t\t\t\t\tlet rl = c.hw.relay;\n\t\t\t\t\tif (rl) {\n\t\t\t\t\t\td.getElementsByName(\"RL\")[0].value   = rl.pin;\n\t\t\t\t\t\td.getElementsByName(\"RM\")[0].checked = rl.rev;\n\t\t\t\t\t\td.getElementsByName(\"RO\")[0].checked = rl.odrain;\n\t\t\t\t\t}\n\t\t\t\t\tlet li = c.light;\n\t\t\t\t\tif (li) {\n\t\t\t\t\t\td.getElementsByName(\"MS\")[0].checked  = li.aseg;\n\t\t\t\t\t}\n\t\t\t\t\tUI();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfunction pinDropdowns() {\n\t\t\tlet fields = [\"IR\",\"RL\"]; // IR & relay\n\t\t\tgId(\"btns\").querySelectorAll('input[type=\"number\"]').forEach((e)=>{fields.push(e.name);}) // buttons\n\t\t\tfor (let i of d.Sf.elements) {\n\t\t\t\tif (i.type === \"number\" && fields.includes(i.name)) { //select all pin select elements\n\t\t\t\t\tlet v = parseInt(i.value);\n\t\t\t\t\tlet sel = addDropdown(i.name,0);\n\t\t\t\t\tfor (var j = -1; j < d.max_gpio; j++) {\n\t\t\t\t\t\tif (d.rsvd.includes(j)) continue;\n\t\t\t\t\t\tlet foundPin = d.um_p.indexOf(j);\n\t\t\t\t\t\tlet txt = (j === -1) ? \"unused\" : `${j}`;\n\t\t\t\t\t\tif (foundPin >= 0 && j !== v) txt += ` used`; // already reserved pin\n\t\t\t\t\t\tif (d.ro_gpio.includes(j)) txt += \" (R/O)\";\n\t\t\t\t\t\tlet opt = addOption(sel, txt, j);\n\t\t\t\t\t\tif (j === v) opt.selected = true; // this is \"our\" pin\n\t\t\t\t\t\telse if (d.um_p.includes(j) && j > -1) opt.disabled = true; // someone else's pin\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// update select options\n\t\t\td.Sf.querySelectorAll(\"select.pin\").forEach((e)=>{pinUpd(e);});\n\t\t\t// add dataset values for LED GPIO pins\n\t\t\td.Sf.querySelectorAll(\".iST input.s[name^=L]\").forEach((i)=>{\n\t\t\t\tif (i.value!==\"\" && i.value>=0)\n\t\t\t\t\ti.dataset.val = i.value;\n\t\t\t});\n\t\t}\n\t\tfunction pinUpd(e) {\n\t\t\t// update changed select options across all usermods\n\t\t\tlet oldV = parseInt(e.dataset.val);\n\t\t\te.dataset.val = e.value;\n\t\t\tlet txt = e.name;\n\t\t\tlet pins = [];\n\t\t\td.Sf.querySelectorAll(\".iST input.s[name^=L]\").forEach((i)=>{\n\t\t\t\tif (i.value!==\"\" && i.value>=0 && i.max<255)\n\t\t\t\t\tpins.push(i.value);\n\t\t\t});\n\t\t\tlet selects = d.Sf.querySelectorAll(\"select.pin\");\n\t\t\tfor (let sel of selects) {\n\t\t\t\tif (sel == e) continue\n\t\t\t\tArray.from(sel.options).forEach((i)=>{\n\t\t\t\t\tlet led = pins.includes(i.value);\n\t\t\t\t\tif (!(i.value==oldV || i.value==e.value || led)) return;\n\t\t\t\t\tif (i.value == -1) {\n\t\t\t\t\t\ti.text = \"unused\";\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\ti.text = i.value;\n\t\t\t\t\tif (i.value==oldV) {\n\t\t\t\t\t\ti.disabled = false;\n\t\t\t\t\t}\n\t\t\t\t\tif (i.value==e.value || led) {\n\t\t\t\t\t\ti.disabled = true;\n\t\t\t\t\t\ti.text += ` ${led?'LED':txt}`;\n\t\t\t\t\t}\n\t\t\t\t\tif (d.ro_gpio.includes(parseInt(i.value))) i.text += \" (R/O)\";\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\t// https://stackoverflow.com/questions/39729741/javascript-change-input-text-to-select-option\n\t\tfunction addDropdown(field) {\n\t\t\tlet sel = cE('select');\n\t\t\tsel.classList.add(\"pin\");\n\t\t\tlet inp = d.getElementsByName(field)[0];\n\t\t\tif (inp && inp.tagName === \"INPUT\" && (inp.type === \"text\" || inp.type === \"number\")) {  // may also use nodeName\n\t\t\t\tlet v = inp.value;\n\t\t\t\tlet n = inp.name;\n\t\t\t\t// copy the existing input element's attributes to the new select element\n\t\t\t\tfor (var i = 0; i < inp.attributes.length; ++ i) {\n\t\t\t\t\tvar att = inp.attributes[i];\n\t\t\t\t\t// type and value don't apply, so skip them\n\t\t\t\t\t// ** you might also want to skip style, or others -- modify as needed **\n\t\t\t\t\tif (att.name != 'type' && att.name != 'value' && att.name != 'class' && att.name != 'style') {\n\t\t\t\t\t\tsel.setAttribute(att.name, att.value);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tsel.setAttribute(\"data-val\", v);\n\t\t\t\tsel.setAttribute(\"onchange\", \"pinUpd(this)\");\n\t\t\t\t// finally, replace the old input element with the new select element\n\t\t\t\tinp.parentElement.replaceChild(sel, inp);\n\t\t\t\treturn sel;\n\t\t\t}\n\t\t\treturn null;\n\t\t}\n\t\tfunction addOption(sel,txt,val) {\n\t\t\tif (sel===null) return; // select object missing\n\t\t\tlet opt = cE(\"option\");\n\t\t\topt.value = val;\n\t\t\topt.text = txt;\n\t\t\tsel.appendChild(opt);\n\t\t\tfor (let i=0; i<sel.childNodes.length; i++) {\n\t\t\t\tlet c = sel.childNodes[i];\n\t\t\t\tif (c.value == sel.dataset.val) sel.selectedIndex = i;\n\t\t\t}\n\t\t\treturn opt;\n\t\t}\n\t\t// calculate channel usage across all buses\n\t\tfunction getDuse() {\n\t\t\tlet rmtUsed = 0, i2sUsed = 0;\n\t\t\tlet I2SType = null;\n\t\t\tlet I2Smem = 0; // DMA memory usage for I2S buses: 3x LED count for single I2S bus, 24x LED count for parallel I2S\n\t\t\tlet maxLEDs = 0; // max number of LEDs for DMA buffer calc\n\t\t\tif (!is8266()) {\n\t\t\t\td.Sf.querySelectorAll(\"#mLC select[name^=LT]\").forEach(sel => {\n\t\t\t\t\tlet n = sel.name.substring(2,3);\n\t\t\t\t\tlet t = parseInt(sel.value);\n\t\t\t\t\tlet driverPref = d.Sf[\"LD\"+n]?.value | 0;\n\t\t\t\t\tlet ledCount = (parseInt(d.Sf[\"LC\"+n].value) || 0) + (parseInt(d.Sf[\"SL\"+n].value) || 0);\n\n\t\t\t\t\tif (isDig(t) && !isD2P(t)) {\n\t\t\t\t\t\tif (driverPref === 1) {\n\t\t\t\t\t\t\ti2sUsed++;\n\t\t\t\t\t\t\tmaxLEDs = maxLEDs > ledCount ? maxLEDs : ledCount; // for parallel I2S the memory buffer is shared, largest bus determines total memory usage\n\t\t\t\t\t\t\tif (!I2SType) I2SType = t; // first I2S bus determines allowed type for all subsequent I2S buses (parallel I2S limitation)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trmtUsed++;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\t// calculate I2S memory usage\n\t\t\t\tif (I2SType) {\n\t\t\t\t\tlet ch = 3*hasRGB(I2SType) + hasW(I2SType) + hasCCT(I2SType); // byte channel count per LED\n\t\t\t\t\tif (is16b(I2SType)) maxLEDs *= 2; // 16 bit LEDs use 2 bytes per channel\n\t\t\t\t\tI2Smem = maxLEDs * ch * (i2sUsed > 1 || isS3() ? 24 : 3); // 3 bytes per LED byte for single I2S, 24 bytes per LED byte for parallel I2S (S3 always uses parallel), assumes 3-step cadence\n\t\t\t\t\tI2Smem = Math.round(I2Smem / i2sUsed); // average memory per I2S bus (used for memory estimation), round to nearest integer to avoid float rounding errors\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { rmtUsed, i2sUsed, I2SType, I2Smem };\n\t\t}\n\n\t\t// dynamically enforce bus type availability based on current usage\n\t\tfunction updateTypeDropdowns() {\n\t\t\tlet LTs = d.Sf.querySelectorAll(\"#mLC select[name^=LT]\");\n\t\t\tlet digitalB = 0, analogB = 0, twopinB = 0, virtB = 0;\n\n\t\t\t// calculate channel usage\n\t\t\tlet usage = getDuse();\n\t\t\tlet firstI2SType = null;\n\n\t\t\t// Count all bus types\n\t\t\tLTs.forEach(sel => {\n\t\t\t\tlet t = parseInt(sel.value);\n\t\t\t\tif (isDig(t) && !isD2P(t)) digitalB++;\n\t\t\t\tif (isPWM(t)) analogB += numPins(t);\n\t\t\t\tif (isD2P(t)) twopinB++;\n\t\t\t\tif (isVir(t)) virtB++;\n\t\t\t});\n\n\t\t\t// update each LED-type and driver dropdown with appropriate constraints\n\t\t\tlet RMTcount = 0;\n\t\t\tlet I2Scount = 0;\n\t\t\tLTs.forEach(sel => {\n\t\t\t\tlet n = sel.name.substring(2,3);\n\t\t\t\tconst curType = parseInt(sel.value);\n\t\t\t\tconst curDriver = d.Sf[\"LD\"+n]?.value | 0;\n\t\t\t\tconst disable = (q) => sel.querySelectorAll(q).forEach(o => o.disabled = true);\n\t\t\t\tconst enable  = (q) => sel.querySelectorAll(q).forEach(o => o.disabled = false);\n\t\t\t\tenable('option'); // reset all first\n\t\t\t\t// Update LED type constraints for digital buses\n\t\t\t\tif (isDig(curType) && !isD2P(curType)) {\n\t\t\t\t\t// If this bus uses I2S and other I2S buses exist, restrict to same type\n\t\t\t\t\tif (curDriver === 1) {\n\t\t\t\t\t\tif (firstI2SType == null) firstI2SType = curType; // set first type, first I2S bus is allowed to change to any digital type\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\tsel.querySelectorAll('option[data-type=\"D\"]').forEach(o => {\n\t\t\t\t\t\t\t\tif (parseInt(o.value) !== firstI2SType) o.disabled = true;\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t \tif (digitalB >= maxD) disable('option[data-type=\"D\"]'); // disable digital bus options if limit reached\n\t\t\t\t\telse if (!is8266() && usage.rmtUsed >= maxRMT && (firstI2SType != null)) {\n\t\t\t\t\t\t// there are still digital buses available, restrict digital bus options to I2S type if RMT is full (ESP32 only)\n\t\t\t\t\t\tsel.querySelectorAll('option[data-type=\"D\"]').forEach(o => {\n\t\t\t\t\t\t\tif (parseInt(o.value) !== firstI2SType) o.disabled = true;\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// 2-pin digital buses limited to 2\n\t\t\t\tif (twopinB >= 2 && !isD2P(curType)) disable('option[data-type=\"2P\"]');\n\t\t\t\t// PWM analog types limited by pin count\n\t\t\t\tdisable(`option[data-type^=\"${'A'.repeat(maxA - analogB + (isPWM(curType)?numPins(curType):0) + 1)}\"]`);\n\n\t\t\t\t// update driver selection dropdowns\n\t\t\t\tlet drvsel = gId(\"drvsel\"+n);\n\t\t\t\tif (drvsel && isDig(curType) && !isD2P(curType)) {\n\t\t\t\t\tlet rmtOpt = drvsel.querySelector('option[value=\"0\"]');\n\t\t\t\t\tlet i2sOpt = drvsel.querySelector('option[value=\"1\"]');\n\t\t\t\t\trmtOpt.disabled = false;\n\t\t\t\t\ti2sOpt.disabled = false;\n\t\t\t\t\tif (curDriver === 0) {\n\t\t\t\t\t\tif ((usage.i2sUsed >= maxI2S))\ti2sOpt.disabled = true; // disable I2S selection on RMT buses if full\n\t\t\t\t\t\tRMTcount++;\n\t\t\t\t\t\tif (RMTcount > maxRMT) {\n\t\t\t\t\t\t\trmtOpt.disabled = true; // disable RMT if now full (other logic disables digital types if both full)\n\t\t\t\t\t\t\td.Sf[\"LD\"+n].value = \"1\"; // switch to I2S (if no I2S available, digital type is disabled above)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tif ((usage.rmtUsed >= maxRMT))\trmtOpt.disabled = true; // disable RMT selection on I2S buses if full\n\t\t\t\t\t\tI2Scount++;\n\t\t\t\t\t\tif (I2Scount > maxI2S) {\n\t\t\t\t\t\t\ti2sOpt.disabled = true; // disable I2S if now full (other logic disables digital types if both full)\n\t\t\t\t\t\t\td.Sf[\"LD\"+n].value = \"0\"; // switch to RMT (if no RMT available, digital type is disabled above)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t</script>\n</head>\n<body>\n<form id=\"form_s\" name=\"Sf\" method=\"post\">\n\t<div class=\"toprow\">\n\t<div class=\"helpB\"><button type=\"button\" onclick=\"H('features/settings/#led-settings')\">?</button></div>\n\t<button type=\"button\" onclick=\"B()\">Back</button><button type=\"submit\">Save</button><hr>\n\t</div>\n\t<h2>LED setup</h2>\n\t<div class=\"sec\">\n\t\tTotal LEDs: <span id=\"lc\">?</span> <span id=\"pc\"></span><br>\n\t\t<i>Recommended power supply for brightest white:</i><br>\n\t\t<b><span id=\"psu\">?</span></b><br>\n\t\t<span id=\"psu2\"><br></span>\n\t\t<br>\n\t\tGlobal brightness factor: <input name=\"BF\" type=\"number\" class=\"m\" min=\"1\" max=\"255\" required> %<br>\n\t\tEnable automatic brightness limiter: <input type=\"checkbox\" name=\"ABL\" onchange=\"enABL()\"><br>\n\t\t<div id=\"abl\">\n\t\t\t<i>Automatically limits brightness to stay close to the limit.<br>\n\t\t\t\tKeep at &lt;1A if powering LEDs directly from the ESP 5V pin!<br>\n\t\t\t\tIf using multiple outputs it is recommended to use per-output limiter.<br>\n\t\t\t\tAnalog (PWM) and virtual LEDs cannot use automatic brightness limiter.<br></i>\n\t\t\t<div id=\"psuMA\">Maximum PSU Current: <input name=\"MA\" type=\"number\" class=\"xl\" min=\"250\" max=\"65000\" oninput=\"UI()\" required> mA<br></div>\n\t\t\tUse per-output limiter: <input type=\"checkbox\" name=\"PPL\" onchange=\"UI()\"><br><br>\n\t\t\t<div id=\"ppldis\" style=\"display:none;\">\n\t\t\t\t<i>Make sure you enter correct value for each LED output.<br>\n\t\t\t\tIf using multiple outputs with only one PSU, distribute its power proportionally amongst outputs.</i><br>\n\t\t\t</div>\n\t\t\t<div id=\"ampwarning\" class=\"warn\" style=\"display: none;\">\n\t\t\t\t&#9888; Your power supply provides high current.<br>\n\t\t\t\tTo improve the safety of your setup,<br>\n\t\t\t\tplease use thick cables,<br>\n\t\t\t\tmultiple power injection points and a fuse!<br>\n\t\t\t</div>\n\t\t</div>\n\t\t<div id=\"mLC\"><h4>LED outputs:</h4></div>\n\t\t<hr class=\"sml\">\n\t\t<button type=\"button\" id=\"+\" onclick=\"addLEDs(1,false)\">+</button>\n\t\t<button type=\"button\" id=\"-\" onclick=\"addLEDs(-1,false)\">-</button><br>\n\t\tLED memory usage: <span id=\"m0\">0</span> / <span id=\"m1\">?</span> B<br>\n\t\t<div id=\"dbar\" style=\"display:inline-block; width: 100px; height: 10px; border-radius: 20px;\"></div><br>\n\t\t<div id=\"ledwarning\" class=\"warn\" style=\"display: none;\">\n\t\t\t&#9888; You might run into stability or lag issues.<br>\n\t\t\tUse less than <span id=\"wreason\">800 LEDs per output</span> for the best experience!<br>\n\t\t</div>\n\t\t<div id=\"chanuse\" style=\"display: none;\">\n\t\t\t<span id=\"chanusemsg\"></span><br>\n\t\t</div>\n\t\t<hr class=\"sml\">\n\t\tShow Advanced Settings <input type=\"checkbox\" name=\"AS\" onchange=\"localStorage.setItem('ASc',this.checked);UI()\"><br>\n\t\tMake a segment for each output: <input type=\"checkbox\" name=\"MS\"><br>\n\t\tCustom bus start indices: <input type=\"checkbox\" onchange=\"tglSi(this.checked)\" id=\"si\"><br>\n\t\t<hr class=\"sml\">\n\t\t<div id=\"color_order_mapping\">\n\t\t\tColor Order Override:\n\t\t\t<div id=\"com_entries\"></div>\n\t\t\t<hr class=\"sml\">\n\t\t\t<button type=\"button\" id=\"com_add\" onclick=\"addCOM()\">+</button>\n\t\t\t<button type=\"button\" id=\"com_rem\" onclick=\"remCOM()\">-</button>\n\t\t</div>\n\t</div>\n\t<div class=\"sec\">\n\t\t<h3>Color & White</h3>\n\t\tUse Gamma correction for color: <input type=\"checkbox\" name=\"GC\"> (strongly recommended)<br>\n\t\tUse Gamma correction for brightness: <input type=\"checkbox\" name=\"GB\"> (not recommended)<br>\n\t\tUse Gamma value: <input name=\"GV\" type=\"number\" class=\"m\" placeholder=\"2.2\" min=\"0.1\" max=\"3\" step=\"0.1\" required><br><br>\n\t\tWhite Balance correction: <input type=\"checkbox\" name=\"CCT\"><br>\n\t\t<div id=\"wc\">\n\t\t\tGlobal override for Auto-calculate white:\n\t\t\t<select name=\"AW\">\n\t\t\t\t<option value=255>Disabled</option>\n\t\t\t\t<option value=0>None</option>\n\t\t\t\t<option value=1>Brighter</option>\n\t\t\t\t<option value=2>Accurate</option>\n\t\t\t\t<option value=3>Dual</option>\n\t\t\t\t<option value=4>Max</option>\n\t\t\t</select>\n\t\t\t<br>\n\t\t\tCalculate CCT from RGB: <input type=\"checkbox\" name=\"CR\"><br>\n\t\t\tCCT IC used (Athom 15W): <input type=\"checkbox\" name=\"IC\"><br>\n\t\t\tCCT blending (±100%): <input type=\"number\" class=\"s\" min=\"-100\" max=\"100\" name=\"CB\" onchange=\"UI()\" required> %<br>\n\t\t\t<small>Positive: additive blend, Negative: exclusive blend<br></small>\n\t\t\t<i class=\"warn\"><b>⚠</b> Set to 0 when using 2-wire (reverse polarity) CCT strips</i><br><br>\n\t\t</div>\n\t</div>\n\t<h2>Hardware setup</h2>\n\t<div class=\"sec\">\n\t\t<h3>Buttons</h3>\n\t\t<div id=\"btn_wrap\">\n\t\t\t<div id=\"btns\"></div>\n\t\t\t<hr class=\"sml\">\n\t\t\t<button type=\"button\" id=\"btn_add\" onclick=\"addBtn(gId('btns').children.length,-1,0)\">+</button>\n\t\t\t<button type=\"button\" id=\"btn_rem\" onclick=\"remBtn()\">-</button>\n\t\t</div>\n\t\tDisable internal pull-up/down: <input type=\"checkbox\" name=\"IP\"><br>\n\t\tTouch threshold: <input type=\"number\" class=\"s\" min=\"0\" max=\"100\" name=\"TT\" required><br><br>\n\t</div>\n\t<div class=\"sec\">\n\t\t<h3>IR Remote</h3>\n\t\tIR GPIO: <input type=\"number\" min=\"-1\" max=\"48\" name=\"IR\" onchange=\"UI()\" class=\"xs\"><select name=\"IT\" onchange=\"UI()\">\n\t\t<option value=0>Remote disabled</option>\n\t\t<option value=1>24-key RGB</option>\n\t\t<option value=2>24-key with CT</option>\n\t\t<option value=3>40-key blue</option>\n\t\t<option value=4>44-key RGB</option>\n\t\t<option value=5>21-key RGB</option>\n\t\t<option value=6>6-key black</option>\n\t\t<option value=7>9-key red</option>\n\t\t<option value=8>JSON remote</option>\n\t\t</select><span style=\"cursor: pointer;\" onclick=\"off('IR')\">&nbsp;&#x2715;</span><br>\n\t\tApply IR change to main segment only: <input type=\"checkbox\" name=\"MSO\"><br>\n\t\t<div id=\"json\" style=\"display:none;\">JSON file: <input type=\"file\" name=\"data\" accept=\".json\"><button type=\"button\" class=\"sml\" onclick=\"uploadFile(d.Sf.data,'/ir.json')\">Upload</button><br></div>\n\t\t<a href=\"https://kno.wled.ge/interfaces/infrared/\" target=\"_blank\">IR info</a><br><br>\n\t</div>\n\t<div class=\"sec\">\n\t\t<h3>Relay</h3>\n\t\tRelay GPIO: <input type=\"number\" min=\"-1\" max=\"48\" name=\"RL\" onchange=\"UI()\" class=\"xs\"><span style=\"cursor: pointer;\" onclick=\"off('RL')\">&nbsp;&#x2715;</span><br>\n\t\tInvert <input type=\"checkbox\" name=\"RM\"> Open drain <input type=\"checkbox\" name=\"RO\"><br><br>\n\t</div>\n\t<h2>General settings</h2>\n\t<div class=\"sec\">\n\t\t<h3>Power up</h3>\n\t\tTurn LEDs on after power up/reset: <input type=\"checkbox\" name=\"BO\"><br>\n\t\twith brightness: <input name=\"CA\" type=\"number\" class=\"m\" min=\"1\" max=\"255\" required> (1-255)<br>\n\t\t<i>(disable if using boot preset to turn LEDs on)</i><br><br>\n\t\tApply preset <input name=\"BP\" type=\"number\" class=\"m\" min=\"0\" max=\"250\" required> at boot (0 = none)<br><br>\n\t</div>\n\t<div class=\"sec\">\n\t\t<h3>Transitions</h3>\n\t\tDefault transition time: <input name=\"TD\" type=\"number\" class=\"xl\" min=\"0\" max=\"65500\"> ms<br><br>\n\t</div>\n\t<div class=\"sec\">\n\t\t<h3>Random Palettes</h3>\n\t\tUse harmonic colors in <i>Random palettes</i>: <input type=\"checkbox\" name=\"TH\"><br>\n\t\t<i>Random Palette</i> Cycle Time: <input name=\"TP\" type=\"number\" class=\"m\" min=\"1\" max=\"255\"> s<br><br>\n\t</div>\n\t<div class=\"sec\">\n\t\t<h3>Timed light</h3>\n\t\tDefault duration: <input name=\"TL\" type=\"number\" class=\"m\" min=\"1\" max=\"255\" required> min<br>\n\t\tDefault target brightness: <input name=\"TB\" type=\"number\" class=\"m\" min=\"0\" max=\"255\" required><br>\n\t\tMode:\n\t\t<select name=\"TW\">\n\t\t\t<option value=\"0\">Wait and set</option>\n\t\t\t<option value=\"1\">Fade</option>\n\t\t\t<option value=\"2\">Fade Color</option>\n\t\t\t<option value=\"3\">Sunrise</option>\n\t\t</select>\n\t\t<br><br>\n\t</div>\n\t<div class=\"sec\">\n\t\t<h3>Advanced</h3>\n\t\tPalette wrapping:\n\t\t<select name=\"PB\">\n\t\t\t<option value=\"0\">Linear (wrap if moving)</option>\n\t\t\t<option value=\"1\">Linear (always wrap)</option>\n\t\t\t<option value=\"2\">Linear (never wrap)</option>\n\t\t\t<option value=\"3\">None (not recommended)</option>\n\t\t</select><br>\n\t\tTarget refresh rate: <input type=\"number\" class=\"s\" min=\"0\" max=\"250\" name=\"FR\" oninput=\"UI()\" required> FPS\n\t\t<div id=\"fpsNone\" class=\"warn\" style=\"display: none;\">&#9888; Unlimited FPS Mode is experimental &#9888;<br></div>\n\t\t<div id=\"fpsHigh\" class=\"warn\" style=\"display: none;\">&#9888; High FPS Mode is experimental.<br></div>\n\t\t<div id=\"fpsWarn\" class=\"warn\" style=\"display: none;\">Please <a class=\"lnk\" href=\"sec#backup\">backup</a> WLED configuration and presets first!<br></div>\n\t\t<br><br>\n\t</div>\n\t<div id=\"cfg\">Config template: <input type=\"file\" name=\"data2\" accept=\".json\"><button type=\"button\" class=\"sml\" onclick=\"loadCfg(d.Sf.data2)\">Apply</button><br></div>\n\t<hr>\n\t<button type=\"button\" onclick=\"B()\">Back</button><button type=\"submit\">Save</button>\n</form>\n<div id=\"toast\"></div>\n</body>\n</html>\n"
  },
  {
    "path": "wled00/data/settings_pin.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\" name=\"viewport\">\n\t<meta charset=\"utf-8\">\n\t<title>PIN required</title>\n\t<script>\n\t\tvar d = document;\n\t\tfunction B() { window.open(\"../settings\",\"_self\"); }\n\t</script>\n\t<style>\n\t\t@import url(\"style.css\");\n\t</style>\n</head>\n<body onload=\"d.getElementsByName('PIN')[0].focus()\">\n\t<form id=\"form_s\" name=\"Sf\" method=\"post\">\n\t<div class=\"sec\">\n\t\t<h2>Please enter settings PIN code</h2>\n\t\t<input type=\"password\" name=\"PIN\" size=\"4\" maxlength=\"4\" minlength=\"4\" pattern=\"[0-9]*\" inputmode=\"numeric\" autofocus>\n\t\t<hr>\n\t\t<button type=\"button\" onclick=\"B()\">Back</button><button type=\"submit\">Submit</button>\n\t</div>\n\t</form>\n</body>\n</html>"
  },
  {
    "path": "wled00/data/settings_pininfo.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"utf-8\">\n\t<meta content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\" name=\"viewport\">\n\t<title>Pin Info</title>\n\t<script>\n\t\t// load common.js with retry on error\n\t\t(function loadFiles() {\n\t\t\tconst l = document.createElement('script');\n\t\t\tl.src = 'common.js';\n\t\t\tl.onload = () => loadResources(['style.css'], S); // load style.css then call S()\n\t\t\tl.onerror = () => setTimeout(loadFiles, 100);\n\t\t\tdocument.head.appendChild(l);\n\t\t})();\n\t\tvar pinsTimer=null, gpioInfo={};\n\t\tfunction S() {\n\t\t\tgetLoc();\n\t\t\tloadJS(getURL('/settings/s.js?p=11'), false, ()=>{\n\t\t\t\td.um_p = [];\n\t\t\t\td.rsvd = [];\n\t\t\t\td.ro_gpio = [];\n\t\t\t\td.max_gpio = 50;\n\t\t\t\td.touch = [];\n\t\t\t}, ()=>{\n\t\t\t\t// Load extended GPIO info and start pin polling\n\t\t\t\tloadPins();\n\t\t\t\tpinsTimer = setInterval(loadPins, 250);\n\t\t\t});\n\t\t}\n\n\t\tfunction B(){window.open(getURL('/settings'),'_self');} // back button\n\n\t\tfunction getOwnerName(o,t,n) {\n\t\t\t// Use firmware-provided name if available\n\t\t\tif(n) return n;\n\t\t\tif(!o) return \"System\"; // no owner provided\n\t\t\tif(o===0x85){ return getBtnTypeName(t); } // button pin\n\t\t\treturn \"UM #\"+o;\n\t\t}\n\t\tfunction getBtnTypeName(t) {\n\t\t\tvar n=[\"None\",\"Reserved\",\"Push\",\"Push Inv\",\"Switch\",\"PIR\",\"Touch\",\"Analog\",\"Analog Inv\",\"Touch Switch\"];\n\t\t\tvar label = n[t] || \"?\";\n\t\t\treturn 'Button <span style=\"font-size:10px;color:#888\">'+label+'</span>';\n\t\t}\n\t\tfunction getCaps(p,c) {\n\t\t\tvar r=[];\n\t\t\t// Use touch info from settings endpoint\n\t\t\tif(d.touch && d.touch.includes(p)) r.push(\"Touch\");\n\t\t\tif(d.ro_gpio && d.ro_gpio.includes(p)) r.push(\"Input Only\");\n\t\t\t// Use other caps from JSON (Analog, Boot, Input Only)\n\t\t\tif(c&0x02) r.push(\"Analog\");\n\t\t\tif(c&0x08) r.push(\"Flash Boot\");\n\t\t\tif(c&0x10) r.push(\"Bootstrap\");\n\t\t\treturn r.length?r.join(\", \"):\"-\";\n\t\t}\n\t\tfunction loadPins() {\n\t\t\tfetch(getURL('/json/pins'),{method:'get'})\n\t\t\t.then(r=>r.json())\n\t\t\t.then(j=>{\n\t\t\t\tvar cn=\"\",pins=j.pins||[];\n\t\t\t\tif(!pins.length) {\n\t\t\t\t\tcn=\"No pins available.\";\n\t\t\t\t}else{\n\t\t\t\t\tcn='<table><tr><th>Pin</th><th>Used by</th><th>Pin Notes</th></tr>';\n\t\t\t\t\tfor(var p of pins){\n\t\t\t\t\t\tvar st=\"\"; // button state indicator\n\t\t\t\t\t\tvar rv=\"\"; // raw value (touch / analog)\n\t\t\t\t\t\tif(typeof p.s!=='undefined'){\n\t\t\t\t\t\t\tst='<span class=\"bs\" style=\"background:'+(p.s?'#0B4':'#666')+'\"></span> ';\t// button state dot, gray=off, green=on\n\t\t\t\t\t\t\tif(typeof p.r!=='undefined') rv=' <span class=\"rv\">'+p.r+'</span>';\t\t\t\t\t// add raw touch reading if available\n\t\t\t\t\t\t}\n\t\t\t\t\t\tvar ow=p.a?getOwnerName(p.o, p.t, p.n):(d.um_p && d.um_p.includes(p.p)) ? \"Usermod\":'<span style=\"color:#08d\">Available</span>';\n\t\t\t\t\t\t//if(typeof p.u!=='undefined')ow+=p.u?' (PU)':' (No PU)';\n\t\t\t\t\t\tcn+='<tr><td>GPIO'+p.p+'</td><td>'+st+ow+rv+'</td><td>'+getCaps(p.p,p.c||0)+'</td></tr>';\n\t\t\t\t\t}\n\t\t\t\t\tcn+='</table>';\n\t\t\t\t}\n\t\t\t\tgId('pins').innerHTML=cn;\n\t\t\t})\n\t\t\t.catch(e=>{gId('pins').innerHTML='Error loading pin info';});\n\t\t}\n\t</script>\n\t<style>\n\t\tbody{text-align:center;background:#222;margin:auto;padding:10px;max-width: 550px}\n\t\ttable{width:100%;border-collapse:collapse;margin:10px 0;font-size:14px;border-radius:6px;overflow:hidden;}\n\t\tth,td{padding:8px;border:3px solid #444;color:#fff}\n\t\tth{background:#444}\n\t\ttr:nth-child(even){background:#222}\n\t\ttr:nth-child(odd){background:#111}\n\t\t.bs{display:inline-block;width:14px;height:14px;border-radius:50%} /* button state dot */\n\t\t.rv{display:inline-block;font-size:10px;color:#888;min-width:6ch;text-align:right;} /* raw value (touch / analog) */\n\t</style>\n</head>\n<body>\n\t<button type=\"button\" onclick=\"B()\">Back</button>\n\t<h2>Pin Info</h2>\n\t<div id=\"pins\">Loading...</div>\n\t<button type=\"button\" onclick=\"B()\">Back</button>\n</body>\n</html>\n"
  },
  {
    "path": "wled00/data/settings_sec.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\" name=\"viewport\">\n\t<meta charset=\"utf-8\">\n\t<title>Security & Update Setup</title>\n\t<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->\n\t<script>\n\t\t// load common.js with retry on error\n\t\t(function loadFiles() {\n\t\t\tconst l = document.createElement('script');\n\t\t\tl.src = 'common.js';\n\t\t\tl.onload = () => loadResources(['style.css'], S); // load style.css then call S()\n\t\t\tl.onerror = () => setTimeout(loadFiles, 100);\n\t\t\tdocument.head.appendChild(l);\n\t\t})();\n\t\tfunction U() { window.open(getURL(\"/update\"),\"_self\"); }\n\t\tfunction checkNum(o) {\n\t\t\tconst specialkeys = [\"Backspace\", \"Tab\", \"Enter\", \"Shift\", \"Control\", \"Alt\", \"Pause\", \"CapsLock\", \"Escape\", \"Space\", \"PageUp\", \"PageDown\", \"End\", \"Home\", \"ArrowLeft\", \"ArrowUp\", \"ArrowRight\", \"ArrowDown\", \"Insert\", \"Delete\"];\n\t\t\t// true if key is a number or a special key\n\t\t\tif(event.key.match(/[0-9]/) || specialkeys.includes(event.key)) return true;\n\t\t\tevent.preventDefault();\n\t\t\treturn false;\n\t\t}\n\t\tfunction setBckFilename(x) {\n\t\t\tx.setAttribute(\"download\",\"wled_\" + x.getAttribute(\"download\") + (sd==\"WLED\"?\"\":(\"_\" +sd)));\n\t\t}\n\t\tfunction S() {\n\t\t\tgetLoc();\n\t\t\tif (loc) {\n\t\t\t\tgId(\"bckcfg\").setAttribute('href',getURL(gId(\"bckcfg\").pathname));\n\t\t\t\tgId(\"bckpresets\").setAttribute('href',getURL(gId(\"bckpresets\").pathname));\n\t\t\t}\n\t\t\tloadJS(getURL('/settings/s.js?p=6'), false, undefined, ()=>{\n\t\t\t\tsetBckFilename(gId(\"bckcfg\"));\n\t\t\t\tsetBckFilename(gId(\"bckpresets\"));\n\t\t\t});\t// If we set async false, file is loaded and executed, then next statement is processed\n\t\t\tif (loc) d.Sf.action = getURL('/settings/sec');\n\t\t}\n\t</script>\n</head>\n<body>\n<form id=\"form_s\" name=\"Sf\" method=\"post\">\n\t<div class=\"toprow\">\n\t<div class=\"helpB\"><button type=\"button\" onclick=\"H('features/settings/#security-settings')\">?</button></div>\n\t<button type=\"button\" onclick=\"B()\">Back</button><button type=\"submit\">Save</button><hr>\n\t</div>\n\t\t<h2>Security & Update Setup</h2>\n\t<div class=\"sec\">\n\t\tSettings PIN: <input type=\"password\" id=\"PIN\" name=\"PIN\" size=\"4\" maxlength=\"4\" minlength=\"4\" onkeydown=\"checkNum(this)\" pattern=\"[0-9]*\" inputmode=\"numeric\" title=\"Please enter a 4 digit number\"><br>\n\t\t<div class=\"warn\">&#9888; Unencrypted transmission. Be prudent when selecting PIN, do NOT use your banking, door, SIM, etc. pin!</div><br>\n\t\tLock wireless (OTA) software update: <input type=\"checkbox\" name=\"NO\"><br>\n\t\tPassphrase: <input type=\"password\" name=\"OP\" maxlength=\"32\"><br>\n\t\tTo enable OTA, for security reasons you need to also enter the correct password!<br>\n\t\tThe password should be changed when OTA is enabled.<br>\n\t\t<b>Disable OTA when not in use, otherwise an attacker can reflash device software!</b><br>\n\t\t<i>Settings on this page are only changeable if OTA lock is disabled!</i><br>\n\t\tDeny access to WiFi settings if locked: <input type=\"checkbox\" name=\"OW\"><br><br>\n\t\tFactory reset: <input type=\"checkbox\" name=\"RS\"><br>\n\t\tAll settings and presets will be erased.<br><br>\n\t\t<div class=\"warn\">&#9888; Unencrypted transmission. An attacker on the same network can intercept form data!</div>\n\t</div>\n\t<div class=\"sec\" id=\"OTA\">\n\t\t<h3>Software Update</h3>\n\t\t<button type=\"button\" onclick=\"U()\">Manual OTA Update</button><br>\n\t\t<div id=\"aOTA\">Enable ArduinoOTA: <input type=\"checkbox\" name=\"AO\"></div>\n\t\tOnly allow update from same network/WiFi: <input type=\"checkbox\" name=\"SU\"><br>\n\t\t<i class=\"warn\">&#9888; If you are using multiple VLANs (i.e. IoT or guest network) either set PIN or disable this option.<br>\n\t\t\tDisabling this option will make your device less secure.</i><br></span>\n\t</div>\n\t<div class=\"sec\" id=\"backup\">\n\t\t<h3>Backup & Restore</h3>\n\t\t<div class=\"warn\">&#9888; Restoring presets/configuration will OVERWRITE your current presets/configuration.<br>\n\t\tIncorrect upload or configuration may require a factory reset or re-flashing of your ESP.<br>\n\t\tFor security reasons, passwords are not backed up.</div>\n\t\t<a class=\"btn lnk\" id=\"bckcfg\" href=\"/presets.json\" download=\"presets\">Backup presets</a><br>\n\t\t<div>Restore presets<br><input type=\"file\" name=\"data\" accept=\".json\"> <button type=\"button\" onclick=\"uploadFile(d.Sf.data,'/presets.json');\">Upload</button><br></div><br>\n\t\t<a class=\"btn lnk\" id=\"bckpresets\" href=\"/cfg.json\" download=\"cfg\">Backup configuration</a><br>\n\t\t<div>Restore configuration<br><input type=\"file\" name=\"data2\" accept=\".json\"> <button type=\"button\" onclick=\"uploadFile(d.Sf.data2,'/cfg.json');\">Upload</button><br></div>\n\t</div>\n\t<div class=\"sec\">\n\t\t<h3>About</h3>\n\t\t<a href=\"https://github.com/wled-dev/WLED/\" target=\"_blank\">WLED</a>&#32;version ##VERSION##<!-- Autoreplaced from package.json --><br><br>\n\t\t<a href=\"https://kno.wled.ge/about/contributors/\" target=\"_blank\">Contributors, dependencies and special thanks</a><br>\n\t\tA huge thank you to everyone who helped me create WLED!<br><br>\n\t\t(c) 2016-2024 Christian Schwinne <br>\n\t\t<i>Licensed under the <a href=\"https://github.com/wled-dev/WLED/blob/main/LICENSE\" target=\"_blank\">EUPL v1.2 license</a></i><br><br>\n\t\tInstalled version: <span class=\"sip\">WLED ##VERSION##</span>\n\t</div>\n\t\t<div id=\"toast\"></div>\n\t\t<button type=\"button\" onclick=\"B()\">Back</button><button type=\"submit\">Save</button>\n\t</form>\n</body>\n</html>\n"
  },
  {
    "path": "wled00/data/settings_sync.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\" name=\"viewport\">\n\t<meta charset=\"utf-8\">\n\t<title>Sync Settings</title>\n\t<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->\n\t<script>\n\t// load common.js with retry on error\n\t(function loadFiles() {\n\t\tconst l = document.createElement('script');\n\t\tl.src = 'common.js';\n\t\tl.onload = () => loadResources(['style.css'], S); // load style.css then call S()\n\t\tl.onerror = () => setTimeout(loadFiles, 100);\n\t\tdocument.head.appendChild(l);\n\t})();\n\tfunction adj(){if (d.Sf.DI.value == 6454) {if (d.Sf.EU.value == 1) d.Sf.EU.value = 0;}\n\t\t\t\t\telse if (d.Sf.DI.value == 5568) {if (d.Sf.DA.value == 0) d.Sf.DA.value = 1; if (d.Sf.EU.value == 0) d.Sf.EU.value = 1;} }\n\tfunction FC()\n\t{\n\t\tfor(j=0;j<8;j++)\n\t\t{\n\t\t\tgId(\"G\"+(j+1)).checked=gId(\"GS\").value>>j&1;\n\t\t\tgId(\"R\"+(j+1)).checked=gId(\"GR\").value>>j&1;\n\t\t}\n\t}\n\tfunction GC()\n\t{\n\t\tvar a=0, b=0;\n\n\t\tvar m=1;\n\t\tfor(j=0;j<8;j++)\n\t\t{\n\t\t\ta+=gId(\"G\"+(j+1)).checked*m;\n\t\t\tb+=gId(\"R\"+(j+1)).checked*m;\n\t\t\tm*=2;\n\t\t}\n\t\tgId(\"GS\").value=a;\n\t\tgId(\"GR\").value=b;\n\t}\n\tfunction SP(){var p = d.Sf.DI.value; gId(\"xp\").style.display = (p > 0)?\"none\":\"block\"; if (p > 0) d.Sf.EP.value = p;}\n\tfunction SetVal(){switch(parseInt(d.Sf.EP.value)){case 5568: d.Sf.DI.value = 5568; break; case 6454: d.Sf.DI.value = 6454; break; case 4048: d.Sf.DI.value = 4048; break; }; SP();FC();}\n\tfunction S(){\n\t\tgetLoc();\n\t\tloadJS(getURL('/settings/s.js?p=4'), false, undefined, ()=>{SetVal();});\t// If we set async false, file is loaded and executed, then next statement is processed\n\t\tif (loc) d.Sf.action = getURL('/settings/sync');\n\t}\n\tfunction getURL(path) {\n\t\treturn (loc ? locproto + \"//\" + locip : \"\") + path;\n\t}\n\tfunction hideDMXInput(){gId(\"dmxInput\").style.display=\"none\";}\n\tfunction hideNoDMXInput(){gId(\"dmxInputOff\").style.display=\"none\";}\n\tfunction hideNoDMXOutput(){gId(\"dmxOnOffOutput\").style.display=\"none\";}\n\t</script>\n</head>\n<body>\n<form id=\"form_s\" name=\"Sf\" method=\"post\" onsubmit=\"GC()\">\n<div class=\"toprow\">\n<div class=\"helpB\"><button type=\"button\" onclick=\"H('interfaces/udp-notifier/')\">?</button></div>\n<button type=\"button\" onclick=\"B()\">Back</button><button type=\"submit\">Save</button><hr>\n</div>\n<h2>Sync setup</h2>\n<div class=\"sec\">\n<h3>WLED Broadcast</h3>\nUDP Port: <input name=\"UP\" type=\"number\" min=\"1\" max=\"65535\" class=\"d5\" required><br>\n2nd Port: <input name=\"U2\" type=\"number\" min=\"1\" max=\"65535\" class=\"d5\" required><br>\n</div>\n<div class=\"sec\">\n<h3>ESP-NOW</h3>\n<div id=\"NoESPNOW\" class=\"hide\">\n<i class=\"warn\">Disabled. Enable ESP-NOW in WiFi settings.<br></i>\n</div>\n<div id=\"ESPNOW\">\nUse ESP-NOW sync: <input type=\"checkbox\" name=\"EN\"><br><i>(in AP mode or no WiFi)</i><br>\n</div>\n</div>\n<div class=\"sec\">\n<h3>Sync groups</h3>\n<input name=\"GS\" id=\"GS\" type=\"number\" style=\"display: none;\"><!-- hidden inputs for bitwise group checkboxes -->\n<input name=\"GR\" id=\"GR\" type=\"number\" style=\"display: none;\">\n<table style=\"margin: 0 auto;\">\n\t<tr>\n\t\t<td></td>\n\t\t<td>1</td>\n\t\t<td>2</td>\n\t\t<td>3</td>\n\t\t<td>4</td>\n\t\t<td>5</td>\n\t\t<td>6</td>\n\t\t<td>7</td>\n\t\t<td>8</td>\n\t</tr>\n\t<tr>\n\t\t<td>Send:</td>\n\t\t<td><input type=\"checkbox\" id=\"G1\" name=\"G1\"></td>\n\t\t<td><input type=\"checkbox\" id=\"G2\" name=\"G2\"></td>\n\t\t<td><input type=\"checkbox\" id=\"G3\" name=\"G3\"></td>\n\t\t<td><input type=\"checkbox\" id=\"G4\" name=\"G4\"></td>\n\t\t<td><input type=\"checkbox\" id=\"G5\" name=\"G5\"></td>\n\t\t<td><input type=\"checkbox\" id=\"G6\" name=\"G6\"></td>\n\t\t<td><input type=\"checkbox\" id=\"G7\" name=\"G7\"></td>\n\t\t<td><input type=\"checkbox\" id=\"G8\" name=\"G8\"></td>\n\t</tr>\n\t<tr>\n\t\t<td>Receive:</td>\n\t\t<td><input type=\"checkbox\" id=\"R1\" name=\"R1\"></td>\n\t\t<td><input type=\"checkbox\" id=\"R2\" name=\"R2\"></td>\n\t\t<td><input type=\"checkbox\" id=\"R3\" name=\"R3\"></td>\n\t\t<td><input type=\"checkbox\" id=\"R4\" name=\"R4\"></td>\n\t\t<td><input type=\"checkbox\" id=\"R5\" name=\"R5\"></td>\n\t\t<td><input type=\"checkbox\" id=\"R6\" name=\"R6\"></td>\n\t\t<td><input type=\"checkbox\" id=\"R7\" name=\"R7\"></td>\n\t\t<td><input type=\"checkbox\" id=\"R8\" name=\"R8\"></td>\n\t</tr>\n</table>\n</div>\n<div class=\"sec\">\n<h3>Receive</h3>\n<nowrap><input type=\"checkbox\" name=\"RB\">Brightness,</nowrap> <nowrap><input type=\"checkbox\" name=\"RC\">Color,</nowrap> <nowrap><input type=\"checkbox\" name=\"RX\">Effects,</nowrap> <nowrap>and <input type=\"checkbox\" name=\"RP\">Palette</nowrap><br>\n<input type=\"checkbox\" name=\"SO\"> Segment options, <input type=\"checkbox\" name=\"SG\"> bounds\n</div>\n<div class=\"sec\">\n<h3>Send</h3>\nEnable Sync on start: <input type=\"checkbox\" name=\"SS\"><br>\nSend notifications on direct change: <input type=\"checkbox\" name=\"SD\"><br>\nSend notifications on button press or IR: <input type=\"checkbox\" name=\"SB\"><br>\nSend Alexa notifications: <input type=\"checkbox\" name=\"SA\"><br>\nSend Philips Hue change notifications: <input type=\"checkbox\" name=\"SH\"><br>\nUDP packet retransmissions: <input name=\"UR\" type=\"number\" min=\"0\" max=\"30\" class=\"d5\" required><br><br>\n<i>Reboot required to apply changes. </i>\n</div>\n<div class=\"sec\">\n<h3>Instance List</h3>\nEnable instance list: <input type=\"checkbox\" name=\"NL\"><br>\nMake this instance discoverable: <input type=\"checkbox\" name=\"NB\">\n</div>\n<div class=\"sec\">\n<h3>Realtime</h3>\nReceive UDP realtime: <input type=\"checkbox\" name=\"RD\"><br>\nUse main segment only: <input type=\"checkbox\" name=\"MO\"><br>\nRespect LED Maps: <input type=\"checkbox\" name=\"RLM\"><br><br>\n<i>Network DMX input</i><br>\nType:\n<select name=DI onchange=\"SP(); adj();\">\n<option value=5568>E1.31 (sACN)</option>\n<option value=6454>Art-Net</option>\n<option value=0 selected>Custom port</option>\n</select><br>\n<div id=xp>Port: <input name=\"EP\" type=\"number\" min=\"1\" max=\"65535\" value=\"5568\" class=\"d5\" required><br></div>\nMulticast: <input type=\"checkbox\" name=\"EM\"><br>\nStart universe: <input name=\"EU\" type=\"number\" min=\"0\" max=\"63999\" required><br>\n<i>Reboot required.</i> Check out <a href=\"https://github.com/LedFx/LedFx\" target=\"_blank\">LedFx</a>!<br>\nSkip out-of-sequence packets: <input type=\"checkbox\" name=\"ES\"><br>\nDMX start address: <input name=\"DA\" type=\"number\" min=\"1\" max=\"510\" required><br>\nDMX segment spacing: <input name=\"XX\" type=\"number\" min=\"0\" max=\"150\" required><br>\nE1.31 port priority: <input name=\"PY\" type=\"number\" min=\"0\" max=\"200\" required><br>\nDMX mode:\n<select name=DM>\n<option value=0>Disabled</option>\n<option value=1>Single RGB</option>\n<option value=2>Single DRGB</option>\n<option value=3>Effect</option>\n<option value=7>Effect + White</option>\n<option value=8>Effect Segment</option>\n<option value=9>Effect Segment + White</option>\n<option value=4>Multi RGB</option>\n<option value=5>Dimmer + Multi RGB</option>\n<option value=6>Multi RGBW</option>\n<option value=10>Preset</option>\n</select><br>\n<a href=\"https://kno.wled.ge/interfaces/e1.31-dmx/\" target=\"_blank\">E1.31 info</a><br>\nTimeout: <input name=\"ET\" type=\"number\" min=\"1\" max=\"65000\" required> ms<br>\nForce max brightness: <input type=\"checkbox\" name=\"FB\"><br>\nDisable realtime gamma correction: <input type=\"checkbox\" name=\"RG\"><br>\nRealtime LED offset: <input name=\"WO\" type=\"number\" min=\"-255\" max=\"255\" required>\n<div id=\"dmxInput\">\n\t<h4>Wired DMX Input Pins</h4>\n\tDMX RX: <input name=\"IDMR\" type=\"number\" min=\"-1\" max=\"99\">RO<br/>\n\tDMX TX: <input name=\"IDMT\" type=\"number\" min=\"-1\" max=\"99\">DI<br/>\n\tDMX Enable: <input name=\"IDME\" type=\"number\" min=\"-1\" max=\"99\">RE+DE<br/>\n\tDMX Port: <input name=\"IDMP\" type=\"number\" min=\"1\" max=\"2\"><br/>\n</div> \n<div id=\"dmxInputOff\">\n\t<br><i class=\"warn\">This firmware build does not include DMX Input support. <br></i>\n</div> \n<div id=\"dmxOnOffOutput\">\n  <br><i class=\"warn\">This firmware build does not include DMX output support. <br></i>\n</div> \n</div>\n<div class=\"sec\">\n<h3>Alexa Voice Assistant</h3>\n<div id=\"NoAlexa\" class=\"hide\">\n\t<i class=\"warn\">This firmware build does not include Alexa support.<br></i><br>\n</div>\n<div id=\"Alexa\">\nEmulate Alexa device: <input type=\"checkbox\" name=\"AL\"><br>\nAlexa invocation name: <input type=\"text\" name=\"AI\" maxlength=\"32\"><br>\nAlso emulate devices to call the first <input name=\"AP\" type=\"number\" class=\"s\" min=\"0\" max=\"9\"> presets<br><br>\n</div>\n</div>\n\n<div class=\"warn\">&#9888; <b>MQTT and Hue sync connect to external hosts!<br>\nThis may impact the responsiveness of WLED.</b><br>\n</div>\nFor best results, only use one of these services at a time.<br>\n(alternatively, connect a second ESP to them and use the UDP sync)\n\n<div class=\"sec\">\n<h3>MQTT</h3>\n<div id=\"NoMQTT\" class=\"hide\">\n\t<i class=\"warn\">This firmware build does not include MQTT support.<br></i>\n</div>\n<div id=\"MQTT\">\nEnable MQTT: <input type=\"checkbox\" name=\"MQ\"><br>\nBroker: <input type=\"text\" name=\"MS\" maxlength=\"32\">\nPort: <input name=\"MQPORT\" type=\"number\" min=\"1\" max=\"65535\" class=\"d5\"><br>\n<b>The MQTT credentials are sent over an unsecured connection.<br>\nNever use the MQTT password for another service!</b><br>\nUsername: <input type=\"text\" name=\"MQUSER\" maxlength=\"40\"><br>\nPassword: <input type=\"password\" name=\"MQPASS\" maxlength=\"64\"><br>\nClient ID: <input type=\"text\" name=\"MQCID\" maxlength=\"40\"><br>\nDevice Topic: <input type=\"text\" name=\"MD\" maxlength=\"32\"><br>\nGroup Topic: <input type=\"text\" name=\"MG\" maxlength=\"32\"><br>\nPublish on button press: <input type=\"checkbox\" name=\"BM\"><br>\nRetain brightness & color messages: <input type=\"checkbox\" name=\"RT\"><br>\n<i>Reboot required to apply changes. </i><a href=\"https://kno.wled.ge/interfaces/mqtt/\" target=\"_blank\">MQTT info</a>\n</div>\n</div>\n<div class=\"sec\">\n<h3>Philips Hue</h3>\n<div id=\"NoHue\" class=\"hide\">\n\t<em class=\"warn\">This firmware build does not include Philips Hue support.<br></em>\n</div>\n<div id=\"Hue\">\n<i>You can find the bridge IP and the light number in the 'About' section of the hue app.</i><br>\nPoll Hue light <input name=\"HL\" type=\"number\" min=\"1\" max=\"99\" > every <input name=\"HI\" type=\"number\" min=\"100\" max=\"65000\"> ms: <input type=\"checkbox\" name=\"HP\"><br>\nThen, receive <input type=\"checkbox\" name=\"HO\"> On/Off, <input type=\"checkbox\" name=\"HB\"> Brightness, and <input type=\"checkbox\" name=\"HC\"> Color<br>\nHue Bridge IP:<br>\n<input name=\"H0\" type=\"number\" class=\"s\" min=\"0\" max=\"255\" > .\n<input name=\"H1\" type=\"number\" class=\"s\" min=\"0\" max=\"255\" > .\n<input name=\"H2\" type=\"number\" class=\"s\" min=\"0\" max=\"255\" > .\n<input name=\"H3\" type=\"number\" class=\"s\" min=\"0\" max=\"255\" ><br>\n<b>Press the pushlink button on the bridge, after that save this page!</b><br>\n(when first connecting)<br>\nHue status: <span class=\"sip\"> Disabled in this build </span>\n</div>\n</div>\n<div class=\"sec\">\n<h3>Serial</h3>\n<div id=\"NoSerial\" class=\"hide\">\n\t<em class=\"warn\">This firmware build does not support Serial interface.<br></em>\n</div>\n<div id=\"Serial\">\nBaud rate:\n<select name=BD>\n<option value=1152>115200</option>\n<option value=2304>230400</option>\n<option value=4608>460800</option>\n<option value=5000>500000</option>\n<option value=5760>576000</option>\n<option value=9216>921600</option>\n<option value=10000>1000000</option>\n<option value=15000>1500000</option>\n</select><br>\n<i>Keep at 115200 to use Improv. Some boards may not support high rates.</i>\n</div>\n</div>\n<hr>\n<button type=\"button\" onclick=\"B()\">Back</button><button type=\"submit\">Save</button>\n</form>\n</body>\n</html>"
  },
  {
    "path": "wled00/data/settings_time.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\" name=\"viewport\">\n\t<meta charset=\"utf-8\">\n\t<title>Time Settings</title>\n\t<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->\n\t<script>\n\tvar el=false;\n\tvar ms=[\"Jan\",\"Feb\",\"Mar\",\"Apr\",\"May\",\"Jun\",\"Jul\",\"Aug\",\"Sep\",\"Oct\",\"Nov\",\"Dec\"];\n\t// load common.js with retry on error\n\t(function loadFiles() {\n\t\tconst l = document.createElement('script');\n\t\tl.src = 'common.js';\n\t\tl.onload = () => loadResources(['style.css'], S); // load style.css then call S()\n\t\tl.onerror = () => setTimeout(loadFiles, 100);\n\t\tdocument.head.appendChild(l);\n\t})();\n\n\tfunction S() {\n\t\tgetLoc();\n\t\tloadJS(getURL('/settings/s.js?p=5'), false, ()=>{BTa();}, ()=>{\n\t\t\tupdLatLon();\n\t\t\tCs();\n\t\t\tFC();\n\t\t});\t// If we set async false, file is loaded and executed, then next statement is processed\n\t\tif (loc) d.Sf.action = getURL('/settings/time');\n\t}\n\tfunction expand(o,i)\n\t{\n\t\tvar t = gId(\"WD\"+i);\n\t\tt.style.display = t.style.display!==\"none\" ? \"none\" : \"\";\n\t\to.innerHTML = t.style.display===\"none\" ? \"&#128197;\" : \"&#x2715;\";\n\t}\n\tfunction Cs() { gId(\"cac\").style.display=(gN(\"OL\").checked)?\"block\":\"none\"; }\n\tfunction BTa()\n\t{\n\t\tvar ih=\"<thead><tr><th>En.</th><th>Hour</th><th>Minute</th><th>Preset</th><th></th></tr></thead>\";\n\t\tfor (i=0;i<8;i++) {\n\t\t\tih+=`<tr><td><input name=\"W${i}\" id=\"W${i}\" type=\"hidden\"><input id=\"W${i}0\" type=\"checkbox\"></td>\n<td><input name=\"H${i}\" class=\"xs\" type=\"number\" min=\"0\" max=\"24\"></td>\n<td><input name=\"N${i}\" class=\"xs\" type=\"number\" min=\"0\" max=\"59\"></td>\n<td><input name=\"T${i}\" class=\"s\" type=\"number\" min=\"0\" max=\"250\"></td>\n<td><div id=\"CB${i}\" onclick=\"expand(this,${i})\" class=\"cal\">&#128197;</div></td></tr>`;\n\t\t\tih+=`<tr><td colspan=5><div id=\"WD${i}\" style=\"display:none;background-color:#444;\"><hr>Run on weekdays`;\n\t\t\tih+=`<table><tr><th>M</th><th>T</th><th>W</th><th>T</th><th>F</th><th>S</th><th>S</th></tr><tr>`\n\t\t\tfor (j=1;j<8;j++) ih+=`<td><input id=\"W${i}${j}\" type=\"checkbox\"></td>`;\n\t\t\tih+=`</tr></table>from <select name=\"M${i}\">`;\n\t\t\tfor (j=0;j<12;j++) ih+=`<option value=\"${j+1}\">${ms[j]}</option>`;\n\t\t\tih+=`</select><input name=\"D${i}\" class=\"xs\" type=\"number\" min=\"1\" max=\"31\"></input> to <select name=\"P${i}\">`;\n\t\t\tfor (j=0;j<12;j++) ih+=`<option value=\"${j+1}\">${ms[j]}</option>`;\n\t\t\tih+=`</select><input name=\"E${i}\" class=\"xs\" type=\"number\" min=\"1\" max=\"31\"></input>\n\t\t<hr></div></td></tr>`;\n\t\t}\n\t\tih+=`<tr><td><input name=\"W8\" id=\"W8\" type=\"hidden\"><input id=\"W80\" type=\"checkbox\"></td>\n<td>Sunrise<input name=\"H8\" value=\"255\" type=\"hidden\"></td>\n<td><input name=\"N8\" class=\"xs\" type=\"number\" min=\"-59\" max=\"59\"></td>\n<td><input name=\"T8\" class=\"s\" type=\"number\" min=\"0\" max=\"250\"></td>\n<td><div id=\"CB8\" onclick=\"expand(this,8)\" class=\"cal\">&#128197;</div></td></tr><tr><td colspan=5>`;\n\t\tih+=`<div id=\"WD8\" style=\"display:none;background-color:#444;\"><hr><table><tr><th>M</th><th>T</th><th>W</th><th>T</th><th>F</th><th>S</th><th>S</th></tr><tr>`;\n\t\tfor (j=1;j<8;j++) ih+=`<td><input id=\"W8${j}\" type=\"checkbox\"></td>`;\n\t\tih+=\"</tr></table><hr></div></td></tr>\";\n\t\tih+=`<tr><td><input name=\"W9\" id=\"W9\" type=\"hidden\"><input id=\"W90\" type=\"checkbox\"></td>\n<td>Sunset<input name=\"H9\" value=\"255\" type=\"hidden\"></td>\n<td><input name=\"N9\" class=\"xs\" type=\"number\" min=\"-59\" max=\"59\"></td>\n<td><input name=\"T9\" class=\"s\" type=\"number\" min=\"0\" max=\"250\"></td>\n<td><div id=\"CB9\" onclick=\"expand(this,9)\" class=\"cal\">&#128197;</div></td></tr><tr><td colspan=5>`;\n\t\tih+=`<div id=\"WD9\" style=\"display:none;background-color:#444;\"><hr><table><tr><th>M</th><th>T</th><th>W</th><th>T</th><th>F</th><th>S</th><th>S</th></tr><tr>`;\n\t\tfor (j=1;j<8;j++) ih+=`<td><input id=\"W9${j}\" type=\"checkbox\"></td>`;\n\t\tih+=\"</tr></table><hr></div></td></tr>\";\n\t\tgId(\"TMT\").innerHTML=ih;\n\t}\n\tfunction FC()\n\t{\n\t\tfor(i=0;i<10;i++)\n\t\t{\n\t\t\tlet wd = gId(\"W\"+i).value;\n\t\t\tfor(j=0;j<8;j++) {\n\t\t\t\tgId(\"W\"+i+j).checked=wd>>j&1;\n\t\t\t}\n\t\t\tif ((wd&254) != 254 || (i<8 && (gN(\"M\"+i).value != 1 || gN(\"D\"+i).value != 1 || gN(\"P\"+i).value != 12 || gN(\"E\"+i).value != 31))) {\n\t\t\t\texpand(gId(\"CB\"+i),i); //expand macros with custom DOW or date range set\n\t\t\t}\n\t\t}\n\t}\n\tfunction Wd()\n\t{\n\t\ta = [0,0,0,0,0,0,0,0,0,0];\n\t\tfor (i=0; i<10; i++) {\n\t\t\tm=1;\n\t\t\tfor(j=0;j<8;j++) { a[i]+=gId((\"W\"+i)+j).checked*m; m*=2;}\n\t\t\tgId(\"W\"+i).value=a[i];\n\t\t}\n\t\tif (d.Sf.LTR.value===\"S\") { d.Sf.LT.value = -1*parseFloat(d.Sf.LT.value); }\n\t\tif (d.Sf.LNR.value===\"W\") { d.Sf.LN.value = -1*parseFloat(d.Sf.LN.value); }\n\t}\n\tfunction addRow(i,p,l,d) {\n\t\tvar t = gId(\"macros\");\t// table\n\t\tvar rCnt = t.rows.length;   // get the number of rows.\n\t\tvar tr = t.insertRow(rCnt); // table row.\n\t\tvar b = String.fromCharCode((i<10?48:55)+i);\n\t\tvar td = document.createElement('td');          // TABLE DEFINITION.\n\t\ttd = tr.insertCell(0);\n\t\ttd.innerHTML = `Button ${i}:`;\n\t\ttd = tr.insertCell(1);\n\t\ttd.innerHTML = `<input name=\"MP${b}\" type=\"number\" class=\"s\" min=\"0\" max=\"250\" value=\"${p}\" required>`;\n\t\ttd = tr.insertCell(2);\n\t\ttd.innerHTML = `<input name=\"ML${b}\" type=\"number\" class=\"s\" min=\"0\" max=\"250\" value=\"${l}\" required>`;\n\t\ttd = tr.insertCell(3);\n\t\ttd.innerHTML = `<input name=\"MD${b}\" type=\"number\" class=\"s\" min=\"0\" max=\"250\" value=\"${d}\" required>`;\n\t}\n\tfunction getLatLon() {\n\t\tif (!el) {\n\t\t\twindow.addEventListener(\"message\", (event) => {\n\t\t\t\tif (event.origin !== \"https://locate.wled.me\") return;\n\t\t\t\tif (event.data instanceof Object) {\n\t\t\t\t\td.Sf.LT.value = event.data.lat;\n\t\t\t\t\td.Sf.LN.value = event.data.lon;\n\t\t\t\t\tupdLatLon();\n\t\t\t\t}\n\t\t\t}, false);\n\t\t\tel = true;\n\t\t}\n\t\twindow.open(\"https://locate.wled.me\",\"_blank\");\n\t}\n\tfunction updLatLon(i) {\n\t\tif (parseFloat(d.Sf.LT.value)<0) { d.Sf.LTR.value = \"S\"; d.Sf.LT.value = -1*parseFloat(d.Sf.LT.value); } else d.Sf.LTR.value = \"N\";\n\t\tif (parseFloat(d.Sf.LN.value)<0) { d.Sf.LNR.value = \"W\"; d.Sf.LN.value = -1*parseFloat(d.Sf.LN.value); } else d.Sf.LNR.value = \"E\";\n\t}\n\t</script>\n</head>\n<body>\n<form id=\"form_s\" name=\"Sf\" method=\"post\" onsubmit=\"Wd()\">\n\t<div class=\"toprow\">\n\t<div class=\"helpB\"><button type=\"button\" onclick=\"H('features/settings/#time-settings')\">?</button></div>\n\t<button type=\"button\" onclick=\"B()\">Back</button><button type=\"submit\">Save</button><hr>\n\t</div>\n\t<h2>Time setup</h2>\n\t<div class=\"sec\">\n\t\tGet time from NTP server: <input type=\"checkbox\" name=\"NT\"><br>\n\t\t<input type=\"text\" name=\"NS\" maxlength=\"32\"><br>\n\t\tUse 24h format: <input type=\"checkbox\" name=\"CF\"><br>\n\t\tTime zone: \n\t\t<select name=\"TZ\">\n\t\t\t<option value=\"0\" selected>GMT(UTC)</option>\n\t\t\t<option value=\"1\">GMT/BST</option>\n\t\t\t<option value=\"2\">CET/CEST</option>\n\t\t\t<option value=\"3\">EET/EEST</option>\n\t\t\t<option value=\"4\">US-EST/EDT</option>\n\t\t\t<option value=\"5\">US-CST/CDT</option>\n\t\t\t<option value=\"6\">US-MST/MDT</option>\n\t\t\t<option value=\"7\">US-AZ</option>\n\t\t\t<option value=\"8\">US-PST/PDT</option>\n\t\t\t<option value=\"9\">CST (AWST, PHST)</option>\n\t\t\t<option value=\"10\">JST (KST)</option>\n\t\t\t<option value=\"11\">AEST/AEDT</option>\n\t\t\t<option value=\"12\">NZST/NZDT</option>\n\t\t\t<option value=\"13\">North Korea</option>\n\t\t\t<option value=\"14\">IST (India)</option>\n\t\t\t<option value=\"15\">CA-Saskatchewan</option>\n\t\t\t<option value=\"16\">ACST</option>\n\t\t\t<option value=\"17\">ACST/ACDT</option>\n\t\t\t<option value=\"18\">HST (Hawaii)</option>\n\t\t\t<option value=\"19\">NOVT (Novosibirsk)</option>\n\t\t\t<option value=\"20\">AKST/AKDT (Anchorage)</option>\n\t\t\t<option value=\"21\">MX-CST</option>\n\t\t\t<option value=\"22\">PKT (Pakistan)</option>\n\t\t\t<option value=\"23\">BRT (Brasília)</option>\n\t\t\t<option value=\"24\">AWST (Perth)</option>\n\t\t</select><br>\n\t\tUTC offset: <input name=\"UO\" type=\"number\" min=\"-65500\" max=\"65500\" required> seconds (max. 18 hours)<br>\n\t\tCurrent local time is <span class=\"times\">unknown</span>.<br>\n\t\tLatitude: <select name=\"LTR\"><option value=\"N\">N</option><option value=\"S\">S</option></select><input name=\"LT\" type=\"number\" class=\"xl\" min=\"0\" max=\"66.6\" step=\"0.01\"><br>\n\t\tLongitude: <select name=\"LNR\"><option value=\"E\">E</option><option value=\"W\">W</option></select><input name=\"LN\" type=\"number\" class=\"xl\" min=\"0\" max=\"180\" step=\"0.01\"><br>\n\t\t<button type=\"button\" id=\"locbtn\" onclick=\"getLatLon()\">Get location</button>\n\t\t<div><i>(opens new tab, only works in browser)</i></div>\n\t\t<div id=\"sun\" class=\"times\"></div>\n\t</div>\n\t<div class=\"sec\">\n\t\t<h3>Clock</h3>\n\t\tAnalog Clock overlay: <input type=\"checkbox\" name=\"OL\" onchange=\"Cs()\"><br>\n\t\t<div id=\"cac\">\n\t\t\tFirst LED: <input name=\"O1\" type=\"number\" min=\"0\" max=\"1024\" required> Last LED: <input name=\"O2\" type=\"number\" min=\"0\" max=\"1024\" required><br>\n\t\t\t12h LED: <input name=\"OM\" type=\"number\" min=\"0\" max=\"1024\" required><br>\n\t\t\tShow 5min marks: <input type=\"checkbox\" name=\"O5\"><br>\n\t\t\tSeconds (as trail): <input type=\"checkbox\" name=\"OS\"><br>\n\t\t\tShow clock overlay only if all LEDs are solid black: <input type=\"checkbox\" name=\"OB\"><br>\n\t\t</div>\n\t\tCountdown Mode: <input type=\"checkbox\" name=\"CE\"><br>\n\t\tCountdown Goal:<br>\n\t\tDate:&nbsp;<nowrap>20<input name=\"CY\" class=\"xs\" type=\"number\" min=\"0\" max=\"99\" required>-<input name=\"CI\" class=\"xs\" type=\"number\" min=\"1\" max=\"12\" required>-<input name=\"CD\" class=\"xs\" type=\"number\" min=\"1\" max=\"31\" required></nowrap><br>\n\t\tTime:&nbsp;<nowrap><input name=\"CH\" class=\"xs\" type=\"number\" min=\"0\" max=\"23\" required>:<input name=\"CM\" class=\"xs\" type=\"number\" min=\"0\" max=\"59\" required>:<input name=\"CS\" class=\"xs\" type=\"number\" min=\"0\" max=\"59\" required></nowrap><br>\n\t</div>\n\t<hr>\n\t<h2>Macro Presets</h2>\n\t<i>Presets can be used as macros for both JSON and HTTP API commands.<br>\n\tEnter the preset ID below.</i>\n\t<i>Use 0 for the default action instead of a preset</i><br>\n\t<a href=\"https://kno.wled.ge/interfaces/json-api/\" target=\"_blank\">JSON API</a><br>\n\t<a href=\"https://kno.wled.ge/interfaces/http-api/\" target=\"_blank\">HTTP API</a><br>\n\t<div class=\"sec\">\n\t\t<h3>Timer & Alexa Presets</h3>\n\t\tCountdown-Over Preset: <input name=\"MC\" class=\"m\" type=\"number\" min=\"0\" max=\"250\" required><br>\n\t\tTimed-Light-Over Presets: <input name=\"MN\" class=\"m\" type=\"number\" min=\"0\" max=\"250\" required><br>\n\t\tAlexa On/Off Preset: <input name=\"A0\" class=\"m\" type=\"number\" min=\"0\" max=\"250\" required> <input name=\"A1\" class=\"m\" type=\"number\" min=\"0\" max=\"250\" required><br>\n\t</div>\n\t<div class=\"sec\">\n\t\t<h3>Button Action Presets</h3>\n\t\t<table style=\"margin: 0 auto;\" id=\"macros\">\n\t\t\t<thead>\n\t\t\t\t<tr>\n\t\t\t\t\t<td>push<br>switch</td>\n\t\t\t\t\t<td>short<br>on-&gt;off</td>\n\t\t\t\t\t<td>long<br>off-&gt;on</td>\n\t\t\t\t\t<td>double<br>N/A</td>\n\t\t\t\t</tr>\n\t\t\t</thead>\n\t\t\t<tbody>\n\t\t\t</tbody>\n\t\t</table>\n\t\t<a href=\"https://kno.wled.ge/features/macros/#analog-button\" target=\"_blank\">Analog Button setup</a>\n\t</div>\n\t<div class=\"sec\">\n\t\t<h3>Time-Controlled Presets</h3>\n\t\t<div style=\"display: inline-block\">\n\t\t<table id=\"TMT\" style=\"min-width:330px;\"></table>\n\t\t</div>\n\t</div>\n\t<hr>\n\t<button type=\"button\" onclick=\"B()\">Back</button><button type=\"submit\">Save</button>\n</form>\n</body>\n</html>\n"
  },
  {
    "path": "wled00/data/settings_ui.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"utf-8\">\n\t<meta content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\" name=\"viewport\">\n\t<title>UI Settings</title>\n\t<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->\n\t<script>\n\tvar initial_ds, initial_st, initial_su, oldUrl;\n\tvar sett = null;\n\t// load common.js with retry on error\n\t(function loadFiles() {\n\t\tconst l = document.createElement('script');\n\t\tl.src = 'common.js';\n\t\tl.onload = () => loadResources(['style.css'], S); // load style.css then call S()\n\t\tl.onerror = () => setTimeout(loadFiles, 100);\n\t\tdocument.head.appendChild(l);\n\t})();\n\n\tvar l = {\n\t\t\"comp\":{\n\t\t\t\"labels\":\"Show button labels\",\n\t\t\t\"colors\":{\n\t\t\t\t\"LABEL\":\"Color selection methods\",\n\t\t\t\t\"picker\": \"Color Wheel\",\n\t\t\t\t\"rgb\": \"RGB sliders\",\n\t\t\t\t\"quick\": \"Quick color selectors\",\n\t\t\t\t\"hex\": \"HEX color input\"\n\t\t\t\t},\n\t\t\t\"pcmbot\": \"Show bottom tab bar in PC mode\",\n\t\t\t\"pid\": \"Show preset IDs\",\n\t\t\t\"seglen\": \"Set segment length instead of stop LED\",\n\t\t\t\"segpwr\": \"Hide segment power &amp; brightness\",\n\t\t\t\"segexp\" : \"Always expand first segment\",\n\t\t\t\"css\": \"Enable custom CSS\",\n\t\t\t\"hdays\": \"Enable custom Holidays list\",\n\t\t\t\"fxdef\": \"Use effect default parameters\",\n\t\t\t\"on\": \"Power button preset override for On\",\n\t\t\t\"off\": \"Power button preset override for Off\",\n\t\t\t\"idsort\": \"Sort presets by ID\"\n\t\t},\n\t\t\"theme\":{\n\t\t\t\"alpha\": {\n\t\t\t\t\"bg\":\"Background opacity\",\n\t\t\t\t\"tab\":\"Button opacity\"\n\t\t\t},\n\t\t\t\"bg\":{\n\t\t\t\t\"url\":\"BG image URL\",\n\t\t\t\t\"rnd\":\"Random BG image\",\n\t\t\t\t\"rndGrayscale\":\"Grayscale\",\n\t\t\t\t\"rndBlur\":\"Blur\"\n\t\t\t},\n\t\t\t\"color\":{\n\t\t\t\t\"bg\":\"BG HEX color\"\n\t\t\t}\n\t\t}\n\t};\n\tfunction set(path, obj, val) {\n\t\tvar tar = obj;\n\t\tvar pList = path.split('_');\n\t\tvar len = pList.length;\n\t\tfor(var i = 0; i < len-1; i++) {\n\t\t\tvar elem = pList[i];\n\t\t\tif( !tar[elem] ) tar[elem] = {}\n\t\t\ttar = tar[elem];\n\t\t}\n\t\ttar[pList[len-1]] = val;\n\t}\n\tfunction addRec(s, path = \"\", label = null)\n\t{\n\t\tvar str = \"\";\n\t\tfor (let i in s)\n\t\t{\n\t\t\tvar fk = path + (path?'_':'') + i;\n\t\t\tif (isO(s[i])) {\n\t\t\t\tif (label && label[i] && label[i][\"LABEL\"]) str += `<h3>${label[i][\"LABEL\"]}</h3>`;\n\t\t\t\tstr += addRec(s[i], fk, label? label[i] : null);\n\t\t\t} else {\n\t\t\t\tvar lb = fk;\n\t\t\t\tif (label && label[i]) lb = label[i];\n\t\t\t\telse if (s[i+'LABEL']) lb = s[i+'LABEL'];\n\t\t\t\tif (i.indexOf('LABEL') > 0) continue;\n\t\t\t\tvar t = typeof s[i];\n\t\t\t\tif (gId(fk)) { //already exists\n\t\t\t\t\tif(t === 'boolean')\n\t\t\t\t\t{\n\t\t\t\t\t\tgId(fk).checked = s[i];\n\t\t\t\t\t} else {\n\t\t\t\t\t\tgId(fk).value = s[i];\n\t\t\t\t\t}\n\t\t\t\t\tif (gId(fk).previousElementSibling.matches('.l')) {\n\t\t\t\t\t\tgId(fk).previousElementSibling.innerHTML = lb;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif(t === 'boolean')\n\t\t\t\t\t{\n\t\t\t\t\t\tstr += `${lb}: <input class=\"agi cb\" type=\"checkbox\" id=${fk} ${s[i]?\"checked\":\"\"}><br>`;\n\t\t\t\t\t} else if (t === 'number')\n\t\t\t\t\t{\n\t\t\t\t\t\tstr += `${lb}: <input class=\"agi\" type=\"number\" id=${fk} value=${s[i]}><br>`;\n\t\t\t\t\t} else if (t === 'string')\n\t\t\t\t\t{\n\t\t\t\t\t\tstr += `${lb}:<br><input class=\"agi sml\" id=${fk} value=${s[i]}><br>`;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn str;\n\t}\n\n\tfunction genForm(s) {\n\t\tvar str = \"\";\n\t\tstr = addRec(s,\"\",l);\n\t\toldUrl = \"\";\n\t\t\n\t\tgId('gen').innerHTML = str;\n\t\tif (gId('theme_bg_rnd').checked) {\n\t\t\ttoggle(\"Image\");\n\t\t} else if (gId('theme_bg_url').value.startsWith('data:')) {\n\t\t\tgId(\"bg_url\").classList.add(\"hide\");\n\t\t} else oldUrl = gId(\"theme_bg_url\").value;\n\t}\n\tfunction GetLS()\n\t{\n\t\tsett = localStorage.getItem('wledUiCfg');\n\t\tif (!sett) gId('lserr').style.display = \"inline\";\n\t\ttry {\n\t\t\tsett = JSON.parse(sett);\n\t\t} catch (e) {\n\t\t\tsett = {};\n\t\t\tgId('lserr').style.display = \"inline\";\n\t\t\tgId('lserr').innerHTML = \"&#9888; Settings JSON parsing failed. (\" + e + \")\";\n\t\t}\n\t\tgenForm(sett);\n\t\tgId('dm').checked = (gId('theme_base').value === 'light');\n\t}\n\n\tfunction SetLS()\n\t{\n\t\tvar l = d.querySelectorAll('.agi');\n\t\tfor (var i = 0; i < l.length; i++) {\n\t\t\tvar e = l[i];\n\t\t\tvar val = e.classList.contains('cb') ? e.checked : e.value;\n\t\t\tset(e.id, sett, val);\n\t\t\tconsole.log(`${e.id} set to ${val}`);\n\t\t}\n\t\ttry {\n\t\t\tlocalStorage.setItem('wledUiCfg', JSON.stringify(sett));\n\t\t\tgId('lssuc').style.display = \"inline\";\n\t\t} catch (e) {\n\t\t\tgId('lssuc').style.display = \"none\";\n\t\t\tgId('lserr').style.display = \"inline\";\n\t\t\tgId('lserr').innerHTML = \"&#9888; Settings JSON saving failed. (\" + e + \")\";\n\t\t}\n\t}\n\n\tfunction cLS()\n\t{\n\t\tlocalStorage.removeItem('wledP');\n\t\tlocalStorage.removeItem('wledPmt');\n\t\tlocalStorage.removeItem('wledPalx');\n\t\tshowToast(\"Cleared.\");\n\t}\n\t\n\tfunction Save() {\n\t\tSetLS();\n\t\tif (d.Sf.DS.value != initial_ds || /*d.Sf.ST.checked != initial_st ||*/ d.Sf.SU.checked != initial_su) d.Sf.submit();\n\t}\n\t\n\tfunction S() {\n\t\tgetLoc();\n\t\tloadJS(getURL('/settings/s.js?p=3'), false, undefined, ()=>{\n\t\t\tinitial_ds = d.Sf.DS.value;\n\t\t\t//initial_st = d.Sf.ST.checked;\n\t\t\tinitial_su = d.Sf.SU.checked;\n\t\t\tGetLS();\n\t\t});\t// If we set async false, file is loaded and executed, then next statement is processed\n\t\tif (loc) d.Sf.action = getURL('/settings/ui');\n\t}\n\tfunction UI()\n\t{\n\t\tgId('idonthateyou').style.display = (gId('dm').checked) ? 'inline':'none';\n\t\tvar f = gId('theme_base');\n\t\tif (f) f.value = (gId('dm').checked) ? 'light':'dark';\n\t}\n\n\t// random BG image\n\tfunction randomBg() {\n\t\tlet url = oldUrl;\n\t\tlet t = \"theme_bg_rnd\";\n\t\tif (gId(t).checked) {\n\t\t\turl = \"https://picsum.photos/1920/1080\";\n\t\t\tif (gId(`${t}Grayscale`).checked) url += \"?grayscale\";\n\t\t\tif (gId(`${t}Blur`).checked) url += (url.includes(\"?\") ? \"&\" : \"?\") + \"blur\";\n\t\t\tgId(\"theme_bg_img\").value = \"\";\n\t\t\tgId(\"bg_url\").classList.remove(\"hide\");\n\t\t}\n\t\tgId(\"theme_bg_url\").value = url;\n\t}\n\t// own BG image\n\tfunction ownBg(element) {\n\t\tconst file = element.files[0];\n\t\tconst reader = new FileReader();\n\t\treader.onload = () => {\n\t\t\tgId(\"theme_bg_url\").value = reader.result;\n\t\t\tgId(\"bg_url\").classList.add(\"hide\");\n\t\t\tif (gId(\"theme_bg_rnd\").checked) toggle(\"Image\");\n\t\t\tgId(\"theme_bg_rnd\").checked = false;\n\t\t}\n\t\treader.readAsDataURL(file);\n\t}\n\tfunction removeBgImg() {\n\t\tgId(\"theme_bg_url\").value = \"\";\n\t\tgId(\"theme_bg_img\").value = \"\";\n\t\tgId(\"bg_url\").classList.remove(\"hide\");\n\t\tif (gId(\"theme_bg_rnd\").checked) toggle(\"Image\");\n\t\tgId(\"theme_bg_rnd\").checked = false;\n\t}\n\t</script>\n</head>\n<body>\n<form id=\"form_s\" name=\"Sf\" method=\"post\">\n\t<div class=\"toprow\">\n\t<div class=\"helpB\"><button type=\"button\" onclick=\"H('features/settings/#user-interface-settings')\">?</button></div>\n\t<button type=\"button\" onclick=\"B()\">Back</button><button type=\"button\" onclick=\"Save()\">Save</button><br>\n\t<span id=\"lssuc\" style=\"color:green; display:none\">&#10004; Local UI settings saved!</span>\n\t<span id=\"lserr\" style=\"color:red; display:none\">&#9888; Could not access local storage. Make sure it is enabled in your browser.</span><hr>\n\t</div>\n\t<h2>User Interface</h2>\n\t<div class=\"sec\">\n\t\tDevice Name: <input type=\"text\" name=\"DS\" maxlength=\"32\"><br>\n\t\tEnable simplified UI: <input type=\"checkbox\" name=\"SU\"><br>\n\t</div>\n  <div class=\"sec\">\n\t\t<i>The following UI customization settings are unique both to the WLED device and this browser.<br>\n\t\tYou will need to set them again if using a different browser, device or WLED IP address.<br>\n\t\tRefresh the main UI to apply changes.</i><br>\n\n\t\t<div id=\"gen\">Loading settings...</div>\n\n\t\t<h3>UI Appearance</h3>\n\t\t<span class=\"l\"></span>: <input type=\"checkbox\" id=\"comp_labels\" class=\"agi cb\"><br>\n\t\t<span class=\"l\"></span>: <input type=\"checkbox\" id=\"comp_idsort\" class=\"agi cb\"><br>\n\t\t<span class=\"l\"></span>: <input type=\"checkbox\" id=\"comp_pcmbot\" class=\"agi cb\"><br>\n\t\t<span class=\"l\"></span>: <input type=\"checkbox\" id=\"comp_pid\" class=\"agi cb\"><br>\n\t\t<span class=\"l\"></span>: <input type=\"checkbox\" id=\"comp_seglen\" class=\"agi cb\"><br>\n\t\t<span class=\"l\"></span>: <input type=\"checkbox\" id=\"comp_segpwr\" class=\"agi cb\"><br>\n\t\t<span class=\"l\"></span>: <input type=\"checkbox\" id=\"comp_segexp\" class=\"agi cb\"><br>\n\t\t<span class=\"l\"></span>: <input type=\"checkbox\" id=\"comp_fxdef\" class=\"agi cb\"><br>\n\t\t<span class=\"l\"></span>: <input type=\"number\" min=0 max=250 step=1 id=\"comp_on\" class=\"agi\"><br>\n\t\t<span class=\"l\"></span>: <input type=\"number\" min=0 max=250 step=1 id=\"comp_off\" class=\"agi\"><br>\n\t\tI hate dark mode: <input type=\"checkbox\" id=\"dm\" onchange=\"UI()\"><br>\n\t\t<span id=\"idonthateyou\" style=\"display:none\"><i>Why would you? </i>&#x1F97A;<br></span>\n\t\t<span class=\"l\"></span>: <input type=\"number\" min=0.0 max=1.0 step=0.01 id=\"theme_alpha_tab\" class=\"agi\"><br>\n\t\t<span class=\"l\"></span>: <input type=\"checkbox\" id=\"comp_css\" class=\"agi cb\"><br>\n\t\t<div id=\"skin\">Custom CSS: <input type=\"file\" name=\"data\" accept=\".css\"> <input type=\"button\" value=\"Upload\" onclick=\"uploadFile(d.Sf.data,'/skin.css');\"><br></div>\n\n\t\t<h3>UI Background</h3>\n\t\t<span class=\"l\"></span>: <input type=\"number\" min=0.0 max=1.0 step=0.01 id=\"theme_alpha_bg\" class=\"agi\"><br>\n\t\t<span class=\"l\"></span>: <input type=\"text\" id=\"theme_color_bg\" maxlength=\"9\" class=\"agi\"><br>\n\t\tBG image: <input type=\"file\" id=\"theme_bg_img\" accept=\"image/*\" onchange=\"ownBg(this)\"> <input type=\"button\" value=\"Remove\" onclick=\"removeBgImg()\"><br>\n\t\t<span class=\"l\"></span>: <input type=\"checkbox\" id=\"theme_bg_rnd\" class=\"agi cb\" onchange=\"randomBg();toggle('Image');\">\n\t\t<div id=\"Image\">\n\t\t\t<div id=\"bg_url\">\n\t\t\t\t<span class=\"l\"></span>: <input type=\"text\" id=\"theme_bg_url\" class=\"agi\"><br>\n\t\t\t</div>\n\t\t</div>\n\t\t<div id=\"NoImage\" class=\"hide\">\n\t\t\t<h4>Random BG image settings</h4>\n\t\t\t<span class=\"l\"></span>: <input type=\"checkbox\" id=\"theme_bg_rndGrayscale\" class=\"agi cb\" onchange=\"randomBg()\"><br>\n\t\t\t<span class=\"l\"></span>: <input type=\"checkbox\" id=\"theme_bg_rndBlur\" class=\"agi cb\" onchange=\"randomBg()\"><br>\n\t\t</div>\n\t\t<input id=\"theme_base\" class=\"agi\" style=\"display:none\">\n\t\t<span class=\"l\"></span>: <input type=\"checkbox\" id=\"comp_hdays\" class=\"agi cb\"><br>\n\t\t<div id=\"holidays\">Holidays: <input type=\"file\" name=\"data2\" accept=\".json\"> <input type=\"button\" value=\"Upload\" onclick=\"uploadFile(d.Sf.data2,'/holidays.json');\"><br></div>\n\t</div>\n\t\t<div id=\"toast\"></div>\n\t\t<hr><button type=\"button\" onclick=\"cLS()\">Clear local storage</button>\n\t\t<hr><button type=\"button\" onclick=\"B()\">Back</button><button type=\"button\" onclick=\"Save()\">Save</button>\n\t</form>\n</body>\n</html>"
  },
  {
    "path": "wled00/data/settings_um.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"utf-8\">\n\t<meta content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\" name=\"viewport\">\n\t<title>Usermod Settings</title>\n\t<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->\n\t<script>\n\tvar umCfg = {};\n\tvar pins = [], pinO = [], owner;\n\tvar urows;\n\tvar numM = 0;\n\t// load common.js with retry on error\n\t(function loadFiles() {\n\t\tconst l = document.createElement('script');\n\t\tl.src = 'common.js';\n\t\tl.onload = () => loadResources(['style.css'], S); // load style.css then call S()\n\t\tl.onerror = () => setTimeout(loadFiles, 100);\n\t\tdocument.head.appendChild(l);\n\t})();\n\n\n\tfunction S() {\n\t\tgetLoc();\n\t\t// load settings and insert values into DOM\n\t\tfetch(getURL('/json/cfg'), {\n\t\t\tmethod: 'get'\n\t\t})\n\t\t.then(res => {\n\t\t\tif (!res.ok) gId('lserr').style.display = \"inline\";\n\t\t\treturn res.json();\n\t\t})\n\t\t.then(json => {\n\t\t\tumCfg = json.um;\n\t\t\tgetPins(json);\n\t\t\turows=\"\";\n\t\t\tif (isO(umCfg)) {\n\t\t\t\tfor (const [k,o] of Object.entries(umCfg)) {\n\t\t\t\t\turows += `<div class=\"sec\"><h3>${k}</h3>`;\n\t\t\t\t\taddField(k,'unknown',o);\n\t\t\t\t\turows += `</div>`;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (urows===\"\") urows = \"Usermods configuration not found.<br>Press <i>Save</i> to initialize defaults.\";\n\t\t\tgId(\"um\").innerHTML = urows;\n\t\t\tloadJS(getURL('/settings/s.js?p=8'), false, ()=>{\n\t\t\t\td.max_gpio = 50;\n\t\t\t\td.um_p = [];\n\t\t\t\td.rsvd = [];\n\t\t\t\td.ro_gpio = [];\n\t\t\t\td.extra = [];\n\t\t\t}, ()=>{\n\t\t\t\tfor (let r of d.rsvd) { pins.push(r); pinO.push(\"rsvd\"); } // reserved pins\n\t\t\t\tif (d.um_p[0]==-1) d.um_p.shift();\t// remove filler\n\t\t\t\td.Sf.SDA.max = d.Sf.SCL.max = d.Sf.MOSI.max = d.Sf.SCLK.max = d.Sf.MISO.max = d.max_gpio;\n\t\t\t\t//for (let i of d.getElementsByTagName(\"input\")) if (i.type === \"number\" && i.name.replace(\"[]\",\"\").substr(-3) === \"pin\") i.max = d.max_gpio;\n\t\t\t\tpinDD(); // convert INPUT to SELECT for pins\n\t\t\t});\t// If we set async false, file is loaded and executed, then next statement is processed\n\t\t})\n\t\t.catch((error)=>{\n\t\t\tgId('lserr').style.display = \"inline\";\n\t\t\tconsole.log(error);\n\t\t});\n\t\tif (!numM) gId(\"um\").innerHTML = \"No Usermods installed.\";\n\t\tif (loc) d.Sf.action = getURL('/settings/um');\n\t}\n\tfunction check(o,k) {   // input object, pin owner key\n\t\t/* no longer necessary with pin dropdown fields\n\t\tvar n = o.name.replace(\"[]\",\"\").substr(-3);\n\t\tif (o.type==\"number\" && n.substr(0,3)==\"pin\") {\n\t\t\tfor (var i=0; i<pins.length; i++) {\n\t\t\t\tif (k==pinO[i]) continue;\n\t\t\t\tif (o.value==pins[i] && pinO[i]===\"if\") { o.style.color=\"lime\"; break; }\n\t\t\t\tif (o.value==pins[i] || o.value<-1 || o.value>d.max_gpio-1) { o.style.color=\"red\"; break; } else o.style.color=d.ro_gpio.some((e)=>e==parseInt(o.value,10))?\"orange\":\"#fff\";\n\t\t\t}\n\t\t} else {\n\t\t\tswitch (o.name) {\n\t\t\t\tcase \"SDA\": break;\n\t\t\t\tcase \"SCL\": break;\n\t\t\t\tcase \"MOSI\": break;\n\t\t\t\tcase \"SCLK\": break;\n\t\t\t\tcase \"MISO\": break;\n\t\t\t\tdefault: return;\n\t\t\t}\n\t\t\tfor (var i=0; i<pins.length; i++) {\n\t\t\t\t//if (k==pinO[i]) continue; // same owner\n\t\t\t\tif (o.value==pins[i] && pinO[i]===\"if\") { o.style.color=\"tomato\"; break; }\n\t\t\t\tif (o.value==pins[i] || o.value<-1 || o.value>d.max_gpio-1) { o.style.color=\"red\"; break; } else o.style.color=d.ro_gpio.some((e)=>e==parseInt(o.value,10))?\"orange\":\"#fff\";\n\t\t\t}\n\t\t}\n\t\t*/\n\t}\n\tfunction getPins(o) {\n\t\tif (isO(o)) {\n\t\t\tfor (const [k,v] of Object.entries(o)) {\n\t\t\t\tif (isO(v)) {\n\t\t\t\t\tlet oldO = owner; // keep parent name\n\t\t\t\t\towner = k;\n\t\t\t\t\tgetPins(v);\n\t\t\t\t\towner = oldO;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (k.replace(\"[]\",\"\").substr(-3)==\"pin\") {\n\t\t\t\t\tif (Array.isArray(v)) {\n\t\t\t\t\t\tfor (var i=0; i<v.length; i++) if (v[i]>=0) { pins.push(v[i]); pinO.push(owner); }\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (v>=0) { pins.push(v); pinO.push(owner); }\n\t\t\t\t\t}\n\t\t\t\t} else if (Array.isArray(v)) {\n\t\t\t\t\tfor (var i=0; i<v.length; i++) getPins(v[i]);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tfunction initCap(s) {\n  \t\tif (typeof s !== 'string') return '';\n\t\t// https://www.freecodecamp.org/news/how-to-capitalize-words-in-javascript/\n\t\treturn s.replace(/[\\W_]/g,' ').replace(/(^\\w{1})|(\\s+\\w{1})/g, l=>l.toUpperCase()); // replace - and _ with space, capitalize every 1st letter\n\t}\n\tfunction addField(k,f,o,a=false) {  //key, field, (sub)object, isArray\n\t\tif (isO(o)) {\n\t\t\turows += '<hr class=\"sml\">';\n\t\t\tif (f!=='unknown' && !k.includes(\":\")) urows += `<p><u>${initCap(f)}</u></p>`; //show group title\n\t\t\tfor (const [s,v] of Object.entries(o)) {\n\t\t\t\t// possibility to nest objects (only 1 level)\n\t\t\t\tif (f!=='unknown' && !k.includes(\":\")) addField(k+\":\"+f,s,v);\n\t\t\t\telse addField(k,s,v);\n\t\t\t}\n\t\t} else if (Array.isArray(o)) {\n\t\t\tfor (var j=0; j<o.length; j++) {\n\t\t\t\taddField(k,f,o[j],true);\n\t\t\t}\n\t\t} else {\n\t\t\tvar c, t = typeof o;\n\t\t\tswitch (t) {\n\t\t\t\tcase \"boolean\":\n\t\t\t\t\tt = \"checkbox\"; c = 'value=\"true\"' + (o ? ' checked' : '');\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"number\":\n\t\t\t\t\tc = `value=\"${o}\"`;\n\t\t\t\t\tif (f.substr(-3)===\"pin\") {\n\t\t\t\t\t\tc += ` max=\"${d.max_gpio-1}\" min=\"-1\" class=\"s\"`;\n\t\t\t\t\t\tt = \"int\";\n\t\t\t\t\t} else {\n\t\t\t\t\t\tc += ' step=\"any\" class=\"xxl\"';\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tt = \"text\"; c = `value=\"${o}\" style=\"width:250px;\"`;\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t\turows += ` ${initCap(f)} `; //only show field (key is shown in grouping)\n\t\t\t// https://stackoverflow.com/questions/11657123/posting-both-checked-and-unchecked-checkboxes\n\t\t\tif (t==\"checkbox\") urows += `<input type=\"hidden\" name=\"${k}:${f}${a?\"[]\":\"\"}\" value=\"false\">`;\n\t\t\telse if (!a)       urows += `<input type=\"hidden\" name=\"${k}:${f}${a?\"[]\":\"\"}\" value=\"${t}\">`;\n\t\t\turows += `<input type=\"${t===\"int\"?\"number\":t}\" name=\"${k}:${f}${a?\"[]\":\"\"}\" ${c} oninput=\"check(this,'${k.substr(k.indexOf(\":\")+1)}')\"><br>`;\n\t\t}\n\t}\n\tfunction pinDD() {\n\t\tfor (let i of d.Sf.elements) {\n\t\t\tif (i.type === \"number\" && (i.name.includes(\"pin\") || [\"SDA\",\"SCL\",\"MOSI\",\"MISO\",\"SCLK\"].includes(i.name))) { //select all pin select elements\n\t\t\t\tlet v = parseInt(i.value);\n\t\t\t\tlet sel = addDD(i.name,0);\n\t\t\t\tfor (var j = -1; j < d.max_gpio; j++) {\n\t\t\t\t\tif (d.rsvd.includes(j)) continue;\n\t\t\t\t\tlet foundPin = pins.indexOf(j);\n\t\t\t\t\tlet txt = (j === -1) ? \"unused\" : `${j}`;\n\t\t\t\t\tif (foundPin >= 0 && j !== v) txt += ` ${pinO[foundPin]==\"if\"?\"global\":pinO[foundPin]}`; // already reserved pin\n\t\t\t\t\tif (d.ro_gpio.includes(j)) txt += \" (R/O)\";\n\t\t\t\t\tlet opt = addO(sel, txt, j);\n\t\t\t\t\tif (j === v) opt.selected = true; // this is \"our\" pin\n\t\t\t\t\telse if (pins.includes(j)) opt.disabled = true; // someone else's pin\n\t\t\t\t}\n\t\t\t\tlet um = i.name.split(\":\")[0];\n\t\t\t\td.extra.forEach((o)=>{\n\t\t\t\t\tif (o[um] && o[um].pin) o[um].pin.forEach((e)=>{\n\t\t\t\t\t\tlet opt = addO(sel,e[0],e[1]);\n\t\t\t\t\t\tif (e[1]==v) opt.selected = true;\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\tfunction UI(e) {\n\t\t// update changed select options across all usermods\n\t\tlet oldV = parseInt(e.dataset.val);\n\t\te.dataset.val = e.value;\n\t\tlet txt = e.name.split(\":\")[e.name.split(\":\").length-2];\n\t\tlet selects = d.Sf.querySelectorAll(\"select[class='pin']\");\n\t\tfor (let sel of selects) {\n\t\t\tif (sel == e) continue\n\t\t\tArray.from(sel.options).forEach((i)=>{\n\t\t\t\tif (!(i.value==oldV || i.value==e.value)) return;\n\t\t\t\tif (i.value == -1) {\n\t\t\t\t\ti.text = \"unused\";\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (i.value<100) { // TODO remove this hack and use d.extra\n\t\t\t\t\ti.text = i.value;\n\t\t\t\t\tif (i.value==oldV) {\n\t\t\t\t\t\ti.disabled = false;\n\t\t\t\t\t}\n\t\t\t\t\tif (i.value==e.value) {\n\t\t\t\t\t\ti.disabled = true;\n\t\t\t\t\t\ti.text += ` ${txt}`;\n\t\t\t\t\t}\n\t\t\t\t\tif (d.ro_gpio.includes(parseInt(i.value))) i.text += \" (R/O)\";\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\t// https://stackoverflow.com/questions/39729741/javascript-change-input-text-to-select-option\n\tfunction addDD(um,fld) {\n\t\tlet sel = cE('select');\n\t\tif (typeof(fld) === \"string\") {\t// parameter from usermod (field name)\n\t\t\tif (fld.includes(\"pin\")) sel.classList.add(\"pin\");\n\t\t\tum += \":\"+fld;\n\t\t} else if (typeof(fld) === \"number\") sel.classList.add(\"pin\"); // a hack to add a class\n\t\tlet arr = d.getElementsByName(um);\n\t\tlet idx = arr[0].type===\"hidden\"?1:0; // ignore hidden field\n\t\tif (arr.length > 1+idx) {\n\t\t\t// we have array of values (usually pins)\n\t\t\tfor (let i of arr) {\n\t\t\t\tif (i.nodeName === \"INPUT\" && i.type === \"number\") break;\n\t\t\t\tidx++;\n\t\t\t}\n\t\t}\n\t\tlet inp = arr[idx];\n\t\tif (inp && inp.tagName === \"INPUT\" && (inp.type === \"text\" || inp.type === \"number\")) {  // may also use nodeName\n\t\t\tlet v = inp.value;\n\t\t\tlet n = inp.name;\n\t\t\t// copy the existing input element's attributes to the new select element\n\t\t\tfor (var i = 0; i < inp.attributes.length; ++ i) {\n\t\t\t\tvar att = inp.attributes[i];\n\t\t\t\t// type and value don't apply, so skip them\n\t\t\t\t// ** you might also want to skip style, or others -- modify as needed **\n\t\t\t\tif (att.name != 'type' && att.name != 'value' && att.name != 'class' && att.name != 'style' && att.name != 'oninput' && att.name != 'max' && att.name != 'min') {\n\t\t\t\t\tsel.setAttribute(att.name, att.value);\n\t\t\t\t}\n\t\t\t}\n\t\t\tsel.setAttribute(\"data-val\", v);\n\t\t\tsel.setAttribute(\"onchange\", \"UI(this)\");\n\t\t\t// finally, replace the old input element with the new select element\n\t\t\tinp.parentElement.replaceChild(sel, inp);\n\t\t\treturn sel;\n\t\t}\n\t\treturn null;\n\t}\n\tvar addDropdown = addDD; // backwards compatibility\n\tfunction addO(sel,txt,val) {\n\t\tif (sel===null) return; // select object missing\n\t\tlet opt = cE(\"option\");\n\t\topt.value = val;\n\t\topt.text = txt;\n\t\tsel.appendChild(opt);\n\t\tfor (let i=0; i<sel.childNodes.length; i++) {\n\t\t\tlet c = sel.childNodes[i];\n\t\t\tif (c.value == sel.dataset.val) sel.selectedIndex = i;\n\t\t}\n\t\treturn opt;\n\t}\n\tvar addOption = addO; // backwards compatibility\n\t// https://stackoverflow.com/questions/26440494/insert-text-after-this-input-element-with-javascript\n\tfunction addI(name,el,txt, txt2=\"\") {\n\t\tlet obj = d.getElementsByName(name);\n\t\tif (!obj.length) return;\n\t\tif (typeof el === \"string\" && obj[0]) obj[0].placeholder = el;\n\t\telse if (obj[el]) {\n\t\t\tif (txt!=\"\") obj[el].insertAdjacentHTML('afterend', '&nbsp;'+txt);\n\t\t\tif (txt2!=\"\") obj[el].insertAdjacentHTML('beforebegin', txt2 + '&nbsp;');  //add pre texts\n\t\t}\n\t}\n\tvar addInfo = addI; // backwards compatibility\n\t// add Help Button\n\tfunction addHB(um) {\n\t\taddI(um + ':help',0,`<button onclick=\"location.href='https://kno.wled.ge/usermods/${um}'\" type=\"button\">?</button>`);\n\t}\n\tfunction svS(e) {\n\t\te.preventDefault();\n\t\tif (d.Sf.checkValidity()) d.Sf.submit(); //https://stackoverflow.com/q/37323914\n\t}\n\t</script>\n</head>\n\n<body>\n\t<form id=\"form_s\" name=\"Sf\" method=\"post\" onsubmit=\"svS(event)\">\n\t\t<div class=\"toprow\">\n\t\t<div class=\"helpB\"><button type=\"button\" onclick=\"H()\">?</button></div>\n\t\t<button type=\"button\" onclick=\"B()\">Back</button><button type=\"submit\">Save</button><br>\n\t\t<span id=\"lssuc\" style=\"color:green; display:none\">&#10004; Configuration saved!</span>\n\t\t<span id=\"lserr\" style=\"color:red; display:none\">&#9888; Could not load configuration.</span><hr>\n\t\t</div>\n\t\t<h2>Usermod Setup</h2>\n\t\t<div class=\"sec\">\n\t\t\t<h3>Global I<sup>2</sup>C & SPI</h3>\n\t\t\t\n\t\t\t<b>I<sup>2</sup>C GPIOs (HW)</b><br>\n\t\t\tSDA:<input type=\"number\" min=\"-1\" max=\"48\" name=\"SDA\" onchange=\"check(this,'if')\" class=\"s\" placeholder=\"SDA\">\n\t\t\tSCL:<input type=\"number\" min=\"-1\" max=\"48\" name=\"SCL\" onchange=\"check(this,'if')\" class=\"s\" placeholder=\"SCL\"><br>\n\t\t\t<br><b>SPI GPIOs (HW)</b><br>\n\t\t\t<i class=\"warn\">only changable on ESP32</i><br>\n\t\t\tMOSI:<input type=\"number\" min=\"-1\" max=\"48\" name=\"MOSI\" onchange=\"check(this,'if')\" class=\"s\" placeholder=\"MOSI\">\n\t\t\tMISO:<input type=\"number\" min=\"-1\" max=\"48\" name=\"MISO\" onchange=\"check(this,'if')\" class=\"s\" placeholder=\"MISO\">\n\t\t\tSCLK:<input type=\"number\" min=\"-1\" max=\"48\" name=\"SCLK\" onchange=\"check(this,'if')\" class=\"s\" placeholder=\"SCLK\"><br>\n\t\t\t<br><i class=\"warn\">change requires reboot!</i><br>\n\t\t\tReboot after save? <input type=\"checkbox\" name=\"RBT\">\n\t\t</div>\n\t\t<div id=\"um\">Loading settings...</div>\n\t\t<hr><button type=\"button\" onclick=\"B()\">Back</button><button type=\"submit\">Save</button>\n\t</form>\n</body>\n\n</html>"
  },
  {
    "path": "wled00/data/settings_wifi.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\">\n\t<meta content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\" name=\"viewport\">\n\t<title>WiFi Settings</title>\n\t<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->\n\t<script>\n\t\t// load common.js with retry on error\n\t\t(function loadFiles() {\n\t\t\tconst l = document.createElement('script');\n\t\t\tl.src = 'common.js';\n\t\t\tl.onload = () => loadResources(['style.css'], S); // load style.css then call S()\n\t\t\tl.onerror = () => setTimeout(loadFiles, 100);\n\t\t\tdocument.head.appendChild(l);\n\t\t})();\n\n\t\tvar scanLoops = 0, preScanSSID = \"\";\n\t\tvar maxNetworks = 3;\n\t\tfunction N() {\n\t\t\tconst button = gId(\"scan\");\n\t\t\tbutton.disabled = true;\n\t\t\tbutton.textContent = \"Scanning...\";\n\n\t\t\tfetch(getURL(\"/json/net\")).then((response) => {\n\t\t\t\treturn response.json();\n\t\t\t}).then((json) => {\n\t\t\t\t// Get the list of networks only, defaulting to an empty array.\n\t\t\t\treturn Object.assign(\n\t\t\t\t\t{},\n\t\t\t\t\t{\"networks\": []},\n\t\t\t\t\tjson,\n\t\t\t\t).networks.sort(\n\t\t\t\t\t// Sort by signal strength, descending.\n\t\t\t\t\t(a, b) => b.rssi - a.rssi\n\t\t\t\t).reduce(\n\t\t\t\t\t// Filter out duplicate SSIDs. Since it is sorted by signal\n\t\t\t\t\t// strength, the strongest signal will be kept in the\n\t\t\t\t\t// order it as originally appeared in the array.\n\t\t\t\t\t(unique, other) => {\n\t\t\t\t\t\tif(!unique.some(obj => obj.ssid === other.ssid)) {\n\t\t\t\t\t\t\tunique.push(other);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn unique;\n\t\t\t\t\t},\n\t\t\t\t\t[],\n\t\t\t\t);\n\t\t\t}).then((networks) => {\n\t\t\t\t// If there are no networks, fetch it again in a second.\n\t\t\t\t// but only do this a few times.\n\t\t\t\tif (networks.length === 0 && scanLoops < 10) {\n\t\t\t\t\tscanLoops++;\n\t\t\t\t\tsetTimeout(N, 1000);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tscanLoops = 0;\n\n\t\t\t\tif (networks.length > 0) {\n\t\t\t\t\tlet cs = d.querySelectorAll(\"#wifi_entries input[type=text][name^=CS]\");\n\t\t\t\t\tfor (let input of (cs||[])) {\n\t\t\t\t\t\tlet found = false;\n\t\t\t\t\t\tlet select = cE(\"select\");\n\t\t\t\t\t\tselect.id = input.id;\n\t\t\t\t\t\tselect.name = input.name;\n\t\t\t\t\t\tselect.setAttribute(\"onchange\", \"T(this)\");\n\t\t\t\t\t\tpreScanSSID = input.value;\n\n\t\t\t\t\t\tfor (let i = 0; i < select.children.length; i++) {\n\t\t\t\t\t\t\tselect.removeChild(select.children[i]);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tfor (let i = 0; i < networks.length; i++) {\n\t\t\t\t\t\t\tconst option = cE(\"option\");\n\n\t\t\t\t\t\t\toption.setAttribute(\"value\", networks[i].ssid);\n\t\t\t\t\t\t\toption.textContent = `${networks[i].ssid} (${networks[i].rssi} dBm)`; // [${networks[i].bssid.replaceAll(':','')}]\n\n\t\t\t\t\t\t\tif (networks[i].ssid === input.value) {\n\t\t\t\t\t\t\t\toption.setAttribute(\"selected\", \"selected\");\n\t\t\t\t\t\t\t\tfound = true;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tselect.appendChild(option);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst option = cE(\"option\");\n\n\t\t\t\t\t\toption.setAttribute(\"value\", \"!Cs\");\n\t\t\t\t\t\toption.textContent = \"Other network...\";\n\t\t\t\t\t\tselect.appendChild(option);\n\n\t\t\t\t\t\tif (input.value === \"\" || input.value === \"Your_Network\" || found) input.replaceWith(select);\n\t\t\t\t\t\telse select.remove(); \n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tbutton.disabled = false;\n\t\t\t\tbutton.textContent = \"Scan\";\n\t\t\t});\n\t\t}\n\t\t// replace WiFi select with custom SSID input field again\n\t\tfunction T(cs) {\n\t\t\tif (!cs || cs.value != \"!Cs\") return;\n\t\t\tlet input = cE(\"input\");\n\t\t\tinput.type = \"text\";\n\t\t\tinput.id = cs.id;\n\t\t\tinput.name = cs.name;\n\t\t\tinput.setAttribute(\"maxlength\",32);\n\t\t\tinput.value = preScanSSID;\n\t\t\tcs.replaceWith(input);\n\t\t}\n\t\tfunction resetWiFi(maxN = undefined) {\n\t\t\tif (maxN) maxNetworks = maxN;\n\t\t\tlet entries = gId(\"wifi_entries\").children\n\t\t\tfor (let i = entries.length; i > 0; i--) entries[i-1].remove();\n\t\t\tbtnWiFi(0);\n\t\t}\n\t\tfunction btnWiFi(i) {\n\t\t\tgId(\"wifi_add\").style.display = (i<maxNetworks) ? \"inline\":\"none\";\n\t\t\tgId(\"wifi_rem\").style.display = (i>1) ? \"inline\":\"none\";\n\t\t}\n\t\tfunction addWiFi(ssid=\"\",pass=\"\",bssid=\"\",ip=0,gw=0,sn=0x00ffffff,type=-1,anon=\"\",ident=\"\") { // little endian\n\t\t\tvar i = gId(\"wifi_entries\").childNodes.length;\n\t\t\tif (i >= maxNetworks) return;\n\t\t\tvar encryptionTypeField = \"\";\n\t\t\tif (type >=0 && type < 2) {\n\t\t\t\tencryptionTypeField = `WiFi encryption type:<br>\n<select id=\"ET${i}\" name=\"ET${i}\" onchange=\"E(${i})\">\n<option value=\"0\"${(type==0) ? ' selected':''}>None/WPA/WPA2</option>\n<option value=\"1\"${(type==1) ? ' selected':''}>WPA/WPA2-Enterprise</option>\n</select><br>\n<div id=\"IDS${i}\" style=\"${(type==0) ? 'display:none;':''}\">\nAnonymous identity:<br><input type=\"text\" id=\"EA${i}\" name=\"EA${i}\" maxlength=\"64\" value=\"${anon}\"><br>\nIdentity:<br><input type=\"text\" id=\"EI${i}\" name=\"EI${i}\" maxlength=\"64\" value=\"${ident}\"><br>\n</div>`;\n\t\t\t}\n\t\t\tvar b = `<div id=\"net${i}\"><hr class=\"sml\">\nNetwork name (SSID${i==0?\", empty to not connect\":\"\"}):<br><input type=\"text\" id=\"CS${i}\" name=\"CS${i}\" maxlength=\"32\" value=\"${ssid}\" ${i>0?\"required\":\"\"}><br>\n${encryptionTypeField}\nNetwork password:<br><input type=\"password\" name=\"PW${i}\" maxlength=\"64\" value=\"${pass}\"><br>\nBSSID (optional):<br><input type=\"text\" id=\"BS${i}\" name=\"BS${i}\" maxlength=\"12\" value=\"${bssid}\"><br>\nStatic IP (leave at 0.0.0.0 for DHCP)${i==0?\"<br>Also used by Ethernet\":\"\"}:<br>\n<input name=\"IP${i}0\" type=\"number\" class=\"s\" min=\"0\" max=\"255\" value=\"${ip&0xFF}\" required>.<input name=\"IP${i}1\" type=\"number\" class=\"s\" min=\"0\" max=\"255\" value=\"${(ip>>8)&0xFF}\" required>.<input name=\"IP${i}2\" type=\"number\" class=\"s\" min=\"0\" max=\"255\" value=\"${(ip>>16)&0xFF}\" required>.<input name=\"IP${i}3\" type=\"number\" class=\"s\" min=\"0\" max=\"255\" value=\"${(ip>>24)&0xFF}\" required><br>\nStatic gateway:<br>\n<input name=\"GW${i}0\" type=\"number\" class=\"s\" min=\"0\" max=\"255\" value=\"${gw&0xFF}\" required>.<input name=\"GW${i}1\" type=\"number\" class=\"s\" min=\"0\" max=\"255\" value=\"${(gw>>8)&0xFF}\" required>.<input name=\"GW${i}2\" type=\"number\" class=\"s\" min=\"0\" max=\"255\" value=\"${(gw>>16)&0xFF}\" required>.<input name=\"GW${i}3\" type=\"number\" class=\"s\" min=\"0\" max=\"255\" value=\"${(gw>>24)&0xFF}\" required><br>\nStatic subnet mask:<br>\n<input name=\"SN${i}0\" type=\"number\" class=\"s\" min=\"0\" max=\"255\" value=\"${sn&0xFF}\" required>.<input name=\"SN${i}1\" type=\"number\" class=\"s\" min=\"0\" max=\"255\" value=\"${(sn>>8)&0xFF}\" required>.<input name=\"SN${i}2\" type=\"number\" class=\"s\" min=\"0\" max=\"255\" value=\"${(sn>>16)&0xFF}\" required>.<input name=\"SN${i}3\" type=\"number\" class=\"s\" min=\"0\" max=\"255\" value=\"${(sn>>24)&0xFF}\" required></div>`;\n\t\t\tgId(\"wifi_entries\").insertAdjacentHTML(\"beforeend\", b);\n\t\t\tbtnWiFi(i+1);\n\t\t}\n\t\tfunction remWiFi() {\n\t\t\tconst entries = gId(\"wifi_entries\").children;\n\t\t\tconst i = entries.length;\n\t\t\tif (i < 2) return;\n\t\t\tentries[i-1].remove();\n\t\t\tbtnWiFi(i-1);\n\t\t}\n\t\tfunction S() {\n\t\t\tgetLoc();\n\t\t\tloadJS(getURL('/settings/s.js?p=1'), false);\t// If we set async false, file is loaded and executed, then next statement is processed\n\t\t\tif (loc) d.Sf.action = getURL('/settings/wifi');\n\t\t\tsetTimeout(tE, 500); // wait for DOM to load before calling tE()\n\t\t}\n\n\t\tvar rC = 0; // remote count\n\t\t// toggle visibility of ESP-NOW remote list based on checkbox state\n\t\tfunction tE() {\n\t\t\t// keep the hidden input with MAC addresses, only toggle visibility of the list UI\n\t\t\tgId('rlc').style.display = d.Sf.RE.checked ? 'block' : 'none';\n\t\t}\n\t\t// reset remotes: initialize empty list (called from xml.cpp)\n\t\tfunction rstR() {\n\t\t\tgId('rml').innerHTML = ''; // clear remote list\n\t\t}\n\t\t// add remote MAC to the list\n\t\tfunction aR(id, mac) {\n\t\t\tif (!/^[0-9A-F]{12}$/i.test(mac)) return; // check for valid hex string\n\t\t\tlet inputs = d.querySelectorAll(\"#rml input\");\n\t\t\tfor (let i of (inputs || [])) {\n\t\t\t\tif (i.value === mac) return;\n\t\t\t}\n\t\t\tlet l = gId('rml'), r = cE('div'), i = cE('input');\n\t\t\ti.type = 'text';\n\t\t\ti.name = id;\n\t\t\ti.value = mac;\n\t\t\ti.maxLength = 12;\n\t\t\ti.minLength = 12;\n\t\t\t//i.onchange = uR;\n\t\t\tr.appendChild(i);\n\t\t\tlet b = cE('button');\n\t\t\tb.type = 'button';\n\t\t\tb.className = 'sml';\n\t\t\tb.innerText = '-';\n\t\t\tb.onclick = (e) => {\n \t\t\t\tr.remove();\n \t\t\t};\n\t\t\tr.appendChild(b);\n\t\t\tl.appendChild(r);\n\t\t\trC++;\n\t\t\tgId('+').style.display = gId(\"rml\").childElementCount < 10 ? 'inline' : 'none'; // can't append to list anymore, hide button\n\t\t}\n\t\tfunction E(i) {\n\t\t\tconst select = gId(\"ET\"+i);\n\t\t\tif (select.value === \"0\") {\n\t\t\t\tgId(\"IDS\"+i).style.display = \"none\";\n\t\t\t} else {\n\t\t\t\tgId(\"IDS\"+i).style.display = \"\";\n\t\t\t}\n\t\t}\n\n\t</script>\n</head>\n<body>\n<form id=\"form_s\" name=\"Sf\" method=\"post\">\n\t<div class=\"toprow\">\n\t<div class=\"helpB\"><button type=\"button\" onclick=\"H('features/settings/#wifi-settings')\">?</button></div>\n\t<button type=\"button\" onclick=\"B()\">Back</button><button type=\"submit\">Save & Connect</button><hr>\n\t</div>\n\t<h2>WiFi & Network Settings</h2>\n\t<div class=\"sec\">\n\t\t<h3>Wireless network</h3>\n\t\t<button type=\"button\" id=\"scan\" onclick=\"N()\">Scan</button><br>\n\t\t<div id=\"wifi\">\n\t\t\t<div id=\"wifi_entries\"></div>\n\t\t\t<hr class=\"sml\">\n\t\t\t<button type=\"button\" id=\"wifi_add\" onclick=\"addWiFi()\">+</button>\n\t\t\t<button type=\"button\" id=\"wifi_rem\" onclick=\"remWiFi()\">-</button><br>\n\t\t</div>\n\t</div>\n\t<div class=\"sec\" id=\"ethd\">\n\t\t<h3>Ethernet Type</h3>\n\t\t<select name=\"ETH\">\n\t\t\t<option value=\"0\">None</option>\n\t\t\t<option value=\"6\">IoTorero/ESP32Deux/RGB2Go</option>\n\t\t\t<option value=\"9\">ABC! WLED V43 & compatible</option>\n\t\t\t<option value=\"2\">ESP32-POE</option>\n\t\t\t<option value=\"11\">ESP32-POE-WROVER</option>\n\t\t\t<option value=\"7\">KIT-VE</option>\n\t\t\t<option value=\"12\">LILYGO T-POE Pro</option>\n\t\t\t<option value=\"8\">QuinLED-Dig-Octa & T-ETH-POE</option>\n\t\t\t<option value=\"4\">QuinLED-ESP32</option>\n\t\t\t<option value=\"10\">Serg74-ETH32</option>\n\t\t\t<option value=\"5\">TwilightLord-ESP32</option>\n\t\t\t<option value=\"3\">WESP32</option>\n\t\t\t<option value=\"1\">WT32-ETH01</option>\n\t\t\t<option value=\"13\">Gledopto</option>\n\t\t</select><br><br>\n\t</div>\n\t<div class=\"sec\">\n\t\t<h3>DNS & mDNS</h3>\n\t\tDNS server address:<br>\n\t\t<input name=\"D0\" type=\"number\" class=\"s\" min=\"0\" max=\"255\" required>.<input name=\"D1\" type=\"number\" class=\"s\" min=\"0\" max=\"255\" required>.<input name=\"D2\" type=\"number\" class=\"s\" min=\"0\" max=\"255\" required>.<input name=\"D3\" type=\"number\" class=\"s\" min=\"0\" max=\"255\" required><br>\n\t\t<br>\n\t\tmDNS address (leave empty for no mDNS):<br>\n\t\thttp:// <input type=\"text\" name=\"CM\" maxlength=\"32\"> .local<br>\n\t\tClient IP: <span class=\"sip\"> Not connected </span> <br>\n\t</div>\n\t<div class=\"sec\">\n\t\t<h3>Configure Access Point</h3>\n\t\tAP SSID (leave empty for no AP):<br> <input type=\"text\" name=\"AS\" maxlength=\"32\"><br>\n\t\tHide AP name: <input type=\"checkbox\" name=\"AH\"><br>\n\t\tAP password (leave empty for open):<br> <input type=\"password\" name=\"AP\" maxlength=\"63\" pattern=\"(.{8,63})|()\" title=\"Empty or min. 8 characters\"><br>\n\t\tAccess Point WiFi channel: <input name=\"AC\" type=\"number\" class=\"xs\" min=\"1\" max=\"13\" required><br>\n\t\tAP opens:\n\t\t<select name=\"AB\">\n\t\t\t<option value=\"0\">No connection after boot</option>\n\t\t\t<option value=\"1\">Disconnected</option>\n\t\t\t<option value=\"2\">Always</option>\n\t\t\t<option value=\"3\">Never (not recommended)</option>\n\t\t\t<option value=\"4\">Temporary (no connection after boot)</option>\n\t\t</select><br>\n\t\tAP IP: <span class=\"sip\"> Not active </span><br>\n\t</div>\n\t<div class=\"sec\">\n\t\t<h3>WiFi Power</h3>\n\t\tForce 802.11g mode (ESP8266 only): <input type=\"checkbox\" name=\"FG\"><br>\n\t\tDisable WiFi sleep: <input type=\"checkbox\" name=\"WS\"><br>\n\t\t<i>Disabling WiFi sleep increases power consumption<br>\n\t\tbut can help with connectivity issues and sync.</i><br><br>\n\t\t<div id=\"tx\">Max. TX power: <select name=\"TX\">\n\t\t\t<option value=\"78\">19.5 dBm</option>\n\t\t\t<option value=\"76\">19 dBm</option>\n\t\t\t<option value=\"74\">18.5 dBm</option>\n\t\t\t<option value=\"68\">17 dBm</option>\n\t\t\t<option value=\"60\">15 dBm</option>\n\t\t\t<option value=\"52\">13 dBm</option>\n\t\t\t<option value=\"44\">11 dBm</option>\n\t\t\t<option value=\"34\">8.5 dBm</option>\n\t\t\t<option value=\"28\">7 dBm</option>\n\t\t\t<option value=\"20\">5 dBm</option>\n\t\t\t<option value=\"8\">2 dBm</option>\n\t\t</select><br>\n\t\t<i class=\"warn\">WARNING: Modifying TX power may render device unreachable.</i>\n\t\t</div>\n\t</div>\n\t<div class=\"sec\">\n\t\t<h3>ESP-NOW Wireless</h3>\n\t\t<div id=\"NoESPNOW\" class=\"hide\">\n\t\t\t<i class=\"warn\">This firmware build does not include ESP-NOW support.<br></i>\n\t\t</div>\n\t\t<div id=\"ESPNOW\">\n\t\t\tEnable ESP-NOW: <input type=\"checkbox\" name=\"RE\" onchange=\"tE()\"><br>\n\t\t\t<i>Listen for events over ESP-NOW<br>\n\t\t\tKeep disabled if not using a remote or ESP-NOW sync, increases power consumption.<br></i>\n\t\t\t<div id=\"rlc\">\n\t\t\t\tLast device seen: <span class=\"rlid\" id=\"ld\">None</span>\n\t\t\t\t<button type=\"button\" class=\"sml\" id=\"+\" onclick=\"aR('RM'+rC,gId('ld').textContent)\">+</button><br>\n\t\t\t\tLinked MACs (10 max):<br>\n\t\t\t\t<div id=\"rml\">\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\t<hr>\n\t<button type=\"button\" onclick=\"B()\">Back</button><button type=\"submit\">Save & Connect</button>\n</form>\n</body>\n</html>\n"
  },
  {
    "path": "wled00/data/style.css",
    "content": "html {\n\ttouch-action: manipulation;\n}\nbody {\n  font-family: Verdana, sans-serif;\n  font-size: 1rem;\n  text-align: center;\n  background: #111;\n  color: #fff;\n  line-height: 200%;\n  margin: 0;\n}\nhr {\n  border-color: #666;\n}\nhr.sml {\n  width: 260px;\n}\nh4 {\n  margin: 0;\n}\na, a:hover {\n  color: #28f;\n  text-decoration: none;\n}\n.sec {\n  background: #222;\n  border-radius: 20px;\n  padding: 8px;\n  margin: 12px auto;\n  max-width: 520px;\n}\nbutton, .btn {\n  background: #333;\n  color: #fff;\n  font-family: Verdana, sans-serif;\n  border: 0.3ch solid #333;\n\tborder-radius: 24px;\n  display: inline-block;\n  font-size: 20px;\n  margin: 12px 8px 8px;\n  padding: 8px 12px;\n  min-width: 48px;\n  cursor: pointer;\n  text-decoration: none;\n  transition: all 0.3s ease;\n}\nbutton.sml {\n  padding: 8px;\n  border-radius: 20px;\n  font-size: 15px;\n  min-width: 40px;\n  margin: 0 0 0 10px;\n}\nbutton:hover, .btn:hover{\n  background:#555;\n  border-color:#555;\n}\n\n#scan {\n  margin-top: -10px;\n}\n.toprow {\n  top: 0;\n  position: sticky;\n  background-color:#222;\n  z-index:1;\n}\n.lnk {\n  border: 0;\n}\n.helpB {\n  text-align: left;\n  position: absolute;\n  width: 60px;\n}\n.hide {\n  display: none;\n}\n.err {\n  color: #f00;\n}\n.warn {\n  color: #fa0;\n}\ninput {\n  background: #333;\n  color: #fff;\n  font-family: Verdana, sans-serif;\n  border: 0.5ch solid #333;\n}\ninput:disabled {\n  color: #888;\n}\ninput:invalid {\n  color: #f00;\n}\ninput[type=\"text\"],\ninput[type=\"number\"],\ninput[type=\"password\"],\nselect {\n    font-size: medium;\n    margin: 2px;\n  }\ninput[type=\"number\"] {\n  width: 4em;\n}\ninput[type=\"number\"].xxl {\n  width: 100px;\n}\ninput[type=\"number\"].xl {\n  width: 85px;\n}\ninput[type=\"number\"].l {\n  width: 64px;\n}\ninput[type=\"number\"].m {\n  width: 56px;\n}\ninput[type=\"number\"].s {\n  width: 48px;\n}\ninput[type=\"number\"].xs {\n  width: 40px;\n}\ninput[type=\"checkbox\"] {\n  transform: scale(1.5);\n  margin-right: 10px;\n}\ntd input[type=\"checkbox\"] {\n  margin-right: revert;\n}\ninput[type=file] {\n  font-size: 16px\n}\nselect {\n  margin: 2px;\n  background: #333;\n  color: #fff;\n  font-family: Verdana, sans-serif;\n  border: 0.5ch solid #333;\n}\nselect.pin {\n  max-width: 120px;\n  text-overflow: ellipsis;\n}\ntr {\n  line-height: 100%;\n}\ntd {\n  padding: 2px;\n}\n.d5 {\n  width: 4rem !important;\n}\n.cal {\n  font-size:1.5rem;\n  cursor:pointer\n}\n#TMT table {\n  width: 100%;\n}\n\n#msg {\n  display: none;\n}\n\n#toast {\n\topacity: 0;\n\tbackground-color: #444;\n\tborder-radius: 5px;\n\tbottom: 64px;\n\tcolor: #fff;\n\tfont-size: 17px;\n\tpadding: 16px;\n\tpointer-events: none;\n\tposition: fixed;\n\ttext-align: center;\n\tz-index: 5;\n\ttransform: translateX(-50%);\n  max-width: 90%;\n\tleft: 50%;\n}\n\n#toast.show {\n\topacity: 1;\n\tbackground-color: #264;\n\tanimation: fadein 0.5s, fadein 0.5s 2.5s reverse;\n}\n\n#toast.error {\n\topacity: 1;\n\tbackground-color: #b21;\n\tanimation: fadein 0.5s;\n}\n\n@media screen and (max-width: 767px) {\n  input[type=\"text\"],\n  input[type=\"file\"],\n  input[type=\"number\"],\n  input[type=\"email\"],\n  input[type=\"tel\"],\n  input[type=\"password\"] {\n    font-size: 16px;\n  }\n}\n\n@media screen and (max-width: 480px) {\n  input[type=\"number\"].s {\n    width: 40px;\n  }\n  input[type=\"number\"].xs {\n    width: 32px;\n  }\n  input[type=\"file\"] {\n    width: 224px;\n  }\n  #btns select {\n    width: 144px;\n  }\n}\n"
  },
  {
    "path": "wled00/data/update.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta content='width=device-width' name='viewport'>\n\t<title>WLED Update</title>\n\t<script src=\"common.js\" type=\"text/javascript\"></script>\n\t<script>\n\t\tfunction B() { window.history.back(); }\n\t\tvar cnfr = false;\n\t\tfunction cR() {\n\t\t\tif (!cnfr) {\n\t\t\t\tvar bt = gId('rev');\n\t\t\t\tbt.style.color = \"red\";\n\t\t\t\tbt.innerText = \"Revert!\";\n\t\t\t\tcnfr = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\twindow.open(getURL(\"/update?revert\"),\"_self\");\n\t\t}\n\t\tfunction GetV() {\n\t\t\t// Fetch device info via JSON API instead of compiling it in\n\t\t\tfetch(getURL('/json/info'))\n\t\t\t\t.then(response => response.json())\n\t\t\t\t.then(data => {\n\t\t\t\t\tdocument.querySelector('.installed-version').textContent = `${data.brand} ${data.ver} (${data.vid})`;\n\t\t\t\t\tdocument.querySelector('.release-name').textContent = data.release;\n\t\t\t\t\t// assemble update URL and update release badge\n\t\t\t\t\tif (data.repo && data.repo !== \"unknown\") {\n\t\t\t\t\t\tconst repoUrl = \"https://github.com/\" + data.repo + \"/releases/latest\";\n\t\t\t\t\t\tconst badgeUrl = \"https://img.shields.io/github/release/\" + data.repo + \".svg?style=flat-square\";\n\t\t\t\t\t\tdocument.querySelector('.release-repo').href = repoUrl;\n\t\t\t\t\t\tdocument.querySelector('.release-badge').src = badgeUrl;\n\t\t\t\t\t\ttoggle('release-download'); // only show release download item after receiving a valid data.repo\n\t\t\t\t\t} else {\n\t\t\t\t\t\tgId('Norelease-download').classList.add(\"hide\"); // repo invalid -> hide everything\n\t\t\t\t\t}\n\t\t\t\t\t// TODO - can this be done at build time?\n\t\t\t\t\tif (data.arch == \"esp8266\") {\n\t\t\t\t\t\ttoggle('rev');\n\t\t\t\t\t}\n\t\t\t\t\tconst allowBl = data.arch && (data.arch.toLowerCase() === 'esp32' || data.arch.toLowerCase() === 'esp32-s2');\n\t\t\t\t\tif (allowBl) {\n\t\t\t\t\t\ttoggle('bootupd')\n\t\t\t\t\t\tif (data.bootloaderSHA256) {\n\t\t\t\t\t\t\tgId('bootloader-hash').innerText = 'Current bootloader SHA256: \\n' + data.bootloaderSHA256;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.catch(error => {\n\t\t\t\t\tconsole.log('Could not fetch device info:', error);\n\t\t\t\t\t// Fallback to compiled-in value if API call fails\n\t\t\t\t\tdocument.querySelector('.installed-version').textContent = 'Unknown';\n\t\t\t\t\tdocument.querySelector('.release-name').textContent = 'Unknown';\n\t\t\t\t\tgId('Norelease-download').classList.add(\"hide\"); // fetch failed -> hide everything\n\t\t\t\t});\n\t\t}\n\t\tfunction hideforms() {\n\t\t\tgId('toprow').classList.add(\"hide\");  // hide top row with \"back\" button\n\t\t\tgId('backbtn').classList.add(\"hide\"); // hide \"back\" button on bottom of the page\n\t\t\tgId('bootupd').classList.add(\"hide\");\n\t\t\ttoggle('upd');\t\t\t\n\t\t}\n\t</script>\n\t<style>\n\t\t@import url(\"style.css\");\n\t</style>\n</head>\n<body onload=\"GetV();\">\n\t<div id=\"toprow\" class=\"toprow\">\n\t<div class=\"helpB\"><button type=\"button\" aria-label=\"Help\" title=\"Help\" onclick=\"H('basics/getting-started/#software-update-procedure')\">?</button></div>\n\t<button type=\"button\" onclick=\"B()\">Back</button>\n\t</div>\n\t<br>\n\t<form method='POST' action='./update' id='upd' enctype='multipart/form-data' onsubmit=\"hideforms()\">\n\t<div class=\"sec\">\n\t\t<h2>WLED Software Update</h2>\n\t\t<p>\n\t\tInstalled version: <span class=\"sip installed-version\">Loading...</span><br>\n\t\tRelease: <span class=\"sip release-name\">Loading...</span><br>\n\t\t<span id=\"Norelease-download\">Latest binary: Checking...<br></span>\n\t\t<span id=\"release-download\" class=\"hide\">\n\t\tDownload the latest binary: <a class=\"release-repo\" href=\"https://github.com/wled/WLED/releases/latest\" target=\"_blank\" rel=\"noopener noreferrer\"\n\t\tstyle=\"vertical-align: text-bottom; display: inline-flex;\">\n\t\t<img class=\"release-badge\" alt=\"badge\"></a><br> <!-- start with an empty placeholder, src will be filled after fetching /json/info -->\n\t\t</span>\n\t\t</p>\n\t\t<input type=\"hidden\" name=\"skipValidation\" value=\"\" id=\"sV\">\n\t\t<input type='file' name='update' required><br> <!--should have accept='.bin', but it prevents file upload from android app-->\n\t\t<input type='checkbox' onchange=\"sV.value=checked?1:''\" id=\"skipValidation\">\n\t\t<label for='skipValidation'>Ignore firmware validation</label><br>\n\t\t<button type=\"submit\">Update WLED!</button><br>\n\t\t<span id=\"rev\">\n\t\t\t<hr class=\"sml\">\n\t\t\t<button type=\"button\" onclick=\"cR()\">Revert update</button><br>\n\t\t</span>\n\t</div>\n\t</form>\n\t<div id=\"bootupd\" class=\"hide sec\">\n\t\t<h2>Bootloader Update</h2>\n\t\t<div id=\"bootloader-hash\" class=\"sip\" style=\"margin-bottom:8px;font-size:15px;white-space:pre-wrap;word-break:break-all;\"></div>\n\t\t<form method='POST' action='./updatebootloader' enctype='multipart/form-data' onsubmit=\"hideforms()\">\n\t\t\t<b>Warning:</b> Only upload verified ESP32 bootloader files!<br><br>\n\t\t\t<input type='file' name='update' required><br>\n\t\t\t<button type=\"submit\">Update Bootloader</button>\n\t\t</form>\n\t</div>\n\t<div id=\"Noupd\" class=\"hide sec\"><b>Updating...</b><br>Please do not close or refresh the page :)</div>\n\t<p><button id=\"backbtn\" type=\"button\" onclick=\"B()\">Back</button></p>\n</body>\n</html>"
  },
  {
    "path": "wled00/data/usermod.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<body>No usermod custom web page set.</body>\n\n</html>"
  },
  {
    "path": "wled00/data/welcome.htm",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<meta content='width=device-width' name='viewport'>\n\t\t<meta name=\"theme-color\" content=\"#222222\">\n\t\t<title>Welcome!</title>\n\t\t<style>\n\t\t\tbody {\n\t\t\t\tfont-family: Verdana, Helvetica, sans-serif;\n\t\t\t\ttext-align: center;\n\t\t\t\tbackground-color: #222;\n\t\t\t\tmargin: 0;\n\t\t\t\tcolor: #fff;\n\t\t\t}\n\t\n\t\t\tbutton {\n\t\t\t\toutline: none;\n\t\t\t\tcursor: pointer;\n\t\t\t\tpadding: 8px;\n\t\t\t\tmargin: 10px;\n\t\t\t\twidth: 230px;\n\t\t\t\ttext-transform: uppercase;\n\t\t\t\tfont-family: helvetica;\n\t\t\t\tfont-size: 19px;\n\t\t\t\tbackground-color: #333;\n\t\t\t\tcolor: white;\n\t\t\t\tborder: 0px solid white;\n\t\t\t\tborder-radius: 25px;\n\t\t\t}\n\n\t\t\timg {\n\t\t\t\twidth: 950px;\n\t\t\t\tmax-width: 82%;\n\t\t\t\timage-rendering: pixelated;\n\t\t\t\timage-rendering: crisp-edges;\n\t\t\t\tmargin: 4vh 0 0 0;\n\t\t\t\tanimation: fi 1s;\n\t\t\t}\n\t\t\t\n\t\t\t@keyframes fi {\n\t\t\t\tfrom { opacity: 0; }\n\t\t\t\tto   { opacity: 1; }\n\t\t\t}\n\t\n\t\t\t.main {\n\t\t\t\tanimation: fi 1.5s .7s both;\n\t\t\t}\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<img alt=\"\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAG0AAAAfCAMAAADazLOuAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAABLUExURQAAAAB81gCU/zKq///mo7sWMN8bO+ZIYtZaAP9rAP+HMsCiG+TAIOnMS0KqNU7KPnLUZOrq6v///4CAgGhoaL+/v6CgoExMTAAAAAlm4O8AAAAZdFJOU////////////////////////////////wABNAq3AAAACXBIWXMAAA7DAAAOwwHHb6hkAAACN0lEQVRIS73VjVLCMBAEYIr8CYKkrdj3f1J37zaXFCpTO+piaDgbPq9px9VQ0qyrvKj4q6m0Zr1h+M7xF1zRmnWzqV9/0d2jttGotO1uv9dUObwej5oqp7fzWVPl8n69aprzoOUUbbvdIbV3OLwitXc6vSG1d7m8I3feSEN0j2CeNbOY4MxigjOLCc4sZsTV2l1cCyy4wIILLLjAxtykltq2rbTU+qi01N5rXNO2leaFORoija2l5MM5a02ac9Ya16Sk5tgaPrUpjZub0BL6YqSxKwbH77XUUmSkJXSl8QtaMuyJhq5maL5nTKVpZC13VmtMpTFT2g4vJjTuGfMzzXftiUZnhdtgb1xofvypRon5TjNnxYN9zJo6K5ruSIzQtGuVZn0x91rKvdHBvm39E7SyZ4y06Gz8BDBFKzsXmhcwyfsGZ9VpbhoiCinaxPNmGWmWWrNU2jB0q6HvOhN1JUtCixQtp2g51ZVUXIPS2RMAD++T2nY/DrDjOMDO4wC7jmNYj3d73nrXug8Yt9uNB8xNU1cKNXWlUFNXCjV1pZhGTE83m2vWfYf/NGj4Bg1zu5JD3/MnH5ZWfLOksbmGWGjgXMN5/C2GXYGFFW9Nmtle6Xut0Gm+JsayCj8z0nhjGvYJzVf4aSzmNYsr+u7Q2JIdoX3YOQjOslmsW1jJ3120nE9gfo79hTaNdcsqVR610lvO47pllae9ReZ805zKo2a3iaY5c75pTmVCA6dJ5H7N0sr/asPwBehb7ifEhusRAAAAAElFTkSuQmCC\">\n\t\t<div class=\"main\">\n\t\t\t<h1>Welcome to WLED!</h1>\n\t\t\t<h3>A versatile tool for controlling LEDs</h3>\n\t\t\t<h4>Find out more at wled.me</h4>\n\t\t\t<b>Next steps:</b><br><br>\n\t\t\tConnect to your local WiFi here!<br>\n\t\t\t<button onclick=\"window.location.href='./settings/wifi'\">WiFi settings</button><br>\n\t\t\t<i>Just trying this out in AP mode?</i><br>\n\t\t\t<button onclick=\"window.location.href='./?sliders'\">To the controls!</button><br>\n\t\t</div>\n\t</body>\n</html>"
  },
  {
    "path": "wled00/dmx_input.cpp",
    "content": "#include \"wled.h\"\n\n#ifdef WLED_ENABLE_DMX_INPUT\n\n#ifdef ESP8266\n#error DMX input is only supported on ESP32\n#endif\n\n#include \"dmx_input.h\"\n#include <rdm/responder.h>\n\nvoid rdmPersonalityChangedCb(dmx_port_t dmxPort, const rdm_header_t *header,\n                             void *context)\n{\n  DMXInput *dmx = static_cast<DMXInput *>(context);\n\n  if (!dmx) {\n    DEBUG_PRINTLN(\"DMX: Error: no context in rdmPersonalityChangedCb\");\n    return;\n  }\n\n  if (header->cc == RDM_CC_SET_COMMAND_RESPONSE) {\n    const uint8_t personality = dmx_get_current_personality(dmx->inputPortNum);\n    DMXMode = std::min(DMX_MODE_PRESET, std::max(DMX_MODE_SINGLE_RGB, int(personality)));\n    configNeedsWrite = true;\n    DEBUG_PRINTF(\"DMX personality changed to to: %d\\n\", DMXMode);\n  }\n}\n\nvoid rdmAddressChangedCb(dmx_port_t dmxPort, const rdm_header_t *header,\n                         void *context)\n{\n  DMXInput *dmx = static_cast<DMXInput *>(context);\n\n  if (!dmx) {\n    DEBUG_PRINTLN(\"DMX: Error: no context in rdmAddressChangedCb\");\n    return;\n  }\n\n  if (header->cc == RDM_CC_SET_COMMAND_RESPONSE) {\n    const uint16_t addr = dmx_get_start_address(dmx->inputPortNum);\n    DMXAddress = std::min(512, int(addr));\n    configNeedsWrite = true;\n    DEBUG_PRINTF(\"DMX start addr changed to: %d\\n\", DMXAddress);\n  }\n}\n\nstatic dmx_config_t createConfig()\n{\n  dmx_config_t config;\n  config.pd_size = 255;\n  config.dmx_start_address = DMXAddress;\n  config.model_id = 0;\n  config.product_category = RDM_PRODUCT_CATEGORY_FIXTURE;\n  config.software_version_id = VERSION;\n  strcpy(config.device_label, \"WLED_MM\");\n\n  const std::string dmxWledVersionString = \"WLED_V\" + std::to_string(VERSION);\n  strncpy(config.software_version_label, dmxWledVersionString.c_str(), 32);\n  config.software_version_label[32] = '\\0'; // zero termination in case versionString string was longer than 32 chars\n\n  config.personalities[0].description = \"SINGLE_RGB\";\n  config.personalities[0].footprint = 3;\n  config.personalities[1].description = \"SINGLE_DRGB\";\n  config.personalities[1].footprint = 4;\n  config.personalities[2].description = \"EFFECT\";\n  config.personalities[2].footprint = 15;\n  config.personalities[3].description = \"MULTIPLE_RGB\";\n  config.personalities[3].footprint = std::min(512, int(strip.getLengthTotal()) * 3);\n  config.personalities[4].description = \"MULTIPLE_DRGB\";\n  config.personalities[4].footprint = std::min(512, int(strip.getLengthTotal()) * 3 + 1);\n  config.personalities[5].description = \"MULTIPLE_RGBW\";\n  config.personalities[5].footprint = std::min(512, int(strip.getLengthTotal()) * 4);\n  config.personalities[6].description = \"EFFECT_W\";\n  config.personalities[6].footprint = 18;\n  config.personalities[7].description = \"EFFECT_SEGMENT\";\n  config.personalities[7].footprint = std::min(512, strip.getSegmentsNum() * 15);\n  config.personalities[8].description = \"EFFECT_SEGMENT_W\";\n  config.personalities[8].footprint = std::min(512, strip.getSegmentsNum() * 18);\n  config.personalities[9].description = \"PRESET\";\n  config.personalities[9].footprint = 1;\n\n  config.personality_count = 10;\n  // rdm personalities are numbered from 1, thus we can just set the DMXMode directly.\n  config.current_personality = DMXMode;\n\n  return config;\n}\n\nvoid dmxReceiverTask(void *context)\n{\n  DMXInput *instance = static_cast<DMXInput *>(context);\n  if (instance == nullptr) {\n    return;\n  }\n\n  if (instance->installDriver()) {\n    while (true) {\n      instance->updateInternal();\n    }\n  }\n}\n\nbool DMXInput::installDriver()\n{\n\n  const auto config = createConfig();\n  DEBUG_PRINTF(\"DMX port: %u\\n\", inputPortNum);\n  if (!dmx_driver_install(inputPortNum, &config, DMX_INTR_FLAGS_DEFAULT)) {\n    DEBUG_PRINTF(\"Error: Failed to install dmx driver\\n\");\n    return false;\n  }\n\n  DEBUG_PRINTF(\"Listening for DMX on pin %u\\n\", rxPin);\n  DEBUG_PRINTF(\"Sending DMX on pin %u\\n\", txPin);\n  DEBUG_PRINTF(\"DMX enable pin is: %u\\n\", enPin);\n  dmx_set_pin(inputPortNum, txPin, rxPin, enPin);\n\n  rdm_register_dmx_start_address(inputPortNum, rdmAddressChangedCb, this);\n  rdm_register_dmx_personality(inputPortNum, rdmPersonalityChangedCb, this);\n  initialized = true;\n  return true;\n}\n\nvoid DMXInput::init(uint8_t rxPin, uint8_t txPin, uint8_t enPin, uint8_t inputPortNum)\n{\n\n#ifdef WLED_ENABLE_DMX_OUTPUT\n  //TODO add again once dmx output has been merged\n  // if(inputPortNum == dmxOutputPort)\n  // {\n  //   DEBUG_PRINTF(\"DMXInput: Error: Input port == output port\");\n  //   return;\n  // }\n#endif\n\n  if (inputPortNum <= (SOC_UART_NUM - 1) && inputPortNum > 0) {\n    this->inputPortNum = inputPortNum;\n  }\n  else {\n    DEBUG_PRINTF(\"DMXInput: Error: invalid inputPortNum: %d\\n\", inputPortNum);\n    return;\n  }\n\n  if (rxPin > 0 && enPin > 0 && txPin > 0) {\n\n    const managed_pin_type pins[] = {\n        {(int8_t)txPin, false}, // these are not used as gpio pins, thus isOutput is always false.\n        {(int8_t)rxPin, false},\n        {(int8_t)enPin, false}};\n    const bool pinsAllocated = PinManager::allocateMultiplePins(pins, 3, PinOwner::DMX_INPUT);\n    if (!pinsAllocated) {\n      DEBUG_PRINTF(\"DMXInput: Error: Failed to allocate pins for DMX_INPUT. Pins already in use:\\n\");\n      DEBUG_PRINTF(\"rx in use by: %s\\n\", PinManager::getPinOwner(rxPin));\n      DEBUG_PRINTF(\"tx in use by: %s\\n\", PinManager::getPinOwner(txPin));\n      DEBUG_PRINTF(\"en in use by: %s\\n\", PinManager::getPinOwner(enPin));\n      return;\n    }\n\n    this->rxPin = rxPin;\n    this->txPin = txPin;\n    this->enPin = enPin;\n\n    // put dmx receiver into seperate task because it should not be blocked\n    // pin to core 0 because wled is running on core 1\n    xTaskCreatePinnedToCore(dmxReceiverTask, \"DMX_RCV_TASK\", 10240, this, 2, &task, 0);\n    if (!task) {\n      DEBUG_PRINTF(\"Error: Failed to create dmx rcv task\");\n    }\n  }\n  else {\n    DEBUG_PRINTLN(\"DMX input disabled due to rxPin, enPin or txPin not set\");\n    return;\n  }\n}\n\nvoid DMXInput::updateInternal()\n{\n  if (!initialized) {\n    return;\n  }\n\n  checkAndUpdateConfig();\n\n  dmx_packet_t packet;\n  unsigned long now = millis();\n  if (dmx_receive(inputPortNum, &packet, DMX_TIMEOUT_TICK)) {\n    if (!packet.err) {\n      if(!connected) {\n        DEBUG_PRINTLN(\"DMX Input - connected\");\n      }\n      connected = true;\n      identify = isIdentifyOn();\n      if (!packet.is_rdm) {\n        const std::lock_guard<std::mutex> lock(dmxDataLock);\n        dmx_read(inputPortNum, dmxdata, packet.size);\n      }\n    }\n    else {\n      connected = false;\n    }\n  }\n  else {\n    if(connected) {\n      DEBUG_PRINTLN(\"DMX Input - disconnected\");\n    }\n    connected = false;\n  }\n}\n\n\nvoid DMXInput::update()\n{\n  if (identify) {\n    turnOnAllLeds();\n  }\n  else if (connected) {\n    const std::lock_guard<std::mutex> lock(dmxDataLock);\n    handleDMXData(1, 512, dmxdata, REALTIME_MODE_DMX, 0);\n  }\n}\n\nvoid DMXInput::turnOnAllLeds()\n{\n  // TODO not sure if this is the correct way?\n  const uint16_t numPixels = strip.getLengthTotal();\n  for (uint16_t i = 0; i < numPixels; ++i)\n  {\n    strip.setPixelColor(i, 255, 255, 255, 255);\n  }\n  strip.setBrightness(255, true);\n  strip.show();\n}\n\nvoid DMXInput::disable()\n{\n  if (initialized) {\n    dmx_driver_disable(inputPortNum);\n  }\n}\nvoid DMXInput::enable()\n{\n  if (initialized) {\n    dmx_driver_enable(inputPortNum);\n  }\n}\n\nbool DMXInput::isIdentifyOn() const\n{\n\n  uint8_t identify = 0;\n  const bool gotIdentify = rdm_get_identify_device(inputPortNum, &identify);\n  // gotIdentify should never be false because it is a default parameter in rdm\n  // but just in case we check for it anyway\n  return bool(identify) && gotIdentify;\n}\n\nvoid DMXInput::checkAndUpdateConfig()\n{\n\n  /**\n   * The global configuration variables are modified by the web interface.\n   * If they differ from the driver configuration, we have to update the driver\n   * configuration.\n   */\n\n  const uint8_t currentPersonality = dmx_get_current_personality(inputPortNum);\n  if (currentPersonality != DMXMode) {\n    DEBUG_PRINTF(\"DMX personality has changed from %d to %d\\n\", currentPersonality, DMXMode);\n    dmx_set_current_personality(inputPortNum, DMXMode);\n  }\n\n  const uint16_t currentAddr = dmx_get_start_address(inputPortNum);\n  if (currentAddr != DMXAddress) {\n    DEBUG_PRINTF(\"DMX address has changed from %d to %d\\n\", currentAddr, DMXAddress);\n    dmx_set_start_address(inputPortNum, DMXAddress);\n  }\n}\n\n#endif"
  },
  {
    "path": "wled00/dmx_input.h",
    "content": "#pragma once\n#include <cstdint>\n#include <esp_dmx.h>\n#include <atomic>\n#include <mutex>\n\n/*\n * Support for DMX/RDM input via serial (e.g. max485) on ESP32\n * ESP32 Library from:\n * https://github.com/someweisguy/esp_dmx\n */\nclass DMXInput\n{\npublic:\n  void init(uint8_t rxPin, uint8_t txPin, uint8_t enPin, uint8_t inputPortNum);\n  void update();\n\n  /**disable dmx receiver (do this before disabling the cache)*/\n  void disable();\n  void enable();\n\n  /// True if dmx is currently connected\n  bool isConnected() const { return connected; }\n\nprivate:\n  /// @return true if rdm identify is active\n  bool isIdentifyOn() const;\n\n  /**\n   * Checks if the global dmx config has changed and updates the changes in rdm\n   */\n  void checkAndUpdateConfig();\n\n  /// overrides everything and turns on all leds\n  void turnOnAllLeds();\n\n  /// installs the dmx driver\n  /// @return false on fail\n  bool installDriver();\n\n  /// is called by the dmx receive task regularly to receive new dmx data\n  void updateInternal();\n\n  // is invoked whenver the dmx start address is changed via rdm\n  friend void rdmAddressChangedCb(dmx_port_t dmxPort, const rdm_header_t *header,\n                                  void *context);\n\n  // is invoked whenever the personality is changed via rdm\n  friend void rdmPersonalityChangedCb(dmx_port_t dmxPort, const rdm_header_t *header,\n                                      void *context);\n\n  /// The internal dmx task.\n  /// This is the main loop of the dmx receiver. It never returns.\n  friend void dmxReceiverTask(void * context);\n\n  uint8_t inputPortNum = 255; \n  uint8_t rxPin = 255;\n  uint8_t txPin = 255;\n  uint8_t enPin = 255;\n\n  /// is written to by the dmx receive task.\n  byte dmxdata[DMX_PACKET_SIZE]; \n  /// True once the dmx input has been initialized successfully\n  bool initialized = false; // true once init finished successfully\n  /// True if dmx is currently connected\n  std::atomic<bool> connected{false};\n  std::atomic<bool> identify{false};\n  /// Timestamp of the last time a dmx frame was received\n  unsigned long lastUpdate = 0;\n\n  /// Taskhandle of the dmx task that is running in the background \n  TaskHandle_t task;\n  /// Guards access to dmxData\n  std::mutex dmxDataLock;\n  \n};\n"
  },
  {
    "path": "wled00/dmx_output.cpp",
    "content": "#include \"wled.h\"\n\n/*\n * Support for DMX  output via serial (e.g. MAX485).\n * Change the output pin in src/dependencies/ESPDMX.cpp, if needed (ESP8266)\n * Change the output pin in src/dependencies/SparkFunDMX.cpp, if needed (ESP32)\n * ESP8266 Library from:\n * https://github.com/Rickgg/ESP-Dmx\n * ESP32 Library from:\n * https://github.com/sparkfun/SparkFunDMX\n */\n\n#ifdef WLED_ENABLE_DMX\n\nvoid handleDMXOutput()\n{\n  // don't act, when in DMX Proxy mode\n  if (e131ProxyUniverse != 0) return;\n\n  uint8_t brightness = strip.getBrightness();\n\n  bool calc_brightness = true;\n\n   // check if no shutter channel is set\n   for (unsigned i = 0; i < DMXChannels; i++)\n   {\n     if (DMXFixtureMap[i] == 5) calc_brightness = false;\n   }\n\n  uint16_t len = strip.getLengthTotal();\n  for (int i = DMXStartLED; i < len; i++) {        // uses the amount of LEDs as fixture count\n\n    uint32_t in = strip.getPixelColor(i);     // get the colors for the individual fixtures as suggested by Aircoookie in issue #462\n    byte w = W(in);\n    byte r = R(in);\n    byte g = G(in);\n    byte b = B(in);\n\n    int DMXFixtureStart = DMXStart + (DMXGap * (i - DMXStartLED));\n    for (int j = 0; j < DMXChannels; j++) {\n      int DMXAddr = DMXFixtureStart + j;\n      switch (DMXFixtureMap[j]) {\n        case 0:        // Set this channel to 0. Good way to tell strobe- and fade-functions to fuck right off.\n          dmx.write(DMXAddr, 0);\n          break;\n        case 1:        // Red\n          dmx.write(DMXAddr, calc_brightness ? (r * brightness) / 255 : r);\n          break;\n        case 2:        // Green\n          dmx.write(DMXAddr, calc_brightness ? (g * brightness) / 255 : g);\n          break;\n        case 3:        // Blue\n          dmx.write(DMXAddr, calc_brightness ? (b * brightness) / 255 : b);\n          break;\n        case 4:        // White\n          dmx.write(DMXAddr, calc_brightness ? (w * brightness) / 255 : w);\n          break;\n        case 5:        // Shutter channel. Controls the brightness.\n          dmx.write(DMXAddr, brightness);\n          break;\n        case 6:        // Sets this channel to 255. Like 0, but more wholesome.\n          dmx.write(DMXAddr, 255);\n          break;\n      }\n    }\n  }\n\n  dmx.update();        // update the DMX bus\n}\n\nvoid initDMXOutput() {\n #if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2)\n  dmx.init(512);        // initialize with bus length\n #else\n  dmx.initWrite(512);  // initialize with bus length\n #endif\n}\n#else\nvoid initDMXOutput(){}\nvoid handleDMXOutput() {}\n#endif\n"
  },
  {
    "path": "wled00/dynarray.h",
    "content": "/* dynarray.h\n\nMacros for generating a \"dynamic array\", a static array of objects declared in different translation units\n\n*/\n\n#pragma once\n\n// Declare the beginning and ending elements of a dynamic array of 'type'.\n// This must be used in only one translation unit in your program for any given array.\n#define DECLARE_DYNARRAY(type, array_name) \\\n  static type const DYNARRAY_BEGIN(array_name)[0] __attribute__((__section__(DYNARRAY_SECTION \".\" #array_name \".0\"), unused)) = {}; \\\n  static type const DYNARRAY_END(array_name)[0] __attribute__((__section__(DYNARRAY_SECTION \".\" #array_name \".99999\"), unused)) = {};\n\n// Declare an object that is a member of a dynamic array.  \"member name\" must be unique; \"array_section\" is an integer for ordering items.\n// It is legal to define multiple items with the same section name; the order of those items will be up to the linker.\n#define DYNARRAY_MEMBER(type, array_name, member_name, array_section) type const member_name __attribute__((__section__(DYNARRAY_SECTION \".\" #array_name \".\" #array_section), used))\n\n#define DYNARRAY_BEGIN(array_name) array_name##_begin\n#define DYNARRAY_END(array_name) array_name##_end\n#define DYNARRAY_LENGTH(array_name) (&DYNARRAY_END(array_name)[0] - &DYNARRAY_BEGIN(array_name)[0])\n\n#ifdef ESP8266\n// ESP8266 linker script cannot be extended with a unique section for dynamic arrays.\n// We instead pack them in the \".dtors\" section, as it's sorted and uploaded to the flash\n// (but will never be used in the embedded system)\n#define DYNARRAY_SECTION \".dtors\"\n\n#else /* ESP8266 */\n\n// Use a unique named section; the linker script must be extended to ensure it's correctly placed.\n#define DYNARRAY_SECTION \".dynarray\"\n\n#endif\n"
  },
  {
    "path": "wled00/e131.cpp",
    "content": "#include \"wled.h\"\n\n#define MAX_3_CH_LEDS_PER_UNIVERSE 170\n#define MAX_4_CH_LEDS_PER_UNIVERSE 128\n#define MAX_CHANNELS_PER_UNIVERSE 512\n\n// forward declarations\nstatic void handleDDPPacket(e131_packet_t* p);\nstatic void handleArtnetPollReply(IPAddress ipAddress);\nstatic void prepareArtnetPollReply(ArtPollReply *reply);\nstatic void sendArtnetPollReply(ArtPollReply *reply, IPAddress ipAddress, uint16_t portAddress);\n\n\n/*\n * E1.31 handler\n */\n\n//DDP protocol support, called by handleE131Packet\n//handles RGB data only\nstatic void handleDDPPacket(e131_packet_t* p) {\n  static bool ddpSeenPush = false;  // have we seen a push yet?\n  int lastPushSeq = e131LastSequenceNumber[0];\n\n  //reject late packets belonging to previous frame (assuming 4 packets max. before push)\n  if (e131SkipOutOfSequence && lastPushSeq) {\n    int sn = p->sequenceNum & 0xF;\n    if (sn) {\n      if (lastPushSeq > 5) {\n        if (sn > (lastPushSeq -5) && sn < lastPushSeq) return;\n      } else {\n        if (sn > (10 + lastPushSeq) || sn < lastPushSeq) return;\n      }\n    }\n  }\n\n  unsigned ddpChannelsPerLed = ((p->dataType & 0b00111000)>>3 == 0b011) ? 4 : 3; // data type 0x1B (formerly 0x1A) is RGBW (type 3, 8 bit/channel)\n\n  uint32_t start =  htonl(p->channelOffset) / ddpChannelsPerLed;\n  start += DMXAddress / ddpChannelsPerLed;\n  uint16_t dataLen = htons(p->dataLen);\n  unsigned stop = start + dataLen / ddpChannelsPerLed;\n  uint8_t* data = p->data;\n  unsigned c = 0;\n  if (p->flags & DDP_TIMECODE_FLAG) c = 4; //packet has timecode flag, we do not support it, but data starts 4 bytes later\n\n  unsigned numLeds = stop - start; // stop >= start is guaranteed\n  unsigned maxDataIndex = c + numLeds * ddpChannelsPerLed; // validate bounds before accessing data array\n  if (maxDataIndex > dataLen) {\n    DEBUG_PRINTLN(F(\"DDP packet data bounds exceeded, rejecting.\"));\n    return;\n  }\n\n  if (realtimeMode != REALTIME_MODE_DDP) ddpSeenPush = false; // just starting, no push yet\n  realtimeLock(realtimeTimeoutMs, REALTIME_MODE_DDP);\n\n  if (!realtimeOverride) {\n    for (unsigned i = start; i < stop; i++, c += ddpChannelsPerLed) {\n      setRealtimePixel(i, data[c], data[c+1], data[c+2], ddpChannelsPerLed >3 ? data[c+3] : 0);\n    }\n  }\n\n  bool push = p->flags & DDP_PUSH_FLAG;\n  ddpSeenPush |= push;\n  if (!ddpSeenPush || push) { // if we've never seen a push, or this is one, render display\n    e131NewData = true;\n    int sn = p->sequenceNum & 0xF;\n    if (sn) e131LastSequenceNumber[0] = sn;\n  }\n}\n\n//E1.31 and Art-Net protocol support\nvoid handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){\n\n  int uni = 0, dmxChannels = 0;\n  uint8_t* e131_data = nullptr;\n  int seq = 0, mde = REALTIME_MODE_E131;\n\n  if (protocol == P_ARTNET)\n  {\n    if (p->art_opcode == ARTNET_OPCODE_OPPOLL) {\n      handleArtnetPollReply(clientIP);\n      return;\n    }\n    uni = p->art_universe;\n    dmxChannels = htons(p->art_length);\n    e131_data = p->art_data;\n    seq = p->art_sequence_number;\n    mde = REALTIME_MODE_ARTNET;\n  } else if (protocol == P_E131) {\n    // Ignore PREVIEW data (E1.31: 6.2.6)\n    if ((p->options & 0x80) != 0) return;\n    dmxChannels = htons(p->property_value_count) - 1;\n    // DMX level data is zero start code. Ignore everything else. (E1.11: 8.5)\n    if (dmxChannels == 0 || p->property_values[0] != 0) return;\n    uni = htons(p->universe);\n    e131_data = p->property_values;\n    seq = p->sequence_number;\n    if (e131Priority != 0) {\n      if (p->priority < e131Priority ) return;\n      // track highest priority & skip all lower priorities\n      if (p->priority >= highPriority.get()) highPriority.set(p->priority);\n      if (p->priority < highPriority.get()) return;\n    }\n  } else { //DDP\n    realtimeIP = clientIP;\n    handleDDPPacket(p);\n    return;\n  }\n\n  #ifdef WLED_ENABLE_DMX\n  // does not act on out-of-order packets yet\n  if (e131ProxyUniverse > 0 && uni == e131ProxyUniverse) {\n    for (uint16_t i = 1; i <= dmxChannels; i++)\n      dmx.write(i, e131_data[i]);\n    dmx.update();\n  }\n  #endif\n\n  // only listen for universes we're handling & allocated memory\n  if (uni < e131Universe || uni >= (e131Universe + E131_MAX_UNIVERSE_COUNT)) return;\n\n  unsigned previousUniverses = uni - e131Universe;\n\n  if (e131SkipOutOfSequence)\n    if (seq < e131LastSequenceNumber[previousUniverses] && seq > 20 && e131LastSequenceNumber[previousUniverses] < 250){\n      DEBUG_PRINTF_P(PSTR(\"skipping E1.31 frame (last seq=%d, current seq=%d, universe=%d)\\n\"), e131LastSequenceNumber[previousUniverses], seq, uni);\n      return;\n    }\n  e131LastSequenceNumber[previousUniverses] = seq;\n\n  // update status info\n  realtimeIP = clientIP;\n\n  handleDMXData(uni, dmxChannels, e131_data, mde, previousUniverses);\n}\n\nvoid handleDMXData(uint16_t uni, uint16_t dmxChannels, uint8_t* e131_data, uint8_t mde, uint8_t previousUniverses) {\n  byte wChannel = 0;\n  unsigned totalLen = strip.getLengthTotal();\n  unsigned availDMXLen = 0;\n  unsigned dataOffset = DMXAddress;\n\n  // For legacy DMX start address 0 the available DMX length offset is 0\n  const unsigned dmxLenOffset = (DMXAddress == 0) ? 0 : 1;\n\n  // Check if DMX start address fits in available channels\n  if (dmxChannels >= DMXAddress) {\n    availDMXLen = (dmxChannels - DMXAddress) + dmxLenOffset;\n  }\n\n  // DMX data in Art-Net packet starts at index 0, for E1.31 at index 1\n  if (mde == REALTIME_MODE_ARTNET && dataOffset > 0) {\n    dataOffset--;\n  }\n\n  switch (DMXMode) {\n    case DMX_MODE_DISABLED:\n      return;  // nothing to do\n      break;\n\n    case DMX_MODE_SINGLE_RGB:   // 3 channel: [R,G,B]\n      if (uni != e131Universe) return;\n      if (availDMXLen < 3) return;\n\n      realtimeLock(realtimeTimeoutMs, mde);\n\n      if (realtimeOverride) return;\n\n      wChannel = (availDMXLen > 3) ? e131_data[dataOffset+3] : 0;\n      for (unsigned i = 0; i < totalLen; i++)\n        setRealtimePixel(i, e131_data[dataOffset+0], e131_data[dataOffset+1], e131_data[dataOffset+2], wChannel);\n      break;\n\n    case DMX_MODE_SINGLE_DRGB:  // 4 channel: [Dimmer,R,G,B]\n      if (uni != e131Universe) return;\n      if (availDMXLen < 4) return;\n\n      realtimeLock(realtimeTimeoutMs, mde);\n      if (realtimeOverride) return;\n      wChannel = (availDMXLen > 4) ? e131_data[dataOffset+4] : 0;\n\n      if (bri != e131_data[dataOffset+0]) {\n        bri = e131_data[dataOffset+0];\n        strip.setBrightness(bri, true);\n      }\n\n      for (unsigned i = 0; i < totalLen; i++)\n        setRealtimePixel(i, e131_data[dataOffset+1], e131_data[dataOffset+2], e131_data[dataOffset+3], wChannel);\n      break;\n\n    case DMX_MODE_PRESET:       // 2 channel: [Dimmer,Preset]\n      {\n        if (uni != e131Universe || availDMXLen < 2) return;\n\n        // limit max. selectable preset to 250, even though DMX max. val is 255\n        int dmxValPreset = (e131_data[dataOffset+1] > 250 ? 250 : e131_data[dataOffset+1]);\n        \n        // only apply preset if value changed \n        if (dmxValPreset != 0 && dmxValPreset != currentPreset &&  \n            // only apply preset if not in playlist, or playlist changed\n            (currentPlaylist < 0 || dmxValPreset != currentPlaylist)) { \n          presetCycCurr = dmxValPreset;\n          applyPreset(dmxValPreset, CALL_MODE_NOTIFICATION);\n        }\n\n        // only change brightness if value changed\n        if (bri != e131_data[dataOffset]) {                                        \n          bri = e131_data[dataOffset];\n          strip.setBrightness(bri, false);\n          stateUpdated(CALL_MODE_WS_SEND);\n        }\n        return;\n        break;\n      }\n\n    case DMX_MODE_EFFECT:           // 15 channels [bri,effectCurrent,effectSpeed,effectIntensity,effectPalette,effectOption,R,G,B,R2,G2,B2,R3,G3,B3]\n    case DMX_MODE_EFFECT_W:         // 18 channels, same as above but with extra +3 white channels [..,W,W2,W3]\n    case DMX_MODE_EFFECT_SEGMENT:   // 15 channels per segment;\n    case DMX_MODE_EFFECT_SEGMENT_W: // 18 Channels per segment;\n      {\n        if (uni != e131Universe) return;\n        bool isSegmentMode = DMXMode == DMX_MODE_EFFECT_SEGMENT || DMXMode == DMX_MODE_EFFECT_SEGMENT_W;\n        unsigned dmxEffectChannels = (DMXMode == DMX_MODE_EFFECT || DMXMode == DMX_MODE_EFFECT_SEGMENT) ? 15 : 18;\n        for (unsigned id = 0; id < strip.getSegmentsNum(); id++) {\n          Segment& seg = strip.getSegment(id);\n          if (isSegmentMode)\n            dataOffset = DMXAddress + id * (dmxEffectChannels + DMXSegmentSpacing);\n          else\n            dataOffset = DMXAddress;\n          // Modify address for Art-Net data\n          if (mde == REALTIME_MODE_ARTNET && dataOffset > 0)\n            dataOffset--;\n          // Skip out of universe addresses\n          if (dataOffset > dmxChannels - dmxEffectChannels + 1)\n            return;\n\n          if (e131_data[dataOffset+1] < strip.getModeCount())\n            if (e131_data[dataOffset+1] != seg.mode)      seg.setMode(   e131_data[dataOffset+1]);\n          if (e131_data[dataOffset+2]   != seg.speed)     seg.speed     = e131_data[dataOffset+2];      \n          if (e131_data[dataOffset+3]   != seg.intensity) seg.intensity = e131_data[dataOffset+3];\n          if (e131_data[dataOffset+4]   != seg.palette)   seg.setPalette(e131_data[dataOffset+4]);\n\n          if (bool(e131_data[dataOffset+5] & 0b00000010) != seg.reverse_y) { seg.reverse_y = bool(e131_data[dataOffset+5] & 0b00000010); }\n          if (bool(e131_data[dataOffset+5] & 0b00000100) != seg.mirror_y)  { seg.mirror_y  = bool(e131_data[dataOffset+5] & 0b00000100); }\n          if (bool(e131_data[dataOffset+5] & 0b00001000) != seg.transpose) { seg.transpose = bool(e131_data[dataOffset+5] & 0b00001000); }\n          if ((e131_data[dataOffset+5] & 0b00110000) >> 4 != seg.map1D2D) {\n            seg.map1D2D = (e131_data[dataOffset+5] & 0b00110000) >> 4;\n          }\n          // To maintain backwards compatibility with prior e1.31 values, reverse is fixed to mask 0x01000000\n          if ((e131_data[dataOffset+5] & 0b01000000) != seg.reverse) { seg.reverse = bool(e131_data[dataOffset+5] & 0b01000000); }\n          // To maintain backwards compatibility with prior e1.31 values, mirror is fixed to mask 0x10000000\n          if ((e131_data[dataOffset+5] & 0b10000000) != seg.mirror) { seg.mirror = bool(e131_data[dataOffset+5] & 0b10000000); }\n\n          uint32_t colors[3];\n          byte whites[3] = {0,0,0};\n          if (dmxEffectChannels == 18) {\n            whites[0] = e131_data[dataOffset+15];\n            whites[1] = e131_data[dataOffset+16];\n            whites[2] = e131_data[dataOffset+17];\n          }\n          colors[0] = RGBW32(e131_data[dataOffset+ 6], e131_data[dataOffset+ 7], e131_data[dataOffset+ 8], whites[0]);\n          colors[1] = RGBW32(e131_data[dataOffset+ 9], e131_data[dataOffset+10], e131_data[dataOffset+11], whites[1]);\n          colors[2] = RGBW32(e131_data[dataOffset+12], e131_data[dataOffset+13], e131_data[dataOffset+14], whites[2]);\n          if (colors[0] != seg.colors[0]) seg.setColor(0, colors[0]);\n          if (colors[1] != seg.colors[1]) seg.setColor(1, colors[1]);\n          if (colors[2] != seg.colors[2]) seg.setColor(2, colors[2]);\n\n          // Set segment opacity or global brightness\n          if (isSegmentMode) {\n            if (e131_data[dataOffset] != seg.opacity) seg.setOpacity(e131_data[dataOffset]);\n          } else if ( id == strip.getSegmentsNum()-1U ) {\n            if (bri != e131_data[dataOffset]) {\n              bri = e131_data[dataOffset];\n              strip.setBrightness(bri, true);\n            }\n          }\n        }\n        return;\n        break;\n      }\n      \n    case DMX_MODE_MULTIPLE_DRGB:\n    case DMX_MODE_MULTIPLE_RGB:\n    case DMX_MODE_MULTIPLE_RGBW:\n      {\n        const bool is4Chan = (DMXMode == DMX_MODE_MULTIPLE_RGBW);\n        const unsigned dmxChannelsPerLed = is4Chan ? 4 : 3;\n        const unsigned ledsPerUniverse = is4Chan ? MAX_4_CH_LEDS_PER_UNIVERSE : MAX_3_CH_LEDS_PER_UNIVERSE;\n        uint8_t stripBrightness = bri;\n        unsigned previousLeds, dmxOffset, ledsTotal;\n\n        if (previousUniverses == 0) {\n          if (availDMXLen < 1) return;\n          dmxOffset = dataOffset;\n          previousLeds = 0;\n          // First DMX address is dimmer in DMX_MODE_MULTIPLE_DRGB mode.\n          if (DMXMode == DMX_MODE_MULTIPLE_DRGB) {\n            stripBrightness = e131_data[dmxOffset++];\n            ledsTotal = (availDMXLen - 1) / dmxChannelsPerLed;\n          } else {\n            ledsTotal = availDMXLen / dmxChannelsPerLed;\n          }\n        } else {\n          // All subsequent universes start at the first channel.\n          dmxOffset = (mde == REALTIME_MODE_ARTNET) ? 0 : 1;\n          const unsigned dimmerOffset = (DMXMode == DMX_MODE_MULTIPLE_DRGB) ? 1 : 0;\n          unsigned ledsInFirstUniverse = (((MAX_CHANNELS_PER_UNIVERSE - DMXAddress) + dmxLenOffset) - dimmerOffset) / dmxChannelsPerLed;\n          previousLeds = ledsInFirstUniverse + (previousUniverses - 1) * ledsPerUniverse;\n          ledsTotal = previousLeds + (dmxChannels / dmxChannelsPerLed);\n        }\n\n        // All LEDs already have values\n        if (previousLeds >= totalLen) {\n          return;\n        }\n\n        realtimeLock(realtimeTimeoutMs, mde);\n        if (realtimeOverride) return;\n\n        if (ledsTotal > totalLen) {\n          ledsTotal = totalLen;\n        }\n\n        if (DMXMode == DMX_MODE_MULTIPLE_DRGB && previousUniverses == 0) {\n          if (bri != stripBrightness) {\n            bri = stripBrightness;\n            strip.setBrightness(bri, true);\n          }\n        }\n\n        for (unsigned i = previousLeds; i < ledsTotal; i++) {\n          setRealtimePixel(i, e131_data[dmxOffset], e131_data[dmxOffset+1], e131_data[dmxOffset+2], is4Chan ? e131_data[dmxOffset+3] : 0);\n          dmxOffset += dmxChannelsPerLed;\n        }\n        break;\n      }\n    default:\n      DEBUG_PRINTLN(F(\"unknown E1.31 DMX mode\"));\n      return;  // nothing to do\n      break;\n  }\n\n  e131NewData = true;\n}\n\nstatic void handleArtnetPollReply(IPAddress ipAddress) {\n  ArtPollReply artnetPollReply;\n  prepareArtnetPollReply(&artnetPollReply);\n\n  unsigned startUniverse = e131Universe;\n  unsigned endUniverse = e131Universe;\n\n  switch (DMXMode) {\n    case DMX_MODE_DISABLED:\n      break;\n\n    case DMX_MODE_SINGLE_RGB:\n    case DMX_MODE_SINGLE_DRGB:\n    case DMX_MODE_PRESET:\n    case DMX_MODE_EFFECT:\n    case DMX_MODE_EFFECT_W:\n    case DMX_MODE_EFFECT_SEGMENT:\n    case DMX_MODE_EFFECT_SEGMENT_W:\n      break;  // 1 universe is enough\n\n    case DMX_MODE_MULTIPLE_DRGB:\n    case DMX_MODE_MULTIPLE_RGB:\n    case DMX_MODE_MULTIPLE_RGBW:\n      {\n        bool is4Chan = (DMXMode == DMX_MODE_MULTIPLE_RGBW);\n        const unsigned dmxChannelsPerLed = is4Chan ? 4 : 3;\n        const unsigned dimmerOffset = (DMXMode == DMX_MODE_MULTIPLE_DRGB) ? 1 : 0;\n        const unsigned dmxLenOffset = (DMXAddress == 0) ? 0 : 1; // For legacy DMX start address 0\n        const unsigned ledsInFirstUniverse = (((MAX_CHANNELS_PER_UNIVERSE - DMXAddress) + dmxLenOffset) - dimmerOffset) / dmxChannelsPerLed;\n        const unsigned totalLen = strip.getLengthTotal();\n\n        if (totalLen > ledsInFirstUniverse) {\n          const unsigned ledsPerUniverse = is4Chan ? MAX_4_CH_LEDS_PER_UNIVERSE : MAX_3_CH_LEDS_PER_UNIVERSE;\n          const unsigned remainLED = totalLen - ledsInFirstUniverse;\n\n          endUniverse += (remainLED / ledsPerUniverse);\n\n          if ((remainLED % ledsPerUniverse) > 0) {\n            endUniverse++;\n          }\n\n          if ((endUniverse - startUniverse) > E131_MAX_UNIVERSE_COUNT) {\n            endUniverse = startUniverse + E131_MAX_UNIVERSE_COUNT - 1;\n          }\n        }\n        break;\n      }\n    default:\n      DEBUG_PRINTLN(F(\"unknown E1.31 DMX mode\"));\n      return;  // nothing to do\n      break;\n  }\n\n  if (DMXMode != DMX_MODE_DISABLED) {\n    for (unsigned i = startUniverse; i <= endUniverse; ++i) {\n      sendArtnetPollReply(&artnetPollReply, ipAddress, i);\n    }\n  }\n\n  #ifdef WLED_ENABLE_DMX\n    if (e131ProxyUniverse > 0 && (DMXMode == DMX_MODE_DISABLED || (e131ProxyUniverse < startUniverse || e131ProxyUniverse > endUniverse))) {\n      sendArtnetPollReply(&artnetPollReply, ipAddress, e131ProxyUniverse);\n    }\n  #endif\n}\n\nstatic void prepareArtnetPollReply(ArtPollReply *reply) {\n  // Art-Net\n  reply->reply_id[0] = 0x41;\n  reply->reply_id[1] = 0x72;\n  reply->reply_id[2] = 0x74;\n  reply->reply_id[3] = 0x2d;\n  reply->reply_id[4] = 0x4e;\n  reply->reply_id[5] = 0x65;\n  reply->reply_id[6] = 0x74;\n  reply->reply_id[7] = 0x00;\n\n  reply->reply_opcode = ARTNET_OPCODE_OPPOLLREPLY;\n\n  IPAddress localIP = Network.localIP();\n  for (unsigned i = 0; i < 4; i++) {\n    reply->reply_ip[i] = localIP[i];\n  }\n\n  reply->reply_port = ARTNET_DEFAULT_PORT;\n\n  char * numberEnd = (char*) versionString; // strtol promises not to try to edit this.\n  reply->reply_version_h = (uint8_t)strtol(numberEnd, &numberEnd, 10);\n  numberEnd++;\n  reply->reply_version_l = (uint8_t)strtol(numberEnd, &numberEnd, 10);\n\n  // Switch values depend on universe, set before sending\n  reply->reply_net_sw = 0x00;\n  reply->reply_sub_sw = 0x00;\n\n  reply->reply_oem_h = 0x00; // TODO add assigned oem code\n  reply->reply_oem_l = 0x00;\n\n  reply->reply_ubea_ver = 0x00;\n\n  // Indicators in Normal Mode\n  // All or part of Port-Address programmed by network or Web browser\n  reply->reply_status_1 = 0xE0;\n\n  reply->reply_esta_man = 0x0000;\n\n  strlcpy((char *)(reply->reply_short_name), serverDescription, 18);\n  strlcpy((char *)(reply->reply_long_name), serverDescription, 64);\n\n  reply->reply_node_report[0] = '\\0';\n\n  reply->reply_num_ports_h = 0x00;\n  reply->reply_num_ports_l = 0x01; // One output port\n\n  reply->reply_port_types[0] = 0x80; // Output DMX data\n  reply->reply_port_types[1] = 0x00;\n  reply->reply_port_types[2] = 0x00;\n  reply->reply_port_types[3] = 0x00;\n\n  // No inputs\n  reply->reply_good_input[0] = 0x00;\n  reply->reply_good_input[1] = 0x00;\n  reply->reply_good_input[2] = 0x00;\n  reply->reply_good_input[3] = 0x00;\n\n  // One output\n  reply->reply_good_output_a[0] = 0x80; // Data is being transmitted\n  reply->reply_good_output_a[1] = 0x00;\n  reply->reply_good_output_a[2] = 0x00;\n  reply->reply_good_output_a[3] = 0x00;\n\n  // Values depend on universe, set before sending\n  reply->reply_sw_in[0] = 0x00;\n  reply->reply_sw_in[1] = 0x00;\n  reply->reply_sw_in[2] = 0x00;\n  reply->reply_sw_in[3] = 0x00;\n\n  // Values depend on universe, set before sending\n  reply->reply_sw_out[0] = 0x00;\n  reply->reply_sw_out[1] = 0x00;\n  reply->reply_sw_out[2] = 0x00;\n  reply->reply_sw_out[3] = 0x00;\n\n  reply->reply_sw_video = 0x00;\n  reply->reply_sw_macro = 0x00;\n  reply->reply_sw_remote = 0x00;\n\n  reply->reply_spare[0] = 0x00;\n  reply->reply_spare[1] = 0x00;\n  reply->reply_spare[2] = 0x00;\n\n  // A DMX to / from Art-Net device\n  reply->reply_style = 0x00;\n\n  Network.localMAC(reply->reply_mac);\n\n  for (unsigned i = 0; i < 4; i++) {\n    reply->reply_bind_ip[i] = localIP[i];\n  }\n\n  reply->reply_bind_index = 1;\n\n  // Product supports web browser configuration\n  // Node’s IP is DHCP or manually configured\n  // Node is DHCP capable\n  // Node supports 15 bit Port-Address (Art-Net 3 or 4)\n  // Node is able to switch between ArtNet and sACN\n  reply->reply_status_2 = (multiWiFi[0].staticIP[0] == 0) ? 0x1F : 0x1D;\n\n  // RDM is disabled\n  // Output style is continuous\n  reply->reply_good_output_b[0] = 0xC0;\n  reply->reply_good_output_b[1] = 0xC0;\n  reply->reply_good_output_b[2] = 0xC0;\n  reply->reply_good_output_b[3] = 0xC0;\n\n  // Fail-over state: Hold last state\n  // Node does not support fail-over\n  reply->reply_status_3 = 0x00;\n\n  for (unsigned i = 0; i < 21; i++) {\n    reply->reply_filler[i] = 0x00;\n  }\n}\n\nstatic void sendArtnetPollReply(ArtPollReply *reply, IPAddress ipAddress, uint16_t portAddress) {\n  reply->reply_net_sw = (uint8_t)((portAddress >> 8) & 0x007F);\n  reply->reply_sub_sw = (uint8_t)((portAddress >> 4) & 0x000F);\n  reply->reply_sw_out[0] = (uint8_t)(portAddress & 0x000F);\n\n  snprintf_P((char *)reply->reply_node_report, sizeof(reply->reply_node_report)-1, PSTR(\"#0001 [%04u] OK - WLED v%s\"), pollReplyCount, versionString);\n\n  if (pollReplyCount < 9999) {\n    pollReplyCount++;\n  } else {\n    pollReplyCount = 0;\n  }\n\n  notifierUdp.beginPacket(ipAddress, ARTNET_DEFAULT_PORT);\n  notifierUdp.write(reply->raw, sizeof(ArtPollReply));\n  notifierUdp.endPacket();\n\n  reply->reply_bind_index++;\n}"
  },
  {
    "path": "wled00/fcn_declare.h",
    "content": "#pragma once\n#ifndef WLED_FCN_DECLARE_H\n#define WLED_FCN_DECLARE_H\n#include <dynarray.h>\n\n/*\n * All globally accessible functions are declared here\n */\n\n//alexa.cpp\n#ifndef WLED_DISABLE_ALEXA\nvoid onAlexaChange(EspalexaDevice* dev);\nvoid alexaInit();\nvoid handleAlexa();\nvoid onAlexaChange(EspalexaDevice* dev);\n#endif\n\n//button.cpp\nvoid shortPressAction(uint8_t b=0);\nvoid longPressAction(uint8_t b=0);\nvoid doublePressAction(uint8_t b=0);\nbool isButtonPressed(uint8_t b=0);\nvoid handleButton();\nvoid handleOnOff(bool forceOff = false);\nvoid handleIO();\nvoid IRAM_ATTR touchButtonISR();\n\n//cfg.cpp\nbool backupConfig();\nbool restoreConfig();\nbool verifyConfig();\nbool configBackupExists();\nvoid resetConfig();\nbool deserializeConfig(JsonObject doc, bool fromFS = false);\nbool deserializeConfigFromFS();\nbool deserializeConfigSec();\nvoid serializeConfig(JsonObject doc);\nvoid serializeConfigToFS();\nvoid serializeConfigSec();\n\ntemplate<typename DestType>\nbool getJsonValue(const JsonVariant& element, DestType& destination) {\n  if (element.isNull()) {\n    return false;\n  }\n\n  destination = element.as<DestType>();\n  return true;\n}\n\ntemplate<typename DestType, typename DefaultType>\nbool getJsonValue(const JsonVariant& element, DestType& destination, const DefaultType defaultValue) {\n  if(!getJsonValue(element, destination)) {\n    destination = defaultValue;\n    return false;\n  }\n\n  return true;\n}\n\ntypedef struct WiFiConfig {\n  char clientSSID[33];\n  char clientPass[65];\n  uint8_t bssid[6];\n  IPAddress staticIP;\n  IPAddress staticGW;\n  IPAddress staticSN;\n#ifdef WLED_ENABLE_WPA_ENTERPRISE\n  byte encryptionType;\n  char enterpriseAnonIdentity[65];\n  char enterpriseIdentity[65];\n  WiFiConfig(const char *ssid=\"\", const char *pass=\"\", uint32_t ip=0, uint32_t gw=0, uint32_t subnet=0x00FFFFFF // little endian\n    , byte enc_type=WIFI_ENCRYPTION_TYPE_PSK, const char *ent_anon=\"\", const char *ent_iden=\"\")\n#else\n  WiFiConfig(const char *ssid=\"\", const char *pass=\"\", uint32_t ip=0, uint32_t gw=0, uint32_t subnet=0x00FFFFFF) // little endian\n#endif\n  : staticIP(ip)\n  , staticGW(gw)\n  , staticSN(subnet)\n  {\n    strncpy(clientSSID, ssid, 32); clientSSID[32] = 0;\n    strncpy(clientPass, pass, 64); clientPass[64] = 0;\n#ifdef WLED_ENABLE_WPA_ENTERPRISE\n    encryptionType = enc_type;\n    strncpy(enterpriseAnonIdentity, ent_anon, 64); enterpriseAnonIdentity[64] = 0;\n    strncpy(enterpriseIdentity, ent_iden, 64); enterpriseIdentity[64] = 0;\n#endif\n    memset(bssid, 0, sizeof(bssid));\n  }\n} wifi_config;\n\n//dmx_output.cpp\nvoid initDMXOutput();\nvoid handleDMXOutput();\n\n//dmx_input.cpp\nvoid initDMXInput();\nvoid handleDMXInput();\n\n//e131.cpp\nvoid handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol);\nvoid handleDMXData(uint16_t uni, uint16_t dmxChannels, uint8_t* e131_data, uint8_t mde, uint8_t previousUniverses);\n// void handleArtnetPollReply(IPAddress ipAddress);                                          // local function, only used in e131.cpp\n// void prepareArtnetPollReply(ArtPollReply* reply);                                         // local function, only used in e131.cpp\n// void sendArtnetPollReply(ArtPollReply* reply, IPAddress ipAddress, uint16_t portAddress); // local function, only used in e131.cpp\n\n//file.cpp\nbool handleFileRead(AsyncWebServerRequest*, String path);\nbool writeObjectToFileUsingId(const char* file, uint16_t id, const JsonDocument* content);\nbool writeObjectToFile(const char* file, const char* key, const JsonDocument* content);\nbool readObjectFromFileUsingId(const char* file, uint16_t id, JsonDocument* dest, const JsonDocument* filter = nullptr);\nbool readObjectFromFile(const char* file, const char* key, JsonDocument* dest, const JsonDocument* filter = nullptr);\nvoid updateFSInfo();\nvoid closeFile();\ninline bool writeObjectToFileUsingId(const String &file, uint16_t id, const JsonDocument* content) { return writeObjectToFileUsingId(file.c_str(), id, content); };\ninline bool writeObjectToFile(const String &file, const char* key, const JsonDocument* content) { return writeObjectToFile(file.c_str(), key, content); };\ninline bool readObjectFromFileUsingId(const String &file, uint16_t id, JsonDocument* dest, const JsonDocument* filter = nullptr) { return readObjectFromFileUsingId(file.c_str(), id, dest); };\ninline bool readObjectFromFile(const String &file, const char* key, JsonDocument* dest, const JsonDocument* filter = nullptr) { return readObjectFromFile(file.c_str(), key, dest); };\nbool copyFile(const char* src_path, const char* dst_path);\nbool backupFile(const char* filename);\nbool restoreFile(const char* filename);\nbool checkBackupExists(const char* filename);\nbool validateJsonFile(const char* filename);\nvoid dumpFilesToSerial();\n\n//hue.cpp\nvoid handleHue();\nvoid reconnectHue();\nvoid onHueError(void* arg, AsyncClient* client, int8_t error);\nvoid onHueConnect(void* arg, AsyncClient* client);\nvoid sendHuePoll();\nvoid onHueData(void* arg, AsyncClient* client, void *data, size_t len);\n\n//image_loader.cpp\nclass Segment;\n#ifdef WLED_ENABLE_GIF\nbool fileSeekCallback(unsigned long position);\nunsigned long filePositionCallback(void);\nint fileReadCallback(void);\nint fileReadBlockCallback(void * buffer, int numberOfBytes);\nint fileSizeCallback(void);\nbyte renderImageToSegment(Segment &seg);\nvoid endImagePlayback(Segment* seg);\n#endif\n\n//improv.cpp\nenum ImprovRPCType {\n  Command_Wifi = 0x01,\n  Request_State = 0x02,\n  Request_Info = 0x03,\n  Request_Scan = 0x04\n};\n\nvoid handleImprovPacket();\nvoid sendImprovRPCResult(ImprovRPCType type, uint8_t n_strings = 0, const char **strings = nullptr);\nvoid sendImprovStateResponse(uint8_t state, bool error = false);\nvoid sendImprovInfoResponse();\nvoid startImprovWifiScan();\nvoid handleImprovWifiScan();\nvoid sendImprovIPRPCResult(ImprovRPCType type);\n\n//ir.cpp\nvoid initIR();\nvoid deInitIR();\nvoid handleIR();\n\n//json.cpp\n#include \"ESPAsyncWebServer.h\"\n#include \"src/dependencies/json/ArduinoJson-v6.h\"\n#include \"src/dependencies/json/AsyncJson-v6.h\"\n\nbool deserializeState(JsonObject root, byte callMode = CALL_MODE_DIRECT_CHANGE, byte presetId = 0);\nvoid serializeSegment(const JsonObject& root, const Segment& seg, byte id, bool forPreset = false, bool segmentBounds = true);\nvoid serializeState(JsonObject root, bool forPreset = false, bool includeBri = true, bool segmentBounds = true, bool selectedSegmentsOnly = false);\nvoid serializeInfo(JsonObject root);\nvoid serializeModeNames(JsonArray arr);\nvoid serializeModeData(JsonArray fxdata);\nvoid serializePins(JsonObject root);\nvoid serveJson(AsyncWebServerRequest* request);\n#ifdef WLED_ENABLE_JSONLIVE\nbool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient = 0);\n#endif\n\n//led.cpp\nvoid setValuesFromSegment(uint8_t s);\n#define setValuesFromMainSeg()          setValuesFromSegment(strip.getMainSegmentId())\n#define setValuesFromFirstSelectedSeg() setValuesFromSegment(strip.getFirstSelectedSegId())\nvoid toggleOnOff();\nvoid applyBri();\nvoid applyFinalBri();\nvoid applyValuesToSelectedSegs();\nvoid colorUpdated(byte callMode);\nvoid stateUpdated(byte callMode);\nvoid updateInterfaces(uint8_t callMode);\nvoid handleTransitions();\nvoid handleNightlight();\nbyte scaledBri(byte in);\n\n#ifdef WLED_ENABLE_LOXONE\n//lx_parser.cpp\nbool parseLx(int lxValue, byte* rgbw);\nvoid parseLxJson(int lxValue, byte segId, bool secondary);\n#endif\n\n//mqtt.cpp\nbool initMqtt();\nvoid publishMqtt();\n\n//ntp.cpp\nvoid handleTime();\nvoid handleNetworkTime();\n// void sendNTPPacket();    // local function, only used in ntp.cpp\n// bool checkNTPResponse(); // local function, only used in ntp.cpp\nvoid updateLocalTime();\nvoid getTimeString(char* out);\nbool checkCountdown();\nvoid setCountdown();\nbyte weekdayMondayFirst();\nbool isTodayInDateRange(byte monthStart, byte dayStart, byte monthEnd, byte dayEnd);\nvoid checkTimers();\nvoid calculateSunriseAndSunset();\nvoid setTimeFromAPI(uint32_t timein);\n\n//overlay.cpp\nvoid handleOverlayDraw();\n// void _overlayAnalogCountdown();  // local function, only used in overlay.cpp\n// void _overlayAnalogClock();      // local function, only used in overlay.cpp\n\n//playlist.cpp\nvoid shufflePlaylist();\nvoid unloadPlaylist();\nint16_t loadPlaylist(JsonObject playlistObject, byte presetId = 0);\nvoid handlePlaylist();\nvoid serializePlaylist(JsonObject obj);\n\n//presets.cpp\nconst char *getPresetsFileName(bool persistent = true);\nbool presetNeedsSaving();\nvoid initPresetsFile();\nvoid handlePresets();\nbool applyPreset(byte index, byte callMode = CALL_MODE_DIRECT_CHANGE);\nbool applyPresetFromPlaylist(byte index);\nvoid applyPresetWithFallback(uint8_t presetID, uint8_t callMode, uint8_t effectID = 0, uint8_t paletteID = 0);\ninline bool applyTemporaryPreset() {return applyPreset(255);};\nvoid savePreset(byte index, const char* pname = nullptr, JsonObject saveobj = JsonObject());\ninline void saveTemporaryPreset() {savePreset(255);};\nvoid deletePreset(byte index);\nbool getPresetName(byte index, String& name);\n\n//remote.cpp\nvoid handleWiZdata(uint8_t *incomingData, size_t len);\nvoid handleRemote();\n\n//set.cpp\nbool isAsterisksOnly(const char* str, byte maxLen);\nvoid handleSettingsSet(AsyncWebServerRequest *request, byte subPage);\nbool handleSet(AsyncWebServerRequest *request, const String& req, bool apply=true);\n\n//udp.cpp\nvoid notify(byte callMode, bool followUp=false);\nuint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const uint8_t* buffer, uint8_t bri=255, bool isRGBW=false);\nvoid realtimeLock(uint32_t timeoutMs, byte md = REALTIME_MODE_GENERIC);\nvoid exitRealtime();\nvoid handleNotifications();\nvoid setRealtimePixel(uint16_t i, byte r, byte g, byte b, byte w);\nvoid refreshNodeList();\nvoid sendSysInfoUDP();\n#ifndef WLED_DISABLE_ESPNOW\nvoid espNowSentCB(uint8_t* address, uint8_t status);\nvoid espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rssi, bool broadcast);\n#endif\n\n//network.cpp\nbool initEthernet(); // result is informational\nint  getSignalQuality(int rssi);\nvoid fillMAC2Str(char *str, const uint8_t *mac);\nvoid fillStr2MAC(uint8_t *mac, const char *str);\nint  findWiFi(bool doScan = false);\nbool isWiFiConfigured();\nvoid WiFiEvent(WiFiEvent_t event);\n\n//um_manager.cpp\ntypedef enum UM_Data_Types {\n  UMT_BYTE = 0,\n  UMT_UINT16,\n  UMT_INT16,\n  UMT_UINT32,\n  UMT_INT32,\n  UMT_FLOAT,\n  UMT_DOUBLE,\n  UMT_BYTE_ARR,\n  UMT_UINT16_ARR,\n  UMT_INT16_ARR,\n  UMT_UINT32_ARR,\n  UMT_INT32_ARR,\n  UMT_FLOAT_ARR,\n  UMT_DOUBLE_ARR\n} um_types_t;\ntypedef struct UM_Exchange_Data {\n  // should just use: size_t arr_size, void **arr_ptr, byte *ptr_type\n  size_t       u_size;                 // size of u_data array\n  um_types_t  *u_type;                 // array of data types\n  void       **u_data;                 // array of pointers to data\n  UM_Exchange_Data() {\n    u_size = 0;\n    u_type = nullptr;\n    u_data = nullptr;\n  }\n  ~UM_Exchange_Data() {\n    if (u_type) delete[] u_type;\n    if (u_data) delete[] u_data;\n  }\n} um_data_t;\nconst unsigned int um_data_size = sizeof(um_data_t);  // 12 bytes\n\nclass Usermod {\n  protected:\n    um_data_t *um_data; // um_data should be allocated using new in (derived) Usermod's setup() or constructor\n  public:\n    Usermod() : um_data(nullptr) {};\n    virtual ~Usermod() { if (um_data) delete um_data; }\n    virtual void setup() = 0; // pure virtual, has to be overriden\n    virtual void loop() = 0;  // pure virtual, has to be overriden\n    virtual void handleOverlayDraw() {}                                      // called after all effects have been processed, just before strip.show()\n    virtual bool handleButton(uint8_t b) { return false; }                   // button overrides are possible here\n    virtual bool getUMData(um_data_t **data) { if (data) *data = nullptr; return false; }; // usermod data exchange [see examples for audio effects]\n    virtual void connected() {}                                              // called when WiFi is (re)connected\n    virtual void appendConfigData(Print& settingsScript);                    // helper function called from usermod settings page to add metadata for entry fields\n    virtual void addToJsonState(JsonObject& obj) {}                          // add JSON objects for WLED state\n    virtual void addToJsonInfo(JsonObject& obj) {}                           // add JSON objects for UI Info page\n    virtual void readFromJsonState(JsonObject& obj) {}                       // process JSON messages received from web server\n    virtual void addToConfig(JsonObject& obj) {}                             // add JSON entries that go to cfg.json\n    virtual bool readFromConfig(JsonObject& obj) { return true; } // Note as of 2021-06 readFromConfig() now needs to return a bool, see usermod_v2_example.h\n    virtual void onMqttConnect(bool sessionPresent) {}                       // fired when MQTT connection is established (so usermod can subscribe)\n    virtual bool onMqttMessage(char* topic, char* payload) { return false; } // fired upon MQTT message received (wled topic)\n    virtual bool onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len) { return false; } // fired upon ESP-NOW message received\n    virtual bool onUdpPacket(uint8_t* payload, size_t len) { return false; } //fired upon UDP packet received\n    virtual void onUpdateBegin(bool) {}                                      // fired prior to and after unsuccessful firmware update\n    virtual void onStateChange(uint8_t mode) {}                              // fired upon WLED state change\n    virtual uint16_t getId() {return USERMOD_ID_UNSPECIFIED;}\n\n  // API shims\n  private:\n    static Print* oappend_shim;\n    // old form of appendConfigData; called by default appendConfigData(Print&) with oappend_shim set up\n    // private so it is not accidentally invoked except via Usermod::appendConfigData(Print&)\n    virtual void appendConfigData() {}    \n  protected:\n    // Shim for oappend(), which used to exist in utils.cpp\n    template<typename T> static inline void oappend(const T& t) { oappend_shim->print(t); };\n#ifdef ESP8266\n    // Handle print(PSTR()) without crashing by detecting PROGMEM strings\n    static void oappend(const char* c) { if ((intptr_t) c >= 0x40000000) oappend_shim->print(FPSTR(c)); else oappend_shim->print(c); };\n#endif\n};\n\nnamespace UsermodManager {\n  void loop();\n  void handleOverlayDraw();\n  bool handleButton(uint8_t b);\n  bool getUMData(um_data_t **um_data, uint8_t mod_id = USERMOD_ID_RESERVED); // USERMOD_ID_RESERVED will poll all usermods\n  void setup();\n  void connected();\n  void appendConfigData(Print&);\n  void addToJsonState(JsonObject& obj);\n  void addToJsonInfo(JsonObject& obj);\n  void readFromJsonState(JsonObject& obj);\n  void addToConfig(JsonObject& obj);\n  bool readFromConfig(JsonObject& obj);\n#ifndef WLED_DISABLE_MQTT\n  void onMqttConnect(bool sessionPresent);\n  bool onMqttMessage(char* topic, char* payload);\n#endif\n#ifndef WLED_DISABLE_ESPNOW\n  bool onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len);\n#endif\n  bool onUdpPacket(uint8_t* payload, size_t len);\n  void onUpdateBegin(bool);\n  void onStateChange(uint8_t);\n  Usermod* lookup(uint16_t mod_id);\n  size_t getModCount();\n};\n\n// Register usermods by building a static list via a linker section\n#define REGISTER_USERMOD(x) DYNARRAY_MEMBER(Usermod*, usermods, um_##x, 1) = &x\n\n//usermod.cpp\nvoid userSetup();\nvoid userConnected();\nvoid userLoop();\n\n//util.cpp\n#ifdef ESP8266\n#define HW_RND_REGISTER RANDOM_REG32\n#else // ESP32 family\n#include \"soc/wdev_reg.h\"\n#define HW_RND_REGISTER REG_READ(WDEV_RND_REG)\n#endif\n#define inoise8 perlin8   // fastled legacy alias\n#define inoise16 perlin16 // fastled legacy alias\n#define hex2int(a) (((a)>='0' && (a)<='9') ? (a)-'0' : ((a)>='A' && (a)<='F') ? (a)-'A'+10 : ((a)>='a' && (a)<='f') ? (a)-'a'+10 : 0)\n[[gnu::pure]] int getNumVal(const String &req, uint16_t pos);\nvoid parseNumber(const char* str, byte &val, byte minv=0, byte maxv=255);\nbool getVal(JsonVariant elem, byte &val, byte vmin=0, byte vmax=255); // getVal supports inc/decrementing and random (\"X~Y(r|[w]~[-][Z])\" form)\n[[gnu::pure]] bool getBoolVal(const JsonVariant &elem, bool dflt);\nbool updateVal(const char* req, const char* key, byte &val, byte minv=0, byte maxv=255);\nsize_t printSetFormCheckbox(Print& settingsScript, const char* key, int val);\nsize_t printSetFormValue(Print& settingsScript, const char* key, int val);\nsize_t printSetFormValue(Print& settingsScript, const char* key, const char* val);\nsize_t printSetFormIndex(Print& settingsScript, const char* key, int index);\nsize_t printSetClassElementHTML(Print& settingsScript, const char* key, const int index, const char* val);\nvoid prepareHostname(char* hostname);\n[[gnu::pure]] bool isAsterisksOnly(const char* str, byte maxLen);\nbool requestJSONBufferLock(uint8_t moduleID=JSON_LOCK_UNKNOWN);\nvoid releaseJSONBufferLock();\nuint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLen);\nuint8_t extractModeSlider(uint8_t mode, uint8_t slider, char *dest, uint8_t maxLen, uint8_t *var = nullptr);\nint16_t extractModeDefaults(uint8_t mode, const char *segVar);\nvoid checkSettingsPIN(const char *pin);\nuint16_t crc16(const unsigned char* data_p, size_t length);\nString computeSHA1(const String& input);\nString getDeviceId();\nuint16_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);\nuint16_t beatsin16_t(accum88 beats_per_minute, uint16_t lowest = 0, uint16_t highest = 65535, uint32_t timebase = 0, uint16_t phase_offset = 0);\nuint8_t beatsin8_t(accum88 beats_per_minute, uint8_t lowest = 0, uint8_t highest = 255, uint32_t timebase = 0, uint8_t phase_offset = 0);\num_data_t* simulateSound(uint8_t simulationId);\nvoid enumerateLedmaps();\n[[gnu::hot]] uint8_t get_random_wheel_index(uint8_t pos);\n[[gnu::hot, gnu::pure]] float mapf(float x, float in_min, float in_max, float out_min, float out_max);\nuint32_t hashInt(uint32_t s);\nint32_t perlin1D_raw(uint32_t x, bool is16bit = false);\nint32_t perlin2D_raw(uint32_t x, uint32_t y, bool is16bit = false);\nint32_t perlin3D_raw(uint32_t x, uint32_t y, uint32_t z, bool is16bit = false);\nuint16_t perlin16(uint32_t x);\nuint16_t perlin16(uint32_t x, uint32_t y);\nuint16_t perlin16(uint32_t x, uint32_t y, uint32_t z);\nuint8_t perlin8(uint16_t x);\nuint8_t perlin8(uint16_t x, uint16_t y);\nuint8_t perlin8(uint16_t x, uint16_t y, uint16_t z);\n\n// fast (true) random numbers using hardware RNG, all functions return values in the range lowerlimit to upperlimit-1\n// note: for true random numbers with high entropy, do not call faster than every 200ns (5MHz)\n// tests show it is still highly random reading it quickly in a loop (better than fastled PRNG)\n// for 8bit and 16bit random functions: no limit check is done for best speed\n// 32bit inputs are used for speed and code size, limits don't work if inverted or out of range\n// inlining does save code size except for random(a,b) and 32bit random with limits\n#define random hw_random // replace arduino random()\ninline uint32_t hw_random() { return HW_RND_REGISTER; };\nuint32_t hw_random(uint32_t upperlimit); // not inlined for code size\nint32_t hw_random(int32_t lowerlimit, int32_t upperlimit);\ninline uint16_t hw_random16() { return HW_RND_REGISTER; };\ninline uint16_t hw_random16(uint32_t upperlimit) { return (hw_random16() * upperlimit) >> 16; }; // input range 0-65535 (uint16_t)\ninline 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\ninline uint8_t hw_random8() { return HW_RND_REGISTER; };\ninline uint8_t hw_random8(uint32_t upperlimit) { return (hw_random8() * upperlimit) >> 8; }; // input range 0-255\ninline uint8_t hw_random8(uint32_t lowerlimit, uint32_t upperlimit) { uint32_t range = upperlimit - lowerlimit; return lowerlimit + hw_random8(range); }; // input range 0-255\n\n// memory allocation wrappers (util.cpp)\nextern \"C\" {\n  // prefer DRAM in d_xalloc functions, PSRAM as fallback\n  void *d_malloc(size_t);\n  void *d_calloc(size_t, size_t);\n  void *d_realloc_malloc(void *ptr, size_t size);\n  #ifndef ESP8266\n  inline void d_free(void *ptr) { heap_caps_free(ptr); }\n  #else\n  inline void d_free(void *ptr) { free(ptr); }\n  #endif\n  #if defined(BOARD_HAS_PSRAM)\n  // prefer PSRAM in p_xalloc functions, DRAM as fallback\n  void *p_malloc(size_t);\n  void *p_calloc(size_t, size_t);\n  void *p_realloc_malloc(void *ptr, size_t size);\n  inline void p_free(void *ptr) { heap_caps_free(ptr); }\n  #else\n  #define p_malloc d_malloc\n  #define p_calloc d_calloc\n  #define p_realloc_malloc d_realloc_malloc\n  #define p_free d_free\n  #endif\n}\n#ifndef ESP8266\ninline 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)\ninline size_t getContiguousFreeHeap() { return heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); } // returns largest contiguous free block\n#else\ninline size_t getFreeHeapSize() { return ESP.getFreeHeap(); } // returns free heap\ninline size_t getContiguousFreeHeap() { return ESP.getMaxFreeBlockSize(); } // returns largest contiguous free block\n#endif\n#define BFRALLOC_NOBYTEACCESS    (1 << 0) // ESP32 has 32bit accessible DRAM (usually ~50kB free) that must not be byte-accessed\n#define BFRALLOC_PREFER_DRAM     (1 << 1) // prefer DRAM over PSRAM\n#define BFRALLOC_ENFORCE_DRAM    (1 << 2) // use DRAM only, no PSRAM\n#define BFRALLOC_PREFER_PSRAM    (1 << 3) // prefer PSRAM over DRAM\n#define BFRALLOC_ENFORCE_PSRAM   (1 << 4) // use PSRAM if available, otherwise uses DRAM\n#define BFRALLOC_CLEAR           (1 << 5) // clear allocated buffer after allocation\nvoid *allocate_buffer(size_t size, uint32_t type);\n\nvoid handleBootLoop();   // detect and handle bootloops\n#ifndef ESP8266\nvoid bootloopCheckOTA(); // swap boot image if bootloop is detected instead of restoring config\n#endif\n// RAII guard class for the JSON Buffer lock\n// Modeled after std::lock_guard\nclass JSONBufferGuard {\n  bool holding_lock;\n  public:\n    inline JSONBufferGuard(uint8_t module=JSON_LOCK_UNKNOWN) : holding_lock(requestJSONBufferLock(module)) {};\n    inline ~JSONBufferGuard() { if (holding_lock) releaseJSONBufferLock(); };\n    inline JSONBufferGuard(const JSONBufferGuard&) = delete; // Noncopyable\n    inline JSONBufferGuard& operator=(const JSONBufferGuard&) = delete;\n    inline JSONBufferGuard(JSONBufferGuard&& r) : holding_lock(r.holding_lock) { r.holding_lock = false; };  // but movable\n    inline JSONBufferGuard& operator=(JSONBufferGuard&& r) { holding_lock |= r.holding_lock; r.holding_lock = false; return *this; };\n    inline bool owns_lock() const { return holding_lock; }\n    explicit inline operator bool() const { return owns_lock(); };\n    inline void release() { if (holding_lock) releaseJSONBufferLock(); holding_lock = false; }\n};\n\n//wled_math.cpp\n//float cos_t(float phi); // use float math\n//float sin_t(float phi);\n//float tan_t(float x);\nint16_t sin16_t(uint16_t theta);\nint16_t cos16_t(uint16_t theta);\nuint8_t sin8_t(uint8_t theta);\nuint8_t cos8_t(uint8_t theta);\nfloat sin_approx(float theta); // uses integer math (converted to float), accuracy +/-0.0015 (compared to sinf())\nfloat cos_approx(float theta);\nfloat tan_approx(float x);\nfloat atan2_t(float y, float x);\nfloat acos_t(float x);\nfloat asin_t(float x);\ntemplate <typename T> T atan_t(T x);\nfloat floor_t(float x);\nfloat fmod_t(float num, float denom);\nuint32_t sqrt32_bw(uint32_t x);\n#define sin_t sin_approx\n#define cos_t cos_approx\n#define tan_t tan_approx\n\n/*\n#include <math.h>  // standard math functions. use a lot of flash\n#define sin_t sinf\n#define cos_t cosf\n#define tan_t tanf\n#define asin_t asinf\n#define acos_t acosf\n#define atan_t atanf\n#define fmod_t fmodf\n#define floor_t floorf\n*/\n//wled_serial.cpp\nvoid handleSerial();\nvoid updateBaudRate(uint32_t rate);\n\n//wled_server.cpp\nvoid initServer();\nvoid serveMessage(AsyncWebServerRequest* request, uint16_t code, const String& headl, const String& subl=\"\", byte optionT=255);\nvoid serveJsonError(AsyncWebServerRequest* request, uint16_t code, uint16_t error);\nvoid serveSettings(AsyncWebServerRequest* request, bool post = false);\nvoid serveSettingsJS(AsyncWebServerRequest* request);\n\n//ws.cpp\nvoid handleWs();\nvoid wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len);\nvoid sendDataWs(AsyncWebSocketClient * client = nullptr);\n\n//xml.cpp\nvoid XML_response(Print& dest);\nvoid getSettingsJS(byte subPage, Print& dest);\n\n#endif\n"
  },
  {
    "path": "wled00/file.cpp",
    "content": "#include \"wled.h\"\n\n/*\n * Utility for SPIFFS filesystem\n */\n\n#ifdef ARDUINO_ARCH_ESP32 //FS info bare IDF function until FS wrapper is available for ESP32\n#if WLED_FS != LITTLEFS && ESP_IDF_VERSION_MAJOR < 4\n  #include \"esp_spiffs.h\"\n#endif\n#endif\n\n#define FS_BUFSIZE 256\n\n/*\n * Structural requirements for files managed by writeObjectToFile() and readObjectFromFile() utilities:\n * 1. File must be a string representation of a valid JSON object\n * 2. File must have '{' as first character\n * 3. There must not be any additional characters between a root-level key and its value object (e.g. space, tab, newline)\n * 4. There must not be any characters between an root object-separating ',' and the next object key string\n * 5. There may be any number of spaces, tabs, and/or newlines before such object-separating ','\n * 6. There must not be more than 5 consecutive spaces at any point except for those permitted in condition 5\n * 7. If it is desired to delete the first usable object (e.g. preset file), a dummy object '\"0\":{}' is inserted at the beginning.\n *    It shall be disregarded by receiving software.\n *    The reason for it is that deleting the first preset would require special code to handle commas between it and the 2nd preset\n */\n\n// There are no consecutive spaces longer than this in the file, so if more space is required, findSpace() can return false immediately\n// Actual space may be lower\nconstexpr size_t MAX_SPACE = UINT16_MAX * 2U;           // smallest supported config has 128Kb flash size\nstatic volatile size_t knownLargestSpace = MAX_SPACE;\n\nstatic File f; // don't export to other cpp files\n\n//wrapper to find out how long closing takes\nvoid closeFile() {\n  #ifdef WLED_DEBUG_FS\n    DEBUGFS_PRINT(F(\"Close -> \"));\n    uint32_t s = millis();\n  #endif\n  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.\n  DEBUGFS_PRINTF(\"took %lu ms\\n\", millis() - s);\n  doCloseFile = false;\n}\n\n//find() that reads and buffers data from file stream in 256-byte blocks.\n//Significantly faster, f.find(key) can take SECONDS for multi-kB files\nstatic bool bufferedFind(const char *target, bool fromStart = true) {\n  #ifdef WLED_DEBUG_FS\n    DEBUGFS_PRINT(\"Find \");\n    DEBUGFS_PRINTLN(target);\n    uint32_t s = millis();\n  #endif\n\n  if (!f || !f.size()) return false;\n  size_t targetLen = strlen(target);\n\n  size_t index = 0;\n  byte buf[FS_BUFSIZE];\n  if (fromStart) f.seek(0);\n\n  while (f.position() < f.size() -1) {\n    size_t bufsize = f.read(buf, FS_BUFSIZE); // better to use size_t instead if uint16_t\n    size_t count = 0;\n    while (count < bufsize) {\n      if(buf[count] != target[index])\n      index = 0; // reset index if any char does not match\n\n      if(buf[count] == target[index]) {\n        if(++index >= targetLen) { // return true if all chars in the target match\n          f.seek((f.position() - bufsize) + count +1);\n          DEBUGFS_PRINTF(\"Found at pos %d, took %lu ms\", f.position(), millis() - s);\n          return true;\n        }\n      }\n      count++;\n    }\n  }\n  DEBUGFS_PRINTF(\"No match, took %lu ms\\n\", millis() - s);\n  return false;\n}\n\n//find empty spots in file stream in 256-byte blocks.\nstatic bool bufferedFindSpace(size_t targetLen, bool fromStart = true) {\n\n  #ifdef WLED_DEBUG_FS\n    DEBUGFS_PRINTF(\"Find %d spaces\\n\", targetLen);\n    uint32_t s = millis();\n  #endif\n\n  if (knownLargestSpace < targetLen) {\n    DEBUGFS_PRINT(F(\"No match, KLS \"));\n    DEBUGFS_PRINTLN(knownLargestSpace);\n    return false;\n  }\n\n  if (!f || !f.size()) return false;\n\n  size_t index = 0; // better to use size_t instead if uint16_t\n  byte buf[FS_BUFSIZE];\n  if (fromStart) f.seek(0);\n\n  while (f.position() < f.size() -1) {\n    size_t bufsize = f.read(buf, FS_BUFSIZE);\n    size_t count = 0;\n\n    while (count < bufsize) {\n      if(buf[count] == ' ') {\n        if(++index >= targetLen) { // return true if space long enough\n          if (fromStart) {\n            f.seek((f.position() - bufsize) + count +1 - targetLen);\n            knownLargestSpace = MAX_SPACE; //there may be larger spaces after, so we don't know\n          }\n          DEBUGFS_PRINTF(\"Found at pos %d, took %lu ms\", f.position(), millis() - s);\n          return true;\n        }\n      } else {\n        if (!fromStart) return false;\n        if (index) {\n          if (knownLargestSpace < index || (knownLargestSpace == MAX_SPACE)) knownLargestSpace = index;\n          index = 0; // reset index if not space\n        }\n      }\n\n      count++;\n    }\n  }\n  DEBUGFS_PRINTF(\"No match, took %lu ms\\n\", millis() - s);\n  return false;\n}\n\n//find the closing bracket corresponding to the opening bracket at the file pos when calling this function\nstatic bool bufferedFindObjectEnd() {\n  #ifdef WLED_DEBUG_FS\n    DEBUGFS_PRINTLN(F(\"Find obj end\"));\n    uint32_t s = millis();\n  #endif\n\n  if (!f || !f.size()) return false;\n\n  unsigned objDepth = 0; //num of '{' minus num of '}'. return once 0\n  //size_t start = f.position();\n  byte buf[FS_BUFSIZE];\n\n  while (f.position() < f.size() -1) {\n    size_t bufsize = f.read(buf, FS_BUFSIZE); // better to use size_t instead of uint16_t\n    size_t count = 0;\n\n    while (count < bufsize) {\n      if (buf[count] == '{') objDepth++;\n      if (buf[count] == '}') objDepth--;\n      if (objDepth == 0) {\n        f.seek((f.position() - bufsize) + count +1);\n        DEBUGFS_PRINTF(\"} at pos %d, took %lu ms\", f.position(), millis() - s);\n        return true;\n      }\n      count++;\n    }\n  }\n  DEBUGFS_PRINTF(\"No match, took %lu ms\\n\", millis() - s);\n  return false;\n}\n\n//fills n bytes from current file pos with ' ' characters\nstatic void writeSpace(size_t l)\n{\n  byte buf[FS_BUFSIZE];\n  memset(buf, ' ', FS_BUFSIZE);\n\n  while (l > 0) {\n    size_t block = (l>FS_BUFSIZE) ? FS_BUFSIZE : l;\n    f.write(buf, block);\n    l -= block;\n  }\n\n  if (knownLargestSpace < l) knownLargestSpace = l;\n}\n\nstatic bool appendObjectToFile(const char* key, const JsonDocument* content, uint32_t s, uint32_t contentLen = 0)\n{\n  #ifdef WLED_DEBUG_FS\n    DEBUGFS_PRINTLN(F(\"Append\"));\n    uint32_t s1 = millis();\n  #endif\n  uint32_t pos = 0;\n  if (!f) return false;\n\n  if (f.size() < 3) {\n    char init[10];\n    strcpy_P(init, PSTR(\"{\\\"0\\\":{}}\"));\n    f.print(init);\n  }\n\n  if (content->isNull()) {\n    doCloseFile = true;\n    return true; //nothing  to append\n  }\n\n  //if there is enough empty space in file, insert there instead of appending\n  if (!contentLen) contentLen = measureJson(*content);\n  DEBUGFS_PRINTF(\"CLen %d\\n\", contentLen);\n  if (bufferedFindSpace(contentLen + strlen(key) + 1)) {\n    if (f.position() > 2) f.write(','); //add comma if not first object\n    f.print(key);\n    serializeJson(*content, f);\n    DEBUGFS_PRINTF(\"Inserted, took %lu ms (total %lu)\", millis() - s1, millis() - s);\n    doCloseFile = true;\n    return true;\n  }\n\n  //not enough space, append at end\n\n  //permitted space for presets exceeded\n  updateFSInfo();\n\n  if (f.size() + 9000 > (fsBytesTotal - fsBytesUsed)) { //make sure there is enough space to at least copy the file once\n    errorFlag = ERR_FS_QUOTA;\n    doCloseFile = true;\n    return false;\n  }\n\n  //check if last character in file is '}' (typical)\n  uint32_t eof = f.size() -1;\n  f.seek(eof, SeekSet);\n  if (f.read() == '}') pos = eof;\n\n  if (pos == 0) //not found\n  {\n    DEBUGFS_PRINTLN(F(\"not }\"));\n    f.seek(0);\n    while (bufferedFind(\"}\",false)) //find last closing bracket in JSON if not last char\n    {\n      pos = f.position();\n    }\n    if (pos > 0) pos--;\n  }\n  DEBUGFS_PRINT(\"pos \"); DEBUGFS_PRINTLN(pos);\n  if (pos > 2)\n  {\n    f.seek(pos, SeekSet);\n    f.write(',');\n  } else { //file content is not valid JSON object\n    f.seek(0, SeekSet);\n    f.print('{'); //start JSON\n  }\n\n  f.print(key);\n\n  //Append object\n  serializeJson(*content, f);\n  f.write('}');\n\n  doCloseFile = true;\n  DEBUGFS_PRINTF(\"Appended, took %lu ms (total %lu)\", millis() - s1, millis() - s);\n  return true;\n}\n\nbool writeObjectToFileUsingId(const char* file, uint16_t id, const JsonDocument* content)\n{\n  char objKey[10];\n  sprintf(objKey, \"\\\"%d\\\":\", id);\n  return writeObjectToFile(file, objKey, content);\n}\n\nbool writeObjectToFile(const char* file, const char* key, const JsonDocument* content)\n{\n  uint32_t s = 0; //timing\n  #ifdef WLED_DEBUG_FS\n    DEBUGFS_PRINTF(\"Write to %s with key %s >>>\\n\", file, (key==nullptr)?\"nullptr\":key);\n    serializeJson(*content, Serial); DEBUGFS_PRINTLN();\n    s = millis();\n  #endif\n\n  if (doCloseFile) closeFile(); // This prevents the loss of file data that is still cached in the File object.\n\n  size_t pos = 0;\n  char fileName[129]; strncpy_P(fileName, file, 128); fileName[128] = 0; //use PROGMEM safe copy as FS.open() does not\n  f = WLED_FS.open(fileName, WLED_FS.exists(fileName) ? \"r+\" : \"w+\");\n  if (!f) {\n    DEBUGFS_PRINTLN(F(\"Failed to open!\"));\n    return false;\n  }\n\n  if (!bufferedFind(key)) //key does not exist in file\n  {\n    return appendObjectToFile(key, content, s);\n  }\n\n  //an object with this key already exists, replace or delete it\n  pos = f.position();\n  //measure out end of old object\n  bufferedFindObjectEnd();\n  size_t pos2 = f.position();\n\n  uint32_t oldLen = pos2 - pos;\n  DEBUGFS_PRINTF(\"Old obj len %d\\n\", oldLen);\n\n  //Three cases:\n  //1. The new content is null, overwrite old obj with spaces\n  //2. The new content is smaller than the old, overwrite and fill diff with spaces\n  //3. The new content is larger than the old, but smaller than old + trailing spaces, overwrite with new\n  //4. The new content is larger than old + trailing spaces, delete old and append\n\n  size_t contentLen = 0;\n  if (!content->isNull()) contentLen = measureJson(*content);\n\n  if (contentLen && contentLen <= oldLen) { //replace and fill diff with spaces\n    DEBUGFS_PRINTLN(F(\"replace\"));\n    f.seek(pos);\n    serializeJson(*content, f);\n    writeSpace(pos2 - f.position());\n  } else if (contentLen && bufferedFindSpace(contentLen - oldLen, false)) { //enough leading spaces to replace\n    DEBUGFS_PRINTLN(F(\"replace (trailing)\"));\n    f.seek(pos);\n    serializeJson(*content, f);\n  } else {\n    DEBUGFS_PRINTLN(F(\"delete\"));\n    pos -= strlen(key);\n    if (pos > 3) pos--; //also delete leading comma if not first object\n    f.seek(pos);\n    writeSpace(pos2 - pos);\n    if (contentLen) return appendObjectToFile(key, content, s, contentLen);\n  }\n\n  doCloseFile = true;\n  DEBUGFS_PRINTF(\"Replaced/deleted, took %lu ms\\n\", millis() - s);\n  return true;\n}\n\nbool readObjectFromFileUsingId(const char* file, uint16_t id, JsonDocument* dest, const JsonDocument* filter)\n{\n  char objKey[10];\n  sprintf(objKey, \"\\\"%d\\\":\", id);\n  return readObjectFromFile(file, objKey, dest, filter);\n}\n\n//if the key is a nullptr, deserialize entire object\nbool readObjectFromFile(const char* file, const char* key, JsonDocument* dest, const JsonDocument* filter)\n{\n  if (doCloseFile) closeFile();\n  #ifdef WLED_DEBUG_FS\n    DEBUGFS_PRINTF(\"Read from %s with key %s >>>\\n\", file, (key==nullptr)?\"nullptr\":key);\n    uint32_t s = millis();\n  #endif\n  char fileName[129]; strncpy_P(fileName, file, 128); fileName[128] = 0; //use PROGMEM safe copy as FS.open() does not\n  f = WLED_FS.open(fileName, \"r\");\n  if (!f) return false;\n\n  if (key != nullptr && !bufferedFind(key)) //key does not exist in file\n  {\n    f.close();\n    dest->clear();\n    DEBUGFS_PRINTLN(F(\"Obj not found.\"));\n    return false;\n  }\n\n  if (filter) deserializeJson(*dest, f, DeserializationOption::Filter(*filter));\n  else        deserializeJson(*dest, f);\n\n  f.close();\n  DEBUGFS_PRINTF(\"Read, took %lu ms\\n\", millis() - s);\n  return true;\n}\n\nvoid updateFSInfo() {\n  #ifdef ARDUINO_ARCH_ESP32\n    #if WLED_FS == LITTLEFS || ESP_IDF_VERSION_MAJOR >= 4\n    fsBytesTotal = WLED_FS.totalBytes();\n    fsBytesUsed = WLED_FS.usedBytes();\n    #else\n    esp_spiffs_info(nullptr, &fsBytesTotal, &fsBytesUsed);\n    #endif\n  #else\n    FSInfo fsi;\n    WLED_FS.info(fsi);\n    fsBytesUsed  = fsi.usedBytes;\n    fsBytesTotal = fsi.totalBytes;\n  #endif\n}\n\n\n#ifdef ARDUINO_ARCH_ESP32\n// caching presets in PSRAM may prevent occasional flashes seen when HomeAssitant polls WLED\n// original idea by @akaricchi (https://github.com/Akaricchi)\n// returns a pointer to the PSRAM buffer, updates size parameter\nstatic const uint8_t *getPresetCache(size_t &size) {\n  static unsigned long presetsCachedTime = 0;\n  static uint8_t *presetsCached = nullptr;\n  static size_t presetsCachedSize = 0;\n  static byte presetsCachedValidate = 0;\n\n  //if (presetsModifiedTime != presetsCachedTime) DEBUG_PRINTLN(F(\"getPresetCache(): presetsModifiedTime changed.\"));\n  //if (presetsCachedValidate != cacheInvalidate) DEBUG_PRINTLN(F(\"getPresetCache(): cacheInvalidate changed.\"));\n\n  if ((presetsModifiedTime != presetsCachedTime) || (presetsCachedValidate != cacheInvalidate)) {\n    if (presetsCached) {\n      p_free(presetsCached);\n      presetsCached = nullptr;\n      presetsCachedSize = 0;\n    }\n  }\n\n  if (!presetsCached) {\n    File file = WLED_FS.open(FPSTR(getPresetsFileName()), \"r\");\n    if (file) {\n      presetsCachedTime = presetsModifiedTime;\n      presetsCachedValidate = cacheInvalidate;\n      presetsCachedSize = 0;\n      presetsCached = (uint8_t*)p_malloc(file.size() + 1);\n      if (presetsCached) {\n        presetsCachedSize = file.size();\n        file.read(presetsCached, presetsCachedSize);\n        presetsCached[presetsCachedSize] = 0;\n        file.close();\n      }\n    }\n  }\n\n  size = presetsCachedSize;\n  return presetsCached;\n}\n#endif\n\nbool handleFileRead(AsyncWebServerRequest* request, String path){\n  DEBUGFS_PRINT(F(\"WS FileRead: \")); DEBUGFS_PRINTLN(path);\n  if(path.endsWith(\"/\")) path += \"index.htm\";\n  if(path.indexOf(F(\"sec\")) > -1) return false;\n  #ifdef BOARD_HAS_PSRAM\n  if (path.endsWith(FPSTR(getPresetsFileName()))) {\n    size_t psize;\n    const uint8_t *presets = getPresetCache(psize);\n    if (presets) {\n      AsyncWebServerResponse *response = request->beginResponse_P(200, FPSTR(CONTENT_TYPE_JSON), presets, psize);\n      request->send(response);\n      return true;\n    }\n  }\n  #endif\n  if(WLED_FS.exists(path) || WLED_FS.exists(path + \".gz\")) {\n    request->send(request->beginResponse(WLED_FS, path, {}, request->hasArg(F(\"download\")), {}));\n    return true;\n  }\n  return false;\n}\n\n// copy a file, delete destination file if incomplete to prevent corrupted files\nbool copyFile(const char* src_path, const char* dst_path) {\n  DEBUG_PRINTF(\"copyFile from %s to %s\\n\", src_path, dst_path);\n  if(!WLED_FS.exists(src_path)) {\n   DEBUG_PRINTLN(F(\"file not found\"));\n   return false;\n  }\n\n  bool success = true; // is set to false on error\n  File src = WLED_FS.open(src_path, \"r\");\n  File dst = WLED_FS.open(dst_path, \"w\");\n\n  if (src && dst) {\n    uint8_t buf[128]; // copy file in 128-byte blocks\n    while (src.available() > 0) {\n      size_t bytesRead = src.read(buf, sizeof(buf));\n      if (bytesRead == 0) {\n        success = false;\n        break; // error, no data read\n      }\n      size_t bytesWritten = dst.write(buf, bytesRead);\n      if (bytesWritten != bytesRead) {\n        success = false;\n        break; // error, not all data written\n      }\n    }\n  } else {\n    success = false; // error, could not open files\n  }\n  if(src) src.close();\n  if(dst) dst.close();\n  if (!success) {\n    DEBUG_PRINTLN(F(\"copy failed\"));\n    WLED_FS.remove(dst_path); // delete incomplete file\n  }\n  return success;\n}\n\n// compare two files, return true if identical\nbool compareFiles(const char* path1, const char* path2) {\n  DEBUG_PRINTF(\"compareFile %s and %s\\n\", path1, path2);\n  if (!WLED_FS.exists(path1) || !WLED_FS.exists(path2)) {\n    DEBUG_PRINTLN(F(\"file not found\"));\n    return false;\n  }\n\n  bool identical = true; // set to false on mismatch\n  File f1 = WLED_FS.open(path1, \"r\");\n  File f2 = WLED_FS.open(path2, \"r\");\n\n  if (f1 && f2) {\n    uint8_t buf1[128], buf2[128];\n    while (f1.available() > 0 || f2.available() > 0) {\n      size_t len1 = f1.read(buf1, sizeof(buf1));\n      size_t len2 = f2.read(buf2, sizeof(buf2));\n\n      if (len1 != len2) {\n        identical = false;\n        break; // files differ in size or read failed\n      }\n\n      if (memcmp(buf1, buf2, len1) != 0) {\n        identical = false;\n        break; // files differ in content\n      }\n    }\n  } else {\n    identical = false; // error opening files\n  }\n\n  if (f1) f1.close();\n  if (f2) f2.close();\n  return identical;\n}\n\nstatic const char s_backup_fmt[] PROGMEM = \"/bkp.%s\";\n\nbool backupFile(const char* filename) {\n  DEBUG_PRINTF(\"backup %s \\n\", filename);\n  if (!validateJsonFile(filename)) {\n    DEBUG_PRINTLN(F(\"broken file\"));\n    return false;\n  }\n  char backupname[32];\n  snprintf_P(backupname, sizeof(backupname), s_backup_fmt, filename + 1); // skip leading '/' in filename\n\n  if (copyFile(filename, backupname)) {\n    DEBUG_PRINTLN(F(\"backup ok\"));\n    return true;\n  }\n  DEBUG_PRINTLN(F(\"backup failed\"));\n  return false;\n}\n\nbool restoreFile(const char* filename) {\n  DEBUG_PRINTF(\"restore %s \\n\", filename);\n  char backupname[32];\n  snprintf_P(backupname, sizeof(backupname), s_backup_fmt, filename + 1); // skip leading '/' in filename\n\n  if (!WLED_FS.exists(backupname)) {\n    DEBUG_PRINTLN(F(\"no backup found\"));\n    return false;\n  }\n\n  if (!validateJsonFile(backupname)) {\n    DEBUG_PRINTLN(F(\"broken backup\"));\n    return false;\n  }\n\n  if (copyFile(backupname, filename)) {\n    DEBUG_PRINTLN(F(\"restore ok\"));\n    return true;\n  }\n  DEBUG_PRINTLN(F(\"restore failed\"));\n  return false;\n}\n\nbool checkBackupExists(const char* filename) {\n  char backupname[32];\n  snprintf_P(backupname, sizeof(backupname), s_backup_fmt, filename + 1); // skip leading '/' in filename\n  return WLED_FS.exists(backupname);\n}\n\nbool validateJsonFile(const char* filename) {\n  if (!WLED_FS.exists(filename)) return false;\n  File file = WLED_FS.open(filename, \"r\");\n  if (!file) return false;\n  StaticJsonDocument<0> doc, filter; // https://arduinojson.org/v6/how-to/validate-json/\n  bool result = deserializeJson(doc, file, DeserializationOption::Filter(filter)) == DeserializationError::Ok;\n  file.close();\n  if (!result) {\n    DEBUG_PRINTF_P(PSTR(\"Invalid JSON file %s\\n\"), filename);\n  } else {\n    DEBUG_PRINTF_P(PSTR(\"Valid JSON file %s\\n\"), filename);\n  }\n  return result;\n}\n\n// print contents of all files in root dir to Serial except wsec files\nvoid dumpFilesToSerial() {\n  File rootdir = WLED_FS.open(\"/\", \"r\");\n  File rootfile = rootdir.openNextFile();\n  while (rootfile) {\n    size_t len = strlen(rootfile.name());\n    // skip files starting with \"wsec\" and dont end in .json\n    if (strncmp(rootfile.name(), \"wsec\", 4) != 0 && len >= 6 && strcmp(rootfile.name() + len - 5, \".json\") == 0) {\n      Serial.println(rootfile.name());\n      while (rootfile.available()) {\n        Serial.write(rootfile.read());\n      }\n      Serial.println();\n      Serial.println();\n    }\n    rootfile.close();\n    rootfile = rootdir.openNextFile();\n  }\n}\n\n"
  },
  {
    "path": "wled00/hue.cpp",
    "content": "#include \"wled.h\"\n\n/*\n * Sync to Philips hue lights\n */\n\n#ifndef WLED_DISABLE_HUESYNC\n\nvoid handleHue()\n{\n  if (hueReceived)\n  {\n    colorUpdated(CALL_MODE_HUE); hueReceived = false;\n    if (hueStoreAllowed && hueNewKey)\n    {\n      serializeConfigSec(); //save api key\n      hueStoreAllowed = false;\n      hueNewKey = false;\n    }\n  }\n\n  if (!WLED_CONNECTED || hueClient == nullptr || millis() - hueLastRequestSent < huePollIntervalMs) return;\n\n  hueLastRequestSent = millis();\n  if (huePollingEnabled)\n  {\n    reconnectHue();\n  } else {\n    hueClient->close();\n    if (hueError == HUE_ERROR_ACTIVE) hueError = HUE_ERROR_INACTIVE;\n  }\n}\n\nvoid reconnectHue()\n{\n  if (!WLED_CONNECTED || !huePollingEnabled) return;\n  DEBUG_PRINTLN(F(\"Hue reconnect\"));\n  if (hueClient == nullptr) {\n    hueClient = new AsyncClient();\n    hueClient->onConnect(&onHueConnect, hueClient);\n    hueClient->onData(&onHueData, hueClient);\n    hueClient->onError(&onHueError, hueClient);\n    hueAuthRequired = (strlen(hueApiKey)<20);\n  }\n  hueClient->connect(hueIP, 80);\n}\n\nvoid onHueError(void* arg, AsyncClient* client, int8_t error)\n{\n  DEBUG_PRINTLN(F(\"Hue err\"));\n  hueError = HUE_ERROR_TIMEOUT;\n}\n\nvoid onHueConnect(void* arg, AsyncClient* client)\n{\n  DEBUG_PRINTLN(F(\"Hue connect\"));\n  sendHuePoll();\n}\n\nvoid sendHuePoll()\n{\n  if (hueClient == nullptr || !hueClient->connected()) return;\n  String req = \"\";\n  if (hueAuthRequired)\n  {\n    req += F(\"POST /api HTTP/1.1\\r\\nHost: \");\n    req += hueIP.toString();\n    req += F(\"\\r\\nContent-Length: 25\\r\\n\\r\\n{\\\"devicetype\\\":\\\"wled#esp\\\"}\");\n  } else\n  {\n    req += F(\"GET /api/\");\n    req += hueApiKey;\n    req += F(\"/lights/\");\n    req += String(huePollLightId);\n    req += F(\" HTTP/1.1\\r\\nHost: \");\n    req += hueIP.toString();\n    req += \"\\r\\n\\r\\n\";\n  }\n  hueClient->add(req.c_str(), req.length());\n  hueClient->send();\n  hueLastRequestSent = millis();\n}\n\nvoid onHueData(void* arg, AsyncClient* client, void *data, size_t len)\n{\n  if (!len) return;\n  char* str = (char*)data;\n  DEBUG_PRINTLN(hueApiKey);\n  DEBUG_PRINTLN(str);\n  //only get response body\n  str = strstr(str,\"\\r\\n\\r\\n\");\n  if (str == nullptr) return;\n  str += 4;\n\n  StaticJsonDocument<1024> root;\n  if (str[0] == '[') //is JSON array\n  {\n    auto error = deserializeJson(root, str);\n    if (error)\n    {\n      hueError = HUE_ERROR_JSON_PARSING; return;\n    }\n\n    int hueErrorCode = root[0][F(\"error\")][\"type\"];\n    if (hueErrorCode)//hue bridge returned error\n    {\n      hueError = hueErrorCode;\n      switch (hueErrorCode)\n      {\n        case 1:   hueAuthRequired = true;    break; //Unauthorized user\n        case 3:   huePollingEnabled = false; break; //Invalid light ID\n        case 101: hueAuthRequired = true;    break; //link button not presset\n      }\n      return;\n    }\n\n    if (hueAuthRequired)\n    {\n      const char* apikey = root[0][F(\"success\")][F(\"username\")];\n      if (apikey != nullptr && strlen(apikey) < sizeof(hueApiKey))\n      {\n        strlcpy(hueApiKey, apikey, sizeof(hueApiKey));\n        hueAuthRequired = false;\n        hueNewKey = true;\n      }\n    }\n    return;\n  }\n\n  //else, assume it is JSON object, look for state and only parse that\n  str = strstr(str,\"state\");\n  if (str == nullptr) return;\n  str = strstr(str,\"{\");\n\n  auto error = deserializeJson(root, str);\n  if (error)\n  {\n    hueError = HUE_ERROR_JSON_PARSING; return;\n  }\n\n  float hueX=0, hueY=0;\n  uint16_t hueHue=0, hueCt=0;\n  byte hueBri=0, hueSat=0, hueColormode=0;\n\n  if (root[\"on\"]) {\n    if (root.containsKey(\"bri\")) //Dimmable device\n    {\n      hueBri = root[\"bri\"];\n      hueBri++;\n      const char* cm =root[F(\"colormode\")];\n      if (cm != nullptr) //Color device\n      {\n        if (strstr(cm,(\"ct\")) != nullptr) //ct mode\n        {\n          hueCt = root[\"ct\"];\n          hueColormode = 3;\n        } else if (strstr(cm,\"xy\") != nullptr) //xy mode\n        {\n          hueX = root[\"xy\"][0]; // 0.5051\n          hueY = root[\"xy\"][1]; // 0.4151\n          hueColormode = 1;\n        } else //hs mode\n        {\n          hueHue = root[\"hue\"];\n          hueSat = root[F(\"sat\")];\n          hueColormode = 2;\n        }\n      }\n    } else //On/Off device\n    {\n      hueBri = briLast;\n    }\n  } else\n  {\n    hueBri = 0;\n  }\n\n  hueError = HUE_ERROR_ACTIVE;\n\n  //apply vals\n  if (hueBri != hueBriLast)\n  {\n    if (hueApplyOnOff)\n    {\n      if (hueBri==0) {bri = 0;}\n      else if (bri==0 && hueBri>0) bri = briLast;\n    }\n    if (hueApplyBri)\n    {\n      if (hueBri>0) bri = hueBri;\n    }\n    hueBriLast = hueBri;\n  }\n  if (hueApplyColor)\n  {\n    switch(hueColormode)\n    {\n      case 1: if (hueX != hueXLast || hueY != hueYLast) colorXYtoRGB(hueX,hueY,colPri); hueXLast = hueX; hueYLast = hueY; break;\n      case 2: if (hueHue != hueHueLast || hueSat != hueSatLast) colorHStoRGB(hueHue,hueSat,colPri); hueHueLast = hueHue; hueSatLast = hueSat; break;\n      case 3: if (hueCt != hueCtLast) colorCTtoRGB(hueCt,colPri); hueCtLast = hueCt; break;\n    }\n  }\n  hueReceived = true;\n}\n#else\nvoid handleHue(){}\nvoid reconnectHue(){}\n#endif\n"
  },
  {
    "path": "wled00/image_loader.cpp",
    "content": "#include \"wled.h\"\n\n#ifdef WLED_ENABLE_GIF\n\n#include \"GifDecoder.h\"\n\n\n/*\n * Functions to render images from filesystem to segments, used by the \"Image\" effect\n */\n\nstatic File file;\nstatic char lastFilename[WLED_MAX_SEGNAME_LEN+2] = \"/\"; // enough space for \"/\" + seg.name + '\\0'\nstatic GifDecoder<320,320,12,true> decoder;  // this creates the basic object; parameter lzwMaxBits is not used; decoder.alloc() always allocated \"everything else\" = 24Kb \nstatic bool gifDecodeFailed = false;\nstatic unsigned long lastFrameDisplayTime = 0, currentFrameDelay = 0;\n\nbool fileSeekCallback(unsigned long position) {\n  return file.seek(position);\n}\n\nunsigned long filePositionCallback(void) {\n  return file.position();\n}\n\nint fileReadCallback(void) {\n  return file.read();\n}\n\nint fileReadBlockCallback(void * buffer, int numberOfBytes) {\n  return file.read((uint8_t*)buffer, numberOfBytes);\n}\n\nint fileSizeCallback(void) {\n  return file.size();\n}\n\nbool openGif(const char *filename) {  // side-effect: updates \"file\"\n  file = WLED_FS.open(filename, \"r\");\n  DEBUG_PRINTF_P(PSTR(\"opening GIF file %s\\n\"), filename);\n\n  if (!file) return false;\n  return true;\n}\n\nstatic Segment* activeSeg;\nstatic uint16_t gifWidth, gifHeight;\nstatic int lastCoordinate; // last coordinate (x+y) that was set, used to reduce redundant pixel writes\nstatic uint16_t perPixelX, perPixelY; // scaling factors when upscaling\n\nvoid screenClearCallback(void) {\n  activeSeg->fill(0);\n}\n\n// this callback runs when the decoder has finished painting all pixels\nvoid updateScreenCallback(void) {\n  // perfect time for adding blur\n  if (activeSeg->intensity > 1) {\n    uint8_t blurAmount = activeSeg->intensity;\n    if ((blurAmount < 24) && (activeSeg->is2D())) activeSeg->blurRows(activeSeg->intensity);  // some blur - fast\n    else activeSeg->blur(blurAmount);                                                         // more blur - slower\n  }\n  lastCoordinate = -1; // invalidate last position\n}\n\n// note: GifDecoder drawing is done top right to bottom left, line by line\n\n// 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\nvoid drawPixelCallbackNoScale(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) {\n  activeSeg->setPixelColor(y * gifWidth + x, red, green, blue);\n}\n\nvoid drawPixelCallback1D(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) {\n  // 1D strip: load pixel-by-pixel left to right, top to bottom (0/0 = top-left in gifs)\n  int totalImgPix = (int)gifWidth * gifHeight;\n  int start =  ((int)y * gifWidth + (int)x) * activeSeg->vLength() / totalImgPix; // simple nearest-neighbor scaling\n  if (start == lastCoordinate) return; // skip setting same coordinate again\n  lastCoordinate = start;\n  for (int i = 0; i < perPixelX; i++) {\n    activeSeg->setPixelColor(start + i, red, green, blue);\n  }\n}\n\nvoid drawPixelCallback2D(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) {\n  // simple nearest-neighbor scaling\n  int outY = (int)y * activeSeg->vHeight() / gifHeight;\n  int outX = (int)x * activeSeg->vWidth()  / gifWidth;\n  // Pack coordinates uniquely: outY into upper 16 bits, outX into lower 16 bits\n  if (((outY << 16) | outX) == lastCoordinate) return; // skip setting same coordinate again\n  lastCoordinate = (outY << 16) | outX; // since input is a \"scanline\" this is sufficient to identify a \"unique\" coordinate\n  // set multiple pixels if upscaling\n  for (int i = 0; i < perPixelX; i++) {\n    for (int j = 0; j < perPixelY; j++) {\n      activeSeg->setPixelColorXY(outX + i, outY + j, red, green, blue);\n    }\n  }\n}\n\n#define IMAGE_ERROR_NONE 0\n#define IMAGE_ERROR_NO_NAME 1\n#define IMAGE_ERROR_SEG_LIMIT 2\n#define IMAGE_ERROR_UNSUPPORTED_FORMAT 3\n#define IMAGE_ERROR_FILE_MISSING 4\n#define IMAGE_ERROR_DECODER_ALLOC 5\n#define IMAGE_ERROR_GIF_DECODE 6\n#define IMAGE_ERROR_FRAME_DECODE 7\n#define IMAGE_ERROR_WAITING 254\n#define IMAGE_ERROR_PREV 255\n\n// renders an image (.gif only; .bmp and .fseq to be added soon) from FS to a segment\nbyte renderImageToSegment(Segment &seg) {\n  if (!seg.name) return IMAGE_ERROR_NO_NAME;\n  // disable during effect transition, causes flickering, multiple allocations and depending on image, part of old FX remaining\n  //if (seg.mode != seg.currentMode()) return IMAGE_ERROR_WAITING;\n  if (activeSeg && activeSeg != &seg) {            // only one segment at a time\n    if (!seg.isActive()) return IMAGE_ERROR_SEG_LIMIT; // sanity check: calling segment must be active\n    if (gifDecodeFailed || !activeSeg->isActive())     // decoder failed, or last segment became inactive\n      endImagePlayback(activeSeg);                     // => allow takeover but clean up first\n    else\n      return IMAGE_ERROR_SEG_LIMIT;                \n  }\n\n  activeSeg = &seg;\n\n  if (strncmp(lastFilename +1, seg.name, WLED_MAX_SEGNAME_LEN) != 0) { // segment name changed, load new image\n    strcpy(lastFilename, \"/\");  // filename always starts with '/'\n    strncpy(lastFilename +1, seg.name, WLED_MAX_SEGNAME_LEN);\n    lastFilename[WLED_MAX_SEGNAME_LEN+1] ='\\0';     // ensure proper string termination when segment name was truncated\n    gifDecodeFailed = false;\n    size_t fnameLen = strlen(lastFilename);\n    if ((fnameLen < 4) || strcmp(lastFilename + fnameLen - 4, \".gif\") != 0) { // empty segment name, name too short, or name not ending in .gif\n      gifDecodeFailed = true;\n      DEBUG_PRINTF_P(PSTR(\"GIF decoder unsupported file: %s\\n\"), lastFilename);\n      return IMAGE_ERROR_UNSUPPORTED_FORMAT;\n    }\n    if (file) file.close();\n    if (!openGif(lastFilename)) {\n      gifDecodeFailed = true;\n      DEBUG_PRINTF_P(PSTR(\"GIF file not found: %s\\n\"), lastFilename);\n      return IMAGE_ERROR_FILE_MISSING;\n    }\n    lastCoordinate = -1;\n    decoder.setScreenClearCallback(screenClearCallback);\n    decoder.setUpdateScreenCallback(updateScreenCallback);\n    decoder.setDrawPixelCallback(drawPixelCallbackNoScale); //  default: use \"fast path\" callback without scaling\n    decoder.setFileSeekCallback(fileSeekCallback);\n    decoder.setFilePositionCallback(filePositionCallback);\n    decoder.setFileReadCallback(fileReadCallback);\n    decoder.setFileReadBlockCallback(fileReadBlockCallback);\n    decoder.setFileSizeCallback(fileSizeCallback);\n#if __cpp_exceptions // use exception handler if we can (some targets don't support exceptions)\n    try {\n#endif\n    decoder.alloc(); // this function may throw out-of memory and cause a crash\n#if __cpp_exceptions\n    } catch (...) {  // if we arrive here, the decoder has thrown an OOM exception\n      gifDecodeFailed = true;\n      errorFlag = ERR_NORAM_PX;\n      DEBUG_PRINTLN(F(\"\\nGIF decoder out of memory. Please try a smaller image file.\\n\"));\n      return IMAGE_ERROR_DECODER_ALLOC;\n      // decoder cleanup (hi @coderabbitai): No additonal cleanup necessary - decoder.alloc() ultimately uses \"new AnimatedGIF\". \n      // 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.\n    }\n#endif\n    DEBUG_PRINTLN(F(\"Starting decoding\"));\n    int decoderError = decoder.startDecoding();\n    if(decoderError < 0) {\n      DEBUG_PRINTF_P(PSTR(\"GIF Decoding error %d in startDecoding().\\n\"), decoderError);\n      errorFlag = ERR_NORAM_PX;\n      gifDecodeFailed = true;\n      return IMAGE_ERROR_GIF_DECODE;\n    }\n    DEBUG_PRINTLN(F(\"Decoding started\"));\n    // after startDecoding, we can get GIF size, update static variables and callbacks\n    decoder.getSize(&gifWidth, &gifHeight);\n    if (gifWidth == 0 || gifHeight == 0) {  // bad gif size: prevent division by zero\n      gifDecodeFailed = true;\n      DEBUG_PRINTF_P(PSTR(\"Invalid GIF dimensions: %dx%d\\n\"), gifWidth, gifHeight);\n      return IMAGE_ERROR_GIF_DECODE;\n    }\n    if (activeSeg->is2D()) {\n      perPixelX   = (activeSeg->vWidth()  + gifWidth -1) / gifWidth;\n      perPixelY   = (activeSeg->vHeight() + gifHeight-1) / gifHeight;\n      if (activeSeg->vWidth() != gifWidth || activeSeg->vHeight() != gifHeight) {\n        decoder.setDrawPixelCallback(drawPixelCallback2D);        // use 2D callback with scaling\n        //DEBUG_PRINTLN(F(\"scaling image\"));\n      }\n    } else {\n      int totalImgPix = (int)gifWidth * gifHeight;\n      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)\n      perPixelX   = (activeSeg->vLength() + totalImgPix-1) / totalImgPix;\n      if (totalImgPix != activeSeg->vLength()) {\n        decoder.setDrawPixelCallback(drawPixelCallback1D);        // use 1D callback with scaling\n        //DEBUG_PRINTLN(F(\"scaling image\"));\n      }\n    }\n  }\n\n  if (gifDecodeFailed) return IMAGE_ERROR_PREV;\n  if (!file) { gifDecodeFailed = true; return IMAGE_ERROR_FILE_MISSING; }\n  //if (!decoder) { gifDecodeFailed = true; return IMAGE_ERROR_DECODER_ALLOC; }\n\n  // speed 0 = half speed, 128 = normal, 255 = full FX FPS\n  // TODO: 0 = 4x slow, 64 = 2x slow, 128 = normal, 192 = 2x fast, 255 = 4x fast\n  uint32_t wait = currentFrameDelay * 2 - seg.speed * currentFrameDelay / 128;\n\n  // TODO consider handling this on FX level with a different frametime, but that would cause slow gifs to speed up during transitions\n  if (millis() - lastFrameDisplayTime < wait) return IMAGE_ERROR_WAITING;\n\n  int result = decoder.decodeFrame(false);\n  if (result < 0) {\n    DEBUG_PRINTF_P(PSTR(\"GIF Decoding error %d in decodeFrame().\\n\"), result);\n    gifDecodeFailed = true;\n    return IMAGE_ERROR_FRAME_DECODE;\n  }\n\n  currentFrameDelay = decoder.getFrameDelay_ms();\n  unsigned long tooSlowBy = (millis() - lastFrameDisplayTime) - wait; // if last frame was longer than intended, compensate\n  currentFrameDelay = tooSlowBy > currentFrameDelay ? 0 : currentFrameDelay - tooSlowBy;\n  lastFrameDisplayTime = millis();\n\n  return IMAGE_ERROR_NONE;\n}\n\nvoid endImagePlayback(Segment *seg) {\n  DEBUG_PRINTLN(F(\"Image playback end called\"));\n  if (!activeSeg || activeSeg != seg) return;\n  if (file) file.close();\n  decoder.dealloc();\n  gifDecodeFailed = false;\n  activeSeg = nullptr;\n  strcpy(lastFilename, \"/\");  // reset filename\n  gifWidth = gifHeight = 0;   // reset dimensions\n  DEBUG_PRINTLN(F(\"Image playback ended\"));\n}\n\n#endif"
  },
  {
    "path": "wled00/improv.cpp",
    "content": "#include \"wled.h\"\n\n#ifdef WLED_DEBUG_IMPROV\n  #define DIMPROV_PRINT(x) Serial.print(x)\n  #define DIMPROV_PRINTLN(x) Serial.println(x)\n  #define DIMPROV_PRINTF(x...) Serial.printf(x)\n#else\n  #define DIMPROV_PRINT(x)\n  #define DIMPROV_PRINTLN(x)\n  #define DIMPROV_PRINTF(x...)\n#endif\n\n#if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3)\n#undef WLED_DISABLE_IMPROV_WIFISCAN\n#define WLED_DISABLE_IMPROV_WIFISCAN\n#endif\n\n#define IMPROV_VERSION 1\n// forward declarations\nstatic void parseWiFiCommand(char* rpcData);\n\nenum ImprovPacketType {\n  Current_State = 0x01,\n  Error_State = 0x02,\n  RPC_Command = 0x03,\n  RPC_Response = 0x04\n};\n\nenum ImprovPacketByte {\n  Version = 6,\n  PacketType = 7,\n  Length = 8,\n  RPC_CommandType = 9\n};\n\n#ifndef WLED_DISABLE_IMPROV_WIFISCAN\nstatic bool improvWifiScanRunning = false;\n#endif\n\n//blocking function to parse an Improv Serial packet\nvoid handleImprovPacket() {\n  uint8_t header[6] = {'I','M','P','R','O','V'};\n\n  bool timeout = false;\n  unsigned waitTime = 25;\n  unsigned packetByte = 0;\n  unsigned packetLen = 9;\n  unsigned checksum = 0;\n\n  unsigned rpcCommandType = 0;\n  char rpcData[128];\n  rpcData[0] = 0;\n\n  while (!timeout) {\n    if (Serial.available() < 1) {\n      delay(1);\n      waitTime--;\n      if (!waitTime) timeout = true;\n      continue;\n    }\n    byte next = Serial.read();\n\n    DIMPROV_PRINT(\"Received improv byte: \"); DIMPROV_PRINTF(\"%x\\r\\n\",next);\n\n    switch (packetByte) {\n      case ImprovPacketByte::Version: {\n        if (next != IMPROV_VERSION) {\n          DIMPROV_PRINTLN(F(\"Invalid version\"));\n          return;\n        }\n        break;\n      }\n      case ImprovPacketByte::PacketType: {\n        if (next != ImprovPacketType::RPC_Command) {\n          DIMPROV_PRINTF(\"Non RPC-command improv packet type %i\\n\",next);\n          return;\n        }\n        if (!improvActive) improvActive = 1;\n        break;\n      }\n      case ImprovPacketByte::Length: packetLen = 9 + next; break;\n      case ImprovPacketByte::RPC_CommandType: rpcCommandType = next; break;\n      default: {\n        if (packetByte >= packetLen) { //end of packet, check checksum match\n\n          if (checksum != next) {\n            DIMPROV_PRINTF(\"Got RPC checksum %i, expected %i\",next,checksum);\n            sendImprovStateResponse(0x01, true);\n            return;\n          }\n\n          switch (rpcCommandType) {\n            case ImprovRPCType::Command_Wifi: parseWiFiCommand(rpcData); break;\n            case ImprovRPCType::Request_State: {\n              unsigned improvState = 0x02; //authorized\n              if (WLED_WIFI_CONFIGURED) improvState = 0x03; //provisioning\n              if (Network.isConnected()) improvState = 0x04; //provisioned\n              sendImprovStateResponse(improvState, false);\n              if (improvState == 0x04) sendImprovIPRPCResult(ImprovRPCType::Request_State);\n              break;\n            }\n            case ImprovRPCType::Request_Info: sendImprovInfoResponse(); break;\n            #ifndef WLED_DISABLE_IMPROV_WIFISCAN\n            case ImprovRPCType::Request_Scan: startImprovWifiScan(); break;\n            #endif\n            default: {\n              DIMPROV_PRINTF(\"Unknown RPC command %i\\n\",next);\n              sendImprovStateResponse(0x02, true);\n            }\n          }\n          return;\n        }\n        if (packetByte < 6) { //check header\n          if (next != header[packetByte]) {\n            DIMPROV_PRINTLN(F(\"Invalid improv header\"));\n            return;\n          }\n        } else if (packetByte > 9) { //RPC data\n          rpcData[packetByte - 10] = next;\n          if (packetByte > 137) return; //prevent buffer overflow\n        }\n      }\n    }\n\n    checksum += next;\n    checksum &= 0xFF;\n    packetByte++;\n  }\n}\n\nvoid sendImprovStateResponse(uint8_t state, bool error) {\n  if (!error && improvError > 0 && improvError < 3) sendImprovStateResponse(0x00, true);\n  if (error) improvError = state;\n  char out[11] = {'I','M','P','R','O','V'};\n  out[6] = IMPROV_VERSION;\n  out[7] = error? ImprovPacketType::Error_State : ImprovPacketType::Current_State;\n  out[8] = 1;\n  out[9] = state;\n\n  unsigned checksum = 0;\n  for (unsigned i = 0; i < 10; i++) checksum += out[i];\n  out[10] = checksum;\n  Serial.write((uint8_t*)out, 11);\n  Serial.write('\\n');\n}\n\n// used by sendImprovIPRPCResult(), sendImprovInfoResponse(), and handleImprovWifiScan()\nvoid sendImprovRPCResult(ImprovRPCType type, uint8_t n_strings, const char **strings) {\n  if (improvError > 0 && improvError < 3) sendImprovStateResponse(0x00, true);\n  unsigned packetLen = 12;\n  char out[256] = {'I','M','P','R','O','V'};\n  out[6] = IMPROV_VERSION;\n  out[7] = ImprovPacketType::RPC_Response;\n  //out[8] = 2; //Length (set below)\n  out[9] = type;\n  //out[10] = 0; //Data len (set below)\n  unsigned pos = 11;\n\n  for (unsigned s = 0; s < n_strings; s++) {\n    size_t len = strlen(strings[s]);\n    if (pos + len > 254) continue; // simple buffer overflow guard\n    out[pos++] = len;\n    strcpy(out + pos, strings[s]);\n    pos += len;\n  }\n\n  packetLen = pos  +1;\n  out[8]    = pos  -9; // Length of packet (excluding first 9 header bytes and final checksum byte)\n  out[10]   = pos -11; // Data len\n\n  unsigned checksum = 0;\n  for (unsigned i = 0; i < packetLen -1; i++) checksum += out[i];\n  out[packetLen -1] = checksum;\n  Serial.write((uint8_t*)out, packetLen);\n  Serial.write('\\n');\n  DIMPROV_PRINT(\"RPC result checksum\");\n  DIMPROV_PRINTLN(checksum);\n}\n\nvoid sendImprovIPRPCResult(ImprovRPCType type) {\n  if (Network.isConnected())\n  {\n    char urlStr[64];\n    IPAddress localIP = Network.localIP();\n    unsigned len = sprintf(urlStr, \"http://%d.%d.%d.%d\", localIP[0], localIP[1], localIP[2], localIP[3]);\n    if (len > 24) return; //sprintf fail?\n    const char *str[1] = {urlStr};\n    sendImprovRPCResult(type, 1, str);\n  } else {\n    sendImprovRPCResult(type, 0);\n  }\n\n  improvActive = 1; //no longer provisioning\n}\n\nvoid sendImprovInfoResponse() {\n  char bString[32];\n  #ifdef ESP8266\n  strcpy(bString, \"esp8266\");\n  #else // ESP32\n  strncpy(bString, ESP.getChipModel(), 31);\n  #if CONFIG_IDF_TARGET_ESP32\n  bString[5] = '\\0'; // disregard chip revision for classic ESP32\n  #else\n  bString[31] = '\\0'; // just in case\n  #endif\n  strlwr(bString);\n  #endif\n  //Use serverDescription if it has been changed from the default \"WLED\", else mDNS name\n  bool useMdnsName = (strcmp(serverDescription, \"WLED\") == 0 && strlen(cmDNS) > 0);\n  char vString[32];\n  sprintf_P(vString, PSTR(\"%s/%i\"), versionString, VERSION);\n  const char *str[4] = {\"WLED\", vString, bString, useMdnsName ? cmDNS : serverDescription};\n\n  sendImprovRPCResult(ImprovRPCType::Request_Info, 4, str);\n}\n\n#ifndef WLED_DISABLE_IMPROV_WIFISCAN\nvoid startImprovWifiScan() {\n  if (improvWifiScanRunning) return;\n  WiFi.scanNetworks(true);\n  improvWifiScanRunning = true;\n}\n\nvoid handleImprovWifiScan() {\n  if (!improvWifiScanRunning) return;\n  int16_t status = WiFi.scanComplete();\n  if (status == WIFI_SCAN_RUNNING) return;\n  // here scan completed or failed (-2)\n  improvWifiScanRunning = false;\n\n  for (int i = 0; i < status; i++) {\n    char rssiStr[8];\n    sprintf(rssiStr, \"%d\", WiFi.RSSI(i));\n    #ifdef ESP8266\n    bool isOpen = WiFi.encryptionType(i) == ENC_TYPE_NONE;\n    #else\n    bool isOpen = WiFi.encryptionType(i) == WIFI_AUTH_OPEN;\n    #endif\n\n    char ssidStr[33];\n    strcpy(ssidStr, WiFi.SSID(i).c_str());\n    const char *str[3] = {ssidStr, rssiStr, isOpen ? \"NO\":\"YES\"};\n    sendImprovRPCResult(ImprovRPCType::Request_Scan, 3, str);\n  }\n  sendImprovRPCResult(ImprovRPCType::Request_Scan, 0);\n\n  WiFi.scanDelete();\n}\n#else\nvoid startImprovWifiScan() {}\nvoid handleImprovWifiScan() {}\n#endif\n\nstatic void parseWiFiCommand(char* rpcData) {\n  unsigned len = rpcData[0];\n  if (!len || len > 126) return;\n\n  unsigned ssidLen = rpcData[1];\n  if (ssidLen > len -1 || ssidLen > 32) return;\n  memset(multiWiFi[0].clientSSID, 0, 32);\n  memcpy(multiWiFi[0].clientSSID, rpcData+2, ssidLen);\n\n  memset(multiWiFi[0].clientPass, 0, 64);\n  if (len > ssidLen +1) {\n    unsigned passLen = rpcData[2+ssidLen];\n    memset(multiWiFi[0].clientPass, 0, 64);\n    memcpy(multiWiFi[0].clientPass, rpcData+3+ssidLen, passLen);\n  }\n\n  sendImprovStateResponse(0x03); //provisioning\n  improvActive = 2;\n\n  forceReconnect = true;\n  serializeConfigToFS();\n}"
  },
  {
    "path": "wled00/ir.cpp",
    "content": "#include \"wled.h\"\n\n#ifndef WLED_DISABLE_INFRARED\n#include \"ir_codes.h\"\n\n/*\n * Infrared sensor support for several generic RGB remotes and custom JSON remote\n */\n\nstatic IRrecv* irrecv;\nstatic decode_results results;\nstatic unsigned long irCheckedTime = 0;\nstatic uint32_t lastValidCode = 0;\nstatic byte lastRepeatableAction = ACTION_NONE;\nstatic uint8_t lastRepeatableValue = 0;\nstatic uint16_t irTimesRepeated = 0;\nstatic uint8_t lastIR6ColourIdx = 0;\n\n\n// brightnessSteps: a static array of brightness levels following a geometric\n// progression.  Can be generated from the following Python, adjusting the\n// arbitrary 4.5 value to taste:\n//\n// def values(level):\n//     while level >= 5:\n//         yield int(level)\n//         level -= level / 4.5\n// result = [v for v in reversed(list(values(255)))]\n// print(\"%d values: %s\" % (len(result), result))\n//\n// It would be hard to maintain repeatable steps if calculating this on the fly.\nconst uint8_t brightnessSteps[] = {\n  5, 7, 9, 12, 16, 20, 26, 34, 43, 56, 72, 93, 119, 154, 198, 255\n};\nconst size_t numBrightnessSteps = sizeof(brightnessSteps) / sizeof(uint8_t);\n\n// increment `bri` to the next `brightnessSteps` value\nstatic void incBrightness()\n{\n  // dumb incremental search is efficient enough for so few items\n  for (unsigned index = 0; index < numBrightnessSteps; ++index)\n  {\n    if (brightnessSteps[index] > bri)\n    {\n      bri = brightnessSteps[index];\n      lastRepeatableAction = ACTION_BRIGHT_UP;\n      break;\n    }\n  }\n}\n\n// decrement `bri` to the next `brightnessSteps` value\nstatic void decBrightness()\n{\n  // dumb incremental search is efficient enough for so few items\n  for (int index = numBrightnessSteps - 1; index >= 0; --index)\n  {\n    if (brightnessSteps[index] < bri)\n    {\n      bri = brightnessSteps[index];\n      lastRepeatableAction = ACTION_BRIGHT_DOWN;\n      break;\n    }\n  }\n}\n\nstatic void presetFallback(uint8_t presetID, uint8_t effectID, uint8_t paletteID)\n{\n  applyPresetWithFallback(presetID, CALL_MODE_BUTTON_PRESET, effectID, paletteID);\n}\n\nstatic byte relativeChange(byte property, int8_t amount, byte lowerBoundary = 0, byte higherBoundary = 0xFF)\n{\n  int16_t new_val = (int16_t) property + amount;\n  if (lowerBoundary >= higherBoundary) return property;\n  if (new_val > higherBoundary) new_val = higherBoundary;\n  if (new_val < lowerBoundary)  new_val = lowerBoundary;\n  return (byte)constrain(new_val, 0, 255);\n}\n\nstatic void changeEffect(uint8_t fx)\n{\n  if (irApplyToAllSelected) {\n    for (unsigned i = 0; i < strip.getSegmentsNum(); i++) {\n      Segment& seg = strip.getSegment(i);\n      if (!seg.isActive() || !seg.isSelected()) continue;\n      seg.setMode(fx);\n    }\n    setValuesFromFirstSelectedSeg();\n  } else {\n    strip.getSegment(strip.getMainSegmentId()).setMode(fx);\n    setValuesFromMainSeg();\n  }\n  stateChanged = true;\n}\n\nstatic void changePalette(uint8_t pal)\n{\n  if (irApplyToAllSelected) {\n    for (unsigned i = 0; i < strip.getSegmentsNum(); i++) {\n      Segment& seg = strip.getSegment(i);\n      if (!seg.isActive() || !seg.isSelected()) continue;\n      seg.setPalette(pal);\n    }\n    setValuesFromFirstSelectedSeg();\n  } else {\n    strip.getMainSegment().palette = pal;\n    setValuesFromMainSeg();\n  }\n  stateChanged = true;\n}\n\nstatic void changeEffectSpeed(int8_t amount)\n{\n  if (effectCurrent != 0) {\n    int16_t new_val = (int16_t) effectSpeed + amount;\n    effectSpeed = (byte)constrain(new_val,0,255);\n    if (irApplyToAllSelected) {\n      for (unsigned i = 0; i < strip.getSegmentsNum(); i++) {\n        Segment& seg = strip.getSegment(i);\n        if (!seg.isActive() || !seg.isSelected()) continue;\n        seg.speed = effectSpeed;\n      }\n      setValuesFromFirstSelectedSeg();\n    } else {\n      strip.getMainSegment().speed = effectSpeed;\n      setValuesFromMainSeg();\n    }\n  } else { // if Effect == \"solid Color\", change the hue of the primary color\n    Segment& sseg = irApplyToAllSelected ? strip.getFirstSelectedSeg() : strip.getMainSegment();\n    CRGB fastled_col = CRGB(sseg.colors[0]);\n    CHSV prim_hsv = rgb2hsv(fastled_col);\n    int16_t new_val = (int16_t)prim_hsv.h + amount;\n    if (new_val > 255) new_val -= 255;  // roll-over if  bigger than 255\n    if (new_val < 0) new_val += 255;    // roll-over if smaller than 0\n    prim_hsv.h = (byte)new_val;\n    hsv2rgb_rainbow(prim_hsv, fastled_col);\n    if (irApplyToAllSelected) {\n      for (unsigned i = 0; i < strip.getSegmentsNum(); i++) {\n        Segment& seg = strip.getSegment(i);\n        if (!seg.isActive() || !seg.isSelected()) continue;\n        seg.colors[0] = RGBW32(fastled_col.red, fastled_col.green, fastled_col.blue, W(sseg.colors[0]));\n      }\n      setValuesFromFirstSelectedSeg();\n    } else {\n      strip.getMainSegment().colors[0] = RGBW32(fastled_col.red, fastled_col.green, fastled_col.blue, W(sseg.colors[0]));\n      setValuesFromMainSeg();\n    }\n  }\n  stateChanged = true;\n\n  if(amount > 0) lastRepeatableAction = ACTION_SPEED_UP;\n  if(amount < 0) lastRepeatableAction = ACTION_SPEED_DOWN;\n  lastRepeatableValue = amount;\n}\n\nstatic void changeEffectIntensity(int8_t amount)\n{\n  if (effectCurrent != 0) {\n    int16_t new_val = (int16_t) effectIntensity + amount;\n    effectIntensity = (byte)constrain(new_val,0,255);\n    if (irApplyToAllSelected) {\n      for (unsigned i = 0; i < strip.getSegmentsNum(); i++) {\n        Segment& seg = strip.getSegment(i);\n        if (!seg.isActive() || !seg.isSelected()) continue;\n        seg.intensity = effectIntensity;\n      }\n      setValuesFromFirstSelectedSeg();\n    } else {\n      strip.getMainSegment().speed = effectIntensity;\n      setValuesFromMainSeg();\n    }\n  } else { // if Effect == \"solid Color\", change the saturation of the primary color\n    Segment& sseg = irApplyToAllSelected ? strip.getFirstSelectedSeg() : strip.getMainSegment();\n    CRGB fastled_col = CRGB(sseg.colors[0]);\n    CHSV prim_hsv = rgb2hsv(fastled_col);\n    int16_t new_val = (int16_t) prim_hsv.s + amount;\n    prim_hsv.s = (byte)constrain(new_val,0,255);  // constrain to 0-255\n    hsv2rgb_rainbow(prim_hsv, fastled_col);\n    if (irApplyToAllSelected) {\n      for (unsigned i = 0; i < strip.getSegmentsNum(); i++) {\n        Segment& seg = strip.getSegment(i);\n        if (!seg.isActive() || !seg.isSelected()) continue;\n        seg.colors[0] = RGBW32(fastled_col.red, fastled_col.green, fastled_col.blue, W(sseg.colors[0]));\n      }\n      setValuesFromFirstSelectedSeg();\n    } else {\n      strip.getMainSegment().colors[0] = RGBW32(fastled_col.red, fastled_col.green, fastled_col.blue, W(sseg.colors[0]));\n      setValuesFromMainSeg();\n    }\n  }\n  stateChanged = true;\n\n  if(amount > 0) lastRepeatableAction = ACTION_INTENSITY_UP;\n  if(amount < 0) lastRepeatableAction = ACTION_INTENSITY_DOWN;\n  lastRepeatableValue = amount;\n}\n\nstatic void changeColor(uint32_t c, int16_t cct=-1)\n{\n  if (irApplyToAllSelected) {\n    // main segment may not be selected!\n    for (unsigned i = 0; i < strip.getSegmentsNum(); i++) {\n      Segment& seg = strip.getSegment(i);\n      if (!seg.isActive() || !seg.isSelected()) continue;\n      byte capabilities = seg.getLightCapabilities();\n      uint32_t mask = 0;\n      bool isRGB   = GET_BIT(capabilities, 0);  // is segment RGB capable\n      bool hasW    = GET_BIT(capabilities, 1);  // do we have white/CCT channel\n      bool isCCT   = GET_BIT(capabilities, 2);  // is segment CCT capable\n      bool wSlider = GET_BIT(capabilities, 3);  // is white auto calculated (white slider NOT shown in UI)\n      if (isRGB) mask |= 0x00FFFFFF; // RGB\n      if (hasW)  mask |= 0xFF000000; // white\n      if (hasW && !wSlider && (c & 0xFF000000)) { // segment has white channel & white channel is auto calculated & white specified\n        seg.setColor(0, c | 0xFFFFFF); // for accurate/brighter mode we fake white (since button may not set white color to 0xFFFFFF)\n      } else if (c & mask) seg.setColor(0, c & mask); // only apply if not black\n      if (isCCT && cct >= 0) seg.setCCT(cct);\n    }\n    setValuesFromFirstSelectedSeg();\n  } else {\n    byte i = strip.getMainSegmentId();\n    Segment& seg = strip.getSegment(i);\n    byte capabilities = seg.getLightCapabilities();\n    uint32_t mask = 0;\n    bool isRGB   = GET_BIT(capabilities, 0);  // is segment RGB capable\n    bool hasW    = GET_BIT(capabilities, 1);  // do we have white/CCT channel\n    bool isCCT   = GET_BIT(capabilities, 2);  // is segment CCT capable\n    bool wSlider = GET_BIT(capabilities, 3);  // is white auto calculated (white slider NOT shown in UI)\n    if (isRGB) mask |= 0x00FFFFFF; // RGB\n    if (hasW)  mask |= 0xFF000000; // white\n    if (hasW && !wSlider && (c & 0xFF000000)) { // segment has white channel & white channel is auto calculated & white specified\n      seg.setColor(0, c | 0xFFFFFF); // for accurate/brighter mode we fake white (since button may not set white color to 0xFFFFFF)\n    } else if (c & mask) seg.setColor(0, c & mask); // only apply if not black\n    if (isCCT && cct >= 0) seg.setCCT(cct);\n    setValuesFromMainSeg();\n  }\n  stateChanged = true;\n}\n\nstatic void changeWhite(int8_t amount, int16_t cct=-1)\n{\n  Segment& seg = irApplyToAllSelected ? strip.getFirstSelectedSeg() : strip.getMainSegment();\n  byte r = R(seg.colors[0]);\n  byte g = G(seg.colors[0]);\n  byte b = B(seg.colors[0]);\n  byte w = relativeChange(W(seg.colors[0]), amount, 5);\n  changeColor(RGBW32(r, g, b, w), cct);\n}\n\nstatic void decodeIR24(uint32_t code)\n{\n  switch (code) {\n    case IR24_BRIGHTER  : incBrightness();                                         break;\n    case IR24_DARKER    : decBrightness();                                         break;\n    case IR24_OFF    : if (bri > 0) briLast = bri; bri = 0;                        break;\n    case IR24_ON        : bri = briLast;                                           break;\n    case IR24_RED       : changeColor(COLOR_RED);                                  break;\n    case IR24_REDDISH   : changeColor(COLOR_REDDISH);                              break;\n    case IR24_ORANGE    : changeColor(COLOR_ORANGE);                               break;\n    case IR24_YELLOWISH : changeColor(COLOR_YELLOWISH);                            break;\n    case IR24_YELLOW    : changeColor(COLOR_YELLOW);                               break;\n    case IR24_GREEN     : changeColor(COLOR_GREEN);                                break;\n    case IR24_GREENISH  : changeColor(COLOR_GREENISH);                             break;\n    case IR24_TURQUOISE : changeColor(COLOR_TURQUOISE);                            break;\n    case IR24_CYAN      : changeColor(COLOR_CYAN);                                 break;\n    case IR24_AQUA      : changeColor(COLOR_AQUA);                                 break;\n    case IR24_BLUE      : changeColor(COLOR_BLUE);                                 break;\n    case IR24_DEEPBLUE  : changeColor(COLOR_DEEPBLUE);                             break;\n    case IR24_PURPLE    : changeColor(COLOR_PURPLE);                               break;\n    case IR24_MAGENTA   : changeColor(COLOR_MAGENTA);                              break;\n    case IR24_PINK      : changeColor(COLOR_PINK);                                 break;\n    case IR24_WHITE     : changeColor(COLOR_WHITE); changeEffect(FX_MODE_STATIC);  break;\n    case IR24_FLASH     : presetFallback(1, FX_MODE_COLORTWINKLE, effectPalette);  break;\n    case IR24_STROBE    : presetFallback(2, FX_MODE_RAINBOW_CYCLE, effectPalette); break;\n    case IR24_FADE      : presetFallback(3, FX_MODE_BREATH, effectPalette);        break;\n    case IR24_SMOOTH    : presetFallback(4, FX_MODE_RAINBOW, effectPalette);       break;\n    default: return;\n  }\n  lastValidCode = code;\n}\n\nstatic void decodeIR24OLD(uint32_t code)\n{\n  switch (code) {\n    case IR24_OLD_BRIGHTER  : incBrightness();                                        break;\n    case IR24_OLD_DARKER    : decBrightness();                                        break;\n    case IR24_OLD_OFF       : if (bri > 0) briLast = bri; bri = 0;                    break;\n    case IR24_OLD_ON        : bri = briLast;                                          break;\n    case IR24_OLD_RED       : changeColor(COLOR_RED);                                 break;\n    case IR24_OLD_REDDISH   : changeColor(COLOR_REDDISH);                             break;\n    case IR24_OLD_ORANGE    : changeColor(COLOR_ORANGE);                              break;\n    case IR24_OLD_YELLOWISH : changeColor(COLOR_YELLOWISH);                           break;\n    case IR24_OLD_YELLOW    : changeColor(COLOR_YELLOW);                              break;\n    case IR24_OLD_GREEN     : changeColor(COLOR_GREEN);                               break;\n    case IR24_OLD_GREENISH  : changeColor(COLOR_GREENISH);                            break;\n    case IR24_OLD_TURQUOISE : changeColor(COLOR_TURQUOISE);                           break;\n    case IR24_OLD_CYAN      : changeColor(COLOR_CYAN);                                break;\n    case IR24_OLD_AQUA      : changeColor(COLOR_AQUA);                                break;\n    case IR24_OLD_BLUE      : changeColor(COLOR_BLUE);                                break;\n    case IR24_OLD_DEEPBLUE  : changeColor(COLOR_DEEPBLUE);                            break;\n    case IR24_OLD_PURPLE    : changeColor(COLOR_PURPLE);                              break;\n    case IR24_OLD_MAGENTA   : changeColor(COLOR_MAGENTA);                             break;\n    case IR24_OLD_PINK      : changeColor(COLOR_PINK);                                break;\n    case IR24_OLD_WHITE     : changeColor(COLOR_WHITE); changeEffect(FX_MODE_STATIC); break;\n    case IR24_OLD_FLASH     : presetFallback(1, FX_MODE_COLORTWINKLE, 0);             break;\n    case IR24_OLD_STROBE    : presetFallback(2, FX_MODE_RAINBOW_CYCLE, 0);            break;\n    case IR24_OLD_FADE      : presetFallback(3, FX_MODE_BREATH, 0);                   break;\n    case IR24_OLD_SMOOTH    : presetFallback(4, FX_MODE_RAINBOW, 0);                  break;\n    default: return;\n  }\n  lastValidCode = code;\n}\n\nstatic void decodeIR24CT(uint32_t code)\n{\n  switch (code) {\n    case IR24_CT_BRIGHTER   : incBrightness();                     break;\n    case IR24_CT_DARKER     : decBrightness();                     break;\n    case IR24_CT_OFF        : if (bri > 0) briLast = bri; bri = 0; break;\n    case IR24_CT_ON         : bri = briLast;                       break;\n    case IR24_CT_RED        : changeColor(COLOR_RED);              break;\n    case IR24_CT_REDDISH    : changeColor(COLOR_REDDISH);          break;\n    case IR24_CT_ORANGE     : changeColor(COLOR_ORANGE);           break;\n    case IR24_CT_YELLOWISH  : changeColor(COLOR_YELLOWISH);        break;\n    case IR24_CT_YELLOW     : changeColor(COLOR_YELLOW);           break;\n    case IR24_CT_GREEN      : changeColor(COLOR_GREEN);            break;\n    case IR24_CT_GREENISH   : changeColor(COLOR_GREENISH);         break;\n    case IR24_CT_TURQUOISE  : changeColor(COLOR_TURQUOISE);        break;\n    case IR24_CT_CYAN       : changeColor(COLOR_CYAN);             break;\n    case IR24_CT_AQUA       : changeColor(COLOR_AQUA);             break;\n    case IR24_CT_BLUE       : changeColor(COLOR_BLUE);             break;\n    case IR24_CT_DEEPBLUE   : changeColor(COLOR_DEEPBLUE);         break;\n    case IR24_CT_PURPLE     : changeColor(COLOR_PURPLE);           break;\n    case IR24_CT_MAGENTA    : changeColor(COLOR_MAGENTA);          break;\n    case IR24_CT_PINK       : changeColor(COLOR_PINK);             break;\n    case IR24_CT_COLDWHITE  : changeColor(COLOR_COLDWHITE2,                                             255); changeEffect(FX_MODE_STATIC); break;\n    case IR24_CT_WARMWHITE  : changeColor(COLOR_WARMWHITE2,                                               0); changeEffect(FX_MODE_STATIC); break;\n    case IR24_CT_CTPLUS     : changeColor(COLOR_COLDWHITE, strip.getSegment(strip.getMainSegmentId()).cct+1); changeEffect(FX_MODE_STATIC); break;\n    case IR24_CT_CTMINUS    : changeColor(COLOR_WARMWHITE, strip.getSegment(strip.getMainSegmentId()).cct-1); changeEffect(FX_MODE_STATIC); break;\n    case IR24_CT_MEMORY     : changeColor(COLOR_NEUTRALWHITE,                                           127); changeEffect(FX_MODE_STATIC); break;\n    default: return;\n  }\n  lastValidCode = code;\n}\n\nstatic void decodeIR40(uint32_t code)\n{\n  Segment& seg = irApplyToAllSelected ? strip.getFirstSelectedSeg() : strip.getMainSegment();\n  byte r = R(seg.colors[0]);\n  byte g = G(seg.colors[0]);\n  byte b = B(seg.colors[0]);\n  byte w = W(seg.colors[0]);\n  switch (code) {\n    case IR40_BPLUS        : incBrightness();                            break;\n    case IR40_BMINUS       : decBrightness();                            break;\n    case IR40_OFF          : if (bri > 0) briLast = bri; bri = 0;        break;\n    case IR40_ON           : bri = briLast;                              break;\n    case IR40_RED          : changeColor(COLOR_RED);                     break;\n    case IR40_REDDISH      : changeColor(COLOR_REDDISH);                 break;\n    case IR40_ORANGE       : changeColor(COLOR_ORANGE);                  break;\n    case IR40_YELLOWISH    : changeColor(COLOR_YELLOWISH);               break;\n    case IR40_YELLOW       : changeColor(COLOR_YELLOW);                  break;\n    case IR40_GREEN        : changeColor(COLOR_GREEN);                   break;\n    case IR40_GREENISH     : changeColor(COLOR_GREENISH);                break;\n    case IR40_TURQUOISE    : changeColor(COLOR_TURQUOISE);               break;\n    case IR40_CYAN         : changeColor(COLOR_CYAN);                    break;\n    case IR40_AQUA         : changeColor(COLOR_AQUA);                    break;\n    case IR40_BLUE         : changeColor(COLOR_BLUE);                    break;\n    case IR40_DEEPBLUE     : changeColor(COLOR_DEEPBLUE);                break;\n    case IR40_PURPLE       : changeColor(COLOR_PURPLE);                  break;\n    case IR40_MAGENTA      : changeColor(COLOR_MAGENTA);                 break;\n    case IR40_PINK         : changeColor(COLOR_PINK);                    break;\n    case IR40_WARMWHITE2   : changeColor(COLOR_WARMWHITE2,     0); changeEffect(FX_MODE_STATIC); break;\n    case IR40_WARMWHITE    : changeColor(COLOR_WARMWHITE,     63); changeEffect(FX_MODE_STATIC); break;\n    case IR40_WHITE        : changeColor(COLOR_NEUTRALWHITE, 127); changeEffect(FX_MODE_STATIC); break;\n    case IR40_COLDWHITE    : changeColor(COLOR_COLDWHITE,    191); changeEffect(FX_MODE_STATIC); break;\n    case IR40_COLDWHITE2   : changeColor(COLOR_COLDWHITE2,   255); changeEffect(FX_MODE_STATIC); break;\n    case IR40_WPLUS        : changeWhite(10);                            break;\n    case IR40_WMINUS       : changeWhite(-10);                           break;\n    case IR40_WOFF         : if (w) whiteLast = w; changeColor(RGBW32(r, g, b, 0));              break;\n    case IR40_WON          : changeColor(RGBW32(r, g, b, whiteLast));    break;\n    case IR40_W25          : bri = 63;                                   break;\n    case IR40_W50          : bri = 127;                                  break;\n    case IR40_W75          : bri = 191;                                  break;\n    case IR40_W100         : bri = 255;                                  break;\n    case IR40_QUICK        : changeEffectSpeed( 16);                     break;\n    case IR40_SLOW         : changeEffectSpeed(-16);                     break;\n    case IR40_JUMP7        : changeEffectIntensity( 16);                 break;\n    case IR40_AUTO         : changeEffectIntensity(-16);                 break;\n    case IR40_JUMP3        : presetFallback(1, FX_MODE_STATIC,       0); break;\n    case IR40_FADE3        : presetFallback(2, FX_MODE_BREATH,       0); break;\n    case IR40_FADE7        : presetFallback(3, FX_MODE_FIRE_FLICKER, 0); break;\n    case IR40_FLASH        : presetFallback(4, FX_MODE_RAINBOW,      0); break;\n    default: return;\n  }\n  lastValidCode = code;\n}\n\nstatic void decodeIR44(uint32_t code)\n{\n  switch (code) {\n    case IR44_BPLUS       : incBrightness();                             break;\n    case IR44_BMINUS      : decBrightness();                             break;\n    case IR44_OFF         : if (bri > 0) briLast = bri; bri = 0;         break;\n    case IR44_ON          : bri = briLast;                               break;\n    case IR44_RED         : changeColor(COLOR_RED);                      break;\n    case IR44_REDDISH     : changeColor(COLOR_REDDISH);                  break;\n    case IR44_ORANGE      : changeColor(COLOR_ORANGE);                   break;\n    case IR44_YELLOWISH   : changeColor(COLOR_YELLOWISH);                break;\n    case IR44_YELLOW      : changeColor(COLOR_YELLOW);                   break;\n    case IR44_GREEN       : changeColor(COLOR_GREEN);                    break;\n    case IR44_GREENISH    : changeColor(COLOR_GREENISH);                 break;\n    case IR44_TURQUOISE   : changeColor(COLOR_TURQUOISE);                break;\n    case IR44_CYAN        : changeColor(COLOR_CYAN);                     break;\n    case IR44_AQUA        : changeColor(COLOR_AQUA);                     break;\n    case IR44_BLUE        : changeColor(COLOR_BLUE);                     break;\n    case IR44_DEEPBLUE    : changeColor(COLOR_DEEPBLUE);                 break;\n    case IR44_PURPLE      : changeColor(COLOR_PURPLE);                   break;\n    case IR44_MAGENTA     : changeColor(COLOR_MAGENTA);                  break;\n    case IR44_PINK        : changeColor(COLOR_PINK);                     break;\n    case IR44_WHITE       : changeColor(COLOR_NEUTRALWHITE, 127); changeEffect(FX_MODE_STATIC);  break;\n    case IR44_WARMWHITE2  : changeColor(COLOR_WARMWHITE2,     0); changeEffect(FX_MODE_STATIC);  break;\n    case IR44_WARMWHITE   : changeColor(COLOR_WARMWHITE,     63); changeEffect(FX_MODE_STATIC);  break;\n    case IR44_COLDWHITE   : changeColor(COLOR_COLDWHITE,    191); changeEffect(FX_MODE_STATIC);  break;\n    case IR44_COLDWHITE2  : changeColor(COLOR_COLDWHITE2,   255); changeEffect(FX_MODE_STATIC);  break;\n    case IR44_REDPLUS     : changeEffect(relativeChange(effectCurrent,  1, 0, strip.getModeCount() -1));               break;\n    case IR44_REDMINUS    : changeEffect(relativeChange(effectCurrent, -1, 0, strip.getModeCount() -1));               break;\n    case IR44_GREENPLUS   : changePalette(relativeChange(effectPalette,  1, 0, getPaletteCount() -1)); break;\n    case IR44_GREENMINUS  : changePalette(relativeChange(effectPalette, -1, 0, getPaletteCount() -1)); break;\n    case IR44_BLUEPLUS    : changeEffectIntensity( 16);                  break;\n    case IR44_BLUEMINUS   : changeEffectIntensity(-16);                  break;\n    case IR44_QUICK       : changeEffectSpeed( 16);                      break;\n    case IR44_SLOW        : changeEffectSpeed(-16);                      break;\n    case IR44_DIY1        : presetFallback(1, FX_MODE_STATIC,        0); break;\n    case IR44_DIY2        : presetFallback(2, FX_MODE_BREATH,        0); break;\n    case IR44_DIY3        : presetFallback(3, FX_MODE_FIRE_FLICKER,  0); break;\n    case IR44_DIY4        : presetFallback(4, FX_MODE_RAINBOW,       0); break;\n    case IR44_DIY5        : presetFallback(5, FX_MODE_METEOR,        0); break;\n    case IR44_DIY6        : presetFallback(6, FX_MODE_RAIN,          0); break;\n    case IR44_AUTO        : changeEffect(FX_MODE_STATIC);                break;\n    case IR44_FLASH       : changeEffect(FX_MODE_PALETTE);               break;\n    case IR44_JUMP3       : bri = 63;                                    break;\n    case IR44_JUMP7       : bri = 127;                                   break;\n    case IR44_FADE3       : bri = 191;                                   break;\n    case IR44_FADE7       : bri = 255;                                   break;\n    default: return;\n  }\n  lastValidCode = code;\n}\n\nstatic void decodeIR21(uint32_t code)\n{\n    switch (code) {\n      case IR21_BRIGHTER:  incBrightness();                                        break;\n      case IR21_DARKER:    decBrightness();                                        break;\n      case IR21_OFF:       if (bri > 0) briLast = bri; bri = 0;                    break;\n      case IR21_ON:        bri = briLast;                                          break;\n      case IR21_RED:       changeColor(COLOR_RED);                                 break;\n      case IR21_REDDISH:   changeColor(COLOR_REDDISH);                             break;\n      case IR21_ORANGE:    changeColor(COLOR_ORANGE);                              break;\n      case IR21_YELLOWISH: changeColor(COLOR_YELLOWISH);                           break;\n      case IR21_GREEN:     changeColor(COLOR_GREEN);                               break;\n      case IR21_GREENISH:  changeColor(COLOR_GREENISH);                            break;\n      case IR21_TURQUOISE: changeColor(COLOR_TURQUOISE);                           break;\n      case IR21_CYAN:      changeColor(COLOR_CYAN);                                break;\n      case IR21_BLUE:      changeColor(COLOR_BLUE);                                break;\n      case IR21_DEEPBLUE:  changeColor(COLOR_DEEPBLUE);                            break;\n      case IR21_PURPLE:    changeColor(COLOR_PURPLE);                              break;\n      case IR21_PINK:      changeColor(COLOR_PINK);                                break;\n      case IR21_WHITE:     changeColor(COLOR_WHITE); changeEffect(FX_MODE_STATIC); break;\n      case IR21_FLASH:     presetFallback(1, FX_MODE_COLORTWINKLE,  0);            break;\n      case IR21_STROBE:    presetFallback(2, FX_MODE_RAINBOW_CYCLE, 0);            break;\n      case IR21_FADE:      presetFallback(3, FX_MODE_BREATH,        0);            break;\n      case IR21_SMOOTH:    presetFallback(4, FX_MODE_RAINBOW,       0);            break;\n      default: return;\n    }\n    lastValidCode = code;\n}\n\nstatic void decodeIR6(uint32_t code)\n{\n  switch (code) {\n    case IR6_POWER:        toggleOnOff();                                                    break;\n    case IR6_CHANNEL_UP:   incBrightness();                                                  break;\n    case IR6_CHANNEL_DOWN: decBrightness();                                                  break;\n    case IR6_VOLUME_UP:    changeEffect(relativeChange(effectCurrent, 1, 0, strip.getModeCount() -1)); break;\n    case IR6_VOLUME_DOWN:  changePalette(relativeChange(effectPalette, 1, 0, getPaletteCount() -1));\n      switch(lastIR6ColourIdx) {\n        case 0: changeColor(COLOR_RED);       break;\n        case 1: changeColor(COLOR_REDDISH);   break;\n        case 2: changeColor(COLOR_ORANGE);    break;\n        case 3: changeColor(COLOR_YELLOWISH); break;\n        case 4: changeColor(COLOR_GREEN);     break;\n        case 5: changeColor(COLOR_GREENISH);  break;\n        case 6: changeColor(COLOR_TURQUOISE); break;\n        case 7: changeColor(COLOR_CYAN);      break;\n        case 8: changeColor(COLOR_BLUE);      break;\n        case 9: changeColor(COLOR_DEEPBLUE);  break;\n        case 10:changeColor(COLOR_PURPLE);    break;\n        case 11:changeColor(COLOR_PINK);      break;\n        case 12:changeColor(COLOR_WHITE);     break;\n        default:                              break;\n      }\n      lastIR6ColourIdx++;\n      if(lastIR6ColourIdx > 12) lastIR6ColourIdx = 0;\n      break;\n    case IR6_MUTE: changeEffect(FX_MODE_STATIC); changePalette(0); changeColor(COLOR_WHITE); bri=255; break;\n    default: return;\n  }\n  lastValidCode = code;\n}\n\nstatic void decodeIR9(uint32_t code)\n{\n  switch (code) {\n    case IR9_POWER      : toggleOnOff();                                                    break;\n    case IR9_A          : presetFallback(1, FX_MODE_COLORTWINKLE, effectPalette);           break;\n    case IR9_B          : presetFallback(2, FX_MODE_RAINBOW_CYCLE, effectPalette);          break;\n    case IR9_C          : presetFallback(3, FX_MODE_BREATH, effectPalette);                 break;\n    case IR9_UP         : incBrightness();                                                  break;\n    case IR9_DOWN       : decBrightness();                                                  break;\n    case IR9_LEFT       : changeEffectSpeed(-16);                                           break;\n    case IR9_RIGHT      : changeEffectSpeed(16);                                            break;\n    case IR9_SELECT     : changeEffect(relativeChange(effectCurrent, 1, 0, strip.getModeCount() -1)); break;\n    default: return;\n  }\n  lastValidCode = code;\n}\n\n\n/*\nThis allows users to customize IR actions without the need to edit C code and compile.\nFrom the https://github.com/wled/WLED/wiki/Infrared-Control page, download the starter\nir.json file that corresponds to the number of buttons on your remote.\nMany of the remotes with the same number of buttons emit the same codes, but will have\ndifferent labels or colors. Once you edit the ir.json file, upload it to your controller\nusing the /edit page.\n\nEach key should be the hex encoded IR code. The \"cmd\" property should be the HTTP API\nor JSON API command to execute on button press. If the command contains a relative change (SI=~16),\nit will register as a repeatable command. If the command doesn't contain a \"~\" but is repeatable, add \"rpt\" property\nset to true. Other properties are ignored but having labels and positions can assist with editing\nthe json file.\n\nSample:\n{\n  \"0xFF629D\": {\"cmd\": \"T=2\", \"rpt\": true, \"label\": \"Toggle on/off\"},  // HTTP command\n  \"0xFF9867\": {\"cmd\": \"A=~16\", \"label\": \"Inc brightness\"},            // HTTP command with incrementing\n  \"0xFF38C7\": {\"cmd\": {\"bri\": 10}, \"label\": \"Dim to 10\"},             // JSON command\n  \"0xFF22DD\": {\"cmd\": \"!presetFallback\", \"PL\": 1, \"FX\": 16, \"FP\": 6,  // Custom command\n               \"label\": \"Preset 1, fallback to Saw - Party if not found\"},\n}\n*/\nstatic void decodeIRJson(uint32_t code)\n{\n  char objKey[10];\n  char fileName[16];\n  String cmdStr;\n  JsonObject fdo;\n  JsonObject jsonCmdObj;\n\n  if (!requestJSONBufferLock(JSON_LOCK_IR)) return;\n\n  sprintf_P(objKey, PSTR(\"\\\"0x%lX\\\":\"), (unsigned long)code);\n  strcpy_P(fileName, PSTR(\"/ir.json\")); // for FS.exists()\n\n  // attempt to read command from ir.json\n  // this may fail for two reasons: ir.json does not exist or IR code not found\n  // if the IR code is not found readObjectFromFile() will clean() doc JSON document\n  // so we can differentiate between the two\n  readObjectFromFile(fileName, objKey, pDoc);\n  fdo = pDoc->as<JsonObject>();\n  lastValidCode = 0;\n  if (fdo.isNull()) {\n    //the received code does not exist\n    if (!WLED_FS.exists(fileName)) errorFlag = ERR_FS_IRLOAD; //warn if IR file itself doesn't exist\n    releaseJSONBufferLock();\n    return;\n  }\n\n  cmdStr = fdo[\"cmd\"].as<String>();\n  jsonCmdObj = fdo[\"cmd\"]; //object\n\n  if (jsonCmdObj.isNull())  // we could also use: fdo[\"cmd\"].is<String>()\n  {\n    if (cmdStr.startsWith(\"!\")) {\n      // call limited set of C functions\n      if (cmdStr.startsWith(F(\"!incBri\"))) {\n        lastValidCode = code;\n        incBrightness();\n      } else if (cmdStr.startsWith(F(\"!decBri\"))) {\n        lastValidCode = code;\n        decBrightness();\n      } else if (cmdStr.startsWith(F(\"!presetF\"))) { //!presetFallback\n        uint8_t p1 = fdo[\"PL\"] | 1;\n        uint8_t p2 = fdo[\"FX\"] | hw_random8(strip.getModeCount() -1);\n        uint8_t p3 = fdo[\"FP\"] | 0;\n        presetFallback(p1, p2, p3);\n      }\n    } else {\n      // HTTP API command\n      String apireq = \"win\"; apireq += '&';                        // reduce flash string usage\n      if (cmdStr.indexOf(\"~\") > 0 || fdo[\"rpt\"]) lastValidCode = code; // repeatable action\n      if (!cmdStr.startsWith(apireq)) cmdStr = apireq + cmdStr;    // if no \"win&\" prefix\n      if (!irApplyToAllSelected && cmdStr.indexOf(F(\"SS=\"))<0) {\n        char tmp[10];\n        sprintf_P(tmp, PSTR(\"&SS=%d\"), strip.getMainSegmentId());\n        cmdStr += tmp;\n      }\n      fdo.clear();                                                 // clear JSON buffer (it is no longer needed)\n      handleSet(nullptr, cmdStr, false);                           // no stateUpdated() call here\n    }\n  } else {\n    // command is JSON object\n    if (jsonCmdObj[F(\"psave\")].isNull()) {\n      if (irApplyToAllSelected && jsonCmdObj[\"seg\"].is<JsonArray>()) {\n        JsonObject seg = jsonCmdObj[\"seg\"][0];                    // take 1st segment from array and use it to apply to all selected segments\n        seg.remove(\"id\");                                         // remove segment ID if it exists\n        jsonCmdObj[\"seg\"] = seg;                                  // replace array with object\n      }\n      deserializeState(jsonCmdObj, CALL_MODE_BUTTON_PRESET);      // **will call stateUpdated() with correct CALL_MODE**\n    } else {\n      uint8_t psave = jsonCmdObj[F(\"psave\")].as<int>();\n      char pname[33];\n      sprintf_P(pname, PSTR(\"IR Preset %d\"), psave);\n      fdo.clear();\n      if (psave > 0 && psave < 251) savePreset(psave, pname, fdo);\n    }\n  }\n  releaseJSONBufferLock();\n}\n\nstatic void applyRepeatActions()\n{\n  if (irEnabled == 8) {\n    decodeIRJson(lastValidCode);\n    stateUpdated(CALL_MODE_BUTTON_PRESET);\n    return;\n  } else switch (lastRepeatableAction) {\n    case ACTION_BRIGHT_UP :      incBrightness();                            stateUpdated(CALL_MODE_BUTTON); return;\n    case ACTION_BRIGHT_DOWN :    decBrightness();                            stateUpdated(CALL_MODE_BUTTON); return;\n    case ACTION_SPEED_UP :       changeEffectSpeed(lastRepeatableValue);     stateUpdated(CALL_MODE_BUTTON); return;\n    case ACTION_SPEED_DOWN :     changeEffectSpeed(lastRepeatableValue);     stateUpdated(CALL_MODE_BUTTON); return;\n    case ACTION_INTENSITY_UP :   changeEffectIntensity(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return;\n    case ACTION_INTENSITY_DOWN : changeEffectIntensity(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return;\n    default: break;\n  }\n  if (lastValidCode == IR40_WPLUS) {\n    changeWhite(10);\n    stateUpdated(CALL_MODE_BUTTON);\n  } else if (lastValidCode == IR40_WMINUS) {\n    changeWhite(-10);\n    stateUpdated(CALL_MODE_BUTTON);\n  } else if ((lastValidCode == IR24_ON || lastValidCode == IR40_ON) && irTimesRepeated > 7 ) {\n    nightlightActive = true;\n    nightlightStartTime = millis();\n    stateUpdated(CALL_MODE_BUTTON);\n  }\n}\n\nstatic void decodeIR(uint32_t code)\n{\n  if (code == 0xFFFFFFFF) {\n    //repeated code, continue brightness up/down\n    irTimesRepeated++;\n    applyRepeatActions();\n    return;\n  }\n  lastValidCode = 0; irTimesRepeated = 0;\n  lastRepeatableAction = ACTION_NONE;\n\n  if (irEnabled == 8) { // any remote configurable with ir.json file\n    decodeIRJson(code);\n    stateUpdated(CALL_MODE_BUTTON_PRESET);\n    return;\n  }\n  if (code > 0xFFFFFF) return; //invalid code\n\n  switch (irEnabled) {\n    case 1:\n      if (code > 0xF80000) decodeIR24OLD(code); // white 24-key remote (old) - it sends 0xFF0000 values\n      else                 decodeIR24(code);    // 24-key remote - 0xF70000 to 0xF80000\n      break;\n    case 2: decodeIR24CT(code); break; // white 24-key remote with CW, WW, CT+ and CT- keys\n    case 3: decodeIR40(code);   break; // blue  40-key remote with 25%, 50%, 75% and 100% keys\n    case 4: decodeIR44(code);   break; // white 44-key remote with color-up/down keys and DIY1 to 6 keys\n    case 5: decodeIR21(code);   break; // white 21-key remote\n    case 6: decodeIR6(code);    break; // black 6-key learning remote defaults: \"CH\" controls brightness,\n                                       // \"VOL +\" controls effect, \"VOL -\" controls colour/palette, \"MUTE\"\n                                       // sets bright plain white\n    case 7: decodeIR9(code);    break;\n    //case 8: return; // ir.json file, handled above switch statement\n  }\n\n  if (nightlightActive && bri == 0) nightlightActive = false;\n  stateUpdated(CALL_MODE_BUTTON); //for notifier, IR is considered a button input\n}\n\nvoid initIR()\n{\n  if (irEnabled > 0) {\n    irrecv = new IRrecv(irPin);\n    if (irrecv) irrecv->enableIRIn();\n  } else irrecv = nullptr;\n}\n\nvoid deInitIR()\n{\n  if (irrecv) {\n    irrecv->disableIRIn();\n    delete irrecv;\n  }\n  irrecv = nullptr;\n}\n\nvoid handleIR()\n{\n  unsigned long currentTime = millis();\n  unsigned timeDiff = currentTime - irCheckedTime;\n  if (timeDiff > 120 && irEnabled > 0 && irrecv) {\n    if (strip.isUpdating() && timeDiff < 240) return;  // be nice, but not too nice\n    irCheckedTime = currentTime;\n    if (irrecv->decode(&results)) {\n      if (results.value != 0 && serialCanTX) { // only print results if anything is received ( != 0 )\n        Serial.printf_P(PSTR(\"IR recv: 0x%lX\\n\"), (unsigned long)results.value);\n      }\n      decodeIR(results.value);\n      irrecv->resume();\n    }\n  }\n}\n\n#endif\n"
  },
  {
    "path": "wled00/ir_codes.h",
    "content": "//Infrared codes\n\n//Add your custom codes here\n#define IRCUSTOM_ONOFF  0xA55AEA15 //Pioneer RC-975R \"+FAV\" button (example)\n#define IRCUSTOM_MACRO1 0xFFFFFFFF //placeholder, will never be checked for\n\n// Default IR codes for 6-key learning remote https://www.aliexpress.com/item/4000307837886.html\n// This cheap remote has the advantage of being more powerful (longer range) than cheap credit-card remotes\n#define IR6_POWER        0xFF0FF0\n#define IR6_CHANNEL_UP   0xFF8F70\n#define IR6_CHANNEL_DOWN 0xFF4FB0\n#define IR6_VOLUME_UP    0xFFCF30\n#define IR6_VOLUME_DOWN  0xFF2FD0\n#define IR6_MUTE         0xFFAF50\n\n#define IR9_POWER       0xFF629D\n#define IR9_A           0xFF22DD\n#define IR9_B           0xFF02FD\n#define IR9_C           0xFFC23D\n#define IR9_LEFT        0xFF30CF\n#define IR9_RIGHT       0xFF7A85\n#define IR9_UP          0xFF9867\n#define IR9_DOWN        0xFF38C7\n#define IR9_SELECT      0xFF18E7\n\n//Infrared codes for 24-key remote from http://woodsgood.ca/projects/2015/02/13/rgb-led-strip-controllers-ir-codes/\n#define IR24_BRIGHTER  0xF700FF\n#define IR24_DARKER    0xF7807F\n#define IR24_OFF       0xF740BF\n#define IR24_ON        0xF7C03F\n#define IR24_RED       0xF720DF\n#define IR24_REDDISH   0xF710EF\n#define IR24_ORANGE    0xF730CF\n#define IR24_YELLOWISH 0xF708F7\n#define IR24_YELLOW    0xF728D7\n#define IR24_GREEN     0xF7A05F\n#define IR24_GREENISH  0xF7906F\n#define IR24_TURQUOISE 0xF7B04F\n#define IR24_CYAN      0xF78877\n#define IR24_AQUA      0xF7A857\n#define IR24_BLUE      0xF7609F\n#define IR24_DEEPBLUE  0xF750AF\n#define IR24_PURPLE    0xF7708F\n#define IR24_MAGENTA   0xF748B7\n#define IR24_PINK      0xF76897\n#define IR24_WHITE     0xF7E01F\n#define IR24_FLASH     0xF7D02F\n#define IR24_STROBE    0xF7F00F\n#define IR24_FADE      0xF7C837\n#define IR24_SMOOTH    0xF7E817\n\n// 24-key defs for white remote control with CW / WW / CT+ and CT- keys (from ALDI LED pillar lamp)\n#define IR24_CT_BRIGHTER   0xF700FF // BRI +\n#define IR24_CT_DARKER     0xF7807F // BRI -\n#define IR24_CT_OFF        0xF740BF // OFF\n#define IR24_CT_ON         0xF7C03F // ON\n#define IR24_CT_RED        0xF720DF // RED\n#define IR24_CT_REDDISH    0xF710EF // REDDISH\n#define IR24_CT_ORANGE     0xF730CF // ORANGE\n#define IR24_CT_YELLOWISH  0xF708F7 // YELLOWISH\n#define IR24_CT_YELLOW     0xF728D7 // YELLOW\n#define IR24_CT_GREEN      0xF7A05F // GREEN\n#define IR24_CT_GREENISH   0xF7906F // GREENISH\n#define IR24_CT_TURQUOISE  0xF7B04F // TURQUOISE\n#define IR24_CT_CYAN       0xF78877 // CYAN\n#define IR24_CT_AQUA       0xF7A857 // AQUA\n#define IR24_CT_BLUE       0xF7609F // BLUE\n#define IR24_CT_DEEPBLUE   0xF750AF // DEEPBLUE\n#define IR24_CT_PURPLE     0xF7708F // PURPLE\n#define IR24_CT_MAGENTA    0xF748B7 // MAGENTA\n#define IR24_CT_PINK       0xF76897 // PINK\n#define IR24_CT_COLDWHITE  0xF7E01F // CW\n#define IR24_CT_WARMWHITE  0xF7D02F // WW\n#define IR24_CT_CTPLUS     0xF7F00F // CT+\n#define IR24_CT_CTMINUS    0xF7C837 // CT-\n#define IR24_CT_MEMORY     0xF7E817 // MEMORY\n\n// 24-key defs for old remote control\n#define IR24_OLD_BRIGHTER  0xFF906F // Brightness Up\n#define IR24_OLD_DARKER    0xFFB847 // Brightness Down\n#define IR24_OLD_OFF       0xFFF807 // Power OFF\n#define IR24_OLD_ON        0xFFB04F // Power On\n#define IR24_OLD_RED       0xFF9867 // RED\n#define IR24_OLD_REDDISH   0xFFE817 // Light RED\n#define IR24_OLD_ORANGE    0xFF02FD // Orange\n#define IR24_OLD_YELLOWISH 0xFF50AF // Light Orange\n#define IR24_OLD_YELLOW    0xFF38C7 // YELLOW\n#define IR24_OLD_GREEN     0xFFD827 // GREEN\n#define IR24_OLD_GREENISH  0xFF48B7 // Light GREEN\n#define IR24_OLD_TURQUOISE 0xFF32CD // TURQUOISE\n#define IR24_OLD_CYAN      0xFF7887 // CYAN\n#define IR24_OLD_AQUA      0xFF28D7 // AQUA\n#define IR24_OLD_BLUE      0xFF8877 // BLUE\n#define IR24_OLD_DEEPBLUE  0xFF6897 // Dark BLUE\n#define IR24_OLD_PURPLE    0xFF20DF // PURPLE\n#define IR24_OLD_MAGENTA   0xFF708F // MAGENTA\n#define IR24_OLD_PINK      0xFFF00F // PINK\n#define IR24_OLD_WHITE     0xFFA857 // WHITE\n#define IR24_OLD_FLASH     0xFFB24D // FLASH Mode\n#define IR24_OLD_STROBE    0xFF00FF // STROBE Mode\n#define IR24_OLD_FADE      0xFF58A7 // FADE Mode\n#define IR24_OLD_SMOOTH    0xFF30CF // SMOOTH Mode\n\n// 40-key defs for blue remote control\n#define IR40_BPLUS         0xFF3AC5  //\n#define IR40_BMINUS        0xFFBA45  //\n#define IR40_OFF           0xFF827D  //\n#define IR40_ON            0xFF02FD  //\n#define IR40_RED           0xFF1AE5  //\n#define IR40_GREEN         0xFF9A65  //\n#define IR40_BLUE          0xFFA25D  //\n#define IR40_WHITE         0xFF22DD  // natural white\n#define IR40_REDDISH       0xFF2AD5  //\n#define IR40_GREENISH      0xFFAA55  //\n#define IR40_DEEPBLUE      0xFF926D  //\n#define IR40_WARMWHITE2    0xFF12ED  // warmest white\n#define IR40_ORANGE        0xFF0AF5  //\n#define IR40_TURQUOISE     0xFF8A75  //\n#define IR40_PURPLE        0xFFB24D  //\n#define IR40_WARMWHITE     0xFF32CD  // warm white\n#define IR40_YELLOWISH     0xFF38C7  //\n#define IR40_CYAN          0xFFB847  //\n#define IR40_MAGENTA       0xFF7887  //\n#define IR40_COLDWHITE     0xFFF807  // cold white\n#define IR40_YELLOW        0xFF18E7  //\n#define IR40_AQUA          0xFF9867  //\n#define IR40_PINK          0xFF58A7  //\n#define IR40_COLDWHITE2    0xFFD827  // coldest white\n#define IR40_WPLUS         0xFF28D7  // white chanel bright plus\n#define IR40_WMINUS        0xFFA857  // white chanel bright minus\n#define IR40_WOFF          0xFF6897  // white chanel on\n#define IR40_WON           0xFFE817  // white chanel off\n#define IR40_W25           0xFF08F7  // white chanel 25%\n#define IR40_W50           0xFF8877  // white chanel 50%\n#define IR40_W75           0xFF48B7  // white chanel 75%\n#define IR40_W100          0xFFC837  // white chanel 100%\n#define IR40_JUMP3         0xFF30CF  // JUMP3\n#define IR40_FADE3         0xFFB04F  // FADE3\n#define IR40_JUMP7         0xFF708F  // JUMP7\n#define IR40_QUICK         0xFFF00F  // QUICK\n#define IR40_FADE7         0xFF10EF  // FADE7\n#define IR40_FLASH         0xFF906F  // FLASH\n#define IR40_AUTO          0xFF50AF  // AUTO\n#define IR40_SLOW          0xFFD02F  // SLOW\n\n// 44-key defs\n#define IR44_BPLUS         0xFF3AC5  //\n#define IR44_BMINUS        0xFFBA45  //\n#define IR44_OFF           0xFF827D  //\n#define IR44_ON            0xFF02FD  //\n#define IR44_RED           0xFF1AE5  //\n#define IR44_GREEN         0xFF9A65  //\n#define IR44_BLUE          0xFFA25D  //\n#define IR44_WHITE         0xFF22DD  // natural white\n#define IR44_REDDISH       0xFF2AD5  //\n#define IR44_GREENISH      0xFFAA55  //\n#define IR44_DEEPBLUE      0xFF926D  //\n#define IR44_WARMWHITE2    0xFF12ED  // warmest white\n#define IR44_ORANGE        0xFF0AF5  //\n#define IR44_TURQUOISE     0xFF8A75  //\n#define IR44_PURPLE        0xFFB24D  //\n#define IR44_WARMWHITE     0xFF32CD  // warm white\n#define IR44_YELLOWISH     0xFF38C7  //\n#define IR44_CYAN          0xFFB847  //\n#define IR44_MAGENTA       0xFF7887  //\n#define IR44_COLDWHITE     0xFFF807  // cold white\n#define IR44_YELLOW        0xFF18E7  //\n#define IR44_AQUA          0xFF9867  //\n#define IR44_PINK          0xFF58A7  //\n#define IR44_COLDWHITE2    0xFFD827  // coldest white\n#define IR44_REDPLUS       0xFF28D7  //\n#define IR44_GREENPLUS     0xFFA857  //\n#define IR44_BLUEPLUS      0xFF6897  //\n#define IR44_QUICK         0xFFE817  //\n#define IR44_REDMINUS      0xFF08F7  //\n#define IR44_GREENMINUS    0xFF8877  //\n#define IR44_BLUEMINUS     0xFF48B7  //\n#define IR44_SLOW          0xFFC837  //\n#define IR44_DIY1          0xFF30CF  //\n#define IR44_DIY2          0xFFB04F  //\n#define IR44_DIY3          0xFF708F  //\n#define IR44_AUTO          0xFFF00F  //\n#define IR44_DIY4          0xFF10EF  //\n#define IR44_DIY5          0xFF906F  //\n#define IR44_DIY6          0xFF50AF  //\n#define IR44_FLASH         0xFFD02F  //\n#define IR44_JUMP3         0xFF20DF  //\n#define IR44_JUMP7         0xFFA05F  //\n#define IR44_FADE3         0xFF609F  //\n#define IR44_FADE7         0xFFE01F  //\n\n//Infrared codes for 21-key remote https://images-na.ssl-images-amazon.com/images/I/51NMA0XucnL.jpg\n#define IR21_BRIGHTER      0xFFE01F\n#define IR21_DARKER        0xFFA857\n#define IR21_OFF           0xFF629D\n#define IR21_ON            0xFFA25D\n#define IR21_RED           0xFF6897\n#define IR21_REDDISH       0xFF30CF\n#define IR21_ORANGE        0xFF10EF\n#define IR21_YELLOWISH     0xFF42BD\n#define IR21_GREEN         0xFF9867\n#define IR21_GREENISH      0xFF18E7\n#define IR21_TURQUOISE     0xFF38C7\n#define IR21_CYAN          0xFF4AB5\n#define IR21_BLUE          0xFFB04F\n#define IR21_DEEPBLUE      0xFF7A85\n#define IR21_PURPLE        0xFF5AA5\n#define IR21_PINK          0xFF52AD\n#define IR21_WHITE         0xFF906F\n#define IR21_FLASH         0xFFE21D\n#define IR21_STROBE        0xFF22DD\n#define IR21_FADE          0xFF02FD\n#define IR21_SMOOTH        0xFFC23D\n\n#define COLOR_RED            0xFF0000\n#define COLOR_REDDISH        0xFF7800\n#define COLOR_ORANGE         0xFFA000\n#define COLOR_YELLOWISH      0xFFC800\n#define COLOR_YELLOW         0xFFFF00\n#define COLOR_GREEN          0x00FF00\n#define COLOR_GREENISH       0x00FF78\n#define COLOR_TURQUOISE      0x00FFA0\n#define COLOR_CYAN           0x00FFDC\n#define COLOR_AQUA           0x00C8FF\n#define COLOR_BLUE           0x00A0FF\n#define COLOR_DEEPBLUE       0x0000FF\n#define COLOR_PURPLE         0xAA00FF\n#define COLOR_MAGENTA        0xFF00DC\n#define COLOR_PINK           0xFF00A0\n#define COLOR_WHITE          0xFFFFFFFF\n#define COLOR_WARMWHITE2     0xFFFFAA69\n#define COLOR_WARMWHITE      0xFFFFBF8E\n#define COLOR_NEUTRALWHITE   0xFFFFD4B4\n#define COLOR_COLDWHITE      0xFFFFE9D9\n#define COLOR_COLDWHITE2     0xFFFFFFFF\n\n#define ACTION_NONE             0\n#define ACTION_BRIGHT_UP        1\n#define ACTION_BRIGHT_DOWN      2\n#define ACTION_SPEED_UP         3\n#define ACTION_SPEED_DOWN       4\n#define ACTION_INTENSITY_UP     5\n#define ACTION_INTENSITY_DOWN   6\n#define ACTION_POWER            7\n"
  },
  {
    "path": "wled00/json.cpp",
    "content": "#include \"wled.h\"\n\n\n#define JSON_PATH_STATE      1\n#define JSON_PATH_INFO       2\n#define JSON_PATH_STATE_INFO 3\n#define JSON_PATH_NODES      4\n#define JSON_PATH_PALETTES   5\n#define JSON_PATH_FXDATA     6\n#define JSON_PATH_NETWORKS   7\n#define JSON_PATH_EFFECTS    8\n\n/*\n * JSON API (De)serialization\n */\nnamespace {\n  typedef struct {\n    uint32_t colors[NUM_COLORS];\n    uint16_t start;\n    uint16_t stop;\n    uint16_t offset;\n    uint16_t grouping;\n    uint16_t spacing;\n    uint16_t startY;\n    uint16_t stopY;\n    uint16_t options;\n    uint8_t  mode;\n    uint8_t  palette;\n    uint8_t  opacity;\n    uint8_t  speed;\n    uint8_t  intensity;\n    uint8_t  custom1;\n    uint8_t  custom2;\n    uint8_t  custom3;\n    bool     check1;\n    bool     check2;\n    bool     check3;\n  } SegmentCopy;\n\n  uint8_t differs(const Segment& b, const SegmentCopy& a) {\n    uint8_t d = 0;\n    if (a.start != b.start)         d |= SEG_DIFFERS_BOUNDS;\n    if (a.stop != b.stop)           d |= SEG_DIFFERS_BOUNDS;\n    if (a.offset != b.offset)       d |= SEG_DIFFERS_GSO;\n    if (a.grouping != b.grouping)   d |= SEG_DIFFERS_GSO;\n    if (a.spacing != b.spacing)     d |= SEG_DIFFERS_GSO;\n    if (a.opacity != b.opacity)     d |= SEG_DIFFERS_BRI;\n    if (a.mode != b.mode)           d |= SEG_DIFFERS_FX;\n    if (a.speed != b.speed)         d |= SEG_DIFFERS_FX;\n    if (a.intensity != b.intensity) d |= SEG_DIFFERS_FX;\n    if (a.palette != b.palette)     d |= SEG_DIFFERS_FX;\n    if (a.custom1 != b.custom1)     d |= SEG_DIFFERS_FX;\n    if (a.custom2 != b.custom2)     d |= SEG_DIFFERS_FX;\n    if (a.custom3 != b.custom3)     d |= SEG_DIFFERS_FX;\n    if (a.check1 != b.check1)       d |= SEG_DIFFERS_FX;\n    if (a.check2 != b.check2)       d |= SEG_DIFFERS_FX;\n    if (a.check3 != b.check3)       d |= SEG_DIFFERS_FX;\n    if (a.startY != b.startY)       d |= SEG_DIFFERS_BOUNDS;\n    if (a.stopY != b.stopY)         d |= SEG_DIFFERS_BOUNDS;\n\n    //bit pattern: (msb first)\n    // set:2, sound:2, mapping:3, transposed, mirrorY, reverseY, [reset,] paused, mirrored, on, reverse, [selected]\n    if ((a.options & 0b1111111111011110U) != (b.options & 0b1111111111011110U)) d |= SEG_DIFFERS_OPT;\n    if ((a.options & 0x0001U) != (b.options & 0x0001U))                         d |= SEG_DIFFERS_SEL;\n    for (unsigned i = 0; i < NUM_COLORS; i++) if (a.colors[i] != b.colors[i])   d |= SEG_DIFFERS_COL;\n\n    return d;\n  }\n}\n\nstatic bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0)\n{\n  byte id = elem[\"id\"] | it;\n  if (id >= WS2812FX::getMaxSegments()) return false;\n\n  bool newSeg = false;\n  int stop = elem[\"stop\"] | -1;\n\n  // append segment\n  if (id >= strip.getSegmentsNum()) {\n    if (stop <= 0) return false; // ignore empty/inactive segments\n    strip.appendSegment(0, strip.getLengthTotal());\n    id = strip.getSegmentsNum()-1; // segments are added at the end of list\n    newSeg = true;\n  }\n\n  //DEBUG_PRINTLN(F(\"-- JSON deserialize segment.\"));\n  Segment& seg = strip.getSegment(id);\n  // we do not want to make segment copy as it may use a lot of RAM (effect data and pixel buffer)\n  // so we will create a copy of segment options and compare it with original segment when done processing\n  SegmentCopy prev = {\n    {seg.colors[0], seg.colors[1], seg.colors[2]},\n    seg.start,\n    seg.stop,\n    seg.offset,\n    seg.grouping,\n    seg.spacing,\n    seg.startY,\n    seg.stopY,\n    seg.options,\n    seg.mode,\n    seg.palette,\n    seg.opacity,\n    seg.speed,\n    seg.intensity,\n    seg.custom1,\n    seg.custom2,\n    seg.custom3,\n    seg.check1,\n    seg.check2,\n    seg.check3\n  };\n\n  int start = elem[\"start\"] | seg.start;\n  if (stop < 0) {\n    int len = elem[\"len\"];\n    stop = (len > 0) ? start + len : seg.stop;\n  }\n  // 2D segments\n  int startY = elem[\"startY\"] | seg.startY;\n  int stopY = elem[\"stopY\"] | seg.stopY;\n\n  //repeat, multiplies segment until all LEDs are used, or max segments reached\n  bool repeat = elem[\"rpt\"] | false;\n  if (repeat && stop>0) {\n    elem.remove(\"id\");  // remove for recursive call\n    elem.remove(\"rpt\"); // remove for recursive call\n    elem.remove(\"n\");   // remove for recursive call\n    unsigned len = stop - start;\n    for (size_t i=id+1; i<WS2812FX::getMaxSegments(); i++) {\n      start = start + len;\n      if (start >= strip.getLengthTotal()) break;\n      //TODO: add support for 2D\n      elem[\"start\"] = start;\n      elem[\"stop\"]  = start + len;\n      elem[\"rev\"]   = !elem[\"rev\"]; // alternate reverse on even/odd segments\n      deserializeSegment(elem, i, presetId); // recursive call with new id\n    }\n    return true;\n  }\n\n  if (elem[\"n\"]) {\n    // name field exists\n    const char * name = elem[\"n\"].as<const char*>();\n    seg.setName(name); // will resolve empty and null correctly\n  } else if (start != seg.start || stop != seg.stop) {\n    // clearing or setting segment without name field\n    seg.clearName();\n  }\n\n  uint16_t grp       = elem[\"grp\"] | seg.grouping;\n  uint16_t spc       = elem[F(\"spc\")] | seg.spacing;\n  uint16_t of        = seg.offset;\n  uint8_t  soundSim  = elem[\"si\"] | seg.soundSim;\n  uint8_t  map1D2D   = elem[\"m12\"] | seg.map1D2D;\n  uint8_t  set       = elem[F(\"set\")] | seg.set;\n  bool     selected  = getBoolVal(elem[\"sel\"], seg.selected);\n  bool     reverse   = getBoolVal(elem[\"rev\"], seg.reverse);\n  bool     mirror    = getBoolVal(elem[\"mi\"] , seg.mirror);\n  #ifndef WLED_DISABLE_2D\n  bool     reverse_y = getBoolVal(elem[\"rY\"]   , seg.reverse_y);\n  bool     mirror_y  = getBoolVal(elem[\"mY\"]   , seg.mirror_y);\n  bool     transpose = getBoolVal(elem[F(\"tp\")], seg.transpose);\n  #endif\n\n  // if segment's virtual dimensions change we need to restart effect (segment blending and PS rely on dimensions)\n  if (seg.mirror != mirror) seg.markForReset();\n  #ifndef WLED_DISABLE_2D\n  if (seg.mirror_y != mirror_y || seg.transpose != transpose) seg.markForReset();\n  #endif\n\n  int len = (stop > start) ? stop - start : 1;\n  int offset = elem[F(\"of\")] | INT32_MAX;\n  if (offset != INT32_MAX) {\n    int offsetAbs = abs(offset);\n    if (offsetAbs > len - 1) offsetAbs %= len;\n    if (offset < 0) offsetAbs = len - offsetAbs;\n    of = offsetAbs;\n  }\n  if (stop > start && of > len -1) of = len -1;\n\n  // update segment (delete if necessary)\n  seg.setGeometry(start, stop, grp, spc, of, startY, stopY, map1D2D); // strip needs to be suspended for this to work without issues\n\n  if (newSeg) seg.refreshLightCapabilities(); // fix for #3403\n\n  if (seg.reset && seg.stop == 0) {\n    if (id == strip.getMainSegmentId()) strip.setMainSegmentId(0); // fix for #3403\n    return true; // segment was deleted & is marked for reset, no need to change anything else\n  }\n\n  byte segbri = seg.opacity;\n  if (getVal(elem[\"bri\"], segbri)) {\n    if (segbri > 0) seg.setOpacity(segbri); // use transition\n    seg.setOption(SEG_OPTION_ON, segbri); // use transition\n  }\n\n  seg.setOption(SEG_OPTION_ON, getBoolVal(elem[\"on\"], seg.on)); // use transition\n  seg.freeze = getBoolVal(elem[\"frz\"], seg.freeze);\n\n  seg.setCCT(elem[\"cct\"] | seg.cct);\n\n  JsonArray colarr = elem[\"col\"];\n  if (!colarr.isNull())\n  {\n    if (seg.getLightCapabilities() & 3) {\n      // segment has RGB or White\n      for (size_t i = 0; i < NUM_COLORS; i++) {\n        // JSON \"col\" array can contain the following values for each of segment's colors (primary, background, custom):\n        // \"col\":[int|string|object|array, int|string|object|array, int|string|object|array]\n        //   int = Kelvin temperature or 0 for black\n        //   string = hex representation of [WW]RRGGBB or \"r\" for random color\n        //   object = individual channel control {\"r\":0,\"g\":127,\"b\":255,\"w\":255}, each being optional (valid to send {})\n        //   array = direct channel values [r,g,b,w] (w element being optional)\n        int rgbw[] = {0,0,0,0};\n        bool colValid = false;\n        JsonArray colX = colarr[i];\n        if (colX.isNull()) {\n          JsonObject oCol = colarr[i];\n          if (!oCol.isNull()) {\n            // we have a JSON object for color {\"w\":123,\"r\":123,...}; allows individual channel control\n            rgbw[0] = oCol[\"r\"] | R(seg.colors[i]);\n            rgbw[1] = oCol[\"g\"] | G(seg.colors[i]);\n            rgbw[2] = oCol[\"b\"] | B(seg.colors[i]);\n            rgbw[3] = oCol[\"w\"] | W(seg.colors[i]);\n            colValid = true;\n          } else {\n            byte brgbw[] = {0,0,0,0};\n            const char* hexCol = colarr[i];\n            if (hexCol == nullptr) { //Kelvin color temperature (or invalid), e.g 2400\n              int kelvin = colarr[i] | -1;\n              if (kelvin <  0) continue;\n              if (kelvin == 0) seg.setColor(i, 0);\n              if (kelvin >  0) colorKtoRGB(kelvin, brgbw);\n              colValid = true;\n            } else if (hexCol[0] == 'r' && hexCol[1] == '\\0') { // Random colors via JSON API in Segment object like col=[\"r\",\"r\",\"r\"] · Issue #4996\n              setRandomColor(brgbw);\n              colValid = true;\n            } else { //HEX string, e.g. \"FFAA00\"\n              colValid = colorFromHexString(brgbw, hexCol);\n            }\n            for (size_t c = 0; c < 4; c++) rgbw[c] = brgbw[c];\n          }\n        } else { //Array of ints (RGB or RGBW color), e.g. [255,160,0]\n          byte sz = colX.size();\n          if (sz == 0) continue; //do nothing on empty array\n          copyArray(colX, rgbw, 4);\n          colValid = true;\n        }\n\n        if (!colValid) continue;\n\n        seg.setColor(i, RGBW32(rgbw[0],rgbw[1],rgbw[2],rgbw[3])); // use transition\n        if (seg.mode == FX_MODE_STATIC) strip.trigger(); //instant refresh\n      }\n    } else {\n      // non RGB & non White segment (usually On/Off bus)\n      seg.setColor(0, ULTRAWHITE); // use transition\n      seg.setColor(1, BLACK); // use transition\n    }\n  }\n\n  // lx parser\n  #ifdef WLED_ENABLE_LOXONE\n  int lx = elem[F(\"lx\")] | -1;\n  if (lx >= 0) {\n    parseLxJson(lx, id, false);\n  }\n  int ly = elem[F(\"ly\")] | -1;\n  if (ly >= 0) {\n    parseLxJson(ly, id, true);\n  }\n  #endif\n\n  seg.set       = constrain(set, 0, 3);\n  seg.soundSim  = constrain(soundSim, 0, 3);\n  seg.selected  = selected;\n  seg.reverse   = reverse;\n  seg.mirror    = mirror;\n  #ifndef WLED_DISABLE_2D\n  seg.reverse_y = reverse_y;\n  seg.mirror_y  = mirror_y;\n  seg.transpose = transpose;\n  #endif\n\n  byte fx = seg.mode;\n  if (getVal(elem[\"fx\"], fx, 0, strip.getModeCount())) {\n    if (!presetId && currentPlaylist>=0) unloadPlaylist();\n    if (fx != seg.mode) seg.setMode(fx, elem[F(\"fxdef\")]); // use transition (WARNING: may change map1D2D causing geometry change)\n  }\n\n  getVal(elem[\"sx\"], seg.speed);\n  getVal(elem[\"ix\"], seg.intensity);\n\n  uint8_t pal = seg.palette;\n  if (seg.getLightCapabilities() & 1) {  // ignore palette for White and On/Off segments\n    if (getVal(elem[\"pal\"], pal, 0, getPaletteCount())) seg.setPalette(pal);\n  }\n\n  getVal(elem[\"c1\"], seg.custom1);\n  getVal(elem[\"c2\"], seg.custom2);\n  uint8_t cust3 = seg.custom3;\n  getVal(elem[\"c3\"], cust3, 0, 31); // we can't pass reference to bitfield\n  seg.custom3 = constrain(cust3, 0, 31);\n\n  seg.check1 = getBoolVal(elem[\"o1\"], seg.check1);\n  seg.check2 = getBoolVal(elem[\"o2\"], seg.check2);\n  seg.check3 = getBoolVal(elem[\"o3\"], seg.check3);\n\n  uint8_t blend = seg.blendMode;\n  getVal(elem[\"bm\"], blend, 0, 15); // we can't pass reference to bitfield\n  seg.blendMode = constrain(blend, 0, 15);\n\n  JsonArray iarr = elem[F(\"i\")]; //set individual LEDs\n  if (!iarr.isNull()) {\n    // set brightness immediately and disable transition\n    jsonTransitionOnce = true;\n    if (seg.isInTransition()) seg.startTransition(0); // setting transition time to 0 will stop transition in next frame\n    strip.setTransition(0);\n    strip.setBrightness(bri, true);\n\n    // freeze and init to black\n    if (!seg.freeze) {\n      seg.freeze = true;\n      seg.clear();\n    }\n\n    unsigned iStart = 0, iStop = 0;\n    unsigned iSet = 0; //0 nothing set, 1 start set, 2 range set\n\n    for (size_t i = 0; i < iarr.size(); i++) {\n      if (iarr[i].is<JsonInteger>()) {\n        if (!iSet) {\n          iStart = abs(iarr[i].as<int>());\n          iSet++;\n        } else {\n          iStop = abs(iarr[i].as<int>());\n          iSet++;\n        }\n      } else { //color\n        uint8_t rgbw[] = {0,0,0,0};\n        JsonArray icol = iarr[i];\n        if (!icol.isNull()) { //array, e.g. [255,0,0]\n          byte sz = icol.size();\n          if (sz > 0 && sz < 5) copyArray(icol, rgbw);\n        } else { //hex string, e.g. \"FF0000\"\n          byte brgbw[] = {0,0,0,0};\n          const char* hexCol = iarr[i];\n          if (colorFromHexString(brgbw, hexCol)) {\n            for (size_t c = 0; c < 4; c++) rgbw[c] = brgbw[c];\n          }\n        }\n\n        if (iSet < 2 || iStop <= iStart) iStop = iStart + 1;\n        uint32_t c = RGBW32(rgbw[0], rgbw[1], rgbw[2], rgbw[3]);\n        while (iStart < iStop) seg.setRawPixelColor(iStart++, c); // sets pixel color without 1D->2D expansion, grouping or spacing\n        iSet = 0;\n      }\n    }\n    strip.trigger(); // force segment update\n  }\n  // send UDP/WS if segment options changed (except selection; will also deselect current preset)\n  if (differs(seg, prev) & ~SEG_DIFFERS_SEL) stateChanged = true;\n\n  return true;\n}\n\n// deserializes WLED state\n// presetId is non-0 if called from handlePreset()\nbool deserializeState(JsonObject root, byte callMode, byte presetId)\n{\n  bool stateResponse = root[F(\"v\")] | false;\n\n  #if defined(WLED_DEBUG) && defined(WLED_DEBUG_HOST)\n  netDebugEnabled = root[F(\"debug\")] | netDebugEnabled;\n  #endif\n\n  bool onBefore = bri;\n  getVal(root[\"bri\"], bri);\n  if (bri != briOld) stateChanged = true;\n\n  bool on = root[\"on\"] | (bri > 0);\n  if (!on != !bri) toggleOnOff();\n\n  if (root[\"on\"].is<const char*>() && root[\"on\"].as<const char*>()[0] == 't') {\n    if (onBefore || !bri) toggleOnOff(); // do not toggle off again if just turned on by bri (makes e.g. \"{\"on\":\"t\",\"bri\":32}\" work)\n  }\n\n  if (bri && !onBefore) { // unfreeze all segments when turning on\n    for (size_t s=0; s < strip.getSegmentsNum(); s++) {\n      strip.getSegment(s).freeze = false;\n    }\n    if (realtimeMode && !realtimeOverride && useMainSegmentOnly) { // keep live segment frozen if live\n      strip.getMainSegment().freeze = true;\n    }\n  }\n\n  long tr = -1;\n  if (!presetId || currentPlaylist < 0) { //do not apply transition time from preset if playlist active, as it would override playlist transition times\n    tr = root[F(\"transition\")] | -1;\n    if (tr >= 0) {\n      transitionDelay = tr * 100;\n      strip.setTransition(transitionDelay);\n    }\n  }\n\n  blendingStyle = root[F(\"bs\")] | blendingStyle;\n  blendingStyle &= 0x1F;\n\n  // temporary transition (applies only once)\n  tr = root[F(\"tt\")] | -1;\n  if (tr >= 0) {\n    jsonTransitionOnce = true;\n    strip.setTransition(tr * 100);\n  }\n\n  tr = root[F(\"tb\")] | -1;\n  if (tr >= 0) strip.timebase = (unsigned long)tr - millis();\n\n  JsonObject nl       = root[\"nl\"];\n  if (!nl.isNull()) stateChanged = true;\n  nightlightActive    = getBoolVal(nl[\"on\"], nightlightActive);\n  nightlightDelayMins = nl[\"dur\"]     | nightlightDelayMins;\n  nightlightMode      = nl[\"mode\"]    | nightlightMode;\n  nightlightTargetBri = nl[F(\"tbri\")] | nightlightTargetBri;\n\n  JsonObject udpn      = root[\"udpn\"];\n  sendNotificationsRT  = getBoolVal(udpn[F(\"send\")], sendNotificationsRT);\n  syncGroups           = udpn[F(\"sgrp\")] | syncGroups;\n  receiveGroups        = udpn[F(\"rgrp\")] | receiveGroups;\n  if ((bool)udpn[F(\"nn\")]) callMode = CALL_MODE_NO_NOTIFY; //send no notification just for this request\n\n  unsigned long timein = root[\"time\"] | UINT32_MAX; //backup time source if NTP not synced\n  if (timein != UINT32_MAX) {\n    setTimeFromAPI(timein);\n    if (presetsModifiedTime == 0) presetsModifiedTime = timein;\n  }\n\n  if (root[F(\"psave\")].isNull()) doReboot = root[F(\"rb\")] | doReboot;\n\n  // do not allow changing main segment while in realtime mode (may get odd results else)\n  if (!realtimeMode) strip.setMainSegmentId(root[F(\"mainseg\")] | strip.getMainSegmentId()); // must be before realtimeLock() if \"live\"\n\n  realtimeOverride = root[F(\"lor\")] | realtimeOverride;\n  if (realtimeOverride > 2) realtimeOverride = REALTIME_OVERRIDE_ALWAYS;\n  if (realtimeMode && useMainSegmentOnly) {\n    strip.getMainSegment().freeze = !realtimeOverride;\n    realtimeOverride = REALTIME_OVERRIDE_NONE;  // ignore request for override if using main segment only\n  }\n\n  if (root.containsKey(\"live\")) {\n    if (root[\"live\"].as<bool>()) {\n      jsonTransitionOnce = true;\n      strip.setTransition(0);\n      realtimeLock(65000);\n    } else {\n      exitRealtime();\n    }\n  }\n\n  int it = 0;\n  JsonVariant segVar = root[\"seg\"];\n  if (!segVar.isNull()) {\n    // we may be called during strip.service() so we must not modify segments while effects are executing\n    strip.suspend();\n    strip.waitForIt();\n    if (segVar.is<JsonObject>()) {\n      int id = segVar[\"id\"] | -1;\n      //if \"seg\" is not an array and ID not specified, apply to all selected/checked segments\n      if (id < 0) {\n        //apply all selected segments\n        for (size_t s = 0; s < strip.getSegmentsNum(); s++) {\n          const Segment &sg = strip.getSegment(s);\n          if (sg.isActive() && sg.isSelected()) {\n            deserializeSegment(segVar, s, presetId);\n          }\n        }\n      } else {\n        deserializeSegment(segVar, id, presetId); //apply only the segment with the specified ID\n      }\n    } else {\n      size_t deleted = 0;\n      JsonArray segs = segVar.as<JsonArray>();\n      for (JsonObject elem : segs) {\n        if (deserializeSegment(elem, it++, presetId) && !elem[\"stop\"].isNull() && elem[\"stop\"]==0) deleted++;\n      }\n      if (strip.getSegmentsNum() > 3 && deleted >= strip.getSegmentsNum()/2U) strip.purgeSegments(); // batch deleting more than half segments\n    }\n    strip.resume();\n  }\n\n  UsermodManager::readFromJsonState(root);\n\n  loadLedmap = root[F(\"ledmap\")] | loadLedmap;\n\n  byte ps = root[F(\"psave\")];\n  if (ps > 0 && ps < 251) savePreset(ps, nullptr, root);\n\n  ps = root[F(\"pdel\")]; //deletion\n  if (ps > 0 && ps < 251) deletePreset(ps);\n\n  // HTTP API commands (must be handled before \"ps\")\n  const char* httpwin = root[\"win\"];\n  if (httpwin) {\n    String apireq = \"win\"; apireq += '&'; // reduce flash string usage\n    apireq += httpwin;\n    handleSet(nullptr, apireq, false);    // may set stateChanged\n  }\n\n  // Applying preset from JSON API has 2 cases: a) \"pd\" AKA \"preset direct\" and b) \"ps\" AKA \"preset select\"\n  // a) \"preset direct\" can only be an integer value representing preset ID. \"preset direct\" assumes JSON API contains the rest of preset content (i.e. from UI call)\n  //    \"preset direct\" JSON can contain \"ps\" API (i.e. call from UI to cycle presets) in such case stateChanged has to be false (i.e. no \"win\" or \"seg\" API)\n  // b) \"preset select\" can be cycling (\"1~5~\"\"), random (\"r\" or \"1~5r\"), ID, etc. value allowed from JSON API. This type of call assumes no state changing content in API call\n  byte presetToRestore = 0;\n  if (!root[F(\"pd\")].isNull() && stateChanged) {\n    // a) already applied preset content (requires \"seg\" or \"win\" but will ignore the rest)\n    currentPreset = root[F(\"pd\")] | currentPreset;\n    if (root[\"win\"].isNull()) presetCycCurr = currentPreset; // otherwise presetCycCurr was set in handleSet() [set.cpp]\n    presetToRestore = currentPreset; // stateUpdated() will clear the preset, so we need to restore it after\n    DEBUG_PRINTF_P(PSTR(\"Preset direct: %d\\n\"), currentPreset);\n  } else if (!root[\"ps\"].isNull()) {\n    // we have \"ps\" call (i.e. from button or external API call) or \"pd\" that includes \"ps\" (i.e. from UI call)\n    if (root[\"win\"].isNull() && getVal(root[\"ps\"], presetCycCurr, 1, 250) && presetCycCurr > 0 && presetCycCurr < 251 && presetCycCurr != currentPreset) {\n      DEBUG_PRINTF_P(PSTR(\"Preset select: %d\\n\"), presetCycCurr);\n      // b) preset ID only or preset that does not change state (use embedded cycling limits if they exist in getVal())\n      applyPreset(presetCycCurr, callMode); // async load from file system (only preset ID was specified)\n      return stateResponse;\n    } else presetCycCurr = currentPreset; // restore presetCycCurr\n  }\n\n  JsonObject playlist = root[F(\"playlist\")];\n  if (!playlist.isNull() && loadPlaylist(playlist, presetId)) {\n    //do not notify here, because the first playlist entry will do\n    if (root[\"on\"].isNull()) callMode = CALL_MODE_NO_NOTIFY;\n    else callMode = CALL_MODE_DIRECT_CHANGE;  // possible bugfix for playlist only containing HTTP API preset FX=~\n  }\n\n  if (root.containsKey(F(\"rmcpal\"))) {\n    char fileName[32];\n    sprintf_P(fileName, PSTR(\"/palette%d.json\"), root[F(\"rmcpal\")].as<uint8_t>());\n    if (WLED_FS.exists(fileName)) WLED_FS.remove(fileName);\n    loadCustomPalettes();\n  }\n\n  doAdvancePlaylist = root[F(\"np\")] | doAdvancePlaylist; //advances to next preset in playlist when true\n\n  JsonObject wifi = root[F(\"wifi\")];\n  if (!wifi.isNull()) {\n    bool apMode = getBoolVal(wifi[F(\"ap\")], apActive);\n    if (!apActive && apMode) WLED::instance().initAP();  // start AP mode immediately\n    else if (apActive && !apMode) { // stop AP mode immediately\n      dnsServer.stop();\n      WiFi.softAPdisconnect(true);\n      apActive = false;\n    }\n    //bool restart = wifi[F(\"restart\")] | false;\n    //if (restart) forceReconnect = true;\n  }\n\n  if (stateChanged) stateUpdated(callMode);\n  if (presetToRestore) currentPreset = presetToRestore;\n\n  return stateResponse;\n}\n\nstatic void serializeSegment(JsonObject& root, const Segment& seg, byte id, bool forPreset, bool segmentBounds)\n{\n  root[\"id\"] = id;\n  if (segmentBounds) {\n    root[\"start\"] = seg.start;\n    root[\"stop\"] = seg.stop;\n    #ifndef WLED_DISABLE_2D\n    if (strip.isMatrix) {\n      root[F(\"startY\")] = seg.startY;\n      root[F(\"stopY\")]  = seg.stopY;\n    }\n    #endif\n  }\n  if (!forPreset) root[\"len\"] = seg.stop - seg.start;\n  root[\"grp\"]    = seg.grouping;\n  root[F(\"spc\")] = seg.spacing;\n  root[F(\"of\")]  = seg.offset;\n  root[\"on\"]     = seg.on;\n  root[\"frz\"]    = seg.freeze;\n  byte segbri    = seg.opacity;\n  root[\"bri\"]    = (segbri) ? segbri : 255;\n  root[\"cct\"]    = seg.cct;\n  root[F(\"set\")] = seg.set;\n  root[\"lc\"]     = seg.getLightCapabilities();\n\n  if (seg.name != nullptr) root[\"n\"] = reinterpret_cast<const char *>(seg.name); //not good practice, but decreases required JSON buffer\n  else if (forPreset) root[\"n\"] = \"\";\n\n  // to conserve RAM we will serialize the col array manually\n  // this will reduce RAM footprint from ~300 bytes to 84 bytes per segment\n  char colstr[70]; colstr[0] = '['; colstr[1] = '\\0';  //max len 68 (5 chan, all 255)\n  const char *format = strip.hasWhiteChannel() ? PSTR(\"[%u,%u,%u,%u]\") : PSTR(\"[%u,%u,%u]\");\n  for (size_t i = 0; i < 3; i++)\n  {\n    byte segcol[4]; byte* c = segcol;\n    segcol[0] = R(seg.colors[i]);\n    segcol[1] = G(seg.colors[i]);\n    segcol[2] = B(seg.colors[i]);\n    segcol[3] = W(seg.colors[i]);\n    char tmpcol[22];\n    sprintf_P(tmpcol, format, (unsigned)c[0], (unsigned)c[1], (unsigned)c[2], (unsigned)c[3]);\n    strcat(colstr, i<2 ? strcat(tmpcol, \",\") : tmpcol);\n  }\n  strcat(colstr, \"]\");\n  root[\"col\"] = serialized(colstr);\n\n  root[\"fx\"]  = seg.mode;\n  root[\"sx\"]  = seg.speed;\n  root[\"ix\"]  = seg.intensity;\n  root[\"pal\"] = seg.palette;\n  root[\"c1\"]  = seg.custom1;\n  root[\"c2\"]  = seg.custom2;\n  root[\"c3\"]  = seg.custom3;\n  root[\"sel\"] = seg.isSelected();\n  root[\"rev\"] = seg.reverse;\n  root[\"mi\"]  = seg.mirror;\n  #ifndef WLED_DISABLE_2D\n  if (strip.isMatrix) {\n    root[\"rY\"] = seg.reverse_y;\n    root[\"mY\"] = seg.mirror_y;\n    root[F(\"tp\")] = seg.transpose;\n  }\n  #endif\n  root[\"o1\"]  = seg.check1;\n  root[\"o2\"]  = seg.check2;\n  root[\"o3\"]  = seg.check3;\n  root[\"si\"]  = seg.soundSim;\n  root[\"m12\"] = seg.map1D2D;\n  root[\"bm\"]  = seg.blendMode;\n}\n\nvoid serializeState(JsonObject root, bool forPreset, bool includeBri, bool segmentBounds, bool selectedSegmentsOnly)\n{\n  if (includeBri) {\n    root[\"on\"] = (bri > 0);\n    root[\"bri\"] = briLast;\n    root[F(\"transition\")] = transitionDelay/100; //in 100ms\n    root[F(\"bs\")] = blendingStyle;\n  }\n\n  if (!forPreset) {\n    if (errorFlag) {root[F(\"error\")] = errorFlag; errorFlag = ERR_NONE;} //prevent error message to persist on screen\n\n    root[\"ps\"] = (currentPreset > 0) ? currentPreset : -1;\n    root[F(\"pl\")] = currentPlaylist;\n    root[F(\"ledmap\")] = currentLedmap;\n\n    UsermodManager::addToJsonState(root);\n\n    JsonObject nl = root.createNestedObject(\"nl\");\n    nl[\"on\"] = nightlightActive;\n    nl[\"dur\"] = nightlightDelayMins;\n    nl[\"mode\"] = nightlightMode;\n    nl[F(\"tbri\")] = nightlightTargetBri;\n    nl[F(\"rem\")] = nightlightActive ? (int)(nightlightDelayMs - (millis() - nightlightStartTime)) / 1000 : -1; // seconds remaining\n\n    JsonObject udpn = root.createNestedObject(\"udpn\");\n    udpn[F(\"send\")] = sendNotificationsRT;\n    udpn[F(\"recv\")] = receiveGroups != 0;\n    udpn[F(\"sgrp\")] = syncGroups;\n    udpn[F(\"rgrp\")] = receiveGroups;\n\n    root[F(\"lor\")] = realtimeOverride;\n  }\n\n  root[F(\"mainseg\")] = strip.getMainSegmentId();\n\n  JsonArray seg = root.createNestedArray(\"seg\");\n  for (size_t s = 0; s < WS2812FX::getMaxSegments(); s++) {\n    if (s >= strip.getSegmentsNum()) {\n      if (forPreset && segmentBounds && !selectedSegmentsOnly) { //disable segments not part of preset\n        JsonObject seg0 = seg.createNestedObject();\n        seg0[\"stop\"] = 0;\n        continue;\n      } else\n        break;\n    }\n    const Segment &sg = strip.getSegment(s);\n    if (forPreset && selectedSegmentsOnly && !sg.isSelected()) continue;\n    if (sg.isActive()) {\n      JsonObject seg0 = seg.createNestedObject();\n      serializeSegment(seg0, sg, s, forPreset, segmentBounds);\n    } else if (forPreset && segmentBounds) { //disable segments not part of preset\n      JsonObject seg0 = seg.createNestedObject();\n      seg0[\"stop\"] = 0;\n    }\n  }\n}\n\n\nvoid serializeInfo(JsonObject root)\n{\n  root[F(\"ver\")] = versionString;\n  root[F(\"vid\")] = VERSION;\n  root[F(\"cn\")] = F(WLED_CODENAME);\n  root[F(\"release\")] = releaseString;\n  root[F(\"repo\")] = repoString;\n  root[F(\"deviceId\")] = getDeviceId();\n\n  JsonObject leds = root.createNestedObject(F(\"leds\"));\n  leds[F(\"count\")] = strip.getLengthTotal();\n  leds[F(\"pwr\")] = BusManager::currentMilliamps();\n  leds[\"fps\"] = strip.getFps();\n  leds[F(\"maxpwr\")] = BusManager::currentMilliamps()>0 ? BusManager::ablMilliampsMax() : 0;\n  leds[F(\"maxseg\")] = WS2812FX::getMaxSegments();\n  //leds[F(\"actseg\")] = strip.getActiveSegmentsNum();\n  //leds[F(\"seglock\")] = false; //might be used in the future to prevent modifications to segment config\n  leds[F(\"bootps\")] = bootPreset;\n\n  #ifndef WLED_DISABLE_2D\n  if (strip.isMatrix) {\n    JsonObject matrix = leds.createNestedObject(F(\"matrix\"));\n    matrix[\"w\"] = Segment::maxWidth;\n    matrix[\"h\"] = Segment::maxHeight;\n  }\n  #endif\n\n  unsigned totalLC = 0;\n  JsonArray lcarr = leds.createNestedArray(F(\"seglc\")); // deprecated, use state.seg[].lc\n  size_t nSegs = strip.getSegmentsNum();\n  for (size_t s = 0; s < nSegs; s++) {\n    if (!strip.getSegment(s).isActive()) continue;\n    unsigned lc = strip.getSegment(s).getLightCapabilities();\n    totalLC |= lc;\n    lcarr.add(lc); // deprecated, use state.seg[].lc\n  }\n\n  leds[\"lc\"] = totalLC;\n\n  leds[F(\"rgbw\")] = strip.hasRGBWBus(); // deprecated, use info.leds.lc\n  leds[F(\"wv\")]   = totalLC & 0x02;     // deprecated, true if white slider should be displayed for any segment\n  leds[\"cct\"]     = totalLC & 0x04;     // deprecated, use info.leds.lc\n\n  #ifdef WLED_DEBUG\n  JsonArray i2c = root.createNestedArray(F(\"i2c\"));\n  i2c.add(i2c_sda);\n  i2c.add(i2c_scl);\n  JsonArray spi = root.createNestedArray(F(\"spi\"));\n  spi.add(spi_mosi);\n  spi.add(spi_sclk);\n  spi.add(spi_miso);\n  #endif\n\n  root[F(\"str\")] = false; // sync toggle receive\n\n  root[F(\"name\")] = serverDescription;\n  root[F(\"udpport\")] = udpPort;\n  root[F(\"simplifiedui\")] = simplifiedUI;\n  root[\"live\"] = (bool)realtimeMode;\n  root[F(\"liveseg\")] = useMainSegmentOnly ? strip.getMainSegmentId() : -1;  // if using main segment only for live\n\n  switch (realtimeMode) {\n    case REALTIME_MODE_INACTIVE: root[\"lm\"] = \"\"; break;\n    case REALTIME_MODE_GENERIC:  root[\"lm\"] = \"\"; break;\n    case REALTIME_MODE_UDP:      root[\"lm\"] = F(\"UDP\"); break;\n    case REALTIME_MODE_HYPERION: root[\"lm\"] = F(\"Hyperion\"); break;\n    case REALTIME_MODE_E131:     root[\"lm\"] = F(\"E1.31\"); break;\n    case REALTIME_MODE_ADALIGHT: root[\"lm\"] = F(\"USB Adalight/TPM2\"); break;\n    case REALTIME_MODE_ARTNET:   root[\"lm\"] = F(\"Art-Net\"); break;\n    case REALTIME_MODE_TPM2NET:  root[\"lm\"] = F(\"tpm2.net\"); break;\n    case REALTIME_MODE_DDP:      root[\"lm\"] = F(\"DDP\"); break;\n  }\n\n  root[F(\"lip\")] = realtimeIP[0] == 0 ? \"\" : realtimeIP.toString();\n\n  #ifdef WLED_ENABLE_WEBSOCKETS\n  root[F(\"ws\")] = ws.count();\n  #else\n  root[F(\"ws\")] = -1;\n  #endif\n\n  root[F(\"fxcount\")] = strip.getModeCount();\n  root[F(\"palcount\")] = getPaletteCount();\n  root[F(\"cpalcount\")] = customPalettes.size();   // number of custom palettes\n  root[F(\"cpalmax\")] = WLED_MAX_CUSTOM_PALETTES;  // maximum number of custom palettes\n\n  JsonArray ledmaps = root.createNestedArray(F(\"maps\"));\n  for (size_t i=0; i<WLED_MAX_LEDMAPS; i++) {\n    if ((ledMaps>>i) & 0x00000001U) {\n      JsonObject ledmaps0 = ledmaps.createNestedObject();\n      ledmaps0[\"id\"] = i;\n      #ifndef ESP8266\n      if (i && ledmapNames[i-1]) ledmaps0[\"n\"] = ledmapNames[i-1];\n      #endif\n    }\n  }\n\n  JsonObject wifi_info = root.createNestedObject(F(\"wifi\"));\n  wifi_info[F(\"bssid\")] = WiFi.BSSIDstr();\n  int qrssi = WiFi.RSSI();\n  wifi_info[F(\"rssi\")] = qrssi;\n  wifi_info[F(\"signal\")] = getSignalQuality(qrssi);\n  wifi_info[F(\"channel\")] = WiFi.channel();\n  wifi_info[F(\"ap\")] = apActive;\n\n  JsonObject fs_info = root.createNestedObject(\"fs\");\n  fs_info[\"u\"] = fsBytesUsed / 1000;\n  fs_info[\"t\"] = fsBytesTotal / 1000;\n  fs_info[F(\"pmt\")] = presetsModifiedTime;\n\n  root[F(\"ndc\")] = nodeListEnabled ? (int)Nodes.size() : -1;\n\n#ifdef ARDUINO_ARCH_ESP32\n  #ifdef WLED_DEBUG\n    wifi_info[F(\"txPower\")] = (int) WiFi.getTxPower();\n    wifi_info[F(\"sleep\")] = (bool) WiFi.getSleep();\n  #endif\n  #if defined(ARDUINO_ARCH_ESP32) && defined(CONFIG_IDF_TARGET_ESP32) // classic esp32 only: report \"esp32\" without package details\n    root[F(\"arch\")] = \"esp32\";\n  #else\n    root[F(\"arch\")] = ESP.getChipModel();\n  #endif\n  root[F(\"core\")] = ESP.getSdkVersion();\n  root[F(\"clock\")] = ESP.getCpuFreqMHz();\n  root[F(\"flash\")] = (ESP.getFlashChipSize()/1024)/1024;\n  #ifdef WLED_DEBUG\n  root[F(\"maxalloc\")] = getContiguousFreeHeap();\n  root[F(\"resetReason0\")] = (int)rtc_get_reset_reason(0);\n  root[F(\"resetReason1\")] = (int)rtc_get_reset_reason(1);\n  #endif\n  root[F(\"lwip\")] = 0; //deprecated\n  #ifndef WLED_DISABLE_OTA\n  root[F(\"bootloaderSHA256\")] = getBootloaderSHA256Hex();\n  #endif\n#else\n  root[F(\"arch\")] = \"esp8266\";\n  root[F(\"core\")] = ESP.getCoreVersion();\n  root[F(\"clock\")] = ESP.getCpuFreqMHz();\n  root[F(\"flash\")] = (ESP.getFlashChipSize()/1024)/1024;\n  #ifdef WLED_DEBUG\n  root[F(\"maxalloc\")] = getContiguousFreeHeap();\n  root[F(\"resetReason\")] = (int)ESP.getResetInfoPtr()->reason;\n  #endif\n  root[F(\"lwip\")] = LWIP_VERSION_MAJOR;\n#endif\n\n  root[F(\"freeheap\")] = getFreeHeapSize();\n  #if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM)\n  // Report PSRAM information\n  // Free PSRAM in bytes (backward compatibility)\n  root[F(\"psram\")] = ESP.getFreePsram(); \n  // Total PSRAM size in MB, round up to correct for allocator overhead\n  root[F(\"psrSz\")] = (ESP.getPsramSize() + (1024U * 1024U - 1)) / (1024U * 1024U); \n  #endif\n  root[F(\"uptime\")] = millis()/1000 + rolloverMillis*4294967;\n\n  char time[32];\n  getTimeString(time);\n  root[F(\"time\")] = time;\n\n  UsermodManager::addToJsonInfo(root);\n\n  uint16_t os = 0;\n  #ifdef WLED_DEBUG\n  os  = 0x80;\n    #ifdef WLED_DEBUG_HOST\n    os |= 0x0100;\n    if (!netDebugEnabled) os &= ~0x0080;\n    #endif\n  #endif\n  #ifndef WLED_DISABLE_ALEXA\n  os += 0x40;\n  #endif\n\n  //os += 0x20; // indicated now removed Blynk support, may be reused to indicate another build-time option\n\n  #ifdef USERMOD_CRONIXIE\n  os += 0x10;\n  #endif\n  #ifndef WLED_DISABLE_FILESYSTEM\n  os += 0x08;\n  #endif\n  #ifndef WLED_DISABLE_HUESYNC\n  os += 0x04;\n  #endif\n  #ifdef WLED_ENABLE_ADALIGHT\n  os += 0x02;\n  #endif\n  #ifndef WLED_DISABLE_OTA\n  os += 0x01;\n  #endif\n  root[F(\"opt\")] = os;\n\n  root[F(\"brand\")] = F(WLED_BRAND);\n  root[F(\"product\")] = F(WLED_PRODUCT_NAME);\n  root[\"mac\"] = escapedMac;\n  char s[16] = \"\";\n  if (Network.isConnected())\n  {\n    IPAddress localIP = Network.localIP();\n    sprintf(s, \"%d.%d.%d.%d\", localIP[0], localIP[1], localIP[2], localIP[3]);\n  }\n  root[\"ip\"] = s;\n}\n\nstatic void setPaletteColors(JsonArray json, CRGBPalette16 palette)\n{\n    for (int i = 0; i < 16; i++) {\n      JsonArray colors =  json.createNestedArray();\n      CRGB color = palette[i];\n      colors.add(i<<4);\n      colors.add(color.red);\n      colors.add(color.green);\n      colors.add(color.blue);\n    }\n}\n\nstatic void setPaletteColors(JsonArray json, byte* tcp)\n{\n    TRGBGradientPaletteEntryUnion* ent = (TRGBGradientPaletteEntryUnion*)(tcp);\n    TRGBGradientPaletteEntryUnion u;\n\n    // Count entries\n    unsigned count = 0;\n    do {\n        u = *(ent + count);\n        count++;\n    } while ( u.index != 255);\n\n    u = *ent;\n    int indexstart = 0;\n    while( indexstart < 255) {\n      indexstart = u.index;\n\n      JsonArray colors =  json.createNestedArray();\n      colors.add(u.index);\n      colors.add(u.r);\n      colors.add(u.g);\n      colors.add(u.b);\n\n      ent++;\n      u = *ent;\n    }\n}\n\nvoid serializePalettes(JsonObject root, int page)\n{\n  byte tcp[72];\n  #ifdef ESP8266\n  constexpr int itemPerPage = 5;\n  #else\n  constexpr int itemPerPage = 8;\n  #endif\n\n  const int customPalettesCount = customPalettes.size();\n  const int palettesCount = FIXED_PALETTE_COUNT; // palettesCount is number of palettes, not palette index\n\n  const int maxPage = (palettesCount + customPalettesCount) / itemPerPage;\n  if (page > maxPage) page = maxPage;\n\n  const int start = itemPerPage * page;\n  int end = min(start + itemPerPage, palettesCount + customPalettesCount);\n\n  root[F(\"m\")] = maxPage; // inform caller how many pages there are\n  JsonObject palettes  = root.createNestedObject(\"p\");\n\n  for (int i = start; i < end; i++) {\n    JsonArray curPalette = palettes.createNestedArray(String(i >= palettesCount ? 255 - i + palettesCount : i));\n    switch (i) {\n      case 0: //default palette\n        setPaletteColors(curPalette, PartyColors_p);\n        break;\n      case 1: //random\n           for (int j = 0; j < 4; j++) curPalette.add(\"r\");\n        break;\n      case 2: //primary color only\n        curPalette.add(\"c1\");\n        break;\n      case 3: //primary + secondary\n        curPalette.add(\"c1\");\n        curPalette.add(\"c1\");\n        curPalette.add(\"c2\");\n        curPalette.add(\"c2\");\n        break;\n      case 4: //primary + secondary + tertiary\n        curPalette.add(\"c3\");\n        curPalette.add(\"c2\");\n        curPalette.add(\"c1\");\n        break;\n      case 5: //primary + secondary (+tertiary if not off), more distinct\n        for (int j = 0; j < 5; j++) curPalette.add(\"c1\");\n        for (int j = 0; j < 5; j++) curPalette.add(\"c2\");\n        for (int j = 0; j < 5; j++) curPalette.add(\"c3\");\n        curPalette.add(\"c1\");\n        break;\n      default:\n        if (i >= palettesCount) // custom palettes\n          setPaletteColors(curPalette, customPalettes[i - palettesCount]);\n        else if (i < DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT) // palette 6 - 12, fastled palettes\n          setPaletteColors(curPalette, *fastledPalettes[i - DYNAMIC_PALETTE_COUNT]);\n        else {\n          memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[i - (DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_COUNT)])), sizeof(tcp));\n          setPaletteColors(curPalette, tcp);\n        }\n        break;\n    }\n  }\n}\n\nvoid serializeNetworks(JsonObject root)\n{\n  JsonArray networks = root.createNestedArray(F(\"networks\"));\n  int16_t status = WiFi.scanComplete();\n\n  switch (status) {\n    case WIFI_SCAN_FAILED:\n      WiFi.scanNetworks(true);\n      return;\n    case WIFI_SCAN_RUNNING:\n      return;\n  }\n\n  for (int i = 0; i < status; i++) {\n    JsonObject node = networks.createNestedObject();\n    node[F(\"ssid\")]    = WiFi.SSID(i);\n    node[F(\"rssi\")]    = WiFi.RSSI(i);\n    node[F(\"bssid\")]   = WiFi.BSSIDstr(i);\n    node[F(\"channel\")] = WiFi.channel(i);\n    node[F(\"enc\")]     = WiFi.encryptionType(i);\n  }\n\n  WiFi.scanDelete();\n\n  if (WiFi.scanComplete() == WIFI_SCAN_FAILED) {\n    WiFi.scanNetworks(true);\n  }\n}\n\nvoid serializeNodes(JsonObject root)\n{\n  JsonArray nodes = root.createNestedArray(\"nodes\");\n\n  for (NodesMap::iterator it = Nodes.begin(); it != Nodes.end(); ++it)\n  {\n    if (it->second.ip[0] != 0)\n    {\n      JsonObject node = nodes.createNestedObject();\n      node[F(\"name\")] = it->second.nodeName;\n      node[\"type\"]    = it->second.nodeType;\n      node[\"ip\"]      = it->second.ip.toString();\n      node[F(\"age\")]  = it->second.age;\n      node[F(\"vid\")]  = it->second.build;\n    }\n  }\n}\n\nvoid serializePins(JsonObject root)\n{\n  JsonArray pins = root.createNestedArray(F(\"pins\"));\n  #ifdef ESP8266\n  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\n  #else\n  constexpr int ENUM_PINS = WLED_NUM_PINS;\n  #endif\n  for (int gpio = 0; gpio < ENUM_PINS; gpio++) {\n    bool canInput = PinManager::isPinOk(gpio, false);\n    bool canOutput = PinManager::isPinOk(gpio, true);\n    bool isAllocated = PinManager::isPinAllocated(gpio);\n    // Skip pins that are neither usable nor allocated (truly unusable pins)\n    if (!canInput && !canOutput && !isAllocated) continue;\n\n    JsonObject pinObj = pins.createNestedObject();\n    pinObj[\"p\"] = gpio;  // pin number\n\n    // Pin capabilities\n    // Touch capability is provided by appendGPIOinfo() via d.touch\n    uint8_t caps = 0;\n\n    #ifdef ARDUINO_ARCH_ESP32\n    if (PinManager::isAnalogPin(gpio)) caps |= PIN_CAP_ADC;\n\n    // PWM on all ESP32 variants: all output pins can use ledc PWM so this is redundant\n    //if (canOutput) caps |= PIN_CAP_PWM;\n\n    // Input-only pins (ESP32 classic: GPIO34-39)\n    if (canInput && !canOutput) caps |= PIN_CAP_INPUT_ONLY;\n\n    // Bootloader/strapping pins\n    #if defined(CONFIG_IDF_TARGET_ESP32S3)\n    if (gpio == 0) caps |= PIN_CAP_BOOT;  // pull low to enter bootloader mode\n    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\n    #elif defined(CONFIG_IDF_TARGET_ESP32S2)\n    if (gpio == 0) caps |= PIN_CAP_BOOT; // pull low to enter bootloader mode\n    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\n    #elif defined(CONFIG_IDF_TARGET_ESP32C3)\n    if (gpio == 9) caps |= PIN_CAP_BOOT; // pull low to enter bootloader mode\n    if (gpio == 2 || gpio == 8) caps |= PIN_CAP_BOOTSTRAP; // both GPIO2 and GPIO8 must be high to enter bootloader mode\n    #elif defined(CONFIG_IDF_TARGET_ESP32) // ESP32 classic\n    if (gpio == 0) caps |= PIN_CAP_BOOT; // pull low to enter bootloader mode\n    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\n    #endif\n    #else\n    // ESP8266: GPIO 0-16 + GPIO17=A0\n    // if (gpio < 16) caps |= PIN_CAP_PWM;  // software PWM available on all GPIO except GPIO16\n    // ESP8266 strapping pins\n    if (gpio == 0) caps |= PIN_CAP_BOOT;\n    if (gpio == 2 || gpio == 15) caps |= PIN_CAP_BOOTSTRAP; // GPIO2 must be high, GPIO15 low to boot normally\n    if (gpio == 17) caps = PIN_CAP_INPUT_ONLY | PIN_CAP_ADC; // TODO: display as A0 pin\n    #endif\n\n    pinObj[\"c\"] = caps;  // capabilities\n\n    // Add allocated status and owner\n    pinObj[\"a\"] = isAllocated;  // allocated status\n\n    // check if this pin is used as a button (need to get button type for owner name)\n    int buttonIndex = PinManager::getButtonIndex(gpio); // returns -1 if not a button pin, otherwise returns index in buttons array\n\n    // Add owner ID and name\n    PinOwner owner = PinManager::getPinOwner(gpio);\n    if (isAllocated) {\n      pinObj[\"o\"] = static_cast<uint8_t>(owner);  // owner ID (can be used for UI lookup)\n      pinObj[\"n\"] = PinManager::getPinOwnerName(gpio);  // owner name (string)\n\n      // Relay pin\n      if (owner == PinOwner::Relay) {\n        pinObj[\"m\"] = 1;  // mode: output\n        pinObj[\"s\"] = digitalRead(rlyPin); // read state from hardware (digitalRead returns output state for output pins)\n      }\n      // Button pins, get type and state using isButtonPressed()\n      else if (buttonIndex >= 0) {\n        pinObj[\"m\"] = 0;  // mode: input\n        pinObj[\"t\"] = buttons[buttonIndex].type; // button type\n        pinObj[\"s\"] = isButtonPressed(buttonIndex) ? 1 : 0;  // state\n\n        // for touch buttons, get raw reading value (useful for debugging threshold)\n        #if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3)\n        if (buttons[buttonIndex].type == BTN_TYPE_TOUCH || buttons[buttonIndex].type == BTN_TYPE_TOUCH_SWITCH) {\n          if (digitalPinToTouchChannel(gpio) >= 0) {\n            #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3\n            pinObj[\"r\"] = touchRead(gpio) >> 4; // Touch V2 returns larger values, right shift by 4 to match threshold range, see set.cpp\n            #else\n            pinObj[\"r\"] = touchRead(gpio); // send raw value\n            #endif\n          }\n        }\n        #endif\n        // for analog buttons, get raw reading value\n        if (buttons[buttonIndex].type == BTN_TYPE_ANALOG || buttons[buttonIndex].type == BTN_TYPE_ANALOG_INVERTED) {\n          int analogRaw = 0;\n          #ifdef ESP8266\n          analogRaw = analogRead(A0) >> 2;   // convert 10bit read to 8bit, ESP8266 only has one analog pin\n          #else\n          if (digitalPinToAnalogChannel(gpio) >= 0) {\n            analogRaw = (analogRead(gpio)>>4); // right shift to match button value (8bit) see button.cpp\n          }\n          #endif\n          if (buttons[buttonIndex].type == BTN_TYPE_ANALOG_INVERTED) analogRaw = 255 - analogRaw;\n          pinObj[\"r\"] = analogRaw; // send raw value\n        }\n      }\n      // other allocated output pins that are simple GPIO (BusOnOff, Multi Relay, etc.) TODO: expand for other pin owners as needed\n      else if (owner == PinOwner::BusOnOff || owner == PinOwner::UM_MultiRelay) {\n        pinObj[\"m\"] = 1;  // mode: output\n        pinObj[\"s\"] = digitalRead(gpio);  // read state from hardware (digitalRead returns output state for output pins)\n      }\n    }\n  }\n}\n\n// deserializes mode data string into JsonArray\nvoid serializeModeData(JsonArray fxdata)\n{\n  char lineBuffer[256];\n  for (size_t i = 0; i < strip.getModeCount(); i++) {\n    strncpy_P(lineBuffer, strip.getModeData(i), sizeof(lineBuffer)/sizeof(char)-1);\n    lineBuffer[sizeof(lineBuffer)/sizeof(char)-1] = '\\0'; // terminate string\n    if (lineBuffer[0] != 0) {\n      char* dataPtr = strchr(lineBuffer,'@');\n      if (dataPtr) fxdata.add(dataPtr+1);\n      else         fxdata.add(\"\");\n    }\n  }\n}\n\n// deserializes mode names string into JsonArray\n// also removes effect data extensions (@...) from deserialised names\nvoid serializeModeNames(JsonArray arr)\n{\n  char lineBuffer[256];\n  for (size_t i = 0; i < strip.getModeCount(); i++) {\n    strncpy_P(lineBuffer, strip.getModeData(i), sizeof(lineBuffer)/sizeof(char)-1);\n    lineBuffer[sizeof(lineBuffer)/sizeof(char)-1] = '\\0'; // terminate string\n    if (lineBuffer[0] != 0) {\n      char* dataPtr = strchr(lineBuffer,'@');\n      if (dataPtr) *dataPtr = 0; // terminate mode data after name\n      arr.add(lineBuffer);\n    }\n  }\n}\n\n// Global buffer locking response helper class (to make sure lock is released when AsyncJsonResponse is destroyed)\nclass LockedJsonResponse: public AsyncJsonResponse {\n  bool _holding_lock;\n  public:\n  // WARNING: constructor assumes requestJSONBufferLock() was successfully acquired externally/prior to constructing the instance\n  // Not a good practice with C++. Unfortunately AsyncJsonResponse only has 2 constructors - for dynamic buffer or existing buffer,\n  // with existing buffer it clears its content during construction\n  // if the lock was not acquired (using JSONBufferGuard class) previous implementation still cleared existing buffer\n  inline LockedJsonResponse(JsonDocument* doc, bool isArray) : AsyncJsonResponse(doc, isArray), _holding_lock(true) {};\n\n  virtual size_t _fillBuffer(uint8_t *buf, size_t maxLen) { \n    size_t result = AsyncJsonResponse::_fillBuffer(buf, maxLen);\n    // Release lock as soon as we're done filling content\n    if (((result + _sentLength) >= (_contentLength)) && _holding_lock) {\n      releaseJSONBufferLock();\n      _holding_lock = false;\n    }\n    return result;\n  }\n\n  // destructor will remove JSON buffer lock when response is destroyed in AsyncWebServer\n  virtual ~LockedJsonResponse() { if (_holding_lock) releaseJSONBufferLock(); };\n};\n\nvoid serveJson(AsyncWebServerRequest* request)\n{\n  enum class json_target {\n    all, state, info, state_info, nodes, effects, palettes, fxdata, networks, config, pins\n  };\n  json_target subJson = json_target::all;\n\n  const String& url = request->url();\n  if      (url.indexOf(\"state\")    > 0) subJson = json_target::state;\n  else if (url.indexOf(\"info\")     > 0) subJson = json_target::info;\n  else if (url.indexOf(\"si\")       > 0) subJson = json_target::state_info;\n  else if (url.indexOf(F(\"nodes\")) > 0) subJson = json_target::nodes;\n  else if (url.indexOf(F(\"eff\"))   > 0) subJson = json_target::effects;\n  else if (url.indexOf(F(\"palx\"))  > 0) subJson = json_target::palettes;\n  else if (url.indexOf(F(\"fxda\"))  > 0) subJson = json_target::fxdata;\n  else if (url.indexOf(F(\"net\"))   > 0) subJson = json_target::networks;\n  else if (url.indexOf(F(\"cfg\"))   > 0) subJson = json_target::config;\n  else if (url.indexOf(F(\"pins\"))  > 0) subJson = json_target::pins;\n  #ifdef WLED_ENABLE_JSONLIVE\n  else if (url.indexOf(\"live\")     > 0) {\n    serveLiveLeds(request);\n    return;\n  }\n  #endif\n  else if (url.indexOf(\"pal\") > 0) {\n    request->send_P(200, FPSTR(CONTENT_TYPE_JSON), JSON_palette_names);\n    return;\n  }\n  else if (url.length() > 6) { //not just /json\n    serveJsonError(request, 501, ERR_NOT_IMPL);\n    return;\n  }\n\n  if (!requestJSONBufferLock(JSON_LOCK_SERVEJSON)) {\n    request->deferResponse();    \n    return;\n  }\n  // releaseJSONBufferLock() will be called when \"response\" is destroyed (from AsyncWebServer)\n  // make sure you delete \"response\" if no \"request->send(response);\" is made\n  LockedJsonResponse *response = new LockedJsonResponse(pDoc, subJson==json_target::fxdata || subJson==json_target::effects); // will clear and convert JsonDocument into JsonArray if necessary\n\n  JsonVariant lDoc = response->getRoot();\n\n  switch (subJson)\n  {\n    case json_target::state:\n      serializeState(lDoc); break;\n    case json_target::info:\n      serializeInfo(lDoc); break;\n    case json_target::nodes:\n      serializeNodes(lDoc); break;\n    case json_target::palettes:\n      serializePalettes(lDoc, request->hasParam(F(\"page\")) ? request->getParam(F(\"page\"))->value().toInt() : 0); break;\n    case json_target::effects:\n      serializeModeNames(lDoc); break;\n    case json_target::fxdata:\n      serializeModeData(lDoc); break;\n    case json_target::networks:\n      serializeNetworks(lDoc); break;\n    case json_target::config:\n      serializeConfig(lDoc); break;\n    case json_target::pins:\n      serializePins(lDoc); break;\n    case json_target::state_info:\n    case json_target::all:\n      JsonObject state = lDoc.createNestedObject(\"state\");\n      serializeState(state);\n      JsonObject info = lDoc.createNestedObject(\"info\");\n      serializeInfo(info);\n      if (subJson == json_target::all)\n      {\n        JsonArray effects = lDoc.createNestedArray(F(\"effects\"));\n        serializeModeNames(effects); // remove WLED-SR extensions from effect names\n        lDoc[F(\"palettes\")] = serialized((const __FlashStringHelper*)JSON_palette_names);\n      }\n      //lDoc[\"m\"] = lDoc.memoryUsage(); // JSON buffer usage, for remote debugging\n  }\n\n  DEBUG_PRINTF_P(PSTR(\"JSON buffer size: %u for request: %d\\n\"), lDoc.memoryUsage(), subJson);\n\n  [[maybe_unused]] size_t len = response->setLength();\n  DEBUG_PRINTF_P(PSTR(\"JSON content length: %u\\n\"), len);\n\n  request->send(response);\n}\n\n#ifdef WLED_ENABLE_JSONLIVE\n#define MAX_LIVE_LEDS 256\n\nbool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient)\n{\n  #ifdef WLED_ENABLE_WEBSOCKETS\n  AsyncWebSocketClient * wsc = nullptr;\n  if (!request) { //not HTTP, use Websockets\n    wsc = ws.client(wsClient);\n    if (!wsc || wsc->queueLength() > 0) return false; //only send if queue free\n  }\n  #endif\n\n  unsigned used = strip.getLengthTotal();\n  unsigned n = (used -1) /MAX_LIVE_LEDS +1; //only serve every n'th LED if count over MAX_LIVE_LEDS\n#ifndef WLED_DISABLE_2D\n  if (strip.isMatrix) {\n    // ignore anything behid matrix (i.e. extra strip)\n    used = Segment::maxWidth*Segment::maxHeight; // always the size of matrix (more or less than strip.getLengthTotal())\n    n = 1;\n    if (used > MAX_LIVE_LEDS) n = 2;\n    if (used > MAX_LIVE_LEDS*4) n = 4;\n  }\n#endif\n\n  DynamicBuffer buffer(9 + (9*(1+(used/n))) + 7 + 5 + 6 + 5 + 6 + 5 + 2);  \n  char* buf = buffer.data();      // assign buffer for oappnd() functions\n  strncpy_P(buffer.data(), PSTR(\"{\\\"leds\\\":[\"), buffer.size());\n  buf += 9; // sizeof(PSTR()) from last line\n\n  for (size_t i = 0; i < used; i += n)\n  {\n#ifndef WLED_DISABLE_2D\n    if (strip.isMatrix && n>1 && (i/Segment::maxWidth)%n) i += Segment::maxWidth * (n-1);\n#endif\n    uint32_t c = strip.getPixelColor(i);\n    uint8_t r = R(c);\n    uint8_t g = G(c);\n    uint8_t b = B(c);\n    uint8_t w = W(c);\n    r = scale8(qadd8(w, r), strip.getBrightness()); //R, add white channel to RGB channels as a simple RGBW -> RGB map\n    g = scale8(qadd8(w, g), strip.getBrightness()); //G\n    b = scale8(qadd8(w, b), strip.getBrightness()); //B\n    buf += sprintf_P(buf, PSTR(\"\\\"%06X\\\",\"), RGBW32(r,g,b,0));\n  }\n  buf--;  // remove last comma\n  buf += sprintf_P(buf, PSTR(\"],\\\"n\\\":%d\"), n);\n#ifndef WLED_DISABLE_2D\n  if (strip.isMatrix) {\n    buf += sprintf_P(buf, PSTR(\",\\\"w\\\":%d\"), Segment::maxWidth/n);\n    buf += sprintf_P(buf, PSTR(\",\\\"h\\\":%d\"), Segment::maxHeight/n);\n  }\n#endif\n  (*buf++) = '}';\n  (*buf++) = 0;\n  \n  if (request) {\n    request->send(200, FPSTR(CONTENT_TYPE_JSON), toString(std::move(buffer)));\n  }\n  #ifdef WLED_ENABLE_WEBSOCKETS\n  else {\n    wsc->text(toString(std::move(buffer)));\n  }\n  #endif  \n  return true;\n}\n#endif\n"
  },
  {
    "path": "wled00/led.cpp",
    "content": "#include \"wled.h\"\n\n/*\n * LED methods\n */\n\n // applies chosen setment properties to legacy values\nvoid setValuesFromSegment(uint8_t s) {\n  const Segment& seg = strip.getSegment(s);\n  colPri[0] = R(seg.colors[0]);\n  colPri[1] = G(seg.colors[0]);\n  colPri[2] = B(seg.colors[0]);\n  colPri[3] = W(seg.colors[0]);\n  colSec[0] = R(seg.colors[1]);\n  colSec[1] = G(seg.colors[1]);\n  colSec[2] = B(seg.colors[1]);\n  colSec[3] = W(seg.colors[1]);\n  effectCurrent   = seg.mode;\n  effectSpeed     = seg.speed;\n  effectIntensity = seg.intensity;\n  effectPalette   = seg.palette;\n}\n\n\n// applies global legacy values (colPri, colSec, effectCurrent...) to each selected segment\nvoid applyValuesToSelectedSegs() {\n  for (unsigned i = 0; i < strip.getSegmentsNum(); i++) {\n    Segment& seg = strip.getSegment(i);\n    if (!(seg.isActive() && seg.isSelected())) continue;\n    if (effectSpeed     != seg.speed)     {seg.speed     = effectSpeed;     stateChanged = true;}\n    if (effectIntensity != seg.intensity) {seg.intensity = effectIntensity; stateChanged = true;}\n    if (effectPalette   != seg.palette)   {seg.setPalette(effectPalette);}\n    if (effectCurrent   != seg.mode)      {seg.setMode(effectCurrent);}\n    uint32_t col0 = RGBW32(colPri[0], colPri[1], colPri[2], colPri[3]);\n    uint32_t col1 = RGBW32(colSec[0], colSec[1], colSec[2], colSec[3]);\n    if (col0 != seg.colors[0])            {seg.setColor(0, col0);}\n    if (col1 != seg.colors[1])            {seg.setColor(1, col1);}\n  }\n}\n\n\nvoid toggleOnOff()\n{\n  if (bri == 0)\n  {\n    bri = briLast;\n    strip.restartRuntime();\n  } else\n  {\n    briLast = bri;\n    bri = 0;\n  }\n  stateChanged = true;\n}\n\n\n//scales the brightness with the briMultiplier factor\nbyte scaledBri(byte in)\n{\n  unsigned val = ((unsigned)in*briMultiplier)/100;\n  if (val > 255) val = 255;\n  return (byte)val;\n}\n\n\n//applies global temporary brightness (briT) to strip\nvoid applyBri() {\n  if (realtimeOverride || !(realtimeMode && arlsForceMaxBri))\n  {\n    //DEBUG_PRINTF_P(PSTR(\"Applying strip brightness: %d (%d,%d)\\n\"), (int)briT, (int)bri, (int)briOld);\n    strip.setBrightness(briT);\n  }\n}\n\n\n//applies global brightness and sets it as the \"current\" brightness (no transition)\nvoid applyFinalBri() {\n  briOld = bri;\n  briT = bri;\n  applyBri();\n  strip.trigger(); // force one last update\n}\n\n\n//called after every state changes, schedules interface updates, handles brightness transition and nightlight activation\n//unlike colorUpdated(), does NOT apply any colors or FX to segments\nvoid stateUpdated(byte callMode) {\n  //call for notifier -> 0: init 1: direct change 2: button 3: notification 4: nightlight 5: other (No notification)\n  //                     6: fx changed 7: hue 8: preset cycle 9: blynk 10: alexa 11: ws send only 12: button preset\n  setValuesFromFirstSelectedSeg();  // a much better approach would be to use main segment: setValuesFromMainSeg()\n\n  if (bri != briOld || stateChanged) {\n    if (stateChanged) currentPreset = 0; //something changed, so we are no longer in the preset\n\n    if (callMode != CALL_MODE_NOTIFICATION && callMode != CALL_MODE_NO_NOTIFY) notify(callMode);\n    if (bri != briOld && nodeBroadcastEnabled) sendSysInfoUDP(); // update on state\n\n    //set flag to update ws and mqtt\n    interfaceUpdateCallMode = callMode;\n  } else {\n    if (nightlightActive && !nightlightActiveOld && callMode != CALL_MODE_NOTIFICATION && callMode != CALL_MODE_NO_NOTIFY) {\n      notify(CALL_MODE_NIGHTLIGHT);\n      interfaceUpdateCallMode = CALL_MODE_NIGHTLIGHT;\n    }\n  }\n\n  unsigned long now = millis();\n  if (callMode != CALL_MODE_NO_NOTIFY && nightlightActive && (nightlightMode == NL_MODE_FADE || nightlightMode == NL_MODE_COLORFADE)) {\n    briNlT = bri;\n    nightlightDelayMs -= (now - nightlightStartTime);\n    nightlightStartTime = now;\n  }\n  if (briT == 0) {\n    if (callMode != CALL_MODE_NOTIFICATION) strip.resetTimebase(); //effect start from beginning\n  }\n\n  if (bri > 0) briLast = bri;\n\n  //deactivate nightlight if target brightness is reached\n  if (bri == nightlightTargetBri && callMode != CALL_MODE_NO_NOTIFY && nightlightMode != NL_MODE_SUN) nightlightActive = false;\n\n  // notify usermods of state change\n  UsermodManager::onStateChange(callMode);\n\n  if (strip.getTransition() == 0) {\n    jsonTransitionOnce = false;\n    transitionActive = false;\n    applyFinalBri();\n    strip.trigger();\n  } else {\n    if (transitionActive) {\n      briOld = briT;\n    } else if (bri != briOld || stateChanged)\n      strip.setTransitionMode(true); // force all segments to transition mode\n    transitionActive = true;\n    transitionStartTime = now;\n  }\n  stateChanged = false;\n}\n\n\nvoid updateInterfaces(uint8_t callMode) {\n  if (!interfaceUpdateCallMode || millis() - lastInterfaceUpdate < INTERFACE_UPDATE_COOLDOWN) return;\n\n  sendDataWs();\n  lastInterfaceUpdate = millis();\n  interfaceUpdateCallMode = CALL_MODE_INIT; //disable further updates\n\n  if (callMode == CALL_MODE_WS_SEND) return;\n\n  #ifndef WLED_DISABLE_ALEXA\n  if (espalexaDevice != nullptr && callMode != CALL_MODE_ALEXA) {\n    espalexaDevice->setValue(bri);\n    espalexaDevice->setColor(colPri[0], colPri[1], colPri[2]);\n  }\n  #endif\n  #ifndef WLED_DISABLE_MQTT\n  publishMqtt();\n  #endif\n}\n\n\nvoid handleTransitions() {\n  //handle still pending interface update\n  updateInterfaces(interfaceUpdateCallMode);\n\n  if (transitionActive && strip.getTransition() > 0) {\n    int ti = millis() - transitionStartTime;\n    int tr = strip.getTransition();\n    if (ti/tr) {\n      strip.setTransitionMode(false); // stop all transitions\n      // restore (global) transition time if not called from UDP notifier or single/temporary transition from JSON (also playlist)\n      if (jsonTransitionOnce) strip.setTransition(transitionDelay);\n      transitionActive = false;\n      jsonTransitionOnce = false;\n      applyFinalBri();\n      return;\n    }\n    byte briTO = briT;\n    int deltaBri = (int)bri - (int)briOld;\n    briT = briOld + (deltaBri * ti / tr);\n    if (briTO != briT) applyBri();\n  }\n}\n\n\n// legacy method, applies values from col, effectCurrent, ... to selected segments\nvoid colorUpdated(byte callMode) {\n  applyValuesToSelectedSegs();\n  stateUpdated(callMode);\n}\n\n\nvoid handleNightlight() {\n  unsigned long now = millis();\n  if (now < 100 && lastNlUpdate > 0) lastNlUpdate = 0; // take care of millis() rollover\n  if (now - lastNlUpdate < 100) return; // allow only 10 NL updates per second\n  lastNlUpdate = now;\n\n  if (nightlightActive)\n  {\n    if (!nightlightActiveOld) //init\n    {\n      nightlightStartTime = millis();\n      nightlightDelayMs = (unsigned)(nightlightDelayMins*60000);\n      nightlightActiveOld = true;\n      briNlT = bri;\n      for (unsigned i=0; i<4; i++) colNlT[i] = colPri[i]; // remember starting color\n      if (nightlightMode == NL_MODE_SUN)\n      {\n        //save current\n        colNlT[0] = effectCurrent;\n        colNlT[1] = effectSpeed;\n        colNlT[2] = effectPalette;\n\n        strip.getFirstSelectedSeg().setMode(FX_MODE_STATIC); // make sure seg runtime is reset if it was in sunrise mode\n        effectCurrent = FX_MODE_SUNRISE;            // colorUpdated() will take care of assigning that to all selected segments\n        effectSpeed = nightlightDelayMins;\n        effectPalette = 0;\n        if (effectSpeed > 60) effectSpeed = 60; //currently limited to 60 minutes\n        if (bri) effectSpeed += 60; //sunset if currently on\n        briNlT = !bri; //true == sunrise, false == sunset\n        if (!bri) bri = briLast;\n        colorUpdated(CALL_MODE_NO_NOTIFY);\n      }\n    }\n    float nper = (millis() - nightlightStartTime)/((float)nightlightDelayMs);\n    if (nightlightMode == NL_MODE_FADE || nightlightMode == NL_MODE_COLORFADE)\n    {\n      bri = briNlT + ((nightlightTargetBri - briNlT)*nper);\n      if (nightlightMode == NL_MODE_COLORFADE)                                         // color fading only is enabled with \"NF=2\"\n      {\n        for (unsigned i=0; i<4; i++) colPri[i] = colNlT[i]+ ((colSec[i] - colNlT[i])*nper);   // fading from actual color to secondary color\n      }\n      colorUpdated(CALL_MODE_NO_NOTIFY);\n    }\n    if (nper >= 1) //nightlight duration over\n    {\n      nightlightActive = false;\n      if (nightlightMode == NL_MODE_SET)\n      {\n        bri = nightlightTargetBri;\n        colorUpdated(CALL_MODE_NO_NOTIFY);\n      }\n      if (bri == 0) briLast = briNlT;\n      if (nightlightMode == NL_MODE_SUN)\n      {\n        if (!briNlT) { //turn off if sunset\n          effectCurrent = colNlT[0];\n          effectSpeed = colNlT[1];\n          effectPalette = colNlT[2];\n          toggleOnOff();\n          applyFinalBri();\n        }\n      }\n\n      if (macroNl > 0)\n        applyPreset(macroNl);\n      nightlightActiveOld = false;\n    }\n  } else if (nightlightActiveOld) //early de-init\n  {\n    if (nightlightMode == NL_MODE_SUN) { //restore previous effect\n      effectCurrent = colNlT[0];\n      effectSpeed = colNlT[1];\n      effectPalette = colNlT[2];\n      colorUpdated(CALL_MODE_NO_NOTIFY);\n    }\n    nightlightActiveOld = false;\n  }\n}\n\n//utility for FastLED to use our custom timer\nuint32_t get_millisecond_timer() {\n  return strip.now;\n}\n"
  },
  {
    "path": "wled00/lx_parser.cpp",
    "content": "#include \"wled.h\"\n\n#ifdef WLED_ENABLE_LOXONE\n\n/*\n * Parser for Loxone formats\n */\nbool parseLx(int lxValue, byte* rgbw)\n{\n  DEBUG_PRINT(F(\"LX: Lox = \"));\n  DEBUG_PRINTLN(lxValue);\n\n  bool ok = false;\n  float lxRed = 0, lxGreen = 0, lxBlue = 0;\n\n  if (lxValue < 200000000) {\n    // Loxone RGB\n    ok = true;\n    lxRed = round((lxValue % 1000) * 2.55);\n    lxGreen = round(((lxValue / 1000) % 1000) * 2.55);\n    lxBlue = round(((lxValue / 1000000) % 1000) * 2.55);\n  } else if ((lxValue >= 200000000) && (lxValue <= 201006500)) {\n    // Loxone Lumitech\n    ok = true;\n    float tmpBri = floor((lxValue - 200000000) / 10000);\n    uint16_t ct = (lxValue - 200000000) - (((uint8_t)tmpBri) * 10000);\n\n    tmpBri *= 2.55f;\n    tmpBri = constrain(tmpBri, 0, 255);\n\n    colorKtoRGB(ct, rgbw);\n    lxRed = rgbw[0]; lxGreen = rgbw[1]; lxBlue = rgbw[2];\n\n    lxRed *= (tmpBri/255);\n    lxGreen *= (tmpBri/255);\n    lxBlue *= (tmpBri/255);\n  }\n\n  if (ok) {\n    rgbw[0] = (uint8_t) constrain(lxRed, 0, 255);\n    rgbw[1] = (uint8_t) constrain(lxGreen, 0, 255);\n    rgbw[2] = (uint8_t) constrain(lxBlue, 0, 255);\n    rgbw[3] = 0;\n    return true;\n  }\n  return false;\n}\n\nvoid parseLxJson(int lxValue, byte segId, bool secondary)\n{\n  if (secondary) {\n    DEBUG_PRINT(F(\"LY: Lox secondary = \"));\n  } else {\n    DEBUG_PRINT(F(\"LX: Lox primary = \"));\n  }\n  DEBUG_PRINTLN(lxValue);\n  byte rgbw[] = {0,0,0,0};\n  if (parseLx(lxValue, rgbw)) {\n    if (bri == 0) {\n      DEBUG_PRINTLN(F(\"LX: turn on\"));\n      toggleOnOff();\n    }\n    bri = 255;\n    nightlightActive = false; //always disable nightlight when toggling\n    DEBUG_PRINT(F(\"LX: segment \"));\n    DEBUG_PRINTLN(segId);\n    strip.getSegment(segId).setColor(secondary, RGBW32(rgbw[0], rgbw[1], rgbw[2], rgbw[3])); // legacy values handled as well in json.cpp by stateUpdated()\n  }\n}\n\n#endif\n"
  },
  {
    "path": "wled00/mqtt.cpp",
    "content": "#include \"wled.h\"\n\n/*\n * MQTT communication protocol for home automation\n */\n\n#ifndef WLED_DISABLE_MQTT\n#define MQTT_KEEP_ALIVE_TIME 60    // contact the MQTT broker every 60 seconds\n\n#if MQTT_MAX_TOPIC_LEN > 32\n#warning \"MQTT topics length > 32 is not recommended for compatibility with usermods!\"\n#endif\n\nstatic const char* sTopicFormat PROGMEM = \"%.*s/%s\";\n\n// parse payload for brightness, ON/OFF or toggle\n// briLast is used to remember last brightness value in case of ON/OFF or toggle\n// bri is set to 0 if payload is \"0\" or \"OFF\" or \"false\"\nstatic void parseMQTTBriPayload(char* payload)\n{\n  if      (strstr(payload, \"ON\") || strstr(payload, \"on\") || strstr(payload, \"true\")) {bri = briLast; stateUpdated(CALL_MODE_DIRECT_CHANGE);}\n  else if (strstr(payload, \"T\" ) || strstr(payload, \"t\" )) {toggleOnOff(); stateUpdated(CALL_MODE_DIRECT_CHANGE);}\n  else {\n    uint8_t in = strtoul(payload, NULL, 10);\n    if (in == 0 && bri > 0) briLast = bri;\n    bri = in;\n    stateUpdated(CALL_MODE_DIRECT_CHANGE);\n  }\n}\n\n\nstatic void onMqttConnect(bool sessionPresent)\n{\n  //(re)subscribe to required topics\n  char subuf[MQTT_MAX_TOPIC_LEN + 9];\n\n  if (mqttDeviceTopic[0] != 0) {\n    mqtt->subscribe(mqttDeviceTopic, 0);\n    snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, \"col\");\n    mqtt->subscribe(subuf, 0);\n    snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, \"api\");\n    mqtt->subscribe(subuf, 0);\n  }\n\n  if (mqttGroupTopic[0] != 0) {\n    mqtt->subscribe(mqttGroupTopic, 0);\n    snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttGroupTopic, \"col\");\n    mqtt->subscribe(subuf, 0);\n    snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttGroupTopic, \"api\");\n    mqtt->subscribe(subuf, 0);\n  }\n\n  UsermodManager::onMqttConnect(sessionPresent);\n\n  DEBUG_PRINTLN(F(\"MQTT ready\"));\n\n#ifndef USERMOD_SMARTNEST\n  snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, \"status\");\n  mqtt->publish(subuf, 0, true, \"online\"); // retain message for a LWT\n#endif\n\n  publishMqtt();\n}\n\n\nstatic void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) {\n  static char *payloadStr;\n\n  DEBUG_PRINTF_P(PSTR(\"MQTT msg: %s\\n\"), topic);\n\n  // paranoia check to avoid npe if no payload\n  if (payload==nullptr) {\n    DEBUG_PRINTLN(F(\"no payload -> leave\"));\n    return;\n  }\n\n  if (index == 0) {                       // start (1st partial packet or the only packet)\n    p_free(payloadStr);                   // release buffer if it exists\n    payloadStr = static_cast<char*>(p_malloc(total+1)); // allocate new buffer\n  }\n  if (payloadStr == nullptr) return;      // buffer not allocated\n\n  // copy (partial) packet to buffer and 0-terminate it if it is last packet\n  char* buff = payloadStr + index;\n  memcpy(buff, payload, len);\n  if (index + len >= total) { // at end\n    payloadStr[total] = '\\0'; // terminate c style string\n  } else {\n    DEBUG_PRINTLN(F(\"MQTT partial packet received.\"));\n    return; // process next packet\n  }\n  DEBUG_PRINTLN(payloadStr);\n\n  size_t topicPrefixLen = strlen(mqttDeviceTopic);\n  if (strncmp(topic, mqttDeviceTopic, topicPrefixLen) == 0) {\n    topic += topicPrefixLen;\n  } else {\n    topicPrefixLen = strlen(mqttGroupTopic);\n    if (strncmp(topic, mqttGroupTopic, topicPrefixLen) == 0) {\n      topic += topicPrefixLen;\n    } else {\n      // Non-Wled Topic used here. Probably a usermod subscribed to this topic.\n      UsermodManager::onMqttMessage(topic, payloadStr);\n      p_free(payloadStr);\n      payloadStr = nullptr;\n      return;\n    }\n  }\n\n  //Prefix is stripped from the topic at this point\n\n  if (strcmp_P(topic, PSTR(\"/col\")) == 0) {\n    colorFromDecOrHexString(colPri, payloadStr);\n    colorUpdated(CALL_MODE_DIRECT_CHANGE);\n  } else if (strcmp_P(topic, PSTR(\"/api\")) == 0) {\n    if (requestJSONBufferLock(JSON_LOCK_MQTT)) {\n      if (payloadStr[0] == '{') { //JSON API\n        deserializeJson(*pDoc, payloadStr);\n        deserializeState(pDoc->as<JsonObject>());\n      } else { //HTTP API\n        String apireq = \"win\"; apireq += '&'; // reduce flash string usage\n        apireq += payloadStr;\n        handleSet(nullptr, apireq);\n      }\n      releaseJSONBufferLock();\n    }\n  } else if (strlen(topic) != 0) {\n    // non standard topic, check with usermods\n    UsermodManager::onMqttMessage(topic, payloadStr);\n  } else {\n    // topmost topic (just wled/MAC)\n    parseMQTTBriPayload(payloadStr);\n  }\n  p_free(payloadStr);\n  payloadStr = nullptr;\n}\n\n// Print adapter for flat buffers\nnamespace {\nclass bufferPrint : public Print {\n  char* _buf;\n  size_t _size, _offset;\n  public:\n\n  bufferPrint(char* buf, size_t size) : _buf(buf), _size(size), _offset(0) {};\n\n  size_t write(const uint8_t *buffer, size_t size) {\n    size = std::min(size, _size - _offset);\n    memcpy(_buf + _offset, buffer, size);\n    _offset += size;\n    return size;\n  }\n\n  size_t write(uint8_t c) {\n    return this->write(&c, 1);\n  }\n\n  char* data() const { return _buf; }\n  size_t size() const { return _offset; }\n  size_t capacity() const { return _size; }\n};\n}; // anonymous namespace\n\n\nvoid publishMqtt()\n{\n  if (!WLED_MQTT_CONNECTED) return;\n  DEBUG_PRINTLN(F(\"Publish MQTT\"));\n\n  #ifndef USERMOD_SMARTNEST\n  char s[10];\n  char subuf[MQTT_MAX_TOPIC_LEN + 16];\n\n  sprintf_P(s, PSTR(\"%u\"), bri);\n  snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, \"g\");\n  mqtt->publish(subuf, 0, retainMqttMsg, s);         // optionally retain message (#2263)\n\n  sprintf_P(s, PSTR(\"#%06X\"), (colPri[3] << 24) | (colPri[0] << 16) | (colPri[1] << 8) | (colPri[2]));\n  snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, \"c\");\n  mqtt->publish(subuf, 0, retainMqttMsg, s);         // optionally retain message (#2263)\n\n  snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, \"status\");\n  mqtt->publish(subuf, 0, true, \"online\");  // retain message for a LWT\n\n  // TODO: use a DynamicBufferList.  Requires a list-read-capable MQTT client API.\n  DynamicBuffer buf(1024);\n  bufferPrint pbuf(buf.data(), buf.size());\n  XML_response(pbuf);\n  snprintf_P(subuf, sizeof(subuf)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, \"v\");\n  mqtt->publish(subuf, 0, retainMqttMsg, buf.data(), pbuf.size());   // optionally retain message (#2263)\n  #endif\n}\n\n\n//HA autodiscovery was removed in favor of the native integration in HA v0.102.0\n\nbool initMqtt()\n{\n  if (!mqttEnabled || mqttServer[0] == 0 || !WLED_CONNECTED) return false;\n\n  if (mqtt == nullptr) {\n    void *ptr = p_malloc(sizeof(AsyncMqttClient));\n    mqtt = new (ptr) AsyncMqttClient(); // use placement new (into PSRAM), client will never be deleted\n    if (!mqtt) return false;\n    mqtt->onMessage(onMqttMessage);\n    mqtt->onConnect(onMqttConnect);\n  }\n  if (mqtt->connected()) return true;\n\n  DEBUG_PRINTLN(F(\"Reconnecting MQTT\"));\n  IPAddress mqttIP;\n  if (mqttIP.fromString(mqttServer)) //see if server is IP or domain\n  {\n    mqtt->setServer(mqttIP, mqttPort);\n  } else {\n    #ifdef ARDUINO_ARCH_ESP32\n    String mqttMDNS = mqttServer;\n    mqttMDNS.toLowerCase(); // make sure we have a lowercase hostname\n    int pos = mqttMDNS.indexOf(F(\".local\"));\n    if (pos > 0) mqttMDNS.remove(pos); // remove .local domain if present (and anything following it)\n    if (strlen(cmDNS) > 0 && mqttMDNS.length() > 0 && mqttMDNS.indexOf('.') < 0) { // if mDNS is enabled and server does not have domain\n      mqttIP = MDNS.queryHost(mqttMDNS.c_str());\n      if (mqttIP != IPAddress()) // if MDNS resolved the hostname\n        mqtt->setServer(mqttIP, mqttPort);\n      else\n        mqtt->setServer(mqttServer, mqttPort);\n    } else\n    #endif\n      mqtt->setServer(mqttServer, mqttPort);\n  }\n  mqtt->setClientId(mqttClientID);\n  if (mqttUser[0] && mqttPass[0]) mqtt->setCredentials(mqttUser, mqttPass);\n\n  #ifndef USERMOD_SMARTNEST\n  snprintf_P(mqttStatusTopic, sizeof(mqttStatusTopic)-1, sTopicFormat, MQTT_MAX_TOPIC_LEN, mqttDeviceTopic, \"status\");\n  mqtt->setWill(mqttStatusTopic, 0, true, \"offline\"); // LWT message\n  #endif\n  mqtt->setKeepAlive(MQTT_KEEP_ALIVE_TIME);\n  mqtt->connect();\n  return true;\n}\n#endif\n"
  },
  {
    "path": "wled00/my_config_sample.h",
    "content": "#pragma once\n\n/*\n * Welcome!\n * You can use the file \"my_config.h\" to make changes to the way WLED is compiled!\n * It is possible to enable and disable certain features as well as set defaults for some runtime changeable settings.\n *\n * How to use:\n * PlatformIO: Just compile the unmodified code once! The file \"my_config.h\" will be generated automatically and now you can make your changes.\n *\n * ArduinoIDE: Make a copy of this file and name it \"my_config.h\". Go to wled.h and uncomment \"#define WLED_USE_MY_CONFIG\" in the top of the file.\n *\n * DO NOT make changes to the \"my_config_sample.h\" file directly! Your changes will not be applied.\n */\n\n// uncomment to force the compiler to show a warning to confirm that this file is included\n//#warning **** my_config.h: Settings from this file are honored ****\n\n/* Uncomment to use your WIFI settings as defaults\n  //WARNING: this will hardcode these as the default even after a factory reset\n#define CLIENT_SSID \"Your_SSID\"\n#define CLIENT_PASS \"Your_Password\"\n*/\n\n//#define MAX_LEDS 1500       // Maximum total LEDs. More than 1500 might create a low memory situation on ESP8266.\n//#define MDNS_NAME \"wled\"    // mDNS hostname, ie: *.local\n"
  },
  {
    "path": "wled00/net_debug.cpp",
    "content": "#include \"wled.h\"\n\n#ifdef WLED_DEBUG_HOST\n\nsize_t NetworkDebugPrinter::write(uint8_t c) {\n  if (!WLED_CONNECTED || !netDebugEnabled) return 0;\n\n  if (!debugPrintHostIP && !debugPrintHostIP.fromString(netDebugPrintHost)) {\n    #ifdef ESP8266\n      WiFi.hostByName(netDebugPrintHost, debugPrintHostIP, 750);\n    #else\n      #ifdef WLED_USE_ETHERNET\n        ETH.hostByName(netDebugPrintHost, debugPrintHostIP);\n      #else\n        WiFi.hostByName(netDebugPrintHost, debugPrintHostIP);\n      #endif\n    #endif\n  }\n\n  debugUdp.beginPacket(debugPrintHostIP, netDebugPrintPort);\n  debugUdp.write(c);\n  debugUdp.endPacket();\n  return 1;\n}\n\nsize_t NetworkDebugPrinter::write(const uint8_t *buf, size_t size) {\n  if (!WLED_CONNECTED || buf == nullptr || !netDebugEnabled) return 0;\n\n  if (!debugPrintHostIP && !debugPrintHostIP.fromString(netDebugPrintHost)) {\n    #ifdef ESP8266\n      WiFi.hostByName(netDebugPrintHost, debugPrintHostIP, 750);\n    #else\n      #ifdef WLED_USE_ETHERNET\n        ETH.hostByName(netDebugPrintHost, debugPrintHostIP);\n      #else\n        WiFi.hostByName(netDebugPrintHost, debugPrintHostIP);\n      #endif\n    #endif\n  }\n\n  debugUdp.beginPacket(debugPrintHostIP, netDebugPrintPort);\n  size = debugUdp.write(buf, size);\n  debugUdp.endPacket();\n  return size;\n}\n\nNetworkDebugPrinter NetDebug;\n\n#endif\n"
  },
  {
    "path": "wled00/net_debug.h",
    "content": "#ifndef WLED_NET_DEBUG_H\n#define WLED_NET_DEBUG_H\n\n#include <WString.h>\n#include <WiFiUdp.h>\n\nclass NetworkDebugPrinter : public Print {\n  private:\n    WiFiUDP debugUdp; // needs to be here otherwise UDP messages get truncated upon destruction\n    IPAddress debugPrintHostIP;\n  public:\n    virtual size_t write(uint8_t c);\n    virtual size_t write(const uint8_t *buf, size_t s);\n};\n\n// use it on your linux/macOS with: nc -p 7868 -u -l -s <network ip>\nextern NetworkDebugPrinter NetDebug;\n\n#endif"
  },
  {
    "path": "wled00/network.cpp",
    "content": "#include \"wled.h\"\n#include \"fcn_declare.h\"\n#include \"wled_ethernet.h\"\n\n\n#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET)\n// The following six pins are neither configurable nor\n// can they be re-assigned through IOMUX / GPIO matrix.\n// See https://docs.espressif.com/projects/esp-idf/en/latest/esp32/hw-reference/esp32/get-started-ethernet-kit-v1.1.html#ip101gri-phy-interface\nconst managed_pin_type esp32_nonconfigurable_ethernet_pins[WLED_ETH_RSVD_PINS_COUNT] = {\n    { 21, true  }, // RMII EMAC TX EN  == When high, clocks the data on TXD0 and TXD1 to transmitter\n    { 19, true  }, // RMII EMAC TXD0   == First bit of transmitted data\n    { 22, true  }, // RMII EMAC TXD1   == Second bit of transmitted data\n    { 25, false }, // RMII EMAC RXD0   == First bit of received data\n    { 26, false }, // RMII EMAC RXD1   == Second bit of received data\n    { 27, true  }, // RMII EMAC CRS_DV == Carrier Sense and RX Data Valid\n};\n\nconst ethernet_settings ethernetBoards[] = {\n  // None\n  {\n  },\n\n  // WT32-EHT01\n  // Please note, from my testing only these pins work for LED outputs:\n  //   IO2, IO4, IO12, IO14, IO15\n  // These pins do not appear to work from my testing:\n  //   IO35, IO36, IO39\n  {\n    1,                    // eth_address,\n    16,                   // eth_power,\n    23,                   // eth_mdc,\n    18,                   // eth_mdio,\n    ETH_PHY_LAN8720,      // eth_type,\n    ETH_CLOCK_GPIO0_IN    // eth_clk_mode\n  },\n\n  // ESP32-POE\n  {\n     0,                   // eth_address,\n    12,                   // eth_power,\n    23,                   // eth_mdc,\n    18,                   // eth_mdio,\n    ETH_PHY_LAN8720,      // eth_type,\n    ETH_CLOCK_GPIO17_OUT  // eth_clk_mode\n  },\n\n   // WESP32\n  {\n    0,\t\t\t              // eth_address,\n    -1,\t\t\t              // eth_power,\n    16,\t\t\t              // eth_mdc,\n    17,\t\t\t              // eth_mdio,\n    ETH_PHY_LAN8720,      // eth_type,\n    ETH_CLOCK_GPIO0_IN\t  // eth_clk_mode\n  },\n\n  // QuinLed-ESP32-Ethernet\n  {\n    0,\t\t\t              // eth_address,\n    5,\t\t\t              // eth_power,\n    23,\t\t\t              // eth_mdc,\n    18,\t\t\t              // eth_mdio,\n    ETH_PHY_LAN8720,      // eth_type,\n    ETH_CLOCK_GPIO17_OUT\t// eth_clk_mode\n  },\n\n  // TwilightLord-ESP32 Ethernet Shield\n  {\n    0,\t\t\t              // eth_address,\n    5,\t\t\t              // eth_power,\n    23,\t\t\t              // eth_mdc,\n    18,\t\t\t              // eth_mdio,\n    ETH_PHY_LAN8720,      // eth_type,\n    ETH_CLOCK_GPIO17_OUT\t// eth_clk_mode\n  },\n\n  // ESP3DEUXQuattro\n  {\n    1,                    // eth_address,\n    -1,                   // eth_power,\n    23,                   // eth_mdc,\n    18,                   // eth_mdio,\n    ETH_PHY_LAN8720,      // eth_type,\n    ETH_CLOCK_GPIO17_OUT  // eth_clk_mode\n  },\n\n  // ESP32-ETHERNET-KIT-VE\n  {\n    0,                    // eth_address,\n    5,                    // eth_power,\n    23,                   // eth_mdc,\n    18,                   // eth_mdio,\n    ETH_PHY_IP101,        // eth_type,\n    ETH_CLOCK_GPIO0_IN    // eth_clk_mode\n  },\n\n  // QuinLed-Dig-Octa Brainboard-32-8L and LilyGO-T-ETH-POE\n  {\n    0,\t\t\t              // eth_address,\n    -1,\t\t\t              // eth_power,\n    23,\t\t\t              // eth_mdc,\n    18,\t\t\t              // eth_mdio,\n    ETH_PHY_LAN8720,      // eth_type,\n    ETH_CLOCK_GPIO17_OUT\t// eth_clk_mode\n  },\n\n  // ABC! WLED Controller V43 + Ethernet Shield & compatible\n  {\n    1,                    // eth_address, \n    5,                    // eth_power, \n    23,                   // eth_mdc, \n    33,                   // eth_mdio, \n    ETH_PHY_LAN8720,      // eth_type,\n    ETH_CLOCK_GPIO17_OUT\t// eth_clk_mode\n  },\n\n  // Serg74-ESP32 Ethernet Shield\n  {\n    1,                    // eth_address,\n    5,                    // eth_power,\n    23,                   // eth_mdc,\n    18,                   // eth_mdio,\n    ETH_PHY_LAN8720,      // eth_type,\n    ETH_CLOCK_GPIO17_OUT  // eth_clk_mode\n  },\n\n  // ESP32-POE-WROVER\n  {\n    0,                    // eth_address,\n    12,                   // eth_power,\n    23,                   // eth_mdc,\n    18,                   // eth_mdio,\n    ETH_PHY_LAN8720,      // eth_type,\n    ETH_CLOCK_GPIO0_OUT   // eth_clk_mode\n  },\n  \n  // LILYGO T-POE Pro\n  // https://github.com/Xinyuan-LilyGO/LilyGO-T-ETH-Series/blob/master/schematic/T-POE-PRO.pdf\n  {\n    0,\t\t\t              // eth_address,\n    5,\t\t\t              // eth_power,\n    23,\t\t\t              // eth_mdc,\n    18,\t\t\t              // eth_mdio,\n    ETH_PHY_LAN8720,      // eth_type,\n    ETH_CLOCK_GPIO0_OUT\t// eth_clk_mode\n  },\n\n // Gledopto Series With Ethernet\n {\n    1,                    // eth_address, \n    5,                    // eth_power, \n    23,                   // eth_mdc, \n    33,                   // eth_mdio, \n    ETH_PHY_LAN8720,      // eth_type,\n    ETH_CLOCK_GPIO0_IN\t // eth_clk_mode\n  },\n};\n\nbool initEthernet()\n{\n  static bool successfullyConfiguredEthernet = false;\n\n  if (successfullyConfiguredEthernet) {\n    // DEBUG_PRINTLN(F(\"initE: ETH already successfully configured, ignoring\"));\n    return false;\n  }\n  if (ethernetType == WLED_ETH_NONE) {\n    return false;\n  }\n  if (ethernetType >= WLED_NUM_ETH_TYPES) {\n    DEBUG_PRINTF_P(PSTR(\"initE: Ignoring attempt for invalid ethernetType (%d)\\n\"), ethernetType);\n    return false;\n  }\n\n  DEBUG_PRINTF_P(PSTR(\"initE: Attempting ETH config: %d\\n\"), ethernetType);\n\n  // Ethernet initialization should only succeed once -- else reboot required\n  ethernet_settings es = ethernetBoards[ethernetType];\n  managed_pin_type pinsToAllocate[10] = {\n    // first six pins are non-configurable\n    esp32_nonconfigurable_ethernet_pins[0],\n    esp32_nonconfigurable_ethernet_pins[1],\n    esp32_nonconfigurable_ethernet_pins[2],\n    esp32_nonconfigurable_ethernet_pins[3],\n    esp32_nonconfigurable_ethernet_pins[4],\n    esp32_nonconfigurable_ethernet_pins[5],\n    { (int8_t)es.eth_mdc,   true },  // [6] = MDC  is output and mandatory\n    { (int8_t)es.eth_mdio,  true },  // [7] = MDIO is bidirectional and mandatory\n    { (int8_t)es.eth_power, true },  // [8] = optional pin, not all boards use\n    { ((int8_t)0xFE),       false }, // [9] = replaced with eth_clk_mode, mandatory\n  };\n  // update the clock pin....\n  if (es.eth_clk_mode == ETH_CLOCK_GPIO0_IN) {\n    pinsToAllocate[9].pin = 0;\n    pinsToAllocate[9].isOutput = false;\n  } else if (es.eth_clk_mode == ETH_CLOCK_GPIO0_OUT) {\n    pinsToAllocate[9].pin = 0;\n    pinsToAllocate[9].isOutput = true;\n  } else if (es.eth_clk_mode == ETH_CLOCK_GPIO16_OUT) {\n    pinsToAllocate[9].pin = 16;\n    pinsToAllocate[9].isOutput = true;\n  } else if (es.eth_clk_mode == ETH_CLOCK_GPIO17_OUT) {\n    pinsToAllocate[9].pin = 17;\n    pinsToAllocate[9].isOutput = true;\n  } else {\n    DEBUG_PRINTF_P(PSTR(\"initE: Failing due to invalid eth_clk_mode (%d)\\n\"), es.eth_clk_mode);\n    return false;\n  }\n\n  if (!PinManager::allocateMultiplePins(pinsToAllocate, 10, PinOwner::Ethernet)) {\n    DEBUG_PRINTLN(F(\"initE: Failed to allocate ethernet pins\"));\n    return false;\n  }\n\n  /*\n  For LAN8720 the most correct way is to perform clean reset each time before init\n  applying LOW to power or nRST pin for at least 100 us (please refer to datasheet, page 59)\n  ESP_IDF > V4 implements it (150 us, lan87xx_reset_hw(esp_eth_phy_t *phy) function in \n  /components/esp_eth/src/esp_eth_phy_lan87xx.c, line 280)\n  but ESP_IDF < V4 does not. Lets do it:\n  [not always needed, might be relevant in some EMI situations at startup and for hot resets]\n  */\n  #if ESP_IDF_VERSION_MAJOR==3\n  if(es.eth_power>0 && es.eth_type==ETH_PHY_LAN8720) {\n    pinMode(es.eth_power, OUTPUT);\n    digitalWrite(es.eth_power, 0);\n    delayMicroseconds(150);\n    digitalWrite(es.eth_power, 1);\n    delayMicroseconds(10);\n  }\n  #endif\n\n  if (!ETH.begin(\n                (uint8_t) es.eth_address,\n                (int)     es.eth_power,\n                (int)     es.eth_mdc,\n                (int)     es.eth_mdio,\n                (eth_phy_type_t)   es.eth_type,\n                (eth_clock_mode_t) es.eth_clk_mode\n                )) {\n    DEBUG_PRINTLN(F(\"initE: ETH.begin() failed\"));\n    // de-allocate the allocated pins\n    for (managed_pin_type mpt : pinsToAllocate) {\n      PinManager::deallocatePin(mpt.pin, PinOwner::Ethernet);\n    }\n    return false;\n  }\n\n  // https://github.com/wled/WLED/issues/5247\n  if (multiWiFi[0].staticIP != (uint32_t)0x00000000 && multiWiFi[0].staticGW != (uint32_t)0x00000000) {\n    ETH.config(multiWiFi[0].staticIP, multiWiFi[0].staticGW, multiWiFi[0].staticSN, dnsAddress);\n  } else {\n    ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE);\n  }\n\n  successfullyConfiguredEthernet = true;\n  DEBUG_PRINTLN(F(\"initE: *** Ethernet successfully configured! ***\"));\n  return true;\n}\n#endif\n\n\n//by https://github.com/tzapu/WiFiManager/blob/master/WiFiManager.cpp\nint getSignalQuality(int rssi)\n{\n    int quality = 0;\n\n    if (rssi <= -100)\n    {\n        quality = 0;\n    }\n    else if (rssi >= -50)\n    {\n        quality = 100;\n    }\n    else\n    {\n        quality = 2 * (rssi + 100);\n    }\n    return quality;\n}\n\n\nvoid fillMAC2Str(char *str, const uint8_t *mac) {\n  sprintf_P(str, PSTR(\"%02x%02x%02x%02x%02x%02x\"), MAC2STR(mac));\n  byte nul = 0;\n  for (int i = 0; i < 6; i++) nul |= *mac++;  // do we have 0\n  if (!nul) str[0] = '\\0';                    // empty string\n}\n\nvoid fillStr2MAC(uint8_t *mac, const char *str) {\n  for (int i = 0; i < 6; i++) *mac++ = 0;     // clear\n  if (!str) return;                           // null string\n  uint64_t MAC = strtoull(str, nullptr, 16);\n  for (int i = 0; i < 6; i++) { *--mac = MAC & 0xFF; MAC >>= 8; }\n}\n\n\n// performs asynchronous scan for available networks (which may take couple of seconds to finish)\n// returns configured WiFi ID with the strongest signal (or default if no configured networks available)\nint findWiFi(bool doScan) {\n  if (multiWiFi.size() <= 1) {\n    DEBUG_PRINTF_P(PSTR(\"WiFi: Default SSID (%s) used.\\n\"), multiWiFi[0].clientSSID);\n    return 0;\n  }\n\n  int status = WiFi.scanComplete(); // complete scan may take as much as several seconds (usually <6s with not very crowded air)\n\n  if (doScan || status == WIFI_SCAN_FAILED) {\n    DEBUG_PRINTF_P(PSTR(\"WiFi: Scan started. @ %lus\\n\"), millis()/1000);\n    WiFi.scanNetworks(true);  // start scanning in asynchronous mode (will delete old scan)\n  } else if (status >= 0) {   // status contains number of found networks (including duplicate SSIDs with different BSSID)\n    DEBUG_PRINTF_P(PSTR(\"WiFi: Found %d SSIDs. @ %lus\\n\"), status, millis()/1000);\n    int rssi = -9999;\n    int selected = selectedWiFi;\n    for (int o = 0; o < status; o++) {\n      DEBUG_PRINTF_P(PSTR(\" SSID: %s (BSSID: %s) RSSI: %ddB\\n\"), WiFi.SSID(o).c_str(), WiFi.BSSIDstr(o).c_str(), WiFi.RSSI(o));\n      for (unsigned n = 0; n < multiWiFi.size(); n++)\n        if (!strcmp(WiFi.SSID(o).c_str(), multiWiFi[n].clientSSID)) {\n          bool foundBSSID = memcmp(multiWiFi[n].bssid, WiFi.BSSID(o), 6) == 0;\n          // find the WiFi with the strongest signal (but keep priority of entry if signal difference is not big)\n          if (foundBSSID || (n < selected && WiFi.RSSI(o) > rssi-10) || WiFi.RSSI(o) > rssi) {\n            rssi = foundBSSID ? 0 : WiFi.RSSI(o); // RSSI is only ever negative\n            selected = n;\n          }\n          break;\n        }\n    }\n    DEBUG_PRINTF_P(PSTR(\"WiFi: Selected SSID: %s RSSI: %ddB\\n\"), multiWiFi[selected].clientSSID, rssi);\n    return selected;\n  }\n  //DEBUG_PRINT(F(\"WiFi scan running.\"));\n  return status; // scan is still running or there was an error\n}\n\n\nbool isWiFiConfigured() {\n  return multiWiFi.size() > 1 || (strlen(multiWiFi[0].clientSSID) >= 1 && strcmp_P(multiWiFi[0].clientSSID, PSTR(DEFAULT_CLIENT_SSID)) != 0);\n}\n\n#if defined(ESP8266)\n  #define ARDUINO_EVENT_WIFI_AP_STADISCONNECTED WIFI_EVENT_SOFTAPMODE_STADISCONNECTED\n  #define ARDUINO_EVENT_WIFI_AP_STACONNECTED    WIFI_EVENT_SOFTAPMODE_STACONNECTED\n  #define ARDUINO_EVENT_WIFI_STA_GOT_IP         WIFI_EVENT_STAMODE_GOT_IP\n  #define ARDUINO_EVENT_WIFI_STA_CONNECTED      WIFI_EVENT_STAMODE_CONNECTED\n  #define ARDUINO_EVENT_WIFI_STA_DISCONNECTED   WIFI_EVENT_STAMODE_DISCONNECTED\n#elif defined(ARDUINO_ARCH_ESP32) && !defined(ESP_ARDUINO_VERSION_MAJOR) //ESP_IDF_VERSION_MAJOR==3\n  // not strictly IDF v3 but Arduino core related\n  #define ARDUINO_EVENT_WIFI_AP_STADISCONNECTED SYSTEM_EVENT_AP_STADISCONNECTED\n  #define ARDUINO_EVENT_WIFI_AP_STACONNECTED    SYSTEM_EVENT_AP_STACONNECTED\n  #define ARDUINO_EVENT_WIFI_STA_GOT_IP         SYSTEM_EVENT_STA_GOT_IP\n  #define ARDUINO_EVENT_WIFI_STA_CONNECTED      SYSTEM_EVENT_STA_CONNECTED\n  #define ARDUINO_EVENT_WIFI_STA_DISCONNECTED   SYSTEM_EVENT_STA_DISCONNECTED\n  #define ARDUINO_EVENT_WIFI_AP_START           SYSTEM_EVENT_AP_START\n  #define ARDUINO_EVENT_WIFI_AP_STOP            SYSTEM_EVENT_AP_STOP\n  #define ARDUINO_EVENT_WIFI_SCAN_DONE          SYSTEM_EVENT_SCAN_DONE\n  #define ARDUINO_EVENT_ETH_START               SYSTEM_EVENT_ETH_START\n  #define ARDUINO_EVENT_ETH_CONNECTED           SYSTEM_EVENT_ETH_CONNECTED\n  #define ARDUINO_EVENT_ETH_DISCONNECTED        SYSTEM_EVENT_ETH_DISCONNECTED\n#endif\n\n//handle Ethernet connection event\nvoid WiFiEvent(WiFiEvent_t event)\n{\n  switch (event) {\n    case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED:\n      // AP client disconnected\n      if (--apClients == 0 && isWiFiConfigured()) forceReconnect = true; // no clients reconnect WiFi if awailable\n      DEBUG_PRINTF_P(PSTR(\"WiFi-E: AP Client Disconnected (%d) @ %lus.\\n\"), (int)apClients, millis()/1000);\n      break;\n    case ARDUINO_EVENT_WIFI_AP_STACONNECTED:\n      // AP client connected\n      apClients++;\n      DEBUG_PRINTF_P(PSTR(\"WiFi-E: AP Client Connected (%d) @ %lus.\\n\"), (int)apClients, millis()/1000);\n      break;\n    case ARDUINO_EVENT_WIFI_STA_GOT_IP:\n      DEBUG_PRINT(F(\"WiFi-E: IP address: \")); DEBUG_PRINTLN(Network.localIP());\n      break;\n    case ARDUINO_EVENT_WIFI_STA_CONNECTED:\n      // followed by IDLE and SCAN_DONE\n      DEBUG_PRINTF_P(PSTR(\"WiFi-E: Connected! @ %lus\\n\"), millis()/1000);\n      wasConnected = true;\n      break;\n    case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:\n      if (wasConnected && interfacesInited) {\n        DEBUG_PRINTF_P(PSTR(\"WiFi-E: Disconnected! @ %lus\\n\"), millis()/1000);\n        if (interfacesInited && multiWiFi.size() > 1 && WiFi.scanComplete() >= 0) {\n          findWiFi(true); // reinit WiFi scan\n          forceReconnect = true;\n        }\n        interfacesInited = false;\n      }\n      break;\n  #ifdef ARDUINO_ARCH_ESP32\n    case ARDUINO_EVENT_WIFI_SCAN_DONE:\n      // also triggered when connected to selected SSID\n      DEBUG_PRINTLN(F(\"WiFi-E: SSID scan completed.\"));\n      break;\n    case ARDUINO_EVENT_WIFI_AP_START:\n      DEBUG_PRINTLN(F(\"WiFi-E: AP Started\"));\n      break;\n    case ARDUINO_EVENT_WIFI_AP_STOP:\n      DEBUG_PRINTLN(F(\"WiFi-E: AP Stopped\"));\n      break;\n    #if defined(WLED_USE_ETHERNET)\n    case ARDUINO_EVENT_ETH_START:\n      DEBUG_PRINTLN(F(\"ETH-E: Started\"));\n      break;\n    case ARDUINO_EVENT_ETH_CONNECTED:\n      {\n      DEBUG_PRINTLN(F(\"ETH-E: Connected\"));\n      if (!apActive) {\n        WiFi.disconnect(true); // disable WiFi entirely\n      }\n      // convert the \"serverDescription\" into a valid DNS hostname (alphanumeric)\n      char hostname[64];\n      prepareHostname(hostname);\n      ETH.setHostname(hostname);\n      showWelcomePage = false;\n      break;\n      }\n    case ARDUINO_EVENT_ETH_DISCONNECTED:\n      DEBUG_PRINTLN(F(\"ETH-E: Disconnected\"));\n      // This doesn't really affect ethernet per se,\n      // as it's only configured once.  Rather, it\n      // may be necessary to reconnect the WiFi when\n      // ethernet disconnects, as a way to provide\n      // alternative access to the device.\n      if (interfacesInited && WiFi.scanComplete() >= 0) findWiFi(true); // reinit WiFi scan\n      forceReconnect = true;\n      break;\n    #endif\n  #endif\n    default:\n      DEBUG_PRINTF_P(PSTR(\"WiFi-E: Event %d\\n\"), (int)event);\n      break;\n  }\n}\n\n"
  },
  {
    "path": "wled00/ntp.cpp",
    "content": "#include \"src/dependencies/timezone/Timezone.h\"\n#include \"wled.h\"\n#include \"fcn_declare.h\"\n\n// forward declarations\nstatic void sendNTPPacket();\nstatic bool checkNTPResponse();\n\n\n// WARNING: may cause errors in sunset calculations on ESP8266, see #3400\n// building with `-D WLED_USE_REAL_MATH` will prevent those errors at the expense of flash and RAM\n\n/*\n * Acquires time from NTP server\n */\n//#define WLED_DEBUG_NTP\n#define NTP_SYNC_INTERVAL 42000UL //Get fresh NTP time about twice per day\n\nstatic Timezone* tz;\n\n#define TZ_UTC                  0\n#define TZ_UK                   1\n#define TZ_EUROPE_CENTRAL       2\n#define TZ_EUROPE_EASTERN       3\n#define TZ_US_EASTERN           4\n#define TZ_US_CENTRAL           5\n#define TZ_US_MOUNTAIN          6\n#define TZ_US_ARIZONA           7\n#define TZ_US_PACIFIC           8\n#define TZ_CHINA                9\n#define TZ_JAPAN               10\n#define TZ_AUSTRALIA_EASTERN   11\n#define TZ_NEW_ZEALAND         12\n#define TZ_NORTH_KOREA         13\n#define TZ_INDIA               14\n#define TZ_SASKACHEWAN         15\n#define TZ_AUSTRALIA_NORTHERN  16\n#define TZ_AUSTRALIA_SOUTHERN  17\n#define TZ_HAWAII              18\n#define TZ_NOVOSIBIRSK         19\n#define TZ_ANCHORAGE           20\n#define TZ_MX_CENTRAL          21\n#define TZ_PAKISTAN            22\n#define TZ_BRASILIA            23\n#define TZ_AUSTRALIA_WESTERN   24\n\n#define TZ_COUNT               25\n#define TZ_INIT               255\n\nstatic byte tzCurrent = TZ_INIT; //uninitialized\n\n/* C++11 form -- static std::array<std::pair<TimeChangeRule, TimeChangeRule>, TZ_COUNT> TZ_TABLE PROGMEM = {{ */\nstatic const std::pair<TimeChangeRule, TimeChangeRule> TZ_TABLE[] PROGMEM = {\n    /* TZ_UTC */ {\n      {Last, Sun, Mar, 1, 0}, // UTC\n      {Last, Sun, Mar, 1, 0}  // Same\n    },\n    /* TZ_UK */ {\n      {Last, Sun, Mar, 1, 60},      //British Summer Time\n      {Last, Sun, Oct, 2, 0}       //Standard Time\n    },\n    /* TZ_EUROPE_CENTRAL */ {\n      {Last, Sun, Mar, 2, 120},     //Central European Summer Time\n      {Last, Sun, Oct, 3, 60}      //Central European Standard Time\n    },\n    /* TZ_EUROPE_EASTERN */ {\n      {Last, Sun, Mar, 3, 180},     //East European Summer Time\n      {Last, Sun, Oct, 4, 120}     //East European Standard Time\n    },\n    /* TZ_US_EASTERN */ {\n      {Second, Sun, Mar, 2, -240},  //EDT = UTC - 4 hours\n      {First,  Sun, Nov, 2, -300}  //EST = UTC - 5 hours\n    },\n    /* TZ_US_CENTRAL */ {\n      {Second, Sun, Mar, 2, -300},  //CDT = UTC - 5 hours\n      {First,  Sun, Nov, 2, -360}  //CST = UTC - 6 hours\n    },\n    /* TZ_US_MOUNTAIN */ {\n      {Second, Sun, Mar, 2, -360},  //MDT = UTC - 6 hours\n      {First,  Sun, Nov, 2, -420}  //MST = UTC - 7 hours\n    },\n    /* TZ_US_ARIZONA */ {\n      {First,  Sun, Nov, 2, -420},  //MST = UTC - 7 hours\n      {First,  Sun, Nov, 2, -420}  //MST = UTC - 7 hours\n    },\n    /* TZ_US_PACIFIC */ {\n      {Second, Sun, Mar, 2, -420},  //PDT = UTC - 7 hours\n      {First,  Sun, Nov, 2, -480}  //PST = UTC - 8 hours\n    },\n    /* TZ_CHINA */ {\n      {Last, Sun, Mar, 1, 480},     //CST = UTC + 8 hours\n      {Last, Sun, Mar, 1, 480}\n    },\n    /* TZ_JAPAN */ {\n      {Last, Sun, Mar, 1, 540},     //JST = UTC + 9 hours\n      {Last, Sun, Mar, 1, 540}\n    },\n    /* TZ_AUSTRALIA_EASTERN */ {\n      {First,  Sun, Oct, 2, 660},   //AEDT = UTC + 11 hours\n      {First,  Sun, Apr, 3, 600}   //AEST = UTC + 10 hours\n    },\n    /* TZ_NEW_ZEALAND */ {\n      {Last,   Sun, Sep, 2, 780},   //NZDT = UTC + 13 hours\n      {First,  Sun, Apr, 3, 720}   //NZST = UTC + 12 hours\n    },\n    /* TZ_NORTH_KOREA */ {\n      {Last, Sun, Mar, 1, 510},     //Pyongyang Time = UTC + 8.5 hours\n      {Last, Sun, Mar, 1, 510}\n    },\n    /* TZ_INDIA */ {\n      {Last, Sun, Mar, 1, 330},     //India Standard Time = UTC + 5.5 hours\n      {Last, Sun, Mar, 1, 330}\n    },\n    /* TZ_SASKACHEWAN */ {\n      {First,  Sun, Nov, 2, -360},  //CST = UTC - 6 hours\n      {First,  Sun, Nov, 2, -360}\n    },\n    /* TZ_AUSTRALIA_NORTHERN */ {\n      {First, Sun, Apr, 3, 570},   //ACST = UTC + 9.5 hours\n      {First, Sun, Apr, 3, 570}\n    },\n    /* TZ_AUSTRALIA_SOUTHERN */ {\n      {First, Sun, Oct, 2, 630},   //ACDT = UTC + 10.5 hours\n      {First, Sun, Apr, 3, 570}   //ACST = UTC + 9.5 hours\n    },\n    /* TZ_HAWAII */ {\n      {Last, Sun, Mar, 1, -600},   //HST =  UTC - 10 hours\n      {Last, Sun, Mar, 1, -600}\n    },\n    /* TZ_NOVOSIBIRSK */ {\n      {Last, Sun, Mar, 1, 420},     //CST = UTC + 7 hours\n      {Last, Sun, Mar, 1, 420}\n    },\n    /* TZ_ANCHORAGE */ {\n      {Second, Sun, Mar, 2, -480},  //AKDT = UTC - 8 hours\n      {First, Sun, Nov, 2, -540}   //AKST = UTC - 9 hours\n    },\n     /* TZ_MX_CENTRAL */ {\n      {First, Sun, Apr, 2, -360},  //CST = UTC - 6 hours\n      {First, Sun, Apr, 2, -360}\n    },\n    /* TZ_PAKISTAN */ {\n      {Last, Sun, Mar, 1, 300},     //Pakistan Standard Time = UTC + 5 hours\n      {Last, Sun, Mar, 1, 300}\n    },\n    /* TZ_BRASILIA */ {\n      {Last, Sun, Mar, 1, -180},    //Brasília Standard Time = UTC - 3 hours\n      {Last, Sun, Mar, 1, -180}\n    },\n    /* TZ_AUSTRALIA_WESTERN */ {\n      {Last, Sun, Mar, 1, 480},     //AWST = UTC + 8 hours\n      {Last, Sun, Mar, 1, 480}      //AWST = UTC + 8 hours (no DST)\n    }\n};\n\nvoid updateTimezone() {\n  delete tz;\n  TimeChangeRule tcrDaylight, tcrStandard;\n  auto tz_table_entry = currentTimezone;\n  if (tz_table_entry >= TZ_COUNT) {\n    tz_table_entry = 0;\n  }\n  tzCurrent = currentTimezone;\n  memcpy_P(&tcrDaylight, &TZ_TABLE[tz_table_entry].first, sizeof(tcrDaylight));\n  memcpy_P(&tcrStandard, &TZ_TABLE[tz_table_entry].second, sizeof(tcrStandard));\n\n  tz = new Timezone(tcrDaylight, tcrStandard);\n}\n\nvoid handleTime() {\n  handleNetworkTime();\n\n  toki.millisecond();\n  toki.setTick();\n\n  if (toki.isTick()) //true only in the first loop after a new second started\n  {\n    #ifdef WLED_DEBUG_NTP\n    Serial.print(F(\"TICK! \"));\n    toki.printTime(toki.getTime());\n    #endif\n    updateLocalTime();\n    checkTimers();\n    checkCountdown();\n  }\n}\n\nvoid handleNetworkTime()\n{\n  if (ntpEnabled && ntpConnected && millis() - ntpLastSyncTime > (1000*NTP_SYNC_INTERVAL) && WLED_CONNECTED)\n  {\n    if (millis() - ntpPacketSentTime > 10000)\n    {\n      #ifdef ARDUINO_ARCH_ESP32   // I had problems using udp.flush() on 8266\n      while (ntpUdp.parsePacket() > 0) ntpUdp.flush(); // flush any existing packets\n      #endif\n      sendNTPPacket();\n      ntpPacketSentTime = millis();\n    }\n    if (checkNTPResponse())\n    {\n      ntpLastSyncTime = millis();\n    }\n  }\n}\n\nstatic void sendNTPPacket()\n{\n  if (!ntpServerIP.fromString(ntpServerName)) //see if server is IP or domain\n  {\n    #ifdef ESP8266\n    WiFi.hostByName(ntpServerName, ntpServerIP, 750);\n    #else\n    WiFi.hostByName(ntpServerName, ntpServerIP);\n    #endif\n  }\n\n  DEBUG_PRINTLN(F(\"send NTP\"));\n  byte pbuf[NTP_PACKET_SIZE];\n  memset(pbuf, 0, NTP_PACKET_SIZE);\n\n  pbuf[0] = 0b11100011;   // LI, Version, Mode\n  pbuf[1] = 0;     // Stratum, or type of clock\n  pbuf[2] = 6;     // Polling Interval\n  pbuf[3] = 0xEC;  // Peer Clock Precision\n  // 8 bytes of zero for Root Delay & Root Dispersion\n  pbuf[12]  = 49;\n  pbuf[13]  = 0x4E;\n  pbuf[14]  = 49;\n  pbuf[15]  = 52;\n\n  ntpUdp.beginPacket(ntpServerIP, 123); //NTP requests are to port 123\n  ntpUdp.write(pbuf, NTP_PACKET_SIZE);\n  ntpUdp.endPacket();\n}\n\nstatic bool isValidNtpResponse(const byte* ntpPacket) {\n  // Perform a few validity checks on the packet\n  //   based on https://github.com/taranais/NTPClient/blob/master/NTPClient.cpp\n  if((ntpPacket[0] & 0b11000000) == 0b11000000) return false; //reject LI=UNSYNC\n  // if((ntpPacket[0] & 0b00111000) >> 3 < 0b100) return false; //reject Version < 4\n  if((ntpPacket[0] & 0b00000111) != 0b100)      return false; //reject Mode != Server\n  if((ntpPacket[1] < 1) || (ntpPacket[1] > 15)) return false; //reject invalid Stratum\n  if( ntpPacket[16] == 0 && ntpPacket[17] == 0 && \n      ntpPacket[18] == 0 && ntpPacket[19] == 0 &&\n      ntpPacket[20] == 0 && ntpPacket[21] == 0 &&\n      ntpPacket[22] == 0 && ntpPacket[23] == 0)               //reject ReferenceTimestamp == 0\n    return false;\n\n  return true;\n}\n\nstatic bool checkNTPResponse()\n{\n  int cb = ntpUdp.parsePacket();\n  if (cb < NTP_MIN_PACKET_SIZE) {\n    #ifdef ARDUINO_ARCH_ESP32   // I had problems using udp.flush() on 8266\n    if (cb > 0) ntpUdp.flush();  // this avoids memory leaks on esp32\n    #endif\n    return false;\n  }\n\n  uint32_t ntpPacketReceivedTime = millis();\n  DEBUG_PRINTF_P(PSTR(\"NTP recv, l=%d\\n\"), cb);\n  byte pbuf[NTP_PACKET_SIZE];\n  ntpUdp.read(pbuf, NTP_PACKET_SIZE); // read the packet into the buffer\n  if (!isValidNtpResponse(pbuf)) return false;  // verify we have a valid response to client\n\n  Toki::Time arrived  = toki.fromNTP(pbuf + 32);\n  Toki::Time departed = toki.fromNTP(pbuf + 40);\n  if (departed.sec == 0) return false;\n  //basic half roundtrip estimation\n  uint32_t serverDelay = toki.msDifference(arrived, departed);\n  uint32_t offset = (ntpPacketReceivedTime - ntpPacketSentTime - serverDelay) >> 1;\n  #ifdef WLED_DEBUG_NTP\n  //the time the packet departed the NTP server\n  toki.printTime(departed);\n  #endif\n\n  toki.adjust(departed, offset);\n  toki.setTime(departed, TOKI_TS_NTP);\n\n  #ifdef WLED_DEBUG_NTP\n  Serial.print(\"Arrived: \");\n  toki.printTime(arrived);\n  Serial.print(\"Time: \");\n  toki.printTime(departed);\n  Serial.print(\"Roundtrip: \");\n  Serial.println(ntpPacketReceivedTime - ntpPacketSentTime);\n  Serial.print(\"Offset: \");\n  Serial.println(offset);\n  Serial.print(\"Serverdelay: \");\n  Serial.println(serverDelay);\n  #endif\n\n  if (countdownTime - toki.second() > 0) countdownOverTriggered = false;\n  // if time changed re-calculate sunrise/sunset\n  updateLocalTime();\n  calculateSunriseAndSunset();\n  return true;\n}\n\nvoid updateLocalTime()\n{\n  if (currentTimezone != tzCurrent) updateTimezone();\n  unsigned long tmc = toki.second()+ utcOffsetSecs;\n  localTime = tz->toLocal(tmc);\n}\n\nvoid getTimeString(char* out)\n{\n  updateLocalTime();\n  byte hr = hour(localTime);\n  if (useAMPM)\n  {\n    if (hr > 11) hr -= 12;\n    if (hr == 0) hr  = 12;\n  }\n  sprintf_P(out,PSTR(\"%i-%i-%i, %02d:%02d:%02d\"),year(localTime), month(localTime), day(localTime), hr, minute(localTime), second(localTime));\n  if (useAMPM)\n  {\n    strcat_P(out,PSTR(\" \"));\n    strcat(out,(hour(localTime) > 11)? \"PM\":\"AM\");\n  }\n}\n\nvoid setCountdown()\n{\n  if (currentTimezone != tzCurrent) updateTimezone();\n  countdownTime = tz->toUTC(getUnixTime(countdownHour, countdownMin, countdownSec, countdownDay, countdownMonth, countdownYear));\n  if (countdownTime - toki.second() > 0) countdownOverTriggered = false;\n}\n\n//returns true if countdown just over\nbool checkCountdown()\n{\n  unsigned long n = toki.second();\n  if (countdownMode) localTime = countdownTime - n + utcOffsetSecs;\n  if (n > countdownTime) {\n    if (countdownMode) localTime = n - countdownTime + utcOffsetSecs;\n    if (!countdownOverTriggered)\n    {\n      if (macroCountdown != 0) applyPreset(macroCountdown);\n      countdownOverTriggered = true;\n      return true;\n    }\n  }\n  return false;\n}\n\nbyte weekdayMondayFirst()\n{\n  byte wd = weekday(localTime) -1;\n  if (wd == 0) wd = 7;\n  return wd;\n}\n\nbool isTodayInDateRange(byte monthStart, byte dayStart, byte monthEnd, byte dayEnd)\n{\n\tif (monthStart == 0 || dayStart == 0) return true;\n\tif (monthEnd == 0) monthEnd = monthStart;\n\tif (dayEnd == 0) dayEnd = 31;\n\tbyte d = day(localTime);\n\tbyte m = month(localTime);\n\n\tif (monthStart < monthEnd) {\n\t\tif (m > monthStart && m < monthEnd) return true;\n\t\tif (m == monthStart) return (d >= dayStart);\n\t\tif (m == monthEnd) return (d <= dayEnd);\n\t\treturn false;\n\t}\n\tif (monthEnd < monthStart) { //range spans change of year\n\t\tif (m > monthStart || m < monthEnd) return true;\n\t\tif (m == monthStart) return (d >= dayStart);\n\t\tif (m == monthEnd) return (d <= dayEnd);\n\t\treturn false;\n\t}\n\n\t//start month and end month are the same\n\tif (dayEnd < dayStart) return (m != monthStart || (d <= dayEnd || d >= dayStart)); //all year, except the designated days in this month\n\treturn (m == monthStart && d >= dayStart && d <= dayEnd); //just the designated days this month\n}\n\nvoid checkTimers()\n{\n  if (lastTimerMinute != minute(localTime)) //only check once a new minute begins\n  {\n    lastTimerMinute = minute(localTime);\n\n    // re-calculate sunrise and sunset just after midnight\n    if (!hour(localTime) && minute(localTime)==1) calculateSunriseAndSunset();\n\n    DEBUG_PRINTF_P(PSTR(\"Local time: %02d:%02d\\n\"), hour(localTime), minute(localTime));\n    for (unsigned i = 0; i < 8; i++)\n    {\n      if (timerMacro[i] != 0\n          && (timerWeekday[i] & 0x01) //timer is enabled\n          && (timerHours[i] == hour(localTime) || timerHours[i] == 24) //if hour is set to 24, activate every hour\n          && timerMinutes[i] == minute(localTime)\n          && ((timerWeekday[i] >> weekdayMondayFirst()) & 0x01) //timer should activate at current day of week\n          && isTodayInDateRange(((timerMonth[i] >> 4) & 0x0F), timerDay[i], timerMonth[i] & 0x0F, timerDayEnd[i])\n         )\n      {\n        applyPreset(timerMacro[i]);\n      }\n    }\n    // sunrise macro\n    if (sunrise) {\n      time_t tmp = sunrise + timerMinutes[8]*60;  // NOTE: may not be ok\n      DEBUG_PRINTF_P(PSTR(\"Trigger time: %02d:%02d\\n\"), hour(tmp), minute(tmp));\n      if (timerMacro[8] != 0\n          && hour(tmp) == hour(localTime)\n          && minute(tmp) == minute(localTime)\n          && (timerWeekday[8] & 0x01) //timer is enabled\n          && ((timerWeekday[8] >> weekdayMondayFirst()) & 0x01)) //timer should activate at current day of week\n      {\n        applyPreset(timerMacro[8]);\n        DEBUG_PRINTF_P(PSTR(\"Sunrise macro %d triggered.\"),timerMacro[8]);\n      }\n    }\n    // sunset macro\n    if (sunset) {\n      time_t tmp = sunset + timerMinutes[9]*60;  // NOTE: may not be ok\n      DEBUG_PRINTF_P(PSTR(\"Trigger time: %02d:%02d\\n\"), hour(tmp), minute(tmp));\n      if (timerMacro[9] != 0\n          && hour(tmp) == hour(localTime)\n          && minute(tmp) == minute(localTime)\n          && (timerWeekday[9] & 0x01) //timer is enabled\n          && ((timerWeekday[9] >> weekdayMondayFirst()) & 0x01)) //timer should activate at current day of week\n      {\n        applyPreset(timerMacro[9]);\n        DEBUG_PRINTF_P(PSTR(\"Sunset macro %d triggered.\"),timerMacro[9]);\n      }\n    }\n  }\n}\n\n#define ZENITH -0.83\n// get sunrise (or sunset) time (in minutes) for a given day at a given geo location. Returns >= INT16_MAX in case of \"no sunset\"\nstatic int getSunriseUTC(int year, int month, int day, float lat, float lon, bool sunset=false) {\n  //1. first calculate the day of the year\n  float N1 = 275 * month / 9;\n  float N2 = (month + 9) / 12;\n  float N3 = (1.0f + floor_t((year - 4 * floor_t(year / 4) + 2.0f) / 3.0f));\n  float N = N1 - (N2 * N3) + day - 30.0f;\n\n  //2. convert the longitude to hour value and calculate an approximate time\n  float lngHour = lon / 15.0f;\n  float t = N + (((sunset ? 18 : 6) - lngHour) / 24);\n\n  //3. calculate the Sun's mean anomaly\n  float M = (0.9856f * t) - 3.289f;\n\n  //4. calculate the Sun's true longitude\n  float L = fmod_t(M + (1.916f * sin_t(DEG_TO_RAD*M)) + (0.02f * sin_t(2*DEG_TO_RAD*M)) + 282.634f, 360.0f);\n\n  //5a. calculate the Sun's right ascension\n  float RA = fmod_t(RAD_TO_DEG*atan_t(0.91764f * tan_t(DEG_TO_RAD*L)), 360.0f);\n\n  //5b. right ascension value needs to be in the same quadrant as L\n  float Lquadrant  = floor_t( L/90) * 90;\n  float RAquadrant = floor_t(RA/90) * 90;\n  RA = RA + (Lquadrant - RAquadrant);\n\n  //5c. right ascension value needs to be converted into hours\n  RA /= 15.0f;\n\n  //6. calculate the Sun's declination\n  float sinDec = 0.39782f * sin_t(DEG_TO_RAD*L);\n  float cosDec = cos_t(asin_t(sinDec));\n\n  //7a. calculate the Sun's local hour angle\n  float cosH = (sin_t(DEG_TO_RAD*ZENITH) - (sinDec * sin_t(DEG_TO_RAD*lat))) / (cosDec * cos_t(DEG_TO_RAD*lat));\n  if ((cosH > 1.0f) && !sunset) return INT16_MAX;  // the sun never rises on this location (on the specified date)\n  if ((cosH < -1.0f) && sunset) return INT16_MAX;  // the sun never sets on this location (on the specified date)\n\n  //7b. finish calculating H and convert into hours\n  float H = sunset ? RAD_TO_DEG*acos_t(cosH) : 360 - RAD_TO_DEG*acos_t(cosH);\n  H /= 15.0f;\n\n  //8. calculate local mean time of rising/setting\n  float T = H + RA - (0.06571f * t) - 6.622f;\n\n  //9. adjust back to UTC\n  float UT = fmod_t(T - lngHour, 24.0f);\n\n  // return in minutes from midnight\n\treturn UT*60;\n}\n\n#define SUNSET_MAX (24*60) // 1day = max expected absolute value for sun offset in minutes \n// calculate sunrise and sunset (if longitude and latitude are set)\nvoid calculateSunriseAndSunset() {\n  if ((int)(longitude*10.) || (int)(latitude*10.)) {\n    struct tm tim_0;\n    tim_0.tm_year = year(localTime)-1900;\n    tim_0.tm_mon = month(localTime)-1;\n    tim_0.tm_mday = day(localTime);\n    tim_0.tm_sec = 0;\n    tim_0.tm_isdst = 0;\n\n    // Due to limited accuracy, its possible to get a bad sunrise/sunset displayed as \"00:00\" (see issue #3601)\n    // So in case of invalid result, we try to use the sunset/sunrise of previous day. Max 3 days back, this worked well in all cases I tried.\n    // When latitude = 66,6 (N or S), the functions sometimes returns 2147483647, so this \"unexpected large\" is another condition for retry\n    int minUTC = 0;\n    int retryCount = 0;\n    do {\n      time_t theDay = localTime - retryCount * 86400; // one day back = 86400 seconds\n      minUTC = getSunriseUTC(year(theDay), month(theDay), day(theDay), latitude, longitude, false);\n      DEBUG_PRINTF_P(PSTR(\"* sunrise (minutes from UTC) = %d\\n\"), minUTC);\n      retryCount ++;\n    } while ((abs(minUTC) > SUNSET_MAX)  && (retryCount <= 3));\n\n    if (abs(minUTC) <= SUNSET_MAX) {\n      // there is a sunrise\n      if (minUTC < 0) minUTC += 24*60; // add a day if negative\n      tim_0.tm_hour = minUTC / 60;\n      tim_0.tm_min = minUTC % 60;\n      sunrise = tz->toLocal(mktime(&tim_0) + utcOffsetSecs);\n      DEBUG_PRINTF_P(PSTR(\"Sunrise: %02d:%02d\\n\"), hour(sunrise), minute(sunrise));\n    } else {\n      sunrise = 0;\n    }\n\n    retryCount = 0;\n    do {\n      time_t theDay = localTime - retryCount * 86400; // one day back = 86400 seconds\n      minUTC = getSunriseUTC(year(theDay), month(theDay), day(theDay), latitude, longitude, true);\n      DEBUG_PRINTF_P(PSTR(\"* sunset  (minutes from UTC) = %d\\n\"), minUTC);\n      retryCount ++;\n    } while ((abs(minUTC) > SUNSET_MAX)  && (retryCount <= 3));\n\n    if (abs(minUTC) <= SUNSET_MAX) {\n      // there is a sunset\n      if (minUTC < 0) minUTC += 24*60; // add a day if negative\n      tim_0.tm_hour = minUTC / 60;\n      tim_0.tm_min = minUTC % 60;\n      sunset = tz->toLocal(mktime(&tim_0) + utcOffsetSecs);\n      DEBUG_PRINTF_P(PSTR(\"Sunset: %02d:%02d\\n\"), hour(sunset), minute(sunset));\n    } else {\n      sunset = 0;\n    }\n  }\n}\n\n//time from JSON and HTTP API\nvoid setTimeFromAPI(uint32_t timein) {\n  if (timein == 0 || timein == UINT32_MAX) return;\n  uint32_t prev = toki.second();\n  //only apply if more accurate or there is a significant difference to the \"more accurate\" time source\n  uint32_t diff = (timein > prev) ? timein - prev : prev - timein;\n  if (toki.getTimeSource() > TOKI_TS_JSON && diff < 60U) return;\n\n  toki.setTime(timein, TOKI_NO_MS_ACCURACY, TOKI_TS_JSON);\n  if (diff >= 60U) {\n    updateLocalTime();\n    calculateSunriseAndSunset();\n  }\n  if (presetsModifiedTime == 0) presetsModifiedTime = timein;\n}"
  },
  {
    "path": "wled00/ota_update.cpp",
    "content": "#include \"ota_update.h\"\n#include \"wled.h\"\n\n#ifdef ESP32\n#include <esp_app_format.h>\n#include <esp_ota_ops.h>\n#include <esp_flash.h>\n#include <mbedtls/sha256.h>\n#endif\n\n// Platform-specific metadata locations\n#ifdef ESP32\nconstexpr size_t METADATA_OFFSET = 256;          // ESP32: metadata appears after Espressif metadata\n#define UPDATE_ERROR errorString\n\n// Bootloader is at fixed offset 0x1000 (4KB), 0x0000 (0KB), or 0x2000 (8KB), and is typically 32KB\n// Bootloader offsets for different MCUs => see https://github.com/wled/WLED/issues/5064\n#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6)\nconstexpr size_t BOOTLOADER_OFFSET = 0x0000; // esp32-S3, esp32-C3 and (future support) esp32-c6\nconstexpr size_t BOOTLOADER_SIZE   = 0x8000; // 32KB, typical bootloader size\n#define BOOTLOADER_OTA_UNSUPPORTED    // still needs validation on these platforms.\n#elif defined(CONFIG_IDF_TARGET_ESP32P4) || defined(CONFIG_IDF_TARGET_ESP32C5)\nconstexpr size_t BOOTLOADER_OFFSET = 0x2000; // (future support) esp32-P4 and esp32-C5\nconstexpr size_t BOOTLOADER_SIZE   = 0x8000; // 32KB, typical bootloader size\n#define BOOTLOADER_OTA_UNSUPPORTED    // still needs testing on these platforms\n#else\nconstexpr size_t BOOTLOADER_OFFSET = 0x1000; // esp32 and esp32-s2\nconstexpr size_t BOOTLOADER_SIZE   = 0x8000; // 32KB, typical bootloader size\n#endif\n\n#elif defined(ESP8266)\nconstexpr size_t METADATA_OFFSET = 0x1000;     // ESP8266: metadata appears at 4KB offset\n#define UPDATE_ERROR getErrorString\n#endif\n\nconstexpr size_t METADATA_SEARCH_RANGE = 512;  // bytes\n\n\n/**\n * Check if OTA should be allowed based on release compatibility using custom description\n * @param binaryData Pointer to binary file data (not modified)\n * @param dataSize Size of binary data in bytes\n * @param errorMessage Buffer to store error message if validation fails \n * @param errorMessageLen Maximum length of error message buffer\n * @return true if OTA should proceed, false if it should be blocked\n */\n\nstatic bool validateOTA(const uint8_t* binaryData, size_t dataSize, char* errorMessage, size_t errorMessageLen) {\n  // Clear error message\n  if (errorMessage && errorMessageLen > 0) {\n    errorMessage[0] = '\\0';\n  }\n\n  // Try to extract WLED structure directly from binary data\n  wled_metadata_t extractedDesc;\n  bool hasDesc = findWledMetadata(binaryData, dataSize, &extractedDesc);\n\n  if (hasDesc) {\n    return shouldAllowOTA(extractedDesc, errorMessage, errorMessageLen);\n  } else {\n    // No custom description - this could be a legacy binary\n    if (errorMessage && errorMessageLen > 0) {\n      strncpy_P(errorMessage, PSTR(\"This firmware file is missing compatibility metadata.\"), errorMessageLen - 1);\n      errorMessage[errorMessageLen - 1] = '\\0';\n    }\n    return false;\n  }\n}\n\nstruct UpdateContext {\n  // State flags\n  // FUTURE: the flags could be replaced by a state machine\n  bool replySent = false;\n  bool needsRestart = false;\n  bool updateStarted = false;\n  bool uploadComplete = false;\n  bool releaseCheckPassed = false;\n  String errorMessage;\n\n  // Buffer to hold block data across posts, if needed\n  std::vector<uint8_t> releaseMetadataBuffer;  \n};\n\n\nstatic void endOTA(AsyncWebServerRequest *request) {\n  UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);\n  request->_tempObject = nullptr;\n\n  DEBUG_PRINTF_P(PSTR(\"EndOTA %x --> %x (%d)\\n\"), (uintptr_t)request,(uintptr_t) context, context ? context->uploadComplete : 0);\n  if (context) {\n    if (context->updateStarted) {  // We initialized the update\n      // We use Update.end() because not all forms of Update() support an abort.\n      // If the upload is incomplete, Update.end(false) should error out.\n      if (Update.end(context->uploadComplete)) {\n        // Update successful!\n        #ifndef ESP8266\n        bootloopCheckOTA(); // let the bootloop-checker know there was an OTA update\n        #endif\n        doReboot = true;\n        context->needsRestart = false;\n      }\n    }\n\n    if (context->needsRestart) {\n      strip.resume();\n      UsermodManager::onUpdateBegin(false);\n      #if WLED_WATCHDOG_TIMEOUT > 0\n      WLED::instance().enableWatchdog();\n      #endif\n    }\n    delete context;\n  }\n};\n\nstatic bool beginOTA(AsyncWebServerRequest *request, UpdateContext* context)\n{\n  #ifdef ESP8266\n  Update.runAsync(true);\n  #endif  \n\n  if (Update.isRunning()) {\n      request->send(503);\n      setOTAReplied(request);\n      return false;\n  }\n\n  #if WLED_WATCHDOG_TIMEOUT > 0\n  WLED::instance().disableWatchdog();\n  #endif\n  UsermodManager::onUpdateBegin(true); // notify usermods that update is about to begin (some may require task de-init)\n  \n  strip.suspend();\n  backupConfig(); // backup current config in case the update ends badly\n  strip.resetSegments();  // free as much memory as you can\n  context->needsRestart = true;\n\n  DEBUG_PRINTF_P(PSTR(\"OTA Update Start, %x --> %x\\n\"), (uintptr_t)request,(uintptr_t) context);\n\n  auto skipValidationParam = request->getParam(\"skipValidation\", true);\n  if (skipValidationParam && (skipValidationParam->value() == \"1\")) {\n    context->releaseCheckPassed = true;\n    DEBUG_PRINTLN(F(\"OTA validation skipped by user\"));\n  }\n  \n  // Begin update with the firmware size from content length\n  size_t updateSize = request->contentLength() > 0 ? request->contentLength() : ((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);\n  if (!Update.begin(updateSize)) {    \n    context->errorMessage = Update.UPDATE_ERROR();\n    DEBUG_PRINTF_P(PSTR(\"OTA Failed to begin: %s\\n\"), context->errorMessage.c_str());\n    return false;\n  }\n  \n  context->updateStarted = true;\n  return true;\n}\n\n// Create an OTA context object on an AsyncWebServerRequest\n// Returns true if successful, false on failure.\nbool initOTA(AsyncWebServerRequest *request) {\n  // Allocate update context\n  UpdateContext* context = new (std::nothrow) UpdateContext {};  \n  if (context) {\n    request->_tempObject = context;\n    request->onDisconnect([=]() { endOTA(request); });  // ensures we restart on failure\n  };\n\n  DEBUG_PRINTF_P(PSTR(\"OTA Update init, %x --> %x\\n\"), (uintptr_t)request,(uintptr_t) context);\n  return (context != nullptr);\n}\n\nvoid setOTAReplied(AsyncWebServerRequest *request) {\n  UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);\n  if (!context) return;\n  context->replySent = true;\n};\n\n// Returns pointer to error message, or nullptr if OTA was successful.\nstd::pair<bool, String> getOTAResult(AsyncWebServerRequest* request) {\n  UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);\n  if (!context) return { true, F(\"OTA context unexpectedly missing\") };\n  if (context->replySent) return { false, {} };\n  if (context->errorMessage.length()) return { true, context->errorMessage };\n\n  if (context->updateStarted) {\n    // Release the OTA context now.\n    endOTA(request);\n    if (Update.hasError()) {\n      return { true, Update.UPDATE_ERROR() };\n    } else {\n      return { true, {} };\n    }\n  }\n\n  // Should never happen\n  return { true, F(\"Internal software failure\") };\n}\n\n\n\nvoid handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal)\n{\n  UpdateContext* context = reinterpret_cast<UpdateContext*>(request->_tempObject);\n  if (!context) return;\n\n  //DEBUG_PRINTF_P(PSTR(\"HandleOTAData: %d %d %d\\n\"), index, len, isFinal);\n\n  if (context->replySent || (context->errorMessage.length())) return;\n\n  if (index == 0) {\n    if (!beginOTA(request, context)) return;\n  }\n\n  // Perform validation if we haven't done it yet and we have reached the metadata offset\n  if (!context->releaseCheckPassed && (index+len) > METADATA_OFFSET) {\n    // Current chunk contains the metadata offset\n    size_t availableDataAfterOffset = (index + len) - METADATA_OFFSET;\n\n    DEBUG_PRINTF_P(PSTR(\"OTA metadata check: %d in buffer, %d received, %d available\\n\"), context->releaseMetadataBuffer.size(), len, availableDataAfterOffset);\n\n    if (availableDataAfterOffset >= METADATA_SEARCH_RANGE) {\n      // We have enough data to validate, one way or another\n      const uint8_t* search_data = data;\n      size_t search_len = len;\n      \n      // If we have saved data, use that instead\n      if (context->releaseMetadataBuffer.size()) {\n        // Add this data\n        context->releaseMetadataBuffer.insert(context->releaseMetadataBuffer.end(), data, data+len);\n        search_data = context->releaseMetadataBuffer.data();\n        search_len = context->releaseMetadataBuffer.size();\n      }\n\n      // Do the checking\n      char errorMessage[128];\n      bool OTA_ok = validateOTA(search_data, search_len, errorMessage, sizeof(errorMessage));\n      \n      // Release buffer if there was one\n      context->releaseMetadataBuffer = decltype(context->releaseMetadataBuffer){};\n      \n      if (!OTA_ok) {\n        DEBUG_PRINTF_P(PSTR(\"OTA declined: %s\\n\"), errorMessage);\n        context->errorMessage = errorMessage;\n        context->errorMessage += F(\" Enable 'Ignore firmware validation' to proceed anyway.\");\n        return;\n      } else {\n        DEBUG_PRINTLN(F(\"OTA allowed: Release compatibility check passed\"));\n        context->releaseCheckPassed = true;\n      }        \n    } else {\n      // Store the data we just got for next pass\n      context->releaseMetadataBuffer.insert(context->releaseMetadataBuffer.end(), data, data+len);\n    }\n  }\n\n  // Check if validation was still pending (shouldn't happen normally)\n  // This is done before writing the last chunk, so endOTA can abort \n  if (isFinal && !context->releaseCheckPassed) {\n    DEBUG_PRINTLN(F(\"OTA failed: Validation never completed\"));\n    // Don't write the last chunk to the updater: this will trip an error later\n    context->errorMessage = F(\"Release check data never arrived?\");\n    return;\n  }\n\n  // Write chunk data to OTA update (only if release check passed or still pending)\n  if (!Update.hasError()) {\n    if (Update.write(data, len) != len) {\n      DEBUG_PRINTF_P(PSTR(\"OTA write failed on chunk %zu: %s\\n\"), index, Update.UPDATE_ERROR());\n    }\n  }\n\n  if(isFinal) {\n    DEBUG_PRINTLN(F(\"OTA Update End\"));\n    // Upload complete\n    context->uploadComplete = true;\n  }\n}\n\nvoid markOTAvalid() {\n  #ifndef ESP8266\n  const esp_partition_t* running = esp_ota_get_running_partition();\n  esp_ota_img_states_t ota_state;\n  if (esp_ota_get_state_partition(running, &ota_state) == ESP_OK) {\n    if (ota_state == ESP_OTA_IMG_PENDING_VERIFY) {\n      esp_ota_mark_app_valid_cancel_rollback(); // only needs to be called once, it marks the ota_state as ESP_OTA_IMG_VALID\n      DEBUG_PRINTLN(F(\"Current firmware validated\"));\n    }\n  }\n  #endif\n}\n\n#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA)\n\n// Class for computing the expected bootloader data size given a stream of the data.\n// If the image includes an SHA256 appended after the data stream, we do not consider it here.\nclass BootloaderImageSizer {\npublic:\n\n  bool feed(const uint8_t* data, size_t len) {\n    if (error) return false;\n\n    //DEBUG_PRINTF(\"Feed %d\\n\", len);\n\n    if (imageSize == 0) {\n      // Parse header first\n      if (len < sizeof(esp_image_header_t)) {\n        error = true;\n        return false;\n      }\n\n      esp_image_header_t header;\n      memcpy(&header, data, sizeof(esp_image_header_t));\n\n      if (header.segment_count == 0) {\n        error = true;\n        return false;\n      }\n\n      imageSize = sizeof(esp_image_header_t);\n      segmentsLeft = header.segment_count;\n      data += sizeof(esp_image_header_t);\n      len -= sizeof(esp_image_header_t);\n      //DEBUG_PRINTF(\"BLS parsed image header, segment count %d, is %d\\n\", segmentsLeft, imageSize);\n    }\n\n    while (len && segmentsLeft) {\n      if (segmentHeaderBytes < sizeof(esp_image_segment_header_t)) {\n        size_t headerBytes = std::min(len, sizeof(esp_image_segment_header_t) - segmentHeaderBytes);\n        memcpy(reinterpret_cast<uint8_t*>(&segmentHeader) + segmentHeaderBytes, data, headerBytes);\n        segmentHeaderBytes += headerBytes;\n        if (segmentHeaderBytes < sizeof(esp_image_segment_header_t)) {\n          return true;  // needs more bytes for the header\n        }\n\n        //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);        \n\n        // Validate segment size\n        if (segmentHeader.data_len > BOOTLOADER_SIZE) {\n          error = true;\n          return false;\n        }\n\n        data += headerBytes;\n        len -= headerBytes;\n        imageSize += sizeof(esp_image_segment_header_t) + segmentHeader.data_len;\n        --segmentsLeft;\n        if (segmentsLeft == 0) {\n          // all done, actually; we don't need to read any more\n \n          // Round up to nearest 16 bytes.\n          // Always add 1 to account for the checksum byte.\n          imageSize = ((imageSize/ 16) + 1) * 16;\n\n          //DEBUG_PRINTF(\"BLS complete, is %d\\n\", imageSize);        \n          return false;\n        }        \n      }\n      \n      // If we don't have enough bytes ...\n      if (len < segmentHeader.data_len) {\n        //DEBUG_PRINTF(\"Needs more bytes\\n\");\n        segmentHeader.data_len -= len;\n        return true;  // still in this segment\n      }\n\n      // Segment complete\n      len -= segmentHeader.data_len;\n      data += segmentHeader.data_len;\n      segmentHeaderBytes = 0;\n      //DEBUG_PRINTF(\"Segment complete: len %d\\n\", len);\n    }\n\n    return !error;\n  }\n\n  bool hasError() const { return error; }\n  bool isSizeKnown() const { return !error && imageSize != 0 && segmentsLeft == 0; }\n  size_t totalSize() const {\n    if (!isSizeKnown()) return 0;\n    return imageSize;\n  }\n\nprivate:\n  size_t imageSize = 0;\n  size_t segmentsLeft = 0;\n  esp_image_segment_header_t segmentHeader;\n  size_t segmentHeaderBytes = 0;\n  bool error = false;\n};\n\nstatic bool bootloaderSHA256CacheValid = false;\nstatic uint8_t bootloaderSHA256Cache[32];\n\n/**\n * Calculate and cache the bootloader SHA256 digest\n * Reads the bootloader from flash and computes SHA256 hash\n * \n * Strictly speaking, most bootloader images already contain a hash at the end of the image; \n * we could in theory just read it.  The trouble is that we have to parse the structure anyways\n * to find the actual endpoint, so we might as well always calculate it ourselves rather than\n * handle a special case if the hash isn't stored.\n * \n */\nstatic void calculateBootloaderSHA256() {\n  // Calculate SHA256\n  mbedtls_sha256_context ctx;\n  mbedtls_sha256_init(&ctx);\n  mbedtls_sha256_starts(&ctx, 0); // 0 = SHA256 (not SHA224)\n\n  const size_t chunkSize = 256;\n  alignas(esp_image_header_t) uint8_t buffer[chunkSize];\n  size_t bootloaderSize = BOOTLOADER_SIZE;\n  BootloaderImageSizer sizer;\n  size_t totalHashLen = 0;\n\n  for (uint32_t offset = 0; offset < bootloaderSize; offset += chunkSize) {\n    size_t readSize = min((size_t)(bootloaderSize - offset), chunkSize);\n    if (esp_flash_read(NULL, buffer, BOOTLOADER_OFFSET + offset, readSize) == ESP_OK) {\n      sizer.feed(buffer, readSize);\n\n      size_t hashLen = readSize;\n      if (sizer.isSizeKnown()) {\n        size_t totalSize = sizer.totalSize();\n        if (totalSize > 0 && totalSize <= BOOTLOADER_SIZE) {\n          bootloaderSize = totalSize;\n          if (offset + readSize > totalSize) {\n            hashLen = (totalSize > offset) ? (totalSize - offset) : 0;\n          }\n        }\n      }\n\n      if (hashLen > 0) {\n        totalHashLen += hashLen;\n        mbedtls_sha256_update(&ctx, buffer, hashLen);\n      }\n    }\n  }\n\n  mbedtls_sha256_finish(&ctx, bootloaderSHA256Cache);\n  mbedtls_sha256_free(&ctx);\n\n  bootloaderSHA256CacheValid = true;\n}\n\n// Get bootloader SHA256 as hex string\nString getBootloaderSHA256Hex() {\n  if (!bootloaderSHA256CacheValid) {\n    calculateBootloaderSHA256();\n  }\n\n  // Convert to hex string\n  String result;\n  result.reserve(65);\n  for (int i = 0; i < 32; i++) {\n    unsigned char b1 = bootloaderSHA256Cache[i];\n    unsigned char b2 = b1 >> 4;\n    b1 &= 0x0F;\n    b1 += '0'; b2 += '0';\n    if (b1 > '9') b1 += 39;\n    if (b2 > '9') b2 += 39;\n    result.concat(b2);\n    result.concat(b1);\n  }\n  return result;\n}\n\n/**\n * Invalidate cached bootloader SHA256 (call after bootloader update)\n * Forces recalculation on next call to calculateBootloaderSHA256 or getBootloaderSHA256Hex\n */\nstatic void invalidateBootloaderSHA256Cache() {\n  bootloaderSHA256CacheValid = false;\n}\n\n/**\n * Verify complete buffered bootloader using ESP-IDF validation approach\n * This matches the key validation steps from esp_image_verify() in ESP-IDF\n * @param buffer Reference to pointer to bootloader binary data (will be adjusted if offset detected)\n * @param len Reference to length of bootloader data (will be adjusted to actual size)\n * @param bootloaderErrorMsg Pointer to String to store error message (must not be null)\n * @return true if validation passed, false otherwise\n */\nstatic bool verifyBootloaderImage(const uint8_t* &buffer, size_t &len, String& bootloaderErrorMsg) {\n  const size_t MIN_IMAGE_HEADER_SIZE = sizeof(esp_image_header_t);\n\n  // 1. Validate minimum size for header\n  if (len < MIN_IMAGE_HEADER_SIZE) {\n    bootloaderErrorMsg = \"Too small\";\n    return false;\n  }\n\n  // Check if the bootloader starts at offset 0x1000 (common in partition table dumps)\n  // This happens when someone uploads a complete flash dump instead of just the bootloader\n  if (len > BOOTLOADER_OFFSET + MIN_IMAGE_HEADER_SIZE &&\n      buffer[BOOTLOADER_OFFSET] == ESP_IMAGE_HEADER_MAGIC &&\n      buffer[0] != ESP_IMAGE_HEADER_MAGIC) {\n    DEBUG_PRINTF_P(PSTR(\"Bootloader detected at offset\\n\"));\n    // Adjust buffer pointer to start at the actual bootloader\n    buffer = buffer + BOOTLOADER_OFFSET;\n    len = len - BOOTLOADER_OFFSET;\n\n    // Re-validate size after adjustment\n    if (len < MIN_IMAGE_HEADER_SIZE) {\n      bootloaderErrorMsg = \"Too small\";\n      return false;\n    }\n  }\n\n  size_t availableLen = len;\n  esp_image_header_t imageHeader{};\n  memcpy(&imageHeader, buffer, sizeof(imageHeader));\n\n  // 2. Basic header sanity checks (matches early esp_image_verify checks)\n  if (imageHeader.magic != ESP_IMAGE_HEADER_MAGIC ||\n      imageHeader.segment_count == 0 || imageHeader.segment_count > 16 ||\n      imageHeader.spi_mode > 3 ||\n      imageHeader.entry_addr < 0x40000000 || imageHeader.entry_addr > 0x50000000) {\n    bootloaderErrorMsg = \"Invalid header\";\n    return false;\n  }\n\n  // 3. Chip ID validation (matches esp_image_verify step 3)\n  if (imageHeader.chip_id != CONFIG_IDF_FIRMWARE_CHIP_ID) {\n    bootloaderErrorMsg = \"Chip ID mismatch\";\n    return false;\n  }\n\n  // 4. Validate image size\n  BootloaderImageSizer sizer;\n  sizer.feed(buffer, availableLen);\n  if (!sizer.isSizeKnown()) {\n    bootloaderErrorMsg = \"Invalid image\";\n    return false;\n  }\n  size_t actualBootloaderSize = sizer.totalSize();\n\n  // 5. SHA256 checksum (optional)\n  if (imageHeader.hash_appended == 1) {\n    actualBootloaderSize += 32;\n  }\n \n  if (actualBootloaderSize > len) {\n    // Same as above\n    bootloaderErrorMsg = \"Too small\";\n    return false;\n  }\n\n  DEBUG_PRINTF_P(PSTR(\"Bootloader validation: %d segments, actual size %d bytes (buffer size %d bytes, hash_appended=%d)\\n\"),\n                 imageHeader.segment_count, actualBootloaderSize, len, imageHeader.hash_appended);\n\n  // Update len to reflect actual bootloader size (including hash and checksum, with alignment)\n  // This is critical - we must write the complete image including checksums\n  len = actualBootloaderSize;\n\n  return true;\n}\n\n// Bootloader OTA context structure\nstruct BootloaderUpdateContext {\n  // State flags\n  bool replySent = false;\n  bool uploadComplete = false;\n  String errorMessage;\n\n  // Buffer to hold bootloader data\n  uint8_t* buffer = nullptr;\n  size_t bytesBuffered = 0;\n  const uint32_t bootloaderOffset = 0x1000;\n  const uint32_t maxBootloaderSize = 0x10000; // 64KB buffer size\n};\n\n// Cleanup bootloader OTA context\nstatic void endBootloaderOTA(AsyncWebServerRequest *request) {\n  BootloaderUpdateContext* context = reinterpret_cast<BootloaderUpdateContext*>(request->_tempObject);\n  request->_tempObject = nullptr;\n\n  DEBUG_PRINTF_P(PSTR(\"EndBootloaderOTA %x --> %x\\n\"), (uintptr_t)request, (uintptr_t)context);\n  if (context) {\n    if (context->buffer) {\n      free(context->buffer);\n      context->buffer = nullptr;\n    }\n\n    // If update failed, restore system state\n    if (!context->uploadComplete || !context->errorMessage.isEmpty()) {\n      strip.resume();\n      #if WLED_WATCHDOG_TIMEOUT > 0\n      WLED::instance().enableWatchdog();\n      #endif\n    }\n\n    delete context;\n  }\n}\n\n// Initialize bootloader OTA context\nbool initBootloaderOTA(AsyncWebServerRequest *request) {\n  if (request->_tempObject) {\n    return true; // Already initialized\n  }\n\n  BootloaderUpdateContext* context = new BootloaderUpdateContext();\n  if (!context) {\n    DEBUG_PRINTLN(F(\"Failed to allocate bootloader OTA context\"));\n    return false;\n  }\n  request->_tempObject = context;\n  request->onDisconnect([=]() { endBootloaderOTA(request); });  // ensures cleanup on disconnect\n\n#ifdef BOOTLOADER_OTA_UNSUPPORTED\n  context->errorMessage = F(\"Bootloader update not supported on this chip\");\n  return false;\n#else\n  DEBUG_PRINTLN(F(\"Bootloader Update Start - initializing buffer\"));\n  #if WLED_WATCHDOG_TIMEOUT > 0\n  WLED::instance().disableWatchdog();\n  #endif\n  lastEditTime = millis(); // make sure PIN does not lock during update\n  strip.suspend();\n  strip.resetSegments();\n\n  // Check available heap before attempting allocation\n  DEBUG_PRINTF_P(PSTR(\"Free heap before bootloader buffer allocation: %d bytes (need %d bytes)\\n\"), getContiguousFreeHeap(), context->maxBootloaderSize);\n\n  context->buffer = (uint8_t*)malloc(context->maxBootloaderSize);\n  if (!context->buffer) {\n    size_t freeHeapNow = getContiguousFreeHeap();\n    DEBUG_PRINTF_P(PSTR(\"Failed to allocate %d byte bootloader buffer! Contiguous heap: %d bytes\\n\"), context->maxBootloaderSize, freeHeapNow);\n    context->errorMessage = \"Out of memory! Contiguous heap: \" + String(freeHeapNow) + \" bytes, need: \" + String(context->maxBootloaderSize) + \" bytes\";\n    strip.resume();\n    #if WLED_WATCHDOG_TIMEOUT > 0\n    WLED::instance().enableWatchdog();\n    #endif\n    return false;\n  }\n\n  context->bytesBuffered = 0;\n  return true;\n#endif  \n}\n\n// Set bootloader OTA replied flag\nvoid setBootloaderOTAReplied(AsyncWebServerRequest *request) {\n  BootloaderUpdateContext* context = reinterpret_cast<BootloaderUpdateContext*>(request->_tempObject);\n  if (context) {\n    context->replySent = true;\n  }\n}\n\n// Get bootloader OTA result\nstd::pair<bool, String> getBootloaderOTAResult(AsyncWebServerRequest *request) {\n  BootloaderUpdateContext* context = reinterpret_cast<BootloaderUpdateContext*>(request->_tempObject);\n\n  if (!context) {\n    return std::make_pair(true, String(F(\"Internal error: No bootloader OTA context\")));\n  }\n\n  bool needsReply = !context->replySent;\n  String errorMsg = context->errorMessage;\n\n  // If upload was successful, return empty string and trigger reboot\n  if (context->uploadComplete && errorMsg.isEmpty()) {\n    doReboot = true;\n    endBootloaderOTA(request);\n    return std::make_pair(needsReply, String());\n  }\n\n  // If there was an error, return it\n  if (!errorMsg.isEmpty()) {\n    endBootloaderOTA(request);\n    return std::make_pair(needsReply, errorMsg);\n  }\n\n  // Should never happen\n  return std::make_pair(true, String(F(\"Internal software failure\")));\n}\n\n// Handle bootloader OTA data\nvoid handleBootloaderOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal) {\n  BootloaderUpdateContext* context = reinterpret_cast<BootloaderUpdateContext*>(request->_tempObject);\n\n  if (!context) {\n    DEBUG_PRINTLN(F(\"No bootloader OTA context - ignoring data\"));\n    return;\n  }\n\n  if (!context->errorMessage.isEmpty()) {\n    return;\n  }\n\n  // Buffer the incoming data\n  if (context->buffer && context->bytesBuffered + len <= context->maxBootloaderSize) {\n    memcpy(context->buffer + context->bytesBuffered, data, len);\n    context->bytesBuffered += len;\n    DEBUG_PRINTF_P(PSTR(\"Bootloader buffer progress: %d / %d bytes\\n\"), context->bytesBuffered, context->maxBootloaderSize);\n  } else if (!context->buffer) {\n    DEBUG_PRINTLN(F(\"Bootloader buffer not allocated!\"));\n    context->errorMessage = \"Internal error: Bootloader buffer not allocated\";\n    return;\n  } else {\n    size_t totalSize = context->bytesBuffered + len;\n    DEBUG_PRINTLN(F(\"Bootloader size exceeds maximum!\"));\n    context->errorMessage = \"Bootloader file too large: \" + String(totalSize) + \" bytes (max: \" + String(context->maxBootloaderSize) + \" bytes)\";\n    return;\n  }\n\n  // Only write to flash when upload is complete\n  if (isFinal) {\n    DEBUG_PRINTLN(F(\"Bootloader Upload Complete - validating and flashing\"));\n\n    if (context->buffer && context->bytesBuffered > 0) {\n      // Prepare pointers for verification (may be adjusted if bootloader at offset)\n      const uint8_t* bootloaderData = context->buffer;\n      size_t bootloaderSize = context->bytesBuffered;\n\n      // Verify the complete bootloader image before flashing\n      // Note: verifyBootloaderImage may adjust bootloaderData pointer and bootloaderSize\n      // for validation purposes only\n      if (!verifyBootloaderImage(bootloaderData, bootloaderSize, context->errorMessage)) {\n        DEBUG_PRINTLN(F(\"Bootloader validation failed!\"));\n        // Error message already set by verifyBootloaderImage\n      } else {\n        // Calculate offset to write to flash\n        // If bootloaderData was adjusted (partition table detected), we need to skip it in flash too\n        size_t flashOffset = context->bootloaderOffset;\n        const uint8_t* dataToWrite = context->buffer;\n        size_t bytesToWrite = context->bytesBuffered;\n\n        // If validation adjusted the pointer, it means we have a partition table at the start\n        // In this case, we should skip writing the partition table and write bootloader at 0x1000\n        if (bootloaderData != context->buffer) {\n          // bootloaderData was adjusted - skip partition table in our data\n          size_t partitionTableSize = bootloaderData - context->buffer;\n          dataToWrite = bootloaderData;\n          bytesToWrite = bootloaderSize;\n          DEBUG_PRINTF_P(PSTR(\"Skipping %d bytes of partition table data\\n\"), partitionTableSize);\n        }\n\n        DEBUG_PRINTF_P(PSTR(\"Bootloader validation passed - writing %d bytes to flash at 0x%04X\\n\"),\n                       bytesToWrite, flashOffset);\n\n        // Calculate erase size (must be multiple of 4KB)\n        size_t eraseSize = ((bytesToWrite + 0xFFF) / 0x1000) * 0x1000;\n        if (eraseSize > context->maxBootloaderSize) {\n          eraseSize = context->maxBootloaderSize;\n        }\n\n        // Erase bootloader region\n        DEBUG_PRINTF_P(PSTR(\"Erasing %d bytes at 0x%04X...\\n\"), eraseSize, flashOffset);\n        esp_err_t err = esp_flash_erase_region(NULL, flashOffset, eraseSize);\n        if (err != ESP_OK) {\n          DEBUG_PRINTF_P(PSTR(\"Bootloader erase error: %d\\n\"), err);\n          context->errorMessage = \"Flash erase failed (error code: \" + String(err) + \")\";\n        } else {\n          // Write the validated bootloader data to flash\n          err = esp_flash_write(NULL, dataToWrite, flashOffset, bytesToWrite);\n          if (err != ESP_OK) {\n            DEBUG_PRINTF_P(PSTR(\"Bootloader flash write error: %d\\n\"), err);\n            context->errorMessage = \"Flash write failed (error code: \" + String(err) + \")\";\n          } else {\n            DEBUG_PRINTF_P(PSTR(\"Bootloader Update Success - %d bytes written to 0x%04X\\n\"),\n                           bytesToWrite, flashOffset);\n            // Invalidate cached bootloader hash\n            invalidateBootloaderSHA256Cache();\n            context->uploadComplete = true;\n          }\n        }\n      }\n    } else if (context->bytesBuffered == 0) {\n      context->errorMessage = \"No bootloader data received\";\n    }\n  }\n}\n#endif\n"
  },
  {
    "path": "wled00/ota_update.h",
    "content": "//  WLED OTA update interface\n\n#include <Arduino.h>\n#ifdef ESP8266\n  #include <Updater.h>\n#else\n   #include <Update.h>\n#endif\n\n#pragma once\n\n// Platform-specific metadata locations\n#ifdef ESP32\n#define BUILD_METADATA_SECTION \".rodata_custom_desc\"\n#elif defined(ESP8266)\n#define BUILD_METADATA_SECTION \".ver_number\"\n#endif\n\n\nclass AsyncWebServerRequest;\n\n/**\n *  Create an OTA context object on an AsyncWebServerRequest\n * @param request Pointer to web request object\n * @return true if allocation was successful, false if not\n */\nbool initOTA(AsyncWebServerRequest *request);\n\n/**\n *  Indicate to the OTA subsystem that a reply has already been generated\n * @param request Pointer to web request object\n */\nvoid setOTAReplied(AsyncWebServerRequest *request);\n\n/**\n *  Retrieve the OTA result.\n * @param request Pointer to web request object\n * @return bool indicating if a reply is necessary; string with error message if the update failed.\n */\nstd::pair<bool, String> getOTAResult(AsyncWebServerRequest *request);\n\n/**\n *  Process a block of OTA data.  This is a passthrough of an ArUploadHandlerFunction.\n * Requires that initOTA be called on the handler object before any work will be done.\n * @param request Pointer to web request object\n * @param index Offset in to uploaded file\n * @param data New data bytes\n * @param len Length of new data bytes\n * @param isFinal Indicates that this is the last block\n * @return bool indicating if a reply is necessary; string with error message if the update failed.\n */\nvoid handleOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal);\n\n/**\n * Mark currently running firmware as valid to prevent auto-rollback on reboot.\n * This option can be enabled in some builds/bootloaders, it is an sdkconfig flag.\n */\nvoid markOTAvalid();\n\n#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA)\n\n/**\n * Get bootloader SHA256 as hex string\n * @return String containing 64-character hex representation of SHA256 hash\n */\nString getBootloaderSHA256Hex();\n\n/**\n * Create a bootloader OTA context object on an AsyncWebServerRequest\n * @param request Pointer to web request object\n * @return true if allocation was successful, false if not\n */\nbool initBootloaderOTA(AsyncWebServerRequest *request);\n\n/**\n * Indicate to the bootloader OTA subsystem that a reply has already been generated\n * @param request Pointer to web request object\n */\nvoid setBootloaderOTAReplied(AsyncWebServerRequest *request);\n\n/**\n * Retrieve the bootloader OTA result.\n * @param request Pointer to web request object\n * @return bool indicating if a reply is necessary; string with error message if the update failed.\n */\nstd::pair<bool, String> getBootloaderOTAResult(AsyncWebServerRequest *request);\n\n/**\n * Process a block of bootloader OTA data. This is a passthrough of an ArUploadHandlerFunction.\n * Requires that initBootloaderOTA be called on the handler object before any work will be done.\n * @param request Pointer to web request object\n * @param index Offset in to uploaded file\n * @param data New data bytes\n * @param len Length of new data bytes\n * @param isFinal Indicates that this is the last block\n */\nvoid handleBootloaderOTAData(AsyncWebServerRequest *request, size_t index, uint8_t *data, size_t len, bool isFinal);\n#endif\n\n"
  },
  {
    "path": "wled00/overlay.cpp",
    "content": "#include \"wled.h\"\n\n// forward declarations\nstatic void _overlayAnalogCountdown();\n\n/*\n * Used to draw clock overlays over the strip\n */\n\nstatic void _overlayAnalogClock()\n{\n  int overlaySize = overlayMax - overlayMin +1;\n  if (countdownMode)\n  {\n    _overlayAnalogCountdown(); return;\n  }\n  float hourP = ((float)(hour(localTime)%12))/12.0f;\n  float minuteP = ((float)minute(localTime))/60.0f;\n  hourP = hourP + minuteP/12.0f;\n  float secondP = ((float)second(localTime))/60.0f;\n  unsigned hourPixel = floorf(analogClock12pixel + overlaySize*hourP);\n  if (hourPixel > overlayMax) hourPixel = overlayMin -1 + hourPixel - overlayMax;\n  unsigned minutePixel = floorf(analogClock12pixel + overlaySize*minuteP);\n  if (minutePixel > overlayMax) minutePixel = overlayMin -1 + minutePixel - overlayMax;\n  unsigned secondPixel = floorf(analogClock12pixel + overlaySize*secondP);\n  if (secondPixel > overlayMax) secondPixel = overlayMin -1 + secondPixel - overlayMax;\n  if (analogClockSecondsTrail)\n  {\n    if (secondPixel < analogClock12pixel)\n    {\n      strip.setRange(analogClock12pixel, overlayMax, color_fade(0xFF0000, bri));\n      strip.setRange(overlayMin, secondPixel, color_fade(0xFF0000, bri));\n    } else\n    {\n      strip.setRange(analogClock12pixel, secondPixel, color_fade(0xFF0000, bri));\n    }\n  }\n  if (analogClock5MinuteMarks)\n  {\n    for (unsigned i = 0; i <= 12; i++)\n    {\n      unsigned pix = analogClock12pixel + roundf((overlaySize / 12.0f) *i);\n      if (pix > overlayMax) pix -= overlaySize;\n      strip.setPixelColor(pix, color_fade(0x00FFAA, bri));\n    }\n  }\n  if (!analogClockSecondsTrail) strip.setPixelColor(secondPixel, color_fade(0xFF0000, bri));\n  strip.setPixelColor(minutePixel, color_fade(0x00FF00, bri));\n  strip.setPixelColor(hourPixel, color_fade(0x0000FF, bri));\n}\n\n\nstatic void _overlayAnalogCountdown()\n{\n  if ((unsigned long)toki.second() < countdownTime)\n  {\n    long diff = countdownTime - toki.second();\n    float pval = 60.0f;\n    if (diff > 31557600L) //display in years if more than 365 days\n    {\n      pval = 315576000.0f; //10 years\n    } else if (diff > 2592000L) //display in months if more than a month\n    {\n      pval = 31557600.0f; //1 year\n    } else if (diff > 604800) //display in weeks if more than a week\n    {\n      pval = 2592000.0f; //1 month\n    } else if (diff > 86400) //display in days if more than 24 hours\n    {\n      pval = 604800.0f; //1 week\n    } else if (diff > 3600) //display in hours if more than 60 minutes\n    {\n      pval = 86400.0f; //1 day\n    } else if (diff > 60) //display in minutes if more than 60 seconds\n    {\n      pval = 3600.0f; //1 hour\n    }\n    int overlaySize = overlayMax - overlayMin +1;\n    float perc = (pval-(float)diff)/pval;\n    if (perc > 1.0f) perc = 1.0f;\n    byte pixelCnt = perc*overlaySize;\n    if (analogClock12pixel + pixelCnt > overlayMax)\n    {\n      strip.setRange(analogClock12pixel, overlayMax, ((uint32_t)colSec[3] << 24)| ((uint32_t)colSec[0] << 16) | ((uint32_t)colSec[1] << 8) | colSec[2]);\n      strip.setRange(overlayMin, overlayMin +pixelCnt -(1+ overlayMax -analogClock12pixel), ((uint32_t)colSec[3] << 24)| ((uint32_t)colSec[0] << 16) | ((uint32_t)colSec[1] << 8) | colSec[2]);\n    } else\n    {\n      strip.setRange(analogClock12pixel, analogClock12pixel + pixelCnt, ((uint32_t)colSec[3] << 24)| ((uint32_t)colSec[0] << 16) | ((uint32_t)colSec[1] << 8) | colSec[2]);\n    }\n  }\n}\n\nvoid handleOverlayDraw() {\n  UsermodManager::handleOverlayDraw();\n  if (analogClockSolidBlack) {\n    for (unsigned i = 0; i < strip.getSegmentsNum(); i++) {\n      const Segment& segment = strip.getSegment(i);\n      if (!segment.isActive()) continue;\n      if (segment.mode > 0 || segment.colors[0] > 0) {\n        return;\n      }\n    }\n  }\n  if (overlayCurrent == 1) _overlayAnalogClock();\n}\n\n/*\n * Support for the Cronixie clock has moved to a usermod, compile with \"-D USERMOD_CRONIXIE\" to enable\n */\n"
  },
  {
    "path": "wled00/palettes.cpp",
    "content": "#include \"wled.h\"\n\n/*\n * WLED Color palettes\n *\n * Note: palettes imported from http://seaviewsensing.com/pub/cpt-city are gamma corrected using gammas (1.182, 1.0, 1.136)\n *       this is done to match colors of the palettes after applying the (default) global gamma of 2.2 to versions\n *       prior to WLED 0.16 which used pre-applied gammas of (2.6,2.2,2.5) for these palettes.\n *       Palettes from FastLED are intended to be used without gamma correction, an inverse gamma of 2.2 is applied to original colors\n */\n\n// Gradient palette \"ib_jul01_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/ing/xmas/ib_jul01.c3g\nconst uint8_t ib_jul01_gp[] PROGMEM = {\n    0, 226,   6,  12,\n   94,  26,  96,  78,\n  132, 130, 189,  94,\n  255, 177,   3,   9};\n\n// Gradient palette \"es_vintage_57_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/es/vintage/es_vintage_57.c3g\nconst uint8_t es_vintage_57_gp[] PROGMEM = {\n    0,  29,   8,   3,\n   53,  76,   1,   0,\n  104, 142,  96,  28,\n  153, 211, 191,  61,\n  255, 117, 129,  42};\n\n// Gradient palette \"es_vintage_01_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/es/vintage/es_vintage_01.c3g\nconst uint8_t es_vintage_01_gp[] PROGMEM = {\n    0,  41,  18,  24,\n   51,  73,   0,  22,\n   76, 165, 170,  38,\n  101, 255, 189,  80,\n  127, 139,  56,  40,\n  153,  73,   0,  22,\n  229,  41,  18,  24,\n  255,  41,  18,  24};\n\n// Gradient palette \"es_rivendell_15_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/es/rivendell/es_rivendell_15.c3g\nconst uint8_t es_rivendell_15_gp[] PROGMEM = {\n    0,  24,  69,  44,\n  101,  73, 105,  70,\n  165, 129, 140,  97,\n  242, 200, 204, 166,\n  255, 200, 204, 166};\n\n// Gradient palette \"rgi_15_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/ds/rgi/rgi_15.c3g\nconst uint8_t rgi_15_gp[] PROGMEM = {\n    0,  41,  14,  99,\n   31, 128,  24,  74,\n   63, 227,  34,  50,\n   95, 132,  31,  76,\n  127,  47,  29, 102,\n  159, 109,  47, 101,\n  191, 176,  66, 100,\n  223, 129,  57, 104,\n  255,  84,  48, 108};\n\n// Gradient palette \"retro2_16_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/ma/retro2/retro2_16.c3g\nconst uint8_t retro2_16_gp[] PROGMEM = {\n    0, 222, 191,   8,\n  255, 117,  52,   1};\n\n// Gradient palette \"Analogous_1_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/nd/red/Analogous_1.c3g\nconst uint8_t Analogous_1_gp[] PROGMEM = {\n    0,  38,   0, 255,\n   63,  86,   0, 255,\n  127, 139,   0, 255,\n  191, 196,   0, 117,\n  255, 255,   0,   0};\n\n// Gradient palette \"es_pinksplash_08_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/es/pink_splash/es_pinksplash_08.c3g\nconst uint8_t es_pinksplash_08_gp[] PROGMEM = {\n    0, 186,  63, 255,\n  127, 227,   9,  85,\n  175, 234, 205, 213,\n  221, 205,  38, 176,\n  255, 205,  38, 176,\n};\n\n// Gradient palette \"es_ocean_breeze_036_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/es/ocean_breeze/es_ocean_breeze_036.c3g\nconst uint8_t es_ocean_breeze_036_gp[] PROGMEM = {\n    0,  16,  48,  51,\n   89,  27, 166, 175,\n  153, 197, 233, 255,\n  255,   0, 145, 152};\n\n// Gradient palette \"departure_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/mjf/departure.c3g\nconst uint8_t departure_gp[] PROGMEM = {\n    0,  53,  34,   0,\n   42,  86,  51,   0,\n   63, 147, 108,  49,\n   84, 212, 166, 108,\n  106, 235, 212, 180,\n  116, 255, 255, 255,\n  138, 191, 255, 193,\n  148,  84, 255,  88,\n  170,   0, 255,   0,\n  191,   0, 192,   0,\n  212,   0, 128,   0,\n  255,   0, 128,   0};\n\n// Gradient palette \"es_landscape_64_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/es/landscape/es_landscape_64.c3g\nconst uint8_t es_landscape_64_gp[] PROGMEM = {\n    0,   0,   0,   0,\n   37,  31,  89,  19,\n   76,  72, 178,  43,\n  127, 150, 235,   5,\n  128, 186, 234, 119,\n  130, 222, 233, 252,\n  153, 197, 219, 231,\n  204, 132, 179, 253,\n  255,  28, 107, 225};\n\n// Gradient palette \"es_landscape_33_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/es/landscape/es_landscape_33.c3g\nconst uint8_t es_landscape_33_gp[] PROGMEM = {\n    0,  12,  45,   0,\n   19, 101,  86,   2,\n   38, 207, 128,   4,\n   63, 243, 197,  18,\n   66, 109, 196, 146,\n  255,   5,  39,   7};\n\n// Gradient palette \"rainbowsherbet_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/ma/icecream/rainbowsherbet.c3g\nconst uint8_t rainbowsherbet_gp[] PROGMEM = {\n    0, 255, 102,  41,\n   43, 255, 140,  90,\n   86, 255,  51,  90,\n  127, 255, 153, 169,\n  170, 255, 255, 249,\n  209, 113, 255,  85,\n  255, 157, 255, 137};\n\n// Gradient palette \"gr65_hult_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/hult/gr65_hult.c3g\nconst uint8_t gr65_hult_gp[] PROGMEM = {\n    0, 251, 216, 252,\n   48, 255, 192, 255,\n   89, 239,  95, 241,\n  160,  51, 153, 217,\n  216,  24, 184, 174,\n  255,  24, 184, 174};\n\n// Gradient palette \"gr64_hult_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/hult/gr64_hult.c3g\nconst uint8_t gr64_hult_gp[] PROGMEM = {\n    0,  24, 184, 174,\n   66,   8, 162, 150,\n  104, 124, 137,   7,\n  130, 178, 186,  22,\n  150, 124, 137,   7,\n  201,   6, 156, 144,\n  239,   0, 128, 117,\n  255,   0, 128, 117};\n\n// Gradient palette \"GMT_drywet_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/gmt/GMT_drywet.c3g\nconst uint8_t GMT_drywet_gp[] PROGMEM = {\n    0, 119,  97,  33,\n   42, 235, 199,  88,\n   84, 169, 238, 124,\n  127,  37, 238, 232,\n  170,   7, 120, 236,\n  212,  27,   1, 175,\n  255,   4,  51, 101};\n\n// Gradient palette \"ib15_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/ing/general/ib15.c3g\nconst uint8_t ib15_gp[] PROGMEM = {\n    0, 177, 160, 199,\n   72, 205, 158, 149,\n   89, 233, 155, 101,\n  107, 255,  95,  63,\n  141, 192,  98, 109,\n  255, 132, 101, 159};\n\n// Gradient palette \"Tertiary_01_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/nd/vermillion/Tertiary_01.c3g\nconst uint8_t Tertiary_01_gp[] PROGMEM = {\n    0,   0,  25, 255,\n   63,  38, 140, 117,\n  127,  86, 255,   0,\n  191, 167, 140,  19,\n  255, 255,  25,  41};\n\n// Gradient palette \"lava_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/neota/elem/lava.c3g\nconst uint8_t lava_gp[] PROGMEM = {\n    0,   0,   0,   0,\n   46,  77,   0,   0,\n   96, 177,   0,   0,\n  108, 196,  38,   9,\n  119, 215,  76,  19,\n  146, 235, 115,  29,\n  174, 255, 153,  41,\n  188, 255, 178,  41,\n  202, 255, 204,  41,\n  218, 255, 230,  41,\n  234, 255, 255,  41,\n  244, 255, 255, 143,\n  255, 255, 255, 255};\n\n// Gradient palette \"fierce-ice_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/neota/elem/fierce-ice.c3g\nconst uint8_t fierce_ice_gp[] PROGMEM = {\n    0,   0,   0,   0,\n   59,   0,  51, 117,\n  119,   0, 102, 255,\n  149,  38, 153, 255,\n  180,  86, 204, 255,\n  217, 167, 230, 255,\n  255, 255, 255, 255};\n\n// Gradient palette \"Colorfull_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Colorfull.c3g\nconst uint8_t Colorfull_gp[] PROGMEM = {\n    0,  61, 155,  44,\n   25,  95, 174,  77,\n   60, 132, 193, 113,\n   93, 154, 166, 125,\n  106, 175, 138, 136,\n  109, 183, 121, 137,\n  113, 194, 104, 138,\n  116, 225, 179, 165,\n  124, 255, 255, 192,\n  168, 167, 218, 203,\n  255,  84, 182, 215};\n\n// Gradient palette \"Pink_Purple_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Pink_Purple.c3g\nconst uint8_t Pink_Purple_gp[] PROGMEM = {\n    0,  79,  32, 109,\n   25,  90,  40, 117,\n   51, 102,  48, 124,\n   76, 141, 135, 185,\n  102, 180, 222, 248,\n  109, 208, 236, 252,\n  114, 237, 250, 255,\n  122, 206, 200, 239,\n  149, 177, 149, 222,\n  183, 187, 130, 203,\n  255, 198, 111, 184};\n\n// Gradient palette \"Sunset_Real_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Sunset_Real.c3g\nconst uint8_t Sunset_Real_gp[] PROGMEM = {\n    0, 181,   0,   0,\n   22, 218,  85,   0,\n   51, 255, 170,   0,\n   85, 211,  85,  77,\n  135, 167,   0, 169,\n  198,  73,   0, 188,\n  255,   0,   0, 207};\n\n// Gradient palette \"Sunset_Yellow_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Sunset_Yellow.c3g\nconst uint8_t Sunset_Yellow_gp[] PROGMEM = {\n    0,  61, 135, 184,\n   36, 129, 188, 169,\n   87, 203, 241, 155,\n  100, 228, 237, 141,\n  107, 255, 232, 127,\n  115, 251, 202, 130,\n  120, 248, 172, 133,\n  128, 251, 202, 130,\n  180, 255, 232, 127,\n  223, 255, 242, 120,\n  255, 255, 252, 113};\n\n// Gradient palette \"Beech_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Beech.c3g\nconst uint8_t Beech_gp[] PROGMEM = {\n    0, 255, 254, 236,\n   12, 255, 254, 236,\n   22, 255, 254, 236,\n   26, 223, 224, 178,\n   28, 192, 195, 124,\n   28, 176, 255, 231,\n   50, 123, 251, 236,\n   71,  74, 246, 241,\n   93,  33, 225, 228,\n  120,   0, 204, 215,\n  133,   4, 168, 178,\n  136,  10, 132, 143,\n  136,  51, 189, 212,\n  208,  23, 159, 201,\n  255,   0, 129, 190};\n\n// Gradient palette \"Another_Sunset_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Another_Sunset.c3g\nconst uint8_t Another_Sunset_gp[] PROGMEM = {\n    0, 175, 121,  62,\n   29, 128, 103,  60,\n   68,  84,  84,  58,\n   68, 248, 184,  55,\n   97, 239, 204,  93,\n  124, 230, 225, 133,\n  178, 102, 125, 129,\n  255,   0,  26, 125};\n\n// Gradient palette \"es_autumn_19_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/es/autumn/es_autumn_19.c3g\nconst uint8_t es_autumn_19_gp[] PROGMEM = {\n    0,  90,  14,   5,\n   51, 139,  41,  13,\n   84, 180,  70,  17,\n  104, 192, 202, 125,\n  112, 177, 137,   3,\n  122, 190, 200, 131,\n  124, 192, 202, 124,\n  135, 177, 137,   3,\n  142, 194, 203, 118,\n  163, 177,  68,  17,\n  204, 128,  35,  12,\n  249,  74,   5,   2,\n  255,  74,   5,   2};\n\n// Gradient palette \"BlacK_Blue_Magenta_White_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/nd/basic/BlacK_Blue_Magenta_White.c3g\nconst uint8_t BlacK_Blue_Magenta_White_gp[] PROGMEM = {\n    0,   0,   0,   0,\n   42,   0,   0, 117,\n   84,   0,   0, 255,\n  127, 113,   0, 255,\n  170, 255,   0, 255,\n  212, 255, 128, 255,\n  255, 255, 255, 255};\n\n// Gradient palette \"BlacK_Magenta_Red_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/nd/basic/BlacK_Magenta_Red.c3g\nconst uint8_t BlacK_Magenta_Red_gp[] PROGMEM = {\n    0,   0,   0,   0,\n   63, 113,   0, 117,\n  127, 255,   0, 255,\n  191, 255,   0, 117,\n  255, 255,   0,   0};\n\n// Gradient palette \"BlacK_Red_Magenta_Yellow_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/nd/basic/BlacK_Red_Magenta_Yellow.c3g\nconst uint8_t BlacK_Red_Magenta_Yellow_gp[] PROGMEM = {\n    0,   0,   0,   0,\n   42, 113,   0,   0,\n   84, 255,   0,   0,\n  127, 255,   0, 117,\n  170, 255,   0, 255,\n  212, 255, 128, 117,\n  255, 255, 255,   0};\n\n// Gradient palette \"Blue_Cyan_Yellow_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/nd/basic/Blue_Cyan_Yellow.c3g\nconst uint8_t Blue_Cyan_Yellow_gp[] PROGMEM = {\n    0,   0,   0, 255,\n   63,   0, 128, 255,\n  127,   0, 255, 255,\n  191, 113, 255, 117,\n  255, 255, 255,   0};\n\n//Custom palette by Aircoookie\nconst byte Orange_Teal_gp[] PROGMEM = {\n    0,   0,150, 92,\n   55,   0,150, 92,\n  200, 255, 72,  0,\n  255, 255, 72,  0};\n\n//Custom palette by Aircoookie\nconst byte Tiamat_gp[] PROGMEM = {\n    0,   1,  2, 14, //gc\n   33,   2,  5, 35, //gc from 47, 61,126\n  100,  13,135, 92, //gc from 88,242,247\n  120,  43,255,193, //gc from 135,255,253\n  140, 247,  7,249, //gc from 252, 69,253\n  160, 193, 17,208, //gc from 231, 96,237\n  180,  39,255,154, //gc from 130, 77,213\n  200,   4,213,236, //gc from 57,122,248\n  220,  39,252,135, //gc from 177,254,255\n  240, 193,213,253, //gc from 203,239,253\n  255, 255,249,255};\n\n//Custom palette by Aircoookie\nconst byte April_Night_gp[] PROGMEM = {\n    0,   1,  5, 45, //deep blue\n   10,   1,  5, 45,\n   25,   5,169,175, //light blue\n   40,   1,  5, 45,\n   61,   1,  5, 45,\n   76,  45,175, 31, //green\n   91,   1,  5, 45,\n  112,   1,  5, 45,\n  127, 249,150,  5, //yellow\n  143,   1,  5, 45,\n  162,   1,  5, 45,\n  178, 255, 92,  0, //pastel orange\n  193,   1,  5, 45,\n  214,   1,  5, 45,\n  229, 223, 45, 72, //pink\n  244,   1,  5, 45,\n  255,   1,  5, 45};\n\nconst byte Orangery_gp[] PROGMEM = {\n    0, 255, 95, 23,\n   30, 255, 82,  0,\n   60, 223, 13,  8,\n   90, 144, 44,  2,\n  120, 255,110, 17,\n  150, 255, 69,  0,\n  180, 158, 13, 11,\n  210, 241, 82, 17,\n  255, 213, 37,  4};\n\n//inspired by Mark Kriegsman https://gist.github.com/kriegsman/756ea6dcae8e30845b5a\nconst byte C9_gp[] PROGMEM = {\n    0, 184,  4,  0, //red\n   60, 184,  4,  0,\n   65, 144, 44,  2, //amber\n  125, 144, 44,  2,\n  130,   4, 96,  2, //green\n  190,   4, 96,  2,\n  195,   7,  7, 88, //blue\n  255,   7,  7, 88};\n\nconst byte Sakura_gp[] PROGMEM = {\n    0, 196, 19, 10,\n   65, 255, 69, 45,\n  130, 223, 45, 72,\n  195, 255, 82,103,\n  255, 223, 13, 17};\n\nconst byte Aurora_gp[] PROGMEM = {\n    0,   1,  5, 45, //deep blue\n   64,   0,200, 23,\n  128,   0,255,  0, //green\n  170,   0,243, 45,\n  200,   0,135,  7,\n  255,   1,  5, 45};//deep blue\n\nconst byte Atlantica_gp[] PROGMEM = {\n    0,   0, 28,112, //#001C70\n   50,  32, 96,255, //#2060FF\n  100,   0,243, 45,\n  150,  12, 95, 82, //#0C5F52\n  200,  25,190, 95, //#19BE5F\n  255,  40,170, 80};//#28AA50\n\n  const byte C9_2_gp[] PROGMEM = {\n    0,   6, 126,   2, //green\n   45,   6, 126,   2,\n   46,   4,  30, 114, //blue\n   90,   4,  30, 114,\n   91, 255,   5,   0, //red\n  135, 255,   5,   0,\n  136, 196,  57,   2, //amber\n  180, 196,  57,   2,\n  181, 137,  85,   2, //yellow\n  255, 137,  85,   2};\n\n  //C9, but brighter and with a less purple blue\n  const byte C9_new_gp[] PROGMEM = {\n    0, 255,   5,   0, //red\n   60, 255,   5,   0,\n   61, 196,  57,   2, //amber (start 61?)\n  120, 196,  57,   2,\n  121,   6, 126,   2, //green (start 126?)\n  180,   6, 126,   2,\n  181,   4,  30, 114, //blue (start 191?)\n  255,   4,  30, 114};\n\n// Gradient palette \"temperature_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/arendal/temperature.c3g\nconst uint8_t temperature_gp[] PROGMEM = {\n    0,  20,  92, 171,\n   14,  15, 111, 186,\n   28,   6, 142, 211,\n   42,   2, 161, 227,\n   56,  16, 181, 239,\n   70,  38, 188, 201,\n   84,  86, 204, 200,\n   99, 139, 219, 176,\n  113, 182, 229, 125,\n  127, 196, 230,  63,\n  141, 241, 240,  22,\n  155, 254, 222,  30,\n  170, 251, 199,   4,\n  184, 247, 157,   9,\n  198, 243, 114,  15,\n  226, 213,  30,  29,\n  240, 151,  38,  35,\n  255, 151,  38,  35};\n\n// Gradient palette \"bhw1_01_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_01.c3g\nconst uint8_t retro_clown_gp[] PROGMEM = {\n    0, 242, 168,  38,\n  117, 226,  78,  80,\n  255, 161,  54, 225,\n};\n\n// Gradient palette \"bhw1_04_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_04.c3g\nconst uint8_t candy_gp[] PROGMEM = {\n    0, 243, 242,  23,\n   15, 242, 168,  38,\n  142, 111,  21, 151,\n  198,  74,  22, 150,\n  255,   0,   0, 117};\n\n// Gradient palette \"bhw1_05_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_05.c3g\nconst uint8_t toxy_reaf_gp[] PROGMEM = {\n    0,   2, 239, 126,\n  255, 145,  35, 217};\n\n// Gradient palette \"bhw1_06_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_06.c3g\nconst uint8_t fairy_reaf_gp[] PROGMEM = {\n    0, 220,  19, 187,\n  160,  12, 225, 219,\n  219, 203, 242, 223,\n  255, 255, 255, 255};\n\n// Gradient palette \"bhw1_14_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_14.c3g\nconst uint8_t semi_blue_gp[] PROGMEM = {\n    0,   0,   0,   0,\n   12,  24,   4,  38,\n   53,  55,   8,  84,\n   80,  43,  48, 159,\n  119,  31,  89, 237,\n  145,  50,  59, 166,\n  186,  71,  30,  98,\n  233,  31,  15,  45,\n  255,   0,   0,   0};\n\n// Gradient palette \"bhw1_three_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_three.c3g\nconst uint8_t pink_candy_gp[] PROGMEM = {\n    0, 255, 255, 255,\n   45,  50,  64, 255,\n  112, 242,  16, 186,\n  140, 255, 255, 255,\n  155, 242,  16, 186,\n  196, 116,  13, 166,\n  255, 255, 255, 255};\n\n// Gradient palette \"bhw1_w00t_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_w00t.c3g\nconst uint8_t red_reaf_gp[] PROGMEM = {\n    0,  36,  68, 114,\n  104, 149, 195, 248,\n  188, 255,   0,   0,\n  255,  94,  14,   9};\n\n// Gradient palette \"bhw2_23_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/bhw/bhw2/bhw2_23.c3g\nconst uint8_t aqua_flash_gp[] PROGMEM = {\n    0,   0,   0,   0,\n   66, 130, 242, 245,\n   96, 255, 255,  53,\n  124, 255, 255, 255,\n  153, 255, 255,  53,\n  188, 130, 242, 245,\n  255,   0,   0,   0};\n\n// Gradient palette \"bhw2_xc_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/bhw/bhw2/bhw2_xc.c3g\nconst uint8_t yelblu_hot_gp[] PROGMEM = {\n    0,  43,  30,  57,\n   58,  73,   0, 119,\n  122,  87,   0,  74,\n  158, 197,  57,  22,\n  183, 218, 117,  27,\n  219, 239, 177,  32,\n  255, 246, 247,  27,\n};\n\n// Gradient palette \"bhw2_45_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/bhw/bhw2/bhw2_45.c3g\nconst uint8_t lite_light_gp[] PROGMEM = {\n    0,   0,   0,   0,\n    9,  20,  21,  22,\n   40,  46,  43,  49,\n   66,  46,  43,  49,\n  101,  61,  16,  65,\n  255,   0,   0,   0};\n\n// Gradient palette \"bhw2_22_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/bhw/bhw2/bhw2_22.c3g\nconst uint8_t red_flash_gp[] PROGMEM = {\n    0,   0,   0,   0,\n   99, 242,  12,   8,\n  130, 253, 228, 163,\n  155, 242,  12,   8,\n  255,   0,   0,   0};\n\n// Gradient palette \"bhw3_40_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/bhw/bhw3/bhw3_40.c3g\nconst uint8_t blink_red_gp[] PROGMEM = {\n    0,   4,   7,   4,\n   43,  40,  25,  62,\n   76,  61,  15,  36,\n  109, 207,  39,  96,\n  127, 255, 156, 184,\n  165, 185,  73, 207,\n  204, 105,  66, 240,\n  255,  77,  29,  78};\n\n// Gradient palette \"bhw3_52_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/bhw/bhw3/bhw3_52.c3g\nconst uint8_t red_shift_gp[] PROGMEM = {\n    0,  98,  22,  93,\n   45, 103,  22,  73,\n   99, 192,  45,  56,\n  132, 235, 187,  59,\n  175, 228,  85,  26,\n  201, 228,  56,  48,\n  255,   2,   0,   2};\n\n// Gradient palette \"bhw4_097_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/bhw/bhw4/bhw4_097.c3g\nconst uint8_t red_tide_gp[] PROGMEM = {\n    0, 251,  46,   0,\n   28, 255, 139,  25,\n   43, 246, 158,  63,\n   58, 246, 216, 123,\n   84, 243,  94,  10,\n  114, 177,  65,  11,\n  140, 255, 241, 115,\n  168, 177,  65,  11,\n  196, 250, 233, 158,\n  216, 255,  94,   6,\n  255, 126,   8,   4};\n\n// Gradient palette \"bhw4_017_gp\", originally from\n// http://seaviewsensing.com/pub/cpt-city/bhw/bhw4/bhw4_017.c3g\nconst uint8_t candy2_gp[] PROGMEM = {\n    0, 109, 102, 102,\n   25,  42,  49,  71,\n   48, 121,  96,  84,\n   73, 241, 214,  26,\n   89, 216, 104,  44,\n  130,  42,  49,  71,\n  163, 255, 177,  47,\n  186, 241, 214,  26,\n  211, 109, 102, 102,\n  255,  20,  19,  13};\n\nconst byte trafficlight_gp[] PROGMEM = {\n    0,   0,   0, 0,   //black\n   85,   0, 255, 0,   //green\n  170, 255, 255, 0,   //yellow\n  255, 255,   0, 0};  //red\n\nconst byte Aurora2_gp[] PROGMEM = {\n    0,  17, 177,  13,    //Greenish\n   64, 121, 242,   5,    //Greenish\n  128,  25, 173, 121,    //Turquoise\n  192, 250,  77, 127,    //Pink\n  255, 171, 101, 221};   //Purple\n\n// FastLed palettes, corrected with inverse gamma of 2.2 to match original looks\n\n// Party colors\nconst TProgmemRGBPalette16 PartyColors_gc22 FL_PROGMEM = {\n  0x9B00D5, 0xBD00B8, 0xDA0092, 0xF3005C,\n  0xF45500, 0xDC8F00, 0xD5B400, 0xD5D500,\n  0xD59B00, 0xEF6600, 0xF90044, 0xE10086,\n  0xC400B0, 0xA300CF, 0x7600E8, 0x0032FC};\n\n// Rainbow colors\nconst TProgmemRGBPalette16 RainbowColors_gc22 FL_PROGMEM = {\n  0xFF0000, 0xEB7000, 0xD59B00, 0xD5BA00,\n  0xD5D500, 0x9CEB00, 0x00FF00, 0x00EB70,\n  0x00D59B, 0x009CD4, 0x0000FF, 0x7000EB,\n  0x9B00D5, 0xBA00BB, 0xD5009B, 0xEB0072};\n\n// Rainbow colors with alternatating stripes of black\nconst TProgmemRGBPalette16 RainbowStripeColors_gc22 FL_PROGMEM = {\n  0xFF0000, 0x000000, 0xD59B00, 0x000000,\n  0xD5D500, 0x000000, 0x00FF00, 0x000000,\n  0x00D59B, 0x000000, 0x0000FF, 0x000000,\n  0x9B00D5, 0x000000, 0xD5009B, 0x000000};\n\n// array of fastled palettes (palette 6 - 12)\nconst TProgmemRGBPalette16 *const fastledPalettes[] PROGMEM = {\n  &PartyColors_gc22,            //06-00 Party\n  &CloudColors_p,               //07-01 Cloud\n  &LavaColors_p,                //08-02 Lava\n  &OceanColors_p,               //09-03 Ocean\n  &ForestColors_p,              //10-04 Forest\n  &RainbowColors_gc22,          //11-05 Rainbow\n  &RainbowStripeColors_gc22     //12-06 Rainbow Bands\n};\n\n// Single array of defined cpt-city color palettes.\n// This will let us programmatically choose one based on\n// a number, rather than having to activate each explicitly\n// by name every time.\nconst uint8_t* const gGradientPalettes[] PROGMEM = {\n  Sunset_Real_gp,               //13-00 Sunset\n  es_rivendell_15_gp,           //14-01 Rivendell\n  es_ocean_breeze_036_gp,       //15-02 Breeze\n  rgi_15_gp,                    //16-03 Red & Blue\n  retro2_16_gp,                 //17-04 Yellowout\n  Analogous_1_gp,               //18-05 Analogous\n  es_pinksplash_08_gp,          //19-06 Splash\n  Sunset_Yellow_gp,             //20-07 Pastel\n  Another_Sunset_gp,            //21-08 Sunset2\n  Beech_gp,                     //22-09 Beech\n  es_vintage_01_gp,             //23-10 Vintage\n  departure_gp,                 //24-11 Departure\n  es_landscape_64_gp,           //25-12 Landscape\n  es_landscape_33_gp,           //26-13 Beach\n  rainbowsherbet_gp,            //27-14 Sherbet\n  gr65_hult_gp,                 //28-15 Hult\n  gr64_hult_gp,                 //29-16 Hult64\n  GMT_drywet_gp,                //30-17 Drywet\n  ib_jul01_gp,                  //31-18 Jul\n  es_vintage_57_gp,             //32-19 Grintage\n  ib15_gp,                      //33-20 Rewhi\n  Tertiary_01_gp,               //34-21 Tertiary\n  lava_gp,                      //35-22 Fire\n  fierce_ice_gp,                //36-23 Icefire\n  Colorfull_gp,                 //37-24 Cyane\n  Pink_Purple_gp,               //38-25 Light Pink\n  es_autumn_19_gp,              //39-26 Autumn\n  BlacK_Blue_Magenta_White_gp,  //40-27 Magenta\n  BlacK_Magenta_Red_gp,         //41-28 Magred\n  BlacK_Red_Magenta_Yellow_gp,  //42-29 Yelmag\n  Blue_Cyan_Yellow_gp,          //43-30 Yelblu\n  Orange_Teal_gp,               //44-31 Orange & Teal\n  Tiamat_gp,                    //45-32 Tiamat\n  April_Night_gp,               //46-33 April Night\n  Orangery_gp,                  //47-34 Orangery\n  C9_gp,                        //48-35 C9\n  Sakura_gp,                    //49-36 Sakura\n  Aurora_gp,                    //50-37 Aurora\n  Atlantica_gp,                 //51-38 Atlantica\n  C9_2_gp,                      //52-39 C9 2\n  C9_new_gp,                    //53-40 C9 New\n  temperature_gp,               //54-41 Temperature\n  Aurora2_gp,                   //55-42 Aurora 2\n  retro_clown_gp,               //56-43 Retro Clown\n  candy_gp,                     //57-44 Candy\n  toxy_reaf_gp,                 //58-45 Toxy Reaf\n  fairy_reaf_gp,                //59-46 Fairy Reaf\n  semi_blue_gp,                 //60-47 Semi Blue\n  pink_candy_gp,                //61-48 Pink Candy\n  red_reaf_gp,                  //62-49 Red Reaf\n  aqua_flash_gp,                //63-50 Aqua Flash\n  yelblu_hot_gp,                //64-51 Yelblu Hot\n  lite_light_gp,                //65-52 Lite Light\n  red_flash_gp,                 //66-53 Red Flash\n  blink_red_gp,                 //67-54 Blink Red\n  red_shift_gp,                 //68-55 Red Shift\n  red_tide_gp,                  //69-56 Red Tide\n  candy2_gp,                    //70-57 Candy2\n  trafficlight_gp               //71-58 Traffic Light\n};\n"
  },
  {
    "path": "wled00/pin_manager.cpp",
    "content": "#include \"wled.h\"\n#include \"pin_manager.h\"\n\n#ifdef ARDUINO_ARCH_ESP32\n  #ifdef bitRead\n    // Arduino variants assume 32 bit values\n    #undef bitRead\n    #undef bitSet\n    #undef bitClear\n    #define bitRead(var,bit)      (((unsigned long long)(var)>>(bit))&0x1ULL)\n    #define bitSet(var,bit)       ((var)|=(1ULL<<(bit)))\n    #define bitClear(var,bit)     ((var)&=(~(1ULL<<(bit))))\n  #endif\n#endif\n\n// Pin management state variables\n#ifdef ESP8266\nstatic uint32_t pinAlloc = 0UL;     // 1 bit per pin, we use first 17bits\n#else\nstatic uint64_t pinAlloc = 0ULL;     // 1 bit per pin, we use 50 bits on ESP32-S3\nstatic uint16_t ledcAlloc = 0;    // up to 16 LEDC channels (WLED_MAX_ANALOG_CHANNELS)\n#endif\nstatic uint8_t i2cAllocCount = 0; // allow multiple allocation of I2C bus pins but keep track of allocations\nstatic uint8_t spiAllocCount = 0; // allow multiple allocation of SPI bus pins but keep track of allocations\nstatic PinOwner ownerTag[WLED_NUM_PINS] = { PinOwner::None };\n\n/// Actual allocation/deallocation routines\nbool PinManager::deallocatePin(byte gpio, PinOwner tag)\n{\n  if (gpio == 0xFF) return true;           // explicitly allow clients to free -1 as a no-op\n  if (!isPinOk(gpio, false)) return false; // but return false for any other invalid pin\n\n  // if a non-zero ownerTag, only allow de-allocation if the owner's tag is provided\n  if ((ownerTag[gpio] != PinOwner::None) && (ownerTag[gpio] != tag)) {\n    DEBUG_PRINTF_P(PSTR(\"PIN DEALLOC: FAIL GPIO %d allocated by 0x%02X, but attempted de-allocation by 0x%02X.\\n\"), gpio, static_cast<int>(ownerTag[gpio]), static_cast<int>(tag));\n    return false;\n  }\n\n  bitWrite(pinAlloc, gpio, false);\n  ownerTag[gpio] = PinOwner::None;\n  return true;\n}\n\n// support function for deallocating multiple pins\nbool PinManager::deallocateMultiplePins(const uint8_t *pinArray, byte arrayElementCount, PinOwner tag)\n{\n  bool shouldFail = false;\n  DEBUG_PRINTLN(F(\"MULTIPIN DEALLOC\"));\n  // first verify the pins are OK and allocated by selected owner\n  for (int i = 0; i < arrayElementCount; i++) {\n    byte gpio = pinArray[i];\n    if (gpio == 0xFF) {\n      // explicit support for io -1 as a no-op (no allocation of pin),\n      // as this can greatly simplify configuration arrays\n      continue;\n    }\n    if (isPinAllocated(gpio, tag)) {\n      // if the current pin is allocated by selected owner it is possible to release it\n      continue;\n    }\n    DEBUG_PRINTF_P(PSTR(\"PIN DEALLOC: FAIL GPIO %d allocated by 0x%02X, but attempted de-allocation by 0x%02X.\\n\"), gpio, static_cast<int>(ownerTag[gpio]), static_cast<int>(tag));\n    shouldFail = true;\n  }\n  if (shouldFail) {\n    return false; // no pins deallocated\n  }\n  if (tag==PinOwner::HW_I2C) {\n    if (i2cAllocCount && --i2cAllocCount>0) {\n      // no deallocation done until last owner releases pins\n      return true;\n    }\n  }\n  if (tag==PinOwner::HW_SPI) {\n    if (spiAllocCount && --spiAllocCount>0) {\n      // no deallocation done until last owner releases pins\n      return true;\n    }\n  }\n  for (int i = 0; i < arrayElementCount; i++) {\n    deallocatePin(pinArray[i], tag);\n  }\n  return true;\n}\n\nbool PinManager::deallocateMultiplePins(const managed_pin_type * mptArray, byte arrayElementCount, PinOwner tag)\n{\n  uint8_t pins[arrayElementCount];\n  for (int i=0; i<arrayElementCount; i++) pins[i] = mptArray[i].pin;\n  return deallocateMultiplePins(pins, arrayElementCount, tag);\n}\n\nbool PinManager::allocateMultiplePins(const managed_pin_type * mptArray, byte arrayElementCount, PinOwner tag)\n{\n  bool shouldFail = false;\n  // first verify the pins are OK and not already allocated\n  for (int i = 0; i < arrayElementCount; i++) {\n    byte gpio = mptArray[i].pin;\n    if (gpio == 0xFF) {\n      // explicit support for io -1 as a no-op (no allocation of pin),\n      // as this can greatly simplify configuration arrays\n      continue;\n    }\n    // allow any GPIO for Ethernet (compile time assigned)\n    if (!(isPinOk(gpio, mptArray[i].isOutput) || tag==PinOwner::Ethernet)) {\n      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\"));\n      shouldFail = true;\n    }\n    if ((tag==PinOwner::HW_I2C || tag==PinOwner::HW_SPI) && isPinAllocated(gpio, tag)) {\n      // allow multiple \"allocations\" of HW I2C & SPI bus pins\n      continue;\n    } else if (isPinAllocated(gpio)) {\n      DEBUG_PRINTF_P(PSTR(\"PIN ALLOC: FAIL GPIO %d already allocated by 0x%02X.\\n\"), gpio, static_cast<int>(ownerTag[gpio]));\n      shouldFail = true;\n    }\n  }\n  if (shouldFail) {\n    return false;\n  }\n\n  if (tag==PinOwner::HW_I2C) i2cAllocCount++;\n  if (tag==PinOwner::HW_SPI) spiAllocCount++;\n\n  // all pins are available .. track each one\n  for (int i = 0; i < arrayElementCount; i++) {\n    byte gpio = mptArray[i].pin;\n    if (gpio == 0xFF) {\n      // allow callers to include -1 value as non-requested pin\n      // as this can greatly simplify configuration arrays\n      continue;\n    }\n    if (gpio >= WLED_NUM_PINS)\n      continue; // other unexpected GPIO => avoid array bounds violation\n\n    bitWrite(pinAlloc, gpio, true);\n    ownerTag[gpio] = tag;\n    DEBUG_PRINTF_P(PSTR(\"PIN ALLOC: Pin %d allocated by 0x%02X.\\n\"), gpio, static_cast<int>(tag));\n  }\n  DEBUG_PRINTF_P(PSTR(\"PIN ALLOC: 0x%014llX.\\n\"), (unsigned long long)pinAlloc);\n  return true;\n}\n\nbool PinManager::allocateMultiplePins(const int8_t * mptArray, byte arrayElementCount, PinOwner tag, boolean output) {\n  PinManagerPinType pins[arrayElementCount];\n  for (int i=0; i<arrayElementCount; i++) pins[i] = {mptArray[i], output};\n  return allocateMultiplePins(pins, arrayElementCount, tag);\n}\n\nbool PinManager::allocatePin(byte gpio, bool output, PinOwner tag)\n{\n  // HW I2C & SPI pins have to be allocated using allocateMultiplePins variant since there is always SCL/SDA pair\n  // DMX_INPUT pins have to be allocated using allocateMultiplePins variant since there is always RX/TX/EN triple\n  if (!isPinOk(gpio, output) || (gpio >= WLED_NUM_PINS) || tag==PinOwner::HW_I2C || tag==PinOwner::HW_SPI\n      || tag==PinOwner::DMX_INPUT) {\n    #ifdef WLED_DEBUG\n    if (gpio < 255) {  // 255 (-1) is the \"not defined GPIO\"\n      if (!isPinOk(gpio, output)) {\n        DEBUG_PRINTF_P(PSTR(\"PIN ALLOC: FAIL for owner 0x%02X: GPIO %d \"), static_cast<int>(tag), gpio);\n        if (output) DEBUG_PRINTLN(F(\" cannot be used for i/o on this MCU.\"));\n        else DEBUG_PRINTLN(F(\" cannot be used as input on this MCU.\"));\n      } else {\n        DEBUG_PRINTF_P(PSTR(\"PIN ALLOC: FAIL GPIO %d - HW I2C & SPI pins have to be allocated using allocateMultiplePins.\\n\"), gpio);\n      }\n    }\n    #endif\n    return false;\n  }\n  if (isPinAllocated(gpio)) {\n    DEBUG_PRINTF_P(PSTR(\"PIN ALLOC: FAIL Pin %d already allocated by 0x%02X.\\n\"), gpio, static_cast<int>(ownerTag[gpio]));\n    return false;\n  }\n\n  bitWrite(pinAlloc, gpio, true);\n  ownerTag[gpio] = tag;\n  DEBUG_PRINTF_P(PSTR(\"PIN ALLOC: Pin %d successfully allocated by 0x%02X.\\n\"), gpio, static_cast<int>(ownerTag[gpio]));\n  DEBUG_PRINTF_P(PSTR(\"PIN ALLOC: 0x%014llX.\\n\"), (unsigned long long)pinAlloc);\n\n  return true;\n}\n\n// if tag is set to PinOwner::None, checks for ANY owner of the pin.\n// if tag is set to any other value, checks if that tag is the current owner of the pin.\nbool PinManager::isPinAllocated(byte gpio, PinOwner tag)\n{\n  if (!isPinOk(gpio, false)) return true;\n  if ((tag != PinOwner::None) && (ownerTag[gpio] != tag)) return false;\n  return bitRead(pinAlloc, gpio);\n}\n\n/* see https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/gpio.html\n * The ESP32-S3 chip features 45 physical GPIO pins (GPIO0 ~ GPIO21 and GPIO26 ~ GPIO48). Each pin can be used as a general-purpose I/O\n * Strapping pins: GPIO0, GPIO3, GPIO45 and GPIO46 are strapping pins. For more infomation, please refer to ESP32-S3 datasheet.\n * Serial TX = GPIO43, RX = GPIO44; LED BUILTIN is usually GPIO39\n * USB-JTAG: GPIO 19 and 20 are used by USB-JTAG by default. In order to use them as GPIOs, USB-JTAG will be disabled by the drivers.\n * SPI0/1: GPIO26-32 are usually used for SPI flash and PSRAM and not recommended for other uses.\n * When using Octal Flash or Octal PSRAM or both, GPIO33~37 are connected to SPIIO4 ~ SPIIO7 and SPIDQS. Therefore, on boards embedded with ESP32-S3R8 / ESP32-S3R8V chip, GPIO33~37 are also not recommended for other uses.\n *\n * see https://docs.espressif.com/projects/esp-idf/en/v4.4.2/esp32s3/api-reference/peripherals/adc.html\n *     https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/adc_oneshot.html\n * ADC1: GPIO1  - GPIO10 (channel 0..9)\n * ADC2: GPIO11 - GPIO20 (channel 0..9)\n * adc_power_acquire(): Please do not use the interrupt of GPIO36 and GPIO39 when using ADC or Wi-Fi and Bluetooth with sleep mode enabled. As a workaround, call adc_power_acquire() in the APP.\n * Since the ADC2 module is also used by the Wi-Fi, reading operation of adc2_get_raw() may fail between esp_wifi_start() and esp_wifi_stop(). Use the return code to see whether the reading is successful.\n */\n\n// Check if supplied GPIO is ok to use\nbool PinManager::isPinOk(byte gpio, bool output)\n{\n  if (gpio >= WLED_NUM_PINS) return false;     // catch error case, to avoid array out-of-bounds access\n#ifdef ARDUINO_ARCH_ESP32\n  if (digitalPinIsValid(gpio)) {\n  #if defined(CONFIG_IDF_TARGET_ESP32C3)\n    // strapping pins: 2, 8, & 9\n    if (gpio > 11 && gpio < 18) return false;     // 11-17 SPI FLASH\n    #if ARDUINO_USB_CDC_ON_BOOT == 1 || ARDUINO_USB_DFU_ON_BOOT == 1\n    if (gpio > 17 && gpio < 20) return false;     // 18-19 USB-JTAG\n    #endif\n  #elif defined(CONFIG_IDF_TARGET_ESP32S3)\n    // 00 to 18 are for general use. Be careful about straping pins GPIO0 and GPIO3 - these may be pulled-up or pulled-down on your board.\n    #if ARDUINO_USB_CDC_ON_BOOT == 1 || ARDUINO_USB_DFU_ON_BOOT == 1\n    if (gpio > 18 && gpio < 21) return false;     // 19 + 20 = USB-JTAG. Not recommended for other uses.\n    #endif\n    if (gpio > 21 && gpio < 33) return false;     // 22 to 32: not connected + SPI FLASH\n    #if CONFIG_ESPTOOLPY_FLASHMODE_OPI            // 33-37: never available if using _octal_ Flash (opi_opi)\n    if (gpio > 32 && gpio < 38) return false;\n    #endif\n    #if CONFIG_SPIRAM_MODE_OCT                    // 33-37: not available if using _octal_ PSRAM (qio_opi), but free to use on _quad_ PSRAM (qio_qspi)\n    if (gpio > 32 && gpio < 38) return !psramFound();\n    #endif\n    // 38 to 48 are for general use. Be careful about straping pins GPIO45 and GPIO46 - these may be pull-up or pulled-down on your board.\n  #elif defined(CONFIG_IDF_TARGET_ESP32S2)\n    // strapping pins: 0, 45 & 46\n    if (gpio > 21 && gpio < 33) return false;     // 22 to 32: not connected + SPI FLASH\n    // JTAG: GPIO39-42 are usually used for inline debugging\n    // GPIO46 is input only and pulled down\n  #else\n\n    if ((strncmp_P(PSTR(\"ESP32-U4WDH\"), ESP.getChipModel(), 11) == 0) ||    // this is the correct identifier, but....\n        (strncmp_P(PSTR(\"ESP32-PICO-D\"), ESP.getChipModel(), 12) == 0)) {   // https://github.com/espressif/arduino-esp32/issues/10683\n      // this chip has 4 MB of internal Flash and different packaging, so available pins are different!\n      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\n      if (gpio == 16 || gpio == 17) return false;                           // U4WDH/PICO-D?: GPIO 16 and 17 are used for PSRAM\n    } else if (strncmp_P(PSTR(\"ESP32-PICO-V3\"), ESP.getChipModel(), 13) == 0) {\n      if (gpio == 6 || gpio == 11) return false;                            // PICO-V3: uses GPIO 6 and 11 for flash\n      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\n    } else {\n      // for classic ESP32 (non-mini) modules, these are the SPI flash pins\n      if (gpio > 5 && gpio < 12) return false;      //SPI flash pins\n    }\n    if (gpio == 16) return !psramFound(); // PSRAM pins on modules with off-package or in-package PSRAM\n    if (gpio == 17) {\n      if (strncmp_P(PSTR(\"ESP32-D0WDR2-V3\"), ESP.getChipModel(), 15) == 0) {\n        return true;\n      } else {\n        return !psramFound(); // PSRAM pins on modules with in-package PSRAM\n      }\n    }    \n  #endif\n    if (output) return digitalPinCanOutput(gpio);\n    else        return true;\n  }\n#else\n  if (gpio <  6) return true;\n  if (gpio < 12) return false; //SPI flash pins\n  if (gpio < 17) return true;\n#endif\n  return false;\n}\n\nbool PinManager::isReadOnlyPin(byte gpio)\n{\n#ifdef ARDUINO_ARCH_ESP32\n  if (gpio < WLED_NUM_PINS) return (digitalPinIsValid(gpio) && !digitalPinCanOutput(gpio));\n#endif\n  return false;\n}\n\nPinOwner PinManager::getPinOwner(byte gpio)\n{\n  if (!isPinOk(gpio, false)) return PinOwner::None;\n  return ownerTag[gpio];\n}\n\n#ifdef ARDUINO_ARCH_ESP32\nbyte PinManager::allocateLedc(byte channels)\n{\n  if (channels > WLED_MAX_ANALOG_CHANNELS || channels == 0) return 255;\n  unsigned ca = 0;\n  for (unsigned i = 0; i < WLED_MAX_ANALOG_CHANNELS; i++) {\n    if (bitRead(ledcAlloc, i)) { //found occupied pin\n      ca = 0;\n    } else {\n      // if we have PWM CCT bus allocation (2 channels) we need to make sure both channels share the same timer\n      // for phase shifting purposes (otherwise phase shifts may not be accurate)\n      if (channels == 2) { // will skip odd channel for first channel for phase shifting\n        if (ca == 0 && i % 2 == 0) ca++;  // even LEDC channels is 1st PWM channel\n        if (ca == 1 && i % 2 == 1) ca++;  // odd LEDC channel is 2nd PWM channel\n      } else\n        ca++;\n    }\n    if (ca >= channels) { //enough free channels\n      unsigned in = (i + 1) - ca;\n      for (unsigned j = 0; j < ca; j++) {\n        bitWrite(ledcAlloc, in+j, true);\n      }\n      return in;\n    }\n  }\n  return 255; //not enough consecutive free LEDC channels\n}\n\nvoid PinManager::deallocateLedc(byte pos, byte channels)\n{\n  for (unsigned j = pos; j < pos + channels && j < WLED_MAX_ANALOG_CHANNELS; j++) {\n    bitWrite(ledcAlloc, j, false);\n  }\n}\n#endif\n\n// Convert PinOwner enum to string for allocated pins\nconst char* PinManager::getPinOwnerName(uint8_t gpio) {\n  PinOwner owner = PinManager::getPinOwner(gpio); // returns \"none\" if allocated by system, unallocated or unavailable\n  switch (owner) {\n    case PinOwner::None:          return PinManager::isPinAllocated(gpio) ? \"System\" : \"Unknown\";\n    case PinOwner::Ethernet:      return \"Ethernet\";\n    case PinOwner::BusDigital:    return \"LED Digital\";\n    case PinOwner::BusOnOff:      return \"LED On/Off\";\n    case PinOwner::BusPwm:        return \"LED PWM\";\n    case PinOwner::Button:        return \"Button\";\n    case PinOwner::IR:            return \"IR Receiver\";\n    case PinOwner::Relay:         return \"Relay\";\n    case PinOwner::SPI_RAM:       return \"SPI RAM\";\n    case PinOwner::DebugOut:      return \"Debug\";\n    case PinOwner::DMX:           return \"DMX Output\";\n    case PinOwner::HW_I2C:        return \"I2C\";\n    case PinOwner::HW_SPI:        return \"SPI\";\n    case PinOwner::DMX_INPUT:     return \"DMX Input\";\n    case PinOwner::HUB75:         return \"HUB75\";\n    // Usermods - return generic name for now\n    // TODO: Get actual usermod name from UsermodManager\n    default:\n      // Check if it's a usermod (high bit not set)\n      if (static_cast<uint8_t>(owner) > 0 && !(static_cast<uint8_t>(owner) & 0x80)) {\n        return \"Usermod\";\n      }\n      return \"Unknown\";\n  }\n}\n\nint PinManager::getButtonIndex(byte gpio) {\n  for (size_t b = 0; b < buttons.size(); b++) {\n    if (buttons[b].pin == gpio && buttons[b].type != BTN_TYPE_NONE) {\n      return b;\n    }\n  }\n  return -1;\n}\n\nbool PinManager::isAnalogPin(byte gpio) {\n  #ifdef ARDUINO_ARCH_ESP32\n  // Check ADC capability: only ADC1 channels can be used (ADC2 channels are not usable when WiFi is active)\n  #if CONFIG_IDF_TARGET_ESP32\n  // ESP32: ADC1 channels 0-7 (GPIO 36, 37, 38, 39, 32, 33, 34, 35)\n  int adc_channel = digitalPinToAnalogChannel(gpio);\n  if (adc_channel >= 0 && adc_channel <= 7) return true;\n  #elif CONFIG_IDF_TARGET_ESP32S2\n  // ESP32-S2: ADC1 channels 0-9 (GPIO 1-10)\n  int adc_channel = digitalPinToAnalogChannel(gpio);\n  if (adc_channel >= 0 && adc_channel <= 9) return true;\n  #elif CONFIG_IDF_TARGET_ESP32S3\n  // ESP32-S3: ADC1 channels 0-9 (GPIO 1-10)\n  int adc_channel = digitalPinToAnalogChannel(gpio);\n  if (adc_channel >= 0 && adc_channel <= 9) return true;\n  #elif CONFIG_IDF_TARGET_ESP32C3\n  // ESP32-C3: ADC1 channels 0-4 (GPIO 0-4)\n  int adc_channel = digitalPinToAnalogChannel(gpio);\n  if (adc_channel >= 0 && adc_channel <= 4) return true;\n  #endif\n  #endif\n  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\n}\n"
  },
  {
    "path": "wled00/pin_manager.h",
    "content": "#ifndef WLED_PIN_MANAGER_H\n#define WLED_PIN_MANAGER_H\n/*\n * Registers pins so there is no attempt for two interfaces to use the same pin\n */\n\n#ifdef ESP8266\n#define WLED_NUM_PINS (GPIO_PIN_COUNT+1) // somehow they forgot GPIO 16 (0-16==17)\n#else\n#define WLED_NUM_PINS (GPIO_PIN_COUNT)\n#endif\n\n// Pin capability flags - only \"special\" capabilities useful for debugging (note: touch capability is provided by appendGPIOinfo() via d.touch)\n#define PIN_CAP_ADC          0x02   // has ADC capability (analog input)\n#define PIN_CAP_PWM          0x04   // can be used for PWM (analog LED output) -> unused, all pins can use ledc PWM\n#define PIN_CAP_BOOT         0x08   // bootloader pin\n#define PIN_CAP_BOOTSTRAP    0x10   // bootstrap pin (strapping pin affecting boot mode)\n#define PIN_CAP_INPUT_ONLY   0x20   // input only pin (cannot be used as output)\n\ntypedef struct PinManagerPinType {\n  int8_t pin;\n  bool   isOutput;\n} managed_pin_type;\n\n/*\n * Allows PinManager to \"lock\" an allocation to a specific\n * owner, so someone else doesn't accidentally de-allocate\n * a pin it hasn't allocated.  Also enhances debugging.\n *\n * RAM Cost:\n *     17 bytes on ESP8266\n *     40 bytes on ESP32\n */\nenum struct PinOwner : uint8_t {\n  None          = 0,      // default == legacy == unspecified owner\n  // High bit is set for all built-in pin owners\n  Ethernet      = 0x81,\n  BusDigital    = 0x82,\n  BusOnOff      = 0x83,\n  BusPwm        = 0x84,   // 'BusP'      == PWM output using BusPwm\n  Button        = 0x85,   // 'Butn'      == button from configuration\n  IR            = 0x86,   // 'IR'        == IR receiver pin from configuration\n  Relay         = 0x87,   // 'Rly'       == Relay pin from configuration\n  SPI_RAM       = 0x88,   // 'SpiR'      == SPI RAM\n  DebugOut      = 0x89,   // 'Dbg'       == debug output always IO1\n  DMX           = 0x8A,   // 'DMX'       == hard-coded to IO2\n  HW_I2C        = 0x8B,   // 'I2C'       == hardware I2C pins (4&5 on ESP8266, 21&22 on ESP32)\n  HW_SPI        = 0x8C,   // 'SPI'       == hardware (V)SPI pins (13,14&15 on ESP8266, 5,18&23 on ESP32)\n  DMX_INPUT     = 0x8D,   // 'DMX_INPUT' == DMX input via serial\n  HUB75         = 0x8E,   // 'Hub75' == Hub75 driver\n  // Use UserMod IDs from const.h here\n  UM_Unspecified       = USERMOD_ID_UNSPECIFIED,        // 0x01\n  UM_Example           = USERMOD_ID_EXAMPLE,            // 0x02 // Usermod \"usermod_v2_example.h\"\n  UM_Temperature       = USERMOD_ID_TEMPERATURE,        // 0x03 // Usermod \"usermod_temperature.h\"\n  // #define USERMOD_ID_FIXNETSERVICES                  // 0x04 // Usermod \"usermod_Fix_unreachable_netservices.h\" -- Does not allocate pins\n  UM_PIR               = USERMOD_ID_PIRSWITCH,          // 0x05 // Usermod \"usermod_PIR_sensor_switch.h\"\n  UM_IMU               = USERMOD_ID_IMU,                // 0x06 // Usermod \"usermod_mpu6050_imu.h\" -- Interrupt pin\n  UM_FourLineDisplay   = USERMOD_ID_FOUR_LINE_DISP,     // 0x07 // Usermod \"usermod_v2_four_line_display.h -- May use \"standard\" HW_I2C pins\n  UM_RotaryEncoderUI   = USERMOD_ID_ROTARY_ENC_UI,      // 0x08 // Usermod \"usermod_v2_rotary_encoder_ui.h\"\n  // #define USERMOD_ID_AUTO_SAVE                       // 0x09 // Usermod \"usermod_v2_auto_save.h\" -- Does not allocate pins\n  // #define USERMOD_ID_DHT                             // 0x0A // Usermod \"usermod_dht.h\" -- Statically allocates pins, not compatible with pinManager?\n  // #define USERMOD_ID_VL53L0X                         // 0x0C // Usermod \"usermod_vl53l0x_gestures.h\" -- Uses \"standard\" HW_I2C pins\n  UM_MultiRelay        = USERMOD_ID_MULTI_RELAY,        // 0x0D // Usermod \"usermod_multi_relay.h\"\n  UM_AnimatedStaircase = USERMOD_ID_ANIMATED_STAIRCASE, // 0x0E // Usermod \"Animated_Staircase.h\"\n  UM_Battery           = USERMOD_ID_BATTERY,            //\n  // #define USERMOD_ID_RTC                             // 0x0F // Usermod \"usermod_rtc.h\" -- Uses \"standard\" HW_I2C pins\n  // #define USERMOD_ID_ELEKSTUBE_IPS                   // 0x10 // Usermod \"usermod_elekstube_ips.h\" -- Uses quite a few pins ... see Hardware.h and User_Setup.h\n  // #define USERMOD_ID_SN_PHOTORESISTOR                // 0x11 // Usermod \"usermod_sn_photoresistor.h\" -- Uses hard-coded pin (PHOTORESISTOR_PIN == A0), but could be easily updated to use pinManager\n  UM_BH1750            = USERMOD_ID_BH1750,             // 0x14 // Usermod \"bh1750.h -- Uses \"standard\" HW_I2C pins\n  UM_RGBRotaryEncoder  = USERMOD_RGB_ROTARY_ENCODER,    // 0x16 // Usermod \"rgb-rotary-encoder.h\"\n  UM_QuinLEDAnPenta    = USERMOD_ID_QUINLED_AN_PENTA,   // 0x17 // Usermod \"quinled-an-penta.h\"\n  UM_BME280            = USERMOD_ID_BME280,             // 0x1E // Usermod \"usermod_bme280.h -- Uses \"standard\" HW_I2C pins\n  UM_Audioreactive     = USERMOD_ID_AUDIOREACTIVE,      // 0x20 // Usermod \"audio_reactive.h\"\n  UM_SdCard            = USERMOD_ID_SD_CARD,            // 0x25 // Usermod \"usermod_sd_card.h\"\n  UM_PWM_OUTPUTS       = USERMOD_ID_PWM_OUTPUTS,        // 0x26 // Usermod \"usermod_pwm_outputs.h\"\n  UM_LDR_DUSK_DAWN     = USERMOD_ID_LDR_DUSK_DAWN,      // 0x2B // Usermod \"usermod_LDR_Dusk_Dawn_v2.h\"\n  UM_MAX17048          = USERMOD_ID_MAX17048,           // 0x2F // Usermod \"usermod_max17048.h\"\n  UM_BME68X            = USERMOD_ID_BME68X,             // 0x31 // Usermod \"usermod_bme68x.h -- Uses \"standard\" HW_I2C pins\n  UM_PIXELS_DICE_TRAY  = USERMOD_ID_PIXELS_DICE_TRAY    // 0x35 // Usermod \"pixels_dice_tray.h\" -- Needs compile time specified 6 pins for display including SPI.\n};\nstatic_assert(0u == static_cast<uint8_t>(PinOwner::None), \"PinOwner::None must be zero, so default array initialization works as expected\");\n\nnamespace PinManager {\n  // De-allocates a single pin\n  bool deallocatePin(byte gpio, PinOwner tag);\n  // De-allocates multiple pins but only if all can be deallocated (PinOwner has to be specified)\n  bool deallocateMultiplePins(const uint8_t *pinArray, byte arrayElementCount, PinOwner tag);\n  bool deallocateMultiplePins(const managed_pin_type *pinArray, byte arrayElementCount, PinOwner tag);\n  // Allocates a single pin, with an owner tag.\n  // De-allocation requires the same owner tag (or override)\n  bool allocatePin(byte gpio, bool output, PinOwner tag);\n  // Allocates all the pins, or allocates none of the pins, with owner tag.\n  // Provided to simplify error condition handling in clients\n  // using more than one pin, such as I2C, SPI, rotary encoders,\n  // ethernet, etc..\n  bool allocateMultiplePins(const managed_pin_type * mptArray, byte arrayElementCount, PinOwner tag );\n  bool allocateMultiplePins(const int8_t * mptArray, byte arrayElementCount, PinOwner tag, boolean output);\n\n  [[deprecated(\"Replaced by three-parameter allocatePin(gpio, output, ownerTag), for improved debugging\")]]\n  inline bool allocatePin(byte gpio, bool output = true) { return allocatePin(gpio, output, PinOwner::None); }\n  [[deprecated(\"Replaced by two-parameter deallocatePin(gpio, ownerTag), for improved debugging\")]]\n  inline void deallocatePin(byte gpio) { deallocatePin(gpio, PinOwner::None); }\n\n  // will return true for reserved pins\n  bool isPinAllocated(byte gpio, PinOwner tag = PinOwner::None);\n  // will return false for reserved pins\n  bool isPinOk(byte gpio, bool output = true);\n\n  bool isReadOnlyPin(byte gpio);\n  int getButtonIndex(byte gpio); // returns button index if pin is used for button, otherwise -1\n  bool isAnalogPin(byte gpio); // returns true if pin has ADC capability, otherwise false\n\n  PinOwner getPinOwner(byte gpio);\n  const char* getPinOwnerName(uint8_t gpio);\n\n  #ifdef ARDUINO_ARCH_ESP32\n  byte allocateLedc(byte channels);\n  void deallocateLedc(byte pos, byte channels);\n  #endif\n};\n\n//extern PinManager pinManager;\n#endif\n"
  },
  {
    "path": "wled00/playlist.cpp",
    "content": "#include \"wled.h\"\n\n/*\n * Handles playlists, timed sequences of presets\n */\n\ntypedef struct PlaylistEntry {\n  uint8_t preset; //ID of the preset to apply\n  uint16_t dur;   //Duration of the entry (in tenths of seconds)\n  uint16_t tr;    //Duration of the transition TO this entry (in tenths of seconds)\n} ple;\n\nstatic byte           playlistRepeat = 1;        //how many times to repeat the playlist (0 = infinitely)\nstatic byte           playlistEndPreset = 0;     //what preset to apply after playlist end (0 = stay on last preset)\nstatic byte           playlistOptions = 0;       //bit 0: shuffle playlist after each iteration. bits 1-7 TBD\n\nstatic PlaylistEntry *playlistEntries = nullptr;\nstatic byte           playlistLen;               //number of playlist entries\nstatic int8_t         playlistIndex = -1;\nstatic uint16_t       playlistEntryDur = 0;      //duration of the current entry in tenths of seconds\n\n//values we need to keep about the parent playlist while inside sub-playlist\nstatic int16_t        parentPlaylistIndex = -1;\nstatic byte           parentPlaylistRepeat = 0;\nstatic byte           parentPlaylistPresetId = 0; //for re-loading\n\n\nvoid shufflePlaylist() {\n  int currentIndex = playlistLen;\n  PlaylistEntry temporaryValue;\n\n  // While there remain elements to shuffle...\n  while (currentIndex--) {\n    // Pick a random element...\n    int randomIndex = random(0, currentIndex);\n    // And swap it with the current element.\n    temporaryValue = playlistEntries[currentIndex];\n    playlistEntries[currentIndex] = playlistEntries[randomIndex];\n    playlistEntries[randomIndex] = temporaryValue;\n  }\n  DEBUG_PRINTLN(F(\"Playlist shuffle.\"));\n}\n\n\nvoid unloadPlaylist() {\n  if (playlistEntries != nullptr) {\n    delete[] playlistEntries;\n    playlistEntries = nullptr;\n  }\n  currentPlaylist = playlistIndex = -1;\n  playlistLen = playlistEntryDur = playlistOptions = 0;\n  DEBUG_PRINTLN(F(\"Playlist unloaded.\"));\n}\n\n\nint16_t loadPlaylist(JsonObject playlistObj, byte presetId) {\n  if (currentPlaylist > 0 && parentPlaylistPresetId > 0) return -1; // we are already in nested playlist, do nothing\n  if (currentPlaylist > 0) {\n    parentPlaylistIndex = playlistIndex;\n    parentPlaylistRepeat = playlistRepeat;\n    parentPlaylistPresetId = currentPlaylist;\n  }\n  unloadPlaylist();\n\n  JsonArray presets = playlistObj[\"ps\"];\n  playlistLen = presets.size();\n  if (playlistLen == 0) return -1;\n  if (playlistLen > 100) playlistLen = 100;\n\n  playlistEntries = new(std::nothrow) PlaylistEntry[playlistLen];\n  if (playlistEntries == nullptr) return -1;\n\n  byte it = 0;\n  for (int ps : presets) {\n    if (it >= playlistLen) break;\n    playlistEntries[it].preset = ps;\n    it++;\n  }\n\n  it = 0;\n  JsonArray durations = playlistObj[\"dur\"];\n  if (durations.isNull()) {\n    playlistEntries[0].dur = playlistObj[\"dur\"] | 100; //10 seconds as fallback\n    it = 1;\n  } else {\n    for (int dur : durations) {\n      if (it >= playlistLen) break;\n      playlistEntries[it].dur = constrain(dur, 0, 65530);\n      it++;\n    }\n  }\n  if (it > 0) // should never happen but just in case\n    for (int i = it; i < playlistLen; i++) playlistEntries[i].dur = playlistEntries[it -1].dur;\n\n  it = 0;\n  JsonArray tr = playlistObj[F(\"transition\")];\n  if (tr.isNull()) {\n    playlistEntries[0].tr = playlistObj[F(\"transition\")] | (transitionDelay / 100);\n    it = 1;\n  } else {\n    for (int transition : tr) {\n      if (it >= playlistLen) break;\n      playlistEntries[it].tr = transition;\n      it++;\n    }\n  }\n  for (int i = it; i < playlistLen; i++) playlistEntries[i].tr = playlistEntries[it -1].tr;\n\n  int rep = playlistObj[F(\"repeat\")];\n  bool shuffle = false;\n  if (rep < 0) { //support negative values as infinite + shuffle\n    rep = 0; shuffle = true;\n  }\n\n  playlistRepeat = rep;\n  if (playlistRepeat > 0) playlistRepeat++; //add one extra repetition immediately since it will be deducted on first start\n  playlistEndPreset = playlistObj[\"end\"] | 0;\n  // if end preset is 255 restore original preset (if any running) upon playlist end\n  if (playlistEndPreset == 255 && currentPreset > 0) {\n    playlistEndPreset = currentPreset;\n    playlistOptions |= PL_OPTION_RESTORE; // for async save operation\n  }\n  if (playlistEndPreset > 250) playlistEndPreset = 0;\n  shuffle = shuffle || playlistObj[\"r\"];\n  if (shuffle) playlistOptions |= PL_OPTION_SHUFFLE;\n\n  if (parentPlaylistPresetId == 0 && parentPlaylistIndex > -1) {\n    // we are re-loading playlist when returning from nested playlist\n    playlistIndex = parentPlaylistIndex;\n    playlistRepeat = parentPlaylistRepeat;\n    parentPlaylistIndex = -1;\n    parentPlaylistRepeat = 0;\n  } else if (rep == 0) {\n    // endless playlist will never return to parent so erase parent information if it was called from it\n    parentPlaylistPresetId = 0;\n    parentPlaylistIndex = -1;\n    parentPlaylistRepeat = 0;\n  }\n\n  currentPlaylist = presetId;\n  DEBUG_PRINTLN(F(\"Playlist loaded.\"));\n  return currentPlaylist;\n}\n\n\nvoid handlePlaylist() {\n  static unsigned long presetCycledTime = 0;\n  if (currentPlaylist < 0 || playlistEntries == nullptr) return;\n\n  if ((playlistEntryDur < UINT16_MAX && millis() - presetCycledTime > 100 * playlistEntryDur) || doAdvancePlaylist) {\n    presetCycledTime = millis();\n    if (bri == 0 || nightlightActive) return;\n\n    ++playlistIndex %= playlistLen; // -1 at 1st run (limit to playlistLen)\n\n    // playlist roll-over\n    if (!playlistIndex) {\n      if (playlistRepeat == 1) { //stop if all repetitions are done\n        unloadPlaylist();\n        if (parentPlaylistPresetId > 0) {\n          applyPresetFromPlaylist(parentPlaylistPresetId); // reload previous playlist (unfortunately asynchronous)\n          parentPlaylistPresetId = 0; // reset previous playlist but do not reset Index or Repeat (they will be loaded & reset in loadPlaylist())\n        } else if (playlistEndPreset) applyPresetFromPlaylist(playlistEndPreset);\n        return;\n      }\n      if (playlistRepeat > 1) playlistRepeat--; // decrease repeat count on each index reset if not an endless playlist\n      // playlistRepeat == 0: endless loop\n      if (playlistOptions & PL_OPTION_SHUFFLE) shufflePlaylist(); // shuffle playlist and start over\n    }\n\n    jsonTransitionOnce = true;\n    strip.setTransition(playlistEntries[playlistIndex].tr * 100);\n    playlistEntryDur = playlistEntries[playlistIndex].dur > 0 ? playlistEntries[playlistIndex].dur : UINT16_MAX;\n    applyPresetFromPlaylist(playlistEntries[playlistIndex].preset);\n    doAdvancePlaylist = false;\n  }\n}\n\n\nvoid serializePlaylist(JsonObject sObj) {\n  JsonObject playlist = sObj.createNestedObject(F(\"playlist\"));\n  JsonArray ps = playlist.createNestedArray(\"ps\");\n  JsonArray dur = playlist.createNestedArray(\"dur\");\n  JsonArray transition = playlist.createNestedArray(F(\"transition\"));\n  playlist[F(\"repeat\")] = (playlistIndex < 0 && playlistRepeat > 0) ? playlistRepeat - 1 : playlistRepeat; // remove added repetition count (if not yet running)\n  playlist[\"end\"] = playlistOptions & PL_OPTION_RESTORE ? 255 : playlistEndPreset;\n  playlist[\"r\"] = playlistOptions & PL_OPTION_SHUFFLE;\n  for (int i=0; i<playlistLen; i++) {\n    ps.add(playlistEntries[i].preset);\n    dur.add(playlistEntries[i].dur);\n    transition.add(playlistEntries[i].tr);\n  }\n}\n"
  },
  {
    "path": "wled00/presets.cpp",
    "content": "#include \"wled.h\"\n\n/*\n * Methods to handle saving and loading presets to/from the filesystem\n */\n\n#ifdef ARDUINO_ARCH_ESP32\nstatic char *tmpRAMbuffer = nullptr;\n#endif\n\nstatic volatile byte presetToApply = 0;\nstatic volatile byte callModeToApply = 0;\nstatic volatile byte presetToSave = 0;\nstatic volatile int8_t saveLedmap = -1;\nstatic char *quickLoad = nullptr;\nstatic char *saveName = nullptr;\nstatic bool includeBri = true, segBounds = true, selectedOnly = false, playlistSave = false;;\n\nstatic const char presets_json[] PROGMEM = \"/presets.json\";\nstatic const char tmp_json[] PROGMEM = \"/tmp.json\";\nconst char *getPresetsFileName(bool persistent) {\n  return persistent ? presets_json : tmp_json;\n}\n\nbool presetNeedsSaving() {\n  return presetToSave;\n}\n\nstatic void doSaveState() {\n  bool persist = (presetToSave < 251);\n\n  unsigned long maxWait = millis() + strip.getFrameTime();\n  while (strip.isUpdating() && millis() < maxWait) delay(1); // wait for strip to finish updating, accessing FS during sendout causes glitches\n\n  if (!requestJSONBufferLock(JSON_LOCK_PRESET_SAVE)) return;\n\n  initPresetsFile(); // just in case if someone deleted presets.json using /edit\n  JsonObject sObj = pDoc->to<JsonObject>();\n\n  DEBUG_PRINTLN(F(\"Serialize current state\"));\n  if (playlistSave) {\n    serializePlaylist(sObj);\n    if (includeBri) sObj[\"on\"] = true;\n  } else {\n    serializeState(sObj, true, includeBri, segBounds, selectedOnly);\n  }\n  if (saveName) sObj[\"n\"] = saveName;\n  else          sObj[\"n\"] = F(\"Unkonwn preset\"); // should not happen, but just in case...\n  if (quickLoad && quickLoad[0]) sObj[F(\"ql\")] = quickLoad;\n  if (saveLedmap >= 0) sObj[F(\"ledmap\")] = saveLedmap;\n/*\n  #ifdef WLED_DEBUG\n    DEBUG_PRINTLN(F(\"Serialized preset\"));\n    serializeJson(*pDoc,Serial);\n    DEBUG_PRINTLN();\n  #endif\n*/\n  #if defined(ARDUINO_ARCH_ESP32)\n  if (!persist) {\n    p_free(tmpRAMbuffer);\n    size_t len = measureJson(*pDoc) + 1;\n    // if possible use SPI RAM on ESP32\n    tmpRAMbuffer = (char*)p_malloc(len);\n    if (tmpRAMbuffer!=nullptr) {\n      serializeJson(*pDoc, tmpRAMbuffer, len);\n    } else {\n      writeObjectToFileUsingId(getPresetsFileName(persist), presetToSave, pDoc);\n    }\n  } else\n  #endif\n  writeObjectToFileUsingId(getPresetsFileName(persist), presetToSave, pDoc);\n\n  if (persist) presetsModifiedTime = toki.second(); //unix time\n  releaseJSONBufferLock();\n  updateFSInfo();\n\n  // clean up\n  saveLedmap   = -1;\n  presetToSave = 0;\n  p_free(saveName);\n  p_free(quickLoad);\n  saveName = nullptr;\n  quickLoad = nullptr;\n  playlistSave = false;\n}\n\nbool getPresetName(byte index, String& name)\n{\n  if (!requestJSONBufferLock(JSON_LOCK_PRESET_NAME)) return false;\n  bool presetExists = false;\n  if (readObjectFromFileUsingId(getPresetsFileName(), index, pDoc)) {\n    JsonObject fdo = pDoc->as<JsonObject>();\n    if (fdo[\"n\"]) {\n      name = (const char*)(fdo[\"n\"]);\n      presetExists = true;\n    }\n  }\n  releaseJSONBufferLock();\n  return presetExists;\n}\n\nvoid initPresetsFile()\n{\n  char fileName[33]; strncpy_P(fileName, getPresetsFileName(), 32); fileName[32] = 0; //use PROGMEM safe copy as FS.open() does not\n  if (WLED_FS.exists(fileName)) return;\n\n  StaticJsonDocument<64> doc;\n  JsonObject sObj = doc.to<JsonObject>();\n  sObj.createNestedObject(\"0\");\n  File f = WLED_FS.open(fileName, \"w\");\n  if (!f) {\n    errorFlag = ERR_FS_GENERAL;\n    return;\n  }\n  serializeJson(doc, f);\n  f.close();\n}\n\nbool applyPresetFromPlaylist(byte index)\n{\n  DEBUG_PRINTF_P(PSTR(\"Request to apply preset: %d\\n\"), index);\n  presetToApply = index;\n  callModeToApply = CALL_MODE_DIRECT_CHANGE;\n  return true;\n}\n\nbool applyPreset(byte index, byte callMode)\n{\n  unloadPlaylist(); // applying a preset unloads the playlist (#3827)\n  DEBUG_PRINTF_P(PSTR(\"Request to apply preset: %u\\n\"), index);\n  presetToApply = index;\n  callModeToApply = callMode;\n  return true;\n}\n\n// apply preset or fallback to a effect and palette if it doesn't exist\nvoid applyPresetWithFallback(uint8_t index, uint8_t callMode, uint8_t effectID, uint8_t paletteID)\n{\n  applyPreset(index, callMode);\n  //these two will be overwritten if preset exists in handlePresets()\n  effectCurrent = effectID;\n  effectPalette = paletteID;\n}\n\nvoid handlePresets()\n{\n  byte presetErrFlag = ERR_NONE;\n  if (presetToSave) {\n    strip.suspend();\n    doSaveState();\n    strip.resume();\n    return;\n  }\n\n  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\n\n  bool changePreset = false;\n  uint8_t tmpPreset = presetToApply; // store temporary since deserializeState() may call applyPreset()\n  uint8_t tmpMode   = callModeToApply;\n\n  JsonObject fdo;\n\n  presetToApply = 0; //clear request for preset\n  callModeToApply = 0;\n\n  DEBUG_PRINTF_P(PSTR(\"Applying preset: %u\\n\"), (unsigned)tmpPreset);\n\n  #if defined(ARDUINO_ARCH_ESP32S2) || defined(ARDUINO_ARCH_ESP32C3)\n  unsigned long maxWait = millis() + strip.getFrameTime();\n  while (strip.isUpdating() && millis() < maxWait) delay(1); // wait for strip to finish updating, accessing FS during sendout causes glitches\n  #endif\n\n  #ifdef ARDUINO_ARCH_ESP32\n  if (tmpPreset==255 && tmpRAMbuffer!=nullptr) {\n    deserializeJson(*pDoc,tmpRAMbuffer);\n  } else\n  #endif\n  {\n  presetErrFlag = readObjectFromFileUsingId(getPresetsFileName(tmpPreset < 255), tmpPreset, pDoc) ? ERR_NONE : ERR_FS_PLOAD;\n  }\n  fdo = pDoc->as<JsonObject>();\n\n  // only reset errorflag if previous error was preset-related\n  if ((errorFlag == ERR_NONE) || (errorFlag == ERR_FS_PLOAD)) errorFlag = presetErrFlag;\n\n  //HTTP API commands\n  const char* httpwin = fdo[\"win\"];\n  if (httpwin) {\n    String apireq = \"win\"; // reduce flash string usage\n    apireq += F(\"&IN&\"); // internal call\n    apireq += httpwin;\n    handleSet(nullptr, apireq, false); // may call applyPreset() via PL=\n    setValuesFromFirstSelectedSeg(); // fills legacy values\n    changePreset = true;\n  } else {\n    if (!fdo[\"seg\"].isNull() || !fdo[\"on\"].isNull() || !fdo[\"bri\"].isNull() || !fdo[\"nl\"].isNull() || !fdo[\"ps\"].isNull() || !fdo[F(\"playlist\")].isNull()) changePreset = true;\n    if (!(tmpMode == CALL_MODE_BUTTON_PRESET && fdo[\"ps\"].is<const char *>() && strchr(fdo[\"ps\"].as<const char *>(),'~') != strrchr(fdo[\"ps\"].as<const char *>(),'~')))\n      fdo.remove(\"ps\"); // remove load request for presets to prevent recursive crash (if not called by button and contains preset cycling string \"1~5~\")\n    deserializeState(fdo, CALL_MODE_NO_NOTIFY, tmpPreset); // may change presetToApply by calling applyPreset()\n  }\n  if (!errorFlag && tmpPreset < 255 && changePreset) currentPreset = tmpPreset;\n\n  #if defined(ARDUINO_ARCH_ESP32)\n  //Aircoookie recommended not to delete buffer\n  if (tmpPreset==255 && tmpRAMbuffer!=nullptr) {\n    p_free(tmpRAMbuffer);\n    tmpRAMbuffer = nullptr;\n  }\n  #endif\n\n  releaseJSONBufferLock();\n  if (changePreset) notify(tmpMode); // force UDP notification\n  stateUpdated(tmpMode);  // was colorUpdated() if anything breaks\n  updateInterfaces(tmpMode);\n}\n\n//called from handleSet(PS=) [network callback (sObj is empty), IR (irrational), deserializeState, UDP] and deserializeState() [network callback (filedoc!=nullptr)]\nvoid savePreset(byte index, const char* pname, JsonObject sObj)\n{\n  if (!saveName) saveName = static_cast<char*>(p_malloc(33));\n  if (!quickLoad) quickLoad = static_cast<char*>(p_malloc(9));\n  if (!saveName || !quickLoad) return;\n\n  if (index == 0 || (index > 250 && index < 255)) return;\n  if (pname) strlcpy(saveName, pname, 33);\n  else {\n    if (sObj[\"n\"].is<const char*>()) strlcpy(saveName, sObj[\"n\"].as<const char*>(), 33);\n    else                             sprintf_P(saveName, PSTR(\"Preset %d\"), index);\n  }\n\n  DEBUG_PRINTF_P(PSTR(\"Saving preset (%d) %s\\n\"), index, saveName);\n\n  presetToSave = index;\n  playlistSave = false;\n  if (sObj[F(\"ql\")].is<const char*>()) strlcpy(quickLoad, sObj[F(\"ql\")].as<const char*>(), 9); // client limits QL to 2 chars, buffer for 8 bytes to allow unicode\n  else quickLoad[0] = 0;\n\n  const char *bootPS = PSTR(\"bootps\");\n  if (!sObj[FPSTR(bootPS)].isNull()) {\n    bootPreset = sObj[FPSTR(bootPS)] | bootPreset;\n    sObj.remove(FPSTR(bootPS));\n    configNeedsWrite = true;\n  }\n\n  if (sObj.size()==0 || sObj[\"o\"].isNull()) { // no \"o\" means not a playlist or custom API call, saving of state is async (not immediately)\n    includeBri   = sObj[\"ib\"].as<bool>() || sObj.size()==0 || index==255; // temporary preset needs brightness\n    segBounds    = sObj[\"sb\"].as<bool>() || sObj.size()==0 || index==255; // temporary preset needs bounds\n    selectedOnly = sObj[F(\"sc\")].as<bool>();\n    saveLedmap   = sObj[F(\"ledmap\")] | -1;\n  } else {\n    // this is a playlist or API call\n    if (sObj[F(\"playlist\")].isNull()) {\n      // we will save API call immediately (often causes presets.json corruption)\n      presetToSave = 0;\n      if (index <= 250) { // cannot save API calls to temporary preset (255)\n        sObj.remove(\"o\");\n        sObj.remove(\"v\");\n        sObj.remove(\"time\");\n        sObj.remove(F(\"error\"));\n        sObj.remove(F(\"psave\"));\n        if (sObj[\"n\"].isNull()) sObj[\"n\"] = saveName;\n        initPresetsFile(); // just in case if someone deleted presets.json using /edit\n        writeObjectToFileUsingId(getPresetsFileName(), index, pDoc);\n        presetsModifiedTime = toki.second(); //unix time\n        updateFSInfo();\n      }\n      p_free(saveName);\n      p_free(quickLoad);\n      saveName = nullptr;\n      quickLoad = nullptr;\n    } else {\n      // store playlist\n      // 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\n      includeBri   = true; // !sObj[\"on\"].isNull();\n      playlistSave = true;\n    }\n  }\n}\n\nvoid deletePreset(byte index) {\n  StaticJsonDocument<24> empty;\n  writeObjectToFileUsingId(getPresetsFileName(), index, &empty);\n  presetsModifiedTime = toki.second(); //unix time\n  updateFSInfo();\n}"
  },
  {
    "path": "wled00/remote.cpp",
    "content": "#include \"wled.h\"\n#ifndef WLED_DISABLE_ESPNOW\n\n#define ESPNOW_BUSWAIT_TIMEOUT 24 // one frame timeout to wait for bus to finish updating\n\n#define NIGHT_MODE_DEACTIVATED     -1\n#define NIGHT_MODE_BRIGHTNESS      5\n\n#define WIZMOTE_BUTTON_ON          1\n#define WIZMOTE_BUTTON_OFF         2\n#define WIZMOTE_BUTTON_NIGHT       3\n#define WIZMOTE_BUTTON_ONE         16\n#define WIZMOTE_BUTTON_TWO         17\n#define WIZMOTE_BUTTON_THREE       18\n#define WIZMOTE_BUTTON_FOUR        19\n#define WIZMOTE_BUTTON_FIVE        20\n#define WIZMOTE_BUTTON_SIX         21\n#define WIZMOTE_BUTTON_SEVEN       22\n#define WIZMOTE_BUTTON_BRIGHT_UP   9\n#define WIZMOTE_BUTTON_BRIGHT_DOWN 8\n\n#define WIZ_SMART_BUTTON_ON          100\n#define WIZ_SMART_BUTTON_OFF         101\n#define WIZ_SMART_BUTTON_BRIGHT_UP   102\n#define WIZ_SMART_BUTTON_BRIGHT_DOWN 103\n\n// This is kind of an esoteric strucure because it's pulled from the \"Wizmote\"\n// product spec. That remote is used as the baseline for behavior and availability\n// since it's broadly commercially available and works out of the box as a drop-in\ntypedef struct WizMoteMessageStructure {\n  uint8_t program;  // 0x91 for ON button, 0x81 for all others\n  uint8_t seq[4];   // Incremetal sequence number 32 bit unsigned integer LSB first\n  uint8_t dt1;      // Button Data Type (0x20)\n  uint8_t button;   // Identifies which button is being pressed\n  uint8_t dt2;      // Battery Level Data Type (0x01)\n  uint8_t batLevel; // Battery Level 0-100\n  \n  uint8_t byte10;   // Unknown, maybe checksum\n  uint8_t byte11;   // Unknown, maybe checksum\n  uint8_t byte12;   // Unknown, maybe checksum\n  uint8_t byte13;   // Unknown, maybe checksum\n} message_structure_t;\n\nstatic uint32_t last_seq = UINT32_MAX;\nstatic int brightnessBeforeNightMode = NIGHT_MODE_DEACTIVATED;\nstatic int16_t ESPNowButton = -1; // set in callback if new button value is received\n\n// Pulled from the IR Remote logic but reduced to 10 steps with a constant of 3\nstatic const byte brightnessSteps[] = {\n  6, 9, 14, 22, 33, 50, 75, 113, 170, 255\n};\nstatic const size_t numBrightnessSteps = sizeof(brightnessSteps) / sizeof(byte);\n\ninline bool nightModeActive() {\n  return brightnessBeforeNightMode != NIGHT_MODE_DEACTIVATED;\n}\n\nstatic void activateNightMode() {\n  if (nightModeActive()) return;\n  brightnessBeforeNightMode = bri;\n  bri = NIGHT_MODE_BRIGHTNESS;\n  stateUpdated(CALL_MODE_BUTTON);\n}\n\nstatic bool resetNightMode() {\n  if (!nightModeActive()) return false;\n  bri = brightnessBeforeNightMode;\n  brightnessBeforeNightMode = NIGHT_MODE_DEACTIVATED;\n  stateUpdated(CALL_MODE_BUTTON);\n  return true;\n}\n\n// increment `bri` to the next `brightnessSteps` value\nstatic void brightnessUp() {\n  if (nightModeActive()) return;\n  // dumb incremental search is efficient enough for so few items\n  for (unsigned index = 0; index < numBrightnessSteps; ++index) {\n    if (brightnessSteps[index] > bri) {\n      bri = brightnessSteps[index];\n      break;\n    }\n  }\n  stateUpdated(CALL_MODE_BUTTON);\n}\n\n// decrement `bri` to the next `brightnessSteps` value\nstatic void brightnessDown() {\n  if (nightModeActive()) return;\n  // dumb incremental search is efficient enough for so few items\n  for (int index = numBrightnessSteps - 1; index >= 0; --index) {\n    if (brightnessSteps[index] < bri) {\n      bri = brightnessSteps[index];\n      break;\n    }\n  }\n  stateUpdated(CALL_MODE_BUTTON);\n}\n\nstatic void setOn() {\n  resetNightMode();\n  if (!bri) {\n    toggleOnOff();\n    stateUpdated(CALL_MODE_BUTTON);\n  }\n}\n\nstatic void setOff() {\n  resetNightMode();\n  if (bri) {\n    toggleOnOff();\n    stateUpdated(CALL_MODE_BUTTON);\n  }\n}\n\nstatic void presetWithFallback(uint8_t presetID, uint8_t effectID, uint8_t paletteID) {\n  resetNightMode();\n  applyPresetWithFallback(presetID, CALL_MODE_BUTTON_PRESET, effectID, paletteID);\n}\n\n// this function follows the same principle as decodeIRJson()\nstatic bool remoteJson(int button)\n{\n  char objKey[10];\n  bool parsed = false;\n\n  if (!requestJSONBufferLock(JSON_LOCK_REMOTE)) return false;\n\n  sprintf_P(objKey, PSTR(\"\\\"%d\\\":\"), button);\n\n  unsigned long start = millis();\n  while (strip.isUpdating() && millis()-start < ESPNOW_BUSWAIT_TIMEOUT) yield(); // wait for strip to finish updating, accessing FS during sendout causes glitches\n\n  // attempt to read command from remote.json\n  readObjectFromFile(PSTR(\"/remote.json\"), objKey, pDoc);\n  JsonObject fdo = pDoc->as<JsonObject>();\n  if (fdo.isNull()) {\n    // the received button does not exist\n    //if (!WLED_FS.exists(F(\"/remote.json\"))) errorFlag = ERR_FS_RMLOAD; //warn if file itself doesn't exist\n    releaseJSONBufferLock();\n    return parsed;\n  }\n\n  String cmdStr = fdo[\"cmd\"].as<String>();\n  JsonObject jsonCmdObj = fdo[\"cmd\"]; //object\n\n  if (jsonCmdObj.isNull())  // we could also use: fdo[\"cmd\"].is<String>()\n  {\n    if (cmdStr.startsWith(\"!\")) {\n      // call limited set of C functions\n      if (cmdStr.startsWith(F(\"!incBri\"))) {\n        brightnessUp();\n        parsed = true;\n      } else if (cmdStr.startsWith(F(\"!decBri\"))) {\n        brightnessDown();\n        parsed = true;\n      } else if (cmdStr.startsWith(F(\"!presetF\"))) { //!presetFallback\n        uint8_t p1 = fdo[\"PL\"] | 1;\n        uint8_t p2 = fdo[\"FX\"] | hw_random8(strip.getModeCount() -1);\n        uint8_t p3 = fdo[\"FP\"] | 0;\n        presetWithFallback(p1, p2, p3);\n        parsed = true;\n      }\n    } else {\n      // HTTP API command\n      String apireq = \"win\"; apireq += '&';                        // reduce flash string usage\n      //if (cmdStr.indexOf(\"~\") || fdo[\"rpt\"]) lastValidCode = code; // repeatable action\n      if (!cmdStr.startsWith(apireq)) cmdStr = apireq + cmdStr;    // if no \"win&\" prefix\n      if (!irApplyToAllSelected && cmdStr.indexOf(F(\"SS=\"))<0) {\n        char tmp[10];\n        sprintf_P(tmp, PSTR(\"&SS=%d\"), strip.getMainSegmentId());\n        cmdStr += tmp;\n      }\n      fdo.clear();                                                 // clear JSON buffer (it is no longer needed)\n      handleSet(nullptr, cmdStr, false);                           // no stateUpdated() call here\n      stateUpdated(CALL_MODE_BUTTON);\n      parsed = true;\n    }\n  } else {\n    // command is JSON object (TODO: currently will not handle irApplyToAllSelected correctly)\n    deserializeState(jsonCmdObj, CALL_MODE_BUTTON);\n    parsed = true;\n  }\n  releaseJSONBufferLock();\n  return parsed;\n}\n\n// Callback function that will be executed when data is received from a linked remote\nvoid handleWiZdata(uint8_t *incomingData, size_t len) {\n  message_structure_t *incoming = reinterpret_cast<message_structure_t *>(incomingData);\n\n  if (len != sizeof(message_structure_t)) {\n    DEBUG_PRINTF_P(PSTR(\"Unknown incoming ESP Now message received of length %u\\n\"), len);\n    return;\n  }\n\n  uint32_t cur_seq = incoming->seq[0] | (incoming->seq[1] << 8) | (incoming->seq[2] << 16) | (incoming->seq[3] << 24);\n  if (cur_seq == last_seq) {\n    return;\n  }\n\n  DEBUG_PRINT(F(\"Incoming ESP Now Packet [\"));\n  DEBUG_PRINT(cur_seq);\n  DEBUG_PRINT(F(\"] from sender [\"));\n  DEBUG_PRINT(last_signal_src);\n  DEBUG_PRINT(F(\"] button: \"));\n  DEBUG_PRINTLN(incoming->button);\n\n  ESPNowButton = incoming->button; // save state, do not process in callback (can cause glitches)\n  last_seq = cur_seq;\n}\n\n// process ESPNow button data (acesses FS, should not be called while update to avoid glitches)\nvoid handleRemote() {\n  if(ESPNowButton >= 0) {\n  if (!remoteJson(ESPNowButton))\n    switch (ESPNowButton) {\n      case WIZMOTE_BUTTON_ON             : setOn();                                         break;\n      case WIZMOTE_BUTTON_OFF            : setOff();                                        break;\n      case WIZMOTE_BUTTON_ONE            : presetWithFallback(1, FX_MODE_STATIC,        0); break;\n      case WIZMOTE_BUTTON_TWO            : presetWithFallback(2, FX_MODE_BREATH,        0); break;\n      case WIZMOTE_BUTTON_THREE          : presetWithFallback(3, FX_MODE_FIRE_FLICKER,  0); break;\n      case WIZMOTE_BUTTON_FOUR           : presetWithFallback(4, FX_MODE_RAINBOW,       0); break;\n      case WIZMOTE_BUTTON_FIVE           : presetWithFallback(5, FX_MODE_CANDLE,        0); break;\n      case WIZMOTE_BUTTON_SIX            : presetWithFallback(6, FX_MODE_RANDOM_COLOR,  0); break;\n      case WIZMOTE_BUTTON_SEVEN          : presetWithFallback(7, FX_MODE_FADE,          0); break;\n      case WIZMOTE_BUTTON_NIGHT          : activateNightMode();                             break;\n      case WIZMOTE_BUTTON_BRIGHT_UP      : brightnessUp();                                  break;\n      case WIZMOTE_BUTTON_BRIGHT_DOWN    : brightnessDown();                                break;\n      case WIZ_SMART_BUTTON_ON           : setOn();                                         break;\n      case WIZ_SMART_BUTTON_OFF          : setOff();                                        break;\n      case WIZ_SMART_BUTTON_BRIGHT_UP    : brightnessUp();                                  break;\n      case WIZ_SMART_BUTTON_BRIGHT_DOWN  : brightnessDown();                                break;\n      default: break;\n    }\n  }\n  ESPNowButton = -1;\n}\n\n#else\nvoid handleRemote() {}\n#endif\n"
  },
  {
    "path": "wled00/set.cpp",
    "content": "#include \"wled.h\"\n\n/*\n * Receives client input\n */\n\n//called upon POST settings form submit\nvoid handleSettingsSet(AsyncWebServerRequest *request, byte subPage)\n{\n  if (subPage == SUBPAGE_PINREQ)\n  {\n    checkSettingsPIN(request->arg(F(\"PIN\")).c_str());\n    return;\n  }\n\n  //0: menu 1: wifi 2: leds 3: ui 4: sync 5: time 6: sec 7: DMX 8: usermods 9: N/A 10: 2D\n  if (subPage < 1 || subPage > 10 || !correctPIN) return;\n\n  //WIFI SETTINGS\n  if (subPage == SUBPAGE_WIFI)\n  {\n    unsigned cnt = 0;\n    for (size_t n = 0; n < WLED_MAX_WIFI_COUNT; n++) {\n      char cs[4] = \"CS\"; cs[2] = 48+n; cs[3] = 0; //client SSID\n      char pw[4] = \"PW\"; pw[2] = 48+n; pw[3] = 0; //client password\n      char bs[4] = \"BS\"; bs[2] = 48+n; bs[3] = 0; //BSSID\n      char ip[5] = \"IP\"; ip[2] = 48+n; ip[4] = 0; //IP address\n      char gw[5] = \"GW\"; gw[2] = 48+n; gw[4] = 0; //GW address\n      char sn[5] = \"SN\"; sn[2] = 48+n; sn[4] = 0; //subnet mask\n#ifdef WLED_ENABLE_WPA_ENTERPRISE\n      char et[4] = \"ET\"; et[2] = 48+n; et[3] = 0; //WiFi encryption type\n      char ea[4] = \"EA\"; ea[2] = 48+n; ea[3] = 0; //enterprise anonymous identity\n      char ei[4] = \"EI\"; ei[2] = 48+n; ei[3] = 0; //enterprise identity\n#endif\n      if (request->hasArg(cs)) {\n        if (n >= multiWiFi.size()) multiWiFi.emplace_back(); // expand vector by one\n        char oldSSID[33]; strcpy(oldSSID, multiWiFi[n].clientSSID);\n        char oldPass[65]; strcpy(oldPass, multiWiFi[n].clientPass);\n        uint8_t oldBSSID[6]; memcpy(oldBSSID, multiWiFi[n].bssid, 6);  // save old BSSID\n\n        strlcpy(multiWiFi[n].clientSSID, request->arg(cs).c_str(), 33);\n        if (strlen(oldSSID) == 0 || strncmp(multiWiFi[n].clientSSID, oldSSID, 32) != 0) {\n          forceReconnect = true;\n        }\n        if (!isAsterisksOnly(request->arg(pw).c_str(), 65)) {\n          strlcpy(multiWiFi[n].clientPass, request->arg(pw).c_str(), 65);\n          forceReconnect = true;\n        }\n        fillStr2MAC(multiWiFi[n].bssid, request->arg(bs).c_str());\n        if (memcmp(oldBSSID, multiWiFi[n].bssid, 6) != 0) {  // check if BSSID changed\n          forceReconnect = true;\n        }\n        for (size_t i = 0; i < 4; i++) {\n          ip[3] = 48+i;\n          gw[3] = 48+i;\n          sn[3] = 48+i;\n          multiWiFi[n].staticIP[i] = request->arg(ip).toInt();\n          multiWiFi[n].staticGW[i] = request->arg(gw).toInt();\n          multiWiFi[n].staticSN[i] = request->arg(sn).toInt();\n        }\n\n#ifdef WLED_ENABLE_WPA_ENTERPRISE\n        byte oldType = multiWiFi[n].encryptionType;\n        char oldAnon[65]; strcpy(oldAnon, multiWiFi[n].enterpriseAnonIdentity);\n        char oldIden[65]; strcpy(oldIden, multiWiFi[n].enterpriseIdentity);\n        if (request->hasArg(et) && request->hasArg(ea) && request->hasArg(ei)) {\n          multiWiFi[n].encryptionType = request->arg(et).toInt();\n          strlcpy(multiWiFi[n].enterpriseAnonIdentity, request->arg(ea).c_str(), 65);\n          strlcpy(multiWiFi[n].enterpriseIdentity, request->arg(ei).c_str(), 65);\n        } else {\n          // No enterprise settings provided, default to PSK\n          multiWiFi[n].encryptionType = WIFI_ENCRYPTION_TYPE_PSK;\n        }\n\n        if (multiWiFi[n].encryptionType == WIFI_ENCRYPTION_TYPE_PSK) {\n          // PSK - Clear the anonymous identity and identity fields\n          multiWiFi[n].enterpriseAnonIdentity[0] = '\\0';\n          multiWiFi[n].enterpriseIdentity[0] = '\\0';\n        }\n        forceReconnect |= oldType != multiWiFi[n].encryptionType;\n        if (strncmp(multiWiFi[n].enterpriseAnonIdentity, oldAnon, 64) != 0) {\n          forceReconnect = true;\n        }\n        if (strncmp(multiWiFi[n].enterpriseIdentity, oldIden, 64) != 0) {\n          forceReconnect = true;\n        }\n#endif\n\n        cnt++;\n      }\n    }\n    // remove unused\n    if (cnt < multiWiFi.size()) {\n      cnt = multiWiFi.size() - cnt;\n      while (cnt--) multiWiFi.pop_back();\n      multiWiFi.shrink_to_fit(); // release memory\n    }\n\n    if (request->hasArg(F(\"D0\"))) {\n      dnsAddress = IPAddress(request->arg(F(\"D0\")).toInt(),request->arg(F(\"D1\")).toInt(),request->arg(F(\"D2\")).toInt(),request->arg(F(\"D3\")).toInt());\n    }\n\n    strlcpy(cmDNS, request->arg(F(\"CM\")).c_str(), 33);\n\n    apBehavior = request->arg(F(\"AB\")).toInt();\n    char oldSSID[33]; strcpy(oldSSID, apSSID);\n    strlcpy(apSSID, request->arg(F(\"AS\")).c_str(), 33);\n    if (!strcmp(oldSSID, apSSID) && apActive) forceReconnect = true;\n    apHide = request->hasArg(F(\"AH\"));\n    int passlen = request->arg(F(\"AP\")).length();\n    if (passlen == 0 || (passlen > 7 && !isAsterisksOnly(request->arg(F(\"AP\")).c_str(), 65))) {\n      strlcpy(apPass, request->arg(F(\"AP\")).c_str(), 65);\n      forceReconnect = true;\n    }\n    int t = request->arg(F(\"AC\")).toInt();\n    if (t != apChannel) forceReconnect = true;\n    if (t > 0 && t < 14) apChannel = t;\n\n    #ifdef ARDUINO_ARCH_ESP32\n    int tx = request->arg(F(\"TX\")).toInt();\n    txPower = min(max(tx, (int)WIFI_POWER_2dBm), (int)WIFI_POWER_19_5dBm);\n    #endif\n\n    force802_3g = request->hasArg(F(\"FG\"));\n    noWifiSleep = request->hasArg(F(\"WS\"));\n\n    #ifndef WLED_DISABLE_ESPNOW\n    bool oldESPNow = enableESPNow;\n    enableESPNow = request->hasArg(F(\"RE\"));\n    if (oldESPNow != enableESPNow) forceReconnect = true;\n    linked_remotes.clear();  // clear old remotes\n    for (size_t n = 0; n < 10; n++) {\n      char rm[4];\n      snprintf(rm, sizeof(rm), \"RM%d\", n); // \"RM0\" to \"RM9\"\n      if (request->hasArg(rm)) {\n        const String& arg = request->arg(rm);\n        if (arg.isEmpty()) continue;\n        std::array<char, 13> mac{};\n        strlcpy(mac.data(), request->arg(rm).c_str(), 13);\n        strlwr(mac.data());\n        if (mac[0] != '\\0') {\n          linked_remotes.emplace_back(mac);\n        }\n      }\n    }\n    #endif\n\n    #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET)\n    ethernetType = request->arg(F(\"ETH\")).toInt();\n    initEthernet();\n    #endif\n  }\n\n  //LED SETTINGS\n  if (subPage == SUBPAGE_LEDS)\n  {\n    int t = 0;\n\n    if (rlyPin>=0 && PinManager::isPinAllocated(rlyPin, PinOwner::Relay)) {\n       PinManager::deallocatePin(rlyPin, PinOwner::Relay);\n    }\n    #ifndef WLED_DISABLE_INFRARED\n    if (irPin>=0 && PinManager::isPinAllocated(irPin, PinOwner::IR)) {\n      deInitIR();\n      PinManager::deallocatePin(irPin, PinOwner::IR);\n    }\n    #endif\n    for (const auto &button : buttons) {\n      if (button.pin >= 0 && PinManager::isPinAllocated(button.pin, PinOwner::Button)) {\n        PinManager::deallocatePin(button.pin, PinOwner::Button);\n        #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a function to check touch state, detach interrupt\n        if (digitalPinToTouchChannel(button.pin) >= 0) // if touch capable pin\n          touchDetachInterrupt(button.pin);            // if not assigned previously, this will do nothing\n        #endif\n      }\n    }\n\n    unsigned colorOrder, type, skip, awmode, channelSwap, maPerLed, driverType;\n    unsigned length, start, maMax;\n    uint8_t pins[OUTPUT_MAX_PINS] = {255, 255, 255, 255, 255};\n    String text;\n\n    // this will set global ABL max current used when per-port ABL is not used\n    unsigned ablMilliampsMax = request->arg(F(\"MA\")).toInt();\n    BusManager::setMilliampsMax(ablMilliampsMax);\n\n    strip.autoSegments = request->hasArg(F(\"MS\"));\n    strip.correctWB = request->hasArg(F(\"CCT\"));\n    strip.cctFromRgb = request->hasArg(F(\"CR\"));\n    cctICused = request->hasArg(F(\"IC\"));\n    uint8_t cctBlending = request->arg(F(\"CB\")).toInt();\n    Bus::setCCTBlend(cctBlending);\n    Bus::setGlobalAWMode(request->arg(F(\"AW\")).toInt());\n    strip.setTargetFps(request->arg(F(\"FR\")).toInt());\n\n    bool busesChanged = false;\n    for (int s = 0; s < 36; s++) { // theoretical limit is 36 : \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n      int offset = s < 10 ? '0' : 'A' - 10;\n      char lp[4] = \"L0\"; lp[2] = offset+s; lp[3] = 0; //ascii 0-9 //strip data pin\n      char lc[4] = \"LC\"; lc[2] = offset+s; lc[3] = 0; //strip length\n      char co[4] = \"CO\"; co[2] = offset+s; co[3] = 0; //strip color order\n      char lt[4] = \"LT\"; lt[2] = offset+s; lt[3] = 0; //strip type\n      char ls[4] = \"LS\"; ls[2] = offset+s; ls[3] = 0; //strip start LED\n      char cv[4] = \"CV\"; cv[2] = offset+s; cv[3] = 0; //strip reverse\n      char sl[4] = \"SL\"; sl[2] = offset+s; sl[3] = 0; //skip first N LEDs\n      char rf[4] = \"RF\"; rf[2] = offset+s; rf[3] = 0; //refresh required\n      char aw[4] = \"AW\"; aw[2] = offset+s; aw[3] = 0; //auto white mode\n      char wo[4] = \"WO\"; wo[2] = offset+s; wo[3] = 0; //channel swap\n      char sp[4] = \"SP\"; sp[2] = offset+s; sp[3] = 0; //bus clock speed (DotStar & PWM)\n      char la[4] = \"LA\"; la[2] = offset+s; la[3] = 0; //LED mA\n      char ma[4] = \"MA\"; ma[2] = offset+s; ma[3] = 0; //max mA\n      char ld[4] = \"LD\"; ld[2] = offset+s; ld[3] = 0; //driver type (RMT=0, I2S=1)\n      char hs[4] = \"HS\"; hs[2] = offset+s; hs[3] = 0; //hostname (for network types, custom text for others)\n      if (!request->hasArg(lp)) {\n        DEBUG_PRINTF_P(PSTR(\"# of buses: %d\\n\"), s+1);\n        break;\n      }\n      for (int i = 0; i < 5; i++) {\n        lp[1] = '0'+i;\n        if (!request->hasArg(lp)) break;\n        pins[i] = (request->arg(lp).length() > 0) ? request->arg(lp).toInt() : 255;\n      }\n      type = request->arg(lt).toInt();\n      skip = request->arg(sl).toInt();\n      colorOrder = request->arg(co).toInt();\n      start = (request->hasArg(ls)) ? request->arg(ls).toInt() : t;\n      if (request->hasArg(lc) && request->arg(lc).toInt() > 0) {\n        t += length = request->arg(lc).toInt();\n      } else {\n        break;  // no parameter\n      }\n      awmode = request->arg(aw).toInt();\n      uint16_t freq = request->arg(sp).toInt();\n      if (Bus::isPWM(type)) {\n        switch (freq) {\n          case 0 : freq = WLED_PWM_FREQ/2;    break;\n          case 1 : freq = WLED_PWM_FREQ*2/3;  break;\n          default:\n          case 2 : freq = WLED_PWM_FREQ;      break;\n          case 3 : freq = WLED_PWM_FREQ*2;    break;\n          case 4 : freq = WLED_PWM_FREQ*10/3; break; // uint16_t max (19531 * 3.333)\n        }\n      } else if (Bus::is2Pin(type)) {\n        switch (freq) {\n          default:\n          case 0 : freq =  1000; break;\n          case 1 : freq =  2000; break;\n          case 2 : freq =  5000; break;\n          case 3 : freq = 10000; break;\n          case 4 : freq = 20000; break;\n        }\n      } else {\n        freq = 0;\n      }\n      channelSwap = Bus::hasWhite(type) ? request->arg(wo).toInt() : 0;\n      if (Bus::isOnOff(type) || Bus::isPWM(type) || Bus::isVirtual(type)) { // analog and virtual\n        maPerLed = 0;\n        maMax = 0;\n      } else {\n        maPerLed = request->arg(la).toInt();\n        maMax = request->arg(ma).toInt() * request->hasArg(F(\"PPL\")); // if PP-ABL is disabled maMax (per bus) must be 0\n      }\n      type |= request->hasArg(rf) << 7; // off refresh override\n      driverType = request->arg(ld).toInt(); // 0=RMT (default), 1=I2S\n      text = request->arg(hs).substring(0,31);\n      // actual finalization is done in WLED::loop() (removing old busses and adding new)\n      // this may happen even before this loop is finished so we do \"doInitBusses\" after the loop\n      busConfigs.emplace_back(type, pins, start, length, colorOrder | (channelSwap<<4), request->hasArg(cv), skip, awmode, freq, maPerLed, maMax, driverType, text);\n      busesChanged = true;\n    }\n    //doInitBusses = busesChanged; // we will do that below to ensure all input data is processed\n\n    // we will not bother with pre-allocating ColorOrderMappings vector\n    BusManager::getColorOrderMap().reset();\n    for (int s = 0; s < WLED_MAX_COLOR_ORDER_MAPPINGS; s++) {\n      int offset = s < 10 ? '0' : 'A' - 10;\n      char xs[4] = \"XS\"; xs[2] = offset+s; xs[3] = 0; //start LED\n      char xc[4] = \"XC\"; xc[2] = offset+s; xc[3] = 0; //strip length\n      char xo[4] = \"XO\"; xo[2] = offset+s; xo[3] = 0; //color order\n      char xw[4] = \"XW\"; xw[2] = offset+s; xw[3] = 0; //W swap\n      if (request->hasArg(xs)) {\n        start = request->arg(xs).toInt();\n        length = request->arg(xc).toInt();\n        colorOrder = request->arg(xo).toInt() & 0x0F;\n        colorOrder |= (request->arg(xw).toInt() & 0x0F) << 4; // add W swap information\n        if (!BusManager::getColorOrderMap().add(start, length, colorOrder)) break;\n      }\n    }\n\n    // update other pins\n    #ifndef WLED_DISABLE_INFRARED\n    int hw_ir_pin = request->arg(F(\"IR\")).toInt();\n    if (PinManager::allocatePin(hw_ir_pin,false, PinOwner::IR)) {\n      irPin = hw_ir_pin;\n    } else {\n      irPin = -1;\n    }\n    irEnabled = request->arg(F(\"IT\")).toInt();\n    initIR();\n    #endif\n    irApplyToAllSelected = !request->hasArg(F(\"MSO\"));\n\n    int hw_rly_pin = request->arg(F(\"RL\")).toInt();\n    if (PinManager::allocatePin(hw_rly_pin,true, PinOwner::Relay)) {\n      rlyPin = hw_rly_pin;\n    } else {\n      rlyPin = -1;\n    }\n    rlyMde = (bool)request->hasArg(F(\"RM\"));\n    rlyOpenDrain = (bool)request->hasArg(F(\"RO\"));\n\n    disablePullUp = (bool)request->hasArg(F(\"IP\"));\n    touchThreshold = request->arg(F(\"TT\")).toInt();\n    for (int i = 0; i < WLED_MAX_BUTTONS; i++) {\n      int offset = i < 10 ? '0' : 'A' - 10;\n      char bt[4] = \"BT\"; bt[2] = offset+i; bt[3] = 0; // button pin (use A,B,C,... if WLED_MAX_BUTTONS>10)\n      char be[4] = \"BE\"; be[2] = offset+i; be[3] = 0; // button type (use A,B,C,... if WLED_MAX_BUTTONS>10)\n      int hw_btn_pin = request->hasArg(bt) ? request->arg(bt).toInt() : -1;\n      if (i >= buttons.size()) buttons.emplace_back(hw_btn_pin, request->arg(be).toInt()); // add button to vector\n      else {\n        buttons[i].pin  = hw_btn_pin;\n        buttons[i].type = request->arg(be).toInt();\n      }\n      if (buttons[i].pin >= 0 && PinManager::allocatePin(buttons[i].pin, false, PinOwner::Button)) {\n        #ifdef ARDUINO_ARCH_ESP32\n        // ESP32 only: check that button pin is a valid gpio\n        if ((buttons[i].type == BTN_TYPE_ANALOG) || (buttons[i].type == BTN_TYPE_ANALOG_INVERTED)) {\n          if (digitalPinToAnalogChannel(buttons[i].pin) < 0) {\n            // not an ADC analog pin\n            DEBUG_PRINTF_P(PSTR(\"PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\\n\"), buttons[i].pin, i);\n            PinManager::deallocatePin(buttons[i].pin, PinOwner::Button);\n            buttons[i].type = BTN_TYPE_NONE;\n          } else {\n            analogReadResolution(12); // see #4040\n          }\n        } else if ((buttons[i].type == BTN_TYPE_TOUCH || buttons[i].type == BTN_TYPE_TOUCH_SWITCH)) {\n          if (digitalPinToTouchChannel(buttons[i].pin) < 0) {\n            // not a touch pin\n            DEBUG_PRINTF_P(PSTR(\"PIN ALLOC error: GPIO%d for touch button #%d is not an touch pin!\\n\"), buttons[i].pin, i);\n            PinManager::deallocatePin(buttons[i].pin, PinOwner::Button);\n            buttons[i].type = BTN_TYPE_NONE;\n          }          \n          #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\n          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)\n          #endif\n        } else\n        #endif\n        {\n          // regular buttons and switches\n          if (disablePullUp) {\n            pinMode(buttons[i].pin, INPUT);\n          } else {\n            #ifdef ESP32\n            pinMode(buttons[i].pin, buttons[i].type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);\n            #else\n            pinMode(buttons[i].pin, INPUT_PULLUP);\n            #endif\n          }\n        }\n      } else {\n        buttons[i].pin  = -1;\n        buttons[i].type = BTN_TYPE_NONE;\n      }\n    }\n    // we should remove all unused buttons from the vector\n    for (int i = buttons.size()-1; i > 0; i--) {\n      if (buttons[i].pin < 0 && buttons[i].type == BTN_TYPE_NONE) {\n        buttons.erase(buttons.begin() + i); // remove button from vector\n      }\n    }\n\n    briS = request->arg(F(\"CA\")).toInt();\n\n    turnOnAtBoot = request->hasArg(F(\"BO\"));\n    t = request->arg(F(\"BP\")).toInt();\n    if (t <= 250) bootPreset = t;\n    gammaCorrectBri = request->hasArg(F(\"GB\"));\n    gammaCorrectCol = request->hasArg(F(\"GC\"));\n    gammaCorrectVal = request->arg(F(\"GV\")).toFloat();\n    if (gammaCorrectVal < 0.1f || gammaCorrectVal > 3) {\n      gammaCorrectVal = 1.0f; // no gamma correction\n      gammaCorrectBri = false;\n      gammaCorrectCol = false;\n    }\n    NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); // fill look-up tables\n\n    t = request->arg(F(\"TD\")).toInt();\n    if (t >= 0) transitionDelayDefault = t;\n    t = request->arg(F(\"TP\")).toInt();\n    randomPaletteChangeTime = MIN(255,MAX(1,t));\n    useHarmonicRandomPalette = request->hasArg(F(\"TH\"));\n\n    nightlightTargetBri = request->arg(F(\"TB\")).toInt();\n    t = request->arg(F(\"TL\")).toInt();\n    if (t > 0) nightlightDelayMinsDefault = t;\n    nightlightDelayMins = nightlightDelayMinsDefault;\n    nightlightMode = request->arg(F(\"TW\")).toInt();\n\n    t = request->arg(F(\"PB\")).toInt();\n    if (t >= 0 && t < 4) paletteBlend = t;\n    t = request->arg(F(\"BF\")).toInt();\n    if (t > 0) briMultiplier = t;\n\n    doInitBusses = busesChanged;\n  }\n\n  //UI\n  if (subPage == SUBPAGE_UI)\n  {\n    strlcpy(serverDescription, request->arg(F(\"DS\")).c_str(), 33);\n    simplifiedUI = request->hasArg(F(\"SU\"));\n    DEBUG_PRINTLN(F(\"Enumerating ledmaps\"));\n    enumerateLedmaps();\n    DEBUG_PRINTLN(F(\"Loading custom palettes\"));\n    loadCustomPalettes(); // (re)load all custom palettes\n  }\n\n  //SYNC\n  if (subPage == SUBPAGE_SYNC)\n  {\n    int t = request->arg(F(\"UP\")).toInt();\n    if (t > 0) udpPort = t;\n    t = request->arg(F(\"U2\")).toInt();\n    if (t > 0) udpPort2 = t;\n\n    #ifndef WLED_DISABLE_ESPNOW\n    useESPNowSync = request->hasArg(F(\"EN\"));\n    #endif\n\n    syncGroups = request->arg(F(\"GS\")).toInt();\n    receiveGroups = request->arg(F(\"GR\")).toInt();\n\n    receiveNotificationBrightness = request->hasArg(F(\"RB\"));\n    receiveNotificationColor = request->hasArg(F(\"RC\"));\n    receiveNotificationEffects = request->hasArg(F(\"RX\"));\n    receiveNotificationPalette = request->hasArg(F(\"RP\"));\n    receiveSegmentOptions = request->hasArg(F(\"SO\"));\n    receiveSegmentBounds = request->hasArg(F(\"SG\"));\n    sendNotifications = request->hasArg(F(\"SS\"));\n    notifyDirect = request->hasArg(F(\"SD\"));\n    notifyButton = request->hasArg(F(\"SB\"));\n    notifyAlexa = request->hasArg(F(\"SA\"));\n    notifyHue = request->hasArg(F(\"SH\"));\n\n    t = request->arg(F(\"UR\")).toInt();\n    if ((t>=0) && (t<30)) udpNumRetries = t;\n\n\n    nodeListEnabled = request->hasArg(F(\"NL\"));\n    if (!nodeListEnabled) Nodes.clear();\n    nodeBroadcastEnabled = request->hasArg(F(\"NB\"));\n\n    receiveDirect = request->hasArg(F(\"RD\")); // UDP realtime\n    useMainSegmentOnly = request->hasArg(F(\"MO\"));\n    realtimeRespectLedMaps = request->hasArg(F(\"RLM\"));\n    e131SkipOutOfSequence = request->hasArg(F(\"ES\"));\n    e131Multicast = request->hasArg(F(\"EM\"));\n    t = request->arg(F(\"EP\")).toInt();\n    if (t > 0) e131Port = t;\n    t = request->arg(F(\"EU\")).toInt();\n    if (t >= 0  && t <= 63999) e131Universe = t;\n    t = request->arg(F(\"DA\")).toInt();\n    if (t >= 0  && t <= 510) DMXAddress = t;\n    t = request->arg(F(\"XX\")).toInt();\n    if (t >= 0  && t <= 150) DMXSegmentSpacing = t;\n    t = request->arg(F(\"PY\")).toInt();\n    if (t >= 0  && t <= 200) e131Priority = t;\n    t = request->arg(F(\"DM\")).toInt();\n    if (t >= DMX_MODE_DISABLED && t <= DMX_MODE_PRESET) DMXMode = t;\n    t = request->arg(F(\"ET\")).toInt();\n    if (t > 99  && t <= 65000) realtimeTimeoutMs = t;\n    arlsForceMaxBri = request->hasArg(F(\"FB\"));\n    arlsDisableGammaCorrection = request->hasArg(F(\"RG\"));\n    t = request->arg(F(\"WO\")).toInt();\n    if (t >= -255  && t <= 255) arlsOffset = t;\n\n#ifdef WLED_ENABLE_DMX_INPUT\n    dmxInputTransmitPin = request->arg(F(\"IDMT\")).toInt();\n    dmxInputReceivePin = request->arg(F(\"IDMR\")).toInt();\n    dmxInputEnablePin = request->arg(F(\"IDME\")).toInt();\n    dmxInputPort = request->arg(F(\"IDMP\")).toInt();\n    if(dmxInputPort <= 0 || dmxInputPort > 2) dmxInputPort = 2;\n#endif\n\n    #ifndef WLED_DISABLE_ALEXA\n    alexaEnabled = request->hasArg(F(\"AL\"));\n    strlcpy(alexaInvocationName, request->arg(F(\"AI\")).c_str(), 33);\n    t = request->arg(F(\"AP\")).toInt();\n    if (t >= 0 && t <= 9) alexaNumPresets = t;\n    #endif\n\n    #ifndef WLED_DISABLE_MQTT\n    mqttEnabled = request->hasArg(F(\"MQ\"));\n    strlcpy(mqttServer, request->arg(F(\"MS\")).c_str(), MQTT_MAX_SERVER_LEN+1);\n    t = request->arg(F(\"MQPORT\")).toInt();\n    if (t > 0) mqttPort = t;\n    strlcpy(mqttUser, request->arg(F(\"MQUSER\")).c_str(), 41);\n    if (!isAsterisksOnly(request->arg(F(\"MQPASS\")).c_str(), 41)) strlcpy(mqttPass, request->arg(F(\"MQPASS\")).c_str(), 65);\n    strlcpy(mqttClientID, request->arg(F(\"MQCID\")).c_str(), 41);\n    strlcpy(mqttDeviceTopic, request->arg(F(\"MD\")).c_str(), MQTT_MAX_TOPIC_LEN+1);\n    strlcpy(mqttGroupTopic, request->arg(F(\"MG\")).c_str(), MQTT_MAX_TOPIC_LEN+1);\n    buttonPublishMqtt = request->hasArg(F(\"BM\"));\n    retainMqttMsg = request->hasArg(F(\"RT\"));\n    #endif\n\n    #ifndef WLED_DISABLE_HUESYNC\n    for (int i=0;i<4;i++){\n      String a = \"H\"+String(i);\n      hueIP[i] = request->arg(a).toInt();\n    }\n\n    t = request->arg(F(\"HL\")).toInt();\n    if (t > 0) huePollLightId = t;\n\n    t = request->arg(F(\"HI\")).toInt();\n    if (t > 50) huePollIntervalMs = t;\n\n    hueApplyOnOff = request->hasArg(F(\"HO\"));\n    hueApplyBri = request->hasArg(F(\"HB\"));\n    hueApplyColor = request->hasArg(F(\"HC\"));\n    huePollingEnabled = request->hasArg(F(\"HP\"));\n    hueStoreAllowed = true;\n    reconnectHue();\n    #endif\n\n    t = request->arg(F(\"BD\")).toInt();\n    if (t >= 96 && t <= 15000) serialBaud = t;\n    updateBaudRate(serialBaud *100);\n  }\n\n  //TIME\n  if (subPage == SUBPAGE_TIME)\n  {\n    ntpEnabled = request->hasArg(F(\"NT\"));\n    strlcpy(ntpServerName, request->arg(F(\"NS\")).c_str(), 33);\n    useAMPM = !request->hasArg(F(\"CF\"));\n    currentTimezone = request->arg(F(\"TZ\")).toInt();\n    utcOffsetSecs = request->arg(F(\"UO\")).toInt();\n\n    //start ntp if not already connected\n    if (ntpEnabled && WLED_CONNECTED && !ntpConnected) ntpConnected = ntpUdp.begin(ntpLocalPort);\n    ntpLastSyncTime = NTP_NEVER; // force new NTP query\n\n    longitude = request->arg(F(\"LN\")).toFloat();\n    latitude = request->arg(F(\"LT\")).toFloat();\n    // force a sunrise/sunset re-calculation\n    calculateSunriseAndSunset();\n\n    overlayCurrent = request->hasArg(F(\"OL\")) ? 1 : 0;\n\n    overlayMin = request->arg(F(\"O1\")).toInt();\n    overlayMax = request->arg(F(\"O2\")).toInt();\n    analogClock12pixel = request->arg(F(\"OM\")).toInt();\n    analogClock5MinuteMarks = request->hasArg(F(\"O5\"));\n    analogClockSecondsTrail = request->hasArg(F(\"OS\"));\n    analogClockSolidBlack = request->hasArg(F(\"OB\"));\n\n    countdownMode = request->hasArg(F(\"CE\"));\n    countdownYear = request->arg(F(\"CY\")).toInt();\n    countdownMonth = request->arg(F(\"CI\")).toInt();\n    countdownDay = request->arg(F(\"CD\")).toInt();\n    countdownHour = request->arg(F(\"CH\")).toInt();\n    countdownMin = request->arg(F(\"CM\")).toInt();\n    countdownSec = request->arg(F(\"CS\")).toInt();\n    setCountdown();\n\n    macroAlexaOn = request->arg(F(\"A0\")).toInt();\n    macroAlexaOff = request->arg(F(\"A1\")).toInt();\n    macroCountdown = request->arg(F(\"MC\")).toInt();\n    macroNl = request->arg(F(\"MN\")).toInt();\n    int ii = 0;\n    for (auto &button : buttons) {\n      char mp[4] = \"MP\"; mp[2] = (ii<10?'0':'A'-10)+ii; mp[3] = 0; // short\n      char ml[4] = \"ML\"; ml[2] = (ii<10?'0':'A'-10)+ii; ml[3] = 0; // long\n      char md[4] = \"MD\"; md[2] = (ii<10?'0':'A'-10)+ii; md[3] = 0; // double\n      //if (!request->hasArg(mp)) break;\n      button.macroButton = request->arg(mp).toInt();      // these will default to 0 if not present\n      button.macroLongPress = request->arg(ml).toInt();\n      button.macroDoublePress = request->arg(md).toInt();\n      ii++;\n    }\n\n    char k[3]; k[2] = 0;\n    for (int i = 0; i<10; i++) {\n      k[1] = i+48;//ascii 0,1,2,3,...\n      k[0] = 'H'; //timer hours\n      timerHours[i] = request->arg(k).toInt();\n      k[0] = 'N'; //minutes\n      timerMinutes[i] = request->arg(k).toInt();\n      k[0] = 'T'; //macros\n      timerMacro[i] = request->arg(k).toInt();\n      k[0] = 'W'; //weekdays\n      timerWeekday[i] = request->arg(k).toInt();\n      if (i<8) {\n        k[0] = 'M'; //start month\n        timerMonth[i] = request->arg(k).toInt() & 0x0F;\n        timerMonth[i] <<= 4;\n        k[0] = 'P'; //end month\n        timerMonth[i] += (request->arg(k).toInt() & 0x0F);\n        k[0] = 'D'; //start day\n        timerDay[i] = request->arg(k).toInt();\n        k[0] = 'E'; //end day\n        timerDayEnd[i] = request->arg(k).toInt();\n      }\n    }\n  }\n\n  //SECURITY\n  if (subPage == SUBPAGE_SEC)\n  {\n    if (request->hasArg(F(\"RS\"))) //complete factory reset\n    {\n      WLED_FS.format();\n      serveMessage(request, 200, F(\"All Settings erased.\"), F(\"Connect to WLED-AP to setup again\"),255);\n      doReboot = true; // may reboot immediately on dual-core system (race condition) which is desireable in this case\n    }\n\n    if (request->hasArg(F(\"PIN\"))) {\n      const char *pin = request->arg(F(\"PIN\")).c_str();\n      unsigned pinLen = strlen(pin);\n      if (pinLen == 4 || pinLen == 0) {\n        unsigned numZeros = 0;\n        for (unsigned i = 0; i < pinLen; i++) numZeros += (pin[i] == '0');\n        if (numZeros < pinLen || pinLen == 0) { // ignore 0000 input (placeholder)\n          strlcpy(settingsPIN, pin, 5);\n        }\n        settingsPIN[4] = 0;\n      }\n    }\n\n    bool pwdCorrect = !otaLock; //always allow access if ota not locked\n    if (request->hasArg(F(\"OP\")))\n    {\n      if (otaLock && strcmp(otaPass,request->arg(F(\"OP\")).c_str()) == 0)\n      {\n        // brute force protection: do not unlock even if correct if last save was less than 3 seconds ago\n        if (millis() - lastEditTime > PIN_RETRY_COOLDOWN) pwdCorrect = true;\n      }\n      if (!otaLock && request->arg(F(\"OP\")).length() > 0)\n      {\n        strlcpy(otaPass,request->arg(F(\"OP\")).c_str(), 33); // set new OTA password\n      }\n    }\n\n    if (pwdCorrect) //allow changes if correct pwd or no ota active\n    {\n      otaLock = request->hasArg(F(\"NO\"));\n      wifiLock = request->hasArg(F(\"OW\"));\n      #ifndef WLED_DISABLE_OTA\n      aOtaEnabled = request->hasArg(F(\"AO\"));\n      #endif\n      otaSameSubnet = request->hasArg(F(\"SU\"));\n    }\n  }\n\n  #ifdef WLED_ENABLE_DMX // include only if DMX is enabled\n  if (subPage == SUBPAGE_DMX)\n  {\n    int t = request->arg(F(\"PU\")).toInt();\n    if (t >= 0  && t <= 63999) e131ProxyUniverse = t;\n\n    t = request->arg(F(\"CN\")).toInt();\n    if (t>0 && t<16) {\n      DMXChannels = t;\n    }\n    t = request->arg(F(\"CS\")).toInt();\n    if (t>0 && t<513) {\n      DMXStart = t;\n    }\n    t = request->arg(F(\"CG\")).toInt();\n    if (t>0 && t<513) {\n      DMXGap = t;\n    }\n    t = request->arg(F(\"SL\")).toInt();\n    if (t>=0 && t < MAX_LEDS) {\n      DMXStartLED = t;\n    }\n    for (int i=0; i<15; i++) {\n      String argname = \"CH\" + String((i+1));\n      t = request->arg(argname).toInt();\n      DMXFixtureMap[i] = t;\n    }\n  }\n  #endif\n\n  //USERMODS\n  if (subPage == SUBPAGE_UM)\n  {\n    if (!requestJSONBufferLock(JSON_LOCK_SETTINGS)) {\n      request->deferResponse();\n      return;\n    }\n\n    // global I2C & SPI pins\n    int8_t hw_sda_pin  = !request->arg(F(\"SDA\")).length() ? -1 : (int)request->arg(F(\"SDA\")).toInt();\n    int8_t hw_scl_pin  = !request->arg(F(\"SCL\")).length() ? -1 : (int)request->arg(F(\"SCL\")).toInt();\n    if (i2c_sda != hw_sda_pin || i2c_scl != hw_scl_pin) {\n      // only if pins changed\n      uint8_t old_i2c[2] = { static_cast<uint8_t>(i2c_scl), static_cast<uint8_t>(i2c_sda) };\n      PinManager::deallocateMultiplePins(old_i2c, 2, PinOwner::HW_I2C); // just in case deallocation of old pins\n\n      PinManagerPinType i2c[2] = { { hw_sda_pin, true }, { hw_scl_pin, true } };\n      if (hw_sda_pin >= 0 && hw_scl_pin >= 0 && PinManager::allocateMultiplePins(i2c, 2, PinOwner::HW_I2C)) {\n        i2c_sda = hw_sda_pin;\n        i2c_scl = hw_scl_pin;\n        // no bus re-initialisation as usermods do not get any notification\n        //Wire.begin(i2c_sda, i2c_scl);\n      } else {\n        // there is no Wire.end()\n        DEBUG_PRINTLN(F(\"Could not allocate I2C pins.\"));\n        i2c_sda = -1;\n        i2c_scl = -1;\n      }\n    }\n    int8_t hw_mosi_pin = !request->arg(F(\"MOSI\")).length() ? -1 : (int)request->arg(F(\"MOSI\")).toInt();\n    int8_t hw_miso_pin = !request->arg(F(\"MISO\")).length() ? -1 : (int)request->arg(F(\"MISO\")).toInt();\n    int8_t hw_sclk_pin = !request->arg(F(\"SCLK\")).length() ? -1 : (int)request->arg(F(\"SCLK\")).toInt();\n    #ifdef ESP8266\n    // cannot change pins on ESP8266\n    if (hw_mosi_pin >= 0 && hw_mosi_pin != HW_PIN_DATASPI)  hw_mosi_pin = HW_PIN_DATASPI;\n    if (hw_miso_pin >= 0 && hw_miso_pin != HW_PIN_MISOSPI)  hw_mosi_pin = HW_PIN_MISOSPI;\n    if (hw_sclk_pin >= 0 && hw_sclk_pin != HW_PIN_CLOCKSPI) hw_sclk_pin = HW_PIN_CLOCKSPI;\n    #endif\n    if (spi_mosi != hw_mosi_pin || spi_miso != hw_miso_pin || spi_sclk != hw_sclk_pin) {\n      // only if pins changed\n      uint8_t old_spi[3] = { static_cast<uint8_t>(spi_mosi), static_cast<uint8_t>(spi_miso), static_cast<uint8_t>(spi_sclk) };\n      PinManager::deallocateMultiplePins(old_spi, 3, PinOwner::HW_SPI); // just in case deallocation of old pins\n      PinManagerPinType spi[3] = { { hw_mosi_pin, true }, { hw_miso_pin, true }, { hw_sclk_pin, true } };\n      if (hw_mosi_pin >= 0 && hw_sclk_pin >= 0 && PinManager::allocateMultiplePins(spi, 3, PinOwner::HW_SPI)) {\n        spi_mosi = hw_mosi_pin;\n        spi_miso = hw_miso_pin;\n        spi_sclk = hw_sclk_pin;\n        // no bus re-initialisation as usermods do not get any notification\n        //SPI.end();\n        #ifdef ESP32\n        //SPI.begin(spi_sclk, spi_miso, spi_mosi);\n        #else\n        //SPI.begin();\n        #endif\n      } else {\n        //SPI.end();\n        DEBUG_PRINTLN(F(\"Could not allocate SPI pins.\"));\n        spi_mosi = -1;\n        spi_miso = -1;\n        spi_sclk = -1;\n      }\n    }\n\n    JsonObject um = pDoc->createNestedObject(\"um\");\n\n    size_t args = request->args();\n    unsigned j=0;\n    for (size_t i=0; i<args; i++) {\n      String name = request->argName(i);\n      String value = request->arg(i);\n\n      // POST request parameters are combined as <usermodname>_<usermodparameter>\n      int umNameEnd = name.indexOf(\":\");\n      if (umNameEnd<1) continue;  // parameter does not contain \":\" or on 1st place -> wrong\n\n      JsonObject mod = um[name.substring(0,umNameEnd)]; // get a usermod JSON object\n      if (mod.isNull()) {\n        mod = um.createNestedObject(name.substring(0,umNameEnd)); // if it does not exist create it\n      }\n      DEBUG_PRINT(name.substring(0,umNameEnd));\n      DEBUG_PRINT(\":\");\n      name = name.substring(umNameEnd+1); // remove mod name from string\n\n      // if the resulting name still contains \":\" this means nested object\n      JsonObject subObj;\n      int umSubObj = name.indexOf(\":\");\n      DEBUG_PRINTF_P(PSTR(\"(%d):\"),umSubObj);\n      if (umSubObj>0) {\n        subObj = mod[name.substring(0,umSubObj)];\n        if (subObj.isNull())\n          subObj = mod.createNestedObject(name.substring(0,umSubObj));\n        name = name.substring(umSubObj+1); // remove nested object name from string\n      } else {\n        subObj = mod;\n      }\n      DEBUG_PRINT(name);\n\n      // check if parameters represent array\n      if (name.endsWith(\"[]\")) {\n        name.replace(\"[]\",\"\");\n        value.replace(\",\",\".\");      // just in case conversion\n        if (!subObj[name].is<JsonArray>()) {\n          JsonArray ar = subObj.createNestedArray(name);\n          if (value.indexOf(\".\") >= 0) ar.add(value.toFloat());  // we do have a float\n          else                         ar.add(value.toInt());    // we may have an int\n          j=0;\n        } else {\n          if (value.indexOf(\".\") >= 0) subObj[name].add(value.toFloat());  // we do have a float\n          else                         subObj[name].add(value.toInt());    // we may have an int\n          j++;\n        }\n        DEBUG_PRINTF_P(PSTR(\"[%d] = %s\\n\"), j, value.c_str());\n      } else {\n        // we are using a hidden field with the same name as our parameter (!before the actual parameter!)\n        // to describe the type of parameter (text,float,int), for boolean parameters the first field contains \"off\"\n        // so checkboxes have one or two fields (first is always \"false\", existence of second depends on checkmark and may be \"true\")\n        if (subObj[name].isNull()) {\n          // the first occurrence of the field describes the parameter type (used in next loop)\n          if (value == \"false\") subObj[name] = false; // checkboxes may have only one field\n          else                  subObj[name] = value;\n        } else {\n          String type = subObj[name].as<String>();  // get previously stored value as a type\n          if (subObj[name].is<bool>())   subObj[name] = true;   // checkbox/boolean\n          else if (type == \"number\") {\n            value.replace(\",\",\".\");      // just in case conversion\n            if (value.indexOf(\".\") >= 0) subObj[name] = value.toFloat();  // we do have a float\n            else                         subObj[name] = value.toInt();    // we may have an int\n          } else if (type == \"int\")      subObj[name] = value.toInt();\n          else                           subObj[name] = value;  // text fields\n        }\n        DEBUG_PRINTF_P(PSTR(\" = %s\\n\"), value.c_str());\n      }\n    }\n    UsermodManager::readFromConfig(um);  // force change of usermod parameters\n    DEBUG_PRINTLN(F(\"Done re-init UsermodManager::\"));\n    releaseJSONBufferLock();\n  }\n\n  #ifndef WLED_DISABLE_2D\n  //2D panels\n  if (subPage == SUBPAGE_2D)\n  {\n    strip.isMatrix = request->arg(F(\"SOMP\")).toInt();\n    strip.panel.clear();\n    if (strip.isMatrix) {\n      unsigned panels = constrain(request->arg(F(\"MPC\")).toInt(), 1, WLED_MAX_PANELS);\n      strip.panel.reserve(panels); // pre-allocate memory\n      for (unsigned i=0; i<panels; i++) {\n        WS2812FX::Panel p;\n        char pO[8] = { '\\0' };\n        snprintf_P(pO, 7, PSTR(\"P%d\"), i);       // WLED_MAX_PANELS is less than 100 so pO will always only be 4 characters or less\n        pO[7] = '\\0';\n        unsigned l = strlen(pO);\n        // create P0B, P1B, ..., P63B, etc for other PxxX\n        pO[l] = 'B'; if (!request->hasArg(pO)) break;\n        pO[l] = 'B'; p.bottomStart = request->arg(pO).toInt();\n        pO[l] = 'R'; p.rightStart  = request->arg(pO).toInt();\n        pO[l] = 'V'; p.vertical    = request->arg(pO).toInt();\n        pO[l] = 'S'; p.serpentine  = request->hasArg(pO);\n        pO[l] = 'X'; p.xOffset     = request->arg(pO).toInt();\n        pO[l] = 'Y'; p.yOffset     = request->arg(pO).toInt();\n        pO[l] = 'W'; p.width       = request->arg(pO).toInt();\n        pO[l] = 'H'; p.height      = request->arg(pO).toInt();\n        strip.panel.push_back(p);\n      }\n    }\n    strip.panel.shrink_to_fit();  // release unused memory\n    // we are changing matrix/ledmap geometry which *will* affect existing segments\n    // since we are not in loop() context we must make sure that effects are not running. credit @blazonchek for properly fixing #4911\n    strip.suspend();\n    strip.waitForIt();\n    strip.deserializeMap(); // (re)load default ledmap (will also setUpMatrix() if ledmap does not exist)\n    strip.makeAutoSegments(true); // force re-creation of segments\n    strip.resume();\n  }\n  #endif\n\n  lastEditTime = millis();\n  // do not save if factory reset or LED settings (which are saved after LED re-init)\n  configNeedsWrite = subPage != SUBPAGE_LEDS && !(subPage == SUBPAGE_SEC && doReboot);\n  if (subPage == SUBPAGE_UM) doReboot = request->hasArg(F(\"RBT\")); // prevent race condition on dual core system (set reboot here, after configNeedsWrite has been set)\n  #ifndef WLED_DISABLE_ALEXA\n  if (subPage == SUBPAGE_SYNC) alexaInit();\n  #endif\n}\n\n\n//HTTP API request parser\nbool handleSet(AsyncWebServerRequest *request, const String& req, bool apply)\n{\n  if (!(req.indexOf(\"win\") >= 0)) return false;\n\n  int pos = 0;\n  DEBUG_PRINTF_P(PSTR(\"API req: %s\\n\"), req.c_str());\n\n  //segment select (sets main segment)\n  pos = req.indexOf(F(\"SM=\"));\n  if (pos > 0 && !realtimeMode) {\n    strip.setMainSegmentId(getNumVal(req, pos));\n  }\n\n  byte selectedSeg = strip.getFirstSelectedSegId();\n\n  bool singleSegment = false;\n\n  pos = req.indexOf(F(\"SS=\"));\n  if (pos > 0) {\n    unsigned t = getNumVal(req, pos);\n    if (t < strip.getSegmentsNum()) {\n      selectedSeg = t;\n      singleSegment = true;\n    }\n  }\n\n  Segment& selseg = strip.getSegment(selectedSeg);\n  pos = req.indexOf(F(\"SV=\")); //segment selected\n  if (pos > 0) {\n    unsigned t = getNumVal(req, pos);\n    if (t == 2) for (unsigned i = 0; i < strip.getSegmentsNum(); i++) strip.getSegment(i).selected = false; // unselect other segments\n    selseg.selected = t;\n  }\n\n  // temporary values, write directly to segments, globals are updated by setValuesFromFirstSelectedSeg()\n  uint32_t col0    = selseg.colors[0];\n  uint32_t col1    = selseg.colors[1];\n  uint32_t col2    = selseg.colors[2];\n  byte colIn[4]    = {R(col0), G(col0), B(col0), W(col0)};\n  byte colInSec[4] = {R(col1), G(col1), B(col1), W(col1)};\n  byte effectIn    = selseg.mode;\n  byte speedIn     = selseg.speed;\n  byte intensityIn = selseg.intensity;\n  byte paletteIn   = selseg.palette;\n  byte custom1In   = selseg.custom1;\n  byte custom2In   = selseg.custom2;\n  byte custom3In   = selseg.custom3;\n  byte check1In    = selseg.check1;\n  byte check2In    = selseg.check2;\n  byte check3In    = selseg.check3;\n  uint16_t startI  = selseg.start;\n  uint16_t stopI   = selseg.stop;\n  uint16_t startY  = selseg.startY;\n  uint16_t stopY   = selseg.stopY;\n  uint8_t  grpI    = selseg.grouping;\n  uint16_t spcI    = selseg.spacing;\n  pos = req.indexOf(F(\"&S=\")); //segment start\n  if (pos > 0) {\n    startI = std::abs(getNumVal(req, pos));\n  }\n  pos = req.indexOf(F(\"S2=\")); //segment stop\n  if (pos > 0) {\n    stopI = std::abs(getNumVal(req, pos));\n  }\n  pos = req.indexOf(F(\"GP=\")); //segment grouping\n  if (pos > 0) {\n    grpI = std::max(1,getNumVal(req, pos));\n  }\n  pos = req.indexOf(F(\"SP=\")); //segment spacing\n  if (pos > 0) {\n    spcI = std::max(0,getNumVal(req, pos));\n  }\n  strip.suspend(); // must suspend strip operations before changing geometry\n  selseg.setGeometry(startI, stopI, grpI, spcI, UINT16_MAX, startY, stopY, selseg.map1D2D);\n  strip.resume();\n\n  pos = req.indexOf(F(\"RV=\")); //Segment reverse\n  if (pos > 0) selseg.reverse = req.charAt(pos+3) != '0';\n\n  pos = req.indexOf(F(\"MI=\")); //Segment mirror\n  if (pos > 0) selseg.mirror = req.charAt(pos+3) != '0';\n\n  pos = req.indexOf(F(\"SB=\")); //Segment brightness/opacity\n  if (pos > 0) {\n    byte segbri = getNumVal(req, pos);\n    selseg.setOption(SEG_OPTION_ON, segbri); // use transition\n    if (segbri) {\n      selseg.setOpacity(segbri);\n    }\n  }\n\n  pos = req.indexOf(F(\"SW=\")); //segment power\n  if (pos > 0) {\n    switch (getNumVal(req, pos)) {\n      case 0:  selseg.setOption(SEG_OPTION_ON, false);      break; // use transition\n      case 1:  selseg.setOption(SEG_OPTION_ON, true);       break; // use transition\n      default: selseg.setOption(SEG_OPTION_ON, !selseg.on); break; // use transition\n    }\n  }\n\n  pos = req.indexOf(F(\"PS=\")); //saves current in preset\n  if (pos > 0) savePreset(getNumVal(req, pos));\n\n  pos = req.indexOf(F(\"P1=\")); //sets first preset for cycle\n  if (pos > 0) presetCycMin = getNumVal(req, pos);\n\n  pos = req.indexOf(F(\"P2=\")); //sets last preset for cycle\n  if (pos > 0) presetCycMax = getNumVal(req, pos);\n\n  //apply preset\n  if (updateVal(req.c_str(), \"PL=\", presetCycCurr, presetCycMin, presetCycMax)) {\n    applyPreset(presetCycCurr);\n  }\n\n  pos = req.indexOf(F(\"NP\")); //advances to next preset in a playlist\n  if (pos > 0) doAdvancePlaylist = true;\n  \n  //set brightness\n  updateVal(req.c_str(), \"&A=\", bri);\n\n  bool col0Changed = false, col1Changed = false, col2Changed = false;\n  //set colors\n  col0Changed |= updateVal(req.c_str(), \"&R=\", colIn[0]);\n  col0Changed |= updateVal(req.c_str(), \"&G=\", colIn[1]);\n  col0Changed |= updateVal(req.c_str(), \"&B=\", colIn[2]);\n  col0Changed |= updateVal(req.c_str(), \"&W=\", colIn[3]);\n\n  col1Changed |= updateVal(req.c_str(), \"R2=\", colInSec[0]);\n  col1Changed |= updateVal(req.c_str(), \"G2=\", colInSec[1]);\n  col1Changed |= updateVal(req.c_str(), \"B2=\", colInSec[2]);\n  col1Changed |= updateVal(req.c_str(), \"W2=\", colInSec[3]);\n\n  #ifdef WLED_ENABLE_LOXONE\n  //lox parser\n  pos = req.indexOf(F(\"LX=\")); // Lox primary color\n  if (pos > 0) {\n    int lxValue = getNumVal(req, pos);\n    if (parseLx(lxValue, colIn)) {\n      bri = 255;\n      nightlightActive = false; //always disable nightlight when toggling\n      col0Changed = true;\n    }\n  }\n  pos = req.indexOf(F(\"LY=\")); // Lox secondary color\n  if (pos > 0) {\n    int lxValue = getNumVal(req, pos);\n    if(parseLx(lxValue, colInSec)) {\n      bri = 255;\n      nightlightActive = false; //always disable nightlight when toggling\n      col1Changed = true;\n    }\n  }\n  #endif\n\n  //set hue\n  pos = req.indexOf(F(\"HU=\"));\n  if (pos > 0) {\n    uint16_t temphue = getNumVal(req, pos);\n    byte tempsat = 255;\n    pos = req.indexOf(F(\"SA=\"));\n    if (pos > 0) {\n      tempsat = getNumVal(req, pos);\n    }\n    byte sec = req.indexOf(F(\"H2\"));\n    colorHStoRGB(temphue, tempsat, (sec>0) ? colInSec : colIn);\n    col0Changed |= (!sec); col1Changed |= sec;\n  }\n\n  //set white spectrum (kelvin)\n  pos = req.indexOf(F(\"&K=\"));\n  if (pos > 0) {\n    byte sec = req.indexOf(F(\"K2\"));\n    colorKtoRGB(getNumVal(req, pos), (sec>0) ? colInSec : colIn);\n    col0Changed |= (!sec); col1Changed |= sec;\n  }\n\n  //set color from HEX or 32bit DEC\n  pos = req.indexOf(F(\"CL=\"));\n  if (pos > 0) {\n    colorFromDecOrHexString(colIn, (char*)req.substring(pos + 3).c_str());\n    col0Changed = true;\n  }\n  pos = req.indexOf(F(\"C2=\"));\n  if (pos > 0) {\n    colorFromDecOrHexString(colInSec, (char*)req.substring(pos + 3).c_str());\n    col1Changed = true;\n  }\n  pos = req.indexOf(F(\"C3=\"));\n  if (pos > 0) {\n    byte tmpCol[4];\n    colorFromDecOrHexString(tmpCol, (char*)req.substring(pos + 3).c_str());\n    col2 = RGBW32(tmpCol[0], tmpCol[1], tmpCol[2], tmpCol[3]);\n    selseg.setColor(2, col2); // defined above (SS= or main)\n    col2Changed = true;\n  }\n\n  //set to random hue SR=0->1st SR=1->2nd\n  pos = req.indexOf(F(\"SR\"));\n  if (pos > 0) {\n    byte sec = getNumVal(req, pos);\n    setRandomColor(sec? colInSec : colIn);\n    col0Changed |= (!sec); col1Changed |= sec;\n  }\n\n  // apply colors to selected segment, and all selected segments if applicable\n  if (col0Changed) {\n    col0 = RGBW32(colIn[0], colIn[1], colIn[2], colIn[3]);\n    selseg.setColor(0, col0);\n  }\n\n  if (col1Changed) {\n    col1 = RGBW32(colInSec[0], colInSec[1], colInSec[2], colInSec[3]);\n    selseg.setColor(1, col1);\n  }\n\n  //swap 2nd & 1st\n  pos = req.indexOf(F(\"SC\"));\n  if (pos > 0) {\n    std::swap(col0,col1);\n    col0Changed = col1Changed = true;\n  }\n\n  bool fxModeChanged = false, speedChanged = false, intensityChanged = false, paletteChanged = false;\n  bool custom1Changed = false, custom2Changed = false, custom3Changed = false, check1Changed = false, check2Changed = false, check3Changed = false;\n  // set effect parameters\n  if (updateVal(req.c_str(), \"FX=\", effectIn, 0, strip.getModeCount()-1)) {\n    if (request != nullptr) unloadPlaylist(); // unload playlist if changing FX using web request\n    fxModeChanged = true;\n  }\n  speedChanged     = updateVal(req.c_str(), \"SX=\", speedIn);\n  intensityChanged = updateVal(req.c_str(), \"IX=\", intensityIn);\n  paletteChanged   = updateVal(req.c_str(), \"FP=\", paletteIn, 0, getPaletteCount()-1);\n  custom1Changed   = updateVal(req.c_str(), \"X1=\", custom1In);\n  custom2Changed   = updateVal(req.c_str(), \"X2=\", custom2In);\n  custom3Changed   = updateVal(req.c_str(), \"X3=\", custom3In);\n  check1Changed    = updateVal(req.c_str(), \"M1=\", check1In);\n  check2Changed    = updateVal(req.c_str(), \"M2=\", check2In);\n  check3Changed    = updateVal(req.c_str(), \"M3=\", check3In);\n\n  stateChanged |= (fxModeChanged || speedChanged || intensityChanged || paletteChanged || custom1Changed || custom2Changed || custom3Changed || check1Changed || check2Changed || check3Changed);\n\n  // apply to main and all selected segments to prevent #1618.\n  for (unsigned i = 0; i < strip.getSegmentsNum(); i++) {\n    Segment& seg = strip.getSegment(i);\n    if (i != selectedSeg && (singleSegment || !seg.isActive() || !seg.isSelected())) continue; // skip non main segments if not applying to all\n    if (fxModeChanged)    seg.setMode(effectIn, req.indexOf(F(\"FXD=\"))>0);  // apply defaults if FXD= is specified\n    if (speedChanged)     seg.speed     = speedIn;\n    if (intensityChanged) seg.intensity = intensityIn;\n    if (paletteChanged)   seg.setPalette(paletteIn);\n    if (col0Changed)      seg.setColor(0, col0);\n    if (col1Changed)      seg.setColor(1, col1);\n    if (col2Changed)      seg.setColor(2, col2);\n    if (custom1Changed)   seg.custom1   = custom1In;\n    if (custom2Changed)   seg.custom2   = custom2In;\n    if (custom3Changed)   seg.custom3   = custom3In;\n    if (check1Changed)    seg.check1    = (bool)check1In;\n    if (check2Changed)    seg.check2    = (bool)check2In;\n    if (check3Changed)    seg.check3    = (bool)check3In;\n  }\n\n  //set advanced overlay\n  pos = req.indexOf(F(\"OL=\"));\n  if (pos > 0) {\n    overlayCurrent = getNumVal(req, pos);\n  }\n\n  //apply macro (deprecated, added for compatibility with pre-0.11 automations)\n  pos = req.indexOf(F(\"&M=\"));\n  if (pos > 0) {\n    applyPreset(getNumVal(req, pos) + 16);\n  }\n\n  //toggle send UDP direct notifications\n  pos = req.indexOf(F(\"SN=\"));\n  if (pos > 0) notifyDirect = (req.charAt(pos+3) != '0');\n\n  //toggle receive UDP direct notifications\n  pos = req.indexOf(F(\"RN=\"));\n  if (pos > 0) receiveGroups = (req.charAt(pos+3) != '0') ? receiveGroups | 1 : receiveGroups & 0xFE;\n\n  //receive live data via UDP/Hyperion\n  pos = req.indexOf(F(\"RD=\"));\n  if (pos > 0) receiveDirect = (req.charAt(pos+3) != '0');\n\n  //main toggle on/off (parse before nightlight, #1214)\n  pos = req.indexOf(F(\"&T=\"));\n  if (pos > 0) {\n    nightlightActive = false; //always disable nightlight when toggling\n    switch (getNumVal(req, pos))\n    {\n      case 0: if (bri != 0){briLast = bri; bri = 0;} break; //off, only if it was previously on\n      case 1: if (bri == 0) bri = briLast; break; //on, only if it was previously off\n      default: toggleOnOff(); //toggle\n    }\n  }\n\n  //toggle nightlight mode\n  bool aNlDef = false;\n  if (req.indexOf(F(\"&ND\")) > 0) aNlDef = true;\n  pos = req.indexOf(F(\"NL=\"));\n  if (pos > 0)\n  {\n    if (req.charAt(pos+3) == '0')\n    {\n      nightlightActive = false;\n    } else {\n      nightlightActive = true;\n      if (!aNlDef) nightlightDelayMins = getNumVal(req, pos);\n      else         nightlightDelayMins = nightlightDelayMinsDefault;\n      nightlightStartTime = millis();\n    }\n  } else if (aNlDef)\n  {\n    nightlightActive = true;\n    nightlightDelayMins = nightlightDelayMinsDefault;\n    nightlightStartTime = millis();\n  }\n\n  //set nightlight target brightness\n  pos = req.indexOf(F(\"NT=\"));\n  if (pos > 0) {\n    nightlightTargetBri = getNumVal(req, pos);\n    nightlightActiveOld = false; //re-init\n  }\n\n  //toggle nightlight fade\n  pos = req.indexOf(F(\"NF=\"));\n  if (pos > 0)\n  {\n    nightlightMode = getNumVal(req, pos);\n\n    nightlightActiveOld = false; //re-init\n  }\n  if (nightlightMode > NL_MODE_SUN) nightlightMode = NL_MODE_SUN;\n\n  pos = req.indexOf(F(\"TT=\"));\n  if (pos > 0) transitionDelay = getNumVal(req, pos);\n  strip.setTransition(transitionDelay);\n\n  //set time (unix timestamp)\n  pos = req.indexOf(F(\"ST=\"));\n  if (pos > 0) {\n    setTimeFromAPI(getNumVal(req, pos));\n  }\n\n  //set countdown goal (unix timestamp)\n  pos = req.indexOf(F(\"CT=\"));\n  if (pos > 0) {\n    countdownTime = getNumVal(req, pos);\n    if (countdownTime - toki.second() > 0) countdownOverTriggered = false;\n  }\n\n  pos = req.indexOf(F(\"LO=\"));\n  if (pos > 0) {\n    realtimeOverride = getNumVal(req, pos);\n    if (realtimeOverride > 2) realtimeOverride = REALTIME_OVERRIDE_ALWAYS;\n    if (realtimeMode && useMainSegmentOnly) {\n      strip.getMainSegment().freeze = !realtimeOverride;\n      realtimeOverride = REALTIME_OVERRIDE_NONE;  // ignore request for override if using main segment only\n    }\n  }\n\n  pos = req.indexOf(F(\"RB\"));\n  if (pos > 0) doReboot = true;\n\n  // clock mode, 0: normal, 1: countdown\n  pos = req.indexOf(F(\"NM=\"));\n  if (pos > 0) countdownMode = (req.charAt(pos+3) != '0');\n\n  pos = req.indexOf(F(\"U0=\")); //user var 0\n  if (pos > 0) {\n    userVar0 = getNumVal(req, pos);\n  }\n\n  pos = req.indexOf(F(\"U1=\")); //user var 1\n  if (pos > 0) {\n    userVar1 = getNumVal(req, pos);\n  }\n  // you can add more if you need\n\n  // global colPri[], effectCurrent, ... are updated in stateChanged()\n  if (!apply) return true; // when called by JSON API, do not call colorUpdated() here\n\n  pos = req.indexOf(F(\"&NN\")); //do not send UDP notifications this time\n  stateUpdated((pos > 0) ? CALL_MODE_NO_NOTIFY : CALL_MODE_DIRECT_CHANGE);\n\n  // internal call, does not send XML response\n  pos = req.indexOf(F(\"IN\"));\n  if ((request != nullptr) && (pos < 1)) {\n    auto response = request->beginResponseStream(\"text/xml\");\n    XML_response(*response);\n    request->send(response);\n  }\n\n  return true;\n}\n"
  },
  {
    "path": "wled00/src/dependencies/dmx/ESPDMX.cpp",
    "content": "// - - - - -\n// ESPDMX - A Arduino library for sending and receiving DMX using the builtin serial hardware port.\n// ESPDMX.cpp: Library implementation file\n//\n// Copyright (C) 2015  Rick <ricardogg95@gmail.com>\n// This work is licensed under a GNU style license.\n//\n// Last change: Marcel Seerig <https://github.com/mseerig>\n//\n// Documentation and samples are available at https://github.com/Rickgg/ESP-Dmx\n// - - - - -\n\n/* ----- LIBRARIES ----- */\n#if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2)\n\n#include <Arduino.h>\n\n#include \"ESPDMX.h\"\n\n\n\n#define dmxMaxChannel  512\n#define defaultMax 32\n\n#define DMXSPEED       250000\n#define DMXFORMAT      SERIAL_8N2\n#define BREAKSPEED     83333\n#define BREAKFORMAT    SERIAL_8N1\n\nbool dmxStarted = false;\nint sendPin = 2;\t\t//default on ESP8266\n\n//DMX value array and size. Entry 0 will hold startbyte, so we need 512+1 elements\nuint8_t dmxDataStore[dmxMaxChannel+1] = {};\nint channelSize;\n\n\nvoid DMXESPSerial::init() {\n  channelSize = defaultMax;\n\n  Serial1.begin(DMXSPEED);\n  pinMode(sendPin, OUTPUT);\n  dmxStarted = true;\n}\n\n// Set up the DMX-Protocol\nvoid DMXESPSerial::init(int chanQuant) {\n\n  if (chanQuant > dmxMaxChannel || chanQuant <= 0) {\n    chanQuant = defaultMax;\n  }\n\n  channelSize = chanQuant;\n\n  Serial1.begin(DMXSPEED);\n  pinMode(sendPin, OUTPUT);\n  dmxStarted = true;\n}\n\n// Function to read DMX data\nuint8_t DMXESPSerial::read(int Channel) {\n  if (dmxStarted == false) init();\n\n  if (Channel < 1) Channel = 1;\n  if (Channel > dmxMaxChannel) Channel = dmxMaxChannel;\n  return(dmxDataStore[Channel]);\n}\n\n// Function to send DMX data\nvoid DMXESPSerial::write(int Channel, uint8_t value) {\n  if (dmxStarted == false) init();\n\n  if (Channel < 1) Channel = 1;\n  if (Channel > channelSize) Channel = channelSize;\n  if (value < 0) value = 0;\n  if (value > 255) value = 255;\n\n  dmxDataStore[Channel] = value;\n}\n\nvoid DMXESPSerial::end() {\n  channelSize = 0;\n  Serial1.end();\n  dmxStarted = false;\n}\n\nvoid DMXESPSerial::update() {\n  if (dmxStarted == false) init();\n\n  //Send break\n  digitalWrite(sendPin, HIGH);\n  Serial1.begin(BREAKSPEED, BREAKFORMAT);\n  Serial1.write(0);\n  Serial1.flush();\n  delay(1);\n  Serial1.end();\n\n  //send data\n  Serial1.begin(DMXSPEED, DMXFORMAT);\n  digitalWrite(sendPin, LOW);\n  Serial1.write(dmxDataStore, channelSize);\n  Serial1.flush();\n  delay(1);\n  Serial1.end();\n}\n\n// Function to update the DMX bus\n\n#endif\n"
  },
  {
    "path": "wled00/src/dependencies/dmx/ESPDMX.h",
    "content": "// - - - - -\n// ESPDMX - A Arduino library for sending and receiving DMX using the builtin serial hardware port.\n// ESPDMX.cpp: Library implementation file\n//\n// Copyright (C) 2015  Rick <ricardogg95@gmail.com>\n// This work is licensed under a GNU style license.\n//\n// Last change: Marcel Seerig <https://github.com/mseerig>\n//\n// Documentation and samples are available at https://github.com/Rickgg/ESP-Dmx\n// - - - - -\n\n#include <inttypes.h>\n\n\n#ifndef ESPDMX_h\n#define ESPDMX_h\n\n// ---- Methods ----\n\nclass DMXESPSerial {\npublic:\n  void init();\n  void init(int MaxChan);\n  uint8_t read(int Channel);\n  void write(int channel, uint8_t value);\n  void update();\n  void end();\n};\n\n#endif\n"
  },
  {
    "path": "wled00/src/dependencies/dmx/LICENSE.md",
    "content": "SparkFun License Information\n============================\n\nSparkFun uses two different licenses for our files — one for hardware and one for code.\n\nHardware\n---------\n\n**SparkFun hardware is released under [Creative Commons Share-alike 4.0 International](http://creativecommons.org/licenses/by-sa/4.0/).**\n\nNote: This is a human-readable summary of (and not a substitute for) the [license](http://creativecommons.org/licenses/by-sa/4.0/legalcode).\n\nYou are free to:\n\nShare — copy and redistribute the material in any medium or format\nAdapt — remix, transform, and build upon the material\nfor any purpose, even commercially.\nThe licensor cannot revoke these freedoms as long as you follow the license terms.\nUnder the following terms:\n\nAttribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.\nShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.\nNo additional restrictions — You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits.\nNotices:\n\nYou do not have to comply with the license for elements of the material in the public domain or where your use is permitted by an applicable exception or limitation.\nNo warranties are given. The license may not give you all of the permissions necessary for your intended use. For example, other rights such as publicity, privacy, or moral rights may limit how you use the material.\n\n\nCode\n--------\n\n**SparkFun code, firmware, and software is released under the MIT License(http://opensource.org/licenses/MIT).**\n\nThe MIT License (MIT)\n\nCopyright (c) 2016 SparkFun Electronics\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "wled00/src/dependencies/dmx/SparkFunDMX.cpp",
    "content": "/******************************************************************************\nSparkFunDMX.h\nArduino Library for the SparkFun ESP32 LED to DMX Shield\nAndy England @ SparkFun Electronics\n7/22/2019\n\nDevelopment environment specifics:\nArduino IDE 1.6.4\n\nThis code is released under the [MIT License](http://opensource.org/licenses/MIT).\nPlease review the LICENSE.md file included with this example. If you have any questions \nor concerns with licensing, please contact techsupport@sparkfun.com.\nDistributed as-is; no warranty is given.\n******************************************************************************/\n\n/* ----- LIBRARIES ----- */\n#if defined(ARDUINO_ARCH_ESP32)\n\n#include <Arduino.h>\n#if !defined(CONFIG_IDF_TARGET_ESP32C3)  && !defined(CONFIG_IDF_TARGET_ESP32S2)\n\n#include \"SparkFunDMX.h\"\n#include <HardwareSerial.h>\n\n#define dmxMaxChannel  512\n#define defaultMax 32\n\n#define DMXSPEED       250000\n#define DMXFORMAT      SERIAL_8N2\n#define BREAKSPEED     83333\n#define BREAKFORMAT    SERIAL_8N1\n\nstatic const int enablePin = -1;\t\t// disable the enable pin because it is not needed\nstatic const int rxPin = -1;       // disable the receiving pin because it is not needed - softhack007: Pin=-1 means \"use default\" not \"disable\"\nstatic const int txPin = 2;        // transmit DMX data over this pin (default is pin 2)\n\n//DMX value array and size. Entry 0 will hold startbyte, so we need 512+1 elements\nstatic uint8_t dmxData[dmxMaxChannel+1] = { 0 };\nstatic int chanSize = 0;\n#if !defined(DMX_SEND_ONLY)\nstatic int currentChannel = 0;\n#endif\n\n// Some new MCUs (-S2, -C3) don't have HardwareSerial(2)\n#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0)\n  #if SOC_UART_NUM < 3\n  #error DMX output is not possible on your MCU, as it does not have HardwareSerial(2)\n  #endif\n#endif\n\nstatic HardwareSerial DMXSerial(2);\n\n/* Interrupt Timer for DMX Receive */\n#if !defined(DMX_SEND_ONLY)\nstatic hw_timer_t * timer = NULL;\nstatic portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;\n#endif\n\nstatic volatile int _interruptCounter = 0;\nstatic volatile bool _startCodeDetected = false;\n\n\n#if !defined(DMX_SEND_ONLY)\n/* Start Code is detected by 21 low interrupts */\nvoid IRAM_ATTR onTimer() {\n\tif ((rxPin >= 0) && (digitalRead(rxPin) == 1))\n\t{\n\t\t_interruptCounter = 0; //If the RX Pin is high, we are not in an interrupt\n\t}\n\telse\n\t{\n\t\t_interruptCounter++;\n\t}\n\tif (_interruptCounter > 9)\n\t{\t\n\t\tportENTER_CRITICAL_ISR(&timerMux);\n\t\t_startCodeDetected = true;\n\t\tDMXSerial.begin(DMXSPEED, DMXFORMAT, rxPin, txPin);\n\t\tportEXIT_CRITICAL_ISR(&timerMux);\n\t\t_interruptCounter = 0;\n\t}\n}\n\nvoid SparkFunDMX::initRead(int chanQuant) {\n\t\n  timer = timerBegin(0, 1, true);\n  timerAttachInterrupt(timer, &onTimer, true);\n  timerAlarmWrite(timer, 320, true);\n  timerAlarmEnable(timer);\n  _READWRITE = _READ;\n  if (chanQuant > dmxMaxChannel || chanQuant <= 0) \n  {\n    chanQuant = defaultMax;\n  }\n  chanSize = chanQuant;\n  if (enablePin >= 0) {\n    pinMode(enablePin, OUTPUT);\n    digitalWrite(enablePin, LOW);\n  }\n  if (rxPin >= 0) pinMode(rxPin, INPUT);\n}\n#endif\n\n// Set up the DMX-Protocol\nvoid SparkFunDMX::initWrite (int chanQuant) {\n\n  _READWRITE = _WRITE;\n  if (chanQuant > dmxMaxChannel || chanQuant <= 0) {\n    chanQuant = defaultMax;\n  }\n\n  chanSize = chanQuant + 1; //Add 1 for start code\n\n  DMXSerial.begin(DMXSPEED, DMXFORMAT, rxPin, txPin);\n  if (enablePin >= 0) {\n    pinMode(enablePin, OUTPUT);\n    digitalWrite(enablePin, HIGH);\n  }\n}\n\n#if !defined(DMX_SEND_ONLY)\n// Function to read DMX data\nuint8_t SparkFunDMX::read(int Channel) {\n  if (Channel > chanSize) Channel = chanSize;\n  return(dmxData[Channel - 1]); //subtract one to account for start byte\n}\n#endif\n\n// Function to send DMX data\nvoid SparkFunDMX::write(int Channel, uint8_t value) {\n  if (Channel < 0) Channel = 0;\n  if (Channel > chanSize) chanSize = Channel;\n  dmxData[0] = 0;\n  dmxData[Channel] = value; //add one to account for start byte\n}\n\n\n\nvoid SparkFunDMX::update() {\n  if (_READWRITE == _WRITE)\n  {\n    //Send DMX break\n    digitalWrite(txPin, HIGH);\n    DMXSerial.begin(BREAKSPEED, BREAKFORMAT, rxPin, txPin);//Begin the Serial port\n    DMXSerial.write(0);\n    DMXSerial.flush();\n    delay(1);\n    DMXSerial.end();\n    \n    //Send DMX data\n    DMXSerial.begin(DMXSPEED, DMXFORMAT, rxPin, txPin);//Begin the Serial port\n    DMXSerial.write(dmxData, chanSize);\n    DMXSerial.flush();\n    DMXSerial.end();//clear our DMX array, end the Hardware Serial port\n  }\n#if !defined(DMX_SEND_ONLY)\n  else if (_READWRITE == _READ)//In a perfect world, this function ends serial communication upon packet completion and attaches RX to a CHANGE interrupt so the start code can be read again\n  { \n\tif (_startCodeDetected == true)\n\t{\n\t\twhile (DMXSerial.available())\n\t\t{\n\t\t\tdmxData[currentChannel++] = DMXSerial.read();\n\t\t}\n\tif (currentChannel > chanSize) //Set the channel counter back to 0 if we reach the known end size of our packet\n\t{\n\t\t\n      portENTER_CRITICAL(&timerMux);\n\t  _startCodeDetected = false;\n\t  DMXSerial.flush();\n\t  DMXSerial.end();\n      portEXIT_CRITICAL(&timerMux);\n\t  currentChannel = 0;\n\t}\n\t}\n  }\n#endif\n}\n\n// Function to update the DMX bus\n#endif\n#endif\n"
  },
  {
    "path": "wled00/src/dependencies/dmx/SparkFunDMX.h",
    "content": "/******************************************************************************\nSparkFunDMX.h\nArduino Library for the SparkFun ESP32 LED to DMX Shield\nAndy England @ SparkFun Electronics\n7/22/2019\n\nDevelopment environment specifics:\nArduino IDE 1.6.4\n\nThis code is released under the [MIT License](http://opensource.org/licenses/MIT).\nPlease review the LICENSE.md file included with this example. If you have any questions \nor concerns with licensing, please contact techsupport@sparkfun.com.\nDistributed as-is; no warranty is given.\n******************************************************************************/\n\n#include <inttypes.h>\n\n\n#ifndef SparkFunDMX_h\n#define SparkFunDMX_h\n\n#define DMX_SEND_ONLY // this disables DMX sending features, and saves us two GPIO pins\n\n// ---- Methods ----\n\nclass SparkFunDMX {\npublic:\n  void initWrite(int maxChan);\n#if !defined(DMX_SEND_ONLY)\n  void initRead(int maxChan);\n  uint8_t read(int Channel);\n#endif\n  void write(int channel, uint8_t value);\n  void update();\nprivate:\n  const uint8_t _startCodeValue = 0xFF;\n  const bool _READ = true;\n  const bool _WRITE = false;\n  bool _READWRITE;\n};\n\n#endif"
  },
  {
    "path": "wled00/src/dependencies/e131/ESPAsyncE131.cpp",
    "content": "/*\n* ESPAsyncE131.cpp\n*\n* Project: ESPAsyncE131 - Asynchronous E.131 (sACN) library for Arduino ESP8266 and ESP32\n* Copyright (c) 2019 Shelby Merrick\n* http://www.forkineye.com\n*\n*  This program is provided free for you to use in any way that you wish,\n*  subject to the laws and regulations where you are using it.  Due diligence\n*  is strongly suggested before using this code.  Please give credit where due.\n*\n*  The Author makes no warranty of any kind, express or implied, with regard\n*  to this program or the documentation contained in this document.  The\n*  Author shall not be liable in any event for incidental or consequential\n*  damages in connection with, or arising out of, the furnishing, performance\n*  or use of these programs.\n*\n*/\n\n#include \"ESPAsyncE131.h\"\n#include \"../network/Network.h\"\n#include <string.h>\n\n// E1.17 ACN Packet Identifier\nconst byte ESPAsyncE131::ACN_ID[12] = { 0x41, 0x53, 0x43, 0x2d, 0x45, 0x31, 0x2e, 0x31, 0x37, 0x00, 0x00, 0x00 };\n\n// Art-Net Packet Identifier\nconst byte ESPAsyncE131::ART_ID[8]  = { 0x41, 0x72, 0x74, 0x2d, 0x4e, 0x65, 0x74, 0x00 };\n\n// Constructor\nESPAsyncE131::ESPAsyncE131(e131_packet_callback_function callback) {\n  _callback = callback;\n}\n\n/////////////////////////////////////////////////////////\n//\n// Public begin() members\n//\n/////////////////////////////////////////////////////////\n\nbool ESPAsyncE131::begin(bool multicast, uint16_t port, uint16_t universe, uint8_t n) {\n  bool success = false;\n\n  if (multicast) {\n\t\tsuccess = initMulticast(port, universe, n);\n\t} else {\n    success = initUnicast(port);\n\t}\n\n  return success;\n}\n\n/////////////////////////////////////////////////////////\n//\n// Private init() members\n//\n/////////////////////////////////////////////////////////\n\nbool ESPAsyncE131::initUnicast(uint16_t port) {\n  bool success = false;\n\n  if (udp.listen(port)) {\n    udp.onPacket(std::bind(&ESPAsyncE131::parsePacket, this, std::placeholders::_1));\n    success = true;\n  }\n  return success;\n}\n\nbool ESPAsyncE131::initMulticast(uint16_t port, uint16_t universe, uint8_t n) {\n  bool success = false;\n\n  IPAddress address = IPAddress(239, 255, ((universe >> 8) & 0xff),\n    ((universe >> 0) & 0xff));\n\n  if (udp.listenMulticast(address, port)) {\n    ip4_addr_t ifaddr;\n    ip4_addr_t multicast_addr;\n\n    ifaddr.addr = static_cast<uint32_t>(Network.localIP());\n    for (uint8_t i = 1; i < n; i++) {\n        multicast_addr.addr = static_cast<uint32_t>(IPAddress(239, 255,\n          (((universe + i) >> 8) & 0xff), (((universe + i) >> 0)\n          & 0xff)));\n      igmp_joingroup(&ifaddr, &multicast_addr);\n    }\n\n    udp.onPacket(std::bind(&ESPAsyncE131::parsePacket, this, std::placeholders::_1));\n\n    success = true;\n  }\n  return success;\n}\n\n/////////////////////////////////////////////////////////\n//\n// Packet parsing - Private\n//\n/////////////////////////////////////////////////////////\n\nvoid ESPAsyncE131::parsePacket(AsyncUDPPacket _packet) {\n  bool error = false;\n  uint8_t protocol = P_E131;\n\n  e131_packet_t *sbuff = reinterpret_cast<e131_packet_t *>(_packet.data());\n\t\n\t//E1.31 packet identifier (\"ACS-E1.17\")\n  if (memcmp(sbuff->acn_id, ESPAsyncE131::ACN_ID, sizeof(sbuff->acn_id)))\n    protocol = P_ARTNET;\n\t\n\tif (protocol == P_ARTNET) {\n\t\tif (memcmp(sbuff->art_id, ESPAsyncE131::ART_ID, sizeof(sbuff->art_id)))\n\t\t\terror = true; //not \"Art-Net\"\n\t\tif (sbuff->art_opcode != ARTNET_OPCODE_OPDMX && sbuff->art_opcode != ARTNET_OPCODE_OPPOLL)\n\t\t\terror = true; //not a DMX or poll packet\n\t} else { //E1.31 error handling\n\t\tif (htonl(sbuff->root_vector) != ESPAsyncE131::VECTOR_ROOT)\n\t\t\terror = true;\n\t\tif (htonl(sbuff->frame_vector) != ESPAsyncE131::VECTOR_FRAME)\n\t\t\terror = true;\n\t\tif (sbuff->dmp_vector != ESPAsyncE131::VECTOR_DMP)\n\t\t\terror = true;\n\t\tif (sbuff->property_values[0] != 0)\n\t\t\terror = true;\n\t} \n  \n  if (error && _packet.localPort() == DDP_DEFAULT_PORT) { //DDP packet\n    error = false;\n    protocol = P_DDP;\n  }\n\n  if (!error) {\n    _callback(sbuff, _packet.remoteIP(), protocol);\n  }\n}"
  },
  {
    "path": "wled00/src/dependencies/e131/ESPAsyncE131.h",
    "content": "/*\n* ESPAsyncE131.h\n*\n* Project: ESPAsyncE131 - Asynchronous E.131 (sACN) library for Arduino ESP8266 and ESP32\n* Copyright (c) 2019 Shelby Merrick\n* http://www.forkineye.com\n*\n*  Project: ESPAsyncDDP - Asynchronous DDP library for Arduino ESP8266 and ESP32\n* Copyright (c) 2019 Daniel Kulp\n*\n*  This program is provided free for you to use in any way that you wish,\n*  subject to the laws and regulations where you are using it.  Due diligence\n*  is strongly suggested before using this code.  Please give credit where due.\n*\n*  The Author makes no warranty of any kind, express or implied, with regard\n*  to this program or the documentation contained in this document.  The\n*  Author shall not be liable in any event for incidental or consequential\n*  damages in connection with, or arising out of, the furnishing, performance\n*  or use of these programs.\n*/\n\n/*\n * Inspired by https://github.com/hideakitai/ArtNet for ArtNet support\n */\n\n#ifndef ESPASYNCE131_H_\n#define ESPASYNCE131_H_\n\n#ifdef ESP32\n#include <WiFi.h>\n#include <AsyncUDP.h>\n#elif defined (ESP8266)\n#include <ESPAsyncUDP.h>\n#include <ESP8266WiFi.h>\n#include <ESP8266WiFiMulti.h>\n#else\n#error Platform not supported\n#endif\n\n#include <lwip/ip_addr.h>\n#include <lwip/igmp.h>\n#include <Arduino.h>\n\n#if LWIP_VERSION_MAJOR == 1\ntypedef struct ip_addr ip4_addr_t;\n#endif\n\n// Defaults\n#define E131_DEFAULT_PORT   5568\n#define ARTNET_DEFAULT_PORT 6454\n#define DDP_DEFAULT_PORT    4048\n\n#define DDP_PUSH_FLAG 0x01\n#define DDP_TIMECODE_FLAG 0x10\n\n#define DDP_TYPE_RGB24  0x0B // 00 001 011 (RGB , 8 bits per channel, 3 channels)\n#define DDP_TYPE_RGBW32 0x1B // 00 011 011 (RGBW, 8 bits per channel, 4 channels)\n\n#define ARTNET_OPCODE_OPDMX 0x5000\n#define ARTNET_OPCODE_OPPOLL 0x2000\n#define ARTNET_OPCODE_OPPOLLREPLY 0x2100\n\n#define P_E131   0\n#define P_ARTNET 1\n#define P_DDP    2\n\n// E1.31 Packet Offsets\n#define E131_ROOT_PREAMBLE_SIZE 0\n#define E131_ROOT_POSTAMBLE_SIZE 2\n#define E131_ROOT_ID 4\n#define E131_ROOT_FLENGTH 16\n#define E131_ROOT_VECTOR 18\n#define E131_ROOT_CID 22\n\n#define E131_FRAME_FLENGTH 38\n#define E131_FRAME_VECTOR 40\n#define E131_FRAME_SOURCE 44\n#define E131_FRAME_PRIORITY 108\n#define E131_FRAME_RESERVED 109\n#define E131_FRAME_SEQ 111\n#define E131_FRAME_OPT 112\n#define E131_FRAME_UNIVERSE 113\n\n#define E131_DMP_FLENGTH 115\n#define E131_DMP_VECTOR 117\n#define E131_DMP_TYPE 118\n#define E131_DMP_ADDR_FIRST 119\n#define E131_DMP_ADDR_INC 121\n#define E131_DMP_COUNT 123\n#define E131_DMP_DATA 125\n\n// E1.31 Packet Structure\ntypedef union {\n    struct { //E1.31 packet\n      // Root Layer\n      uint16_t preamble_size;\n      uint16_t postamble_size;\n      uint8_t  acn_id[12];\n      uint16_t root_flength;\n      uint32_t root_vector;\n      uint8_t  cid[16];\n\n      // Frame Layer\n      uint16_t frame_flength;\n      uint32_t frame_vector;\n      uint8_t  source_name[64];\n      uint8_t  priority;\n      uint16_t reserved;\n      uint8_t  sequence_number;\n      uint8_t  options;\n      uint16_t universe;\n\n      // DMP Layer\n      uint16_t dmp_flength;\n      uint8_t  dmp_vector;\n      uint8_t  type;\n      uint16_t first_address;\n      uint16_t address_increment;\n      uint16_t property_value_count;\n      uint8_t  property_values[513];\n    } __attribute__((packed));\n\t\n\tstruct { //Art-Net packet\n    uint8_t  art_id[8];\n    uint16_t art_opcode;\n    uint16_t art_protocol_ver;\n    uint8_t  art_sequence_number;\n    uint8_t  art_physical;\n    uint16_t art_universe;\n    uint16_t art_length;\n\n    uint8_t  art_data[512];\n  } __attribute__((packed));\n\n  struct { //DDP Header\n    uint8_t flags;\n    uint8_t sequenceNum;\n    uint8_t dataType;\n    uint8_t destination;\n    uint32_t channelOffset;\n    uint16_t dataLen;\n    uint8_t data[1];\n  } __attribute__((packed));\n\n  /*struct { //DDP Time code Header (unsupported)\n    uint8_t flags;\n    uint8_t sequenceNum;\n    uint8_t dataType;\n    uint8_t destination;\n    uint32_t channelOffset;\n    uint16_t dataLen;\n    uint32_t timeCode;\n    uint8_t data[1];\n  } __attribute__((packed));*/\n\n  uint8_t raw[1458];\n} e131_packet_t;\n\ntypedef union {\n  struct {\n    uint8_t reply_id[8];\n    uint16_t reply_opcode;\n    uint8_t reply_ip[4];\n    uint16_t reply_port;\n    uint8_t reply_version_h;\n    uint8_t reply_version_l;\n    uint8_t reply_net_sw;\n    uint8_t reply_sub_sw;\n    uint8_t reply_oem_h;\n    uint8_t reply_oem_l;\n    uint8_t reply_ubea_ver;\n    uint8_t reply_status_1;\n    uint16_t reply_esta_man;\n    uint8_t reply_short_name[18];\n    uint8_t reply_long_name[64];\n    uint8_t reply_node_report[64];\n    uint8_t reply_num_ports_h;\n    uint8_t reply_num_ports_l;\n    uint8_t reply_port_types[4];\n    uint8_t reply_good_input[4];\n    uint8_t reply_good_output_a[4];\n    uint8_t reply_sw_in[4];\n    uint8_t reply_sw_out[4];\n    uint8_t reply_sw_video;\n    uint8_t reply_sw_macro;\n    uint8_t reply_sw_remote;\n    uint8_t reply_spare[3];\n    uint8_t reply_style;\n    uint8_t reply_mac[6];\n    uint8_t reply_bind_ip[4];\n    uint8_t reply_bind_index;\n    uint8_t reply_status_2;\n    uint8_t reply_good_output_b[4];\n    uint8_t reply_status_3;\n    uint8_t reply_filler[21];\n  } __attribute__((packed));\n  \n  uint8_t raw[239];\n} ArtPollReply;\n\n// new packet callback\ntypedef void (*e131_packet_callback_function) (e131_packet_t* p, IPAddress clientIP, byte protocol);\n\nclass ESPAsyncE131 {\n private:\n    // Constants for packet validation\n    static const uint8_t ACN_ID[];\n\t  static const uint8_t ART_ID[];\n    static const uint32_t VECTOR_ROOT = 4;\n    static const uint32_t VECTOR_FRAME = 2;\n    static const uint8_t VECTOR_DMP = 2;\n\n    AsyncUDP        udp;        // AsyncUDP\n\n    // Internal Initializers\n    bool initUnicast(uint16_t port);\n    bool initMulticast(uint16_t port, uint16_t universe, uint8_t n = 1);\n\n    // Packet parser callback\n    void parsePacket(AsyncUDPPacket _packet);\n    \n    e131_packet_callback_function _callback = nullptr;\n\n public:\n    ESPAsyncE131(e131_packet_callback_function callback);\n\n    // Generic UDP listener, no physical or IP configuration\n    bool begin(bool multicast, uint16_t port = E131_DEFAULT_PORT, uint16_t universe = 1, uint8_t n = 1);\n};\n\n// Class to track e131 package priority\nclass E131Priority {\n  private:\n    uint8_t priority;\n    time_t setupTime;\n    uint8_t seconds;\n  \n  public:\n    E131Priority(uint8_t timeout=3) { \n      seconds = timeout;\n      set(0);\n    };\n\n    // Set priority (+ remember time)\n    void set(uint8_t prio) {\n      setupTime = time(0);\n      priority = prio;\n    }\n\n    // Get priority (+ reset & return 0 if older timeout)\n    uint8_t get() {\n      if (time(0) > setupTime + seconds) priority = 0;\n      return priority;\n    }\n};\n\n#endif  // ESPASYNCE131_H_"
  },
  {
    "path": "wled00/src/dependencies/espalexa/Espalexa.h",
    "content": "﻿#ifndef Espalexa_h\n#define Espalexa_h\n\n/*\n * Alexa Voice On/Off/Brightness/Color Control. Emulates a Philips Hue bridge to Alexa.\n * \n * This was put together from these two excellent projects:\n * https://github.com/kakopappa/arduino-esp8266-alexa-wemo-switch\n * https://github.com/probonopd/ESP8266HueEmulator\n */\n/*\n * @title Espalexa library\n * @version 2.7.1\n * @author Christian Schwinne\n * @license MIT\n * @contributors d-999\n */\n\n#include \"Arduino.h\"\n\n//you can use these defines for library config in your sketch. Just use them before #include <Espalexa.h>\n//#define ESPALEXA_ASYNC\n\n//in case this is unwanted in your application (will disable the /espalexa value page)\n//#define ESPALEXA_NO_SUBPAGE\n\n#ifndef ESPALEXA_MAXDEVICES\n #define ESPALEXA_MAXDEVICES 10 //this limit only has memory reasons, set it higher should you need to, max 128\n#endif\n\n//#define ESPALEXA_DEBUG\n\n#ifdef ESPALEXA_ASYNC\n #ifdef ARDUINO_ARCH_ESP32\n  #include <AsyncTCP.h>\n #else\n  #include <ESPAsyncTCP.h>\n #endif\n #include <ESPAsyncWebServer.h>\n#else\n #ifdef ARDUINO_ARCH_ESP32\n  #include <WiFi.h>\n  #include <WebServer.h> //if you get an error here please update to ESP32 arduino core 1.0.0\n #else\n  #include <ESP8266WebServer.h>\n  #include <ESP8266WiFi.h>\n #endif\n#endif\n#include <WiFiUdp.h>\n#include \"../network/Network.h\"\n\n#ifdef ESPALEXA_DEBUG\n #pragma message \"Espalexa 2.7.1 debug mode\"\n #define EA_DEBUG(x)  Serial.print (x)\n #define EA_DEBUGLN(x) Serial.println (x)\n#else\n #define EA_DEBUG(x)\n #define EA_DEBUGLN(x)\n#endif\n\n#include \"EspalexaDevice.h\"\n\n#define DEVICE_UNIQUE_ID_LENGTH 12\n\nclass Espalexa {\nprivate:\n  //private member vars\n  #ifdef ESPALEXA_ASYNC\n  AsyncWebServer* serverAsync;\n  AsyncWebServerRequest* server; //this saves many #defines\n  String body = \"\";\n  #elif defined ARDUINO_ARCH_ESP32\n  WebServer* server;\n  #else\n  ESP8266WebServer* server;\n  #endif\n  uint8_t currentDeviceCount = 0;\n  bool discoverable = true;\n  bool udpConnected = false;\n\n  EspalexaDevice* devices[ESPALEXA_MAXDEVICES] = {};\n  //Keep in mind that Device IDs go from 1 to DEVICES, cpp arrays from 0 to DEVICES-1!!\n  \n  WiFiUDP espalexaUdp;\n  IPAddress ipMulti;\n  uint32_t mac24; //bottom 24 bits of mac\n  String escapedMac=\"\"; //lowercase mac address\n  \n  //private member functions\n  const char* modeString(EspalexaColorMode m)\n  {\n    if (m == EspalexaColorMode::xy) return \"xy\";\n    if (m == EspalexaColorMode::hs) return \"hs\";\n    return \"ct\";\n  }\n  \n  const char* typeString(EspalexaDeviceType t)\n  {\n    switch (t)\n    {\n      case EspalexaDeviceType::dimmable:      return PSTR(\"Dimmable light\");\n      case EspalexaDeviceType::whitespectrum: return PSTR(\"Color temperature light\");\n      case EspalexaDeviceType::color:         return PSTR(\"Color light\");\n      case EspalexaDeviceType::extendedcolor: return PSTR(\"Extended color light\");\n      default: return \"\";\n    }\n  }\n  \n  const char* modelidString(EspalexaDeviceType t)\n  {\n    switch (t)\n    {\n      case EspalexaDeviceType::dimmable:      return \"LWB010\";\n      case EspalexaDeviceType::whitespectrum: return \"LWT010\";\n      case EspalexaDeviceType::color:         return \"LST001\";\n      case EspalexaDeviceType::extendedcolor: return \"LCT015\";\n      default: return \"\";\n    }\n  }\n  \n  void encodeLightId(uint8_t idx, char* out)\n  {\n    String mymac = WiFi.macAddress();\n\tsprintf_P(out, PSTR(\"%02X:%s:AB-%02X\"), idx, mymac.c_str(), idx);\n  }\n\n  // construct 'globally unique' Json dict key fitting into signed int\n  inline int encodeLightKey(uint8_t idx)\n  {\n    //return idx +1;\n    static_assert(ESPALEXA_MAXDEVICES <= 128, \"\");\n    return (mac24<<7) | idx;\n  }\n\n  // get device index from Json key\n  uint8_t decodeLightKey(int key)\n  {\n    //return key -1;\n    return (((uint32_t)key>>7) == mac24) ? (key & 127U) : 255U;\n  }\n\n  //device JSON string: color+temperature device emulates LCT015, dimmable device LWB010, (TODO: on/off Plug 01, color temperature device LWT010, color device LST001)\n  void deviceJsonString(EspalexaDevice* dev, char* buf, size_t maxBuf) // softhack007 \"size\" parameter added, to avoid buffer overrun\n  {\n    char buf_lightid[27];\n    encodeLightId(dev->getId() + 1, buf_lightid);\n    \n    char buf_col[80] = \"\";\n    //color support\n    if (static_cast<uint8_t>(dev->getType()) > 2)\n      //TODO: %f is not working for some reason on ESP8266 in v0.11.0 (was fine in 0.10.2). Need to investigate\n      //sprintf_P(buf_col,PSTR(\",\\\"hue\\\":%u,\\\"sat\\\":%u,\\\"effect\\\":\\\"none\\\",\\\"xy\\\":[%f,%f]\")\n      //  ,dev->getHue(), dev->getSat(), dev->getX(), dev->getY());\n      snprintf_P(buf_col, sizeof(buf_col), PSTR(\",\\\"hue\\\":%u,\\\"sat\\\":%u,\\\"effect\\\":\\\"none\\\",\\\"xy\\\":[%s,%s]\"),dev->getHue(), dev->getSat(),\n        ((String)dev->getX()).c_str(), ((String)dev->getY()).c_str());\n      \n    char buf_ct[16] = \"\";\n    //white spectrum support\n    if (static_cast<uint8_t>(dev->getType()) > 1 && dev->getType() != EspalexaDeviceType::color)\n      snprintf(buf_ct, sizeof(buf_ct), \",\\\"ct\\\":%u\", dev->getCt());\n    \n    char buf_cm[20] = \"\";\n    if (static_cast<uint8_t>(dev->getType()) > 1)\n      snprintf(buf_cm, sizeof(buf_cm), PSTR(\"\\\",\\\"colormode\\\":\\\"%s\"), modeString(dev->getColorMode()));\n    \n    snprintf_P(buf, maxBuf, PSTR(\"{\\\"state\\\":{\\\"on\\\":%s,\\\"bri\\\":%u%s%s,\\\"alert\\\":\\\"none%s\\\",\\\"mode\\\":\\\"homeautomation\\\",\\\"reachable\\\":true},\"\n                   \"\\\"type\\\":\\\"%s\\\",\\\"name\\\":\\\"%s\\\",\\\"modelid\\\":\\\"%s\\\",\\\"manufacturername\\\":\\\"Philips\\\",\\\"productname\\\":\\\"E%u\"\n                   \"\\\",\\\"uniqueid\\\":\\\"%s\\\",\\\"swversion\\\":\\\"espalexa-2.7.0\\\"}\")\n                   \n    , (dev->getValue())?\"true\":\"false\", dev->getLastValue()-1, buf_col, buf_ct, buf_cm, typeString(dev->getType()),\n    dev->getName().c_str(), modelidString(dev->getType()), static_cast<uint8_t>(dev->getType()), buf_lightid);\n  }\n  \n  //Espalexa status page /espalexa\n  #ifndef ESPALEXA_NO_SUBPAGE\n  void servePage()\n  {\n    EA_DEBUGLN(\"HTTP Req espalexa ...\\n\");\n    String res = \"Hello from Espalexa!\\r\\n\\r\\n\";\n    for (int i=0; i<currentDeviceCount; i++)\n    {\n      EspalexaDevice* dev = devices[i];\n      res += \"Value of device \" + String(i+1) + \" (\" + dev->getName() + \"): \" + String(dev->getValue()) + \" (\" + typeString(dev->getType());\n      if (static_cast<uint8_t>(dev->getType()) > 1) //color support\n      {\n        res += \", colormode=\" + String(modeString(dev->getColorMode())) + \", r=\" + String(dev->getR()) + \", g=\" + String(dev->getG()) + \", b=\" + String(dev->getB());\n        res +=\", ct=\" + String(dev->getCt()) + \", hue=\" + String(dev->getHue()) + \", sat=\" + String(dev->getSat()) + \", x=\" + String(dev->getX()) + \", y=\" + String(dev->getY());\n      }\n      res += \")\\r\\n\";\n    }\n    res += \"\\r\\nFree Heap: \" + (String)ESP.getFreeHeap();\n    res += \"\\r\\nUptime: \" + (String)millis();\n    res += \"\\r\\n\\r\\nEspalexa library v2.7.0 by Christian Schwinne 2021\";\n    server->send(200, \"text/plain\", res);\n  }\n  #endif\n\n  //not found URI (only if internal webserver is used)\n  void serveNotFound()\n  {\n    EA_DEBUGLN(\"Not-Found HTTP call:\");\n    #ifndef ESPALEXA_ASYNC\n    EA_DEBUGLN(\"URI: \" + server->uri());\n    EA_DEBUGLN(\"Body: \" + server->arg(0));\n    if(!handleAlexaApiCall(server->uri(), server->arg(0)))\n    #else\n    EA_DEBUGLN(\"URI: \" + server->url());\n    EA_DEBUGLN(\"Body: \" + body);\n    if(!handleAlexaApiCall(server))\n    #endif\n      server->send(404, \"text/plain\", \"Not Found (espalexa)\");\n  }\n\n  //send description.xml device property page\n  void serveDescription()\n  {\n    EA_DEBUGLN(\"# Responding to description.xml ... #\\n\");\n    IPAddress localIP = Network.localIP();\n    char s[16];\n    snprintf(s, sizeof(s), \"%d.%d.%d.%d\", localIP[0], localIP[1], localIP[2], localIP[3]);\n    char buf[1024];\n    \n    snprintf_P(buf, sizeof(buf), PSTR(\"<?xml version=\\\"1.0\\\" ?>\"\n        \"<root xmlns=\\\"urn:schemas-upnp-org:device-1-0\\\">\"\n        \"<specVersion><major>1</major><minor>0</minor></specVersion>\"\n        \"<URLBase>http://%s:80/</URLBase>\"\n        \"<device>\"\n          \"<deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType>\"\n          \"<friendlyName>Espalexa (%s:80)</friendlyName>\"\n          \"<manufacturer>Royal Philips Electronics</manufacturer>\"\n          \"<manufacturerURL>http://www.philips.com</manufacturerURL>\"\n          \"<modelDescription>Philips hue Personal Wireless Lighting</modelDescription>\"\n          \"<modelName>Philips hue bridge 2012</modelName>\"\n          \"<modelNumber>929000226503</modelNumber>\"\n          \"<modelURL>http://www.meethue.com</modelURL>\"\n          \"<serialNumber>%s</serialNumber>\"\n          \"<UDN>uuid:2f402f80-da50-11e1-9b23-%s</UDN>\"\n          \"<presentationURL>index.html</presentationURL>\"\n        \"</device>\"\n        \"</root>\"),s,s,escapedMac.c_str(),escapedMac.c_str());\n          \n    server->send(200, \"text/xml\", buf);\n    \n    EA_DEBUGLN(\"Send setup.xml\");\n    EA_DEBUGLN(buf);\n  }\n  \n  //init the server\n  void startHttpServer()\n  {\n    #ifdef ESPALEXA_ASYNC\n    if (serverAsync == nullptr) {\n      serverAsync = new AsyncWebServer(80);\n      serverAsync->onNotFound([=](AsyncWebServerRequest *request){server = request; serveNotFound();});\n    }\n    \n    serverAsync->onRequestBody([=](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total){\n      char b[len +1];\n      b[len] = 0;\n      memcpy(b, data, len);\n      body = b; //save the body so we can use it for the API call\n      EA_DEBUG(\"Received body: \");\n      EA_DEBUGLN(body);\n    });\n    #ifndef ESPALEXA_NO_SUBPAGE\n    serverAsync->on(\"/espalexa\", HTTP_GET, [=](AsyncWebServerRequest *request){server = request; servePage();});\n    #endif\n    serverAsync->on(\"/description.xml\", HTTP_GET, [=](AsyncWebServerRequest *request){server = request; serveDescription();});\n    serverAsync->begin();\n    \n    #else\n    if (server == nullptr) {\n      #ifdef ARDUINO_ARCH_ESP32\n      server = new WebServer(80);\n      #else\n      server = new ESP8266WebServer(80);  \n      #endif\n      server->onNotFound([=](){serveNotFound();});\n    }\n\n    #ifndef ESPALEXA_NO_SUBPAGE\n    server->on(\"/espalexa\", HTTP_GET, [=](){servePage();});\n    #endif\n    server->on(\"/description.xml\", HTTP_GET, [=](){serveDescription();});\n    server->begin();\n    #endif\n  }\n\n  //respond to UDP SSDP M-SEARCH\n  void respondToSearch()\n  {\n    IPAddress localIP = Network.localIP();\n    char s[16];\n    sprintf(s, \"%d.%d.%d.%d\", localIP[0], localIP[1], localIP[2], localIP[3]);\n\n    char buf[1024];\n\n    snprintf_P(buf, sizeof(buf), PSTR(\"HTTP/1.1 200 OK\\r\\n\"\n      \"EXT:\\r\\n\"\n      \"CACHE-CONTROL: max-age=100\\r\\n\" // SSDP_INTERVAL\n      \"LOCATION: http://%s:80/description.xml\\r\\n\"\n      \"SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.17.0\\r\\n\" // _modelName, _modelNumber\n      \"hue-bridgeid: %s\\r\\n\"\n      \"ST: urn:schemas-upnp-org:device:basic:1\\r\\n\"  // _deviceType\n      \"USN: uuid:2f402f80-da50-11e1-9b23-%s::upnp:rootdevice\\r\\n\" // _uuid::_deviceType\n      \"\\r\\n\"),s,escapedMac.c_str(),escapedMac.c_str());\n\n    espalexaUdp.beginPacket(espalexaUdp.remoteIP(), espalexaUdp.remotePort());\n    #ifdef ARDUINO_ARCH_ESP32\n    espalexaUdp.write((uint8_t*)buf, strlen(buf));\n    #else\n    espalexaUdp.write(buf);\n    #endif\n    espalexaUdp.endPacket();                    \n  }\n\npublic:\n  Espalexa(){}\n\n  //initialize interfaces\n  #ifdef ESPALEXA_ASYNC\n  bool begin(AsyncWebServer* externalServer = nullptr)\n  #elif defined ARDUINO_ARCH_ESP32\n  bool begin(WebServer* externalServer = nullptr)\n  #else\n  bool begin(ESP8266WebServer* externalServer = nullptr)\n  #endif\n  {\n    EA_DEBUGLN(\"Espalexa Begin...\");\n    EA_DEBUG(\"MAXDEVICES \");\n    EA_DEBUGLN(ESPALEXA_MAXDEVICES);\n    escapedMac = WiFi.macAddress();\n    escapedMac.replace(\":\", \"\");\n    escapedMac.toLowerCase();\n\n    String macSubStr = escapedMac.substring(6, 12);\n    mac24 = strtol(macSubStr.c_str(), 0, 16);\n\n    #ifdef ESPALEXA_ASYNC\n    serverAsync = externalServer;\n    #else\n    server = externalServer;\n    #endif\n    #ifdef ARDUINO_ARCH_ESP32\n    udpConnected = espalexaUdp.beginMulticast(IPAddress(239, 255, 255, 250), 1900);\n    #else\n    udpConnected = espalexaUdp.beginMulticast(Network.localIP(), IPAddress(239, 255, 255, 250), 1900);\n    #endif\n\n    if (udpConnected){\n      \n      startHttpServer();\n      EA_DEBUGLN(\"Done\");\n      return true;\n    }\n    EA_DEBUGLN(\"Failed\");\n    return false;\n  }\n\n  // get device count, function only in WLED version of Espalexa\n  uint8_t getDeviceCount() {\n    return currentDeviceCount;\n  }\n\n  //service loop\n  void loop() {\n    #ifndef ESPALEXA_ASYNC\n    if (server == nullptr) return; //only if begin() was not called\n    server->handleClient();\n    #endif\n    \n    if (!udpConnected) return;   \n    int packetSize = espalexaUdp.parsePacket();    \n    if (packetSize < 1) return; //no new udp packet\n    \n    EA_DEBUGLN(\"Got UDP!\");\n\n    unsigned char packetBuffer[packetSize+1]; //buffer to hold incoming udp packet\n    espalexaUdp.read(packetBuffer, packetSize);\n    packetBuffer[packetSize] = 0;\n  \n    espalexaUdp.flush();\n    if (!discoverable) return; //do not reply to M-SEARCH if not discoverable\n  \n    const char* request = (const char *) packetBuffer;\n    if (strstr(request, \"M-SEARCH\") == nullptr) return;\n\n    EA_DEBUGLN(request);\n    if (strstr(request, \"ssdp:disc\")  != nullptr &&  //short for \"ssdp:discover\"\n        (strstr(request, \"upnp:rootd\") != nullptr || //short for \"upnp:rootdevice\"\n         strstr(request, \"ssdp:all\")   != nullptr ||\n         strstr(request, \"asic:1\")     != nullptr )) //short for \"device:basic:1\"\n    {\n      EA_DEBUGLN(\"Responding search req...\");\n      respondToSearch();\n    }\n  }\n\n  // Function only in WLED version of Espalexa, does not actually release memory for names\n  void removeAllDevices()\n  {\n    currentDeviceCount=0;\n    return;\n  }\n\n  // returns device index or 0 on failure\n  uint8_t addDevice(EspalexaDevice* d)\n  {\n    EA_DEBUG(\"Adding device \");\n    EA_DEBUGLN((currentDeviceCount+1));\n    if (currentDeviceCount >= ESPALEXA_MAXDEVICES) return 0;\n    if (d == nullptr) return 0;\n    d->setId(currentDeviceCount);\n    devices[currentDeviceCount] = d;\n    return ++currentDeviceCount;\n  }\n  \n  //brightness-only callback\n  uint8_t addDevice(String deviceName, BrightnessCallbackFunction callback, uint8_t initialValue = 0)\n  {\n    EA_DEBUG(\"Constructing device \");\n    EA_DEBUGLN((currentDeviceCount+1));\n    if (currentDeviceCount >= ESPALEXA_MAXDEVICES) return 0;\n    EspalexaDevice* d = new EspalexaDevice(deviceName, callback, initialValue);\n    return addDevice(d);\n  }\n  \n  //brightness-only callback\n  uint8_t addDevice(String deviceName, ColorCallbackFunction callback, uint8_t initialValue = 0)\n  {\n    EA_DEBUG(\"Constructing device \");\n    EA_DEBUGLN((currentDeviceCount+1));\n    if (currentDeviceCount >= ESPALEXA_MAXDEVICES) return 0;\n    EspalexaDevice* d = new EspalexaDevice(deviceName, callback, initialValue);\n    return addDevice(d);\n  }\n\n\n  uint8_t addDevice(String deviceName, DeviceCallbackFunction callback, EspalexaDeviceType t = EspalexaDeviceType::dimmable, uint8_t initialValue = 0)\n  {\n    EA_DEBUG(\"Constructing device \");\n    EA_DEBUGLN((currentDeviceCount+1));\n    if (currentDeviceCount >= ESPALEXA_MAXDEVICES) return 0;\n    EspalexaDevice* d = new EspalexaDevice(deviceName, callback, t, initialValue);\n    return addDevice(d);\n  }\n\n  void renameDevice(uint8_t id, const String& deviceName)\n  {\n    unsigned int index = id - 1;\n    if (index < currentDeviceCount)\n      devices[index]->setName(deviceName);\n  }\n\n  //basic implementation of Philips hue api functions needed for basic Alexa control\n  #ifdef ESPALEXA_ASYNC\n  bool handleAlexaApiCall(AsyncWebServerRequest* request)\n  {\n    server = request; //copy request reference\n    String req = request->url(); //body from global variable\n    EA_DEBUGLN(request->contentType());\n    if (request->hasParam(\"body\", true)) // This is necessary, otherwise ESP crashes if there is no body\n    {\n      EA_DEBUG(\"BodyMethod2\");\n      body = request->getParam(\"body\", true)->value();\n    }\n    EA_DEBUG(\"FinalBody: \");\n    EA_DEBUGLN(body);\n  #else\n  bool handleAlexaApiCall(String req, String body)\n  {  \n  #endif\n    EA_DEBUG(\"URL: \");\n    EA_DEBUGLN(req);\n    EA_DEBUGLN(\"AlexaApiCall\");\n    if (req.indexOf(\"api\") <0) return false; //return if not an API call\n    EA_DEBUGLN(\"ok\");\n\n    if (body.indexOf(\"devicetype\") > 0) //client wants a hue api username, we don't care and give static\n    {\n      EA_DEBUGLN(\"devType\");\n      body = \"\";\n      server->send(200, \"application/json\", F(\"[{\\\"success\\\":{\\\"username\\\":\\\"2BLEDHardQrI3WHYTHoMcXHgEspsM8ZZRpSKtBGr\\\"}}]\"));\n      return true;\n    }\n\n    if ((req.indexOf(\"state\") > 0) && (body.length() > 0)) //client wants to control light\n    {\n      uint32_t devId = req.substring(req.indexOf(\"lights\")+7).toInt();\n      EA_DEBUG(\"ls\"); EA_DEBUGLN(devId);\n      unsigned idx = decodeLightKey(devId);\n      EA_DEBUGLN(idx);\n      char buf[50];\n      snprintf_P(buf,sizeof(buf),PSTR(\"[{\\\"success\\\":{\\\"/lights/%u/state/\\\": true}}]\"),devId);\n      server->send(200, \"application/json\", buf);\n      if (idx >= currentDeviceCount) return true; //return if invalid ID\n      EspalexaDevice* dev = devices[idx];\n      \n      dev->setPropertyChanged(EspalexaDeviceProperty::none);\n      \n      if (body.indexOf(\"false\")>0) //OFF command\n      {\n        dev->setValue(0);\n        dev->setPropertyChanged(EspalexaDeviceProperty::off);\n        dev->doCallback();\n        return true;\n      }\n      \n      if (body.indexOf(\"true\") >0) //ON command\n      {\n        dev->setValue(dev->getLastValue());\n        dev->setPropertyChanged(EspalexaDeviceProperty::on);\n      }\n      \n      if (body.indexOf(\"bri\")  >0) //BRIGHTNESS command\n      {\n        uint8_t briL = body.substring(body.indexOf(\"bri\") +5).toInt();\n        if (briL == 255)\n        {\n         dev->setValue(255);\n        } else {\n         dev->setValue(briL+1); \n        }\n        dev->setPropertyChanged(EspalexaDeviceProperty::bri);\n      }\n      \n      if (body.indexOf(\"xy\")   >0) //COLOR command (XY mode)\n      {\n        dev->setColorXY(body.substring(body.indexOf(\"[\") +1).toFloat(), body.substring(body.indexOf(\",0\") +1).toFloat());\n        dev->setPropertyChanged(EspalexaDeviceProperty::xy);\n      }\n      \n      if (body.indexOf(\"hue\")  >0) //COLOR command (HS mode)\n      {\n        dev->setColor(body.substring(body.indexOf(\"hue\") +5).toInt(), body.substring(body.indexOf(\"sat\") +5).toInt());\n        dev->setPropertyChanged(EspalexaDeviceProperty::hs);\n      }\n      \n      if (body.indexOf(\"ct\")   >0) //COLOR TEMP command (white spectrum)\n      {\n        dev->setColor(body.substring(body.indexOf(\"ct\") +4).toInt());\n        dev->setPropertyChanged(EspalexaDeviceProperty::ct);\n      }\n      \n      dev->doCallback();\n      \n      #ifdef ESPALEXA_DEBUG\n      if (dev->getLastChangedProperty() == EspalexaDeviceProperty::none)\n        EA_DEBUGLN(\"STATE REQ WITHOUT BODY (likely Content-Type issue #6)\");\n      #endif\n      return true;\n    }\n    \n    int pos = req.indexOf(\"lights\");\n    if (pos > 0) //client wants light info\n    {\n      int devId = req.substring(pos+7).toInt();\n      EA_DEBUG(\"l\"); EA_DEBUGLN(devId);\n\n      if (devId == 0) //client wants all lights\n      {\n        EA_DEBUGLN(\"lAll\");\n        String jsonTemp = \"{\";\n        for (int i = 0; i<currentDeviceCount; i++)\n        {\n          jsonTemp += '\"';\n          jsonTemp += encodeLightKey(i);\n          jsonTemp += '\"';\n          jsonTemp += ':';\n\n          char buf[512];\n          deviceJsonString(devices[i], buf, sizeof(buf)-1);\n          jsonTemp += buf;\n          if (i < currentDeviceCount-1) jsonTemp += ',';\n        }\n        jsonTemp += '}';\n        server->send(200, \"application/json\", jsonTemp);\n      } else //client wants one light (devId)\n      {\n        EA_DEBUGLN(devId);\n        unsigned int idx = decodeLightKey(devId);\n\n        if (idx >= currentDeviceCount) idx = 0; //send first device if invalid\n        if (currentDeviceCount == 0) {\n          server->send(200, \"application/json\", \"{}\");\n          return true;\n        }\n        char buf[512];\n        deviceJsonString(devices[idx], buf, sizeof(buf)-1);\n        server->send(200, \"application/json\", buf);\n      }\n      \n      return true;\n    }\n\n    //we don't care about other api commands at this time and send empty JSON\n    server->send(200, \"application/json\", \"{}\");\n    return true;\n  }\n  \n  //set whether Alexa can discover any devices\n  void setDiscoverable(bool d)\n  {\n    discoverable = d;\n  }\n  \n  //get EspalexaDevice at specific index\n  EspalexaDevice* getDevice(uint8_t index)\n  {\n    if (index >= currentDeviceCount) return nullptr;\n    return devices[index];\n  }\n  \n  //is an unique device ID\n  String getEscapedMac()\n  {\n    return escapedMac;\n  }\n  \n  //convert brightness (0-255) to percentage\n  uint8_t toPercent(uint8_t bri)\n  {\n    uint16_t perc = bri * 100;\n    return perc / 255;\n  }\n  \n  ~Espalexa(){} //note: Espalexa is NOT meant to be destructed\n};\n\n#endif\n"
  },
  {
    "path": "wled00/src/dependencies/espalexa/EspalexaDevice.cpp",
    "content": "//EspalexaDevice Class\n\n#include \"EspalexaDevice.h\"\n\n// debug macros\n#ifdef ESPALEXA_DEBUG\n #define EA_DEBUG(x)  Serial.print (x)\n #define EA_DEBUGLN(x) Serial.println (x)\n#else\n #define EA_DEBUG(x)\n #define EA_DEBUGLN(x)\n#endif\n\nEspalexaDevice::EspalexaDevice(){}\n\nEspalexaDevice::EspalexaDevice(String deviceName, BrightnessCallbackFunction gnCallback, uint8_t initialValue) { //constructor for dimmable device\n  \n  _deviceName = deviceName;\n  _callback = gnCallback;\n  _val = initialValue;\n  _val_last = _val;\n  _type = EspalexaDeviceType::dimmable;\n}\n\nEspalexaDevice::EspalexaDevice(String deviceName, ColorCallbackFunction gnCallback, uint8_t initialValue) { //constructor for color device\n  \n  _deviceName = deviceName;\n  _callbackCol = gnCallback;\n  _val = initialValue;\n  _val_last = _val;\n  _type = EspalexaDeviceType::extendedcolor;\n}\n\nEspalexaDevice::EspalexaDevice(String deviceName, DeviceCallbackFunction gnCallback, EspalexaDeviceType t, uint8_t initialValue) { //constructor for general device\n  \n  _deviceName = deviceName;\n  _callbackDev = gnCallback;\n  _type = t;\n  if (t == EspalexaDeviceType::onoff) _type = EspalexaDeviceType::dimmable; //on/off is broken, so make dimmable device instead\n  if (t == EspalexaDeviceType::whitespectrum) _mode = EspalexaColorMode::ct;\n  _val = initialValue;\n  _val_last = _val;\n}\n\nEspalexaDevice::~EspalexaDevice(){/*nothing to destruct*/}\n\nuint8_t EspalexaDevice::getId()\n{\n  return _id;\n}\n\nEspalexaColorMode EspalexaDevice::getColorMode()\n{\n  return _mode;\n}\n\nEspalexaDeviceType EspalexaDevice::getType()\n{\n  return _type;\n}\n\nString EspalexaDevice::getName()\n{\n  return _deviceName;\n}\n\nEspalexaDeviceProperty EspalexaDevice::getLastChangedProperty()\n{\n  return _changed;\n}\n\nuint8_t EspalexaDevice::getValue()\n{\n  return _val;\n}\n\nbool EspalexaDevice::getState()\n{\n  return _val;\n}\n\nuint8_t EspalexaDevice::getPercent()\n{\n  uint16_t perc = _val * 100;\n  return perc / 255;\n}\n\nuint8_t EspalexaDevice::getDegrees()\n{\n  return getPercent();\n}\n\nuint16_t EspalexaDevice::getHue()\n{\n  return _hue;\n}\n\nuint8_t EspalexaDevice::getSat()\n{\n  return _sat;\n}\n\nfloat EspalexaDevice::getX()\n{\n  return _x;\n}\n\nfloat EspalexaDevice::getY()\n{\n  return _y;\n}\n\nuint16_t EspalexaDevice::getCt()\n{\n  if (_ct == 0) return 500;\n  return _ct;\n}\n\nuint32_t EspalexaDevice::getKelvin()\n{\n  if (_ct == 0) return 2000;\n  return 1000000/_ct;\n}\n\nuint32_t EspalexaDevice::getRGB()\n{\n  if (_rgb != 0) return _rgb; //color has not changed\n  byte rgb[4]{0, 0, 0, 0};\n  \n  if (_mode == EspalexaColorMode::none) return 0;\n\n  if (_mode == EspalexaColorMode::ct)\n  {\n    //TODO tweak a bit to match hue lamp characteristics\n    //based on https://gist.github.com/paulkaplan/5184275\n    float temp = (_ct != 0) ? (10000/ _ct) : 2; //kelvins = 1,000,000/mired (and that /100)   softhack007: avoid division by zero - using \"2\" as substitute\n    float r, g, b;\n\n#ifdef ESPALEXA_DEBUG\n  if (_ct == 0) {EA_DEBUGLN(F(\"EspalexaDevice::getRGB() Warning: ct = 0!\"));}\n#endif\n    if (temp <= 66) { \n      r = 255; \n      g = temp;\n      g = 99.470802 * logf(g) - 161.119568;\n      if (temp <= 19) {\n          b = 0;\n      } else {\n          b = temp-10;\n          b = 138.517731 * logf(b) - 305.044793;\n      }\n    } else {\n      r = temp - 60;\n      r = 329.698727 * pow(r, -0.13320476);\n      g = temp - 60;\n      g = 288.12217 * pow(g, -0.07551485 );\n      b = 255;\n    }\n    \n    rgb[0] = (byte)constrain(r,0.1,255.1);\n    rgb[1] = (byte)constrain(g,0.1,255.1);\n    rgb[2] = (byte)constrain(b,0.1,255.1);\n    \n  } else if (_mode == EspalexaColorMode::hs)\n  {\n    float h = ((float)_hue)/65535.0;\n    float s = ((float)_sat)/255.0;\n    byte i = floor(h*6);\n    float f = h * 6-i;\n    float p = 255 * (1-s);\n    float q = 255 * (1-f*s);\n    float t = 255 * (1-(1-f)*s);\n    switch (i%6) {\n      case 0: rgb[0]=255,rgb[1]=t,rgb[2]=p;break;\n      case 1: rgb[0]=q,rgb[1]=255,rgb[2]=p;break;\n      case 2: rgb[0]=p,rgb[1]=255,rgb[2]=t;break;\n      case 3: rgb[0]=p,rgb[1]=q,rgb[2]=255;break;\n      case 4: rgb[0]=t,rgb[1]=p,rgb[2]=255;break;\n      case 5: rgb[0]=255,rgb[1]=p,rgb[2]=q;\n    }\n  } else if (_mode == EspalexaColorMode::xy)\n  {\n    //Source: https://www.developers.meethue.com/documentation/color-conversions-rgb-xy\n    float z = 1.0f - _x - _y;\n    float X = (1.0f / _y) * _x;\n    float Z = (1.0f / _y) * z;\n    float r = (int)255*(X * 1.656492f - 0.354851f - Z * 0.255038f);\n    float g = (int)255*(-X * 0.707196f + 1.655397f + Z * 0.036152f);\n    float b = (int)255*(X * 0.051713f - 0.121364f + Z * 1.011530f);\n    if (r > b && r > g && r > 1.0f) {\n      // red is too big\n      g = g / r;\n      b = b / r;\n      r = 1.0f;\n    } else if (g > b && g > r && g > 1.0f) {\n      // green is too big\n      r = r / g;\n      b = b / g;\n      g = 1.0f;\n    } else if (b > r && b > g && b > 1.0f) {\n      // blue is too big\n      r = r / b;\n      g = g / b;\n      b = 1.0f;\n    }\n    // Apply gamma correction\n    r = r <= 0.0031308f ? 12.92f * r : (1.0f + 0.055f) * powf(r, (1.0f / 2.4f)) - 0.055f;\n    g = g <= 0.0031308f ? 12.92f * g : (1.0f + 0.055f) * powf(g, (1.0f / 2.4f)) - 0.055f;\n    b = b <= 0.0031308f ? 12.92f * b : (1.0f + 0.055f) * powf(b, (1.0f / 2.4f)) - 0.055f;\n\n    if (r > b && r > g) {\n      // red is biggest\n      if (r > 1.0f) {\n        g = g / r;\n        b = b / r;\n        r = 1.0f;\n      }\n    } else if (g > b && g > r) {\n      // green is biggest\n      if (g > 1.0f) {\n        r = r / g;\n        b = b / g;\n        g = 1.0f;\n      }\n    } else if (b > r && b > g) {\n      // blue is biggest\n      if (b > 1.0f) {\n        r = r / b;\n        g = g / b;\n        b = 1.0f;\n      }\n    }\n    rgb[0] = 255.0*r;\n    rgb[1] = 255.0*g;\n    rgb[2] = 255.0*b;\n  }\n  _rgb = ((rgb[0] << 16) | (rgb[1] << 8) | (rgb[2]));\n  return _rgb;\n}\n\n//white channel for RGBW lights. Always 0 unless colormode is ct\nuint8_t EspalexaDevice::getW()\n{\n  return (getRGB() >> 24) & 0xFF;\n}\n\nuint8_t EspalexaDevice::getR()\n{\n  return (getRGB() >> 16) & 0xFF;\n}\n\nuint8_t EspalexaDevice::getG()\n{\n  return (getRGB() >> 8) & 0xFF;\n}\n\nuint8_t EspalexaDevice::getB()\n{\n  return getRGB() & 0xFF;\n}\n\nuint8_t EspalexaDevice::getLastValue()\n{\n  if (_val_last == 0) return 255;\n  return _val_last;\n}\n\nvoid EspalexaDevice::setPropertyChanged(EspalexaDeviceProperty p)\n{\n  _changed = p;\n}\n\nvoid EspalexaDevice::setId(uint8_t id)\n{\n  _id = id;\n}\n\n//you need to re-discover the device for the Alexa name to change\nvoid EspalexaDevice::setName(String name)\n{\n  _deviceName = name;\n}\n\nvoid EspalexaDevice::setValue(uint8_t val)\n{\n  if (_val != 0)\n  {\n    _val_last = _val;\n  }\n  if (val != 0)\n  {\n    _val_last = val;\n  }\n  _val = val;\n}\n\nvoid EspalexaDevice::setState(bool onoff)\n{\n  if (onoff) \n  {\n    setValue(_val_last);\n  } else {\n    setValue(0);\n  }\n}\n\nvoid EspalexaDevice::setPercent(uint8_t perc)\n{\n  uint16_t val = perc * 255;\n  val /= 100;\n  if (val > 255) val = 255;\n  setValue(val);\n}\n\nvoid EspalexaDevice::setColorXY(float x, float y)\n{\n  _x = x;\n  _y = y;\n  _rgb = 0;\n  _mode = EspalexaColorMode::xy;\n}\n\nvoid EspalexaDevice::setColor(uint16_t hue, uint8_t sat)\n{\n  _hue = hue;\n  _sat = sat;\n  _rgb = 0;\n  _mode = EspalexaColorMode::hs;\n}\n\nvoid EspalexaDevice::setColor(uint16_t ct)\n{\n  _ct = ct;\n  _rgb = 0;\n  _mode =EspalexaColorMode::ct;\n}\n\nvoid EspalexaDevice::setColor(uint8_t r, uint8_t g, uint8_t b)\n{\n  float X = r * 0.664511f + g * 0.154324f + b * 0.162028f;\n  float Y = r * 0.283881f + g * 0.668433f + b * 0.047685f;\n  float Z = r * 0.000088f + g * 0.072310f + b * 0.986039f;\n  if ((r+g+b) > 0) { //   softhack007: avoid division by zero\n    _x = X / (X + Y + Z);\n    _y = Y / (X + Y + Z);\n  } else { _x = _y = 0.5f;}  // softhack007: use default values in case of \"black\"\n  _rgb = ((r << 16) | (g << 8) | b);\n  _mode = EspalexaColorMode::xy;\n}\n\nvoid EspalexaDevice::doCallback()\n{\n  if (_callback != nullptr) {_callback(_val); return;}\n  if (_callbackDev != nullptr) {_callbackDev(this); return;}\n  if (_callbackCol != nullptr) _callbackCol(_val, getRGB());\n}"
  },
  {
    "path": "wled00/src/dependencies/espalexa/EspalexaDevice.h",
    "content": "#ifndef EspalexaDevice_h\n#define EspalexaDevice_h\n\n#include \"Arduino.h\"\n#include <functional>\n\nclass EspalexaDevice;\n\ntypedef std::function<void(uint8_t b)> BrightnessCallbackFunction;\ntypedef std::function<void(EspalexaDevice* d)> DeviceCallbackFunction;\ntypedef std::function<void(uint8_t br, uint32_t col)> ColorCallbackFunction;\n\nenum class EspalexaColorMode : uint8_t { none = 0, ct = 1, hs = 2, xy = 3 };\nenum class EspalexaDeviceType : uint8_t { onoff = 0, dimmable = 1, whitespectrum = 2, color = 3, extendedcolor = 4 };\nenum class EspalexaDeviceProperty : uint8_t { none = 0, on = 1, off = 2, bri = 3, hs = 4, ct = 5, xy = 6 };\n\nclass EspalexaDevice {\nprivate:\n  String _deviceName;\n  BrightnessCallbackFunction _callback = nullptr;\n  DeviceCallbackFunction _callbackDev = nullptr;\n  ColorCallbackFunction _callbackCol = nullptr;\n  uint8_t _val, _val_last, _sat = 0;\n  uint16_t _hue = 0, _ct = 0;\n  float _x = 0.5f, _y = 0.5f;\n  uint32_t _rgb = 0;\n  uint8_t _id = 0;\n  EspalexaDeviceType _type;\n  EspalexaDeviceProperty _changed = EspalexaDeviceProperty::none;\n  EspalexaColorMode _mode = EspalexaColorMode::xy;\n  \npublic:\n  EspalexaDevice();\n  ~EspalexaDevice();\n  EspalexaDevice(String deviceName, BrightnessCallbackFunction bcb, uint8_t initialValue =0);\n  EspalexaDevice(String deviceName, DeviceCallbackFunction dcb, EspalexaDeviceType t =EspalexaDeviceType::dimmable, uint8_t initialValue =0);\n  EspalexaDevice(String deviceName, ColorCallbackFunction ccb, uint8_t initialValue =0);\n  \n  String getName();\n  uint8_t getId();\n  EspalexaDeviceProperty getLastChangedProperty();\n  uint8_t getValue();\n  uint8_t getLastValue(); //last value that was not off (1-255)\n  bool    getState();\n  uint8_t getPercent();\n  uint8_t getDegrees();\n  uint16_t getHue();\n  uint8_t getSat();\n  uint16_t getCt();\n  uint32_t getKelvin();\n  float getX();\n  float getY();\n  uint32_t getRGB();\n  uint8_t getR();\n  uint8_t getG();\n  uint8_t getB();\n  uint8_t getW();\n  EspalexaColorMode getColorMode();\n  EspalexaDeviceType getType();\n  \n  void setId(uint8_t id);\n  void setPropertyChanged(EspalexaDeviceProperty p);\n  void setValue(uint8_t bri);\n  void setState(bool onoff);\n  void setPercent(uint8_t perc);\n  void setName(String name);\n  void setColor(uint16_t ct);\n  void setColor(uint16_t hue, uint8_t sat);\n  void setColorXY(float x, float y);\n  void setColor(uint8_t r, uint8_t g, uint8_t b);\n  \n  void doCallback();\n};\n\n#endif"
  },
  {
    "path": "wled00/src/dependencies/espalexa/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2017 Christian Schwinne\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "wled00/src/dependencies/json/ArduinoJson-v6.h",
    "content": "// ArduinoJson - https://arduinojson.org\n// Copyright Benoit Blanchon 2014-2021\n// MIT License\n\n#pragma once\n\n#ifdef __cplusplus\n\n#if __cplusplus >= 201103L\n#  define ARDUINOJSON_HAS_LONG_LONG 1\n#  define ARDUINOJSON_HAS_RVALUE_REFERENCES 1\n#else\n#  define ARDUINOJSON_HAS_LONG_LONG 0\n#  define ARDUINOJSON_HAS_RVALUE_REFERENCES 0\n#endif\n#ifndef ARDUINOJSON_HAS_NULLPTR\n#  if __cplusplus >= 201103L\n#    define ARDUINOJSON_HAS_NULLPTR 1\n#  else\n#    define ARDUINOJSON_HAS_NULLPTR 0\n#  endif\n#endif\n#if defined(_MSC_VER) && !ARDUINOJSON_HAS_LONG_LONG\n#  define ARDUINOJSON_HAS_INT64 1\n#else\n#  define ARDUINOJSON_HAS_INT64 0\n#endif\n#ifndef ARDUINOJSON_EMBEDDED_MODE\n#  if defined(ARDUINO)                /* Arduino*/                 \\\n      || defined(__IAR_SYSTEMS_ICC__) /* IAR Embedded Workbench */ \\\n      || defined(__XC)                /* MPLAB XC compiler */      \\\n      || defined(__ARMCC_VERSION)     /* Keil ARM Compiler */      \\\n      || defined(__AVR)               /* Atmel AVR8/GNU C Compiler */\n#    define ARDUINOJSON_EMBEDDED_MODE 1\n#  else\n#    define ARDUINOJSON_EMBEDDED_MODE 0\n#  endif\n#endif\n#if !defined(ARDUINOJSON_ENABLE_STD_STREAM) && defined(__has_include)\n#  if __has_include(<istream>) && \\\n    __has_include(<ostream>) && \\\n    !defined(min) && \\\n    !defined(max)\n#    define ARDUINOJSON_ENABLE_STD_STREAM 1\n#  else\n#    define ARDUINOJSON_ENABLE_STD_STREAM 0\n#  endif\n#endif\n#if !defined(ARDUINOJSON_ENABLE_STD_STRING) && defined(__has_include)\n#  if __has_include(<string>) && !defined(min) && !defined(max)\n#    define ARDUINOJSON_ENABLE_STD_STRING 1\n#  else\n#    define ARDUINOJSON_ENABLE_STD_STRING 0\n#  endif\n#endif\n#ifndef ARDUINOJSON_ENABLE_STRING_VIEW\n#  ifdef __has_include\n#    if __has_include(<string_view>) && __cplusplus >= 201703L\n#      define ARDUINOJSON_ENABLE_STRING_VIEW 1\n#    endif\n#  endif\n#endif\n#ifndef ARDUINOJSON_ENABLE_STRING_VIEW\n#  define ARDUINOJSON_ENABLE_STRING_VIEW 0\n#endif\n#if ARDUINOJSON_EMBEDDED_MODE\n#  ifndef ARDUINOJSON_USE_DOUBLE\n#    define ARDUINOJSON_USE_DOUBLE 0\n#  endif\n#  ifndef ARDUINOJSON_USE_LONG_LONG\n#    define ARDUINOJSON_USE_LONG_LONG 0\n#  endif\n#  ifndef ARDUINOJSON_ENABLE_STD_STRING\n#    define ARDUINOJSON_ENABLE_STD_STRING 0\n#  endif\n#  ifndef ARDUINOJSON_ENABLE_STD_STREAM\n#    define ARDUINOJSON_ENABLE_STD_STREAM 0\n#  endif\n#  ifndef ARDUINOJSON_DEFAULT_NESTING_LIMIT\n#    define ARDUINOJSON_DEFAULT_NESTING_LIMIT 10\n#  endif\n#  ifndef ARDUINOJSON_SLOT_OFFSET_SIZE\n#    if defined(__SIZEOF_POINTER__) && __SIZEOF_POINTER__ == 2\n#      define ARDUINOJSON_SLOT_OFFSET_SIZE 1\n#    else\n#      define ARDUINOJSON_SLOT_OFFSET_SIZE 2\n#    endif\n#  endif\n#else  // ARDUINOJSON_EMBEDDED_MODE\n#  ifndef ARDUINOJSON_USE_DOUBLE\n#    define ARDUINOJSON_USE_DOUBLE 1\n#  endif\n#  ifndef ARDUINOJSON_USE_LONG_LONG\n#    if ARDUINOJSON_HAS_LONG_LONG || ARDUINOJSON_HAS_INT64\n#      define ARDUINOJSON_USE_LONG_LONG 1\n#    else\n#      define ARDUINOJSON_USE_LONG_LONG 0\n#    endif\n#  endif\n#  ifndef ARDUINOJSON_ENABLE_STD_STRING\n#    define ARDUINOJSON_ENABLE_STD_STRING 1\n#  endif\n#  ifndef ARDUINOJSON_ENABLE_STD_STREAM\n#    define ARDUINOJSON_ENABLE_STD_STREAM 1\n#  endif\n#  ifndef ARDUINOJSON_DEFAULT_NESTING_LIMIT\n#    define ARDUINOJSON_DEFAULT_NESTING_LIMIT 50\n#  endif\n#  ifndef ARDUINOJSON_SLOT_OFFSET_SIZE\n#    define ARDUINOJSON_SLOT_OFFSET_SIZE 4\n#  endif\n#endif  // ARDUINOJSON_EMBEDDED_MODE\n#ifdef ARDUINO\n#include <Arduino.h>\n#  ifndef ARDUINOJSON_ENABLE_ARDUINO_STRING\n#    define ARDUINOJSON_ENABLE_ARDUINO_STRING 1\n#  endif\n#  ifndef ARDUINOJSON_ENABLE_ARDUINO_STREAM\n#    define ARDUINOJSON_ENABLE_ARDUINO_STREAM 1\n#  endif\n#  ifndef ARDUINOJSON_ENABLE_ARDUINO_PRINT\n#    define ARDUINOJSON_ENABLE_ARDUINO_PRINT 1\n#  endif\n#else  // ARDUINO\n#  ifndef ARDUINOJSON_ENABLE_ARDUINO_STRING\n#    define ARDUINOJSON_ENABLE_ARDUINO_STRING 0\n#  endif\n#  ifndef ARDUINOJSON_ENABLE_ARDUINO_STREAM\n#    define ARDUINOJSON_ENABLE_ARDUINO_STREAM 0\n#  endif\n#  ifndef ARDUINOJSON_ENABLE_ARDUINO_PRINT\n#    define ARDUINOJSON_ENABLE_ARDUINO_PRINT 0\n#  endif\n#endif  // ARDUINO\n#ifndef ARDUINOJSON_ENABLE_PROGMEM\n#  if defined(PROGMEM) && defined(pgm_read_byte) && defined(pgm_read_dword) && \\\n      defined(pgm_read_ptr) && defined(pgm_read_float)\n#    define ARDUINOJSON_ENABLE_PROGMEM 1\n#  else\n#    define ARDUINOJSON_ENABLE_PROGMEM 0\n#  endif\n#endif\n#ifndef ARDUINOJSON_DECODE_UNICODE\n#  define ARDUINOJSON_DECODE_UNICODE 1\n#endif\n#ifndef ARDUINOJSON_ENABLE_COMMENTS\n#  define ARDUINOJSON_ENABLE_COMMENTS 0\n#endif\n#ifndef ARDUINOJSON_ENABLE_NAN\n#  define ARDUINOJSON_ENABLE_NAN 0\n#endif\n#ifndef ARDUINOJSON_ENABLE_INFINITY\n#  define ARDUINOJSON_ENABLE_INFINITY 0\n#endif\n#ifndef ARDUINOJSON_POSITIVE_EXPONENTIATION_THRESHOLD\n#  define ARDUINOJSON_POSITIVE_EXPONENTIATION_THRESHOLD 1e7\n#endif\n#ifndef ARDUINOJSON_NEGATIVE_EXPONENTIATION_THRESHOLD\n#  define ARDUINOJSON_NEGATIVE_EXPONENTIATION_THRESHOLD 1e-5\n#endif\n#ifndef ARDUINOJSON_LITTLE_ENDIAN\n#  if defined(_MSC_VER) ||                           \\\n      (defined(__BYTE_ORDER__) &&                    \\\n       __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) || \\\n      defined(__LITTLE_ENDIAN__) || defined(__i386) || defined(__x86_64)\n#    define ARDUINOJSON_LITTLE_ENDIAN 1\n#  else\n#    define ARDUINOJSON_LITTLE_ENDIAN 0\n#  endif\n#endif\n#ifndef ARDUINOJSON_ENABLE_ALIGNMENT\n#  if defined(__AVR)\n#    define ARDUINOJSON_ENABLE_ALIGNMENT 0\n#  else\n#    define ARDUINOJSON_ENABLE_ALIGNMENT 1\n#  endif\n#endif\n#ifndef ARDUINOJSON_TAB\n#  define ARDUINOJSON_TAB \"  \"\n#endif\n#ifndef ARDUINOJSON_ENABLE_STRING_DEDUPLICATION\n#  define ARDUINOJSON_ENABLE_STRING_DEDUPLICATION 1\n#endif\n#ifndef ARDUINOJSON_STRING_BUFFER_SIZE\n#  define ARDUINOJSON_STRING_BUFFER_SIZE 32\n#endif\n#ifndef ARDUINOJSON_DEBUG\n#  ifdef __PLATFORMIO_BUILD_DEBUG__\n#    define ARDUINOJSON_DEBUG 1\n#  else\n#    define ARDUINOJSON_DEBUG 0\n#  endif\n#endif\n#if ARDUINOJSON_HAS_NULLPTR && defined(nullptr)\n#  error nullptr is defined as a macro. Remove the faulty #define or #undef nullptr\n#endif\n#if !ARDUINOJSON_DEBUG\n#  ifdef __clang__\n#    pragma clang system_header\n#  elif defined __GNUC__\n#    pragma GCC system_header\n#  endif\n#endif\n#define ARDUINOJSON_EXPAND6(a, b, c, d, e, f) a, b, c, d, e, f\n#define ARDUINOJSON_EXPAND9(a, b, c, d, e, f, g, h, i) a, b, c, d, e, f, g, h, i\n#define ARDUINOJSON_EXPAND18(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, \\\n                             q, r)                                           \\\n  a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r\n#define ARDUINOJSON_CONCAT_(A, B) A##B\n#define ARDUINOJSON_CONCAT2(A, B) ARDUINOJSON_CONCAT_(A, B)\n#define ARDUINOJSON_CONCAT4(A, B, C, D) \\\n  ARDUINOJSON_CONCAT2(ARDUINOJSON_CONCAT2(A, B), ARDUINOJSON_CONCAT2(C, D))\n#define ARDUINOJSON_HEX_DIGIT_0000() 0\n#define ARDUINOJSON_HEX_DIGIT_0001() 1\n#define ARDUINOJSON_HEX_DIGIT_0010() 2\n#define ARDUINOJSON_HEX_DIGIT_0011() 3\n#define ARDUINOJSON_HEX_DIGIT_0100() 4\n#define ARDUINOJSON_HEX_DIGIT_0101() 5\n#define ARDUINOJSON_HEX_DIGIT_0110() 6\n#define ARDUINOJSON_HEX_DIGIT_0111() 7\n#define ARDUINOJSON_HEX_DIGIT_1000() 8\n#define ARDUINOJSON_HEX_DIGIT_1001() 9\n#define ARDUINOJSON_HEX_DIGIT_1010() A\n#define ARDUINOJSON_HEX_DIGIT_1011() B\n#define ARDUINOJSON_HEX_DIGIT_1100() C\n#define ARDUINOJSON_HEX_DIGIT_1101() D\n#define ARDUINOJSON_HEX_DIGIT_1110() E\n#define ARDUINOJSON_HEX_DIGIT_1111() F\n#define ARDUINOJSON_HEX_DIGIT_(A, B, C, D) ARDUINOJSON_HEX_DIGIT_##A##B##C##D()\n#define ARDUINOJSON_HEX_DIGIT(A, B, C, D) ARDUINOJSON_HEX_DIGIT_(A, B, C, D)\n#define ARDUINOJSON_VERSION \"6.18.1\"\n#define ARDUINOJSON_VERSION_MAJOR 6\n#define ARDUINOJSON_VERSION_MINOR 18\n#define ARDUINOJSON_VERSION_REVISION 1\n#ifndef ARDUINOJSON_NAMESPACE\n#  define ARDUINOJSON_NAMESPACE                                               \\\n    ARDUINOJSON_CONCAT4(                                                      \\\n        ARDUINOJSON_CONCAT4(ArduinoJson, ARDUINOJSON_VERSION_MAJOR,           \\\n                            ARDUINOJSON_VERSION_MINOR,                        \\\n                            ARDUINOJSON_VERSION_REVISION),                    \\\n        _,                                                                    \\\n        ARDUINOJSON_HEX_DIGIT(                                                \\\n            ARDUINOJSON_ENABLE_PROGMEM, ARDUINOJSON_USE_LONG_LONG,            \\\n            ARDUINOJSON_USE_DOUBLE, ARDUINOJSON_ENABLE_STRING_DEDUPLICATION), \\\n        ARDUINOJSON_HEX_DIGIT(                                                \\\n            ARDUINOJSON_ENABLE_NAN, ARDUINOJSON_ENABLE_INFINITY,              \\\n            ARDUINOJSON_ENABLE_COMMENTS, ARDUINOJSON_DECODE_UNICODE))\n#endif\n#if ARDUINOJSON_DEBUG\n#include <assert.h>\n#  define ARDUINOJSON_ASSERT(X) assert(X)\n#else\n#  define ARDUINOJSON_ASSERT(X) ((void)0)\n#endif\n#include <stddef.h>\nnamespace ARDUINOJSON_NAMESPACE {\nclass MemoryPool;\nclass VariantData;\nclass VariantSlot;\nclass CollectionData {\n  VariantSlot *_head;\n  VariantSlot *_tail;\n public:\n  VariantData *addElement(MemoryPool *pool);\n  VariantData *getElement(size_t index) const;\n  VariantData *getOrAddElement(size_t index, MemoryPool *pool);\n  void removeElement(size_t index);\n  bool equalsArray(const CollectionData &other) const;\n  template <typename TAdaptedString>\n  VariantData *addMember(TAdaptedString key, MemoryPool *pool);\n  template <typename TAdaptedString>\n  VariantData *getMember(TAdaptedString key) const;\n  template <typename TAdaptedString>\n  VariantData *getOrAddMember(TAdaptedString key, MemoryPool *pool);\n  template <typename TAdaptedString>\n  void removeMember(TAdaptedString key) {\n    removeSlot(getSlot(key));\n  }\n  template <typename TAdaptedString>\n  bool containsKey(const TAdaptedString &key) const;\n  bool equalsObject(const CollectionData &other) const;\n  void clear();\n  size_t memoryUsage() const;\n  size_t nesting() const;\n  size_t size() const;\n  VariantSlot *addSlot(MemoryPool *);\n  void removeSlot(VariantSlot *slot);\n  bool copyFrom(const CollectionData &src, MemoryPool *pool);\n  VariantSlot *head() const {\n    return _head;\n  }\n  void movePointers(ptrdiff_t stringDistance, ptrdiff_t variantDistance);\n private:\n  VariantSlot *getSlot(size_t index) const;\n  template <typename TAdaptedString>\n  VariantSlot *getSlot(TAdaptedString key) const;\n  VariantSlot *getPreviousSlot(VariantSlot *) const;\n};\ninline VariantData *arrayAdd(CollectionData *arr, MemoryPool *pool) {\n  return arr ? arr->addElement(pool) : 0;\n}\ntemplate <typename TVisitor>\ninline typename TVisitor::result_type arrayAccept(const CollectionData *arr,\n                                                  TVisitor &visitor) {\n  if (arr)\n    return visitor.visitArray(*arr);\n  else\n    return visitor.visitNull();\n}\ninline bool arrayEquals(const CollectionData *lhs, const CollectionData *rhs) {\n  if (lhs == rhs)\n    return true;\n  if (!lhs || !rhs)\n    return false;\n  return lhs->equalsArray(*rhs);\n}\n#if ARDUINOJSON_ENABLE_ALIGNMENT\ninline bool isAligned(size_t value) {\n  const size_t mask = sizeof(void *) - 1;\n  size_t addr = value;\n  return (addr & mask) == 0;\n}\ninline size_t addPadding(size_t bytes) {\n  const size_t mask = sizeof(void *) - 1;\n  return (bytes + mask) & ~mask;\n}\ntemplate <size_t bytes>\nstruct AddPadding {\n  static const size_t mask = sizeof(void *) - 1;\n  static const size_t value = (bytes + mask) & ~mask;\n};\n#else\ninline bool isAligned(size_t) {\n  return true;\n}\ninline size_t addPadding(size_t bytes) {\n  return bytes;\n}\ntemplate <size_t bytes>\nstruct AddPadding {\n  static const size_t value = bytes;\n};\n#endif\ntemplate <typename T>\ninline bool isAligned(T *ptr) {\n  return isAligned(reinterpret_cast<size_t>(ptr));\n}\ntemplate <typename T>\ninline T *addPadding(T *p) {\n  size_t address = addPadding(reinterpret_cast<size_t>(p));\n  return reinterpret_cast<T *>(address);\n}\ntemplate <size_t X, size_t Y, bool MaxIsX = (X > Y)>\nstruct Max {};\ntemplate <size_t X, size_t Y>\nstruct Max<X, Y, true> {\n  static const size_t value = X;\n};\ntemplate <size_t X, size_t Y>\nstruct Max<X, Y, false> {\n  static const size_t value = Y;\n};\n}  // namespace ARDUINOJSON_NAMESPACE\n#include <string.h>\n#include <stdint.h>\nnamespace ARDUINOJSON_NAMESPACE {\ninline int safe_strcmp(const char* a, const char* b) {\n  if (a == b)\n    return 0;\n  if (!a)\n    return -1;\n  if (!b)\n    return 1;\n  return strcmp(a, b);\n}\ninline int safe_strncmp(const char* a, const char* b, size_t n) {\n  if (a == b)\n    return 0;\n  if (!a)\n    return -1;\n  if (!b)\n    return 1;\n  return strncmp(a, b, n);\n}\ntemplate <bool Condition, class TrueType, class FalseType>\nstruct conditional {\n  typedef TrueType type;\n};\ntemplate <class TrueType, class FalseType>\nstruct conditional<false, TrueType, FalseType> {\n  typedef FalseType type;\n};\ntemplate <bool Condition, typename T = void>\nstruct enable_if {};\ntemplate <typename T>\nstruct enable_if<true, T> {\n  typedef T type;\n};\ntemplate <typename T, T v>\nstruct integral_constant {\n  static const T value = v;\n};\ntypedef integral_constant<bool, true> true_type;\ntypedef integral_constant<bool, false> false_type;\ntemplate <typename T>\nstruct is_array : false_type {};\ntemplate <typename T>\nstruct is_array<T[]> : true_type {};\ntemplate <typename T, size_t N>\nstruct is_array<T[N]> : true_type {};\ntemplate <typename TBase, typename TDerived>\nclass is_base_of {\n protected:  // <- to avoid GCC's \"all member functions in class are private\"\n  typedef char Yes[1];\n  typedef char No[2];\n  static Yes &probe(const TBase *);\n  static No &probe(...);\n public:\n  static const bool value =\n      sizeof(probe(reinterpret_cast<TDerived *>(0))) == sizeof(Yes);\n};\ntemplate <typename T>\nT declval();\ntemplate <typename T>\nstruct is_class {\n protected:  // <- to avoid GCC's \"all member functions in class are private\"\n  typedef char Yes[1];\n  typedef char No[2];\n  template <typename U>\n  static Yes &probe(void (U::*)(void));\n  template <typename>\n  static No &probe(...);\n public:\n  static const bool value = sizeof(probe<T>(0)) == sizeof(Yes);\n};\ntemplate <typename T>\nstruct is_const : false_type {};\ntemplate <typename T>\nstruct is_const<const T> : true_type {};\n}  // namespace ARDUINOJSON_NAMESPACE\n#ifdef _MSC_VER\n#  pragma warning(push)\n#  pragma warning(disable : 4244)\n#endif\n#ifdef __ICCARM__\n#pragma diag_suppress=Pa093\n#endif\nnamespace ARDUINOJSON_NAMESPACE {\ntemplate <typename From, typename To>\nstruct is_convertible {\n protected:  // <- to avoid GCC's \"all member functions in class are private\"\n  typedef char Yes[1];\n  typedef char No[2];\n  static Yes &probe(To);\n  static No &probe(...);\n public:\n  static const bool value = sizeof(probe(declval<From>())) == sizeof(Yes);\n};\n}  // namespace ARDUINOJSON_NAMESPACE\n#ifdef _MSC_VER\n#  pragma warning(pop)\n#endif\n#ifdef __ICCARM__\n#pragma diag_default=Pa093\n#endif\nnamespace ARDUINOJSON_NAMESPACE {\ntemplate <typename T, typename U>\nstruct is_same : false_type {};\ntemplate <typename T>\nstruct is_same<T, T> : true_type {};\ntemplate <typename T>\nstruct remove_cv {\n  typedef T type;\n};\ntemplate <typename T>\nstruct remove_cv<const T> {\n  typedef T type;\n};\ntemplate <typename T>\nstruct remove_cv<volatile T> {\n  typedef T type;\n};\ntemplate <typename T>\nstruct remove_cv<const volatile T> {\n  typedef T type;\n};\ntemplate <class T>\nstruct is_floating_point\n    : integral_constant<\n          bool,  //\n          is_same<float, typename remove_cv<T>::type>::value ||\n              is_same<double, typename remove_cv<T>::type>::value> {};\ntemplate <typename T>\nstruct is_integral : integral_constant<bool,\n    is_same<typename remove_cv<T>::type, signed char>::value ||\n    is_same<typename remove_cv<T>::type, unsigned char>::value ||\n    is_same<typename remove_cv<T>::type, signed short>::value ||\n    is_same<typename remove_cv<T>::type, unsigned short>::value ||\n    is_same<typename remove_cv<T>::type, signed int>::value ||\n    is_same<typename remove_cv<T>::type, unsigned int>::value ||\n    is_same<typename remove_cv<T>::type, signed long>::value ||\n    is_same<typename remove_cv<T>::type, unsigned long>::value ||\n#if ARDUINOJSON_HAS_LONG_LONG\n    is_same<typename remove_cv<T>::type, signed long long>::value ||\n    is_same<typename remove_cv<T>::type, unsigned long long>::value ||\n#endif\n#if ARDUINOJSON_HAS_INT64\n    is_same<typename remove_cv<T>::type, signed __int64>::value ||\n    is_same<typename remove_cv<T>::type, unsigned __int64>::value ||\n#endif\n    is_same<typename remove_cv<T>::type, char>::value ||\n    is_same<typename remove_cv<T>::type, bool>::value> {};\ntemplate <typename T>\nstruct is_enum {\n  static const bool value = is_convertible<T, int>::value &&\n                            !is_class<T>::value && !is_integral<T>::value &&\n                            !is_floating_point<T>::value;\n};\ntemplate <typename T>\nstruct is_pointer : false_type {};\ntemplate <typename T>\nstruct is_pointer<T*> : true_type {};\ntemplate <typename T>\nstruct is_signed : integral_constant<bool, \n    is_same<typename remove_cv<T>::type, char>::value ||\n    is_same<typename remove_cv<T>::type, signed char>::value ||\n    is_same<typename remove_cv<T>::type, signed short>::value ||\n    is_same<typename remove_cv<T>::type, signed int>::value ||\n    is_same<typename remove_cv<T>::type, signed long>::value ||\n#if ARDUINOJSON_HAS_LONG_LONG\n    is_same<typename remove_cv<T>::type, signed long long>::value ||\n#endif\n#if ARDUINOJSON_HAS_INT64\n    is_same<typename remove_cv<T>::type, signed __int64>::value ||\n#endif\n    is_same<typename remove_cv<T>::type, float>::value ||\n    is_same<typename remove_cv<T>::type, double>::value> {};\ntemplate <typename T>\nstruct is_unsigned : integral_constant<bool,\n    is_same<typename remove_cv<T>::type, unsigned char>::value ||\n    is_same<typename remove_cv<T>::type, unsigned short>::value ||\n    is_same<typename remove_cv<T>::type, unsigned int>::value ||\n    is_same<typename remove_cv<T>::type, unsigned long>::value ||\n#if ARDUINOJSON_HAS_INT64\n    is_same<typename remove_cv<T>::type, unsigned __int64>::value ||\n#endif\n#if ARDUINOJSON_HAS_LONG_LONG\n    is_same<typename remove_cv<T>::type, unsigned long long>::value ||\n#endif\n    is_same<typename remove_cv<T>::type, bool>::value> {};\ntemplate <typename T>\nstruct type_identity {\n  typedef T type;\n};\ntemplate <typename T>\nstruct make_unsigned;\ntemplate <>\nstruct make_unsigned<char> : type_identity<unsigned char> {};\ntemplate <>\nstruct make_unsigned<signed char> : type_identity<unsigned char> {};\ntemplate <>\nstruct make_unsigned<unsigned char> : type_identity<unsigned char> {};\ntemplate <>\nstruct make_unsigned<signed short> : type_identity<unsigned short> {};\ntemplate <>\nstruct make_unsigned<unsigned short> : type_identity<unsigned short> {};\ntemplate <>\nstruct make_unsigned<signed int> : type_identity<unsigned int> {};\ntemplate <>\nstruct make_unsigned<unsigned int> : type_identity<unsigned int> {};\ntemplate <>\nstruct make_unsigned<signed long> : type_identity<unsigned long> {};\ntemplate <>\nstruct make_unsigned<unsigned long> : type_identity<unsigned long> {};\n#if ARDUINOJSON_HAS_LONG_LONG\ntemplate <>\nstruct make_unsigned<signed long long> : type_identity<unsigned long long> {};\ntemplate <>\nstruct make_unsigned<unsigned long long> : type_identity<unsigned long long> {};\n#endif\n#if ARDUINOJSON_HAS_INT64\ntemplate <>\nstruct make_unsigned<signed __int64> : type_identity<unsigned __int64> {};\ntemplate <>\nstruct make_unsigned<unsigned __int64> : type_identity<unsigned __int64> {};\n#endif\ntemplate <typename T>\nstruct remove_const {\n  typedef T type;\n};\ntemplate <typename T>\nstruct remove_const<const T> {\n  typedef T type;\n};\ntemplate <typename T>\nstruct remove_reference {\n  typedef T type;\n};\ntemplate <typename T>\nstruct remove_reference<T&> {\n  typedef T type;\n};\ntemplate <typename>\nstruct IsString : false_type {};\ntemplate <typename T>\nstruct IsString<const T> : IsString<T> {};\ntemplate <typename T>\nstruct IsString<T&> : IsString<T> {};\nnamespace storage_policies {\nstruct store_by_address {};\nstruct store_by_copy {};\nstruct decide_at_runtime {};\n}  // namespace storage_policies\nclass ConstRamStringAdapter {\n public:\n  ConstRamStringAdapter(const char* str = 0) : _str(str) {}\n  int compare(const char* other) const {\n    return safe_strcmp(_str, other);\n  }\n  bool equals(const char* expected) const {\n    return compare(expected) == 0;\n  }\n  bool isNull() const {\n    return !_str;\n  }\n  size_t size() const {\n    if (!_str)\n      return 0;\n    return strlen(_str);\n  }\n  const char* data() const {\n    return _str;\n  }\n  typedef storage_policies::store_by_address storage_policy;\n protected:\n  const char* _str;\n};\ntemplate <>\nstruct IsString<const char*> : true_type {};\ntemplate <int N>\nstruct IsString<const char[N]> : true_type {};\ninline ConstRamStringAdapter adaptString(const char* str) {\n  return ConstRamStringAdapter(str);\n}\nclass RamStringAdapter : public ConstRamStringAdapter {\n public:\n  RamStringAdapter(const char* str) : ConstRamStringAdapter(str) {}\n  void copyTo(char* p, size_t n) const {\n    memcpy(p, _str, n);\n  }\n  typedef ARDUINOJSON_NAMESPACE::storage_policies::store_by_copy storage_policy;\n};\ntemplate <typename TChar>\ninline RamStringAdapter adaptString(const TChar* str) {\n  return RamStringAdapter(reinterpret_cast<const char*>(str));\n}\ninline RamStringAdapter adaptString(char* str) {\n  return RamStringAdapter(str);\n}\ntemplate <typename TChar>\nstruct IsString<TChar*> {\n  static const bool value = sizeof(TChar) == 1;\n};\ntemplate <>\nstruct IsString<void*> {\n  static const bool value = false;\n};\nclass SizedRamStringAdapter {\n public:\n  SizedRamStringAdapter(const char* str, size_t n) : _str(str), _size(n) {}\n  int compare(const char* other) const {\n    return safe_strncmp(_str, other, _size);\n  }\n  bool equals(const char* expected) const {\n    return compare(expected) == 0;\n  }\n  bool isNull() const {\n    return !_str;\n  }\n  void copyTo(char* p, size_t n) const {\n    memcpy(p, _str, n);\n  }\n  size_t size() const {\n    return _size;\n  }\n  typedef storage_policies::store_by_copy storage_policy;\n private:\n  const char* _str;\n  size_t _size;\n};\ntemplate <typename TChar>\ninline SizedRamStringAdapter adaptString(const TChar* str, size_t size) {\n  return SizedRamStringAdapter(reinterpret_cast<const char*>(str), size);\n}\n}  // namespace ARDUINOJSON_NAMESPACE\n#if ARDUINOJSON_ENABLE_STD_STRING\n#include <string>\nnamespace ARDUINOJSON_NAMESPACE {\ntemplate <typename TString>\nclass StdStringAdapter {\n public:\n  StdStringAdapter(const TString& str) : _str(&str) {}\n  void copyTo(char* p, size_t n) const {\n    memcpy(p, _str->c_str(), n);\n  }\n  bool isNull() const {\n    return false;\n  }\n  int compare(const char* other) const {\n    if (!other)\n      return 1;\n    return _str->compare(other);\n  }\n  bool equals(const char* expected) const {\n    if (!expected)\n      return false;\n    return *_str == expected;\n  }\n  size_t size() const {\n    return _str->size();\n  }\n  typedef storage_policies::store_by_copy storage_policy;\n private:\n  const TString* _str;\n};\ntemplate <typename TCharTraits, typename TAllocator>\nstruct IsString<std::basic_string<char, TCharTraits, TAllocator> > : true_type {\n};\ntemplate <typename TCharTraits, typename TAllocator>\ninline StdStringAdapter<std::basic_string<char, TCharTraits, TAllocator> >\nadaptString(const std::basic_string<char, TCharTraits, TAllocator>& str) {\n  return StdStringAdapter<std::basic_string<char, TCharTraits, TAllocator> >(\n      str);\n}\n}  // namespace ARDUINOJSON_NAMESPACE\n#endif\n#if ARDUINOJSON_ENABLE_STRING_VIEW\n#include <string_view>\nnamespace ARDUINOJSON_NAMESPACE {\nclass StringViewAdapter {\n public:\n  StringViewAdapter(std::string_view str) : _str(str) {}\n  void copyTo(char* p, size_t n) const {\n    memcpy(p, _str.data(), n);\n  }\n  bool isNull() const {\n    return false;\n  }\n  int compare(const char* other) const {\n    if (!other)\n      return 1;\n    return _str.compare(other);\n  }\n  bool equals(const char* expected) const {\n    if (!expected)\n      return false;\n    return _str == expected;\n  }\n  size_t size() const {\n    return _str.size();\n  }\n  typedef storage_policies::store_by_copy storage_policy;\n private:\n  std::string_view _str;\n};\ntemplate <>\nstruct IsString<std::string_view> : true_type {};\ninline StringViewAdapter adaptString(const std::string_view& str) {\n  return StringViewAdapter(str);\n}\n}  // namespace ARDUINOJSON_NAMESPACE\n#endif\n#if ARDUINOJSON_ENABLE_ARDUINO_STRING\nnamespace ARDUINOJSON_NAMESPACE {\nclass ArduinoStringAdapter {\n public:\n  ArduinoStringAdapter(const ::String& str) : _str(&str) {}\n  void copyTo(char* p, size_t n) const {\n    memcpy(p, _str->c_str(), n);\n  }\n  bool isNull() const {\n    return !_str->c_str();\n  }\n  int compare(const char* other) const {\n    const char* me = _str->c_str();\n    return safe_strcmp(me, other);\n  }\n  bool equals(const char* expected) const {\n    return compare(expected) == 0;\n  }\n  size_t size() const {\n    return _str->length();\n  }\n  typedef storage_policies::store_by_copy storage_policy;\n private:\n  const ::String* _str;\n};\ntemplate <>\nstruct IsString< ::String> : true_type {};\ntemplate <>\nstruct IsString< ::StringSumHelper> : true_type {};\ninline ArduinoStringAdapter adaptString(const ::String& str) {\n  return ArduinoStringAdapter(str);\n}\n}  // namespace ARDUINOJSON_NAMESPACE\n#endif\n#if ARDUINOJSON_ENABLE_PROGMEM\nnamespace ARDUINOJSON_NAMESPACE {\nstruct pgm_p {\n  pgm_p(const char* p) : address(p) {}\n  const char* address;\n};\n}  // namespace ARDUINOJSON_NAMESPACE\n#ifndef strlen_P\ninline size_t strlen_P(ARDUINOJSON_NAMESPACE::pgm_p s) {\n  const char* p = s.address;\n  ARDUINOJSON_ASSERT(p != NULL);\n  while (pgm_read_byte(p)) p++;\n  return size_t(p - s.address);\n}\n#endif\n#ifndef strncmp_P\ninline int strncmp_P(const char* a, ARDUINOJSON_NAMESPACE::pgm_p b, size_t n) {\n  const char* s1 = a;\n  const char* s2 = b.address;\n  ARDUINOJSON_ASSERT(s1 != NULL);\n  ARDUINOJSON_ASSERT(s2 != NULL);\n  while (n-- > 0) {\n    char c1 = *s1++;\n    char c2 = static_cast<char>(pgm_read_byte(s2++));\n    if (c1 < c2)\n      return -1;\n    if (c1 > c2)\n      return 1;\n    if (c1 == 0 /* and c2 as well */)\n      return 0;\n  }\n  return 0;\n}\n#endif\n#ifndef strcmp_P\ninline int strcmp_P(const char* a, ARDUINOJSON_NAMESPACE::pgm_p b) {\n  const char* s1 = a;\n  const char* s2 = b.address;\n  ARDUINOJSON_ASSERT(s1 != NULL);\n  ARDUINOJSON_ASSERT(s2 != NULL);\n  for (;;) {\n    char c1 = *s1++;\n    char c2 = static_cast<char>(pgm_read_byte(s2++));\n    if (c1 < c2)\n      return -1;\n    if (c1 > c2)\n      return 1;\n    if (c1 == 0 /* and c2 as well */)\n      return 0;\n  }\n}\n#endif\n#ifndef memcpy_P\ninline void* memcpy_P(void* dst, ARDUINOJSON_NAMESPACE::pgm_p src, size_t n) {\n  uint8_t* d = reinterpret_cast<uint8_t*>(dst);\n  const char* s = src.address;\n  ARDUINOJSON_ASSERT(d != NULL);\n  ARDUINOJSON_ASSERT(s != NULL);\n  while (n-- > 0) {\n    *d++ = pgm_read_byte(s++);\n  }\n  return dst;\n}\n#endif\nnamespace ARDUINOJSON_NAMESPACE {\nclass FlashStringAdapter {\n public:\n  FlashStringAdapter(const __FlashStringHelper* str) : _str(str) {}\n  int compare(const char* other) const {\n    if (!other && !_str)\n      return 0;\n    if (!_str)\n      return -1;\n    if (!other)\n      return 1;\n    return -strcmp_P(other, reinterpret_cast<const char*>(_str));\n  }\n  bool equals(const char* expected) const {\n    return compare(expected) == 0;\n  }\n  bool isNull() const {\n    return !_str;\n  }\n  void copyTo(char* p, size_t n) const {\n    memcpy_P(p, reinterpret_cast<const char*>(_str), n);\n  }\n  size_t size() const {\n    if (!_str)\n      return 0;\n    return strlen_P(reinterpret_cast<const char*>(_str));\n  }\n  typedef storage_policies::store_by_copy storage_policy;\n private:\n  const __FlashStringHelper* _str;\n};\ninline FlashStringAdapter adaptString(const __FlashStringHelper* str) {\n  return FlashStringAdapter(str);\n}\ntemplate <>\nstruct IsString<const __FlashStringHelper*> : true_type {};\nclass SizedFlashStringAdapter {\n public:\n  SizedFlashStringAdapter(const __FlashStringHelper* str, size_t sz)\n      : _str(str), _size(sz) {}\n  int compare(const char* other) const {\n    if (!other && !_str)\n      return 0;\n    if (!_str)\n      return -1;\n    if (!other)\n      return 1;\n    return -strncmp_P(other, reinterpret_cast<const char*>(_str), _size);\n  }\n  bool equals(const char* expected) const {\n    return compare(expected) == 0;\n  }\n  bool isNull() const {\n    return !_str;\n  }\n  void copyTo(char* p, size_t n) const {\n    memcpy_P(p, reinterpret_cast<const char*>(_str), n);\n  }\n  size_t size() const {\n    return _size;\n  }\n  typedef storage_policies::store_by_copy storage_policy;\n private:\n  const __FlashStringHelper* _str;\n  size_t _size;\n};\ninline SizedFlashStringAdapter adaptString(const __FlashStringHelper* str,\n                                           size_t sz) {\n  return SizedFlashStringAdapter(str, sz);\n}\n}  // namespace ARDUINOJSON_NAMESPACE\n#endif\nnamespace ARDUINOJSON_NAMESPACE {\ntemplate <int Bits>\nstruct int_t;\ntemplate <>\nstruct int_t<8> {\n  typedef int8_t type;\n};\ntemplate <>\nstruct int_t<16> {\n  typedef int16_t type;\n};\ntemplate <>\nstruct int_t<32> {\n  typedef int32_t type;\n};\n}  // namespace ARDUINOJSON_NAMESPACE\n#ifdef _MSC_VER\n#  pragma warning(push)\n#  pragma warning(disable : 4310)\n#endif\nnamespace ARDUINOJSON_NAMESPACE {\ntemplate <typename T, typename Enable = void>\nstruct numeric_limits;\ntemplate <typename T>\nstruct numeric_limits<T, typename enable_if<is_unsigned<T>::value>::type> {\n  static T lowest() {\n    return 0;\n  }\n  static T highest() {\n    return T(-1);\n  }\n};\ntemplate <typename T>\nstruct numeric_limits<\n    T, typename enable_if<is_integral<T>::value && is_signed<T>::value>::type> {\n  static T lowest() {\n    return T(T(1) << (sizeof(T) * 8 - 1));\n  }\n  static T highest() {\n    return T(~lowest());\n  }\n};\n}  // namespace ARDUINOJSON_NAMESPACE\n#ifdef _MSC_VER\n#  pragma warning(pop)\n#endif\nnamespace ARDUINOJSON_NAMESPACE {\n#if ARDUINOJSON_USE_DOUBLE\ntypedef double Float;\n#else\ntypedef float Float;\n#endif\n#if ARDUINOJSON_USE_LONG_LONG\ntypedef int64_t Integer;\ntypedef uint64_t UInt;\n#else\ntypedef long Integer;\ntypedef unsigned long UInt;\n#endif\n}  // namespace ARDUINOJSON_NAMESPACE\n#if ARDUINOJSON_HAS_LONG_LONG && !ARDUINOJSON_USE_LONG_LONG\n#  define ARDUINOJSON_ASSERT_INTEGER_TYPE_IS_SUPPORTED(T)                  \\\n    static_assert(sizeof(T) <= sizeof(ARDUINOJSON_NAMESPACE::Integer),     \\\n                  \"To use 64-bit integers with ArduinoJson, you must set \" \\\n                  \"ARDUINOJSON_USE_LONG_LONG to 1. See \"                   \\\n                  \"https://arduinojson.org/v6/api/config/use_long_long/\");\n#else\n#  define ARDUINOJSON_ASSERT_INTEGER_TYPE_IS_SUPPORTED(T)\n#endif\nnamespace ARDUINOJSON_NAMESPACE {\nenum {\n  VALUE_MASK = 0x7F,\n  OWNED_VALUE_BIT = 0x01,\n  VALUE_IS_NULL = 0,\n  VALUE_IS_LINKED_RAW = 0x02,\n  VALUE_IS_OWNED_RAW = 0x03,\n  VALUE_IS_LINKED_STRING = 0x04,\n  VALUE_IS_OWNED_STRING = 0x05,\n  VALUE_IS_BOOLEAN = 0x06,\n  NUMBER_BIT = 0x08,\n  VALUE_IS_UNSIGNED_INTEGER = 0x08,\n  VALUE_IS_SIGNED_INTEGER = 0x0A,\n  VALUE_IS_FLOAT = 0x0C,\n  COLLECTION_MASK = 0x60,\n  VALUE_IS_OBJECT = 0x20,\n  VALUE_IS_ARRAY = 0x40,\n  OWNED_KEY_BIT = 0x80\n};\nstruct RawData {\n  const char *data;\n  size_t size;\n};\nunion VariantContent {\n  Float asFloat;\n  bool asBoolean;\n  UInt asUnsignedInteger;\n  Integer asSignedInteger;\n  CollectionData asCollection;\n  const char *asString;\n  struct {\n    const char *data;\n    size_t size;\n  } asRaw;\n};\ntypedef int_t<ARDUINOJSON_SLOT_OFFSET_SIZE * 8>::type VariantSlotDiff;\nclass VariantSlot {\n  VariantContent _content;\n  uint8_t _flags;\n  VariantSlotDiff _next;\n  const char* _key;\n public:\n  VariantData* data() {\n    return reinterpret_cast<VariantData*>(&_content);\n  }\n  const VariantData* data() const {\n    return reinterpret_cast<const VariantData*>(&_content);\n  }\n  VariantSlot* next() {\n    return _next ? this + _next : 0;\n  }\n  const VariantSlot* next() const {\n    return const_cast<VariantSlot*>(this)->next();\n  }\n  VariantSlot* next(size_t distance) {\n    VariantSlot* slot = this;\n    while (distance--) {\n      if (!slot->_next)\n        return 0;\n      slot += slot->_next;\n    }\n    return slot;\n  }\n  const VariantSlot* next(size_t distance) const {\n    return const_cast<VariantSlot*>(this)->next(distance);\n  }\n  void setNext(VariantSlot* slot) {\n    ARDUINOJSON_ASSERT(!slot || slot - this >=\n                                    numeric_limits<VariantSlotDiff>::lowest());\n    ARDUINOJSON_ASSERT(!slot || slot - this <=\n                                    numeric_limits<VariantSlotDiff>::highest());\n    _next = VariantSlotDiff(slot ? slot - this : 0);\n  }\n  void setNextNotNull(VariantSlot* slot) {\n    ARDUINOJSON_ASSERT(slot != 0);\n    ARDUINOJSON_ASSERT(slot - this >=\n                       numeric_limits<VariantSlotDiff>::lowest());\n    ARDUINOJSON_ASSERT(slot - this <=\n                       numeric_limits<VariantSlotDiff>::highest());\n    _next = VariantSlotDiff(slot - this);\n  }\n  void setKey(const char* k, storage_policies::store_by_copy) {\n    ARDUINOJSON_ASSERT(k != NULL);\n    _flags |= OWNED_KEY_BIT;\n    _key = k;\n  }\n  void setKey(const char* k, storage_policies::store_by_address) {\n    ARDUINOJSON_ASSERT(k != NULL);\n    _flags &= VALUE_MASK;\n    _key = k;\n  }\n  const char* key() const {\n    return _key;\n  }\n  bool ownsKey() const {\n    return (_flags & OWNED_KEY_BIT) != 0;\n  }\n  void clear() {\n    _next = 0;\n    _flags = 0;\n    _key = 0;\n  }\n  void movePointers(ptrdiff_t stringDistance, ptrdiff_t variantDistance) {\n    if (_flags & OWNED_KEY_BIT)\n      _key += stringDistance;\n    if (_flags & OWNED_VALUE_BIT)\n      _content.asString += stringDistance;\n    if (_flags & COLLECTION_MASK)\n      _content.asCollection.movePointers(stringDistance, variantDistance);\n  }\n};\n}  // namespace ARDUINOJSON_NAMESPACE\n#define JSON_STRING_SIZE(SIZE) (SIZE + 1)\nnamespace ARDUINOJSON_NAMESPACE {\nclass MemoryPool {\n public:\n  MemoryPool(char* buf, size_t capa)\n      : _begin(buf),\n        _left(buf),\n        _right(buf ? buf + capa : 0),\n        _end(buf ? buf + capa : 0),\n        _overflowed(false) {\n    ARDUINOJSON_ASSERT(isAligned(_begin));\n    ARDUINOJSON_ASSERT(isAligned(_right));\n    ARDUINOJSON_ASSERT(isAligned(_end));\n  }\n  void* buffer() {\n    return _begin;  // NOLINT(clang-analyzer-unix.Malloc)\n  }\n  size_t capacity() const {\n    return size_t(_end - _begin);\n  }\n  size_t size() const {\n    return size_t(_left - _begin + _end - _right);\n  }\n  bool overflowed() const {\n    return _overflowed;\n  }\n  VariantSlot* allocVariant() {\n    return allocRight<VariantSlot>();\n  }\n  template <typename TAdaptedString>\n  const char* saveString(const TAdaptedString& str) {\n    if (str.isNull())\n      return 0;\n#if ARDUINOJSON_ENABLE_STRING_DEDUPLICATION\n    const char* existingCopy = findString(str);\n    if (existingCopy)\n      return existingCopy;\n#endif\n    size_t n = str.size();\n    char* newCopy = allocString(n + 1);\n    if (newCopy) {\n      str.copyTo(newCopy, n);\n      newCopy[n] = 0;  // force null-terminator\n    }\n    return newCopy;\n  }\n  void getFreeZone(char** zoneStart, size_t* zoneSize) const {\n    *zoneStart = _left;\n    *zoneSize = size_t(_right - _left);\n  }\n  const char* saveStringFromFreeZone(size_t len) {\n#if ARDUINOJSON_ENABLE_STRING_DEDUPLICATION\n    const char* dup = findString(adaptString(_left));\n    if (dup)\n      return dup;\n#endif\n    const char* str = _left;\n    _left += len;\n    checkInvariants();\n    return str;\n  }\n  void markAsOverflowed() {\n    _overflowed = true;\n  }\n  void clear() {\n    _left = _begin;\n    _right = _end;\n    _overflowed = false;\n  }\n  bool canAlloc(size_t bytes) const {\n    return _left + bytes <= _right;\n  }\n  bool owns(void* p) const {\n    return _begin <= p && p < _end;\n  }\n  void* operator new(size_t, void* p) {\n    return p;\n  }\n  ptrdiff_t squash() {\n    char* new_right = addPadding(_left);\n    if (new_right >= _right)\n      return 0;\n    size_t right_size = static_cast<size_t>(_end - _right);\n    memmove(new_right, _right, right_size);\n    ptrdiff_t bytes_reclaimed = _right - new_right;\n    _right = new_right;\n    _end = new_right + right_size;\n    return bytes_reclaimed;\n  }\n  void movePointers(ptrdiff_t offset) {\n    _begin += offset;\n    _left += offset;\n    _right += offset;\n    _end += offset;\n  }\n private:\n  void checkInvariants() {\n    ARDUINOJSON_ASSERT(_begin <= _left);\n    ARDUINOJSON_ASSERT(_left <= _right);\n    ARDUINOJSON_ASSERT(_right <= _end);\n    ARDUINOJSON_ASSERT(isAligned(_right));\n  }\n#if ARDUINOJSON_ENABLE_STRING_DEDUPLICATION\n  template <typename TAdaptedString>\n  const char* findString(const TAdaptedString& str) {\n    for (char* next = _begin; next < _left; ++next) {\n      if (str.equals(next))\n        return next;\n      while (*next) ++next;\n    }\n    return 0;\n  }\n#endif\n  char* allocString(size_t n) {\n    if (!canAlloc(n)) {\n      _overflowed = true;\n      return 0;\n    }\n    char* s = _left;\n    _left += n;\n    checkInvariants();\n    return s;\n  }\n  template <typename T>\n  T* allocRight() {\n    return reinterpret_cast<T*>(allocRight(sizeof(T)));\n  }\n  void* allocRight(size_t bytes) {\n    if (!canAlloc(bytes)) {\n      _overflowed = true;\n      return 0;\n    }\n    _right -= bytes;\n    return _right;\n  }\n  char *_begin, *_left, *_right, *_end;\n  bool _overflowed;\n};\ntemplate <typename T>\nclass SerializedValue {\n public:\n  explicit SerializedValue(T str) : _str(str) {}\n  operator T() const {\n    return _str;\n  }\n  const char* data() const {\n    return _str.c_str();\n  }\n  size_t size() const {\n    return _str.length();\n  }\n private:\n  T _str;\n};\ntemplate <typename TChar>\nclass SerializedValue<TChar*> {\n public:\n  explicit SerializedValue(TChar* p, size_t n) : _data(p), _size(n) {}\n  operator TChar*() const {\n    return _data;\n  }\n  TChar* data() const {\n    return _data;\n  }\n  size_t size() const {\n    return _size;\n  }\n private:\n  TChar* _data;\n  size_t _size;\n};\ntemplate <typename T>\ninline SerializedValue<T> serialized(T str) {\n  return SerializedValue<T>(str);\n}\ntemplate <typename TChar>\ninline SerializedValue<TChar*> serialized(TChar* p) {\n  return SerializedValue<TChar*>(p, adaptString(p).size());\n}\ntemplate <typename TChar>\ninline SerializedValue<TChar*> serialized(TChar* p, size_t n) {\n  return SerializedValue<TChar*>(p, n);\n}\n}  // namespace ARDUINOJSON_NAMESPACE\n#if defined(__clang__)\n#  pragma clang diagnostic push\n#  pragma clang diagnostic ignored \"-Wconversion\"\n#elif defined(__GNUC__)\n#  if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6)\n#    pragma GCC diagnostic push\n#  endif\n#  pragma GCC diagnostic ignored \"-Wconversion\"\n#endif\nnamespace ARDUINOJSON_NAMESPACE {\ntemplate <typename TOut, typename TIn>\ntypename enable_if<is_integral<TIn>::value && is_unsigned<TIn>::value &&\n                       is_integral<TOut>::value && sizeof(TOut) <= sizeof(TIn),\n                   bool>::type\ncanConvertNumber(TIn value) {\n  return value <= TIn(numeric_limits<TOut>::highest());\n}\ntemplate <typename TOut, typename TIn>\ntypename enable_if<is_integral<TIn>::value && is_unsigned<TIn>::value &&\n                       is_integral<TOut>::value && sizeof(TIn) < sizeof(TOut),\n                   bool>::type\ncanConvertNumber(TIn) {\n  return true;\n}\ntemplate <typename TOut, typename TIn>\ntypename enable_if<is_integral<TIn>::value && is_floating_point<TOut>::value,\n                   bool>::type\ncanConvertNumber(TIn) {\n  return true;\n}\ntemplate <typename TOut, typename TIn>\ntypename enable_if<is_integral<TIn>::value && is_signed<TIn>::value &&\n                       is_integral<TOut>::value && is_signed<TOut>::value &&\n                       sizeof(TOut) < sizeof(TIn),\n                   bool>::type\ncanConvertNumber(TIn value) {\n  return value >= TIn(numeric_limits<TOut>::lowest()) &&\n         value <= TIn(numeric_limits<TOut>::highest());\n}\ntemplate <typename TOut, typename TIn>\ntypename enable_if<is_integral<TIn>::value && is_signed<TIn>::value &&\n                       is_integral<TOut>::value && is_signed<TOut>::value &&\n                       sizeof(TIn) <= sizeof(TOut),\n                   bool>::type\ncanConvertNumber(TIn) {\n  return true;\n}\ntemplate <typename TOut, typename TIn>\ntypename enable_if<is_integral<TIn>::value && is_signed<TIn>::value &&\n                       is_integral<TOut>::value && is_unsigned<TOut>::value &&\n                       sizeof(TOut) >= sizeof(TIn),\n                   bool>::type\ncanConvertNumber(TIn value) {\n  if (value < 0)\n    return false;\n  return TOut(value) <= numeric_limits<TOut>::highest();\n}\ntemplate <typename TOut, typename TIn>\ntypename enable_if<is_integral<TIn>::value && is_signed<TIn>::value &&\n                       is_integral<TOut>::value && is_unsigned<TOut>::value &&\n                       sizeof(TOut) < sizeof(TIn),\n                   bool>::type\ncanConvertNumber(TIn value) {\n  if (value < 0)\n    return false;\n  return value <= TIn(numeric_limits<TOut>::highest());\n}\ntemplate <typename TOut, typename TIn>\ntypename enable_if<is_floating_point<TIn>::value &&\n                       !is_floating_point<TOut>::value,\n                   bool>::type\ncanConvertNumber(TIn value) {\n  return value >= numeric_limits<TOut>::lowest() &&\n         value <= numeric_limits<TOut>::highest();\n}\ntemplate <typename TOut, typename TIn>\nTOut convertNumber(TIn value) {\n  return canConvertNumber<TOut>(value) ? TOut(value) : 0;\n}\n}  // namespace ARDUINOJSON_NAMESPACE\n#if defined(__clang__)\n#  pragma clang diagnostic pop\n#elif defined(__GNUC__)\n#  if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6)\n#    pragma GCC diagnostic pop\n#  endif\n#endif\n#if defined(__GNUC__)\n#  if __GNUC__ >= 7\n#    pragma GCC diagnostic push\n#    pragma GCC diagnostic ignored \"-Wmaybe-uninitialized\"\n#    pragma GCC diagnostic ignored \"-Wuninitialized\"\n#  endif\n#endif\nnamespace ARDUINOJSON_NAMESPACE {\nclass VariantData {\n  VariantContent _content;  // must be first to allow cast from array to variant\n  uint8_t _flags;\n public:\n  void init() {\n    _flags = VALUE_IS_NULL;\n  }\n  template <typename TVisitor>\n  typename TVisitor::result_type accept(TVisitor &visitor) const {\n    switch (type()) {\n      case VALUE_IS_FLOAT:\n        return visitor.visitFloat(_content.asFloat);\n      case VALUE_IS_ARRAY:\n        return visitor.visitArray(_content.asCollection);\n      case VALUE_IS_OBJECT:\n        return visitor.visitObject(_content.asCollection);\n      case VALUE_IS_LINKED_STRING:\n      case VALUE_IS_OWNED_STRING:\n        return visitor.visitString(_content.asString);\n      case VALUE_IS_OWNED_RAW:\n      case VALUE_IS_LINKED_RAW:\n        return visitor.visitRawJson(_content.asRaw.data, _content.asRaw.size);\n      case VALUE_IS_SIGNED_INTEGER:\n        return visitor.visitSignedInteger(_content.asSignedInteger);\n      case VALUE_IS_UNSIGNED_INTEGER:\n        return visitor.visitUnsignedInteger(_content.asUnsignedInteger);\n      case VALUE_IS_BOOLEAN:\n        return visitor.visitBoolean(_content.asBoolean != 0);\n      default:\n        return visitor.visitNull();\n    }\n  }\n  template <typename T>\n  T asIntegral() const;\n  template <typename T>\n  T asFloat() const;\n  const char *asString() const;\n  bool asBoolean() const;\n  CollectionData *asArray() {\n    return isArray() ? &_content.asCollection : 0;\n  }\n  const CollectionData *asArray() const {\n    return const_cast<VariantData *>(this)->asArray();\n  }\n  CollectionData *asObject() {\n    return isObject() ? &_content.asCollection : 0;\n  }\n  const CollectionData *asObject() const {\n    return const_cast<VariantData *>(this)->asObject();\n  }\n  bool copyFrom(const VariantData &src, MemoryPool *pool) {\n    switch (src.type()) {\n      case VALUE_IS_ARRAY:\n        return toArray().copyFrom(src._content.asCollection, pool);\n      case VALUE_IS_OBJECT:\n        return toObject().copyFrom(src._content.asCollection, pool);\n      case VALUE_IS_OWNED_STRING:\n        return setString(RamStringAdapter(src._content.asString), pool);\n      case VALUE_IS_OWNED_RAW:\n        return setOwnedRaw(\n            serialized(src._content.asRaw.data, src._content.asRaw.size), pool);\n      default:\n        setType(src.type());\n        _content = src._content;\n        return true;\n    }\n  }\n  bool isArray() const {\n    return (_flags & VALUE_IS_ARRAY) != 0;\n  }\n  bool isBoolean() const {\n    return type() == VALUE_IS_BOOLEAN;\n  }\n  bool isCollection() const {\n    return (_flags & COLLECTION_MASK) != 0;\n  }\n  template <typename T>\n  bool isInteger() const {\n    switch (type()) {\n      case VALUE_IS_UNSIGNED_INTEGER:\n        return canConvertNumber<T>(_content.asUnsignedInteger);\n      case VALUE_IS_SIGNED_INTEGER:\n        return canConvertNumber<T>(_content.asSignedInteger);\n      default:\n        return false;\n    }\n  }\n  bool isFloat() const {\n    return (_flags & NUMBER_BIT) != 0;\n  }\n  bool isString() const {\n    return type() == VALUE_IS_LINKED_STRING || type() == VALUE_IS_OWNED_STRING;\n  }\n  bool isObject() const {\n    return (_flags & VALUE_IS_OBJECT) != 0;\n  }\n  bool isNull() const {\n    return type() == VALUE_IS_NULL;\n  }\n  bool isEnclosed() const {\n    return !isFloat();\n  }\n  void remove(size_t index) {\n    if (isArray())\n      _content.asCollection.removeElement(index);\n  }\n  template <typename TAdaptedString>\n  void remove(TAdaptedString key) {\n    if (isObject())\n      _content.asCollection.removeMember(key);\n  }\n  void setBoolean(bool value) {\n    setType(VALUE_IS_BOOLEAN);\n    _content.asBoolean = value;\n  }\n  void setFloat(Float value) {\n    setType(VALUE_IS_FLOAT);\n    _content.asFloat = value;\n  }\n  void setLinkedRaw(SerializedValue<const char *> value) {\n    if (value.data()) {\n      setType(VALUE_IS_LINKED_RAW);\n      _content.asRaw.data = value.data();\n      _content.asRaw.size = value.size();\n    } else {\n      setType(VALUE_IS_NULL);\n    }\n  }\n  template <typename T>\n  bool setOwnedRaw(SerializedValue<T> value, MemoryPool *pool) {\n    const char *dup = pool->saveString(adaptString(value.data(), value.size()));\n    if (dup) {\n      setType(VALUE_IS_OWNED_RAW);\n      _content.asRaw.data = dup;\n      _content.asRaw.size = value.size();\n      return true;\n    } else {\n      setType(VALUE_IS_NULL);\n      return false;\n    }\n  }\n  template <typename T>\n  typename enable_if<is_unsigned<T>::value>::type setInteger(T value) {\n    setType(VALUE_IS_UNSIGNED_INTEGER);\n    _content.asUnsignedInteger = static_cast<UInt>(value);\n  }\n  template <typename T>\n  typename enable_if<is_signed<T>::value>::type setInteger(T value) {\n    setType(VALUE_IS_SIGNED_INTEGER);\n    _content.asSignedInteger = value;\n  }\n  void setNull() {\n    setType(VALUE_IS_NULL);\n  }\n  void setStringPointer(const char *s, storage_policies::store_by_copy) {\n    ARDUINOJSON_ASSERT(s != 0);\n    setType(VALUE_IS_OWNED_STRING);\n    _content.asString = s;\n  }\n  void setStringPointer(const char *s, storage_policies::store_by_address) {\n    ARDUINOJSON_ASSERT(s != 0);\n    setType(VALUE_IS_LINKED_STRING);\n    _content.asString = s;\n  }\n  template <typename TAdaptedString>\n  bool setString(TAdaptedString value, MemoryPool *pool) {\n    return storeString(value, pool, typename TAdaptedString::storage_policy());\n  }\n  CollectionData &toArray() {\n    setType(VALUE_IS_ARRAY);\n    _content.asCollection.clear();\n    return _content.asCollection;\n  }\n  CollectionData &toObject() {\n    setType(VALUE_IS_OBJECT);\n    _content.asCollection.clear();\n    return _content.asCollection;\n  }\n  size_t memoryUsage() const {\n    switch (type()) {\n      case VALUE_IS_OWNED_STRING:\n        return strlen(_content.asString) + 1;\n      case VALUE_IS_OWNED_RAW:\n        return _content.asRaw.size;\n      case VALUE_IS_OBJECT:\n      case VALUE_IS_ARRAY:\n        return _content.asCollection.memoryUsage();\n      default:\n        return 0;\n    }\n  }\n  size_t nesting() const {\n    return isCollection() ? _content.asCollection.nesting() : 0;\n  }\n  size_t size() const {\n    return isCollection() ? _content.asCollection.size() : 0;\n  }\n  VariantData *addElement(MemoryPool *pool) {\n    if (isNull())\n      toArray();\n    if (!isArray())\n      return 0;\n    return _content.asCollection.addElement(pool);\n  }\n  VariantData *getElement(size_t index) const {\n    return isArray() ? _content.asCollection.getElement(index) : 0;\n  }\n  VariantData *getOrAddElement(size_t index, MemoryPool *pool) {\n    if (isNull())\n      toArray();\n    if (!isArray())\n      return 0;\n    return _content.asCollection.getOrAddElement(index, pool);\n  }\n  template <typename TAdaptedString>\n  VariantData *getMember(TAdaptedString key) const {\n    return isObject() ? _content.asCollection.getMember(key) : 0;\n  }\n  template <typename TAdaptedString>\n  VariantData *getOrAddMember(TAdaptedString key, MemoryPool *pool) {\n    if (isNull())\n      toObject();\n    if (!isObject())\n      return 0;\n    return _content.asCollection.getOrAddMember(key, pool);\n  }\n  void movePointers(ptrdiff_t stringDistance, ptrdiff_t variantDistance) {\n    if (_flags & OWNED_VALUE_BIT)\n      _content.asString += stringDistance;\n    if (_flags & COLLECTION_MASK)\n      _content.asCollection.movePointers(stringDistance, variantDistance);\n  }\n  uint8_t type() const {\n    return _flags & VALUE_MASK;\n  }\n private:\n  void setType(uint8_t t) {\n    _flags &= OWNED_KEY_BIT;\n    _flags |= t;\n  }\n  template <typename TAdaptedString>\n  inline bool storeString(TAdaptedString value, MemoryPool *pool,\n                          storage_policies::decide_at_runtime) {\n    if (value.isStatic())\n      return storeString(value, pool, storage_policies::store_by_address());\n    else\n      return storeString(value, pool, storage_policies::store_by_copy());\n  }\n  template <typename TAdaptedString>\n  inline bool storeString(TAdaptedString value, MemoryPool *,\n                          storage_policies::store_by_address) {\n    if (value.isNull())\n      setNull();\n    else\n      setStringPointer(value.data(), storage_policies::store_by_address());\n    return true;\n  }\n  template <typename TAdaptedString>\n  inline bool storeString(TAdaptedString value, MemoryPool *pool,\n                          storage_policies::store_by_copy) {\n    if (value.isNull()) {\n      setNull();\n      return true;\n    }\n    const char *copy = pool->saveString(value);\n    if (!copy) {\n      setNull();\n      return false;\n    }\n    setStringPointer(copy, storage_policies::store_by_copy());\n    return true;\n  }\n};\n}  // namespace ARDUINOJSON_NAMESPACE\n#if defined(__GNUC__)\n#  if __GNUC__ >= 8\n#    pragma GCC diagnostic pop\n#  endif\n#endif\nnamespace ARDUINOJSON_NAMESPACE {\ntemplate <typename TAdaptedString>\ninline bool slotSetKey(VariantSlot* var, TAdaptedString key, MemoryPool* pool) {\n  if (!var)\n    return false;\n  return slotSetKey(var, key, pool, typename TAdaptedString::storage_policy());\n}\ntemplate <typename TAdaptedString>\ninline bool slotSetKey(VariantSlot* var, TAdaptedString key, MemoryPool* pool,\n                       storage_policies::decide_at_runtime) {\n  if (key.isStatic()) {\n    return slotSetKey(var, key, pool, storage_policies::store_by_address());\n  } else {\n    return slotSetKey(var, key, pool, storage_policies::store_by_copy());\n  }\n}\ntemplate <typename TAdaptedString>\ninline bool slotSetKey(VariantSlot* var, TAdaptedString key, MemoryPool*,\n                       storage_policies::store_by_address) {\n  ARDUINOJSON_ASSERT(var);\n  var->setKey(key.data(), storage_policies::store_by_address());\n  return true;\n}\ntemplate <typename TAdaptedString>\ninline bool slotSetKey(VariantSlot* var, TAdaptedString key, MemoryPool* pool,\n                       storage_policies::store_by_copy) {\n  const char* dup = pool->saveString(key);\n  if (!dup)\n    return false;\n  ARDUINOJSON_ASSERT(var);\n  var->setKey(dup, storage_policies::store_by_copy());\n  return true;\n}\ninline size_t slotSize(const VariantSlot* var) {\n  size_t n = 0;\n  while (var) {\n    n++;\n    var = var->next();\n  }\n  return n;\n}\ninline VariantData* slotData(VariantSlot* slot) {\n  return reinterpret_cast<VariantData*>(slot);\n}\nstruct Visitable {\n};\ntemplate <typename T>\nstruct IsVisitable : is_base_of<Visitable, T> {};\ntemplate <typename T>\nstruct IsVisitable<T &> : IsVisitable<T> {};\ntemplate <typename T, typename Enable = void>\nstruct Converter;\ntemplate <typename T1, typename T2>\nclass InvalidConversion;  // Error here? See https://arduinojson.org/v6/invalid-conversion/\n}  // namespace ARDUINOJSON_NAMESPACE\n#ifdef _MSC_VER  // Visual Studio\n#  define FORCE_INLINE  // __forceinline causes C4714 when returning std::string\n#  define NO_INLINE __declspec(noinline)\n#  ifndef ARDUINOJSON_DEPRECATED\n#    define ARDUINOJSON_DEPRECATED(msg) __declspec(deprecated(msg))\n#  endif\n#elif defined(__GNUC__)  // GCC or Clang\n#  define FORCE_INLINE __attribute__((always_inline))\n#  define NO_INLINE __attribute__((noinline))\n#  ifndef ARDUINOJSON_DEPRECATED\n#    if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 5)\n#      define ARDUINOJSON_DEPRECATED(msg) __attribute__((deprecated(msg)))\n#    else\n#      define ARDUINOJSON_DEPRECATED(msg) __attribute__((deprecated))\n#    endif\n#  endif\n#else  // Other compilers\n#  define FORCE_INLINE\n#  define NO_INLINE\n#  ifndef ARDUINOJSON_DEPRECATED\n#    define ARDUINOJSON_DEPRECATED(msg)\n#  endif\n#endif\n#if __cplusplus >= 201103L\n#  define NOEXCEPT noexcept\n#else\n#  define NOEXCEPT throw()\n#endif\n#if defined(__has_attribute)\n#  if __has_attribute(no_sanitize)\n#    define ARDUINOJSON_NO_SANITIZE(check) __attribute__((no_sanitize(check)))\n#  else\n#    define ARDUINOJSON_NO_SANITIZE(check)\n#  endif\n#else\n#  define ARDUINOJSON_NO_SANITIZE(check)\n#endif\nnamespace ARDUINOJSON_NAMESPACE {\ntemplate <typename TVisitor>\ninline typename TVisitor::result_type variantAccept(const VariantData *var,\n                                                    TVisitor &visitor) {\n  if (var != 0)\n    return var->accept(visitor);\n  else\n    return visitor.visitNull();\n}\ninline const CollectionData *variantAsArray(const VariantData *var) {\n  return var != 0 ? var->asArray() : 0;\n}\ninline const CollectionData *variantAsObject(const VariantData *var) {\n  return var != 0 ? var->asObject() : 0;\n}\ninline CollectionData *variantAsObject(VariantData *var) {\n  return var != 0 ? var->asObject() : 0;\n}\ninline bool variantCopyFrom(VariantData *dst, const VariantData *src,\n                            MemoryPool *pool) {\n  if (!dst)\n    return false;\n  if (!src) {\n    dst->setNull();\n    return true;\n  }\n  return dst->copyFrom(*src, pool);\n}\ninline int variantCompare(const VariantData *a, const VariantData *b);\ninline void variantSetNull(VariantData *var) {\n  if (!var)\n    return;\n  var->setNull();\n}\ntemplate <typename TAdaptedString>\ninline bool variantSetString(VariantData *var, TAdaptedString value,\n                             MemoryPool *pool) {\n  if (!var)\n    return false;\n  return var->setString(value, pool);\n}\ninline size_t variantSize(const VariantData *var) {\n  return var != 0 ? var->size() : 0;\n}\ninline CollectionData *variantToArray(VariantData *var) {\n  if (!var)\n    return 0;\n  return &var->toArray();\n}\ninline CollectionData *variantToObject(VariantData *var) {\n  if (!var)\n    return 0;\n  return &var->toObject();\n}\ninline NO_INLINE VariantData *variantAddElement(VariantData *var,\n                                                MemoryPool *pool) {\n  return var != 0 ? var->addElement(pool) : 0;\n}\ninline NO_INLINE VariantData *variantGetOrAddElement(VariantData *var,\n                                                     size_t index,\n                                                     MemoryPool *pool) {\n  return var != 0 ? var->getOrAddElement(index, pool) : 0;\n}\ntemplate <typename TChar>\nNO_INLINE VariantData *variantGetOrAddMember(VariantData *var, TChar *key,\n                                             MemoryPool *pool) {\n  return var != 0 ? var->getOrAddMember(adaptString(key), pool) : 0;\n}\ntemplate <typename TString>\nNO_INLINE VariantData *variantGetOrAddMember(VariantData *var,\n                                             const TString &key,\n                                             MemoryPool *pool) {\n  return var != 0 ? var->getOrAddMember(adaptString(key), pool) : 0;\n}\ninline bool variantIsNull(const VariantData *var) {\n  return var == 0 || var->isNull();\n}\nenum CompareResult {\n  COMPARE_RESULT_DIFFER = 0,\n  COMPARE_RESULT_EQUAL = 1,\n  COMPARE_RESULT_GREATER = 2,\n  COMPARE_RESULT_LESS = 4,\n  COMPARE_RESULT_GREATER_OR_EQUAL = 3,\n  COMPARE_RESULT_LESS_OR_EQUAL = 5\n};\ntemplate <typename T>\nCompareResult arithmeticCompare(const T &lhs, const T &rhs) {\n  if (lhs < rhs)\n    return COMPARE_RESULT_LESS;\n  else if (lhs > rhs)\n    return COMPARE_RESULT_GREATER;\n  else\n    return COMPARE_RESULT_EQUAL;\n}\ntemplate <typename T1, typename T2>\nCompareResult arithmeticCompare(\n    const T1 &lhs, const T2 &rhs,\n    typename enable_if<is_integral<T1>::value && is_integral<T2>::value &&\n                           sizeof(T1) < sizeof(T2),\n                       int  // Using int instead of void to avoid C2572 on\n                       >::type * = 0) {\n  return arithmeticCompare<T2>(static_cast<T2>(lhs), rhs);\n}\ntemplate <typename T1, typename T2>\nCompareResult arithmeticCompare(\n    const T1 &lhs, const T2 &rhs,\n    typename enable_if<is_integral<T1>::value && is_integral<T2>::value &&\n                       sizeof(T2) < sizeof(T1)>::type * = 0) {\n  return arithmeticCompare<T1>(lhs, static_cast<T1>(rhs));\n}\ntemplate <typename T1, typename T2>\nCompareResult arithmeticCompare(\n    const T1 &lhs, const T2 &rhs,\n    typename enable_if<is_integral<T1>::value && is_integral<T2>::value &&\n                       is_signed<T1>::value == is_signed<T2>::value &&\n                       sizeof(T2) == sizeof(T1)>::type * = 0) {\n  return arithmeticCompare<T1>(lhs, static_cast<T1>(rhs));\n}\ntemplate <typename T1, typename T2>\nCompareResult arithmeticCompare(\n    const T1 &lhs, const T2 &rhs,\n    typename enable_if<is_integral<T1>::value && is_integral<T2>::value &&\n                       is_unsigned<T1>::value && is_signed<T2>::value &&\n                       sizeof(T2) == sizeof(T1)>::type * = 0) {\n  if (rhs < 0)\n    return COMPARE_RESULT_GREATER;\n  return arithmeticCompare<T1>(lhs, static_cast<T1>(rhs));\n}\ntemplate <typename T1, typename T2>\nCompareResult arithmeticCompare(\n    const T1 &lhs, const T2 &rhs,\n    typename enable_if<is_integral<T1>::value && is_integral<T2>::value &&\n                       is_signed<T1>::value && is_unsigned<T2>::value &&\n                       sizeof(T2) == sizeof(T1)>::type * = 0) {\n  if (lhs < 0)\n    return COMPARE_RESULT_LESS;\n  return arithmeticCompare<T2>(static_cast<T2>(lhs), rhs);\n}\ntemplate <typename T1, typename T2>\nCompareResult arithmeticCompare(\n    const T1 &lhs, const T2 &rhs,\n    typename enable_if<is_floating_point<T1>::value ||\n                       is_floating_point<T2>::value>::type * = 0) {\n  return arithmeticCompare<double>(static_cast<double>(lhs),\n                                   static_cast<double>(rhs));\n}\ntemplate <typename T2>\nCompareResult arithmeticCompareNegateLeft(\n    UInt, const T2 &, typename enable_if<is_unsigned<T2>::value>::type * = 0) {\n  return COMPARE_RESULT_LESS;\n}\ntemplate <typename T2>\nCompareResult arithmeticCompareNegateLeft(\n    UInt lhs, const T2 &rhs,\n    typename enable_if<is_signed<T2>::value>::type * = 0) {\n  if (rhs > 0)\n    return COMPARE_RESULT_LESS;\n  return arithmeticCompare(-rhs, static_cast<T2>(lhs));\n}\ntemplate <typename T1>\nCompareResult arithmeticCompareNegateRight(\n    const T1 &, UInt, typename enable_if<is_unsigned<T1>::value>::type * = 0) {\n  return COMPARE_RESULT_GREATER;\n}\ntemplate <typename T1>\nCompareResult arithmeticCompareNegateRight(\n    const T1 &lhs, UInt rhs,\n    typename enable_if<is_signed<T1>::value>::type * = 0) {\n  if (lhs > 0)\n    return COMPARE_RESULT_GREATER;\n  return arithmeticCompare(static_cast<T1>(rhs), -lhs);\n}\nstruct VariantTag {};\ntemplate <typename T>\nstruct IsVariant : is_base_of<VariantTag, T> {};\ntemplate <typename T1, typename T2>\nCompareResult compare(const T1 &lhs, const T2 &rhs);  // VariantCompare.cpp\ntemplate <typename TVariant>\nstruct VariantOperators {\n  template <typename T>\n  friend\n      typename enable_if<!IsVariant<T>::value && !is_array<T>::value, T>::type\n      operator|(const TVariant &variant, const T &defaultValue) {\n    if (variant.template is<T>())\n      return variant.template as<T>();\n    else\n      return defaultValue;\n  }\n  friend const char *operator|(const TVariant &variant,\n                               const char *defaultValue) {\n    if (variant.template is<const char *>())\n      return variant.template as<const char *>();\n    else\n      return defaultValue;\n  }\n  template <typename T>\n  friend typename enable_if<IsVariant<T>::value, typename T::variant_type>::type\n  operator|(const TVariant &variant, T defaultValue) {\n    if (variant)\n      return variant;\n    else\n      return defaultValue;\n  }\n  template <typename T>\n  friend bool operator==(T *lhs, TVariant rhs) {\n    return compare(rhs, lhs) == COMPARE_RESULT_EQUAL;\n  }\n  template <typename T>\n  friend bool operator==(const T &lhs, TVariant rhs) {\n    return compare(rhs, lhs) == COMPARE_RESULT_EQUAL;\n  }\n  template <typename T>\n  friend bool operator==(TVariant lhs, T *rhs) {\n    return compare(lhs, rhs) == COMPARE_RESULT_EQUAL;\n  }\n  template <typename T>\n  friend typename enable_if<!IsVisitable<T>::value, bool>::type operator==(\n      TVariant lhs, const T &rhs) {\n    return compare(lhs, rhs) == COMPARE_RESULT_EQUAL;\n  }\n  template <typename T>\n  friend bool operator!=(T *lhs, TVariant rhs) {\n    return compare(rhs, lhs) != COMPARE_RESULT_EQUAL;\n  }\n  template <typename T>\n  friend bool operator!=(const T &lhs, TVariant rhs) {\n    return compare(rhs, lhs) != COMPARE_RESULT_EQUAL;\n  }\n  template <typename T>\n  friend bool operator!=(TVariant lhs, T *rhs) {\n    return compare(lhs, rhs) != COMPARE_RESULT_EQUAL;\n  }\n  template <typename T>\n  friend typename enable_if<!IsVisitable<T>::value, bool>::type operator!=(\n      TVariant lhs, const T &rhs) {\n    return compare(lhs, rhs) != COMPARE_RESULT_EQUAL;\n  }\n  template <typename T>\n  friend bool operator<(T *lhs, TVariant rhs) {\n    return compare(rhs, lhs) == COMPARE_RESULT_GREATER;\n  }\n  template <typename T>\n  friend bool operator<(const T &lhs, TVariant rhs) {\n    return compare(rhs, lhs) == COMPARE_RESULT_GREATER;\n  }\n  template <typename T>\n  friend bool operator<(TVariant lhs, T *rhs) {\n    return compare(lhs, rhs) == COMPARE_RESULT_LESS;\n  }\n  template <typename T>\n  friend typename enable_if<!IsVisitable<T>::value, bool>::type operator<(\n      TVariant lhs, const T &rhs) {\n    return compare(lhs, rhs) == COMPARE_RESULT_LESS;\n  }\n  template <typename T>\n  friend bool operator<=(T *lhs, TVariant rhs) {\n    return (compare(rhs, lhs) & COMPARE_RESULT_GREATER_OR_EQUAL) != 0;\n  }\n  template <typename T>\n  friend bool operator<=(const T &lhs, TVariant rhs) {\n    return (compare(rhs, lhs) & COMPARE_RESULT_GREATER_OR_EQUAL) != 0;\n  }\n  template <typename T>\n  friend bool operator<=(TVariant lhs, T *rhs) {\n    return (compare(lhs, rhs) & COMPARE_RESULT_LESS_OR_EQUAL) != 0;\n  }\n  template <typename T>\n  friend typename enable_if<!IsVisitable<T>::value, bool>::type operator<=(\n      TVariant lhs, const T &rhs) {\n    return (compare(lhs, rhs) & COMPARE_RESULT_LESS_OR_EQUAL) != 0;\n  }\n  template <typename T>\n  friend bool operator>(T *lhs, TVariant rhs) {\n    return compare(rhs, lhs) == COMPARE_RESULT_LESS;\n  }\n  template <typename T>\n  friend bool operator>(const T &lhs, TVariant rhs) {\n    return compare(rhs, lhs) == COMPARE_RESULT_LESS;\n  }\n  template <typename T>\n  friend bool operator>(TVariant lhs, T *rhs) {\n    return compare(lhs, rhs) == COMPARE_RESULT_GREATER;\n  }\n  template <typename T>\n  friend typename enable_if<!IsVisitable<T>::value, bool>::type operator>(\n      TVariant lhs, const T &rhs) {\n    return compare(lhs, rhs) == COMPARE_RESULT_GREATER;\n  }\n  template <typename T>\n  friend bool operator>=(T *lhs, TVariant rhs) {\n    return (compare(rhs, lhs) & COMPARE_RESULT_LESS_OR_EQUAL) != 0;\n  }\n  template <typename T>\n  friend bool operator>=(const T &lhs, TVariant rhs) {\n    return (compare(rhs, lhs) & COMPARE_RESULT_LESS_OR_EQUAL) != 0;\n  }\n  template <typename T>\n  friend bool operator>=(TVariant lhs, T *rhs) {\n    return (compare(lhs, rhs) & COMPARE_RESULT_GREATER_OR_EQUAL) != 0;\n  }\n  template <typename T>\n  friend typename enable_if<!IsVisitable<T>::value, bool>::type operator>=(\n      TVariant lhs, const T &rhs) {\n    return (compare(lhs, rhs) & COMPARE_RESULT_GREATER_OR_EQUAL) != 0;\n  }\n};\nclass ArrayRef;\nclass ObjectRef;\ntemplate <typename>\nclass ElementProxy;\ntemplate <typename TArray>\nclass ArrayShortcuts {\n public:\n  FORCE_INLINE ElementProxy<TArray> operator[](size_t index) const;\n  FORCE_INLINE ObjectRef createNestedObject() const;\n  FORCE_INLINE ArrayRef createNestedArray() const;\n  template <typename T>\n  FORCE_INLINE bool add(const T &value) const {\n    return impl()->addElement().set(value);\n  }\n  template <typename T>\n  FORCE_INLINE bool add(T *value) const {\n    return impl()->addElement().set(value);\n  }\n private:\n  const TArray *impl() const {\n    return static_cast<const TArray *>(this);\n  }\n};\ntemplate <typename TParent, typename TStringRef>\nclass MemberProxy;\ntemplate <typename TObject>\nclass ObjectShortcuts {\n public:\n  template <typename TString>\n  FORCE_INLINE typename enable_if<IsString<TString>::value, bool>::type\n  containsKey(const TString &key) const;\n  template <typename TChar>\n  FORCE_INLINE typename enable_if<IsString<TChar *>::value, bool>::type\n  containsKey(TChar *key) const;\n  template <typename TString>\n  FORCE_INLINE typename enable_if<IsString<TString>::value,\n                                  MemberProxy<TObject, TString> >::type\n  operator[](const TString &key) const;\n  template <typename TChar>\n  FORCE_INLINE typename enable_if<IsString<TChar *>::value,\n                                  MemberProxy<TObject, TChar *> >::type\n  operator[](TChar *key) const;\n  template <typename TString>\n  FORCE_INLINE ArrayRef createNestedArray(const TString &key) const;\n  template <typename TChar>\n  FORCE_INLINE ArrayRef createNestedArray(TChar *key) const;\n  template <typename TString>\n  ObjectRef createNestedObject(const TString &key) const;\n  template <typename TChar>\n  ObjectRef createNestedObject(TChar *key) const;\n private:\n  const TObject *impl() const {\n    return static_cast<const TObject *>(this);\n  }\n};\ntemplate <typename TVariant>\nclass VariantShortcuts : public ObjectShortcuts<TVariant>,\n                         public ArrayShortcuts<TVariant> {\n public:\n  using ArrayShortcuts<TVariant>::createNestedArray;\n  using ArrayShortcuts<TVariant>::createNestedObject;\n  using ArrayShortcuts<TVariant>::operator[];\n  using ObjectShortcuts<TVariant>::createNestedArray;\n  using ObjectShortcuts<TVariant>::createNestedObject;\n  using ObjectShortcuts<TVariant>::operator[];\n};\nclass ArrayRef;\nclass ObjectRef;\ntemplate <typename TData>\nclass VariantRefBase : public VariantTag {\n public:\n  FORCE_INLINE bool isNull() const {\n    return variantIsNull(_data);\n  }\n  FORCE_INLINE bool isUndefined() const {\n    return !_data;\n  }\n  FORCE_INLINE size_t memoryUsage() const {\n    return _data ? _data->memoryUsage() : 0;\n  }\n  FORCE_INLINE size_t nesting() const {\n    return _data ? _data->nesting() : 0;\n  }\n  size_t size() const {\n    return variantSize(_data);\n  }\n protected:\n  VariantRefBase(TData *data) : _data(data) {}\n  TData *_data;\n  friend TData *getData(const VariantRefBase &variant) {\n    return variant._data;\n  }\n};\nclass VariantRef : public VariantRefBase<VariantData>,\n                   public VariantOperators<VariantRef>,\n                   public VariantShortcuts<VariantRef>,\n                   public Visitable {\n  typedef VariantRefBase<VariantData> base_type;\n  friend class VariantConstRef;\n public:\n  FORCE_INLINE VariantRef(MemoryPool *pool, VariantData *data)\n      : base_type(data), _pool(pool) {}\n  FORCE_INLINE VariantRef() : base_type(0), _pool(0) {}\n  FORCE_INLINE void clear() const {\n    return variantSetNull(_data);\n  }\n  template <typename T>\n  FORCE_INLINE bool set(const T &value) const {\n    return Converter<T>::toJson(value, *this);\n  }\n  bool ARDUINOJSON_DEPRECATED(\n      \"Support for char is deprecated, use int8_t or uint8_t instead\")\n      set(char value) const;\n  template <typename T>\n  FORCE_INLINE bool set(T *value) const {\n    return Converter<T *>::toJson(value, *this);\n  }\n  template <typename T>\n  FORCE_INLINE\n      typename enable_if<!is_same<T, char *>::value && !is_same<T, char>::value,\n                         T>::type\n      as() const {\n    return Converter<T>::fromJson(*this);\n  }\n  template <typename T>\n  FORCE_INLINE typename enable_if<is_same<T, char *>::value, const char *>::type\n  ARDUINOJSON_DEPRECATED(\"Replace as<char*>() with as<const char*>()\")\n      as() const {\n    return as<const char *>();\n  }\n  template <typename T>\n  FORCE_INLINE typename enable_if<is_same<T, char>::value, char>::type\n  ARDUINOJSON_DEPRECATED(\n      \"Support for char is deprecated, use int8_t or uint8_t instead\")\n      as() const {\n    return as<signed char>();\n  }\n  template <typename T>\n  FORCE_INLINE\n      typename enable_if<!is_same<T, char *>::value && !is_same<T, char>::value,\n                         bool>::type\n      is() const {\n    return Converter<T>::checkJson(*this);\n  }\n  template <typename T>\n  FORCE_INLINE typename enable_if<is_same<T, char *>::value, bool>::type\n  ARDUINOJSON_DEPRECATED(\"Replace is<char*>() with is<const char*>()\")\n      is() const {\n    return is<const char *>();\n  }\n  template <typename T>\n  FORCE_INLINE typename enable_if<is_same<T, char>::value, bool>::type\n  ARDUINOJSON_DEPRECATED(\n      \"Support for char is deprecated, use int8_t or uint8_t instead\")\n      is() const {\n    return is<signed char>();\n  }\n  template <typename T>\n  FORCE_INLINE operator T() const {\n    return as<T>();\n  }\n  template <typename TVisitor>\n  typename TVisitor::result_type accept(TVisitor &visitor) const {\n    return variantAccept(_data, visitor);\n  }\n  template <typename T>\n  typename enable_if<is_same<T, ArrayRef>::value, ArrayRef>::type to() const;\n  template <typename T>\n  typename enable_if<is_same<T, ObjectRef>::value, ObjectRef>::type to() const;\n  template <typename T>\n  typename enable_if<is_same<T, VariantRef>::value, VariantRef>::type to()\n      const;\n  VariantRef addElement() const;\n  FORCE_INLINE VariantRef getElement(size_t) const;\n  FORCE_INLINE VariantRef getOrAddElement(size_t) const;\n  template <typename TChar>\n  FORCE_INLINE VariantRef getMember(TChar *) const;\n  template <typename TString>\n  FORCE_INLINE typename enable_if<IsString<TString>::value, VariantRef>::type\n  getMember(const TString &) const;\n  template <typename TChar>\n  FORCE_INLINE VariantRef getOrAddMember(TChar *) const;\n  template <typename TString>\n  FORCE_INLINE VariantRef getOrAddMember(const TString &) const;\n  FORCE_INLINE void remove(size_t index) const {\n    if (_data)\n      _data->remove(index);\n  }\n  template <typename TChar>\n  FORCE_INLINE typename enable_if<IsString<TChar *>::value>::type remove(\n      TChar *key) const {\n    if (_data)\n      _data->remove(adaptString(key));\n  }\n  template <typename TString>\n  FORCE_INLINE typename enable_if<IsString<TString>::value>::type remove(\n      const TString &key) const {\n    if (_data)\n      _data->remove(adaptString(key));\n  }\n private:\n  MemoryPool *_pool;\n  friend MemoryPool *getPool(const VariantRef &variant) {\n    return variant._pool;\n  }\n};\nclass VariantConstRef : public VariantRefBase<const VariantData>,\n                        public VariantOperators<VariantConstRef>,\n                        public VariantShortcuts<VariantConstRef>,\n                        public Visitable {\n  typedef VariantRefBase<const VariantData> base_type;\n  friend class VariantRef;\n public:\n  VariantConstRef() : base_type(0) {}\n  VariantConstRef(const VariantData *data) : base_type(data) {}\n  VariantConstRef(VariantRef var) : base_type(var._data) {}\n  template <typename TVisitor>\n  typename TVisitor::result_type accept(TVisitor &visitor) const {\n    return variantAccept(_data, visitor);\n  }\n  template <typename T>\n  FORCE_INLINE\n      typename enable_if<!is_same<T, char *>::value && !is_same<T, char>::value,\n                         T>::type\n      as() const {\n    return Converter<T>::fromJson(*this);\n  }\n  template <typename T>\n  FORCE_INLINE typename enable_if<is_same<T, char *>::value, const char *>::type\n  ARDUINOJSON_DEPRECATED(\"Replace as<char*>() with as<const char*>()\")\n      as() const {\n    return as<const char *>();\n  }\n  template <typename T>\n  FORCE_INLINE typename enable_if<is_same<T, char>::value, char>::type\n  ARDUINOJSON_DEPRECATED(\n      \"Support for char is deprecated, use int8_t or uint8_t instead\")\n      as() const {\n    return as<signed char>();\n  }\n  template <typename T>\n  FORCE_INLINE\n      typename enable_if<!is_same<T, char *>::value && !is_same<T, char>::value,\n                         bool>::type\n      is() const {\n    return Converter<T>::checkJson(*this);\n  }\n  template <typename T>\n  FORCE_INLINE typename enable_if<is_same<T, char *>::value, bool>::type\n  ARDUINOJSON_DEPRECATED(\"Replace is<char*>() with is<const char*>()\")\n      is() const {\n    return is<const char *>();\n  }\n  template <typename T>\n  FORCE_INLINE typename enable_if<is_same<T, char>::value, bool>::type\n  ARDUINOJSON_DEPRECATED(\n      \"Support for char is deprecated, use int8_t or uint8_t instead\")\n      is() const {\n    return is<signed char>();\n  }\n  template <typename T>\n  FORCE_INLINE operator T() const {\n    return as<T>();\n  }\n  FORCE_INLINE VariantConstRef getElement(size_t) const;\n  FORCE_INLINE VariantConstRef operator[](size_t index) const {\n    return getElement(index);\n  }\n  template <typename TString>\n  FORCE_INLINE VariantConstRef getMember(const TString &key) const {\n    return VariantConstRef(\n        objectGetMember(variantAsObject(_data), adaptString(key)));\n  }\n  template <typename TChar>\n  FORCE_INLINE VariantConstRef getMember(TChar *key) const {\n    const CollectionData *obj = variantAsObject(_data);\n    return VariantConstRef(obj ? obj->getMember(adaptString(key)) : 0);\n  }\n  template <typename TString>\n  FORCE_INLINE\n      typename enable_if<IsString<TString>::value, VariantConstRef>::type\n      operator[](const TString &key) const {\n    return getMember(key);\n  }\n  template <typename TChar>\n  FORCE_INLINE\n      typename enable_if<IsString<TChar *>::value, VariantConstRef>::type\n      operator[](TChar *key) const {\n    return getMember(key);\n  }\n};\ntemplate <>\nstruct Converter<VariantRef> {\n  static bool toJson(VariantRef src, VariantRef dst) {\n    return variantCopyFrom(getData(dst), getData(src), getPool(dst));\n  }\n  static VariantRef fromJson(VariantRef src) {\n    return src;\n  }\n  static InvalidConversion<VariantConstRef, VariantRef> fromJson(\n      VariantConstRef);\n  static bool checkJson(VariantRef src) {\n    VariantData *data = getData(src);\n    return !!data;\n  }\n  static bool checkJson(VariantConstRef) {\n    return false;\n  }\n};\ntemplate <>\nstruct Converter<VariantConstRef> {\n  static bool toJson(VariantConstRef src, VariantRef dst) {\n    return variantCopyFrom(getData(dst), getData(src), getPool(dst));\n  }\n  static VariantConstRef fromJson(VariantConstRef src) {\n    return VariantConstRef(getData(src));\n  }\n  static bool checkJson(VariantConstRef src) {\n    const VariantData *data = getData(src);\n    return !!data;\n  }\n};\nclass VariantPtr {\n public:\n  VariantPtr(MemoryPool *pool, VariantData *data) : _variant(pool, data) {}\n  VariantRef *operator->() {\n    return &_variant;\n  }\n  VariantRef &operator*() {\n    return _variant;\n  }\n private:\n  VariantRef _variant;\n};\nclass ArrayIterator {\n public:\n  ArrayIterator() : _slot(0) {}\n  explicit ArrayIterator(MemoryPool *pool, VariantSlot *slot)\n      : _pool(pool), _slot(slot) {}\n  VariantRef operator*() const {\n    return VariantRef(_pool, _slot->data());\n  }\n  VariantPtr operator->() {\n    return VariantPtr(_pool, _slot->data());\n  }\n  bool operator==(const ArrayIterator &other) const {\n    return _slot == other._slot;\n  }\n  bool operator!=(const ArrayIterator &other) const {\n    return _slot != other._slot;\n  }\n  ArrayIterator &operator++() {\n    _slot = _slot->next();\n    return *this;\n  }\n  ArrayIterator &operator+=(size_t distance) {\n    _slot = _slot->next(distance);\n    return *this;\n  }\n  VariantSlot *internal() {\n    return _slot;\n  }\n private:\n  MemoryPool *_pool;\n  VariantSlot *_slot;\n};\nclass VariantConstPtr {\n public:\n  VariantConstPtr(const VariantData *data) : _variant(data) {}\n  VariantConstRef *operator->() {\n    return &_variant;\n  }\n  VariantConstRef &operator*() {\n    return _variant;\n  }\n private:\n  VariantConstRef _variant;\n};\nclass ArrayConstRefIterator {\n public:\n  ArrayConstRefIterator() : _slot(0) {}\n  explicit ArrayConstRefIterator(const VariantSlot *slot) : _slot(slot) {}\n  VariantConstRef operator*() const {\n    return VariantConstRef(_slot->data());\n  }\n  VariantConstPtr operator->() {\n    return VariantConstPtr(_slot->data());\n  }\n  bool operator==(const ArrayConstRefIterator &other) const {\n    return _slot == other._slot;\n  }\n  bool operator!=(const ArrayConstRefIterator &other) const {\n    return _slot != other._slot;\n  }\n  ArrayConstRefIterator &operator++() {\n    _slot = _slot->next();\n    return *this;\n  }\n  ArrayConstRefIterator &operator+=(size_t distance) {\n    _slot = _slot->next(distance);\n    return *this;\n  }\n  const VariantSlot *internal() {\n    return _slot;\n  }\n private:\n  const VariantSlot *_slot;\n};\n}  // namespace ARDUINOJSON_NAMESPACE\n#define JSON_ARRAY_SIZE(NUMBER_OF_ELEMENTS) \\\n  ((NUMBER_OF_ELEMENTS) * sizeof(ARDUINOJSON_NAMESPACE::VariantSlot))\nnamespace ARDUINOJSON_NAMESPACE {\nclass ObjectRef;\ntemplate <typename>\nclass ElementProxy;\ntemplate <typename TData>\nclass ArrayRefBase {\n public:\n  operator VariantConstRef() const {\n    const void* data = _data;  // prevent warning cast-align\n    return VariantConstRef(reinterpret_cast<const VariantData*>(data));\n  }\n  template <typename TVisitor>\n  FORCE_INLINE typename TVisitor::result_type accept(TVisitor& visitor) const {\n    return arrayAccept(_data, visitor);\n  }\n  FORCE_INLINE bool isNull() const {\n    return _data == 0;\n  }\n  FORCE_INLINE operator bool() const {\n    return _data != 0;\n  }\n  FORCE_INLINE size_t memoryUsage() const {\n    return _data ? _data->memoryUsage() : 0;\n  }\n  FORCE_INLINE size_t nesting() const {\n    return _data ? _data->nesting() : 0;\n  }\n  FORCE_INLINE size_t size() const {\n    return _data ? _data->size() : 0;\n  }\n protected:\n  ArrayRefBase(TData* data) : _data(data) {}\n  TData* _data;\n};\nclass ArrayConstRef : public ArrayRefBase<const CollectionData>,\n                      public Visitable {\n  friend class ArrayRef;\n  typedef ArrayRefBase<const CollectionData> base_type;\n public:\n  typedef ArrayConstRefIterator iterator;\n  FORCE_INLINE iterator begin() const {\n    if (!_data)\n      return iterator();\n    return iterator(_data->head());\n  }\n  FORCE_INLINE iterator end() const {\n    return iterator();\n  }\n  FORCE_INLINE ArrayConstRef() : base_type(0) {}\n  FORCE_INLINE ArrayConstRef(const CollectionData* data) : base_type(data) {}\n  FORCE_INLINE bool operator==(ArrayConstRef rhs) const {\n    return arrayEquals(_data, rhs._data);\n  }\n  FORCE_INLINE VariantConstRef operator[](size_t index) const {\n    return getElement(index);\n  }\n  FORCE_INLINE VariantConstRef getElement(size_t index) const {\n    return VariantConstRef(_data ? _data->getElement(index) : 0);\n  }\n};\nclass ArrayRef : public ArrayRefBase<CollectionData>,\n                 public ArrayShortcuts<ArrayRef>,\n                 public Visitable {\n  typedef ArrayRefBase<CollectionData> base_type;\n public:\n  typedef ArrayIterator iterator;\n  FORCE_INLINE ArrayRef() : base_type(0), _pool(0) {}\n  FORCE_INLINE ArrayRef(MemoryPool* pool, CollectionData* data)\n      : base_type(data), _pool(pool) {}\n  operator VariantRef() {\n    void* data = _data;  // prevent warning cast-align\n    return VariantRef(_pool, reinterpret_cast<VariantData*>(data));\n  }\n  operator ArrayConstRef() const {\n    return ArrayConstRef(_data);\n  }\n  VariantRef addElement() const {\n    return VariantRef(_pool, arrayAdd(_data, _pool));\n  }\n  FORCE_INLINE iterator begin() const {\n    if (!_data)\n      return iterator();\n    return iterator(_pool, _data->head());\n  }\n  FORCE_INLINE iterator end() const {\n    return iterator();\n  }\n  FORCE_INLINE bool set(ArrayConstRef src) const {\n    if (!_data || !src._data)\n      return false;\n    return _data->copyFrom(*src._data, _pool);\n  }\n  FORCE_INLINE bool operator==(ArrayRef rhs) const {\n    return arrayEquals(_data, rhs._data);\n  }\n  FORCE_INLINE VariantRef getOrAddElement(size_t index) const {\n    return VariantRef(_pool, _data ? _data->getOrAddElement(index, _pool) : 0);\n  }\n  FORCE_INLINE VariantRef getElement(size_t index) const {\n    return VariantRef(_pool, _data ? _data->getElement(index) : 0);\n  }\n  FORCE_INLINE void remove(iterator it) const {\n    if (!_data)\n      return;\n    _data->removeSlot(it.internal());\n  }\n  FORCE_INLINE void remove(size_t index) const {\n    if (!_data)\n      return;\n    _data->removeElement(index);\n  }\n  void clear() const {\n    if (!_data)\n      return;\n    _data->clear();\n  }\n private:\n  MemoryPool* _pool;\n};\ntemplate <>\nstruct Converter<ArrayConstRef> {\n  static bool toJson(VariantConstRef src, VariantRef dst) {\n    return variantCopyFrom(getData(dst), getData(src), getPool(dst));\n  }\n  static ArrayConstRef fromJson(VariantConstRef src) {\n    return ArrayConstRef(variantAsArray(getData(src)));\n  }\n  static bool checkJson(VariantConstRef src) {\n    const VariantData* data = getData(src);\n    return data && data->isArray();\n  }\n};\ntemplate <>\nstruct Converter<ArrayRef> {\n  static bool toJson(VariantConstRef src, VariantRef dst) {\n    return variantCopyFrom(getData(dst), getData(src), getPool(dst));\n  }\n  static ArrayRef fromJson(VariantRef src) {\n    VariantData* data = getData(src);\n    MemoryPool* pool = getPool(src);\n    return ArrayRef(pool, data != 0 ? data->asArray() : 0);\n  }\n  static InvalidConversion<VariantConstRef, ArrayRef> fromJson(VariantConstRef);\n  static bool checkJson(VariantConstRef) {\n    return false;\n  }\n  static bool checkJson(VariantRef src) {\n    VariantData* data = getData(src);\n    return data && data->isArray();\n  }\n};\ntemplate <typename TVisitor>\ntypename TVisitor::result_type objectAccept(const CollectionData *obj,\n                                            TVisitor &visitor) {\n  if (obj)\n    return visitor.visitObject(*obj);\n  else\n    return visitor.visitNull();\n}\ninline bool objectEquals(const CollectionData *lhs, const CollectionData *rhs) {\n  if (lhs == rhs)\n    return true;\n  if (!lhs || !rhs)\n    return false;\n  return lhs->equalsObject(*rhs);\n}\ntemplate <typename TAdaptedString>\ninline VariantData *objectGetMember(const CollectionData *obj,\n                                    TAdaptedString key) {\n  if (!obj)\n    return 0;\n  return obj->getMember(key);\n}\ntemplate <typename TAdaptedString>\nvoid objectRemove(CollectionData *obj, TAdaptedString key) {\n  if (!obj)\n    return;\n  obj->removeMember(key);\n}\ntemplate <typename TAdaptedString>\ninline VariantData *objectGetOrAddMember(CollectionData *obj,\n                                         TAdaptedString key, MemoryPool *pool) {\n  if (!obj)\n    return 0;\n  return obj->getOrAddMember(key, pool);\n}\nclass String {\n public:\n  String() : _data(0), _isStatic(true) {}\n  String(const char* data, bool isStaticData = true)\n      : _data(data), _isStatic(isStaticData) {}\n  const char* c_str() const {\n    return _data;\n  }\n  bool isNull() const {\n    return !_data;\n  }\n  bool isStatic() const {\n    return _isStatic;\n  }\n  friend bool operator==(String lhs, String rhs) {\n    if (lhs._data == rhs._data)\n      return true;\n    if (!lhs._data)\n      return false;\n    if (!rhs._data)\n      return false;\n    return strcmp(lhs._data, rhs._data) == 0;\n  }\n  friend bool operator!=(String lhs, String rhs) {\n    if (lhs._data == rhs._data)\n      return false;\n    if (!lhs._data)\n      return true;\n    if (!rhs._data)\n      return true;\n    return strcmp(lhs._data, rhs._data) != 0;\n  }\n private:\n  const char* _data;\n  bool _isStatic;\n};\nclass StringAdapter : public RamStringAdapter {\n public:\n  StringAdapter(const String& str)\n      : RamStringAdapter(str.c_str()), _isStatic(str.isStatic()) {}\n  bool isStatic() const {\n    return _isStatic;\n  }\n  typedef storage_policies::decide_at_runtime storage_policy;\n private:\n  bool _isStatic;\n};\ntemplate <>\nstruct IsString<String> : true_type {};\ninline StringAdapter adaptString(const String& str) {\n  return StringAdapter(str);\n}\nclass Pair {\n public:\n  Pair(MemoryPool* pool, VariantSlot* slot) {\n    if (slot) {\n      _key = String(slot->key(), !slot->ownsKey());\n      _value = VariantRef(pool, slot->data());\n    }\n  }\n  String key() const {\n    return _key;\n  }\n  VariantRef value() const {\n    return _value;\n  }\n private:\n  String _key;\n  VariantRef _value;\n};\nclass PairConst {\n public:\n  PairConst(const VariantSlot* slot) {\n    if (slot) {\n      _key = String(slot->key(), !slot->ownsKey());\n      _value = VariantConstRef(slot->data());\n    }\n  }\n  String key() const {\n    return _key;\n  }\n  VariantConstRef value() const {\n    return _value;\n  }\n private:\n  String _key;\n  VariantConstRef _value;\n};\nclass PairPtr {\n public:\n  PairPtr(MemoryPool *pool, VariantSlot *slot) : _pair(pool, slot) {}\n  const Pair *operator->() const {\n    return &_pair;\n  }\n  const Pair &operator*() const {\n    return _pair;\n  }\n private:\n  Pair _pair;\n};\nclass ObjectIterator {\n public:\n  ObjectIterator() : _slot(0) {}\n  explicit ObjectIterator(MemoryPool *pool, VariantSlot *slot)\n      : _pool(pool), _slot(slot) {}\n  Pair operator*() const {\n    return Pair(_pool, _slot);\n  }\n  PairPtr operator->() {\n    return PairPtr(_pool, _slot);\n  }\n  bool operator==(const ObjectIterator &other) const {\n    return _slot == other._slot;\n  }\n  bool operator!=(const ObjectIterator &other) const {\n    return _slot != other._slot;\n  }\n  ObjectIterator &operator++() {\n    _slot = _slot->next();\n    return *this;\n  }\n  ObjectIterator &operator+=(size_t distance) {\n    _slot = _slot->next(distance);\n    return *this;\n  }\n  VariantSlot *internal() {\n    return _slot;\n  }\n private:\n  MemoryPool *_pool;\n  VariantSlot *_slot;\n};\nclass PairConstPtr {\n public:\n  PairConstPtr(const VariantSlot *slot) : _pair(slot) {}\n  const PairConst *operator->() const {\n    return &_pair;\n  }\n  const PairConst &operator*() const {\n    return _pair;\n  }\n private:\n  PairConst _pair;\n};\nclass ObjectConstIterator {\n public:\n  ObjectConstIterator() : _slot(0) {}\n  explicit ObjectConstIterator(const VariantSlot *slot) : _slot(slot) {}\n  PairConst operator*() const {\n    return PairConst(_slot);\n  }\n  PairConstPtr operator->() {\n    return PairConstPtr(_slot);\n  }\n  bool operator==(const ObjectConstIterator &other) const {\n    return _slot == other._slot;\n  }\n  bool operator!=(const ObjectConstIterator &other) const {\n    return _slot != other._slot;\n  }\n  ObjectConstIterator &operator++() {\n    _slot = _slot->next();\n    return *this;\n  }\n  ObjectConstIterator &operator+=(size_t distance) {\n    _slot = _slot->next(distance);\n    return *this;\n  }\n  const VariantSlot *internal() {\n    return _slot;\n  }\n private:\n  const VariantSlot *_slot;\n};\n}  // namespace ARDUINOJSON_NAMESPACE\n#define JSON_OBJECT_SIZE(NUMBER_OF_ELEMENTS) \\\n  ((NUMBER_OF_ELEMENTS) * sizeof(ARDUINOJSON_NAMESPACE::VariantSlot))\nnamespace ARDUINOJSON_NAMESPACE {\ntemplate <typename TData>\nclass ObjectRefBase {\n public:\n  operator VariantConstRef() const {\n    const void* data = _data;  // prevent warning cast-align\n    return VariantConstRef(reinterpret_cast<const VariantData*>(data));\n  }\n  template <typename TVisitor>\n  typename TVisitor::result_type accept(TVisitor& visitor) const {\n    return objectAccept(_data, visitor);\n  }\n  FORCE_INLINE bool isNull() const {\n    return _data == 0;\n  }\n  FORCE_INLINE operator bool() const {\n    return _data != 0;\n  }\n  FORCE_INLINE size_t memoryUsage() const {\n    return _data ? _data->memoryUsage() : 0;\n  }\n  FORCE_INLINE size_t nesting() const {\n    return _data ? _data->nesting() : 0;\n  }\n  FORCE_INLINE size_t size() const {\n    return _data ? _data->size() : 0;\n  }\n protected:\n  ObjectRefBase(TData* data) : _data(data) {}\n  TData* _data;\n};\nclass ObjectConstRef : public ObjectRefBase<const CollectionData>,\n                       public Visitable {\n  friend class ObjectRef;\n  typedef ObjectRefBase<const CollectionData> base_type;\n public:\n  typedef ObjectConstIterator iterator;\n  ObjectConstRef() : base_type(0) {}\n  ObjectConstRef(const CollectionData* data) : base_type(data) {}\n  FORCE_INLINE iterator begin() const {\n    if (!_data)\n      return iterator();\n    return iterator(_data->head());\n  }\n  FORCE_INLINE iterator end() const {\n    return iterator();\n  }\n  template <typename TString>\n  FORCE_INLINE bool containsKey(const TString& key) const {\n    return !getMember(key).isUndefined();\n  }\n  template <typename TChar>\n  FORCE_INLINE bool containsKey(TChar* key) const {\n    return !getMember(key).isUndefined();\n  }\n  template <typename TString>\n  FORCE_INLINE VariantConstRef getMember(const TString& key) const {\n    return get_impl(adaptString(key));\n  }\n  template <typename TChar>\n  FORCE_INLINE VariantConstRef getMember(TChar* key) const {\n    return get_impl(adaptString(key));\n  }\n  template <typename TString>\n  FORCE_INLINE\n      typename enable_if<IsString<TString>::value, VariantConstRef>::type\n      operator[](const TString& key) const {\n    return get_impl(adaptString(key));\n  }\n  template <typename TChar>\n  FORCE_INLINE\n      typename enable_if<IsString<TChar*>::value, VariantConstRef>::type\n      operator[](TChar* key) const {\n    return get_impl(adaptString(key));\n  }\n  FORCE_INLINE bool operator==(ObjectConstRef rhs) const {\n    return objectEquals(_data, rhs._data);\n  }\n private:\n  template <typename TAdaptedString>\n  FORCE_INLINE VariantConstRef get_impl(TAdaptedString key) const {\n    return VariantConstRef(objectGetMember(_data, key));\n  }\n};\nclass ObjectRef : public ObjectRefBase<CollectionData>,\n                  public ObjectShortcuts<ObjectRef>,\n                  public Visitable {\n  typedef ObjectRefBase<CollectionData> base_type;\n public:\n  typedef ObjectIterator iterator;\n  FORCE_INLINE ObjectRef() : base_type(0), _pool(0) {}\n  FORCE_INLINE ObjectRef(MemoryPool* buf, CollectionData* data)\n      : base_type(data), _pool(buf) {}\n  operator VariantRef() const {\n    void* data = _data;  // prevent warning cast-align\n    return VariantRef(_pool, reinterpret_cast<VariantData*>(data));\n  }\n  operator ObjectConstRef() const {\n    return ObjectConstRef(_data);\n  }\n  FORCE_INLINE iterator begin() const {\n    if (!_data)\n      return iterator();\n    return iterator(_pool, _data->head());\n  }\n  FORCE_INLINE iterator end() const {\n    return iterator();\n  }\n  void clear() const {\n    if (!_data)\n      return;\n    _data->clear();\n  }\n  FORCE_INLINE bool set(ObjectConstRef src) {\n    if (!_data || !src._data)\n      return false;\n    return _data->copyFrom(*src._data, _pool);\n  }\n  template <typename TString>\n  FORCE_INLINE VariantRef getMember(const TString& key) const {\n    return VariantRef(_pool, objectGetMember(_data, adaptString(key)));\n  }\n  template <typename TChar>\n  FORCE_INLINE VariantRef getMember(TChar* key) const {\n    return VariantRef(_pool, objectGetMember(_data, adaptString(key)));\n  }\n  template <typename TString>\n  FORCE_INLINE VariantRef getOrAddMember(const TString& key) const {\n    return VariantRef(_pool,\n                      objectGetOrAddMember(_data, adaptString(key), _pool));\n  }\n  template <typename TChar>\n  FORCE_INLINE VariantRef getOrAddMember(TChar* key) const {\n    return VariantRef(_pool,\n                      objectGetOrAddMember(_data, adaptString(key), _pool));\n  }\n  FORCE_INLINE bool operator==(ObjectRef rhs) const {\n    return objectEquals(_data, rhs._data);\n  }\n  FORCE_INLINE void remove(iterator it) const {\n    if (!_data)\n      return;\n    _data->removeSlot(it.internal());\n  }\n  template <typename TString>\n  FORCE_INLINE void remove(const TString& key) const {\n    objectRemove(_data, adaptString(key));\n  }\n  template <typename TChar>\n  FORCE_INLINE void remove(TChar* key) const {\n    objectRemove(_data, adaptString(key));\n  }\n private:\n  MemoryPool* _pool;\n};\ntemplate <>\nstruct Converter<ObjectConstRef> {\n  static bool toJson(VariantConstRef src, VariantRef dst) {\n    return variantCopyFrom(getData(dst), getData(src), getPool(dst));\n  }\n  static ObjectConstRef fromJson(VariantConstRef src) {\n    return ObjectConstRef(variantAsObject(getData(src)));\n  }\n  static bool checkJson(VariantConstRef src) {\n    const VariantData* data = getData(src);\n    return data && data->isObject();\n  }\n};\ntemplate <>\nstruct Converter<ObjectRef> {\n  static bool toJson(VariantConstRef src, VariantRef dst) {\n    return variantCopyFrom(getData(dst), getData(src), getPool(dst));\n  }\n  static ObjectRef fromJson(VariantRef src) {\n    VariantData* data = getData(src);\n    MemoryPool* pool = getPool(src);\n    return ObjectRef(pool, data != 0 ? data->asObject() : 0);\n  }\n  static InvalidConversion<VariantConstRef, ObjectRef> fromJson(\n      VariantConstRef);\n  static bool checkJson(VariantConstRef) {\n    return false;\n  }\n  static bool checkJson(VariantRef src) {\n    VariantData* data = getData(src);\n    return data && data->isObject();\n  }\n};\nclass ArrayRef;\nclass ObjectRef;\nclass VariantRef;\ntemplate <typename T>\nstruct VariantTo {};\ntemplate <>\nstruct VariantTo<ArrayRef> {\n  typedef ArrayRef type;\n};\ntemplate <>\nstruct VariantTo<ObjectRef> {\n  typedef ObjectRef type;\n};\ntemplate <>\nstruct VariantTo<VariantRef> {\n  typedef VariantRef type;\n};\n}  // namespace ARDUINOJSON_NAMESPACE\n#ifdef _MSC_VER\n#  pragma warning(push)\n#  pragma warning(disable : 4522)\n#endif\nnamespace ARDUINOJSON_NAMESPACE {\ntemplate <typename TArray>\nclass ElementProxy : public VariantOperators<ElementProxy<TArray> >,\n                     public VariantShortcuts<ElementProxy<TArray> >,\n                     public Visitable,\n                     public VariantTag {\n  typedef ElementProxy<TArray> this_type;\n public:\n  typedef VariantRef variant_type;\n  FORCE_INLINE ElementProxy(TArray array, size_t index)\n      : _array(array), _index(index) {}\n  FORCE_INLINE ElementProxy(const ElementProxy& src)\n      : _array(src._array), _index(src._index) {}\n  FORCE_INLINE this_type& operator=(const this_type& src) {\n    getOrAddUpstreamElement().set(src.as<VariantConstRef>());\n    return *this;\n  }\n  template <typename T>\n  FORCE_INLINE this_type& operator=(const T& src) {\n    getOrAddUpstreamElement().set(src);\n    return *this;\n  }\n  template <typename T>\n  FORCE_INLINE this_type& operator=(T* src) {\n    getOrAddUpstreamElement().set(src);\n    return *this;\n  }\n  FORCE_INLINE void clear() const {\n    getUpstreamElement().clear();\n  }\n  FORCE_INLINE bool isNull() const {\n    return getUpstreamElement().isNull();\n  }\n  template <typename T>\n  FORCE_INLINE typename enable_if<!is_same<T, char*>::value, T>::type as()\n      const {\n    return getUpstreamElement().template as<T>();\n  }\n  template <typename T>\n  FORCE_INLINE typename enable_if<is_same<T, char*>::value, const char*>::type\n  ARDUINOJSON_DEPRECATED(\"Replace as<char*>() with as<const char*>()\")\n      as() const {\n    return as<const char*>();\n  }\n  template <typename T>\n  FORCE_INLINE operator T() const {\n    return getUpstreamElement();\n  }\n  template <typename T>\n  FORCE_INLINE bool is() const {\n    return getUpstreamElement().template is<T>();\n  }\n  template <typename T>\n  FORCE_INLINE typename VariantTo<T>::type to() const {\n    return getOrAddUpstreamElement().template to<T>();\n  }\n  template <typename TValue>\n  FORCE_INLINE bool set(const TValue& value) const {\n    return getOrAddUpstreamElement().set(value);\n  }\n  template <typename TValue>\n  FORCE_INLINE bool set(TValue* value) const {\n    return getOrAddUpstreamElement().set(value);\n  }\n  template <typename TVisitor>\n  typename TVisitor::result_type accept(TVisitor& visitor) const {\n    return getUpstreamElement().accept(visitor);\n  }\n  FORCE_INLINE size_t size() const {\n    return getUpstreamElement().size();\n  }\n  template <typename TNestedKey>\n  VariantRef getMember(TNestedKey* key) const {\n    return getUpstreamElement().getMember(key);\n  }\n  template <typename TNestedKey>\n  VariantRef getMember(const TNestedKey& key) const {\n    return getUpstreamElement().getMember(key);\n  }\n  template <typename TNestedKey>\n  VariantRef getOrAddMember(TNestedKey* key) const {\n    return getOrAddUpstreamElement().getOrAddMember(key);\n  }\n  template <typename TNestedKey>\n  VariantRef getOrAddMember(const TNestedKey& key) const {\n    return getOrAddUpstreamElement().getOrAddMember(key);\n  }\n  VariantRef addElement() const {\n    return getOrAddUpstreamElement().addElement();\n  }\n  VariantRef getElement(size_t index) const {\n    return getOrAddUpstreamElement().getElement(index);\n  }\n  VariantRef getOrAddElement(size_t index) const {\n    return getOrAddUpstreamElement().getOrAddElement(index);\n  }\n  FORCE_INLINE void remove(size_t index) const {\n    getUpstreamElement().remove(index);\n  }\n  template <typename TChar>\n  FORCE_INLINE typename enable_if<IsString<TChar*>::value>::type remove(\n      TChar* key) const {\n    getUpstreamElement().remove(key);\n  }\n  template <typename TString>\n  FORCE_INLINE typename enable_if<IsString<TString>::value>::type remove(\n      const TString& key) const {\n    getUpstreamElement().remove(key);\n  }\n private:\n  FORCE_INLINE VariantRef getUpstreamElement() const {\n    return _array.getElement(_index);\n  }\n  FORCE_INLINE VariantRef getOrAddUpstreamElement() const {\n    return _array.getOrAddElement(_index);\n  }\n  friend bool convertToJson(const this_type& src, VariantRef dst) {\n    return dst.set(src.getUpstreamElement());\n  }\n  TArray _array;\n  const size_t _index;\n};\n}  // namespace ARDUINOJSON_NAMESPACE\n#ifdef _MSC_VER\n#  pragma warning(pop)\n#endif\n#ifdef _MSC_VER\n#  pragma warning(push)\n#  pragma warning(disable : 4522)\n#endif\nnamespace ARDUINOJSON_NAMESPACE {\ntemplate <typename TObject, typename TStringRef>\nclass MemberProxy : public VariantOperators<MemberProxy<TObject, TStringRef> >,\n                    public VariantShortcuts<MemberProxy<TObject, TStringRef> >,\n                    public Visitable,\n                    public VariantTag {\n  typedef MemberProxy<TObject, TStringRef> this_type;\n public:\n  typedef VariantRef variant_type;\n  FORCE_INLINE MemberProxy(TObject variant, TStringRef key)\n      : _object(variant), _key(key) {}\n  FORCE_INLINE MemberProxy(const MemberProxy &src)\n      : _object(src._object), _key(src._key) {}\n  FORCE_INLINE operator VariantConstRef() const {\n    return getUpstreamMember();\n  }\n  FORCE_INLINE this_type &operator=(const this_type &src) {\n    getOrAddUpstreamMember().set(src);\n    return *this;\n  }\n  template <typename TValue>\n  FORCE_INLINE typename enable_if<!is_array<TValue>::value, this_type &>::type\n  operator=(const TValue &src) {\n    getOrAddUpstreamMember().set(src);\n    return *this;\n  }\n  template <typename TChar>\n  FORCE_INLINE this_type &operator=(TChar *src) {\n    getOrAddUpstreamMember().set(src);\n    return *this;\n  }\n  FORCE_INLINE void clear() const {\n    getUpstreamMember().clear();\n  }\n  FORCE_INLINE bool isNull() const {\n    return getUpstreamMember().isNull();\n  }\n  template <typename T>\n  FORCE_INLINE typename enable_if<!is_same<T, char *>::value, T>::type as()\n      const {\n    return getUpstreamMember().template as<T>();\n  }\n  template <typename T>\n  FORCE_INLINE typename enable_if<is_same<T, char *>::value, const char *>::type\n  ARDUINOJSON_DEPRECATED(\"Replace as<char*>() with as<const char*>()\")\n      as() const {\n    return as<const char *>();\n  }\n  template <typename T>\n  FORCE_INLINE operator T() const {\n    return getUpstreamMember();\n  }\n  template <typename TValue>\n  FORCE_INLINE bool is() const {\n    return getUpstreamMember().template is<TValue>();\n  }\n  FORCE_INLINE size_t size() const {\n    return getUpstreamMember().size();\n  }\n  FORCE_INLINE void remove(size_t index) const {\n    getUpstreamMember().remove(index);\n  }\n  template <typename TChar>\n  FORCE_INLINE typename enable_if<IsString<TChar *>::value>::type remove(\n      TChar *key) const {\n    getUpstreamMember().remove(key);\n  }\n  template <typename TString>\n  FORCE_INLINE typename enable_if<IsString<TString>::value>::type remove(\n      const TString &key) const {\n    getUpstreamMember().remove(key);\n  }\n  template <typename TValue>\n  FORCE_INLINE typename VariantTo<TValue>::type to() {\n    return getOrAddUpstreamMember().template to<TValue>();\n  }\n  template <typename TValue>\n  FORCE_INLINE bool set(const TValue &value) {\n    return getOrAddUpstreamMember().set(value);\n  }\n  template <typename TChar>\n  FORCE_INLINE bool set(TChar *value) {\n    return getOrAddUpstreamMember().set(value);\n  }\n  template <typename TVisitor>\n  typename TVisitor::result_type accept(TVisitor &visitor) const {\n    return getUpstreamMember().accept(visitor);\n  }\n  FORCE_INLINE VariantRef addElement() const {\n    return getOrAddUpstreamMember().addElement();\n  }\n  FORCE_INLINE VariantRef getElement(size_t index) const {\n    return getUpstreamMember().getElement(index);\n  }\n  FORCE_INLINE VariantRef getOrAddElement(size_t index) const {\n    return getOrAddUpstreamMember().getOrAddElement(index);\n  }\n  template <typename TChar>\n  FORCE_INLINE VariantRef getMember(TChar *key) const {\n    return getUpstreamMember().getMember(key);\n  }\n  template <typename TString>\n  FORCE_INLINE VariantRef getMember(const TString &key) const {\n    return getUpstreamMember().getMember(key);\n  }\n  template <typename TChar>\n  FORCE_INLINE VariantRef getOrAddMember(TChar *key) const {\n    return getOrAddUpstreamMember().getOrAddMember(key);\n  }\n  template <typename TString>\n  FORCE_INLINE VariantRef getOrAddMember(const TString &key) const {\n    return getOrAddUpstreamMember().getOrAddMember(key);\n  }\n private:\n  FORCE_INLINE VariantRef getUpstreamMember() const {\n    return _object.getMember(_key);\n  }\n  FORCE_INLINE VariantRef getOrAddUpstreamMember() const {\n    return _object.getOrAddMember(_key);\n  }\n  friend bool convertToJson(const this_type &src, VariantRef dst) {\n    return dst.set(src.getUpstreamMember());\n  }\n  TObject _object;\n  TStringRef _key;\n};\n}  // namespace ARDUINOJSON_NAMESPACE\n#ifdef _MSC_VER\n#  pragma warning(pop)\n#endif\nnamespace ARDUINOJSON_NAMESPACE {\nclass JsonDocument : public Visitable {\n public:\n  template <typename TVisitor>\n  typename TVisitor::result_type accept(TVisitor& visitor) const {\n    return getVariant().accept(visitor);\n  }\n  template <typename T>\n  T as() {\n    return getVariant().template as<T>();\n  }\n  template <typename T>\n  T as() const {\n    return getVariant().template as<T>();\n  }\n  void clear() {\n    _pool.clear();\n    _data.init();\n  }\n  template <typename T>\n  bool is() {\n    return getVariant().template is<T>();\n  }\n  template <typename T>\n  bool is() const {\n    return getVariant().template is<T>();\n  }\n  bool isNull() const {\n    return getVariant().isNull();\n  }\n  size_t memoryUsage() const {\n    return _pool.size();\n  }\n  bool overflowed() const {\n    return _pool.overflowed();\n  }\n  size_t nesting() const {\n    return _data.nesting();\n  }\n  size_t capacity() const {\n    return _pool.capacity();\n  }\n  size_t size() const {\n    return _data.size();\n  }\n  bool set(const JsonDocument& src) {\n    return to<VariantRef>().set(src.as<VariantConstRef>());\n  }\n  template <typename T>\n  typename enable_if<!is_base_of<JsonDocument, T>::value, bool>::type set(\n      const T& src) {\n    return to<VariantRef>().set(src);\n  }\n  template <typename T>\n  typename VariantTo<T>::type to() {\n    clear();\n    return getVariant().template to<T>();\n  }\n  MemoryPool& memoryPool() {\n    return _pool;\n  }\n  VariantData& data() {\n    return _data;\n  }\n  ArrayRef createNestedArray() {\n    return addElement().to<ArrayRef>();\n  }\n  template <typename TChar>\n  ArrayRef createNestedArray(TChar* key) {\n    return getOrAddMember(key).template to<ArrayRef>();\n  }\n  template <typename TString>\n  ArrayRef createNestedArray(const TString& key) {\n    return getOrAddMember(key).template to<ArrayRef>();\n  }\n  ObjectRef createNestedObject() {\n    return addElement().to<ObjectRef>();\n  }\n  template <typename TChar>\n  ObjectRef createNestedObject(TChar* key) {\n    return getOrAddMember(key).template to<ObjectRef>();\n  }\n  template <typename TString>\n  ObjectRef createNestedObject(const TString& key) {\n    return getOrAddMember(key).template to<ObjectRef>();\n  }\n  template <typename TChar>\n  bool containsKey(TChar* key) const {\n    return !getMember(key).isUndefined();\n  }\n  template <typename TString>\n  bool containsKey(const TString& key) const {\n    return !getMember(key).isUndefined();\n  }\n  template <typename TString>\n  FORCE_INLINE typename enable_if<IsString<TString>::value,\n                                  MemberProxy<JsonDocument&, TString> >::type\n  operator[](const TString& key) {\n    return MemberProxy<JsonDocument&, TString>(*this, key);\n  }\n  template <typename TChar>\n  FORCE_INLINE typename enable_if<IsString<TChar*>::value,\n                                  MemberProxy<JsonDocument&, TChar*> >::type\n  operator[](TChar* key) {\n    return MemberProxy<JsonDocument&, TChar*>(*this, key);\n  }\n  template <typename TString>\n  FORCE_INLINE\n      typename enable_if<IsString<TString>::value, VariantConstRef>::type\n      operator[](const TString& key) const {\n    return getMember(key);\n  }\n  template <typename TChar>\n  FORCE_INLINE\n      typename enable_if<IsString<TChar*>::value, VariantConstRef>::type\n      operator[](TChar* key) const {\n    return getMember(key);\n  }\n  FORCE_INLINE ElementProxy<JsonDocument&> operator[](size_t index) {\n    return ElementProxy<JsonDocument&>(*this, index);\n  }\n  FORCE_INLINE VariantConstRef operator[](size_t index) const {\n    return getElement(index);\n  }\n  FORCE_INLINE VariantRef getElement(size_t index) {\n    return VariantRef(&_pool, _data.getElement(index));\n  }\n  FORCE_INLINE VariantConstRef getElement(size_t index) const {\n    return VariantConstRef(_data.getElement(index));\n  }\n  FORCE_INLINE VariantRef getOrAddElement(size_t index) {\n    return VariantRef(&_pool, _data.getOrAddElement(index, &_pool));\n  }\n  template <typename TChar>\n  FORCE_INLINE VariantConstRef getMember(TChar* key) const {\n    return VariantConstRef(_data.getMember(adaptString(key)));\n  }\n  template <typename TString>\n  FORCE_INLINE\n      typename enable_if<IsString<TString>::value, VariantConstRef>::type\n      getMember(const TString& key) const {\n    return VariantConstRef(_data.getMember(adaptString(key)));\n  }\n  template <typename TChar>\n  FORCE_INLINE VariantRef getMember(TChar* key) {\n    return VariantRef(&_pool, _data.getMember(adaptString(key)));\n  }\n  template <typename TString>\n  FORCE_INLINE typename enable_if<IsString<TString>::value, VariantRef>::type\n  getMember(const TString& key) {\n    return VariantRef(&_pool, _data.getMember(adaptString(key)));\n  }\n  template <typename TChar>\n  FORCE_INLINE VariantRef getOrAddMember(TChar* key) {\n    return VariantRef(&_pool, _data.getOrAddMember(adaptString(key), &_pool));\n  }\n  template <typename TString>\n  FORCE_INLINE VariantRef getOrAddMember(const TString& key) {\n    return VariantRef(&_pool, _data.getOrAddMember(adaptString(key), &_pool));\n  }\n  FORCE_INLINE VariantRef addElement() {\n    return VariantRef(&_pool, _data.addElement(&_pool));\n  }\n  template <typename TValue>\n  FORCE_INLINE bool add(const TValue& value) {\n    return addElement().set(value);\n  }\n  template <typename TChar>\n  FORCE_INLINE bool add(TChar* value) {\n    return addElement().set(value);\n  }\n  FORCE_INLINE void remove(size_t index) {\n    _data.remove(index);\n  }\n  template <typename TChar>\n  FORCE_INLINE typename enable_if<IsString<TChar*>::value>::type remove(\n      TChar* key) {\n    _data.remove(adaptString(key));\n  }\n  template <typename TString>\n  FORCE_INLINE typename enable_if<IsString<TString>::value>::type remove(\n      const TString& key) {\n    _data.remove(adaptString(key));\n  }\n  FORCE_INLINE operator VariantConstRef() const {\n    return VariantConstRef(&_data);\n  }\n  bool operator==(VariantConstRef rhs) const {\n    return getVariant() == rhs;\n  }\n  bool operator!=(VariantConstRef rhs) const {\n    return getVariant() != rhs;\n  }\n protected:\n  JsonDocument() : _pool(0, 0) {\n    _data.init();\n  }\n  JsonDocument(MemoryPool pool) : _pool(pool) {\n    _data.init();\n  }\n  JsonDocument(char* buf, size_t capa) : _pool(buf, capa) {\n    _data.init();\n  }\n  ~JsonDocument() {}\n  void replacePool(MemoryPool pool) {\n    _pool = pool;\n  }\n  VariantRef getVariant() {\n    return VariantRef(&_pool, &_data);\n  }\n  VariantConstRef getVariant() const {\n    return VariantConstRef(&_data);\n  }\n  MemoryPool _pool;\n  VariantData _data;\n private:\n  JsonDocument(const JsonDocument&);\n  JsonDocument& operator=(const JsonDocument&);\n};\ninline bool convertToJson(const JsonDocument& src, VariantRef dst) {\n  return dst.set(src.as<VariantConstRef>());\n}\ntemplate <typename TAllocator>\nclass AllocatorOwner {\n public:\n  AllocatorOwner() {}\n  AllocatorOwner(const AllocatorOwner& src) : _allocator(src._allocator) {}\n  AllocatorOwner(TAllocator a) : _allocator(a) {}\n  void* allocate(size_t size) {\n    return _allocator.allocate(size);\n  }\n  void deallocate(void* ptr) {\n    if (ptr)\n      _allocator.deallocate(ptr);\n  }\n  void* reallocate(void* ptr, size_t new_size) {\n    return _allocator.reallocate(ptr, new_size);\n  }\n  TAllocator& allocator() {\n    return _allocator;\n  }\n private:\n  TAllocator _allocator;\n};\ntemplate <typename TAllocator>\nclass BasicJsonDocument : AllocatorOwner<TAllocator>, public JsonDocument {\n public:\n  explicit BasicJsonDocument(size_t capa, TAllocator alloc = TAllocator())\n      : AllocatorOwner<TAllocator>(alloc), JsonDocument(allocPool(capa)) {}\n  BasicJsonDocument(const BasicJsonDocument& src)\n      : AllocatorOwner<TAllocator>(src), JsonDocument() {\n    copyAssignFrom(src);\n  }\n#if ARDUINOJSON_HAS_RVALUE_REFERENCES\n  BasicJsonDocument(BasicJsonDocument&& src) : AllocatorOwner<TAllocator>(src) {\n    moveAssignFrom(src);\n  }\n#endif\n  BasicJsonDocument(const JsonDocument& src) {\n    copyAssignFrom(src);\n  }\n  template <typename T>\n  BasicJsonDocument(\n      const T& src,\n      typename enable_if<\n          is_same<T, VariantRef>::value || is_same<T, VariantConstRef>::value ||\n          is_same<T, ArrayRef>::value || is_same<T, ArrayConstRef>::value ||\n          is_same<T, ObjectRef>::value ||\n          is_same<T, ObjectConstRef>::value>::type* = 0)\n      : JsonDocument(allocPool(src.memoryUsage())) {\n    set(src);\n  }\n  BasicJsonDocument(VariantRef src)\n      : JsonDocument(allocPool(src.memoryUsage())) {\n    set(src);\n  }\n  ~BasicJsonDocument() {\n    freePool();\n  }\n  BasicJsonDocument& operator=(const BasicJsonDocument& src) {\n    copyAssignFrom(src);\n    return *this;\n  }\n#if ARDUINOJSON_HAS_RVALUE_REFERENCES\n  BasicJsonDocument& operator=(BasicJsonDocument&& src) {\n    moveAssignFrom(src);\n    return *this;\n  }\n#endif\n  template <typename T>\n  BasicJsonDocument& operator=(const T& src) {\n    reallocPoolIfTooSmall(src.memoryUsage());\n    set(src);\n    return *this;\n  }\n  void shrinkToFit() {\n    ptrdiff_t bytes_reclaimed = _pool.squash();\n    if (bytes_reclaimed == 0)\n      return;\n    void* old_ptr = _pool.buffer();\n    void* new_ptr = this->reallocate(old_ptr, _pool.capacity());\n    ptrdiff_t ptr_offset =\n        static_cast<char*>(new_ptr) - static_cast<char*>(old_ptr);\n    _pool.movePointers(ptr_offset);\n    _data.movePointers(ptr_offset, ptr_offset - bytes_reclaimed);\n  }\n  bool garbageCollect() {\n    BasicJsonDocument tmp(*this);\n    if (!tmp.capacity())\n      return false;\n    tmp.set(*this);\n    moveAssignFrom(tmp);\n    return true;\n  }\n  using AllocatorOwner<TAllocator>::allocator;\n private:\n  MemoryPool allocPool(size_t requiredSize) {\n    size_t capa = addPadding(requiredSize);\n    return MemoryPool(reinterpret_cast<char*>(this->allocate(capa)), capa);\n  }\n  void reallocPoolIfTooSmall(size_t requiredSize) {\n    if (requiredSize <= capacity())\n      return;\n    freePool();\n    replacePool(allocPool(addPadding(requiredSize)));\n  }\n  void freePool() {\n    this->deallocate(memoryPool().buffer());\n  }\n  void copyAssignFrom(const JsonDocument& src) {\n    reallocPoolIfTooSmall(src.capacity());\n    set(src);\n  }\n  void moveAssignFrom(BasicJsonDocument& src) {\n    freePool();\n    _data = src._data;\n    _pool = src._pool;\n    src._data.setNull();\n    src._pool = MemoryPool(0, 0);\n  }\n};\n}  // namespace ARDUINOJSON_NAMESPACE\n#include <stdlib.h>\nnamespace ARDUINOJSON_NAMESPACE {\nstruct DefaultAllocator {\n  void* allocate(size_t size) {\n    return malloc(size);\n  }\n  void deallocate(void* ptr) {\n    free(ptr);\n  }\n  void* reallocate(void* ptr, size_t new_size) {\n    return realloc(ptr, new_size);\n  }\n};\ntypedef BasicJsonDocument<DefaultAllocator> DynamicJsonDocument;\ntemplate <size_t desiredCapacity>\nclass StaticJsonDocument : public JsonDocument {\n  static const size_t _capacity =\n      AddPadding<Max<1, desiredCapacity>::value>::value;\n public:\n  StaticJsonDocument() : JsonDocument(_buffer, _capacity) {}\n  StaticJsonDocument(const StaticJsonDocument& src)\n      : JsonDocument(_buffer, _capacity) {\n    set(src);\n  }\n  template <typename T>\n  StaticJsonDocument(const T& src,\n                     typename enable_if<IsVisitable<T>::value>::type* = 0)\n      : JsonDocument(_buffer, _capacity) {\n    set(src);\n  }\n  StaticJsonDocument(VariantRef src) : JsonDocument(_buffer, _capacity) {\n    set(src);\n  }\n  StaticJsonDocument operator=(const StaticJsonDocument& src) {\n    set(src);\n    return *this;\n  }\n  template <typename T>\n  StaticJsonDocument operator=(const T& src) {\n    set(src);\n    return *this;\n  }\n  void garbageCollect() {\n    StaticJsonDocument tmp(*this);\n    set(tmp);\n  }\n private:\n  char _buffer[_capacity];\n};\ntemplate <typename TArray>\ninline ArrayRef ArrayShortcuts<TArray>::createNestedArray() const {\n  return impl()->addElement().template to<ArrayRef>();\n}\ntemplate <typename TArray>\ninline ObjectRef ArrayShortcuts<TArray>::createNestedObject() const {\n  return impl()->addElement().template to<ObjectRef>();\n}\ntemplate <typename TArray>\ninline ElementProxy<TArray> ArrayShortcuts<TArray>::operator[](\n    size_t index) const {\n  return ElementProxy<TArray>(*impl(), index);\n}\ntemplate <typename TResult>\nstruct Visitor {\n  typedef TResult result_type;\n  TResult visitArray(const CollectionData &) {\n    return TResult();\n  }\n  TResult visitBoolean(bool) {\n    return TResult();\n  }\n  TResult visitFloat(Float) {\n    return TResult();\n  }\n  TResult visitSignedInteger(Integer) {\n    return TResult();\n  }\n  TResult visitNull() {\n    return TResult();\n  }\n  TResult visitObject(const CollectionData &) {\n    return TResult();\n  }\n  TResult visitUnsignedInteger(UInt) {\n    return TResult();\n  }\n  TResult visitRawJson(const char *, size_t) {\n    return TResult();\n  }\n  TResult visitString(const char *) {\n    return TResult();\n  }\n};\ntemplate <typename T, size_t N, typename TDestination>\ninline typename enable_if<!is_array<T>::value &&\n                              !is_base_of<JsonDocument, TDestination>::value,\n                          bool>::type\ncopyArray(T (&src)[N], const TDestination& dst) {\n  return copyArray(src, N, dst);\n}\ntemplate <typename T, size_t N>\ninline bool copyArray(T (&src)[N], JsonDocument& dst) {\n  return copyArray(src, dst.to<ArrayRef>());\n}\ntemplate <typename T, typename TDestination>\ninline typename enable_if<!is_array<T>::value &&\n                              !is_base_of<JsonDocument, TDestination>::value,\n                          bool>::type\ncopyArray(T* src, size_t len, const TDestination& dst) {\n  bool ok = true;\n  for (size_t i = 0; i < len; i++) {\n    ok &= dst.add(src[i]);\n  }\n  return ok;\n}\ntemplate <typename T>\ninline bool copyArray(T* src, size_t len, JsonDocument& dst) {\n  return copyArray(src, len, dst.to<ArrayRef>());\n}\ntemplate <typename T, size_t N1, size_t N2, typename TDestination>\ninline typename enable_if<!is_base_of<JsonDocument, TDestination>::value,\n                          bool>::type\ncopyArray(T (&src)[N1][N2], const TDestination& dst) {\n  bool ok = true;\n  for (size_t i = 0; i < N1; i++) {\n    ArrayRef nestedArray = dst.createNestedArray();\n    for (size_t j = 0; j < N2; j++) {\n      ok &= nestedArray.add(src[i][j]);\n    }\n  }\n  return ok;\n}\ntemplate <typename T, size_t N1, size_t N2>\ninline bool copyArray(T (&src)[N1][N2], JsonDocument& dst) {\n  return copyArray(src, dst.to<ArrayRef>());\n}\ntemplate <typename T>\nclass ArrayCopier1D : public Visitor<size_t> {\n public:\n  ArrayCopier1D(T* destination, size_t capacity)\n      : _destination(destination), _capacity(capacity) {}\n  size_t visitArray(const CollectionData& array) {\n    size_t size = 0;\n    VariantSlot* slot = array.head();\n    while (slot != 0 && size < _capacity) {\n      _destination[size++] =\n          Converter<T>::fromJson(VariantConstRef(slot->data()));\n      slot = slot->next();\n    }\n    return size;\n  }\n private:\n  T* _destination;\n  size_t _capacity;\n};\ntemplate <typename T, size_t N1, size_t N2>\nclass ArrayCopier2D : public Visitor<void> {\n public:\n  ArrayCopier2D(T (*destination)[N1][N2]) : _destination(destination) {}\n  void visitArray(const CollectionData& array) {\n    VariantSlot* slot = array.head();\n    size_t n = 0;\n    while (slot != 0 && n < N1) {\n      ArrayCopier1D<T> copier((*_destination)[n++], N2);\n      variantAccept(slot->data(), copier);\n      slot = slot->next();\n    }\n  }\n private:\n  T (*_destination)[N1][N2];\n  size_t _capacity1, _capacity2;\n};\ntemplate <typename TSource, typename T, size_t N>\ninline typename enable_if<!is_array<T>::value, size_t>::type copyArray(\n    const TSource& src, T (&dst)[N]) {\n  return copyArray(src, dst, N);\n}\ntemplate <typename TSource, typename T>\ninline size_t copyArray(const TSource& src, T* dst, size_t len) {\n  ArrayCopier1D<T> copier(dst, len);\n  return src.accept(copier);\n}\ntemplate <typename TSource, typename T, size_t N1, size_t N2>\ninline void copyArray(const TSource& src, T (&dst)[N1][N2]) {\n  ArrayCopier2D<T, N1, N2> copier(&dst);\n  src.accept(copier);\n}\ninline bool variantEquals(const VariantData* a, const VariantData* b) {\n  return variantCompare(a, b) == COMPARE_RESULT_EQUAL;\n}\ninline VariantSlot* CollectionData::addSlot(MemoryPool* pool) {\n  VariantSlot* slot = pool->allocVariant();\n  if (!slot)\n    return 0;\n  if (_tail) {\n    _tail->setNextNotNull(slot);\n    _tail = slot;\n  } else {\n    _head = slot;\n    _tail = slot;\n  }\n  slot->clear();\n  return slot;\n}\ninline VariantData* CollectionData::addElement(MemoryPool* pool) {\n  return slotData(addSlot(pool));\n}\ntemplate <typename TAdaptedString>\ninline VariantData* CollectionData::addMember(TAdaptedString key,\n                                              MemoryPool* pool) {\n  VariantSlot* slot = addSlot(pool);\n  if (!slotSetKey(slot, key, pool)) {\n    removeSlot(slot);\n    return 0;\n  }\n  return slot->data();\n}\ninline void CollectionData::clear() {\n  _head = 0;\n  _tail = 0;\n}\ntemplate <typename TAdaptedString>\ninline bool CollectionData::containsKey(const TAdaptedString& key) const {\n  return getSlot(key) != 0;\n}\ninline bool CollectionData::copyFrom(const CollectionData& src,\n                                     MemoryPool* pool) {\n  clear();\n  for (VariantSlot* s = src._head; s; s = s->next()) {\n    VariantData* var;\n    if (s->key() != 0) {\n      if (s->ownsKey())\n        var = addMember(RamStringAdapter(s->key()), pool);\n      else\n        var = addMember(ConstRamStringAdapter(s->key()), pool);\n    } else {\n      var = addElement(pool);\n    }\n    if (!var)\n      return false;\n    if (!var->copyFrom(*s->data(), pool))\n      return false;\n  }\n  return true;\n}\ninline bool CollectionData::equalsObject(const CollectionData& other) const {\n  size_t count = 0;\n  for (VariantSlot* slot = _head; slot; slot = slot->next()) {\n    VariantData* v1 = slot->data();\n    VariantData* v2 = other.getMember(adaptString(slot->key()));\n    if (!variantEquals(v1, v2))\n      return false;\n    count++;\n  }\n  return count == other.size();\n}\ninline bool CollectionData::equalsArray(const CollectionData& other) const {\n  VariantSlot* s1 = _head;\n  VariantSlot* s2 = other._head;\n  for (;;) {\n    if (s1 == s2)\n      return true;\n    if (!s1 || !s2)\n      return false;\n    if (!variantEquals(s1->data(), s2->data()))\n      return false;\n    s1 = s1->next();\n    s2 = s2->next();\n  }\n}\ntemplate <typename TAdaptedString>\ninline VariantSlot* CollectionData::getSlot(TAdaptedString key) const {\n  VariantSlot* slot = _head;\n  while (slot) {\n    if (key.equals(slot->key()))\n      break;\n    slot = slot->next();\n  }\n  return slot;\n}\ninline VariantSlot* CollectionData::getSlot(size_t index) const {\n  if (!_head)\n    return 0;\n  return _head->next(index);\n}\ninline VariantSlot* CollectionData::getPreviousSlot(VariantSlot* target) const {\n  VariantSlot* current = _head;\n  while (current) {\n    VariantSlot* next = current->next();\n    if (next == target)\n      return current;\n    current = next;\n  }\n  return 0;\n}\ntemplate <typename TAdaptedString>\ninline VariantData* CollectionData::getMember(TAdaptedString key) const {\n  VariantSlot* slot = getSlot(key);\n  return slot ? slot->data() : 0;\n}\ntemplate <typename TAdaptedString>\ninline VariantData* CollectionData::getOrAddMember(TAdaptedString key,\n                                                   MemoryPool* pool) {\n  if (key.isNull())\n    return 0;\n  VariantSlot* slot = getSlot(key);\n  if (slot)\n    return slot->data();\n  return addMember(key, pool);\n}\ninline VariantData* CollectionData::getElement(size_t index) const {\n  VariantSlot* slot = getSlot(index);\n  return slot ? slot->data() : 0;\n}\ninline VariantData* CollectionData::getOrAddElement(size_t index,\n                                                    MemoryPool* pool) {\n  VariantSlot* slot = _head;\n  while (slot && index > 0) {\n    slot = slot->next();\n    index--;\n  }\n  if (!slot)\n    index++;\n  while (index > 0) {\n    slot = addSlot(pool);\n    index--;\n  }\n  return slotData(slot);\n}\ninline void CollectionData::removeSlot(VariantSlot* slot) {\n  if (!slot)\n    return;\n  VariantSlot* prev = getPreviousSlot(slot);\n  VariantSlot* next = slot->next();\n  if (prev)\n    prev->setNext(next);\n  else\n    _head = next;\n  if (!next)\n    _tail = prev;\n}\ninline void CollectionData::removeElement(size_t index) {\n  removeSlot(getSlot(index));\n}\ninline size_t CollectionData::memoryUsage() const {\n  size_t total = 0;\n  for (VariantSlot* s = _head; s; s = s->next()) {\n    total += sizeof(VariantSlot) + s->data()->memoryUsage();\n    if (s->ownsKey())\n      total += strlen(s->key()) + 1;\n  }\n  return total;\n}\ninline size_t CollectionData::nesting() const {\n  size_t maxChildNesting = 0;\n  for (VariantSlot* s = _head; s; s = s->next()) {\n    size_t childNesting = s->data()->nesting();\n    if (childNesting > maxChildNesting)\n      maxChildNesting = childNesting;\n  }\n  return maxChildNesting + 1;\n}\ninline size_t CollectionData::size() const {\n  return slotSize(_head);\n}\ntemplate <typename T>\ninline void movePointer(T*& p, ptrdiff_t offset) {\n  if (!p)\n    return;\n  p = reinterpret_cast<T*>(\n      reinterpret_cast<void*>(reinterpret_cast<char*>(p) + offset));\n  ARDUINOJSON_ASSERT(isAligned(p));\n}\ninline void CollectionData::movePointers(ptrdiff_t stringDistance,\n                                         ptrdiff_t variantDistance) {\n  movePointer(_head, variantDistance);\n  movePointer(_tail, variantDistance);\n  for (VariantSlot* slot = _head; slot; slot = slot->next())\n    slot->movePointers(stringDistance, variantDistance);\n}\ntemplate <typename TObject>\ntemplate <typename TString>\ninline ArrayRef ObjectShortcuts<TObject>::createNestedArray(\n    const TString& key) const {\n  return impl()->getOrAddMember(key).template to<ArrayRef>();\n}\ntemplate <typename TObject>\ntemplate <typename TChar>\ninline ArrayRef ObjectShortcuts<TObject>::createNestedArray(TChar* key) const {\n  return impl()->getOrAddMember(key).template to<ArrayRef>();\n}\ntemplate <typename TObject>\ntemplate <typename TString>\ninline ObjectRef ObjectShortcuts<TObject>::createNestedObject(\n    const TString& key) const {\n  return impl()->getOrAddMember(key).template to<ObjectRef>();\n}\ntemplate <typename TObject>\ntemplate <typename TChar>\ninline ObjectRef ObjectShortcuts<TObject>::createNestedObject(\n    TChar* key) const {\n  return impl()->getOrAddMember(key).template to<ObjectRef>();\n}\ntemplate <typename TObject>\ntemplate <typename TString>\ninline typename enable_if<IsString<TString>::value, bool>::type\nObjectShortcuts<TObject>::containsKey(const TString& key) const {\n  return !impl()->getMember(key).isUndefined();\n}\ntemplate <typename TObject>\ntemplate <typename TChar>\ninline typename enable_if<IsString<TChar*>::value, bool>::type\nObjectShortcuts<TObject>::containsKey(TChar* key) const {\n  return !impl()->getMember(key).isUndefined();\n}\ntemplate <typename TObject>\ntemplate <typename TString>\ninline typename enable_if<IsString<TString*>::value,\n                          MemberProxy<TObject, TString*> >::type\nObjectShortcuts<TObject>::operator[](TString* key) const {\n  return MemberProxy<TObject, TString*>(*impl(), key);\n}\ntemplate <typename TObject>\ntemplate <typename TString>\ninline typename enable_if<IsString<TString>::value,\n                          MemberProxy<TObject, TString> >::type\nObjectShortcuts<TObject>::operator[](const TString& key) const {\n  return MemberProxy<TObject, TString>(*impl(), key);\n}\n}  // namespace ARDUINOJSON_NAMESPACE\n#if ARDUINOJSON_ENABLE_ARDUINO_STRING\n#endif\n#if ARDUINOJSON_ENABLE_STD_STRING\n#endif\nnamespace ARDUINOJSON_NAMESPACE {\ntemplate <typename>\nstruct IsWriteableString : false_type {};\n#if ARDUINOJSON_ENABLE_ARDUINO_STRING\ntemplate <>\nstruct IsWriteableString< ::String> : true_type {};\n#endif\n#if ARDUINOJSON_ENABLE_STD_STRING\ntemplate <typename TCharTraits, typename TAllocator>\nstruct IsWriteableString<std::basic_string<char, TCharTraits, TAllocator> >\n    : true_type {};\n#endif\ntemplate <typename T, typename Enable>\nstruct Converter {\n  static bool toJson(const T& src, VariantRef dst) {\n    return convertToJson(src, dst); // Error here? See https://arduinojson.org/v6/unsupported-set/\n  }\n  static T fromJson(VariantConstRef src) {\n    T result; // Error here? See https://arduinojson.org/v6/non-default-constructible/\n    convertFromJson(src, result);  // Error here? See https://arduinojson.org/v6/unsupported-as/\n    return result;\n  }\n  static bool checkJson(VariantConstRef src) {\n    T dummy;\n    return canConvertFromJson(src, dummy);  // Error here? See https://arduinojson.org/v6/unsupported-is/\n  }\n};\ntemplate <typename T>\nstruct Converter<\n    T, typename enable_if<is_integral<T>::value && !is_same<bool, T>::value &&\n                          !is_same<char, T>::value>::type> {\n  static bool toJson(T src, VariantRef dst) {\n    VariantData* data = getData(dst);\n    ARDUINOJSON_ASSERT_INTEGER_TYPE_IS_SUPPORTED(T);\n    if (!data)\n      return false;\n    data->setInteger(src);\n    return true;\n  }\n  static T fromJson(VariantConstRef src) {\n    ARDUINOJSON_ASSERT_INTEGER_TYPE_IS_SUPPORTED(T);\n    const VariantData* data = getData(src);\n    return data ? data->asIntegral<T>() : T();\n  }\n  static bool checkJson(VariantConstRef src) {\n    const VariantData* data = getData(src);\n    return data && data->isInteger<T>();\n  }\n};\ntemplate <typename T>\nstruct Converter<T, typename enable_if<is_enum<T>::value>::type> {\n  static bool toJson(T src, VariantRef dst) {\n    return dst.set(static_cast<Integer>(src));\n  }\n  static T fromJson(VariantConstRef src) {\n    const VariantData* data = getData(src);\n    return data ? static_cast<T>(data->asIntegral<int>()) : T();\n  }\n  static bool checkJson(VariantConstRef src) {\n    const VariantData* data = getData(src);\n    return data && data->isInteger<int>();\n  }\n};\ntemplate <>\nstruct Converter<bool> {\n  static bool toJson(bool src, VariantRef dst) {\n    VariantData* data = getData(dst);\n    if (!data)\n      return false;\n    data->setBoolean(src);\n    return true;\n  }\n  static bool fromJson(VariantConstRef src) {\n    const VariantData* data = getData(src);\n    return data ? data->asBoolean() : false;\n  }\n  static bool checkJson(VariantConstRef src) {\n    const VariantData* data = getData(src);\n    return data && data->isBoolean();\n  }\n};\ntemplate <typename T>\nstruct Converter<T, typename enable_if<is_floating_point<T>::value>::type> {\n  static bool toJson(T src, VariantRef dst) {\n    VariantData* data = getData(dst);\n    if (!data)\n      return false;\n    data->setFloat(static_cast<Float>(src));\n    return true;\n  }\n  static T fromJson(VariantConstRef src) {\n    const VariantData* data = getData(src);\n    return data ? data->asFloat<T>() : false;\n  }\n  static bool checkJson(VariantConstRef src) {\n    const VariantData* data = getData(src);\n    return data && data->isFloat();\n  }\n};\ntemplate <>\nstruct Converter<const char*> {\n  static bool toJson(const char* src, VariantRef dst) {\n    return variantSetString(getData(dst), adaptString(src), getPool(dst));\n  }\n  static const char* fromJson(VariantConstRef src) {\n    const VariantData* data = getData(src);\n    return data ? data->asString() : 0;\n  }\n  static bool checkJson(VariantConstRef src) {\n    const VariantData* data = getData(src);\n    return data && data->isString();\n  }\n};\ntemplate <typename T>\ninline typename enable_if<IsString<T>::value, bool>::type convertToJson(\n    const T& src, VariantRef dst) {\n  VariantData* data = getData(dst);\n  MemoryPool* pool = getPool(dst);\n  return variantSetString(data, adaptString(src), pool);\n}\ntemplate <typename T>\ninline typename enable_if<IsWriteableString<T>::value>::type convertFromJson(\n    VariantConstRef src, T& dst) {\n  const VariantData* data = getData(src);\n  const char* cstr = data != 0 ? data->asString() : 0;\n  if (cstr)\n    dst = cstr;\n  else\n    serializeJson(src, dst);\n}\ntemplate <typename T>\ninline typename enable_if<IsWriteableString<T>::value, bool>::type\ncanConvertFromJson(VariantConstRef src, const T&) {\n  const VariantData* data = getData(src);\n  return data && data->isString();\n}\ntemplate <>\nstruct Converter<SerializedValue<const char*> > {\n  static bool toJson(SerializedValue<const char*> src, VariantRef dst) {\n    VariantData* data = getData(dst);\n    if (!data)\n      return false;\n    data->setLinkedRaw(src);\n    return true;\n  }\n};\ntemplate <typename T>\nstruct Converter<SerializedValue<T>,\n                 typename enable_if<!is_same<const char*, T>::value>::type> {\n  static bool toJson(SerializedValue<T> src, VariantRef dst) {\n    VariantData* data = getData(dst);\n    MemoryPool* pool = getPool(dst);\n    return data != 0 && data->setOwnedRaw(src, pool);\n  }\n};\n#if ARDUINOJSON_HAS_NULLPTR\ntemplate <>\nstruct Converter<decltype(nullptr)> {\n  static bool toJson(decltype(nullptr), VariantRef dst) {\n    variantSetNull(getData(dst));\n    return true;\n  }\n  static decltype(nullptr) fromJson(VariantConstRef) {\n    return nullptr;\n  }\n  static bool checkJson(VariantConstRef src) {\n    const VariantData* data = getData(src);\n    return data == 0 || data->isNull();\n  }\n};\n#endif\n#if ARDUINOJSON_ENABLE_ARDUINO_STREAM\nclass MemoryPoolPrint : public Print {\n public:\n  MemoryPoolPrint(MemoryPool* pool) : _pool(pool), _size(0) {\n    pool->getFreeZone(&_string, &_capacity);\n  }\n  const char* c_str() {\n    _string[_size++] = 0;\n    ARDUINOJSON_ASSERT(_size <= _capacity);\n    return _pool->saveStringFromFreeZone(_size);\n  }\n  size_t write(uint8_t c) {\n    if (_size >= _capacity)\n      return 0;\n    _string[_size++] = char(c);\n    return 1;\n  }\n  size_t write(const uint8_t* buffer, size_t size) {\n    if (_size + size >= _capacity) {\n      _size = _capacity;  // mark as overflowed\n      return 0;\n    }\n    memcpy(&_string[_size], buffer, size);\n    _size += size;\n    return size;\n  }\n  bool overflowed() const {\n    return _size >= _capacity;\n  }\n private:\n  MemoryPool* _pool;\n  size_t _size;\n  char* _string;\n  size_t _capacity;\n};\ninline bool convertToJson(const ::Printable& src, VariantRef dst) {\n  MemoryPool* pool = getPool(dst);\n  VariantData* data = getData(dst);\n  if (!pool || !data)\n    return false;\n  MemoryPoolPrint print(pool);\n  src.printTo(print);\n  if (print.overflowed()) {\n    pool->markAsOverflowed();\n    data->setNull();\n    return false;\n  }\n  data->setStringPointer(print.c_str(), storage_policies::store_by_copy());\n  return true;\n}\n#endif\nclass CollectionData;\nstruct ComparerBase : Visitor<CompareResult> {};\ntemplate <typename T, typename Enable = void>\nstruct Comparer;\ntemplate <typename T>\nstruct Comparer<T, typename enable_if<IsString<T>::value>::type>\n    : ComparerBase {\n  T rhs;\n  explicit Comparer(T value) : rhs(value) {}\n  CompareResult visitString(const char *lhs) {\n    int i = adaptString(rhs).compare(lhs);\n    if (i < 0)\n      return COMPARE_RESULT_GREATER;\n    else if (i > 0)\n      return COMPARE_RESULT_LESS;\n    else\n      return COMPARE_RESULT_EQUAL;\n  }\n  CompareResult visitNull() {\n    if (adaptString(rhs).isNull())\n      return COMPARE_RESULT_EQUAL;\n    else\n      return COMPARE_RESULT_DIFFER;\n  }\n};\ntemplate <typename T>\nstruct Comparer<T, typename enable_if<is_integral<T>::value ||\n                                      is_floating_point<T>::value>::type>\n    : ComparerBase {\n  T rhs;\n  explicit Comparer(T value) : rhs(value) {}\n  CompareResult visitFloat(Float lhs) {\n    return arithmeticCompare(lhs, rhs);\n  }\n  CompareResult visitSignedInteger(Integer lhs) {\n    return arithmeticCompare(lhs, rhs);\n  }\n  CompareResult visitUnsignedInteger(UInt lhs) {\n    return arithmeticCompare(lhs, rhs);\n  }\n  CompareResult visitBoolean(bool lhs) {\n    return visitUnsignedInteger(static_cast<UInt>(lhs));\n  }\n};\nstruct NullComparer : ComparerBase {\n  CompareResult visitNull() {\n    return COMPARE_RESULT_EQUAL;\n  }\n};\n#if ARDUINOJSON_HAS_NULLPTR\ntemplate <>\nstruct Comparer<decltype(nullptr), void> : NullComparer {\n  explicit Comparer(decltype(nullptr)) : NullComparer() {}\n};\n#endif\nstruct ArrayComparer : ComparerBase {\n  const CollectionData *_rhs;\n  explicit ArrayComparer(const CollectionData &rhs) : _rhs(&rhs) {}\n  CompareResult visitArray(const CollectionData &lhs) {\n    if (lhs.equalsArray(*_rhs))\n      return COMPARE_RESULT_EQUAL;\n    else\n      return COMPARE_RESULT_DIFFER;\n  }\n};\nstruct ObjectComparer : ComparerBase {\n  const CollectionData *_rhs;\n  explicit ObjectComparer(const CollectionData &rhs) : _rhs(&rhs) {}\n  CompareResult visitObject(const CollectionData &lhs) {\n    if (lhs.equalsObject(*_rhs))\n      return COMPARE_RESULT_EQUAL;\n    else\n      return COMPARE_RESULT_DIFFER;\n  }\n};\nstruct RawComparer : ComparerBase {\n  const char *_rhsData;\n  size_t _rhsSize;\n  explicit RawComparer(const char *rhsData, size_t rhsSize)\n      : _rhsData(rhsData), _rhsSize(rhsSize) {}\n  CompareResult visitRawJson(const char *lhsData, size_t lhsSize) {\n    size_t size = _rhsSize < lhsSize ? _rhsSize : lhsSize;\n    int n = memcmp(lhsData, _rhsData, size);\n    if (n < 0)\n      return COMPARE_RESULT_LESS;\n    else if (n > 0)\n      return COMPARE_RESULT_GREATER;\n    else\n      return COMPARE_RESULT_EQUAL;\n  }\n};\ntemplate <typename T>\nstruct Comparer<T, typename enable_if<IsVisitable<T>::value>::type>\n    : ComparerBase {\n  T rhs;\n  explicit Comparer(T value) : rhs(value) {}\n  CompareResult visitArray(const CollectionData &lhs) {\n    ArrayComparer comparer(lhs);\n    return accept(comparer);\n  }\n  CompareResult visitObject(const CollectionData &lhs) {\n    ObjectComparer comparer(lhs);\n    return accept(comparer);\n  }\n  CompareResult visitFloat(Float lhs) {\n    Comparer<Float> comparer(lhs);\n    return accept(comparer);\n  }\n  CompareResult visitString(const char *lhs) {\n    Comparer<const char *> comparer(lhs);\n    return accept(comparer);\n  }\n  CompareResult visitRawJson(const char *lhsData, size_t lhsSize) {\n    RawComparer comparer(lhsData, lhsSize);\n    return accept(comparer);\n  }\n  CompareResult visitSignedInteger(Integer lhs) {\n    Comparer<Integer> comparer(lhs);\n    return accept(comparer);\n  }\n  CompareResult visitUnsignedInteger(UInt lhs) {\n    Comparer<UInt> comparer(lhs);\n    return accept(comparer);\n  }\n  CompareResult visitBoolean(bool lhs) {\n    Comparer<bool> comparer(lhs);\n    return accept(comparer);\n  }\n  CompareResult visitNull() {\n    NullComparer comparer;\n    return accept(comparer);\n  }\n private:\n  template <typename TComparer>\n  CompareResult accept(TComparer &comparer) {\n    CompareResult reversedResult = rhs.accept(comparer);\n    switch (reversedResult) {\n      case COMPARE_RESULT_GREATER:\n        return COMPARE_RESULT_LESS;\n      case COMPARE_RESULT_LESS:\n        return COMPARE_RESULT_GREATER;\n      default:\n        return reversedResult;\n    }\n  }\n};\ntemplate <typename T1, typename T2>\nCompareResult compare(const T1 &lhs, const T2 &rhs) {\n  Comparer<T2> comparer(rhs);\n  return lhs.accept(comparer);\n}\ninline int variantCompare(const VariantData *a, const VariantData *b) {\n  return compare(VariantConstRef(a), VariantConstRef(b));\n}\n#ifndef isnan\ntemplate <typename T>\nbool isnan(T x) {\n  return x != x;\n}\n#endif\n#ifndef isinf\ntemplate <typename T>\nbool isinf(T x) {\n  return x != 0.0 && x * 2 == x;\n}\n#endif\ntemplate <typename T, typename F>\nstruct alias_cast_t {\n  union {\n    F raw;\n    T data;\n  };\n};\ntemplate <typename T, typename F>\nT alias_cast(F raw_data) {\n  alias_cast_t<T, F> ac;\n  ac.raw = raw_data;\n  return ac.data;\n}\n}  // namespace ARDUINOJSON_NAMESPACE\n#if ARDUINOJSON_ENABLE_PROGMEM\nnamespace ARDUINOJSON_NAMESPACE {\ntemplate <typename T>\ntypename enable_if<is_pointer<T>::value, T>::type pgm_read(const void* p) {\n  return reinterpret_cast<T>(pgm_read_ptr(p));\n}\ntemplate <typename T>\ntypename enable_if<is_floating_point<T>::value &&\n                       sizeof(T) == sizeof(float),  // on AVR sizeof(double) ==\n                   T>::type\npgm_read(const void* p) {\n  return pgm_read_float(p);\n}\ntemplate <typename T>\ntypename enable_if<is_same<T, uint32_t>::value, T>::type pgm_read(\n    const void* p) {\n  return pgm_read_dword(p);\n}\n}  // namespace ARDUINOJSON_NAMESPACE\n#  ifndef ARDUINOJSON_DEFINE_STATIC_ARRAY\n#    define ARDUINOJSON_DEFINE_STATIC_ARRAY(type, name, value) \\\n      static type const name[] PROGMEM = value;\n#  endif\n#  ifndef ARDUINOJSON_READ_STATIC_ARRAY\n#    define ARDUINOJSON_READ_STATIC_ARRAY(type, name, index) \\\n      pgm_read<type>(name + index)\n#  endif\n#else  // i.e. ARDUINOJSON_ENABLE_PROGMEM == 0\n#  ifndef ARDUINOJSON_DEFINE_STATIC_ARRAY\n#    define ARDUINOJSON_DEFINE_STATIC_ARRAY(type, name, value) \\\n      static type const name[] = value;\n#  endif\n#  ifndef ARDUINOJSON_READ_STATIC_ARRAY\n#    define ARDUINOJSON_READ_STATIC_ARRAY(type, name, index) name[index]\n#  endif\n#endif\nnamespace ARDUINOJSON_NAMESPACE {\ntemplate <typename T, size_t = sizeof(T)>\nstruct FloatTraits {};\ntemplate <typename T>\nstruct FloatTraits<T, 8 /*64bits*/> {\n  typedef uint64_t mantissa_type;\n  static const short mantissa_bits = 52;\n  static const mantissa_type mantissa_max =\n      (mantissa_type(1) << mantissa_bits) - 1;\n  typedef int16_t exponent_type;\n  static const exponent_type exponent_max = 308;\n  template <typename TExponent>\n  static T make_float(T m, TExponent e) {\n    if (e > 0) {\n      for (uint8_t index = 0; e != 0; index++) {\n        if (e & 1)\n          m *= positiveBinaryPowerOfTen(index);\n        e >>= 1;\n      }\n    } else {\n      e = TExponent(-e);\n      for (uint8_t index = 0; e != 0; index++) {\n        if (e & 1)\n          m *= negativeBinaryPowerOfTen(index);\n        e >>= 1;\n      }\n    }\n    return m;\n  }\n  static T positiveBinaryPowerOfTen(int index) {\n    ARDUINOJSON_DEFINE_STATIC_ARRAY(  //\n        uint32_t, factors,\n        ARDUINOJSON_EXPAND18({\n            0x40240000, 0x00000000,  // 1e1\n            0x40590000, 0x00000000,  // 1e2\n            0x40C38800, 0x00000000,  // 1e4\n            0x4197D784, 0x00000000,  // 1e8\n            0x4341C379, 0x37E08000,  // 1e16\n            0x4693B8B5, 0xB5056E17,  // 1e32\n            0x4D384F03, 0xE93FF9F5,  // 1e64\n            0x5A827748, 0xF9301D32,  // 1e128\n            0x75154FDD, 0x7F73BF3C   // 1e256\n        }));\n    return forge(\n        ARDUINOJSON_READ_STATIC_ARRAY(uint32_t, factors, 2 * index),\n        ARDUINOJSON_READ_STATIC_ARRAY(uint32_t, factors, 2 * index + 1));\n  }\n  static T negativeBinaryPowerOfTen(int index) {\n    ARDUINOJSON_DEFINE_STATIC_ARRAY(  //\n        uint32_t, factors,\n        ARDUINOJSON_EXPAND18({\n            0x3FB99999, 0x9999999A,  // 1e-1\n            0x3F847AE1, 0x47AE147B,  // 1e-2\n            0x3F1A36E2, 0xEB1C432D,  // 1e-4\n            0x3E45798E, 0xE2308C3A,  // 1e-8\n            0x3C9CD2B2, 0x97D889BC,  // 1e-16\n            0x3949F623, 0xD5A8A733,  // 1e-32\n            0x32A50FFD, 0x44F4A73D,  // 1e-64\n            0x255BBA08, 0xCF8C979D,  // 1e-128\n            0x0AC80628, 0x64AC6F43   // 1e-256\n        }));\n    return forge(\n        ARDUINOJSON_READ_STATIC_ARRAY(uint32_t, factors, 2 * index),\n        ARDUINOJSON_READ_STATIC_ARRAY(uint32_t, factors, 2 * index + 1));\n  }\n  static T negativeBinaryPowerOfTenPlusOne(int index) {\n    ARDUINOJSON_DEFINE_STATIC_ARRAY(  //\n        uint32_t, factors,\n        ARDUINOJSON_EXPAND18({\n            0x3FF00000, 0x00000000,  // 1e0\n            0x3FB99999, 0x9999999A,  // 1e-1\n            0x3F50624D, 0xD2F1A9FC,  // 1e-3\n            0x3E7AD7F2, 0x9ABCAF48,  // 1e-7\n            0x3CD203AF, 0x9EE75616,  // 1e-15\n            0x398039D6, 0x65896880,  // 1e-31\n            0x32DA53FC, 0x9631D10D,  // 1e-63\n            0x25915445, 0x81B7DEC2,  // 1e-127\n            0x0AFE07B2, 0x7DD78B14   // 1e-255\n        }));\n    return forge(\n        ARDUINOJSON_READ_STATIC_ARRAY(uint32_t, factors, 2 * index),\n        ARDUINOJSON_READ_STATIC_ARRAY(uint32_t, factors, 2 * index + 1));\n  }\n  static T nan() {\n    return forge(0x7ff80000, 0x00000000);\n  }\n  static T inf() {\n    return forge(0x7ff00000, 0x00000000);\n  }\n  static T highest() {\n    return forge(0x7FEFFFFF, 0xFFFFFFFF);\n  }\n  static T lowest() {\n    return forge(0xFFEFFFFF, 0xFFFFFFFF);\n  }\n  static T forge(uint32_t msb, uint32_t lsb) {\n    return alias_cast<T>((uint64_t(msb) << 32) | lsb);\n  }\n};\ntemplate <typename T>\nstruct FloatTraits<T, 4 /*32bits*/> {\n  typedef uint32_t mantissa_type;\n  static const short mantissa_bits = 23;\n  static const mantissa_type mantissa_max =\n      (mantissa_type(1) << mantissa_bits) - 1;\n  typedef int8_t exponent_type;\n  static const exponent_type exponent_max = 38;\n  template <typename TExponent>\n  static T make_float(T m, TExponent e) {\n    if (e > 0) {\n      for (uint8_t index = 0; e != 0; index++) {\n        if (e & 1)\n          m *= positiveBinaryPowerOfTen(index);\n        e >>= 1;\n      }\n    } else {\n      e = -e;\n      for (uint8_t index = 0; e != 0; index++) {\n        if (e & 1)\n          m *= negativeBinaryPowerOfTen(index);\n        e >>= 1;\n      }\n    }\n    return m;\n  }\n  static T positiveBinaryPowerOfTen(int index) {\n    ARDUINOJSON_DEFINE_STATIC_ARRAY(\n        T, factors,\n        ARDUINOJSON_EXPAND6({1e1f, 1e2f, 1e4f, 1e8f, 1e16f, 1e32f}));\n    return ARDUINOJSON_READ_STATIC_ARRAY(T, factors, index);\n  }\n  static T negativeBinaryPowerOfTen(int index) {\n    ARDUINOJSON_DEFINE_STATIC_ARRAY(\n        T, factors,\n        ARDUINOJSON_EXPAND6({1e-1f, 1e-2f, 1e-4f, 1e-8f, 1e-16f, 1e-32f}));\n    return ARDUINOJSON_READ_STATIC_ARRAY(T, factors, index);\n  }\n  static T negativeBinaryPowerOfTenPlusOne(int index) {\n    ARDUINOJSON_DEFINE_STATIC_ARRAY(\n        T, factors,\n        ARDUINOJSON_EXPAND6({1e0f, 1e-1f, 1e-3f, 1e-7f, 1e-15f, 1e-31f}));\n    return ARDUINOJSON_READ_STATIC_ARRAY(T, factors, index);\n  }\n  static T forge(uint32_t bits) {\n    return alias_cast<T>(bits);\n  }\n  static T nan() {\n    return forge(0x7fc00000);\n  }\n  static T inf() {\n    return forge(0x7f800000);\n  }\n  static T highest() {\n    return forge(0x7f7fffff);\n  }\n  static T lowest() {\n    return forge(0xFf7fffff);\n  }\n};\n#ifndef isdigit\ninline bool isdigit(char c) {\n  return '0' <= c && c <= '9';\n}\n#endif\ninline bool issign(char c) {\n  return '-' == c || c == '+';\n}\ntemplate <typename A, typename B>\nstruct choose_largest : conditional<(sizeof(A) > sizeof(B)), A, B> {};\ninline bool parseNumber(const char* s, VariantData& result) {\n  typedef FloatTraits<Float> traits;\n  typedef choose_largest<traits::mantissa_type, UInt>::type mantissa_t;\n  typedef traits::exponent_type exponent_t;\n  ARDUINOJSON_ASSERT(s != 0);\n  bool is_negative = false;\n  switch (*s) {\n    case '-':\n      is_negative = true;\n      s++;\n      break;\n    case '+':\n      s++;\n      break;\n  }\n#if ARDUINOJSON_ENABLE_NAN\n  if (*s == 'n' || *s == 'N') {\n    result.setFloat(traits::nan());\n    return true;\n  }\n#endif\n#if ARDUINOJSON_ENABLE_INFINITY\n  if (*s == 'i' || *s == 'I') {\n    result.setFloat(is_negative ? -traits::inf() : traits::inf());\n    return true;\n  }\n#endif\n  if (!isdigit(*s) && *s != '.')\n    return false;\n  mantissa_t mantissa = 0;\n  exponent_t exponent_offset = 0;\n  const mantissa_t maxUint = UInt(-1);\n  while (isdigit(*s)) {\n    uint8_t digit = uint8_t(*s - '0');\n    if (mantissa > maxUint / 10)\n      break;\n    mantissa *= 10;\n    if (mantissa > maxUint - digit)\n      break;\n    mantissa += digit;\n    s++;\n  }\n  if (*s == '\\0') {\n    if (is_negative) {\n      const mantissa_t sintMantissaMax = mantissa_t(1)\n                                         << (sizeof(Integer) * 8 - 1);\n      if (mantissa <= sintMantissaMax) {\n        result.setInteger(Integer(~mantissa + 1));\n        return true;\n      }\n    } else {\n      result.setInteger(UInt(mantissa));\n      return true;\n    }\n  }\n  while (mantissa > traits::mantissa_max) {\n    mantissa /= 10;\n    exponent_offset++;\n  }\n  while (isdigit(*s)) {\n    exponent_offset++;\n    s++;\n  }\n  if (*s == '.') {\n    s++;\n    while (isdigit(*s)) {\n      if (mantissa < traits::mantissa_max / 10) {\n        mantissa = mantissa * 10 + uint8_t(*s - '0');\n        exponent_offset--;\n      }\n      s++;\n    }\n  }\n  int exponent = 0;\n  if (*s == 'e' || *s == 'E') {\n    s++;\n    bool negative_exponent = false;\n    if (*s == '-') {\n      negative_exponent = true;\n      s++;\n    } else if (*s == '+') {\n      s++;\n    }\n    while (isdigit(*s)) {\n      exponent = exponent * 10 + (*s - '0');\n      if (exponent + exponent_offset > traits::exponent_max) {\n        if (negative_exponent)\n          result.setFloat(is_negative ? -0.0f : 0.0f);\n        else\n          result.setFloat(is_negative ? -traits::inf() : traits::inf());\n        return true;\n      }\n      s++;\n    }\n    if (negative_exponent)\n      exponent = -exponent;\n  }\n  exponent += exponent_offset;\n  if (*s != '\\0')\n    return false;\n  Float final_result =\n      traits::make_float(static_cast<Float>(mantissa), exponent);\n  result.setFloat(is_negative ? -final_result : final_result);\n  return true;\n}\ntemplate <typename T>\ninline T parseNumber(const char* s) {\n  VariantData value;\n  value.init();  // VariantData is a POD, so it has no constructor\n  parseNumber(s, value);\n  return Converter<T>::fromJson(VariantConstRef(&value));\n}\ntemplate <typename T>\ninline T VariantData::asIntegral() const {\n  switch (type()) {\n    case VALUE_IS_BOOLEAN:\n      return _content.asBoolean;\n    case VALUE_IS_UNSIGNED_INTEGER:\n      return convertNumber<T>(_content.asUnsignedInteger);\n    case VALUE_IS_SIGNED_INTEGER:\n      return convertNumber<T>(_content.asSignedInteger);\n    case VALUE_IS_LINKED_STRING:\n    case VALUE_IS_OWNED_STRING:\n      return parseNumber<T>(_content.asString);\n    case VALUE_IS_FLOAT:\n      return convertNumber<T>(_content.asFloat);\n    default:\n      return 0;\n  }\n}\ninline bool VariantData::asBoolean() const {\n  switch (type()) {\n    case VALUE_IS_BOOLEAN:\n      return _content.asBoolean;\n    case VALUE_IS_SIGNED_INTEGER:\n    case VALUE_IS_UNSIGNED_INTEGER:\n      return _content.asUnsignedInteger != 0;\n    case VALUE_IS_FLOAT:\n      return _content.asFloat != 0;\n    case VALUE_IS_NULL:\n      return false;\n    default:\n      return true;\n  }\n}\ntemplate <typename T>\ninline T VariantData::asFloat() const {\n  switch (type()) {\n    case VALUE_IS_BOOLEAN:\n      return static_cast<T>(_content.asBoolean);\n    case VALUE_IS_UNSIGNED_INTEGER:\n      return static_cast<T>(_content.asUnsignedInteger);\n    case VALUE_IS_SIGNED_INTEGER:\n      return static_cast<T>(_content.asSignedInteger);\n    case VALUE_IS_LINKED_STRING:\n    case VALUE_IS_OWNED_STRING:\n      return parseNumber<T>(_content.asString);\n    case VALUE_IS_FLOAT:\n      return static_cast<T>(_content.asFloat);\n    default:\n      return 0;\n  }\n}\ninline const char *VariantData::asString() const {\n  switch (type()) {\n    case VALUE_IS_LINKED_STRING:\n    case VALUE_IS_OWNED_STRING:\n      return _content.asString;\n    default:\n      return 0;\n  }\n}\ntemplate <typename T>\ninline typename enable_if<is_same<T, ArrayRef>::value, ArrayRef>::type\nVariantRef::to() const {\n  return ArrayRef(_pool, variantToArray(_data));\n}\ntemplate <typename T>\ntypename enable_if<is_same<T, ObjectRef>::value, ObjectRef>::type\nVariantRef::to() const {\n  return ObjectRef(_pool, variantToObject(_data));\n}\ntemplate <typename T>\ntypename enable_if<is_same<T, VariantRef>::value, VariantRef>::type\nVariantRef::to() const {\n  variantSetNull(_data);\n  return *this;\n}\ninline VariantConstRef VariantConstRef::getElement(size_t index) const {\n  return ArrayConstRef(_data != 0 ? _data->asArray() : 0)[index];\n}\ninline VariantRef VariantRef::addElement() const {\n  return VariantRef(_pool, variantAddElement(_data, _pool));\n}\ninline VariantRef VariantRef::getElement(size_t index) const {\n  return VariantRef(_pool, _data != 0 ? _data->getElement(index) : 0);\n}\ninline VariantRef VariantRef::getOrAddElement(size_t index) const {\n  return VariantRef(_pool, variantGetOrAddElement(_data, index, _pool));\n}\ntemplate <typename TChar>\ninline VariantRef VariantRef::getMember(TChar *key) const {\n  return VariantRef(_pool, _data != 0 ? _data->getMember(adaptString(key)) : 0);\n}\ntemplate <typename TString>\ninline typename enable_if<IsString<TString>::value, VariantRef>::type\nVariantRef::getMember(const TString &key) const {\n  return VariantRef(_pool, _data != 0 ? _data->getMember(adaptString(key)) : 0);\n}\ntemplate <typename TChar>\ninline VariantRef VariantRef::getOrAddMember(TChar *key) const {\n  return VariantRef(_pool, variantGetOrAddMember(_data, key, _pool));\n}\ntemplate <typename TString>\ninline VariantRef VariantRef::getOrAddMember(const TString &key) const {\n  return VariantRef(_pool, variantGetOrAddMember(_data, key, _pool));\n}\ninline VariantConstRef operator|(VariantConstRef preferedValue,\n                                 VariantConstRef defaultValue) {\n  return preferedValue ? preferedValue : defaultValue;\n}\ninline bool VariantRef::set(char value) const {\n  return set<signed char>(value);\n}\n}  // namespace ARDUINOJSON_NAMESPACE\n#if ARDUINOJSON_ENABLE_STD_STREAM\n#include <ostream>\n#endif\nnamespace ARDUINOJSON_NAMESPACE {\nclass DeserializationError {\n  typedef void (DeserializationError::*bool_type)() const;\n  void safeBoolHelper() const {}\n public:\n  enum Code {\n    Ok,\n    EmptyInput,\n    IncompleteInput,\n    InvalidInput,\n    NoMemory,\n    TooDeep\n  };\n  DeserializationError() {}\n  DeserializationError(Code c) : _code(c) {}\n  friend bool operator==(const DeserializationError& lhs,\n                         const DeserializationError& rhs) {\n    return lhs._code == rhs._code;\n  }\n  friend bool operator!=(const DeserializationError& lhs,\n                         const DeserializationError& rhs) {\n    return lhs._code != rhs._code;\n  }\n  friend bool operator==(const DeserializationError& lhs, Code rhs) {\n    return lhs._code == rhs;\n  }\n  friend bool operator==(Code lhs, const DeserializationError& rhs) {\n    return lhs == rhs._code;\n  }\n  friend bool operator!=(const DeserializationError& lhs, Code rhs) {\n    return lhs._code != rhs;\n  }\n  friend bool operator!=(Code lhs, const DeserializationError& rhs) {\n    return lhs != rhs._code;\n  }\n  operator bool_type() const {\n    return _code != Ok ? &DeserializationError::safeBoolHelper : 0;\n  }\n  friend bool operator==(bool value, const DeserializationError& err) {\n    return static_cast<bool>(err) == value;\n  }\n  friend bool operator==(const DeserializationError& err, bool value) {\n    return static_cast<bool>(err) == value;\n  }\n  friend bool operator!=(bool value, const DeserializationError& err) {\n    return static_cast<bool>(err) != value;\n  }\n  friend bool operator!=(const DeserializationError& err, bool value) {\n    return static_cast<bool>(err) != value;\n  }\n  Code code() const {\n    return _code;\n  }\n  const char* c_str() const {\n    static const char* messages[] = {\n        \"Ok\",           \"EmptyInput\", \"IncompleteInput\",\n        \"InvalidInput\", \"NoMemory\",   \"TooDeep\"};\n    ARDUINOJSON_ASSERT(static_cast<size_t>(_code) <\n                       sizeof(messages) / sizeof(messages[0]));\n    return messages[_code];\n  }\n#if ARDUINOJSON_ENABLE_PROGMEM\n  const __FlashStringHelper* f_str() const {\n    ARDUINOJSON_DEFINE_STATIC_ARRAY(char, s0, \"Ok\");\n    ARDUINOJSON_DEFINE_STATIC_ARRAY(char, s1, \"EmptyInput\");\n    ARDUINOJSON_DEFINE_STATIC_ARRAY(char, s2, \"IncompleteInput\");\n    ARDUINOJSON_DEFINE_STATIC_ARRAY(char, s3, \"InvalidInput\");\n    ARDUINOJSON_DEFINE_STATIC_ARRAY(char, s4, \"NoMemory\");\n    ARDUINOJSON_DEFINE_STATIC_ARRAY(char, s5, \"TooDeep\");\n    ARDUINOJSON_DEFINE_STATIC_ARRAY(\n        const char*, messages, ARDUINOJSON_EXPAND6({s0, s1, s2, s3, s4, s5}));\n    return ARDUINOJSON_READ_STATIC_ARRAY(const __FlashStringHelper*, messages,\n                                         _code);\n  }\n#endif\n private:\n  Code _code;\n};\n#if ARDUINOJSON_ENABLE_STD_STREAM\ninline std::ostream& operator<<(std::ostream& s,\n                                const DeserializationError& e) {\n  s << e.c_str();\n  return s;\n}\ninline std::ostream& operator<<(std::ostream& s, DeserializationError::Code c) {\n  s << DeserializationError(c).c_str();\n  return s;\n}\n#endif\nclass Filter {\n public:\n  explicit Filter(VariantConstRef v) : _variant(v) {}\n  bool allow() const {\n    return _variant;\n  }\n  bool allowArray() const {\n    return _variant == true || _variant.is<ArrayConstRef>();\n  }\n  bool allowObject() const {\n    return _variant == true || _variant.is<ObjectConstRef>();\n  }\n  bool allowValue() const {\n    return _variant == true;\n  }\n  template <typename TKey>\n  Filter operator[](const TKey& key) const {\n    if (_variant == true)  // \"true\" means \"allow recursively\"\n      return *this;\n    else\n      return Filter(_variant[key] | _variant[\"*\"]);\n  }\n private:\n  VariantConstRef _variant;\n};\nstruct AllowAllFilter {\n  bool allow() const {\n    return true;\n  }\n  bool allowArray() const {\n    return true;\n  }\n  bool allowObject() const {\n    return true;\n  }\n  bool allowValue() const {\n    return true;\n  }\n  template <typename TKey>\n  AllowAllFilter operator[](const TKey&) const {\n    return AllowAllFilter();\n  }\n};\nclass NestingLimit {\n public:\n  NestingLimit() : _value(ARDUINOJSON_DEFAULT_NESTING_LIMIT) {}\n  explicit NestingLimit(uint8_t n) : _value(n) {}\n  NestingLimit decrement() const {\n    ARDUINOJSON_ASSERT(_value > 0);\n    return NestingLimit(static_cast<uint8_t>(_value - 1));\n  }\n  bool reached() const {\n    return _value == 0;\n  }\n private:\n  uint8_t _value;\n};\ntemplate <typename TSource, typename Enable = void>\nstruct Reader {\n public:\n  Reader(TSource& source) : _source(&source) {}\n  int read() {\n    return _source->read();\n  }\n  size_t readBytes(char* buffer, size_t length) {\n    return _source->readBytes(buffer, length);\n  }\n private:\n  TSource* _source;\n};\ntemplate <typename TSource, typename Enable = void>\nstruct BoundedReader {\n};\ntemplate <typename TIterator>\nclass IteratorReader {\n  TIterator _ptr, _end;\n public:\n  explicit IteratorReader(TIterator begin, TIterator end)\n      : _ptr(begin), _end(end) {}\n  int read() {\n    if (_ptr < _end)\n      return static_cast<unsigned char>(*_ptr++);\n    else\n      return -1;\n  }\n  size_t readBytes(char* buffer, size_t length) {\n    size_t i = 0;\n    while (i < length && _ptr < _end) buffer[i++] = *_ptr++;\n    return i;\n  }\n};\ntemplate <typename T>\nstruct void_ {\n  typedef void type;\n};\ntemplate <typename TSource>\nstruct Reader<TSource, typename void_<typename TSource::const_iterator>::type>\n    : IteratorReader<typename TSource::const_iterator> {\n  explicit Reader(const TSource& source)\n      : IteratorReader<typename TSource::const_iterator>(source.begin(),\n                                                         source.end()) {}\n};\ntemplate <typename T>\nstruct IsCharOrVoid {\n  static const bool value =\n      is_same<T, void>::value || is_same<T, char>::value ||\n      is_same<T, unsigned char>::value || is_same<T, signed char>::value;\n};\ntemplate <typename T>\nstruct IsCharOrVoid<const T> : IsCharOrVoid<T> {};\ntemplate <typename TSource>\nstruct Reader<TSource*,\n              typename enable_if<IsCharOrVoid<TSource>::value>::type> {\n  const char* _ptr;\n public:\n  explicit Reader(const void* ptr)\n      : _ptr(ptr ? reinterpret_cast<const char*>(ptr) : \"\") {}\n  int read() {\n    return static_cast<unsigned char>(*_ptr++);\n  }\n  size_t readBytes(char* buffer, size_t length) {\n    for (size_t i = 0; i < length; i++) buffer[i] = *_ptr++;\n    return length;\n  }\n};\ntemplate <typename TSource>\nstruct BoundedReader<TSource*,\n                     typename enable_if<IsCharOrVoid<TSource>::value>::type>\n    : public IteratorReader<const char*> {\n public:\n  explicit BoundedReader(const void* ptr, size_t len)\n      : IteratorReader<const char*>(reinterpret_cast<const char*>(ptr),\n                                    reinterpret_cast<const char*>(ptr) + len) {}\n};\ntemplate <typename TArray>\nstruct Reader<ElementProxy<TArray>, void> : Reader<char*, void> {\n  explicit Reader(const ElementProxy<TArray>& x)\n      : Reader<char*, void>(x.template as<const char*>()) {}\n};\ntemplate <typename TObject, typename TStringRef>\nstruct Reader<MemberProxy<TObject, TStringRef>, void> : Reader<char*, void> {\n  explicit Reader(const MemberProxy<TObject, TStringRef>& x)\n      : Reader<char*, void>(x.template as<const char*>()) {}\n};\ntemplate <>\nstruct Reader<VariantRef, void> : Reader<char*, void> {\n  explicit Reader(VariantRef x) : Reader<char*, void>(x.as<const char*>()) {}\n};\ntemplate <>\nstruct Reader<VariantConstRef, void> : Reader<char*, void> {\n  explicit Reader(VariantConstRef x)\n      : Reader<char*, void>(x.as<const char*>()) {}\n};\n}  // namespace ARDUINOJSON_NAMESPACE\n#if ARDUINOJSON_ENABLE_ARDUINO_STREAM\nnamespace ARDUINOJSON_NAMESPACE {\ntemplate <typename TSource>\nstruct Reader<TSource,\n              typename enable_if<is_base_of<Stream, TSource>::value>::type> {\n public:\n  explicit Reader(Stream& stream) : _stream(&stream) {}\n  int read() {\n    char c;\n    return _stream->readBytes(&c, 1) ? static_cast<unsigned char>(c) : -1;\n  }\n  size_t readBytes(char* buffer, size_t length) {\n    return _stream->readBytes(buffer, length);\n  }\n private:\n  Stream* _stream;\n};\n}  // namespace ARDUINOJSON_NAMESPACE\n#endif\n#if ARDUINOJSON_ENABLE_ARDUINO_STRING\nnamespace ARDUINOJSON_NAMESPACE {\ntemplate <typename TSource>\nstruct Reader<TSource,\n              typename enable_if<is_base_of< ::String, TSource>::value>::type>\n    : BoundedReader<const char*> {\n  explicit Reader(const ::String& s)\n      : BoundedReader<const char*>(s.c_str(), s.length()) {}\n};\n}  // namespace ARDUINOJSON_NAMESPACE\n#endif\n#if ARDUINOJSON_ENABLE_PROGMEM\nnamespace ARDUINOJSON_NAMESPACE {\ntemplate <>\nstruct Reader<const __FlashStringHelper*, void> {\n  const char* _ptr;\n public:\n  explicit Reader(const __FlashStringHelper* ptr)\n      : _ptr(reinterpret_cast<const char*>(ptr)) {}\n  int read() {\n    return pgm_read_byte(_ptr++);\n  }\n  size_t readBytes(char* buffer, size_t length) {\n    memcpy_P(buffer, _ptr, length);\n    _ptr += length;\n    return length;\n  }\n};\ntemplate <>\nstruct BoundedReader<const __FlashStringHelper*, void> {\n  const char* _ptr;\n  const char* _end;\n public:\n  explicit BoundedReader(const __FlashStringHelper* ptr, size_t size)\n      : _ptr(reinterpret_cast<const char*>(ptr)), _end(_ptr + size) {}\n  int read() {\n    if (_ptr < _end)\n      return pgm_read_byte(_ptr++);\n    else\n      return -1;\n  }\n  size_t readBytes(char* buffer, size_t length) {\n    size_t available = static_cast<size_t>(_end - _ptr);\n    if (available < length)\n      length = available;\n    memcpy_P(buffer, _ptr, length);\n    _ptr += length;\n    return length;\n  }\n};\n}  // namespace ARDUINOJSON_NAMESPACE\n#endif\n#if ARDUINOJSON_ENABLE_STD_STREAM\n#include <istream>\nnamespace ARDUINOJSON_NAMESPACE {\ntemplate <typename TSource>\nstruct Reader<TSource, typename enable_if<\n                           is_base_of<std::istream, TSource>::value>::type> {\n public:\n  explicit Reader(std::istream& stream) : _stream(&stream) {}\n  int read() {\n    return _stream->get();\n  }\n  size_t readBytes(char* buffer, size_t length) {\n    _stream->read(buffer, static_cast<std::streamsize>(length));\n    return static_cast<size_t>(_stream->gcount());\n  }\n private:\n  std::istream* _stream;\n};\n}  // namespace ARDUINOJSON_NAMESPACE\n#endif\nnamespace ARDUINOJSON_NAMESPACE {\nclass StringCopier {\n public:\n  StringCopier(MemoryPool& pool) : _pool(&pool) {}\n  void startString() {\n    _pool->getFreeZone(&_ptr, &_capacity);\n    _size = 0;\n  }\n  const char* save() {\n    ARDUINOJSON_ASSERT(_ptr);\n    return _pool->saveStringFromFreeZone(_size);\n  }\n  void append(const char* s) {\n    while (*s) append(*s++);\n  }\n  void append(const char* s, size_t n) {\n    while (n-- > 0) append(*s++);\n  }\n  void append(char c) {\n    if (!_ptr)\n      return;\n    if (_size >= _capacity) {\n      _ptr = 0;\n      _pool->markAsOverflowed();\n      return;\n    }\n    _ptr[_size++] = c;\n  }\n  bool isValid() {\n    return _ptr != 0;\n  }\n  const char* c_str() {\n    return _ptr;\n  }\n  typedef storage_policies::store_by_copy storage_policy;\n private:\n  MemoryPool* _pool;\n  char* _ptr;\n  size_t _size, _capacity;\n};\nclass StringMover {\n public:\n  StringMover(char* ptr) : _writePtr(ptr) {}\n  void startString() {\n    _startPtr = _writePtr;\n  }\n  const char* save() const {\n    return _startPtr;\n  }\n  void append(char c) {\n    *_writePtr++ = c;\n  }\n  bool isValid() const {\n    return true;\n  }\n  const char* c_str() const {\n    return _startPtr;\n  }\n  typedef storage_policies::store_by_address storage_policy;\n private:\n  char* _writePtr;\n  char* _startPtr;\n};\ntemplate <typename TInput>\nStringCopier makeStringStorage(TInput&, MemoryPool& pool) {\n  return StringCopier(pool);\n}\ntemplate <typename TChar>\nStringMover makeStringStorage(\n    TChar* input, MemoryPool&,\n    typename enable_if<!is_const<TChar>::value>::type* = 0) {\n  return StringMover(reinterpret_cast<char*>(input));\n}\ntemplate <template <typename, typename> class TDeserializer, typename TReader,\n          typename TWriter>\nTDeserializer<TReader, TWriter> makeDeserializer(MemoryPool &pool,\n                                                 TReader reader,\n                                                 TWriter writer) {\n  return TDeserializer<TReader, TWriter>(pool, reader, writer);\n}\ntemplate <template <typename, typename> class TDeserializer, typename TString,\n          typename TFilter>\ntypename enable_if<!is_array<TString>::value, DeserializationError>::type\ndeserialize(JsonDocument &doc, const TString &input, NestingLimit nestingLimit,\n            TFilter filter) {\n  Reader<TString> reader(input);\n  doc.clear();\n  return makeDeserializer<TDeserializer>(\n             doc.memoryPool(), reader,\n             makeStringStorage(input, doc.memoryPool()))\n      .parse(doc.data(), filter, nestingLimit);\n}\ntemplate <template <typename, typename> class TDeserializer, typename TChar,\n          typename TFilter>\nDeserializationError deserialize(JsonDocument &doc, TChar *input,\n                                 size_t inputSize, NestingLimit nestingLimit,\n                                 TFilter filter) {\n  BoundedReader<TChar *> reader(input, inputSize);\n  doc.clear();\n  return makeDeserializer<TDeserializer>(\n             doc.memoryPool(), reader,\n             makeStringStorage(input, doc.memoryPool()))\n      .parse(doc.data(), filter, nestingLimit);\n}\ntemplate <template <typename, typename> class TDeserializer, typename TStream,\n          typename TFilter>\nDeserializationError deserialize(JsonDocument &doc, TStream &input,\n                                 NestingLimit nestingLimit, TFilter filter) {\n  Reader<TStream> reader(input);\n  doc.clear();\n  return makeDeserializer<TDeserializer>(\n             doc.memoryPool(), reader,\n             makeStringStorage(input, doc.memoryPool()))\n      .parse(doc.data(), filter, nestingLimit);\n}\nclass EscapeSequence {\n public:\n  static char escapeChar(char c) {\n    const char *p = escapeTable(true);\n    while (p[0] && p[1] != c) {\n      p += 2;\n    }\n    return p[0];\n  }\n  static char unescapeChar(char c) {\n    const char *p = escapeTable(false);\n    for (;;) {\n      if (p[0] == '\\0')\n        return 0;\n      if (p[0] == c)\n        return p[1];\n      p += 2;\n    }\n  }\n private:\n  static const char *escapeTable(bool excludeSolidus) {\n    return &\"//\\\"\\\"\\\\\\\\b\\bf\\fn\\nr\\rt\\t\"[excludeSolidus ? 2 : 0];\n  }\n};\ntemplate <typename TReader>\nclass Latch {\n public:\n  Latch(TReader reader) : _reader(reader), _loaded(false) {\n#if ARDUINOJSON_DEBUG\n    _ended = false;\n#endif\n  }\n  void clear() {\n    _loaded = false;\n  }\n  int last() const {\n    return _current;\n  }\n  FORCE_INLINE char current() {\n    if (!_loaded) {\n      load();\n    }\n    return _current;\n  }\n private:\n  void load() {\n    ARDUINOJSON_ASSERT(!_ended);\n    int c = _reader.read();\n#if ARDUINOJSON_DEBUG\n    if (c <= 0)\n      _ended = true;\n#endif\n    _current = static_cast<char>(c > 0 ? c : 0);\n    _loaded = true;\n  }\n  TReader _reader;\n  char _current;  // NOLINT(clang-analyzer-optin.cplusplus.UninitializedObject)\n  bool _loaded;\n#if ARDUINOJSON_DEBUG\n  bool _ended;\n#endif\n};\n}  // namespace ARDUINOJSON_NAMESPACE\n#if defined(__GNUC__)\n#  if __GNUC__ >= 7\n#    pragma GCC diagnostic push\n#    pragma GCC diagnostic ignored \"-Wmaybe-uninitialized\"\n#  endif\n#endif\nnamespace ARDUINOJSON_NAMESPACE {\nnamespace Utf16 {\ninline bool isHighSurrogate(uint16_t codeunit) {\n  return codeunit >= 0xD800 && codeunit < 0xDC00;\n}\ninline bool isLowSurrogate(uint16_t codeunit) {\n  return codeunit >= 0xDC00 && codeunit < 0xE000;\n}\nclass Codepoint {\n public:\n  Codepoint() : _highSurrogate(0), _codepoint(0) {}\n  bool append(uint16_t codeunit) {\n    if (isHighSurrogate(codeunit)) {\n      _highSurrogate = codeunit & 0x3FF;\n      return false;\n    }\n    if (isLowSurrogate(codeunit)) {\n      _codepoint =\n          uint32_t(0x10000 + ((_highSurrogate << 10) | (codeunit & 0x3FF)));\n      return true;\n    }\n    _codepoint = codeunit;\n    return true;\n  }\n  uint32_t value() const {\n    return _codepoint;\n  }\n private:\n  uint16_t _highSurrogate;\n  uint32_t _codepoint;\n};\n}  // namespace Utf16\n}  // namespace ARDUINOJSON_NAMESPACE\n#if defined(__GNUC__)\n#  if __GNUC__ >= 8\n#    pragma GCC diagnostic pop\n#  endif\n#endif\nnamespace ARDUINOJSON_NAMESPACE {\nnamespace Utf8 {\ntemplate <typename TStringBuilder>\ninline void encodeCodepoint(uint32_t codepoint32, TStringBuilder& str) {\n  char buf[5];\n  char* p = buf;\n  *(p++) = 0;\n  if (codepoint32 < 0x80) {\n    *(p++) = char((codepoint32));\n  } else {\n    *(p++) = char((codepoint32 | 0x80) & 0xBF);\n    uint16_t codepoint16 = uint16_t(codepoint32 >> 6);\n    if (codepoint16 < 0x20) {  // 0x800\n      *(p++) = char(codepoint16 | 0xC0);\n    } else {\n      *(p++) = char((codepoint16 | 0x80) & 0xBF);\n      codepoint16 = uint16_t(codepoint16 >> 6);\n      if (codepoint16 < 0x10) {  // 0x10000\n        *(p++) = char(codepoint16 | 0xE0);\n      } else {\n        *(p++) = char((codepoint16 | 0x80) & 0xBF);\n        codepoint16 = uint16_t(codepoint16 >> 6);\n        *(p++) = char(codepoint16 | 0xF0);\n      }\n    }\n  }\n  while (*(--p)) {\n    str.append(*p);\n  }\n}\n}  // namespace Utf8\ntemplate <typename TReader, typename TStringStorage>\nclass JsonDeserializer {\n public:\n  JsonDeserializer(MemoryPool &pool, TReader reader,\n                   TStringStorage stringStorage)\n      : _stringStorage(stringStorage),\n        _foundSomething(false),\n        _latch(reader),\n        _pool(&pool),\n        _error(DeserializationError::Ok) {}\n  template <typename TFilter>\n  DeserializationError parse(VariantData &variant, TFilter filter,\n                             NestingLimit nestingLimit) {\n    parseVariant(variant, filter, nestingLimit);\n    if (!_error && _latch.last() != 0 && !variant.isEnclosed()) {\n      return DeserializationError::InvalidInput;\n    }\n    return _error;\n  }\n private:\n  char current() {\n    return _latch.current();\n  }\n  void move() {\n    _latch.clear();\n  }\n  bool eat(char charToSkip) {\n    if (current() != charToSkip)\n      return false;\n    move();\n    return true;\n  }\n  template <typename TFilter>\n  bool parseVariant(VariantData &variant, TFilter filter,\n                    NestingLimit nestingLimit) {\n    if (!skipSpacesAndComments())\n      return false;\n    switch (current()) {\n      case '[':\n        if (filter.allowArray())\n          return parseArray(variant.toArray(), filter, nestingLimit);\n        else\n          return skipArray(nestingLimit);\n      case '{':\n        if (filter.allowObject())\n          return parseObject(variant.toObject(), filter, nestingLimit);\n        else\n          return skipObject(nestingLimit);\n      case '\\\"':\n      case '\\'':\n        if (filter.allowValue())\n          return parseStringValue(variant);\n        else\n          return skipString();\n      default:\n        if (filter.allowValue())\n          return parseNumericValue(variant);\n        else\n          return skipNumericValue();\n    }\n  }\n  bool skipVariant(NestingLimit nestingLimit) {\n    if (!skipSpacesAndComments())\n      return false;\n    switch (current()) {\n      case '[':\n        return skipArray(nestingLimit);\n      case '{':\n        return skipObject(nestingLimit);\n      case '\\\"':\n      case '\\'':\n        return skipString();\n      default:\n        return skipNumericValue();\n    }\n  }\n  template <typename TFilter>\n  bool parseArray(CollectionData &array, TFilter filter,\n                  NestingLimit nestingLimit) {\n    if (nestingLimit.reached()) {\n      _error = DeserializationError::TooDeep;\n      return false;\n    }\n    ARDUINOJSON_ASSERT(current() == '[');\n    move();\n    if (!skipSpacesAndComments())\n      return false;\n    if (eat(']'))\n      return true;\n    TFilter memberFilter = filter[0UL];\n    for (;;) {\n      if (memberFilter.allow()) {\n        VariantData *value = array.addElement(_pool);\n        if (!value) {\n          _error = DeserializationError::NoMemory;\n          return false;\n        }\n        if (!parseVariant(*value, memberFilter, nestingLimit.decrement()))\n          return false;\n      } else {\n        if (!skipVariant(nestingLimit.decrement()))\n          return false;\n      }\n      if (!skipSpacesAndComments())\n        return false;\n      if (eat(']'))\n        return true;\n      if (!eat(',')) {\n        _error = DeserializationError::InvalidInput;\n        return false;\n      }\n    }\n  }\n  bool skipArray(NestingLimit nestingLimit) {\n    if (nestingLimit.reached()) {\n      _error = DeserializationError::TooDeep;\n      return false;\n    }\n    ARDUINOJSON_ASSERT(current() == '[');\n    move();\n    for (;;) {\n      if (!skipVariant(nestingLimit.decrement()))\n        return false;\n      if (!skipSpacesAndComments())\n        return false;\n      if (eat(']'))\n        return true;\n      if (!eat(',')) {\n        _error = DeserializationError::InvalidInput;\n        return false;\n      }\n    }\n  }\n  template <typename TFilter>\n  bool parseObject(CollectionData &object, TFilter filter,\n                   NestingLimit nestingLimit) {\n    if (nestingLimit.reached()) {\n      _error = DeserializationError::TooDeep;\n      return false;\n    }\n    ARDUINOJSON_ASSERT(current() == '{');\n    move();\n    if (!skipSpacesAndComments())\n      return false;\n    if (eat('}'))\n      return true;\n    for (;;) {\n      if (!parseKey())\n        return false;\n      if (!skipSpacesAndComments())\n        return false;\n      if (!eat(':')) {\n        _error = DeserializationError::InvalidInput;\n        return false;\n      }\n      const char *key = _stringStorage.c_str();\n      TFilter memberFilter = filter[key];\n      if (memberFilter.allow()) {\n        VariantData *variant = object.getMember(adaptString(key));\n        if (!variant) {\n          key = _stringStorage.save();\n          VariantSlot *slot = object.addSlot(_pool);\n          if (!slot) {\n            _error = DeserializationError::NoMemory;\n            return false;\n          }\n          slot->setKey(key, typename TStringStorage::storage_policy());\n          variant = slot->data();\n        }\n        if (!parseVariant(*variant, memberFilter, nestingLimit.decrement()))\n          return false;\n      } else {\n        if (!skipVariant(nestingLimit.decrement()))\n          return false;\n      }\n      if (!skipSpacesAndComments())\n        return false;\n      if (eat('}'))\n        return true;\n      if (!eat(',')) {\n        _error = DeserializationError::InvalidInput;\n        return false;\n      }\n      if (!skipSpacesAndComments())\n        return false;\n    }\n  }\n  bool skipObject(NestingLimit nestingLimit) {\n    if (nestingLimit.reached()) {\n      _error = DeserializationError::TooDeep;\n      return false;\n    }\n    ARDUINOJSON_ASSERT(current() == '{');\n    move();\n    if (!skipSpacesAndComments())\n      return false;\n    if (eat('}'))\n      return true;\n    for (;;) {\n      if (!skipVariant(nestingLimit.decrement()))\n        return false;\n      if (!skipSpacesAndComments())\n        return false;\n      if (!eat(':')) {\n        _error = DeserializationError::InvalidInput;\n        return false;\n      }\n      if (!skipVariant(nestingLimit.decrement()))\n        return false;\n      if (!skipSpacesAndComments())\n        return false;\n      if (eat('}'))\n        return true;\n      if (!eat(',')) {\n        _error = DeserializationError::InvalidInput;\n        return false;\n      }\n    }\n  }\n  bool parseKey() {\n    _stringStorage.startString();\n    if (isQuote(current())) {\n      return parseQuotedString();\n    } else {\n      return parseNonQuotedString();\n    }\n  }\n  bool parseStringValue(VariantData &variant) {\n    _stringStorage.startString();\n    if (!parseQuotedString())\n      return false;\n    const char *value = _stringStorage.save();\n    variant.setStringPointer(value, typename TStringStorage::storage_policy());\n    return true;\n  }\n  bool parseQuotedString() {\n#if ARDUINOJSON_DECODE_UNICODE\n    Utf16::Codepoint codepoint;\n#endif\n    const char stopChar = current();\n    move();\n    for (;;) {\n      char c = current();\n      move();\n      if (c == stopChar)\n        break;\n      if (c == '\\0') {\n        _error = DeserializationError::IncompleteInput;\n        return false;\n      }\n      if (c == '\\\\') {\n        c = current();\n        if (c == '\\0') {\n          _error = DeserializationError::IncompleteInput;\n          return false;\n        }\n        if (c == 'u') {\n#if ARDUINOJSON_DECODE_UNICODE\n          move();\n          uint16_t codeunit;\n          if (!parseHex4(codeunit))\n            return false;\n          if (codepoint.append(codeunit))\n            Utf8::encodeCodepoint(codepoint.value(), _stringStorage);\n#else\n          _stringStorage.append('\\\\');\n#endif\n          continue;\n        }\n        c = EscapeSequence::unescapeChar(c);\n        if (c == '\\0') {\n          _error = DeserializationError::InvalidInput;\n          return false;\n        }\n        move();\n      }\n      _stringStorage.append(c);\n    }\n    _stringStorage.append('\\0');\n    if (!_stringStorage.isValid()) {\n      _error = DeserializationError::NoMemory;\n      return false;\n    }\n    return true;\n  }\n  bool parseNonQuotedString() {\n    char c = current();\n    ARDUINOJSON_ASSERT(c);\n    if (canBeInNonQuotedString(c)) {  // no quotes\n      do {\n        move();\n        _stringStorage.append(c);\n        c = current();\n      } while (canBeInNonQuotedString(c));\n    } else {\n      _error = DeserializationError::InvalidInput;\n      return false;\n    }\n    _stringStorage.append('\\0');\n    if (!_stringStorage.isValid()) {\n      _error = DeserializationError::NoMemory;\n      return false;\n    }\n    return true;\n  }\n  bool skipString() {\n    const char stopChar = current();\n    move();\n    for (;;) {\n      char c = current();\n      move();\n      if (c == stopChar)\n        break;\n      if (c == '\\0') {\n        _error = DeserializationError::IncompleteInput;\n        return false;\n      }\n      if (c == '\\\\') {\n        if (current() != '\\0')\n          move();\n      }\n    }\n    return true;\n  }\n  bool parseNumericValue(VariantData &result) {\n    uint8_t n = 0;\n    char c = current();\n    while (canBeInNonQuotedString(c) && n < 63) {\n      move();\n      _buffer[n++] = c;\n      c = current();\n    }\n    _buffer[n] = 0;\n    c = _buffer[0];\n    if (c == 't') {  // true\n      result.setBoolean(true);\n      if (n != 4) {\n        _error = DeserializationError::IncompleteInput;\n        return false;\n      }\n      return true;\n    }\n    if (c == 'f') {  // false\n      result.setBoolean(false);\n      if (n != 5) {\n        _error = DeserializationError::IncompleteInput;\n        return false;\n      }\n      return true;\n    }\n    if (c == 'n') {  // null\n      if (n != 4) {\n        _error = DeserializationError::IncompleteInput;\n        return false;\n      }\n      return true;\n    }\n    if (!parseNumber(_buffer, result)) {\n      _error = DeserializationError::InvalidInput;\n      return false;\n    }\n    return true;\n  }\n  bool skipNumericValue() {\n    char c = current();\n    while (canBeInNonQuotedString(c)) {\n      move();\n      c = current();\n    }\n    return true;\n  }\n  bool parseHex4(uint16_t &result) {\n    result = 0;\n    for (uint8_t i = 0; i < 4; ++i) {\n      char digit = current();\n      if (!digit) {\n        _error = DeserializationError::IncompleteInput;\n        return false;\n      }\n      uint8_t value = decodeHex(digit);\n      if (value > 0x0F) {\n        _error = DeserializationError::InvalidInput;\n        return false;\n      }\n      result = uint16_t((result << 4) | value);\n      move();\n    }\n    return true;\n  }\n  static inline bool isBetween(char c, char min, char max) {\n    return min <= c && c <= max;\n  }\n  static inline bool canBeInNonQuotedString(char c) {\n    return isBetween(c, '0', '9') || isBetween(c, '_', 'z') ||\n           isBetween(c, 'A', 'Z') || c == '+' || c == '-' || c == '.';\n  }\n  static inline bool isQuote(char c) {\n    return c == '\\'' || c == '\\\"';\n  }\n  static inline uint8_t decodeHex(char c) {\n    if (c < 'A')\n      return uint8_t(c - '0');\n    c = char(c & ~0x20);  // uppercase\n    return uint8_t(c - 'A' + 10);\n  }\n  bool skipSpacesAndComments() {\n    for (;;) {\n      switch (current()) {\n        case '\\0':\n          _error = _foundSomething ? DeserializationError::IncompleteInput\n                                   : DeserializationError::EmptyInput;\n          return false;\n        case ' ':\n        case '\\t':\n        case '\\r':\n        case '\\n':\n          move();\n          continue;\n#if ARDUINOJSON_ENABLE_COMMENTS\n        case '/':\n          move();  // skip '/'\n          switch (current()) {\n            case '*': {\n              move();  // skip '*'\n              bool wasStar = false;\n              for (;;) {\n                char c = current();\n                if (c == '\\0') {\n                  _error = DeserializationError::IncompleteInput;\n                  return false;\n                }\n                if (c == '/' && wasStar) {\n                  move();\n                  break;\n                }\n                wasStar = c == '*';\n                move();\n              }\n              break;\n            }\n            case '/':\n              for (;;) {\n                move();\n                char c = current();\n                if (c == '\\0') {\n                  _error = DeserializationError::IncompleteInput;\n                  return false;\n                }\n                if (c == '\\n')\n                  break;\n              }\n              break;\n            default:\n              _error = DeserializationError::InvalidInput;\n              return false;\n          }\n          break;\n#endif\n        default:\n          _foundSomething = true;\n          return true;\n      }\n    }\n  }\n  TStringStorage _stringStorage;\n  bool _foundSomething;\n  Latch<TReader> _latch;\n  MemoryPool *_pool;\n  char _buffer[64];  // using a member instead of a local variable because it\n  DeserializationError _error;\n};\ntemplate <typename TString>\nDeserializationError deserializeJson(\n    JsonDocument &doc, const TString &input,\n    NestingLimit nestingLimit = NestingLimit()) {\n  return deserialize<JsonDeserializer>(doc, input, nestingLimit,\n                                       AllowAllFilter());\n}\ntemplate <typename TString>\nDeserializationError deserializeJson(\n    JsonDocument &doc, const TString &input, Filter filter,\n    NestingLimit nestingLimit = NestingLimit()) {\n  return deserialize<JsonDeserializer>(doc, input, nestingLimit, filter);\n}\ntemplate <typename TString>\nDeserializationError deserializeJson(JsonDocument &doc, const TString &input,\n                                     NestingLimit nestingLimit, Filter filter) {\n  return deserialize<JsonDeserializer>(doc, input, nestingLimit, filter);\n}\ntemplate <typename TStream>\nDeserializationError deserializeJson(\n    JsonDocument &doc, TStream &input,\n    NestingLimit nestingLimit = NestingLimit()) {\n  return deserialize<JsonDeserializer>(doc, input, nestingLimit,\n                                       AllowAllFilter());\n}\ntemplate <typename TStream>\nDeserializationError deserializeJson(\n    JsonDocument &doc, TStream &input, Filter filter,\n    NestingLimit nestingLimit = NestingLimit()) {\n  return deserialize<JsonDeserializer>(doc, input, nestingLimit, filter);\n}\ntemplate <typename TStream>\nDeserializationError deserializeJson(JsonDocument &doc, TStream &input,\n                                     NestingLimit nestingLimit, Filter filter) {\n  return deserialize<JsonDeserializer>(doc, input, nestingLimit, filter);\n}\ntemplate <typename TChar>\nDeserializationError deserializeJson(\n    JsonDocument &doc, TChar *input,\n    NestingLimit nestingLimit = NestingLimit()) {\n  return deserialize<JsonDeserializer>(doc, input, nestingLimit,\n                                       AllowAllFilter());\n}\ntemplate <typename TChar>\nDeserializationError deserializeJson(\n    JsonDocument &doc, TChar *input, Filter filter,\n    NestingLimit nestingLimit = NestingLimit()) {\n  return deserialize<JsonDeserializer>(doc, input, nestingLimit, filter);\n}\ntemplate <typename TChar>\nDeserializationError deserializeJson(JsonDocument &doc, TChar *input,\n                                     NestingLimit nestingLimit, Filter filter) {\n  return deserialize<JsonDeserializer>(doc, input, nestingLimit, filter);\n}\ntemplate <typename TChar>\nDeserializationError deserializeJson(\n    JsonDocument &doc, TChar *input, size_t inputSize,\n    NestingLimit nestingLimit = NestingLimit()) {\n  return deserialize<JsonDeserializer>(doc, input, inputSize, nestingLimit,\n                                       AllowAllFilter());\n}\ntemplate <typename TChar>\nDeserializationError deserializeJson(\n    JsonDocument &doc, TChar *input, size_t inputSize, Filter filter,\n    NestingLimit nestingLimit = NestingLimit()) {\n  return deserialize<JsonDeserializer>(doc, input, inputSize, nestingLimit,\n                                       filter);\n}\ntemplate <typename TChar>\nDeserializationError deserializeJson(JsonDocument &doc, TChar *input,\n                                     size_t inputSize,\n                                     NestingLimit nestingLimit, Filter filter) {\n  return deserialize<JsonDeserializer>(doc, input, inputSize, nestingLimit,\n                                       filter);\n}\ntemplate <typename TFloat>\nstruct FloatParts {\n  uint32_t integral;\n  uint32_t decimal;\n  int16_t exponent;\n  int8_t decimalPlaces;\n  FloatParts(TFloat value) {\n    uint32_t maxDecimalPart = sizeof(TFloat) >= 8 ? 1000000000 : 1000000;\n    decimalPlaces = sizeof(TFloat) >= 8 ? 9 : 6;\n    exponent = normalize(value);\n    integral = uint32_t(value);\n    for (uint32_t tmp = integral; tmp >= 10; tmp /= 10) {\n      maxDecimalPart /= 10;\n      decimalPlaces--;\n    }\n    TFloat remainder = (value - TFloat(integral)) * TFloat(maxDecimalPart);\n    decimal = uint32_t(remainder);\n    remainder = remainder - TFloat(decimal);\n    decimal += uint32_t(remainder * 2);\n    if (decimal >= maxDecimalPart) {\n      decimal = 0;\n      integral++;\n      if (exponent && integral >= 10) {\n        exponent++;\n        integral = 1;\n      }\n    }\n    while (decimal % 10 == 0 && decimalPlaces > 0) {\n      decimal /= 10;\n      decimalPlaces--;\n    }\n  }\n  static int16_t normalize(TFloat& value) {\n    typedef FloatTraits<TFloat> traits;\n    int16_t powersOf10 = 0;\n    int8_t index = sizeof(TFloat) == 8 ? 8 : 5;\n    int bit = 1 << index;\n    if (value >= ARDUINOJSON_POSITIVE_EXPONENTIATION_THRESHOLD) {\n      for (; index >= 0; index--) {\n        if (value >= traits::positiveBinaryPowerOfTen(index)) {\n          value *= traits::negativeBinaryPowerOfTen(index);\n          powersOf10 = int16_t(powersOf10 + bit);\n        }\n        bit >>= 1;\n      }\n    }\n    if (value > 0 && value <= ARDUINOJSON_NEGATIVE_EXPONENTIATION_THRESHOLD) {\n      for (; index >= 0; index--) {\n        if (value < traits::negativeBinaryPowerOfTenPlusOne(index)) {\n          value *= traits::positiveBinaryPowerOfTen(index);\n          powersOf10 = int16_t(powersOf10 - bit);\n        }\n        bit >>= 1;\n      }\n    }\n    return powersOf10;\n  }\n};\ntemplate <typename TWriter>\nclass CountingDecorator {\n public:\n  explicit CountingDecorator(TWriter& writer) : _writer(writer), _count(0) {}\n  void write(uint8_t c) {\n    _count += _writer.write(c);\n  }\n  void write(const uint8_t* s, size_t n) {\n    _count += _writer.write(s, n);\n  }\n  size_t count() const {\n    return _count;\n  }\n private:\n  TWriter _writer;\n  size_t _count;\n};\ntemplate <typename TWriter>\nclass TextFormatter {\n public:\n  explicit TextFormatter(TWriter writer) : _writer(writer) {}\n  size_t bytesWritten() const {\n    return _writer.count();\n  }\n  void writeBoolean(bool value) {\n    if (value)\n      writeRaw(\"true\");\n    else\n      writeRaw(\"false\");\n  }\n  void writeString(const char *value) {\n    ARDUINOJSON_ASSERT(value != NULL);\n    writeRaw('\\\"');\n    while (*value) writeChar(*value++);\n    writeRaw('\\\"');\n  }\n  void writeChar(char c) {\n    char specialChar = EscapeSequence::escapeChar(c);\n    if (specialChar) {\n      writeRaw('\\\\');\n      writeRaw(specialChar);\n    } else {\n      writeRaw(c);\n    }\n  }\n  template <typename T>\n  void writeFloat(T value) {\n    if (isnan(value))\n      return writeRaw(ARDUINOJSON_ENABLE_NAN ? \"NaN\" : \"null\");\n#if ARDUINOJSON_ENABLE_INFINITY\n    if (value < 0.0) {\n      writeRaw('-');\n      value = -value;\n    }\n    if (isinf(value))\n      return writeRaw(\"Infinity\");\n#else\n    if (isinf(value))\n      return writeRaw(\"null\");\n    if (value < 0.0) {\n      writeRaw('-');\n      value = -value;\n    }\n#endif\n    FloatParts<T> parts(value);\n    writeInteger(parts.integral);\n    if (parts.decimalPlaces)\n      writeDecimals(parts.decimal, parts.decimalPlaces);\n    if (parts.exponent) {\n      writeRaw('e');\n      writeInteger(parts.exponent);\n    }\n  }\n  template <typename T>\n  typename enable_if<is_signed<T>::value>::type writeInteger(T value) {\n    typedef typename make_unsigned<T>::type unsigned_type;\n    unsigned_type unsigned_value;\n    if (value < 0) {\n      writeRaw('-');\n      unsigned_value = unsigned_type(unsigned_type(~value) + 1);\n    } else {\n      unsigned_value = unsigned_type(value);\n    }\n    writeInteger(unsigned_value);\n  }\n  template <typename T>\n  typename enable_if<is_unsigned<T>::value>::type writeInteger(T value) {\n    char buffer[22];\n    char *end = buffer + sizeof(buffer);\n    char *begin = end;\n    do {\n      *--begin = char(value % 10 + '0');\n      value = T(value / 10);\n    } while (value);\n    writeRaw(begin, end);\n  }\n  void writeDecimals(uint32_t value, int8_t width) {\n    char buffer[16];\n    char *end = buffer + sizeof(buffer);\n    char *begin = end;\n    while (width--) {\n      *--begin = char(value % 10 + '0');\n      value /= 10;\n    }\n    *--begin = '.';\n    writeRaw(begin, end);\n  }\n  void writeRaw(const char *s) {\n    _writer.write(reinterpret_cast<const uint8_t *>(s), strlen(s));\n  }\n  void writeRaw(const char *s, size_t n) {\n    _writer.write(reinterpret_cast<const uint8_t *>(s), n);\n  }\n  void writeRaw(const char *begin, const char *end) {\n    _writer.write(reinterpret_cast<const uint8_t *>(begin),\n                  static_cast<size_t>(end - begin));\n  }\n  template <size_t N>\n  void writeRaw(const char (&s)[N]) {\n    _writer.write(reinterpret_cast<const uint8_t *>(s), N - 1);\n  }\n  void writeRaw(char c) {\n    _writer.write(static_cast<uint8_t>(c));\n  }\n protected:\n  CountingDecorator<TWriter> _writer;\n private:\n  TextFormatter &operator=(const TextFormatter &);  // cannot be assigned\n};\nclass DummyWriter {\n public:\n  size_t write(uint8_t) {\n    return 1;\n  }\n  size_t write(const uint8_t*, size_t n) {\n    return n;\n  }\n};\ntemplate <template <typename> class TSerializer, typename TSource>\nsize_t measure(const TSource &source) {\n  DummyWriter dp;\n  TSerializer<DummyWriter> serializer(dp);\n  return source.accept(serializer);\n}\ntemplate <typename TDestination, typename Enable = void>\nclass Writer {\n public:\n  explicit Writer(TDestination& dest) : _dest(&dest) {}\n  size_t write(uint8_t c) {\n    return _dest->write(c);\n  }\n  size_t write(const uint8_t* s, size_t n) {\n    return _dest->write(s, n);\n  }\n private:\n  TDestination* _dest;\n};\nclass StaticStringWriter {\n public:\n  StaticStringWriter(char *buf, size_t size) : end(buf + size), p(buf) {}\n  size_t write(uint8_t c) {\n    if (p >= end)\n      return 0;\n    *p++ = static_cast<char>(c);\n    return 1;\n  }\n  size_t write(const uint8_t *s, size_t n) {\n    char *begin = p;\n    while (p < end && n > 0) {\n      *p++ = static_cast<char>(*s++);\n      n--;\n    }\n    return size_t(p - begin);\n  }\n private:\n  char *end;\n  char *p;\n};\n}  // namespace ARDUINOJSON_NAMESPACE\n#if ARDUINOJSON_ENABLE_STD_STRING\nnamespace ARDUINOJSON_NAMESPACE {\ntemplate <class T>\nstruct is_std_string : false_type {};\ntemplate <class TCharTraits, class TAllocator>\nstruct is_std_string<std::basic_string<char, TCharTraits, TAllocator> >\n    : true_type {};\ntemplate <typename TDestination>\nclass Writer<TDestination,\n             typename enable_if<is_std_string<TDestination>::value>::type> {\n public:\n  Writer(TDestination &str) : _str(&str) {}\n  size_t write(uint8_t c) {\n    _str->operator+=(static_cast<char>(c));\n    return 1;\n  }\n  size_t write(const uint8_t *s, size_t n) {\n    _str->append(reinterpret_cast<const char *>(s), n);\n    return n;\n  }\n private:\n  TDestination *_str;\n};\n}  // namespace ARDUINOJSON_NAMESPACE\n#endif\n#if ARDUINOJSON_ENABLE_ARDUINO_STRING\nnamespace ARDUINOJSON_NAMESPACE {\ntemplate <>\nclass Writer< ::String, void> {\n  static const size_t bufferCapacity = ARDUINOJSON_STRING_BUFFER_SIZE;\n public:\n  explicit Writer(::String &str) : _destination(&str) {\n    _size = 0;\n  }\n  ~Writer() {\n    flush();\n  }\n  size_t write(uint8_t c) {\n    if (_size + 1 >= bufferCapacity)\n      if (flush() != 0)\n        return 0;\n    _buffer[_size++] = static_cast<char>(c);\n    return 1;\n  }\n  size_t write(const uint8_t *s, size_t n) {\n    for (size_t i = 0; i < n; i++) {\n      write(s[i]);\n    }\n    return n;\n  }\n  size_t flush() {\n    ARDUINOJSON_ASSERT(_size < bufferCapacity);\n    _buffer[_size] = 0;\n    if (_destination->concat(_buffer))\n      _size = 0;\n    return _size;\n  }\n private:\n  ::String *_destination;\n  char _buffer[bufferCapacity];\n  size_t _size;\n};\n}  // namespace ARDUINOJSON_NAMESPACE\n#endif\n#if ARDUINOJSON_ENABLE_STD_STREAM\nnamespace ARDUINOJSON_NAMESPACE {\ntemplate <typename TDestination>\nclass Writer<\n    TDestination,\n    typename enable_if<is_base_of<std::ostream, TDestination>::value>::type> {\n public:\n  explicit Writer(std::ostream& os) : _os(&os) {}\n  size_t write(uint8_t c) {\n    _os->put(static_cast<char>(c));\n    return 1;\n  }\n  size_t write(const uint8_t* s, size_t n) {\n    _os->write(reinterpret_cast<const char*>(s),\n               static_cast<std::streamsize>(n));\n    return n;\n  }\n private:\n  std::ostream* _os;\n};\n}  // namespace ARDUINOJSON_NAMESPACE\n#endif\n#if ARDUINOJSON_ENABLE_ARDUINO_PRINT\nnamespace ARDUINOJSON_NAMESPACE {\ntemplate <typename TDestination>\nclass Writer<\n    TDestination,\n    typename enable_if<is_base_of< ::Print, TDestination>::value>::type> {\n public:\n  explicit Writer(::Print& print) : _print(&print) {}\n  size_t write(uint8_t c) {\n    return _print->write(c);\n  }\n  size_t write(const uint8_t* s, size_t n) {\n    return _print->write(s, n);\n  }\n private:\n  ::Print* _print;\n};\n}  // namespace ARDUINOJSON_NAMESPACE\n#endif\nnamespace ARDUINOJSON_NAMESPACE {\ntemplate <template <typename> class TSerializer, typename TSource,\n          typename TWriter>\nsize_t doSerialize(const TSource &source, TWriter writer) {\n  TSerializer<TWriter> serializer(writer);\n  return source.accept(serializer);\n}\ntemplate <template <typename> class TSerializer, typename TSource,\n          typename TDestination>\nsize_t serialize(const TSource &source, TDestination &destination) {\n  Writer<TDestination> writer(destination);\n  return doSerialize<TSerializer>(source, writer);\n}\ntemplate <template <typename> class TSerializer, typename TSource>\ntypename enable_if<!TSerializer<StaticStringWriter>::producesText, size_t>::type\nserialize(const TSource &source, void *buffer, size_t bufferSize) {\n  StaticStringWriter writer(reinterpret_cast<char *>(buffer), bufferSize);\n  return doSerialize<TSerializer>(source, writer);\n}\ntemplate <template <typename> class TSerializer, typename TSource>\ntypename enable_if<TSerializer<StaticStringWriter>::producesText, size_t>::type\nserialize(const TSource &source, void *buffer, size_t bufferSize) {\n  StaticStringWriter writer(reinterpret_cast<char *>(buffer), bufferSize);\n  size_t n = doSerialize<TSerializer>(source, writer);\n  if (n < bufferSize)\n    reinterpret_cast<char *>(buffer)[n] = 0;\n  return n;\n}\ntemplate <template <typename> class TSerializer, typename TSource,\n          typename TChar, size_t N>\n#if defined _MSC_VER && _MSC_VER < 1900\ntypename enable_if<sizeof(remove_reference<TChar>::type) == 1, size_t>::type\n#else\ntypename enable_if<sizeof(TChar) == 1, size_t>::type\n#endif\nserialize(const TSource &source, TChar (&buffer)[N]) {\n  return serialize<TSerializer>(source, buffer, N);\n}\ntemplate <typename TWriter>\nclass JsonSerializer : public Visitor<size_t> {\n public:\n  static const bool producesText = true;\n  JsonSerializer(TWriter writer) : _formatter(writer) {}\n  FORCE_INLINE size_t visitArray(const CollectionData &array) {\n    write('[');\n    VariantSlot *slot = array.head();\n    while (slot != 0) {\n      slot->data()->accept(*this);\n      slot = slot->next();\n      if (slot == 0)\n        break;\n      write(',');\n    }\n    write(']');\n    return bytesWritten();\n  }\n  size_t visitObject(const CollectionData &object) {\n    write('{');\n    VariantSlot *slot = object.head();\n    while (slot != 0) {\n      _formatter.writeString(slot->key());\n      write(':');\n      slot->data()->accept(*this);\n      slot = slot->next();\n      if (slot == 0)\n        break;\n      write(',');\n    }\n    write('}');\n    return bytesWritten();\n  }\n  size_t visitFloat(Float value) {\n    _formatter.writeFloat(value);\n    return bytesWritten();\n  }\n  size_t visitString(const char *value) {\n    _formatter.writeString(value);\n    return bytesWritten();\n  }\n  size_t visitRawJson(const char *data, size_t n) {\n    _formatter.writeRaw(data, n);\n    return bytesWritten();\n  }\n  size_t visitSignedInteger(Integer value) {\n    _formatter.writeInteger(value);\n    return bytesWritten();\n  }\n  size_t visitUnsignedInteger(UInt value) {\n    _formatter.writeInteger(value);\n    return bytesWritten();\n  }\n  size_t visitBoolean(bool value) {\n    _formatter.writeBoolean(value);\n    return bytesWritten();\n  }\n  size_t visitNull() {\n    _formatter.writeRaw(\"null\");\n    return bytesWritten();\n  }\n protected:\n  size_t bytesWritten() const {\n    return _formatter.bytesWritten();\n  }\n  void write(char c) {\n    _formatter.writeRaw(c);\n  }\n  void write(const char *s) {\n    _formatter.writeRaw(s);\n  }\n private:\n  TextFormatter<TWriter> _formatter;\n};\ntemplate <typename TSource, typename TDestination>\nsize_t serializeJson(const TSource &source, TDestination &destination) {\n  return serialize<JsonSerializer>(source, destination);\n}\ntemplate <typename TSource>\nsize_t serializeJson(const TSource &source, void *buffer, size_t bufferSize) {\n  return serialize<JsonSerializer>(source, buffer, bufferSize);\n}\ntemplate <typename TSource>\nsize_t measureJson(const TSource &source) {\n  return measure<JsonSerializer>(source);\n}\n#if ARDUINOJSON_ENABLE_STD_STREAM\ntemplate <typename T>\ninline typename enable_if<IsVisitable<T>::value, std::ostream &>::type\noperator<<(std::ostream &os, const T &source) {\n  serializeJson(source, os);\n  return os;\n}\n#endif\ntemplate <typename TWriter>\nclass PrettyJsonSerializer : public JsonSerializer<TWriter> {\n  typedef JsonSerializer<TWriter> base;\n public:\n  PrettyJsonSerializer(TWriter writer) : base(writer), _nesting(0) {}\n  size_t visitArray(const CollectionData &array) {\n    VariantSlot *slot = array.head();\n    if (slot) {\n      base::write(\"[\\r\\n\");\n      _nesting++;\n      while (slot != 0) {\n        indent();\n        slot->data()->accept(*this);\n        slot = slot->next();\n        base::write(slot ? \",\\r\\n\" : \"\\r\\n\");\n      }\n      _nesting--;\n      indent();\n      base::write(\"]\");\n    } else {\n      base::write(\"[]\");\n    }\n    return this->bytesWritten();\n  }\n  size_t visitObject(const CollectionData &object) {\n    VariantSlot *slot = object.head();\n    if (slot) {\n      base::write(\"{\\r\\n\");\n      _nesting++;\n      while (slot != 0) {\n        indent();\n        base::visitString(slot->key());\n        base::write(\": \");\n        slot->data()->accept(*this);\n        slot = slot->next();\n        base::write(slot ? \",\\r\\n\" : \"\\r\\n\");\n      }\n      _nesting--;\n      indent();\n      base::write(\"}\");\n    } else {\n      base::write(\"{}\");\n    }\n    return this->bytesWritten();\n  }\n private:\n  void indent() {\n    for (uint8_t i = 0; i < _nesting; i++) base::write(ARDUINOJSON_TAB);\n  }\n  uint8_t _nesting;\n};\ntemplate <typename TSource, typename TDestination>\nsize_t serializeJsonPretty(const TSource &source, TDestination &destination) {\n  return serialize<PrettyJsonSerializer>(source, destination);\n}\ntemplate <typename TSource>\nsize_t serializeJsonPretty(const TSource &source, void *buffer,\n                           size_t bufferSize) {\n  return serialize<PrettyJsonSerializer>(source, buffer, bufferSize);\n}\ntemplate <typename TSource>\nsize_t measureJsonPretty(const TSource &source) {\n  return measure<PrettyJsonSerializer>(source);\n}\ntemplate <typename T>\ninline void swap(T& a, T& b) {\n  T t(a);\n  a = b;\n  b = t;\n}\n#if ARDUINOJSON_HAS_RVALUE_REFERENCES\ntemplate <typename T>\ntypename remove_reference<T>::type&& move(T&& t) {\n  return static_cast<typename remove_reference<T>::type&&>(t);\n}\n#else\ntemplate <typename T>\nT& move(T& t) {\n  return t;\n}\n#endif\n#if ARDUINOJSON_LITTLE_ENDIAN\ninline void fixEndianess(uint8_t *p, integral_constant<size_t, 8>) {\n  swap(p[0], p[7]);\n  swap(p[1], p[6]);\n  swap(p[2], p[5]);\n  swap(p[3], p[4]);\n}\ninline void fixEndianess(uint8_t *p, integral_constant<size_t, 4>) {\n  swap(p[0], p[3]);\n  swap(p[1], p[2]);\n}\ninline void fixEndianess(uint8_t *p, integral_constant<size_t, 2>) {\n  swap(p[0], p[1]);\n}\ninline void fixEndianess(uint8_t *, integral_constant<size_t, 1>) {}\ntemplate <typename T>\ninline void fixEndianess(T &value) {\n  fixEndianess(reinterpret_cast<uint8_t *>(&value),\n               integral_constant<size_t, sizeof(T)>());\n}\n#else\ntemplate <typename T>\ninline void fixEndianess(T &) {}\n#endif\ninline void doubleToFloat(const uint8_t d[8], uint8_t f[4]) {\n  f[0] = uint8_t((d[0] & 0xC0) | (d[0] << 3 & 0x3f) | (d[1] >> 5));\n  f[1] = uint8_t((d[1] << 3) | (d[2] >> 5));\n  f[2] = uint8_t((d[2] << 3) | (d[3] >> 5));\n  f[3] = uint8_t((d[3] << 3) | (d[4] >> 5));\n}\ntemplate <typename TReader, typename TStringStorage>\nclass MsgPackDeserializer {\n public:\n  MsgPackDeserializer(MemoryPool &pool, TReader reader,\n                      TStringStorage stringStorage)\n      : _pool(&pool),\n        _reader(reader),\n        _stringStorage(stringStorage),\n        _error(DeserializationError::Ok),\n        _foundSomething(false) {}\n  template <typename TFilter>\n  DeserializationError parse(VariantData &variant, TFilter filter,\n                             NestingLimit nestingLimit) {\n    parseVariant(&variant, filter, nestingLimit);\n    return _foundSomething ? _error : DeserializationError::EmptyInput;\n  }\n private:\n  bool invalidInput() {\n    _error = DeserializationError::InvalidInput;\n    return false;\n  }\n  template <typename TFilter>\n  bool parseVariant(VariantData *variant, TFilter filter,\n                    NestingLimit nestingLimit) {\n    uint8_t code = 0;  // TODO: why do we need to initialize this variable?\n    if (!readByte(code))\n      return false;\n    _foundSomething = true;\n    bool allowValue = filter.allowValue();\n    switch (code) {\n      case 0xc0:\n        return true;\n      case 0xc1:\n        return invalidInput();\n      case 0xc2:\n        if (allowValue)\n          variant->setBoolean(false);\n        return true;\n      case 0xc3:\n        if (allowValue)\n          variant->setBoolean(true);\n        return true;\n      case 0xc4:  // bin 8 (not supported)\n        return skipString<uint8_t>();\n      case 0xc5:  // bin 16 (not supported)\n        return skipString<uint16_t>();\n      case 0xc6:  // bin 32 (not supported)\n        return skipString<uint32_t>();\n      case 0xc7:  // ext 8 (not supported)\n        return skipExt<uint8_t>();\n      case 0xc8:  // ext 16 (not supported)\n        return skipExt<uint16_t>();\n      case 0xc9:  // ext 32 (not supported)\n        return skipExt<uint32_t>();\n      case 0xca:\n        if (allowValue)\n          return readFloat<float>(variant);\n        else\n          return skipBytes(4);\n      case 0xcb:\n        if (allowValue)\n          return readDouble<double>(variant);\n        else\n          return skipBytes(8);\n      case 0xcc:\n        if (allowValue)\n          return readInteger<uint8_t>(variant);\n        else\n          return skipBytes(1);\n      case 0xcd:\n        if (allowValue)\n          return readInteger<uint16_t>(variant);\n        else\n          return skipBytes(2);\n      case 0xce:\n        if (allowValue)\n          return readInteger<uint32_t>(variant);\n        else\n          return skipBytes(4);\n      case 0xcf:\n#if ARDUINOJSON_USE_LONG_LONG\n        if (allowValue)\n          return readInteger<uint64_t>(variant);\n        else\n          return skipBytes(8);\n#else\n        return skipBytes(8);  // not supported\n#endif\n      case 0xd0:\n        if (allowValue)\n          return readInteger<int8_t>(variant);\n        else\n          return skipBytes(1);\n      case 0xd1:\n        if (allowValue)\n          return readInteger<int16_t>(variant);\n        else\n          return skipBytes(2);\n      case 0xd2:\n        if (allowValue)\n          return readInteger<int32_t>(variant);\n        else\n          return skipBytes(4);\n      case 0xd3:\n#if ARDUINOJSON_USE_LONG_LONG\n        if (allowValue)\n          return readInteger<int64_t>(variant);\n        else\n          return skipBytes(8);  // not supported\n#else\n        return skipBytes(8);\n#endif\n      case 0xd4:  // fixext 1 (not supported)\n        return skipBytes(2);\n      case 0xd5:  // fixext 2 (not supported)\n        return skipBytes(3);\n      case 0xd6:  // fixext 4 (not supported)\n        return skipBytes(5);\n      case 0xd7:  // fixext 8 (not supported)\n        return skipBytes(9);\n      case 0xd8:  // fixext 16 (not supported)\n        return skipBytes(17);\n      case 0xd9:\n        if (allowValue)\n          return readString<uint8_t>(variant);\n        else\n          return skipString<uint8_t>();\n      case 0xda:\n        if (allowValue)\n          return readString<uint16_t>(variant);\n        else\n          return skipString<uint16_t>();\n      case 0xdb:\n        if (allowValue)\n          return readString<uint32_t>(variant);\n        else\n          return skipString<uint32_t>();\n      case 0xdc:\n        return readArray<uint16_t>(variant, filter, nestingLimit);\n      case 0xdd:\n        return readArray<uint32_t>(variant, filter, nestingLimit);\n      case 0xde:\n        return readObject<uint16_t>(variant, filter, nestingLimit);\n      case 0xdf:\n        return readObject<uint32_t>(variant, filter, nestingLimit);\n    }\n    switch (code & 0xf0) {\n      case 0x80:\n        return readObject(variant, code & 0x0F, filter, nestingLimit);\n      case 0x90:\n        return readArray(variant, code & 0x0F, filter, nestingLimit);\n    }\n    if ((code & 0xe0) == 0xa0) {\n      if (allowValue)\n        return readString(variant, code & 0x1f);\n      else\n        return skipBytes(code & 0x1f);\n    }\n    if (allowValue)\n      variant->setInteger(static_cast<int8_t>(code));\n    return true;\n  }\n  bool readByte(uint8_t &value) {\n    int c = _reader.read();\n    if (c < 0) {\n      _error = DeserializationError::IncompleteInput;\n      return false;\n    }\n    value = static_cast<uint8_t>(c);\n    return true;\n  }\n  bool readBytes(uint8_t *p, size_t n) {\n    if (_reader.readBytes(reinterpret_cast<char *>(p), n) == n)\n      return true;\n    _error = DeserializationError::IncompleteInput;\n    return false;\n  }\n  template <typename T>\n  bool readBytes(T &value) {\n    return readBytes(reinterpret_cast<uint8_t *>(&value), sizeof(value));\n  }\n  bool skipBytes(size_t n) {\n    for (; n; --n) {\n      if (_reader.read() < 0) {\n        _error = DeserializationError::IncompleteInput;\n        return false;\n      }\n    }\n    return true;\n  }\n  template <typename T>\n  bool readInteger(T &value) {\n    if (!readBytes(value))\n      return false;\n    fixEndianess(value);\n    return true;\n  }\n  template <typename T>\n  bool readInteger(VariantData *variant) {\n    T value;\n    if (!readInteger(value))\n      return false;\n    variant->setInteger(value);\n    return true;\n  }\n  template <typename T>\n  typename enable_if<sizeof(T) == 4, bool>::type readFloat(\n      VariantData *variant) {\n    T value;\n    if (!readBytes(value))\n      return false;\n    fixEndianess(value);\n    variant->setFloat(value);\n    return true;\n  }\n  template <typename T>\n  typename enable_if<sizeof(T) == 8, bool>::type readDouble(\n      VariantData *variant) {\n    T value;\n    if (!readBytes(value))\n      return false;\n    fixEndianess(value);\n    variant->setFloat(value);\n    return true;\n  }\n  template <typename T>\n  typename enable_if<sizeof(T) == 4, bool>::type readDouble(\n      VariantData *variant) {\n    uint8_t i[8];  // input is 8 bytes\n    T value;       // output is 4 bytes\n    uint8_t *o = reinterpret_cast<uint8_t *>(&value);\n    if (!readBytes(i, 8))\n      return false;\n    doubleToFloat(i, o);\n    fixEndianess(value);\n    variant->setFloat(value);\n    return true;\n  }\n  template <typename T>\n  bool readString(VariantData *variant) {\n    T size;\n    if (!readInteger(size))\n      return false;\n    return readString(variant, size);\n  }\n  template <typename T>\n  bool readString() {\n    T size;\n    if (!readInteger(size))\n      return false;\n    return readString(size);\n  }\n  template <typename T>\n  bool skipString() {\n    T size;\n    if (!readInteger(size))\n      return false;\n    return skipBytes(size);\n  }\n  bool readString(VariantData *variant, size_t n) {\n    if (!readString(n))\n      return false;\n    variant->setStringPointer(_stringStorage.save(),\n                              typename TStringStorage::storage_policy());\n    return true;\n  }\n  bool readString(size_t n) {\n    _stringStorage.startString();\n    for (; n; --n) {\n      uint8_t c;\n      if (!readBytes(c))\n        return false;\n      _stringStorage.append(static_cast<char>(c));\n    }\n    _stringStorage.append('\\0');\n    if (!_stringStorage.isValid()) {\n      _error = DeserializationError::NoMemory;\n      return false;\n    }\n    return true;\n  }\n  template <typename TSize, typename TFilter>\n  bool readArray(VariantData *variant, TFilter filter,\n                 NestingLimit nestingLimit) {\n    TSize size;\n    if (!readInteger(size))\n      return false;\n    return readArray(variant, size, filter, nestingLimit);\n  }\n  template <typename TFilter>\n  bool readArray(VariantData *variant, size_t n, TFilter filter,\n                 NestingLimit nestingLimit) {\n    if (nestingLimit.reached()) {\n      _error = DeserializationError::TooDeep;\n      return false;\n    }\n    bool allowArray = filter.allowArray();\n    CollectionData *array = allowArray ? &variant->toArray() : 0;\n    TFilter memberFilter = filter[0U];\n    for (; n; --n) {\n      VariantData *value;\n      if (memberFilter.allow()) {\n        value = array->addElement(_pool);\n        if (!value) {\n          _error = DeserializationError::NoMemory;\n          return false;\n        }\n      } else {\n        value = 0;\n      }\n      if (!parseVariant(value, memberFilter, nestingLimit.decrement()))\n        return false;\n    }\n    return true;\n  }\n  template <typename TSize, typename TFilter>\n  bool readObject(VariantData *variant, TFilter filter,\n                  NestingLimit nestingLimit) {\n    TSize size;\n    if (!readInteger(size))\n      return false;\n    return readObject(variant, size, filter, nestingLimit);\n  }\n  template <typename TFilter>\n  bool readObject(VariantData *variant, size_t n, TFilter filter,\n                  NestingLimit nestingLimit) {\n    if (nestingLimit.reached()) {\n      _error = DeserializationError::TooDeep;\n      return false;\n    }\n    CollectionData *object = filter.allowObject() ? &variant->toObject() : 0;\n    for (; n; --n) {\n      if (!readKey())\n        return false;\n      const char *key = _stringStorage.c_str();\n      TFilter memberFilter = filter[key];\n      VariantData *member;\n      if (memberFilter.allow()) {\n        key = _stringStorage.save();\n        VariantSlot *slot = object->addSlot(_pool);\n        if (!slot) {\n          _error = DeserializationError::NoMemory;\n          return false;\n        }\n        slot->setKey(key, typename TStringStorage::storage_policy());\n        member = slot->data();\n      } else {\n        member = 0;\n      }\n      if (!parseVariant(member, memberFilter, nestingLimit.decrement()))\n        return false;\n    }\n    return true;\n  }\n  bool readKey() {\n    uint8_t code;\n    if (!readByte(code))\n      return false;\n    if ((code & 0xe0) == 0xa0)\n      return readString(code & 0x1f);\n    switch (code) {\n      case 0xd9:\n        return readString<uint8_t>();\n      case 0xda:\n        return readString<uint16_t>();\n      case 0xdb:\n        return readString<uint32_t>();\n      default:\n        return invalidInput();\n    }\n  }\n  template <typename T>\n  bool skipExt() {\n    T size;\n    if (!readInteger(size))\n      return false;\n    return skipBytes(size + 1);\n  }\n  MemoryPool *_pool;\n  TReader _reader;\n  TStringStorage _stringStorage;\n  DeserializationError _error;\n  bool _foundSomething;\n};\ntemplate <typename TString>\nDeserializationError deserializeMsgPack(\n    JsonDocument &doc, const TString &input,\n    NestingLimit nestingLimit = NestingLimit()) {\n  return deserialize<MsgPackDeserializer>(doc, input, nestingLimit,\n                                          AllowAllFilter());\n}\ntemplate <typename TString>\nDeserializationError deserializeMsgPack(\n    JsonDocument &doc, const TString &input, Filter filter,\n    NestingLimit nestingLimit = NestingLimit()) {\n  return deserialize<MsgPackDeserializer>(doc, input, nestingLimit, filter);\n}\ntemplate <typename TString>\nDeserializationError deserializeMsgPack(JsonDocument &doc, const TString &input,\n                                        NestingLimit nestingLimit,\n                                        Filter filter) {\n  return deserialize<MsgPackDeserializer>(doc, input, nestingLimit, filter);\n}\ntemplate <typename TStream>\nDeserializationError deserializeMsgPack(\n    JsonDocument &doc, TStream &input,\n    NestingLimit nestingLimit = NestingLimit()) {\n  return deserialize<MsgPackDeserializer>(doc, input, nestingLimit,\n                                          AllowAllFilter());\n}\ntemplate <typename TStream>\nDeserializationError deserializeMsgPack(\n    JsonDocument &doc, TStream &input, Filter filter,\n    NestingLimit nestingLimit = NestingLimit()) {\n  return deserialize<MsgPackDeserializer>(doc, input, nestingLimit, filter);\n}\ntemplate <typename TStream>\nDeserializationError deserializeMsgPack(JsonDocument &doc, TStream &input,\n                                        NestingLimit nestingLimit,\n                                        Filter filter) {\n  return deserialize<MsgPackDeserializer>(doc, input, nestingLimit, filter);\n}\ntemplate <typename TChar>\nDeserializationError deserializeMsgPack(\n    JsonDocument &doc, TChar *input,\n    NestingLimit nestingLimit = NestingLimit()) {\n  return deserialize<MsgPackDeserializer>(doc, input, nestingLimit,\n                                          AllowAllFilter());\n}\ntemplate <typename TChar>\nDeserializationError deserializeMsgPack(\n    JsonDocument &doc, TChar *input, Filter filter,\n    NestingLimit nestingLimit = NestingLimit()) {\n  return deserialize<MsgPackDeserializer>(doc, input, nestingLimit, filter);\n}\ntemplate <typename TChar>\nDeserializationError deserializeMsgPack(JsonDocument &doc, TChar *input,\n                                        NestingLimit nestingLimit,\n                                        Filter filter) {\n  return deserialize<MsgPackDeserializer>(doc, input, nestingLimit, filter);\n}\ntemplate <typename TChar>\nDeserializationError deserializeMsgPack(\n    JsonDocument &doc, TChar *input, size_t inputSize,\n    NestingLimit nestingLimit = NestingLimit()) {\n  return deserialize<MsgPackDeserializer>(doc, input, inputSize, nestingLimit,\n                                          AllowAllFilter());\n}\ntemplate <typename TChar>\nDeserializationError deserializeMsgPack(\n    JsonDocument &doc, TChar *input, size_t inputSize, Filter filter,\n    NestingLimit nestingLimit = NestingLimit()) {\n  return deserialize<MsgPackDeserializer>(doc, input, inputSize, nestingLimit,\n                                          filter);\n}\ntemplate <typename TChar>\nDeserializationError deserializeMsgPack(JsonDocument &doc, TChar *input,\n                                        size_t inputSize,\n                                        NestingLimit nestingLimit,\n                                        Filter filter) {\n  return deserialize<MsgPackDeserializer>(doc, input, inputSize, nestingLimit,\n                                          filter);\n}\ntemplate <typename TWriter>\nclass MsgPackSerializer : public Visitor<size_t> {\n public:\n  static const bool producesText = false;\n  MsgPackSerializer(TWriter writer) : _writer(writer) {}\n  template <typename T>\n  typename enable_if<sizeof(T) == 4, size_t>::type visitFloat(T value32) {\n    writeByte(0xCA);\n    writeInteger(value32);\n    return bytesWritten();\n  }\n  template <typename T>\n  ARDUINOJSON_NO_SANITIZE(\"float-cast-overflow\")\n  typename enable_if<sizeof(T) == 8, size_t>::type visitFloat(T value64) {\n    float value32 = float(value64);\n    if (value32 == value64) {\n      writeByte(0xCA);\n      writeInteger(value32);\n    } else {\n      writeByte(0xCB);\n      writeInteger(value64);\n    }\n    return bytesWritten();\n  }\n  size_t visitArray(const CollectionData& array) {\n    size_t n = array.size();\n    if (n < 0x10) {\n      writeByte(uint8_t(0x90 + array.size()));\n    } else if (n < 0x10000) {\n      writeByte(0xDC);\n      writeInteger(uint16_t(n));\n    } else {\n      writeByte(0xDD);\n      writeInteger(uint32_t(n));\n    }\n    for (VariantSlot* slot = array.head(); slot; slot = slot->next()) {\n      slot->data()->accept(*this);\n    }\n    return bytesWritten();\n  }\n  size_t visitObject(const CollectionData& object) {\n    size_t n = object.size();\n    if (n < 0x10) {\n      writeByte(uint8_t(0x80 + n));\n    } else if (n < 0x10000) {\n      writeByte(0xDE);\n      writeInteger(uint16_t(n));\n    } else {\n      writeByte(0xDF);\n      writeInteger(uint32_t(n));\n    }\n    for (VariantSlot* slot = object.head(); slot; slot = slot->next()) {\n      visitString(slot->key());\n      slot->data()->accept(*this);\n    }\n    return bytesWritten();\n  }\n  size_t visitString(const char* value) {\n    ARDUINOJSON_ASSERT(value != NULL);\n    size_t n = strlen(value);\n    if (n < 0x20) {\n      writeByte(uint8_t(0xA0 + n));\n    } else if (n < 0x100) {\n      writeByte(0xD9);\n      writeInteger(uint8_t(n));\n    } else if (n < 0x10000) {\n      writeByte(0xDA);\n      writeInteger(uint16_t(n));\n    } else {\n      writeByte(0xDB);\n      writeInteger(uint32_t(n));\n    }\n    writeBytes(reinterpret_cast<const uint8_t*>(value), n);\n    return bytesWritten();\n  }\n  size_t visitRawJson(const char* data, size_t size) {\n    writeBytes(reinterpret_cast<const uint8_t*>(data), size);\n    return bytesWritten();\n  }\n  size_t visitSignedInteger(Integer value) {\n    if (value > 0) {\n      visitUnsignedInteger(static_cast<UInt>(value));\n    } else if (value >= -0x20) {\n      writeInteger(int8_t(value));\n    } else if (value >= -0x80) {\n      writeByte(0xD0);\n      writeInteger(int8_t(value));\n    } else if (value >= -0x8000) {\n      writeByte(0xD1);\n      writeInteger(int16_t(value));\n    }\n#if ARDUINOJSON_USE_LONG_LONG\n    else if (value >= -0x80000000LL)\n#else\n    else\n#endif\n    {\n      writeByte(0xD2);\n      writeInteger(int32_t(value));\n    }\n#if ARDUINOJSON_USE_LONG_LONG\n    else {\n      writeByte(0xD3);\n      writeInteger(int64_t(value));\n    }\n#endif\n    return bytesWritten();\n  }\n  size_t visitUnsignedInteger(UInt value) {\n    if (value <= 0x7F) {\n      writeInteger(uint8_t(value));\n    } else if (value <= 0xFF) {\n      writeByte(0xCC);\n      writeInteger(uint8_t(value));\n    } else if (value <= 0xFFFF) {\n      writeByte(0xCD);\n      writeInteger(uint16_t(value));\n    }\n#if ARDUINOJSON_USE_LONG_LONG\n    else if (value <= 0xFFFFFFFF)\n#else\n    else\n#endif\n    {\n      writeByte(0xCE);\n      writeInteger(uint32_t(value));\n    }\n#if ARDUINOJSON_USE_LONG_LONG\n    else {\n      writeByte(0xCF);\n      writeInteger(uint64_t(value));\n    }\n#endif\n    return bytesWritten();\n  }\n  size_t visitBoolean(bool value) {\n    writeByte(value ? 0xC3 : 0xC2);\n    return bytesWritten();\n  }\n  size_t visitNull() {\n    writeByte(0xC0);\n    return bytesWritten();\n  }\n private:\n  size_t bytesWritten() const {\n    return _writer.count();\n  }\n  void writeByte(uint8_t c) {\n    _writer.write(c);\n  }\n  void writeBytes(const uint8_t* p, size_t n) {\n    _writer.write(p, n);\n  }\n  template <typename T>\n  void writeInteger(T value) {\n    fixEndianess(value);\n    writeBytes(reinterpret_cast<uint8_t*>(&value), sizeof(value));\n  }\n  CountingDecorator<TWriter> _writer;\n};\ntemplate <typename TSource, typename TDestination>\ninline size_t serializeMsgPack(const TSource& source, TDestination& output) {\n  return serialize<MsgPackSerializer>(source, output);\n}\ntemplate <typename TSource>\ninline size_t serializeMsgPack(const TSource& source, void* output,\n                               size_t size) {\n  return serialize<MsgPackSerializer>(source, output, size);\n}\ntemplate <typename TSource>\ninline size_t measureMsgPack(const TSource& source) {\n  return measure<MsgPackSerializer>(source);\n}\n}  // namespace ARDUINOJSON_NAMESPACE\n#ifdef __GNUC__\n#define ARDUINOJSON_PRAGMA(x) _Pragma(#x)\n#define ARDUINOJSON_COMPILE_ERROR(msg) ARDUINOJSON_PRAGMA(GCC error msg)\n#define ARDUINOJSON_STRINGIFY(S) #S\n#define ARDUINOJSON_DEPRECATION_ERROR(X, Y) \\\n  ARDUINOJSON_COMPILE_ERROR(ARDUINOJSON_STRINGIFY(X is a Y from ArduinoJson 5. Please see https:/\\/arduinojson.org/upgrade to learn how to upgrade your program to ArduinoJson version 6))\n#define StaticJsonBuffer ARDUINOJSON_DEPRECATION_ERROR(StaticJsonBuffer, class)\n#define DynamicJsonBuffer ARDUINOJSON_DEPRECATION_ERROR(DynamicJsonBuffer, class)\n#define JsonBuffer ARDUINOJSON_DEPRECATION_ERROR(JsonBuffer, class)\n#define RawJson ARDUINOJSON_DEPRECATION_ERROR(RawJson, function)\n#endif\nnamespace ArduinoJson {\ntypedef ARDUINOJSON_NAMESPACE::ArrayConstRef JsonArrayConst;\ntypedef ARDUINOJSON_NAMESPACE::ArrayRef JsonArray;\ntypedef ARDUINOJSON_NAMESPACE::Float JsonFloat;\ntypedef ARDUINOJSON_NAMESPACE::Integer JsonInteger;\ntypedef ARDUINOJSON_NAMESPACE::ObjectConstRef JsonObjectConst;\ntypedef ARDUINOJSON_NAMESPACE::ObjectRef JsonObject;\ntypedef ARDUINOJSON_NAMESPACE::Pair JsonPair;\ntypedef ARDUINOJSON_NAMESPACE::PairConst JsonPairConst;\ntypedef ARDUINOJSON_NAMESPACE::String JsonString;\ntypedef ARDUINOJSON_NAMESPACE::UInt JsonUInt;\ntypedef ARDUINOJSON_NAMESPACE::VariantConstRef JsonVariantConst;\ntypedef ARDUINOJSON_NAMESPACE::VariantRef JsonVariant;\nusing ARDUINOJSON_NAMESPACE::BasicJsonDocument;\nusing ARDUINOJSON_NAMESPACE::copyArray;\nusing ARDUINOJSON_NAMESPACE::DeserializationError;\nusing ARDUINOJSON_NAMESPACE::deserializeJson;\nusing ARDUINOJSON_NAMESPACE::deserializeMsgPack;\nusing ARDUINOJSON_NAMESPACE::DynamicJsonDocument;\nusing ARDUINOJSON_NAMESPACE::JsonDocument;\nusing ARDUINOJSON_NAMESPACE::measureJson;\nusing ARDUINOJSON_NAMESPACE::serialized;\nusing ARDUINOJSON_NAMESPACE::serializeJson;\nusing ARDUINOJSON_NAMESPACE::serializeJsonPretty;\nusing ARDUINOJSON_NAMESPACE::serializeMsgPack;\nusing ARDUINOJSON_NAMESPACE::StaticJsonDocument;\nnamespace DeserializationOption {\nusing ARDUINOJSON_NAMESPACE::Filter;\nusing ARDUINOJSON_NAMESPACE::NestingLimit;\n}  // namespace DeserializationOption\n}  // namespace ArduinoJson\n\nusing namespace ArduinoJson;\n\n#else\n\n#error ArduinoJson requires a C++ compiler, please change file extension to .cc or .cpp\n\n#endif\n"
  },
  {
    "path": "wled00/src/dependencies/json/AsyncJson-v6.h",
    "content": "// AsyncJson-v6.h\n/*\n  Original file at: https://github.com/baggior/ESPAsyncWebServer/blob/master/src/AsyncJson.h\n  Only changes are ArduinoJson lib path and removed content-type check\n  \n  Async Response to use with ArduinoJson and AsyncWebServer\n  Written by Andrew Melvin (SticilFace) with help from me-no-dev and BBlanchon.\n\n  --------------------\n  Async Request to use with ArduinoJson and AsyncWebServer\n  Written by Arsène von Wyss (avonwyss)\n*/\n#ifndef ASYNC_JSON_H_\n#define ASYNC_JSON_H_\n#include \"ArduinoJson-v6.h\"\n#include <Print.h>\n\n#ifdef ESP8266\n  #define DYNAMIC_JSON_DOCUMENT_SIZE 8192\n#else\n  #define DYNAMIC_JSON_DOCUMENT_SIZE 16384\n#endif\n\n/*\n * Json Response\n * */\n\nclass ChunkPrint : public Print {\n  private:\n    uint8_t* _destination;\n    size_t _to_skip;\n    size_t _to_write;\n    size_t _pos;\n  public:\n    ChunkPrint(uint8_t* destination, size_t from, size_t len)\n      : _destination(destination), _to_skip(from), _to_write(len), _pos{0} {}\n    virtual ~ChunkPrint(){}\n    size_t write(uint8_t c){\n      if (_to_skip > 0) {\n        _to_skip--;\n        return 1;\n      } else if (_to_write > 0) {\n        _to_write--;\n        _destination[_pos++] = c;\n        return 1;\n      }\n      return 0;\n    }\n    size_t write(const uint8_t *buffer, size_t size)\n    {\n      return this->Print::write(buffer, size);\n    }\n};\n\nclass AsyncJsonResponse: public AsyncAbstractResponse {\n  private:\n\n    DynamicJsonDocument _jsonBuffer;\n\n    JsonVariant _root;\n    bool _isValid;\n\n  public:    \n\n    AsyncJsonResponse(JsonDocument *ref, bool isArray=false) : _jsonBuffer(1), _isValid{false} {\n      _code = 200;\n      _contentType = FPSTR(CONTENT_TYPE_JSON);\n      if(isArray)\n        _root = ref->to<JsonArray>();\n      else\n        _root = ref->to<JsonObject>();\n    }\n\n    AsyncJsonResponse(size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE, bool isArray=false) : _jsonBuffer(maxJsonBufferSize), _isValid{false} {\n      _code = 200;\n      _contentType = FPSTR(CONTENT_TYPE_JSON);\n      if(isArray)\n        _root = _jsonBuffer.createNestedArray();\n      else\n        _root = _jsonBuffer.createNestedObject();\n    }\n\n    ~AsyncJsonResponse() {}\n    JsonVariant & getRoot() { return _root; }\n    bool _sourceValid() const { return _isValid; }\n    size_t setLength() {\n\n      _contentLength = measureJson(_root);\n\n      if (_contentLength) { _isValid = true; }\n      return _contentLength;\n    }\n\n    size_t getSize() { return _root.size(); }\n\n    size_t _fillBuffer(uint8_t *data, size_t len){\n      ChunkPrint dest(data, _sentLength, len);\n\n      serializeJson(_root, dest);\n      return len;\n    }\n};\n\ntypedef std::function<void(AsyncWebServerRequest *request)> ArJsonRequestHandlerFunction;\n\nclass AsyncCallbackJsonWebHandler: public AsyncWebHandler {\nprivate:\nprotected:\n  const String _uri;\n  WebRequestMethodComposite _method;\n  ArJsonRequestHandlerFunction _onRequest;\n  int _contentLength;\n  const size_t maxJsonBufferSize;\n  int _maxContentLength;\npublic:\n\n  AsyncCallbackJsonWebHandler(const String& uri, ArJsonRequestHandlerFunction onRequest, size_t maxJsonBufferSize=DYNAMIC_JSON_DOCUMENT_SIZE) \n  : _uri(uri), _method(HTTP_POST|HTTP_PUT|HTTP_PATCH), _onRequest(onRequest), maxJsonBufferSize(maxJsonBufferSize), _maxContentLength(16384) {}\n  \n  void setMethod(WebRequestMethodComposite method){ _method = method; }\n  void setMaxContentLength(int maxContentLength){ _maxContentLength = maxContentLength; }\n  void onRequest(ArJsonRequestHandlerFunction fn){ _onRequest = fn; }\n\n  virtual bool canHandle(AsyncWebServerRequest *request) override final{\n    if(!_onRequest)\n      return false;\n\n    if(!(_method & request->method()))\n      return false;\n\n    if(_uri.length() && (_uri != request->url() && !request->url().startsWith(_uri+\"/\")))\n      return false;\n\n    request->addInterestingHeader(\"ANY\");\n    return true;\n  }\n\n  virtual void handleRequest(AsyncWebServerRequest *request) override final {\n    if(_onRequest) {\n      if (request->_tempObject != NULL) {\n        _onRequest(request);\n        return;\n      }\n      request->send(_contentLength > _maxContentLength ? 413 : 400);\n    } else {\n      request->send(500);\n    }\n  }\n  virtual void handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) override final {\n  }\n  virtual void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override final {\n    if (_onRequest) {\n      _contentLength = total;\n      if (total > 0 && request->_tempObject == NULL && (int)total < _maxContentLength) {\n        request->_tempObject = malloc(total);\n      }\n      if (request->_tempObject != NULL) {\n        memcpy((uint8_t*)(request->_tempObject) + index, data, len);\n      }\n    }\n  }\n  virtual bool isRequestHandlerTrivial() override final {return _onRequest ? false : true;}\n};\n#endif"
  },
  {
    "path": "wled00/src/dependencies/network/Network.cpp",
    "content": "#include \"Network.h\"\n\nIPAddress NetworkClass::localIP()\n{\n  IPAddress localIP;\n#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET)\n  localIP = ETH.localIP();\n  if (localIP[0] != 0) {\n    return localIP;\n  }\n#endif\n  localIP = WiFi.localIP();\n  if (localIP[0] != 0) {\n    return localIP;\n  }\n\n  return INADDR_NONE;\n}\n\nIPAddress NetworkClass::subnetMask()\n{\n#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET)\n  if (ETH.localIP()[0] != 0) {\n    return ETH.subnetMask();\n  }\n#endif\n  if (WiFi.localIP()[0] != 0) {\n    return WiFi.subnetMask();\n  }\n  return IPAddress(255, 255, 255, 0);\n}\n\nIPAddress NetworkClass::gatewayIP()\n{\n#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET)\n  if (ETH.localIP()[0] != 0) {\n      return ETH.gatewayIP();\n  }\n#endif\n  if (WiFi.localIP()[0] != 0) {\n      return WiFi.gatewayIP();\n  }\n  return INADDR_NONE;\n}\n\nvoid NetworkClass::localMAC(uint8_t* MAC)\n{\n#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET)\n  // ETH.macAddress(MAC); // Does not work because of missing ETHClass:: in ETH.ccp\n\n  // Start work around\n  String macString = ETH.macAddress();\n  char macChar[18];\n  char * octetEnd = macChar;\n\n  strlcpy(macChar, macString.c_str(), 18);\n\n  for (uint8_t i = 0; i < 6; i++) {\n    MAC[i] = (uint8_t)strtol(octetEnd, &octetEnd, 16);\n    octetEnd++;\n  }\n  // End work around\n\n  for (uint8_t i = 0; i < 6; i++) {\n    if (MAC[i] != 0x00) {\n      return;\n    }\n  }\n#endif\n  WiFi.macAddress(MAC);\n  return;\n}\n\nbool NetworkClass::isConnected()\n{\n  return (WiFi.localIP()[0] != 0 && WiFi.status() == WL_CONNECTED) || isEthernet();\n}\n\nbool NetworkClass::isEthernet()\n{\n#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET)\n  return (ETH.localIP()[0] != 0) && ETH.linkUp();\n#endif\n  return false;\n}\n\nNetworkClass Network;"
  },
  {
    "path": "wled00/src/dependencies/network/Network.h",
    "content": "#ifdef ESP8266\n  #include <ESP8266WiFi.h>\n#else // ESP32\n  #include <WiFi.h>\n  #include <ETH.h>\n#endif\n\n#ifndef Network_h\n#define Network_h\n\nclass NetworkClass\n{\npublic:\n  IPAddress localIP();\n  IPAddress subnetMask();\n  IPAddress gatewayIP();\n  void localMAC(uint8_t* MAC);\n  bool isConnected();\n  bool isEthernet();\n};\n\nextern NetworkClass Network;\n\n#endif"
  },
  {
    "path": "wled00/src/dependencies/time/DS1307RTC.cpp",
    "content": "/*\n * DS1307RTC.h - library for DS1307 RTC\n  \n  Copyright (c) Michael Margolis 2009\n  This library is intended to be uses with Arduino Time library functions\n\n  The library is free software; you can redistribute it and/or\n  modify it under the terms of the GNU Lesser General Public\n  License as published by the Free Software Foundation; either\n  version 2.1 of the License, or (at your option) any later version.\n\n  This library is distributed in the hope that it will be useful,\n  but WITHOUT ANY WARRANTY; without even the implied warranty of\n  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n  Lesser General Public License for more details.\n\n  You should have received a copy of the GNU Lesser General Public\n  License along with this library; if not, write to the Free Software\n  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA\n  \n  30 Dec 2009 - Initial release\n  5 Sep 2011 updated for Arduino 1.0\n */\n\n\n#if defined (__AVR_ATtiny84__) || defined(__AVR_ATtiny85__) || (__AVR_ATtiny2313__)\n#include <TinyWireM.h>\n#define Wire TinyWireM\n#else\n#include <Wire.h>\n#endif\n#include \"DS1307RTC.h\"\n\n#define DS1307_CTRL_ID 0x68 \n\n// PUBLIC FUNCTIONS\ntime_t DS1307RTC::get()   // Acquire data from buffer and convert to time_t\n{\n  tmElements_t tm;\n  if (read(tm) == false) return 0;\n  return(makeTime(tm));\n}\n\nbool DS1307RTC::set(time_t t)\n{\n  tmElements_t tm;\n  breakTime(t, tm);\n  return write(tm); \n}\n\n// Acquire data from the RTC chip in BCD format\nbool DS1307RTC::read(tmElements_t &tm)\n{\n  uint8_t sec;\n  Wire.beginTransmission(DS1307_CTRL_ID);\n#if ARDUINO >= 100  \n  Wire.write((uint8_t)0x00); \n#else\n  Wire.send(0x00);\n#endif  \n  if (Wire.endTransmission() != 0) {\n    exists = false;\n    return false;\n  }\n  exists = true;\n\n  // request the 7 data fields   (secs, min, hr, dow, date, mth, yr)\n  Wire.requestFrom(DS1307_CTRL_ID, tmNbrFields);\n  if (Wire.available() < tmNbrFields) return false;\n#if ARDUINO >= 100\n  sec = Wire.read();\n  tm.Second = bcd2dec(sec & 0x7f);   \n  tm.Minute = bcd2dec(Wire.read() );\n  tm.Hour =   bcd2dec(Wire.read() & 0x3f);  // mask assumes 24hr clock\n  tm.Wday = bcd2dec(Wire.read() );\n  tm.Day = bcd2dec(Wire.read() );\n  tm.Month = bcd2dec(Wire.read() );\n  tm.Year = y2kYearToTm((bcd2dec(Wire.read())));\n#else\n  sec = Wire.receive();\n  tm.Second = bcd2dec(sec & 0x7f);   \n  tm.Minute = bcd2dec(Wire.receive() );\n  tm.Hour =   bcd2dec(Wire.receive() & 0x3f);  // mask assumes 24hr clock\n  tm.Wday = bcd2dec(Wire.receive() );\n  tm.Day = bcd2dec(Wire.receive() );\n  tm.Month = bcd2dec(Wire.receive() );\n  tm.Year = y2kYearToTm((bcd2dec(Wire.receive())));\n#endif\n  if (sec & 0x80) return false; // clock is halted\n  return true;\n}\n\nbool DS1307RTC::write(tmElements_t &tm)\n{\n  // To eliminate any potential race conditions,\n  // stop the clock before writing the values,\n  // then restart it after.\n  Wire.beginTransmission(DS1307_CTRL_ID);\n#if ARDUINO >= 100  \n  Wire.write((uint8_t)0x00); // reset register pointer  \n  Wire.write((uint8_t)0x80); // Stop the clock. The seconds will be written last\n  Wire.write(dec2bcd(tm.Minute));\n  Wire.write(dec2bcd(tm.Hour));      // sets 24 hour format\n  Wire.write(dec2bcd(tm.Wday));   \n  Wire.write(dec2bcd(tm.Day));\n  Wire.write(dec2bcd(tm.Month));\n  Wire.write(dec2bcd(tmYearToY2k(tm.Year))); \n#else  \n  Wire.send(0x00); // reset register pointer  \n  Wire.send(0x80); // Stop the clock. The seconds will be written last\n  Wire.send(dec2bcd(tm.Minute));\n  Wire.send(dec2bcd(tm.Hour));      // sets 24 hour format\n  Wire.send(dec2bcd(tm.Wday));   \n  Wire.send(dec2bcd(tm.Day));\n  Wire.send(dec2bcd(tm.Month));\n  Wire.send(dec2bcd(tmYearToY2k(tm.Year)));   \n#endif\n  if (Wire.endTransmission() != 0) {\n    exists = false;\n    return false;\n  }\n  exists = true;\n\n  // Now go back and set the seconds, starting the clock back up as a side effect\n  Wire.beginTransmission(DS1307_CTRL_ID);\n#if ARDUINO >= 100  \n  Wire.write((uint8_t)0x00); // reset register pointer  \n  Wire.write(dec2bcd(tm.Second)); // write the seconds, with the stop bit clear to restart\n#else  \n  Wire.send(0x00); // reset register pointer  \n  Wire.send(dec2bcd(tm.Second)); // write the seconds, with the stop bit clear to restart\n#endif\n  if (Wire.endTransmission() != 0) {\n    exists = false;\n    return false;\n  }\n  exists = true;\n  return true;\n}\n\nunsigned char DS1307RTC::isRunning()\n{\n  Wire.beginTransmission(DS1307_CTRL_ID);\n#if ARDUINO >= 100  \n  Wire.write((uint8_t)0x00); \n#else\n  Wire.send(0x00);\n#endif  \n  Wire.endTransmission();\n\n  // Just fetch the seconds register and check the top bit\n  Wire.requestFrom(DS1307_CTRL_ID, 1);\n#if ARDUINO >= 100\n  return !(Wire.read() & 0x80);\n#else\n  return !(Wire.receive() & 0x80);\n#endif  \n}\n\nvoid DS1307RTC::setCalibration(char calValue)\n{\n  unsigned char calReg = abs(calValue) & 0x1f;\n  if (calValue >= 0) calReg |= 0x20; // S bit is positive to speed up the clock\n  Wire.beginTransmission(DS1307_CTRL_ID);\n#if ARDUINO >= 100  \n  Wire.write((uint8_t)0x07); // Point to calibration register\n  Wire.write(calReg);\n#else  \n  Wire.send(0x07); // Point to calibration register\n  Wire.send(calReg);\n#endif\n  Wire.endTransmission();  \n}\n\nchar DS1307RTC::getCalibration()\n{\n  Wire.beginTransmission(DS1307_CTRL_ID);\n#if ARDUINO >= 100  \n  Wire.write((uint8_t)0x07); \n#else\n  Wire.send(0x07);\n#endif  \n  Wire.endTransmission();\n\n  Wire.requestFrom(DS1307_CTRL_ID, 1);\n#if ARDUINO >= 100\n  unsigned char calReg = Wire.read();\n#else\n  unsigned char calReg = Wire.receive();\n#endif\n  char out = calReg & 0x1f;\n  if (!(calReg & 0x20)) out = -out; // S bit clear means a negative value\n  return out;\n}\n\n// PRIVATE FUNCTIONS\n\n// Convert Decimal to Binary Coded Decimal (BCD)\nuint8_t DS1307RTC::dec2bcd(uint8_t num)\n{\n  return ((num/10 * 16) + (num % 10));\n}\n\n// Convert Binary Coded Decimal (BCD) to Decimal\nuint8_t DS1307RTC::bcd2dec(uint8_t num)\n{\n  return ((num/16 * 10) + (num % 16));\n}\n\nbool DS1307RTC::exists = false;\n\nDS1307RTC RTC = DS1307RTC(); // create an instance for the user\n\n"
  },
  {
    "path": "wled00/src/dependencies/time/DS1307RTC.h",
    "content": "/*\n * DS1307RTC.h - library for DS1307 RTC\n * This library is intended to be uses with Arduino Time library functions\n */\n\n#ifndef DS1307RTC_h\n#define DS1307RTC_h\n\n#include \"TimeLib.h\"\n#include \"Wire.h\"\n\n// library interface description\nclass DS1307RTC\n{\n  // user-accessible \"public\" interface\n  public:\n    DS1307RTC() {}\n    static void begin() { Wire.begin(); }\n    static time_t get();\n    static bool set(time_t t);\n    static bool read(tmElements_t &tm);\n    static bool write(tmElements_t &tm);\n    static bool chipPresent() { return exists; }\n    static unsigned char isRunning();\n    static void setCalibration(char calValue);\n    static char getCalibration();\n\n  private:\n    static bool exists;\n    static uint8_t dec2bcd(uint8_t num);\n    static uint8_t bcd2dec(uint8_t num);\n};\n\n#ifdef RTC\n#undef RTC // workaround for Arduino Due, which defines \"RTC\"...\n#endif\n\nextern DS1307RTC RTC;\n\n#endif\n \n\n"
  },
  {
    "path": "wled00/src/dependencies/time/DateStrings.cpp",
    "content": "/* DateStrings.cpp\n * Definitions for date strings for use with the Time library\n *\n * Updated for Arduino 1.5.7 18 July 2014\n *\n * No memory is consumed in the sketch if your code does not call any of the string methods\n * You can change the text of the strings, make sure the short strings are each exactly 3 characters \n * the long strings can be any length up to the constant dt_MAX_STRING_LEN defined in TimeLib.h\n * \n */\n\n#if defined(__AVR__)\n#include <avr/pgmspace.h>\n#else\n// for compatiblity with Arduino Due and Teensy 3.0 and maybe others?\n#define PROGMEM\n#define PGM_P  const char *\n#define pgm_read_byte(addr) (*(const unsigned char *)(addr))\n#define pgm_read_word(addr) (*(const unsigned char **)(addr))\n#define strcpy_P(dest, src) strcpy((dest), (src))\n#endif\n#include <string.h> // for strcpy_P or strcpy\n#include \"TimeLib.h\"\n \n// the short strings for each day or month must be exactly dt_SHORT_STR_LEN\n#define dt_SHORT_STR_LEN  3 // the length of short strings\n\nstatic char buffer[dt_MAX_STRING_LEN+1];  // must be big enough for longest string and the terminating null\n\nconst char monthStr0[] PROGMEM = \"\";\nconst char monthStr1[] PROGMEM = \"January\";\nconst char monthStr2[] PROGMEM = \"February\";\nconst char monthStr3[] PROGMEM = \"March\";\nconst char monthStr4[] PROGMEM = \"April\";\nconst char monthStr5[] PROGMEM = \"May\";\nconst char monthStr6[] PROGMEM = \"June\";\nconst char monthStr7[] PROGMEM = \"July\";\nconst char monthStr8[] PROGMEM = \"August\";\nconst char monthStr9[] PROGMEM = \"September\";\nconst char monthStr10[] PROGMEM = \"October\";\nconst char monthStr11[] PROGMEM = \"November\";\nconst char monthStr12[] PROGMEM = \"December\";\n\nconst PROGMEM char * const PROGMEM monthNames_P[] =\n{\n    monthStr0,monthStr1,monthStr2,monthStr3,monthStr4,monthStr5,monthStr6,\n    monthStr7,monthStr8,monthStr9,monthStr10,monthStr11,monthStr12\n};\n\nconst char monthShortNames_P[] PROGMEM = \"ErrJanFebMarAprMayJunJulAugSepOctNovDec\";\n\nconst char dayStr0[] PROGMEM = \"Err\";\nconst char dayStr1[] PROGMEM = \"Sunday\";\nconst char dayStr2[] PROGMEM = \"Monday\";\nconst char dayStr3[] PROGMEM = \"Tuesday\";\nconst char dayStr4[] PROGMEM = \"Wednesday\";\nconst char dayStr5[] PROGMEM = \"Thursday\";\nconst char dayStr6[] PROGMEM = \"Friday\";\nconst char dayStr7[] PROGMEM = \"Saturday\";\n\nconst PROGMEM char * const PROGMEM dayNames_P[] =\n{\n   dayStr0,dayStr1,dayStr2,dayStr3,dayStr4,dayStr5,dayStr6,dayStr7\n};\n\nconst char dayShortNames_P[] PROGMEM = \"ErrSunMonTueWedThuFriSat\";\n\n/* functions to return date strings */\n\nchar* monthStr(uint8_t month)\n{\n    strcpy_P(buffer, (PGM_P)pgm_read_word(&(monthNames_P[month])));\n    return buffer;\n}\n\nchar* monthShortStr(uint8_t month)\n{\n   for (int i=0; i < dt_SHORT_STR_LEN; i++)      \n      buffer[i] = pgm_read_byte(&(monthShortNames_P[i+ (month*dt_SHORT_STR_LEN)]));  \n   buffer[dt_SHORT_STR_LEN] = 0;\n   return buffer;\n}\n\nchar* dayStr(uint8_t day) \n{\n   strcpy_P(buffer, (PGM_P)pgm_read_word(&(dayNames_P[day])));\n   return buffer;\n}\n\nchar* dayShortStr(uint8_t day) \n{\n   uint8_t index = day*dt_SHORT_STR_LEN;\n   for (int i=0; i < dt_SHORT_STR_LEN; i++)      \n      buffer[i] = pgm_read_byte(&(dayShortNames_P[index + i]));  \n   buffer[dt_SHORT_STR_LEN] = 0; \n   return buffer;\n}\n"
  },
  {
    "path": "wled00/src/dependencies/time/LICENSE.txt",
    "content": "https://github.com/PaulStoffregen/Time/\n\ntime.c - low level time and date functions\nCopyright (c) Michael Margolis 2009-2014\nThis library is free software; you can redistribute it and/or\nmodify it under the terms of the GNU Lesser General Public\nLicense as published by the Free Software Foundation; either\nversion 2.1 of the License, or (at your option) any later version.\nThis library is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\nLesser General Public License for more details.\nYou should have received a copy of the GNU Lesser General Public\nLicense along with this library; if not, write to the Free Software\nFoundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA\n\n1.0  6  Jan 2010 - initial release\n1.1  12 Feb 2010 - fixed leap year calculation error\n1.2  1  Nov 2010 - fixed setTime bug (thanks to Korman for this)\n1.3  24 Mar 2012 - many edits by Paul Stoffregen: fixed timeStatus() to update\n\t\t\t\t status, updated examples for Arduino 1.0, fixed ARM\n\t\t\t\t compatibility issues, added TimeArduinoDue and TimeTeensy3\n\t\t\t\t examples, add error checking and messages to RTC examples,\n\t\t\t\t add examples to DS1307RTC library.\n1.4  5  Sep 2014 - compatibility with Arduino 1.5.7\n"
  },
  {
    "path": "wled00/src/dependencies/time/Readme.txt",
    "content": "Readme file for Arduino Time Library\n\n! MODIFIED DISTRIBUTION FOR WLED\nAll timekeeping functions are removed, only conversion functions used.\nPlease see https://github.com/PaulStoffregen/Time for the full, original library\n\nTime is a library that provides timekeeping functionality for Arduino.\n\nThe code is derived from the Playground DateTime library but is updated\nto provide an API that is more flexable and easier to use.\n\nA primary goal was to enable date and time functionality that can be used with\na variety of external time sources with minimum differences required in sketch logic.\n\nExample sketches illustrate how similar sketch code can be used with: a Real Time Clock,\ninternet NTP time service, GPS time data, and Serial time messages from a computer\nfor time synchronization.\n\nThe functions available in the library include:\n\nThe time and date functions take a parameter for the time. This prevents\nerrors if the time rolls over between elements. For example, if a new minute begins\nbetween getting the minute and second, the values will be inconsistent. Using the\nfollowing functions eliminates this problem\n  hour(t);          // returns the hour for the given time t\n  minute(t);        // returns the minute for the given time t\n  second(t);        // returns the second for the given time t\n  day(t);           // the day for the given time t\n  weekday(t);       // day of the week for the given time t\n  month(t);         // the month for the given time t\n  year(t);          // the year for the given time t\n\n\nThere are many convenience macros in the time.h file for time constants and conversion\nof time units.\n\nTo use the library, copy the download to the Library directory.\n\nThe Time directory contains the Time library and some example sketches\nillustrating how the library can be used with various time sources:\n\n- TimeSerial.pde shows Arduino as a clock without external hardware.\n  It is synchronized by time messages sent over the serial port.\n  A companion Processing sketch will automatically provide these messages\n  if it is running and connected to the Arduino serial port.\n\n- TimeSerialDateStrings.pde adds day and month name strings to the sketch above\n  Short (3 character) and long strings are available to print the days of\n  the week and names of the months.\n\n- TimeRTC uses a DS1307 real time clock to provide time synchronization.\n  A basic RTC library named DS1307RTC is included in the download.\n  To run this sketch the DS1307RTC library must be installed.\n\n- TimeRTCSet is similar to the above and adds the ability to set the Real Time Clock\n\n- TimeRTCLog demonstrates how to calculate the difference between times.\n  It is a vary simple logger application that monitors events on digtial pins\n  and prints (to the serial port) the time of an event and the time period since\n  the previous event.\n\n- TimeNTP uses the Arduino Ethernet shield to access time using the internet NTP time service.\n  The NTP protocol uses UDP and the UdpBytewise library is required, see:\n  http://bitbucket.org/bjoern/arduino_osc/src/14667490521f/libraries/Ethernet/\n\n- TimeGPS gets time from a GPS\n  This requires the TinyGPS library from Mikal Hart:\n  http://arduiniana.org/libraries/TinyGPS\n\nDifferences between this code and the playground DateTime library\nalthough the Time library is based on the DateTime codebase, the API has changed.\nChanges in the Time library API:\n- time elements are functions returning int (they are variables in DateTime)\n- Years start from 1970\n- days of the week and months start from 1 (they start from 0 in DateTime)\n- DateStrings do not require a seperate library\n- time elements can be accessed non-atomically (in DateTime they are always atomic)\n- function added to automatically sync time with extrnal source\n- localTime and maketime parameters changed, localTime renamed to breakTime\n\nTechnical notes:\n\nInternal system time is based on the standard Unix time_t.\nThe value is the number of seconds since Jan 1 1970.\nSystem time begins at zero when the sketch starts.\n\nThe internal time can be automatically synchronized at regular intervals to an external time source.\nThis is enabled by calling the setSyncProvider(provider) function - the provider argument is\nthe address of a function that returns the current time as a time_t.\nSee the sketches in the examples directory for usage.\n\nThe default interval for re-syncing the time is 5 minutes but can be changed by calling the\nsetSyncInterval( interval) method to set the number of seconds between re-sync attempts.\n\nThe Time library defines a structure for holding time elements that is a compact version of the  C tm structure.\nAll the members of the Arduino tm structure are bytes and the year is offset from 1970.\nConvenience macros provide conversion to and from the Arduino format.\n\nLow level functions to convert between system time and individual time elements are provided:\n  breakTime(time, &tm);  // break time_t into elements stored in tm struct\n  makeTime(&tm);         // return time_t  from elements stored in tm struct\n\nThe DS1307RTC library included in the download provides an example of how a time provider\ncan use the low level functions to interface with the Time library.\n"
  },
  {
    "path": "wled00/src/dependencies/time/Time.cpp",
    "content": "/*\n  time.c - low level time and date functions\n  Copyright (c) Michael Margolis 2009-2014\n\n  This library is free software; you can redistribute it and/or\n  modify it under the terms of the GNU Lesser General Public\n  License as published by the Free Software Foundation; either\n  version 2.1 of the License, or (at your option) any later version.\n\n  This library is distributed in the hope that it will be useful,\n  but WITHOUT ANY WARRANTY; without even the implied warranty of\n  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n  Lesser General Public License for more details.\n\n  You should have received a copy of the GNU Lesser General Public\n  License along with this library; if not, write to the Free Software\n  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA\n  \n  1.0  6  Jan 2010 - initial release\n  1.1  12 Feb 2010 - fixed leap year calculation error\n  1.2  1  Nov 2010 - fixed setTime bug (thanks to Korman for this)\n  1.3  24 Mar 2012 - many edits by Paul Stoffregen: fixed timeStatus() to update\n                     status, updated examples for Arduino 1.0, fixed ARM\n                     compatibility issues, added TimeArduinoDue and TimeTeensy3\n                     examples, add error checking and messages to RTC examples,\n                     add examples to DS1307RTC library.\n  1.4  5  Sep 2014 - compatibility with Arduino 1.5.7\n  2.0  25 May 2021 - removed timing code, only used for conversion between unix and time\n*/\n\n#if ARDUINO >= 100\n#include <Arduino.h> \n#else\n#include <WProgram.h> \n#endif\n\n#include \"TimeLib.h\"\n\nstatic tmElements_t tm;          // a cache of time elements\nstatic time_t cacheTime;   // the time the cache was updated\n\nvoid refreshCache(time_t t) {\n  if (t != cacheTime) {\n    breakTime(t, tm); \n    cacheTime = t; \n  }\n}\n\nint hour(time_t t) { // the hour for the given time\n  refreshCache(t);\n  return tm.Hour;  \n}\n\nint hourFormat12(time_t t) { // the hour for the given time in 12 hour format\n  refreshCache(t);\n  if( tm.Hour == 0 )\n    return 12; // 12 midnight\n  else if( tm.Hour  > 12)\n    return tm.Hour - 12 ;\n  else\n    return tm.Hour ;\n}\n\nuint8_t isAM(time_t t) { // returns true if given time is AM\n  return !isPM(t);  \n}\n\nuint8_t isPM(time_t t) { // returns true if PM\n  return (hour(t) >= 12); \n}\n\nint minute(time_t t) { // the minute for the given time\n  refreshCache(t);\n  return tm.Minute;  \n}\n\nint second(time_t t) {  // the second for the given time\n  refreshCache(t);\n  return tm.Second;\n}\n\nint day(time_t t) { // the day for the given time (0-6)\n  refreshCache(t);\n  return tm.Day;\n}\n\nint weekday(time_t t) {\n  refreshCache(t);\n  return tm.Wday;\n}\n\nint month(time_t t) {  // the month for the given time\n  refreshCache(t);\n  return tm.Month;\n}\n\nint year(time_t t) { // the year for the given time\n  refreshCache(t);\n  return tmYearToCalendar(tm.Year);\n}\n\n/*============================================================================*/\t\n/* functions to convert to and from system time */\n/* These are for interfacing with time services and are not normally needed in a sketch */\n\n// leap year calculator expects year argument as years offset from 1970\n#define LEAP_YEAR(Y)     ( ((1970+Y)>0) && !((1970+Y)%4) && ( ((1970+Y)%100) || !((1970+Y)%400) ) )\n\nstatic  const uint8_t monthDays[]={31,28,31,30,31,30,31,31,30,31,30,31}; // API starts months from 1, this array starts from 0\n \nvoid breakTime(time_t timeInput, tmElements_t &tm){\n// break the given time_t into time components\n// this is a more compact version of the C library localtime function\n// note that year is offset from 1970 !!!\n\n  uint8_t year;\n  uint8_t month, monthLength;\n  uint32_t time;\n  unsigned long days;\n\n  time = (uint32_t)timeInput;\n  tm.Second = time % 60;\n  time /= 60; // now it is minutes\n  tm.Minute = time % 60;\n  time /= 60; // now it is hours\n  tm.Hour = time % 24;\n  time /= 24; // now it is days\n  tm.Wday = ((time + 4) % 7) + 1;  // Sunday is day 1 \n  \n  year = 0;  \n  days = 0;\n  while((unsigned)(days += (LEAP_YEAR(year) ? 366 : 365)) <= time) {\n    year++;\n  }\n  tm.Year = year; // year is offset from 1970 \n  \n  days -= LEAP_YEAR(year) ? 366 : 365;\n  time  -= days; // now it is days in this year, starting at 0\n  \n  days=0;\n  month=0;\n  monthLength=0;\n  for (month=0; month<12; month++) {\n    if (month==1) { // february\n      if (LEAP_YEAR(year)) {\n        monthLength=29;\n      } else {\n        monthLength=28;\n      }\n    } else {\n      monthLength = monthDays[month];\n    }\n    \n    if (time >= monthLength) {\n      time -= monthLength;\n    } else {\n        break;\n    }\n  }\n  tm.Month = month + 1;  // jan is month 1  \n  tm.Day = time + 1;     // day of month\n}\n\ntime_t makeTime(tmElements_t &tm){   \n// assemble time elements into time_t \n// note year argument is offset from 1970 (see macros in time.h to convert to other formats)\n// previous version used full four digit year (or digits since 2000),i.e. 2009 was 2009 or 9\n  \n  int i;\n  uint32_t seconds;\n\n  // seconds from 1970 till 1 jan 00:00:00 of the given year\n  seconds= tm.Year*(SECS_PER_DAY * 365);\n  for (i = 0; i < tm.Year; i++) {\n    if (LEAP_YEAR(i)) {\n      seconds +=  SECS_PER_DAY;   // add extra days for leap years\n    }\n  }\n  \n  // add days for this year, months start from 1\n  for (i = 1; i < tm.Month; i++) {\n    if ( (i == 2) && LEAP_YEAR(tm.Year)) { \n      seconds += SECS_PER_DAY * 29;\n    } else {\n      seconds += SECS_PER_DAY * monthDays[i-1];  //monthDay array starts from 0\n    }\n  }\n  seconds+= (tm.Day-1) * SECS_PER_DAY;\n  seconds+= tm.Hour * SECS_PER_HOUR;\n  seconds+= tm.Minute * SECS_PER_MIN;\n  seconds+= tm.Second;\n  return (time_t)seconds; \n}\n\ntime_t getUnixTime(int hr,int min,int sec,int dy, int mnth, int yr){\n // year can be given as full four digit year or two digits (2010 or 10 for 2010);  \n //it is converted to years since 1970\n  if( yr > 99)\n      yr = yr - 1970;\n  else\n      yr += 30;  \n  tm.Year = yr;\n  tm.Month = mnth;\n  tm.Day = dy;\n  tm.Hour = hr;\n  tm.Minute = min;\n  tm.Second = sec;\n  return makeTime(tm);\n}"
  },
  {
    "path": "wled00/src/dependencies/time/TimeLib.h",
    "content": "/*\n  time.h - low level time and date functions\n*/\n\n/*\n  July 3 2011 - fixed elapsedSecsThisWeek macro (thanks Vincent Valdy for this)\n              - fixed  daysToTime_t macro (thanks maniacbug)\n*/     \n\n#ifndef _Time_h\n#ifdef __cplusplus\n#define _Time_h\n\n#include <inttypes.h>\n#ifndef __AVR__\n#include <sys/types.h> // for __time_t_defined, but avr libc lacks sys/types.h\n#endif\n\n\n#if !defined(__time_t_defined) // avoid conflict with newlib or other posix libc\ntypedef unsigned long time_t;\n#endif\n\n\n// This ugly hack allows us to define C++ overloaded functions, when included\n// from within an extern \"C\", as newlib's sys/stat.h does.  Actually it is\n// intended to include \"time.h\" from the C library (on ARM, but AVR does not\n// have that file at all).  On Mac and Windows, the compiler will find this\n// \"Time.h\" instead of the C library \"time.h\", so we may cause other weird\n// and unpredictable effects by conflicting with the C library header \"time.h\",\n// but at least this hack lets us define C++ functions as intended.  Hopefully\n// nothing too terrible will result from overriding the C library header?!\nextern \"C++\" {\n\ntypedef enum {\n    dowInvalid, dowSunday, dowMonday, dowTuesday, dowWednesday, dowThursday, dowFriday, dowSaturday\n} timeDayOfWeek_t;\n\ntypedef enum {\n    tmSecond, tmMinute, tmHour, tmWday, tmDay,tmMonth, tmYear, tmNbrFields\n} tmByteFields;\t   \n\ntypedef struct  { \n  uint8_t Second; \n  uint8_t Minute; \n  uint8_t Hour; \n  uint8_t Wday;   // day of week, sunday is day 1\n  uint8_t Day;\n  uint8_t Month; \n  uint8_t Year;   // offset from 1970; \n} \ttmElements_t, TimeElements, *tmElementsPtr_t;\n\n//convenience macros to convert to and from tm years \n#define  tmYearToCalendar(Y) ((Y) + 1970)  // full four digit year \n#define  CalendarYrToTm(Y)   ((Y) - 1970)\n#define  tmYearToY2k(Y)      ((Y) - 30)    // offset is from 2000\n#define  y2kYearToTm(Y)      ((Y) + 30)   \n\ntypedef time_t(*getExternalTime)();\n//typedef void  (*setExternalTime)(const time_t); // not used in this version\n\n\n/*==============================================================================*/\n/* Useful Constants */\n#define SECS_PER_MIN  ((time_t)(60UL))\n#define SECS_PER_HOUR ((time_t)(3600UL))\n#define SECS_PER_DAY  ((time_t)(SECS_PER_HOUR * 24UL))\n#define DAYS_PER_WEEK ((time_t)(7UL))\n#define SECS_PER_WEEK ((time_t)(SECS_PER_DAY * DAYS_PER_WEEK))\n#define SECS_PER_YEAR ((time_t)(SECS_PER_WEEK * 52UL))\n#define SECS_YR_2000  ((time_t)(946684800UL)) // the time at the start of y2k\n \n/* Useful Macros for getting elapsed time */\n#define numberOfSeconds(_time_) (_time_ % SECS_PER_MIN)  \n#define numberOfMinutes(_time_) ((_time_ / SECS_PER_MIN) % SECS_PER_MIN) \n#define numberOfHours(_time_) (( _time_% SECS_PER_DAY) / SECS_PER_HOUR)\n#define dayOfWeek(_time_)  ((( _time_ / SECS_PER_DAY + 4)  % DAYS_PER_WEEK)+1) // 1 = Sunday\n#define elapsedDays(_time_) ( _time_ / SECS_PER_DAY)  // this is number of days since Jan 1 1970\n#define elapsedSecsToday(_time_)  (_time_ % SECS_PER_DAY)   // the number of seconds since last midnight \n// The following macros are used in calculating alarms and assume the clock is set to a date later than Jan 1 1971\n// Always set the correct time before settting alarms\n#define previousMidnight(_time_) (( _time_ / SECS_PER_DAY) * SECS_PER_DAY)  // time at the start of the given day\n#define nextMidnight(_time_) ( previousMidnight(_time_)  + SECS_PER_DAY )   // time at the end of the given day \n#define elapsedSecsThisWeek(_time_)  (elapsedSecsToday(_time_) +  ((dayOfWeek(_time_)-1) * SECS_PER_DAY) )   // note that week starts on day 1\n#define previousSunday(_time_)  (_time_ - elapsedSecsThisWeek(_time_))      // time at the start of the week for the given time\n#define nextSunday(_time_) ( previousSunday(_time_)+SECS_PER_WEEK)          // time at the end of the week for the given time\n\n\n/* Useful Macros for converting elapsed time to a time_t */\n#define minutesToTime_t ((M)) ( (M) * SECS_PER_MIN)  \n#define hoursToTime_t   ((H)) ( (H) * SECS_PER_HOUR)  \n#define daysToTime_t    ((D)) ( (D) * SECS_PER_DAY) // fixed on Jul 22 2011\n#define weeksToTime_t   ((W)) ( (W) * SECS_PER_WEEK)   \n\n/*============================================================================*/\n/*  time and date functions   */ \nint     hour(time_t t);    // the hour for the given time\nint     hourFormat12(time_t t); // the hour for the given time in 12 hour format\nuint8_t isAM(time_t t);    // returns true the given time is AM\nuint8_t isPM(time_t t);    // returns true the given time is PM\nint     minute(time_t t);  // the minute for the given time\nint     second(time_t t);  // the second for the given time\nint     day(time_t t);     // the day for the given time\nint     weekday(time_t t); // the weekday for the given time \nint     month(time_t t);   // the month for the given time\nint     year(time_t t);    // the year for the given time\n\ntime_t\tgetUnixTime(int hr,int min,int sec,int day, int month, int yr); //added by Aircoookie to get epoch time\n\n/* date strings */ \n#define dt_MAX_STRING_LEN 9 // length of longest date string (excluding terminating null)\nchar* monthStr(uint8_t month);\nchar* dayStr(uint8_t day);\nchar* monthShortStr(uint8_t month);\nchar* dayShortStr(uint8_t day);\n\n/* low level functions to convert to and from system time */\nvoid breakTime(time_t time, tmElements_t &tm);  // break time_t into elements\ntime_t makeTime(tmElements_t &tm);  // convert time elements into time_t\n\n} // extern \"C++\"\n#endif // __cplusplus\n#endif /* _Time_h */\n\n"
  },
  {
    "path": "wled00/src/dependencies/time/library.json",
    "content": "{\n    \"name\": \"Time\",\n    \"description\": \"Time keeping library\",\n    \"keywords\": \"Time, date, hour, minute, second, day, week, month, year, RTC\",\n    \"authors\": [\n        {\n            \"name\": \"Michael Margolis\"\n        },\n        {\n            \"name\": \"Paul Stoffregen\",\n            \"email\": \"paul@pjrc.com\",\n            \"url\": \"http://www.pjrc.com\",\n            \"maintainer\": true\n        }\n    ],\n    \"repository\": {\n        \"type\": \"git\",\n         \"url\": \"https://github.com/PaulStoffregen/Time\"\n    },\n    \"version\": \"1.5\",\n    \"homepage\": \"http://playground.arduino.cc/Code/Time\",\n    \"frameworks\": \"Arduino\",\n    \"examples\": [\n        \"examples/*/*.ino\"\n    ]\n}\n"
  },
  {
    "path": "wled00/src/dependencies/time/library.properties",
    "content": "name=Time\nversion=1.5\nauthor=Michael Margolis\nmaintainer=Paul Stoffregen\nsentence=Timekeeping functionality for Arduino\nparagraph=Date and Time functions, with provisions to synchronize to external time sources like GPS and NTP (Internet).  This library is often used together with TimeAlarms and DS1307RTC.\ncategory=Timing\nurl=http://playground.arduino.cc/code/time\narchitectures=*\n\n"
  },
  {
    "path": "wled00/src/dependencies/timezone/LICENSE.md",
    "content": "#Arduino Timezone Library v1.0\nhttps://github.com/JChristensen/Timezone  \nLICENSE file  \nJack Christensen Mar 2012  \n\n![CC BY-SA](http://mirrors.creativecommons.org/presskit/buttons/88x31/png/by-sa.png)\n##CC BY-SA\nArduino Timezone Library by Jack Christensen is licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to:  \nCreative Commons  \n444 Castro Street, Suite 900  \nMountain View, CA 94041  \n"
  },
  {
    "path": "wled00/src/dependencies/timezone/ReadMe.md",
    "content": "#Arduino Timezone Library v1.0\nhttps://github.com/JChristensen/Timezone  \nReadMe file  \nJack Christensen Mar 2012  \n\n![CC BY-SA](http://mirrors.creativecommons.org/presskit/buttons/80x15/png/by-sa.png)\n\n##Introduction\nThe **Timezone** library is designed to work in conjunction with the [Arduino Time library](http://www.arduino.cc/playground/Code/Time).  The Time library must be installed and referenced in your sketch with `#include <Time.h>`.  This documentation assumes some familiarity with the Time library.\n\nThe primary aim of the **Timezone** library is to convert Universal Coordinated Time (UTC) to the correct local time, whether it is daylight saving time (a.k.a. summer time) or standard time. The time source could be a GPS receiver, an NTP server, or a Real-Time Clock (RTC) set to UTC.  But whether a hardware RTC or other time source is even present is immaterial; although the Time library can function as a software RTC without additional hardware, its accuracy is dependent on the accuracy of the microcontroller's system clock.\n\nThe **Timezone** library implements two objects to facilitate time zone conversions:\n- A **TimeChangeRule** object describes when local time changes to daylight (summer) time, or to standard time, for a particular locale.\n- A **Timezone** object uses **TimeChangeRule**s to perform conversions and related functions.  It can also write its **TimeChangeRule**s to EEPROM, or read them from EEPROM.  Multiple time zones can be represented by defining multiple **Timezone** objects.\n\n##Installation\nTo use the **Timezone** library:  \n- Go to https://github.com/JChristensen/Timezone and click the **Download ZIP** button to download the repository as a ZIP file to a convenient location on your PC.\n- Uncompress the downloaded file.  This will result in a folder containing all the files for the library, that has a name that includes the branch name, for example **Timezone-master**.\n- Rename the folder to just **Timezone**.\n- Copy the renamed folder to the Arduino sketchbook\\libraries folder.\n\n##Examples\nThe following example sketches are included with the **Timezone** library:\n- **Clock:** A simple self-adjusting clock for a single time zone.  **TimeChangeRule**s may be optionally read from EEPROM.\n- **HardwareRTC:** A self-adjusting clock for one time zone using an external real-time clock, either a DS1307 or DS3231 (e.g. Chronodot) which is set to UTC.  \n- **WorldClock:** A self-adjusting clock for multiple time zones.\n- **WriteRules:** A sketch to write **TimeChangeRule**s to EEPROM.\n\n##Coding TimeChangeRules\nNormally these will be coded in pairs for a given time zone: One rule to describe when daylight (summer) time starts, and one to describe when standard time starts.\n\nAs an example, here in the Eastern US time zone, Eastern Daylight Time (EDT) starts on the 2nd Sunday in March at 02:00 local time. Eastern Standard Time (EST) starts on the 1st Sunday in November at 02:00 local time.\n\nDefine a **TimeChangeRule** as follows:\n\n`TimeChangeRule myRule = {abbrev, week, dow, month, hour, offset};`\n\nWhere:\n\n**abbrev** is a character string abbreviation for the time zone; it must be no longer than five characters.\n\n**week** is the week of the month that the rule starts.\n\n**dow** is the day of the week that the rule starts.\n\n**hour** is the hour in local time that the rule starts (0-23).\n\n**offset** is the UTC offset _in minutes_ for the time zone being defined.\n\nFor convenience, the following symbolic names can be used:\n\n**week:** First, Second, Third, Fourth, Last  \n**dow:** Sun, Mon, Tue, Wed, Thu, Fri, Sat  \n**month:** Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec\n\nFor the Eastern US time zone, the **TimeChangeRule**s could be defined as follows:\n\n```c++\nTimeChangeRule usEDT = {\"EDT\", Second, Sun, Mar, 2, -240};  //UTC - 4 hours\nTimeChangeRule usEST = {\"EST\", First, Sun, Nov, 2, -300};   //UTC - 5 hours\n```\n\nFor a time zone that does not change to daylight/summer time, pass the same rule twice to the constructor, for example:  \n`Timezone usAZ(usMST, usMST);`\n\n##Coding Timezone objects\nThere are two ways to define **Timezone** objects.\n\nBy first defining **TimeChangeRule**s (as above) and giving the daylight time rule and the standard time rule (assuming usEDT and usEST defined as above):  \n`Timezone usEastern(usEDT, usEST);`\n\nBy reading rules previously stored in EEPROM.  This reads both the daylight and standard time rules previously stored at EEPROM address 100:  \n`Timezone usPacific(100);`\n\nNote that **TimeChangeRule**s require 12 bytes of storage each, so the pair of rules associated with a Timezone object requires 24 bytes total.  This could possibly change in future versions of the library.  The size of a **TimeChangeRule** can be checked with `sizeof(usEDT)`.\n\n##Timezone library methods\nNote that the `time_t` data type is defined by the Arduino Time library <Time.h>. See the [Time library documentation](http://www.arduino.cc/playground/Code/Time) for additional details.\n\n###time_t toLocal(time_t utc);\n#####Description\nConverts the given UTC time to local time, standard or daylight as appropriate.\n#####Syntax\n`myTZ.toLocal(utc);`\n#####Parameters\n***utc:*** Universal Coordinated Time *(time_t)*  \n#####Returns\nLocal time *(time_t)*  \n#####Example\n```c++\ntime_t eastern, utc;\nTimeChangeRule usEDT = {\"EDT\", Second, Sun, Mar, 2, -240};  //UTC - 4 hours\nTimeChangeRule usEST = {\"EST\", First, Sun, Nov, 2, -300};   //UTC - 5 hours\nTimezone usEastern(usEDT, usEST);\nutc = now();\t//current time from the Time Library\neastern = usEastern.toLocal(utc);\n```\n\n###time_t toLocal(time_t utc, TimeChangeRule **tcr);\n#####Description\nAs above, converts the given UTC time to local time, and also returns a pointer to the **TimeChangeRule** that was applied to do the conversion. This could then be used, for example, to include the time zone abbreviation as part of a time display.  The caller must take care not to alter the pointed **TimeChangeRule**, as this will then result in incorrect conversions.\n#####Syntax\n`myTZ.toLocal(utc, &tcr);`  \n#####Parameters\n***utc:*** Universal Coordinated Time *(time_t)*  \n***tcr:*** Address of a pointer to a **TimeChangeRule** _(\\*\\*TimeChangeRule)_   \n#####Returns\nLocal time *(time_t)*  \nPointer to **TimeChangeRule**  _(\\*\\*TimeChangeRule)_    \n#####Example\n```c++\ntime_t eastern, utc;\nTimeChangeRule *tcr;\nTimeChangeRule usEDT = {\"EDT\", Second, Sun, Mar, 2, -240};  //UTC - 4 hours\nTimeChangeRule usEST = {\"EST\", First, Sun, Nov, 2, -300};   //UTC - 5 hours\nTimezone usEastern(usEDT, usEST);\nutc = now();\t//current time from the Time Library\neastern = usEastern.toLocal(utc, &tcr);\nSerial.print(\"The time zone is: \");\nSerial.println(tcr -> abbrev);\n```\n\n###boolean utcIsDST(time_t utc);\n###boolean locIsDST(time_t local);\n#####Description\nThese functions determine whether a given UTC time or a given local time is within the daylight saving (summer) time interval, and return true or false accordingly.\n#####Syntax\n`utcIsDST(utc);`  \n`locIsDST(local);`  \n#####Parameters\n***utc:*** Universal Coordinated Time *(time_t)*  \n***local:*** Local Time *(time_t)*  \n#####Returns\ntrue or false *(boolean)*\n#####Example\n`if (usEastern.utcIsDST(utc)) { /*do something*/ }`\n\n###void readRules(int address);\n###void writeRules(int address);\n#####Description\nThese functions read or write a **Timezone** object's two **TimeChangeRule**s from or to EEPROM.\n#####Syntax\n`myTZ.readRules(address);`  \n`myTZ.writeRules(address);`  \n#####Parameters\n***address:*** The beginning EEPROM address to write to or read from *(int)*\n#####Returns\nNone.\n#####Example\n`usEastern.writeRules(100);  //write rules beginning at EEPROM address 100`\n\n###time_t toUTC(time_t local);\n#####Description\nConverts the given local time to UTC time.\n\n**WARNING:** This function is provided for completeness, but should seldom be needed and should be used sparingly and carefully.\n\nAmbiguous situations occur after the Standard-to-DST and the DST-to-Standard time transitions. When changing to DST, there is one hour of local time that does not exist, since the clock moves forward one hour. Similarly, when changing to standard time, there is one hour of local time that occurs twice since the clock moves back one hour.\n\nThis function does not test whether it is passed an erroneous time value during the Local-to-DST transition that does not exist. If passed such a time, an incorrect UTC time value will be returned.\n\nIf passed a local time value during the DST-to-Local transition that occurs twice, it will be treated as the earlier time, i.e. the time that occurs before the transition.\n\nCalling this function with local times during a transition interval should be\navoided!\n#####Syntax\n`myTZ.toUTC(local);`\n#####Parameters\n***local:*** Local Time *(time_t)*  \n#####Returns\nUTC *(time_t)*  \n"
  },
  {
    "path": "wled00/src/dependencies/timezone/Timezone.cpp",
    "content": "/*----------------------------------------------------------------------*\n * Arduino Timezone Library v1.0                                        *\n * Jack Christensen Mar 2012                                            *\n *                                                                      *\n * This work is licensed under the Creative Commons Attribution-        *\n * ShareAlike 3.0 Unported License. To view a copy of this license,     *\n * visit http://creativecommons.org/licenses/by-sa/3.0/ or send a       *\n * letter to Creative Commons, 171 Second Street, Suite 300,            *\n * San Francisco, California, 94105, USA.                               *\n *----------------------------------------------------------------------*/\n\n#include \"Timezone.h\"\n\n//THIS LINE WAS ADDED FOR COMPATIBILITY WITH THE WLED DEPENDENCY STRUCTURE. REMOVE IF YOU USE IT OUTSIDE OF WLED!\n#include \"../time/TimeLib.h\"\n\n#ifdef __AVR__\n\t#include <avr/eeprom.h>\n#endif\n\n\n/*----------------------------------------------------------------------*\n * Create a Timezone object from the given time change rules.           *\n *----------------------------------------------------------------------*/\nTimezone::Timezone(TimeChangeRule dstStart, TimeChangeRule stdStart)\n{\n    _dst = dstStart;\n    _std = stdStart;\n}\n\n#ifdef __AVR__\n/*----------------------------------------------------------------------*\n * Create a Timezone object from time change rules stored in EEPROM     *\n * at the given address.                                                *\n *----------------------------------------------------------------------*/\nTimezone::Timezone(int address)\n{\n    readRules(address);\n}\n#endif\n\n/*----------------------------------------------------------------------*\n * Convert the given UTC time to local time, standard or                *\n * daylight time, as appropriate.                                       *\n *----------------------------------------------------------------------*/\ntime_t Timezone::toLocal(time_t utc)\n{\n    //recalculate the time change points if needed\n    if (year(utc) != year(_dstUTC)) calcTimeChanges(year(utc));\n\n    if (utcIsDST(utc))\n        return utc + _dst.offset * SECS_PER_MIN;\n    else\n        return utc + _std.offset * SECS_PER_MIN;\n}\n\n/*----------------------------------------------------------------------*\n * Convert the given UTC time to local time, standard or                *\n * daylight time, as appropriate, and return a pointer to the time      *\n * change rule used to do the conversion. The caller must take care     *\n * not to alter this rule.                                              *\n *----------------------------------------------------------------------*/\ntime_t Timezone::toLocal(time_t utc, TimeChangeRule **tcr)\n{\n    //recalculate the time change points if needed\n    if (year(utc) != year(_dstUTC)) calcTimeChanges(year(utc));\n\n    if (utcIsDST(utc)) {\n        *tcr = &_dst;\n        return utc + _dst.offset * SECS_PER_MIN;\n    }\n    else {\n        *tcr = &_std;\n        return utc + _std.offset * SECS_PER_MIN;\n    }\n}\n\n/*----------------------------------------------------------------------*\n * Convert the given local time to UTC time.                            *\n *                                                                      *\n * WARNING:                                                             *\n * This function is provided for completeness, but should seldom be     *\n * needed and should be used sparingly and carefully.                   *\n *                                                                      *\n * Ambiguous situations occur after the Standard-to-DST and the         *\n * DST-to-Standard time transitions. When changing to DST, there is     *\n * one hour of local time that does not exist, since the clock moves    *\n * forward one hour. Similarly, when changing to standard time, there   *\n * is one hour of local times that occur twice since the clock moves    *\n * back one hour.                                                       *\n *                                                                      *\n * This function does not test whether it is passed an erroneous time   *\n * value during the Local -> DST transition that does not exist.        *\n * If passed such a time, an incorrect UTC time value will be returned. *\n *                                                                      *\n * If passed a local time value during the DST -> Local transition      *\n * that occurs twice, it will be treated as the earlier time, i.e.      *\n * the time that occurs before the transistion.                         *\n *                                                                      *\n * Calling this function with local times during a transition interval  *\n * should be avoided!                                                   *\n *----------------------------------------------------------------------*/\ntime_t Timezone::toUTC(time_t local)\n{\n    //recalculate the time change points if needed\n    if (year(local) != year(_dstLoc)) calcTimeChanges(year(local));\n\n    if (locIsDST(local))\n        return local - _dst.offset * SECS_PER_MIN;\n    else\n        return local - _std.offset * SECS_PER_MIN;\n}\n\n/*----------------------------------------------------------------------*\n * Determine whether the given UTC time_t is within the DST interval    *\n * or the Standard time interval.                                       *\n *----------------------------------------------------------------------*/\nboolean Timezone::utcIsDST(time_t utc)\n{\n    //recalculate the time change points if needed\n    if (year(utc) != year(_dstUTC)) calcTimeChanges(year(utc));\n\n    if (_stdUTC > _dstUTC)    //northern hemisphere\n        return (utc >= _dstUTC && utc < _stdUTC);\n    else                      //southern hemisphere\n        return !(utc >= _stdUTC && utc < _dstUTC);\n}\n\n/*----------------------------------------------------------------------*\n * Determine whether the given Local time_t is within the DST interval  *\n * or the Standard time interval.                                       *\n *----------------------------------------------------------------------*/\nboolean Timezone::locIsDST(time_t local)\n{\n    //recalculate the time change points if needed\n    if (year(local) != year(_dstLoc)) calcTimeChanges(year(local));\n\n    if (_stdLoc > _dstLoc)    //northern hemisphere\n        return (local >= _dstLoc && local < _stdLoc);\n    else                      //southern hemisphere\n        return !(local >= _stdLoc && local < _dstLoc);\n}\n\n/*----------------------------------------------------------------------*\n * Calculate the DST and standard time change points for the given      *\n * given year as local and UTC time_t values.                           *\n *----------------------------------------------------------------------*/\nvoid Timezone::calcTimeChanges(int yr)\n{\n    _dstLoc = toTime_t(_dst, yr);\n    _stdLoc = toTime_t(_std, yr);\n    _dstUTC = _dstLoc - _std.offset * SECS_PER_MIN;\n    _stdUTC = _stdLoc - _dst.offset * SECS_PER_MIN;\n}\n\n/*----------------------------------------------------------------------*\n * Convert the given DST change rule to a time_t value                  *\n * for the given year.                                                  *\n *----------------------------------------------------------------------*/\ntime_t Timezone::toTime_t(TimeChangeRule r, int yr)\n{\n    tmElements_t tm;\n    time_t t;\n    uint8_t m, w;            //temp copies of r.month and r.week\n\n    m = r.month;\n    w = r.week;\n    if (w == 0) {            //Last week = 0\n        if (++m > 12) {      //for \"Last\", go to the next month\n            m = 1;\n            yr++;\n        }\n        w = 1;               //and treat as first week of next month, subtract 7 days later\n    }\n\n    tm.Hour = r.hour;\n    tm.Minute = 0;\n    tm.Second = 0;\n    tm.Day = 1;\n    tm.Month = m;\n    tm.Year = yr - 1970;\n    t = makeTime(tm);        //first day of the month, or first day of next month for \"Last\" rules\n    t += (7 * (w - 1) + (r.dow - weekday(t) + 7) % 7) * SECS_PER_DAY;\n    if (r.week == 0) t -= 7 * SECS_PER_DAY;    //back up a week if this is a \"Last\" rule\n    return t;\n}\n\n#ifdef __AVR__\n/*----------------------------------------------------------------------*\n * Read the daylight and standard time rules from EEPROM at\t\t\t\t*\n * the given address.                                                   *\n *----------------------------------------------------------------------*/\nvoid Timezone::readRules(int address)\n{\n    eeprom_read_block((void *) &_dst, (void *) address, sizeof(_dst));\n    address += sizeof(_dst);\n    eeprom_read_block((void *) &_std, (void *) address, sizeof(_std));\n}\n\n/*----------------------------------------------------------------------*\n * Write the daylight and standard time rules to EEPROM at\t\t\t\t*\n * the given address.                                                   *\n *----------------------------------------------------------------------*/\nvoid Timezone::writeRules(int address)\n{\n    eeprom_write_block((void *) &_dst, (void *) address, sizeof(_dst));\n    address += sizeof(_dst);\n    eeprom_write_block((void *) &_std, (void *) address, sizeof(_std));\n}\n\n#endif"
  },
  {
    "path": "wled00/src/dependencies/timezone/Timezone.h",
    "content": "/*----------------------------------------------------------------------*\n * Arduino Timezone Library v1.0                                        *\n * Jack Christensen Mar 2012                                            *\n *                                                                      *\n * This work is licensed under the Creative Commons Attribution-        *\n * ShareAlike 3.0 Unported License. To view a copy of this license,     *\n * visit http://creativecommons.org/licenses/by-sa/3.0/ or send a       *\n * letter to Creative Commons, 171 Second Street, Suite 300,            *\n * San Francisco, California, 94105, USA.                               *\n *----------------------------------------------------------------------*/ \n \n#ifndef Timezone_h\n#define Timezone_h\n#if ARDUINO >= 100\n#include <Arduino.h> \n#else\n#include <WProgram.h> \n#endif\n\n#include \"../time/TimeLib.h\"      //http://www.arduino.cc/playground/Code/Time\n\n//convenient constants for dstRules\nenum week_t {Last, First, Second, Third, Fourth}; \nenum dow_t {Sun=1, Mon, Tue, Wed, Thu, Fri, Sat};\nenum month_t {Jan=1, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec};\n\n//structure to describe rules for when daylight/summer time begins,\n//or when standard time begins.\nstruct TimeChangeRule\n{\n    uint8_t week;      //First, Second, Third, Fourth, or Last week of the month\n    uint8_t dow;       //day of week, 1=Sun, 2=Mon, ... 7=Sat\n    uint8_t month;     //1=Jan, 2=Feb, ... 12=Dec\n    uint8_t hour;      //0-23\n    int16_t offset;        //offset from UTC in minutes\n};\n        \nclass Timezone\n{\n    public:\n        Timezone(TimeChangeRule dstStart, TimeChangeRule stdStart);\n        Timezone(int address);\n        time_t toLocal(time_t utc);\n        time_t toLocal(time_t utc, TimeChangeRule **tcr);\n        time_t toUTC(time_t local);\n        boolean utcIsDST(time_t utc);\n        boolean locIsDST(time_t local);\n        void readRules(int address);\n        void writeRules(int address);\n\n    private:\n        void calcTimeChanges(int yr);\n        time_t toTime_t(TimeChangeRule r, int yr);\n        TimeChangeRule _dst;    //rule for start of dst or summer time for any year\n        TimeChangeRule _std;    //rule for start of standard time for any year\n        time_t _dstUTC;         //dst start for given/current year, given in UTC\n        time_t _stdUTC;         //std time start for given/current year, given in UTC\n        time_t _dstLoc;         //dst start for given/current year, given in local time\n        time_t _stdLoc;         //std time start for given/current year, given in local time\n};\n#endif\n"
  },
  {
    "path": "wled00/src/dependencies/toki/Toki.h",
    "content": "/*\n  Toki.h - Minimal millisecond accurate timekeeping.\n\n  LICENSE\n  The MIT License (MIT)\n  Copyright (c) 2021 Christian Schwinne\n  Permission is hereby granted, free of charge, to any person obtaining a copy\n  of this software and associated documentation files (the \"Software\"), to deal\n  in the Software without restriction, including without limitation the rights\n  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n  copies of the Software, and to permit persons to whom the Software is\n  furnished to do so, subject to the following conditions:\n  The above copyright notice and this permission notice shall be included in\n  all copies or substantial portions of the Software.\n  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n  THE SOFTWARE.\n*/\n\n#include <Arduino.h> \n\n#define YEARS_70 2208988800UL\n\n#define TOKI_NO_MS_ACCURACY 1000\n\n//Time source. Sub-100 is second accuracy, higher ms accuracy. Higher value generally means more accurate\n#define TOKI_TS_NONE      0 //unsynced (e.g. just after boot)\n#define TOKI_TS_UDP       5 //synced via UDP from an instance whose time source is unsynced\n#define TOKI_TS_BAD      10 //synced from a time source less than about +- 2s accurate\n#define TOKI_TS_UDP_SEC  20 //synced via UDP from an instance whose time source is set from RTC/JSON\n#define TOKI_TS_SEC      40 //general second-accurate time source\n#define TOKI_TS_RTC      60 //second-accurate real time clock\n#define TOKI_TS_JSON     70 //synced second-accurate from a client via JSON-API\n\n#define TOKI_TS_UDP_NTP 110 //synced via UDP from an instance whose time source is NTP\n#define TOKI_TS_MS      120 //general better-than-second accuracy time source\n#define TOKI_TS_NTP     150 //NTP time, simple round trip estimation. Depending on network, mostly +- 50ms accurate\n#define TOKI_TS_NTP_P   170 //NTP time with multi-step sync, higher accuracy. Not implemented in WLED\n\nclass Toki {\n  typedef enum {\n    inactive, marked, active\n  } TickT;\n\n  public: \n  typedef struct {\n    uint32_t sec;\n    uint16_t ms;\n  } Time;\n\n  private:\n    uint32_t fullSecondMillis = 0;\n    uint32_t unix = 0;\n    TickT tick = TickT::inactive;\n    uint8_t timeSrc = TOKI_TS_NONE;\n\n  public:\n    void setTime(Time t, uint8_t timeSource = TOKI_TS_MS) {\n      fullSecondMillis = millis() - t.ms;\n      unix = t.sec;\n      timeSrc = timeSource;\n    }\n\n    void setTime(uint32_t sec, uint16_t ms=TOKI_NO_MS_ACCURACY, uint8_t timeSource = TOKI_TS_MS) {\n      if (ms >= TOKI_NO_MS_ACCURACY) {\n        ms = millisecond(); //just keep current ms if not provided\n        if (timeSource > 99) timeSource = TOKI_TS_SEC; //lies\n      }\n      Time t = {sec, ms};\n      setTime(t, timeSource);\n    }\n\n    Time fromNTP(byte *timestamp) { //ntp timestamp is 8 bytes, 4 bytes second and 4 bytes sub-second fraction\n      unsigned long highWord = word(timestamp[0], timestamp[1]);\n      unsigned long lowWord = word(timestamp[2], timestamp[3]);\n    \n      unsigned long unix = highWord << 16 | lowWord;\n      if (!unix) return {0,0};\n      unix -= YEARS_70; //NTP begins 1900, Unix 1970\n\n      unsigned long frac = word(timestamp[4], timestamp[5]); //65536ths of a second\n      frac = (frac*1000) >> 16; //convert to ms\n      return {unix, (uint16_t)frac};\n    }\n\n    uint16_t millisecond() {\n      uint32_t ms = millis() - fullSecondMillis;\n      while (ms > 999) {\n        ms -= 1000;\n        fullSecondMillis += 1000;\n        unix++;\n        if (tick == TickT::inactive) tick = TickT::marked; //marked, will be active on next loop\n      }\n      return ms;\n    }\n\n    uint32_t second() {\n      millisecond();\n      return unix;\n    }\n\n    //gets the absolute difference between two timestamps in milliseconds\n    uint32_t msDifference(const Time &t0, const Time &t1) {\n      bool t1BiggerSec = (t1.sec > t0.sec);\n      uint32_t secDiff = (t1BiggerSec) ? t1.sec - t0.sec : t0.sec - t1.sec;\n      uint32_t t0ms = t0.ms, t1ms = t1.ms;\n      if (t1BiggerSec) t1ms += secDiff*1000;\n      else t0ms += secDiff*1000;\n      uint32_t msDiff = (t1ms > t0ms) ? t1ms - t0ms : t0ms - t1ms;\n      return msDiff;\n    }\n\n    //return true if t1 is later than t0\n    bool isLater(const Time &t0, const Time &t1) {\n      if (t1.sec > t0.sec) return true;\n      if (t1.sec < t0.sec) return false;\n      if (t1.ms  > t0.ms) return true;\n      return false;\n    }\n\n    void adjust(Time&t, int32_t offset) {\n      int32_t secs = offset /1000;\n      int32_t ms = offset - secs*1000;\n      t.sec += secs;\n      int32_t nms = t.ms + ms;\n      if (nms > 1000) {nms -= 1000; t.sec++;}\n      if (nms < 0) {nms += 1000; t.sec--;}\n      t.ms = nms;\n    }\n\n    Time getTime() {\n      Time t;\n      t.ms = millisecond();\n      t.sec = unix;\n      return t;\n    }\n\n    uint8_t getTimeSource() {\n      return timeSrc;\n    }\n\n    void setTick() {\n      if (tick == TickT::marked) tick = TickT::active;\n    }\n\n    void resetTick() {\n      if (tick == TickT::active) tick = TickT::inactive;\n    }\n\n    bool isTick() {\n      return (tick == TickT::active);\n    }\n\n    void printTime(const Time& t, Print &dest = Serial) {\n      dest.printf_P(PSTR(\"%u,%03u\\n\"),t.sec,t.ms);\n    }\n};"
  },
  {
    "path": "wled00/src/dependencies/ws2812fx/LICENSE.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2016 Harm Aldick\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "wled00/src/dependencies/ws2812fx/readme.txt",
    "content": "https://github.com/kitesurfer1404/WS2812FX/\n\nThe WS2812FX implementation was heavily altered and differs from its master branch.\nDue to regular changes to the library code it is kept in the source dir for now.\n"
  },
  {
    "path": "wled00/src/font/console_font_4x6.h",
    "content": "// font courtesy of https://github.com/idispatch/raster-fonts\nstatic const unsigned char console_font_4x6[] PROGMEM = {\n\n// code points 0-31 and 127-255 are commented out to save memory, they contain extra characters (CP437),\n// which could be used with an UTF-8 to CP437 conversion\n\n    // /*\n    //  * code=0, hex=0x00, ascii=\"^@\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=1, hex=0x01, ascii=\"^A\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x50,  /* 0101 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=2, hex=0x02, ascii=\"^B\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=3, hex=0x03, ascii=\"^C\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x70,  /* 0111 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=4, hex=0x04, ascii=\"^D\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x70,  /* 0111 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=5, hex=0x05, ascii=\"^E\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x70,  /* 0111 */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=6, hex=0x06, ascii=\"^F\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=7, hex=0x07, ascii=\"^G\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=8, hex=0x08, ascii=\"^H\"\n    //  */\n    // 0xF0,  /* 1111 */\n    // 0xF0,  /* 1111 */\n    // 0xD0,  /* 1101 */\n    // 0xF0,  /* 1111 */\n    // 0xF0,  /* 1111 */\n    // 0xF0,  /* 1111 */\n\n    // /*\n    //  * code=9, hex=0x09, ascii=\"^I\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x70,  /* 0111 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=10, hex=0x0A, ascii=\"^J\"\n    //  */\n    // 0xF0,  /* 1111 */\n    // 0x80,  /* 1000 */\n    // 0xA0,  /* 1010 */\n    // 0x80,  /* 1000 */\n    // 0xF0,  /* 1111 */\n    // 0xF0,  /* 1111 */\n\n    // /*\n    //  * code=11, hex=0x0B, ascii=\"^K\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x30,  /* 0011 */\n    // 0x10,  /* 0001 */\n    // 0x60,  /* 0110 */\n    // 0x60,  /* 0110 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=12, hex=0x0C, ascii=\"^L\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x50,  /* 0101 */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=13, hex=0x0D, ascii=\"^M\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x30,  /* 0011 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x60,  /* 0110 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=14, hex=0x0E, ascii=\"^N\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x30,  /* 0011 */\n    // 0x50,  /* 0101 */\n    // 0x10,  /* 0001 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=15, hex=0x0F, ascii=\"^O\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=16, hex=0x10, ascii=\"^P\"\n    //  */\n    // 0x40,  /* 0100 */\n    // 0x60,  /* 0110 */\n    // 0x70,  /* 0111 */\n    // 0x60,  /* 0110 */\n    // 0x40,  /* 0100 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=17, hex=0x11, ascii=\"^Q\"\n    //  */\n    // 0x10,  /* 0001 */\n    // 0x30,  /* 0011 */\n    // 0x70,  /* 0111 */\n    // 0x30,  /* 0011 */\n    // 0x10,  /* 0001 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=18, hex=0x12, ascii=\"^R\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=19, hex=0x13, ascii=\"^S\"\n    //  */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x00,  /* 0000 */\n    // 0x50,  /* 0101 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=20, hex=0x14, ascii=\"^T\"\n    //  */\n    // 0x70,  /* 0111 */\n    // 0xD0,  /* 1101 */\n    // 0xD0,  /* 1101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=21, hex=0x15, ascii=\"^U\"\n    //  */\n    // 0x30,  /* 0011 */\n    // 0x60,  /* 0110 */\n    // 0x50,  /* 0101 */\n    // 0x30,  /* 0011 */\n    // 0x60,  /* 0110 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=22, hex=0x16, ascii=\"^V\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=23, hex=0x17, ascii=\"^W\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n\n    // /*\n    //  * code=24, hex=0x18, ascii=\"^X\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=25, hex=0x19, ascii=\"^Y\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=26, hex=0x1A, ascii=\"^Z\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x20,  /* 0010 */\n    // 0xF0,  /* 1111 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=27, hex=0x1B, ascii=\"^[\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x40,  /* 0100 */\n    // 0xF0,  /* 1111 */\n    // 0x40,  /* 0100 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=28, hex=0x1C, ascii=\"^\\\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x40,  /* 0100 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=29, hex=0x1D, ascii=\"^]\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x50,  /* 0101 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=30, hex=0x1E, ascii=\"^^\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=31, hex=0x1F, ascii=\"^_\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x70,  /* 0111 */\n    // 0x70,  /* 0111 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    /*\n     * code=32, hex=0x20, ascii=\" \"\n     */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=33, hex=0x21, ascii=\"!\"\n     */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=34, hex=0x22, ascii=\"\"\"\n     */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=35, hex=0x23, ascii=\"#\"\n     */\n    0x50,  /* 0101 */\n    0x70,  /* 0111 */\n    0x50,  /* 0101 */\n    0x70,  /* 0111 */\n    0x50,  /* 0101 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=36, hex=0x24, ascii=\"$\"\n     */\n    0x20,  /* 0010 */\n    0x30,  /* 0011 */\n    0x60,  /* 0110 */\n    0x30,  /* 0011 */\n    0x60,  /* 0110 */\n    0x20,  /* 0010 */\n\n    /*\n     * code=37, hex=0x25, ascii=\"%\"\n     */\n    0x40,  /* 0100 */\n    0x10,  /* 0001 */\n    0x20,  /* 0010 */\n    0x40,  /* 0100 */\n    0x10,  /* 0001 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=38, hex=0x26, ascii=\"&\"\n     */\n    0x20,  /* 0010 */\n    0x50,  /* 0101 */\n    0x30,  /* 0011 */\n    0x50,  /* 0101 */\n    0x70,  /* 0111 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=39, hex=0x27, ascii=\"'\"\n     */\n    0x60,  /* 0110 */\n    0x40,  /* 0100 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=40, hex=0x28, ascii=\"(\"\n     */\n    0x20,  /* 0010 */\n    0x40,  /* 0100 */\n    0x40,  /* 0100 */\n    0x40,  /* 0100 */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=41, hex=0x29, ascii=\")\"\n     */\n    0x40,  /* 0100 */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x40,  /* 0100 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=42, hex=0x2A, ascii=\"*\"\n     */\n    0x50,  /* 0101 */\n    0x20,  /* 0010 */\n    0x70,  /* 0111 */\n    0x20,  /* 0010 */\n    0x50,  /* 0101 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=43, hex=0x2B, ascii=\"+\"\n     */\n    0x00,  /* 0000 */\n    0x20,  /* 0010 */\n    0x70,  /* 0111 */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=44, hex=0x2C, ascii=\",\"\n     */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x60,  /* 0110 */\n    0x40,  /* 0100 */\n\n    /*\n     * code=45, hex=0x2D, ascii=\"-\"\n     */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x70,  /* 0111 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=46, hex=0x2E, ascii=\".\"\n     */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=47, hex=0x2F, ascii=\"/\"\n     */\n    0x10,  /* 0001 */\n    0x10,  /* 0001 */\n    0x20,  /* 0010 */\n    0x40,  /* 0100 */\n    0x40,  /* 0100 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=48, hex=0x30, ascii=\"0\"\n     */\n    0x30,  /* 0011 */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x60,  /* 0110 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=49, hex=0x31, ascii=\"1\"\n     */\n    0x20,  /* 0010 */\n    0x60,  /* 0110 */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x70,  /* 0111 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=50, hex=0x32, ascii=\"2\"\n     */\n    0x60,  /* 0110 */\n    0x10,  /* 0001 */\n    0x20,  /* 0010 */\n    0x40,  /* 0100 */\n    0x70,  /* 0111 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=51, hex=0x33, ascii=\"3\"\n     */\n    0x60,  /* 0110 */\n    0x10,  /* 0001 */\n    0x20,  /* 0010 */\n    0x10,  /* 0001 */\n    0x60,  /* 0110 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=52, hex=0x34, ascii=\"4\"\n     */\n    0x10,  /* 0001 */\n    0x50,  /* 0101 */\n    0x70,  /* 0111 */\n    0x10,  /* 0001 */\n    0x10,  /* 0001 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=53, hex=0x35, ascii=\"5\"\n     */\n    0x70,  /* 0111 */\n    0x40,  /* 0100 */\n    0x60,  /* 0110 */\n    0x10,  /* 0001 */\n    0x60,  /* 0110 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=54, hex=0x36, ascii=\"6\"\n     */\n    0x20,  /* 0010 */\n    0x40,  /* 0100 */\n    0x60,  /* 0110 */\n    0x50,  /* 0101 */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=55, hex=0x37, ascii=\"7\"\n     */\n    0x70,  /* 0111 */\n    0x10,  /* 0001 */\n    0x30,  /* 0011 */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=56, hex=0x38, ascii=\"8\"\n     */\n    0x20,  /* 0010 */\n    0x50,  /* 0101 */\n    0x20,  /* 0010 */\n    0x50,  /* 0101 */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=57, hex=0x39, ascii=\"9\"\n     */\n    0x20,  /* 0010 */\n    0x50,  /* 0101 */\n    0x30,  /* 0011 */\n    0x10,  /* 0001 */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=58, hex=0x3A, ascii=\":\"\n     */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=59, hex=0x3B, ascii=\";\"\n     */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n    0x60,  /* 0110 */\n    0x40,  /* 0100 */\n\n    /*\n     * code=60, hex=0x3C, ascii=\"<\"\n     */\n    0x10,  /* 0001 */\n    0x20,  /* 0010 */\n    0x40,  /* 0100 */\n    0x20,  /* 0010 */\n    0x10,  /* 0001 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=61, hex=0x3D, ascii=\"=\"\n     */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x70,  /* 0111 */\n    0x00,  /* 0000 */\n    0x70,  /* 0111 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=62, hex=0x3E, ascii=\">\"\n     */\n    0x40,  /* 0100 */\n    0x20,  /* 0010 */\n    0x10,  /* 0001 */\n    0x20,  /* 0010 */\n    0x40,  /* 0100 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=63, hex=0x3F, ascii=\"?\"\n     */\n    0x60,  /* 0110 */\n    0x10,  /* 0001 */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=64, hex=0x40, ascii=\"@\"\n     */\n    0x70,  /* 0111 */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x40,  /* 0100 */\n    0x70,  /* 0111 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=65, hex=0x41, ascii=\"A\"\n     */\n    0x20,  /* 0010 */\n    0x50,  /* 0101 */\n    0x70,  /* 0111 */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=66, hex=0x42, ascii=\"B\"\n     */\n    0x60,  /* 0110 */\n    0x50,  /* 0101 */\n    0x60,  /* 0110 */\n    0x50,  /* 0101 */\n    0x60,  /* 0110 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=67, hex=0x43, ascii=\"C\"\n     */\n    0x30,  /* 0011 */\n    0x40,  /* 0100 */\n    0x40,  /* 0100 */\n    0x40,  /* 0100 */\n    0x30,  /* 0011 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=68, hex=0x44, ascii=\"D\"\n     */\n    0x60,  /* 0110 */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x60,  /* 0110 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=69, hex=0x45, ascii=\"E\"\n     */\n    0x70,  /* 0111 */\n    0x40,  /* 0100 */\n    0x60,  /* 0110 */\n    0x40,  /* 0100 */\n    0x70,  /* 0111 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=70, hex=0x46, ascii=\"F\"\n     */\n    0x70,  /* 0111 */\n    0x40,  /* 0100 */\n    0x60,  /* 0110 */\n    0x40,  /* 0100 */\n    0x40,  /* 0100 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=71, hex=0x47, ascii=\"G\"\n     */\n    0x30,  /* 0011 */\n    0x40,  /* 0100 */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x30,  /* 0011 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=72, hex=0x48, ascii=\"H\"\n     */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x70,  /* 0111 */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=73, hex=0x49, ascii=\"I\"\n     */\n    0x70,  /* 0111 */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x70,  /* 0111 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=74, hex=0x4A, ascii=\"J\"\n     */\n    0x10,  /* 0001 */\n    0x10,  /* 0001 */\n    0x10,  /* 0001 */\n    0x50,  /* 0101 */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=75, hex=0x4B, ascii=\"K\"\n     */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x60,  /* 0110 */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=76, hex=0x4C, ascii=\"L\"\n     */\n    0x40,  /* 0100 */\n    0x40,  /* 0100 */\n    0x40,  /* 0100 */\n    0x40,  /* 0100 */\n    0x70,  /* 0111 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=77, hex=0x4D, ascii=\"M\"\n     */\n    0x50,  /* 0101 */\n    0x70,  /* 0111 */\n    0x70,  /* 0111 */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=78, hex=0x4E, ascii=\"N\"\n     */\n    0x50,  /* 0101 */\n    0x70,  /* 0111 */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=79, hex=0x4F, ascii=\"O\"\n     */\n    0x20,  /* 0010 */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=80, hex=0x50, ascii=\"P\"\n     */\n    0x60,  /* 0110 */\n    0x50,  /* 0101 */\n    0x60,  /* 0110 */\n    0x40,  /* 0100 */\n    0x40,  /* 0100 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=81, hex=0x51, ascii=\"Q\"\n     */\n    0x20,  /* 0010 */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x70,  /* 0111 */\n    0x30,  /* 0011 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=82, hex=0x52, ascii=\"R\"\n     */\n    0x60,  /* 0110 */\n    0x50,  /* 0101 */\n    0x60,  /* 0110 */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=83, hex=0x53, ascii=\"S\"\n     */\n    0x30,  /* 0011 */\n    0x40,  /* 0100 */\n    0x70,  /* 0111 */\n    0x10,  /* 0001 */\n    0x60,  /* 0110 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=84, hex=0x54, ascii=\"T\"\n     */\n    0x70,  /* 0111 */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=85, hex=0x55, ascii=\"U\"\n     */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x70,  /* 0111 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=86, hex=0x56, ascii=\"V\"\n     */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=87, hex=0x57, ascii=\"W\"\n     */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x70,  /* 0111 */\n    0x70,  /* 0111 */\n    0x50,  /* 0101 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=88, hex=0x58, ascii=\"X\"\n     */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x20,  /* 0010 */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=89, hex=0x59, ascii=\"Y\"\n     */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=90, hex=0x5A, ascii=\"Z\"\n     */\n    0x70,  /* 0111 */\n    0x10,  /* 0001 */\n    0x20,  /* 0010 */\n    0x40,  /* 0100 */\n    0x70,  /* 0111 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=91, hex=0x5B, ascii=\"[\"\n     */\n    0x60,  /* 0110 */\n    0x40,  /* 0100 */\n    0x40,  /* 0100 */\n    0x40,  /* 0100 */\n    0x60,  /* 0110 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=92, hex=0x5C, ascii=\"\\\"\n     */\n    0x40,  /* 0100 */\n    0x40,  /* 0100 */\n    0x20,  /* 0010 */\n    0x10,  /* 0001 */\n    0x10,  /* 0001 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=93, hex=0x5D, ascii=\"]\"\n     */\n    0x60,  /* 0110 */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x60,  /* 0110 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=94, hex=0x5E, ascii=\"^\"\n     */\n    0x20,  /* 0010 */\n    0x50,  /* 0101 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=95, hex=0x5F, ascii=\"_\"\n     */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0xF0,  /* 1111 */\n\n    /*\n     * code=96, hex=0x60, ascii=\"`\"\n     */\n    0x60,  /* 0110 */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=97, hex=0x61, ascii=\"a\"\n     */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x30,  /* 0011 */\n    0x50,  /* 0101 */\n    0x70,  /* 0111 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=98, hex=0x62, ascii=\"b\"\n     */\n    0x40,  /* 0100 */\n    0x40,  /* 0100 */\n    0x60,  /* 0110 */\n    0x50,  /* 0101 */\n    0x60,  /* 0110 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=99, hex=0x63, ascii=\"c\"\n     */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x30,  /* 0011 */\n    0x40,  /* 0100 */\n    0x30,  /* 0011 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=100, hex=0x64, ascii=\"d\"\n     */\n    0x10,  /* 0001 */\n    0x10,  /* 0001 */\n    0x30,  /* 0011 */\n    0x50,  /* 0101 */\n    0x30,  /* 0011 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=101, hex=0x65, ascii=\"e\"\n     */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x70,  /* 0111 */\n    0x60,  /* 0110 */\n    0x30,  /* 0011 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=102, hex=0x66, ascii=\"f\"\n     */\n    0x10,  /* 0001 */\n    0x20,  /* 0010 */\n    0x70,  /* 0111 */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=103, hex=0x67, ascii=\"g\"\n     */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x70,  /* 0111 */\n    0x50,  /* 0101 */\n    0x10,  /* 0001 */\n    0x70,  /* 0111 */\n\n    /*\n     * code=104, hex=0x68, ascii=\"h\"\n     */\n    0x40,  /* 0100 */\n    0x40,  /* 0100 */\n    0x60,  /* 0110 */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=105, hex=0x69, ascii=\"i\"\n     */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=106, hex=0x6A, ascii=\"j\"\n     */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x60,  /* 0110 */\n\n    /*\n     * code=107, hex=0x6B, ascii=\"k\"\n     */\n    0x40,  /* 0100 */\n    0x40,  /* 0100 */\n    0x50,  /* 0101 */\n    0x60,  /* 0110 */\n    0x50,  /* 0101 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=108, hex=0x6C, ascii=\"l\"\n     */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=109, hex=0x6D, ascii=\"m\"\n     */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x70,  /* 0111 */\n    0x70,  /* 0111 */\n    0x50,  /* 0101 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=110, hex=0x6E, ascii=\"n\"\n     */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x60,  /* 0110 */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=111, hex=0x6F, ascii=\"o\"\n     */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x20,  /* 0010 */\n    0x50,  /* 0101 */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=112, hex=0x70, ascii=\"p\"\n     */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x60,  /* 0110 */\n    0x50,  /* 0101 */\n    0x60,  /* 0110 */\n    0x40,  /* 0100 */\n\n    /*\n     * code=113, hex=0x71, ascii=\"q\"\n     */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x30,  /* 0011 */\n    0x50,  /* 0101 */\n    0x30,  /* 0011 */\n    0x10,  /* 0001 */\n\n    /*\n     * code=114, hex=0x72, ascii=\"r\"\n     */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x60,  /* 0110 */\n    0x40,  /* 0100 */\n    0x40,  /* 0100 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=115, hex=0x73, ascii=\"s\"\n     */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x30,  /* 0011 */\n    0x20,  /* 0010 */\n    0x60,  /* 0110 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=116, hex=0x74, ascii=\"t\"\n     */\n    0x00,  /* 0000 */\n    0x20,  /* 0010 */\n    0x70,  /* 0111 */\n    0x20,  /* 0010 */\n    0x30,  /* 0011 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=117, hex=0x75, ascii=\"u\"\n     */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x70,  /* 0111 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=118, hex=0x76, ascii=\"v\"\n     */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=119, hex=0x77, ascii=\"w\"\n     */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x50,  /* 0101 */\n    0x70,  /* 0111 */\n    0x70,  /* 0111 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=120, hex=0x78, ascii=\"x\"\n     */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x50,  /* 0101 */\n    0x20,  /* 0010 */\n    0x50,  /* 0101 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=121, hex=0x79, ascii=\"y\"\n     */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x50,  /* 0101 */\n    0x50,  /* 0101 */\n    0x20,  /* 0010 */\n    0x40,  /* 0100 */\n\n    /*\n     * code=122, hex=0x7A, ascii=\"z\"\n     */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x60,  /* 0110 */\n    0x20,  /* 0010 */\n    0x30,  /* 0011 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=123, hex=0x7B, ascii=\"{\"\n     */\n    0x30,  /* 0011 */\n    0x20,  /* 0010 */\n    0x60,  /* 0110 */\n    0x20,  /* 0010 */\n    0x30,  /* 0011 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=124, hex=0x7C, ascii=\"|\"\n     */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x20,  /* 0010 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=125, hex=0x7D, ascii=\"}\"\n     */\n    0x60,  /* 0110 */\n    0x20,  /* 0010 */\n    0x30,  /* 0011 */\n    0x20,  /* 0010 */\n    0x60,  /* 0110 */\n    0x00,  /* 0000 */\n\n    /*\n     * code=126, hex=0x7E, ascii=\"~\"\n     */\n    0x50,  /* 0101 */\n    0xA0,  /* 1010 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n    0x00,  /* 0000 */\n\n    // /*\n    //  * code=127, hex=0x7F, ascii=\"^?\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x20,  /* 0010 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=128, hex=0x80, ascii=\"!^@\"\n    //  */\n    // 0x30,  /* 0011 */\n    // 0x40,  /* 0100 */\n    // 0x40,  /* 0100 */\n    // 0x70,  /* 0111 */\n    // 0x20,  /* 0010 */\n    // 0x40,  /* 0100 */\n\n    // /*\n    //  * code=129, hex=0x81, ascii=\"!^A\"\n    //  */\n    // 0x50,  /* 0101 */\n    // 0x00,  /* 0000 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x30,  /* 0011 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=130, hex=0x82, ascii=\"!^B\"\n    //  */\n    // 0x10,  /* 0001 */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x60,  /* 0110 */\n    // 0x30,  /* 0011 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=131, hex=0x83, ascii=\"!^C\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x50,  /* 0101 */\n    // 0x30,  /* 0011 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=132, hex=0x84, ascii=\"!^D\"\n    //  */\n    // 0x50,  /* 0101 */\n    // 0x00,  /* 0000 */\n    // 0x30,  /* 0011 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=133, hex=0x85, ascii=\"!^E\"\n    //  */\n    // 0x40,  /* 0100 */\n    // 0x20,  /* 0010 */\n    // 0x30,  /* 0011 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=134, hex=0x86, ascii=\"!^F\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n    // 0x30,  /* 0011 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=135, hex=0x87, ascii=\"!^G\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x70,  /* 0111 */\n    // 0x40,  /* 0100 */\n    // 0x70,  /* 0111 */\n    // 0x20,  /* 0010 */\n    // 0x60,  /* 0110 */\n\n    // /*\n    //  * code=136, hex=0x88, ascii=\"!^H\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x60,  /* 0110 */\n    // 0x30,  /* 0011 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=137, hex=0x89, ascii=\"!^I\"\n    //  */\n    // 0x50,  /* 0101 */\n    // 0x00,  /* 0000 */\n    // 0x70,  /* 0111 */\n    // 0x60,  /* 0110 */\n    // 0x30,  /* 0011 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=138, hex=0x8A, ascii=\"!^J\"\n    //  */\n    // 0x40,  /* 0100 */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x60,  /* 0110 */\n    // 0x30,  /* 0011 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=139, hex=0x8B, ascii=\"!^K\"\n    //  */\n    // 0x50,  /* 0101 */\n    // 0x00,  /* 0000 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=140, hex=0x8C, ascii=\"!^L\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x50,  /* 0101 */\n    // 0x00,  /* 0000 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=141, hex=0x8D, ascii=\"!^M\"\n    //  */\n    // 0x40,  /* 0100 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=142, hex=0x8E, ascii=\"!^N\"\n    //  */\n    // 0x50,  /* 0101 */\n    // 0x20,  /* 0010 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x50,  /* 0101 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=143, hex=0x8F, ascii=\"!^O\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x50,  /* 0101 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=144, hex=0x90, ascii=\"!^P\"\n    //  */\n    // 0x10,  /* 0001 */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x60,  /* 0110 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=145, hex=0x91, ascii=\"!^Q\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x30,  /* 0011 */\n    // 0x70,  /* 0111 */\n    // 0x60,  /* 0110 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=146, hex=0x92, ascii=\"!^R\"\n    //  */\n    // 0x30,  /* 0011 */\n    // 0x60,  /* 0110 */\n    // 0x70,  /* 0111 */\n    // 0x60,  /* 0110 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=147, hex=0x93, ascii=\"!^S\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x50,  /* 0101 */\n    // 0x20,  /* 0010 */\n    // 0x50,  /* 0101 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=148, hex=0x94, ascii=\"!^T\"\n    //  */\n    // 0x50,  /* 0101 */\n    // 0x00,  /* 0000 */\n    // 0x20,  /* 0010 */\n    // 0x50,  /* 0101 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=149, hex=0x95, ascii=\"!^U\"\n    //  */\n    // 0x40,  /* 0100 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x50,  /* 0101 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=150, hex=0x96, ascii=\"!^V\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x50,  /* 0101 */\n    // 0x00,  /* 0000 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=151, hex=0x97, ascii=\"!^W\"\n    //  */\n    // 0x40,  /* 0100 */\n    // 0x20,  /* 0010 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=152, hex=0x98, ascii=\"!^X\"\n    //  */\n    // 0x50,  /* 0101 */\n    // 0x00,  /* 0000 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x20,  /* 0010 */\n    // 0x40,  /* 0100 */\n\n    // /*\n    //  * code=153, hex=0x99, ascii=\"!^Y\"\n    //  */\n    // 0x50,  /* 0101 */\n    // 0x20,  /* 0010 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=154, hex=0x9A, ascii=\"!^Z\"\n    //  */\n    // 0x50,  /* 0101 */\n    // 0x00,  /* 0000 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=155, hex=0x9B, ascii=\"!^[\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x40,  /* 0100 */\n    // 0x70,  /* 0111 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=156, hex=0x9C, ascii=\"!^\\\"\n    //  */\n    // 0x10,  /* 0001 */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=157, hex=0x9D, ascii=\"!^]\"\n    //  */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=158, hex=0x9E, ascii=\"!^^\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x60,  /* 0110 */\n    // 0x60,  /* 0110 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=159, hex=0x9F, ascii=\"!^_\"\n    //  */\n    // 0x30,  /* 0011 */\n    // 0x20,  /* 0010 */\n    // 0x30,  /* 0011 */\n    // 0x20,  /* 0010 */\n    // 0x60,  /* 0110 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=160, hex=0xA0, ascii=\"! \"\n    //  */\n    // 0x10,  /* 0001 */\n    // 0x20,  /* 0010 */\n    // 0x30,  /* 0011 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=161, hex=0xA1, ascii=\"!!\"\n    //  */\n    // 0x10,  /* 0001 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=162, hex=0xA2, ascii=\"!\"\"\n    //  */\n    // 0x10,  /* 0001 */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=163, hex=0xA3, ascii=\"!#\"\n    //  */\n    // 0x10,  /* 0001 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=164, hex=0xA4, ascii=\"!$\"\n    //  */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n    // 0x70,  /* 0111 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=165, hex=0xA5, ascii=\"!%\"\n    //  */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x50,  /* 0101 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=166, hex=0xA6, ascii=\"!&\"\n    //  */\n    // 0x30,  /* 0011 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=167, hex=0xA7, ascii=\"!'\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x50,  /* 0101 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=168, hex=0xA8, ascii=\"!(\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n    // 0x20,  /* 0010 */\n    // 0x40,  /* 0100 */\n    // 0x30,  /* 0011 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=169, hex=0xA9, ascii=\"!)\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x70,  /* 0111 */\n    // 0x40,  /* 0100 */\n    // 0x40,  /* 0100 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=170, hex=0xAA, ascii=\"!*\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0xE0,  /* 1110 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=171, hex=0xAB, ascii=\"!+\"\n    //  */\n    // 0x40,  /* 0100 */\n    // 0x50,  /* 0101 */\n    // 0x20,  /* 0010 */\n    // 0x50,  /* 0101 */\n    // 0x30,  /* 0011 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=172, hex=0xAC, ascii=\"!,\"\n    //  */\n    // 0x40,  /* 0100 */\n    // 0x50,  /* 0101 */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x10,  /* 0001 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=173, hex=0xAD, ascii=\"!-\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=174, hex=0xAE, ascii=\"!.\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x50,  /* 0101 */\n    // 0xA0,  /* 1010 */\n    // 0x50,  /* 0101 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=175, hex=0xAF, ascii=\"!/\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0xA0,  /* 1010 */\n    // 0x50,  /* 0101 */\n    // 0xA0,  /* 1010 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=176, hex=0xB0, ascii=\"!0\"\n    //  */\n    // 0x40,  /* 0100 */\n    // 0x10,  /* 0001 */\n    // 0x40,  /* 0100 */\n    // 0x10,  /* 0001 */\n    // 0x40,  /* 0100 */\n    // 0x10,  /* 0001 */\n\n    // /*\n    //  * code=177, hex=0xB1, ascii=\"!1\"\n    //  */\n    // 0x50,  /* 0101 */\n    // 0xA0,  /* 1010 */\n    // 0x50,  /* 0101 */\n    // 0xA0,  /* 1010 */\n    // 0x50,  /* 0101 */\n    // 0xA0,  /* 1010 */\n\n    // /*\n    //  * code=178, hex=0xB2, ascii=\"!2\"\n    //  */\n    // 0xB0,  /* 1011 */\n    // 0xE0,  /* 1110 */\n    // 0xB0,  /* 1011 */\n    // 0xE0,  /* 1110 */\n    // 0xB0,  /* 1011 */\n    // 0xE0,  /* 1110 */\n\n    // /*\n    //  * code=179, hex=0xB3, ascii=\"!3\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n\n    // /*\n    //  * code=180, hex=0xB4, ascii=\"!4\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0xE0,  /* 1110 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n\n    // /*\n    //  * code=181, hex=0xB5, ascii=\"!5\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0xE0,  /* 1110 */\n    // 0x20,  /* 0010 */\n    // 0xE0,  /* 1110 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n\n    // /*\n    //  * code=182, hex=0xB6, ascii=\"!6\"\n    //  */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0xD0,  /* 1101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n\n    // /*\n    //  * code=183, hex=0xB7, ascii=\"!7\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0xF0,  /* 1111 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n\n    // /*\n    //  * code=184, hex=0xB8, ascii=\"!8\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0xE0,  /* 1110 */\n    // 0x20,  /* 0010 */\n    // 0xE0,  /* 1110 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n\n    // /*\n    //  * code=185, hex=0xB9, ascii=\"!9\"\n    //  */\n    // 0x50,  /* 0101 */\n    // 0xD0,  /* 1101 */\n    // 0x10,  /* 0001 */\n    // 0xD0,  /* 1101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n\n    // /*\n    //  * code=186, hex=0xBA, ascii=\"!:\"\n    //  */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n\n    // /*\n    //  * code=187, hex=0xBB, ascii=\"!;\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0xF0,  /* 1111 */\n    // 0x10,  /* 0001 */\n    // 0xD0,  /* 1101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n\n    // /*\n    //  * code=188, hex=0xBC, ascii=\"!<\"\n    //  */\n    // 0x50,  /* 0101 */\n    // 0xD0,  /* 1101 */\n    // 0x10,  /* 0001 */\n    // 0xF0,  /* 1111 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=189, hex=0xBD, ascii=\"!=\"\n    //  */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0xF0,  /* 1111 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=190, hex=0xBE, ascii=\"!>\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0xE0,  /* 1110 */\n    // 0x20,  /* 0010 */\n    // 0xE0,  /* 1110 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=191, hex=0xBF, ascii=\"!?\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0xE0,  /* 1110 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n\n    // /*\n    //  * code=192, hex=0xC0, ascii=\"!@\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x30,  /* 0011 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=193, hex=0xC1, ascii=\"!A\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0xF0,  /* 1111 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=194, hex=0xC2, ascii=\"!B\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0xF0,  /* 1111 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n\n    // /*\n    //  * code=195, hex=0xC3, ascii=\"!C\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x30,  /* 0011 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n\n    // /*\n    //  * code=196, hex=0xC4, ascii=\"!D\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0xF0,  /* 1111 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=197, hex=0xC5, ascii=\"!E\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0xF0,  /* 1111 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n\n    // /*\n    //  * code=198, hex=0xC6, ascii=\"!F\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x30,  /* 0011 */\n    // 0x20,  /* 0010 */\n    // 0x30,  /* 0011 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n\n    // /*\n    //  * code=199, hex=0xC7, ascii=\"!G\"\n    //  */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n\n    // /*\n    //  * code=200, hex=0xC8, ascii=\"!H\"\n    //  */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x40,  /* 0100 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=201, hex=0xC9, ascii=\"!I\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x70,  /* 0111 */\n    // 0x40,  /* 0100 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n\n    // /*\n    //  * code=202, hex=0xCA, ascii=\"!J\"\n    //  */\n    // 0x50,  /* 0101 */\n    // 0xD0,  /* 1101 */\n    // 0x00,  /* 0000 */\n    // 0xF0,  /* 1111 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=203, hex=0xCB, ascii=\"!K\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0xF0,  /* 1111 */\n    // 0x00,  /* 0000 */\n    // 0xD0,  /* 1101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n\n    // /*\n    //  * code=204, hex=0xCC, ascii=\"!L\"\n    //  */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x40,  /* 0100 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n\n    // /*\n    //  * code=205, hex=0xCD, ascii=\"!M\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0xF0,  /* 1111 */\n    // 0x00,  /* 0000 */\n    // 0xF0,  /* 1111 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=206, hex=0xCE, ascii=\"!N\"\n    //  */\n    // 0x50,  /* 0101 */\n    // 0xD0,  /* 1101 */\n    // 0x00,  /* 0000 */\n    // 0xD0,  /* 1101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n\n    // /*\n    //  * code=207, hex=0xCF, ascii=\"!O\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0xF0,  /* 1111 */\n    // 0x00,  /* 0000 */\n    // 0xF0,  /* 1111 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=208, hex=0xD0, ascii=\"!P\"\n    //  */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0xF0,  /* 1111 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=209, hex=0xD1, ascii=\"!Q\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0xF0,  /* 1111 */\n    // 0x00,  /* 0000 */\n    // 0xF0,  /* 1111 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n\n    // /*\n    //  * code=210, hex=0xD2, ascii=\"!R\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0xF0,  /* 1111 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n\n    // /*\n    //  * code=211, hex=0xD3, ascii=\"!S\"\n    //  */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=212, hex=0xD4, ascii=\"!T\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x30,  /* 0011 */\n    // 0x20,  /* 0010 */\n    // 0x30,  /* 0011 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=213, hex=0xD5, ascii=\"!U\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x30,  /* 0011 */\n    // 0x20,  /* 0010 */\n    // 0x30,  /* 0011 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n\n    // /*\n    //  * code=214, hex=0xD6, ascii=\"!V\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x70,  /* 0111 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n\n    // /*\n    //  * code=215, hex=0xD7, ascii=\"!W\"\n    //  */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0xD0,  /* 1101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n\n    // /*\n    //  * code=216, hex=0xD8, ascii=\"!X\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0xF0,  /* 1111 */\n    // 0x00,  /* 0000 */\n    // 0xF0,  /* 1111 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n\n    // /*\n    //  * code=217, hex=0xD9, ascii=\"!Y\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0xE0,  /* 1110 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=218, hex=0xDA, ascii=\"!Z\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x30,  /* 0011 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n\n    // /*\n    //  * code=219, hex=0xDB, ascii=\"![\"\n    //  */\n    // 0xF0,  /* 1111 */\n    // 0xF0,  /* 1111 */\n    // 0xF0,  /* 1111 */\n    // 0xF0,  /* 1111 */\n    // 0xF0,  /* 1111 */\n    // 0xF0,  /* 1111 */\n\n    // /*\n    //  * code=220, hex=0xDC, ascii=\"!\\\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0xF0,  /* 1111 */\n    // 0xF0,  /* 1111 */\n    // 0xF0,  /* 1111 */\n\n    // /*\n    //  * code=221, hex=0xDD, ascii=\"!]\"\n    //  */\n    // 0xC0,  /* 1100 */\n    // 0xC0,  /* 1100 */\n    // 0xC0,  /* 1100 */\n    // 0xC0,  /* 1100 */\n    // 0xC0,  /* 1100 */\n    // 0xC0,  /* 1100 */\n\n    // /*\n    //  * code=222, hex=0xDE, ascii=\"!^\"\n    //  */\n    // 0x30,  /* 0011 */\n    // 0x30,  /* 0011 */\n    // 0x30,  /* 0011 */\n    // 0x30,  /* 0011 */\n    // 0x30,  /* 0011 */\n    // 0x30,  /* 0011 */\n\n    // /*\n    //  * code=223, hex=0xDF, ascii=\"!_\"\n    //  */\n    // 0xF0,  /* 1111 */\n    // 0xF0,  /* 1111 */\n    // 0xF0,  /* 1111 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=224, hex=0xE0, ascii=\"!`\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x70,  /* 0111 */\n    // 0x60,  /* 0110 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=225, hex=0xE1, ascii=\"!a\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x50,  /* 0101 */\n    // 0x60,  /* 0110 */\n    // 0x50,  /* 0101 */\n    // 0x60,  /* 0110 */\n    // 0x40,  /* 0100 */\n\n    // /*\n    //  * code=226, hex=0xE2, ascii=\"!b\"\n    //  */\n    // 0x70,  /* 0111 */\n    // 0x50,  /* 0101 */\n    // 0x40,  /* 0100 */\n    // 0x40,  /* 0100 */\n    // 0x40,  /* 0100 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=227, hex=0xE3, ascii=\"!c\"\n    //  */\n    // 0x70,  /* 0111 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=228, hex=0xE4, ascii=\"!d\"\n    //  */\n    // 0x70,  /* 0111 */\n    // 0x40,  /* 0100 */\n    // 0x20,  /* 0010 */\n    // 0x40,  /* 0100 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=229, hex=0xE5, ascii=\"!e\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x30,  /* 0011 */\n    // 0x50,  /* 0101 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=230, hex=0xE6, ascii=\"!f\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x40,  /* 0100 */\n\n    // /*\n    //  * code=231, hex=0xE7, ascii=\"!g\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x10,  /* 0001 */\n    // 0x60,  /* 0110 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=232, hex=0xE8, ascii=\"!h\"\n    //  */\n    // 0x70,  /* 0111 */\n    // 0x20,  /* 0010 */\n    // 0x50,  /* 0101 */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=233, hex=0xE9, ascii=\"!i\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x50,  /* 0101 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=234, hex=0xEA, ascii=\"!j\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x20,  /* 0010 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=235, hex=0xEB, ascii=\"!k\"\n    //  */\n    // 0x30,  /* 0011 */\n    // 0x40,  /* 0100 */\n    // 0x20,  /* 0010 */\n    // 0x50,  /* 0101 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=236, hex=0xEC, ascii=\"!l\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x70,  /* 0111 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=237, hex=0xED, ascii=\"!m\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x50,  /* 0101 */\n    // 0x70,  /* 0111 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=238, hex=0xEE, ascii=\"!n\"\n    //  */\n    // 0x30,  /* 0011 */\n    // 0x40,  /* 0100 */\n    // 0x70,  /* 0111 */\n    // 0x40,  /* 0100 */\n    // 0x30,  /* 0011 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=239, hex=0xEF, ascii=\"!o\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=240, hex=0xF0, ascii=\"!p\"\n    //  */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=241, hex=0xF1, ascii=\"!q\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=242, hex=0xF2, ascii=\"!r\"\n    //  */\n    // 0x60,  /* 0110 */\n    // 0x10,  /* 0001 */\n    // 0x60,  /* 0110 */\n    // 0x00,  /* 0000 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=243, hex=0xF3, ascii=\"!s\"\n    //  */\n    // 0x30,  /* 0011 */\n    // 0x40,  /* 0100 */\n    // 0x30,  /* 0011 */\n    // 0x00,  /* 0000 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=244, hex=0xF4, ascii=\"!t\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x10,  /* 0001 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n\n    // /*\n    //  * code=245, hex=0xF5, ascii=\"!u\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x40,  /* 0100 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=246, hex=0xF6, ascii=\"!v\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n    // 0x70,  /* 0111 */\n    // 0x00,  /* 0000 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=247, hex=0xF7, ascii=\"!w\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x50,  /* 0101 */\n    // 0xA0,  /* 1010 */\n    // 0x50,  /* 0101 */\n    // 0xA0,  /* 1010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=248, hex=0xF8, ascii=\"!x\"\n    //  */\n    // 0x20,  /* 0010 */\n    // 0x50,  /* 0101 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=249, hex=0xF9, ascii=\"!y\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x20,  /* 0010 */\n    // 0x70,  /* 0111 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=250, hex=0xFA, ascii=\"!z\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=251, hex=0xFB, ascii=\"!{\"\n    //  */\n    // 0x30,  /* 0011 */\n    // 0x20,  /* 0010 */\n    // 0x20,  /* 0010 */\n    // 0x60,  /* 0110 */\n    // 0x20,  /* 0010 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=252, hex=0xFC, ascii=\"!|\"\n    //  */\n    // 0x70,  /* 0111 */\n    // 0x50,  /* 0101 */\n    // 0x50,  /* 0101 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=253, hex=0xFD, ascii=\"!}\"\n    //  */\n    // 0x60,  /* 0110 */\n    // 0x20,  /* 0010 */\n    // 0x40,  /* 0100 */\n    // 0x60,  /* 0110 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=254, hex=0xFE, ascii=\"!~\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x60,  /* 0110 */\n    // 0x60,  /* 0110 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n\n    // /*\n    //  * code=255, hex=0xFF, ascii=\"!^\"\n    //  */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n    // 0x00,  /* 0000 */\n};\n"
  },
  {
    "path": "wled00/src/font/console_font_5x12.h",
    "content": "// font courtesy of https://github.com/idispatch/raster-fonts\nstatic const unsigned char console_font_5x12[] PROGMEM = {\n\n// code points 0-31 and 127-255 are commented out to save memory, they contain extra characters (CP437),\n// which could be used with an UTF-8 to CP437 conversion\n\n    // /*\n    //  * code=0, hex=0x00, ascii=\"^@\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=1, hex=0x01, ascii=\"^A\"\n    //  */\n    // 0x70,  /* 01110 */\n    // 0x88,  /* 10001 */\n    // 0x88,  /* 10001 */\n    // 0xD8,  /* 11011 */\n    // 0x88,  /* 10001 */\n    // 0x88,  /* 10001 */\n    // 0xD8,  /* 11011 */\n    // 0xA8,  /* 10101 */\n    // 0x88,  /* 10001 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=2, hex=0x02, ascii=\"^B\"\n    //  */\n    // 0x70,  /* 01110 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xA8,  /* 10101 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xA8,  /* 10101 */\n    // 0xD8,  /* 11011 */\n    // 0xF8,  /* 11111 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=3, hex=0x03, ascii=\"^C\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0xD8,  /* 11011 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0x70,  /* 01110 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=4, hex=0x04, ascii=\"^D\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x70,  /* 01110 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0x70,  /* 01110 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=5, hex=0x05, ascii=\"^E\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x70,  /* 01110 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=6, hex=0x06, ascii=\"^F\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x70,  /* 01110 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=7, hex=0x07, ascii=\"^G\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x60,  /* 01100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=8, hex=0x08, ascii=\"^H\"\n    //  */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0x98,  /* 10011 */\n    // 0x98,  /* 10011 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n\n    // /*\n    //  * code=9, hex=0x09, ascii=\"^I\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x70,  /* 01110 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=10, hex=0x0A, ascii=\"^J\"\n    //  */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0x88,  /* 10001 */\n    // 0xA8,  /* 10101 */\n    // 0xA8,  /* 10101 */\n    // 0x88,  /* 10001 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n\n    // /*\n    //  * code=11, hex=0x0B, ascii=\"^K\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x38,  /* 00111 */\n    // 0x18,  /* 00011 */\n    // 0x28,  /* 00101 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x60,  /* 01100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=12, hex=0x0C, ascii=\"^L\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x60,  /* 01100 */\n    // 0x40,  /* 01000 */\n    // 0x40,  /* 01000 */\n    // 0xE0,  /* 11100 */\n    // 0x40,  /* 01000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=13, hex=0x0D, ascii=\"^M\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x30,  /* 00110 */\n    // 0x30,  /* 00110 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0xE0,  /* 11100 */\n    // 0xC0,  /* 11000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=14, hex=0x0E, ascii=\"^N\"\n    //  */\n    // 0x30,  /* 00110 */\n    // 0x50,  /* 01010 */\n    // 0x70,  /* 01110 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x70,  /* 01110 */\n    // 0x70,  /* 01110 */\n    // 0xC0,  /* 11000 */\n    // 0xC0,  /* 11000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=15, hex=0x0F, ascii=\"^O\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xA8,  /* 10101 */\n    // 0x70,  /* 01110 */\n    // 0x88,  /* 10001 */\n    // 0x70,  /* 01110 */\n    // 0xA8,  /* 10101 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=16, hex=0x10, ascii=\"^P\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x80,  /* 10000 */\n    // 0xC0,  /* 11000 */\n    // 0xE0,  /* 11100 */\n    // 0xF0,  /* 11110 */\n    // 0xE0,  /* 11100 */\n    // 0xC0,  /* 11000 */\n    // 0x80,  /* 10000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=17, hex=0x11, ascii=\"^Q\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x10,  /* 00010 */\n    // 0x30,  /* 00110 */\n    // 0x70,  /* 01110 */\n    // 0xF0,  /* 11110 */\n    // 0x70,  /* 01110 */\n    // 0x30,  /* 00110 */\n    // 0x10,  /* 00010 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=18, hex=0x12, ascii=\"^R\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0xA8,  /* 10101 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0xA8,  /* 10101 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=19, hex=0x13, ascii=\"^S\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=20, hex=0x14, ascii=\"^T\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x70,  /* 01110 */\n    // 0xD0,  /* 11010 */\n    // 0xD0,  /* 11010 */\n    // 0xD0,  /* 11010 */\n    // 0xD0,  /* 11010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=21, hex=0x15, ascii=\"^U\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x80,  /* 10000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x60,  /* 01100 */\n    // 0x10,  /* 00010 */\n    // 0x90,  /* 10010 */\n    // 0x60,  /* 01100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=22, hex=0x16, ascii=\"^V\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF0,  /* 11110 */\n    // 0xF0,  /* 11110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=23, hex=0x17, ascii=\"^W\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0xA8,  /* 10101 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0xA8,  /* 10101 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=24, hex=0x18, ascii=\"^X\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0xA8,  /* 10101 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=25, hex=0x19, ascii=\"^Y\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0xA8,  /* 10101 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=26, hex=0x1A, ascii=\"^Z\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x40,  /* 01000 */\n    // 0x20,  /* 00100 */\n    // 0xF0,  /* 11110 */\n    // 0x20,  /* 00100 */\n    // 0x40,  /* 01000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=27, hex=0x1B, ascii=\"^[\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x40,  /* 01000 */\n    // 0xF0,  /* 11110 */\n    // 0x40,  /* 01000 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=28, hex=0x1C, ascii=\"^\\\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x80,  /* 10000 */\n    // 0x80,  /* 10000 */\n    // 0xF0,  /* 11110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=29, hex=0x1D, ascii=\"^]\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0xF8,  /* 11111 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=30, hex=0x1E, ascii=\"^^\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x70,  /* 01110 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=31, hex=0x1F, ascii=\"^_\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x70,  /* 01110 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    /*\n     * code=32, hex=0x20, ascii=\" \"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=33, hex=0x21, ascii=\"!\"\n     */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=34, hex=0x22, ascii=\"\"\"\n     */\n    0x50,  /* 01010 */\n    0x50,  /* 01010 */\n    0x50,  /* 01010 */\n    0x50,  /* 01010 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=35, hex=0x23, ascii=\"#\"\n     */\n    0x00,  /* 00000 */\n    0x50,  /* 01010 */\n    0x50,  /* 01010 */\n    0xF8,  /* 11111 */\n    0x50,  /* 01010 */\n    0x50,  /* 01010 */\n    0xF8,  /* 11111 */\n    0x50,  /* 01010 */\n    0x50,  /* 01010 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=36, hex=0x24, ascii=\"$\"\n     */\n    0x00,  /* 00000 */\n    0x40,  /* 01000 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x80,  /* 10000 */\n    0x40,  /* 01000 */\n    0x20,  /* 00100 */\n    0x10,  /* 00010 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=37, hex=0x25, ascii=\"%\"\n     */\n    0x00,  /* 00000 */\n    0xD0,  /* 11010 */\n    0xD0,  /* 11010 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0xB0,  /* 10110 */\n    0xB0,  /* 10110 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=38, hex=0x26, ascii=\"&\"\n     */\n    0x00,  /* 00000 */\n    0x40,  /* 01000 */\n    0xA0,  /* 10100 */\n    0xA0,  /* 10100 */\n    0xA0,  /* 10100 */\n    0x40,  /* 01000 */\n    0xA8,  /* 10101 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x68,  /* 01101 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=39, hex=0x27, ascii=\"'\"\n     */\n    0x30,  /* 00110 */\n    0x30,  /* 00110 */\n    0x10,  /* 00010 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=40, hex=0x28, ascii=\"(\"\n     */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=41, hex=0x29, ascii=\")\"\n     */\n    0x00,  /* 00000 */\n    0x40,  /* 01000 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=42, hex=0x2A, ascii=\"*\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0xA8,  /* 10101 */\n    0x70,  /* 01110 */\n    0x70,  /* 01110 */\n    0xA8,  /* 10101 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=43, hex=0x2B, ascii=\"+\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0xF8,  /* 11111 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=44, hex=0x2C, ascii=\",\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x60,  /* 01100 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n\n    /*\n     * code=45, hex=0x2D, ascii=\"-\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0xF0,  /* 11110 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=46, hex=0x2E, ascii=\".\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=47, hex=0x2F, ascii=\"/\"\n     */\n    0x00,  /* 00000 */\n    0x10,  /* 00010 */\n    0x10,  /* 00010 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=48, hex=0x30, ascii=\"0\"\n     */\n    0x00,  /* 00000 */\n    0x70,  /* 01110 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0xE0,  /* 11100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=49, hex=0x31, ascii=\"1\"\n     */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x60,  /* 01100 */\n    0xA0,  /* 10100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0xF0,  /* 11110 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=50, hex=0x32, ascii=\"2\"\n     */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x10,  /* 00010 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0xF0,  /* 11110 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=51, hex=0x33, ascii=\"3\"\n     */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x10,  /* 00010 */\n    0x10,  /* 00010 */\n    0x60,  /* 01100 */\n    0x10,  /* 00010 */\n    0x10,  /* 00010 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=52, hex=0x34, ascii=\"4\"\n     */\n    0x00,  /* 00000 */\n    0x10,  /* 00010 */\n    0x30,  /* 00110 */\n    0x50,  /* 01010 */\n    0x50,  /* 01010 */\n    0x90,  /* 10010 */\n    0xF0,  /* 11110 */\n    0x10,  /* 00010 */\n    0x10,  /* 00010 */\n    0x10,  /* 00010 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=53, hex=0x35, ascii=\"5\"\n     */\n    0x00,  /* 00000 */\n    0xF0,  /* 11110 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0xE0,  /* 11100 */\n    0x10,  /* 00010 */\n    0x10,  /* 00010 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=54, hex=0x36, ascii=\"6\"\n     */\n    0x00,  /* 00000 */\n    0x30,  /* 00110 */\n    0x40,  /* 01000 */\n    0x80,  /* 10000 */\n    0xE0,  /* 11100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=55, hex=0x37, ascii=\"7\"\n     */\n    0x00,  /* 00000 */\n    0xF0,  /* 11110 */\n    0x10,  /* 00010 */\n    0x10,  /* 00010 */\n    0x10,  /* 00010 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=56, hex=0x38, ascii=\"8\"\n     */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=57, hex=0x39, ascii=\"9\"\n     */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x70,  /* 01110 */\n    0x10,  /* 00010 */\n    0x20,  /* 00100 */\n    0xC0,  /* 11000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=58, hex=0x3A, ascii=\":\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=59, hex=0x3B, ascii=\";\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x60,  /* 01100 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n\n    /*\n     * code=60, hex=0x3C, ascii=\"<\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x10,  /* 00010 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x40,  /* 01000 */\n    0x20,  /* 00100 */\n    0x10,  /* 00010 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=61, hex=0x3D, ascii=\"=\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0xF0,  /* 11110 */\n    0x00,  /* 00000 */\n    0xF0,  /* 11110 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=62, hex=0x3E, ascii=\">\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x80,  /* 10000 */\n    0x40,  /* 01000 */\n    0x20,  /* 00100 */\n    0x10,  /* 00010 */\n    0x10,  /* 00010 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n    0x80,  /* 10000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=63, hex=0x3F, ascii=\"?\"\n     */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x10,  /* 00010 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=64, hex=0x40, ascii=\"@\"\n     */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0xB0,  /* 10110 */\n    0xB0,  /* 10110 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x70,  /* 01110 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=65, hex=0x41, ascii=\"A\"\n     */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0xF0,  /* 11110 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=66, hex=0x42, ascii=\"B\"\n     */\n    0x00,  /* 00000 */\n    0xE0,  /* 11100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0xE0,  /* 11100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0xE0,  /* 11100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=67, hex=0x43, ascii=\"C\"\n     */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=68, hex=0x44, ascii=\"D\"\n     */\n    0x00,  /* 00000 */\n    0xE0,  /* 11100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0xE0,  /* 11100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=69, hex=0x45, ascii=\"E\"\n     */\n    0x00,  /* 00000 */\n    0xF0,  /* 11110 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0xF0,  /* 11110 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0xF0,  /* 11110 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=70, hex=0x46, ascii=\"F\"\n     */\n    0x00,  /* 00000 */\n    0xF0,  /* 11110 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0xF0,  /* 11110 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=71, hex=0x47, ascii=\"G\"\n     */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0xB0,  /* 10110 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=72, hex=0x48, ascii=\"H\"\n     */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0xF0,  /* 11110 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=73, hex=0x49, ascii=\"I\"\n     */\n    0x00,  /* 00000 */\n    0x70,  /* 01110 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x70,  /* 01110 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=74, hex=0x4A, ascii=\"J\"\n     */\n    0x00,  /* 00000 */\n    0x70,  /* 01110 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0xA0,  /* 10100 */\n    0xA0,  /* 10100 */\n    0x40,  /* 01000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=75, hex=0x4B, ascii=\"K\"\n     */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0xA0,  /* 10100 */\n    0xA0,  /* 10100 */\n    0xC0,  /* 11000 */\n    0xA0,  /* 10100 */\n    0xA0,  /* 10100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=76, hex=0x4C, ascii=\"L\"\n     */\n    0x00,  /* 00000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0xF0,  /* 11110 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=77, hex=0x4D, ascii=\"M\"\n     */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0xF0,  /* 11110 */\n    0xF0,  /* 11110 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=78, hex=0x4E, ascii=\"N\"\n     */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0xD0,  /* 11010 */\n    0xD0,  /* 11010 */\n    0xB0,  /* 10110 */\n    0xB0,  /* 10110 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=79, hex=0x4F, ascii=\"O\"\n     */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=80, hex=0x50, ascii=\"P\"\n     */\n    0x00,  /* 00000 */\n    0xE0,  /* 11100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0xE0,  /* 11100 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=81, hex=0x51, ascii=\"Q\"\n     */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x20,  /* 00100 */\n    0x10,  /* 00010 */\n\n    /*\n     * code=82, hex=0x52, ascii=\"R\"\n     */\n    0x00,  /* 00000 */\n    0xE0,  /* 11100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0xE0,  /* 11100 */\n    0xC0,  /* 11000 */\n    0xA0,  /* 10100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=83, hex=0x53, ascii=\"S\"\n     */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x80,  /* 10000 */\n    0x60,  /* 01100 */\n    0x10,  /* 00010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=84, hex=0x54, ascii=\"T\"\n     */\n    0x00,  /* 00000 */\n    0xF8,  /* 11111 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=85, hex=0x55, ascii=\"U\"\n     */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=86, hex=0x56, ascii=\"V\"\n     */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x50,  /* 01010 */\n    0x50,  /* 01010 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=87, hex=0x57, ascii=\"W\"\n     */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0xF0,  /* 11110 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=88, hex=0x58, ascii=\"X\"\n     */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x60,  /* 01100 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=89, hex=0x59, ascii=\"Y\"\n     */\n    0x00,  /* 00000 */\n    0x88,  /* 10001 */\n    0x88,  /* 10001 */\n    0x88,  /* 10001 */\n    0x50,  /* 01010 */\n    0x50,  /* 01010 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=90, hex=0x5A, ascii=\"Z\"\n     */\n    0x00,  /* 00000 */\n    0xF0,  /* 11110 */\n    0x10,  /* 00010 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0xF0,  /* 11110 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=91, hex=0x5B, ascii=\"[\"\n     */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=92, hex=0x5C, ascii=\"\\\"\n     */\n    0x00,  /* 00000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x10,  /* 00010 */\n    0x10,  /* 00010 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=93, hex=0x5D, ascii=\"]\"\n     */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=94, hex=0x5E, ascii=\"^\"\n     */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x50,  /* 01010 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=95, hex=0x5F, ascii=\"_\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0xF8,  /* 11111 */\n\n    /*\n     * code=96, hex=0x60, ascii=\"`\"\n     */\n    0x60,  /* 01100 */\n    0x60,  /* 01100 */\n    0x40,  /* 01000 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=97, hex=0x61, ascii=\"a\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x10,  /* 00010 */\n    0x70,  /* 01110 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x70,  /* 01110 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=98, hex=0x62, ascii=\"b\"\n     */\n    0x00,  /* 00000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0xE0,  /* 11100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0xE0,  /* 11100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=99, hex=0x63, ascii=\"c\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x70,  /* 01110 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x70,  /* 01110 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=100, hex=0x64, ascii=\"d\"\n     */\n    0x00,  /* 00000 */\n    0x10,  /* 00010 */\n    0x10,  /* 00010 */\n    0x10,  /* 00010 */\n    0x70,  /* 01110 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x70,  /* 01110 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=101, hex=0x65, ascii=\"e\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0xF0,  /* 11110 */\n    0x80,  /* 10000 */\n    0x70,  /* 01110 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=102, hex=0x66, ascii=\"f\"\n     */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0xE0,  /* 11100 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=103, hex=0x67, ascii=\"g\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x70,  /* 01110 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x70,  /* 01110 */\n    0x10,  /* 00010 */\n    0x60,  /* 01100 */\n\n    /*\n     * code=104, hex=0x68, ascii=\"h\"\n     */\n    0x00,  /* 00000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0xE0,  /* 11100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=105, hex=0x69, ascii=\"i\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x70,  /* 01110 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=106, hex=0x6A, ascii=\"j\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0xA0,  /* 10100 */\n    0x40,  /* 01000 */\n\n    /*\n     * code=107, hex=0x6B, ascii=\"k\"\n     */\n    0x00,  /* 00000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x90,  /* 10010 */\n    0xA0,  /* 10100 */\n    0xC0,  /* 11000 */\n    0xC0,  /* 11000 */\n    0xA0,  /* 10100 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=108, hex=0x6C, ascii=\"l\"\n     */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x70,  /* 01110 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=109, hex=0x6D, ascii=\"m\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0xF0,  /* 11110 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=110, hex=0x6E, ascii=\"n\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0xA0,  /* 10100 */\n    0xD0,  /* 11010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=111, hex=0x6F, ascii=\"o\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=112, hex=0x70, ascii=\"p\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0xE0,  /* 11100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0xE0,  /* 11100 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n\n    /*\n     * code=113, hex=0x71, ascii=\"q\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x70,  /* 01110 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x70,  /* 01110 */\n    0x10,  /* 00010 */\n    0x10,  /* 00010 */\n\n    /*\n     * code=114, hex=0x72, ascii=\"r\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0xB0,  /* 10110 */\n    0x50,  /* 01010 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=115, hex=0x73, ascii=\"s\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x40,  /* 01000 */\n    0x20,  /* 00100 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=116, hex=0x74, ascii=\"t\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0xE0,  /* 11100 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=117, hex=0x75, ascii=\"u\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x70,  /* 01110 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=118, hex=0x76, ascii=\"v\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x50,  /* 01010 */\n    0x50,  /* 01010 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=119, hex=0x77, ascii=\"w\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0xF0,  /* 11110 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=120, hex=0x78, ascii=\"x\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=121, hex=0x79, ascii=\"y\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x70,  /* 01110 */\n    0x10,  /* 00010 */\n    0xE0,  /* 11100 */\n\n    /*\n     * code=122, hex=0x7A, ascii=\"z\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0xF0,  /* 11110 */\n    0x10,  /* 00010 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n    0x80,  /* 10000 */\n    0xF0,  /* 11110 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=123, hex=0x7B, ascii=\"{\"\n     */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x80,  /* 10000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=124, hex=0x7C, ascii=\"|\"\n     */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=125, hex=0x7D, ascii=\"}\"\n     */\n    0x00,  /* 00000 */\n    0x40,  /* 01000 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x10,  /* 00010 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=126, hex=0x7E, ascii=\"~\"\n     */\n    0x00,  /* 00000 */\n    0x50,  /* 01010 */\n    0xA0,  /* 10100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    // /*\n    //  * code=127, hex=0x7F, ascii=\"^?\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x88,  /* 10001 */\n    // 0x88,  /* 10001 */\n    // 0x88,  /* 10001 */\n    // 0x88,  /* 10001 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=128, hex=0x80, ascii=\"!^@\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x80,  /* 10000 */\n    // 0x80,  /* 10000 */\n    // 0x80,  /* 10000 */\n    // 0x80,  /* 10000 */\n    // 0x90,  /* 10010 */\n    // 0x60,  /* 01100 */\n    // 0x20,  /* 00100 */\n    // 0x40,  /* 01000 */\n\n    // /*\n    //  * code=129, hex=0x81, ascii=\"!^A\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=130, hex=0x82, ascii=\"!^B\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x30,  /* 00110 */\n    // 0x40,  /* 01000 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0xF0,  /* 11110 */\n    // 0x80,  /* 10000 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=131, hex=0x83, ascii=\"!^C\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x10,  /* 00010 */\n    // 0x70,  /* 01110 */\n    // 0x90,  /* 10010 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=132, hex=0x84, ascii=\"!^D\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x10,  /* 00010 */\n    // 0x70,  /* 01110 */\n    // 0x90,  /* 10010 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=133, hex=0x85, ascii=\"!^E\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0xC0,  /* 11000 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x10,  /* 00010 */\n    // 0x70,  /* 01110 */\n    // 0x90,  /* 10010 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=134, hex=0x86, ascii=\"!^F\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x50,  /* 01010 */\n    // 0x20,  /* 00100 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x10,  /* 00010 */\n    // 0x70,  /* 01110 */\n    // 0x90,  /* 10010 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=135, hex=0x87, ascii=\"!^G\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x70,  /* 01110 */\n    // 0x80,  /* 10000 */\n    // 0x80,  /* 10000 */\n    // 0x80,  /* 10000 */\n    // 0x80,  /* 10000 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0xC0,  /* 11000 */\n\n    // /*\n    //  * code=136, hex=0x88, ascii=\"!^H\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0xF0,  /* 11110 */\n    // 0x80,  /* 10000 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=137, hex=0x89, ascii=\"!^I\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0xF0,  /* 11110 */\n    // 0x80,  /* 10000 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=138, hex=0x8A, ascii=\"!^J\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0xC0,  /* 11000 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0xF0,  /* 11110 */\n    // 0x80,  /* 10000 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=139, hex=0x8B, ascii=\"!^K\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=140, hex=0x8C, ascii=\"!^L\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=141, hex=0x8D, ascii=\"!^M\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x10,  /* 00010 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=142, hex=0x8E, ascii=\"!^N\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0xF0,  /* 11110 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=143, hex=0x8F, ascii=\"!^O\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x50,  /* 01010 */\n    // 0x20,  /* 00100 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0xF0,  /* 11110 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=144, hex=0x90, ascii=\"!^P\"\n    //  */\n    // 0x30,  /* 00110 */\n    // 0x40,  /* 01000 */\n    // 0x00,  /* 00000 */\n    // 0xF0,  /* 11110 */\n    // 0x80,  /* 10000 */\n    // 0x80,  /* 10000 */\n    // 0xF0,  /* 11110 */\n    // 0x80,  /* 10000 */\n    // 0x80,  /* 10000 */\n    // 0xF0,  /* 11110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=145, hex=0x91, ascii=\"!^Q\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x70,  /* 01110 */\n    // 0x28,  /* 00101 */\n    // 0x28,  /* 00101 */\n    // 0x70,  /* 01110 */\n    // 0xA0,  /* 10100 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=146, hex=0x92, ascii=\"!^R\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x70,  /* 01110 */\n    // 0xE0,  /* 11100 */\n    // 0xA0,  /* 10100 */\n    // 0xA0,  /* 10100 */\n    // 0xF0,  /* 11110 */\n    // 0xA0,  /* 10100 */\n    // 0xA0,  /* 10100 */\n    // 0xB0,  /* 10110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=147, hex=0x93, ascii=\"!^S\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x60,  /* 01100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=148, hex=0x94, ascii=\"!^T\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x60,  /* 01100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=149, hex=0x95, ascii=\"!^U\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x10,  /* 00010 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x60,  /* 01100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=150, hex=0x96, ascii=\"!^V\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x00,  /* 00000 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=151, hex=0x97, ascii=\"!^W\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0xC0,  /* 11000 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=152, hex=0x98, ascii=\"!^X\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x70,  /* 01110 */\n    // 0x10,  /* 00010 */\n    // 0xE0,  /* 11100 */\n\n    // /*\n    //  * code=153, hex=0x99, ascii=\"!^Y\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x60,  /* 01100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=154, hex=0x9A, ascii=\"!^Z\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=155, hex=0x9B, ascii=\"!^[\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x88,  /* 10001 */\n    // 0x80,  /* 10000 */\n    // 0x88,  /* 10001 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=156, hex=0x9C, ascii=\"!^\\\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x50,  /* 01010 */\n    // 0x40,  /* 01000 */\n    // 0x40,  /* 01000 */\n    // 0xE0,  /* 11100 */\n    // 0x40,  /* 01000 */\n    // 0x40,  /* 01000 */\n    // 0xD0,  /* 11010 */\n    // 0xF0,  /* 11110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=157, hex=0x9D, ascii=\"!^]\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x88,  /* 10001 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0xF8,  /* 11111 */\n    // 0x20,  /* 00100 */\n    // 0xF8,  /* 11111 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=158, hex=0x9E, ascii=\"!^^\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0xE0,  /* 11100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0xE0,  /* 11100 */\n    // 0x90,  /* 10010 */\n    // 0xB8,  /* 10111 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=159, hex=0x9F, ascii=\"!^_\"\n    //  */\n    // 0x10,  /* 00010 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0xA0,  /* 10100 */\n    // 0x40,  /* 01000 */\n\n    // /*\n    //  * code=160, hex=0xA0, ascii=\"! \"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x30,  /* 00110 */\n    // 0x40,  /* 01000 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x10,  /* 00010 */\n    // 0x70,  /* 01110 */\n    // 0x90,  /* 10010 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=161, hex=0xA1, ascii=\"!!\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x30,  /* 00110 */\n    // 0x40,  /* 01000 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=162, hex=0xA2, ascii=\"!\"\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x30,  /* 00110 */\n    // 0x40,  /* 01000 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x60,  /* 01100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=163, hex=0xA3, ascii=\"!#\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x30,  /* 00110 */\n    // 0x40,  /* 01000 */\n    // 0x00,  /* 00000 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=164, hex=0xA4, ascii=\"!$\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0xA0,  /* 10100 */\n    // 0x00,  /* 00000 */\n    // 0xA0,  /* 10100 */\n    // 0xD0,  /* 11010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=165, hex=0xA5, ascii=\"!%\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0xA0,  /* 10100 */\n    // 0x00,  /* 00000 */\n    // 0x90,  /* 10010 */\n    // 0xD0,  /* 11010 */\n    // 0xF0,  /* 11110 */\n    // 0xB0,  /* 10110 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=166, hex=0xA6, ascii=\"!&\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x10,  /* 00010 */\n    // 0x70,  /* 01110 */\n    // 0x90,  /* 10010 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0xF0,  /* 11110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=167, hex=0xA7, ascii=\"!'\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x60,  /* 01100 */\n    // 0x00,  /* 00000 */\n    // 0xF0,  /* 11110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=168, hex=0xA8, ascii=\"!(\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x40,  /* 01000 */\n    // 0x40,  /* 01000 */\n    // 0x00,  /* 00000 */\n    // 0x40,  /* 01000 */\n    // 0x40,  /* 01000 */\n    // 0x80,  /* 10000 */\n    // 0x80,  /* 10000 */\n    // 0x90,  /* 10010 */\n    // 0x60,  /* 01100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=169, hex=0xA9, ascii=\"!)\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF0,  /* 11110 */\n    // 0x80,  /* 10000 */\n    // 0x80,  /* 10000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=170, hex=0xAA, ascii=\"!*\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF0,  /* 11110 */\n    // 0x10,  /* 00010 */\n    // 0x10,  /* 00010 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=171, hex=0xAB, ascii=\"!+\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x40,  /* 01000 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x20,  /* 00100 */\n    // 0x40,  /* 01000 */\n    // 0x70,  /* 01110 */\n    // 0x90,  /* 10010 */\n    // 0xA0,  /* 10100 */\n    // 0x30,  /* 00110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=172, hex=0xAC, ascii=\"!,\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x40,  /* 01000 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x20,  /* 00100 */\n    // 0x40,  /* 01000 */\n    // 0x50,  /* 01010 */\n    // 0xB0,  /* 10110 */\n    // 0xB0,  /* 10110 */\n    // 0x10,  /* 00010 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=173, hex=0xAD, ascii=\"!-\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=174, hex=0xAE, ascii=\"!.\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0xA0,  /* 10100 */\n    // 0xA0,  /* 10100 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=175, hex=0xAF, ascii=\"!/\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xA0,  /* 10100 */\n    // 0xA0,  /* 10100 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0xA0,  /* 10100 */\n    // 0xA0,  /* 10100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=176, hex=0xB0, ascii=\"!0\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xA8,  /* 10101 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xA8,  /* 10101 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=177, hex=0xB1, ascii=\"!1\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0xA8,  /* 10101 */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0xA8,  /* 10101 */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0xA8,  /* 10101 */\n\n    // /*\n    //  * code=178, hex=0xB2, ascii=\"!2\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0xA8,  /* 10101 */\n    // 0x50,  /* 01010 */\n    // 0xA8,  /* 10101 */\n    // 0x50,  /* 01010 */\n    // 0xA8,  /* 10101 */\n    // 0x50,  /* 01010 */\n    // 0xA8,  /* 10101 */\n    // 0x50,  /* 01010 */\n    // 0xA8,  /* 10101 */\n    // 0x50,  /* 01010 */\n    // 0xA8,  /* 10101 */\n\n    // /*\n    //  * code=179, hex=0xB3, ascii=\"!3\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=180, hex=0xB4, ascii=\"!4\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0xE0,  /* 11100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=181, hex=0xB5, ascii=\"!5\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0xE0,  /* 11100 */\n    // 0x20,  /* 00100 */\n    // 0xE0,  /* 11100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=182, hex=0xB6, ascii=\"!6\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0xD0,  /* 11010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=183, hex=0xB7, ascii=\"!7\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF0,  /* 11110 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=184, hex=0xB8, ascii=\"!8\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xE0,  /* 11100 */\n    // 0x20,  /* 00100 */\n    // 0xE0,  /* 11100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=185, hex=0xB9, ascii=\"!9\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0xD0,  /* 11010 */\n    // 0x10,  /* 00010 */\n    // 0xD0,  /* 11010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=186, hex=0xBA, ascii=\"!:\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=187, hex=0xBB, ascii=\"!;\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF0,  /* 11110 */\n    // 0x10,  /* 00010 */\n    // 0xD0,  /* 11010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=188, hex=0xBC, ascii=\"!<\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0xD0,  /* 11010 */\n    // 0x10,  /* 00010 */\n    // 0xF0,  /* 11110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=189, hex=0xBD, ascii=\"!=\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0xF0,  /* 11110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=190, hex=0xBE, ascii=\"!>\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0xE0,  /* 11100 */\n    // 0x20,  /* 00100 */\n    // 0xE0,  /* 11100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=191, hex=0xBF, ascii=\"!?\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xE0,  /* 11100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=192, hex=0xC0, ascii=\"!@\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x38,  /* 00111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=193, hex=0xC1, ascii=\"!A\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=194, hex=0xC2, ascii=\"!B\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=195, hex=0xC3, ascii=\"!C\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x38,  /* 00111 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=196, hex=0xC4, ascii=\"!D\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=197, hex=0xC5, ascii=\"!E\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0xF8,  /* 11111 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=198, hex=0xC6, ascii=\"!F\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x38,  /* 00111 */\n    // 0x20,  /* 00100 */\n    // 0x38,  /* 00111 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=199, hex=0xC7, ascii=\"!G\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x58,  /* 01011 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=200, hex=0xC8, ascii=\"!H\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x58,  /* 01011 */\n    // 0x40,  /* 01000 */\n    // 0x78,  /* 01111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=201, hex=0xC9, ascii=\"!I\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x78,  /* 01111 */\n    // 0x40,  /* 01000 */\n    // 0x58,  /* 01011 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=202, hex=0xCA, ascii=\"!J\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0xD8,  /* 11011 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=203, hex=0xCB, ascii=\"!K\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0xD8,  /* 11011 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=204, hex=0xCC, ascii=\"!L\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x58,  /* 01011 */\n    // 0x40,  /* 01000 */\n    // 0x58,  /* 01011 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=205, hex=0xCD, ascii=\"!M\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=206, hex=0xCE, ascii=\"!N\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0xD8,  /* 11011 */\n    // 0x00,  /* 00000 */\n    // 0xD8,  /* 11011 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=207, hex=0xCF, ascii=\"!O\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=208, hex=0xD0, ascii=\"!P\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=209, hex=0xD1, ascii=\"!Q\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=210, hex=0xD2, ascii=\"!R\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=211, hex=0xD3, ascii=\"!S\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x78,  /* 01111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=212, hex=0xD4, ascii=\"!T\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x38,  /* 00111 */\n    // 0x20,  /* 00100 */\n    // 0x38,  /* 00111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=213, hex=0xD5, ascii=\"!U\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x38,  /* 00111 */\n    // 0x20,  /* 00100 */\n    // 0x38,  /* 00111 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=214, hex=0xD6, ascii=\"!V\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x78,  /* 01111 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=215, hex=0xD7, ascii=\"!W\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0xD8,  /* 11011 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=216, hex=0xD8, ascii=\"!X\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=217, hex=0xD9, ascii=\"!Y\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0xE0,  /* 11100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=218, hex=0xDA, ascii=\"!Z\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x38,  /* 00111 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=219, hex=0xDB, ascii=\"![\"\n    //  */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n\n    // /*\n    //  * code=220, hex=0xDC, ascii=\"!\\\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n\n    // /*\n    //  * code=221, hex=0xDD, ascii=\"!]\"\n    //  */\n    // 0xE0,  /* 11100 */\n    // 0xE0,  /* 11100 */\n    // 0xE0,  /* 11100 */\n    // 0xE0,  /* 11100 */\n    // 0xE0,  /* 11100 */\n    // 0xE0,  /* 11100 */\n    // 0xE0,  /* 11100 */\n    // 0xE0,  /* 11100 */\n    // 0xE0,  /* 11100 */\n    // 0xE0,  /* 11100 */\n    // 0xE0,  /* 11100 */\n    // 0xE0,  /* 11100 */\n\n    // /*\n    //  * code=222, hex=0xDE, ascii=\"!^\"\n    //  */\n    // 0x38,  /* 00111 */\n    // 0x38,  /* 00111 */\n    // 0x38,  /* 00111 */\n    // 0x38,  /* 00111 */\n    // 0x38,  /* 00111 */\n    // 0x38,  /* 00111 */\n    // 0x38,  /* 00111 */\n    // 0x38,  /* 00111 */\n    // 0x38,  /* 00111 */\n    // 0x38,  /* 00111 */\n    // 0x38,  /* 00111 */\n    // 0x38,  /* 00111 */\n\n    // /*\n    //  * code=223, hex=0xDF, ascii=\"!_\"\n    //  */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=224, hex=0xE0, ascii=\"!`\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x48,  /* 01001 */\n    // 0xB0,  /* 10110 */\n    // 0xA0,  /* 10100 */\n    // 0xA0,  /* 10100 */\n    // 0xB0,  /* 10110 */\n    // 0x48,  /* 01001 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=225, hex=0xE1, ascii=\"!a\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0xA0,  /* 10100 */\n    // 0xE0,  /* 11100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0xE0,  /* 11100 */\n    // 0x80,  /* 10000 */\n    // 0x80,  /* 10000 */\n\n    // /*\n    //  * code=226, hex=0xE2, ascii=\"!b\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x78,  /* 01111 */\n    // 0x48,  /* 01001 */\n    // 0x40,  /* 01000 */\n    // 0x40,  /* 01000 */\n    // 0x40,  /* 01000 */\n    // 0x40,  /* 01000 */\n    // 0x40,  /* 01000 */\n    // 0x40,  /* 01000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=227, hex=0xE3, ascii=\"!c\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=228, hex=0xE4, ascii=\"!d\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x48,  /* 01001 */\n    // 0x20,  /* 00100 */\n    // 0x10,  /* 00010 */\n    // 0x20,  /* 00100 */\n    // 0x48,  /* 01001 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=229, hex=0xE5, ascii=\"!e\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0xA0,  /* 10100 */\n    // 0xA0,  /* 10100 */\n    // 0xA0,  /* 10100 */\n    // 0x40,  /* 01000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=230, hex=0xE6, ascii=\"!f\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0xE0,  /* 11100 */\n    // 0x80,  /* 10000 */\n    // 0x80,  /* 10000 */\n\n    // /*\n    //  * code=231, hex=0xE7, ascii=\"!g\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0xA0,  /* 10100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=232, hex=0xE8, ascii=\"!h\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=233, hex=0xE9, ascii=\"!i\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x50,  /* 01010 */\n    // 0x88,  /* 10001 */\n    // 0xF8,  /* 11111 */\n    // 0x88,  /* 10001 */\n    // 0x50,  /* 01010 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=234, hex=0xEA, ascii=\"!j\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x50,  /* 01010 */\n    // 0x88,  /* 10001 */\n    // 0x88,  /* 10001 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0xD8,  /* 11011 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=235, hex=0xEB, ascii=\"!k\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x30,  /* 00110 */\n    // 0x40,  /* 01000 */\n    // 0x40,  /* 01000 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x60,  /* 01100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=236, hex=0xEC, ascii=\"!l\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0xA8,  /* 10101 */\n    // 0xA8,  /* 10101 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=237, hex=0xED, ascii=\"!m\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0xA8,  /* 10101 */\n    // 0xA8,  /* 10101 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=238, hex=0xEE, ascii=\"!n\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x70,  /* 01110 */\n    // 0x80,  /* 10000 */\n    // 0x80,  /* 10000 */\n    // 0xE0,  /* 11100 */\n    // 0x80,  /* 10000 */\n    // 0x80,  /* 10000 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=239, hex=0xEF, ascii=\"!o\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=240, hex=0xF0, ascii=\"!p\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF0,  /* 11110 */\n    // 0x00,  /* 00000 */\n    // 0xF0,  /* 11110 */\n    // 0x00,  /* 00000 */\n    // 0xF0,  /* 11110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=241, hex=0xF1, ascii=\"!q\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0xF8,  /* 11111 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=242, hex=0xF2, ascii=\"!r\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x80,  /* 10000 */\n    // 0x40,  /* 01000 */\n    // 0x20,  /* 00100 */\n    // 0x10,  /* 00010 */\n    // 0x20,  /* 00100 */\n    // 0x40,  /* 01000 */\n    // 0x80,  /* 10000 */\n    // 0x00,  /* 00000 */\n    // 0xF0,  /* 11110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=243, hex=0xF3, ascii=\"!s\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x10,  /* 00010 */\n    // 0x20,  /* 00100 */\n    // 0x40,  /* 01000 */\n    // 0x80,  /* 10000 */\n    // 0x40,  /* 01000 */\n    // 0x20,  /* 00100 */\n    // 0x10,  /* 00010 */\n    // 0x00,  /* 00000 */\n    // 0xF0,  /* 11110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=244, hex=0xF4, ascii=\"!t\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x10,  /* 00010 */\n    // 0x28,  /* 00101 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=245, hex=0xF5, ascii=\"!u\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0xA0,  /* 10100 */\n    // 0x40,  /* 01000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=246, hex=0xF6, ascii=\"!v\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=247, hex=0xF7, ascii=\"!w\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0xA0,  /* 10100 */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0xA0,  /* 10100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=248, hex=0xF8, ascii=\"!x\"\n    //  */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x60,  /* 01100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=249, hex=0xF9, ascii=\"!y\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=250, hex=0xFA, ascii=\"!z\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=251, hex=0xFB, ascii=\"!{\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x38,  /* 00111 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0xA0,  /* 10100 */\n    // 0xA0,  /* 10100 */\n    // 0x60,  /* 01100 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=252, hex=0xFC, ascii=\"!|\"\n    //  */\n    // 0xA0,  /* 10100 */\n    // 0xD0,  /* 11010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=253, hex=0xFD, ascii=\"!}\"\n    //  */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x10,  /* 00010 */\n    // 0x60,  /* 01100 */\n    // 0x80,  /* 10000 */\n    // 0xF0,  /* 11110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=254, hex=0xFE, ascii=\"!~\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x70,  /* 01110 */\n    // 0x70,  /* 01110 */\n    // 0x70,  /* 01110 */\n    // 0x70,  /* 01110 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=255, hex=0xFF, ascii=\"!^\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n};\n"
  },
  {
    "path": "wled00/src/font/console_font_5x8.h",
    "content": "// font courtesy of https://github.com/idispatch/raster-fonts\nstatic const unsigned char console_font_5x8[] PROGMEM = {\n\n// code points 0-31 and 127-255 are commented out to save memory, they contain extra characters (CP437),\n// which could be used with an UTF-8 to CP437 conversion\n\n    // /*\n    //  * code=0, hex=0x00, ascii=\"^@\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=1, hex=0x01, ascii=\"^A\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x70,  /* 01110 */\n    // 0xA8,  /* 10101 */\n    // 0xF8,  /* 11111 */\n    // 0xD8,  /* 11011 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=2, hex=0x02, ascii=\"^B\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x70,  /* 01110 */\n    // 0xA8,  /* 10101 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=3, hex=0x03, ascii=\"^C\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=4, hex=0x04, ascii=\"^D\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0xF8,  /* 11111 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=5, hex=0x05, ascii=\"^E\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x70,  /* 01110 */\n    // 0xA8,  /* 10101 */\n    // 0xF8,  /* 11111 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=6, hex=0x06, ascii=\"^F\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0xF8,  /* 11111 */\n    // 0xA8,  /* 10101 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=7, hex=0x07, ascii=\"^G\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=8, hex=0x08, ascii=\"^H\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xD8,  /* 11011 */\n    // 0x88,  /* 10001 */\n    // 0xD8,  /* 11011 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n\n    // /*\n    //  * code=9, hex=0x09, ascii=\"^I\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x50,  /* 01010 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=10, hex=0x0A, ascii=\"^J\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xD8,  /* 11011 */\n    // 0x88,  /* 10001 */\n    // 0xD8,  /* 11011 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n\n    // /*\n    //  * code=11, hex=0x0B, ascii=\"^K\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x38,  /* 00111 */\n    // 0x18,  /* 00011 */\n    // 0x68,  /* 01101 */\n    // 0xA0,  /* 10100 */\n    // 0x40,  /* 01000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=12, hex=0x0C, ascii=\"^L\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x50,  /* 01010 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=13, hex=0x0D, ascii=\"^M\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x50,  /* 01010 */\n    // 0x40,  /* 01000 */\n    // 0xC0,  /* 11000 */\n    // 0x80,  /* 10000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=14, hex=0x0E, ascii=\"^N\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x38,  /* 00111 */\n    // 0x48,  /* 01001 */\n    // 0x58,  /* 01011 */\n    // 0xD0,  /* 11010 */\n    // 0x80,  /* 10000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=15, hex=0x0F, ascii=\"^O\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x50,  /* 01010 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=16, hex=0x10, ascii=\"^P\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x40,  /* 01000 */\n    // 0x60,  /* 01100 */\n    // 0x70,  /* 01110 */\n    // 0x60,  /* 01100 */\n    // 0x40,  /* 01000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=17, hex=0x11, ascii=\"^Q\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x10,  /* 00010 */\n    // 0x30,  /* 00110 */\n    // 0x70,  /* 01110 */\n    // 0x30,  /* 00110 */\n    // 0x10,  /* 00010 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=18, hex=0x12, ascii=\"^R\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=19, hex=0x13, ascii=\"^S\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=20, hex=0x14, ascii=\"^T\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x78,  /* 01111 */\n    // 0xD0,  /* 11010 */\n    // 0xD0,  /* 11010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=21, hex=0x15, ascii=\"^U\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x18,  /* 00011 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x48,  /* 01001 */\n    // 0x30,  /* 00110 */\n    // 0xC0,  /* 11000 */\n\n    // /*\n    //  * code=22, hex=0x16, ascii=\"^V\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=23, hex=0x17, ascii=\"^W\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n\n    // /*\n    //  * code=24, hex=0x18, ascii=\"^X\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=25, hex=0x19, ascii=\"^Y\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=26, hex=0x1A, ascii=\"^Z\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x10,  /* 00010 */\n    // 0xF8,  /* 11111 */\n    // 0x10,  /* 00010 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=27, hex=0x1B, ascii=\"^[\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x40,  /* 01000 */\n    // 0xF8,  /* 11111 */\n    // 0x40,  /* 01000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=28, hex=0x1C, ascii=\"^\\\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x80,  /* 10000 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=29, hex=0x1D, ascii=\"^]\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0xF8,  /* 11111 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=30, hex=0x1E, ascii=\"^^\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=31, hex=0x1F, ascii=\"^_\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n\n    /*\n     * code=32, hex=0x20, ascii=\" \"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=33, hex=0x21, ascii=\"!\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=34, hex=0x22, ascii=\"\"\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x50,  /* 01010 */\n    0x50,  /* 01010 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=35, hex=0x23, ascii=\"#\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x50,  /* 01010 */\n    0xF8,  /* 11111 */\n    0x50,  /* 01010 */\n    0xF8,  /* 11111 */\n    0x50,  /* 01010 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=36, hex=0x24, ascii=\"$\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x30,  /* 00110 */\n    0x40,  /* 01000 */\n    0x30,  /* 00110 */\n    0x60,  /* 01100 */\n    0x20,  /* 00100 */\n\n    /*\n     * code=37, hex=0x25, ascii=\"%\"\n     */\n    0x00,  /* 00000 */\n    0x40,  /* 01000 */\n    0xA8,  /* 10101 */\n    0x50,  /* 01010 */\n    0x30,  /* 00110 */\n    0x68,  /* 01101 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=38, hex=0x26, ascii=\"&\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x30,  /* 00110 */\n    0x40,  /* 01000 */\n    0x68,  /* 01101 */\n    0x90,  /* 10010 */\n    0x68,  /* 01101 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=39, hex=0x27, ascii=\"'\"\n     */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=40, hex=0x28, ascii=\"(\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=41, hex=0x29, ascii=\")\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x40,  /* 01000 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=42, hex=0x2A, ascii=\"*\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x50,  /* 01010 */\n    0x20,  /* 00100 */\n    0x70,  /* 01110 */\n    0x20,  /* 00100 */\n    0x50,  /* 01010 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=43, hex=0x2B, ascii=\"+\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x70,  /* 01110 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=44, hex=0x2C, ascii=\",\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n\n    /*\n     * code=45, hex=0x2D, ascii=\"-\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0xF0,  /* 11110 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=46, hex=0x2E, ascii=\".\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=47, hex=0x2F, ascii=\"/\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x10,  /* 00010 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=48, hex=0x30, ascii=\"0\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=49, hex=0x31, ascii=\"1\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x60,  /* 01100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=50, hex=0x32, ascii=\"2\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n    0xF0,  /* 11110 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=51, hex=0x33, ascii=\"3\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0xE0,  /* 11100 */\n    0x10,  /* 00010 */\n    0x60,  /* 01100 */\n    0x10,  /* 00010 */\n    0xE0,  /* 11100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=52, hex=0x34, ascii=\"4\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x10,  /* 00010 */\n    0x30,  /* 00110 */\n    0x50,  /* 01010 */\n    0xF0,  /* 11110 */\n    0x10,  /* 00010 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=53, hex=0x35, ascii=\"5\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0xF0,  /* 11110 */\n    0x80,  /* 10000 */\n    0xE0,  /* 11100 */\n    0x10,  /* 00010 */\n    0xE0,  /* 11100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=54, hex=0x36, ascii=\"6\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x80,  /* 10000 */\n    0xE0,  /* 11100 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=55, hex=0x37, ascii=\"7\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0xF0,  /* 11110 */\n    0x10,  /* 00010 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=56, hex=0x38, ascii=\"8\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=57, hex=0x39, ascii=\"9\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x70,  /* 01110 */\n    0x10,  /* 00010 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=58, hex=0x3A, ascii=\":\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=59, hex=0x3B, ascii=\";\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n\n    /*\n     * code=60, hex=0x3C, ascii=\"<\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x10,  /* 00010 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n    0x20,  /* 00100 */\n    0x10,  /* 00010 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=61, hex=0x3D, ascii=\"=\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x70,  /* 01110 */\n    0x00,  /* 00000 */\n    0x70,  /* 01110 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=62, hex=0x3E, ascii=\">\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x40,  /* 01000 */\n    0x20,  /* 00100 */\n    0x10,  /* 00010 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=63, hex=0x3F, ascii=\"?\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x10,  /* 00010 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n    0x40,  /* 01000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=64, hex=0x40, ascii=\"@\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x70,  /* 01110 */\n    0x88,  /* 10001 */\n    0xB0,  /* 10110 */\n    0x80,  /* 10000 */\n    0x70,  /* 01110 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=65, hex=0x41, ascii=\"A\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0xF0,  /* 11110 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=66, hex=0x42, ascii=\"B\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0xE0,  /* 11100 */\n    0x90,  /* 10010 */\n    0xE0,  /* 11100 */\n    0x90,  /* 10010 */\n    0xE0,  /* 11100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=67, hex=0x43, ascii=\"C\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x70,  /* 01110 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x70,  /* 01110 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=68, hex=0x44, ascii=\"D\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0xE0,  /* 11100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0xE0,  /* 11100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=69, hex=0x45, ascii=\"E\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0xF0,  /* 11110 */\n    0x80,  /* 10000 */\n    0xE0,  /* 11100 */\n    0x80,  /* 10000 */\n    0xF0,  /* 11110 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=70, hex=0x46, ascii=\"F\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0xF0,  /* 11110 */\n    0x80,  /* 10000 */\n    0xE0,  /* 11100 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=71, hex=0x47, ascii=\"G\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x80,  /* 10000 */\n    0x90,  /* 10010 */\n    0x70,  /* 01110 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=72, hex=0x48, ascii=\"H\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0xF0,  /* 11110 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=73, hex=0x49, ascii=\"I\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x70,  /* 01110 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x70,  /* 01110 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=74, hex=0x4A, ascii=\"J\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x10,  /* 00010 */\n    0x10,  /* 00010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=75, hex=0x4B, ascii=\"K\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0xA0,  /* 10100 */\n    0xC0,  /* 11000 */\n    0xA0,  /* 10100 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=76, hex=0x4C, ascii=\"L\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0xF0,  /* 11110 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=77, hex=0x4D, ascii=\"M\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0xF0,  /* 11110 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=78, hex=0x4E, ascii=\"N\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0xD0,  /* 11010 */\n    0xB0,  /* 10110 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=79, hex=0x4F, ascii=\"O\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=80, hex=0x50, ascii=\"P\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0xE0,  /* 11100 */\n    0x90,  /* 10010 */\n    0xE0,  /* 11100 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=81, hex=0x51, ascii=\"Q\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x10,  /* 00010 */\n\n    /*\n     * code=82, hex=0x52, ascii=\"R\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0xE0,  /* 11100 */\n    0x90,  /* 10010 */\n    0xE0,  /* 11100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=83, hex=0x53, ascii=\"S\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x70,  /* 01110 */\n    0x80,  /* 10000 */\n    0x60,  /* 01100 */\n    0x10,  /* 00010 */\n    0xE0,  /* 11100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=84, hex=0x54, ascii=\"T\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0xF8,  /* 11111 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=85, hex=0x55, ascii=\"U\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=86, hex=0x56, ascii=\"V\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=87, hex=0x57, ascii=\"W\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x88,  /* 10001 */\n    0xA8,  /* 10101 */\n    0xA8,  /* 10101 */\n    0x50,  /* 01010 */\n    0x50,  /* 01010 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=88, hex=0x58, ascii=\"X\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x50,  /* 01010 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=89, hex=0x59, ascii=\"Y\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x50,  /* 01010 */\n    0x50,  /* 01010 */\n    0x50,  /* 01010 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=90, hex=0x5A, ascii=\"Z\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0xF0,  /* 11110 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n    0x80,  /* 10000 */\n    0xF0,  /* 11110 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=91, hex=0x5B, ascii=\"[\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=92, hex=0x5C, ascii=\"\\\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x10,  /* 00010 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=93, hex=0x5D, ascii=\"]\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=94, hex=0x5E, ascii=\"^\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x50,  /* 01010 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=95, hex=0x5F, ascii=\"_\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0xF8,  /* 11111 */\n\n    /*\n     * code=96, hex=0x60, ascii=\"`\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x40,  /* 01000 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=97, hex=0x61, ascii=\"a\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x10,  /* 00010 */\n    0x70,  /* 01110 */\n    0x50,  /* 01010 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=98, hex=0x62, ascii=\"b\"\n     */\n    0x00,  /* 00000 */\n    0x80,  /* 10000 */\n    0x80,  /* 10000 */\n    0xE0,  /* 11100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0xE0,  /* 11100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=99, hex=0x63, ascii=\"c\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x30,  /* 00110 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x30,  /* 00110 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=100, hex=0x64, ascii=\"d\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x10,  /* 00010 */\n    0x70,  /* 01110 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x70,  /* 01110 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=101, hex=0x65, ascii=\"e\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0xF0,  /* 11110 */\n    0x80,  /* 10000 */\n    0x70,  /* 01110 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=102, hex=0x66, ascii=\"f\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x30,  /* 00110 */\n    0x40,  /* 01000 */\n    0xE0,  /* 11100 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=103, hex=0x67, ascii=\"g\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x70,  /* 01110 */\n    0x90,  /* 10010 */\n    0x70,  /* 01110 */\n    0x10,  /* 00010 */\n    0x60,  /* 01100 */\n\n    /*\n     * code=104, hex=0x68, ascii=\"h\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x80,  /* 10000 */\n    0xE0,  /* 11100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=105, hex=0x69, ascii=\"i\"\n     */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x70,  /* 01110 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=106, hex=0x6A, ascii=\"j\"\n     */\n    0x00,  /* 00000 */\n    0x10,  /* 00010 */\n    0x00,  /* 00000 */\n    0x10,  /* 00010 */\n    0x10,  /* 00010 */\n    0x10,  /* 00010 */\n    0x10,  /* 00010 */\n    0x60,  /* 01100 */\n\n    /*\n     * code=107, hex=0x6B, ascii=\"k\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x80,  /* 10000 */\n    0xA0,  /* 10100 */\n    0xC0,  /* 11000 */\n    0xA0,  /* 10100 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=108, hex=0x6C, ascii=\"l\"\n     */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x70,  /* 01110 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=109, hex=0x6D, ascii=\"m\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0xF0,  /* 11110 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=110, hex=0x6E, ascii=\"n\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0xE0,  /* 11100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=111, hex=0x6F, ascii=\"o\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=112, hex=0x70, ascii=\"p\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0xE0,  /* 11100 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0xE0,  /* 11100 */\n    0x80,  /* 10000 */\n\n    /*\n     * code=113, hex=0x71, ascii=\"q\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x70,  /* 01110 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x70,  /* 01110 */\n    0x10,  /* 00010 */\n\n    /*\n     * code=114, hex=0x72, ascii=\"r\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x50,  /* 01010 */\n    0x60,  /* 01100 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=115, hex=0x73, ascii=\"s\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x70,  /* 01110 */\n    0xC0,  /* 11000 */\n    0x30,  /* 00110 */\n    0xE0,  /* 11100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=116, hex=0x74, ascii=\"t\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x40,  /* 01000 */\n    0xF0,  /* 11110 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x30,  /* 00110 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=117, hex=0x75, ascii=\"u\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x70,  /* 01110 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=118, hex=0x76, ascii=\"v\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x60,  /* 01100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=119, hex=0x77, ascii=\"w\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0xF0,  /* 11110 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=120, hex=0x78, ascii=\"x\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0x60,  /* 01100 */\n    0x60,  /* 01100 */\n    0x90,  /* 10010 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=121, hex=0x79, ascii=\"y\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x90,  /* 10010 */\n    0x90,  /* 10010 */\n    0x70,  /* 01110 */\n    0x10,  /* 00010 */\n    0x60,  /* 01100 */\n\n    /*\n     * code=122, hex=0x7A, ascii=\"z\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0xF0,  /* 11110 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n    0xF0,  /* 11110 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=123, hex=0x7B, ascii=\"{\"\n     */\n    0x00,  /* 00000 */\n    0x10,  /* 00010 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n    0x40,  /* 01000 */\n    0x20,  /* 00100 */\n    0x10,  /* 00010 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=124, hex=0x7C, ascii=\"|\"\n     */\n    0x00,  /* 00000 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x20,  /* 00100 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=125, hex=0x7D, ascii=\"}\"\n     */\n    0x00,  /* 00000 */\n    0x40,  /* 01000 */\n    0x20,  /* 00100 */\n    0x10,  /* 00010 */\n    0x10,  /* 00010 */\n    0x20,  /* 00100 */\n    0x40,  /* 01000 */\n    0x00,  /* 00000 */\n\n    /*\n     * code=126, hex=0x7E, ascii=\"~\"\n     */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x50,  /* 01010 */\n    0xA0,  /* 10100 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n    0x00,  /* 00000 */\n\n    // /*\n    //  * code=127, hex=0x7F, ascii=\"^?\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x50,  /* 01010 */\n    // 0x88,  /* 10001 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=128, hex=0x80, ascii=\"!^@\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x70,  /* 01110 */\n    // 0x80,  /* 10000 */\n    // 0x80,  /* 10000 */\n    // 0x80,  /* 10000 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=129, hex=0x81, ascii=\"!^A\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=130, hex=0x82, ascii=\"!^B\"\n    //  */\n    // 0x10,  /* 00010 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0xF0,  /* 11110 */\n    // 0x80,  /* 10000 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=131, hex=0x83, ascii=\"!^C\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0xC0,  /* 11000 */\n    // 0x20,  /* 00100 */\n    // 0xA0,  /* 10100 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=132, hex=0x84, ascii=\"!^D\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0xC0,  /* 11000 */\n    // 0x20,  /* 00100 */\n    // 0x60,  /* 01100 */\n    // 0xB0,  /* 10110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=133, hex=0x85, ascii=\"!^E\"\n    //  */\n    // 0x40,  /* 01000 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0xC0,  /* 11000 */\n    // 0x20,  /* 00100 */\n    // 0x60,  /* 01100 */\n    // 0xB0,  /* 10110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=134, hex=0x86, ascii=\"!^F\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0xC0,  /* 11000 */\n    // 0x20,  /* 00100 */\n    // 0x60,  /* 01100 */\n    // 0xB0,  /* 10110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=135, hex=0x87, ascii=\"!^G\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x30,  /* 00110 */\n    // 0x40,  /* 01000 */\n    // 0x40,  /* 01000 */\n    // 0x30,  /* 00110 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=136, hex=0x88, ascii=\"!^H\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0xF0,  /* 11110 */\n    // 0x80,  /* 10000 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=137, hex=0x89, ascii=\"!^I\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0xF0,  /* 11110 */\n    // 0x80,  /* 10000 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=138, hex=0x8A, ascii=\"!^J\"\n    //  */\n    // 0x40,  /* 01000 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0xF0,  /* 11110 */\n    // 0x80,  /* 10000 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=139, hex=0x8B, ascii=\"!^K\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=140, hex=0x8C, ascii=\"!^L\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=141, hex=0x8D, ascii=\"!^M\"\n    //  */\n    // 0x40,  /* 01000 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=142, hex=0x8E, ascii=\"!^N\"\n    //  */\n    // 0xA0,  /* 10100 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0xF0,  /* 11110 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=143, hex=0x8F, ascii=\"!^O\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0xF0,  /* 11110 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=144, hex=0x90, ascii=\"!^P\"\n    //  */\n    // 0x10,  /* 00010 */\n    // 0x20,  /* 00100 */\n    // 0xF0,  /* 11110 */\n    // 0x80,  /* 10000 */\n    // 0xE0,  /* 11100 */\n    // 0x80,  /* 10000 */\n    // 0xF0,  /* 11110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=145, hex=0x91, ascii=\"!^Q\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xD8,  /* 11011 */\n    // 0x78,  /* 01111 */\n    // 0xE0,  /* 11100 */\n    // 0xB8,  /* 10111 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=146, hex=0x92, ascii=\"!^R\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x70,  /* 01110 */\n    // 0xA0,  /* 10100 */\n    // 0xF0,  /* 11110 */\n    // 0xA0,  /* 10100 */\n    // 0xB0,  /* 10110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=147, hex=0x93, ascii=\"!^S\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x60,  /* 01100 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=148, hex=0x94, ascii=\"!^T\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x60,  /* 01100 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=149, hex=0x95, ascii=\"!^U\"\n    //  */\n    // 0x40,  /* 01000 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x60,  /* 01100 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=150, hex=0x96, ascii=\"!^V\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=151, hex=0x97, ascii=\"!^W\"\n    //  */\n    // 0x40,  /* 01000 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=152, hex=0x98, ascii=\"!^X\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x70,  /* 01110 */\n    // 0x10,  /* 00010 */\n    // 0x60,  /* 01100 */\n\n    // /*\n    //  * code=153, hex=0x99, ascii=\"!^Y\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x60,  /* 01100 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=154, hex=0x9A, ascii=\"!^Z\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x60,  /* 01100 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=155, hex=0x9B, ascii=\"!^[\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x80,  /* 10000 */\n    // 0x80,  /* 10000 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=156, hex=0x9C, ascii=\"!^\\\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x30,  /* 00110 */\n    // 0x50,  /* 01010 */\n    // 0x40,  /* 01000 */\n    // 0xE0,  /* 11100 */\n    // 0x40,  /* 01000 */\n    // 0xF0,  /* 11110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=157, hex=0x9D, ascii=\"!^]\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0xD8,  /* 11011 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=158, hex=0x9E, ascii=\"!^^\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0xC0,  /* 11000 */\n    // 0xA0,  /* 10100 */\n    // 0xB0,  /* 10110 */\n    // 0xF8,  /* 11111 */\n    // 0x90,  /* 10010 */\n    // 0x88,  /* 10001 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=159, hex=0x9F, ascii=\"!^_\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x30,  /* 00110 */\n    // 0x40,  /* 01000 */\n    // 0x40,  /* 01000 */\n    // 0xF0,  /* 11110 */\n    // 0x40,  /* 01000 */\n    // 0x40,  /* 01000 */\n    // 0x80,  /* 10000 */\n\n    // /*\n    //  * code=160, hex=0xA0, ascii=\"! \"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x40,  /* 01000 */\n    // 0x00,  /* 00000 */\n    // 0xC0,  /* 11000 */\n    // 0x20,  /* 00100 */\n    // 0x60,  /* 01100 */\n    // 0xB0,  /* 10110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=161, hex=0xA1, ascii=\"!!\"\n    //  */\n    // 0x10,  /* 00010 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=162, hex=0xA2, ascii=\"!\"\"\n    //  */\n    // 0x10,  /* 00010 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x60,  /* 01100 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=163, hex=0xA3, ascii=\"!#\"\n    //  */\n    // 0x10,  /* 00010 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=164, hex=0xA4, ascii=\"!$\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0xA0,  /* 10100 */\n    // 0x00,  /* 00000 */\n    // 0xE0,  /* 11100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=165, hex=0xA5, ascii=\"!%\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0xA0,  /* 10100 */\n    // 0x90,  /* 10010 */\n    // 0xD0,  /* 11010 */\n    // 0xD0,  /* 11010 */\n    // 0xB0,  /* 10110 */\n    // 0x90,  /* 10010 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=166, hex=0xA6, ascii=\"!&\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x50,  /* 01010 */\n    // 0x30,  /* 00110 */\n    // 0x00,  /* 00000 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=167, hex=0xA7, ascii=\"!'\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x50,  /* 01010 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=168, hex=0xA8, ascii=\"!(\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x40,  /* 01000 */\n    // 0x90,  /* 10010 */\n    // 0x60,  /* 01100 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=169, hex=0xA9, ascii=\"!)\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x80,  /* 10000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=170, hex=0xAA, ascii=\"!*\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x08,  /* 00001 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=171, hex=0xAB, ascii=\"!+\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x80,  /* 10000 */\n    // 0x90,  /* 10010 */\n    // 0xA0,  /* 10100 */\n    // 0x58,  /* 01011 */\n    // 0x88,  /* 10001 */\n    // 0x38,  /* 00111 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=172, hex=0xAC, ascii=\"!,\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x88,  /* 10001 */\n    // 0x90,  /* 10010 */\n    // 0xA0,  /* 10100 */\n    // 0x48,  /* 01001 */\n    // 0x98,  /* 10011 */\n    // 0x38,  /* 00111 */\n    // 0x08,  /* 00001 */\n\n    // /*\n    //  * code=173, hex=0xAD, ascii=\"!-\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=174, hex=0xAE, ascii=\"!.\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0xA0,  /* 10100 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=175, hex=0xAF, ascii=\"!/\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xA0,  /* 10100 */\n    // 0x50,  /* 01010 */\n    // 0xA0,  /* 10100 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=176, hex=0xB0, ascii=\"!0\"\n    //  */\n    // 0xA8,  /* 10101 */\n    // 0x50,  /* 01010 */\n    // 0xA8,  /* 10101 */\n    // 0x50,  /* 01010 */\n    // 0xA8,  /* 10101 */\n    // 0x50,  /* 01010 */\n    // 0xA8,  /* 10101 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=177, hex=0xB1, ascii=\"!1\"\n    //  */\n    // 0xE8,  /* 11101 */\n    // 0x50,  /* 01010 */\n    // 0xB8,  /* 10111 */\n    // 0x50,  /* 01010 */\n    // 0xE8,  /* 11101 */\n    // 0x50,  /* 01010 */\n    // 0xB8,  /* 10111 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=178, hex=0xB2, ascii=\"!2\"\n    //  */\n    // 0xD8,  /* 11011 */\n    // 0x70,  /* 01110 */\n    // 0xD8,  /* 11011 */\n    // 0x70,  /* 01110 */\n    // 0xD8,  /* 11011 */\n    // 0x70,  /* 01110 */\n    // 0xD8,  /* 11011 */\n    // 0x70,  /* 01110 */\n\n    // /*\n    //  * code=179, hex=0xB3, ascii=\"!3\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=180, hex=0xB4, ascii=\"!4\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0xE0,  /* 11100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=181, hex=0xB5, ascii=\"!5\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0xE0,  /* 11100 */\n    // 0x20,  /* 00100 */\n    // 0xE0,  /* 11100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=182, hex=0xB6, ascii=\"!6\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0xD0,  /* 11010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=183, hex=0xB7, ascii=\"!7\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF0,  /* 11110 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=184, hex=0xB8, ascii=\"!8\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xE0,  /* 11100 */\n    // 0x20,  /* 00100 */\n    // 0xE0,  /* 11100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=185, hex=0xB9, ascii=\"!9\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0xD0,  /* 11010 */\n    // 0x10,  /* 00010 */\n    // 0xD0,  /* 11010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=186, hex=0xBA, ascii=\"!:\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=187, hex=0xBB, ascii=\"!;\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF0,  /* 11110 */\n    // 0x10,  /* 00010 */\n    // 0xD0,  /* 11010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=188, hex=0xBC, ascii=\"!<\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0xD0,  /* 11010 */\n    // 0x10,  /* 00010 */\n    // 0xF0,  /* 11110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=189, hex=0xBD, ascii=\"!=\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0xF0,  /* 11110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=190, hex=0xBE, ascii=\"!>\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0xE0,  /* 11100 */\n    // 0x20,  /* 00100 */\n    // 0xE0,  /* 11100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=191, hex=0xBF, ascii=\"!?\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xE0,  /* 11100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=192, hex=0xC0, ascii=\"!@\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x38,  /* 00111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=193, hex=0xC1, ascii=\"!A\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=194, hex=0xC2, ascii=\"!B\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=195, hex=0xC3, ascii=\"!C\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x38,  /* 00111 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=196, hex=0xC4, ascii=\"!D\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=197, hex=0xC5, ascii=\"!E\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0xF8,  /* 11111 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=198, hex=0xC6, ascii=\"!F\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x38,  /* 00111 */\n    // 0x20,  /* 00100 */\n    // 0x38,  /* 00111 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=199, hex=0xC7, ascii=\"!G\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x58,  /* 01011 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=200, hex=0xC8, ascii=\"!H\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x58,  /* 01011 */\n    // 0x40,  /* 01000 */\n    // 0x78,  /* 01111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=201, hex=0xC9, ascii=\"!I\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x78,  /* 01111 */\n    // 0x40,  /* 01000 */\n    // 0x58,  /* 01011 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=202, hex=0xCA, ascii=\"!J\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0xD8,  /* 11011 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=203, hex=0xCB, ascii=\"!K\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0xD8,  /* 11011 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=204, hex=0xCC, ascii=\"!L\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x58,  /* 01011 */\n    // 0x40,  /* 01000 */\n    // 0x58,  /* 01011 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=205, hex=0xCD, ascii=\"!M\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=206, hex=0xCE, ascii=\"!N\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0xD8,  /* 11011 */\n    // 0x00,  /* 00000 */\n    // 0xD8,  /* 11011 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=207, hex=0xCF, ascii=\"!O\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=208, hex=0xD0, ascii=\"!P\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=209, hex=0xD1, ascii=\"!Q\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=210, hex=0xD2, ascii=\"!R\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=211, hex=0xD3, ascii=\"!S\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x78,  /* 01111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=212, hex=0xD4, ascii=\"!T\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x38,  /* 00111 */\n    // 0x20,  /* 00100 */\n    // 0x38,  /* 00111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=213, hex=0xD5, ascii=\"!U\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x38,  /* 00111 */\n    // 0x20,  /* 00100 */\n    // 0x38,  /* 00111 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=214, hex=0xD6, ascii=\"!V\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x78,  /* 01111 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=215, hex=0xD7, ascii=\"!W\"\n    //  */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0xF8,  /* 11111 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n\n    // /*\n    //  * code=216, hex=0xD8, ascii=\"!X\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0xF8,  /* 11111 */\n    // 0x20,  /* 00100 */\n    // 0xF8,  /* 11111 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=217, hex=0xD9, ascii=\"!Y\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0xE0,  /* 11100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=218, hex=0xDA, ascii=\"!Z\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x38,  /* 00111 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=219, hex=0xDB, ascii=\"![\"\n    //  */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n\n    // /*\n    //  * code=220, hex=0xDC, ascii=\"!\\\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n\n    // /*\n    //  * code=221, hex=0xDD, ascii=\"!]\"\n    //  */\n    // 0xE0,  /* 11100 */\n    // 0xE0,  /* 11100 */\n    // 0xE0,  /* 11100 */\n    // 0xE0,  /* 11100 */\n    // 0xE0,  /* 11100 */\n    // 0xE0,  /* 11100 */\n    // 0xE0,  /* 11100 */\n    // 0xE0,  /* 11100 */\n\n    // /*\n    //  * code=222, hex=0xDE, ascii=\"!^\"\n    //  */\n    // 0x18,  /* 00011 */\n    // 0x18,  /* 00011 */\n    // 0x18,  /* 00011 */\n    // 0x18,  /* 00011 */\n    // 0x18,  /* 00011 */\n    // 0x18,  /* 00011 */\n    // 0x18,  /* 00011 */\n    // 0x18,  /* 00011 */\n\n    // /*\n    //  * code=223, hex=0xDF, ascii=\"!_\"\n    //  */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=224, hex=0xE0, ascii=\"!`\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x68,  /* 01101 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x68,  /* 01101 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=225, hex=0xE1, ascii=\"!a\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0xF0,  /* 11110 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0xE0,  /* 11100 */\n    // 0x80,  /* 10000 */\n\n    // /*\n    //  * code=226, hex=0xE2, ascii=\"!b\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x70,  /* 01110 */\n    // 0x40,  /* 01000 */\n    // 0x40,  /* 01000 */\n    // 0x40,  /* 01000 */\n    // 0x40,  /* 01000 */\n    // 0x40,  /* 01000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=227, hex=0xE3, ascii=\"!c\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x70,  /* 01110 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=228, hex=0xE4, ascii=\"!d\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x48,  /* 01001 */\n    // 0x20,  /* 00100 */\n    // 0x40,  /* 01000 */\n    // 0x88,  /* 10001 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=229, hex=0xE5, ascii=\"!e\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x78,  /* 01111 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x60,  /* 01100 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=230, hex=0xE6, ascii=\"!f\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0xE8,  /* 11101 */\n    // 0x80,  /* 10000 */\n\n    // /*\n    //  * code=231, hex=0xE7, ascii=\"!g\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x98,  /* 10011 */\n    // 0x50,  /* 01010 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=232, hex=0xE8, ascii=\"!h\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x88,  /* 10001 */\n    // 0x70,  /* 01110 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=233, hex=0xE9, ascii=\"!i\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x70,  /* 01110 */\n    // 0x88,  /* 10001 */\n    // 0xF8,  /* 11111 */\n    // 0x88,  /* 10001 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=234, hex=0xEA, ascii=\"!j\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x70,  /* 01110 */\n    // 0x88,  /* 10001 */\n    // 0x88,  /* 10001 */\n    // 0x50,  /* 01010 */\n    // 0xD8,  /* 11011 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=235, hex=0xEB, ascii=\"!k\"\n    //  */\n    // 0x60,  /* 01100 */\n    // 0x80,  /* 10000 */\n    // 0x40,  /* 01000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x60,  /* 01100 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=236, hex=0xEC, ascii=\"!l\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x70,  /* 01110 */\n    // 0xA8,  /* 10101 */\n    // 0xA8,  /* 10101 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=237, hex=0xED, ascii=\"!m\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x08,  /* 00001 */\n    // 0x70,  /* 01110 */\n    // 0xA8,  /* 10101 */\n    // 0x48,  /* 01001 */\n    // 0xB0,  /* 10110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=238, hex=0xEE, ascii=\"!n\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x30,  /* 00110 */\n    // 0x40,  /* 01000 */\n    // 0x70,  /* 01110 */\n    // 0x40,  /* 01000 */\n    // 0x40,  /* 01000 */\n    // 0x30,  /* 00110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=239, hex=0xEF, ascii=\"!o\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x90,  /* 10010 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=240, hex=0xF0, ascii=\"!p\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0xF0,  /* 11110 */\n    // 0x00,  /* 00000 */\n    // 0xF0,  /* 11110 */\n    // 0x00,  /* 00000 */\n    // 0xF0,  /* 11110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=241, hex=0xF1, ascii=\"!q\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0xF8,  /* 11111 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0xF8,  /* 11111 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=242, hex=0xF2, ascii=\"!r\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x40,  /* 01000 */\n    // 0x20,  /* 00100 */\n    // 0x10,  /* 00010 */\n    // 0x20,  /* 00100 */\n    // 0x40,  /* 01000 */\n    // 0xF0,  /* 11110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=243, hex=0xF3, ascii=\"!s\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x10,  /* 00010 */\n    // 0x20,  /* 00100 */\n    // 0x40,  /* 01000 */\n    // 0x20,  /* 00100 */\n    // 0x10,  /* 00010 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=244, hex=0xF4, ascii=\"!t\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x18,  /* 00011 */\n    // 0x28,  /* 00101 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n\n    // /*\n    //  * code=245, hex=0xF5, ascii=\"!u\"\n    //  */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0x20,  /* 00100 */\n    // 0xA0,  /* 10100 */\n    // 0xC0,  /* 11000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=246, hex=0xF6, ascii=\"!v\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x00,  /* 00000 */\n    // 0xF0,  /* 11110 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=247, hex=0xF7, ascii=\"!w\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0xA0,  /* 10100 */\n    // 0x00,  /* 00000 */\n    // 0x50,  /* 01010 */\n    // 0xA0,  /* 10100 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=248, hex=0xF8, ascii=\"!x\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x50,  /* 01010 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=249, hex=0xF9, ascii=\"!y\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x60,  /* 01100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=250, hex=0xFA, ascii=\"!z\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x20,  /* 00100 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=251, hex=0xFB, ascii=\"!{\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x18,  /* 00011 */\n    // 0x10,  /* 00010 */\n    // 0x20,  /* 00100 */\n    // 0xA0,  /* 10100 */\n    // 0x40,  /* 01000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=252, hex=0xFC, ascii=\"!|\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x50,  /* 01010 */\n    // 0x50,  /* 01010 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=253, hex=0xFD, ascii=\"!}\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x60,  /* 01100 */\n    // 0x10,  /* 00010 */\n    // 0x20,  /* 00100 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=254, hex=0xFE, ascii=\"!~\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x70,  /* 01110 */\n    // 0x70,  /* 01110 */\n    // 0x70,  /* 01110 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n\n    // /*\n    //  * code=255, hex=0xFF, ascii=\"!^\"\n    //  */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n    // 0x00,  /* 00000 */\n};\n"
  },
  {
    "path": "wled00/src/font/console_font_6x8.h",
    "content": "// font courtesy of https://github.com/idispatch/raster-fonts\nstatic const unsigned char console_font_6x8[] PROGMEM = {\n\n// code points 0-31 and 127-255 are commented out to save memory, they contain extra characters (CP437),\n// which could be used with an UTF-8 to CP437 conversion\n\n    // /*\n    //  * code=0, hex=0x00, ascii=\"^@\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=1, hex=0x01, ascii=\"^A\"\n    //  */\n    // 0x38,  /* 001110 */\n    // 0x44,  /* 010001 */\n    // 0x6C,  /* 011011 */\n    // 0x44,  /* 010001 */\n    // 0x54,  /* 010101 */\n    // 0x44,  /* 010001 */\n    // 0x38,  /* 001110 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=2, hex=0x02, ascii=\"^B\"\n    //  */\n    // 0x38,  /* 001110 */\n    // 0x7C,  /* 011111 */\n    // 0x54,  /* 010101 */\n    // 0x7C,  /* 011111 */\n    // 0x44,  /* 010001 */\n    // 0x7C,  /* 011111 */\n    // 0x38,  /* 001110 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=3, hex=0x03, ascii=\"^C\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x28,  /* 001010 */\n    // 0x7C,  /* 011111 */\n    // 0x7C,  /* 011111 */\n    // 0x7C,  /* 011111 */\n    // 0x38,  /* 001110 */\n    // 0x10,  /* 000100 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=4, hex=0x04, ascii=\"^D\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x10,  /* 000100 */\n    // 0x38,  /* 001110 */\n    // 0x7C,  /* 011111 */\n    // 0x7C,  /* 011111 */\n    // 0x38,  /* 001110 */\n    // 0x10,  /* 000100 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=5, hex=0x05, ascii=\"^E\"\n    //  */\n    // 0x10,  /* 000100 */\n    // 0x38,  /* 001110 */\n    // 0x38,  /* 001110 */\n    // 0x10,  /* 000100 */\n    // 0x7C,  /* 011111 */\n    // 0x7C,  /* 011111 */\n    // 0x10,  /* 000100 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=6, hex=0x06, ascii=\"^F\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x10,  /* 000100 */\n    // 0x38,  /* 001110 */\n    // 0x7C,  /* 011111 */\n    // 0x7C,  /* 011111 */\n    // 0x10,  /* 000100 */\n    // 0x38,  /* 001110 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=7, hex=0x07, ascii=\"^G\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x30,  /* 001100 */\n    // 0x30,  /* 001100 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=8, hex=0x08, ascii=\"^H\"\n    //  */\n    // 0xFC,  /* 111111 */\n    // 0xFC,  /* 111111 */\n    // 0xFC,  /* 111111 */\n    // 0xCC,  /* 110011 */\n    // 0xCC,  /* 110011 */\n    // 0xFC,  /* 111111 */\n    // 0xFC,  /* 111111 */\n    // 0xFC,  /* 111111 */\n\n    // /*\n    //  * code=9, hex=0x09, ascii=\"^I\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x78,  /* 011110 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x78,  /* 011110 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=10, hex=0x0A, ascii=\"^J\"\n    //  */\n    // 0xFC,  /* 111111 */\n    // 0xFC,  /* 111111 */\n    // 0x84,  /* 100001 */\n    // 0xB4,  /* 101101 */\n    // 0xB4,  /* 101101 */\n    // 0x84,  /* 100001 */\n    // 0xFC,  /* 111111 */\n    // 0xFC,  /* 111111 */\n\n    // /*\n    //  * code=11, hex=0x0B, ascii=\"^K\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x1C,  /* 000111 */\n    // 0x0C,  /* 000011 */\n    // 0x34,  /* 001101 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x30,  /* 001100 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=12, hex=0x0C, ascii=\"^L\"\n    //  */\n    // 0x38,  /* 001110 */\n    // 0x44,  /* 010001 */\n    // 0x44,  /* 010001 */\n    // 0x38,  /* 001110 */\n    // 0x10,  /* 000100 */\n    // 0x38,  /* 001110 */\n    // 0x10,  /* 000100 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=13, hex=0x0D, ascii=\"^M\"\n    //  */\n    // 0x10,  /* 000100 */\n    // 0x18,  /* 000110 */\n    // 0x14,  /* 000101 */\n    // 0x10,  /* 000100 */\n    // 0x30,  /* 001100 */\n    // 0x70,  /* 011100 */\n    // 0x60,  /* 011000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=14, hex=0x0E, ascii=\"^N\"\n    //  */\n    // 0x0C,  /* 000011 */\n    // 0x34,  /* 001101 */\n    // 0x2C,  /* 001011 */\n    // 0x34,  /* 001101 */\n    // 0x2C,  /* 001011 */\n    // 0x6C,  /* 011011 */\n    // 0x60,  /* 011000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=15, hex=0x0F, ascii=\"^O\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x54,  /* 010101 */\n    // 0x38,  /* 001110 */\n    // 0x6C,  /* 011011 */\n    // 0x38,  /* 001110 */\n    // 0x54,  /* 010101 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=16, hex=0x10, ascii=\"^P\"\n    //  */\n    // 0x20,  /* 001000 */\n    // 0x30,  /* 001100 */\n    // 0x38,  /* 001110 */\n    // 0x3C,  /* 001111 */\n    // 0x38,  /* 001110 */\n    // 0x30,  /* 001100 */\n    // 0x20,  /* 001000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=17, hex=0x11, ascii=\"^Q\"\n    //  */\n    // 0x08,  /* 000010 */\n    // 0x18,  /* 000110 */\n    // 0x38,  /* 001110 */\n    // 0x78,  /* 011110 */\n    // 0x38,  /* 001110 */\n    // 0x18,  /* 000110 */\n    // 0x08,  /* 000010 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=18, hex=0x12, ascii=\"^R\"\n    //  */\n    // 0x10,  /* 000100 */\n    // 0x38,  /* 001110 */\n    // 0x7C,  /* 011111 */\n    // 0x10,  /* 000100 */\n    // 0x7C,  /* 011111 */\n    // 0x38,  /* 001110 */\n    // 0x10,  /* 000100 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=19, hex=0x13, ascii=\"^S\"\n    //  */\n    // 0x28,  /* 001010 */\n    // 0x28,  /* 001010 */\n    // 0x28,  /* 001010 */\n    // 0x28,  /* 001010 */\n    // 0x28,  /* 001010 */\n    // 0x00,  /* 000000 */\n    // 0x28,  /* 001010 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=20, hex=0x14, ascii=\"^T\"\n    //  */\n    // 0x3C,  /* 001111 */\n    // 0x54,  /* 010101 */\n    // 0x54,  /* 010101 */\n    // 0x34,  /* 001101 */\n    // 0x14,  /* 000101 */\n    // 0x14,  /* 000101 */\n    // 0x14,  /* 000101 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=21, hex=0x15, ascii=\"^U\"\n    //  */\n    // 0x38,  /* 001110 */\n    // 0x44,  /* 010001 */\n    // 0x30,  /* 001100 */\n    // 0x28,  /* 001010 */\n    // 0x18,  /* 000110 */\n    // 0x44,  /* 010001 */\n    // 0x38,  /* 001110 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=22, hex=0x16, ascii=\"^V\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x78,  /* 011110 */\n    // 0x78,  /* 011110 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=23, hex=0x17, ascii=\"^W\"\n    //  */\n    // 0x10,  /* 000100 */\n    // 0x38,  /* 001110 */\n    // 0x7C,  /* 011111 */\n    // 0x10,  /* 000100 */\n    // 0x7C,  /* 011111 */\n    // 0x38,  /* 001110 */\n    // 0x10,  /* 000100 */\n    // 0x38,  /* 001110 */\n\n    // /*\n    //  * code=24, hex=0x18, ascii=\"^X\"\n    //  */\n    // 0x10,  /* 000100 */\n    // 0x38,  /* 001110 */\n    // 0x7C,  /* 011111 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=25, hex=0x19, ascii=\"^Y\"\n    //  */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x7C,  /* 011111 */\n    // 0x38,  /* 001110 */\n    // 0x10,  /* 000100 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=26, hex=0x1A, ascii=\"^Z\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x10,  /* 000100 */\n    // 0x18,  /* 000110 */\n    // 0x7C,  /* 011111 */\n    // 0x18,  /* 000110 */\n    // 0x10,  /* 000100 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=27, hex=0x1B, ascii=\"^[\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x10,  /* 000100 */\n    // 0x30,  /* 001100 */\n    // 0x7C,  /* 011111 */\n    // 0x30,  /* 001100 */\n    // 0x10,  /* 000100 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=28, hex=0x1C, ascii=\"^\\\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x40,  /* 010000 */\n    // 0x40,  /* 010000 */\n    // 0x40,  /* 010000 */\n    // 0x7C,  /* 011111 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=29, hex=0x1D, ascii=\"^]\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x28,  /* 001010 */\n    // 0x28,  /* 001010 */\n    // 0x7C,  /* 011111 */\n    // 0x28,  /* 001010 */\n    // 0x28,  /* 001010 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=30, hex=0x1E, ascii=\"^^\"\n    //  */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x38,  /* 001110 */\n    // 0x38,  /* 001110 */\n    // 0x7C,  /* 011111 */\n    // 0x7C,  /* 011111 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=31, hex=0x1F, ascii=\"^_\"\n    //  */\n    // 0x7C,  /* 011111 */\n    // 0x7C,  /* 011111 */\n    // 0x38,  /* 001110 */\n    // 0x38,  /* 001110 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    /*\n     * code=32, hex=0x20, ascii=\" \"\n     */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=33, hex=0x21, ascii=\"!\"\n     */\n    0x10,  /* 000100 */\n    0x38,  /* 001110 */\n    0x38,  /* 001110 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x00,  /* 000000 */\n    0x10,  /* 000100 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=34, hex=0x22, ascii=\"\"\"\n     */\n    0x6C,  /* 011011 */\n    0x6C,  /* 011011 */\n    0x48,  /* 010010 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=35, hex=0x23, ascii=\"#\"\n     */\n    0x00,  /* 000000 */\n    0x28,  /* 001010 */\n    0x7C,  /* 011111 */\n    0x28,  /* 001010 */\n    0x28,  /* 001010 */\n    0x7C,  /* 011111 */\n    0x28,  /* 001010 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=36, hex=0x24, ascii=\"$\"\n     */\n    0x20,  /* 001000 */\n    0x38,  /* 001110 */\n    0x40,  /* 010000 */\n    0x30,  /* 001100 */\n    0x08,  /* 000010 */\n    0x70,  /* 011100 */\n    0x10,  /* 000100 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=37, hex=0x25, ascii=\"%\"\n     */\n    0x64,  /* 011001 */\n    0x64,  /* 011001 */\n    0x08,  /* 000010 */\n    0x10,  /* 000100 */\n    0x20,  /* 001000 */\n    0x4C,  /* 010011 */\n    0x4C,  /* 010011 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=38, hex=0x26, ascii=\"&\"\n     */\n    0x20,  /* 001000 */\n    0x50,  /* 010100 */\n    0x50,  /* 010100 */\n    0x20,  /* 001000 */\n    0x54,  /* 010101 */\n    0x48,  /* 010010 */\n    0x34,  /* 001101 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=39, hex=0x27, ascii=\"'\"\n     */\n    0x30,  /* 001100 */\n    0x30,  /* 001100 */\n    0x20,  /* 001000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=40, hex=0x28, ascii=\"(\"\n     */\n    0x10,  /* 000100 */\n    0x20,  /* 001000 */\n    0x20,  /* 001000 */\n    0x20,  /* 001000 */\n    0x20,  /* 001000 */\n    0x20,  /* 001000 */\n    0x10,  /* 000100 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=41, hex=0x29, ascii=\")\"\n     */\n    0x20,  /* 001000 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x20,  /* 001000 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=42, hex=0x2A, ascii=\"*\"\n     */\n    0x00,  /* 000000 */\n    0x28,  /* 001010 */\n    0x38,  /* 001110 */\n    0x7C,  /* 011111 */\n    0x38,  /* 001110 */\n    0x28,  /* 001010 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=43, hex=0x2B, ascii=\"+\"\n     */\n    0x00,  /* 000000 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x7C,  /* 011111 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=44, hex=0x2C, ascii=\",\"\n     */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x30,  /* 001100 */\n    0x30,  /* 001100 */\n    0x20,  /* 001000 */\n\n    /*\n     * code=45, hex=0x2D, ascii=\"-\"\n     */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x7C,  /* 011111 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=46, hex=0x2E, ascii=\".\"\n     */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x30,  /* 001100 */\n    0x30,  /* 001100 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=47, hex=0x2F, ascii=\"/\"\n     */\n    0x00,  /* 000000 */\n    0x04,  /* 000001 */\n    0x08,  /* 000010 */\n    0x10,  /* 000100 */\n    0x20,  /* 001000 */\n    0x40,  /* 010000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=48, hex=0x30, ascii=\"0\"\n     */\n    0x38,  /* 001110 */\n    0x44,  /* 010001 */\n    0x4C,  /* 010011 */\n    0x54,  /* 010101 */\n    0x64,  /* 011001 */\n    0x44,  /* 010001 */\n    0x38,  /* 001110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=49, hex=0x31, ascii=\"1\"\n     */\n    0x10,  /* 000100 */\n    0x30,  /* 001100 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x38,  /* 001110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=50, hex=0x32, ascii=\"2\"\n     */\n    0x38,  /* 001110 */\n    0x44,  /* 010001 */\n    0x04,  /* 000001 */\n    0x18,  /* 000110 */\n    0x20,  /* 001000 */\n    0x40,  /* 010000 */\n    0x7C,  /* 011111 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=51, hex=0x33, ascii=\"3\"\n     */\n    0x38,  /* 001110 */\n    0x44,  /* 010001 */\n    0x04,  /* 000001 */\n    0x38,  /* 001110 */\n    0x04,  /* 000001 */\n    0x44,  /* 010001 */\n    0x38,  /* 001110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=52, hex=0x34, ascii=\"4\"\n     */\n    0x08,  /* 000010 */\n    0x18,  /* 000110 */\n    0x28,  /* 001010 */\n    0x48,  /* 010010 */\n    0x7C,  /* 011111 */\n    0x08,  /* 000010 */\n    0x08,  /* 000010 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=53, hex=0x35, ascii=\"5\"\n     */\n    0x7C,  /* 011111 */\n    0x40,  /* 010000 */\n    0x40,  /* 010000 */\n    0x78,  /* 011110 */\n    0x04,  /* 000001 */\n    0x44,  /* 010001 */\n    0x38,  /* 001110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=54, hex=0x36, ascii=\"6\"\n     */\n    0x18,  /* 000110 */\n    0x20,  /* 001000 */\n    0x40,  /* 010000 */\n    0x78,  /* 011110 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x38,  /* 001110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=55, hex=0x37, ascii=\"7\"\n     */\n    0x7C,  /* 011111 */\n    0x04,  /* 000001 */\n    0x08,  /* 000010 */\n    0x10,  /* 000100 */\n    0x20,  /* 001000 */\n    0x20,  /* 001000 */\n    0x20,  /* 001000 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=56, hex=0x38, ascii=\"8\"\n     */\n    0x38,  /* 001110 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x38,  /* 001110 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x38,  /* 001110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=57, hex=0x39, ascii=\"9\"\n     */\n    0x38,  /* 001110 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x3C,  /* 001111 */\n    0x04,  /* 000001 */\n    0x08,  /* 000010 */\n    0x30,  /* 001100 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=58, hex=0x3A, ascii=\":\"\n     */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x30,  /* 001100 */\n    0x30,  /* 001100 */\n    0x00,  /* 000000 */\n    0x30,  /* 001100 */\n    0x30,  /* 001100 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=59, hex=0x3B, ascii=\";\"\n     */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x30,  /* 001100 */\n    0x30,  /* 001100 */\n    0x00,  /* 000000 */\n    0x30,  /* 001100 */\n    0x30,  /* 001100 */\n    0x20,  /* 001000 */\n\n    /*\n     * code=60, hex=0x3C, ascii=\"<\"\n     */\n    0x08,  /* 000010 */\n    0x10,  /* 000100 */\n    0x20,  /* 001000 */\n    0x40,  /* 010000 */\n    0x20,  /* 001000 */\n    0x10,  /* 000100 */\n    0x08,  /* 000010 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=61, hex=0x3D, ascii=\"=\"\n     */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x7C,  /* 011111 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x7C,  /* 011111 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=62, hex=0x3E, ascii=\">\"\n     */\n    0x20,  /* 001000 */\n    0x10,  /* 000100 */\n    0x08,  /* 000010 */\n    0x04,  /* 000001 */\n    0x08,  /* 000010 */\n    0x10,  /* 000100 */\n    0x20,  /* 001000 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=63, hex=0x3F, ascii=\"?\"\n     */\n    0x38,  /* 001110 */\n    0x44,  /* 010001 */\n    0x04,  /* 000001 */\n    0x18,  /* 000110 */\n    0x10,  /* 000100 */\n    0x00,  /* 000000 */\n    0x10,  /* 000100 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=64, hex=0x40, ascii=\"@\"\n     */\n    0x38,  /* 001110 */\n    0x44,  /* 010001 */\n    0x5C,  /* 010111 */\n    0x54,  /* 010101 */\n    0x5C,  /* 010111 */\n    0x40,  /* 010000 */\n    0x38,  /* 001110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=65, hex=0x41, ascii=\"A\"\n     */\n    0x38,  /* 001110 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x7C,  /* 011111 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=66, hex=0x42, ascii=\"B\"\n     */\n    0x78,  /* 011110 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x78,  /* 011110 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x78,  /* 011110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=67, hex=0x43, ascii=\"C\"\n     */\n    0x38,  /* 001110 */\n    0x44,  /* 010001 */\n    0x40,  /* 010000 */\n    0x40,  /* 010000 */\n    0x40,  /* 010000 */\n    0x44,  /* 010001 */\n    0x38,  /* 001110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=68, hex=0x44, ascii=\"D\"\n     */\n    0x78,  /* 011110 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x78,  /* 011110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=69, hex=0x45, ascii=\"E\"\n     */\n    0x7C,  /* 011111 */\n    0x40,  /* 010000 */\n    0x40,  /* 010000 */\n    0x78,  /* 011110 */\n    0x40,  /* 010000 */\n    0x40,  /* 010000 */\n    0x7C,  /* 011111 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=70, hex=0x46, ascii=\"F\"\n     */\n    0x7C,  /* 011111 */\n    0x40,  /* 010000 */\n    0x40,  /* 010000 */\n    0x78,  /* 011110 */\n    0x40,  /* 010000 */\n    0x40,  /* 010000 */\n    0x40,  /* 010000 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=71, hex=0x47, ascii=\"G\"\n     */\n    0x38,  /* 001110 */\n    0x44,  /* 010001 */\n    0x40,  /* 010000 */\n    0x5C,  /* 010111 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x3C,  /* 001111 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=72, hex=0x48, ascii=\"H\"\n     */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x7C,  /* 011111 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=73, hex=0x49, ascii=\"I\"\n     */\n    0x38,  /* 001110 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x38,  /* 001110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=74, hex=0x4A, ascii=\"J\"\n     */\n    0x04,  /* 000001 */\n    0x04,  /* 000001 */\n    0x04,  /* 000001 */\n    0x04,  /* 000001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x38,  /* 001110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=75, hex=0x4B, ascii=\"K\"\n     */\n    0x44,  /* 010001 */\n    0x48,  /* 010010 */\n    0x50,  /* 010100 */\n    0x60,  /* 011000 */\n    0x50,  /* 010100 */\n    0x48,  /* 010010 */\n    0x44,  /* 010001 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=76, hex=0x4C, ascii=\"L\"\n     */\n    0x40,  /* 010000 */\n    0x40,  /* 010000 */\n    0x40,  /* 010000 */\n    0x40,  /* 010000 */\n    0x40,  /* 010000 */\n    0x40,  /* 010000 */\n    0x7C,  /* 011111 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=77, hex=0x4D, ascii=\"M\"\n     */\n    0x44,  /* 010001 */\n    0x6C,  /* 011011 */\n    0x54,  /* 010101 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=78, hex=0x4E, ascii=\"N\"\n     */\n    0x44,  /* 010001 */\n    0x64,  /* 011001 */\n    0x54,  /* 010101 */\n    0x4C,  /* 010011 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=79, hex=0x4F, ascii=\"O\"\n     */\n    0x38,  /* 001110 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x38,  /* 001110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=80, hex=0x50, ascii=\"P\"\n     */\n    0x78,  /* 011110 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x78,  /* 011110 */\n    0x40,  /* 010000 */\n    0x40,  /* 010000 */\n    0x40,  /* 010000 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=81, hex=0x51, ascii=\"Q\"\n     */\n    0x38,  /* 001110 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x54,  /* 010101 */\n    0x48,  /* 010010 */\n    0x34,  /* 001101 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=82, hex=0x52, ascii=\"R\"\n     */\n    0x78,  /* 011110 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x78,  /* 011110 */\n    0x48,  /* 010010 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=83, hex=0x53, ascii=\"S\"\n     */\n    0x38,  /* 001110 */\n    0x44,  /* 010001 */\n    0x40,  /* 010000 */\n    0x38,  /* 001110 */\n    0x04,  /* 000001 */\n    0x44,  /* 010001 */\n    0x38,  /* 001110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=84, hex=0x54, ascii=\"T\"\n     */\n    0x7C,  /* 011111 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=85, hex=0x55, ascii=\"U\"\n     */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x38,  /* 001110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=86, hex=0x56, ascii=\"V\"\n     */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x28,  /* 001010 */\n    0x10,  /* 000100 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=87, hex=0x57, ascii=\"W\"\n     */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x54,  /* 010101 */\n    0x54,  /* 010101 */\n    0x54,  /* 010101 */\n    0x54,  /* 010101 */\n    0x28,  /* 001010 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=88, hex=0x58, ascii=\"X\"\n     */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x28,  /* 001010 */\n    0x10,  /* 000100 */\n    0x28,  /* 001010 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=89, hex=0x59, ascii=\"Y\"\n     */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x28,  /* 001010 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=90, hex=0x5A, ascii=\"Z\"\n     */\n    0x78,  /* 011110 */\n    0x08,  /* 000010 */\n    0x10,  /* 000100 */\n    0x20,  /* 001000 */\n    0x40,  /* 010000 */\n    0x40,  /* 010000 */\n    0x78,  /* 011110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=91, hex=0x5B, ascii=\"[\"\n     */\n    0x38,  /* 001110 */\n    0x20,  /* 001000 */\n    0x20,  /* 001000 */\n    0x20,  /* 001000 */\n    0x20,  /* 001000 */\n    0x20,  /* 001000 */\n    0x38,  /* 001110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=92, hex=0x5C, ascii=\"\\\"\n     */\n    0x00,  /* 000000 */\n    0x40,  /* 010000 */\n    0x20,  /* 001000 */\n    0x10,  /* 000100 */\n    0x08,  /* 000010 */\n    0x04,  /* 000001 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=93, hex=0x5D, ascii=\"]\"\n     */\n    0x38,  /* 001110 */\n    0x08,  /* 000010 */\n    0x08,  /* 000010 */\n    0x08,  /* 000010 */\n    0x08,  /* 000010 */\n    0x08,  /* 000010 */\n    0x38,  /* 001110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=94, hex=0x5E, ascii=\"^\"\n     */\n    0x10,  /* 000100 */\n    0x28,  /* 001010 */\n    0x44,  /* 010001 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=95, hex=0x5F, ascii=\"_\"\n     */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0xFC,  /* 111111 */\n\n    /*\n     * code=96, hex=0x60, ascii=\"`\"\n     */\n    0x30,  /* 001100 */\n    0x30,  /* 001100 */\n    0x10,  /* 000100 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=97, hex=0x61, ascii=\"a\"\n     */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x38,  /* 001110 */\n    0x04,  /* 000001 */\n    0x3C,  /* 001111 */\n    0x44,  /* 010001 */\n    0x3C,  /* 001111 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=98, hex=0x62, ascii=\"b\"\n     */\n    0x40,  /* 010000 */\n    0x40,  /* 010000 */\n    0x78,  /* 011110 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x78,  /* 011110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=99, hex=0x63, ascii=\"c\"\n     */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x38,  /* 001110 */\n    0x44,  /* 010001 */\n    0x40,  /* 010000 */\n    0x44,  /* 010001 */\n    0x38,  /* 001110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=100, hex=0x64, ascii=\"d\"\n     */\n    0x04,  /* 000001 */\n    0x04,  /* 000001 */\n    0x3C,  /* 001111 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x3C,  /* 001111 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=101, hex=0x65, ascii=\"e\"\n     */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x38,  /* 001110 */\n    0x44,  /* 010001 */\n    0x78,  /* 011110 */\n    0x40,  /* 010000 */\n    0x38,  /* 001110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=102, hex=0x66, ascii=\"f\"\n     */\n    0x18,  /* 000110 */\n    0x20,  /* 001000 */\n    0x20,  /* 001000 */\n    0x78,  /* 011110 */\n    0x20,  /* 001000 */\n    0x20,  /* 001000 */\n    0x20,  /* 001000 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=103, hex=0x67, ascii=\"g\"\n     */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x3C,  /* 001111 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x3C,  /* 001111 */\n    0x04,  /* 000001 */\n    0x38,  /* 001110 */\n\n    /*\n     * code=104, hex=0x68, ascii=\"h\"\n     */\n    0x40,  /* 010000 */\n    0x40,  /* 010000 */\n    0x70,  /* 011100 */\n    0x48,  /* 010010 */\n    0x48,  /* 010010 */\n    0x48,  /* 010010 */\n    0x48,  /* 010010 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=105, hex=0x69, ascii=\"i\"\n     */\n    0x10,  /* 000100 */\n    0x00,  /* 000000 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x18,  /* 000110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=106, hex=0x6A, ascii=\"j\"\n     */\n    0x08,  /* 000010 */\n    0x00,  /* 000000 */\n    0x18,  /* 000110 */\n    0x08,  /* 000010 */\n    0x08,  /* 000010 */\n    0x08,  /* 000010 */\n    0x48,  /* 010010 */\n    0x30,  /* 001100 */\n\n    /*\n     * code=107, hex=0x6B, ascii=\"k\"\n     */\n    0x40,  /* 010000 */\n    0x40,  /* 010000 */\n    0x48,  /* 010010 */\n    0x50,  /* 010100 */\n    0x60,  /* 011000 */\n    0x50,  /* 010100 */\n    0x48,  /* 010010 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=108, hex=0x6C, ascii=\"l\"\n     */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x18,  /* 000110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=109, hex=0x6D, ascii=\"m\"\n     */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x68,  /* 011010 */\n    0x54,  /* 010101 */\n    0x54,  /* 010101 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=110, hex=0x6E, ascii=\"n\"\n     */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x70,  /* 011100 */\n    0x48,  /* 010010 */\n    0x48,  /* 010010 */\n    0x48,  /* 010010 */\n    0x48,  /* 010010 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=111, hex=0x6F, ascii=\"o\"\n     */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x38,  /* 001110 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x38,  /* 001110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=112, hex=0x70, ascii=\"p\"\n     */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x78,  /* 011110 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x78,  /* 011110 */\n    0x40,  /* 010000 */\n\n    /*\n     * code=113, hex=0x71, ascii=\"q\"\n     */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x3C,  /* 001111 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x3C,  /* 001111 */\n    0x04,  /* 000001 */\n\n    /*\n     * code=114, hex=0x72, ascii=\"r\"\n     */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x58,  /* 010110 */\n    0x24,  /* 001001 */\n    0x20,  /* 001000 */\n    0x20,  /* 001000 */\n    0x70,  /* 011100 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=115, hex=0x73, ascii=\"s\"\n     */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x38,  /* 001110 */\n    0x40,  /* 010000 */\n    0x38,  /* 001110 */\n    0x04,  /* 000001 */\n    0x38,  /* 001110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=116, hex=0x74, ascii=\"t\"\n     */\n    0x00,  /* 000000 */\n    0x20,  /* 001000 */\n    0x78,  /* 011110 */\n    0x20,  /* 001000 */\n    0x20,  /* 001000 */\n    0x28,  /* 001010 */\n    0x10,  /* 000100 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=117, hex=0x75, ascii=\"u\"\n     */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x48,  /* 010010 */\n    0x48,  /* 010010 */\n    0x48,  /* 010010 */\n    0x58,  /* 010110 */\n    0x28,  /* 001010 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=118, hex=0x76, ascii=\"v\"\n     */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x28,  /* 001010 */\n    0x10,  /* 000100 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=119, hex=0x77, ascii=\"w\"\n     */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x44,  /* 010001 */\n    0x44,  /* 010001 */\n    0x54,  /* 010101 */\n    0x7C,  /* 011111 */\n    0x28,  /* 001010 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=120, hex=0x78, ascii=\"x\"\n     */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x48,  /* 010010 */\n    0x48,  /* 010010 */\n    0x30,  /* 001100 */\n    0x48,  /* 010010 */\n    0x48,  /* 010010 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=121, hex=0x79, ascii=\"y\"\n     */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x48,  /* 010010 */\n    0x48,  /* 010010 */\n    0x48,  /* 010010 */\n    0x38,  /* 001110 */\n    0x10,  /* 000100 */\n    0x60,  /* 011000 */\n\n    /*\n     * code=122, hex=0x7A, ascii=\"z\"\n     */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x78,  /* 011110 */\n    0x08,  /* 000010 */\n    0x30,  /* 001100 */\n    0x40,  /* 010000 */\n    0x78,  /* 011110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=123, hex=0x7B, ascii=\"{\"\n     */\n    0x18,  /* 000110 */\n    0x20,  /* 001000 */\n    0x20,  /* 001000 */\n    0x60,  /* 011000 */\n    0x20,  /* 001000 */\n    0x20,  /* 001000 */\n    0x18,  /* 000110 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=124, hex=0x7C, ascii=\"|\"\n     */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x00,  /* 000000 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x10,  /* 000100 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=125, hex=0x7D, ascii=\"}\"\n     */\n    0x30,  /* 001100 */\n    0x08,  /* 000010 */\n    0x08,  /* 000010 */\n    0x0C,  /* 000011 */\n    0x08,  /* 000010 */\n    0x08,  /* 000010 */\n    0x30,  /* 001100 */\n    0x00,  /* 000000 */\n\n    /*\n     * code=126, hex=0x7E, ascii=\"~\"\n     */\n    0x28,  /* 001010 */\n    0x50,  /* 010100 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n    0x00,  /* 000000 */\n\n    // /*\n    //  * code=127, hex=0x7F, ascii=\"^?\"\n    //  */\n    // 0x10,  /* 000100 */\n    // 0x38,  /* 001110 */\n    // 0x6C,  /* 011011 */\n    // 0x44,  /* 010001 */\n    // 0x44,  /* 010001 */\n    // 0x7C,  /* 011111 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=128, hex=0x80, ascii=\"!^@\"\n    //  */\n    // 0x38,  /* 001110 */\n    // 0x44,  /* 010001 */\n    // 0x40,  /* 010000 */\n    // 0x40,  /* 010000 */\n    // 0x44,  /* 010001 */\n    // 0x38,  /* 001110 */\n    // 0x10,  /* 000100 */\n    // 0x30,  /* 001100 */\n\n    // /*\n    //  * code=129, hex=0x81, ascii=\"!^A\"\n    //  */\n    // 0x48,  /* 010010 */\n    // 0x00,  /* 000000 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x58,  /* 010110 */\n    // 0x28,  /* 001010 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=130, hex=0x82, ascii=\"!^B\"\n    //  */\n    // 0x0C,  /* 000011 */\n    // 0x00,  /* 000000 */\n    // 0x38,  /* 001110 */\n    // 0x44,  /* 010001 */\n    // 0x78,  /* 011110 */\n    // 0x40,  /* 010000 */\n    // 0x38,  /* 001110 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=131, hex=0x83, ascii=\"!^C\"\n    //  */\n    // 0x38,  /* 001110 */\n    // 0x00,  /* 000000 */\n    // 0x38,  /* 001110 */\n    // 0x04,  /* 000001 */\n    // 0x3C,  /* 001111 */\n    // 0x44,  /* 010001 */\n    // 0x3C,  /* 001111 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=132, hex=0x84, ascii=\"!^D\"\n    //  */\n    // 0x28,  /* 001010 */\n    // 0x00,  /* 000000 */\n    // 0x38,  /* 001110 */\n    // 0x04,  /* 000001 */\n    // 0x3C,  /* 001111 */\n    // 0x44,  /* 010001 */\n    // 0x3C,  /* 001111 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=133, hex=0x85, ascii=\"!^E\"\n    //  */\n    // 0x30,  /* 001100 */\n    // 0x00,  /* 000000 */\n    // 0x38,  /* 001110 */\n    // 0x04,  /* 000001 */\n    // 0x3C,  /* 001111 */\n    // 0x44,  /* 010001 */\n    // 0x3C,  /* 001111 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=134, hex=0x86, ascii=\"!^F\"\n    //  */\n    // 0x38,  /* 001110 */\n    // 0x28,  /* 001010 */\n    // 0x38,  /* 001110 */\n    // 0x04,  /* 000001 */\n    // 0x3C,  /* 001111 */\n    // 0x44,  /* 010001 */\n    // 0x3C,  /* 001111 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=135, hex=0x87, ascii=\"!^G\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x38,  /* 001110 */\n    // 0x44,  /* 010001 */\n    // 0x40,  /* 010000 */\n    // 0x44,  /* 010001 */\n    // 0x38,  /* 001110 */\n    // 0x10,  /* 000100 */\n    // 0x30,  /* 001100 */\n\n    // /*\n    //  * code=136, hex=0x88, ascii=\"!^H\"\n    //  */\n    // 0x38,  /* 001110 */\n    // 0x00,  /* 000000 */\n    // 0x38,  /* 001110 */\n    // 0x44,  /* 010001 */\n    // 0x78,  /* 011110 */\n    // 0x40,  /* 010000 */\n    // 0x38,  /* 001110 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=137, hex=0x89, ascii=\"!^I\"\n    //  */\n    // 0x28,  /* 001010 */\n    // 0x00,  /* 000000 */\n    // 0x38,  /* 001110 */\n    // 0x44,  /* 010001 */\n    // 0x78,  /* 011110 */\n    // 0x40,  /* 010000 */\n    // 0x38,  /* 001110 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=138, hex=0x8A, ascii=\"!^J\"\n    //  */\n    // 0x30,  /* 001100 */\n    // 0x00,  /* 000000 */\n    // 0x38,  /* 001110 */\n    // 0x44,  /* 010001 */\n    // 0x78,  /* 011110 */\n    // 0x40,  /* 010000 */\n    // 0x38,  /* 001110 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=139, hex=0x8B, ascii=\"!^K\"\n    //  */\n    // 0x28,  /* 001010 */\n    // 0x00,  /* 000000 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x18,  /* 000110 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=140, hex=0x8C, ascii=\"!^L\"\n    //  */\n    // 0x10,  /* 000100 */\n    // 0x28,  /* 001010 */\n    // 0x00,  /* 000000 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x18,  /* 000110 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=141, hex=0x8D, ascii=\"!^M\"\n    //  */\n    // 0x20,  /* 001000 */\n    // 0x00,  /* 000000 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x18,  /* 000110 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=142, hex=0x8E, ascii=\"!^N\"\n    //  */\n    // 0x28,  /* 001010 */\n    // 0x00,  /* 000000 */\n    // 0x10,  /* 000100 */\n    // 0x28,  /* 001010 */\n    // 0x44,  /* 010001 */\n    // 0x7C,  /* 011111 */\n    // 0x44,  /* 010001 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=143, hex=0x8F, ascii=\"!^O\"\n    //  */\n    // 0x38,  /* 001110 */\n    // 0x28,  /* 001010 */\n    // 0x38,  /* 001110 */\n    // 0x6C,  /* 011011 */\n    // 0x44,  /* 010001 */\n    // 0x7C,  /* 011111 */\n    // 0x44,  /* 010001 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=144, hex=0x90, ascii=\"!^P\"\n    //  */\n    // 0x0C,  /* 000011 */\n    // 0x00,  /* 000000 */\n    // 0x7C,  /* 011111 */\n    // 0x40,  /* 010000 */\n    // 0x78,  /* 011110 */\n    // 0x40,  /* 010000 */\n    // 0x7C,  /* 011111 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=145, hex=0x91, ascii=\"!^Q\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x78,  /* 011110 */\n    // 0x14,  /* 000101 */\n    // 0x7C,  /* 011111 */\n    // 0x50,  /* 010100 */\n    // 0x3C,  /* 001111 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=146, hex=0x92, ascii=\"!^R\"\n    //  */\n    // 0x3C,  /* 001111 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x7C,  /* 011111 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x5C,  /* 010111 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=147, hex=0x93, ascii=\"!^S\"\n    //  */\n    // 0x38,  /* 001110 */\n    // 0x00,  /* 000000 */\n    // 0x30,  /* 001100 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x30,  /* 001100 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=148, hex=0x94, ascii=\"!^T\"\n    //  */\n    // 0x28,  /* 001010 */\n    // 0x00,  /* 000000 */\n    // 0x30,  /* 001100 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x30,  /* 001100 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=149, hex=0x95, ascii=\"!^U\"\n    //  */\n    // 0x60,  /* 011000 */\n    // 0x00,  /* 000000 */\n    // 0x30,  /* 001100 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x30,  /* 001100 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=150, hex=0x96, ascii=\"!^V\"\n    //  */\n    // 0x38,  /* 001110 */\n    // 0x00,  /* 000000 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x58,  /* 010110 */\n    // 0x28,  /* 001010 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=151, hex=0x97, ascii=\"!^W\"\n    //  */\n    // 0x60,  /* 011000 */\n    // 0x00,  /* 000000 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x58,  /* 010110 */\n    // 0x28,  /* 001010 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=152, hex=0x98, ascii=\"!^X\"\n    //  */\n    // 0x28,  /* 001010 */\n    // 0x00,  /* 000000 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x38,  /* 001110 */\n    // 0x10,  /* 000100 */\n    // 0x60,  /* 011000 */\n\n    // /*\n    //  * code=153, hex=0x99, ascii=\"!^Y\"\n    //  */\n    // 0x48,  /* 010010 */\n    // 0x30,  /* 001100 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x30,  /* 001100 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=154, hex=0x9A, ascii=\"!^Z\"\n    //  */\n    // 0x28,  /* 001010 */\n    // 0x00,  /* 000000 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x30,  /* 001100 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=155, hex=0x9B, ascii=\"!^[\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x10,  /* 000100 */\n    // 0x38,  /* 001110 */\n    // 0x40,  /* 010000 */\n    // 0x40,  /* 010000 */\n    // 0x38,  /* 001110 */\n    // 0x10,  /* 000100 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=156, hex=0x9C, ascii=\"!^\\\"\n    //  */\n    // 0x18,  /* 000110 */\n    // 0x24,  /* 001001 */\n    // 0x20,  /* 001000 */\n    // 0x78,  /* 011110 */\n    // 0x20,  /* 001000 */\n    // 0x24,  /* 001001 */\n    // 0x5C,  /* 010111 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=157, hex=0x9D, ascii=\"!^]\"\n    //  */\n    // 0x44,  /* 010001 */\n    // 0x28,  /* 001010 */\n    // 0x10,  /* 000100 */\n    // 0x7C,  /* 011111 */\n    // 0x10,  /* 000100 */\n    // 0x7C,  /* 011111 */\n    // 0x10,  /* 000100 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=158, hex=0x9E, ascii=\"!^^\"\n    //  */\n    // 0x60,  /* 011000 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x68,  /* 011010 */\n    // 0x5C,  /* 010111 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=159, hex=0x9F, ascii=\"!^_\"\n    //  */\n    // 0x08,  /* 000010 */\n    // 0x14,  /* 000101 */\n    // 0x10,  /* 000100 */\n    // 0x38,  /* 001110 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x50,  /* 010100 */\n    // 0x20,  /* 001000 */\n\n    // /*\n    //  * code=160, hex=0xA0, ascii=\"! \"\n    //  */\n    // 0x18,  /* 000110 */\n    // 0x00,  /* 000000 */\n    // 0x38,  /* 001110 */\n    // 0x04,  /* 000001 */\n    // 0x3C,  /* 001111 */\n    // 0x44,  /* 010001 */\n    // 0x3C,  /* 001111 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=161, hex=0xA1, ascii=\"!!\"\n    //  */\n    // 0x18,  /* 000110 */\n    // 0x00,  /* 000000 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x18,  /* 000110 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=162, hex=0xA2, ascii=\"!\"\"\n    //  */\n    // 0x18,  /* 000110 */\n    // 0x00,  /* 000000 */\n    // 0x30,  /* 001100 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x30,  /* 001100 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=163, hex=0xA3, ascii=\"!#\"\n    //  */\n    // 0x18,  /* 000110 */\n    // 0x00,  /* 000000 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x58,  /* 010110 */\n    // 0x28,  /* 001010 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=164, hex=0xA4, ascii=\"!$\"\n    //  */\n    // 0x28,  /* 001010 */\n    // 0x50,  /* 010100 */\n    // 0x00,  /* 000000 */\n    // 0x70,  /* 011100 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=165, hex=0xA5, ascii=\"!%\"\n    //  */\n    // 0x28,  /* 001010 */\n    // 0x50,  /* 010100 */\n    // 0x00,  /* 000000 */\n    // 0x48,  /* 010010 */\n    // 0x68,  /* 011010 */\n    // 0x58,  /* 010110 */\n    // 0x48,  /* 010010 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=166, hex=0xA6, ascii=\"!&\"\n    //  */\n    // 0x38,  /* 001110 */\n    // 0x04,  /* 000001 */\n    // 0x3C,  /* 001111 */\n    // 0x44,  /* 010001 */\n    // 0x3C,  /* 001111 */\n    // 0x00,  /* 000000 */\n    // 0x3C,  /* 001111 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=167, hex=0xA7, ascii=\"!'\"\n    //  */\n    // 0x30,  /* 001100 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x30,  /* 001100 */\n    // 0x00,  /* 000000 */\n    // 0x78,  /* 011110 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=168, hex=0xA8, ascii=\"!(\"\n    //  */\n    // 0x10,  /* 000100 */\n    // 0x00,  /* 000000 */\n    // 0x10,  /* 000100 */\n    // 0x30,  /* 001100 */\n    // 0x40,  /* 010000 */\n    // 0x44,  /* 010001 */\n    // 0x38,  /* 001110 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=169, hex=0xA9, ascii=\"!)\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x7C,  /* 011111 */\n    // 0x40,  /* 010000 */\n    // 0x40,  /* 010000 */\n    // 0x40,  /* 010000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=170, hex=0xAA, ascii=\"!*\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0xFC,  /* 111111 */\n    // 0x04,  /* 000001 */\n    // 0x04,  /* 000001 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=171, hex=0xAB, ascii=\"!+\"\n    //  */\n    // 0x40,  /* 010000 */\n    // 0x48,  /* 010010 */\n    // 0x50,  /* 010100 */\n    // 0x38,  /* 001110 */\n    // 0x44,  /* 010001 */\n    // 0x08,  /* 000010 */\n    // 0x1C,  /* 000111 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=172, hex=0xAC, ascii=\"!,\"\n    //  */\n    // 0x40,  /* 010000 */\n    // 0x48,  /* 010010 */\n    // 0x50,  /* 010100 */\n    // 0x2C,  /* 001011 */\n    // 0x54,  /* 010101 */\n    // 0x1C,  /* 000111 */\n    // 0x04,  /* 000001 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=173, hex=0xAD, ascii=\"!-\"\n    //  */\n    // 0x10,  /* 000100 */\n    // 0x00,  /* 000000 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x38,  /* 001110 */\n    // 0x38,  /* 001110 */\n    // 0x10,  /* 000100 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=174, hex=0xAE, ascii=\"!.\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x24,  /* 001001 */\n    // 0x48,  /* 010010 */\n    // 0x24,  /* 001001 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=175, hex=0xAF, ascii=\"!/\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x48,  /* 010010 */\n    // 0x24,  /* 001001 */\n    // 0x48,  /* 010010 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=176, hex=0xB0, ascii=\"!0\"\n    //  */\n    // 0x54,  /* 010101 */\n    // 0x00,  /* 000000 */\n    // 0xA8,  /* 101010 */\n    // 0x00,  /* 000000 */\n    // 0x54,  /* 010101 */\n    // 0x00,  /* 000000 */\n    // 0xA8,  /* 101010 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=177, hex=0xB1, ascii=\"!1\"\n    //  */\n    // 0x54,  /* 010101 */\n    // 0xA8,  /* 101010 */\n    // 0x54,  /* 010101 */\n    // 0xA8,  /* 101010 */\n    // 0x54,  /* 010101 */\n    // 0xA8,  /* 101010 */\n    // 0x54,  /* 010101 */\n    // 0xA8,  /* 101010 */\n\n    // /*\n    //  * code=178, hex=0xB2, ascii=\"!2\"\n    //  */\n    // 0xA8,  /* 101010 */\n    // 0xFC,  /* 111111 */\n    // 0x54,  /* 010101 */\n    // 0xFC,  /* 111111 */\n    // 0xA8,  /* 101010 */\n    // 0xFC,  /* 111111 */\n    // 0x54,  /* 010101 */\n    // 0xFC,  /* 111111 */\n\n    // /*\n    //  * code=179, hex=0xB3, ascii=\"!3\"\n    //  */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n\n    // /*\n    //  * code=180, hex=0xB4, ascii=\"!4\"\n    //  */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0xF0,  /* 111100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n\n    // /*\n    //  * code=181, hex=0xB5, ascii=\"!5\"\n    //  */\n    // 0x10,  /* 000100 */\n    // 0xF0,  /* 111100 */\n    // 0x10,  /* 000100 */\n    // 0xF0,  /* 111100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n\n    // /*\n    //  * code=182, hex=0xB6, ascii=\"!6\"\n    //  */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0xD0,  /* 110100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n\n    // /*\n    //  * code=183, hex=0xB7, ascii=\"!7\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0xF0,  /* 111100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n\n    // /*\n    //  * code=184, hex=0xB8, ascii=\"!8\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0xF0,  /* 111100 */\n    // 0x10,  /* 000100 */\n    // 0xF0,  /* 111100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n\n    // /*\n    //  * code=185, hex=0xB9, ascii=\"!9\"\n    //  */\n    // 0x50,  /* 010100 */\n    // 0xD0,  /* 110100 */\n    // 0x10,  /* 000100 */\n    // 0xD0,  /* 110100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n\n    // /*\n    //  * code=186, hex=0xBA, ascii=\"!:\"\n    //  */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n\n    // /*\n    //  * code=187, hex=0xBB, ascii=\"!;\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0xF0,  /* 111100 */\n    // 0x10,  /* 000100 */\n    // 0xD0,  /* 110100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n\n    // /*\n    //  * code=188, hex=0xBC, ascii=\"!<\"\n    //  */\n    // 0x50,  /* 010100 */\n    // 0xD0,  /* 110100 */\n    // 0x10,  /* 000100 */\n    // 0xF0,  /* 111100 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=189, hex=0xBD, ascii=\"!=\"\n    //  */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0xF0,  /* 111100 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=190, hex=0xBE, ascii=\"!>\"\n    //  */\n    // 0x10,  /* 000100 */\n    // 0xF0,  /* 111100 */\n    // 0x10,  /* 000100 */\n    // 0xF0,  /* 111100 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=191, hex=0xBF, ascii=\"!?\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0xF0,  /* 111100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n\n    // /*\n    //  * code=192, hex=0xC0, ascii=\"!@\"\n    //  */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x1C,  /* 000111 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=193, hex=0xC1, ascii=\"!A\"\n    //  */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0xFC,  /* 111111 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=194, hex=0xC2, ascii=\"!B\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0xFC,  /* 111111 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n\n    // /*\n    //  * code=195, hex=0xC3, ascii=\"!C\"\n    //  */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x1C,  /* 000111 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n\n    // /*\n    //  * code=196, hex=0xC4, ascii=\"!D\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0xFC,  /* 111111 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=197, hex=0xC5, ascii=\"!E\"\n    //  */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0xFC,  /* 111111 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n\n    // /*\n    //  * code=198, hex=0xC6, ascii=\"!F\"\n    //  */\n    // 0x10,  /* 000100 */\n    // 0x1C,  /* 000111 */\n    // 0x10,  /* 000100 */\n    // 0x1C,  /* 000111 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n\n    // /*\n    //  * code=199, hex=0xC7, ascii=\"!G\"\n    //  */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x5C,  /* 010111 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n\n    // /*\n    //  * code=200, hex=0xC8, ascii=\"!H\"\n    //  */\n    // 0x50,  /* 010100 */\n    // 0x5C,  /* 010111 */\n    // 0x40,  /* 010000 */\n    // 0x7C,  /* 011111 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=201, hex=0xC9, ascii=\"!I\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x7C,  /* 011111 */\n    // 0x40,  /* 010000 */\n    // 0x5C,  /* 010111 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n\n    // /*\n    //  * code=202, hex=0xCA, ascii=\"!J\"\n    //  */\n    // 0x50,  /* 010100 */\n    // 0xDC,  /* 110111 */\n    // 0x00,  /* 000000 */\n    // 0xFC,  /* 111111 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=203, hex=0xCB, ascii=\"!K\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0xFC,  /* 111111 */\n    // 0x00,  /* 000000 */\n    // 0xDC,  /* 110111 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n\n    // /*\n    //  * code=204, hex=0xCC, ascii=\"!L\"\n    //  */\n    // 0x50,  /* 010100 */\n    // 0x5C,  /* 010111 */\n    // 0x40,  /* 010000 */\n    // 0x5C,  /* 010111 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n\n    // /*\n    //  * code=205, hex=0xCD, ascii=\"!M\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0xFC,  /* 111111 */\n    // 0x00,  /* 000000 */\n    // 0xFC,  /* 111111 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=206, hex=0xCE, ascii=\"!N\"\n    //  */\n    // 0x50,  /* 010100 */\n    // 0xDC,  /* 110111 */\n    // 0x00,  /* 000000 */\n    // 0xDC,  /* 110111 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n\n    // /*\n    //  * code=207, hex=0xCF, ascii=\"!O\"\n    //  */\n    // 0x10,  /* 000100 */\n    // 0xFC,  /* 111111 */\n    // 0x00,  /* 000000 */\n    // 0xFC,  /* 111111 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=208, hex=0xD0, ascii=\"!P\"\n    //  */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0xFC,  /* 111111 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=209, hex=0xD1, ascii=\"!Q\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0xFC,  /* 111111 */\n    // 0x00,  /* 000000 */\n    // 0xFC,  /* 111111 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n\n    // /*\n    //  * code=210, hex=0xD2, ascii=\"!R\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0xFC,  /* 111111 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n\n    // /*\n    //  * code=211, hex=0xD3, ascii=\"!S\"\n    //  */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x7C,  /* 011111 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=212, hex=0xD4, ascii=\"!T\"\n    //  */\n    // 0x10,  /* 000100 */\n    // 0x1C,  /* 000111 */\n    // 0x10,  /* 000100 */\n    // 0x1C,  /* 000111 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=213, hex=0xD5, ascii=\"!U\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x1C,  /* 000111 */\n    // 0x10,  /* 000100 */\n    // 0x1C,  /* 000111 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n\n    // /*\n    //  * code=214, hex=0xD6, ascii=\"!V\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x7C,  /* 011111 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n\n    // /*\n    //  * code=215, hex=0xD7, ascii=\"!W\"\n    //  */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0xDC,  /* 110111 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n\n    // /*\n    //  * code=216, hex=0xD8, ascii=\"!X\"\n    //  */\n    // 0x10,  /* 000100 */\n    // 0xFC,  /* 111111 */\n    // 0x00,  /* 000000 */\n    // 0xFC,  /* 111111 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n\n    // /*\n    //  * code=217, hex=0xD9, ascii=\"!Y\"\n    //  */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0xF0,  /* 111100 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=218, hex=0xDA, ascii=\"!Z\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x1C,  /* 000111 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n\n    // /*\n    //  * code=219, hex=0xDB, ascii=\"![\"\n    //  */\n    // 0xFC,  /* 111111 */\n    // 0xFC,  /* 111111 */\n    // 0xFC,  /* 111111 */\n    // 0xFC,  /* 111111 */\n    // 0xFC,  /* 111111 */\n    // 0xFC,  /* 111111 */\n    // 0xFC,  /* 111111 */\n    // 0xFC,  /* 111111 */\n\n    // /*\n    //  * code=220, hex=0xDC, ascii=\"!\\\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0xFC,  /* 111111 */\n    // 0xFC,  /* 111111 */\n    // 0xFC,  /* 111111 */\n    // 0xFC,  /* 111111 */\n\n    // /*\n    //  * code=221, hex=0xDD, ascii=\"!]\"\n    //  */\n    // 0xE0,  /* 111000 */\n    // 0xE0,  /* 111000 */\n    // 0xE0,  /* 111000 */\n    // 0xE0,  /* 111000 */\n    // 0xE0,  /* 111000 */\n    // 0xE0,  /* 111000 */\n    // 0xE0,  /* 111000 */\n    // 0xE0,  /* 111000 */\n\n    // /*\n    //  * code=222, hex=0xDE, ascii=\"!^\"\n    //  */\n    // 0x1C,  /* 000111 */\n    // 0x1C,  /* 000111 */\n    // 0x1C,  /* 000111 */\n    // 0x1C,  /* 000111 */\n    // 0x1C,  /* 000111 */\n    // 0x1C,  /* 000111 */\n    // 0x1C,  /* 000111 */\n    // 0x1C,  /* 000111 */\n\n    // /*\n    //  * code=223, hex=0xDF, ascii=\"!_\"\n    //  */\n    // 0xFC,  /* 111111 */\n    // 0xFC,  /* 111111 */\n    // 0xFC,  /* 111111 */\n    // 0xFC,  /* 111111 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=224, hex=0xE0, ascii=\"!`\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x34,  /* 001101 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x34,  /* 001101 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=225, hex=0xE1, ascii=\"!a\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x70,  /* 011100 */\n    // 0x48,  /* 010010 */\n    // 0x70,  /* 011100 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x70,  /* 011100 */\n    // 0x40,  /* 010000 */\n\n    // /*\n    //  * code=226, hex=0xE2, ascii=\"!b\"\n    //  */\n    // 0x78,  /* 011110 */\n    // 0x48,  /* 010010 */\n    // 0x40,  /* 010000 */\n    // 0x40,  /* 010000 */\n    // 0x40,  /* 010000 */\n    // 0x40,  /* 010000 */\n    // 0x40,  /* 010000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=227, hex=0xE3, ascii=\"!c\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x7C,  /* 011111 */\n    // 0x28,  /* 001010 */\n    // 0x28,  /* 001010 */\n    // 0x28,  /* 001010 */\n    // 0x28,  /* 001010 */\n    // 0x28,  /* 001010 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=228, hex=0xE4, ascii=\"!d\"\n    //  */\n    // 0x78,  /* 011110 */\n    // 0x48,  /* 010010 */\n    // 0x20,  /* 001000 */\n    // 0x10,  /* 000100 */\n    // 0x20,  /* 001000 */\n    // 0x48,  /* 010010 */\n    // 0x78,  /* 011110 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=229, hex=0xE5, ascii=\"!e\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x3C,  /* 001111 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x30,  /* 001100 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=230, hex=0xE6, ascii=\"!f\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x70,  /* 011100 */\n    // 0x40,  /* 010000 */\n    // 0x40,  /* 010000 */\n\n    // /*\n    //  * code=231, hex=0xE7, ascii=\"!g\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x28,  /* 001010 */\n    // 0x50,  /* 010100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=232, hex=0xE8, ascii=\"!h\"\n    //  */\n    // 0x38,  /* 001110 */\n    // 0x10,  /* 000100 */\n    // 0x38,  /* 001110 */\n    // 0x44,  /* 010001 */\n    // 0x38,  /* 001110 */\n    // 0x10,  /* 000100 */\n    // 0x38,  /* 001110 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=233, hex=0xE9, ascii=\"!i\"\n    //  */\n    // 0x30,  /* 001100 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x78,  /* 011110 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x30,  /* 001100 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=234, hex=0xEA, ascii=\"!j\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x38,  /* 001110 */\n    // 0x44,  /* 010001 */\n    // 0x44,  /* 010001 */\n    // 0x28,  /* 001010 */\n    // 0x28,  /* 001010 */\n    // 0x6C,  /* 011011 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=235, hex=0xEB, ascii=\"!k\"\n    //  */\n    // 0x30,  /* 001100 */\n    // 0x40,  /* 010000 */\n    // 0x20,  /* 001000 */\n    // 0x10,  /* 000100 */\n    // 0x38,  /* 001110 */\n    // 0x48,  /* 010010 */\n    // 0x30,  /* 001100 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=236, hex=0xEC, ascii=\"!l\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x28,  /* 001010 */\n    // 0x54,  /* 010101 */\n    // 0x54,  /* 010101 */\n    // 0x28,  /* 001010 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=237, hex=0xED, ascii=\"!m\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x10,  /* 000100 */\n    // 0x38,  /* 001110 */\n    // 0x54,  /* 010101 */\n    // 0x54,  /* 010101 */\n    // 0x38,  /* 001110 */\n    // 0x10,  /* 000100 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=238, hex=0xEE, ascii=\"!n\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x38,  /* 001110 */\n    // 0x40,  /* 010000 */\n    // 0x78,  /* 011110 */\n    // 0x40,  /* 010000 */\n    // 0x38,  /* 001110 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=239, hex=0xEF, ascii=\"!o\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x30,  /* 001100 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=240, hex=0xF0, ascii=\"!p\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x78,  /* 011110 */\n    // 0x00,  /* 000000 */\n    // 0x78,  /* 011110 */\n    // 0x00,  /* 000000 */\n    // 0x78,  /* 011110 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=241, hex=0xF1, ascii=\"!q\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x10,  /* 000100 */\n    // 0x38,  /* 001110 */\n    // 0x10,  /* 000100 */\n    // 0x00,  /* 000000 */\n    // 0x38,  /* 001110 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=242, hex=0xF2, ascii=\"!r\"\n    //  */\n    // 0x40,  /* 010000 */\n    // 0x30,  /* 001100 */\n    // 0x08,  /* 000010 */\n    // 0x30,  /* 001100 */\n    // 0x40,  /* 010000 */\n    // 0x00,  /* 000000 */\n    // 0x78,  /* 011110 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=243, hex=0xF3, ascii=\"!s\"\n    //  */\n    // 0x08,  /* 000010 */\n    // 0x30,  /* 001100 */\n    // 0x40,  /* 010000 */\n    // 0x30,  /* 001100 */\n    // 0x08,  /* 000010 */\n    // 0x00,  /* 000000 */\n    // 0x78,  /* 011110 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=244, hex=0xF4, ascii=\"!t\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x08,  /* 000010 */\n    // 0x14,  /* 000101 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n\n    // /*\n    //  * code=245, hex=0xF5, ascii=\"!u\"\n    //  */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x50,  /* 010100 */\n    // 0x20,  /* 001000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=246, hex=0xF6, ascii=\"!v\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x10,  /* 000100 */\n    // 0x00,  /* 000000 */\n    // 0x7C,  /* 011111 */\n    // 0x00,  /* 000000 */\n    // 0x10,  /* 000100 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=247, hex=0xF7, ascii=\"!w\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x28,  /* 001010 */\n    // 0x50,  /* 010100 */\n    // 0x00,  /* 000000 */\n    // 0x28,  /* 001010 */\n    // 0x50,  /* 010100 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=248, hex=0xF8, ascii=\"!x\"\n    //  */\n    // 0x30,  /* 001100 */\n    // 0x48,  /* 010010 */\n    // 0x48,  /* 010010 */\n    // 0x30,  /* 001100 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=249, hex=0xF9, ascii=\"!y\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x30,  /* 001100 */\n    // 0x30,  /* 001100 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=250, hex=0xFA, ascii=\"!z\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x20,  /* 001000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=251, hex=0xFB, ascii=\"!{\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x1C,  /* 000111 */\n    // 0x10,  /* 000100 */\n    // 0x10,  /* 000100 */\n    // 0x50,  /* 010100 */\n    // 0x50,  /* 010100 */\n    // 0x20,  /* 001000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=252, hex=0xFC, ascii=\"!|\"\n    //  */\n    // 0x50,  /* 010100 */\n    // 0x28,  /* 001010 */\n    // 0x28,  /* 001010 */\n    // 0x28,  /* 001010 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=253, hex=0xFD, ascii=\"!}\"\n    //  */\n    // 0x60,  /* 011000 */\n    // 0x10,  /* 000100 */\n    // 0x20,  /* 001000 */\n    // 0x70,  /* 011100 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=254, hex=0xFE, ascii=\"!~\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x78,  /* 011110 */\n    // 0x78,  /* 011110 */\n    // 0x78,  /* 011110 */\n    // 0x78,  /* 011110 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n\n    // /*\n    //  * code=255, hex=0xFF, ascii=\"!^ź\"\n    //  */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00,  /* 000000 */\n    // 0x00   /* 000000 */\n};\n"
  },
  {
    "path": "wled00/src/font/console_font_7x9.h",
    "content": "// font courtesy of https://github.com/idispatch/raster-fonts\nstatic const unsigned char console_font_7x9[] PROGMEM = {\n\n// code points 0-31 and 127-255 are commented out to save memory, they contain extra characters (CP437),\n// which could be used with an UTF-8 to CP437 conversion\n\n    // /*\n    //  * code=0, hex=0x00, ascii=\"^@\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=1, hex=0x01, ascii=\"^A\"\n    //  */\n    // 0x38,  /* 0011100 */\n    // 0x44,  /* 0100010 */\n    // 0xAA,  /* 1010101 */\n    // 0xAA,  /* 1010101 */\n    // 0x82,  /* 1000001 */\n    // 0xAA,  /* 1010101 */\n    // 0x94,  /* 1001010 */\n    // 0x78,  /* 0111100 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=2, hex=0x02, ascii=\"^B\"\n    //  */\n    // 0x38,  /* 0011100 */\n    // 0x7C,  /* 0111110 */\n    // 0xD6,  /* 1101011 */\n    // 0xD6,  /* 1101011 */\n    // 0xFE,  /* 1111111 */\n    // 0xBA,  /* 1011101 */\n    // 0xC6,  /* 1100011 */\n    // 0x7C,  /* 0111110 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=3, hex=0x03, ascii=\"^C\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x6C,  /* 0110110 */\n    // 0xFE,  /* 1111111 */\n    // 0xFE,  /* 1111111 */\n    // 0xFE,  /* 1111111 */\n    // 0x7C,  /* 0111110 */\n    // 0x38,  /* 0011100 */\n    // 0x10,  /* 0001000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=4, hex=0x04, ascii=\"^D\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x10,  /* 0001000 */\n    // 0x38,  /* 0011100 */\n    // 0x7C,  /* 0111110 */\n    // 0xFE,  /* 1111111 */\n    // 0x7C,  /* 0111110 */\n    // 0x38,  /* 0011100 */\n    // 0x10,  /* 0001000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=5, hex=0x05, ascii=\"^E\"\n    //  */\n    // 0x38,  /* 0011100 */\n    // 0x38,  /* 0011100 */\n    // 0x10,  /* 0001000 */\n    // 0xD6,  /* 1101011 */\n    // 0xFE,  /* 1111111 */\n    // 0xD6,  /* 1101011 */\n    // 0x10,  /* 0001000 */\n    // 0x7C,  /* 0111110 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=6, hex=0x06, ascii=\"^F\"\n    //  */\n    // 0x10,  /* 0001000 */\n    // 0x38,  /* 0011100 */\n    // 0x7C,  /* 0111110 */\n    // 0xFE,  /* 1111111 */\n    // 0xFE,  /* 1111111 */\n    // 0x54,  /* 0101010 */\n    // 0x10,  /* 0001000 */\n    // 0x38,  /* 0011100 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=7, hex=0x07, ascii=\"^G\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x18,  /* 0001100 */\n    // 0x3C,  /* 0011110 */\n    // 0x3C,  /* 0011110 */\n    // 0x18,  /* 0001100 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=8, hex=0x08, ascii=\"^H\"\n    //  */\n    // 0xFE,  /* 1111111 */\n    // 0xFE,  /* 1111111 */\n    // 0xE6,  /* 1110011 */\n    // 0xC2,  /* 1100001 */\n    // 0xC2,  /* 1100001 */\n    // 0xE6,  /* 1110011 */\n    // 0xFE,  /* 1111111 */\n    // 0xFE,  /* 1111111 */\n    // 0xFE,  /* 1111111 */\n\n    // /*\n    //  * code=9, hex=0x09, ascii=\"^I\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x18,  /* 0001100 */\n    // 0x3C,  /* 0011110 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x3C,  /* 0011110 */\n    // 0x18,  /* 0001100 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=10, hex=0x0A, ascii=\"^J\"\n    //  */\n    // 0xFE,  /* 1111111 */\n    // 0xE6,  /* 1110011 */\n    // 0xC2,  /* 1100001 */\n    // 0x98,  /* 1001100 */\n    // 0x98,  /* 1001100 */\n    // 0xC2,  /* 1100001 */\n    // 0xE6,  /* 1110011 */\n    // 0xFE,  /* 1111111 */\n    // 0xFE,  /* 1111111 */\n\n    // /*\n    //  * code=11, hex=0x0B, ascii=\"^K\"\n    //  */\n    // 0x0E,  /* 0000111 */\n    // 0x06,  /* 0000011 */\n    // 0x0A,  /* 0000101 */\n    // 0x18,  /* 0001100 */\n    // 0x3C,  /* 0011110 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x3C,  /* 0011110 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=12, hex=0x0C, ascii=\"^L\"\n    //  */\n    // 0x3C,  /* 0011110 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x3C,  /* 0011110 */\n    // 0x18,  /* 0001100 */\n    // 0x3C,  /* 0011110 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=13, hex=0x0D, ascii=\"^M\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x38,  /* 0011100 */\n    // 0x2C,  /* 0010110 */\n    // 0x20,  /* 0010000 */\n    // 0x20,  /* 0010000 */\n    // 0x20,  /* 0010000 */\n    // 0x60,  /* 0110000 */\n    // 0x60,  /* 0110000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=14, hex=0x0E, ascii=\"^N\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x3C,  /* 0011110 */\n    // 0x24,  /* 0010010 */\n    // 0x3C,  /* 0011110 */\n    // 0x24,  /* 0010010 */\n    // 0x24,  /* 0010010 */\n    // 0x6C,  /* 0110110 */\n    // 0x6C,  /* 0110110 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=15, hex=0x0F, ascii=\"^O\"\n    //  */\n    // 0x92,  /* 1001001 */\n    // 0x54,  /* 0101010 */\n    // 0x38,  /* 0011100 */\n    // 0x28,  /* 0010100 */\n    // 0xEE,  /* 1110111 */\n    // 0x38,  /* 0011100 */\n    // 0x54,  /* 0101010 */\n    // 0x92,  /* 1001001 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=16, hex=0x10, ascii=\"^P\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x20,  /* 0010000 */\n    // 0x30,  /* 0011000 */\n    // 0x38,  /* 0011100 */\n    // 0x3C,  /* 0011110 */\n    // 0x38,  /* 0011100 */\n    // 0x30,  /* 0011000 */\n    // 0x20,  /* 0010000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=17, hex=0x11, ascii=\"^Q\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x04,  /* 0000010 */\n    // 0x0C,  /* 0000110 */\n    // 0x1C,  /* 0001110 */\n    // 0x3C,  /* 0011110 */\n    // 0x1C,  /* 0001110 */\n    // 0x0C,  /* 0000110 */\n    // 0x04,  /* 0000010 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=18, hex=0x12, ascii=\"^R\"\n    //  */\n    // 0x10,  /* 0001000 */\n    // 0x38,  /* 0011100 */\n    // 0x7C,  /* 0111110 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x7C,  /* 0111110 */\n    // 0x38,  /* 0011100 */\n    // 0x10,  /* 0001000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=19, hex=0x13, ascii=\"^S\"\n    //  */\n    // 0x6C,  /* 0110110 */\n    // 0x6C,  /* 0110110 */\n    // 0x6C,  /* 0110110 */\n    // 0x6C,  /* 0110110 */\n    // 0x6C,  /* 0110110 */\n    // 0x00,  /* 0000000 */\n    // 0x6C,  /* 0110110 */\n    // 0x6C,  /* 0110110 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=20, hex=0x14, ascii=\"^T\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x3C,  /* 0011110 */\n    // 0x54,  /* 0101010 */\n    // 0x54,  /* 0101010 */\n    // 0x3C,  /* 0011110 */\n    // 0x14,  /* 0001010 */\n    // 0x14,  /* 0001010 */\n    // 0x14,  /* 0001010 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=21, hex=0x15, ascii=\"^U\"\n    //  */\n    // 0x3C,  /* 0011110 */\n    // 0x66,  /* 0110011 */\n    // 0x60,  /* 0110000 */\n    // 0x3C,  /* 0011110 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x3C,  /* 0011110 */\n    // 0x06,  /* 0000011 */\n    // 0x66,  /* 0110011 */\n\n    // /*\n    //  * code=22, hex=0x16, ascii=\"^V\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x7C,  /* 0111110 */\n    // 0x7C,  /* 0111110 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=23, hex=0x17, ascii=\"^W\"\n    //  */\n    // 0x10,  /* 0001000 */\n    // 0x38,  /* 0011100 */\n    // 0x7C,  /* 0111110 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x7C,  /* 0111110 */\n    // 0x38,  /* 0011100 */\n    // 0x10,  /* 0001000 */\n    // 0x7C,  /* 0111110 */\n\n    // /*\n    //  * code=24, hex=0x18, ascii=\"^X\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x18,  /* 0001100 */\n    // 0x3C,  /* 0011110 */\n    // 0x5A,  /* 0101101 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=25, hex=0x19, ascii=\"^Y\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x5A,  /* 0101101 */\n    // 0x3C,  /* 0011110 */\n    // 0x18,  /* 0001100 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=26, hex=0x1A, ascii=\"^Z\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x18,  /* 0001100 */\n    // 0x0C,  /* 0000110 */\n    // 0x7E,  /* 0111111 */\n    // 0x0C,  /* 0000110 */\n    // 0x18,  /* 0001100 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=27, hex=0x1B, ascii=\"^[\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x18,  /* 0001100 */\n    // 0x30,  /* 0011000 */\n    // 0x7E,  /* 0111111 */\n    // 0x30,  /* 0011000 */\n    // 0x18,  /* 0001100 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=28, hex=0x1C, ascii=\"^\\\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x60,  /* 0110000 */\n    // 0x60,  /* 0110000 */\n    // 0x7C,  /* 0111110 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=29, hex=0x1D, ascii=\"^]\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x24,  /* 0010010 */\n    // 0x66,  /* 0110011 */\n    // 0xFE,  /* 1111111 */\n    // 0x66,  /* 0110011 */\n    // 0x24,  /* 0010010 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=30, hex=0x1E, ascii=\"^^\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x10,  /* 0001000 */\n    // 0x38,  /* 0011100 */\n    // 0x7C,  /* 0111110 */\n    // 0xFE,  /* 1111111 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=31, hex=0x1F, ascii=\"^_\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0xFE,  /* 1111111 */\n    // 0x7C,  /* 0111110 */\n    // 0x38,  /* 0011100 */\n    // 0x10,  /* 0001000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    /*\n     * code=32, hex=0x20, ascii=\" \"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=33, hex=0x21, ascii=\"!\"\n     */\n    0x00,  /* 0000000 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x00,  /* 0000000 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=34, hex=0x22, ascii=\"\"\"\n     */\n    0x00,  /* 0000000 */\n    0x6C,  /* 0110110 */\n    0x6C,  /* 0110110 */\n    0x44,  /* 0100010 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=35, hex=0x23, ascii=\"#\"\n     */\n    0x00,  /* 0000000 */\n    0x6C,  /* 0110110 */\n    0x6C,  /* 0110110 */\n    0xFE,  /* 1111111 */\n    0x6C,  /* 0110110 */\n    0xFE,  /* 1111111 */\n    0x6C,  /* 0110110 */\n    0x6C,  /* 0110110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=36, hex=0x24, ascii=\"$\"\n     */\n    0x08,  /* 0000100 */\n    0x18,  /* 0001100 */\n    0x3C,  /* 0011110 */\n    0x60,  /* 0110000 */\n    0x3C,  /* 0011110 */\n    0x06,  /* 0000011 */\n    0x7C,  /* 0111110 */\n    0x18,  /* 0001100 */\n    0x10,  /* 0001000 */\n\n    /*\n     * code=37, hex=0x25, ascii=\"%\"\n     */\n    0x70,  /* 0111000 */\n    0x52,  /* 0101001 */\n    0x76,  /* 0111011 */\n    0x0C,  /* 0000110 */\n    0x18,  /* 0001100 */\n    0x3E,  /* 0011111 */\n    0x6A,  /* 0110101 */\n    0x0E,  /* 0000111 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=38, hex=0x26, ascii=\"&\"\n     */\n    0x38,  /* 0011100 */\n    0x6C,  /* 0110110 */\n    0x6C,  /* 0110110 */\n    0x38,  /* 0011100 */\n    0x6E,  /* 0110111 */\n    0x6C,  /* 0110110 */\n    0x6C,  /* 0110110 */\n    0x3E,  /* 0011111 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=39, hex=0x27, ascii=\"'\"\n     */\n    0x00,  /* 0000000 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x30,  /* 0011000 */\n    0x30,  /* 0011000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=40, hex=0x28, ascii=\"(\"\n     */\n    0x00,  /* 0000000 */\n    0x0C,  /* 0000110 */\n    0x18,  /* 0001100 */\n    0x30,  /* 0011000 */\n    0x30,  /* 0011000 */\n    0x30,  /* 0011000 */\n    0x18,  /* 0001100 */\n    0x0C,  /* 0000110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=41, hex=0x29, ascii=\")\"\n     */\n    0x00,  /* 0000000 */\n    0x30,  /* 0011000 */\n    0x18,  /* 0001100 */\n    0x0C,  /* 0000110 */\n    0x0C,  /* 0000110 */\n    0x0C,  /* 0000110 */\n    0x18,  /* 0001100 */\n    0x30,  /* 0011000 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=42, hex=0x2A, ascii=\"*\"\n     */\n    0x00,  /* 0000000 */\n    0x44,  /* 0100010 */\n    0x6C,  /* 0110110 */\n    0x38,  /* 0011100 */\n    0xFE,  /* 1111111 */\n    0x38,  /* 0011100 */\n    0x6C,  /* 0110110 */\n    0x44,  /* 0100010 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=43, hex=0x2B, ascii=\"+\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x7E,  /* 0111111 */\n    0x7E,  /* 0111111 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=44, hex=0x2C, ascii=\",\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x30,  /* 0011000 */\n    0x30,  /* 0011000 */\n    0x60,  /* 0110000 */\n\n    /*\n     * code=45, hex=0x2D, ascii=\"-\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x7C,  /* 0111110 */\n    0x7C,  /* 0111110 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=46, hex=0x2E, ascii=\".\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x30,  /* 0011000 */\n    0x30,  /* 0011000 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=47, hex=0x2F, ascii=\"/\"\n     */\n    0x00,  /* 0000000 */\n    0x0C,  /* 0000110 */\n    0x0C,  /* 0000110 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x30,  /* 0011000 */\n    0x30,  /* 0011000 */\n    0x60,  /* 0110000 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=48, hex=0x30, ascii=\"0\"\n     */\n    0x00,  /* 0000000 */\n    0x3C,  /* 0011110 */\n    0x66,  /* 0110011 */\n    0x6E,  /* 0110111 */\n    0x76,  /* 0111011 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x3C,  /* 0011110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=49, hex=0x31, ascii=\"1\"\n     */\n    0x00,  /* 0000000 */\n    0x18,  /* 0001100 */\n    0x78,  /* 0111100 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x7E,  /* 0111111 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=50, hex=0x32, ascii=\"2\"\n     */\n    0x00,  /* 0000000 */\n    0x3C,  /* 0011110 */\n    0x66,  /* 0110011 */\n    0x46,  /* 0100011 */\n    0x1C,  /* 0001110 */\n    0x30,  /* 0011000 */\n    0x66,  /* 0110011 */\n    0x7E,  /* 0111111 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=51, hex=0x33, ascii=\"3\"\n     */\n    0x00,  /* 0000000 */\n    0x3C,  /* 0011110 */\n    0x66,  /* 0110011 */\n    0x06,  /* 0000011 */\n    0x1C,  /* 0001110 */\n    0x06,  /* 0000011 */\n    0x66,  /* 0110011 */\n    0x3C,  /* 0011110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=52, hex=0x34, ascii=\"4\"\n     */\n    0x00,  /* 0000000 */\n    0x0C,  /* 0000110 */\n    0x1C,  /* 0001110 */\n    0x3C,  /* 0011110 */\n    0x6C,  /* 0110110 */\n    0x7E,  /* 0111111 */\n    0x0C,  /* 0000110 */\n    0x1E,  /* 0001111 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=53, hex=0x35, ascii=\"5\"\n     */\n    0x00,  /* 0000000 */\n    0x7E,  /* 0111111 */\n    0x66,  /* 0110011 */\n    0x60,  /* 0110000 */\n    0x7C,  /* 0111110 */\n    0x06,  /* 0000011 */\n    0x66,  /* 0110011 */\n    0x3C,  /* 0011110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=54, hex=0x36, ascii=\"6\"\n     */\n    0x00,  /* 0000000 */\n    0x1C,  /* 0001110 */\n    0x30,  /* 0011000 */\n    0x60,  /* 0110000 */\n    0x7C,  /* 0111110 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x3C,  /* 0011110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=55, hex=0x37, ascii=\"7\"\n     */\n    0x00,  /* 0000000 */\n    0x7E,  /* 0111111 */\n    0x66,  /* 0110011 */\n    0x06,  /* 0000011 */\n    0x0C,  /* 0000110 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=56, hex=0x38, ascii=\"8\"\n     */\n    0x00,  /* 0000000 */\n    0x3C,  /* 0011110 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x3C,  /* 0011110 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x3C,  /* 0011110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=57, hex=0x39, ascii=\"9\"\n     */\n    0x00,  /* 0000000 */\n    0x3C,  /* 0011110 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x3E,  /* 0011111 */\n    0x06,  /* 0000011 */\n    0x0C,  /* 0000110 */\n    0x38,  /* 0011100 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=58, hex=0x3A, ascii=\":\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x30,  /* 0011000 */\n    0x30,  /* 0011000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x30,  /* 0011000 */\n    0x30,  /* 0011000 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=59, hex=0x3B, ascii=\";\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x30,  /* 0011000 */\n    0x30,  /* 0011000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x30,  /* 0011000 */\n    0x30,  /* 0011000 */\n    0x60,  /* 0110000 */\n\n    /*\n     * code=60, hex=0x3C, ascii=\"<\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x18,  /* 0001100 */\n    0x30,  /* 0011000 */\n    0x60,  /* 0110000 */\n    0x30,  /* 0011000 */\n    0x18,  /* 0001100 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=61, hex=0x3D, ascii=\"=\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x7C,  /* 0111110 */\n    0x00,  /* 0000000 */\n    0x7C,  /* 0111110 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=62, hex=0x3E, ascii=\">\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x30,  /* 0011000 */\n    0x18,  /* 0001100 */\n    0x0C,  /* 0000110 */\n    0x18,  /* 0001100 */\n    0x30,  /* 0011000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=63, hex=0x3F, ascii=\"?\"\n     */\n    0x00,  /* 0000000 */\n    0x3C,  /* 0011110 */\n    0x66,  /* 0110011 */\n    0x06,  /* 0000011 */\n    0x0C,  /* 0000110 */\n    0x18,  /* 0001100 */\n    0x00,  /* 0000000 */\n    0x18,  /* 0001100 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=64, hex=0x40, ascii=\"@\"\n     */\n    0x00,  /* 0000000 */\n    0x3C,  /* 0011110 */\n    0x62,  /* 0110001 */\n    0x6E,  /* 0110111 */\n    0x6A,  /* 0110101 */\n    0x6C,  /* 0110110 */\n    0x62,  /* 0110001 */\n    0x3C,  /* 0011110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=65, hex=0x41, ascii=\"A\"\n     */\n    0x00,  /* 0000000 */\n    0x10,  /* 0001000 */\n    0x38,  /* 0011100 */\n    0x28,  /* 0010100 */\n    0x6C,  /* 0110110 */\n    0x7C,  /* 0111110 */\n    0xC6,  /* 1100011 */\n    0xC6,  /* 1100011 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=66, hex=0x42, ascii=\"B\"\n     */\n    0x00,  /* 0000000 */\n    0x7C,  /* 0111110 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x7C,  /* 0111110 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x7C,  /* 0111110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=67, hex=0x43, ascii=\"C\"\n     */\n    0x00,  /* 0000000 */\n    0x3C,  /* 0011110 */\n    0x66,  /* 0110011 */\n    0x60,  /* 0110000 */\n    0x60,  /* 0110000 */\n    0x60,  /* 0110000 */\n    0x66,  /* 0110011 */\n    0x3C,  /* 0011110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=68, hex=0x44, ascii=\"D\"\n     */\n    0x00,  /* 0000000 */\n    0x7C,  /* 0111110 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x7C,  /* 0111110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=69, hex=0x45, ascii=\"E\"\n     */\n    0x00,  /* 0000000 */\n    0x7E,  /* 0111111 */\n    0x66,  /* 0110011 */\n    0x60,  /* 0110000 */\n    0x78,  /* 0111100 */\n    0x60,  /* 0110000 */\n    0x66,  /* 0110011 */\n    0x7E,  /* 0111111 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=70, hex=0x46, ascii=\"F\"\n     */\n    0x00,  /* 0000000 */\n    0x7E,  /* 0111111 */\n    0x66,  /* 0110011 */\n    0x60,  /* 0110000 */\n    0x7C,  /* 0111110 */\n    0x60,  /* 0110000 */\n    0x60,  /* 0110000 */\n    0x60,  /* 0110000 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=71, hex=0x47, ascii=\"G\"\n     */\n    0x00,  /* 0000000 */\n    0x3C,  /* 0011110 */\n    0x66,  /* 0110011 */\n    0x60,  /* 0110000 */\n    0x6E,  /* 0110111 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x3E,  /* 0011111 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=72, hex=0x48, ascii=\"H\"\n     */\n    0x00,  /* 0000000 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x7E,  /* 0111111 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=73, hex=0x49, ascii=\"I\"\n     */\n    0x00,  /* 0000000 */\n    0x3C,  /* 0011110 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x3C,  /* 0011110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=74, hex=0x4A, ascii=\"J\"\n     */\n    0x00,  /* 0000000 */\n    0x1E,  /* 0001111 */\n    0x0C,  /* 0000110 */\n    0x0C,  /* 0000110 */\n    0x0C,  /* 0000110 */\n    0x6C,  /* 0110110 */\n    0x6C,  /* 0110110 */\n    0x38,  /* 0011100 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=75, hex=0x4B, ascii=\"K\"\n     */\n    0x00,  /* 0000000 */\n    0x66,  /* 0110011 */\n    0x6C,  /* 0110110 */\n    0x6C,  /* 0110110 */\n    0x78,  /* 0111100 */\n    0x6C,  /* 0110110 */\n    0x6C,  /* 0110110 */\n    0x66,  /* 0110011 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=76, hex=0x4C, ascii=\"L\"\n     */\n    0x00,  /* 0000000 */\n    0x60,  /* 0110000 */\n    0x60,  /* 0110000 */\n    0x60,  /* 0110000 */\n    0x60,  /* 0110000 */\n    0x60,  /* 0110000 */\n    0x66,  /* 0110011 */\n    0x7E,  /* 0111111 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=77, hex=0x4D, ascii=\"M\"\n     */\n    0x00,  /* 0000000 */\n    0xC6,  /* 1100011 */\n    0xC6,  /* 1100011 */\n    0xEE,  /* 1110111 */\n    0xFE,  /* 1111111 */\n    0xD6,  /* 1101011 */\n    0xD6,  /* 1101011 */\n    0xD6,  /* 1101011 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=78, hex=0x4E, ascii=\"N\"\n     */\n    0x00,  /* 0000000 */\n    0x66,  /* 0110011 */\n    0x76,  /* 0111011 */\n    0x76,  /* 0111011 */\n    0x7E,  /* 0111111 */\n    0x6E,  /* 0110111 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=79, hex=0x4F, ascii=\"O\"\n     */\n    0x00,  /* 0000000 */\n    0x3C,  /* 0011110 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x3C,  /* 0011110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=80, hex=0x50, ascii=\"P\"\n     */\n    0x00,  /* 0000000 */\n    0x7C,  /* 0111110 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x7C,  /* 0111110 */\n    0x60,  /* 0110000 */\n    0x60,  /* 0110000 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=81, hex=0x51, ascii=\"Q\"\n     */\n    0x00,  /* 0000000 */\n    0x3C,  /* 0011110 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x76,  /* 0111011 */\n    0x6E,  /* 0110111 */\n    0x3C,  /* 0011110 */\n    0x06,  /* 0000011 */\n\n    /*\n     * code=82, hex=0x52, ascii=\"R\"\n     */\n    0x00,  /* 0000000 */\n    0x7C,  /* 0111110 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x6C,  /* 0110110 */\n    0x78,  /* 0111100 */\n    0x6C,  /* 0110110 */\n    0x66,  /* 0110011 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=83, hex=0x53, ascii=\"S\"\n     */\n    0x00,  /* 0000000 */\n    0x3C,  /* 0011110 */\n    0x66,  /* 0110011 */\n    0x60,  /* 0110000 */\n    0x3C,  /* 0011110 */\n    0x06,  /* 0000011 */\n    0x66,  /* 0110011 */\n    0x3C,  /* 0011110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=84, hex=0x54, ascii=\"T\"\n     */\n    0x00,  /* 0000000 */\n    0x7E,  /* 0111111 */\n    0x5A,  /* 0101101 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x3C,  /* 0011110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=85, hex=0x55, ascii=\"U\"\n     */\n    0x00,  /* 0000000 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x3C,  /* 0011110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=86, hex=0x56, ascii=\"V\"\n     */\n    0x00,  /* 0000000 */\n    0xC6,  /* 1100011 */\n    0xC6,  /* 1100011 */\n    0x6C,  /* 0110110 */\n    0x6C,  /* 0110110 */\n    0x38,  /* 0011100 */\n    0x38,  /* 0011100 */\n    0x10,  /* 0001000 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=87, hex=0x57, ascii=\"W\"\n     */\n    0x00,  /* 0000000 */\n    0xC6,  /* 1100011 */\n    0xD6,  /* 1101011 */\n    0xD6,  /* 1101011 */\n    0xD6,  /* 1101011 */\n    0x7C,  /* 0111110 */\n    0x6C,  /* 0110110 */\n    0x44,  /* 0100010 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=88, hex=0x58, ascii=\"X\"\n     */\n    0x00,  /* 0000000 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x3C,  /* 0011110 */\n    0x18,  /* 0001100 */\n    0x3C,  /* 0011110 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=89, hex=0x59, ascii=\"Y\"\n     */\n    0x00,  /* 0000000 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x3C,  /* 0011110 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x3C,  /* 0011110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=90, hex=0x5A, ascii=\"Z\"\n     */\n    0x00,  /* 0000000 */\n    0x7E,  /* 0111111 */\n    0x66,  /* 0110011 */\n    0x0C,  /* 0000110 */\n    0x18,  /* 0001100 */\n    0x30,  /* 0011000 */\n    0x66,  /* 0110011 */\n    0x7E,  /* 0111111 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=91, hex=0x5B, ascii=\"[\"\n     */\n    0x00,  /* 0000000 */\n    0x3C,  /* 0011110 */\n    0x30,  /* 0011000 */\n    0x30,  /* 0011000 */\n    0x30,  /* 0011000 */\n    0x30,  /* 0011000 */\n    0x30,  /* 0011000 */\n    0x3C,  /* 0011110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=92, hex=0x5C, ascii=\"\\\"\n     */\n    0x00,  /* 0000000 */\n    0x60,  /* 0110000 */\n    0x60,  /* 0110000 */\n    0x30,  /* 0011000 */\n    0x30,  /* 0011000 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x0C,  /* 0000110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=93, hex=0x5D, ascii=\"]\"\n     */\n    0x00,  /* 0000000 */\n    0x3C,  /* 0011110 */\n    0x0C,  /* 0000110 */\n    0x0C,  /* 0000110 */\n    0x0C,  /* 0000110 */\n    0x0C,  /* 0000110 */\n    0x0C,  /* 0000110 */\n    0x3C,  /* 0011110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=94, hex=0x5E, ascii=\"^\"\n     */\n    0x00,  /* 0000000 */\n    0x10,  /* 0001000 */\n    0x38,  /* 0011100 */\n    0x6C,  /* 0110110 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=95, hex=0x5F, ascii=\"_\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x7C,  /* 0111110 */\n    0x7C,  /* 0111110 */\n\n    /*\n     * code=96, hex=0x60, ascii=\"`\"\n     */\n    0x00,  /* 0000000 */\n    0x30,  /* 0011000 */\n    0x30,  /* 0011000 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=97, hex=0x61, ascii=\"a\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x3C,  /* 0011110 */\n    0x06,  /* 0000011 */\n    0x3E,  /* 0011111 */\n    0x66,  /* 0110011 */\n    0x3A,  /* 0011101 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=98, hex=0x62, ascii=\"b\"\n     */\n    0x00,  /* 0000000 */\n    0x60,  /* 0110000 */\n    0x60,  /* 0110000 */\n    0x7C,  /* 0111110 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x5C,  /* 0101110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=99, hex=0x63, ascii=\"c\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x3C,  /* 0011110 */\n    0x66,  /* 0110011 */\n    0x60,  /* 0110000 */\n    0x66,  /* 0110011 */\n    0x3C,  /* 0011110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=100, hex=0x64, ascii=\"d\"\n     */\n    0x00,  /* 0000000 */\n    0x06,  /* 0000011 */\n    0x06,  /* 0000011 */\n    0x3E,  /* 0011111 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x3A,  /* 0011101 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=101, hex=0x65, ascii=\"e\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x3C,  /* 0011110 */\n    0x66,  /* 0110011 */\n    0x7E,  /* 0111111 */\n    0x60,  /* 0110000 */\n    0x3C,  /* 0011110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=102, hex=0x66, ascii=\"f\"\n     */\n    0x00,  /* 0000000 */\n    0x1C,  /* 0001110 */\n    0x36,  /* 0011011 */\n    0x30,  /* 0011000 */\n    0x7C,  /* 0111110 */\n    0x30,  /* 0011000 */\n    0x30,  /* 0011000 */\n    0x78,  /* 0111100 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=103, hex=0x67, ascii=\"g\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x3A,  /* 0011101 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x3E,  /* 0011111 */\n    0x06,  /* 0000011 */\n    0x3C,  /* 0011110 */\n\n    /*\n     * code=104, hex=0x68, ascii=\"h\"\n     */\n    0x00,  /* 0000000 */\n    0x60,  /* 0110000 */\n    0x60,  /* 0110000 */\n    0x6C,  /* 0110110 */\n    0x76,  /* 0111011 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=105, hex=0x69, ascii=\"i\"\n     */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x00,  /* 0000000 */\n    0x18,  /* 0001100 */\n    0x38,  /* 0011100 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x3C,  /* 0011110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=106, hex=0x6A, ascii=\"j\"\n     */\n    0x0C,  /* 0000110 */\n    0x0C,  /* 0000110 */\n    0x00,  /* 0000000 */\n    0x0C,  /* 0000110 */\n    0x3C,  /* 0011110 */\n    0x0C,  /* 0000110 */\n    0x4C,  /* 0100110 */\n    0x6C,  /* 0110110 */\n    0x38,  /* 0011100 */\n\n    /*\n     * code=107, hex=0x6B, ascii=\"k\"\n     */\n    0x00,  /* 0000000 */\n    0x60,  /* 0110000 */\n    0x60,  /* 0110000 */\n    0x66,  /* 0110011 */\n    0x6C,  /* 0110110 */\n    0x78,  /* 0111100 */\n    0x6C,  /* 0110110 */\n    0x66,  /* 0110011 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=108, hex=0x6C, ascii=\"l\"\n     */\n    0x00,  /* 0000000 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x3C,  /* 0011110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=109, hex=0x6D, ascii=\"m\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x6C,  /* 0110110 */\n    0xFE,  /* 1111111 */\n    0xD6,  /* 1101011 */\n    0xD6,  /* 1101011 */\n    0xD6,  /* 1101011 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=110, hex=0x6E, ascii=\"n\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x6C,  /* 0110110 */\n    0x76,  /* 0111011 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=111, hex=0x6F, ascii=\"o\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x3C,  /* 0011110 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x3C,  /* 0011110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=112, hex=0x70, ascii=\"p\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x7C,  /* 0111110 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x76,  /* 0111011 */\n    0x6C,  /* 0110110 */\n    0x60,  /* 0110000 */\n\n    /*\n     * code=113, hex=0x71, ascii=\"q\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x3A,  /* 0011101 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x6E,  /* 0110111 */\n    0x36,  /* 0011011 */\n    0x06,  /* 0000011 */\n\n    /*\n     * code=114, hex=0x72, ascii=\"r\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x26,  /* 0010011 */\n    0x7E,  /* 0111111 */\n    0x30,  /* 0011000 */\n    0x30,  /* 0011000 */\n    0x78,  /* 0111100 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=115, hex=0x73, ascii=\"s\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x3C,  /* 0011110 */\n    0x60,  /* 0110000 */\n    0x3C,  /* 0011110 */\n    0x06,  /* 0000011 */\n    0x3C,  /* 0011110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=116, hex=0x74, ascii=\"t\"\n     */\n    0x00,  /* 0000000 */\n    0x10,  /* 0001000 */\n    0x30,  /* 0011000 */\n    0xFC,  /* 1111110 */\n    0x30,  /* 0011000 */\n    0x30,  /* 0011000 */\n    0x36,  /* 0011011 */\n    0x1C,  /* 0001110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=117, hex=0x75, ascii=\"u\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x6E,  /* 0110111 */\n    0x36,  /* 0011011 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=118, hex=0x76, ascii=\"v\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0xC6,  /* 1100011 */\n    0xC6,  /* 1100011 */\n    0x6C,  /* 0110110 */\n    0x38,  /* 0011100 */\n    0x10,  /* 0001000 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=119, hex=0x77, ascii=\"w\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0xD6,  /* 1101011 */\n    0xD6,  /* 1101011 */\n    0x7C,  /* 0111110 */\n    0x6C,  /* 0110110 */\n    0x44,  /* 0100010 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=120, hex=0x78, ascii=\"x\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0xC6,  /* 1100011 */\n    0x6C,  /* 0110110 */\n    0x38,  /* 0011100 */\n    0x6C,  /* 0110110 */\n    0xC6,  /* 1100011 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=121, hex=0x79, ascii=\"y\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x66,  /* 0110011 */\n    0x66,  /* 0110011 */\n    0x36,  /* 0011011 */\n    0x1C,  /* 0001110 */\n    0x6C,  /* 0110110 */\n    0x38,  /* 0011100 */\n\n    /*\n     * code=122, hex=0x7A, ascii=\"z\"\n     */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x7E,  /* 0111111 */\n    0x06,  /* 0000011 */\n    0x18,  /* 0001100 */\n    0x30,  /* 0011000 */\n    0x7E,  /* 0111111 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=123, hex=0x7B, ascii=\"{\"\n     */\n    0x00,  /* 0000000 */\n    0x1C,  /* 0001110 */\n    0x30,  /* 0011000 */\n    0x30,  /* 0011000 */\n    0x60,  /* 0110000 */\n    0x30,  /* 0011000 */\n    0x30,  /* 0011000 */\n    0x1C,  /* 0001110 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=124, hex=0x7C, ascii=\"|\"\n     */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x00,  /* 0000000 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=125, hex=0x7D, ascii=\"}\"\n     */\n    0x00,  /* 0000000 */\n    0x70,  /* 0111000 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x0C,  /* 0000110 */\n    0x18,  /* 0001100 */\n    0x18,  /* 0001100 */\n    0x70,  /* 0111000 */\n    0x00,  /* 0000000 */\n\n    /*\n     * code=126, hex=0x7E, ascii=\"~\"\n     */\n    0x00,  /* 0000000 */\n    0x10,  /* 0001000 */\n    0x3A,  /* 0011101 */\n    0x6E,  /* 0110111 */\n    0x04,  /* 0000010 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n    0x00,  /* 0000000 */\n\n    // /*\n    //  * code=127, hex=0x7F, ascii=\"^?\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x08,  /* 0000100 */\n    // 0x1C,  /* 0001110 */\n    // 0x36,  /* 0011011 */\n    // 0x62,  /* 0110001 */\n    // 0x7E,  /* 0111111 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=128, hex=0x80, ascii=\"!^@\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x3C,  /* 0011110 */\n    // 0x66,  /* 0110011 */\n    // 0x60,  /* 0110000 */\n    // 0x60,  /* 0110000 */\n    // 0x60,  /* 0110000 */\n    // 0x66,  /* 0110011 */\n    // 0x3C,  /* 0011110 */\n    // 0x78,  /* 0111100 */\n\n    // /*\n    //  * code=129, hex=0x81, ascii=\"!^A\"\n    //  */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x00,  /* 0000000 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x3A,  /* 0011101 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=130, hex=0x82, ascii=\"!^B\"\n    //  */\n    // 0x0C,  /* 0000110 */\n    // 0x18,  /* 0001100 */\n    // 0x00,  /* 0000000 */\n    // 0x3E,  /* 0011111 */\n    // 0x62,  /* 0110001 */\n    // 0x7E,  /* 0111111 */\n    // 0x60,  /* 0110000 */\n    // 0x3E,  /* 0011111 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=131, hex=0x83, ascii=\"!^C\"\n    //  */\n    // 0x1C,  /* 0001110 */\n    // 0x36,  /* 0011011 */\n    // 0x00,  /* 0000000 */\n    // 0x3C,  /* 0011110 */\n    // 0x06,  /* 0000011 */\n    // 0x3E,  /* 0011111 */\n    // 0x66,  /* 0110011 */\n    // 0x3E,  /* 0011111 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=132, hex=0x84, ascii=\"!^D\"\n    //  */\n    // 0x36,  /* 0011011 */\n    // 0x36,  /* 0011011 */\n    // 0x00,  /* 0000000 */\n    // 0x3C,  /* 0011110 */\n    // 0x06,  /* 0000011 */\n    // 0x3E,  /* 0011111 */\n    // 0x66,  /* 0110011 */\n    // 0x3E,  /* 0011111 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=133, hex=0x85, ascii=\"!^E\"\n    //  */\n    // 0x18,  /* 0001100 */\n    // 0x0C,  /* 0000110 */\n    // 0x00,  /* 0000000 */\n    // 0x3C,  /* 0011110 */\n    // 0x06,  /* 0000011 */\n    // 0x3E,  /* 0011111 */\n    // 0x66,  /* 0110011 */\n    // 0x3A,  /* 0011101 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=134, hex=0x86, ascii=\"!^F\"\n    //  */\n    // 0x1C,  /* 0001110 */\n    // 0x14,  /* 0001010 */\n    // 0x00,  /* 0000000 */\n    // 0x3C,  /* 0011110 */\n    // 0x06,  /* 0000011 */\n    // 0x3E,  /* 0011111 */\n    // 0x66,  /* 0110011 */\n    // 0x3A,  /* 0011101 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=135, hex=0x87, ascii=\"!^G\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x1C,  /* 0001110 */\n    // 0x36,  /* 0011011 */\n    // 0x60,  /* 0110000 */\n    // 0x36,  /* 0011011 */\n    // 0x1C,  /* 0001110 */\n    // 0x78,  /* 0111100 */\n\n    // /*\n    //  * code=136, hex=0x88, ascii=\"!^H\"\n    //  */\n    // 0x08,  /* 0000100 */\n    // 0x1C,  /* 0001110 */\n    // 0x00,  /* 0000000 */\n    // 0x3C,  /* 0011110 */\n    // 0x66,  /* 0110011 */\n    // 0x7E,  /* 0111111 */\n    // 0x60,  /* 0110000 */\n    // 0x3E,  /* 0011111 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=137, hex=0x89, ascii=\"!^I\"\n    //  */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x00,  /* 0000000 */\n    // 0x3C,  /* 0011110 */\n    // 0x66,  /* 0110011 */\n    // 0x7E,  /* 0111111 */\n    // 0x60,  /* 0110000 */\n    // 0x3E,  /* 0011111 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=138, hex=0x8A, ascii=\"!^J\"\n    //  */\n    // 0x18,  /* 0001100 */\n    // 0x0C,  /* 0000110 */\n    // 0x00,  /* 0000000 */\n    // 0x3C,  /* 0011110 */\n    // 0x66,  /* 0110011 */\n    // 0x7E,  /* 0111111 */\n    // 0x60,  /* 0110000 */\n    // 0x3E,  /* 0011111 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=139, hex=0x8B, ascii=\"!^K\"\n    //  */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x00,  /* 0000000 */\n    // 0x38,  /* 0011100 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x3C,  /* 0011110 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=140, hex=0x8C, ascii=\"!^L\"\n    //  */\n    // 0x10,  /* 0001000 */\n    // 0x38,  /* 0011100 */\n    // 0x00,  /* 0000000 */\n    // 0x38,  /* 0011100 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x3C,  /* 0011110 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=141, hex=0x8D, ascii=\"!^M\"\n    //  */\n    // 0x30,  /* 0011000 */\n    // 0x18,  /* 0001100 */\n    // 0x00,  /* 0000000 */\n    // 0x38,  /* 0011100 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x3C,  /* 0011110 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=142, hex=0x8E, ascii=\"!^N\"\n    //  */\n    // 0xC6,  /* 1100011 */\n    // 0x10,  /* 0001000 */\n    // 0x38,  /* 0011100 */\n    // 0x28,  /* 0010100 */\n    // 0x6C,  /* 0110110 */\n    // 0x7C,  /* 0111110 */\n    // 0xC6,  /* 1100011 */\n    // 0xC6,  /* 1100011 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=143, hex=0x8F, ascii=\"!^O\"\n    //  */\n    // 0x38,  /* 0011100 */\n    // 0x28,  /* 0010100 */\n    // 0x38,  /* 0011100 */\n    // 0x28,  /* 0010100 */\n    // 0x6C,  /* 0110110 */\n    // 0x7C,  /* 0111110 */\n    // 0xC6,  /* 1100011 */\n    // 0xC6,  /* 1100011 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=144, hex=0x90, ascii=\"!^P\"\n    //  */\n    // 0x1C,  /* 0001110 */\n    // 0x30,  /* 0011000 */\n    // 0x7E,  /* 0111111 */\n    // 0x60,  /* 0110000 */\n    // 0x7C,  /* 0111110 */\n    // 0x60,  /* 0110000 */\n    // 0x60,  /* 0110000 */\n    // 0x7E,  /* 0111111 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=145, hex=0x91, ascii=\"!^Q\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x7C,  /* 0111110 */\n    // 0x1A,  /* 0001101 */\n    // 0x7E,  /* 0111111 */\n    // 0xD8,  /* 1101100 */\n    // 0x7E,  /* 0111111 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=146, hex=0x92, ascii=\"!^R\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x1E,  /* 0001111 */\n    // 0x38,  /* 0011100 */\n    // 0x58,  /* 0101100 */\n    // 0x5E,  /* 0101111 */\n    // 0xF8,  /* 1111100 */\n    // 0xD8,  /* 1101100 */\n    // 0xDE,  /* 1101111 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=147, hex=0x93, ascii=\"!^S\"\n    //  */\n    // 0x10,  /* 0001000 */\n    // 0x38,  /* 0011100 */\n    // 0x00,  /* 0000000 */\n    // 0x3C,  /* 0011110 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x3C,  /* 0011110 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=148, hex=0x94, ascii=\"!^T\"\n    //  */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x00,  /* 0000000 */\n    // 0x3C,  /* 0011110 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x3C,  /* 0011110 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=149, hex=0x95, ascii=\"!^U\"\n    //  */\n    // 0x18,  /* 0001100 */\n    // 0x0C,  /* 0000110 */\n    // 0x00,  /* 0000000 */\n    // 0x3C,  /* 0011110 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x3C,  /* 0011110 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=150, hex=0x96, ascii=\"!^V\"\n    //  */\n    // 0x08,  /* 0000100 */\n    // 0x1C,  /* 0001110 */\n    // 0x00,  /* 0000000 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x3A,  /* 0011101 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=151, hex=0x97, ascii=\"!^W\"\n    //  */\n    // 0x18,  /* 0001100 */\n    // 0x0C,  /* 0000110 */\n    // 0x00,  /* 0000000 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x3A,  /* 0011101 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=152, hex=0x98, ascii=\"!^X\"\n    //  */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x00,  /* 0000000 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x36,  /* 0011011 */\n    // 0x1C,  /* 0001110 */\n    // 0x6C,  /* 0110110 */\n    // 0x38,  /* 0011100 */\n\n    // /*\n    //  * code=153, hex=0x99, ascii=\"!^Y\"\n    //  */\n    // 0x66,  /* 0110011 */\n    // 0x00,  /* 0000000 */\n    // 0x3C,  /* 0011110 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x3C,  /* 0011110 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=154, hex=0x9A, ascii=\"!^Z\"\n    //  */\n    // 0x66,  /* 0110011 */\n    // 0x00,  /* 0000000 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x3C,  /* 0011110 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=155, hex=0x9B, ascii=\"!^[\"\n    //  */\n    // 0x08,  /* 0000100 */\n    // 0x08,  /* 0000100 */\n    // 0x3C,  /* 0011110 */\n    // 0x60,  /* 0110000 */\n    // 0x60,  /* 0110000 */\n    // 0x3C,  /* 0011110 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=156, hex=0x9C, ascii=\"!^\\\"\n    //  */\n    // 0x1C,  /* 0001110 */\n    // 0x36,  /* 0011011 */\n    // 0x30,  /* 0011000 */\n    // 0x30,  /* 0011000 */\n    // 0x7C,  /* 0111110 */\n    // 0x30,  /* 0011000 */\n    // 0x3C,  /* 0011110 */\n    // 0x66,  /* 0110011 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=157, hex=0x9D, ascii=\"!^]\"\n    //  */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x3C,  /* 0011110 */\n    // 0x18,  /* 0001100 */\n    // 0x7E,  /* 0111111 */\n    // 0x18,  /* 0001100 */\n    // 0x7E,  /* 0111111 */\n    // 0x18,  /* 0001100 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=158, hex=0x9E, ascii=\"!^^\"\n    //  */\n    // 0xE0,  /* 1110000 */\n    // 0xD0,  /* 1101000 */\n    // 0xD0,  /* 1101000 */\n    // 0xF4,  /* 1111010 */\n    // 0xCC,  /* 1100110 */\n    // 0xDE,  /* 1101111 */\n    // 0xCC,  /* 1100110 */\n    // 0x06,  /* 0000011 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=159, hex=0x9F, ascii=\"!^_\"\n    //  */\n    // 0x0E,  /* 0000111 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x7E,  /* 0111111 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x70,  /* 0111000 */\n\n    // /*\n    //  * code=160, hex=0xA0, ascii=\"! \"\n    //  */\n    // 0x06,  /* 0000011 */\n    // 0x0C,  /* 0000110 */\n    // 0x00,  /* 0000000 */\n    // 0x3C,  /* 0011110 */\n    // 0x06,  /* 0000011 */\n    // 0x3E,  /* 0011111 */\n    // 0x66,  /* 0110011 */\n    // 0x3A,  /* 0011101 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=161, hex=0xA1, ascii=\"!!\"\n    //  */\n    // 0x0C,  /* 0000110 */\n    // 0x18,  /* 0001100 */\n    // 0x00,  /* 0000000 */\n    // 0x38,  /* 0011100 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x3C,  /* 0011110 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=162, hex=0xA2, ascii=\"!\"\"\n    //  */\n    // 0x0C,  /* 0000110 */\n    // 0x18,  /* 0001100 */\n    // 0x00,  /* 0000000 */\n    // 0x3C,  /* 0011110 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x3C,  /* 0011110 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=163, hex=0xA3, ascii=\"!#\"\n    //  */\n    // 0x0C,  /* 0000110 */\n    // 0x18,  /* 0001100 */\n    // 0x00,  /* 0000000 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x3A,  /* 0011101 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=164, hex=0xA4, ascii=\"!$\"\n    //  */\n    // 0x76,  /* 0111011 */\n    // 0xDC,  /* 1101110 */\n    // 0x00,  /* 0000000 */\n    // 0x6C,  /* 0110110 */\n    // 0x76,  /* 0111011 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=165, hex=0xA5, ascii=\"!%\"\n    //  */\n    // 0x76,  /* 0111011 */\n    // 0xDC,  /* 1101110 */\n    // 0x00,  /* 0000000 */\n    // 0x66,  /* 0110011 */\n    // 0x76,  /* 0111011 */\n    // 0x7E,  /* 0111111 */\n    // 0x6E,  /* 0110111 */\n    // 0x66,  /* 0110011 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=166, hex=0xA6, ascii=\"!&\"\n    //  */\n    // 0x38,  /* 0011100 */\n    // 0x0C,  /* 0000110 */\n    // 0x3C,  /* 0011110 */\n    // 0x6C,  /* 0110110 */\n    // 0x34,  /* 0011010 */\n    // 0x00,  /* 0000000 */\n    // 0x7C,  /* 0111110 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=167, hex=0xA7, ascii=\"!'\"\n    //  */\n    // 0x3C,  /* 0011110 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x3C,  /* 0011110 */\n    // 0x00,  /* 0000000 */\n    // 0x7E,  /* 0111111 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=168, hex=0xA8, ascii=\"!(\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x18,  /* 0001100 */\n    // 0x00,  /* 0000000 */\n    // 0x18,  /* 0001100 */\n    // 0x30,  /* 0011000 */\n    // 0x60,  /* 0110000 */\n    // 0x66,  /* 0110011 */\n    // 0x3C,  /* 0011110 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=169, hex=0xA9, ascii=\"!)\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x3C,  /* 0011110 */\n    // 0x3C,  /* 0011110 */\n    // 0x30,  /* 0011000 */\n    // 0x30,  /* 0011000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=170, hex=0xAA, ascii=\"!*\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x7C,  /* 0111110 */\n    // 0x7C,  /* 0111110 */\n    // 0x0C,  /* 0000110 */\n    // 0x0C,  /* 0000110 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=171, hex=0xAB, ascii=\"!+\"\n    //  */\n    // 0x60,  /* 0110000 */\n    // 0x60,  /* 0110000 */\n    // 0x60,  /* 0110000 */\n    // 0x6E,  /* 0110111 */\n    // 0x1A,  /* 0001101 */\n    // 0x04,  /* 0000010 */\n    // 0x18,  /* 0001100 */\n    // 0x1E,  /* 0001111 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=172, hex=0xAC, ascii=\"!,\"\n    //  */\n    // 0x60,  /* 0110000 */\n    // 0x60,  /* 0110000 */\n    // 0x60,  /* 0110000 */\n    // 0x6C,  /* 0110110 */\n    // 0x7C,  /* 0111110 */\n    // 0x2C,  /* 0010110 */\n    // 0x7C,  /* 0111110 */\n    // 0x0C,  /* 0000110 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=173, hex=0xAD, ascii=\"!-\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x18,  /* 0001100 */\n    // 0x00,  /* 0000000 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x3C,  /* 0011110 */\n    // 0x3C,  /* 0011110 */\n    // 0x18,  /* 0001100 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=174, hex=0xAE, ascii=\"!.\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x32,  /* 0011001 */\n    // 0x66,  /* 0110011 */\n    // 0xCC,  /* 1100110 */\n    // 0x66,  /* 0110011 */\n    // 0x32,  /* 0011001 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=175, hex=0xAF, ascii=\"!/\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0xCC,  /* 1100110 */\n    // 0x66,  /* 0110011 */\n    // 0x32,  /* 0011001 */\n    // 0x66,  /* 0110011 */\n    // 0xCC,  /* 1100110 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=176, hex=0xB0, ascii=\"!0\"\n    //  */\n    // 0x54,  /* 0101010 */\n    // 0x00,  /* 0000000 */\n    // 0xAA,  /* 1010101 */\n    // 0x00,  /* 0000000 */\n    // 0x54,  /* 0101010 */\n    // 0x00,  /* 0000000 */\n    // 0xAA,  /* 1010101 */\n    // 0x00,  /* 0000000 */\n    // 0x54,  /* 0101010 */\n\n    // /*\n    //  * code=177, hex=0xB1, ascii=\"!1\"\n    //  */\n    // 0x92,  /* 1001001 */\n    // 0x48,  /* 0100100 */\n    // 0x24,  /* 0010010 */\n    // 0x92,  /* 1001001 */\n    // 0x48,  /* 0100100 */\n    // 0x24,  /* 0010010 */\n    // 0x92,  /* 1001001 */\n    // 0x48,  /* 0100100 */\n    // 0x24,  /* 0010010 */\n\n    // /*\n    //  * code=178, hex=0xB2, ascii=\"!2\"\n    //  */\n    // 0xAA,  /* 1010101 */\n    // 0x54,  /* 0101010 */\n    // 0xAA,  /* 1010101 */\n    // 0x54,  /* 0101010 */\n    // 0xAA,  /* 1010101 */\n    // 0x54,  /* 0101010 */\n    // 0xAA,  /* 1010101 */\n    // 0x54,  /* 0101010 */\n    // 0xAA,  /* 1010101 */\n\n    // /*\n    //  * code=179, hex=0xB3, ascii=\"!3\"\n    //  */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n\n    // /*\n    //  * code=180, hex=0xB4, ascii=\"!4\"\n    //  */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0xF0,  /* 1111000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n\n    // /*\n    //  * code=181, hex=0xB5, ascii=\"!5\"\n    //  */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0xF0,  /* 1111000 */\n    // 0x10,  /* 0001000 */\n    // 0xF0,  /* 1111000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n\n    // /*\n    //  * code=182, hex=0xB6, ascii=\"!6\"\n    //  */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0xE8,  /* 1110100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n\n    // /*\n    //  * code=183, hex=0xB7, ascii=\"!7\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0xF8,  /* 1111100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n\n    // /*\n    //  * code=184, hex=0xB8, ascii=\"!8\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0xF0,  /* 1111000 */\n    // 0x10,  /* 0001000 */\n    // 0xF0,  /* 1111000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n\n    // /*\n    //  * code=185, hex=0xB9, ascii=\"!9\"\n    //  */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0xE8,  /* 1110100 */\n    // 0x08,  /* 0000100 */\n    // 0xE8,  /* 1110100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n\n    // /*\n    //  * code=186, hex=0xBA, ascii=\"!:\"\n    //  */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n\n    // /*\n    //  * code=187, hex=0xBB, ascii=\"!;\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0xF8,  /* 1111100 */\n    // 0x08,  /* 0000100 */\n    // 0xE8,  /* 1110100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n\n    // /*\n    //  * code=188, hex=0xBC, ascii=\"!<\"\n    //  */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0xE8,  /* 1110100 */\n    // 0x08,  /* 0000100 */\n    // 0xF8,  /* 1111100 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=189, hex=0xBD, ascii=\"!=\"\n    //  */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0xF8,  /* 1111100 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=190, hex=0xBE, ascii=\"!>\"\n    //  */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0xF0,  /* 1111000 */\n    // 0x10,  /* 0001000 */\n    // 0xF0,  /* 1111000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=191, hex=0xBF, ascii=\"!?\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0xF0,  /* 1111000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n\n    // /*\n    //  * code=192, hex=0xC0, ascii=\"!@\"\n    //  */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x1E,  /* 0001111 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=193, hex=0xC1, ascii=\"!A\"\n    //  */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0xFE,  /* 1111111 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=194, hex=0xC2, ascii=\"!B\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0xFE,  /* 1111111 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n\n    // /*\n    //  * code=195, hex=0xC3, ascii=\"!C\"\n    //  */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x1E,  /* 0001111 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n\n    // /*\n    //  * code=196, hex=0xC4, ascii=\"!D\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0xFE,  /* 1111111 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=197, hex=0xC5, ascii=\"!E\"\n    //  */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0xFE,  /* 1111111 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n\n    // /*\n    //  * code=198, hex=0xC6, ascii=\"!F\"\n    //  */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x1E,  /* 0001111 */\n    // 0x10,  /* 0001000 */\n    // 0x1E,  /* 0001111 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n\n    // /*\n    //  * code=199, hex=0xC7, ascii=\"!G\"\n    //  */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x2E,  /* 0010111 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n\n    // /*\n    //  * code=200, hex=0xC8, ascii=\"!H\"\n    //  */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x2E,  /* 0010111 */\n    // 0x20,  /* 0010000 */\n    // 0x3E,  /* 0011111 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=201, hex=0xC9, ascii=\"!I\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x3E,  /* 0011111 */\n    // 0x20,  /* 0010000 */\n    // 0x2E,  /* 0010111 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n\n    // /*\n    //  * code=202, hex=0xCA, ascii=\"!J\"\n    //  */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0xEE,  /* 1110111 */\n    // 0x00,  /* 0000000 */\n    // 0xFE,  /* 1111111 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=203, hex=0xCB, ascii=\"!K\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0xFE,  /* 1111111 */\n    // 0x00,  /* 0000000 */\n    // 0xEE,  /* 1110111 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n\n    // /*\n    //  * code=204, hex=0xCC, ascii=\"!L\"\n    //  */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x2E,  /* 0010111 */\n    // 0x20,  /* 0010000 */\n    // 0x2E,  /* 0010111 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n\n    // /*\n    //  * code=205, hex=0xCD, ascii=\"!M\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0xFE,  /* 1111111 */\n    // 0x00,  /* 0000000 */\n    // 0xFE,  /* 1111111 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=206, hex=0xCE, ascii=\"!N\"\n    //  */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0xEE,  /* 1110111 */\n    // 0x00,  /* 0000000 */\n    // 0xEE,  /* 1110111 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n\n    // /*\n    //  * code=207, hex=0xCF, ascii=\"!O\"\n    //  */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0xFE,  /* 1111111 */\n    // 0x00,  /* 0000000 */\n    // 0xFE,  /* 1111111 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=208, hex=0xD0, ascii=\"!P\"\n    //  */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0xF8,  /* 1111100 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=209, hex=0xD1, ascii=\"!Q\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0xFE,  /* 1111111 */\n    // 0x00,  /* 0000000 */\n    // 0xFE,  /* 1111111 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n\n    // /*\n    //  * code=210, hex=0xD2, ascii=\"!R\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0xF8,  /* 1111100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n\n    // /*\n    //  * code=211, hex=0xD3, ascii=\"!S\"\n    //  */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x3E,  /* 0011111 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=212, hex=0xD4, ascii=\"!T\"\n    //  */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x1E,  /* 0001111 */\n    // 0x10,  /* 0001000 */\n    // 0x1E,  /* 0001111 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=213, hex=0xD5, ascii=\"!U\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x1E,  /* 0001111 */\n    // 0x10,  /* 0001000 */\n    // 0x1E,  /* 0001111 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n\n    // /*\n    //  * code=214, hex=0xD6, ascii=\"!V\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x3E,  /* 0011111 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n\n    // /*\n    //  * code=215, hex=0xD7, ascii=\"!W\"\n    //  */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0xE8,  /* 1110100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n    // 0x28,  /* 0010100 */\n\n    // /*\n    //  * code=216, hex=0xD8, ascii=\"!X\"\n    //  */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0xFE,  /* 1111111 */\n    // 0x10,  /* 0001000 */\n    // 0xFE,  /* 1111111 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n\n    // /*\n    //  * code=217, hex=0xD9, ascii=\"!Y\"\n    //  */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0xF0,  /* 1111000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=218, hex=0xDA, ascii=\"!Z\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x1E,  /* 0001111 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n    // 0x10,  /* 0001000 */\n\n    // /*\n    //  * code=219, hex=0xDB, ascii=\"![\"\n    //  */\n    // 0xFE,  /* 1111111 */\n    // 0xFE,  /* 1111111 */\n    // 0xFE,  /* 1111111 */\n    // 0xFE,  /* 1111111 */\n    // 0xFE,  /* 1111111 */\n    // 0xFE,  /* 1111111 */\n    // 0xFE,  /* 1111111 */\n    // 0xFE,  /* 1111111 */\n    // 0xFE,  /* 1111111 */\n\n    // /*\n    //  * code=220, hex=0xDC, ascii=\"!\\\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0xFE,  /* 1111111 */\n    // 0xFE,  /* 1111111 */\n    // 0xFE,  /* 1111111 */\n    // 0xFE,  /* 1111111 */\n\n    // /*\n    //  * code=221, hex=0xDD, ascii=\"!]\"\n    //  */\n    // 0xF0,  /* 1111000 */\n    // 0xF0,  /* 1111000 */\n    // 0xF0,  /* 1111000 */\n    // 0xF0,  /* 1111000 */\n    // 0xF0,  /* 1111000 */\n    // 0xF0,  /* 1111000 */\n    // 0xF0,  /* 1111000 */\n    // 0xF0,  /* 1111000 */\n    // 0xF0,  /* 1111000 */\n\n    // /*\n    //  * code=222, hex=0xDE, ascii=\"!^\"\n    //  */\n    // 0x0E,  /* 0000111 */\n    // 0x0E,  /* 0000111 */\n    // 0x0E,  /* 0000111 */\n    // 0x0E,  /* 0000111 */\n    // 0x0E,  /* 0000111 */\n    // 0x0E,  /* 0000111 */\n    // 0x0E,  /* 0000111 */\n    // 0x0E,  /* 0000111 */\n    // 0x0E,  /* 0000111 */\n\n    // /*\n    //  * code=223, hex=0xDF, ascii=\"!_\"\n    //  */\n    // 0xFE,  /* 1111111 */\n    // 0xFE,  /* 1111111 */\n    // 0xFE,  /* 1111111 */\n    // 0xFE,  /* 1111111 */\n    // 0xFE,  /* 1111111 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=224, hex=0xE0, ascii=\"!`\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x34,  /* 0011010 */\n    // 0x68,  /* 0110100 */\n    // 0x68,  /* 0110100 */\n    // 0x68,  /* 0110100 */\n    // 0x34,  /* 0011010 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=225, hex=0xE1, ascii=\"!a\"\n    //  */\n    // 0x7C,  /* 0111110 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x6C,  /* 0110110 */\n    // 0x66,  /* 0110011 */\n    // 0x62,  /* 0110001 */\n    // 0x66,  /* 0110011 */\n    // 0x6C,  /* 0110110 */\n    // 0x08,  /* 0000100 */\n\n    // /*\n    //  * code=226, hex=0xE2, ascii=\"!b\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x7E,  /* 0111111 */\n    // 0x62,  /* 0110001 */\n    // 0x60,  /* 0110000 */\n    // 0x60,  /* 0110000 */\n    // 0x60,  /* 0110000 */\n    // 0x60,  /* 0110000 */\n    // 0x60,  /* 0110000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=227, hex=0xE3, ascii=\"!c\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x6C,  /* 0110110 */\n    // 0xFE,  /* 1111111 */\n    // 0xF6,  /* 1111011 */\n    // 0x66,  /* 0110011 */\n    // 0x6C,  /* 0110110 */\n    // 0x6C,  /* 0110110 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=228, hex=0xE4, ascii=\"!d\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0xFE,  /* 1111111 */\n    // 0xC6,  /* 1100011 */\n    // 0x60,  /* 0110000 */\n    // 0x38,  /* 0011100 */\n    // 0x30,  /* 0011000 */\n    // 0x66,  /* 0110011 */\n    // 0xFE,  /* 1111111 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=229, hex=0xE5, ascii=\"!e\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x3E,  /* 0011111 */\n    // 0x6C,  /* 0110110 */\n    // 0x6C,  /* 0110110 */\n    // 0x6C,  /* 0110110 */\n    // 0x38,  /* 0011100 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=230, hex=0xE6, ascii=\"!f\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x36,  /* 0011011 */\n    // 0x36,  /* 0011011 */\n    // 0x36,  /* 0011011 */\n    // 0x3E,  /* 0011111 */\n    // 0x62,  /* 0110001 */\n    // 0x40,  /* 0100000 */\n\n    // /*\n    //  * code=231, hex=0xE7, ascii=\"!g\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x7A,  /* 0111101 */\n    // 0x6A,  /* 0110101 */\n    // 0x0E,  /* 0000111 */\n    // 0x0C,  /* 0000110 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=232, hex=0xE8, ascii=\"!h\"\n    //  */\n    // 0x3C,  /* 0011110 */\n    // 0x18,  /* 0001100 */\n    // 0x3C,  /* 0011110 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x3C,  /* 0011110 */\n    // 0x18,  /* 0001100 */\n    // 0x3C,  /* 0011110 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=233, hex=0xE9, ascii=\"!i\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x3C,  /* 0011110 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x7E,  /* 0111111 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x3C,  /* 0011110 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=234, hex=0xEA, ascii=\"!j\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x3C,  /* 0011110 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x24,  /* 0010010 */\n    // 0x66,  /* 0110011 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=235, hex=0xEB, ascii=\"!k\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x3C,  /* 0011110 */\n    // 0x60,  /* 0110000 */\n    // 0x30,  /* 0011000 */\n    // 0x18,  /* 0001100 */\n    // 0x3C,  /* 0011110 */\n    // 0x66,  /* 0110011 */\n    // 0x3C,  /* 0011110 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=236, hex=0xEC, ascii=\"!l\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x34,  /* 0011010 */\n    // 0x4A,  /* 0100101 */\n    // 0x4A,  /* 0100101 */\n    // 0x4A,  /* 0100101 */\n    // 0x34,  /* 0011010 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=237, hex=0xED, ascii=\"!m\"\n    //  */\n    // 0x04,  /* 0000010 */\n    // 0x3C,  /* 0011110 */\n    // 0x66,  /* 0110011 */\n    // 0x6E,  /* 0110111 */\n    // 0x76,  /* 0111011 */\n    // 0x66,  /* 0110011 */\n    // 0x3C,  /* 0011110 */\n    // 0x10,  /* 0001000 */\n    // 0x20,  /* 0010000 */\n\n    // /*\n    //  * code=238, hex=0xEE, ascii=\"!n\"\n    //  */\n    // 0x1E,  /* 0001111 */\n    // 0x30,  /* 0011000 */\n    // 0x60,  /* 0110000 */\n    // 0x60,  /* 0110000 */\n    // 0x7E,  /* 0111111 */\n    // 0x60,  /* 0110000 */\n    // 0x30,  /* 0011000 */\n    // 0x1E,  /* 0001111 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=239, hex=0xEF, ascii=\"!o\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x3C,  /* 0011110 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=240, hex=0xF0, ascii=\"!p\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x7C,  /* 0111110 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x7C,  /* 0111110 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x7C,  /* 0111110 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=241, hex=0xF1, ascii=\"!q\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x7E,  /* 0111111 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x00,  /* 0000000 */\n    // 0x7E,  /* 0111111 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=242, hex=0xF2, ascii=\"!r\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x30,  /* 0011000 */\n    // 0x18,  /* 0001100 */\n    // 0x0C,  /* 0000110 */\n    // 0x18,  /* 0001100 */\n    // 0x30,  /* 0011000 */\n    // 0x00,  /* 0000000 */\n    // 0x3C,  /* 0011110 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=243, hex=0xF3, ascii=\"!s\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x0C,  /* 0000110 */\n    // 0x18,  /* 0001100 */\n    // 0x30,  /* 0011000 */\n    // 0x18,  /* 0001100 */\n    // 0x0C,  /* 0000110 */\n    // 0x00,  /* 0000000 */\n    // 0x3C,  /* 0011110 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=244, hex=0xF4, ascii=\"!t\"\n    //  */\n    // 0x0C,  /* 0000110 */\n    // 0x1A,  /* 0001101 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n\n    // /*\n    //  * code=245, hex=0xF5, ascii=\"!u\"\n    //  */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x58,  /* 0101100 */\n    // 0x30,  /* 0011000 */\n\n    // /*\n    //  * code=246, hex=0xF6, ascii=\"!v\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x00,  /* 0000000 */\n    // 0x7E,  /* 0111111 */\n    // 0x00,  /* 0000000 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=247, hex=0xF7, ascii=\"!w\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x1A,  /* 0001101 */\n    // 0x76,  /* 0111011 */\n    // 0x00,  /* 0000000 */\n    // 0x1A,  /* 0001101 */\n    // 0x76,  /* 0111011 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=248, hex=0xF8, ascii=\"!x\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x3C,  /* 0011110 */\n    // 0x66,  /* 0110011 */\n    // 0x66,  /* 0110011 */\n    // 0x3C,  /* 0011110 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=249, hex=0xF9, ascii=\"!y\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x18,  /* 0001100 */\n    // 0x3C,  /* 0011110 */\n    // 0x3C,  /* 0011110 */\n    // 0x18,  /* 0001100 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=250, hex=0xFA, ascii=\"!z\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x18,  /* 0001100 */\n    // 0x18,  /* 0001100 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=251, hex=0xFB, ascii=\"!{\"\n    //  */\n    // 0x0E,  /* 0000111 */\n    // 0x0C,  /* 0000110 */\n    // 0x0C,  /* 0000110 */\n    // 0x0C,  /* 0000110 */\n    // 0x0C,  /* 0000110 */\n    // 0x6C,  /* 0110110 */\n    // 0x3C,  /* 0011110 */\n    // 0x0C,  /* 0000110 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=252, hex=0xFC, ascii=\"!|\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x78,  /* 0111100 */\n    // 0x6C,  /* 0110110 */\n    // 0x6C,  /* 0110110 */\n    // 0x6C,  /* 0110110 */\n    // 0x6C,  /* 0110110 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=253, hex=0xFD, ascii=\"!}\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x38,  /* 0011100 */\n    // 0x4C,  /* 0100110 */\n    // 0x18,  /* 0001100 */\n    // 0x30,  /* 0011000 */\n    // 0x7C,  /* 0111110 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=254, hex=0xFE, ascii=\"!~\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x7C,  /* 0111110 */\n    // 0x7C,  /* 0111110 */\n    // 0x7C,  /* 0111110 */\n    // 0x7C,  /* 0111110 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n\n    // /*\n    //  * code=255, hex=0xFF, ascii=\"!^\"\n    //  */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n    // 0x00,  /* 0000000 */\n};\n"
  },
  {
    "path": "wled00/udp.cpp",
    "content": "#include \"wled.h\"\n\n/*\n * UDP sync notifier / Realtime / Hyperion / TPM2.NET\n */\n\n#define UDP_SEG_SIZE 36\n#define SEG_OFFSET (41)\n#define WLEDPACKETSIZE (41+(WS2812FX::getMaxSegments()*UDP_SEG_SIZE)+0)\n#define UDP_IN_MAXSIZE 1472\n#define PRESUMED_NETWORK_DELAY 3 //how many ms could it take on avg to reach the receiver? This will be added to transmitted times\n\ntypedef struct PartialEspNowPacket {\n  uint8_t magic;\n  uint8_t packet;\n  uint8_t noOfPackets;\n  uint8_t data[247];\n} partial_packet_t;\n\nvoid notify(byte callMode, bool followUp)\n{\n#ifndef WLED_DISABLE_ESPNOW\n  if (!udpConnected && !useESPNowSync) return;\n#else\n  if (!udpConnected) return;\n#endif\n  if (!syncGroups || !sendNotificationsRT) return;\n  switch (callMode)\n  {\n    case CALL_MODE_INIT:          return;\n    case CALL_MODE_DIRECT_CHANGE: if (!notifyDirect) return; break;\n    case CALL_MODE_BUTTON:        if (!notifyButton) return; break;\n    case CALL_MODE_BUTTON_PRESET: if (!notifyButton) return; break;\n    case CALL_MODE_NIGHTLIGHT:    if (!notifyDirect) return; break;\n    case CALL_MODE_HUE:           if (!notifyHue)    return; break;\n    case CALL_MODE_PRESET_CYCLE:  if (!notifyDirect) return; break;\n    case CALL_MODE_ALEXA:         if (!notifyAlexa)  return; break;\n    default: return;\n  }\n  byte udpOut[WLEDPACKETSIZE];  //TODO: optimize size to use only active segments\n  Segment& mainseg = strip.getMainSegment();\n  udpOut[0] = 0; //0: wled notifier protocol 1: WARLS protocol\n  udpOut[1] = callMode;\n  udpOut[2] = bri;\n  uint32_t col = mainseg.colors[0];\n  udpOut[3] = R(col);\n  udpOut[4] = G(col);\n  udpOut[5] = B(col);\n  udpOut[6] = nightlightActive;\n  udpOut[7] = nightlightDelayMins;\n  udpOut[8] = mainseg.mode;\n  udpOut[9] = mainseg.speed;\n  udpOut[10] = W(col);\n  //compatibilityVersionByte:\n  //0: old 1: supports white 2: supports secondary color\n  //3: supports FX intensity, 24 byte packet 4: supports transitionDelay 5: sup palette\n  //6: supports timebase syncing, 29 byte packet 7: supports tertiary color 8: supports sys time sync, 36 byte packet\n  //9: supports sync groups, 37 byte packet 10: supports CCT, 39 byte packet 11: per segment options, variable packet length (40+WS2812FX::getMaxSegments()*3)\n  //12: enhanced effect sliders, 2D & mapping options\n  udpOut[11] = 12;\n  col = mainseg.colors[1];\n  udpOut[12] = R(col);\n  udpOut[13] = G(col);\n  udpOut[14] = B(col);\n  udpOut[15] = W(col);\n  udpOut[16] = mainseg.intensity;\n  udpOut[17] = (transitionDelay >> 0) & 0xFF;\n  udpOut[18] = (transitionDelay >> 8) & 0xFF;\n  udpOut[19] = mainseg.palette;\n  col = mainseg.colors[2];\n  udpOut[20] = R(col);\n  udpOut[21] = G(col);\n  udpOut[22] = B(col);\n  udpOut[23] = W(col);\n\n  udpOut[24] = followUp;\n  uint32_t t = millis() + strip.timebase;\n  udpOut[25] = (t >> 24) & 0xFF;\n  udpOut[26] = (t >> 16) & 0xFF;\n  udpOut[27] = (t >>  8) & 0xFF;\n  udpOut[28] = (t >>  0) & 0xFF;\n\n  //sync system time\n  udpOut[29] = toki.getTimeSource();\n  Toki::Time tm = toki.getTime();\n  uint32_t unix = tm.sec;\n  udpOut[30] = (unix >> 24) & 0xFF;\n  udpOut[31] = (unix >> 16) & 0xFF;\n  udpOut[32] = (unix >>  8) & 0xFF;\n  udpOut[33] = (unix >>  0) & 0xFF;\n  uint16_t ms = tm.ms;\n  udpOut[34] = (ms >> 8) & 0xFF;\n  udpOut[35] = (ms >> 0) & 0xFF;\n\n  //sync groups\n  udpOut[36] = syncGroups;\n\n  //Might be changed to Kelvin in the future, receiver code should handle that case\n  //0: byte 38 contains 0-255 value, 255: no valid CCT, 1-254: Kelvin value MSB\n  udpOut[37] = strip.hasCCTBus() ? 0 : 255; //check this is 0 for the next value to be significant\n  udpOut[38] = mainseg.cct;\n\n  udpOut[39] = strip.getActiveSegmentsNum();\n  udpOut[40] = UDP_SEG_SIZE; //size of each loop iteration (one segment)\n  size_t s = 0, nsegs = strip.getSegmentsNum();\n  for (size_t i = 0; i < nsegs; i++) {\n    const Segment &selseg = strip.getSegment(i);\n    if (!selseg.isActive()) continue;\n    unsigned ofs = 41 + s*UDP_SEG_SIZE; //start of segment offset byte\n    udpOut[0 +ofs] = s;\n    udpOut[1 +ofs] = selseg.start >> 8;\n    udpOut[2 +ofs] = selseg.start & 0xFF;\n    udpOut[3 +ofs] = selseg.stop >> 8;\n    udpOut[4 +ofs] = selseg.stop & 0xFF;\n    udpOut[5 +ofs] = selseg.grouping;\n    udpOut[6 +ofs] = selseg.spacing;\n    udpOut[7 +ofs] = selseg.offset >> 8;\n    udpOut[8 +ofs] = selseg.offset & 0xFF;\n    udpOut[9 +ofs] = selseg.options & 0x8F; //only take into account selected, mirrored, on, reversed, reverse_y (for 2D); ignore freeze, reset, transitional\n    udpOut[10+ofs] = selseg.opacity;\n    udpOut[11+ofs] = selseg.mode;\n    udpOut[12+ofs] = selseg.speed;\n    udpOut[13+ofs] = selseg.intensity;\n    udpOut[14+ofs] = selseg.palette;\n    udpOut[15+ofs] = R(selseg.colors[0]);\n    udpOut[16+ofs] = G(selseg.colors[0]);\n    udpOut[17+ofs] = B(selseg.colors[0]);\n    udpOut[18+ofs] = W(selseg.colors[0]);\n    udpOut[19+ofs] = R(selseg.colors[1]);\n    udpOut[20+ofs] = G(selseg.colors[1]);\n    udpOut[21+ofs] = B(selseg.colors[1]);\n    udpOut[22+ofs] = W(selseg.colors[1]);\n    udpOut[23+ofs] = R(selseg.colors[2]);\n    udpOut[24+ofs] = G(selseg.colors[2]);\n    udpOut[25+ofs] = B(selseg.colors[2]);\n    udpOut[26+ofs] = W(selseg.colors[2]);\n    udpOut[27+ofs] = selseg.cct;\n    udpOut[28+ofs] = (selseg.options>>8) & 0xFF; //mirror_y, transpose, 2D mapping & sound\n    udpOut[29+ofs] = selseg.custom1;\n    udpOut[30+ofs] = selseg.custom2;\n    udpOut[31+ofs] = selseg.custom3 | (selseg.check1<<5) | (selseg.check2<<6) | (selseg.check3<<7);\n    udpOut[32+ofs] = selseg.startY >> 8;    // ATM always 0 as Segment::startY is 8-bit\n    udpOut[33+ofs] = selseg.startY & 0xFF;\n    udpOut[34+ofs] = selseg.stopY >> 8;     // ATM always 0 as Segment::stopY is 8-bit\n    udpOut[35+ofs] = selseg.stopY & 0xFF;\n    ++s;\n  }\n\n  //uint16_t offs = SEG_OFFSET;\n  //next value to be added has index: udpOut[offs + 0]\n\n#ifndef WLED_DISABLE_ESPNOW\n  if (enableESPNow && useESPNowSync && statusESPNow == ESP_NOW_STATE_ON) {\n    partial_packet_t buffer = {'W', 0, 1, {0}};\n    // send global data\n    DEBUG_PRINTLN(F(\"ESP-NOW sending first packet.\"));\n    const size_t bufferSize = sizeof(buffer.data)/sizeof(uint8_t);\n    size_t packetSize = 41;\n    size_t s0 = 0;\n    memcpy(buffer.data, udpOut, packetSize);\n    // stuff as many segments in first packet as possible (normally up to 5)\n    for (size_t i = 0; packetSize < bufferSize && i < s; i++) {\n      memcpy(buffer.data + packetSize, &udpOut[41+i*UDP_SEG_SIZE], UDP_SEG_SIZE);\n      packetSize += UDP_SEG_SIZE;\n      s0++;\n    }\n    if (s > s0) buffer.noOfPackets += 1 + ((s - s0) * UDP_SEG_SIZE) / bufferSize; // set number of packets\n    auto err = quickEspNow.send(ESPNOW_BROADCAST_ADDRESS, reinterpret_cast<const uint8_t*>(&buffer), packetSize+3);\n    if (!err && s0 < s) {\n      // send rest of the segments\n      buffer.packet++;\n      packetSize = 0;\n      // WARNING: this will only work for up to 3 messages (~17 segments) as QuickESPNOW only has a ring buffer capable of holding 3 queued messages\n      // to work around that limitation it is mandatory to utilize onDataSent() callback which should reduce number queued messages\n      // and wait until at least one space is available in the buffer\n      for (size_t i = s0; i < s; i++) {\n        memcpy(buffer.data + packetSize, &udpOut[41+i*UDP_SEG_SIZE], UDP_SEG_SIZE);\n        packetSize += UDP_SEG_SIZE;\n        if (packetSize + UDP_SEG_SIZE < bufferSize) continue;\n        DEBUG_PRINTF_P(PSTR(\"ESP-NOW sending packet: %d (%u)\\n\"), (int)buffer.packet, packetSize+3);\n        err = quickEspNow.send(ESPNOW_BROADCAST_ADDRESS, reinterpret_cast<const uint8_t*>(&buffer), packetSize+3);\n        buffer.packet++;\n        packetSize = 0;\n        if (err) break;\n      }\n      if (!err && packetSize > 0) {\n        DEBUG_PRINTF_P(PSTR(\"ESP-NOW sending last packet: %d (%d)\\n\"), (int)buffer.packet, packetSize+3);\n        err = quickEspNow.send(ESPNOW_BROADCAST_ADDRESS, reinterpret_cast<const uint8_t*>(&buffer), packetSize+3);\n      }\n    }\n    if (err) {\n      DEBUG_PRINTLN(F(\"ESP-NOW sending packet failed.\"));\n    }\n  }\n  if (udpConnected) \n#endif\n  {\n    DEBUG_PRINTLN(F(\"UDP sending packet.\"));\n    IPAddress broadcastIp = ~uint32_t(Network.subnetMask()) | uint32_t(Network.gatewayIP());\n    notifierUdp.beginPacket(broadcastIp, udpPort);\n    notifierUdp.write(udpOut, WLEDPACKETSIZE); // TODO: add actual used buffer size\n    notifierUdp.endPacket();\n  }\n  notificationSentCallMode = callMode;\n  notificationSentTime = millis();\n  notificationCount = followUp ? notificationCount + 1 : 0;\n}\n\nstatic void parseNotifyPacket(const uint8_t *udpIn) {\n  //ignore notification if received within a second after sending a notification ourselves\n  if (millis() - notificationSentTime < 1000) return;\n  if (udpIn[1] > 199) return; //do not receive custom versions\n\n  //compatibilityVersionByte:\n  byte version = udpIn[11];\n  DEBUG_PRINTF_P(PSTR(\"UDP packet version: %d\\n\"), (int)version);\n\n  // if we are not part of any sync group ignore message\n  if (version < 9) {\n    // legacy senders are treated as if sending in sync group 1 only\n    if (!(receiveGroups & 0x01)) return;\n  } else if (!(receiveGroups & udpIn[36])) return;\n\n  bool someSel = (receiveNotificationBrightness || receiveNotificationColor || receiveNotificationEffects || receiveNotificationPalette);\n\n  // set transition time before making any segment changes\n  if (version > 3) {\n    jsonTransitionOnce = true;\n    strip.setTransition(((udpIn[17] << 0) & 0xFF) + ((udpIn[18] << 8) & 0xFF00));\n  }\n\n  //apply colors from notification to main segment, only if not syncing full segments\n  if ((receiveNotificationColor || !someSel) && (version < 11 || !receiveSegmentOptions)) {\n    // primary color, only apply white if intented (version > 0)\n    strip.getMainSegment().setColor(0, RGBW32(udpIn[3], udpIn[4], udpIn[5], (version > 0) ? udpIn[10] : 0));\n    if (version > 1) {\n      strip.getMainSegment().setColor(1, RGBW32(udpIn[12], udpIn[13], udpIn[14], udpIn[15])); // secondary color\n    }\n    if (version > 6) {\n      strip.getMainSegment().setColor(2, RGBW32(udpIn[20], udpIn[21], udpIn[22], udpIn[23])); // tertiary color\n      if (version > 9 && udpIn[37] < 255) { // valid CCT/Kelvin value\n        unsigned cct = udpIn[38];\n        if (udpIn[37] > 0) { //Kelvin\n          cct |= (udpIn[37] << 8);\n        }\n        strip.setCCT(cct);\n      }\n    }\n  }\n\n  bool timebaseUpdated = false;\n  //apply effects from notification\n  bool applyEffects = (receiveNotificationEffects || !someSel);\n  if (applyEffects && currentPlaylist >= 0) unloadPlaylist();\n  if (version > 10 && (receiveSegmentOptions || receiveSegmentBounds)) {\n    unsigned numSrcSegs = udpIn[39];\n    DEBUG_PRINTF_P(PSTR(\"UDP segments: %d\\n\"), numSrcSegs);\n    // are we syncing bounds and slave has more active segments than master?\n    if (receiveSegmentBounds && numSrcSegs < strip.getActiveSegmentsNum()) {\n      DEBUG_PRINTLN(F(\"Removing excessive segments.\"));\n      strip.suspend(); //should not be needed as UDP handling is not done in ISR callbacks but still added \"just in case\"\n      for (size_t i=strip.getSegmentsNum(); i>numSrcSegs && i>0; i--) {\n        Segment &seg = strip.getSegment(i-1);\n        if (seg.isActive()) seg.deactivate(); // delete segment\n      }\n      strip.resume();\n    }\n    size_t inactiveSegs = 0;\n    for (size_t i = 0; i < numSrcSegs && i < WS2812FX::getMaxSegments(); i++) {\n      unsigned ofs = 41 + i*udpIn[40]; //start of segment offset byte\n      unsigned id = udpIn[0 +ofs];\n      DEBUG_PRINTF_P(PSTR(\"UDP segment received: %u\\n\"), id);\n      if      (id >  strip.getSegmentsNum()) break;\n      else if (id == strip.getSegmentsNum()) {\n        if (receiveSegmentBounds && id < WS2812FX::getMaxSegments()) strip.appendSegment();\n        else break;\n      }\n      DEBUG_PRINTF_P(PSTR(\"UDP segment check: %u\\n\"), id);\n      Segment& selseg = strip.getSegment(id);\n      // if we are not syncing bounds skip unselected segments\n      if (selseg.isActive() && !(selseg.isSelected() || receiveSegmentBounds)) continue;\n      // ignore segment if it is inactive and we are not syncing bounds\n      if (!receiveSegmentBounds) {\n        if (!selseg.isActive()) {\n          inactiveSegs++;\n          DEBUG_PRINTLN(F(\"Inactive segment.\"));\n          continue;\n        } else {\n          id += inactiveSegs; // adjust id\n        }\n      }\n      DEBUG_PRINTF_P(PSTR(\"UDP segment processing: %u\\n\"), id);\n\n      uint16_t start  = (udpIn[1+ofs] << 8 | udpIn[2+ofs]);\n      uint16_t stop   = (udpIn[3+ofs] << 8 | udpIn[4+ofs]);\n      uint16_t startY = version > 11 ? (udpIn[32+ofs] << 8 | udpIn[33+ofs]) : 0;\n      uint16_t stopY  = version > 11 ? (udpIn[34+ofs] << 8 | udpIn[35+ofs]) : 1;\n      uint16_t offset = (udpIn[7+ofs] << 8 | udpIn[8+ofs]);\n      if (!receiveSegmentOptions) {\n        DEBUG_PRINTF_P(PSTR(\"Set segment w/o options: %d [%d,%d;%d,%d]\\n\"), id, (int)start, (int)stop, (int)startY, (int)stopY);\n        strip.suspend(); //should not be needed as UDP handling is not done in ISR callbacks but still added \"just in case\"\n        selseg.setGeometry(start, stop, selseg.grouping, selseg.spacing, offset, startY, stopY, selseg.map1D2D);\n        strip.resume();\n        continue; // we do receive bounds, but not options\n      }\n      selseg.options = (selseg.options & 0x0071U) | (udpIn[9 +ofs] & 0x0E); // ignore selected, freeze, reset & transitional\n      selseg.setOpacity(udpIn[10+ofs]);\n      if (applyEffects) {\n        DEBUG_PRINTF_P(PSTR(\"Apply effect: %u\\n\"), id);\n        selseg.setMode(udpIn[11+ofs]);\n        selseg.speed     = udpIn[12+ofs];\n        selseg.intensity = udpIn[13+ofs];\n      }\n      if (receiveNotificationPalette || !someSel) {\n        DEBUG_PRINTF_P(PSTR(\"Apply palette: %u\\n\"), id);\n        selseg.palette   = udpIn[14+ofs];\n      }\n      if (receiveNotificationColor || !someSel) {\n        DEBUG_PRINTF_P(PSTR(\"Apply color: %u\\n\"), id);\n        selseg.setColor(0, RGBW32(udpIn[15+ofs],udpIn[16+ofs],udpIn[17+ofs],udpIn[18+ofs]));\n        selseg.setColor(1, RGBW32(udpIn[19+ofs],udpIn[20+ofs],udpIn[21+ofs],udpIn[22+ofs]));\n        selseg.setColor(2, RGBW32(udpIn[23+ofs],udpIn[24+ofs],udpIn[25+ofs],udpIn[26+ofs]));\n        selseg.setCCT(udpIn[27+ofs]);\n      }\n      if (version > 11) {\n        // when applying synced options ignore selected as it may be used as indicator of which segments to sync\n        // freeze, reset should never be synced\n        // LSB to MSB: select, reverse, on, mirror, freeze, reset, reverse_y, mirror_y, transpose, map1d2d (3), ssim (2), set (2)\n        DEBUG_PRINTF_P(PSTR(\"Apply options: %u\\n\"), id);\n        selseg.options = (selseg.options & 0b0000000000110001U) | ((uint16_t)udpIn[28+ofs]<<8) | (udpIn[9 +ofs] & 0b11001110U); // ignore selected, freeze, reset\n        if (applyEffects) {\n          DEBUG_PRINTF_P(PSTR(\"Apply sliders: %u\\n\"), id);\n          selseg.custom1 = udpIn[29+ofs];\n          selseg.custom2 = udpIn[30+ofs];\n          selseg.custom3 = udpIn[31+ofs] & 0x1F;\n          selseg.check1  = (udpIn[31+ofs]>>5) & 0x1;\n          selseg.check2  = (udpIn[31+ofs]>>6) & 0x1;\n          selseg.check3  = (udpIn[31+ofs]>>7) & 0x1;\n        }\n      }\n      if (receiveSegmentBounds) {\n        DEBUG_PRINTF_P(PSTR(\"Set segment w/ options: %d [%d,%d;%d,%d]\\n\"), id, (int)start, (int)stop, (int)startY, (int)stopY);\n        strip.suspend(); //should not be needed as UDP handling is not done in ISR callbacks but still added \"just in case\"\n        selseg.setGeometry(start, stop, udpIn[5+ofs], udpIn[6+ofs], offset, startY, stopY, selseg.map1D2D);\n        strip.resume();\n      } else {\n        DEBUG_PRINTF_P(PSTR(\"Set segment grouping: %d [%d,%d]\\n\"), id, (int)udpIn[5+ofs], (int)udpIn[6+ofs]);\n        strip.suspend(); //should not be needed as UDP handling is not done in ISR callbacks but still added \"just in case\"\n        selseg.setGeometry(selseg.start, selseg.stop, udpIn[5+ofs], udpIn[6+ofs], selseg.offset, selseg.startY, selseg.stopY, selseg.map1D2D);\n        strip.resume();\n      }\n    }\n    stateChanged = true;\n  }\n\n  // simple effect sync, applies to all selected segments\n  if ((applyEffects || receiveNotificationPalette) && (version < 11 || !receiveSegmentOptions)) {\n    for (size_t i = 0; i < strip.getSegmentsNum(); i++) {\n      Segment& seg = strip.getSegment(i);\n      if (!seg.isActive() || !seg.isSelected()) continue;\n      if (applyEffects) {\n        seg.setMode(udpIn[8]);\n        seg.speed = udpIn[9];\n        if (version > 2) seg.intensity = udpIn[16];\n      }\n      if (version > 4 && receiveNotificationPalette) seg.setPalette(udpIn[19]);\n    }\n    stateChanged = true;\n  }\n\n  if (applyEffects && version > 5) {\n    uint32_t t = (udpIn[25] << 24) | (udpIn[26] << 16) | (udpIn[27] << 8) | (udpIn[28]);\n    t += PRESUMED_NETWORK_DELAY; //adjust trivially for network delay\n    t -= millis();\n    strip.timebase = t;\n    timebaseUpdated = true;\n  }\n\n  //adjust system time, but only if sender is more accurate than self\n  if (version > 7) {\n    Toki::Time tm;\n    tm.sec = (udpIn[30] << 24) | (udpIn[31] << 16) | (udpIn[32] << 8) | (udpIn[33]);\n    tm.ms = (udpIn[34] << 8) | (udpIn[35]);\n    if (udpIn[29] > toki.getTimeSource()) { //if sender's time source is more accurate\n      toki.adjust(tm, PRESUMED_NETWORK_DELAY); //adjust trivially for network delay\n      uint8_t ts = TOKI_TS_UDP;\n      if (udpIn[29] > 99) ts = TOKI_TS_UDP_NTP;\n      else if (udpIn[29] >= TOKI_TS_SEC) ts = TOKI_TS_UDP_SEC;\n      toki.setTime(tm, ts);\n    } else if (timebaseUpdated && toki.getTimeSource() > 99) { //if we both have good times, get a more accurate timebase\n      Toki::Time myTime = toki.getTime();\n      uint32_t diff = toki.msDifference(tm, myTime);\n      strip.timebase -= PRESUMED_NETWORK_DELAY; //no need to presume, use difference between NTP times at send and receive points\n      if (toki.isLater(tm, myTime)) {\n        strip.timebase += diff;\n      } else {\n        strip.timebase -= diff;\n      }\n    }\n  }\n\n  nightlightActive = udpIn[6];\n  if (nightlightActive) nightlightDelayMins = udpIn[7];\n\n  if (receiveNotificationBrightness || !someSel) bri = udpIn[2];\n  stateUpdated(CALL_MODE_NOTIFICATION);\n}\n\n// realtimeLock() is called from UDP notifications, JSON API or serial Ada\nvoid realtimeLock(uint32_t timeoutMs, byte md)\n{\n  if (!realtimeMode && !realtimeOverride) {\n    if (useMainSegmentOnly) {\n      Segment& mainseg = strip.getMainSegment();\n      mainseg.clear(); // clear entire segment (in case sender transmits less pixels)\n      mainseg.freeze = true;\n      // if WLED was off and using main segment only, freeze non-main segments so they stay off\n      if (bri == 0) {\n        for (size_t s = 0; s < strip.getSegmentsNum(); s++) strip.getSegment(s).freeze = true;\n      }\n    } else {\n      // clear entire strip\n      strip.fill(BLACK);\n    }\n    // if strip is off (bri==0) and not already in RTM\n    if (briT == 0) {\n      strip.setBrightness(briLast, true);\n    }\n  }\n\n  if (realtimeTimeout != UINT32_MAX) {\n    realtimeTimeout = (timeoutMs == 255001 || timeoutMs == 65000) ? UINT32_MAX : millis() + timeoutMs;\n  }\n  realtimeMode = md;\n\n  if (realtimeOverride) return;\n  if (arlsForceMaxBri) strip.setBrightness(255, true);\n  if (briT > 0 && md == REALTIME_MODE_GENERIC) strip.show();\n}\n\nvoid exitRealtime() {\n  if (!realtimeMode) return;\n  if (realtimeOverride == REALTIME_OVERRIDE_ONCE) realtimeOverride = REALTIME_OVERRIDE_NONE;\n  strip.setBrightness(bri, true);\n  realtimeTimeout = 0; // cancel realtime mode immediately\n  realtimeMode = REALTIME_MODE_INACTIVE; // inform UI immediately\n  realtimeIP[0] = 0;\n  if (useMainSegmentOnly) { // unfreeze live segment again\n    strip.getMainSegment().freeze = false;\n    strip.trigger();\n  } else {\n    strip.show(); // possible fix for #3589\n  }\n  updateInterfaces(CALL_MODE_WS_SEND);\n}\n\n\n#define TMP2NET_OUT_PORT 65442\n\nstatic void sendTPM2Ack() {\n  notifierUdp.beginPacket(notifierUdp.remoteIP(), TMP2NET_OUT_PORT);\n  uint8_t response_ack = 0xac;\n  notifierUdp.write(&response_ack, 1);\n  notifierUdp.endPacket();\n}\n\n\nvoid handleNotifications()\n{\n  IPAddress localIP;\n\n  //send second notification if enabled\n  if(udpConnected && (notificationCount < udpNumRetries) && ((millis()-notificationSentTime) > 250)){\n    notify(notificationSentCallMode,true);\n  }\n\n  if (e131NewData && millis() - strip.getLastShow() > 15)\n  {\n    e131NewData = false;\n    if (useMainSegmentOnly) strip.trigger();\n    else                    strip.show();\n  }\n\n  //unlock strip when realtime UDP times out\n  if (realtimeMode && millis() > realtimeTimeout) exitRealtime();\n\n  //receive UDP notifications\n  if (!udpConnected) return;\n\n  bool isSupp = false;\n  size_t packetSize = notifierUdp.parsePacket();\n  if (!packetSize && udp2Connected) {\n    packetSize = notifier2Udp.parsePacket();\n    isSupp = true;\n  }\n\n  //hyperion / raw RGB\n  if (!packetSize && udpRgbConnected) {\n    packetSize = rgbUdp.parsePacket();\n    if (packetSize) {\n      if (!receiveDirect) return;\n      if (packetSize > UDP_IN_MAXSIZE || packetSize < 3) return;\n      realtimeIP = rgbUdp.remoteIP();\n      DEBUG_PRINTLN(rgbUdp.remoteIP());\n      uint8_t lbuf[packetSize];\n      rgbUdp.read(lbuf, packetSize);\n      realtimeLock(realtimeTimeoutMs, REALTIME_MODE_HYPERION);\n      if (realtimeOverride) return;\n      unsigned totalLen = strip.getLengthTotal();\n      for (size_t i = 0, id = 0; i < packetSize -2 && id < totalLen; i += 3, id++) {\n        setRealtimePixel(id, lbuf[i], lbuf[i+1], lbuf[i+2], 0);\n      }\n      if (useMainSegmentOnly) strip.trigger();\n      else                    strip.show();\n      return;\n    }\n  }\n\n  localIP = Network.localIP();\n  //notifier and UDP realtime\n  if (!packetSize || packetSize > UDP_IN_MAXSIZE) return;\n  if (!isSupp && notifierUdp.remoteIP() == localIP) return; //don't process broadcasts we send ourselves\n\n  uint8_t udpIn[packetSize +1];\n  unsigned len;\n  if (isSupp) len = notifier2Udp.read(udpIn, packetSize);\n  else        len =  notifierUdp.read(udpIn, packetSize);\n\n  // WLED nodes info notifications\n  if (isSupp && udpIn[0] == 255 && udpIn[1] == 1 && len >= 40) {\n    if (!nodeListEnabled || notifier2Udp.remoteIP() == localIP) return;\n\n    unsigned unit = udpIn[39];\n    NodesMap::iterator it = Nodes.find(unit);\n    if (it == Nodes.end() && Nodes.size() < WLED_MAX_NODES) { // Create a new element when not present\n      Nodes[unit].age = 0;\n      it = Nodes.find(unit);\n    }\n\n    if (it != Nodes.end()) {\n      for (size_t x = 0; x < 4; x++) {\n        it->second.ip[x] = udpIn[x + 2];\n      }\n      it->second.age = 0; // reset 'age counter'\n      char tmpNodeName[33] = { 0 };\n      memcpy(&tmpNodeName[0], reinterpret_cast<byte *>(&udpIn[6]), 32);\n      tmpNodeName[32]     = 0;\n      it->second.nodeName = tmpNodeName;\n      it->second.nodeName.trim();\n      it->second.nodeType = udpIn[38];\n      uint32_t build = 0;\n      if (len >= 44)\n        for (size_t i=0; i<sizeof(uint32_t); i++)\n          build |= udpIn[40+i]<<(8*i);\n      it->second.build = build;\n    }\n    return;\n  }\n\n  //wled notifier, ignore if realtime packets active\n  if (udpIn[0] == 0 && !realtimeMode && receiveGroups)\n  {\n    DEBUG_PRINTF_P(PSTR(\"UDP notification from: %d.%d.%d.%d\\n\"), notifierUdp.remoteIP()[0], notifierUdp.remoteIP()[1], notifierUdp.remoteIP()[2], notifierUdp.remoteIP()[3]);\n    parseNotifyPacket(udpIn);\n    return;\n  }\n\n  if (receiveDirect) {\n    //TPM2.NET\n    if (udpIn[0] == 0x9c) {\n      //WARNING: this code assumes that the final TMP2.NET payload is evenly distributed if using multiple packets (ie. frame size is constant)\n      //if the number of LEDs in your installation doesn't allow that, please include padding bytes at the end of the last packet\n      byte tpmType = udpIn[1];\n      if (tpmType == 0xaa) { //TPM2.NET polling, expect answer\n        sendTPM2Ack(); return;\n      }\n      if (tpmType != 0xda) return; //return if notTPM2.NET data\n\n      realtimeIP = (isSupp) ? notifier2Udp.remoteIP() : notifierUdp.remoteIP();\n      realtimeLock(realtimeTimeoutMs, REALTIME_MODE_TPM2NET);\n      if (realtimeOverride) return;\n\n      tpmPacketCount++; //increment the packet count\n      if (tpmPacketCount == 1) tpmPayloadFrameSize = (udpIn[2] << 8) + udpIn[3]; //save frame size for the whole payload if this is the first packet\n      byte packetNum = udpIn[4]; //starts with 1!\n      byte numPackets = udpIn[5];\n\n      unsigned id = (tpmPayloadFrameSize/3)*(packetNum-1); //start LED\n      unsigned totalLen = strip.getLengthTotal();\n      for (size_t i = 6; i < tpmPayloadFrameSize + 4U && id < totalLen; i += 3, id++) {\n        setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0);\n      }\n      if (tpmPacketCount == numPackets) { //reset packet count and show if all packets were received\n        tpmPacketCount = 0;\n        if (useMainSegmentOnly) strip.trigger();\n        else                    strip.show();\n      }\n      return;\n    }\n\n    //UDP realtime: 1 warls 2 drgb 3 drgbw 4 dnrgb 5 dnrgbw\n    if (udpIn[0] > 0 && udpIn[0] < 6) {\n      realtimeIP = (isSupp) ? notifier2Udp.remoteIP() : notifierUdp.remoteIP();\n      DEBUG_PRINTLN(realtimeIP);\n      if (packetSize < 2) return;\n\n      if (udpIn[1] == 0) {\n        realtimeTimeout = 0; // cancel realtime mode immediately\n        return;\n      } else {\n        realtimeLock(udpIn[1]*1000 +1, REALTIME_MODE_UDP);\n      }\n      if (realtimeOverride) return;\n\n      unsigned totalLen = strip.getLengthTotal();\n      if (udpIn[0] == 1 && packetSize > 5) { //warls\n        for (size_t i = 2; i < packetSize -3; i += 4) {\n          setRealtimePixel(udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3], 0);\n        }\n      } else if (udpIn[0] == 2 && packetSize > 4) { //drgb\n        for (size_t i = 2, id = 0; i < packetSize -2 && id < totalLen; i += 3, id++)\n          {\n            setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0);\n          }\n      } else if (udpIn[0] == 3 && packetSize > 6) { //drgbw\n          for (size_t i = 2, id = 0; i < packetSize -3 && id < totalLen; i += 4, id++) {\n            setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3]);\n          }\n      } else if (udpIn[0] == 4 && packetSize > 7) { //dnrgb\n        unsigned id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00);\n        for (size_t i = 4; i < packetSize -2 && id < totalLen; i += 3, id++) {\n          setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0);\n        }\n      } else if (udpIn[0] == 5 && packetSize > 8) { //dnrgbw\n        unsigned id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00);\n        for (size_t i = 4; i < packetSize -2 && id < totalLen; i += 4, id++) {\n          setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3]);\n        }\n      }\n      if (useMainSegmentOnly) strip.trigger();\n      else                    strip.show();\n      return;\n    }\n  }\n\n  // API over UDP\n  udpIn[packetSize] = '\\0';\n\n  if (requestJSONBufferLock(JSON_LOCK_NOTIFY)) {\n    if (udpIn[0] >= 'A' && udpIn[0] <= 'Z') { //HTTP API\n      String apireq = \"win\"; apireq += '&'; // reduce flash string usage\n      apireq += (char*)udpIn;\n      handleSet(nullptr, apireq);\n    } else if (udpIn[0] == '{') { //JSON API\n      DeserializationError error = deserializeJson(*pDoc, udpIn);\n      JsonObject root = pDoc->as<JsonObject>();\n      if (!error && !root.isNull()) deserializeState(root);\n    }\n    releaseJSONBufferLock();\n  }\n\n  UsermodManager::onUdpPacket(udpIn, packetSize);\n}\n\n\nvoid setRealtimePixel(uint16_t i, byte r, byte g, byte b, byte w)\n{\n  unsigned pix = i + arlsOffset;\n  strip.setRealtimePixelColor(pix, RGBW32(r,g,b,w));\n}\n\n/*********************************************************************************************\\\n   Refresh aging for remote units, drop if too old...\n\\*********************************************************************************************/\nvoid refreshNodeList()\n{\n  for (NodesMap::iterator it = Nodes.begin(); it != Nodes.end();) {\n    bool mustRemove = true;\n\n    if (it->second.ip[0] != 0) {\n      if (it->second.age < 10) {\n        it->second.age++;\n        mustRemove = false;\n        ++it;\n      }\n    }\n\n    if (mustRemove) {\n      it = Nodes.erase(it);\n    }\n  }\n}\n\n/*********************************************************************************************\\\n   Broadcast system info to other nodes. (to update node lists)\n\\*********************************************************************************************/\nvoid sendSysInfoUDP()\n{\n  if (!udp2Connected) return;\n\n  IPAddress ip = Network.localIP();\n  if (!ip || ip == IPAddress(255,255,255,255)) ip = IPAddress(4,3,2,1);\n\n  // TODO: make a nice struct of it and clean up\n  //  0: 1 byte 'binary token 255'\n  //  1: 1 byte id '1'\n  //  2: 4 byte ip\n  //  6: 32 char name\n  // 38: 1 byte node type id\n  // 39: 1 byte node id\n  // 40: 4 byte version ID\n  // 44 bytes total\n\n  // send my info to the world...\n  uint8_t data[44] = {0};\n  data[0] = 255;\n  data[1] = 1;\n\n  for (size_t x = 0; x < 4; x++) {\n    data[x + 2] = ip[x];\n  }\n  memcpy((byte *)data + 6, serverDescription, 32);\n  #ifdef ESP8266\n  data[38] = NODE_TYPE_ID_ESP8266;\n  #elif defined(CONFIG_IDF_TARGET_ESP32C3)\n  data[38] = NODE_TYPE_ID_ESP32C3;\n  #elif defined(CONFIG_IDF_TARGET_ESP32S3)\n  data[38] = NODE_TYPE_ID_ESP32S3;\n  #elif defined(CONFIG_IDF_TARGET_ESP32S2)\n  data[38] = NODE_TYPE_ID_ESP32S2;\n  #elif defined(ARDUINO_ARCH_ESP32)\n  data[38] = NODE_TYPE_ID_ESP32;\n  #else\n  data[38] = NODE_TYPE_ID_UNDEFINED;\n  #endif\n  if (bri) data[38] |= 0x80U;  // add on/off state\n  data[39] = ip[3]; // unit ID == last IP number\n\n  uint32_t build = VERSION;\n  for (size_t i=0; i<sizeof(uint32_t); i++)\n    data[40+i] = (build>>(8*i)) & 0xFF;\n\n  IPAddress broadcastIP(255, 255, 255, 255);\n  notifier2Udp.beginPacket(broadcastIP, udpPort2);\n  notifier2Udp.write(data, sizeof(data));\n  notifier2Udp.endPacket();\n}\n\n\n/*********************************************************************************************\\\n * Art-Net, DDP, E131 output - work in progress\n\\*********************************************************************************************/\n\n#define DDP_HEADER_LEN 10\n#define DDP_SYNCPACKET_LEN 10\n\n#define DDP_FLAGS1_VER 0xc0  // version mask\n#define DDP_FLAGS1_VER1 0x40 // version=1\n#define DDP_FLAGS1_PUSH 0x01\n#define DDP_FLAGS1_QUERY 0x02\n#define DDP_FLAGS1_REPLY 0x04\n#define DDP_FLAGS1_STORAGE 0x08\n#define DDP_FLAGS1_TIME 0x10\n\n#define DDP_ID_DISPLAY 1\n#define DDP_ID_CONFIG 250\n#define DDP_ID_STATUS 251\n\n// 1440 channels per packet\n#define DDP_CHANNELS_PER_PACKET 1440 // 480 leds\n\n//\n// Send real time UDP updates to the specified client\n//\n// type   - protocol type (0=DDP, 1=E1.31, 2=ArtNet)\n// client - the IP address to send to\n// length - the number of pixels\n// buffer - a buffer of at least length*4 bytes long\n// isRGBW - true if the buffer contains 4 components per pixel\n\nstatic       size_t sequenceNumber = 0; // this needs to be shared across all outputs\nstatic const size_t ART_NET_HEADER_SIZE = 12;\nstatic const byte   ART_NET_HEADER[] PROGMEM = {0x41,0x72,0x74,0x2d,0x4e,0x65,0x74,0x00,0x00,0x50,0x00,0x0e};\n\nuint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const uint8_t *buffer, uint8_t bri, bool isRGBW)  {\n  if (!(apActive || interfacesInited) || !client[0] || !length) return 1;  // network not initialised or dummy/unset IP address  031522 ajn added check for ap\n\n  WiFiUDP ddpUdp;\n\n  switch (type) {\n    case 0: // DDP\n    {\n      // calculate the number of UDP packets we need to send\n      size_t channelCount = length * (isRGBW? 4:3); // 1 channel for every R,G,B value\n      size_t packetCount = ((channelCount-1) / DDP_CHANNELS_PER_PACKET) +1;\n\n      // there are 3 channels per RGB pixel\n      uint32_t channel = 0; // TODO: allow specifying the start channel\n      // the current position in the buffer\n      size_t bufferOffset = 0;\n\n      for (size_t currentPacket = 0; currentPacket < packetCount; currentPacket++) {\n        if (sequenceNumber > 15) sequenceNumber = 0;\n\n        if (!ddpUdp.beginPacket(client, DDP_DEFAULT_PORT)) {  // port defined in ESPAsyncE131.h\n          //DEBUG_PRINTLN(F(\"WiFiUDP.beginPacket returned an error\"));\n          return 1; // problem\n        }\n\n        // the amount of data is AFTER the header in the current packet\n        size_t packetSize = DDP_CHANNELS_PER_PACKET;\n\n        uint8_t flags = DDP_FLAGS1_VER1;\n        if (currentPacket == (packetCount - 1U)) {\n          // last packet, set the push flag\n          // TODO: determine if we want to send an empty push packet to each destination after sending the pixel data\n          flags = DDP_FLAGS1_VER1 | DDP_FLAGS1_PUSH;\n          if (channelCount % DDP_CHANNELS_PER_PACKET) {\n            packetSize = channelCount % DDP_CHANNELS_PER_PACKET;\n          }\n        }\n\n        // write the header\n        /*0*/ddpUdp.write(flags);\n        /*1*/ddpUdp.write(sequenceNumber++ & 0x0F); // sequence may be unnecessary unless we are sending twice (as requested in Sync settings)\n        /*2*/ddpUdp.write(isRGBW ?  DDP_TYPE_RGBW32 : DDP_TYPE_RGB24);\n        /*3*/ddpUdp.write(DDP_ID_DISPLAY);\n        // data offset in bytes, 32-bit number, MSB first\n        /*4*/ddpUdp.write(0xFF & (channel >> 24));\n        /*5*/ddpUdp.write(0xFF & (channel >> 16));\n        /*6*/ddpUdp.write(0xFF & (channel >>  8));\n        /*7*/ddpUdp.write(0xFF & (channel      ));\n        // data length in bytes, 16-bit number, MSB first\n        /*8*/ddpUdp.write(0xFF & (packetSize >> 8));\n        /*9*/ddpUdp.write(0xFF & (packetSize     ));\n\n        // write the colors, the write write(const uint8_t *buffer, size_t size)\n        // function is just a loop internally too\n        for (size_t i = 0; i < packetSize; i += (isRGBW?4:3)) {\n          ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // R\n          ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // G\n          ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // B\n          if (isRGBW) ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // W\n        }\n\n        if (!ddpUdp.endPacket()) {\n          //DEBUG_PRINTLN(F(\"WiFiUDP.endPacket returned an error\"));\n          return 1; // problem\n        }\n\n        channel += packetSize;\n      }\n    } break;\n\n    case 1: //E1.31\n    {\n    } break;\n\n    case 2: //ArtNet\n    {\n      // calculate the number of UDP packets we need to send\n      const size_t channelCount = length * (isRGBW?4:3); // 1 channel for every R,G,B,(W?) value\n      const size_t ARTNET_CHANNELS_PER_PACKET = isRGBW?512:510; // 512/4=128 RGBW LEDs, 510/3=170 RGB LEDs\n      const size_t packetCount = ((channelCount-1)/ARTNET_CHANNELS_PER_PACKET)+1;\n\n      uint32_t channel = 0; \n      size_t bufferOffset = 0;\n\n      sequenceNumber++;\n\n      for (size_t currentPacket = 0; currentPacket < packetCount; currentPacket++) {\n\n        if (sequenceNumber > 255) sequenceNumber = 0;\n\n        if (!ddpUdp.beginPacket(client, ARTNET_DEFAULT_PORT)) {\n          DEBUG_PRINTLN(F(\"Art-Net WiFiUDP.beginPacket returned an error\"));\n          return 1; // borked\n        }\n\n        size_t packetSize = ARTNET_CHANNELS_PER_PACKET;\n\n        if (currentPacket == (packetCount - 1U)) {\n          // last packet\n          if (channelCount % ARTNET_CHANNELS_PER_PACKET) {\n            packetSize = channelCount % ARTNET_CHANNELS_PER_PACKET;\n          }\n        }\n\n        byte header_buffer[ART_NET_HEADER_SIZE];\n        memcpy_P(header_buffer, ART_NET_HEADER, ART_NET_HEADER_SIZE);\n        ddpUdp.write(header_buffer, ART_NET_HEADER_SIZE); // This doesn't change. Hard coded ID, OpCode, and protocol version.\n        ddpUdp.write(sequenceNumber & 0xFF); // sequence number. 1..255\n        ddpUdp.write(0x00); // physical - more an FYI, not really used for anything. 0..3\n        ddpUdp.write((currentPacket) & 0xFF); // Universe LSB. 1 full packet == 1 full universe, so just use current packet number.\n        ddpUdp.write(0x00); // Universe MSB, unused.\n        ddpUdp.write(0xFF & (packetSize >> 8)); // 16-bit length of channel data, MSB\n        ddpUdp.write(0xFF & (packetSize     )); // 16-bit length of channel data, LSB\n\n        for (size_t i = 0; i < packetSize; i += (isRGBW?4:3)) {\n          ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // R\n          ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // G\n          ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // B\n          if (isRGBW) ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // W\n        }\n\n        if (!ddpUdp.endPacket()) {\n          DEBUG_PRINTLN(F(\"Art-Net WiFiUDP.endPacket returned an error\"));\n          return 1; // borked\n        }\n        channel += packetSize;\n      }\n    } break;\n  }\n  return 0;\n}\n\n#ifndef WLED_DISABLE_ESPNOW\n// ESP-NOW message sent callback function\nvoid espNowSentCB(uint8_t* address, uint8_t status) {\n    DEBUG_PRINTF_P(PSTR(\"Message sent to \" MACSTR \", status: %d\\n\"), MAC2STR(address), status);\n}\n\n// ESP-NOW message receive callback function\nvoid espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rssi, bool broadcast) {\n  sprintf_P(last_signal_src, PSTR(\"%02x%02x%02x%02x%02x%02x\"), address[0], address[1], address[2], address[3], address[4], address[5]);\n\n  #ifdef WLED_DEBUG\n    DEBUG_PRINT(F(\"ESP-NOW: \")); DEBUG_PRINT(last_signal_src); DEBUG_PRINT(F(\" -> \")); DEBUG_PRINTLN(len);\n    for (int i=0; i<len; i++) DEBUG_PRINTF_P(PSTR(\"%02x \"), data[i]);\n    DEBUG_PRINTLN();\n  #endif\n\n  // usermods hook can override processing\n  if (UsermodManager::onEspNowMessage(address, data, len)) return;\n\n  bool knownRemote = false;\n  for (const auto& mac : linked_remotes) {\n    if (strlen(mac.data()) == 12 && strcmp(last_signal_src, mac.data()) == 0) {\n      knownRemote = true;\n      break;\n    }\n  }\n  if (!knownRemote) {\n    DEBUG_PRINT(F(\"ESP Now Message Received from Unlinked Sender: \"));\n    DEBUG_PRINTLN(last_signal_src);\n    return;\n  }\n\n  // handle WiZ Mote data\n  if (data[0] == 0x91 || data[0] == 0x81 || data[0] == 0x80) {\n    handleWiZdata(data, len);\n    return;\n  }\n\n  partial_packet_t *buffer = reinterpret_cast<partial_packet_t *>(data);\n  if (len < 3 || !broadcast || buffer->magic != 'W' || !useESPNowSync || WLED_CONNECTED) {\n    DEBUG_PRINTLN(F(\"ESP-NOW unexpected packet, not syncing or connected to WiFi.\"));\n    return;\n  }\n\n  static uint8_t *udpIn = nullptr;\n  static uint8_t packetsReceived = 0;\n  static uint8_t segsReceived = 0;\n  static unsigned long lastProcessed = 0;\n\n  if (buffer->packet == 0) {\n    packetsReceived = 0; // it will increment later (this is to make sure we start counting packets correctly)\n    if (udpIn == nullptr) {\n      udpIn = (uint8_t *)malloc(WLEDPACKETSIZE); // we cannot use stack as we are in callback\n      if (!udpIn) return; // memory alocation failed\n      DEBUG_PRINTLN(F(\"ESP-NOW inited UDP buffer.\"));\n    }\n    memcpy(udpIn, buffer->data, len-3); // global data (41 bytes + up to 5 segments)\n    segsReceived = (len - 3 - 41) / UDP_SEG_SIZE;\n  } else if (buffer->packet == packetsReceived && udpIn && ((len - 3) / UDP_SEG_SIZE) * UDP_SEG_SIZE == (len-3)) {\n    // we received a packet full of segments\n    if (segsReceived >= MAX_NUM_SEGMENTS) {\n      // we are already past max segments, just ignore\n      DEBUG_PRINTLN(F(\"ESP-NOW received segments past maximum.\"));\n      len = 3;\n    } else if ((segsReceived + ((len - 3) / UDP_SEG_SIZE)) >= MAX_NUM_SEGMENTS) {\n      len = ((MAX_NUM_SEGMENTS - segsReceived) * UDP_SEG_SIZE) + 3; // we have reached max number of segments\n    }\n    if (len > 3) {\n      memcpy(udpIn + 41 + (segsReceived * UDP_SEG_SIZE), buffer->data, len-3);\n      segsReceived += (len - 3) / UDP_SEG_SIZE;\n    }\n  } else {\n    // any out of order packet or incorrectly sized packet or if we have no UDP buffer will abort\n    DEBUG_PRINTF_P(PSTR(\"ESP-NOW incorrect packet: %d (%d) [%d]\\n\"), (int)buffer->packet, (int)len-3, (int)UDP_SEG_SIZE);\n    if (udpIn) free(udpIn);\n    udpIn = nullptr;\n    packetsReceived = 0;\n    segsReceived = 0;\n    return;\n  }\n  if (!udpIn) return;\n\n  packetsReceived++;\n  DEBUG_PRINTF_P(PSTR(\"ESP-NOW packet received: %d (%d/%d) s:[%d/%d]\\n\"), (int)buffer->packet, (int)packetsReceived, (int)buffer->noOfPackets, (int)segsReceived, MAX_NUM_SEGMENTS);\n  if (packetsReceived >= buffer->noOfPackets) {\n    // last packet received\n    if (millis() - lastProcessed > 250) {\n      DEBUG_PRINTLN(F(\"ESP-NOW processing complete message.\"));\n      parseNotifyPacket(udpIn);\n      lastProcessed = millis();\n    } else {\n      DEBUG_PRINTLN(F(\"ESP-NOW ignoring complete message.\"));\n    }\n    free(udpIn);\n    udpIn = nullptr;\n    packetsReceived = 0;\n    segsReceived = 0;\n  }\n}\n#endif\n"
  },
  {
    "path": "wled00/um_manager.cpp",
    "content": "#include \"wled.h\"\n/*\n * Registration and management utility for v2 usermods\n */\n\n// Global usermod instance list\n// Table begin and end references\n// Zero-length arrays -- so they'll get assigned addresses, but consume no flash\n// The numeric suffix ensures they're put in the right place; the linker script will sort them\n// We stick them in the '.dtors' segment because it's always included by the linker scripts\n// even though it never gets called.  Who calls exit() in an embedded program anyways?\n// If someone ever does, though, it'll explode as these aren't function pointers.\nDECLARE_DYNARRAY(Usermod*, usermods);\n\nstatic size_t getCount() {  \n  return DYNARRAY_LENGTH(usermods);\n}\n\n\n//Usermod Manager internals\nvoid UsermodManager::setup()             { for (auto mod = DYNARRAY_BEGIN(usermods); mod < DYNARRAY_END(usermods); ++mod) (*mod)->setup(); }\nvoid UsermodManager::connected()         { for (auto mod = DYNARRAY_BEGIN(usermods); mod < DYNARRAY_END(usermods); ++mod) (*mod)->connected(); }\nvoid UsermodManager::loop()              { for (auto mod = DYNARRAY_BEGIN(usermods); mod < DYNARRAY_END(usermods); ++mod) (*mod)->loop();  }\nvoid UsermodManager::handleOverlayDraw() { for (auto mod = DYNARRAY_BEGIN(usermods); mod < DYNARRAY_END(usermods); ++mod) (*mod)->handleOverlayDraw(); }\nvoid UsermodManager::appendConfigData(Print& dest)  { for (auto mod = DYNARRAY_BEGIN(usermods); mod < DYNARRAY_END(usermods); ++mod) (*mod)->appendConfigData(dest); }\nbool UsermodManager::handleButton(uint8_t b) {\n  bool overrideIO = false;\n  for (auto mod = DYNARRAY_BEGIN(usermods); mod < DYNARRAY_END(usermods); ++mod) {\n    if ((*mod)->handleButton(b)) overrideIO = true;\n  }\n  return overrideIO;\n}\nbool UsermodManager::getUMData(um_data_t **data, uint8_t mod_id) {\n  for (auto mod = DYNARRAY_BEGIN(usermods); mod < DYNARRAY_END(usermods); ++mod) {\n    if (mod_id > 0 && (*mod)->getId() != mod_id) continue;  // only get data form requested usermod if provided\n    if ((*mod)->getUMData(data)) return true;               // if usermod does provide data return immediately (only one usermod can provide data at one time)\n  }\n  return false;\n}\nvoid UsermodManager::addToJsonState(JsonObject& obj)    { for (auto mod = DYNARRAY_BEGIN(usermods); mod < DYNARRAY_END(usermods); ++mod) (*mod)->addToJsonState(obj); }\nvoid UsermodManager::addToJsonInfo(JsonObject& obj)     {\n  auto um_id_list = obj.createNestedArray(\"um\");  \n  for (auto mod = DYNARRAY_BEGIN(usermods); mod < DYNARRAY_END(usermods); ++mod) {\n    um_id_list.add((*mod)->getId());\n    (*mod)->addToJsonInfo(obj);\n  }\n}\nvoid UsermodManager::readFromJsonState(JsonObject& obj) { for (auto mod = DYNARRAY_BEGIN(usermods); mod < DYNARRAY_END(usermods); ++mod) (*mod)->readFromJsonState(obj); }\nvoid UsermodManager::addToConfig(JsonObject& obj)       { for (auto mod = DYNARRAY_BEGIN(usermods); mod < DYNARRAY_END(usermods); ++mod) (*mod)->addToConfig(obj); }\nbool UsermodManager::readFromConfig(JsonObject& obj)    {\n  bool allComplete = true;  \n  for (auto mod = DYNARRAY_BEGIN(usermods); mod < DYNARRAY_END(usermods); ++mod) {\n    if (!(*mod)->readFromConfig(obj)) allComplete = false;\n  }\n  return allComplete;\n}\n#ifndef WLED_DISABLE_MQTT\nvoid UsermodManager::onMqttConnect(bool sessionPresent) { for (auto mod = DYNARRAY_BEGIN(usermods); mod < DYNARRAY_END(usermods); ++mod) (*mod)->onMqttConnect(sessionPresent); }\nbool UsermodManager::onMqttMessage(char* topic, char* payload) {\n  for (auto mod = DYNARRAY_BEGIN(usermods); mod < DYNARRAY_END(usermods); ++mod) if ((*mod)->onMqttMessage(topic, payload)) return true;\n  return false;\n}\n#endif\n#ifndef WLED_DISABLE_ESPNOW\nbool UsermodManager::onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len) {\n  for (auto mod = DYNARRAY_BEGIN(usermods); mod < DYNARRAY_END(usermods); ++mod) if ((*mod)->onEspNowMessage(sender, payload, len)) return true;\n  return false;\n}\n#endif\nbool UsermodManager::onUdpPacket(uint8_t* payload, size_t len) {\n  for (auto mod = DYNARRAY_BEGIN(usermods); mod < DYNARRAY_END(usermods); ++mod) if ((*mod)->onUdpPacket(payload, len)) return true;\n  return false;\n}\nvoid UsermodManager::onUpdateBegin(bool init) { for (auto mod = DYNARRAY_BEGIN(usermods); mod < DYNARRAY_END(usermods); ++mod) (*mod)->onUpdateBegin(init); } // notify usermods that update is to begin\nvoid UsermodManager::onStateChange(uint8_t mode) { for (auto mod = DYNARRAY_BEGIN(usermods); mod < DYNARRAY_END(usermods); ++mod) (*mod)->onStateChange(mode); } // notify usermods that WLED state changed\n\n/*\n * Enables usermods to lookup another Usermod.\n */\nUsermod* UsermodManager::lookup(uint16_t mod_id) {\n  for (auto mod = DYNARRAY_BEGIN(usermods); mod < DYNARRAY_END(usermods); ++mod) {\n    if ((*mod)->getId() == mod_id) {\n      return *mod;\n    }\n  }\n  return nullptr;\n}\n\nsize_t UsermodManager::getModCount() { return getCount(); };\n\n/* Usermod v2 interface shim for oappend */\nPrint* Usermod::oappend_shim = nullptr;\n\nvoid Usermod::appendConfigData(Print& settingsScript) {\n  assert(!oappend_shim);\n  oappend_shim = &settingsScript;\n  this->appendConfigData();\n  oappend_shim = nullptr;\n}\n"
  },
  {
    "path": "wled00/usermod.cpp",
    "content": "#include \"wled.h\"\n/*\n * This v1 usermod file allows you to add own functionality to WLED more easily\n * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality\n * EEPROM bytes 2750+ are reserved for your custom use case. (if you extend #define EEPSIZE in const.h)\n * If you just need 8 bytes, use 2551-2559 (you do not need to increase EEPSIZE)\n *\n * Consider the v2 usermod API if you need a more advanced feature set!\n */\n\n//Use userVar0 and userVar1 (API calls &U0=,&U1=, uint16_t)\n\n//gets called once at boot. Do all initialization that doesn't depend on network here\nvoid userSetup()\n{\n\n}\n\n//gets called every time WiFi is (re-)connected. Initialize own network interfaces here\nvoid userConnected()\n{\n\n}\n\n//loop. You can use \"if (WLED_CONNECTED)\" to check for successful connection\nvoid userLoop()\n{\n\n}\n"
  },
  {
    "path": "wled00/util.cpp",
    "content": "#include \"wled.h\"\n#include \"fcn_declare.h\"\n#include \"const.h\"\n#ifdef ESP8266\n#include \"user_interface.h\" // for bootloop detection\n#include <Hash.h>            // for SHA1 on ESP8266\n#else\n#include <Update.h>\n#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)\n  #include \"esp32/rtc.h\"    // for bootloop detection\n#elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(3, 3, 0)\n  #include \"soc/rtc.h\"\n#endif\n#include \"mbedtls/sha1.h\"   // for SHA1 on ESP32\n#include \"esp_efuse.h\"\n#endif\n\n\n//helper to get int value at a position in string\nint getNumVal(const String &req, uint16_t pos)\n{\n  return req.substring(pos+3).toInt();\n}\n\n\n//helper to get int value with in/decrementing support via ~ syntax\nvoid parseNumber(const char* str, byte &val, byte minv, byte maxv)\n{\n  if (str == nullptr || str[0] == '\\0') return;\n  if (str[0] == 'r') {val = hw_random8(minv,maxv?maxv:255); return;} // maxv for random cannot be 0\n  bool wrap = false;\n  if (str[0] == 'w' && strlen(str) > 1) {str++; wrap = true;}\n  if (str[0] == '~') {\n    int out = atoi(str +1);\n    if (out == 0) {\n      if (str[1] == '0') return;\n      if (str[1] == '-') {\n        val = (int)(val -1) < (int)minv ? maxv : min((int)maxv,(val -1)); //-1, wrap around\n      } else {\n        val = (int)(val +1) > (int)maxv ? minv : max((int)minv,(val +1)); //+1, wrap around\n      }\n    } else {\n      if (wrap && val == maxv && out > 0) out = minv;\n      else if (wrap && val == minv && out < 0) out = maxv;\n      else {\n        out += val;\n        if (out > maxv) out = maxv;\n        if (out < minv) out = minv;\n      }\n      val = out;\n    }\n    return;\n  } else if (minv == maxv && minv == 0) { // limits \"unset\" i.e. both 0\n    byte p1 = atoi(str);\n    const char* str2 = strchr(str,'~'); // min/max range (for preset cycle, e.g. \"1~5~\")\n    if (str2) {\n      byte p2 = atoi(++str2);           // skip ~\n      if (p2 > 0) {\n        while (isdigit(*(++str2)));     // skip digits\n        parseNumber(str2, val, p1, p2);\n        return;\n      }\n    }\n  }\n  val = atoi(str);\n}\n\n//getVal supports inc/decrementing and random (\"X~Y(r|~[w][-][Z])\" form)\nbool getVal(JsonVariant elem, byte &val, byte vmin, byte vmax) {\n  if (elem.is<int>()) {\n\t\tif (elem < 0) return false; //ignore e.g. {\"ps\":-1}\n    val = elem;\n    return true;\n  } else if (elem.is<const char*>()) {\n    const char* str = elem;\n    size_t len = strnlen(str, 14);\n    if (len == 0 || len > 12) return false;\n    // fix for #3605 & #4346\n    // ignore vmin and vmax and use as specified in API\n    if (len > 3 && (strchr(str,'r') || strchr(str,'~') != strrchr(str,'~'))) vmax = vmin = 0; // we have \"X~Y(r|~[w][-][Z])\" form\n    // end fix\n    parseNumber(str, val, vmin, vmax);\n    return true;\n  }\n  return false; //key does not exist\n}\n\n\nbool getBoolVal(const JsonVariant &elem, bool dflt) {\n  if (elem.is<const char*>() && elem.as<const char*>()[0] == 't') {\n    return !dflt;\n  } else {\n    return elem | dflt;\n  }\n}\n\n\nbool updateVal(const char* req, const char* key, byte &val, byte minv, byte maxv)\n{\n  const char *v = strstr(req, key);\n  if (v) v += strlen(key);\n  else return false;\n  parseNumber(v, val, minv, maxv);\n  return true;\n}\n\nstatic size_t printSetFormInput(Print& settingsScript, const char* key, const char* selector, int value) {\n  return settingsScript.printf_P(PSTR(\"d.Sf.%s.%s=%d;\"), key, selector, value);\n}\n\nsize_t printSetFormCheckbox(Print& settingsScript, const char* key, int val) {\n  return printSetFormInput(settingsScript, key, PSTR(\"checked\"), val);\n}\nsize_t printSetFormValue(Print& settingsScript, const char* key, int val) {\n  return printSetFormInput(settingsScript, key, PSTR(\"value\"), val);\n}\nsize_t printSetFormIndex(Print& settingsScript, const char* key, int index) {\n  return printSetFormInput(settingsScript, key, PSTR(\"selectedIndex\"), index);\n}\n\nsize_t printSetFormValue(Print& settingsScript, const char* key, const char* val) {\n  return settingsScript.printf_P(PSTR(\"d.Sf.%s.value=\\\"%s\\\";\"),key,val);\n}\n\nsize_t printSetClassElementHTML(Print& settingsScript, const char* key, const int index, const char* val) {\n  return settingsScript.printf_P(PSTR(\"d.getElementsByClassName(\\\"%s\\\")[%d].innerHTML=\\\"%s\\\";\"), key, index, val);\n}\n\n\n\nvoid prepareHostname(char* hostname)\n{\n  sprintf_P(hostname, PSTR(\"wled-%*s\"), 6, escapedMac.c_str() + 6);\n  const char *pC = serverDescription;\n  unsigned pos = 5;          // keep \"wled-\"\n  while (*pC && pos < 24) { // while !null and not over length\n    if (isalnum(*pC)) {     // if the current char is alpha-numeric append it to the hostname\n      hostname[pos] = *pC;\n      pos++;\n    } else if (*pC == ' ' || *pC == '_' || *pC == '-' || *pC == '+' || *pC == '!' || *pC == '?' || *pC == '*') {\n      hostname[pos] = '-';\n      pos++;\n    }\n    // else do nothing - no leading hyphens and do not include hyphens for all other characters.\n    pC++;\n  }\n  //last character must not be hyphen\n  if (pos > 5) {\n    while (pos > 4 && hostname[pos -1] == '-') pos--;\n    hostname[pos] = '\\0'; // terminate string (leave at least \"wled\")\n  }\n}\n\n\nbool isAsterisksOnly(const char* str, byte maxLen)\n{\n  for (unsigned i = 0; i < maxLen; i++) {\n    if (str[i] == 0) break;\n    if (str[i] != '*') return false;\n  }\n  //at this point the password contains asterisks only\n  return (str[0] != 0); //false on empty string\n}\n\n\n//threading/network callback details: https://github.com/wled-dev/WLED/pull/2336#discussion_r762276994\nbool requestJSONBufferLock(uint8_t moduleID)\n{\n  if (pDoc == nullptr) {\n    DEBUG_PRINTLN(F(\"ERROR: JSON buffer not allocated!\"));\n    return false;\n  }\n\n#if defined(ARDUINO_ARCH_ESP32)\n  // Use a recursive mutex type in case our task is the one holding the JSON buffer.\n  // This can happen during large JSON web transactions.  In this case, we continue immediately\n  // and then will return out below if the lock is still held.\n  if (xSemaphoreTakeRecursive(jsonBufferLockMutex, 250) == pdFALSE) return false;  // timed out waiting\n#elif defined(ARDUINO_ARCH_ESP8266)\n  // If we're in system context, delay() won't return control to the user context, so there's\n  // no point in waiting.\n  if (can_yield()) {\n    unsigned long now = millis();\n    while (jsonBufferLock && (millis()-now < 250)) delay(1); // wait for fraction for buffer lock\n  }\n#else\n  #error Unsupported task framework - fix requestJSONBufferLock\n#endif  \n  // If the lock is still held - by us, or by another task\n  if (jsonBufferLock) {\n    DEBUG_PRINTF_P(PSTR(\"ERROR: Locking JSON buffer (%d) failed! (still locked by %d)\\n\"), moduleID, jsonBufferLock);\n#ifdef ARDUINO_ARCH_ESP32\n    xSemaphoreGiveRecursive(jsonBufferLockMutex);\n#endif\n    return false;\n  }\n\n  jsonBufferLock = moduleID ? moduleID : 255;\n  DEBUG_PRINTF_P(PSTR(\"JSON buffer locked. (%d)\\n\"), jsonBufferLock);\n  pDoc->clear();\n  return true;\n}\n\n\nvoid releaseJSONBufferLock()\n{\n  DEBUG_PRINTF_P(PSTR(\"JSON buffer released. (%d)\\n\"), jsonBufferLock);\n  jsonBufferLock = 0;\n#ifdef ARDUINO_ARCH_ESP32\n  xSemaphoreGiveRecursive(jsonBufferLockMutex);\n#endif  \n}\n\n\n// extracts effect mode (or palette) name from names serialized string\n// caller must provide large enough buffer for name (including SR extensions)! maxLen is (buffersize - 1)\nuint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLen)\n{\n  if (src == JSON_mode_names || src == nullptr) {\n    if (mode < strip.getModeCount()) {\n      char lineBuffer[256];\n      //strcpy_P(lineBuffer, (const char*)pgm_read_dword(&(WS2812FX::_modeData[mode])));\n      strncpy_P(lineBuffer, strip.getModeData(mode), sizeof(lineBuffer)/sizeof(char)-1);\n      lineBuffer[sizeof(lineBuffer)/sizeof(char)-1] = '\\0'; // terminate string\n      size_t len = strlen(lineBuffer);\n      size_t j = 0;\n      for (; j < maxLen && j < len; j++) {\n        if (lineBuffer[j] == '\\0' || lineBuffer[j] == '@') break;\n        dest[j] = lineBuffer[j];\n      }\n      dest[j] = 0; // terminate string\n      return strlen(dest);\n    } else return 0;\n  }\n\n  if (src == JSON_palette_names && mode > 255-customPalettes.size()) {\n    snprintf_P(dest, maxLen, PSTR(\"~ Custom %d ~\"), 255-mode);\n    dest[maxLen] = '\\0';\n    return strlen(dest);\n  }\n\n  unsigned qComma = 0;\n  bool insideQuotes = false;\n  unsigned printedChars = 0;\n  char singleJsonSymbol;\n  size_t len = strlen_P(src);\n\n  // Find the mode name in JSON\n  for (size_t i = 0; i < len; i++) {\n    singleJsonSymbol = pgm_read_byte_near(src + i);\n    if (singleJsonSymbol == '\\0') break;\n    if (singleJsonSymbol == '@' && insideQuotes && qComma == mode) break; //stop when SR extension encountered\n    switch (singleJsonSymbol) {\n      case '\"':\n        insideQuotes = !insideQuotes;\n        break;\n      case '[':\n      case ']':\n        break;\n      case ',':\n        if (!insideQuotes) qComma++;\n      default:\n        if (!insideQuotes || (qComma != mode)) break;\n        dest[printedChars++] = singleJsonSymbol;\n    }\n    if ((qComma > mode) || (printedChars >= maxLen)) break;\n  }\n  dest[printedChars] = '\\0';\n  return strlen(dest);\n}\n\n\n// extracts effect slider data (1st group after @)\nuint8_t extractModeSlider(uint8_t mode, uint8_t slider, char *dest, uint8_t maxLen, uint8_t *var)\n{\n  dest[0] = '\\0'; // start by clearing buffer\n\n  if (mode < strip.getModeCount()) {\n    String lineBuffer = FPSTR(strip.getModeData(mode));\n    if (lineBuffer.length() > 0) {\n      int start = lineBuffer.indexOf('@');   // String::indexOf() returns an int, not an unsigned; -1 means \"not found\"\n      int stop  = lineBuffer.indexOf(';', start);\n      if (start>0 && stop>0) {\n        String names = lineBuffer.substring(start, stop); // include @\n        int nameBegin = 1, nameEnd, nameDefault;\n        if (slider < 10) {\n          for (size_t i=0; i<=slider; i++) {\n            const char *tmpstr;\n            dest[0] = '\\0'; //clear dest buffer\n            if (nameBegin <= 0) break; // there are no more names\n            nameEnd = names.indexOf(',', nameBegin);\n            if (i == slider) {\n              nameDefault = names.indexOf('=', nameBegin); // find default value\n              if (nameDefault > 0 && var && ((nameEnd>0 && nameDefault<nameEnd) || nameEnd<0)) {\n                *var = (uint8_t)atoi(names.substring(nameDefault+1).c_str());\n              }\n              if (names.charAt(nameBegin) == '!') {\n                switch (slider) {\n                  case  0: tmpstr = PSTR(\"FX Speed\");     break;\n                  case  1: tmpstr = PSTR(\"FX Intensity\"); break;\n                  case  2: tmpstr = PSTR(\"FX Custom 1\");  break;\n                  case  3: tmpstr = PSTR(\"FX Custom 2\");  break;\n                  case  4: tmpstr = PSTR(\"FX Custom 3\");  break;\n                  default: tmpstr = PSTR(\"FX Custom\");    break;\n                }\n                strncpy_P(dest, tmpstr, maxLen); // copy the name into buffer (replacing previous)\n                dest[maxLen-1] = '\\0';\n              } else {\n                if (nameEnd<0) tmpstr = names.substring(nameBegin).c_str(); // did not find \",\", last name?\n                else           tmpstr = names.substring(nameBegin, nameEnd).c_str();\n                strlcpy(dest, tmpstr, maxLen); // copy the name into buffer (replacing previous)\n              }\n            }\n            nameBegin = nameEnd+1; // next name (if \",\" is not found it will be 0)\n          } // next slider\n        } else if (slider == 255) {\n          // palette\n          strlcpy(dest, \"pal\", maxLen);\n          names = lineBuffer.substring(stop+1); // stop has index of color slot names\n          nameBegin = names.indexOf(';'); // look for palette\n          if (nameBegin >= 0) {\n            nameEnd = names.indexOf(';', nameBegin+1);\n            if (!isdigit(names[nameBegin+1])) nameBegin = names.indexOf('=', nameBegin+1); // look for default value\n            if (nameEnd >= 0 && nameBegin > nameEnd) nameBegin = -1;\n            if (nameBegin >= 0 && var) {\n              *var = (uint8_t)atoi(names.substring(nameBegin+1).c_str());\n            }\n          }\n        }\n        // we have slider name (including default value) in the dest buffer\n        for (size_t i=0; i<strlen(dest); i++) if (dest[i]=='=') { dest[i]='\\0'; break; } // truncate default value\n\n      } else {\n        // defaults to just speed and intensity since there is no slider data\n        switch (slider) {\n          case 0:  strncpy_P(dest, PSTR(\"FX Speed\"), maxLen); break;\n          case 1:  strncpy_P(dest, PSTR(\"FX Intensity\"), maxLen); break;\n        }\n        dest[maxLen-1] = '\\0'; // strncpy does not necessarily null terminate string\n      }\n    }\n    return strlen(dest);\n  }\n  return 0;\n}\n\n\n// extracts mode parameter defaults from last section of mode data (e.g. \"Juggle@!,Trail;!,!,;!;012;sx=16,ix=240\")\nint16_t extractModeDefaults(uint8_t mode, const char *segVar)\n{\n  if (mode < strip.getModeCount()) {\n    char lineBuffer[256];\n    strncpy_P(lineBuffer, strip.getModeData(mode), sizeof(lineBuffer)/sizeof(char)-1);\n    lineBuffer[sizeof(lineBuffer)/sizeof(char)-1] = '\\0'; // terminate string\n    if (lineBuffer[0] != 0) {\n      char* startPtr = strrchr(lineBuffer, ';'); // last \";\" in FX data\n      if (!startPtr) return -1;\n\n      char* stopPtr = strstr(startPtr, segVar);\n      if (!stopPtr) return -1;\n\n      stopPtr += strlen(segVar) +1; // skip \"=\"\n      return atoi(stopPtr);\n    }\n  }\n  return -1;\n}\n\n\nvoid checkSettingsPIN(const char* pin) {\n  if (!pin) return;\n  if (!correctPIN && millis() - lastEditTime < PIN_RETRY_COOLDOWN) return; // guard against PIN brute force\n  //bool correctBefore = correctPIN; // unused\n  correctPIN = (strlen(settingsPIN) == 0 || strncmp(settingsPIN, pin, 4) == 0);\n  lastEditTime = millis();\n}\n\n\nuint16_t crc16(const unsigned char* data_p, size_t length) {\n  uint8_t x;\n  uint16_t crc = 0xFFFF;\n  if (!length) return 0x1D0F;\n  while (length--) {\n    x = crc >> 8 ^ *data_p++;\n    x ^= x>>4;\n    crc = (crc << 8) ^ ((uint16_t)(x << 12)) ^ ((uint16_t)(x <<5)) ^ ((uint16_t)x);\n  }\n  return crc;\n}\n\n// fastled beatsin: 1:1 replacements to remove the use of fastled sin16()\n// Generates a 16-bit sine wave at a given BPM that oscillates within a given range. see fastled for details.\nuint16_t beatsin88_t(accum88 beats_per_minute_88, uint16_t lowest, uint16_t highest, uint32_t timebase, uint16_t phase_offset)\n{\n    uint16_t beat = beat88( beats_per_minute_88, timebase);\n    uint16_t beatsin (sin16_t( beat + phase_offset) + 32768);\n    uint16_t rangewidth = highest - lowest;\n    uint16_t scaledbeat = scale16( beatsin, rangewidth);\n    uint16_t result = lowest + scaledbeat;\n    return result;\n}\n\n// Generates a 16-bit sine wave at a given BPM that oscillates within a given range. see fastled for details.\nuint16_t beatsin16_t(accum88 beats_per_minute, uint16_t lowest, uint16_t highest, uint32_t timebase, uint16_t phase_offset)\n{\n    uint16_t beat = beat16( beats_per_minute, timebase);\n    uint16_t beatsin = (sin16_t( beat + phase_offset) + 32768);\n    uint16_t rangewidth = highest - lowest;\n    uint16_t scaledbeat = scale16( beatsin, rangewidth);\n    uint16_t result = lowest + scaledbeat;\n    return result;\n}\n\n// Generates an 8-bit sine wave at a given BPM that oscillates within a given range. see fastled for details.\nuint8_t beatsin8_t(accum88 beats_per_minute, uint8_t lowest, uint8_t highest, uint32_t timebase, uint8_t phase_offset)\n{\n    uint8_t beat = beat8( beats_per_minute, timebase);\n    uint8_t beatsin = sin8_t( beat + phase_offset);\n    uint8_t rangewidth = highest - lowest;\n    uint8_t scaledbeat = scale8( beatsin, rangewidth);\n    uint8_t result = lowest + scaledbeat;\n    return result;\n}\n\n///////////////////////////////////////////////////////////////////////////////\n// Begin simulateSound (to enable audio enhanced effects to display something)\n///////////////////////////////////////////////////////////////////////////////\n// Currently 4 types defined, to be fine tuned and new types added\n// (only 2 used as stored in 1 bit in segment options, consider switching to a single global simulation type)\ntypedef enum UM_SoundSimulations {\n  UMS_BeatSin = 0,\n  UMS_WeWillRockYou,\n  UMS_10_13,\n  UMS_14_3\n} um_soundSimulations_t;\n\num_data_t* simulateSound(uint8_t simulationId)\n{\n  static uint8_t samplePeak;\n  static float   FFT_MajorPeak;\n  static uint8_t maxVol;\n  static uint8_t binNum;\n\n  static float    volumeSmth;\n  static uint16_t volumeRaw;\n  static float    my_magnitude;\n\n  //arrays\n  uint8_t *fftResult;\n\n  static um_data_t* um_data = nullptr;\n\n  if (!um_data) {\n    //claim storage for arrays\n    fftResult = (uint8_t *)malloc(sizeof(uint8_t) * 16);\n\n    // initialize um_data pointer structure\n    // NOTE!!!\n    // This may change as AudioReactive usermod may change\n    um_data = new um_data_t;\n    um_data->u_size = 8;\n    um_data->u_type = new um_types_t[um_data->u_size];\n    um_data->u_data = new void*[um_data->u_size];\n    um_data->u_data[0] = &volumeSmth;\n    um_data->u_data[1] = &volumeRaw;\n    um_data->u_data[2] = fftResult;\n    um_data->u_data[3] = &samplePeak;\n    um_data->u_data[4] = &FFT_MajorPeak;\n    um_data->u_data[5] = &my_magnitude;\n    um_data->u_data[6] = &maxVol;\n    um_data->u_data[7] = &binNum;\n  } else {\n    // get arrays from um_data\n    fftResult =  (uint8_t*)um_data->u_data[2];\n  }\n\n  uint32_t ms = millis();\n\n  switch (simulationId) {\n    default:\n    case UMS_BeatSin:\n      for (int i = 0; i<16; i++)\n        fftResult[i] = beatsin8_t(120 / (i+1), 0, 255);\n        // fftResult[i] = (beatsin8_t(120, 0, 255) + (256/16 * i)) % 256;\n      volumeSmth = fftResult[8];\n      break;\n    case UMS_WeWillRockYou:\n      if (ms%2000 < 200) {\n        volumeSmth = hw_random8();\n        for (int i = 0; i<5; i++)\n          fftResult[i] = hw_random8();\n      }\n      else if (ms%2000 < 400) {\n        volumeSmth = 0;\n        for (int i = 0; i<16; i++)\n          fftResult[i] = 0;\n      }\n      else if (ms%2000 < 600) {\n        volumeSmth = hw_random8();\n        for (int i = 5; i<11; i++)\n          fftResult[i] = hw_random8();\n      }\n      else if (ms%2000 < 800) {\n        volumeSmth = 0;\n        for (int i = 0; i<16; i++)\n          fftResult[i] = 0;\n      }\n      else if (ms%2000 < 1000) {\n        volumeSmth = hw_random8();\n        for (int i = 11; i<16; i++)\n          fftResult[i] = hw_random8();\n      }\n      else {\n        volumeSmth = 0;\n        for (int i = 0; i<16; i++)\n          fftResult[i] = 0;\n      }\n      break;\n    case UMS_10_13:\n      for (int i = 0; i<16; i++)\n        fftResult[i] = perlin8(beatsin8_t(90 / (i+1), 0, 200)*15 + (ms>>10), ms>>3);\n      volumeSmth = fftResult[8];\n      break;\n    case UMS_14_3:\n      for (int i = 0; i<16; i++)\n        fftResult[i] = perlin8(beatsin8_t(120 / (i+1), 10, 30)*10 + (ms>>14), ms>>3);\n      volumeSmth = fftResult[8];\n      break;\n  }\n\n  samplePeak    = hw_random8() > 250;\n  FFT_MajorPeak = 21 + (volumeSmth*volumeSmth) / 8.0f; // walk thru full range of 21hz...8200hz\n  maxVol        = 31;  // this gets feedback fro UI\n  binNum        = 8;   // this gets feedback fro UI\n  volumeRaw = volumeSmth;\n  my_magnitude = 10000.0f / 8.0f; //no idea if 10000 is a good value for FFT_Magnitude ???\n  if (volumeSmth < 1 ) my_magnitude = 0.001f;             // noise gate closed - mute\n\n  return um_data;\n}\n\nstatic const char s_ledmap_tmpl[] PROGMEM = \"ledmap%d.json\";\n// enumerate all ledmapX.json files on FS and extract ledmap names if existing\nvoid enumerateLedmaps() {\n  StaticJsonDocument<64> filter;\n  filter[\"n\"] = true;\n  ledMaps = 1;\n  for (size_t i=1; i<WLED_MAX_LEDMAPS; i++) {\n    char fileName[33] = \"/\";\n    sprintf_P(fileName+1, s_ledmap_tmpl, i);\n    bool isFile = WLED_FS.exists(fileName);\n\n    #ifndef ESP8266\n    if (ledmapNames[i-1]) { //clear old name\n      free(ledmapNames[i-1]);\n      ledmapNames[i-1] = nullptr;\n    }\n    #endif\n\n    if (isFile) {\n      ledMaps |= 1 << i;\n\n      #ifndef ESP8266\n      if (requestJSONBufferLock(JSON_LOCK_LEDMAP_ENUM)) {\n        if (readObjectFromFile(fileName, nullptr, pDoc, &filter)) {\n          size_t len = 0;\n          JsonObject root = pDoc->as<JsonObject>();\n          if (!root[\"n\"].isNull()) {\n            // name field exists\n            const char *name = root[\"n\"].as<const char*>();\n            if (name != nullptr) len = strlen(name);\n            if (len > 0 && len < 33) {\n              ledmapNames[i-1] = static_cast<char*>(malloc(len+1));\n              if (ledmapNames[i-1]) strlcpy(ledmapNames[i-1], name, 33);\n            }\n          }\n          if (!ledmapNames[i-1]) {\n            char tmp[33];\n            snprintf_P(tmp, 32, s_ledmap_tmpl, i);\n            len = strlen(tmp);\n            ledmapNames[i-1] = static_cast<char*>(malloc(len+1));\n            if (ledmapNames[i-1]) strlcpy(ledmapNames[i-1], tmp, 33);\n          }\n        }\n        releaseJSONBufferLock();\n      }\n      #endif\n    }\n\n  }\n}\n\n/*\n * Returns a new, random color wheel index with a minimum distance of 42 from pos.\n */\nuint8_t get_random_wheel_index(uint8_t pos) {\n  uint8_t r = 0, x = 0, y = 0, d = 0;\n  while (d < 42) {\n    r = hw_random8();\n    x = abs(pos - r);\n    y = 255 - x;\n    d = MIN(x, y);\n  }\n  return r;\n}\n\n// float version of map()\nfloat mapf(float x, float in_min, float in_max, float out_min, float out_max) {\n  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;\n}\n\nuint32_t hashInt(uint32_t s) {\n  // borrowed from https://stackoverflow.com/questions/664014/what-integer-hash-function-are-good-that-accepts-an-integer-hash-key\n  s = ((s >> 16) ^ s) * 0x45d9f3b;\n  s = ((s >> 16) ^ s) * 0x45d9f3b;\n  return (s >> 16) ^ s;\n}\n\n// 32 bit random number generator, inlining uses more code, use hw_random16() if speed is critical (see fcn_declare.h)\nuint32_t hw_random(uint32_t upperlimit) {\n  uint32_t rnd = hw_random();\n  uint64_t scaled = uint64_t(rnd) * uint64_t(upperlimit);\n  return scaled >> 32;\n}\n\nint32_t hw_random(int32_t lowerlimit, int32_t upperlimit) {\n  if(lowerlimit >= upperlimit) {\n    return lowerlimit;\n  }\n  uint32_t diff = upperlimit - lowerlimit;\n  return hw_random(diff) + lowerlimit;\n}\n\n// PSRAM compile time checks to provide info for misconfigured env\n#if defined(BOARD_HAS_PSRAM)\n  #if defined(IDF_TARGET_ESP32C3) || defined(ESP8266)\n    #error \"ESP32-C3 and ESP8266 with PSRAM is not supported, please remove BOARD_HAS_PSRAM definition\"\n  #else\n  #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) // PSRAM fix only needed for classic esp32\n    // BOARD_HAS_PSRAM also means that compiler flag \"-mfix-esp32-psram-cache-issue\" has to be used for old \"rev.1\" esp32\n    #warning \"BOARD_HAS_PSRAM defined, make sure to use -mfix-esp32-psram-cache-issue to prevent issues on rev.1 ESP32 boards \\\n              see https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/external-ram.html#esp32-rev-v1-0\"\n  #endif\n  #endif\n#else\n  #if !defined(IDF_TARGET_ESP32C3) && !defined(ESP8266)\n    #pragma message(\"BOARD_HAS_PSRAM not defined, not using PSRAM.\")\n  #endif\n#endif\n\n// memory allocation functions with minimum free heap size check\n#ifdef ESP8266\nstatic void *validateFreeHeap(void *buffer) {\n  // make sure there is enough free heap left if buffer was allocated in DRAM region, free it if not\n  // note: ESP826 needs very little contiguous heap for webserver, checking total free heap works better\n  if (getFreeHeapSize() < MIN_HEAP_SIZE) {\n    free(buffer);\n    return nullptr;\n  }\n  return buffer;\n}\n\nvoid *d_malloc(size_t size) {\n  // note: using \"if (getContiguousFreeHeap() > MIN_HEAP_SIZE + size)\" did perform worse in tests with regards to keeping heap healthy and UI working\n  void *buffer = malloc(size);\n  return validateFreeHeap(buffer);\n}\n\nvoid *d_calloc(size_t count, size_t size) {\n  void *buffer = calloc(count, size);\n  return validateFreeHeap(buffer);\n}\n\n// 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\nvoid *d_realloc_malloc(void *ptr, size_t size) {\n  //void *buffer = realloc(ptr, size);\n  //buffer = validateFreeHeap(buffer);\n  //if (buffer) return buffer; // realloc successful\n  //d_free(ptr); // free old buffer if realloc failed (or min heap was exceeded)\n  //return d_malloc(size); // fallback to malloc\n  free(ptr);\n  return d_malloc(size);\n}\n#else\nstatic void *validateFreeHeap(void *buffer) {\n  // make sure there is enough free heap left if buffer was allocated in DRAM region, free it if not\n  // 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\n  if ((uintptr_t)buffer > SOC_DRAM_LOW && (uintptr_t)buffer < SOC_DRAM_HIGH && getContiguousFreeHeap() < MIN_HEAP_SIZE) {\n    free(buffer);\n    return nullptr;\n  }\n  return buffer;\n}\n\n#ifdef BOARD_HAS_PSRAM\n#define RTC_RAM_THRESHOLD 1024 // use RTC RAM for allocations smaller than this size\n#else\n#define RTC_RAM_THRESHOLD 65535 // without PSRAM, allow any size into RTC RAM (useful especially on S2 without PSRAM)\n#endif\n\nvoid *d_malloc(size_t size) {\n  void *buffer = nullptr;\n  #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3)\n  // the newer ESP32 variants have byte-accessible fast RTC memory that can be used as heap, access speed is on-par with DRAM\n  // 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\n  // use RTC RAM for small allocations or if DRAM is running low to improve fragmentation\n  if (size <= RTC_RAM_THRESHOLD || getContiguousFreeHeap() < 2*MIN_HEAP_SIZE + size)\n    buffer = heap_caps_malloc_prefer(size, 2, MALLOC_CAP_RTCRAM, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);\n  if (buffer == nullptr) // no RTC RAM allocation: use DRAM\n  #endif\n  buffer = heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); // allocate in any available heap memory\n  buffer = validateFreeHeap(buffer); // make sure there is enough free heap left\n  #ifdef BOARD_HAS_PSRAM\n  if (!buffer)\n    return heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); // DRAM failed, use PSRAM if available\n  #endif\n  return buffer;\n}\n\nvoid *d_calloc(size_t count, size_t size) {\n  void *buffer = d_malloc(count * size);\n  if (buffer) memset(buffer, 0, count * size); // clear allocated buffer\n  return buffer;\n}\n\n// realloc with malloc fallback, original buffer is freed if realloc fails but not copied!\nvoid *d_realloc_malloc(void *ptr, size_t size) {\n  void *buffer = heap_caps_realloc(ptr, size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);\n  buffer = validateFreeHeap(buffer);\n  if (buffer) return buffer; // realloc successful\n  d_free(ptr); // free old buffer if realloc failed (or min heap was exceeded)\n  return d_malloc(size); // fallback to malloc\n}\n\n#ifdef BOARD_HAS_PSRAM\n// p_xalloc: prefer PSRAM, use DRAM as fallback\nvoid *p_malloc(size_t size) {\n  void *buffer = heap_caps_malloc_prefer(size, 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);\n  return validateFreeHeap(buffer);\n}\n\nvoid *p_calloc(size_t count, size_t size) {\n  void *buffer = p_malloc(count * size);\n  if (buffer) memset(buffer, 0, count * size); // clear allocated buffer\n  return buffer;\n}\n\n// realloc with malloc fallback, original buffer is freed if realloc fails but not copied!\nvoid *p_realloc_malloc(void *ptr, size_t size) {\n  void *buffer = heap_caps_realloc(ptr, size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);\n  if (buffer) return buffer; // realloc successful\n  p_free(ptr); // free old buffer if realloc failed\n  return p_malloc(size); // fallback to malloc\n}\n#endif\n#endif\n\n// allocation function for buffers like pixel-buffers and segment data\n// optimises the use of memory types to balance speed and heap availability, always favours DRAM if possible\n// if multiple conflicting types are defined, the lowest bits of \"type\" take priority (see fcn_declare.h for types)\nvoid *allocate_buffer(size_t size, uint32_t type) {\n  void *buffer = nullptr;\n  #ifdef CONFIG_IDF_TARGET_ESP32\n  // only classic ESP32 has \"32bit accessible only\" aka IRAM type. Using it frees up normal DRAM for other purposes\n  // this memory region is used for IRAM_ATTR functions, whatever is left is unused and can be used for pixel buffers\n  // 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\n  if (type & BFRALLOC_NOBYTEACCESS) {\n    // prefer 32bit region, then PSRAM, fallback to any heap. Note: if adding \"INTERNAL\"-flag this wont work\n    buffer = heap_caps_malloc_prefer(size, 3, MALLOC_CAP_32BIT, MALLOC_CAP_SPIRAM, MALLOC_CAP_8BIT);\n    buffer = validateFreeHeap(buffer);\n  }\n  else\n  #endif\n  #if !defined(BOARD_HAS_PSRAM)\n  buffer = d_malloc(size);\n  #else\n  if (type & BFRALLOC_PREFER_DRAM) {\n    if (getContiguousFreeHeap() < 3*(MIN_HEAP_SIZE/2) + size && size > PSRAM_THRESHOLD)\n      buffer = p_malloc(size); // prefer PSRAM for large allocations & when DRAM is low\n    else\n      buffer = d_malloc(size); // allocate in DRAM if enough free heap is available, PSRAM as fallback\n  }\n  else if (type & BFRALLOC_ENFORCE_DRAM)\n    buffer = heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); // use DRAM only, otherwise return nullptr\n  else if (type & BFRALLOC_PREFER_PSRAM) {\n    // 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\n    if (getContiguousFreeHeap() > 4*MIN_HEAP_SIZE + size + ((uint32_t)(MAX_SEGMENT_DATA - Segment::getUsedSegmentData())))\n      buffer = d_malloc(size);\n    else\n      buffer = p_malloc(size); // prefer PSRAM\n  }\n  else if (type & BFRALLOC_ENFORCE_PSRAM)\n    buffer = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); // use PSRAM only, otherwise return nullptr\n  buffer = validateFreeHeap(buffer);\n  #endif\n  if (buffer && (type & BFRALLOC_CLEAR))\n    memset(buffer, 0, size); // clear allocated buffer\n  /*\n  #if !defined(ESP8266) && defined(WLED_DEBUG)\n  if (buffer) {\n    DEBUG_PRINTF_P(PSTR(\"*Buffer allocated: size:%d, address:%p\"), size, (uintptr_t)buffer);\n    if ((uintptr_t)buffer > SOC_DRAM_LOW && (uintptr_t)buffer < SOC_DRAM_HIGH)\n      DEBUG_PRINTLN(F(\" in DRAM\"));\n    #ifndef CONFIG_IDF_TARGET_ESP32C3\n    else if ((uintptr_t)buffer > SOC_EXTRAM_DATA_LOW && (uintptr_t)buffer < SOC_EXTRAM_DATA_HIGH)\n      DEBUG_PRINTLN(F(\" in PSRAM\"));\n    #endif\n    #ifdef CONFIG_IDF_TARGET_ESP32\n    else if ((uintptr_t)buffer > SOC_IRAM_LOW && (uintptr_t)buffer < SOC_IRAM_HIGH)\n      DEBUG_PRINTLN(F(\" in IRAM\"));   // only used on ESP32 (MALLOC_CAP_32BIT)\n    #else\n    else if ((uintptr_t)buffer > SOC_RTC_DRAM_LOW && (uintptr_t)buffer < SOC_RTC_DRAM_HIGH)\n      DEBUG_PRINTLN(F(\" in RTCRAM\")); // not available on ESP32\n    #endif\n    else\n      DEBUG_PRINTLN(F(\" in ???\")); // unknown (check soc.h for other memory regions)\n  } else\n    DEBUG_PRINTF_P(PSTR(\"Buffer allocation failed: size:%d\\n\"), size);\n  #endif \n  */\n  return buffer;\n}\n\n// bootloop detection and handling\n// checks if the ESP reboots multiple times due to a crash or watchdog timeout\n// if a bootloop is detected: restore settings from backup, then reset settings, then switch boot image (and repeat)\n\n#define BOOTLOOP_INTERVAL_MILLIS 120000  // time limit between crashes: 120 seconds (2 minutes)\n#define BOOTLOOP_THRESHOLD       5     // number of consecutive crashes to trigger bootloop detection\n#define BOOTLOOP_ACTION_RESTORE  0     // default action: restore config from /bkp.cfg.json\n#define BOOTLOOP_ACTION_RESET    1     // if restore does not work, reset config (rename /cfg.json to /rst.cfg.json)\n#define BOOTLOOP_ACTION_OTA      2     // swap the boot partition\n#define BOOTLOOP_ACTION_DUMP     3     // nothing seems to help, dump files to serial and reboot (until hardware reset)\n\n// Platform-agnostic abstraction\nenum class ResetReason {\n  Power,\n  Software,\n  Crash,\n  Brownout\n};\n\n#ifdef ESP8266\n// Place variables in RTC memory via references, since RTC memory is not exposed via the linker in the Non-OS SDK\n// Use an offset of 32 as there's some hints that the first 128 bytes of \"user\" memory are used by the OTA system\n// Ref: https://github.com/esp8266/Arduino/blob/78d0d0aceacc1553f45ad8154592b0af22d1eede/cores/esp8266/Esp.cpp#L168\nstatic volatile uint32_t& bl_last_boottime = *(RTC_USER_MEM + 32);\nstatic volatile uint32_t& bl_crashcounter = *(RTC_USER_MEM + 33);\nstatic volatile uint32_t& bl_actiontracker = *(RTC_USER_MEM + 34);\n\nstatic inline ResetReason rebootReason() {\n  uint32_t resetReason = system_get_rst_info()->reason;\n  if (resetReason == REASON_EXCEPTION_RST\n      || resetReason == REASON_WDT_RST\n      || resetReason == REASON_SOFT_WDT_RST)\n      return ResetReason::Crash;\n  if (resetReason == REASON_SOFT_RESTART)\n    return ResetReason::Software;\n  return ResetReason::Power;\n}\n\nstatic inline uint32_t getRtcMillis() { return system_get_rtc_time() / 160; };  // rtc ticks ~160000Hz\n\n#else\n// variables in RTC_NOINIT memory persist between reboots (but not on hardware reset)\nRTC_NOINIT_ATTR static uint32_t bl_last_boottime;\nRTC_NOINIT_ATTR static uint32_t bl_crashcounter;\nRTC_NOINIT_ATTR static uint32_t bl_actiontracker;\n\nstatic inline ResetReason rebootReason() {\n  esp_reset_reason_t reason = esp_reset_reason();\n  if (reason == ESP_RST_BROWNOUT) return ResetReason::Brownout;\n  if (reason == ESP_RST_SW) return ResetReason::Software;\n  if (reason == ESP_RST_PANIC || reason == ESP_RST_WDT || reason == ESP_RST_INT_WDT || reason == ESP_RST_TASK_WDT) return ResetReason::Crash;\n  return ResetReason::Power;\n}\n\n#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)\nstatic inline uint32_t getRtcMillis() { return esp_rtc_get_time_us() / 1000; }\n#elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(3, 3, 0)\nstatic inline uint32_t getRtcMillis() { return rtc_time_slowclk_to_us(rtc_time_get(), rtc_clk_slow_freq_get_hz()) / 1000; }\n#endif\n\nvoid bootloopCheckOTA() { bl_actiontracker = BOOTLOOP_ACTION_OTA; } // swap boot image if bootloop is detected instead of restoring config\n\n#endif\n\n// detect bootloop by checking the reset reason and the time since last boot\nstatic bool detectBootLoop() {\n  uint32_t rtctime = getRtcMillis();\n  bool result = false;\n\n  switch(rebootReason()) {\n    case ResetReason::Power:\n      bl_actiontracker = BOOTLOOP_ACTION_RESTORE; // init action tracker if not an intentional reboot (e.g. from OTA or bootloop handler)\n      // fall through\n    case ResetReason::Software:\n      // no crash detected, reset counter\n      bl_crashcounter = 0;\n      break;\n\n    case ResetReason::Crash:\n    {\n      DEBUG_PRINTLN(F(\"crash detected!\"));\n      uint32_t rebootinterval = rtctime - bl_last_boottime;\n      if (rebootinterval < BOOTLOOP_INTERVAL_MILLIS) {\n        bl_crashcounter++;\n        if (bl_crashcounter >= BOOTLOOP_THRESHOLD) {\n          DEBUG_PRINTLN(F(\"!BOOTLOOP DETECTED!\"));\n          bl_crashcounter = 0;  \n          if(bl_actiontracker > BOOTLOOP_ACTION_DUMP) bl_actiontracker = BOOTLOOP_ACTION_RESTORE; // reset action tracker if out of bounds\n          result = true;\n        }\n      } else {\n        // Reset counter on long intervals to track only consecutive short-interval crashes\n        bl_crashcounter = 0;\n        // TODO: crash reporting goes here\n      }\n      break;\n    }\n\n    case ResetReason::Brownout:\n      // crash due to brownout can't be detected unless using flash memory to store bootloop variables\n      DEBUG_PRINTLN(F(\"brownout detected\"));\n      //restoreConfig(); // TODO: blindly restoring config if brownout detected is a bad idea, need a better way (if at all)\n      break;\n  }\n\n  bl_last_boottime = rtctime; // store current runtime for next reboot\n\n  return result;\n}\n\nvoid handleBootLoop() {\n  DEBUG_PRINTF_P(PSTR(\"checking for bootloop: time %d, counter %d, action %d\\n\"), bl_last_boottime, bl_crashcounter, bl_actiontracker);\n  if (!detectBootLoop()) return; // no bootloop detected\n\n  switch(bl_actiontracker) {\n    case BOOTLOOP_ACTION_RESTORE:\n      restoreConfig();\n      ++bl_actiontracker;\n      break;\n    case BOOTLOOP_ACTION_RESET:\n      resetConfig();\n      ++bl_actiontracker;\n      break;\n    case BOOTLOOP_ACTION_OTA:\n#ifndef ESP8266\n      if(Update.canRollBack()) {\n        DEBUG_PRINTLN(F(\"Swapping boot partition...\"));\n        Update.rollBack(); // swap boot partition\n      }\n      ++bl_actiontracker;\n      break;\n#else\n      // fall through\n#endif\n    case BOOTLOOP_ACTION_DUMP:\n      dumpFilesToSerial();\n      break;\n  }\n\n  ESP.restart(); // restart cleanly and don't wait for another crash\n}\n\n/*\n * Fixed point integer based Perlin noise functions by @dedehai\n * Note: optimized for speed and to mimic fastled inoise functions, not for accuracy or best randomness\n */\n#define PERLIN_SHIFT 1\n\n// calculate gradient for corner from hash value\nstatic inline __attribute__((always_inline)) int32_t hashToGradient(uint32_t h) {\n  // 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)\n  // return (h & 0xFF) - 128; // use PERLIN_SHIFT 7\n  // return (h & 0x0F) - 8; // use PERLIN_SHIFT 3\n  // return (h & 0x07) - 4; // use PERLIN_SHIFT 2\n  return (h & 0x03) - 2; // use PERLIN_SHIFT 1 -> closest to original fastled version\n}\n\n// Gradient functions for 1D, 2D and 3D Perlin noise  note: forcing inline produces smaller code and makes it 3x faster!\nstatic inline __attribute__((always_inline)) int32_t gradient1D(uint32_t x0, int32_t dx) {\n  uint32_t h = x0 * 0x27D4EB2D;\n  h ^= h >> 15;\n  h *= 0x92C3412B;\n  h ^= h >> 13;\n  h ^= h >> 7;\n  return (hashToGradient(h) * dx) >> PERLIN_SHIFT;\n}\n\nstatic inline __attribute__((always_inline)) int32_t gradient2D(uint32_t x0, int32_t dx, uint32_t y0, int32_t dy) {\n  uint32_t h = (x0 * 0x27D4EB2D) ^ (y0 * 0xB5297A4D);\n  h ^= h >> 15;\n  h *= 0x92C3412B;\n  h ^= h >> 13;\n  return (hashToGradient(h) * dx + hashToGradient(h>>PERLIN_SHIFT) * dy) >> (1 + PERLIN_SHIFT);\n}\n\nstatic 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) {\n  // fast and good entropy hash from corner coordinates\n  uint32_t h = (x0 * 0x27D4EB2D) ^ (y0 * 0xB5297A4D) ^ (z0 * 0x1B56C4E9);\n  h ^= h >> 15;\n  h *= 0x92C3412B;\n  h ^= h >> 13;\n  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\n}\n\n// fast cubic smoothstep: t*(3 - 2t²), optimized for fixed point, scaled to avoid overflows\nstatic uint32_t smoothstep(const uint32_t t) {\n  uint32_t t_squared = (t * t) >> 16;\n  uint32_t factor = (3 << 16) - ((t << 1));\n  return (t_squared * factor) >> 18; // scale to avoid overflows and give best resolution\n}\n\n// simple linear interpolation for fixed-point values, scaled for perlin noise use\nstatic inline int32_t lerpPerlin(int32_t a, int32_t b, int32_t t) {\n    return a + (((b - a) * t) >> 14); // match scaling with smoothstep to yield 16.16bit values\n}\n\n// 1D Perlin noise function that returns a value in range of -24691 to 24689\nint32_t perlin1D_raw(uint32_t x, bool is16bit) {\n  // integer and fractional part coordinates\n  int32_t x0 = x >> 16;\n  int32_t x1 = x0 + 1;\n  if(is16bit) x1 = x1 & 0xFF; // wrap back to zero at 0xFF instead of 0xFFFF\n\n  int32_t dx0 = x & 0xFFFF;\n  int32_t dx1 = dx0 - 0x10000;\n  // gradient values for the two corners\n  int32_t g0 = gradient1D(x0, dx0);\n  int32_t g1 = gradient1D(x1, dx1);\n  // interpolate and smooth function\n  int32_t tx = smoothstep(dx0);\n  int32_t noise = lerpPerlin(g0, g1, tx);\n  return noise;\n}\n\n// 2D Perlin noise function that returns a value in range of -20633 to 20629\nint32_t perlin2D_raw(uint32_t x, uint32_t y, bool is16bit) {\n  int32_t x0 = x >> 16;\n  int32_t y0 = y >> 16;\n  int32_t x1 = x0 + 1;\n  int32_t y1 = y0 + 1;\n\n  if(is16bit) {\n    x1 = x1 & 0xFF; // wrap back to zero at 0xFF instead of 0xFFFF\n    y1 = y1 & 0xFF;\n  }\n\n  int32_t dx0 = x & 0xFFFF;\n  int32_t dy0 = y & 0xFFFF;\n  int32_t dx1 = dx0 - 0x10000;\n  int32_t dy1 = dy0 - 0x10000;\n\n  int32_t g00 = gradient2D(x0, dx0, y0, dy0);\n  int32_t g10 = gradient2D(x1, dx1, y0, dy0);\n  int32_t g01 = gradient2D(x0, dx0, y1, dy1);\n  int32_t g11 = gradient2D(x1, dx1, y1, dy1);\n\n  uint32_t tx = smoothstep(dx0);\n  uint32_t ty = smoothstep(dy0);\n\n  int32_t nx0 = lerpPerlin(g00, g10, tx);\n  int32_t nx1 = lerpPerlin(g01, g11, tx);\n\n  int32_t noise = lerpPerlin(nx0, nx1, ty);\n  return noise;\n}\n\n// 3D Perlin noise function that returns a value in range of -16788 to 16381\nint32_t perlin3D_raw(uint32_t x, uint32_t y, uint32_t z, bool is16bit) {\n  int32_t x0 = x >> 16;\n  int32_t y0 = y >> 16;\n  int32_t z0 = z >> 16;\n  int32_t x1 = x0 + 1;\n  int32_t y1 = y0 + 1;\n  int32_t z1 = z0 + 1;\n\n  if(is16bit) {\n    x1 = x1 & 0xFF; // wrap back to zero at 0xFF instead of 0xFFFF\n    y1 = y1 & 0xFF;\n    z1 = z1 & 0xFF;\n  }\n\n  int32_t dx0 = x & 0xFFFF;\n  int32_t dy0 = y & 0xFFFF;\n  int32_t dz0 = z & 0xFFFF;\n  int32_t dx1 = dx0 - 0x10000;\n  int32_t dy1 = dy0 - 0x10000;\n  int32_t dz1 = dz0 - 0x10000;\n\n  int32_t g000 = gradient3D(x0, dx0, y0, dy0, z0, dz0);\n  int32_t g001 = gradient3D(x0, dx0, y0, dy0, z1, dz1);\n  int32_t g010 = gradient3D(x0, dx0, y1, dy1, z0, dz0);\n  int32_t g011 = gradient3D(x0, dx0, y1, dy1, z1, dz1);\n  int32_t g100 = gradient3D(x1, dx1, y0, dy0, z0, dz0);\n  int32_t g101 = gradient3D(x1, dx1, y0, dy0, z1, dz1);\n  int32_t g110 = gradient3D(x1, dx1, y1, dy1, z0, dz0);\n  int32_t g111 = gradient3D(x1, dx1, y1, dy1, z1, dz1);\n\n  uint32_t tx = smoothstep(dx0);\n  uint32_t ty = smoothstep(dy0);\n  uint32_t tz = smoothstep(dz0);\n\n  int32_t nx0 = lerpPerlin(g000, g100, tx);\n  int32_t nx1 = lerpPerlin(g010, g110, tx);\n  int32_t nx2 = lerpPerlin(g001, g101, tx);\n  int32_t nx3 = lerpPerlin(g011, g111, tx);\n  int32_t ny0 = lerpPerlin(nx0, nx1, ty);\n  int32_t ny1 = lerpPerlin(nx2, nx3, ty);\n\n  int32_t noise = lerpPerlin(ny0, ny1, tz);\n  return noise;\n}\n\n// scaling functions for fastled replacement\nuint16_t perlin16(uint32_t x) {\n  return ((perlin1D_raw(x) * 1159) >> 10) + 32803; //scale to 16bit and offset (fastled range: about 4838 to 60766)\n}\n\nuint16_t perlin16(uint32_t x, uint32_t y) {\n return ((perlin2D_raw(x, y) * 1537) >> 10) + 32725; //scale to 16bit and offset (fastled range: about 1748 to 63697)\n}\n\nuint16_t perlin16(uint32_t x, uint32_t y, uint32_t z) {\n  return ((perlin3D_raw(x, y, z) * 1731) >> 10) + 33147; //scale to 16bit and offset (fastled range: about 4766 to 60840)\n}\n\nuint8_t perlin8(uint16_t x) {\n  return (((perlin1D_raw((uint32_t)x << 8, true) * 1353) >> 10) + 32769) >> 8; //scale to 16 bit, offset, then scale to 8bit\n}\n\nuint8_t perlin8(uint16_t x, uint16_t y) {\n  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\n}\n\nuint8_t perlin8(uint16_t x, uint16_t y, uint16_t z) {\n  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\n}\n\n// Platform-agnostic SHA1 computation from String input\nString computeSHA1(const String& input) {\n  #ifdef ESP8266\n    return sha1(input); // ESP8266 has built-in sha1() function\n  #else\n    // ESP32: Compute SHA1 hash using mbedtls\n    unsigned char shaResult[20]; // SHA1 produces 20 bytes\n    mbedtls_sha1_context ctx;\n\n    mbedtls_sha1_init(&ctx);\n    mbedtls_sha1_starts_ret(&ctx);\n    mbedtls_sha1_update_ret(&ctx, (const unsigned char*)input.c_str(), input.length());\n    mbedtls_sha1_finish_ret(&ctx, shaResult);\n    mbedtls_sha1_free(&ctx);\n\n    // Convert to hexadecimal string\n    char hexString[41];\n    for (int i = 0; i < 20; i++) {\n      sprintf(&hexString[i*2], \"%02x\", shaResult[i]);\n    }\n    hexString[40] = '\\0';\n\n    return String(hexString);\n  #endif\n}\n\n#ifdef ESP32\n#include \"esp_adc_cal.h\"\nString generateDeviceFingerprint() {\n  uint32_t fp[2] = {0, 0}; // create 64 bit fingerprint\n  esp_chip_info_t chip_info;\n  esp_chip_info(&chip_info);\n  esp_efuse_mac_get_default((uint8_t*)fp);\n  fp[1] ^= ESP.getFlashChipSize();\n  fp[0] ^= chip_info.full_revision | (chip_info.model << 16);\n  // mix in ADC calibration data:\n  esp_adc_cal_characteristics_t ch;\n  #if SOC_ADC_MAX_BITWIDTH == 13 // S2 has 13 bit ADC\n  constexpr auto myBIT_WIDTH = ADC_WIDTH_BIT_13;\n  #else\n  constexpr auto myBIT_WIDTH = ADC_WIDTH_BIT_12;\n  #endif\n  esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_12, myBIT_WIDTH, 1100, &ch);\n  fp[0] ^= ch.coeff_a;\n  fp[1] ^= ch.coeff_b;\n  if (ch.low_curve) {\n    for (int i = 0; i < 8; i++) {\n      fp[0] ^= ch.low_curve[i];\n    }\n  }\n  if (ch.high_curve) {\n    for (int i = 0; i < 8; i++) {\n      fp[1] ^= ch.high_curve[i];\n    }\n  }\n  char fp_string[17];  // 16 hex chars + null terminator\n  sprintf(fp_string, \"%08X%08X\", fp[1], fp[0]);\n  return String(fp_string);\n}\n#else // ESP8266\nString generateDeviceFingerprint() {\n  uint32_t fp[2] = {0, 0}; // create 64 bit fingerprint\n  WiFi.macAddress((uint8_t*)&fp); // use MAC address as fingerprint base\n  fp[0] ^= ESP.getFlashChipId();\n  fp[1] ^= ESP.getFlashChipSize() | ESP.getFlashChipVendorId() << 16;\n  char fp_string[17];  // 16 hex chars + null terminator\n  sprintf(fp_string, \"%08X%08X\", fp[1], fp[0]);\n  return String(fp_string);\n}\n#endif\n\n// Generate a device ID based on SHA1 hash of MAC address salted with other unique device info\n// Returns: original SHA1 + last 2 chars of double-hashed SHA1 (42 chars total)\nString getDeviceId() {\n  static String cachedDeviceId = \"\";\n  if (cachedDeviceId.length() > 0) return cachedDeviceId;\n  // The device string is deterministic as it needs to be consistent for the same device, even after a full flash erase\n  // MAC is salted with other consistent device info to avoid rainbow table attacks.\n  // If the MAC address is known by malicious actors, they could precompute SHA1 hashes to impersonate devices,\n  // but as WLED developers are just looking at statistics and not authenticating devices, this is acceptable.\n  // If the usage data was exfiltrated, you could not easily determine the MAC from the device ID without brute forcing SHA1\n\n  String firstHash = computeSHA1(generateDeviceFingerprint());\n\n  // Second hash: SHA1 of the first hash\n  String secondHash = computeSHA1(firstHash);\n\n  // Concatenate first hash + last 2 chars of second hash\n  cachedDeviceId = firstHash + secondHash.substring(38);\n\n  return cachedDeviceId;\n}\n\n"
  },
  {
    "path": "wled00/wled.cpp",
    "content": "#define WLED_DEFINE_GLOBAL_VARS //only in one source file, wled.cpp!\n#include \"wled.h\"\n#include \"wled_ethernet.h\"\n#include \"ota_update.h\"\n#ifdef WLED_ENABLE_AOTA\n  #define NO_OTA_PORT\n  #include <ArduinoOTA.h>\n#endif\n\n#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DISABLE_BROWNOUT_DET)\n#include \"soc/soc.h\"\n#include \"soc/rtc_cntl_reg.h\"\n#endif\n\nextern \"C\" void usePWMFixedNMI();\n\n/*\n * Main WLED class implementation. Mostly initialization and connection logic\n */\n\nWLED::WLED()\n{\n}\n\n// turns all LEDs off and restarts ESP\nvoid WLED::reset()\n{\n  briT = 0;\n  #ifdef WLED_ENABLE_WEBSOCKETS\n  ws.closeAll(1012);\n  #endif\n  unsigned long dly = millis();\n  while (millis() - dly < 450) {\n    yield();        // enough time to send response to client\n  }\n  applyBri();\n  DEBUG_PRINTLN(F(\"WLED RESET\"));\n  ESP.restart();\n}\n\nvoid WLED::loop()\n{\n  static uint16_t      heapTime = 0;   // timestamp for heap check\n  static uint8_t       heapDanger = 0; // counter for consecutive low-heap readings\n#ifdef WLED_DEBUG\n  static unsigned long lastRun = 0;\n  unsigned long        loopMillis = millis();\n  size_t               loopDelay = loopMillis - lastRun;\n  if (lastRun == 0) loopDelay=0; // startup - don't have valid data from last run.\n  if (loopDelay > 2) DEBUG_PRINTF_P(PSTR(\"Loop delayed more than %ums.\\n\"), loopDelay);\n  static unsigned long maxLoopMillis = 0;\n  static size_t        avgLoopMillis = 0;\n  static unsigned long maxUsermodMillis = 0;\n  static size_t        avgUsermodMillis = 0;\n  static unsigned long maxStripMillis = 0;\n  static size_t        avgStripMillis = 0;\n  unsigned long        stripMillis;\n#endif\n\n  handleTime();\n  #ifndef WLED_DISABLE_INFRARED\n  handleIR();        // 2nd call to function needed for ESP32 to return valid results -- should be good for ESP8266, too\n  #endif\n  handleConnection();\n  #ifdef WLED_ENABLE_ADALIGHT\n  handleSerial();\n  #endif\n  handleImprovWifiScan();\n  handleNotifications();\n  handleTransitions();\n  #ifdef WLED_ENABLE_DMX\n  handleDMXOutput();\n  #endif\n  #ifdef WLED_ENABLE_DMX_INPUT\n  dmxInput.update();\n  #endif\n\n  #ifdef WLED_DEBUG\n  unsigned long usermodMillis = millis();\n  #endif\n  userLoop();\n  UsermodManager::loop();\n  #ifdef WLED_DEBUG\n  usermodMillis = millis() - usermodMillis;\n  avgUsermodMillis += usermodMillis;\n  if (usermodMillis > maxUsermodMillis) maxUsermodMillis = usermodMillis;\n  #endif\n\n  yield();\n  handleIO();\n  #ifndef WLED_DISABLE_INFRARED\n  handleIR();\n  #endif\n  #ifndef WLED_DISABLE_ESPNOW\n  handleRemote();\n  #endif\n  #ifndef WLED_DISABLE_ALEXA\n  handleAlexa();\n  #endif\n\n  if (doCloseFile) {\n    closeFile();\n    yield();\n  }\n\n  #ifdef WLED_DEBUG\n  stripMillis = millis();\n  #endif\n  if (!realtimeMode || realtimeOverride || (realtimeMode && useMainSegmentOnly))  // block stuff if WARLS/Adalight is enabled\n  {\n    if (apActive) dnsServer.processNextRequest();\n    #ifdef WLED_ENABLE_AOTA\n    if (Network.isConnected() && aOtaEnabled && !otaLock && correctPIN) ArduinoOTA.handle();\n    #endif\n    handleNightlight();\n    yield();\n\n    #ifndef WLED_DISABLE_HUESYNC\n    handleHue();\n    yield();\n    #endif\n\n    if (!presetNeedsSaving()) {\n      handlePlaylist();\n      yield();\n    }\n    handlePresets();\n    yield();\n\n    if (!offMode || strip.isOffRefreshRequired() || strip.needsUpdate())\n      strip.service();\n    #ifdef ESP8266\n    else if (!noWifiSleep)\n      delay(1); //required to make sure ESP enters modem sleep (see #1184)\n    #endif\n  }\n  #ifdef WLED_DEBUG\n  stripMillis = millis() - stripMillis;\n  avgStripMillis += stripMillis;\n  if (stripMillis > maxStripMillis) maxStripMillis = stripMillis;\n  #endif\n\n  yield();\n#ifdef ESP8266\n  MDNS.update();\n#endif\n\n  //millis() rolls over every 50 days\n  if (lastMqttReconnectAttempt > millis()) {\n    rolloverMillis++;\n    lastMqttReconnectAttempt = 0;\n    ntpLastSyncTime = NTP_NEVER;  // force new NTP query\n    strip.restartRuntime();\n  }\n  if (millis() - lastMqttReconnectAttempt > 30000 || lastMqttReconnectAttempt == 0) { // lastMqttReconnectAttempt==0 forces immediate broadcast\n    lastMqttReconnectAttempt = millis();\n    #ifndef WLED_DISABLE_MQTT\n    initMqtt();\n    #endif\n    yield();\n    // refresh WLED nodes list\n    refreshNodeList();\n    if (nodeBroadcastEnabled) sendSysInfoUDP();\n    yield();\n  }\n\n  // 15min PIN time-out\n  if (strlen(settingsPIN)>0 && correctPIN && millis() - lastEditTime > PIN_TIMEOUT) {\n    correctPIN = false;\n  }\n\n   // free memory and reconnect WiFi to clear stale allocations if heap is too low for too long, check once every 5s\n  if ((uint16_t)(millis() - heapTime) > 5000) {\n    #ifdef ESP8266\n    uint32_t heap = getFreeHeapSize(); // ESP8266 needs ~8k of free heap for UI to work properly\n    #else\n    #ifdef CONFIG_IDF_TARGET_ESP32C3\n    // calling getContiguousFreeHeap() during led update causes glitches on C3\n    // this can (probably) be removed once RMT driver for C3 is fixed\n    unsigned t0 = millis();\n    while (strip.isUpdating() && (millis() - t0 < 15)) delay(1);    // be nice, but not too nice. Waits up to 15ms\n    #endif\n    uint32_t heap = getContiguousFreeHeap(); // ESP32 family needs ~10k of contiguous free heap for UI to work properly\n    #endif\n    if (heap < MIN_HEAP_SIZE - 1024) heapDanger+=5; // allow 1k of \"wiggle room\" for things that do not respect min heap limits\n    else heapDanger = 0;\n    switch (heapDanger) {\n      case 15: // 15 consecutive seconds\n        DEBUG_PRINTLN(F(\"Heap low, purging segments\"));\n        strip.purgeSegments();\n        strip.setTransition(0); // disable transitions\n        for (unsigned i = 0; i < strip.getSegmentsNum(); i++) {\n          strip.getSegments()[i].setMode(FX_MODE_STATIC); // set static mode to free effect memory\n        }\n        errorFlag = ERR_NORAM; // alert UI  TODO: make this a distinct error: segment reset\n        break;\n      case 30: // 30 consecutive seconds\n        DEBUG_PRINTLN(F(\"Heap low, reset segments\"));\n        strip.resetSegments(); // remove all but one segments from memory\n        errorFlag = ERR_NORAM; // alert UI  TODO: make this a distinct error: segment reset\n        break;\n      case 45: // 45 consecutive seconds\n        DEBUG_PRINTF_P(PSTR(\"Heap panic! Reset strip, reset connection\\n\"));\n        strip.~WS2812FX();      // deallocate strip and all its memory\n        new(&strip) WS2812FX(); // re-create strip object, respecting current memory limits\n        if (!Update.isRunning()) forceReconnect = true; // in case wifi is broken, make sure UI comes back, set disableForceReconnect = true to avert\n        errorFlag = ERR_NORAM; // alert UI  TODO: make this a distinct error: strip reset\n        break;\n      default:\n        break;\n    }\n    heapTime = (uint16_t)millis();\n  }\n\n  //LED settings have been saved, re-init busses\n  //This code block causes severe FPS drop on ESP32 with the original \"if (busConfigs[0] != nullptr)\" conditional. Investigate!\n  if (doInitBusses) {\n    doInitBusses = false;\n    DEBUG_PRINTLN(F(\"Re-init busses.\"));\n    bool aligned = strip.checkSegmentAlignment(); //see if old segments match old bus(ses)\n    strip.finalizeInit(); // will create buses and also load default ledmap if present\n    if (aligned) strip.makeAutoSegments();\n    else strip.fixInvalidSegments();\n    BusManager::setBrightness(scaledBri(bri)); // fix re-initialised bus' brightness #4005 and #4824\n    configNeedsWrite = true;\n  }\n  if (loadLedmap >= 0) {\n    strip.deserializeMap(loadLedmap);\n    loadLedmap = -1;\n  }\n  yield();\n  if (configNeedsWrite) serializeConfigToFS();\n\n  yield();\n  handleWs();\n#if defined(STATUSLED)\n  handleStatusLED();\n#endif\n\n  toki.resetTick();\n\n#if WLED_WATCHDOG_TIMEOUT > 0\n  // we finished our mainloop, reset the watchdog timer\n  static unsigned long lastWDTFeed = 0;\n  if (!strip.isUpdating() || millis() - lastWDTFeed > (WLED_WATCHDOG_TIMEOUT*500)) {\n  #ifdef ARDUINO_ARCH_ESP32\n    esp_task_wdt_reset();\n  #else\n    ESP.wdtFeed();\n  #endif\n    lastWDTFeed = millis();\n  }\n#endif\n\n  if (doReboot && (!doInitBusses || !configNeedsWrite)) // if busses have to be inited & saved, wait until next iteration\n    reset();\n\n// DEBUG serial logging (every 30s)\n#ifdef WLED_DEBUG\n  loopMillis = millis() - loopMillis;\n  //if (loopMillis > 30) {\n  //  DEBUG_PRINTF_P(PSTR(\"Loop took %lums.\\n\"), loopMillis);\n  //  DEBUG_PRINTF_P(PSTR(\"Usermods took %lums.\\n\"), usermodMillis);\n  //  DEBUG_PRINTF_P(PSTR(\"Strip took %lums.\\n\"), stripMillis);\n  //}\n  avgLoopMillis += loopMillis;\n  if (loopMillis > maxLoopMillis) maxLoopMillis = loopMillis;\n  if (millis() - debugTime > 29999) {\n    DEBUG_PRINTLN(F(\"---DEBUG INFO---\"));\n    DEBUG_PRINTF_P(PSTR(\"Runtime: %lu\\n\"),  millis());\n    DEBUG_PRINTF_P(PSTR(\"Unix time: %u,%03u\\n\"), toki.getTime().sec, toki.getTime().ms);\n    #if defined(ARDUINO_ARCH_ESP32)\n    DEBUG_PRINTLN(F(\"=== Memory Info ===\"));\n    // Internal DRAM (standard 8-bit accessible heap)\n    size_t dram_free = heap_caps_get_free_size(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL);\n    size_t dram_largest = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL);\n    DEBUG_PRINTF_P(PSTR(\"DRAM 8-bit:   Free: %7u bytes | Largest block: %7u bytes\\n\"), dram_free, dram_largest);\n    #ifdef BOARD_HAS_PSRAM\n    size_t psram_free = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);\n    size_t psram_largest = heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM);\n    DEBUG_PRINTF_P(PSTR(\"PSRAM:        Free: %7u bytes | Largest block: %6u bytes\\n\"), psram_free, psram_largest);\n    #endif\n    #if defined(CONFIG_IDF_TARGET_ESP32)\n    // 32-bit DRAM (not byte accessible, only available on ESP32)\n    size_t dram32_free = heap_caps_get_free_size(MALLOC_CAP_32BIT | MALLOC_CAP_INTERNAL) - dram_free; // returns all 32bit DRAM, subtract 8bit DRAM\n    //size_t dram32_largest = heap_caps_get_largest_free_block(MALLOC_CAP_32BIT | MALLOC_CAP_INTERNAL); // returns largest DRAM block -> not useful\n    DEBUG_PRINTF_P(PSTR(\"DRAM 32-bit:  Free: %7u bytes | Largest block: N/A\\n\"), dram32_free);\n    #else\n    // Fast RTC Memory (not available on ESP32)\n    size_t rtcram_free = heap_caps_get_free_size(MALLOC_CAP_RTCRAM);\n    size_t rtcram_largest = heap_caps_get_largest_free_block(MALLOC_CAP_RTCRAM);\n    DEBUG_PRINTF_P(PSTR(\"RTC RAM:      Free: %7u bytes | Largest block: %7u bytes\\n\"), rtcram_free, rtcram_largest);\n    #endif\n    if (psramFound()) {\n      DEBUG_PRINTF_P(PSTR(\"PSRAM: %dkB/%dkB\\n\"), ESP.getFreePsram()/1024, ESP.getPsramSize()/1024);\n    #ifndef BOARD_HAS_PSRAM\n      DEBUG_PRINTLN(F(\"BOARD_HAS_PSRAM not defined, not using PSRAM.\"));\n    #endif\n    }\n    DEBUG_PRINTF_P(PSTR(\"TX power: %d/%d\\n\"), WiFi.getTxPower(), txPower);\n    #else // ESP8266\n    DEBUG_PRINTF_P(PSTR(\"Free heap/contiguous: %u/%u\\n\"), getFreeHeapSize(), getContiguousFreeHeap());\n    #endif\n    DEBUG_PRINTF_P(PSTR(\"Wifi state: %d\\n\"), WiFi.status());\n    #ifndef WLED_DISABLE_ESPNOW\n    DEBUG_PRINTF_P(PSTR(\"ESP-NOW state: %u\\n\"), statusESPNow);\n    #endif\n\n    if (WiFi.status() != lastWifiState) {\n      wifiStateChangedTime = millis();\n    }\n    lastWifiState = WiFi.status();\n    DEBUG_PRINTF_P(PSTR(\"State time: %lu\\n\"),        wifiStateChangedTime);\n    DEBUG_PRINTF_P(PSTR(\"NTP last sync: %lu\\n\"),     ntpLastSyncTime);\n    DEBUG_PRINTF_P(PSTR(\"Client IP: %u.%u.%u.%u\\n\"), Network.localIP()[0], Network.localIP()[1], Network.localIP()[2], Network.localIP()[3]);\n    if (loops > 0) { // avoid division by zero\n      DEBUG_PRINTF_P(PSTR(\"Loops/sec: %u\\n\"),         loops / 30);\n      DEBUG_PRINTF_P(PSTR(\"Loop time[ms]: %u/%lu\\n\"), avgLoopMillis/loops,    maxLoopMillis);\n      DEBUG_PRINTF_P(PSTR(\"UM time[ms]: %u/%lu\\n\"),   avgUsermodMillis/loops, maxUsermodMillis);\n      DEBUG_PRINTF_P(PSTR(\"Strip time[ms]:%u/%lu\\n\"), avgStripMillis/loops,   maxStripMillis);\n    }\n    strip.printSize();\n    server.printStatus(DEBUGOUT);\n    loops = 0;\n    maxLoopMillis = 0;\n    maxUsermodMillis = 0;\n    maxStripMillis = 0;\n    avgLoopMillis = 0;\n    avgUsermodMillis = 0;\n    avgStripMillis = 0;\n    debugTime = millis();\n  }\n  loops++;\n  lastRun = millis();\n#endif        // WLED_DEBUG\n}\n\n#if WLED_WATCHDOG_TIMEOUT > 0\nvoid WLED::enableWatchdog() {\n  #ifdef ARDUINO_ARCH_ESP32\n  esp_err_t watchdog = esp_task_wdt_init(WLED_WATCHDOG_TIMEOUT, true);\n  DEBUG_PRINT(F(\"Watchdog enabled: \"));\n  if (watchdog == ESP_OK) {\n    DEBUG_PRINTLN(F(\"OK\"));\n  } else {\n    DEBUG_PRINTLN(watchdog);\n    return;\n  }\n  esp_task_wdt_add(NULL);\n  #else\n  ESP.wdtEnable(WLED_WATCHDOG_TIMEOUT * 1000);\n  #endif\n}\n\nvoid WLED::disableWatchdog() {\n  DEBUG_PRINTLN(F(\"Watchdog: disabled\"));\n  #ifdef ARDUINO_ARCH_ESP32\n  esp_task_wdt_delete(NULL);\n  #else\n  ESP.wdtDisable();\n  #endif\n}\n#endif\n\nvoid WLED::setup()\n{\n  #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DISABLE_BROWNOUT_DET)\n  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detection\n  #endif\n\n  #ifdef ARDUINO_ARCH_ESP32\n  pinMode(hardwareRX, INPUT_PULLDOWN); delay(1);        // suppress noise in case RX pin is floating (at low noise energy) - see issue #3128\n  #endif\n  #ifdef WLED_BOOTUPDELAY\n  delay(WLED_BOOTUPDELAY); // delay to let voltage stabilize, helps with boot issues on some setups\n  #endif\n  Serial.begin(115200);\n  #if !ARDUINO_USB_CDC_ON_BOOT\n  Serial.setTimeout(50);  // this causes troubles on new MCUs that have a \"virtual\" USB Serial (HWCDC)\n  #else\n  #endif\n  #if defined(WLED_DEBUG) && defined(ARDUINO_ARCH_ESP32) && (defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || ARDUINO_USB_CDC_ON_BOOT)\n  delay(2500);  // allow CDC USB serial to initialise\n  #endif\n  #if !defined(WLED_DEBUG) && defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DEBUG_HOST) && ARDUINO_USB_CDC_ON_BOOT\n  Serial.setDebugOutput(false); // switch off kernel messages when using USBCDC\n  #endif\n  DEBUG_PRINTLN();\n  DEBUG_PRINTF_P(PSTR(\"---WLED %s %u INIT---\\n\"), versionString, VERSION);\n  DEBUG_PRINTLN();\n#ifdef ARDUINO_ARCH_ESP32\n  DEBUG_PRINTF_P(PSTR(\"esp32 %s\\n\"), ESP.getSdkVersion());\n  #if defined(ESP_ARDUINO_VERSION)\n    DEBUG_PRINTF_P(PSTR(\"arduino-esp32 v%d.%d.%d\\n\"), int(ESP_ARDUINO_VERSION_MAJOR), int(ESP_ARDUINO_VERSION_MINOR), int(ESP_ARDUINO_VERSION_PATCH));  // available since v2.0.0\n  #else\n    DEBUG_PRINTLN(F(\"arduino-esp32 v1.0.x\\n\"));  // we can't say in more detail.\n  #endif\n  DEBUG_PRINTF_P(PSTR(\"CPU:   %s rev.%d, %d core(s), %d MHz.\\n\"), ESP.getChipModel(), (int)ESP.getChipRevision(), ESP.getChipCores(), ESP.getCpuFreqMHz());\n  DEBUG_PRINTF_P(PSTR(\"FLASH: %d MB, Mode %d \"), (ESP.getFlashChipSize()/1024)/1024, (int)ESP.getFlashChipMode());\n  #ifdef WLED_DEBUG\n  switch (ESP.getFlashChipMode()) {\n    // missing: Octal modes\n    case FM_QIO:  DEBUG_PRINT(F(\"(QIO)\")); break;\n    case FM_QOUT: DEBUG_PRINT(F(\"(QOUT)\"));break;\n    case FM_DIO:  DEBUG_PRINT(F(\"(DIO)\")); break;\n    case FM_DOUT: DEBUG_PRINT(F(\"(DOUT)\"));break;\n    #if defined(CONFIG_IDF_TARGET_ESP32S3) && CONFIG_ESPTOOLPY_FLASHMODE_OPI\n    case FM_FAST_READ: DEBUG_PRINT(F(\"(OPI)\")); break;\n    #else\n    case FM_FAST_READ: DEBUG_PRINT(F(\"(fast_read)\")); break;\n    #endif\n    case FM_SLOW_READ: DEBUG_PRINT(F(\"(slow_read)\")); break;\n    default: break;\n  }\n  #endif\n  DEBUG_PRINTF_P(PSTR(\", speed %u MHz.\\n\"), ESP.getFlashChipSpeed()/1000000);\n\n#else\n  DEBUG_PRINTF_P(PSTR(\"esp8266 @ %u MHz.\\nCore: %s\\n\"), ESP.getCpuFreqMHz(), ESP.getCoreVersion());\n  DEBUG_PRINTF_P(PSTR(\"FLASH: %u MB\\n\"), (ESP.getFlashChipSize()/1024)/1024);\n#endif\n  DEBUG_PRINTF_P(PSTR(\"heap %u\\n\"), getFreeHeapSize());\n\n#if defined(BOARD_HAS_PSRAM)\n  // if JSON buffer allocation fails requestJsonBufferLock() will always return false preventing crashes\n  if (psramFound() && ESP.getPsramSize()) {\n    pDoc = new PSRAMDynamicJsonDocument(2 * JSON_BUFFER_SIZE);\n    DEBUG_PRINTF_P(PSTR(\"JSON buffer size: %ubytes\\n\"), (2 * JSON_BUFFER_SIZE));\n    DEBUG_PRINTF_P(PSTR(\"PSRAM: %dkB/%dkB\\n\"), ESP.getFreePsram()/1024, ESP.getPsramSize()/1024);\n  } else {\n    pDoc = new DynamicJsonDocument(JSON_BUFFER_SIZE);  // Use onboard RAM instead as a fallback\n  }\n#endif\n\n#if defined(ARDUINO_ARCH_ESP32)\n  DEBUG_PRINTF_P(PSTR(\"TX power: %d/%d\\n\"), WiFi.getTxPower(), txPower);\n#endif\n\n#ifdef ESP8266\n  usePWMFixedNMI(); // link the NMI fix\n#endif\n\n#if defined(WLED_DEBUG) && !defined(WLED_DEBUG_HOST)\n  PinManager::allocatePin(hardwareTX, true, PinOwner::DebugOut); // TX (GPIO1 on ESP32) reserved for debug output\n#endif\n#ifdef WLED_ENABLE_DMX //reserve GPIO2 as hardcoded DMX pin\n  PinManager::allocatePin(2, true, PinOwner::DMX);\n#endif\n\n  DEBUG_PRINTF_P(PSTR(\"heap %u\\n\"), getFreeHeapSize());\n\n  bool fsinit = false;\n  DEBUGFS_PRINTLN(F(\"Mount FS\"));\n#ifdef ARDUINO_ARCH_ESP32\n  fsinit = WLED_FS.begin(true);\n#else\n  fsinit = WLED_FS.begin();\n#endif\n  if (!fsinit) {\n    DEBUGFS_PRINTLN(F(\"FS failed!\"));\n    errorFlag = ERR_FS_BEGIN;\n  }\n\n  handleBootLoop(); // check for bootloop and take action (requires WLED_FS)\n  initPresetsFile();\n  updateFSInfo();\n\n  // generate module IDs must be done before AP setup\n  escapedMac = WiFi.macAddress();\n  escapedMac.replace(\":\", \"\");\n  escapedMac.toLowerCase();\n\n  WLED_SET_AP_SSID(); // otherwise it is empty on first boot until config is saved\n  multiWiFi.push_back(WiFiConfig(CLIENT_SSID,CLIENT_PASS)); // initialise vector with default WiFi\n\n  if(!verifyConfig()) {\n    if(!restoreConfig()) {\n      resetConfig();\n    }\n  }\n  DEBUG_PRINTLN(F(\"Reading config\"));\n  bool needsCfgSave = deserializeConfigFromFS();\n  DEBUG_PRINTF_P(PSTR(\"heap %u\\n\"), getFreeHeapSize());\n\n#if defined(STATUSLED) && STATUSLED>=0\n  if (!PinManager::isPinAllocated(STATUSLED)) {\n    // NOTE: Special case: The status LED should *NOT* be allocated.\n    //       See comments in handleStatusLed().\n    pinMode(STATUSLED, OUTPUT);\n  }\n#endif\n\n  DEBUG_PRINTLN(F(\"Initializing strip\"));\n  beginStrip();\n  DEBUG_PRINTF_P(PSTR(\"heap %u\\n\"), getFreeHeapSize());\n\n  DEBUG_PRINTLN(F(\"Usermods setup\"));\n  userSetup();\n  UsermodManager::setup();\n  DEBUG_PRINTF_P(PSTR(\"heap %u\\n\"), getFreeHeapSize());\n\n  if (needsCfgSave) serializeConfigToFS(); // usermods required new parameters; need to wait for strip to be initialised #4752\n\n  if (strcmp(multiWiFi[0].clientSSID, DEFAULT_CLIENT_SSID) == 0 && !configBackupExists())\n    showWelcomePage = true;\n\n  #ifndef ESP8266\n  WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN);\n  WiFi.persistent(true); // storing credentials in NVM fixes boot-up pause as connection is much faster, is disabled after first connection\n  #else\n  WiFi.persistent(false); // on ES8266 using NVM for wifi config has no benefit of faster connection\n  #endif\n  WiFi.onEvent(WiFiEvent);\n  WiFi.mode(WIFI_STA); // enable scanning\n  findWiFi(true);      // start scanning for available WiFi-s\n\n  // all GPIOs are allocated at this point\n  serialCanRX = !PinManager::isPinAllocated(hardwareRX); // Serial RX pin (GPIO 3 on ESP32 and ESP8266)\n  serialCanTX = !PinManager::isPinAllocated(hardwareTX) || PinManager::getPinOwner(hardwareTX) == PinOwner::DebugOut; // Serial TX pin (GPIO 1 on ESP32 and ESP8266)\n\n  #ifdef WLED_ENABLE_ADALIGHT\n  //Serial RX (Adalight, Improv, Serial JSON) only possible if GPIO3 unused\n  //Serial TX (Debug, Improv, Serial JSON) only possible if GPIO1 unused\n  if (serialCanRX && serialCanTX) {\n    Serial.println(F(\"Ada\"));\n  }\n  #endif\n\n  // fill in unique mdns default\n  if (strcmp(cmDNS, DEFAULT_MDNS_NAME) == 0) sprintf_P(cmDNS, PSTR(\"wled-%*s\"), 6, escapedMac.c_str() + 6);\n#ifndef WLED_DISABLE_MQTT\n  if (mqttDeviceTopic[0] == 0) sprintf_P(mqttDeviceTopic, PSTR(\"wled/%*s\"), 6, escapedMac.c_str() + 6);\n  if (mqttClientID[0] == 0)    sprintf_P(mqttClientID, PSTR(\"WLED-%*s\"), 6, escapedMac.c_str() + 6);\n#endif\n\n#ifdef WLED_ENABLE_AOTA\n  if (aOtaEnabled) {\n    ArduinoOTA.onStart([]() {\n      #ifdef ESP8266\n      wifi_set_sleep_type(NONE_SLEEP_T);\n      #endif\n      #if WLED_WATCHDOG_TIMEOUT > 0\n      WLED::instance().disableWatchdog();\n      #endif\n      DEBUG_PRINTLN(F(\"Start ArduinoOTA\"));\n    });\n    ArduinoOTA.onError([](ota_error_t error) {\n      #if WLED_WATCHDOG_TIMEOUT > 0\n      // reenable watchdog on failed update\n      WLED::instance().enableWatchdog();\n      #endif\n    });\n    if (strlen(cmDNS) > 0)\n      ArduinoOTA.setHostname(cmDNS);\n  }\n#endif\n#ifdef WLED_ENABLE_DMX\n  initDMXOutput();\n#endif\n#ifdef WLED_ENABLE_DMX_INPUT\n  dmxInput.init(dmxInputReceivePin, dmxInputTransmitPin, dmxInputEnablePin, dmxInputPort);\n#endif\n\n#ifdef WLED_ENABLE_ADALIGHT\n  if (serialCanRX && Serial.available() > 0 && Serial.peek() == 'I') handleImprovPacket();\n#endif\n\n  // HTTP server page init\n  DEBUG_PRINTLN(F(\"initServer\"));\n  initServer();\n  DEBUG_PRINTF_P(PSTR(\"heap %u\\n\"), getFreeHeapSize());\n\n#ifndef WLED_DISABLE_INFRARED\n  // init IR\n  DEBUG_PRINTLN(F(\"initIR\"));\n  initIR();\n  DEBUG_PRINTF_P(PSTR(\"heap %u\\n\"), getFreeHeapSize());\n#endif\n\n  // Seed FastLED random functions with an esp random value, which already works properly at this point.\n  const uint32_t seed32 = hw_random();\n  random16_set_seed((uint16_t)seed32);\n\n  #if WLED_WATCHDOG_TIMEOUT > 0\n  enableWatchdog();\n  #endif\n\n  #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DISABLE_BROWNOUT_DET)\n  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 1); //enable brownout detector\n  #endif\n  markOTAvalid();\n}\n\nvoid WLED::beginStrip()\n{\n  // Initialize NeoPixel Strip and button\n  strip.setTransition(0); // temporarily prevent transitions to reduce segment copies\n  strip.finalizeInit(); // busses created during deserializeConfig() if config existed\n  strip.makeAutoSegments();\n  strip.setBrightness(0);\n  strip.setShowCallback(handleOverlayDraw);\n  doInitBusses = false;\n\n  // init offMode and relay\n  offMode = false;   // init to on state to allow proper relay init\n  handleOnOff(true); // init relay and force off\n\n  if (turnOnAtBoot) {\n    if (briS > 0) bri = briS;\n    else if (bri == 0) bri = 128;\n  } else {\n    // fix for #3196\n    if (bootPreset > 0) {\n      // set all segments black (no transition)\n      for (unsigned i = 0; i < strip.getSegmentsNum(); i++) {\n        Segment &seg = strip.getSegment(i);\n        if (seg.isActive()) seg.colors[0] = BLACK;\n      }\n      colPri[0] = colPri[1] = colPri[2] = colPri[3] = 0;  // needed for colorUpdated()\n    }\n    briLast = briS; bri = 0;\n    strip.fill(BLACK);\n    if (rlyPin < 0)\n      strip.show(); // ensure LEDs are off if no relay is used\n  }\n  colorUpdated(CALL_MODE_INIT); // will not send notification but will initiate transition\n  if (bootPreset > 0) {\n    applyPreset(bootPreset, CALL_MODE_INIT);\n  }\n\n  strip.setTransition(transitionDelayDefault);  // restore transitions\n}\n\nvoid WLED::initAP(bool resetAP)\n{\n  if (apBehavior == AP_BEHAVIOR_BUTTON_ONLY && !resetAP)\n    return;\n\n  if (resetAP) {\n    WLED_SET_AP_SSID();\n    strcpy_P(apPass, PSTR(WLED_AP_PASS));\n  }\n  DEBUG_PRINT(F(\"Opening access point \"));\n  DEBUG_PRINTLN(apSSID);\n  WiFi.softAPConfig(IPAddress(4, 3, 2, 1), IPAddress(4, 3, 2, 1), IPAddress(255, 255, 255, 0));\n  WiFi.softAP(apSSID, apPass, apChannel, apHide);\n  #ifdef ARDUINO_ARCH_ESP32\n  WiFi.setTxPower(wifi_power_t(txPower));\n  #endif\n\n  if (!apActive) // start captive portal if AP active\n  {\n    DEBUG_PRINTLN(F(\"Init AP interfaces\"));\n    server.begin();\n    if (udpPort > 0 && udpPort != ntpLocalPort) {\n      udpConnected = notifierUdp.begin(udpPort);\n    }\n    if (udpRgbPort > 0 && udpRgbPort != ntpLocalPort && udpRgbPort != udpPort) {\n      udpRgbConnected = rgbUdp.begin(udpRgbPort);\n    }\n    if (udpPort2 > 0 && udpPort2 != ntpLocalPort && udpPort2 != udpPort && udpPort2 != udpRgbPort) {\n      udp2Connected = notifier2Udp.begin(udpPort2);\n    }\n    e131.begin(false, e131Port, e131Universe, E131_MAX_UNIVERSE_COUNT);\n    ddp.begin(false, DDP_DEFAULT_PORT);\n\n    dnsServer.setErrorReplyCode(DNSReplyCode::NoError);\n    dnsServer.start(53, \"*\", WiFi.softAPIP());\n  }\n  apActive = true;\n}\n\nvoid WLED::initConnection()\n{\n  DEBUG_PRINTF_P(PSTR(\"initConnection() called @ %lus.\\n\"), millis()/1000);\n  #ifdef WLED_ENABLE_WEBSOCKETS\n  ws.onEvent(wsEvent);\n  #endif\n\n#ifndef WLED_DISABLE_ESPNOW\n  if (statusESPNow == ESP_NOW_STATE_ON) {\n    DEBUG_PRINTLN(F(\"ESP-NOW stopping.\"));\n    quickEspNow.stop();\n    statusESPNow = ESP_NOW_STATE_UNINIT;\n  }\n#endif\n\n  WiFi.disconnect(true); // close old connections\n  delay(5);              // wait for hardware to be ready\n#ifdef ESP8266\n  WiFi.setPhyMode(force802_3g ? WIFI_PHY_MODE_11G : WIFI_PHY_MODE_11N);\n#endif\n\n  if (multiWiFi[selectedWiFi].staticIP != 0U && multiWiFi[selectedWiFi].staticGW != 0U) {\n    WiFi.config(multiWiFi[selectedWiFi].staticIP, multiWiFi[selectedWiFi].staticGW, multiWiFi[selectedWiFi].staticSN, dnsAddress);\n  } else {\n    WiFi.config(IPAddress((uint32_t)0), IPAddress((uint32_t)0), IPAddress((uint32_t)0));\n  }\n\n  lastReconnectAttempt = millis();\n\n  if (!WLED_WIFI_CONFIGURED) {\n    DEBUG_PRINTLN(F(\"No connection configured.\"));\n    if (!apActive) initAP();        // instantly go to ap mode\n  } else if (!apActive) {\n    if (apBehavior == AP_BEHAVIOR_ALWAYS) {\n      DEBUG_PRINTLN(F(\"Access point ALWAYS enabled.\"));\n      initAP();\n    } else {\n      DEBUG_PRINTLN(F(\"Access point disabled (init).\"));\n      WiFi.softAPdisconnect(true);\n      WiFi.mode(WIFI_STA);\n    }\n  }\n\n  if (WLED_WIFI_CONFIGURED) {\n    showWelcomePage = false;\n    \n    DEBUG_PRINTF_P(PSTR(\"Connecting to %s...\\n\"), multiWiFi[selectedWiFi].clientSSID);\n\n    // convert the \"serverDescription\" into a valid DNS hostname (alphanumeric)\n    char hostname[25];\n    prepareHostname(hostname);\n\n#ifdef WLED_ENABLE_WPA_ENTERPRISE\n    if (multiWiFi[selectedWiFi].encryptionType == WIFI_ENCRYPTION_TYPE_PSK) {\n      DEBUG_PRINTLN(F(\"Using PSK\"));\n#ifdef ESP8266\n      wifi_station_set_wpa2_enterprise_auth(0);\n      wifi_station_clear_enterprise_ca_cert();\n      wifi_station_clear_enterprise_cert_key();\n      wifi_station_clear_enterprise_identity();\n      wifi_station_clear_enterprise_username();\n      wifi_station_clear_enterprise_password();\n#endif\n      uint8_t *bssid = nullptr;\n      // check if user BSSID is non zero for current WiFi config\n      for (int i = 0; i < sizeof(multiWiFi[selectedWiFi].bssid); i++) {\n        if (multiWiFi[selectedWiFi].bssid[i] != 0) {\n          bssid = multiWiFi[selectedWiFi].bssid; // BSSID set, assign pointer and continue\n          break;\n        }\n      }\n      WiFi.begin(multiWiFi[selectedWiFi].clientSSID, multiWiFi[selectedWiFi].clientPass, 0, bssid); // no harm if called multiple times\n    } else { // WIFI_ENCRYPTION_TYPE_ENTERPRISE\n      DEBUG_PRINTF_P(PSTR(\"Using WPA2_AUTH_PEAP (Anon: %s, Ident: %s)\\n\"), multiWiFi[selectedWiFi].enterpriseAnonIdentity, multiWiFi[selectedWiFi].enterpriseIdentity);\n#ifdef ESP8266\n      struct station_config sta_conf;\n      os_memset(&sta_conf, 0, sizeof(sta_conf));\n      os_memcpy(sta_conf.ssid, multiWiFi[selectedWiFi].clientSSID, 32);\n      os_memcpy(sta_conf.password, multiWiFi[selectedWiFi].clientPass, 64);\n      wifi_station_set_config(&sta_conf);\n      wifi_station_set_wpa2_enterprise_auth(1);\n      wifi_station_set_enterprise_identity((u8*)(void*)multiWiFi[selectedWiFi].enterpriseAnonIdentity, os_strlen(multiWiFi[selectedWiFi].enterpriseAnonIdentity));\n      wifi_station_set_enterprise_username((u8*)(void*)multiWiFi[selectedWiFi].enterpriseIdentity, os_strlen(multiWiFi[selectedWiFi].enterpriseIdentity));\n      wifi_station_set_enterprise_password((u8*)(void*)multiWiFi[selectedWiFi].clientPass, os_strlen(multiWiFi[selectedWiFi].clientPass));\n      wifi_station_connect();\n#else\n      WiFi.begin(multiWiFi[selectedWiFi].clientSSID, WPA2_AUTH_PEAP, multiWiFi[selectedWiFi].enterpriseAnonIdentity, multiWiFi[selectedWiFi].enterpriseIdentity, multiWiFi[selectedWiFi].clientPass);\n#endif\n    }\n#else // WLED_ENABLE_WPA_ENTERPRISE\n    uint8_t *bssid = nullptr;\n    // check if user BSSID is non zero for current WiFi config\n    for (int i = 0; i < sizeof(multiWiFi[selectedWiFi].bssid); i++) {\n      if (multiWiFi[selectedWiFi].bssid[i] != 0) {\n        bssid = multiWiFi[selectedWiFi].bssid; // BSSID set, assign pointer and continue\n        break;\n      }\n    }\n    WiFi.begin(multiWiFi[selectedWiFi].clientSSID, multiWiFi[selectedWiFi].clientPass, 0, bssid); // no harm if called multiple times\n#endif // WLED_ENABLE_WPA_ENTERPRISE\n\n#ifdef ARDUINO_ARCH_ESP32\n    WiFi.setTxPower(wifi_power_t(txPower));\n    WiFi.setSleep(!noWifiSleep);\n    WiFi.setHostname(hostname);\n#else\n    wifi_set_sleep_type((noWifiSleep) ? NONE_SLEEP_T : MODEM_SLEEP_T);\n    WiFi.hostname(hostname);\n#endif\n  }\n\n#ifndef WLED_DISABLE_ESPNOW\n  if (enableESPNow) {\n    quickEspNow.onDataSent(espNowSentCB);     // see udp.cpp\n    quickEspNow.onDataRcvd(espNowReceiveCB);  // see udp.cpp\n    bool espNowOK;\n    if (apActive) {\n      DEBUG_PRINTLN(F(\"ESP-NOW initing in AP mode.\"));\n      #ifdef ESP32\n      quickEspNow.setWiFiBandwidth(WIFI_IF_AP, WIFI_BW_HT20); // Only needed for ESP32 in case you need coexistence with ESP8266 in the same network\n      #endif //ESP32\n      espNowOK = quickEspNow.begin(apChannel, WIFI_IF_AP);  // Same channel must be used for both AP and ESP-NOW\n    } else {\n      DEBUG_PRINTLN(F(\"ESP-NOW initing in STA mode.\"));\n      espNowOK = quickEspNow.begin(); // Use no parameters to start ESP-NOW on same channel as WiFi, in STA mode\n    }\n    statusESPNow = espNowOK ? ESP_NOW_STATE_ON : ESP_NOW_STATE_ERROR;\n  }\n#endif\n}\n\nvoid WLED::initInterfaces()\n{\n  DEBUG_PRINTLN(F(\"Init STA interfaces\"));\n\n#ifndef WLED_DISABLE_HUESYNC\n  IPAddress ipAddress = Network.localIP();\n  if (hueIP[0] == 0) {\n    hueIP[0] = ipAddress[0];\n    hueIP[1] = ipAddress[1];\n    hueIP[2] = ipAddress[2];\n  }\n#endif\n\n#ifndef WLED_DISABLE_ALEXA\n  // init Alexa hue emulation\n  if (alexaEnabled)\n    alexaInit();\n#endif\n\n#ifdef WLED_ENABLE_AOTA\n  if (aOtaEnabled) ArduinoOTA.begin();\n#endif\n\n  // Set up mDNS responder:\n  if (strlen(cmDNS) > 0) {\n    // \"end\" must be called before \"begin\" is called a 2nd time\n    // see https://github.com/esp8266/Arduino/issues/7213\n    MDNS.end();\n    MDNS.begin(cmDNS);\n\n    DEBUG_PRINTLN(F(\"mDNS started\"));\n    MDNS.addService(\"http\", \"tcp\", 80);\n    MDNS.addService(\"wled\", \"tcp\", 80);\n    MDNS.addServiceTxt(\"wled\", \"tcp\", \"mac\", escapedMac.c_str());\n  }\n  server.begin();\n\n  if (udpPort > 0 && udpPort != ntpLocalPort) {\n    udpConnected = notifierUdp.begin(udpPort);\n    if (udpConnected && udpRgbPort != udpPort)\n      udpRgbConnected = rgbUdp.begin(udpRgbPort);\n    if (udpConnected && udpPort2 != udpPort && udpPort2 != udpRgbPort)\n      udp2Connected = notifier2Udp.begin(udpPort2);\n  }\n  if (ntpEnabled)\n    ntpConnected = ntpUdp.begin(ntpLocalPort);\n\n  e131.begin(e131Multicast, e131Port, e131Universe, E131_MAX_UNIVERSE_COUNT);\n  ddp.begin(false, DDP_DEFAULT_PORT);\n  reconnectHue();\n  interfacesInited = true;\n  wasConnected = true;\n}\n\nvoid WLED::handleConnection()\n{\n  static bool scanDone = true;\n  static byte stacO = 0;\n  const unsigned long now = millis();\n  #ifdef WLED_DEBUG\n  const unsigned long nowS = now/1000;\n  #endif\n  const bool wifiConfigured = WLED_WIFI_CONFIGURED;\n\n  // ignore connection handling if WiFi is configured and scan still running\n  // or within first 2s if WiFi is not configured or AP is always active\n  if ((wifiConfigured && multiWiFi.size() > 1 && WiFi.scanComplete() < 0) || (now < 2000 && (!wifiConfigured || apBehavior == AP_BEHAVIOR_ALWAYS)))\n    return;\n\n  if (lastReconnectAttempt == 0 || forceReconnect) {\n    DEBUG_PRINTF_P(PSTR(\"Initial connect or forced reconnect (@ %lus).\\n\"), nowS);\n    selectedWiFi = findWiFi(); // find strongest WiFi\n    initConnection();\n    interfacesInited = false;\n    forceReconnect = false;\n    wasConnected = false;\n    return;\n  }\n\n  byte stac = 0;\n  if (apActive) {\n#ifdef ESP8266\n    stac = wifi_softap_get_station_num();\n#else\n    wifi_sta_list_t stationList;\n    esp_wifi_ap_get_sta_list(&stationList);\n    stac = stationList.num;\n#endif\n    if (stac != stacO) {\n      stacO = stac;\n      DEBUG_PRINTF_P(PSTR(\"Connected AP clients: %d\\n\"), (int)stac);\n      if (!Network.isConnected() && wifiConfigured) {        // trying to connect, but not connected\n        if (stac)\n          WiFi.disconnect();        // disable search so that AP can work\n        else\n          initConnection();         // restart search\n      }\n    }\n  }\n\n  if (!Network.isConnected()) {\n    if (interfacesInited) {\n      if (scanDone && multiWiFi.size() > 1) {\n        DEBUG_PRINTLN(F(\"WiFi scan initiated on disconnect.\"));\n        findWiFi(true); // reinit scan\n        scanDone = false;\n        return;         // try to connect in next iteration\n      }\n      DEBUG_PRINTLN(F(\"Disconnected!\"));\n      selectedWiFi = findWiFi();\n      initConnection();\n      interfacesInited = false;\n      scanDone = true;\n      return;\n    }\n    //send improv failed 6 seconds after second init attempt (24 sec. after provisioning)\n    if (improvActive > 2 && now - lastReconnectAttempt > 6000) {\n      sendImprovStateResponse(0x03, true);\n      improvActive = 2;\n    }\n    if (now - lastReconnectAttempt > ((stac) ? 300000 : 18000) && wifiConfigured) {\n      if (improvActive == 2) improvActive = 3;\n      DEBUG_PRINTF_P(PSTR(\"Last reconnect (%lus) too old (@ %lus).\\n\"), lastReconnectAttempt/1000, nowS);\n      if (++selectedWiFi >= multiWiFi.size()) selectedWiFi = 0; // we couldn't connect, try with another network from the list\n      initConnection();\n    }\n    if (!apActive && now - lastReconnectAttempt > 12000 && (!wasConnected || apBehavior == AP_BEHAVIOR_NO_CONN)) {\n      if (!(apBehavior == AP_BEHAVIOR_TEMPORARY && now > WLED_AP_TIMEOUT)) {\n        DEBUG_PRINTF_P(PSTR(\"Not connected AP (@ %lus).\\n\"), nowS);\n        initAP();  // start AP only within first 5min\n      }\n    }\n    if (apActive && apBehavior == AP_BEHAVIOR_TEMPORARY && now > WLED_AP_TIMEOUT && stac == 0) { // disconnect AP after 5min if no clients connected\n      // if AP was enabled more than 10min after boot or if client was connected more than 10min after boot do not disconnect AP mode\n      if (now < 2*WLED_AP_TIMEOUT) {\n        dnsServer.stop();\n        WiFi.softAPdisconnect(true);\n        apActive = false;\n        DEBUG_PRINTF_P(PSTR(\"Temporary AP disabled (@ %lus).\\n\"), nowS);\n      }\n    }\n  } else if (!interfacesInited) { //newly connected\n    DEBUG_PRINTLN();\n    DEBUG_PRINT(F(\"Connected! IP address: \"));\n    DEBUG_PRINTLN(Network.localIP());\n    #ifdef ARDUINO_ARCH_ESP32\n    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)\n    #endif\n    if (improvActive) {\n      if (improvError == 3) sendImprovStateResponse(0x00, true);\n      sendImprovStateResponse(0x04);\n      if (improvActive > 1) sendImprovIPRPCResult(ImprovRPCType::Command_Wifi);\n    }\n    initInterfaces();\n    userConnected();\n    UsermodManager::connected();\n    lastMqttReconnectAttempt = 0; // force immediate update\n\n    // shut down AP\n    if (apBehavior != AP_BEHAVIOR_ALWAYS && apActive) {\n      dnsServer.stop();\n      WiFi.softAPdisconnect(true);\n      apActive = false;\n      DEBUG_PRINTLN(F(\"Access point disabled (connected).\"));\n    }\n  }\n}\n\n// If status LED pin is allocated for other uses, does nothing\n// else blink at 1Hz when Network.isConnected() is false (no WiFi, ?? no Ethernet ??)\n// else blink at 2Hz when MQTT is enabled but not connected\n// else turn the status LED off\n#if defined(STATUSLED)\nvoid WLED::handleStatusLED()\n{\n  uint32_t c = 0;\n\n  #if STATUSLED>=0\n  if (PinManager::isPinAllocated(STATUSLED)) {\n    return; //lower priority if something else uses the same pin\n  }\n  #endif\n\n  if (Network.isConnected()) {\n    c = RGBW32(0,255,0,0);\n    ledStatusType = 2;\n  } else if (WLED_MQTT_CONNECTED) {\n    c = RGBW32(0,128,0,0);\n    ledStatusType = 4;\n  } else if (apActive) {\n    c = RGBW32(0,0,255,0);\n    ledStatusType = 1;\n  }\n  if (ledStatusType) {\n    if (millis() - ledStatusLastMillis >= (1000/ledStatusType)) {\n      ledStatusLastMillis = millis();\n      ledStatusState = !ledStatusState;\n      #if STATUSLED>=0\n      digitalWrite(STATUSLED, ledStatusState);\n      #else\n      BusManager::setStatusPixel(ledStatusState ? c : 0);\n      #endif\n    }\n  } else {\n    #if STATUSLED>=0\n      #ifdef STATUSLEDINVERTED\n      digitalWrite(STATUSLED, HIGH);\n      #else\n      digitalWrite(STATUSLED, LOW);\n      #endif\n    #else\n      BusManager::setStatusPixel(0);\n    #endif\n  }\n}\n#endif\n"
  },
  {
    "path": "wled00/wled.h",
    "content": "#ifndef WLED_H\n#define WLED_H\n/*\n   Main sketch, global variable declarations\n   @title WLED project sketch\n   @author Christian Schwinne\n */\n\n// version code in format yymmddb (b = daily build)\n#define VERSION 2602141\n\n//uncomment this if you have a \"my_config.h\" file you'd like to use\n//#define WLED_USE_MY_CONFIG\n\n// ESP8266-01 (blue) got too little storage space to work with WLED. 0.10.2 is the last release supporting this unit.\n\n// ESP8266-01 (black) has 1MB flash and can thus fit the whole program, although OTA update is not possible. Use 1M(128K SPIFFS).\n// 2-step OTA may still be possible: https://github.com/wled-dev/WLED/issues/2040#issuecomment-981111096\n// Uncomment some of the following lines to disable features:\n// Alternatively, with platformio pass your chosen flags to your custom build target in platformio_override.ini\n\n// You are required to disable over-the-air updates:\n//#define WLED_DISABLE_OTA         // saves 14kb\n#ifdef WLED_ENABLE_AOTA\n  #if defined(WLED_DISABLE_OTA)\n    #warning WLED_DISABLE_OTA was defined but it will be ignored due to WLED_ENABLE_AOTA.\n  #endif\n  #undef WLED_DISABLE_OTA\n#endif\n\n// You can choose some of these features to disable:\n//#define WLED_DISABLE_ALEXA       // saves 11kb\n//#define WLED_DISABLE_HUESYNC     // saves 4kb\n//#define WLED_DISABLE_INFRARED    // saves 12kb, there is no pin left for this on ESP8266-01\n#ifndef WLED_DISABLE_MQTT\n  #define WLED_ENABLE_MQTT         // saves 12kb\n#endif\n#ifndef WLED_DISABLE_ADALIGHT      // can be used to disable reading commands from serial RX pin (see issue #3128).\n  #define WLED_ENABLE_ADALIGHT     // disable saves 5Kb (uses GPIO3 (RX) for serial). Related serial protocols: Adalight/TPM2, Improv, Serial JSON, Continuous Serial Streaming\n#else\n  #undef WLED_ENABLE_ADALIGHT      // disable has priority over enable\n#endif\n//#define WLED_ENABLE_DMX          // uses 3.5kb\n#ifndef WLED_DISABLE_LOXONE\n  #define WLED_ENABLE_LOXONE       // uses 1.2kb\n#endif\n#ifndef WLED_DISABLE_WEBSOCKETS\n  #define WLED_ENABLE_WEBSOCKETS\n#else\n  #define WLED_ENABLE_JSONLIVE     // peek LED output via /json/live (WS binary peek is always enabled)\n#endif\n\n//#define WLED_DISABLE_ESPNOW      // Removes dependence on esp now\n\n#define WLED_ENABLE_FS_EDITOR      // enable /edit page for editing FS content. Will also be disabled with OTA lock\n\n// to toggle usb serial debug (un)comment the following line\n//#define WLED_DEBUG\n\n// filesystem specific debugging\n//#define WLED_DEBUG_FS\n\n#ifndef WLED_WATCHDOG_TIMEOUT\n  // 3 seconds should be enough to detect a lockup\n  // define WLED_WATCHDOG_TIMEOUT=0 to disable watchdog, default\n  #define WLED_WATCHDOG_TIMEOUT 0\n#endif\n\n//optionally disable brownout detector on ESP32.\n//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\n//#define WLED_DISABLE_BROWNOUT_DET\n\n#include <cstddef>\n#include <vector>\n\n// Library inclusions.\n#include <Arduino.h>\n#ifdef ESP8266\n  #include <ESP8266WiFi.h>\n  #ifdef WLED_ENABLE_WPA_ENTERPRISE\n    #include \"wpa2_enterprise.h\"\n  #endif\n  #include <ESP8266mDNS.h>\n  #include <ESPAsyncTCP.h>\n  #include <LittleFS.h>\n  extern \"C\"\n  {\n  #include <user_interface.h>\n  }\n  #ifndef WLED_DISABLE_ESPNOW\n    #include <espnow.h>\n    #define WIFI_MODE_STA WIFI_STA\n    #define WIFI_MODE_AP WIFI_AP\n    #include <QuickEspNow.h>\n  #endif\n#else // ESP32\n  #include <HardwareSerial.h>  // ensure we have the correct \"Serial\" on new MCUs (depends on ARDUINO_USB_MODE and ARDUINO_USB_CDC_ON_BOOT)\n  #include <WiFi.h>\n  #include <ETH.h>\n  #include \"esp_wifi.h\"\n  #include <ESPmDNS.h>\n  #include <AsyncTCP.h>\n  #if LOROL_LITTLEFS\n    #ifndef CONFIG_LITTLEFS_FOR_IDF_3_2\n      #define CONFIG_LITTLEFS_FOR_IDF_3_2\n    #endif\n    #include <LITTLEFS.h>\n  #else\n    #include <LittleFS.h>\n  #endif\n  #include \"esp_task_wdt.h\"\n\n  #ifndef WLED_DISABLE_ESPNOW\n    #include <esp_now.h>\n    #include <QuickEspNow.h>\n  #endif\n#endif\n#include <Wire.h>\n#include <SPI.h>\n\n#include \"src/dependencies/network/Network.h\"\n\n#ifdef WLED_USE_MY_CONFIG\n  #include \"my_config.h\"\n#endif\n\n#include <ESPAsyncWebServer.h>\n#include <WiFiUdp.h>\n#include <DNSServer.h>\n#include <SPIFFSEditor.h>\n#include \"src/dependencies/time/TimeLib.h\"\n#include \"src/dependencies/timezone/Timezone.h\"\n#include \"src/dependencies/toki/Toki.h\"\n\n#ifndef WLED_DISABLE_ALEXA\n  #define ESPALEXA_ASYNC\n  #define ESPALEXA_NO_SUBPAGE\n  #define ESPALEXA_MAXDEVICES 10\n  // #define ESPALEXA_DEBUG\n  #include \"src/dependencies/espalexa/Espalexa.h\"\n  #include \"src/dependencies/espalexa/EspalexaDevice.h\"\n#endif\n\n#ifdef WLED_ENABLE_DMX\n #if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2)\n  #include \"src/dependencies/dmx/ESPDMX.h\"\n #else //ESP32\n  #include \"src/dependencies/dmx/SparkFunDMX.h\"\n #endif\n#endif\n\n#ifdef WLED_ENABLE_DMX_INPUT\n  #include \"dmx_input.h\"\n#endif\n\n#include \"src/dependencies/e131/ESPAsyncE131.h\"\n#ifndef WLED_DISABLE_MQTT\n#include <AsyncMqttClient.h>\n#endif\n\n#define ARDUINOJSON_DECODE_UNICODE 0\n#include \"src/dependencies/json/AsyncJson-v6.h\"\n#include \"src/dependencies/json/ArduinoJson-v6.h\"\n\n// ESP32-WROVER features SPI RAM (aka PSRAM) which can be allocated using ps_malloc()\n// we can create custom PSRAMDynamicJsonDocument to use such feature (replacing DynamicJsonDocument)\n// The following is a construct to enable code to compile without it.\n// There is a code that will still not use PSRAM though:\n//    AsyncJsonResponse is a derived class that implements DynamicJsonDocument (AsyncJson-v6.h)\n#if defined(BOARD_HAS_PSRAM)\nstruct PSRAM_Allocator {\n  void* allocate(size_t size) {\n    return ps_malloc(size); // use PSRAM\n  }\n  void* reallocate(void* ptr, size_t new_size) {\n    return ps_realloc(ptr, new_size); // use PSRAM\n  }\n  void deallocate(void* pointer) {\n    free(pointer);\n  }\n};\nusing PSRAMDynamicJsonDocument = BasicJsonDocument<PSRAM_Allocator>;\n#else\n#define PSRAMDynamicJsonDocument DynamicJsonDocument\n#endif\n\n#define FASTLED_INTERNAL //remove annoying pragma messages\n#define USE_GET_MILLISECOND_TIMER\n#include \"FastLED.h\"\n#include \"const.h\"\n#include \"fcn_declare.h\"\n#ifndef WLED_DISABLE_OTA\n  #include \"ota_update.h\"\n#endif\n#include \"NodeStruct.h\"\n#include \"pin_manager.h\"\n#include \"colors.h\"\n#include \"bus_manager.h\"\n#include \"FX.h\"\n#include \"wled_metadata.h\"\n\n#ifndef CLIENT_SSID\n  #define CLIENT_SSID DEFAULT_CLIENT_SSID\n#endif\n\n#ifndef CLIENT_PASS\n  #define CLIENT_PASS \"\"\n#endif\n\n#ifndef MDNS_NAME\n  #define MDNS_NAME DEFAULT_MDNS_NAME\n#endif\n\n#if defined(WLED_AP_PASS) && !defined(WLED_AP_SSID)\n  #error WLED_AP_PASS is defined but WLED_AP_SSID is still the default. \\\n         Please change WLED_AP_SSID to something unique.\n#endif\n\n#ifndef WLED_AP_SSID\n  #define WLED_AP_SSID DEFAULT_AP_SSID\n#endif\n\n#ifndef WLED_AP_PASS\n  #define WLED_AP_PASS DEFAULT_AP_PASS\n#endif\n\n#ifndef WLED_PIN\n  #define WLED_PIN \"\"\n#endif\n\n#ifndef SPIFFS_EDITOR_AIRCOOOKIE\n  #error You are not using the Aircoookie fork of the ESPAsyncWebserver library.\\\n  Using upstream puts your WiFi password at risk of being served by the filesystem.\\\n  Comment out this error message to build regardless.\n#endif\n\n#ifndef WLED_DISABLE_INFRARED\n  #include <IRremoteESP8266.h>\n  #include <IRrecv.h>\n  #include <IRutils.h>\n#endif\n\n//Filesystem to use for preset and config files. SPIFFS or LittleFS on ESP8266, SPIFFS only on ESP32 (now using LITTLEFS port by lorol)\n#ifdef ESP8266\n  #define WLED_FS LittleFS\n#else\n  #if LOROL_LITTLEFS\n    #define WLED_FS LITTLEFS\n  #else\n    #define WLED_FS LittleFS\n  #endif\n#endif\n\n// GLOBAL VARIABLES\n// both declared and defined in header (solution from http://www.keil.com/support/docs/1868.htm)\n//\n//e.g. byte test = 2 becomes WLED_GLOBAL byte test _INIT(2);\n//     int arr[]{0,1,2} becomes WLED_GLOBAL int arr[] _INIT_N(({0,1,2}));\n\n#ifndef WLED_DEFINE_GLOBAL_VARS\n  #define WLED_GLOBAL extern\n  #define _INIT(x)\n  #define _INIT_N(x)\n  #define _INIT_PROGMEM(x)\n#else\n  #define WLED_GLOBAL\n  #define _INIT(x) = x\n  //needed to ignore commas in array definitions\n  #define UNPACK( ... ) __VA_ARGS__\n  #define _INIT_N(x) UNPACK x\n  #define _INIT_PROGMEM(x) PROGMEM = x\n#endif\n\n#define STRINGIFY(X) #X\n#define TOSTRING(X) STRINGIFY(X)\n\n#define WLED_CODENAME \"Niji\"\n\n// AP and OTA default passwords (for maximum security change them!)\nWLED_GLOBAL char apPass[65]  _INIT(WLED_AP_PASS);\n#ifdef WLED_OTA_PASS\nWLED_GLOBAL char otaPass[33] _INIT(WLED_OTA_PASS);\n#else\nWLED_GLOBAL char otaPass[33] _INIT(DEFAULT_OTA_PASS);\n#endif\n\n// Hardware and pin config\n#ifndef BTNPIN\n  #define BTNPIN 0\n#endif\n#ifndef BTNTYPE\n  #define BTNTYPE BTN_TYPE_PUSH\n#endif\n#ifndef RLYPIN\nWLED_GLOBAL int8_t rlyPin _INIT(-1);\n#else\nWLED_GLOBAL int8_t rlyPin _INIT(RLYPIN);\n#endif\n//Relay mode (1 = active high, 0 = active low, flipped in cfg.json)\n#ifndef RLYMDE\nWLED_GLOBAL bool rlyMde _INIT(true);\n#else\nWLED_GLOBAL bool rlyMde _INIT(RLYMDE);\n#endif\n//Use open drain (floating pin) when relay should be off\n#ifndef RLYODRAIN\nWLED_GLOBAL bool rlyOpenDrain _INIT(false);\n#else\nWLED_GLOBAL bool rlyOpenDrain _INIT(RLYODRAIN);\n#endif\n#ifndef IRPIN\n  #define IRPIN -1\n#endif\n#ifndef IRTYPE\n  #define IRTYPE 0\n#endif\n\n#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2) || (defined(RX) && defined(TX))\n  // use RX/TX as set by the framework - these boards do _not_ have RX=3 and TX=1\n  constexpr uint8_t hardwareRX = RX;\n  constexpr uint8_t hardwareTX = TX;\n#else\n  // use defaults for RX/TX\n  constexpr uint8_t hardwareRX = 3;\n  constexpr uint8_t hardwareTX = 1;\n#endif\n\nWLED_GLOBAL char ntpServerName[33] _INIT(\"0.wled.pool.ntp.org\");   // NTP server to use\n\n// WiFi CONFIG (all these can be changed via web UI, no need to set them here)\nWLED_GLOBAL std::vector<WiFiConfig> multiWiFi;\nWLED_GLOBAL IPAddress dnsAddress _INIT_N(((  8,   8,  8,  8)));   // Google's DNS\nWLED_GLOBAL char cmDNS[33]       _INIT(MDNS_NAME);                // mDNS address (*.local, replaced by wledXXXXXX if default is used)\nWLED_GLOBAL char apSSID[33]      _INIT(\"\");                       // AP off by default (unless setup)\n#ifdef WLED_SAVE_RAM\ntypedef class WiFiOptions {\n  public:\n    struct {\n      uint8_t selectedWiFi : 4; // max 16 SSIDs\n      uint8_t apChannel    : 4;\n      uint8_t apHide       : 3;\n      uint8_t apBehavior   : 3;\n      bool    noWifiSleep  : 1;\n      bool    force802_3g  : 1;\n    };\n    WiFiOptions(uint8_t s, uint8_t c, bool h, uint8_t b, bool sl, bool g) {\n      selectedWiFi = s;\n      apChannel = c;\n      apHide = h;\n      apBehavior = b;\n      noWifiSleep = sl;\n      force802_3g = g;\n    }\n} __attribute__ ((aligned(1), packed)) wifi_options_t;\n  #ifdef ARDUINO_ARCH_ESP32\nWLED_GLOBAL wifi_options_t wifiOpt _INIT_N(({0, 1, false, AP_BEHAVIOR_BOOT_NO_CONN, true, false}));\n  #else\nWLED_GLOBAL wifi_options_t wifiOpt _INIT_N(({0, 1, false, AP_BEHAVIOR_BOOT_NO_CONN, false, false}));\n  #endif\n#define selectedWiFi wifiOpt.selectedWiFi\n#define apChannel    wifiOpt.apChannel\n#define apHide       wifiOpt.apHide\n#define apBehavior   wifiOpt.apBehavior\n#define noWifiSleep  wifiOpt.noWifiSleep\n#define force802_3g  wifiOpt.force802_3g\n#else\nWLED_GLOBAL int8_t selectedWiFi  _INIT(0);\nWLED_GLOBAL byte apChannel       _INIT(6);                        // 2.4GHz WiFi AP channel (1-13)\nWLED_GLOBAL byte apHide          _INIT(0);                        // hidden AP SSID\nWLED_GLOBAL byte apBehavior      _INIT(AP_BEHAVIOR_BOOT_NO_CONN); // access point opens when no connection after boot by default\n  #ifdef ARDUINO_ARCH_ESP32\nWLED_GLOBAL bool noWifiSleep _INIT(true);                         // disabling modem sleep modes will increase heat output and power usage, but may help with connection issues\n  #else\nWLED_GLOBAL bool noWifiSleep _INIT(false);\n  #endif\nWLED_GLOBAL bool force802_3g _INIT(false);\n#endif // WLED_SAVE_RAM\n#ifdef ARDUINO_ARCH_ESP32\n  #if defined(LOLIN_WIFI_FIX) && (defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3))\nWLED_GLOBAL uint8_t txPower _INIT(WIFI_POWER_8_5dBm);\n  #else\nWLED_GLOBAL uint8_t txPower _INIT(WIFI_POWER_19_5dBm);\n  #endif\n#endif\n#define WLED_WIFI_CONFIGURED isWiFiConfigured()\n\n#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET)\n  #ifdef WLED_ETH_DEFAULT                                          // default ethernet board type if specified\n    WLED_GLOBAL int ethernetType _INIT(WLED_ETH_DEFAULT);          // ethernet board type\n  #else\n    WLED_GLOBAL int ethernetType _INIT(WLED_ETH_NONE);             // use none for ethernet board type if default not defined\n  #endif\n#endif\n\n// LED CONFIG\nWLED_GLOBAL bool turnOnAtBoot _INIT(true);                // turn on LEDs at power-up\nWLED_GLOBAL byte bootPreset   _INIT(0);                   // save preset to load after power-up\n\n//if true, a segment per bus will be created on boot and LED settings save\n//if false, only one segment spanning the total LEDs is created,\n//but not on LED settings save if there is more than one segment currently\n#ifdef ESP8266\nWLED_GLOBAL bool useGlobalLedBuffer _INIT(false); // double buffering disabled on ESP8266\n#else\nWLED_GLOBAL bool useGlobalLedBuffer _INIT(true);  // double buffering enabled on ESP32\n#endif\n#ifdef WLED_USE_IC_CCT\nWLED_GLOBAL bool cctICused          _INIT(true);  // CCT IC used (Athom 15W bulbs)\n#else\nWLED_GLOBAL bool cctICused          _INIT(false); // CCT IC used (Athom 15W bulbs)\n#endif\nWLED_GLOBAL bool gammaCorrectCol    _INIT(true);  // use gamma correction on colors\nWLED_GLOBAL bool gammaCorrectBri    _INIT(false); // use gamma correction on brightness\nWLED_GLOBAL float gammaCorrectVal   _INIT(2.2f);  // gamma correction value\n\nWLED_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.\nWLED_GLOBAL byte colSec[] _INIT_N(({ 0, 0, 0, 0 }));      // current RGB(W) secondary color\n\nWLED_GLOBAL byte nightlightTargetBri _INIT(0);      // brightness after nightlight is over\nWLED_GLOBAL byte nightlightDelayMins _INIT(60);\nWLED_GLOBAL byte nightlightMode      _INIT(NL_MODE_FADE); // See const.h for available modes. Was nightlightFade\n\nWLED_GLOBAL byte briMultiplier _INIT(100);          // % of brightness to set (to limit power, if you set it to 50 and set bri to 255, actual brightness will be 127)\n\n// User Interface CONFIG\n#ifndef SERVERNAME\nWLED_GLOBAL char serverDescription[33] _INIT(\"WLED\");  // Name of module - use default\n#else\nWLED_GLOBAL char serverDescription[33] _INIT(SERVERNAME);  // use predefined name\n#endif\nWLED_GLOBAL bool simplifiedUI          _INIT(false);   // enable simplified UI\nWLED_GLOBAL byte cacheInvalidate       _INIT(0);       // used to invalidate browser cache\n\n// Sync CONFIG\nWLED_GLOBAL NodesMap Nodes;\nWLED_GLOBAL bool nodeListEnabled _INIT(true);\nWLED_GLOBAL bool nodeBroadcastEnabled _INIT(true);\n\n#ifndef WLED_DISABLE_INFRARED\nWLED_GLOBAL int8_t irPin        _INIT(IRPIN);\nWLED_GLOBAL byte irEnabled      _INIT(IRTYPE); // Infrared receiver\n#endif\nWLED_GLOBAL bool irApplyToAllSelected _INIT(true); //apply IR or ESP-NOW to all selected segments\n\n#ifndef WLED_DISABLE_ALEXA\nWLED_GLOBAL bool alexaEnabled _INIT(false);                       // enable device discovery by Amazon Echo\nWLED_GLOBAL char alexaInvocationName[33] _INIT(\"Light\");          // speech control name of device. Choose something voice-to-text can understand\nWLED_GLOBAL byte alexaNumPresets _INIT(0);                        // number of presets to expose to Alexa, starting from preset 1, up to 9\n#endif\n\nWLED_GLOBAL uint16_t realtimeTimeoutMs _INIT(2500);               // ms timeout of realtime mode before returning to normal mode\nWLED_GLOBAL int arlsOffset _INIT(0);                              // realtime LED offset\nWLED_GLOBAL bool arlsDisableGammaCorrection _INIT(true);          // activate if gamma correction is handled by the source\nWLED_GLOBAL bool arlsForceMaxBri _INIT(false);                    // enable to force max brightness if source has very dark colors that would be black\n\n#ifdef WLED_ENABLE_DMX\n #if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2)\n  WLED_GLOBAL DMXESPSerial dmx;\n #else //ESP32\n  WLED_GLOBAL SparkFunDMX dmx;\n #endif\n  WLED_GLOBAL uint16_t e131ProxyUniverse _INIT(0);                  // output this E1.31 (sACN) / ArtNet universe via MAX485 (0 = disabled)\n  // dmx CONFIG\n  WLED_GLOBAL byte DMXChannels _INIT(7);        // number of channels per fixture\n  WLED_GLOBAL byte DMXFixtureMap[15] _INIT_N(({ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }));\n  // assigns the different channels to different functions. See wled21_dmx.ino for more information.\n  WLED_GLOBAL uint16_t DMXGap _INIT(10);          // gap between the fixtures. makes addressing easier because you don't have to memorize odd numbers when climbing up onto a rig.\n  WLED_GLOBAL uint16_t DMXStart _INIT(10);        // start address of the first fixture\n  WLED_GLOBAL uint16_t DMXStartLED _INIT(0);      // LED from which DMX fixtures start\n#endif\n#ifdef WLED_ENABLE_DMX_INPUT\n  WLED_GLOBAL int dmxInputTransmitPin _INIT(0);\n  WLED_GLOBAL int dmxInputReceivePin _INIT(0);\n  WLED_GLOBAL int dmxInputEnablePin _INIT(0);\n  WLED_GLOBAL int dmxInputPort _INIT(2);\n  WLED_GLOBAL DMXInput dmxInput;\n#endif\n\nWLED_GLOBAL uint16_t e131Universe _INIT(1);                       // settings for E1.31 (sACN) protocol (only DMX_MODE_MULTIPLE_* can span over consequtive universes)\nWLED_GLOBAL uint16_t e131Port _INIT(5568);                        // DMX in port. E1.31 default is 5568, Art-Net is 6454\nWLED_GLOBAL byte e131Priority _INIT(0);                           // E1.31 port priority (if != 0 priority handling is active)\nWLED_GLOBAL E131Priority highPriority _INIT(3);                   // E1.31 highest priority tracking, init = timeout in seconds\nWLED_GLOBAL byte DMXMode _INIT(DMX_MODE_MULTIPLE_RGB);            // DMX mode (s.a.)\nWLED_GLOBAL uint16_t DMXAddress _INIT(1);                         // DMX start address of fixture, a.k.a. first Channel [for E1.31 (sACN) protocol]\nWLED_GLOBAL uint16_t DMXSegmentSpacing _INIT(0);                  // Number of void/unused channels between each segments DMX channels\nWLED_GLOBAL byte e131LastSequenceNumber[E131_MAX_UNIVERSE_COUNT]; // to detect packet loss\nWLED_GLOBAL bool e131Multicast _INIT(false);                      // multicast or unicast\nWLED_GLOBAL bool e131SkipOutOfSequence _INIT(false);              // freeze instead of flickering\nWLED_GLOBAL uint16_t pollReplyCount _INIT(0);                     // count number of replies for ArtPoll node report\n\n// mqtt\nWLED_GLOBAL unsigned long lastMqttReconnectAttempt _INIT(0);  // used for other periodic tasks too\n#ifndef WLED_DISABLE_MQTT\n  #ifndef MQTT_MAX_TOPIC_LEN\n    #define MQTT_MAX_TOPIC_LEN 32\n  #endif\n  #ifndef MQTT_MAX_SERVER_LEN\n    #define MQTT_MAX_SERVER_LEN 32\n  #endif\nWLED_GLOBAL AsyncMqttClient *mqtt _INIT(NULL);\nWLED_GLOBAL bool mqttEnabled _INIT(false);\nWLED_GLOBAL char mqttStatusTopic[MQTT_MAX_TOPIC_LEN + 8] _INIT(\"\");         // this must be global because of async handlers\nWLED_GLOBAL char mqttDeviceTopic[MQTT_MAX_TOPIC_LEN + 1] _INIT(\"\");         // main MQTT topic (individual per device, default is wled/mac)\nWLED_GLOBAL char mqttGroupTopic[MQTT_MAX_TOPIC_LEN + 1]  _INIT(\"wled/all\"); // second MQTT topic (for example to group devices)\nWLED_GLOBAL char mqttServer[MQTT_MAX_SERVER_LEN + 1]     _INIT(\"\");         // both domains and IPs should work (no SSL)\nWLED_GLOBAL char mqttUser[41] _INIT(\"\");                   // optional: username for MQTT auth\nWLED_GLOBAL char mqttPass[65] _INIT(\"\");                   // optional: password for MQTT auth\nWLED_GLOBAL char mqttClientID[41] _INIT(\"\");               // override the client ID\nWLED_GLOBAL uint16_t mqttPort _INIT(1883);\nWLED_GLOBAL bool retainMqttMsg _INIT(false);               // retain brightness and color\n#define WLED_MQTT_CONNECTED (mqtt != nullptr && mqtt->connected())\n#else\n#define WLED_MQTT_CONNECTED false\n#endif\n\n#ifndef WLED_DISABLE_HUESYNC\nWLED_GLOBAL bool huePollingEnabled _INIT(false);           // poll hue bridge for light state\nWLED_GLOBAL uint16_t huePollIntervalMs _INIT(2500);        // low values (< 1sec) may cause lag but offer quicker response\nWLED_GLOBAL char hueApiKey[47] _INIT(\"api\");               // key token will be obtained from bridge\nWLED_GLOBAL byte huePollLightId _INIT(1);                  // ID of hue lamp to sync to. Find the ID in the hue app (\"about\" section)\nWLED_GLOBAL IPAddress hueIP _INIT_N(((0, 0, 0, 0))); // IP address of the bridge\nWLED_GLOBAL bool hueApplyOnOff _INIT(true);\nWLED_GLOBAL bool hueApplyBri _INIT(true);\nWLED_GLOBAL bool hueApplyColor _INIT(true);\n#endif\n\nWLED_GLOBAL uint16_t serialBaud _INIT(1152); // serial baud rate, multiply by 100\nWLED_GLOBAL bool     serialCanRX _INIT(false);\nWLED_GLOBAL bool     serialCanTX _INIT(false);\n\n#ifndef WLED_DISABLE_ESPNOW\nWLED_GLOBAL bool enableESPNow        _INIT(false);  // global on/off for ESP-NOW\nWLED_GLOBAL byte statusESPNow        _INIT(ESP_NOW_STATE_UNINIT); // state of ESP-NOW stack (0 uninitialised, 1 initialised, 2 error)\nWLED_GLOBAL bool useESPNowSync       _INIT(false);  // use ESP-NOW wireless technology for sync\n//WLED_GLOBAL char linked_remote[13]   _INIT(\"\");     // MAC of ESP-NOW remote (Wiz Mote)\nWLED_GLOBAL std::vector<std::array<char, 13>> linked_remotes; // MAC of ESP-NOW remotes (Wiz Mote)\nWLED_GLOBAL char last_signal_src[13] _INIT(\"\");     // last seen ESP-NOW sender\n#endif\n\n// Time CONFIG\n#ifndef WLED_NTP_ENABLED\n  #define WLED_NTP_ENABLED false\n#endif\n#ifndef WLED_TIMEZONE\n  #define WLED_TIMEZONE 0\n#endif\n#ifndef WLED_UTC_OFFSET\n  #define WLED_UTC_OFFSET 0\n#endif\nWLED_GLOBAL bool ntpEnabled      _INIT(WLED_NTP_ENABLED); // get internet time. Only required if you use clock overlays or time-activated macros\nWLED_GLOBAL bool useAMPM         _INIT(false);            // 12h/24h clock format\nWLED_GLOBAL byte currentTimezone _INIT(WLED_TIMEZONE);    // Timezone ID. Refer to timezones array in wled10_ntp.ino\nWLED_GLOBAL int utcOffsetSecs    _INIT(WLED_UTC_OFFSET);  // Seconds to offset from UTC before timzone calculation\n\nWLED_GLOBAL byte overlayCurrent _INIT(0);    // 0: no overlay 1: analog clock 2: was single-digit clock 3: was cronixie\nWLED_GLOBAL uint16_t overlayMin _INIT(0), overlayMax _INIT(DEFAULT_LED_COUNT - 1);   // boundaries of overlay mode\n\nWLED_GLOBAL byte analogClock12pixel _INIT(0);               // The pixel in your strip where \"midnight\" would be\nWLED_GLOBAL bool analogClockSecondsTrail _INIT(false);      // Display seconds as trail of LEDs instead of a single pixel\nWLED_GLOBAL bool analogClock5MinuteMarks _INIT(false);      // Light pixels at every 5-minute position\nWLED_GLOBAL bool analogClockSolidBlack _INIT(false);        // Show clock overlay only if all LEDs are solid black (effect is 0 and color is black)\n\nWLED_GLOBAL bool countdownMode _INIT(false);                         // Clock will count down towards date\nWLED_GLOBAL byte countdownYear _INIT(20), countdownMonth _INIT(1);   // Countdown target date, year is last two digits\nWLED_GLOBAL byte countdownDay  _INIT(1) , countdownHour  _INIT(0);\nWLED_GLOBAL byte countdownMin  _INIT(0) , countdownSec   _INIT(0);\n\nWLED_GLOBAL byte macroNl   _INIT(0);        // after nightlight delay over\nWLED_GLOBAL byte macroCountdown _INIT(0);\nWLED_GLOBAL byte macroAlexaOn _INIT(0), macroAlexaOff _INIT(0);\n\n// Security CONFIG\n#ifdef WLED_OTA_PASS\nWLED_GLOBAL bool otaLock        _INIT(true);     // prevents OTA firmware updates without password. ALWAYS enable if system exposed to any public networks\n#else\nWLED_GLOBAL bool otaLock        _INIT(false);     // prevents OTA firmware updates without password. ALWAYS enable if system exposed to any public networks\n#endif\nWLED_GLOBAL bool wifiLock       _INIT(false);     // prevents access to WiFi settings when OTA lock is enabled\n#ifdef WLED_ENABLE_AOTA\nWLED_GLOBAL bool aOtaEnabled    _INIT(true);      // ArduinoOTA allows easy updates directly from the IDE. Careful, it does not auto-disable when OTA lock is on\n#else\nWLED_GLOBAL bool aOtaEnabled    _INIT(false);     // ArduinoOTA allows easy updates directly from the IDE. Careful, it does not auto-disable when OTA lock is on\n#endif\nWLED_GLOBAL bool otaSameSubnet  _INIT(true);      // prevent OTA updates from other subnets (e.g. internet) if no PIN is set\nWLED_GLOBAL char settingsPIN[5] _INIT(WLED_PIN);  // PIN for settings pages\nWLED_GLOBAL bool correctPIN     _INIT(!strlen(settingsPIN));\nWLED_GLOBAL unsigned long lastEditTime _INIT(0);\n\nWLED_GLOBAL uint16_t userVar0 _INIT(0), userVar1 _INIT(0); //available for use in usermod\n\n// internal global variable declarations\n// wifi\nWLED_GLOBAL bool apActive _INIT(false);\nWLED_GLOBAL byte apClients _INIT(0);\nWLED_GLOBAL bool forceReconnect _INIT(false);\nWLED_GLOBAL unsigned long lastReconnectAttempt _INIT(0);\nWLED_GLOBAL bool interfacesInited _INIT(false);\nWLED_GLOBAL bool wasConnected _INIT(false);\n\n// color\nWLED_GLOBAL byte lastRandomIndex _INIT(0);        // used to save last random color so the new one is not the same\nWLED_GLOBAL std::vector<CRGBPalette16> customPalettes;  // custom palettes\nWLED_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\n\n// transitions\nWLED_GLOBAL uint8_t       blendingStyle            _INIT(0);      // effect blending/transitionig style\nWLED_GLOBAL bool          transitionActive         _INIT(false);\nWLED_GLOBAL uint16_t      transitionDelay          _INIT(750);    // global transition duration\nWLED_GLOBAL uint16_t      transitionDelayDefault   _INIT(750);    // default transition time (stored in cfg.json)\nWLED_GLOBAL unsigned long transitionStartTime;\nWLED_GLOBAL bool          jsonTransitionOnce       _INIT(false);  // flag to override transitionDelay (playlist, JSON API: \"live\" & \"seg\":{\"i\"} & \"tt\")\nWLED_GLOBAL uint8_t       randomPaletteChangeTime  _INIT(5);      // amount of time [s] between random palette changes (min: 1s, max: 255s)\nWLED_GLOBAL bool          useHarmonicRandomPalette _INIT(true);   // use *harmonic* random palette generation (nicer looking) or truly random\n\n// nightlight\nWLED_GLOBAL bool nightlightActive _INIT(false);\nWLED_GLOBAL bool nightlightActiveOld _INIT(false);\nWLED_GLOBAL uint32_t nightlightDelayMs _INIT(10);\nWLED_GLOBAL byte nightlightDelayMinsDefault _INIT(nightlightDelayMins);\nWLED_GLOBAL unsigned long nightlightStartTime;\nWLED_GLOBAL unsigned long lastNlUpdate;\nWLED_GLOBAL byte briNlT _INIT(0);                     // current nightlight brightness\nWLED_GLOBAL byte colNlT[] _INIT_N(({ 0, 0, 0, 0 }));        // current nightlight color\n\n// brightness\nWLED_GLOBAL unsigned long lastOnTime _INIT(0);\nWLED_GLOBAL bool offMode             _INIT(!turnOnAtBoot);\nWLED_GLOBAL byte briS                _INIT(128);           // default brightness\nWLED_GLOBAL byte bri                 _INIT(briS);          // global brightness (set)\nWLED_GLOBAL byte briOld              _INIT(0);             // global brightness while in transition loop (previous iteration)\nWLED_GLOBAL byte briT                _INIT(0);             // global brightness during transition\nWLED_GLOBAL byte briLast             _INIT(128);           // brightness before turned off. Used for toggle function\nWLED_GLOBAL byte whiteLast           _INIT(128);           // white channel before turned off. Used for toggle function in ir.cpp\n\n// button\nstruct Button {\n  unsigned long pressedTime;        // time button was pressed\n  unsigned long waitTime;           // time to wait for next button press\n  int8_t        pin;                // pin number\n  struct {\n    uint8_t     type          : 6;  // button type (push, long, double, etc.)\n    bool        pressedBefore : 1;  // button was pressed before\n    bool        longPressed   : 1;  // button was long pressed\n  };\n  uint8_t       macroButton;        // macro/preset to call on button press\n  uint8_t       macroLongPress;     // macro/preset to call on long press\n  uint8_t       macroDoublePress;   // macro/preset to call on double press\n\n  Button(int8_t p, uint8_t t, uint8_t mB = 0, uint8_t mLP = 0, uint8_t mDP = 0)\n  : pressedTime(0)\n  , waitTime(0)\n  , pin(p)\n  , type(t)\n  , pressedBefore(false)\n  , longPressed(false)\n  , macroButton(mB)\n  , macroLongPress(mLP)\n  , macroDoublePress(mDP) {}\n};\nWLED_GLOBAL std::vector<Button> buttons; // vector of button structs\nWLED_GLOBAL bool buttonPublishMqtt                            _INIT(false);\nWLED_GLOBAL bool disablePullUp                                _INIT(false);\nWLED_GLOBAL byte touchThreshold                               _INIT(TOUCH_THRESHOLD);\n\n// notifications\nWLED_GLOBAL bool sendNotifications    _INIT(false);           // master notification switch\nWLED_GLOBAL bool sendNotificationsRT  _INIT(false);           // master notification switch (runtime)\nWLED_GLOBAL unsigned long notificationSentTime _INIT(0);\nWLED_GLOBAL byte notificationSentCallMode _INIT(CALL_MODE_INIT);\nWLED_GLOBAL uint8_t notificationCount _INIT(0);\nWLED_GLOBAL uint8_t syncGroups    _INIT(0x01);                // sync send groups this instance syncs to (bit mapped)\nWLED_GLOBAL uint8_t receiveGroups _INIT(0x01);                // sync receive groups this instance belongs to (bit mapped)\n#ifdef WLED_SAVE_RAM\n// this will save us 8 bytes of RAM while increasing code by ~400 bytes\ntypedef class Receive {\n  public:\n    union {\n      uint8_t   Options;\n      struct {\n        bool    Brightness     : 1;\n        bool    Color          : 1;\n        bool    Effects        : 1;\n        bool    SegmentOptions : 1;\n        bool    SegmentBounds  : 1;\n        bool    Direct         : 1;\n        bool    Palette        : 1;\n        uint8_t reserved       : 1;\n      };\n    };\n    Receive(int i) { Options = i; }\n    Receive(bool b, bool c, bool e, bool sO, bool sB, bool p)\n    : Brightness(b)\n    , Color(c)\n    , Effects(e)\n    , SegmentOptions(sO)\n    , SegmentBounds(sB)\n    , Palette(p)\n    {};\n} __attribute__ ((aligned(1), packed)) receive_notification_t;\ntypedef class Send {\n  public:\n    union {\n      uint8_t Options;\n      struct {\n        bool Direct : 1;\n        bool Button : 1;\n        bool Alexa  : 1;\n        bool Hue    : 1;\n        uint8_t reserved : 4;\n      };\n    };\n  Send(int o) { Options = o; }\n  Send(bool d, bool b, bool a, bool h) {\n    Direct = d;\n    Button = b;\n    Alexa = a;\n    Hue = h;\n  }\n} __attribute__ ((aligned(1), packed)) send_notification_t;\nWLED_GLOBAL receive_notification_t receiveN _INIT(0b01100111);\nWLED_GLOBAL send_notification_t    notifyG  _INIT(0b00001111);\n#define receiveNotificationBrightness receiveN.Brightness\n#define receiveNotificationColor      receiveN.Color\n#define receiveNotificationEffects    receiveN.Effects\n#define receiveNotificationPalette    receiveN.Palette\n#define receiveSegmentOptions         receiveN.SegmentOptions\n#define receiveSegmentBounds          receiveN.SegmentBounds\n#define receiveDirect                 receiveN.Direct\n#define notifyDirect notifyG.Direct\n#define notifyButton notifyG.Button\n#define notifyAlexa  notifyG.Alexa\n#define notifyHue    notifyG.Hue\n#else\nWLED_GLOBAL bool receiveNotificationBrightness _INIT(true);       // apply brightness from incoming notifications\nWLED_GLOBAL bool receiveNotificationColor      _INIT(true);       // apply color\nWLED_GLOBAL bool receiveNotificationEffects    _INIT(true);       // apply effects setup\nWLED_GLOBAL bool receiveNotificationPalette    _INIT(true);       // apply palette\nWLED_GLOBAL bool receiveSegmentOptions         _INIT(false);      // apply segment options\nWLED_GLOBAL bool receiveSegmentBounds          _INIT(false);      // apply segment bounds (start, stop, offset)\nWLED_GLOBAL bool receiveDirect _INIT(true);                       // receive UDP/Hyperion realtime\nWLED_GLOBAL bool notifyDirect _INIT(true);                        // send notification if change via UI or HTTP API\nWLED_GLOBAL bool notifyButton _INIT(true);                        // send if updated by button or infrared remote\nWLED_GLOBAL bool notifyAlexa  _INIT(false);                       // send notification if updated via Alexa\nWLED_GLOBAL bool notifyHue    _INIT(false);                       // send notification if Hue light changes\n#endif\n\n// effects\nWLED_GLOBAL byte effectCurrent _INIT(0);\nWLED_GLOBAL byte effectSpeed _INIT(128);\nWLED_GLOBAL byte effectIntensity _INIT(128);\nWLED_GLOBAL byte effectPalette _INIT(0);\nWLED_GLOBAL bool stateChanged _INIT(false);\n\n// network\n#ifdef WLED_SAVE_RAM\n// this will save us 2 bytes of RAM while increasing code by ~400 bytes\ntypedef class Udp {\n  public:\n    uint16_t  Port;\n    uint16_t  Port2;\n    uint16_t  RgbPort;\n    struct {\n      uint8_t NumRetries : 5;\n      bool    Connected : 1;\n      bool    Connected2 : 1;\n      bool    RgbConnected : 1;\n    };\n    Udp(int p1, int p2, int p3, int r, bool c1, bool c2, bool c3) {\n      Port = p1;\n      Port2 = p2;\n      RgbPort = p3;\n      NumRetries = r;\n      Connected = c1;\n      Connected2 = c2;\n      RgbConnected = c3;\n    }\n} __attribute__ ((aligned(1), packed)) udp_port_t;\nWLED_GLOBAL udp_port_t udp _INIT_N(({21234, 65506, 19446, 0, false, false, false}));\n#define udpPort         udp.Port\n#define udpPort2        udp.Port2\n#define udpRgbPort      udp.RgbPort\n#define udpNumRetries   udp.NumRetries\n#define udpConnected    udp.Connected\n#define udp2Connected   udp.Connected2\n#define udpRgbConnected udp.RgbConnected\n#else\nWLED_GLOBAL uint16_t udpPort    _INIT(21324); // WLED notifier default port\nWLED_GLOBAL uint16_t udpPort2   _INIT(65506); // WLED notifier supplemental port\nWLED_GLOBAL uint16_t udpRgbPort _INIT(19446); // Hyperion port\nWLED_GLOBAL uint8_t  udpNumRetries _INIT(0);  // Number of times a UDP sync message is retransmitted. Increase to increase reliability\nWLED_GLOBAL bool     udpConnected _INIT(false);\nWLED_GLOBAL bool     udp2Connected _INIT(false);\nWLED_GLOBAL bool     udpRgbConnected _INIT(false);\n#endif\n\n// ui style\nWLED_GLOBAL bool showWelcomePage _INIT(false);\n\n// hue\n#ifndef WLED_DISABLE_HUESYNC\nWLED_GLOBAL byte hueError _INIT(HUE_ERROR_INACTIVE);\n// WLED_GLOBAL uint16_t hueFailCount _INIT(0);\nWLED_GLOBAL float hueXLast _INIT(0), hueYLast _INIT(0);\nWLED_GLOBAL uint16_t hueHueLast _INIT(0), hueCtLast _INIT(0);\nWLED_GLOBAL byte hueSatLast _INIT(0), hueBriLast _INIT(0);\nWLED_GLOBAL unsigned long hueLastRequestSent _INIT(0);\nWLED_GLOBAL bool hueAuthRequired _INIT(false);\nWLED_GLOBAL bool hueReceived _INIT(false);\nWLED_GLOBAL bool hueStoreAllowed _INIT(false), hueNewKey _INIT(false);\n#endif\n\n// countdown\nWLED_GLOBAL unsigned long countdownTime _INIT(1514764800L);\nWLED_GLOBAL bool countdownOverTriggered _INIT(true);\n\n//timer\nWLED_GLOBAL byte lastTimerMinute  _INIT(0);\nWLED_GLOBAL byte timerHours[]     _INIT_N(({ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }));\nWLED_GLOBAL int8_t timerMinutes[] _INIT_N(({ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }));\nWLED_GLOBAL byte timerMacro[]     _INIT_N(({ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }));\n//weekdays to activate on, bit pattern of arr elem: 0b11111111: sun,sat,fri,thu,wed,tue,mon,validity\nWLED_GLOBAL byte timerWeekday[]   _INIT_N(({ 254, 254, 254, 254, 254, 254, 254, 254, 254, 254 }));\n//upper 4 bits start, lower 4 bits end month (default 28: start month 1 and end month 12)\nWLED_GLOBAL byte timerMonth[]     _INIT_N(({28,28,28,28,28,28,28,28}));\nWLED_GLOBAL byte timerDay[]       _INIT_N(({1,1,1,1,1,1,1,1}));\nWLED_GLOBAL byte timerDayEnd[]\t\t_INIT_N(({31,31,31,31,31,31,31,31}));\nWLED_GLOBAL bool doAdvancePlaylist _INIT(false);\n\n//improv\nWLED_GLOBAL byte improvActive _INIT(0); //0: no improv packet received, 1: improv active, 2: provisioning\nWLED_GLOBAL byte improvError _INIT(0);\n\n//playlists\nWLED_GLOBAL int16_t currentPlaylist _INIT(-1);\n//still used for \"PL=~\" HTTP API command\nWLED_GLOBAL byte presetCycCurr _INIT(0);\nWLED_GLOBAL byte presetCycMin _INIT(1);\nWLED_GLOBAL byte presetCycMax _INIT(5);\n\n// realtime\nWLED_GLOBAL byte realtimeMode _INIT(REALTIME_MODE_INACTIVE);\nWLED_GLOBAL byte realtimeOverride _INIT(REALTIME_OVERRIDE_NONE);\nWLED_GLOBAL IPAddress realtimeIP _INIT_N(((0, 0, 0, 0)));\nWLED_GLOBAL unsigned long realtimeTimeout _INIT(0);\nWLED_GLOBAL uint8_t tpmPacketCount _INIT(0);\nWLED_GLOBAL uint16_t tpmPayloadFrameSize _INIT(0);\nWLED_GLOBAL bool useMainSegmentOnly _INIT(false);\nWLED_GLOBAL bool realtimeRespectLedMaps _INIT(true);                     // Respect LED maps when receiving realtime data\n\nWLED_GLOBAL unsigned long lastInterfaceUpdate _INIT(0);\nWLED_GLOBAL byte interfaceUpdateCallMode _INIT(CALL_MODE_INIT);\n\n// alexa udp\nWLED_GLOBAL String escapedMac;\n#ifndef WLED_DISABLE_ALEXA\n  WLED_GLOBAL Espalexa espalexa;\n  WLED_GLOBAL EspalexaDevice* espalexaDevice;\n#endif\n\n// dns server\nWLED_GLOBAL DNSServer dnsServer;\n\n// network time\n#ifndef WLED_LAT\n  #define WLED_LAT 0.0f\n#endif\n#ifndef WLED_LON\n  #define WLED_LON 0.0f\n#endif\n#define NTP_NEVER 999000000L\nWLED_GLOBAL bool ntpConnected _INIT(false);\nWLED_GLOBAL time_t localTime _INIT(0);\nWLED_GLOBAL unsigned long ntpLastSyncTime _INIT(NTP_NEVER);\nWLED_GLOBAL unsigned long ntpPacketSentTime _INIT(NTP_NEVER);\nWLED_GLOBAL IPAddress ntpServerIP;\nWLED_GLOBAL uint16_t ntpLocalPort _INIT(2390);\nWLED_GLOBAL uint16_t rolloverMillis _INIT(0);\nWLED_GLOBAL float longitude _INIT(WLED_LON);\nWLED_GLOBAL float latitude _INIT(WLED_LAT);\nWLED_GLOBAL time_t sunrise _INIT(0);\nWLED_GLOBAL time_t sunset _INIT(0);\nWLED_GLOBAL Toki toki _INIT(Toki());\n\n// General filesystem\nWLED_GLOBAL size_t fsBytesUsed _INIT(0);\nWLED_GLOBAL size_t fsBytesTotal _INIT(0);\nWLED_GLOBAL unsigned long presetsModifiedTime _INIT(0L);\nWLED_GLOBAL bool doCloseFile _INIT(false);\n\n// presets\nWLED_GLOBAL byte currentPreset _INIT(0);\n\nWLED_GLOBAL byte errorFlag _INIT(0);\n\nWLED_GLOBAL String messageHead, messageSub;\nWLED_GLOBAL byte optionType;\n\nWLED_GLOBAL bool configNeedsWrite  _INIT(false);        // flag to initiate saving of config\nWLED_GLOBAL bool doReboot          _INIT(false);        // flag to initiate reboot from async handlers\n\n// status led\n#if defined(STATUSLED)\nWLED_GLOBAL unsigned long ledStatusLastMillis _INIT(0);\nWLED_GLOBAL uint8_t ledStatusType _INIT(0); // current status type - corresponds to number of blinks per second\nWLED_GLOBAL bool ledStatusState _INIT(false); // the current LED state\n#endif\n\n// server library objects\nWLED_GLOBAL AsyncWebServer server _INIT_N(((80, {0, WLED_REQUEST_MAX_QUEUE, WLED_REQUEST_MIN_HEAP, WLED_REQUEST_HEAP_USAGE})));\n#ifdef WLED_ENABLE_WEBSOCKETS\nWLED_GLOBAL AsyncWebSocket ws _INIT_N(((\"/ws\")));\n#endif\n#ifndef WLED_DISABLE_HUESYNC\nWLED_GLOBAL AsyncClient     *hueClient _INIT(NULL);\n#endif\nWLED_GLOBAL AsyncWebHandler *editHandler _INIT(nullptr);\n\n// udp interface objects\nWLED_GLOBAL WiFiUDP notifierUdp, rgbUdp, notifier2Udp;\nWLED_GLOBAL WiFiUDP ntpUdp;\nWLED_GLOBAL ESPAsyncE131 e131 _INIT_N(((handleE131Packet)));\nWLED_GLOBAL ESPAsyncE131 ddp  _INIT_N(((handleE131Packet)));\nWLED_GLOBAL bool e131NewData _INIT(false);\n\n// led fx library object\nWLED_GLOBAL WS2812FX   strip         _INIT(WS2812FX());\nWLED_GLOBAL std::vector<BusConfig> busConfigs;    //temporary, to remember values from network callback until after\nWLED_GLOBAL bool       doInitBusses  _INIT(false);\nWLED_GLOBAL int8_t     loadLedmap    _INIT(-1);\nWLED_GLOBAL uint8_t    currentLedmap _INIT(0);\n#ifndef ESP8266\nWLED_GLOBAL char  *ledmapNames[WLED_MAX_LEDMAPS-1] _INIT_N(({nullptr}));\n#endif\n#if WLED_MAX_LEDMAPS>16\nWLED_GLOBAL uint32_t ledMaps _INIT(0); // bitfield representation of available ledmaps\n#else\nWLED_GLOBAL uint16_t ledMaps _INIT(0); // bitfield representation of available ledmaps\n#endif\n\n// global I2C SDA pin (used for usermods)\n#ifndef I2CSDAPIN\nWLED_GLOBAL int8_t i2c_sda  _INIT(-1);\n#else\nWLED_GLOBAL int8_t i2c_sda  _INIT(I2CSDAPIN);\n#endif\n// global I2C SCL pin (used for usermods)\n#ifndef I2CSCLPIN\nWLED_GLOBAL int8_t i2c_scl  _INIT(-1);\n#else\nWLED_GLOBAL int8_t i2c_scl  _INIT(I2CSCLPIN);\n#endif\n\n// global SPI DATA/MOSI pin (used for usermods)\n#ifndef SPIMOSIPIN\nWLED_GLOBAL int8_t spi_mosi  _INIT(-1);\n#else\nWLED_GLOBAL int8_t spi_mosi  _INIT(SPIMOSIPIN);\n#endif\n// global SPI DATA/MISO pin (used for usermods)\n#ifndef SPIMISOPIN\nWLED_GLOBAL int8_t spi_miso  _INIT(-1);\n#else\nWLED_GLOBAL int8_t spi_miso  _INIT(SPIMISOPIN);\n#endif\n// global SPI CLOCK/SCLK pin (used for usermods)\n#ifndef SPISCLKPIN\nWLED_GLOBAL int8_t spi_sclk  _INIT(-1);\n#else\nWLED_GLOBAL int8_t spi_sclk  _INIT(SPISCLKPIN);\n#endif\n\n// global ArduinoJson buffer\n#if defined(ARDUINO_ARCH_ESP32)\nWLED_GLOBAL SemaphoreHandle_t jsonBufferLockMutex _INIT(xSemaphoreCreateRecursiveMutex());\n#endif\n#ifdef BOARD_HAS_PSRAM\n// if board has PSRAM, use it for JSON document (allocated in setup())\nWLED_GLOBAL JsonDocument *pDoc _INIT(nullptr);\n#else\nWLED_GLOBAL StaticJsonDocument<JSON_BUFFER_SIZE> gDoc;\nWLED_GLOBAL JsonDocument *pDoc _INIT(&gDoc);\n#endif\nWLED_GLOBAL volatile uint8_t jsonBufferLock _INIT(0);\n\n// enable additional debug output\n#if defined(WLED_DEBUG_HOST)\n  #include \"net_debug.h\"\n  // On the host side, use netcat to receive the log statements: nc -l 7868 -u\n  // use -D WLED_DEBUG_HOST='\"192.168.xxx.xxx\"' or FQDN within quotes\n  #define DEBUGOUT NetDebug\n  WLED_GLOBAL bool netDebugEnabled _INIT(true);\n  WLED_GLOBAL char netDebugPrintHost[33] _INIT(WLED_DEBUG_HOST);\n  #ifndef WLED_DEBUG_PORT\n    #define WLED_DEBUG_PORT 7868\n  #endif\n  WLED_GLOBAL int netDebugPrintPort _INIT(WLED_DEBUG_PORT);\n#else\n  #define DEBUGOUT Serial\n#endif\n\n#ifdef WLED_DEBUG\n  #ifndef ESP8266\n  #include <rom/rtc.h>\n  #endif\n  #define DEBUG_PRINT(x) DEBUGOUT.print(x)\n  #define DEBUG_PRINTLN(x) DEBUGOUT.println(x)\n  #define DEBUG_PRINTF(x...) DEBUGOUT.printf(x)\n  #define DEBUG_PRINTF_P(x...) DEBUGOUT.printf_P(x)\n#else\n  #define DEBUG_PRINT(x)\n  #define DEBUG_PRINTLN(x)\n  #define DEBUG_PRINTF(x...)\n  #define DEBUG_PRINTF_P(x...)\n#endif\n\n#ifdef WLED_DEBUG_FS\n  #define DEBUGFS_PRINT(x) DEBUGOUT.print(x)\n  #define DEBUGFS_PRINTLN(x) DEBUGOUT.println(x)\n  #define DEBUGFS_PRINTF(x...) DEBUGOUT.printf(x)\n#else\n  #define DEBUGFS_PRINT(x)\n  #define DEBUGFS_PRINTLN(x)\n  #define DEBUGFS_PRINTF(x...)\n#endif\n\n// debug macro variable definitions\n#ifdef WLED_DEBUG\n  WLED_GLOBAL unsigned long debugTime _INIT(0);\n  WLED_GLOBAL int lastWifiState _INIT(3);\n  WLED_GLOBAL unsigned long wifiStateChangedTime _INIT(0);\n  WLED_GLOBAL unsigned loops _INIT(0);\n#endif\n\n#define WLED_CONNECTED (Network.isConnected())\n\n#ifndef WLED_AP_SSID_UNIQUE\n  #define WLED_SET_AP_SSID() do { \\\n    strcpy_P(apSSID, PSTR(WLED_AP_SSID)); \\\n  } while(0)\n#else\n  #define WLED_SET_AP_SSID() do { \\\n    snprintf_P(\\\n      apSSID, \\\n      sizeof(apSSID)-1, \\\n      PSTR(\"%s-%s\"), \\\n      WLED_BRAND, \\\n      escapedMac.c_str()+6 \\\n    ); \\\n  } while(0)\n#endif\n\n//macro to convert F to const\n#define SET_F(x)  (const char*)F(x)\n\n//color mangling macros\n#define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b))))\n#define R(c) (byte((c) >> 16))\n#define G(c) (byte((c) >> 8))\n#define B(c) (byte(c))\n#define W(c) (byte((c) >> 24))\n\nclass WLED {\npublic:\n  WLED();\n  static WLED& instance()\n  {\n    static WLED instance;\n    return instance;\n  }\n\n  // boot starts here\n  void setup();\n\n  void loop();\n  void reset();\n\n  void beginStrip();\n  void handleConnection();\n  void initAP(bool resetAP = false);\n  void initConnection();\n  void initInterfaces();\n  #if defined(STATUSLED)\n  void handleStatusLED();\n  #endif\n  #if WLED_WATCHDOG_TIMEOUT > 0\n  void enableWatchdog();\n  void disableWatchdog();\n  #endif\n};\n#endif        // WLED_H\n"
  },
  {
    "path": "wled00/wled_ethernet.h",
    "content": "#ifndef WLED_ETHERNET_H\n#define WLED_ETHERNET_H\n\n#include \"pin_manager.h\"\n\n#ifdef WLED_USE_ETHERNET\n\n// For ESP32, the remaining five pins are at least somewhat configurable.\n// eth_address  is in range [0..31], indicates which PHY (MAC?) address should be allocated to the interface\n// eth_power    is an output GPIO pin used to enable/disable the ethernet port (and/or external oscillator)\n// eth_mdc      is an output GPIO pin used to provide the clock for the management data\n// eth_mdio     is an input/output GPIO pin used to transfer management data\n// eth_type     is the physical ethernet module's type (ETH_PHY_LAN8720, ETH_PHY_TLK110)\n// eth_clk_mode defines the GPIO pin and GPIO mode for the clock signal\n//              However, there are really only four configurable options on ESP32:\n//              ETH_CLOCK_GPIO0_IN    == External oscillator, clock input  via GPIO0\n//              ETH_CLOCK_GPIO0_OUT   == ESP32 provides 50MHz clock output via GPIO0\n//              ETH_CLOCK_GPIO16_OUT  == ESP32 provides 50MHz clock output via GPIO16\n//              ETH_CLOCK_GPIO17_OUT  == ESP32 provides 50MHz clock output via GPIO17\ntypedef struct EthernetSettings {\n  uint8_t        eth_address;\n  int            eth_power;\n  int            eth_mdc;\n  int            eth_mdio;\n  eth_phy_type_t eth_type;\n  eth_clock_mode_t eth_clk_mode;\n} ethernet_settings;\n\nextern const ethernet_settings ethernetBoards[];\n\n#define WLED_ETH_RSVD_PINS_COUNT 6\nextern const managed_pin_type esp32_nonconfigurable_ethernet_pins[WLED_ETH_RSVD_PINS_COUNT];\n#endif\n\n#endif"
  },
  {
    "path": "wled00/wled_main.cpp",
    "content": "#include <Arduino.h>\n/*\n * WLED Arduino IDE compatibility file.\n * (this is the former wled00.ino)\n * \n * Where has everything gone?\n * \n * In April 2020, the project's structure underwent a major change.\n * We now use the platformIO build system, and building WLED in Arduino IDE is not supported any more.\n * Global variables are now found in file \"wled.h\"\n * Global function declarations are found in \"fcn_declare.h\"\n * \n * Usermod compatibility: Existing wled06_usermod.ino mods should continue to work. Delete usermod.cpp.\n * New usermods should use usermod.cpp instead.\n */\n#include \"wled.h\"\n\nvoid setup() {\n  WLED::instance().setup();\n}\n\nvoid loop() {\n  WLED::instance().loop();\n}\n"
  },
  {
    "path": "wled00/wled_math.cpp",
    "content": "/*\n * Contains some trigonometric functions.\n * The ANSI C equivalents are likely faster, but using any sin/cos/tan function incurs a memory penalty of 460 bytes on ESP8266, likely for lookup tables.\n * This implementation has no extra static memory usage.\n *\n * Source of the cos_t() function: https://web.eecs.utk.edu/~azh/blog/cosine.html (cos_taylor_literal_6terms)\n */\n\n#include <Arduino.h> //PI constant\n\n//#define WLED_DEBUG_MATH\n\n// Note: cos_t, sin_t and tan_t are very accurate but slow\n// the math.h functions use several kB of flash and are to be avoided if possible\n// sin16_t / cos16_t are faster and much more accurate than the fastled variants\n// sin_approx and cos_approx are float wrappers for sin16_t/cos16_t and have an accuracy better than +/-0.0015 compared to sinf()\n// sin8_t / cos8_t are fastled replacements and use sin16_t / cos16_t. Slightly slower than fastled version but very accurate\n\n\n// Taylor series approximations, replaced with Bhaskara I's approximation\n/*\n#define modd(x, y) ((x) - (int)((x) / (y)) * (y))\n\nfloat cos_t(float phi)\n{\n  float x = modd(phi, M_TWOPI);\n  if (x < 0) x = -1 * x;\n  int8_t sign = 1;\n  if (x > M_PI)\n  {\n      x -= M_PI;\n      sign = -1;\n  }\n  float xx = x * x;\n\n  float res = sign * (1 - ((xx) / (2)) + ((xx * xx) / (24)) - ((xx * xx * xx) / (720)) + ((xx * xx * xx * xx) / (40320)) - ((xx * xx * xx * xx * xx) / (3628800)) + ((xx * xx * xx * xx * xx * xx) / (479001600)));\n  #ifdef WLED_DEBUG_MATH\n  Serial.printf(\"cos: %f,%f,%f,(%f)\\n\",phi,res,cos(x),res-cos(x));\n  #endif\n  return res;\n}\n\nfloat sin_t(float phi) {\n  float res =  cos_t(M_PI_2 - phi);\n  #ifdef WLED_DEBUG_MATH\n  Serial.printf(\"sin: %f,%f,%f,(%f)\\n\",x,res,sin(x),res-sin(x));\n  #endif\n  return res;\n}\n\nfloat tan_t(float x) {\n  float c = cos_t(x);\n  if (c==0.0f) return 0;\n  float res = sin_t(x) / c;\n  #ifdef WLED_DEBUG_MATH\n  Serial.printf(\"tan: %f,%f,%f,(%f)\\n\",x,res,tan(x),res-tan(x));\n  #endif\n  return res;\n}\n*/\n\n// 16-bit, integer based Bhaskara I's sine approximation: 16*x*(pi - x) / (5*pi^2 - 4*x*(pi - x))\n// input is 16bit unsigned (0-65535), output is 16bit signed (-32767 to +32767)\n// optimized integer implementation by @dedehai\nint16_t sin16_t(uint16_t theta) {\n  int scale = 1;\n  if (theta > 0x7FFF) {\n    theta = 0xFFFF - theta;\n    scale = -1; // second half of the sine function is negative (pi - 2*pi)\n  }\n  uint32_t precal = theta * (0x7FFF - theta);\n  uint64_t numerator = (uint64_t)precal * (4 * 0x7FFF); // 64bit required\n  int32_t denominator = 1342095361 - precal; // 1342095361 is 5 * 0x7FFF^2 / 4\n  int16_t result = numerator / denominator;\n  return result * scale;\n}\n\nint16_t cos16_t(uint16_t theta) {\n  return sin16_t(theta + 0x4000); //cos(x) = sin(x+pi/2)\n}\n\nuint8_t sin8_t(uint8_t theta) {\n  int32_t sin16 = sin16_t((uint16_t)theta * 257); // 255 * 257 = 0xFFFF\n  sin16 += 0x7FFF + 128; //shift result to range 0-0xFFFF, +128 for rounding\n  return min(sin16, int32_t(0xFFFF)) >> 8; // min performs saturation, and prevents overflow\n}\n\nuint8_t cos8_t(uint8_t theta) {\n  return sin8_t(theta + 64); //cos(x) = sin(x+pi/2)\n}\n\nfloat sin_approx(float theta) {\n  uint16_t scaled_theta = (int)(theta * (float)(0xFFFF / M_TWOPI)); // note: do not cast negative float to uint! cast to int first (undefined on C3)\n  int32_t result = sin16_t(scaled_theta);\n  float sin = float(result) / 0x7FFF;\n  return sin;\n}\n\nfloat cos_approx(float theta) {\n  uint16_t scaled_theta = (int)(theta * (float)(0xFFFF / M_TWOPI)); // note: do not cast negative float to uint! cast to int first (undefined on C3)\n  int32_t result = sin16_t(scaled_theta + 0x4000);\n  float cos = float(result) / 0x7FFF;\n  return cos;\n}\n\nfloat tan_approx(float x) {\n  float c = cos_approx(x);\n  if (c==0.0f) return 0;\n  float res = sin_approx(x) / c;\n  return res;\n}\n\n#define ATAN2_CONST_A 0.1963f\n#define ATAN2_CONST_B 0.9817f\n\n// atan2_t approximation, with the idea from https://gist.github.com/volkansalma/2972237?permalink_comment_id=3872525#gistcomment-3872525\nfloat atan2_t(float y, float x) {\n\tfloat abs_y = fabs(y);\n  float abs_x = fabs(x);\n  float r = (abs_x - abs_y) / (abs_y + abs_x + 1e-10f); // avoid division by zero by adding a small nubmer\n  float angle;\n  if(x < 0) {\n    r = -r;\n    angle = M_PI_2 + M_PI_4;\n  }\n  else\n    angle = M_PI_2 - M_PI_4;\n\n  float add = (ATAN2_CONST_A * (r * r) - ATAN2_CONST_B) * r;\n\tangle += add;\n  angle = y < 0 ? -angle : angle;\n\treturn angle;\n}\n\n//https://stackoverflow.com/questions/3380628\n// Absolute error <= 6.7e-5\nfloat acos_t(float x) {\n  float negate = float(x < 0);\n  float xabs = std::abs(x);\n  float ret = -0.0187293f;\n  ret = ret * xabs;\n  ret = ret + 0.0742610f;\n  ret = ret * xabs;\n  ret = ret - 0.2121144f;\n  ret = ret * xabs;\n  ret = ret + M_PI_2;\n  ret = ret * sqrt(1.0f-xabs);\n  ret = ret - 2 * negate * ret;\n  float res = negate * M_PI + ret;\n  #ifdef WLED_DEBUG_MATH\n  Serial.printf(\"acos: %f,%f,%f,(%f)\\n\",x,res,acos(x),res-acos(x));\n  #endif\n  return res;\n}\n\nfloat asin_t(float x) {\n  float res = M_PI_2 - acos_t(x);\n  #ifdef WLED_DEBUG_MATH\n  Serial.printf(\"asin: %f,%f,%f,(%f)\\n\",x,res,asin(x),res-asin(x));\n  #endif\n  return res;\n}\n\n// declare a template with no implementation, and only one specialization\n// this allows hiding the constants, while ensuring ODR causes optimizations\n// to still apply.  (Fixes issues with conflicting 3rd party #define's)\ntemplate <typename T> T atan_t(T x);\ntemplate<>\nfloat atan_t(float x) {\n  //For A/B/C, see https://stackoverflow.com/a/42542593\n  static const double A { 0.0776509570923569 };\n  static const double B { -0.287434475393028 };\n  static const double C { ((M_PI_4) - A - B) };\n  // polynominal factors for approximation between 1 and 5\n  static const float C0 {  0.089494f };\n  static const float C1 {  0.974207f };\n  static const float C2 { -0.326175f };\n  static const float C3 {  0.05375f  };\n  static const float C4 { -0.003445f };\n\n  #ifdef WLED_DEBUG_MATH\n  float xinput = x;\n  #endif\n  bool neg = (x < 0);\n  x = std::abs(x);\n  float res;\n  if (x > 5.0f) { // atan(x) converges to pi/2 - (1/x) for large values\n    res = M_PI_2 - (1.0f/x);\n  } else if (x > 1.0f) { //1 < x < 5\n    float xx = x * x;\n    res = (C4*xx*xx)+(C3*xx*x)+(C2*xx)+(C1*x)+C0;\n  } else { // this approximation is only for x <= 1\n    float xx = x * x;\n    res = ((A*xx + B)*xx + C)*x;\n  }\n  if (neg) {\n    res = -res;\n  }\n  #ifdef WLED_DEBUG_MATH\n  Serial.printf(\"atan: %f,%f,%f,(%f)\\n\",xinput,res,atan(xinput),res-atan(xinput));\n  #endif\n  return res;\n}\n\nfloat floor_t(float x) {\n  bool neg = x < 0;\n  int val = x;\n  if (neg) val--;\n  #ifdef WLED_DEBUG_MATH\n  Serial.printf(\"floor: %f,%f,%f\\n\",x,(float)val,floor(x));\n  #endif\n  return val;\n}\n\nfloat fmod_t(float num, float denom) {\n  int tquot = num / denom;\n  float res = num - tquot * denom;\n  #ifdef WLED_DEBUG_MATH\n  Serial.printf(\"fmod: %f,%f,(%f)\\n\",res,fmod(num,denom),res-fmod(num,denom));\n  #endif\n  return res;\n}\n\n// bit-wise integer square root calculation (exact)\nuint32_t sqrt32_bw(uint32_t x) {\n  uint32_t res = 0;\n  uint32_t bit;\n  uint32_t num = x; // use 32bit for faster calculation\n\n  if(num < 1 << 10)  bit = 1 << 10; // speed optimization for small numbers < 32^2\n  else if (num < 1 << 20) bit = 1 << 20; // speed optimization for medium numbers < 1024^2\n  else bit = 1 << 30; // start with highest power of 4 <= 2^32\n\n  while (bit > num) bit >>= 2; // reduce iterations\n\n  while (bit != 0) {\n    if (num >= res + bit) {\n      num -= res + bit;\n      res = (res >> 1) + bit;\n    } else {\n      res >>= 1;\n    }\n    bit >>= 2;\n  }\n  return res;\n}\n"
  },
  {
    "path": "wled00/wled_metadata.cpp",
    "content": "#include \"ota_update.h\"\n#include \"wled.h\"\n#include \"wled_metadata.h\"\n\n#ifndef WLED_VERSION\n  #warning WLED_VERSION was not set - using default value of 'dev'\n  #define WLED_VERSION dev\n#endif\n#ifndef WLED_RELEASE_NAME\n  #warning WLED_RELEASE_NAME was not set - using default value of 'Custom'\n  #define WLED_RELEASE_NAME \"Custom\"\n#endif\n#ifndef WLED_REPO\n  // No warning for this one: integrators are not always on GitHub\n  #define WLED_REPO \"unknown\"\n#endif\n\nconstexpr uint32_t WLED_CUSTOM_DESC_MAGIC = 0x57535453;  // \"WSTS\" (WLED System Tag Structure)\nconstexpr uint32_t WLED_CUSTOM_DESC_VERSION = 2;    // v1 - original PR; v2 - \"safe to update from\" version\n\n// Compile-time validation that release name doesn't exceed maximum length\nstatic_assert(sizeof(WLED_RELEASE_NAME) <= WLED_RELEASE_NAME_MAX_LEN, \n              \"WLED_RELEASE_NAME exceeds maximum length of WLED_RELEASE_NAME_MAX_LEN characters\");\n\n\n/**\n * DJB2 hash function (C++11 compatible constexpr)\n * Used for compile-time hash computation to validate structure contents\n * Recursive for compile time: not usable at runtime due to stack depth\n * \n * Note that this only works on strings; there is no way to produce a compile-time\n * hash of a struct in C++11 without explicitly listing all the struct members.\n * So for now, we hash only the release name.  This suffices for a \"did you find \n * valid structure\" check.\n * \n */\nconstexpr uint32_t djb2_hash_constexpr(const char* str, uint32_t hash = 5381) {\n    return (*str == '\\0') ? hash : djb2_hash_constexpr(str + 1, ((hash << 5) + hash) + *str);\n}\n\n/**\n * Runtime DJB2 hash function for validation\n */\ninline uint32_t djb2_hash_runtime(const char* str) {\n    uint32_t hash = 5381;\n    while (*str) {\n        hash = ((hash << 5) + hash) + *str++;\n    }\n    return hash;\n}\n\n// ------------------------------------\n// GLOBAL VARIABLES\n// ------------------------------------\n// Structure instantiation for this build \nconst wled_metadata_t __attribute__((section(BUILD_METADATA_SECTION))) WLED_BUILD_DESCRIPTION = {\n    WLED_CUSTOM_DESC_MAGIC,                   // magic\n    /*WLED_CUSTOM_DESC_VERSION*/ 1,           // structure version.  Currently set to 1 to allow OTA from broken original version. FIXME before 0.16 release.\n    TOSTRING(WLED_VERSION),\n    WLED_RELEASE_NAME,                        // release_name\n    std::integral_constant<uint32_t, djb2_hash_constexpr(WLED_RELEASE_NAME)>::value, // hash - computed at compile time; integral_constant enforces this\n#if defined(ESP32) && defined(CONFIG_IDF_TARGET_ESP32)\n    { 0, 15, 3 },  // Some older ESP32 might have bootloader issues; assume we'll have it sorted by 0.15.3\n#else    \n    { 0, 15, 2 },  // All other platforms can update safely\n#endif\n};\n\nstatic const char repoString_s[] PROGMEM = WLED_REPO;\nconst __FlashStringHelper* repoString = FPSTR(repoString_s);\n\nstatic const char productString_s[] PROGMEM = WLED_PRODUCT_NAME;\nconst __FlashStringHelper* productString = FPSTR(productString_s);\n\nstatic const char brandString_s [] PROGMEM = WLED_BRAND;\nconst __FlashStringHelper* brandString = FPSTR(brandString_s);\n\n\n\n/**\n * Extract WLED custom description structure from binary\n * @param binaryData Pointer to binary file data\n * @param dataSize Size of binary data in bytes\n * @param extractedDesc Buffer to store extracted custom description structure\n * @return true if structure was found and extracted, false otherwise\n */\nbool findWledMetadata(const uint8_t* binaryData, size_t dataSize, wled_metadata_t* extractedDesc) {\n  if (!binaryData || !extractedDesc || dataSize < sizeof(wled_metadata_t)) {\n    return false;\n  }\n\n  for (size_t offset = 0; offset <= dataSize - sizeof(wled_metadata_t); offset++) {\n    if ((binaryData[offset]) == static_cast<char>(WLED_CUSTOM_DESC_MAGIC)) {\n      // First byte matched; check next in an alignment-safe way\n      uint32_t data_magic;\n      memcpy(&data_magic, binaryData + offset, sizeof(data_magic));\n      \n      // Check for magic number\n      if (data_magic == WLED_CUSTOM_DESC_MAGIC) {            \n        wled_metadata_t candidate;\n        memcpy(&candidate, binaryData + offset, sizeof(candidate));\n\n        // Validate hash using runtime function\n        uint32_t expected_hash = djb2_hash_runtime(candidate.release_name);\n        if (candidate.hash != expected_hash) {\n          DEBUG_PRINTF_P(PSTR(\"Found WLED structure at offset %u but hash mismatch\\n\"), offset);\n          continue;\n        }\n        \n        // Valid structure found - copy entire structure\n        *extractedDesc = candidate;\n        \n        DEBUG_PRINTF_P(PSTR(\"Extracted WLED structure at offset %u: '%s'\\n\"), \n                      offset, extractedDesc->release_name);\n        return true;\n      }\n    }\n  }\n  \n  DEBUG_PRINTLN(F(\"No WLED custom description found in binary\"));\n  return false;\n}\n\n\n// Strip \"_V4\" suffix from a release name to allow upgrading between IDF v4 and newer IDF builds.\nstatic String normalizeReleaseName(const String& name) {\n  if (name.endsWith(\"_V4\")) return name.substring(0, name.length() - 3);\n  return name;\n}\n\ntemplate<size_t len>\nstatic inline String bufToString(const char (&buf)[len]) {\n  char sbuf[len+1];\n  size_t real_len = strnlen(buf, len);\n  memcpy(sbuf, buf, real_len);\n  sbuf[len] = '\\0';\n  return sbuf;\n}\n\n/**\n * Check if OTA should be allowed based on release compatibility using custom description\n * @param firmwareDescription Description object from proposed new firmware\n * @param errorMessage Buffer to store error message if validation fails \n * @param errorMessageLen Maximum length of error message buffer\n * @return true if OTA should proceed, false if it should be blocked\n */\nbool shouldAllowOTA(const wled_metadata_t& firmwareDescription, char* errorMessage, size_t errorMessageLen) {\n  // Clear error message\n  if (errorMessage && errorMessageLen > 0) {\n    errorMessage[0] = '\\0';\n  }\n\n  const String uploadedRelease = bufToString(firmwareDescription.release_name);\n  \n  if (normalizeReleaseName(uploadedRelease) != normalizeReleaseName(releaseString)) {\n    if (errorMessage && errorMessageLen > 0) {\n      snprintf_P(errorMessage, errorMessageLen, PSTR(\"Firmware release name mismatch: current='%s', uploaded='%s'.\"),\n                releaseString, uploadedRelease.c_str());\n      errorMessage[errorMessageLen - 1] = '\\0'; // Ensure null termination\n    }\n    return false;\n  }\n\n  if (firmwareDescription.desc_version > 1) {\n    // Add safe version check\n    // Parse our version (x.y.z) and compare it to the \"safe version\" array\n    const char* our_version = versionString;\n    for(unsigned v_index = 0; v_index < 3; ++v_index) {\n      char* our_version_end = nullptr;\n      long our_v_parsed = strtol(our_version, &our_version_end, 10); \n      if (!our_version_end || (our_version_end == our_version)) {\n        // We were built with a malformed version string\n        // We blame the integrator and attempt the update anyways - nothing the user can do to fix this\n        break;\n      }\n\n      if (firmwareDescription.safe_update_version[v_index] > our_v_parsed) {\n        if (errorMessage && errorMessageLen > 0) {\n          snprintf_P(errorMessage, errorMessageLen, PSTR(\"Cannot update from this version: requires at least %d.%d.%d, current='%s'.\"), \n                  firmwareDescription.safe_update_version[0], firmwareDescription.safe_update_version[1], firmwareDescription.safe_update_version[2],\n                  versionString);\n          errorMessage[errorMessageLen - 1] = '\\0'; // Ensure null termination\n        }\n        return false;\n      } else if (firmwareDescription.safe_update_version[v_index] < our_v_parsed) {\n        break;  // no need to check the other components\n      }\n\n      if (*our_version_end == '.') ++our_version_end;\n      our_version = our_version_end;\n    }  \n  }\n\n  // TODO: additional checks go here\n\n  return true;\n}\n"
  },
  {
    "path": "wled00/wled_metadata.h",
    "content": "/*\n  WLED build metadata\n\n  Manages and exports information about the current WLED build.\n*/\n\n\n#pragma once\n\n#include <cstdint>\n#include <string.h>\n#include <WString.h>\n\n#define WLED_VERSION_MAX_LEN 48\n#define WLED_RELEASE_NAME_MAX_LEN 48\n\n/**\n * WLED Custom Description Structure\n * This structure is embedded in platform-specific sections at an approximately\n * fixed offset in ESP32/ESP8266 binaries, where it can be found and validated \n * by the OTA process.\n */\ntypedef struct {\n    uint32_t magic;               // Magic number to identify WLED custom description\n    uint32_t desc_version;        // Structure version for future compatibility\n    char wled_version[WLED_VERSION_MAX_LEN];\n    char release_name[WLED_RELEASE_NAME_MAX_LEN]; // Release name (null-terminated)    \n    uint32_t hash;               // Structure sanity check\n    uint8_t safe_update_version[3]; // Indicates version it's known to be safe to install this update from: major, minor, patch\n} __attribute__((packed)) wled_metadata_t;\n\n\n// Global build description\nextern const wled_metadata_t WLED_BUILD_DESCRIPTION;\n\n// Convenient metdata pointers\n#define versionString (WLED_BUILD_DESCRIPTION.wled_version)   // Build version, WLED_VERSION\n#define releaseString (WLED_BUILD_DESCRIPTION.release_name)   // Release name,  WLED_RELEASE_NAME\nextern const __FlashStringHelper* repoString;                       // Github repository (if available)\nextern const __FlashStringHelper* productString;                    // Product, WLED_PRODUCT_NAME -- deprecated, use WLED_RELEASE_NAME\nextern const __FlashStringHelper* brandString ;                     // Brand\n\n\n// Metadata analysis functions\n\n/**\n * Extract WLED custom description structure from binary data\n * @param binaryData Pointer to binary file data\n * @param dataSize Size of binary data in bytes\n * @param extractedDesc Buffer to store extracted custom description structure\n * @return true if structure was found and extracted, false otherwise\n */\nbool findWledMetadata(const uint8_t* binaryData, size_t dataSize, wled_metadata_t* extractedDesc);\n\n/**\n * Check if OTA should be allowed based on release compatibility\n * @param firmwareDescription Pointer to firmware description\n * @param errorMessage Buffer to store error message if validation fails \n * @param errorMessageLen Maximum length of error message buffer\n * @return true if OTA should proceed, false if it should be blocked\n */\nbool shouldAllowOTA(const wled_metadata_t& firmwareDescription, char* errorMessage, size_t errorMessageLen);\n"
  },
  {
    "path": "wled00/wled_serial.cpp",
    "content": "#include \"wled.h\"\n\n// forward declarations\nstatic void sendBytes();\n\n/*\n * Adalight and TPM2 handler\n */\n\nenum class AdaState {\n  Header_A,\n  Header_d,\n  Header_a,\n  Header_CountHi,\n  Header_CountLo,\n  Header_CountCheck,\n  Data_Red,\n  Data_Green,\n  Data_Blue,\n  TPM2_Header_Type,\n  TPM2_Header_CountHi,\n  TPM2_Header_CountLo,\n};\n\nstatic uint16_t currentBaud = 1152; //default baudrate 115200 (divided by 100)\nstatic bool continuousSendLED = false;\nstatic uint32_t lastUpdate = 0;\n\nvoid updateBaudRate(uint32_t rate){\n  unsigned rate100 = rate/100;\n  if (rate100 == currentBaud || rate100 < 96) return;\n  currentBaud = rate100;\n\n  if (serialCanTX){\n    Serial.print(F(\"Baud is now \")); Serial.println(rate);\n  }\n\n  Serial.flush();\n  Serial.begin(rate);\n}\n\n// RGB LED data return as JSON array. Slow, but easy to use on the other end.\nstatic inline void sendJSON(){\n  if (serialCanTX) {\n    unsigned used = strip.getLengthTotal();\n    Serial.write('[');\n    for (unsigned i=0; i<used; i++) {\n      Serial.print(strip.getPixelColor(i));\n      if (i != used-1) Serial.write(',');\n    }\n    Serial.println(\"]\");\n  }\n}\n\n// RGB LED data returned as bytes in TPM2 format. Faster, and slightly less easy to use on the other end.\nstatic void sendBytes(){\n  if (serialCanTX) {\n    Serial.write(0xC9); Serial.write(0xDA);\n    unsigned used = strip.getLengthTotal();\n    unsigned len = used*3;\n    Serial.write(highByte(len));\n    Serial.write(lowByte(len));\n    for (unsigned i=0; i < used; i++) {\n      uint32_t c = strip.getPixelColor(i);\n      Serial.write(qadd8(W(c), R(c))); //R, add white channel to RGB channels as a simple RGBW -> RGB map\n      Serial.write(qadd8(W(c), G(c))); //G\n      Serial.write(qadd8(W(c), B(c))); //B\n    }\n    Serial.write(0x36); Serial.write('\\n');\n  }\n}\n\nvoid handleSerial()\n{\n  if (!(serialCanRX && Serial)) return; // arduino docs: `if (Serial)` indicates whether or not the USB CDC serial connection is open. For all non-USB CDC ports, this will always return true\n\n  static auto state = AdaState::Header_A;\n  static uint16_t count = 0;\n  static uint16_t pixel = 0;\n  static byte check = 0x00;\n  static byte red   = 0x00;\n  static byte green = 0x00;\n\n  while (Serial.available() > 0)\n  {\n    yield();\n    byte next = Serial.peek();\n    switch (state) {\n      case AdaState::Header_A:\n        if      (next == 'A')  { state = AdaState::Header_d; }\n        else if (next == 0xC9) { state = AdaState::TPM2_Header_Type; } //TPM2 start byte\n        else if (next == 'I')  { handleImprovPacket(); return; }\n        else if (next == 'v')  { Serial.print(\"WLED\"); Serial.write(' '); Serial.println(VERSION); }\n        else if (next == 0xB0) { updateBaudRate( 115200); }\n        else if (next == 0xB1) { updateBaudRate( 230400); }\n        else if (next == 0xB2) { updateBaudRate( 460800); }\n        else if (next == 0xB3) { updateBaudRate( 500000); }\n        else if (next == 0xB4) { updateBaudRate( 576000); }\n        else if (next == 0xB5) { updateBaudRate( 921600); }\n        else if (next == 0xB6) { updateBaudRate(1000000); }\n        else if (next == 0xB7) { updateBaudRate(1500000); }\n        else if (next == 'l')  { sendJSON(); } // Send LED data as JSON Array\n        else if (next == 'L')  { sendBytes(); } // Send LED data as TPM2 Data Packet\n        else if (next == 'o')  { continuousSendLED = false; } // Disable Continuous Serial Streaming\n        else if (next == 'O')  { continuousSendLED = true; } // Enable Continuous Serial Streaming\n        else if (next == '{')  { //JSON API\n          bool verboseResponse = false;\n          if (!requestJSONBufferLock(JSON_LOCK_SERIAL)) {\n            Serial.printf_P(PSTR(\"{\\\"error\\\":%d}\\n\"), ERR_NOBUF);\n            return;\n          }\n          Serial.setTimeout(100);\n          DeserializationError error = deserializeJson(*pDoc, Serial);\n          if (!error) {\n            verboseResponse = deserializeState(pDoc->as<JsonObject>());\n            //only send response if TX pin is unused for other purposes\n            if (verboseResponse && serialCanTX) {\n              pDoc->clear();\n              JsonObject stateDoc = pDoc->createNestedObject(\"state\");\n              serializeState(stateDoc);\n              JsonObject info  = pDoc->createNestedObject(\"info\");\n              serializeInfo(info);\n\n              serializeJson(*pDoc, Serial);\n              Serial.println();\n            }\n          }\n          releaseJSONBufferLock();\n        }\n        break;\n      case AdaState::Header_d:\n        if (next == 'd') state = AdaState::Header_a;\n        else             state = AdaState::Header_A;\n        break;\n      case AdaState::Header_a:\n        if (next == 'a') state = AdaState::Header_CountHi;\n        else             state = AdaState::Header_A;\n        break;\n      case AdaState::Header_CountHi:\n        pixel = 0;\n        count = next * 0x100;\n        check = next;\n        state = AdaState::Header_CountLo;\n        break;\n      case AdaState::Header_CountLo:\n        count += next + 1;\n        check = check ^ next ^ 0x55;\n        state = AdaState::Header_CountCheck;\n        break;\n      case AdaState::Header_CountCheck:\n        if (check == next) state = AdaState::Data_Red;\n        else               state = AdaState::Header_A;\n        break;\n      case AdaState::TPM2_Header_Type:\n        state = AdaState::Header_A; //(unsupported) TPM2 command or invalid type\n        if (next == 0xDA) state = AdaState::TPM2_Header_CountHi; //TPM2 data\n        else if (next == 0xAA) Serial.write(0xAC); //TPM2 ping\n        break;\n      case AdaState::TPM2_Header_CountHi:\n        pixel = 0;\n        count = (next * 0x100) /3;\n        state = AdaState::TPM2_Header_CountLo;\n        break;\n      case AdaState::TPM2_Header_CountLo:\n        count += next /3;\n        state = AdaState::Data_Red;\n        break;\n      case AdaState::Data_Red:\n        red   = next;\n        state = AdaState::Data_Green;\n        break;\n      case AdaState::Data_Green:\n        green = next;\n        state = AdaState::Data_Blue;\n        break;\n      case AdaState::Data_Blue:\n        byte blue  = next;\n        if (!realtimeOverride) setRealtimePixel(pixel++, red, green, blue, 0);\n        if (--count > 0) state = AdaState::Data_Red;\n        else {\n          realtimeLock(realtimeTimeoutMs, REALTIME_MODE_ADALIGHT);\n\n          if (!realtimeOverride) strip.show();\n          state = AdaState::Header_A;\n        }\n        break;\n    }\n\n    // All other received bytes will disable Continuous Serial Streaming\n    if (continuousSendLED && next != 'O'){\n      continuousSendLED = false;\n    }\n\n    Serial.read(); //discard the byte\n  }\n\n  // If Continuous Serial Streaming is enabled, send new LED data as bytes\n  if (continuousSendLED && (lastUpdate != strip.getLastShow())){\n    sendBytes();\n    lastUpdate = strip.getLastShow();\n  }\n}\n"
  },
  {
    "path": "wled00/wled_server.cpp",
    "content": "#include \"wled.h\"\n\n#ifndef WLED_DISABLE_OTA\n  #include \"ota_update.h\"  \n#endif\n#include \"html_ui.h\"\n#include \"html_settings.h\"\n#include \"html_other.h\"\n#include \"js_iro.h\"\n#include \"js_omggif.h\"\n#ifdef WLED_ENABLE_PIXART\n  #include \"html_pixart.h\"\n#endif\n#ifdef WLED_ENABLE_PXMAGIC\n  #include \"html_pxmagic.h\"\n#endif\n#ifndef WLED_DISABLE_PIXELFORGE\n  #include \"html_pixelforge.h\"\n#endif\n#include \"html_cpal.h\"\n#include \"html_edit.h\"\n\n// forward declarations\nstatic void createEditHandler();\n\n\n// define flash strings once (saves flash memory)\nstatic const char s_redirecting[] PROGMEM = \"Redirecting...\";\nstatic const char s_content_enc[] PROGMEM = \"Content-Encoding\";\nstatic const char s_unlock_ota [] PROGMEM = \"Please unlock OTA in security settings!\";\nstatic const char s_unlock_cfg [] PROGMEM = \"Please unlock settings using PIN code!\";\nstatic const char s_rebooting  [] PROGMEM = \"Rebooting now...\";\nstatic const char s_notimplemented[] PROGMEM = \"Not implemented\";\nstatic const char s_accessdenied[]   PROGMEM = \"Access Denied\";\nstatic const char s_not_found[]      PROGMEM = \"Not found\";\nstatic const char s_wsec[]           PROGMEM = \"wsec.json\";\nstatic const char s_func[]           PROGMEM = \"func\";\nstatic const char s_list[]           PROGMEM = \"list\";\nstatic const char s_path[]           PROGMEM = \"path\";\nstatic const char s_cache_control[]  PROGMEM = \"Cache-Control\";\nstatic const char s_no_store[]       PROGMEM = \"no-store\";\nstatic const char s_expires[]        PROGMEM = \"Expires\";\nstatic const char _common_js[]       PROGMEM = \"/common.js\";\nstatic const char _iro_js[]          PROGMEM = \"/iro.js\";\nstatic const char _omggif_js[]       PROGMEM = \"/omggif.js\";\n\n//Is this an IP?\nstatic bool isIp(const String &str) {\n  for (size_t i = 0; i < str.length(); i++) {\n    int c = str.charAt(i);\n    if (c != '.' && (c < '0' || c > '9')) {\n      return false;\n    }\n  }\n  return true;\n}\n\nstatic bool inSubnet(const IPAddress &ip, const IPAddress &subnet, const IPAddress &mask) {\n  return (((uint32_t)ip & (uint32_t)mask) == ((uint32_t)subnet & (uint32_t)mask));\n}\n\nstatic bool inSameSubnet(const IPAddress &client) {\n  return inSubnet(client, Network.localIP(), Network.subnetMask());\n}\n\nstatic bool inLocalSubnet(const IPAddress &client) {\n  return  inSubnet(client, IPAddress(10,0,0,0),    IPAddress(255,0,0,0))                  // 10.x.x.x\n      ||  inSubnet(client, IPAddress(192,168,0,0), IPAddress(255,255,0,0))                // 192.168.x.x\n      ||  inSubnet(client, IPAddress(172,16,0,0),  IPAddress(255,240,0,0))                // 172.16.x.x\n      || (inSubnet(client, IPAddress(4,3,2,0),     IPAddress(255,255,255,0)) && apActive) // WLED AP\n      ||  inSameSubnet(client);                                                           // same subnet as WLED device\n}\n\n/*\n * Integrated HTTP web server page declarations\n */\n\nstatic void generateEtag(char *etag, uint16_t eTagSuffix) {\n  sprintf_P(etag, PSTR(\"%u-%02x-%04x\"), WEB_BUILD_TIME, cacheInvalidate, eTagSuffix);\n}\n\nstatic void setStaticContentCacheHeaders(AsyncWebServerResponse *response, int code, uint16_t eTagSuffix = 0) {\n  // Only send ETag for 200 (OK) responses\n  if (code != 200) return;\n\n  // https://medium.com/@codebyamir/a-web-developers-guide-to-browser-caching-cc41f3b73e7c\n  #ifndef WLED_DEBUG\n  // this header name is misleading, \"no-cache\" will not disable cache,\n  // it just revalidates on every load using the \"If-None-Match\" header with the last ETag value\n  response->addHeader(FPSTR(s_cache_control), F(\"no-cache\"));\n  #else\n  response->addHeader(FPSTR(s_cache_control), F(\"no-store,max-age=0\"));  // prevent caching if debug build\n  #endif\n  char etag[32];\n  generateEtag(etag, eTagSuffix);\n  response->addHeader(F(\"ETag\"), etag);\n}\n\nstatic bool handleIfNoneMatchCacheHeader(AsyncWebServerRequest *request, int code, uint16_t eTagSuffix = 0) {\n  // Only send 304 (Not Modified) if response code is 200 (OK)\n  if (code != 200) return false;\n\n  AsyncWebHeader *header = request->getHeader(F(\"If-None-Match\"));\n  char etag[32];\n  generateEtag(etag, eTagSuffix);\n  if (header && header->value() == etag) {\n    AsyncWebServerResponse *response = request->beginResponse(304);\n    setStaticContentCacheHeaders(response, code, eTagSuffix);\n    request->send(response);\n    return true;\n  }\n  return false;\n}\n\n/**\n * Handles the request for a static file.\n * If the file was found in the filesystem, it will be sent to the client.\n * Otherwise it will be checked if the browser cached the file and if so, a 304 response will be sent.\n * If the file was not found in the filesystem and not in the browser cache, the request will be handled as a 200 response with the content of the page.\n *\n * @param request The request object\n * @param path If a file with this path exists in the filesystem, it will be sent to the client. Set to \"\" to skip this check.\n * @param code The HTTP status code\n * @param contentType The content type of the web page\n * @param content Content of the web page\n * @param len Length of the content\n * @param gzip Optional. Defaults to true. If false, the gzip header will not be added.\n * @param eTagSuffix Optional. Defaults to 0. A suffix that will be added to the ETag header. This can be used to invalidate the cache for a specific page.\n */\nstatic void handleStaticContent(AsyncWebServerRequest *request, const String &path, int code, const String &contentType, const uint8_t *content, size_t len, bool gzip = true, uint16_t eTagSuffix = 0) {\n  if (path != \"\" && handleFileRead(request, path)) return;\n  if (handleIfNoneMatchCacheHeader(request, code, eTagSuffix)) return;\n  AsyncWebServerResponse *response = request->beginResponse_P(code, contentType, content, len);\n  if (gzip) response->addHeader(FPSTR(s_content_enc), F(\"gzip\"));\n  setStaticContentCacheHeaders(response, code, eTagSuffix);\n  request->send(response);\n}\n\n#ifdef WLED_ENABLE_DMX\nstatic String dmxProcessor(const String& var)\n{\n  String mapJS;\n  if (var == F(\"DMXVARS\")) {\n    mapJS += F(\"\\nCN=\");\n    mapJS += String(DMXChannels);\n    mapJS += F(\";\\nCS=\");\n    mapJS += String(DMXStart);\n    mapJS += F(\";\\nCG=\");\n    mapJS += String(DMXGap);\n    mapJS += F(\";\\nLC=\");\n    mapJS += String(strip.getLengthTotal());\n    mapJS += F(\";\\nvar CH=[\");\n    for (int i=0; i<15; i++) {\n      mapJS += String(DMXFixtureMap[i]) + ',';\n    }\n    mapJS += F(\"0];\");\n  }\n  return mapJS;\n}\n#endif\n\nstatic String msgProcessor(const String& var)\n{\n  if (var == \"MSG\") {\n    String messageBody = messageHead;\n    messageBody += F(\"</h2>\");\n    messageBody += messageSub;\n    uint32_t optt = optionType;\n\n    if (optt < 60) //redirect to settings after optionType seconds\n    {\n      messageBody += F(\"<script>setTimeout(RS,\");\n      messageBody += String(optt*1000);\n      messageBody += F(\")</script>\");\n    } else if (optt < 120) //redirect back after optionType-60 seconds, unused\n    {\n      //messageBody += \"<script>setTimeout(B,\" + String((optt-60)*1000) + \")</script>\";\n    } else if (optt < 180) //reload parent after optionType-120 seconds\n    {\n      messageBody += F(\"<script>setTimeout(RP,\");\n      messageBody += String((optt-120)*1000);\n      messageBody += F(\")</script>\");\n    } else if (optt == 253)\n    {\n      messageBody += F(\"<br><br><form action=/settings><button class=\\\"bt\\\" type=submit>Back</button></form>\"); //button to settings\n    } else if (optt == 254)\n    {\n      messageBody += F(\"<br><br><button type=\\\"button\\\" class=\\\"bt\\\" onclick=\\\"B()\\\">Back</button>\");\n    }\n    return messageBody;\n  }\n  return String();\n}\n\n\nstatic void handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool isFinal) {\n  if (!correctPIN) {\n    if (isFinal) request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_unlock_cfg));\n    return;\n  }\n  if (!index) {\n    String finalname = filename;\n    if (finalname.charAt(0) != '/') {\n      finalname = '/' + finalname; // prepend slash if missing\n    }\n\n    request->_tempFile = WLED_FS.open(finalname, \"w\");\n    DEBUG_PRINTF_P(PSTR(\"Uploading %s\\n\"), finalname.c_str());\n    if (finalname.equals(FPSTR(getPresetsFileName()))) presetsModifiedTime = toki.second();\n  }\n  if (len) {\n    request->_tempFile.write(data,len);\n  }\n  if (isFinal) {\n    request->_tempFile.close();\n    if (filename.indexOf(F(\"cfg.json\")) >= 0) { // check for filename with or without slash\n      doReboot = true;\n      request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F(\"Config restore ok.\\nRebooting...\"));\n    } else {\n      if (filename.indexOf(F(\"palette\")) >= 0 && filename.indexOf(F(\".json\")) >= 0) loadCustomPalettes();\n      request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F(\"File Uploaded!\"));\n    }\n    cacheInvalidate++;\n  }\n}\n\nstatic const char _edit_htm[] PROGMEM = \"/edit.htm\";\n\nstatic void createEditHandler() {\n  if (editHandler != nullptr) server.removeHandler(editHandler);\n\n  editHandler = &server.on(F(\"/edit\"), static_cast<WebRequestMethod>(HTTP_GET), [](AsyncWebServerRequest *request) {\n    // PIN check for GET/DELETE, for POST it is done in handleUpload()\n    if (!correctPIN) {\n      serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254);\n      return;\n    }\n    const String& func = request->arg(FPSTR(s_func));\n    bool legacyList = false;\n    if (request->hasArg(FPSTR(s_list))) {\n      legacyList = true; // support for '?list=/'\n    }\n\n    if(func.length() == 0 && !legacyList) {\n      // default: serve the editor page\n      handleStaticContent(request, FPSTR(_edit_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_edit, PAGE_edit_length);\n      return;\n    }\n\n    if (func == FPSTR(s_list) || legacyList) {\n      bool first = true;\n      AsyncResponseStream* response = request->beginResponseStream(FPSTR(CONTENT_TYPE_JSON));\n      response->addHeader(FPSTR(s_cache_control), FPSTR(s_no_store));\n      response->addHeader(FPSTR(s_expires), F(\"0\"));\n      response->write('[');\n\n      File rootdir = WLED_FS.open(\"/\", \"r\");\n      File rootfile = rootdir.openNextFile();\n      while (rootfile) {\n        String name = rootfile.name();\n        if (name.indexOf(FPSTR(s_wsec)) >= 0) {\n          rootfile = rootdir.openNextFile(); // skip wsec.json\n          continue;\n        }\n        if (!first) response->write(',');\n        first = false;\n        response->printf_P(PSTR(\"{\\\"name\\\":\\\"%s\\\",\\\"type\\\":\\\"file\\\",\\\"size\\\":%u}\"), name.c_str(), rootfile.size());\n        rootfile = rootdir.openNextFile();\n      }\n      rootfile.close();\n      rootdir.close();\n      response->write(']');\n      request->send(response);\n      return;\n    }\n\n    String path = request->arg(FPSTR(s_path)); // remaining functions expect a path\n\n    if (path.length() == 0) {\n      request->send(400, FPSTR(CONTENT_TYPE_PLAIN), F(\"Missing path\"));\n      return;\n    }\n\n    if (path.charAt(0) != '/') {\n      path = '/' + path; // prepend slash if missing\n    }\n\n    if (!WLED_FS.exists(path)) {\n      request->send(404, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_not_found));\n      return;\n    }\n\n    if (path.indexOf(FPSTR(s_wsec)) >= 0) {\n      request->send(403, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_accessdenied)); // skip wsec.json\n      return;\n    }\n\n    if (func == \"edit\") {\n      request->send(WLED_FS, path);\n      return;\n    }\n\n    if (func == \"download\") {\n      request->send(WLED_FS, path, String(), true);\n      return;\n    }\n\n    if (func == \"delete\") {\n      if (!WLED_FS.remove(path))\n        request->send(500, FPSTR(CONTENT_TYPE_PLAIN), F(\"Delete failed\"));\n      else\n        request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F(\"File deleted\"));\n      return;\n    }\n\n    // unrecognized func\n    request->send(400, FPSTR(CONTENT_TYPE_PLAIN), F(\"Invalid function\"));\n  });\n}\n\nstatic bool captivePortal(AsyncWebServerRequest *request)\n{\n  if (!apActive) return false; //only serve captive in AP mode\n  if (!request->hasHeader(F(\"Host\"))) return false;\n\n  String hostH = request->getHeader(F(\"Host\"))->value();\n  if (!isIp(hostH) && hostH.indexOf(F(\"wled.me\")) < 0 && hostH.indexOf(cmDNS) < 0 && hostH.indexOf(':') < 0) {\n    DEBUG_PRINTLN(F(\"Captive portal\"));\n    AsyncWebServerResponse *response = request->beginResponse(302);\n    response->addHeader(F(\"Location\"), F(\"http://4.3.2.1\"));\n    request->send(response);\n    return true;\n  }\n  return false;\n}\n\nvoid initServer()\n{\n  //CORS compatiblity\n  DefaultHeaders::Instance().addHeader(F(\"Access-Control-Allow-Origin\"), \"*\");\n  DefaultHeaders::Instance().addHeader(F(\"Access-Control-Allow-Methods\"), \"*\");\n  DefaultHeaders::Instance().addHeader(F(\"Access-Control-Allow-Headers\"), \"*\");\n\n#ifdef WLED_ENABLE_WEBSOCKETS\n  #ifndef WLED_DISABLE_2D \n  server.on(F(\"/liveview2D\"), HTTP_GET, [](AsyncWebServerRequest *request) {\n    handleStaticContent(request, \"\", 200, FPSTR(CONTENT_TYPE_HTML), PAGE_liveviewws2D, PAGE_liveviewws2D_length);\n  });\n  #endif\n#endif\n  server.on(F(\"/liveview\"), HTTP_GET, [](AsyncWebServerRequest *request) {\n    handleStaticContent(request, \"\", 200, FPSTR(CONTENT_TYPE_HTML), PAGE_liveview, PAGE_liveview_length);\n  });\n\n  server.on(_common_js, HTTP_GET, [](AsyncWebServerRequest *request) {\n    handleStaticContent(request, FPSTR(_common_js), 200, FPSTR(CONTENT_TYPE_JAVASCRIPT), JS_common, JS_common_length);\n  });\n\n  server.on(_iro_js, HTTP_GET, [](AsyncWebServerRequest *request) {\n    handleStaticContent(request, FPSTR(_iro_js), 200, FPSTR(CONTENT_TYPE_JAVASCRIPT), JS_iro, JS_iro_length);\n  });\n\n  server.on(_omggif_js, HTTP_GET, [](AsyncWebServerRequest *request) {\n    handleStaticContent(request, FPSTR(_omggif_js), 200, FPSTR(CONTENT_TYPE_JAVASCRIPT), JS_omggif, JS_omggif_length);\n  });\n\n  //settings page\n  server.on(F(\"/settings\"), HTTP_GET, [](AsyncWebServerRequest *request){\n    serveSettings(request);\n  });\n\n  // \"/settings/settings.js&p=x\" request also handled by serveSettings()\n  static const char _style_css[] PROGMEM = \"/style.css\";\n  server.on(_style_css, HTTP_GET, [](AsyncWebServerRequest *request) {\n    handleStaticContent(request, FPSTR(_style_css), 200, FPSTR(CONTENT_TYPE_CSS), PAGE_settingsCss, PAGE_settingsCss_length);\n  });\n\n  static const char _favicon_ico[] PROGMEM = \"/favicon.ico\";\n  server.on(_favicon_ico, HTTP_GET, [](AsyncWebServerRequest *request) {\n    handleStaticContent(request, FPSTR(_favicon_ico), 200, F(\"image/x-icon\"), favicon, favicon_length, false);\n  });\n\n  static const char _skin_css[] PROGMEM = \"/skin.css\";\n  server.on(_skin_css, HTTP_GET, [](AsyncWebServerRequest *request) {\n    if (handleFileRead(request, FPSTR(_skin_css))) return;\n    AsyncWebServerResponse *response = request->beginResponse(200, FPSTR(CONTENT_TYPE_CSS));\n    request->send(response);\n  });\n\n  server.on(F(\"/welcome\"), HTTP_GET, [](AsyncWebServerRequest *request){\n    serveSettings(request);\n  });\n\n  server.on(F(\"/reset\"), HTTP_GET, [](AsyncWebServerRequest *request){\n    serveMessage(request, 200, FPSTR(s_rebooting), F(\"Please wait ~10 seconds.\"), 131);\n    doReboot = true;\n  });\n\n  server.on(F(\"/settings\"), HTTP_POST, [](AsyncWebServerRequest *request){\n    serveSettings(request, true);\n  });\n\n  const static char _json[] PROGMEM = \"/json\";\n  server.on(FPSTR(_json), HTTP_GET, [](AsyncWebServerRequest *request){\n    serveJson(request);\n  });\n\n  AsyncCallbackJsonWebHandler* handler = new AsyncCallbackJsonWebHandler(FPSTR(_json), [](AsyncWebServerRequest *request) {\n    bool verboseResponse = false;\n    bool isConfig = false;\n\n    if (!requestJSONBufferLock(JSON_LOCK_SERVER)) {\n      request->deferResponse();\n      return;\n    }\n\n    DeserializationError error = deserializeJson(*pDoc, (uint8_t*)(request->_tempObject));\n    JsonObject root = pDoc->as<JsonObject>();\n    if (error || root.isNull()) {\n      releaseJSONBufferLock();\n      serveJsonError(request, 400, ERR_JSON);\n      return;\n    }\n    if (root.containsKey(\"pin\")) checkSettingsPIN(root[\"pin\"].as<const char*>());\n\n    const String& url = request->url();\n    isConfig = url.indexOf(F(\"cfg\")) > -1;\n    if (!isConfig) {\n      /*\n      #ifdef WLED_DEBUG\n        DEBUG_PRINTLN(F(\"Serialized HTTP\"));\n        serializeJson(root,Serial);\n        DEBUG_PRINTLN();\n      #endif\n      */\n      verboseResponse = deserializeState(root);\n    } else {\n      if (!correctPIN && strlen(settingsPIN)>0) {\n        releaseJSONBufferLock();\n        serveJsonError(request, 401, ERR_DENIED);\n        return;\n      }\n      verboseResponse = deserializeConfig(root); //use verboseResponse to determine whether cfg change should be saved immediately\n    }\n    releaseJSONBufferLock();\n\n    if (verboseResponse) {\n      if (!isConfig) {\n        lastInterfaceUpdate = millis(); // prevent WS update until cooldown\n        interfaceUpdateCallMode = CALL_MODE_WS_SEND; // override call mode & schedule WS update\n        #ifndef WLED_DISABLE_MQTT\n        // publish state to MQTT as requested in wled#4643 even if only WS response selected\n        publishMqtt();\n        #endif\n        serveJson(request);\n        return; //if JSON contains \"v\"\n      } else {\n        configNeedsWrite = true; //Save new settings to FS\n      }\n    }\n    request->send(200, CONTENT_TYPE_JSON, F(\"{\\\"success\\\":true}\"));\n  }, JSON_BUFFER_SIZE);\n  server.addHandler(handler);\n\n  server.on(F(\"/version\"), HTTP_GET, [](AsyncWebServerRequest *request){\n    request->send(200, FPSTR(CONTENT_TYPE_PLAIN), (String)VERSION);\n  });\n\n  server.on(F(\"/uptime\"), HTTP_GET, [](AsyncWebServerRequest *request){\n    request->send(200, FPSTR(CONTENT_TYPE_PLAIN), (String)millis());\n  });\n\n  server.on(F(\"/freeheap\"), HTTP_GET, [](AsyncWebServerRequest *request){\n    request->send(200, FPSTR(CONTENT_TYPE_PLAIN), (String)getFreeHeapSize());\n  });\n\n#ifdef WLED_ENABLE_USERMOD_PAGE\n  server.on(\"/u\", HTTP_GET, [](AsyncWebServerRequest *request) {\n    handleStaticContent(request, \"\", 200, FPSTR(CONTENT_TYPE_HTML), PAGE_usermod, PAGE_usermod_length);\n  });\n#endif\n\n  server.on(F(\"/teapot\"), HTTP_GET, [](AsyncWebServerRequest *request){\n    serveMessage(request, 418, F(\"418. I'm a teapot.\"), F(\"(Tangible Embedded Advanced Project Of Twinkling)\"), 254);\n  });\n\n  server.on(F(\"/upload\"), HTTP_POST, [](AsyncWebServerRequest *request) {},\n        [](AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data,\n                      size_t len, bool isFinal) {handleUpload(request, filename, index, data, len, isFinal);}\n  );\n\n  createEditHandler(); // initialize \"/edit\" handler, access is protected by \"correctPIN\"\n\n  static const char _update[] PROGMEM = \"/update\";\n#ifndef WLED_DISABLE_OTA\n  //init ota page\n  server.on(_update, HTTP_GET, [](AsyncWebServerRequest *request){\n    if (otaLock) {\n      serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_ota), 254);\n    } else\n      serveSettings(request); // checks for \"upd\" in URL and handles PIN\n  });\n\n  server.on(_update, HTTP_POST, [](AsyncWebServerRequest *request){\n    if (request->_tempObject) {\n      auto ota_result = getOTAResult(request);\n      if (ota_result.first) {\n        if (ota_result.second.length() > 0) {\n          serveMessage(request, 500, F(\"Update failed!\"), ota_result.second, 254);\n        } else {\n          serveMessage(request, 200, F(\"Update successful!\"), FPSTR(s_rebooting), 131);\n        }\n      }\n    } else {\n      // No context structure - something's gone horribly wrong\n      serveMessage(request, 500, F(\"Update failed!\"), F(\"Internal server fault\"), 254);\n    }\n  },[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){\n    if (index == 0) { \n      // Allocate the context structure\n      if (!initOTA(request)) {\n        return; // Error will be dealt with after upload in response handler, above\n      }\n\n      // Privilege checks\n      IPAddress client  = request->client()->remoteIP();\n      if (((otaSameSubnet && !inSameSubnet(client)) && !strlen(settingsPIN)) || (!otaSameSubnet && !inLocalSubnet(client))) {        \n        DEBUG_PRINTLN(F(\"Attempted OTA update from different/non-local subnet!\"));\n        serveMessage(request, 401, FPSTR(s_accessdenied), F(\"Client is not on local subnet.\"), 254);\n        setOTAReplied(request);\n        return;\n      }\n      if (!correctPIN) {\n        serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254);\n        setOTAReplied(request);\n        return;\n      };\n      if (otaLock) {\n        serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_ota), 254);\n        setOTAReplied(request);\n        return;\n      }      \n    }\n\n    handleOTAData(request, index, data, len, isFinal);\n  });\n#else\n  const auto notSupported = [](AsyncWebServerRequest *request){\n    serveMessage(request, 501, FPSTR(s_notimplemented), F(\"This build does not support OTA update.\"), 254);\n  };\n  server.on(_update, HTTP_GET, notSupported);\n  server.on(_update, HTTP_POST, notSupported, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){});\n#endif\n\n#if defined(ARDUINO_ARCH_ESP32) && !defined(WLED_DISABLE_OTA)\n  // ESP32 bootloader update endpoint\n  server.on(F(\"/updatebootloader\"), HTTP_POST, [](AsyncWebServerRequest *request){\n    if (request->_tempObject) {\n      auto bootloader_result = getBootloaderOTAResult(request);\n      if (bootloader_result.first) {\n        if (bootloader_result.second.length() > 0) {\n          serveMessage(request, 500, F(\"Bootloader update failed!\"), bootloader_result.second, 254);\n        } else {\n          serveMessage(request, 200, F(\"Bootloader updated successfully!\"), FPSTR(s_rebooting), 131);\n        }\n      }\n    } else {\n      // No context structure - something's gone horribly wrong\n      serveMessage(request, 500, F(\"Bootloader update failed!\"), F(\"Internal server fault\"), 254);\n    }\n  },[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){\n    if (index == 0) {\n      // Privilege checks\n      IPAddress client = request->client()->remoteIP();\n      if (((otaSameSubnet && !inSameSubnet(client)) && !strlen(settingsPIN)) || (!otaSameSubnet && !inLocalSubnet(client))) {\n        DEBUG_PRINTLN(F(\"Attempted bootloader update from different/non-local subnet!\"));\n        serveMessage(request, 401, FPSTR(s_accessdenied), F(\"Client is not on local subnet.\"), 254);\n        setBootloaderOTAReplied(request);\n        return;\n      }\n      if (!correctPIN) {\n        serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254);\n        setBootloaderOTAReplied(request);\n        return;\n      }\n      if (otaLock) {\n        serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_ota), 254);\n        setBootloaderOTAReplied(request);\n        return;\n      }\n\n      // Allocate the context structure\n      if (!initBootloaderOTA(request)) {\n        return; // Error will be dealt with after upload in response handler, above\n      }\n    }\n\n    handleBootloaderOTAData(request, index, data, len, isFinal);\n  });\n#endif\n\n#ifdef WLED_ENABLE_DMX\n  server.on(F(\"/dmxmap\"), HTTP_GET, [](AsyncWebServerRequest *request){\n    request->send_P(200, FPSTR(CONTENT_TYPE_HTML), PAGE_dmxmap, dmxProcessor);\n  });\n#endif\n\n  server.on(\"/\", HTTP_GET, [](AsyncWebServerRequest *request) {\n    if (captivePortal(request)) return;\n    if (!showWelcomePage || request->hasArg(F(\"sliders\"))) {\n      handleStaticContent(request, F(\"/index.htm\"), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_index, PAGE_index_length);\n    } else {\n      serveSettings(request);\n    }\n  });\n\n#ifndef WLED_DISABLE_2D\n  #ifdef WLED_ENABLE_PIXART\n  static const char _pixart_htm[] PROGMEM = \"/pixart.htm\";\n  server.on(_pixart_htm, HTTP_GET, [](AsyncWebServerRequest *request) {\n    handleStaticContent(request, FPSTR(_pixart_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pixart, PAGE_pixart_length);\n  });\n  #endif\n\n  #ifdef WLED_ENABLE_PXMAGIC\n  static const char _pxmagic_htm[] PROGMEM = \"/pxmagic.htm\";\n  server.on(_pxmagic_htm, HTTP_GET, [](AsyncWebServerRequest *request) {\n    handleStaticContent(request, FPSTR(_pxmagic_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pxmagic, PAGE_pxmagic_length);\n  });\n  #endif\n\n  #ifndef WLED_DISABLE_PIXELFORGE\n  static const char _pixelforge_htm[] PROGMEM = \"/pixelforge.htm\";\n  server.on(_pixelforge_htm, HTTP_GET, [](AsyncWebServerRequest *request) {\n    handleStaticContent(request, FPSTR(_pixelforge_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pixelforge, PAGE_pixelforge_length);\n  });\n  #endif\n#endif\n\n  static const char _cpal_htm[] PROGMEM = \"/cpal.htm\";\n  server.on(_cpal_htm, HTTP_GET, [](AsyncWebServerRequest *request) {\n    handleStaticContent(request, FPSTR(_cpal_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_cpal, PAGE_cpal_length);\n  });\n\n#ifdef WLED_ENABLE_WEBSOCKETS\n  server.addHandler(&ws);\n#endif\n\n  //called when the url is not defined here, ajax-in; get-settings\n  server.onNotFound([](AsyncWebServerRequest *request){\n    DEBUG_PRINTF_P(PSTR(\"Not-Found HTTP call: %s\\n\"), request->url().c_str());\n    if (captivePortal(request)) return;\n\n    //make API CORS compatible\n    if (request->method() == HTTP_OPTIONS)\n    {\n      AsyncWebServerResponse *response = request->beginResponse(200);\n      response->addHeader(F(\"Access-Control-Max-Age\"), F(\"7200\"));\n      request->send(response);\n      return;\n    }\n\n    if(handleSet(request, request->url())) return;\n    #ifndef WLED_DISABLE_ALEXA\n    if(espalexa.handleAlexaApiCall(request)) return;\n    #endif\n    handleStaticContent(request, request->url(), 404, FPSTR(CONTENT_TYPE_HTML), PAGE_404, PAGE_404_length);\n  });\n}\n\n\nvoid serveMessage(AsyncWebServerRequest* request, uint16_t code, const String& headl, const String& subl, byte optionT)\n{\n  messageHead = headl;\n  messageSub = subl;\n  optionType = optionT;\n\n  request->send_P(code, FPSTR(CONTENT_TYPE_HTML), PAGE_msg, msgProcessor);\n}\n\n\nvoid serveJsonError(AsyncWebServerRequest* request, uint16_t code, uint16_t error)\n{\n    AsyncJsonResponse *response = new AsyncJsonResponse(64);\n    if (error < ERR_NOT_IMPL) response->addHeader(F(\"Retry-After\"), F(\"1\"));\n    response->setContentType(CONTENT_TYPE_JSON);\n    response->setCode(code);\n    JsonObject obj = response->getRoot();\n    obj[F(\"error\")] = error;\n    response->setLength();\n    request->send(response);\n}\n\n\nvoid serveSettingsJS(AsyncWebServerRequest* request)\n{\n  if (request->url().indexOf(FPSTR(_common_js)) > 0) {\n    handleStaticContent(request, FPSTR(_common_js), 200, FPSTR(CONTENT_TYPE_JAVASCRIPT), JS_common, JS_common_length);\n    return;\n  }\n  byte subPage = request->arg(F(\"p\")).toInt();\n  if (subPage > SUBPAGE_LAST) {\n    request->send_P(501, FPSTR(CONTENT_TYPE_JAVASCRIPT), PSTR(\"alert('Settings for this request are not implemented.');\"));\n    return;\n  }\n  if (subPage > 0 && !correctPIN && strlen(settingsPIN)>0) {\n    request->send_P(401, FPSTR(CONTENT_TYPE_JAVASCRIPT), PSTR(\"alert('PIN incorrect.');\"));\n    return;\n  }\n  \n  AsyncResponseStream *response = request->beginResponseStream(FPSTR(CONTENT_TYPE_JAVASCRIPT));\n  response->addHeader(FPSTR(s_cache_control), FPSTR(s_no_store));\n  response->addHeader(FPSTR(s_expires), F(\"0\"));\n\n  response->print(F(\"function GetV(){var d=document;\"));\n  getSettingsJS(subPage, *response);\n  response->print(F(\"}\"));\n  request->send(response);\n}\n\n\nvoid serveSettings(AsyncWebServerRequest* request, bool post) {\n  byte subPage = 0, originalSubPage = 0;\n  const String& url = request->url();\n\n  if (url.indexOf(\"sett\") >= 0) {\n    if      (url.indexOf(F(\".js\"))  > 0) subPage = SUBPAGE_JS;\n    else if (url.indexOf(F(\".css\")) > 0) subPage = SUBPAGE_CSS;\n    else if (url.indexOf(F(\"wifi\")) > 0) subPage = SUBPAGE_WIFI;\n    else if (url.indexOf(F(\"leds\")) > 0) subPage = SUBPAGE_LEDS;\n    else if (url.indexOf(F(\"ui\"))   > 0) subPage = SUBPAGE_UI;\n    else if (url.indexOf(  \"sync\")  > 0) subPage = SUBPAGE_SYNC;\n    else if (url.indexOf(  \"time\")  > 0) subPage = SUBPAGE_TIME;\n    else if (url.indexOf(F(\"sec\"))  > 0) subPage = SUBPAGE_SEC;\n#ifdef WLED_ENABLE_DMX\n    else if (url.indexOf(  \"dmx\")   > 0) subPage = SUBPAGE_DMX;\n#endif\n    else if (url.indexOf(  \"um\")    > 0) subPage = SUBPAGE_UM;\n#ifndef WLED_DISABLE_2D\n    else if (url.indexOf(  \"2D\")    > 0) subPage = SUBPAGE_2D;\n#endif\n    else if (url.indexOf(F(\"pins\")) > 0) subPage = SUBPAGE_PINS;\n    else if (url.indexOf(F(\"lock\")) > 0) subPage = SUBPAGE_LOCK;\n  }\n  else if (url.indexOf(\"/update\") >= 0) subPage = SUBPAGE_UPDATE; // update page, for PIN check\n  //else if (url.indexOf(\"/edit\")   >= 0) subPage = 10;\n  else subPage = SUBPAGE_WELCOME;\n\n  bool pinRequired = !correctPIN && strlen(settingsPIN) > 0 && (subPage > (WLED_WIFI_CONFIGURED ? SUBPAGE_MENU : SUBPAGE_WIFI) && subPage < SUBPAGE_LOCK);\n  if (pinRequired) {\n    originalSubPage = subPage;\n    subPage = SUBPAGE_PINREQ; // require PIN\n  }\n\n  // if OTA locked or too frequent PIN entry requests fail hard\n  if ((subPage == SUBPAGE_WIFI && wifiLock && otaLock) || (post && pinRequired && millis()-lastEditTime < PIN_RETRY_COOLDOWN))\n  {\n    serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_ota), 254); return;\n  }\n\n  if (post) { //settings/set POST request, saving\n    IPAddress client = request->client()->remoteIP();\n    if (!inLocalSubnet(client)) { // includes same subnet check\n      serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_redirecting), 123);\n      return;\n    }\n    if (subPage != SUBPAGE_WIFI || !(wifiLock && otaLock)) handleSettingsSet(request, subPage);\n\n    char s[32];\n    char s2[45] = \"\";\n\n    switch (subPage) {\n      case SUBPAGE_WIFI   : strcpy_P(s, PSTR(\"WiFi\")); strcpy_P(s2, PSTR(\"Please connect to the new IP (if changed)\")); break;\n      case SUBPAGE_LEDS   : strcpy_P(s, PSTR(\"LED\")); break;\n      case SUBPAGE_UI     : strcpy_P(s, PSTR(\"UI\")); break;\n      case SUBPAGE_SYNC   : strcpy_P(s, PSTR(\"Sync\")); break;\n      case SUBPAGE_TIME   : strcpy_P(s, PSTR(\"Time\")); break;\n      case SUBPAGE_SEC    : strcpy_P(s, PSTR(\"Security\")); if (doReboot) strcpy_P(s2, PSTR(\"Rebooting, please wait ~10 seconds...\")); break;\n#ifdef WLED_ENABLE_DMX\n      case SUBPAGE_DMX    : strcpy_P(s, PSTR(\"DMX\")); break;\n#endif\n      case SUBPAGE_UM     : strcpy_P(s, PSTR(\"Usermods\")); break;\n#ifndef WLED_DISABLE_2D\n      case SUBPAGE_2D     : strcpy_P(s, PSTR(\"2D\")); break;\n#endif\n      case SUBPAGE_PINREQ : strcpy_P(s, correctPIN ? PSTR(\"PIN accepted\") : PSTR(\"PIN rejected\")); break;\n    }\n\n    if (subPage != SUBPAGE_PINREQ) strcat_P(s, PSTR(\" settings saved.\"));\n\n    if (subPage == SUBPAGE_PINREQ && correctPIN) {\n      subPage = originalSubPage; // on correct PIN load settings page the user intended\n    } else {\n      if (!s2[0]) strcpy_P(s2, s_redirecting);\n\n      bool redirectAfter9s = (subPage == SUBPAGE_WIFI || ((subPage == SUBPAGE_SEC || subPage == SUBPAGE_UM) && doReboot));\n      serveMessage(request, (!pinRequired ? 200 : 401), s, s2, redirectAfter9s ? 129 : (!pinRequired ? 1 : 3));\n      return;\n    }\n  }\n\n  int code = 200;\n  String contentType = FPSTR(CONTENT_TYPE_HTML);\n  const uint8_t* content;\n  size_t len;\n\n  switch (subPage) {\n    case SUBPAGE_WIFI    :  content = PAGE_settings_wifi; len = PAGE_settings_wifi_length; break;\n    case SUBPAGE_LEDS    :  content = PAGE_settings_leds; len = PAGE_settings_leds_length; break;\n    case SUBPAGE_UI      :  content = PAGE_settings_ui;   len = PAGE_settings_ui_length;   break;\n    case SUBPAGE_SYNC    :  content = PAGE_settings_sync; len = PAGE_settings_sync_length; break;\n    case SUBPAGE_TIME    :  content = PAGE_settings_time; len = PAGE_settings_time_length; break;\n    case SUBPAGE_SEC     :  content = PAGE_settings_sec;  len = PAGE_settings_sec_length;  break;\n#ifdef WLED_ENABLE_DMX\n    case SUBPAGE_DMX     :  content = PAGE_settings_dmx;  len = PAGE_settings_dmx_length;  break;\n#endif\n    case SUBPAGE_UM      :  content = PAGE_settings_um;   len = PAGE_settings_um_length;   break;\n#ifndef WLED_DISABLE_OTA\n    case SUBPAGE_UPDATE  :  content = PAGE_update;        len = PAGE_update_length;\n      #ifdef ARDUINO_ARCH_ESP32\n      if (request->hasArg(F(\"revert\")) && inLocalSubnet(request->client()->remoteIP()) && Update.canRollBack()) {\n        doReboot = Update.rollBack();\n        if (doReboot) {\n          serveMessage(request, 200, F(\"Reverted to previous version!\"), FPSTR(s_rebooting), 133);\n        } else {\n          serveMessage(request, 500, F(\"Rollback failed!\"), F(\"Please reboot and retry.\"), 254);\n        }\n        return;\n      }\n      #endif\n      break;\n#endif\n#ifndef WLED_DISABLE_2D\n    case SUBPAGE_2D      :  content = PAGE_settings_2D;   len = PAGE_settings_2D_length;   break;\n#endif\n    case SUBPAGE_PINS    :  content = PAGE_settings_pininfo; len = PAGE_settings_pininfo_length; break;\n    case SUBPAGE_LOCK    : {\n      correctPIN = !strlen(settingsPIN); // lock if a pin is set\n      serveMessage(request, 200, strlen(settingsPIN) > 0 ? PSTR(\"Settings locked\") : PSTR(\"No PIN set\"), FPSTR(s_redirecting), 1);\n      return;\n    }\n    case SUBPAGE_PINREQ  :  content = PAGE_settings_pin;  len = PAGE_settings_pin_length; code = 401;                 break;\n    case SUBPAGE_CSS     :  content = PAGE_settingsCss;   len = PAGE_settingsCss_length;  contentType = FPSTR(CONTENT_TYPE_CSS); break;\n    case SUBPAGE_JS      :  serveSettingsJS(request); return;\n    case SUBPAGE_WELCOME :  content = PAGE_welcome;       len = PAGE_welcome_length;       break;\n    default:                content = PAGE_settings;      len = PAGE_settings_length;      break;\n  }\n  handleStaticContent(request, \"\", code, contentType, content, len);\n}\n"
  },
  {
    "path": "wled00/ws.cpp",
    "content": "#include \"wled.h\"\n\n/*\n * WebSockets server for bidirectional communication\n */\n#ifdef WLED_ENABLE_WEBSOCKETS\n\n// forward declarations\nstatic bool sendLiveLedsWs(uint32_t wsClient);\n\n// define some constants for binary protocols, dont use defines but C++ style constexpr\nconstexpr uint8_t BINARY_PROTOCOL_GENERIC = 0xFF; // generic / auto detect NOT IMPLEMENTED\nconstexpr uint8_t BINARY_PROTOCOL_E131    = P_E131; // = 0, untested!\nconstexpr uint8_t BINARY_PROTOCOL_ARTNET  = P_ARTNET; // = 1, untested!\nconstexpr uint8_t BINARY_PROTOCOL_DDP     = P_DDP; // = 2\n\nstatic uint16_t wsLiveClientId = 0;\nstatic unsigned long wsLastLiveTime = 0;\n//static uint8_t* wsFrameBuffer = nullptr;\n\n#define WS_LIVE_INTERVAL 40\n\nvoid wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len)\n{\n  if(type == WS_EVT_CONNECT){\n    //client connected\n    DEBUG_PRINTLN(F(\"WS client connected.\"));\n    sendDataWs(client);\n  } else if(type == WS_EVT_DISCONNECT){\n    //client disconnected\n    if (client->id() == wsLiveClientId) wsLiveClientId = 0;\n    DEBUG_PRINTLN(F(\"WS client disconnected.\"));\n  } else if(type == WS_EVT_DATA){\n    // data packet\n    AwsFrameInfo * info = (AwsFrameInfo*)arg;\n    if(info->final && info->index == 0 && info->len == len){\n      // the whole message is in a single frame and we got all of its data (max. 1428 bytes / ESP8266: 528 bytes)\n      if(info->opcode == WS_TEXT)\n      {\n        if (len > 0 && len < 10 && data[0] == 'p') {\n          // application layer ping/pong heartbeat.\n          // client-side socket layer ping packets are unanswered (investigate)\n          client->text(F(\"pong\"));\n          return;\n        }\n\n        bool verboseResponse = false;\n        if (!requestJSONBufferLock(JSON_LOCK_WS_RECEIVE)) {\n          client->text(F(\"{\\\"error\\\":3}\")); // ERR_NOBUF\n          return;\n        }\n\n        DeserializationError error = deserializeJson(*pDoc, data, len);\n        JsonObject root = pDoc->as<JsonObject>();\n        if (error || root.isNull()) {\n          releaseJSONBufferLock();\n          return;\n        }\n        if (root[\"v\"] && root.size() == 1) {\n          //if the received value is just \"{\"v\":true}\", send only to this client\n          verboseResponse = true;\n        } else if (root.containsKey(\"lv\")) {\n          wsLiveClientId = root[\"lv\"] ? client->id() : 0;\n        } else {\n          verboseResponse = deserializeState(root);\n        }\n        releaseJSONBufferLock();\n\n        if (!interfaceUpdateCallMode) { // individual client response only needed if no WS broadcast soon\n          if (verboseResponse) {\n            #ifndef WLED_DISABLE_MQTT\n            // publish state to MQTT as requested in wled#4643 even if only WS response selected\n            publishMqtt();\n            #endif\n            sendDataWs(client);\n          } else {\n            // we have to send something back otherwise WS connection closes\n            client->text(F(\"{\\\"success\\\":true}\"));\n          }\n          // force broadcast in 500ms after updating client\n          //lastInterfaceUpdate = millis() - (INTERFACE_UPDATE_COOLDOWN -500); // ESP8266 does not like this\n        }\n      } else if (info->opcode == WS_BINARY) {\n        // first byte determines protocol. Note: since e131_packet_t is \"packed\", the compiler handles alignment issues\n        //DEBUG_PRINTF_P(PSTR(\"WS binary message: len %u, byte0: %u\\n\"), len, data[0]);\n        constexpr int offset = 1; // offset to skip protocol byte\n        if (!data || len < offset+1) return; // catch invalid / single-byte payload\n        switch (data[0]) {\n          case BINARY_PROTOCOL_E131:\n            handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_E131);\n            break;\n          case BINARY_PROTOCOL_ARTNET:\n            handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_ARTNET);\n            break;\n          case BINARY_PROTOCOL_DDP:\n            if (len < 10 + offset) return; // DDP header is 10 bytes (+1 protocol byte)\n            size_t ddpDataLen = (data[8+offset] << 8) | data[9+offset]; // data length in bytes from DDP header\n            uint8_t flags = data[0+offset];\n            if ((flags & DDP_TIMECODE_FLAG) ) ddpDataLen += 4; // timecode flag adds 4 bytes to data length\n            if (len < (10 + offset + ddpDataLen)) return; // not enough data, prevent out of bounds read\n            // could be a valid DDP packet, forward to handler\n            handleE131Packet((e131_packet_t*)&data[offset], client->remoteIP(), P_DDP);\n        }\n      }\n    } else {\n      DEBUG_PRINTF_P(PSTR(\"WS multipart message: final %u index %u len %u total %u\\n\"), info->final, info->index, len, (uint32_t)info->len);\n      //message is comprised of multiple frames or the frame is split into multiple packets\n      //if(info->index == 0){\n        //if (!wsFrameBuffer && len < 4096) wsFrameBuffer = new uint8_t[4096];\n      //}\n\n      //if (wsFrameBuffer && len < 4096 && info->index + info->)\n      //{\n\n      //}\n\n      if((info->index + len) == info->len){\n        if(info->final){\n          if(info->message_opcode == WS_TEXT) {\n            client->text(F(\"{\\\"error\\\":9}\")); // ERR_JSON we do not handle split packets right now\n          }\n        }\n      }\n      DEBUG_PRINTLN(F(\"WS multipart message.\"));\n    }\n  } else if(type == WS_EVT_ERROR){\n    //error was received from the other end\n    DEBUG_PRINTLN(F(\"WS error.\"));\n\n  } else if(type == WS_EVT_PONG){\n    //pong message was received (in response to a ping request maybe)\n    DEBUG_PRINTLN(F(\"WS pong.\"));\n\n  } else {\n    DEBUG_PRINTLN(F(\"WS unknown event.\"));\n  }\n}\n\nvoid sendDataWs(AsyncWebSocketClient * client)\n{\n  if (!ws.count()) return;\n\n  if (!requestJSONBufferLock(JSON_LOCK_WS_SEND)) {\n    const char* error = PSTR(\"{\\\"error\\\":3}\");\n    if (client) {\n      client->text(FPSTR(error)); // ERR_NOBUF\n    } else {\n      ws.textAll(FPSTR(error)); // ERR_NOBUF\n    }\n    return;\n  }\n\n  JsonObject state = pDoc->createNestedObject(\"state\");\n  serializeState(state);\n  JsonObject info  = pDoc->createNestedObject(\"info\");\n  serializeInfo(info);\n\n  size_t len = measureJson(*pDoc);\n  DEBUG_PRINTF_P(PSTR(\"JSON buffer size: %u for WS request (%u).\\n\"), pDoc->memoryUsage(), len);\n\n  // the following may no longer be necessary as heap management has been fixed by @willmmiles in AWS\n  size_t heap1 = getFreeHeapSize();\n  DEBUG_PRINTF_P(PSTR(\"heap %u\\n\"), getFreeHeapSize());\n  AsyncWebSocketBuffer buffer(len);\n  #ifdef ESP8266\n  size_t heap2 = getFreeHeapSize();\n  DEBUG_PRINTF_P(PSTR(\"heap %u\\n\"), getFreeHeapSize());\n  #else\n  size_t heap2 = 0; // ESP32 variants do not have the same issue and will work without checking heap allocation\n  #endif\n  if (!buffer || heap1-heap2<len) {\n    releaseJSONBufferLock();\n    DEBUG_PRINTLN(F(\"WS buffer allocation failed.\"));\n    ws.closeAll(1013); //code 1013 = temporary overload, try again later\n    ws.cleanupClients(0); //disconnect all clients to release memory\n    return; //out of memory\n  }\n  serializeJson(*pDoc, (char *)buffer.data(), len);\n\n  DEBUG_PRINT(F(\"Sending WS data \"));\n  if (client) {\n    DEBUG_PRINTLN(F(\"to a single client.\"));\n    client->text(std::move(buffer));\n  } else {\n    DEBUG_PRINTLN(F(\"to multiple clients.\"));\n    ws.textAll(std::move(buffer));\n  }\n\n  releaseJSONBufferLock();\n}\n\nstatic bool sendLiveLedsWs(uint32_t wsClient)\n{\n  AsyncWebSocketClient * wsc = ws.client(wsClient);\n  if (!wsc || wsc->queueLength() > 0) return false; //only send if queue free\n\n  size_t used = strip.getLengthTotal();\n#ifdef ESP8266\n  const size_t MAX_LIVE_LEDS_WS = 256U;\n#else\n  const size_t MAX_LIVE_LEDS_WS = 1024U;\n#endif\n  size_t n = ((used -1)/MAX_LIVE_LEDS_WS) +1; //only serve every n'th LED if count over MAX_LIVE_LEDS_WS\n  size_t pos = 2;  // start of data\n#ifndef WLED_DISABLE_2D\n  if (strip.isMatrix) {\n    // ignore anything behid matrix (i.e. extra strip)\n    used = Segment::maxWidth*Segment::maxHeight; // always the size of matrix (more or less than strip.getLengthTotal())\n    n = 1;\n    if (used > MAX_LIVE_LEDS_WS) n = 2;\n    if (used > MAX_LIVE_LEDS_WS*4) n = 4;\n    pos = 4;\n  }\n#endif\n  size_t bufSize = pos + (used/n)*3;\n\n  AsyncWebSocketBuffer wsBuf(bufSize);\n  if (!wsBuf) return false; //out of memory\n  uint8_t* buffer = reinterpret_cast<uint8_t*>(wsBuf.data());\n  if (!buffer) return false; //out of memory\n  buffer[0] = 'L';\n  buffer[1] = 1; //version\n\n#ifndef WLED_DISABLE_2D\n  if (strip.isMatrix) {\n    buffer[1] = 2; //version\n    buffer[2] = Segment::maxWidth/n;\n    buffer[3] = Segment::maxHeight/n;\n  }\n#endif\n\n  for (size_t i = 0; pos < bufSize -2; i += n)\n  {\n#ifndef WLED_DISABLE_2D\n    if (strip.isMatrix && n>1 && (i/Segment::maxWidth)%n) i += Segment::maxWidth * (n-1);\n#endif\n    uint32_t c = strip.getPixelColor(i); // note: LEDs mapped outside of valid range are set to black\n    uint8_t r = R(c);\n    uint8_t g = G(c);\n    uint8_t b = B(c);\n    uint8_t w = W(c);\n    buffer[pos++] = bri ? qadd8(w, r) : 0; //R, add white channel to RGB channels as a simple RGBW -> RGB map\n    buffer[pos++] = bri ? qadd8(w, g) : 0; //G\n    buffer[pos++] = bri ? qadd8(w, b) : 0; //B\n  }\n\n  wsc->binary(std::move(wsBuf));\n  return true;\n}\n\nvoid handleWs()\n{\n  if (millis() - wsLastLiveTime > WS_LIVE_INTERVAL)\n  {\n    #ifdef ESP8266\n    ws.cleanupClients(3);\n    #else\n    ws.cleanupClients();\n    #endif\n    bool success = true;\n    if (wsLiveClientId) success = sendLiveLedsWs(wsLiveClientId);\n    wsLastLiveTime = millis();\n    if (!success) wsLastLiveTime -= 20; //try again in 20ms if failed due to non-empty WS queue\n  }\n}\n\n#else\nvoid handleWs() {}\nvoid sendDataWs(AsyncWebSocketClient * client) {}\n#endif"
  },
  {
    "path": "wled00/xml.cpp",
    "content": "#include \"wled.h\"\n#include \"wled_ethernet.h\"\n\n/*\n * Sending XML status files to client\n */\n\n// forward declarations\nstatic void appendGPIOinfo(Print& settingsScript);\n\n//build XML response to HTTP /win API request\nvoid XML_response(Print& dest)\n{\n  dest.printf_P(PSTR(\"<?xml version=\\\"1.0\\\" ?><vs><ac>%d</ac>\"), (nightlightActive && nightlightMode > NL_MODE_SET) ? briT : bri);\n  for (int i = 0; i < 3; i++)\n  {\n   dest.printf_P(PSTR(\"<cl>%d</cl>\"), colPri[i]);\n  }\n  for (int i = 0; i < 3; i++)\n  {\n    dest.printf_P(PSTR(\"<cs>%d</cs>\"), colSec[i]);\n  }\n  dest.printf_P(PSTR(\"<ns>%d</ns><nr>%d</nr><nl>%d</nl><nf>%d</nf><nd>%d</nd><nt>%d</nt><fx>%d</fx><sx>%d</sx><ix>%d</ix><fp>%d</fp><wv>%d</wv><ws>%d</ws><ps>%d</ps><cy>%d</cy><ds>%s%s</ds><ss>%d</ss></vs>\"),\n    notifyDirect, receiveGroups!=0, nightlightActive, nightlightMode > NL_MODE_SET, nightlightDelayMins,\n    nightlightTargetBri, effectCurrent, effectSpeed, effectIntensity, effectPalette,\n    strip.hasWhiteChannel() ? colPri[3] : -1, colSec[3], currentPreset, currentPlaylist >= 0,\n    serverDescription, realtimeMode ? PSTR(\" (live)\") : \"\",\n    strip.getFirstSelectedSegId()\n  );\n}\n\nstatic void extractPin(Print& settingsScript, const JsonObject &obj, const char *key)\n{\n  if (obj[key].is<JsonArray>()) {\n    JsonArray pins = obj[key].as<JsonArray>();\n    for (JsonVariant pv : pins) {\n      if (pv.as<int>() > -1) { settingsScript.print(\",\"); settingsScript.print(pv.as<int>()); }\n    }\n  } else {\n    if (obj[key].as<int>() > -1) { settingsScript.print(\",\"); settingsScript.print(obj[key].as<int>()); }\n  }\n}\n\nstatic void fillWLEDVersion(char *buf, size_t len)\n{\n  if (!buf || len == 0) return;\n\n  snprintf_P(buf,len,PSTR(\"WLED %s (%d)<br>\\\\\\\"%s\\\\\\\"<br>(Processor: %s)\"),\n    versionString,\n    VERSION,\n    releaseString,\n  #if defined(ARDUINO_ARCH_ESP32)\n    ESP.getChipModel()\n  #else\n    \"ESP8266\"\n  #endif\n  );\n}\n\n// print used pins by scanning JsonObject (1 level deep)\nstatic void fillUMPins(Print& settingsScript, const JsonObject &mods)\n{\n  for (JsonPair kv : mods) {\n    // kv.key() is usermod name or subobject key\n    // kv.value() is object itself\n    JsonObject obj = kv.value();\n    if (!obj.isNull()) {\n      // element is an JsonObject\n      if (!obj[\"pin\"].isNull()) {\n        extractPin(settingsScript, obj, \"pin\");\n      } else {\n        // scan keys (just one level deep as is possible with usermods)\n        for (JsonPair so : obj) {\n          const char *key = so.key().c_str();\n          if (strstr(key, \"pin\")) {\n            // we found a key containing \"pin\" substring\n            if (strlen(strstr(key, \"pin\")) == 3) {\n              // and it is at the end, we found another pin\n              extractPin(settingsScript, obj, key);\n              continue;\n            }\n          }\n          if (!obj[so.key()].is<JsonObject>()) continue;\n          JsonObject subObj = obj[so.key()];\n          if (!subObj[\"pin\"].isNull()) {\n            // get pins from subobject\n            extractPin(settingsScript, subObj, \"pin\");\n          }\n        }\n      }\n    }\n  }\n}\n\nstatic void appendGPIOinfo(Print& settingsScript)\n{\n  settingsScript.print(F(\"d.um_p=[-1\")); // has to have 1 element\n  if (i2c_sda > -1 && i2c_scl > -1) {\n    settingsScript.printf_P(PSTR(\",%d,%d\"), i2c_sda, i2c_scl);\n  }\n  if (spi_mosi > -1 && spi_sclk > -1) {\n    settingsScript.printf_P(PSTR(\",%d,%d\"), spi_mosi, spi_sclk);\n  }\n  // usermod pin reservations will become unnecessary when settings pages will read cfg.json directly\n  if (requestJSONBufferLock(JSON_LOCK_XML)) {\n    // if we can't allocate JSON buffer ignore usermod pins\n    JsonObject mods = pDoc->createNestedObject(\"um\");\n    UsermodManager::addToConfig(mods);\n    if (!mods.isNull()) fillUMPins(settingsScript, mods);\n    releaseJSONBufferLock();\n  }\n  settingsScript.print(F(\"];\"));\n\n  // add reserved (unusable) pins\n  bool firstPin = true;\n  settingsScript.print(F(\"d.rsvd=[\"));\n  for (unsigned i = 0; i < WLED_NUM_PINS; i++) {\n    if (!PinManager::isPinOk(i, false)) {  // include readonly pins\n      if (!firstPin) settingsScript.print(',');\n      settingsScript.print(i);\n      firstPin = false;\n    }\n  }\n  #ifdef WLED_ENABLE_DMX\n  if (!firstPin) settingsScript.print(',');\n  settingsScript.print(2); // DMX hardcoded pin\n  firstPin = false;\n  #endif\n  #if defined(WLED_DEBUG) && !defined(WLED_DEBUG_HOST)\n  if (!firstPin) settingsScript.print(',');\n  settingsScript.print(hardwareTX); // debug output (TX) pin\n  firstPin = false;\n  #endif\n  #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET)\n  if (ethernetType != WLED_ETH_NONE && ethernetType < WLED_NUM_ETH_TYPES) {\n    if (!firstPin) settingsScript.print(',');\n    for (unsigned p=0; p<WLED_ETH_RSVD_PINS_COUNT; p++) { settingsScript.printf(\"%d,\",esp32_nonconfigurable_ethernet_pins[p].pin); }\n    if (ethernetBoards[ethernetType].eth_power >= 0)    { settingsScript.printf(\"%d,\",ethernetBoards[ethernetType].eth_power); }\n    if (ethernetBoards[ethernetType].eth_mdc >= 0)      { settingsScript.printf(\"%d,\",ethernetBoards[ethernetType].eth_mdc); }\n    if (ethernetBoards[ethernetType].eth_mdio >= 0)     { settingsScript.printf(\"%d,\",ethernetBoards[ethernetType].eth_mdio); }\n    switch (ethernetBoards[ethernetType].eth_clk_mode)  {\n      case ETH_CLOCK_GPIO0_IN:\n      case ETH_CLOCK_GPIO0_OUT:\n        settingsScript.print(0);\n        break;\n      case ETH_CLOCK_GPIO16_OUT:\n        settingsScript.print(16);\n        break;\n      case ETH_CLOCK_GPIO17_OUT:\n        settingsScript.print(17);\n        break;\n    }\n  }\n  #endif\n  settingsScript.print(F(\"];\")); // rsvd\n\n  // add info for read-only GPIO\n  settingsScript.print(F(\"d.ro_gpio=[\"));\n  firstPin = true;\n  for (unsigned i = 0; i < WLED_NUM_PINS; i++) {\n    if (PinManager::isReadOnlyPin(i)) {\n      // No comma before the first pin\n      if (!firstPin) settingsScript.print(',');\n      settingsScript.print(i);\n      firstPin = false;\n    }\n  }\n  settingsScript.print(F(\"];\"));\n\n  // add info about max. # of pins\n  settingsScript.printf_P(PSTR(\"d.max_gpio=%d;\"),WLED_NUM_PINS);\n\n  // add info about touch-capable GPIO (ESP32 only, not on C3)\n  #if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3)\n  settingsScript.print(F(\"d.touch=[\"));\n  firstPin = true;\n  for (unsigned i = 0; i < WLED_NUM_PINS; i++) {\n    if (digitalPinToTouchChannel(i) >= 0) {\n      if (!firstPin) settingsScript.print(',');\n      settingsScript.print(i);\n      firstPin = false;\n    }\n  }\n  settingsScript.print(F(\"];\"));\n  #else\n  settingsScript.print(F(\"d.touch=[];\"));\n  #endif\n}\n\n//get values for settings form in javascript\nvoid getSettingsJS(byte subPage, Print& settingsScript)\n{\n  //0: menu 1: wifi 2: leds 3: ui 4: sync 5: time 6: sec\n  DEBUG_PRINTF_P(PSTR(\"settings resp %u\\n\"), (unsigned)subPage);\n\n  if (subPage <0 || subPage >SUBPAGE_LAST) return;\n  char nS[32];\n\n  if (subPage == SUBPAGE_MENU)\n  {\n  #ifdef WLED_DISABLE_2D // include only if 2D is not compiled in\n    settingsScript.print(F(\"gId('2dbtn').style.display='none';\"));\n  #endif\n  #ifdef WLED_ENABLE_DMX // include only if DMX is enabled\n    settingsScript.print(F(\"gId('dmxbtn').style.display='';\"));\n  #endif\n  }\n\n  if (subPage == SUBPAGE_WIFI)\n  {\n    size_t l;\n    settingsScript.printf_P(PSTR(\"resetWiFi(%d);\"), WLED_MAX_WIFI_COUNT);\n    for (size_t n = 0; n < multiWiFi.size(); n++) {\n      l = strlen(multiWiFi[n].clientPass);\n      char fpass[l+1]; //fill password field with ***\n      fpass[l] = 0;\n      memset(fpass,'*',l);\n      char bssid[13];\n      fillMAC2Str(bssid, multiWiFi[n].bssid);\n#ifdef WLED_ENABLE_WPA_ENTERPRISE\n      settingsScript.printf_P(PSTR(\"addWiFi(\\\"%s\\\",\\\"%s\\\",\\\"%s\\\",0x%X,0x%X,0x%X,\\\"%u\\\",\\\"%s\\\",\\\"%s\\\");\"),\n        multiWiFi[n].clientSSID,\n        fpass,\n        bssid,\n        (uint32_t) multiWiFi[n].staticIP, // explicit cast required as this is a struct\n        (uint32_t) multiWiFi[n].staticGW,\n        (uint32_t) multiWiFi[n].staticSN,\n        multiWiFi[n].encryptionType,\n        multiWiFi[n].enterpriseAnonIdentity,\n        multiWiFi[n].enterpriseIdentity);\n#else\n      settingsScript.printf_P(PSTR(\"addWiFi(\\\"%s\\\",\\\"%s\\\",\\\"%s\\\",0x%X,0x%X,0x%X);\"),\n        multiWiFi[n].clientSSID,\n        fpass,\n        bssid,\n        (uint32_t) multiWiFi[n].staticIP, // explicit cast required as this is a struct\n        (uint32_t) multiWiFi[n].staticGW,\n        (uint32_t) multiWiFi[n].staticSN);\n#endif\n    }\n\n    printSetFormValue(settingsScript,PSTR(\"D0\"),dnsAddress[0]);\n    printSetFormValue(settingsScript,PSTR(\"D1\"),dnsAddress[1]);\n    printSetFormValue(settingsScript,PSTR(\"D2\"),dnsAddress[2]);\n    printSetFormValue(settingsScript,PSTR(\"D3\"),dnsAddress[3]);\n\n    printSetFormValue(settingsScript,PSTR(\"CM\"),cmDNS);\n    printSetFormIndex(settingsScript,PSTR(\"AB\"),apBehavior);\n    printSetFormValue(settingsScript,PSTR(\"AS\"),apSSID);\n    printSetFormCheckbox(settingsScript,PSTR(\"AH\"),apHide);\n\n    l = strlen(apPass);\n    char fapass[l+1]; //fill password field with ***\n    fapass[l] = 0;\n    memset(fapass,'*',l);\n    printSetFormValue(settingsScript,PSTR(\"AP\"),fapass);\n\n    printSetFormValue(settingsScript,PSTR(\"AC\"),apChannel);\n    #ifdef ARDUINO_ARCH_ESP32\n    printSetFormValue(settingsScript,PSTR(\"TX\"),txPower);\n    #else\n    settingsScript.print(F(\"gId('tx').style.display='none';\"));\n    #endif\n    printSetFormCheckbox(settingsScript,PSTR(\"FG\"),force802_3g);\n    printSetFormCheckbox(settingsScript,PSTR(\"WS\"),noWifiSleep);\n\n    #ifndef WLED_DISABLE_ESPNOW\n    printSetFormCheckbox(settingsScript,PSTR(\"RE\"),enableESPNow);\n    settingsScript.printf_P(PSTR(\"rstR();\")); // reset remote list\n    for (size_t i = 0; i < linked_remotes.size(); i++) {\n      settingsScript.printf_P(PSTR(\"aR(\\\"RM%u\\\",\\\"%s\\\");\"), i, linked_remotes[i].data()); // add remote to list\n    }\n    settingsScript.print(F(\"tE();\")); // fill fields\n    #else\n    //hide remote settings if not compiled\n    settingsScript.print(F(\"toggle('ESPNOW');\"));  // hide ESP-NOW setting\n    #endif\n\n    #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET)\n    printSetFormValue(settingsScript,PSTR(\"ETH\"),ethernetType);\n    #else\n    //hide ethernet setting if not compiled in\n    settingsScript.print(F(\"gId('ethd').style.display='none';\"));\n    #endif\n\n    if (Network.isConnected()) //is connected\n    {\n      char s[32];\n      IPAddress localIP = Network.localIP();\n      sprintf(s, \"%d.%d.%d.%d\", localIP[0], localIP[1], localIP[2], localIP[3]);\n\n      #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET)\n      if (Network.isEthernet()) strcat_P(s ,PSTR(\" (Ethernet)\"));\n      #endif\n      printSetClassElementHTML(settingsScript,PSTR(\"sip\"),0,s);\n    } else\n    {\n      printSetClassElementHTML(settingsScript,PSTR(\"sip\"),0,(char*)F(\"Not connected\"));\n    }\n\n    if (WiFi.softAPIP()[0] != 0) //is active\n    {\n      char s[16];\n      IPAddress apIP = WiFi.softAPIP();\n      sprintf(s, \"%d.%d.%d.%d\", apIP[0], apIP[1], apIP[2], apIP[3]);\n      printSetClassElementHTML(settingsScript,PSTR(\"sip\"),1,s);\n    } else\n    {\n      printSetClassElementHTML(settingsScript,PSTR(\"sip\"),1,(char*)F(\"Not active\"));\n    }\n\n    #ifndef WLED_DISABLE_ESPNOW\n    if (strlen(last_signal_src) > 0) { //Have seen an ESP-NOW Remote\n      printSetClassElementHTML(settingsScript,PSTR(\"rlid\"),0,last_signal_src);\n    }\n    #endif\n  }\n\n  if (subPage == SUBPAGE_LEDS)\n  {\n    appendGPIOinfo(settingsScript);\n\n    settingsScript.printf_P(PSTR(\"d.ledTypes=%s;\"), BusManager::getLEDTypesJSONString().c_str());\n\n    // set limits\n    settingsScript.printf_P(PSTR(\"bLimits(%d,%d,%d,%d,%d,%d,%d,%d,%d,%d);\"),\n      WLED_PLATFORM_ID, // TODO: replace with a info json lookup\n      MAX_LEDS_PER_BUS,\n      MAX_LED_MEMORY,\n      MAX_LEDS,\n      WLED_MAX_COLOR_ORDER_MAPPINGS,\n      WLED_MAX_DIGITAL_CHANNELS,\n      WLED_MAX_RMT_CHANNELS,\n      WLED_MAX_I2S_CHANNELS,\n      WLED_MAX_ANALOG_CHANNELS,\n      WLED_MAX_BUTTONS\n    );\n\n    printSetFormCheckbox(settingsScript,PSTR(\"MS\"),strip.autoSegments);\n    printSetFormCheckbox(settingsScript,PSTR(\"CCT\"),strip.correctWB);\n    printSetFormCheckbox(settingsScript,PSTR(\"IC\"),cctICused);\n    printSetFormCheckbox(settingsScript,PSTR(\"CR\"),strip.cctFromRgb);\n    printSetFormValue(settingsScript,PSTR(\"CB\"),Bus::getCCTBlend());\n    printSetFormValue(settingsScript,PSTR(\"FR\"),strip.getTargetFps());\n    printSetFormValue(settingsScript,PSTR(\"AW\"),Bus::getGlobalAWMode());\n\n    unsigned sumMa = 0;\n    for (size_t s = 0; s < BusManager::getNumBusses(); s++) {\n      const Bus *bus = BusManager::getBus(s);\n      if (!bus) break; // should not happen but for safety\n      int offset = s < 10 ? '0' : 'A' - 10;\n      char lp[4] = \"L0\"; lp[2] = offset+s; lp[3] = 0; //ascii 0-9 //strip data pin\n      char lc[4] = \"LC\"; lc[2] = offset+s; lc[3] = 0; //strip length\n      char co[4] = \"CO\"; co[2] = offset+s; co[3] = 0; //strip color order\n      char lt[4] = \"LT\"; lt[2] = offset+s; lt[3] = 0; //strip type\n      char ld[4] = \"LD\"; ld[2] = offset+s; ld[3] = 0; //driver type (RMT=0, I2S=1)\n      char ls[4] = \"LS\"; ls[2] = offset+s; ls[3] = 0; //strip start LED\n      char cv[4] = \"CV\"; cv[2] = offset+s; cv[3] = 0; //strip reverse\n      char sl[4] = \"SL\"; sl[2] = offset+s; sl[3] = 0; //skip 1st LED\n      char rf[4] = \"RF\"; rf[2] = offset+s; rf[3] = 0; //off refresh\n      char aw[4] = \"AW\"; aw[2] = offset+s; aw[3] = 0; //auto white mode\n      char wo[4] = \"WO\"; wo[2] = offset+s; wo[3] = 0; //swap channels\n      char sp[4] = \"SP\"; sp[2] = offset+s; sp[3] = 0; //bus clock speed\n      char la[4] = \"LA\"; la[2] = offset+s; la[3] = 0; //LED current\n      char ma[4] = \"MA\"; ma[2] = offset+s; ma[3] = 0; //max per-port PSU current\n      char hs[4] = \"HS\"; hs[2] = offset+s; hs[3] = 0; //hostname (for network types, custom text for others)\n      settingsScript.print(F(\"addLEDs(1);\"));\n      uint8_t pins[OUTPUT_MAX_PINS];\n      int nPins = bus->getPins(pins);\n      for (int i = 0; i < nPins; i++) {\n        lp[1] = '0'+i;\n        if (PinManager::isPinOk(pins[i]) || bus->isVirtual() || Bus::isHub75(bus->getType())) printSetFormValue(settingsScript,lp,pins[i]);\n      }\n      printSetFormValue(settingsScript,lc,bus->getLength());\n      printSetFormValue(settingsScript,lt,bus->getType());\n      printSetFormValue(settingsScript,ld,bus->getDriverType());\n      printSetFormValue(settingsScript,co,bus->getColorOrder() & 0x0F);\n      printSetFormValue(settingsScript,ls,bus->getStart());\n      printSetFormCheckbox(settingsScript,cv,bus->isReversed());\n      printSetFormValue(settingsScript,sl,bus->skippedLeds());\n      printSetFormCheckbox(settingsScript,rf,bus->isOffRefreshRequired());\n      printSetFormValue(settingsScript,aw,bus->getAutoWhiteMode());\n      printSetFormValue(settingsScript,wo,bus->getColorOrder() >> 4);\n      unsigned speed = bus->getFrequency();\n      if (bus->isPWM()) {\n        switch (speed) {\n          case WLED_PWM_FREQ/2    : speed = 0; break;\n          case WLED_PWM_FREQ*2/3  : speed = 1; break;\n          default:\n          case WLED_PWM_FREQ      : speed = 2; break;\n          case WLED_PWM_FREQ*2    : speed = 3; break;\n          case WLED_PWM_FREQ*10/3 : speed = 4; break; // uint16_t max (19531 * 3.333)\n        }\n      } else if (bus->is2Pin()) {\n        switch (speed) {\n          case  1000 : speed = 0; break;\n          case  2000 : speed = 1; break;\n          default:\n          case  5000 : speed = 2; break;\n          case 10000 : speed = 3; break;\n          case 20000 : speed = 4; break;\n        }\n      }\n      printSetFormValue(settingsScript,sp,speed);\n      printSetFormValue(settingsScript,la,bus->getLEDCurrent());\n      printSetFormValue(settingsScript,ma,bus->getMaxCurrent());\n      printSetFormValue(settingsScript,hs,bus->getCustomText().c_str());\n      sumMa += bus->getMaxCurrent();\n    }\n    printSetFormValue(settingsScript,PSTR(\"MA\"),BusManager::ablMilliampsMax() ? BusManager::ablMilliampsMax() : sumMa);\n    printSetFormCheckbox(settingsScript,PSTR(\"ABL\"),BusManager::ablMilliampsMax() || sumMa > 0);\n    printSetFormCheckbox(settingsScript,PSTR(\"PPL\"),!BusManager::ablMilliampsMax() && sumMa > 0);\n\n    settingsScript.printf_P(PSTR(\"resetCOM(%d);\"), WLED_MAX_COLOR_ORDER_MAPPINGS);\n    const ColorOrderMap& com = BusManager::getColorOrderMap();\n    for (int s = 0; s < com.count(); s++) {\n      const ColorOrderMapEntry* entry = com.get(s);\n      if (!entry || !entry->len) break;\n      settingsScript.printf_P(PSTR(\"addCOM(%d,%d,%d);\"), entry->start, entry->len, entry->colorOrder);\n    }\n\n    printSetFormValue(settingsScript,PSTR(\"CA\"),briS);\n\n    printSetFormCheckbox(settingsScript,PSTR(\"BO\"),turnOnAtBoot);\n    printSetFormValue(settingsScript,PSTR(\"BP\"),bootPreset);\n\n    printSetFormCheckbox(settingsScript,PSTR(\"GB\"),gammaCorrectBri);\n    printSetFormCheckbox(settingsScript,PSTR(\"GC\"),gammaCorrectCol);\n    dtostrf(gammaCorrectVal,3,1,nS); printSetFormValue(settingsScript,PSTR(\"GV\"),nS);\n    printSetFormValue(settingsScript,PSTR(\"TD\"),transitionDelayDefault);\n    printSetFormValue(settingsScript,PSTR(\"TP\"),randomPaletteChangeTime);\n    printSetFormCheckbox(settingsScript,PSTR(\"TH\"),useHarmonicRandomPalette);\n    printSetFormValue(settingsScript,PSTR(\"BF\"),briMultiplier);\n    printSetFormValue(settingsScript,PSTR(\"TB\"),nightlightTargetBri);\n    printSetFormValue(settingsScript,PSTR(\"TL\"),nightlightDelayMinsDefault);\n    printSetFormValue(settingsScript,PSTR(\"TW\"),nightlightMode);\n    printSetFormIndex(settingsScript,PSTR(\"PB\"),paletteBlend);\n    printSetFormValue(settingsScript,PSTR(\"RL\"),rlyPin);\n    printSetFormCheckbox(settingsScript,PSTR(\"RM\"),rlyMde);\n    printSetFormCheckbox(settingsScript,PSTR(\"RO\"),rlyOpenDrain);\n    int i = 0;\n    for (const auto &button : buttons) {\n      settingsScript.printf_P(PSTR(\"addBtn(%d,%d,%d);\"), i++, button.pin, button.type);\n    }\n    printSetFormCheckbox(settingsScript,PSTR(\"IP\"),disablePullUp);\n    printSetFormValue(settingsScript,PSTR(\"TT\"),touchThreshold);\n#ifndef WLED_DISABLE_INFRARED\n    printSetFormValue(settingsScript,PSTR(\"IR\"),irPin);\n    printSetFormValue(settingsScript,PSTR(\"IT\"),irEnabled);\n#endif    \n    printSetFormCheckbox(settingsScript,PSTR(\"MSO\"),!irApplyToAllSelected);\n  }\n\n  if (subPage == SUBPAGE_UI)\n  {\n    printSetFormValue(settingsScript,PSTR(\"DS\"),serverDescription);\n    printSetFormCheckbox(settingsScript,PSTR(\"SU\"),simplifiedUI);\n  }\n\n  if (subPage == SUBPAGE_SYNC)\n  {\n    printSetFormValue(settingsScript,PSTR(\"UP\"),udpPort);\n    printSetFormValue(settingsScript,PSTR(\"U2\"),udpPort2);\n  #ifndef WLED_DISABLE_ESPNOW\n    if (enableESPNow) printSetFormCheckbox(settingsScript,PSTR(\"EN\"),useESPNowSync);\n    else              settingsScript.print(F(\"toggle('ESPNOW');\"));  // hide ESP-NOW setting\n  #else\n    settingsScript.print(F(\"toggle('ESPNOW');\"));  // hide ESP-NOW setting\n  #endif\n    printSetFormValue(settingsScript,PSTR(\"GS\"),syncGroups);\n    printSetFormValue(settingsScript,PSTR(\"GR\"),receiveGroups);\n\n    printSetFormCheckbox(settingsScript,PSTR(\"RB\"),receiveNotificationBrightness);\n    printSetFormCheckbox(settingsScript,PSTR(\"RC\"),receiveNotificationColor);\n    printSetFormCheckbox(settingsScript,PSTR(\"RX\"),receiveNotificationEffects);\n    printSetFormCheckbox(settingsScript,PSTR(\"RP\"),receiveNotificationPalette);\n    printSetFormCheckbox(settingsScript,PSTR(\"SO\"),receiveSegmentOptions);\n    printSetFormCheckbox(settingsScript,PSTR(\"SG\"),receiveSegmentBounds);\n    printSetFormCheckbox(settingsScript,PSTR(\"SS\"),sendNotifications);\n    printSetFormCheckbox(settingsScript,PSTR(\"SD\"),notifyDirect);\n    printSetFormCheckbox(settingsScript,PSTR(\"SB\"),notifyButton);\n    printSetFormCheckbox(settingsScript,PSTR(\"SH\"),notifyHue);\n    printSetFormValue(settingsScript,PSTR(\"UR\"),udpNumRetries);\n\n    printSetFormCheckbox(settingsScript,PSTR(\"NL\"),nodeListEnabled);\n    printSetFormCheckbox(settingsScript,PSTR(\"NB\"),nodeBroadcastEnabled);\n\n    printSetFormCheckbox(settingsScript,PSTR(\"RD\"),receiveDirect);\n    printSetFormCheckbox(settingsScript,PSTR(\"MO\"),useMainSegmentOnly);\n    printSetFormCheckbox(settingsScript,PSTR(\"RLM\"),realtimeRespectLedMaps);\n    printSetFormValue(settingsScript,PSTR(\"EP\"),e131Port);\n    printSetFormCheckbox(settingsScript,PSTR(\"ES\"),e131SkipOutOfSequence);\n    printSetFormCheckbox(settingsScript,PSTR(\"EM\"),e131Multicast);\n    printSetFormValue(settingsScript,PSTR(\"EU\"),e131Universe);\n#ifdef WLED_ENABLE_DMX\n    settingsScript.print(SET_F(\"hideNoDMXOutput();\"));  // hide \"not compiled in\" message\n#endif\n#ifndef WLED_ENABLE_DMX_INPUT\n    settingsScript.print(SET_F(\"hideDMXInput();\"));  // hide \"dmx input\" settings\n#else\n    settingsScript.print(SET_F(\"hideNoDMXInput();\"));  //hide \"not compiled in\" message\n    printSetFormValue(settingsScript,SET_F(\"IDMT\"),dmxInputTransmitPin);\n    printSetFormValue(settingsScript,SET_F(\"IDMR\"),dmxInputReceivePin);\n    printSetFormValue(settingsScript,SET_F(\"IDME\"),dmxInputEnablePin);\n    printSetFormValue(settingsScript,SET_F(\"IDMP\"),dmxInputPort);\n#endif\n    printSetFormValue(settingsScript,PSTR(\"DA\"),DMXAddress);\n    printSetFormValue(settingsScript,PSTR(\"XX\"),DMXSegmentSpacing);\n    printSetFormValue(settingsScript,PSTR(\"PY\"),e131Priority);\n    printSetFormValue(settingsScript,PSTR(\"DM\"),DMXMode);\n    printSetFormValue(settingsScript,PSTR(\"ET\"),realtimeTimeoutMs);\n    printSetFormCheckbox(settingsScript,PSTR(\"FB\"),arlsForceMaxBri);\n    printSetFormCheckbox(settingsScript,PSTR(\"RG\"),arlsDisableGammaCorrection);\n    printSetFormValue(settingsScript,PSTR(\"WO\"),arlsOffset);\n    #ifndef WLED_DISABLE_ALEXA\n    printSetFormCheckbox(settingsScript,PSTR(\"AL\"),alexaEnabled);\n    printSetFormValue(settingsScript,PSTR(\"AI\"),alexaInvocationName);\n    printSetFormCheckbox(settingsScript,PSTR(\"SA\"),notifyAlexa);\n    printSetFormValue(settingsScript,PSTR(\"AP\"),alexaNumPresets);\n    #else\n    settingsScript.print(F(\"toggle('Alexa');\"));  // hide Alexa settings\n    #endif\n\n    #ifndef WLED_DISABLE_MQTT\n    printSetFormCheckbox(settingsScript,PSTR(\"MQ\"),mqttEnabled);\n    printSetFormValue(settingsScript,PSTR(\"MS\"),mqttServer);\n    printSetFormValue(settingsScript,PSTR(\"MQPORT\"),mqttPort);\n    printSetFormValue(settingsScript,PSTR(\"MQUSER\"),mqttUser);\n    byte l = strlen(mqttPass);\n    char fpass[l+1]; //fill password field with ***\n    fpass[l] = 0;\n    memset(fpass,'*',l);\n    printSetFormValue(settingsScript,PSTR(\"MQPASS\"),fpass);\n    printSetFormValue(settingsScript,PSTR(\"MQCID\"),mqttClientID);\n    printSetFormValue(settingsScript,PSTR(\"MD\"),mqttDeviceTopic);\n    printSetFormValue(settingsScript,PSTR(\"MG\"),mqttGroupTopic);\n    printSetFormCheckbox(settingsScript,PSTR(\"BM\"),buttonPublishMqtt);\n    printSetFormCheckbox(settingsScript,PSTR(\"RT\"),retainMqttMsg);\n    settingsScript.printf_P(PSTR(\"d.Sf.MD.maxLength=%d;d.Sf.MG.maxLength=%d;d.Sf.MS.maxLength=%d;\"),\n                  MQTT_MAX_TOPIC_LEN, MQTT_MAX_TOPIC_LEN, MQTT_MAX_SERVER_LEN);\n    #else\n    settingsScript.print(F(\"toggle('MQTT');\"));    // hide MQTT settings\n    #endif\n\n    #ifndef WLED_DISABLE_HUESYNC\n    printSetFormValue(settingsScript,PSTR(\"H0\"),hueIP[0]);\n    printSetFormValue(settingsScript,PSTR(\"H1\"),hueIP[1]);\n    printSetFormValue(settingsScript,PSTR(\"H2\"),hueIP[2]);\n    printSetFormValue(settingsScript,PSTR(\"H3\"),hueIP[3]);\n    printSetFormValue(settingsScript,PSTR(\"HL\"),huePollLightId);\n    printSetFormValue(settingsScript,PSTR(\"HI\"),huePollIntervalMs);\n    printSetFormCheckbox(settingsScript,PSTR(\"HP\"),huePollingEnabled);\n    printSetFormCheckbox(settingsScript,PSTR(\"HO\"),hueApplyOnOff);\n    printSetFormCheckbox(settingsScript,PSTR(\"HB\"),hueApplyBri);\n    printSetFormCheckbox(settingsScript,PSTR(\"HC\"),hueApplyColor);\n    char hueErrorString[25];\n    switch (hueError)\n    {\n      case HUE_ERROR_INACTIVE     : strcpy_P(hueErrorString,PSTR(\"Inactive\"));                break;\n      case HUE_ERROR_ACTIVE       : strcpy_P(hueErrorString,PSTR(\"Active\"));                  break;\n      case HUE_ERROR_UNAUTHORIZED : strcpy_P(hueErrorString,PSTR(\"Unauthorized\"));            break;\n      case HUE_ERROR_LIGHTID      : strcpy_P(hueErrorString,PSTR(\"Invalid light ID\"));        break;\n      case HUE_ERROR_PUSHLINK     : strcpy_P(hueErrorString,PSTR(\"Link button not pressed\")); break;\n      case HUE_ERROR_JSON_PARSING : strcpy_P(hueErrorString,PSTR(\"JSON parsing error\"));      break;\n      case HUE_ERROR_TIMEOUT      : strcpy_P(hueErrorString,PSTR(\"Timeout\"));                 break;\n      default: sprintf_P(hueErrorString,PSTR(\"Bridge Error %i\"),hueError);\n    }\n\n    printSetClassElementHTML(settingsScript,PSTR(\"sip\"),0,hueErrorString);\n    #else\n    settingsScript.print(F(\"toggle('Hue');\"));    // hide Hue Sync settings\n    #endif\n    printSetFormValue(settingsScript,PSTR(\"BD\"),serialBaud);\n    #ifndef WLED_ENABLE_ADALIGHT\n    settingsScript.print(F(\"toggle('Serial');\"));\n    #endif\n  }\n\n  if (subPage == SUBPAGE_TIME)\n  {\n    printSetFormCheckbox(settingsScript,PSTR(\"NT\"),ntpEnabled);\n    printSetFormValue(settingsScript,PSTR(\"NS\"),ntpServerName);\n    printSetFormCheckbox(settingsScript,PSTR(\"CF\"),!useAMPM);\n    printSetFormIndex(settingsScript,PSTR(\"TZ\"),currentTimezone);\n    printSetFormValue(settingsScript,PSTR(\"UO\"),utcOffsetSecs);\n    char tm[32];\n    dtostrf(longitude,4,2,tm);\n    printSetFormValue(settingsScript,PSTR(\"LN\"),tm);\n    dtostrf(latitude,4,2,tm);\n    printSetFormValue(settingsScript,PSTR(\"LT\"),tm);\n    getTimeString(tm);\n    printSetClassElementHTML(settingsScript,PSTR(\"times\"),0,tm);\n    if ((int)(longitude*10.0f) || (int)(latitude*10.0f)) {\n      sprintf_P(tm, PSTR(\"Sunrise: %02d:%02d Sunset: %02d:%02d\"), hour(sunrise), minute(sunrise), hour(sunset), minute(sunset));\n      printSetClassElementHTML(settingsScript,PSTR(\"times\"),1,tm);\n    }\n    printSetFormCheckbox(settingsScript,PSTR(\"OL\"),overlayCurrent);\n    printSetFormValue(settingsScript,PSTR(\"O1\"),overlayMin);\n    printSetFormValue(settingsScript,PSTR(\"O2\"),overlayMax);\n    printSetFormValue(settingsScript,PSTR(\"OM\"),analogClock12pixel);\n    printSetFormCheckbox(settingsScript,PSTR(\"OS\"),analogClockSecondsTrail);\n    printSetFormCheckbox(settingsScript,PSTR(\"O5\"),analogClock5MinuteMarks);\n    printSetFormCheckbox(settingsScript,PSTR(\"OB\"),analogClockSolidBlack);\n\n    printSetFormCheckbox(settingsScript,PSTR(\"CE\"),countdownMode);\n    printSetFormValue(settingsScript,PSTR(\"CY\"),countdownYear);\n    printSetFormValue(settingsScript,PSTR(\"CI\"),countdownMonth);\n    printSetFormValue(settingsScript,PSTR(\"CD\"),countdownDay);\n    printSetFormValue(settingsScript,PSTR(\"CH\"),countdownHour);\n    printSetFormValue(settingsScript,PSTR(\"CM\"),countdownMin);\n    printSetFormValue(settingsScript,PSTR(\"CS\"),countdownSec);\n\n    printSetFormValue(settingsScript,PSTR(\"A0\"),macroAlexaOn);\n    printSetFormValue(settingsScript,PSTR(\"A1\"),macroAlexaOff);\n    printSetFormValue(settingsScript,PSTR(\"MC\"),macroCountdown);\n    printSetFormValue(settingsScript,PSTR(\"MN\"),macroNl);\n    int ii = 0;\n    for (const auto &button : buttons) {\n      settingsScript.printf_P(PSTR(\"addRow(%d,%d,%d,%d);\"), ii++, button.macroButton, button.macroLongPress, button.macroDoublePress);\n    }\n\n    char k[4];\n    k[2] = 0; //Time macros\n    for (int i = 0; i<10; i++)\n    {\n      k[1] = 48+i; //ascii 0,1,2,3\n      if (i<8) { k[0] = 'H'; printSetFormValue(settingsScript,k,timerHours[i]); }\n      k[0] = 'N'; printSetFormValue(settingsScript,k,timerMinutes[i]);\n      k[0] = 'T'; printSetFormValue(settingsScript,k,timerMacro[i]);\n      k[0] = 'W'; printSetFormValue(settingsScript,k,timerWeekday[i]);\n      if (i<8) {\n        k[0] = 'M'; printSetFormValue(settingsScript,k,(timerMonth[i] >> 4) & 0x0F);\n\t\t\t\tk[0] = 'P'; printSetFormValue(settingsScript,k,timerMonth[i] & 0x0F);\n        k[0] = 'D'; printSetFormValue(settingsScript,k,timerDay[i]);\n\t\t\t\tk[0] = 'E'; printSetFormValue(settingsScript,k,timerDayEnd[i]);\n      }\n    }\n  }\n\n  if (subPage == SUBPAGE_SEC)\n  {\n    byte l = strlen(settingsPIN);\n    char fpass[l+1]; //fill PIN field with 0000\n    fpass[l] = 0;\n    memset(fpass,'0',l);\n    printSetFormValue(settingsScript,PSTR(\"PIN\"),fpass);\n    printSetFormCheckbox(settingsScript,PSTR(\"NO\"),otaLock);\n    printSetFormCheckbox(settingsScript,PSTR(\"OW\"),wifiLock);\n    printSetFormCheckbox(settingsScript,PSTR(\"AO\"),aOtaEnabled);\n    printSetFormCheckbox(settingsScript,PSTR(\"SU\"),otaSameSubnet);\n    char tmp_buf[128];\n    fillWLEDVersion(tmp_buf,sizeof(tmp_buf));\n    printSetClassElementHTML(settingsScript,PSTR(\"sip\"),0,tmp_buf);\n    settingsScript.printf_P(PSTR(\"sd=\\\"%s\\\";\"), serverDescription);\n    //hide settings if not compiled\n    #ifdef WLED_DISABLE_OTA\n    settingsScript.print(F(\"toggle('OTA');\"));  // hide update section\n    #endif\n    #ifndef WLED_ENABLE_AOTA\n    settingsScript.print(F(\"toggle('aOTA');\"));  // hide ArduinoOTA checkbox\n    #endif\n  }\n\n  #ifdef WLED_ENABLE_DMX // include only if DMX is enabled\n  if (subPage == SUBPAGE_DMX)\n  {\n    printSetFormValue(settingsScript,PSTR(\"PU\"),e131ProxyUniverse);\n\n    printSetFormValue(settingsScript,PSTR(\"CN\"),DMXChannels);\n    printSetFormValue(settingsScript,PSTR(\"CG\"),DMXGap);\n    printSetFormValue(settingsScript,PSTR(\"CS\"),DMXStart);\n    printSetFormValue(settingsScript,PSTR(\"SL\"),DMXStartLED);\n\n    printSetFormIndex(settingsScript,PSTR(\"CH1\"),DMXFixtureMap[0]);\n    printSetFormIndex(settingsScript,PSTR(\"CH2\"),DMXFixtureMap[1]);\n    printSetFormIndex(settingsScript,PSTR(\"CH3\"),DMXFixtureMap[2]);\n    printSetFormIndex(settingsScript,PSTR(\"CH4\"),DMXFixtureMap[3]);\n    printSetFormIndex(settingsScript,PSTR(\"CH5\"),DMXFixtureMap[4]);\n    printSetFormIndex(settingsScript,PSTR(\"CH6\"),DMXFixtureMap[5]);\n    printSetFormIndex(settingsScript,PSTR(\"CH7\"),DMXFixtureMap[6]);\n    printSetFormIndex(settingsScript,PSTR(\"CH8\"),DMXFixtureMap[7]);\n    printSetFormIndex(settingsScript,PSTR(\"CH9\"),DMXFixtureMap[8]);\n    printSetFormIndex(settingsScript,PSTR(\"CH10\"),DMXFixtureMap[9]);\n    printSetFormIndex(settingsScript,PSTR(\"CH11\"),DMXFixtureMap[10]);\n    printSetFormIndex(settingsScript,PSTR(\"CH12\"),DMXFixtureMap[11]);\n    printSetFormIndex(settingsScript,PSTR(\"CH13\"),DMXFixtureMap[12]);\n    printSetFormIndex(settingsScript,PSTR(\"CH14\"),DMXFixtureMap[13]);\n    printSetFormIndex(settingsScript,PSTR(\"CH15\"),DMXFixtureMap[14]);\n  }\n  #endif\n\n  if (subPage == SUBPAGE_UM) //usermods\n  {\n    appendGPIOinfo(settingsScript);\n    settingsScript.printf_P(PSTR(\"numM=%d;\"), UsermodManager::getModCount());\n    printSetFormValue(settingsScript,PSTR(\"SDA\"),i2c_sda);\n    printSetFormValue(settingsScript,PSTR(\"SCL\"),i2c_scl);\n    printSetFormValue(settingsScript,PSTR(\"MOSI\"),spi_mosi);\n    printSetFormValue(settingsScript,PSTR(\"MISO\"),spi_miso);\n    printSetFormValue(settingsScript,PSTR(\"SCLK\"),spi_sclk);\n    settingsScript.printf_P(PSTR(\"addInfo('SDA','%d');\"\n                 \"addInfo('SCL','%d');\"\n                 \"addInfo('MOSI','%d');\"\n                 \"addInfo('MISO','%d');\"\n                 \"addInfo('SCLK','%d');\"),\n      HW_PIN_SDA, HW_PIN_SCL, HW_PIN_DATASPI, HW_PIN_MISOSPI, HW_PIN_CLOCKSPI\n    );\n    UsermodManager::appendConfigData(settingsScript);\n  }\n\n  if (subPage == SUBPAGE_2D) // 2D matrices\n  {\n    printSetFormValue(settingsScript,PSTR(\"SOMP\"),strip.isMatrix);\n    #ifndef WLED_DISABLE_2D\n    settingsScript.printf_P(PSTR(\"maxPanels=%d;resetPanels();\"),WLED_MAX_PANELS);\n    if (strip.isMatrix) {\n      printSetFormValue(settingsScript,PSTR(\"PW\"),strip.panel.size()>0?strip.panel[0].width:8); //Set generator Width and Height to first panel size for convenience\n      printSetFormValue(settingsScript,PSTR(\"PH\"),strip.panel.size()>0?strip.panel[0].height:8);\n      printSetFormValue(settingsScript,PSTR(\"MPC\"),strip.panel.size());\n      // panels\n      for (unsigned i=0; i<strip.panel.size(); i++) {\n        settingsScript.printf_P(PSTR(\"addPanel(%d);\"), i);\n        char pO[8] = { '\\0' };\n        snprintf_P(pO, 7, PSTR(\"P%d\"), i);       // WLED_WLED_MAX_PANELS is less than 100 so pO will always only be 4 characters or less\n        pO[7] = '\\0';\n        unsigned l = strlen(pO);\n        // create P0B, P1B, ..., P63B, etc for other PxxX\n        pO[l] = 'B'; printSetFormValue(settingsScript,pO,strip.panel[i].bottomStart);\n        pO[l] = 'R'; printSetFormValue(settingsScript,pO,strip.panel[i].rightStart);\n        pO[l] = 'V'; printSetFormValue(settingsScript,pO,strip.panel[i].vertical);\n        pO[l] = 'S'; printSetFormCheckbox(settingsScript,pO,strip.panel[i].serpentine);\n        pO[l] = 'X'; printSetFormValue(settingsScript,pO,strip.panel[i].xOffset);\n        pO[l] = 'Y'; printSetFormValue(settingsScript,pO,strip.panel[i].yOffset);\n        pO[l] = 'W'; printSetFormValue(settingsScript,pO,strip.panel[i].width);\n        pO[l] = 'H'; printSetFormValue(settingsScript,pO,strip.panel[i].height);\n      }\n    }\n    #else\n    settingsScript.print(F(\"gId(\\\"somp\\\").remove(1);\")); // remove 2D option from dropdown\n    #endif\n  }\n\n  if (subPage == SUBPAGE_PINS) // pins info\n  {\n    appendGPIOinfo(settingsScript);\n  }\n}\n"
  }
]