diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index d09c8a60..1340da91 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -2,12 +2,7 @@ # [Choice] Python version: 3, 3.9, 3.8, 3.7, 3.6 ARG VARIANT="3" -FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} - -# [Option] Install Node.js -ARG INSTALL_NODE="true" -ARG NODE_VERSION="lts/*" -RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi +FROM mcr.microsoft.com/devcontainers/python:0-${VARIANT} # [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. # COPY requirements.txt /tmp/pip-tmp/ diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2a8e4712..58864182 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,10 +5,7 @@ "context": "..", "args": { // Update 'VARIANT' to pick a Python version: 3, 3.6, 3.7, 3.8, 3.9 - "VARIANT": "3", - // Options - "INSTALL_NODE": "true", - "NODE_VERSION": "lts/*" + "VARIANT": "3" } }, @@ -27,34 +24,35 @@ // risk to running the build directly on the host. // "runArgs": ["--privileged", "-v", "/dev/bus/usb:/dev/bus/usb", "--group-add", "dialout"], - // Set *default* container specific settings.json values on container create. - "settings": { - "terminal.integrated.shell.linux": "/bin/bash", - "python.pythonPath": "/usr/local/bin/python", - "python.linting.enabled": true, - "python.linting.pylintEnabled": true, - "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", - "python.formatting.blackPath": "/usr/local/py-utils/bin/black", - "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", - "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", - "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", - "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", - "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", - "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", - "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint" + "customizations": { + "vscode": { + "settings": { + "terminal.integrated.shell.linux": "/bin/bash", + "python.pythonPath": "/usr/local/bin/python", + "python.linting.enabled": true, + "python.linting.pylintEnabled": true, + "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", + "python.formatting.blackPath": "/usr/local/py-utils/bin/black", + "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", + "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", + "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", + "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", + "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", + "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", + "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint" + }, + "extensions": [ + "ms-python.python", + "platformio.platformio-ide" + ] + } }, - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "ms-python.python", - "platformio.platformio-ide" - ], - // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "npm install", + "postCreateCommand": "bash -i -c 'nvm install && npm ci'", // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "vscode" diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 285ad419..6f010aa6 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -80,7 +80,7 @@ body: id: terms attributes: label: Code of Conduct - description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/Aircoookie/WLED/blob/master/CODE_OF_CONDUCT.md) + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/wled-dev/WLED/blob/main/CODE_OF_CONDUCT.md) options: - label: I agree to follow this project's Code of Conduct required: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e5fdfc5a..6f2c373a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: build: - name: Build Enviornments + name: Build Environments runs-on: ubuntu-latest needs: get_default_envs strategy: @@ -38,6 +38,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: + node-version-file: '.nvmrc' cache: 'npm' - run: npm ci - name: Cache PlatformIO @@ -56,6 +57,7 @@ jobs: cache: 'pip' - name: Install PlatformIO run: pip install -r requirements.txt + - name: Build firmware run: pio run -e ${{ matrix.environment }} - uses: actions/upload-artifact@v4 @@ -74,7 +76,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: '20.x' + node-version-file: '.nvmrc' cache: 'npm' - run: npm ci - run: npm test diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 00000000..818ab94e --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,47 @@ + +name: Deploy Nightly +on: + # This can be used to automatically publish nightlies at UTC nighttime + schedule: + - cron: '0 2 * * *' # run at 2 AM UTC + # This can be used to allow manually triggering nightlies from the web interface + workflow_dispatch: + +jobs: + wled_build: + uses: ./.github/workflows/build.yml + nightly: + name: Deploy nightly + runs-on: ubuntu-latest + needs: wled_build + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + merge-multiple: true + - name: Show Files + run: ls -la + - name: "✏️ Generate release changelog" + id: changelog + uses: janheinrichmerker/action-github-changelog-generator@v2.3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + sinceTag: v0.15.0 + - name: Update Nightly Release + uses: andelf/nightly-release@main + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: nightly + name: 'Nightly Release $$' + prerelease: true + body: ${{ steps.changelog.outputs.changelog }} + files: | + *.bin + *.bin.gz + - name: Repository Dispatch + uses: peter-evans/repository-dispatch@v3 + with: + repository: wled/WLED-WebInstaller + event-type: release-nightly + token: ${{ secrets.PAT_PUBLIC }} diff --git a/.github/workflows/pr-merge.yaml b/.github/workflows/pr-merge.yaml new file mode 100644 index 00000000..5f216100 --- /dev/null +++ b/.github/workflows/pr-merge.yaml @@ -0,0 +1,33 @@ + name: Notify Discord on PR Merge + on: + workflow_dispatch: + pull_request: + types: [closed] + + jobs: + notify: + runs-on: ubuntu-latest + steps: + - name: Get User Permission + id: checkAccess + uses: actions-cool/check-user-permission@v2 + with: + require: write + username: ${{ github.triggering_actor }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Check User Permission + if: steps.checkAccess.outputs.require-result == 'false' + run: | + echo "${{ github.triggering_actor }} does not have permissions on this repo." + echo "Current permission level is ${{ steps.checkAccess.outputs.user-permission }}" + echo "Job originally triggered by ${{ github.actor }}" + exit 1 + - name: Checkout code + uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} # This is dangerous without the first access check + - name: Send Discord notification + # if: github.event.pull_request.merged == true + run: | + curl -H "Content-Type: application/json" -d '{"content": "Pull Request ${{ github.event.pull_request.number }} merged by ${{ github.actor }}"}' ${{ secrets.DISCORD_WEBHOOK_BETA_TESTERS }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 27beec99..dfcb808d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,9 +18,16 @@ jobs: - uses: actions/download-artifact@v4 with: merge-multiple: true + - name: "✏️ Generate release changelog" + id: changelog + uses: janheinrichmerker/action-github-changelog-generator@v2.3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + sinceTag: v0.15.0 - name: Create draft release uses: softprops/action-gh-release@v1 with: + body: ${{ steps.changelog.outputs.changelog }} draft: True files: | *.bin diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 00000000..a9b7aa9b --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,13 @@ +on: + workflow_dispatch: + +jobs: + dispatch: + runs-on: ubuntu-latest + steps: + - name: Repository Dispatch + uses: peter-evans/repository-dispatch@v3 + with: + repository: wled/WLED-WebInstaller + event-type: release-nightly + token: ${{ secrets.PAT_PUBLIC }} diff --git a/.gitignore b/.gitignore index 8f083e3f..51d321d9 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ wled-update.sh /build_output/ /node_modules/ +/logs/ /wled00/extLibs /wled00/LittleFS diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..10fef252 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20.18 diff --git a/CHANGELOG.md b/CHANGELOG.md index c570ac1f..f591fc2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -173,7 +173,7 @@ - v0.15.0-b2 - WS2805 support (RGB + WW + CW, 600kbps) - Unified PSRAM use -- NeoPixelBus v2.7.9 +- NeoPixelBus v2.7.9 (for future WS2805 support) - Ubiquitous PSRAM mode for all variants of ESP32 - SSD1309_64 I2C Support for FLD Usermod (#3836 by @THATDONFC) - Palette cycling fix (add support for `{"seg":[{"pal":"X~Y~"}]}` or `{"seg":[{"pal":"X~Yr"}]}`) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 06c221fc..e2078df7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ A good description helps us to review and understand your proposed changes. For ### Target branch for pull requests -Please make all PRs against the `0_15` branch. +Please make all PRs against the `main` branch. ### Updating your code While the PR is open - and under review by maintainers - you may be asked to modify your PR source code. @@ -27,7 +27,7 @@ Github will pick up the changes so your PR stays up-to-date. > For example, we regularly lost review comments when the PR author force-pushes code changes. So, pretty please, do not force-push. -You can find a collection of very useful tips and tricks here: https://github.com/Aircoookie/WLED/wiki/How-to-properly-submit-a-PR +You can find a collection of very useful tips and tricks here: https://github.com/wled-dev/WLED/wiki/How-to-properly-submit-a-PR ### Code style @@ -105,4 +105,4 @@ Good: There is no hard character limit for a comment within a line, though as a rule of thumb consider wrapping after 120 characters. -Inline comments are OK if they describe that line only and are not exceedingly wide. \ No newline at end of file +Inline comments are OK if they describe that line only and are not exceedingly wide. diff --git a/boards/lolin_s3_mini.json b/boards/lolin_s3_mini.json new file mode 100644 index 00000000..7f55f0bd --- /dev/null +++ b/boards/lolin_s3_mini.json @@ -0,0 +1,47 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_qspi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_LOLIN_S3_MINI", + "-DARDUINO_USB_MODE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [ + [ + "0x303A", + "0x8167" + ] + ], + "mcu": "esp32s3", + "variant": "lolin_s3_mini" + }, + "connectivity": [ + "bluetooth", + "wifi" + ], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "WEMOS LOLIN S3 Mini", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 327680, + "maximum_size": 4194304, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://www.wemos.cc/en/latest/s3/index.html", + "vendor": "WEMOS" +} + \ No newline at end of file diff --git a/lib/ESP8266PWM/src/core_esp8266_waveform_phase.cpp b/lib/ESP8266PWM/src/core_esp8266_waveform_phase.cpp new file mode 100644 index 00000000..68cb9010 --- /dev/null +++ b/lib/ESP8266PWM/src/core_esp8266_waveform_phase.cpp @@ -0,0 +1,504 @@ +/* esp8266_waveform imported from platform source code + Modified for WLED to work around a fault in the NMI handling, + which can result in the system locking up and hard WDT crashes. + + Imported from https://github.com/esp8266/Arduino/blob/7e0d20e2b9034994f573a236364e0aef17fd66de/cores/esp8266/core_esp8266_waveform_phase.cpp +*/ + + +/* + esp8266_waveform - General purpose waveform generation and control, + supporting outputs on all pins in parallel. + + Copyright (c) 2018 Earle F. Philhower, III. All rights reserved. + Copyright (c) 2020 Dirk O. Kaar. + + The core idea is to have a programmable waveform generator with a unique + high and low period (defined in microseconds or CPU clock cycles). TIMER1 is + set to 1-shot mode and is always loaded with the time until the next edge + of any live waveforms. + + Up to one waveform generator per pin supported. + + Each waveform generator is synchronized to the ESP clock cycle counter, not the + timer. This allows for removing interrupt jitter and delay as the counter + always increments once per 80MHz clock. Changes to a waveform are + contiguous and only take effect on the next waveform transition, + allowing for smooth transitions. + + This replaces older tone(), analogWrite(), and the Servo classes. + + Everywhere in the code where "ccy" or "ccys" is used, it means ESP.getCycleCount() + clock cycle time, or an interval measured in clock cycles, but not TIMER1 + cycles (which may be 2 CPU clock cycles @ 160MHz). + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "core_esp8266_waveform.h" +#include +#include "debug.h" +#include "ets_sys.h" +#include + + +// ----- @willmmiles begin patch ----- +// Linker magic +extern "C" void usePWMFixedNMI(void) {}; + +// NMI crash workaround +// Sometimes the NMI fails to return, stalling the CPU. When this happens, +// the next NMI gets a return address /inside the NMI handler function/. +// We work around this by caching the last NMI return address, and restoring +// the epc3 and eps3 registers to the previous values if the observed epc3 +// happens to be pointing to the _NMILevelVector function. +extern "C" void _NMILevelVector(); +extern "C" void _UserExceptionVector_1(); // the next function after _NMILevelVector +static inline IRAM_ATTR void nmiCrashWorkaround() { + static uintptr_t epc3_backup, eps3_backup; + + uintptr_t epc3, eps3; + __asm__ __volatile__("rsr %0,epc3; rsr %1,eps3":"=a"(epc3),"=a" (eps3)); + if ((epc3 < (uintptr_t) &_NMILevelVector) || (epc3 >= (uintptr_t) &_UserExceptionVector_1)) { + // Address is good; save backup + epc3_backup = epc3; + eps3_backup = eps3; + } else { + // Address is inside the NMI handler -- restore from backup + __asm__ __volatile__("wsr %0,epc3; wsr %1,eps3"::"a"(epc3_backup),"a"(eps3_backup)); + } +} +// ----- @willmmiles end patch ----- + + +// No-op calls to override the PWM implementation +extern "C" void _setPWMFreq_weak(uint32_t freq) { (void) freq; } +extern "C" IRAM_ATTR bool _stopPWM_weak(int pin) { (void) pin; return false; } +extern "C" bool _setPWM_weak(int pin, uint32_t val, uint32_t range) { (void) pin; (void) val; (void) range; return false; } + + +// Timer is 80MHz fixed. 160MHz CPU frequency need scaling. +constexpr bool ISCPUFREQ160MHZ = clockCyclesPerMicrosecond() == 160; +// Maximum delay between IRQs, Timer1, <= 2^23 / 80MHz +constexpr int32_t MAXIRQTICKSCCYS = microsecondsToClockCycles(10000); +// Maximum servicing time for any single IRQ +constexpr uint32_t ISRTIMEOUTCCYS = microsecondsToClockCycles(18); +// The latency between in-ISR rearming of the timer and the earliest firing +constexpr int32_t IRQLATENCYCCYS = microsecondsToClockCycles(2); +// The SDK and hardware take some time to actually get to our NMI code +constexpr int32_t DELTAIRQCCYS = ISCPUFREQ160MHZ ? + microsecondsToClockCycles(2) >> 1 : microsecondsToClockCycles(2); + +// for INFINITE, the NMI proceeds on the waveform without expiry deadline. +// for EXPIRES, the NMI expires the waveform automatically on the expiry ccy. +// for UPDATEEXPIRY, the NMI recomputes the exact expiry ccy and transitions to EXPIRES. +// for UPDATEPHASE, the NMI recomputes the target timings +// for INIT, the NMI initializes nextPeriodCcy, and if expiryCcy != 0 includes UPDATEEXPIRY. +enum class WaveformMode : uint8_t {INFINITE = 0, EXPIRES = 1, UPDATEEXPIRY = 2, UPDATEPHASE = 3, INIT = 4}; + +// Waveform generator can create tones, PWM, and servos +typedef struct { + uint32_t nextPeriodCcy; // ESP clock cycle when a period begins. + uint32_t endDutyCcy; // ESP clock cycle when going from duty to off + int32_t dutyCcys; // Set next off cycle at low->high to maintain phase + int32_t adjDutyCcys; // Temporary correction for next period + int32_t periodCcys; // Set next phase cycle at low->high to maintain phase + uint32_t expiryCcy; // For time-limited waveform, the CPU clock cycle when this waveform must stop. If WaveformMode::UPDATE, temporarily holds relative ccy count + WaveformMode mode; + bool autoPwm; // perform PWM duty to idle cycle ratio correction under high load at the expense of precise timings +} Waveform; + +namespace { + + static struct { + Waveform pins[17]; // State of all possible pins + uint32_t states = 0; // Is the pin high or low, updated in NMI so no access outside the NMI code + uint32_t enabled = 0; // Is it actively running, updated in NMI so no access outside the NMI code + + // Enable lock-free by only allowing updates to waveform.states and waveform.enabled from IRQ service routine + int32_t toSetBits = 0; // Message to the NMI handler to start/modify exactly one waveform + int32_t toDisableBits = 0; // Message to the NMI handler to disable exactly one pin from waveform generation + + // toSetBits temporaries + // cheaper than packing them in every Waveform, since we permit only one use at a time + uint32_t phaseCcy; // positive phase offset ccy count + int8_t alignPhase; // < 0 no phase alignment, otherwise starts waveform in relative phase offset to given pin + + uint32_t(*timer1CB)() = nullptr; + + bool timer1Running = false; + + uint32_t nextEventCcy; + } waveform; + +} + +// Interrupt on/off control +static IRAM_ATTR void timer1Interrupt(); + +// Non-speed critical bits +#pragma GCC optimize ("Os") + +static void initTimer() { + timer1_disable(); + ETS_FRC_TIMER1_INTR_ATTACH(NULL, NULL); + ETS_FRC_TIMER1_NMI_INTR_ATTACH(timer1Interrupt); + timer1_enable(TIM_DIV1, TIM_EDGE, TIM_SINGLE); + waveform.timer1Running = true; + timer1_write(IRQLATENCYCCYS); // Cause an interrupt post-haste +} + +static void IRAM_ATTR deinitTimer() { + ETS_FRC_TIMER1_NMI_INTR_ATTACH(NULL); + timer1_disable(); + timer1_isr_init(); + waveform.timer1Running = false; +} + +extern "C" { + +// Set a callback. Pass in NULL to stop it +void setTimer1Callback_weak(uint32_t (*fn)()) { + waveform.timer1CB = fn; + std::atomic_thread_fence(std::memory_order_acq_rel); + if (!waveform.timer1Running && fn) { + initTimer(); + } else if (waveform.timer1Running && !fn && !waveform.enabled) { + deinitTimer(); + } +} + +// Start up a waveform on a pin, or change the current one. Will change to the new +// waveform smoothly on next low->high transition. For immediate change, stopWaveform() +// first, then it will immediately begin. +int startWaveformClockCycles_weak(uint8_t pin, uint32_t highCcys, uint32_t lowCcys, + uint32_t runTimeCcys, int8_t alignPhase, uint32_t phaseOffsetCcys, bool autoPwm) { + uint32_t periodCcys = highCcys + lowCcys; + if (periodCcys < MAXIRQTICKSCCYS) { + if (!highCcys) { + periodCcys = (MAXIRQTICKSCCYS / periodCcys) * periodCcys; + } + else if (!lowCcys) { + highCcys = periodCcys = (MAXIRQTICKSCCYS / periodCcys) * periodCcys; + } + } + // sanity checks, including mixed signed/unsigned arithmetic safety + if ((pin > 16) || isFlashInterfacePin(pin) || (alignPhase > 16) || + static_cast(periodCcys) <= 0 || + static_cast(highCcys) < 0 || static_cast(lowCcys) < 0) { + return false; + } + Waveform& wave = waveform.pins[pin]; + wave.dutyCcys = highCcys; + wave.adjDutyCcys = 0; + wave.periodCcys = periodCcys; + wave.autoPwm = autoPwm; + waveform.alignPhase = (alignPhase < 0) ? -1 : alignPhase; + waveform.phaseCcy = phaseOffsetCcys; + + std::atomic_thread_fence(std::memory_order_acquire); + const uint32_t pinBit = 1UL << pin; + if (!(waveform.enabled & pinBit)) { + // wave.nextPeriodCcy and wave.endDutyCcy are initialized by the ISR + wave.expiryCcy = runTimeCcys; // in WaveformMode::INIT, temporarily hold relative cycle count + wave.mode = WaveformMode::INIT; + if (!wave.dutyCcys) { + // If initially at zero duty cycle, force GPIO off + if (pin == 16) { + GP16O = 0; + } + else { + GPOC = pinBit; + } + } + std::atomic_thread_fence(std::memory_order_release); + waveform.toSetBits = 1UL << pin; + std::atomic_thread_fence(std::memory_order_release); + if (!waveform.timer1Running) { + initTimer(); + } + else if (T1V > IRQLATENCYCCYS) { + // Must not interfere if Timer is due shortly + timer1_write(IRQLATENCYCCYS); + } + } + else { + wave.mode = WaveformMode::INFINITE; // turn off possible expiry to make update atomic from NMI + std::atomic_thread_fence(std::memory_order_release); + if (runTimeCcys) { + wave.expiryCcy = runTimeCcys; // in WaveformMode::UPDATEEXPIRY, temporarily hold relative cycle count + wave.mode = WaveformMode::UPDATEEXPIRY; + std::atomic_thread_fence(std::memory_order_release); + waveform.toSetBits = 1UL << pin; + } else if (alignPhase >= 0) { + // @willmmiles new feature + wave.mode = WaveformMode::UPDATEPHASE; // recalculate start + std::atomic_thread_fence(std::memory_order_release); + waveform.toSetBits = 1UL << pin; + } + } + std::atomic_thread_fence(std::memory_order_acq_rel); + while (waveform.toSetBits) { + esp_yield(); // Wait for waveform to update + std::atomic_thread_fence(std::memory_order_acquire); + } + return true; +} + +// Stops a waveform on a pin +IRAM_ATTR int stopWaveform_weak(uint8_t pin) { + // Can't possibly need to stop anything if there is no timer active + if (!waveform.timer1Running) { + return false; + } + // If user sends in a pin >16 but <32, this will always point to a 0 bit + // If they send >=32, then the shift will result in 0 and it will also return false + std::atomic_thread_fence(std::memory_order_acquire); + const uint32_t pinBit = 1UL << pin; + if (waveform.enabled & pinBit) { + waveform.toDisableBits = 1UL << pin; + std::atomic_thread_fence(std::memory_order_release); + // Must not interfere if Timer is due shortly + if (T1V > IRQLATENCYCCYS) { + timer1_write(IRQLATENCYCCYS); + } + while (waveform.toDisableBits) { + /* no-op */ // Can't delay() since stopWaveform may be called from an IRQ + std::atomic_thread_fence(std::memory_order_acquire); + } + } + if (!waveform.enabled && !waveform.timer1CB) { + deinitTimer(); + } + return true; +} + +}; + +// Speed critical bits +#pragma GCC optimize ("O2") + +// For dynamic CPU clock frequency switch in loop the scaling logic would have to be adapted. +// Using constexpr makes sure that the CPU clock frequency is compile-time fixed. +static inline IRAM_ATTR int32_t scaleCcys(const int32_t ccys, const bool isCPU2X) { + if (ISCPUFREQ160MHZ) { + return isCPU2X ? ccys : (ccys >> 1); + } + else { + return isCPU2X ? (ccys << 1) : ccys; + } +} + +static IRAM_ATTR void timer1Interrupt() { + const uint32_t isrStartCcy = ESP.getCycleCount(); + //int32_t clockDrift = isrStartCcy - waveform.nextEventCcy; + + // ----- @willmmiles begin patch ----- + nmiCrashWorkaround(); + // ----- @willmmiles end patch ----- + + const bool isCPU2X = CPU2X & 1; + if ((waveform.toSetBits && !(waveform.enabled & waveform.toSetBits)) || waveform.toDisableBits) { + // Handle enable/disable requests from main app. + waveform.enabled = (waveform.enabled & ~waveform.toDisableBits) | waveform.toSetBits; // Set the requested waveforms on/off + // Find the first GPIO being generated by checking GCC's find-first-set (returns 1 + the bit of the first 1 in an int32_t) + waveform.toDisableBits = 0; + } + + if (waveform.toSetBits) { + const int toSetPin = __builtin_ffs(waveform.toSetBits) - 1; + Waveform& wave = waveform.pins[toSetPin]; + switch (wave.mode) { + case WaveformMode::INIT: + waveform.states &= ~waveform.toSetBits; // Clear the state of any just started + if (waveform.alignPhase >= 0 && waveform.enabled & (1UL << waveform.alignPhase)) { + wave.nextPeriodCcy = waveform.pins[waveform.alignPhase].nextPeriodCcy + scaleCcys(waveform.phaseCcy, isCPU2X); + } + else { + wave.nextPeriodCcy = waveform.nextEventCcy; + } + if (!wave.expiryCcy) { + wave.mode = WaveformMode::INFINITE; + break; + } + // fall through + case WaveformMode::UPDATEEXPIRY: + // in WaveformMode::UPDATEEXPIRY, expiryCcy temporarily holds relative CPU cycle count + wave.expiryCcy = wave.nextPeriodCcy + scaleCcys(wave.expiryCcy, isCPU2X); + wave.mode = WaveformMode::EXPIRES; + break; + // @willmmiles new feature + case WaveformMode::UPDATEPHASE: + // in WaveformMode::UPDATEPHASE, we recalculate the targets + if ((waveform.alignPhase >= 0) && (waveform.enabled & (1UL << waveform.alignPhase))) { + // Compute phase shift to realign with target + auto const newPeriodCcy = waveform.pins[waveform.alignPhase].nextPeriodCcy + scaleCcys(waveform.phaseCcy, isCPU2X); + auto const period = scaleCcys(wave.periodCcys, isCPU2X); + auto shift = ((static_cast (newPeriodCcy - wave.nextPeriodCcy) + period/2) % period) - (period/2); + wave.nextPeriodCcy += static_cast(shift); + if (static_cast(wave.endDutyCcy - wave.nextPeriodCcy) > 0) { + wave.endDutyCcy = wave.nextPeriodCcy; + } + } + default: + break; + } + waveform.toSetBits = 0; + } + + // Exit the loop if the next event, if any, is sufficiently distant. + const uint32_t isrTimeoutCcy = isrStartCcy + ISRTIMEOUTCCYS; + uint32_t busyPins = waveform.enabled; + waveform.nextEventCcy = isrStartCcy + MAXIRQTICKSCCYS; + + uint32_t now = ESP.getCycleCount(); + uint32_t isrNextEventCcy = now; + while (busyPins) { + if (static_cast(isrNextEventCcy - now) > IRQLATENCYCCYS) { + waveform.nextEventCcy = isrNextEventCcy; + break; + } + isrNextEventCcy = waveform.nextEventCcy; + uint32_t loopPins = busyPins; + while (loopPins) { + const int pin = __builtin_ffsl(loopPins) - 1; + const uint32_t pinBit = 1UL << pin; + loopPins ^= pinBit; + + Waveform& wave = waveform.pins[pin]; + +/* @willmmiles - wtf? We don't want to accumulate drift + if (clockDrift) { + wave.endDutyCcy += clockDrift; + wave.nextPeriodCcy += clockDrift; + wave.expiryCcy += clockDrift; + } +*/ + + uint32_t waveNextEventCcy = (waveform.states & pinBit) ? wave.endDutyCcy : wave.nextPeriodCcy; + if (WaveformMode::EXPIRES == wave.mode && + static_cast(waveNextEventCcy - wave.expiryCcy) >= 0 && + static_cast(now - wave.expiryCcy) >= 0) { + // Disable any waveforms that are done + waveform.enabled ^= pinBit; + busyPins ^= pinBit; + } + else { + const int32_t overshootCcys = now - waveNextEventCcy; + if (overshootCcys >= 0) { + const int32_t periodCcys = scaleCcys(wave.periodCcys, isCPU2X); + if (waveform.states & pinBit) { + // active configuration and forward are 100% duty + if (wave.periodCcys == wave.dutyCcys) { + wave.nextPeriodCcy += periodCcys; + wave.endDutyCcy = wave.nextPeriodCcy; + } + else { + if (wave.autoPwm) { + wave.adjDutyCcys += overshootCcys; + } + waveform.states ^= pinBit; + if (16 == pin) { + GP16O = 0; + } + else { + GPOC = pinBit; + } + } + waveNextEventCcy = wave.nextPeriodCcy; + } + else { + wave.nextPeriodCcy += periodCcys; + if (!wave.dutyCcys) { + wave.endDutyCcy = wave.nextPeriodCcy; + } + else { + int32_t dutyCcys = scaleCcys(wave.dutyCcys, isCPU2X); + if (dutyCcys <= wave.adjDutyCcys) { + dutyCcys >>= 1; + wave.adjDutyCcys -= dutyCcys; + } + else if (wave.adjDutyCcys) { + dutyCcys -= wave.adjDutyCcys; + wave.adjDutyCcys = 0; + } + wave.endDutyCcy = now + dutyCcys; + if (static_cast(wave.endDutyCcy - wave.nextPeriodCcy) > 0) { + wave.endDutyCcy = wave.nextPeriodCcy; + } + waveform.states |= pinBit; + if (16 == pin) { + GP16O = 1; + } + else { + GPOS = pinBit; + } + } + waveNextEventCcy = wave.endDutyCcy; + } + + if (WaveformMode::EXPIRES == wave.mode && static_cast(waveNextEventCcy - wave.expiryCcy) > 0) { + waveNextEventCcy = wave.expiryCcy; + } + } + + if (static_cast(waveNextEventCcy - isrTimeoutCcy) >= 0) { + busyPins ^= pinBit; + if (static_cast(waveform.nextEventCcy - waveNextEventCcy) > 0) { + waveform.nextEventCcy = waveNextEventCcy; + } + } + else if (static_cast(isrNextEventCcy - waveNextEventCcy) > 0) { + isrNextEventCcy = waveNextEventCcy; + } + } + now = ESP.getCycleCount(); + } + //clockDrift = 0; + } + + int32_t callbackCcys = 0; + if (waveform.timer1CB) { + callbackCcys = scaleCcys(waveform.timer1CB(), isCPU2X); + } + now = ESP.getCycleCount(); + int32_t nextEventCcys = waveform.nextEventCcy - now; + // Account for unknown duration of timer1CB(). + if (waveform.timer1CB && nextEventCcys > callbackCcys) { + waveform.nextEventCcy = now + callbackCcys; + nextEventCcys = callbackCcys; + } + + // Timer is 80MHz fixed. 160MHz CPU frequency need scaling. + int32_t deltaIrqCcys = DELTAIRQCCYS; + int32_t irqLatencyCcys = IRQLATENCYCCYS; + if (isCPU2X) { + nextEventCcys >>= 1; + deltaIrqCcys >>= 1; + irqLatencyCcys >>= 1; + } + + // Firing timer too soon, the NMI occurs before ISR has returned. + if (nextEventCcys < irqLatencyCcys + deltaIrqCcys) { + waveform.nextEventCcy = now + IRQLATENCYCCYS + DELTAIRQCCYS; + nextEventCcys = irqLatencyCcys; + } + else { + nextEventCcys -= deltaIrqCcys; + } + + // Register access is fast and edge IRQ was configured before. + T1L = nextEventCcys; +} diff --git a/lib/ESP8266PWM/src/core_esp8266_waveform_pwm.cpp b/lib/ESP8266PWM/src/core_esp8266_waveform_pwm.cpp deleted file mode 100644 index 78c7160d..00000000 --- a/lib/ESP8266PWM/src/core_esp8266_waveform_pwm.cpp +++ /dev/null @@ -1,717 +0,0 @@ -/* esp8266_waveform imported from platform source code - Modified for WLED to work around a fault in the NMI handling, - which can result in the system locking up and hard WDT crashes. - - Imported from https://github.com/esp8266/Arduino/blob/7e0d20e2b9034994f573a236364e0aef17fd66de/cores/esp8266/core_esp8266_waveform_pwm.cpp -*/ - -/* - esp8266_waveform - General purpose waveform generation and control, - supporting outputs on all pins in parallel. - - Copyright (c) 2018 Earle F. Philhower, III. All rights reserved. - - The core idea is to have a programmable waveform generator with a unique - high and low period (defined in microseconds or CPU clock cycles). TIMER1 - is set to 1-shot mode and is always loaded with the time until the next - edge of any live waveforms. - - Up to one waveform generator per pin supported. - - Each waveform generator is synchronized to the ESP clock cycle counter, not - the timer. This allows for removing interrupt jitter and delay as the - counter always increments once per 80MHz clock. Changes to a waveform are - contiguous and only take effect on the next waveform transition, - allowing for smooth transitions. - - This replaces older tone(), analogWrite(), and the Servo classes. - - Everywhere in the code where "cycles" is used, it means ESP.getCycleCount() - clock cycle count, or an interval measured in CPU clock cycles, but not - TIMER1 cycles (which may be 2 CPU clock cycles @ 160MHz). - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ - - -#include -#include -#include "ets_sys.h" -#include "core_esp8266_waveform.h" -#include "user_interface.h" - -extern "C" { - -// Linker magic -void usePWMFixedNMI() {}; - -// Maximum delay between IRQs -#define MAXIRQUS (10000) - -// Waveform generator can create tones, PWM, and servos -typedef struct { - uint32_t nextServiceCycle; // ESP cycle timer when a transition required - uint32_t expiryCycle; // For time-limited waveform, the cycle when this waveform must stop - uint32_t timeHighCycles; // Actual running waveform period (adjusted using desiredCycles) - uint32_t timeLowCycles; // - uint32_t desiredHighCycles; // Ideal waveform period to drive the error signal - uint32_t desiredLowCycles; // - uint32_t lastEdge; // Cycle when this generator last changed -} Waveform; - -class WVFState { -public: - Waveform waveform[17]; // State of all possible pins - uint32_t waveformState = 0; // Is the pin high or low, updated in NMI so no access outside the NMI code - uint32_t waveformEnabled = 0; // Is it actively running, updated in NMI so no access outside the NMI code - - // Enable lock-free by only allowing updates to waveformState and waveformEnabled from IRQ service routine - uint32_t waveformToEnable = 0; // Message to the NMI handler to start a waveform on a inactive pin - uint32_t waveformToDisable = 0; // Message to the NMI handler to disable a pin from waveform generation - - uint32_t waveformToChange = 0; // Mask of pin to change. One bit set in main app, cleared when effected in the NMI - uint32_t waveformNewHigh = 0; - uint32_t waveformNewLow = 0; - - uint32_t (*timer1CB)() = NULL; - - // Optimize the NMI inner loop by keeping track of the min and max GPIO that we - // are generating. In the common case (1 PWM) these may be the same pin and - // we can avoid looking at the other pins. - uint16_t startPin = 0; - uint16_t endPin = 0; -}; -static WVFState wvfState; - - -// Ensure everything is read/written to RAM -#define MEMBARRIER() { __asm__ volatile("" ::: "memory"); } - -// Non-speed critical bits -#pragma GCC optimize ("Os") - -// Interrupt on/off control -static IRAM_ATTR void timer1Interrupt(); -static bool timerRunning = false; - -static __attribute__((noinline)) void initTimer() { - if (!timerRunning) { - timer1_disable(); - ETS_FRC_TIMER1_INTR_ATTACH(NULL, NULL); - ETS_FRC_TIMER1_NMI_INTR_ATTACH(timer1Interrupt); - timer1_enable(TIM_DIV1, TIM_EDGE, TIM_SINGLE); - timerRunning = true; - timer1_write(microsecondsToClockCycles(10)); - } -} - -static IRAM_ATTR void forceTimerInterrupt() { - if (T1L > microsecondsToClockCycles(10)) { - T1L = microsecondsToClockCycles(10); - } -} - -// PWM implementation using special purpose state machine -// -// Keep an ordered list of pins with the delta in cycles between each -// element, with a terminal entry making up the remainder of the PWM -// period. With this method sum(all deltas) == PWM period clock cycles. -// -// At t=0 set all pins high and set the timeout for the 1st edge. -// On interrupt, if we're at the last element reset to t=0 state -// Otherwise, clear that pin down and set delay for next element -// and so forth. - -constexpr int maxPWMs = 8; - -// PWM machine state -typedef struct PWMState { - uint32_t mask; // Bitmask of active pins - uint32_t cnt; // How many entries - uint32_t idx; // Where the state machine is along the list - uint8_t pin[maxPWMs + 1]; - uint32_t delta[maxPWMs + 1]; - uint32_t nextServiceCycle; // Clock cycle for next step - struct PWMState *pwmUpdate; // Set by main code, cleared by ISR -} PWMState; - -static PWMState pwmState; -static uint32_t _pwmFreq = 1000; -static uint32_t _pwmPeriod = microsecondsToClockCycles(1000000UL) / _pwmFreq; - - -// If there are no more scheduled activities, shut down Timer 1. -// Otherwise, do nothing. -static IRAM_ATTR void disableIdleTimer() { - if (timerRunning && !wvfState.waveformEnabled && !pwmState.cnt && !wvfState.timer1CB) { - ETS_FRC_TIMER1_NMI_INTR_ATTACH(NULL); - timer1_disable(); - timer1_isr_init(); - timerRunning = false; - } -} - -// Notify the NMI that a new PWM state is available through the mailbox. -// Wait for mailbox to be emptied (either busy or delay() as needed) -static IRAM_ATTR void _notifyPWM(PWMState *p, bool idle) { - p->pwmUpdate = nullptr; - pwmState.pwmUpdate = p; - MEMBARRIER(); - forceTimerInterrupt(); - while (pwmState.pwmUpdate) { - if (idle) { - esp_yield(); - } - MEMBARRIER(); - } -} - -static void _addPWMtoList(PWMState &p, int pin, uint32_t val, uint32_t range); - - -// Called when analogWriteFreq() changed to update the PWM total period -//extern void _setPWMFreq_weak(uint32_t freq) __attribute__((weak)); -void _setPWMFreq_weak(uint32_t freq) { - _pwmFreq = freq; - - // Convert frequency into clock cycles - uint32_t cc = microsecondsToClockCycles(1000000UL) / freq; - - // Simple static adjustment to bring period closer to requested due to overhead - // Empirically determined as a constant PWM delay and a function of the number of PWMs -#if F_CPU == 80000000 - cc -= ((microsecondsToClockCycles(pwmState.cnt) * 13) >> 4) + 110; -#else - cc -= ((microsecondsToClockCycles(pwmState.cnt) * 10) >> 4) + 75; -#endif - - if (cc == _pwmPeriod) { - return; // No change - } - - _pwmPeriod = cc; - - if (pwmState.cnt) { - PWMState p; // The working copy since we can't edit the one in use - p.mask = 0; - p.cnt = 0; - for (uint32_t i = 0; i < pwmState.cnt; i++) { - auto pin = pwmState.pin[i]; - _addPWMtoList(p, pin, wvfState.waveform[pin].desiredHighCycles, wvfState.waveform[pin].desiredLowCycles); - } - // Update and wait for mailbox to be emptied - initTimer(); - _notifyPWM(&p, true); - disableIdleTimer(); - } -} -/* -static void _setPWMFreq_bound(uint32_t freq) __attribute__((weakref("_setPWMFreq_weak"))); -void _setPWMFreq(uint32_t freq) { - _setPWMFreq_bound(freq); -} -*/ - -// Helper routine to remove an entry from the state machine -// and clean up any marked-off entries -static void _cleanAndRemovePWM(PWMState *p, int pin) { - uint32_t leftover = 0; - uint32_t in, out; - for (in = 0, out = 0; in < p->cnt; in++) { - if ((p->pin[in] != pin) && (p->mask & (1<pin[in]))) { - p->pin[out] = p->pin[in]; - p->delta[out] = p->delta[in] + leftover; - leftover = 0; - out++; - } else { - leftover += p->delta[in]; - p->mask &= ~(1<pin[in]); - } - } - p->cnt = out; - // Final pin is never used: p->pin[out] = 0xff; - p->delta[out] = p->delta[in] + leftover; -} - - -// Disable PWM on a specific pin (i.e. when a digitalWrite or analogWrite(0%/100%)) -//extern bool _stopPWM_weak(uint8_t pin) __attribute__((weak)); -IRAM_ATTR bool _stopPWM_weak(uint8_t pin) { - if (!((1<= _pwmPeriod) { - cc = _pwmPeriod - 1; - } - - if (p.cnt == 0) { - // Starting up from scratch, special case 1st element and PWM period - p.pin[0] = pin; - p.delta[0] = cc; - // Final pin is never used: p.pin[1] = 0xff; - p.delta[1] = _pwmPeriod - cc; - } else { - uint32_t ttl = 0; - uint32_t i; - // Skip along until we're at the spot to insert - for (i=0; (i <= p.cnt) && (ttl + p.delta[i] < cc); i++) { - ttl += p.delta[i]; - } - // Shift everything out by one to make space for new edge - for (int32_t j = p.cnt; j >= (int)i; j--) { - p.pin[j + 1] = p.pin[j]; - p.delta[j + 1] = p.delta[j]; - } - int off = cc - ttl; // The delta from the last edge to the one we're inserting - p.pin[i] = pin; - p.delta[i] = off; // Add the delta to this new pin - p.delta[i + 1] -= off; // And subtract it from the follower to keep sum(deltas) constant - } - p.cnt++; - p.mask |= 1<= maxPWMs) { - return false; // No space left - } - - // Sanity check for all-on/off - uint32_t cc = (_pwmPeriod * val) / range; - if ((cc == 0) || (cc >= _pwmPeriod)) { - digitalWrite(pin, cc ? HIGH : LOW); - return true; - } - - _addPWMtoList(p, pin, val, range); - - // Set mailbox and wait for ISR to copy it over - initTimer(); - _notifyPWM(&p, true); - disableIdleTimer(); - - // Potentially recalculate the PWM period if we've added another pin - _setPWMFreq(_pwmFreq); - - return true; -} -/* -static bool _setPWM_bound(int pin, uint32_t val, uint32_t range) __attribute__((weakref("_setPWM_weak"))); -bool _setPWM(int pin, uint32_t val, uint32_t range) { - return _setPWM_bound(pin, val, range); -} -*/ - -// Start up a waveform on a pin, or change the current one. Will change to the new -// waveform smoothly on next low->high transition. For immediate change, stopWaveform() -// first, then it will immediately begin. -//extern int startWaveformClockCycles_weak(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles, int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) __attribute__((weak)); -int startWaveformClockCycles_weak(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles, - int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) { - (void) alignPhase; - (void) phaseOffsetUS; - (void) autoPwm; - - if ((pin > 16) || isFlashInterfacePin(pin) || (timeHighCycles == 0)) { - return false; - } - Waveform *wave = &wvfState.waveform[pin]; - wave->expiryCycle = runTimeCycles ? ESP.getCycleCount() + runTimeCycles : 0; - if (runTimeCycles && !wave->expiryCycle) { - wave->expiryCycle = 1; // expiryCycle==0 means no timeout, so avoid setting it - } - - _stopPWM(pin); // Make sure there's no PWM live here - - uint32_t mask = 1<timeHighCycles = timeHighCycles; - wave->desiredHighCycles = timeHighCycles; - wave->timeLowCycles = timeLowCycles; - wave->desiredLowCycles = timeLowCycles; - wave->lastEdge = 0; - wave->nextServiceCycle = ESP.getCycleCount() + microsecondsToClockCycles(1); - wvfState.waveformToEnable |= mask; - MEMBARRIER(); - initTimer(); - forceTimerInterrupt(); - while (wvfState.waveformToEnable) { - esp_yield(); // Wait for waveform to update - MEMBARRIER(); - } - } - - return true; -} -/* -static int startWaveformClockCycles_bound(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles, int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) __attribute__((weakref("startWaveformClockCycles_weak"))); -int startWaveformClockCycles(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles, int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) { - return startWaveformClockCycles_bound(pin, timeHighCycles, timeLowCycles, runTimeCycles, alignPhase, phaseOffsetUS, autoPwm); -} - - -// This version falls-thru to the proper startWaveformClockCycles call and is invariant across waveform generators -int startWaveform(uint8_t pin, uint32_t timeHighUS, uint32_t timeLowUS, uint32_t runTimeUS, - int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) { - return startWaveformClockCycles_bound(pin, - microsecondsToClockCycles(timeHighUS), microsecondsToClockCycles(timeLowUS), - microsecondsToClockCycles(runTimeUS), alignPhase, microsecondsToClockCycles(phaseOffsetUS), autoPwm); -} -*/ - -// Set a callback. Pass in NULL to stop it -//extern void setTimer1Callback_weak(uint32_t (*fn)()) __attribute__((weak)); -void setTimer1Callback_weak(uint32_t (*fn)()) { - wvfState.timer1CB = fn; - if (fn) { - initTimer(); - forceTimerInterrupt(); - } - disableIdleTimer(); -} -/* -static void setTimer1Callback_bound(uint32_t (*fn)()) __attribute__((weakref("setTimer1Callback_weak"))); -void setTimer1Callback(uint32_t (*fn)()) { - setTimer1Callback_bound(fn); -} -*/ - -// Stops a waveform on a pin -//extern int stopWaveform_weak(uint8_t pin) __attribute__((weak)); -IRAM_ATTR int stopWaveform_weak(uint8_t pin) { - // Can't possibly need to stop anything if there is no timer active - if (!timerRunning) { - return false; - } - // If user sends in a pin >16 but <32, this will always point to a 0 bit - // If they send >=32, then the shift will result in 0 and it will also return false - uint32_t mask = 1<= (uintptr_t) &_UserExceptionVector_1)) { - // Address is good; save backup - epc3_backup = epc3; - eps3_backup = eps3; - } else { - // Address is inside the NMI handler -- restore from backup - __asm__ __volatile__("wsr %0,epc3; wsr %1,eps3"::"a"(epc3_backup),"a"(eps3_backup)); - } -} -// ----- @willmmiles end patch ----- - - -// The SDK and hardware take some time to actually get to our NMI code, so -// decrement the next IRQ's timer value by a bit so we can actually catch the -// real CPU cycle counter we want for the waveforms. - -// The SDK also sometimes is running at a different speed the the Arduino core -// so the ESP cycle counter is actually running at a variable speed. -// adjust(x) takes care of adjusting a delta clock cycle amount accordingly. -#if F_CPU == 80000000 - #define DELTAIRQ (microsecondsToClockCycles(9)/4) - #define adjust(x) ((x) << (turbo ? 1 : 0)) -#else - #define DELTAIRQ (microsecondsToClockCycles(9)/8) - #define adjust(x) ((x) >> 0) -#endif - -// When the time to the next edge is greater than this, RTI and set another IRQ to minimize CPU usage -#define MINIRQTIME microsecondsToClockCycles(6) - -static IRAM_ATTR void timer1Interrupt() { - // ----- @willmmiles begin patch ----- - nmiCrashWorkaround(); - // ----- @willmmiles end patch ----- - - // Flag if the core is at 160 MHz, for use by adjust() - bool turbo = (*(uint32_t*)0x3FF00014) & 1 ? true : false; - - uint32_t nextEventCycle = GetCycleCountIRQ() + microsecondsToClockCycles(MAXIRQUS); - uint32_t timeoutCycle = GetCycleCountIRQ() + microsecondsToClockCycles(14); - - if (wvfState.waveformToEnable || wvfState.waveformToDisable) { - // Handle enable/disable requests from main app - wvfState.waveformEnabled = (wvfState.waveformEnabled & ~wvfState.waveformToDisable) | wvfState.waveformToEnable; // Set the requested waveforms on/off - wvfState.waveformState &= ~wvfState.waveformToEnable; // And clear the state of any just started - wvfState.waveformToEnable = 0; - wvfState.waveformToDisable = 0; - // No mem barrier. Globals must be written to RAM on ISR exit. - // Find the first GPIO being generated by checking GCC's find-first-set (returns 1 + the bit of the first 1 in an int32_t) - wvfState.startPin = __builtin_ffs(wvfState.waveformEnabled) - 1; - // Find the last bit by subtracting off GCC's count-leading-zeros (no offset in this one) - wvfState.endPin = 32 - __builtin_clz(wvfState.waveformEnabled); - } else if (!pwmState.cnt && pwmState.pwmUpdate) { - // Start up the PWM generator by copying from the mailbox - pwmState.cnt = 1; - pwmState.idx = 1; // Ensure copy this cycle, cause it to start at t=0 - pwmState.nextServiceCycle = GetCycleCountIRQ(); // Do it this loop! - // No need for mem barrier here. Global must be written by IRQ exit - } - - bool done = false; - if (wvfState.waveformEnabled || pwmState.cnt) { - do { - nextEventCycle = GetCycleCountIRQ() + microsecondsToClockCycles(MAXIRQUS); - - // PWM state machine implementation - if (pwmState.cnt) { - int32_t cyclesToGo; - do { - cyclesToGo = pwmState.nextServiceCycle - GetCycleCountIRQ(); - if (cyclesToGo < 0) { - if (pwmState.idx == pwmState.cnt) { // Start of pulses, possibly copy new - if (pwmState.pwmUpdate) { - // Do the memory copy from temp to global and clear mailbox - pwmState = *(PWMState*)pwmState.pwmUpdate; - } - GPOS = pwmState.mask; // Set all active pins high - if (pwmState.mask & (1<<16)) { - GP16O = 1; - } - pwmState.idx = 0; - } else { - do { - // Drop the pin at this edge - if (pwmState.mask & (1<expiryCycle) { - int32_t expiryToGo = wave->expiryCycle - now; - if (expiryToGo < 0) { - // Done, remove! - if (i == 16) { - GP16O = 0; - } - GPOC = mask; - wvfState.waveformEnabled &= ~mask; - continue; - } - } - - // Check for toggles - int32_t cyclesToGo = wave->nextServiceCycle - now; - if (cyclesToGo < 0) { - uint32_t nextEdgeCycles; - uint32_t desired = 0; - uint32_t *timeToUpdate; - wvfState.waveformState ^= mask; - if (wvfState.waveformState & mask) { - if (i == 16) { - GP16O = 1; - } - GPOS = mask; - - if (wvfState.waveformToChange & mask) { - // Copy over next full-cycle timings - wave->timeHighCycles = wvfState.waveformNewHigh; - wave->desiredHighCycles = wvfState.waveformNewHigh; - wave->timeLowCycles = wvfState.waveformNewLow; - wave->desiredLowCycles = wvfState.waveformNewLow; - wave->lastEdge = 0; - wvfState.waveformToChange = 0; - } - if (wave->lastEdge) { - desired = wave->desiredLowCycles; - timeToUpdate = &wave->timeLowCycles; - } - nextEdgeCycles = wave->timeHighCycles; - } else { - if (i == 16) { - GP16O = 0; - } - GPOC = mask; - desired = wave->desiredHighCycles; - timeToUpdate = &wave->timeHighCycles; - nextEdgeCycles = wave->timeLowCycles; - } - if (desired) { - desired = adjust(desired); - int32_t err = desired - (now - wave->lastEdge); - if (abs(err) < desired) { // If we've lost > the entire phase, ignore this error signal - err /= 2; - *timeToUpdate += err; - } - } - nextEdgeCycles = adjust(nextEdgeCycles); - wave->nextServiceCycle = now + nextEdgeCycles; - wave->lastEdge = now; - } - nextEventCycle = earliest(nextEventCycle, wave->nextServiceCycle); - } - - // Exit the loop if we've hit the fixed runtime limit or the next event is known to be after that timeout would occur - uint32_t now = GetCycleCountIRQ(); - int32_t cycleDeltaNextEvent = nextEventCycle - now; - int32_t cyclesLeftTimeout = timeoutCycle - now; - done = (cycleDeltaNextEvent > MINIRQTIME) || (cyclesLeftTimeout < 0); - } while (!done); - } // if (wvfState.waveformEnabled) - - if (wvfState.timer1CB) { - nextEventCycle = earliest(nextEventCycle, GetCycleCountIRQ() + wvfState.timer1CB()); - } - - int32_t nextEventCycles = nextEventCycle - GetCycleCountIRQ(); - - if (nextEventCycles < MINIRQTIME) { - nextEventCycles = MINIRQTIME; - } - nextEventCycles -= DELTAIRQ; - - // Do it here instead of global function to save time and because we know it's edge-IRQ - T1L = nextEventCycles >> (turbo ? 1 : 0); -} - -}; diff --git a/package-lock.json b/package-lock.json index db709485..5a19be1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,24 +1,28 @@ { "name": "wled", - "version": "0.15.0-b7", + "version": "0.16.0-alpha", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wled", - "version": "0.15.0-b7", + "version": "0.16.0-alpha", "license": "ISC", "dependencies": { "clean-css": "^5.3.3", "html-minifier-terser": "^7.2.0", - "inliner": "^1.13.1", - "nodemon": "^3.1.7" + "nodemon": "^3.1.9", + "web-resource-inliner": "^7.0.0" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -32,6 +36,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -40,6 +45,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -48,6 +54,7 @@ "version": "0.3.6", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -56,21 +63,24 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -78,62 +88,20 @@ "node": ">=0.4.0" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha512-GrTZLRpmp6wIC2ztrWW9MjjTgSKccffgFagbNDOX95/dcjEcYZibYTeaOntySQLcdw1ztBoFkviiUvTMbb9MYg==", - "dependencies": { - "kind-of": "^3.0.2", - "longest": "^1.0.1", - "repeat-string": "^1.5.2" - }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "license": "MIT", "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ansi-escapes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", - "integrity": "sha512-wiXutNjDUlNEDWHcYH3jtZUhd3c4/VojassD8zHdHCY13xbZy2XbW+NKQwA0tWGBVzDA9qEzYwfoSsWmviidhw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", - "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -142,70 +110,17 @@ "node": ">= 8" } }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" - }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", - "engines": { - "node": "*" - } - }, - "node_modules/aws4": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", - "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==" - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "dependencies": { - "tweetnacl": "^0.14.3" - } + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", "engines": { "node": ">=8" }, @@ -213,15 +128,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" - }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -231,6 +142,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -241,89 +153,24 @@ "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" }, "node_modules/camel-case": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "license": "MIT", "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" } }, - "node_modules/camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha512-wzLkDa4K/mzI1OSITC+DUyjgIl/ETNHE9QvYgy6J6Jvqyyz4C0Xfd+lQhb19sX2jMpZV4IssUn0VDVmglV+s4g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" - }, - "node_modules/center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha512-Baz3aNe2gd2LP2qk5U+sDk/m4oSuwSDcBfayTCTBoWpfIGO5XFxPmjILQII4NGiZjD6DoDI6kf7gKaxkf7s3VQ==", - "dependencies": { - "align-text": "^0.1.3", - "lazy-cache": "^1.0.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/charset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/charset/-/charset-1.0.1.tgz", - "integrity": "sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/cheerio": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.19.0.tgz", - "integrity": "sha512-Fwcm3zkR37STnPC8FepSHeSYJM5Rd596TZOcfDUdojR4Q735aK1Xn+M+ISagNneuCwMjK28w4kX+ETILGNT/UQ==", - "dependencies": { - "css-select": "~1.0.0", - "dom-serializer": "~0.1.0", - "entities": "~1.1.1", - "htmlparser2": "~3.8.1", - "lodash": "^3.2.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cheerio/node_modules/entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -343,21 +190,11 @@ "fsevents": "~2.3.2" } }, - "node_modules/clap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz", - "integrity": "sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA==", - "dependencies": { - "chalk": "^1.1.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "license": "MIT", "dependencies": { "source-map": "~0.6.0" }, @@ -365,50 +202,11 @@ "node": ">= 10.0" } }, - "node_modules/cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha512-GIOYRizG+TGoc7Wgc1LiOTLare95R3mzKgoln+Q/lE4ceiYH19gUpl0l0Ffq4lJDEf3FxujMe6IBfOCs7pfqNA==", - "dependencies": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" - } - }, - "node_modules/coa": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/coa/-/coa-1.0.4.tgz", - "integrity": "sha512-KAGck/eNAmCL0dcT3BiuYwLbExK6lduR8DxM3C1TyDzaXhZHyZ8ooX5I5+na2e3dPFuibfxrGdorr0/Lr7RYCQ==", - "dependencies": { - "q": "^1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha512-ENwblkFQpqqia6b++zLD/KUWafYlVY/UNnAp7oz7LY7E924wmpye416wBOmvv/HMWzl8gL1kJlfvId/1Dg176w==", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commander": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "license": "MIT", "engines": { "node": ">=14" } @@ -416,230 +214,135 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "node_modules/configstore": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-1.4.0.tgz", - "integrity": "sha512-Zcx2SVdZC06IuRHd2MhkVYFNJBkZBj166LGdsJXRcqNC8Gs5Bwh8mosStNeCBBmtIm4wNii2uarD50qztjKOjw==", - "dependencies": { - "graceful-fs": "^4.1.2", - "mkdirp": "^0.5.0", - "object-assign": "^4.0.1", - "os-tmpdir": "^1.0.0", - "osenv": "^0.1.0", - "uuid": "^2.0.1", - "write-file-atomic": "^1.1.2", - "xdg-basedir": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/configstore/node_modules/uuid": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", - "integrity": "sha512-FULf7fayPdpASncVy4DLh3xydlXEJJpvIELjYjNeQWYUZ9pclcpvCZSr2gkmN2FrrGcI7G/cJsIEwk5/8vfXpg==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details." - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "node_modules/css-select": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.0.0.tgz", - "integrity": "sha512-/xPlD7betkfd7ChGkLGGWx5HWyiHDOSn7aACLzdH0nwucPvB0EAm8hMBm7Xn7vGfAeRRN7KZ8wumGm8NoNcMRw==", - "dependencies": { - "boolbase": "~1.0.0", - "css-what": "1.0", - "domutils": "1.4", - "nth-check": "~1.0.0" - } - }, - "node_modules/css-what": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-1.0.0.tgz", - "integrity": "sha512-60SUMPBreXrLXgvpM8kYpO0AOyMRhdRlXFX5BMQbZq1SIJCyNE56nqFQhmvREQdUJpedbGRYZ5wOyq3/F6q5Zw==", - "engines": { - "node": "*" - } - }, - "node_modules/csso": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/csso/-/csso-2.0.0.tgz", - "integrity": "sha512-tckZA0LhyEnToPoQDmncCA+TUS3aoIVl/MsSaoipR52Sfa+H83fJvIHRVOHMFn9zW6kIV1L0D7tUDFFjvN28lg==", - "dependencies": { - "clap": "^1.0.9", - "source-map": "^0.5.3" - }, - "bin": { - "csso": "bin/csso" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/csso/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" }, "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "ms": "^2.1.3" + }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/dom-serializer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", - "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", "dependencies": { - "domelementtype": "^1.3.0", - "entities": "^1.1.1" + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, "node_modules/dom-serializer/node_modules/entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } }, "node_modules/domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" }, "node_modules/domhandler": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", - "integrity": "sha512-q9bUwjfp7Eif8jWxxxPSykdRZAb6GkguBGSgvvCrhI9wB71W2K/Kvv4E61CF/mcCfnVJDeDWx/Vb/uAqbDj6UQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", + "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", + "license": "BSD-2-Clause", "dependencies": { - "domelementtype": "1" + "domelementtype": "^2.0.1" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, "node_modules/domutils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.4.3.tgz", - "integrity": "sha512-ZkVgS/PpxjyJMb+S2iVHHEZjVnOUtjGp0/zstqKGTE9lrZtNHlNQmLwP/lhLMEApYbzc08BKMx9IFpKhaSbW1w==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", "dependencies": { - "domelementtype": "1" + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/domutils/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, "node_modules/dot-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "license": "MIT", "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, - "node_modules/duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", - "dependencies": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "node_modules/duplexify/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/duplexify/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/duplexify/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/duplexify/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -647,58 +350,23 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/es6-promise": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-2.3.0.tgz", - "integrity": "sha512-oyOjMhyKMLEjOOtvkwg0G4pAzLQ9WdbbeX7WdqKzvYXu+UFgD0Zo/Brq5Q49zNmnGPPzV5rmYvrr0jz1zWx8Iw==" - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/escape-goat": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", + "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==", + "license": "MIT", "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha512-OarPfz0lFCiW4/AV2Oy1Rp9qu0iusTKqykwTspGCZtPxmF81JR4MmIebvF1F9+UOKth2ZubLQ4XGGaU+hSn99A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" + "node": ">=10" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "node_modules/extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", - "engines": [ - "node >=0.6.0" - ] - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -706,32 +374,12 @@ "node": ">=8" } }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", - "engines": { - "node": "*" - } - }, - "node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -740,18 +388,11 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "dependencies": { - "assert-plus": "^1.0.0" - } - }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -759,75 +400,11 @@ "node": ">= 6" } }, - "node_modules/got": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/got/-/got-3.3.1.tgz", - "integrity": "sha512-7chPlc0pWHjvq7B6dEEXz4GphoDupOvBSSl6AwRsAJX7GPTZ+bturaZiIigX4Dp6KrAP67nvzuKkNc0SLA0DKg==", - "dependencies": { - "duplexify": "^3.2.0", - "infinity-agent": "^2.0.0", - "is-redirect": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "nested-error-stacks": "^1.0.0", - "object-assign": "^3.0.0", - "prepend-http": "^1.0.0", - "read-all-stream": "^3.0.0", - "timed-out": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/got/node_modules/object-assign": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", - "integrity": "sha512-jHP15vXVGeVh1HuaA2wY6lxk+whK/x4KBG88VXeRma7CCun7iGD5qPc4eYykQ9sdQvg8jkwFKsSxHln2ybW3xQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" - }, - "node_modules/har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", - "engines": { - "node": ">=4" - } - }, - "node_modules/har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "deprecated": "this library is no longer supported", - "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", "engines": { "node": ">=4" } @@ -836,6 +413,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==", + "license": "MIT", "dependencies": { "camel-case": "^4.1.2", "clean-css": "~5.3.2", @@ -853,117 +431,40 @@ } }, "node_modules/htmlparser2": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", - "integrity": "sha512-hBxEg3CYXe+rPIua8ETe7tmG3XDn9B0edOE/e9wH2nLczxzgdu0m0aNHY+5wFZiviLWLdANPJTssa92dMcXQ5Q==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz", + "integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==", + "license": "MIT", "dependencies": { - "domelementtype": "1", - "domhandler": "2.3", - "domutils": "1.5", - "entities": "1.0", - "readable-stream": "1.1" - } - }, - "node_modules/htmlparser2/node_modules/domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw==", - "dependencies": { - "dom-serializer": "0", - "domelementtype": "1" + "domelementtype": "^2.0.1", + "domhandler": "^3.3.0", + "domutils": "^2.4.2", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/fb55/htmlparser2?sponsor=1" } }, "node_modules/htmlparser2/node_modules/entities": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", - "integrity": "sha512-LbLqfXgJMmy81t+7c14mnulFHJ170cM6E+0vMXR9k/ZiZwgX8i5pNgjTCX3SO4VeUsFLV+8InixoretwU+MjBQ==" - }, - "node_modules/http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==" - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/infinity-agent": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/infinity-agent/-/infinity-agent-2.0.3.tgz", - "integrity": "sha512-CnfUJe5o2S9aAQWXGMhDZI4UL39MAJV3guOTfHHIdos4tuVHkl1j/J+1XLQn+CLIvqcpgQR/p+xXYXzcrhCe5w==" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, - "node_modules/inliner": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/inliner/-/inliner-1.13.1.tgz", - "integrity": "sha512-yoS+56puOu+Ug8FBRtxtTFnEn2NHqFs8BNQgSOvzh3J0ommbwNw8VKiaVNYjWK6fgPuByq95KyV0LC+qV9IwLw==", - "dependencies": { - "ansi-escapes": "^1.4.0", - "ansi-styles": "^2.2.1", - "chalk": "^1.1.3", - "charset": "^1.0.0", - "cheerio": "^0.19.0", - "debug": "^2.2.0", - "es6-promise": "^2.3.0", - "iconv-lite": "^0.4.11", - "jschardet": "^1.3.0", - "lodash.assign": "^3.2.0", - "lodash.defaults": "^3.1.2", - "lodash.foreach": "^3.0.3", - "mime": "^1.3.4", - "minimist": "^1.1.3", - "request": "^2.74.0", - "svgo": "^0.6.6", - "then-fs": "^2.0.0", - "uglify-js": "^2.8.0", - "update-notifier": "^0.5.0" - }, - "bin": { - "inliner": "cli/index.js" - } + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "license": "ISC" }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -971,34 +472,20 @@ "node": ">=8" } }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/is-finite": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", - "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -1006,310 +493,41 @@ "node": ">=0.10.0" } }, - "node_modules/is-npm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", - "integrity": "sha512-9r39FIr3d+KD9SbX0sfMsHzb5PP3uimOiwr3YupUaUFG4W0l1U57Rx3utpttV7qz5U3jmrO5auUa04LU9pyHsg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", "engines": { "node": ">=0.12.0" } }, - "node_modules/is-redirect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha512-cr/SlUEe5zOGmzvj9bUyC4LVvkNVAXu4GytXLNMr1pny+a65MpQ9IJzFHD5vi7FyJgb4qt27+eS3TuQnqB+RQw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" - }, - "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" - }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" - }, - "node_modules/js-yaml": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.6.1.tgz", - "integrity": "sha512-BLv3oxhfET+w5fjPwq3PsAsxzi9i3qzU//HMpWVz0A6KplF86HdR9x2TGnv9DXhSUrO7LO8czUiTd3yb3mLSvg==", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^2.6.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" - }, - "node_modules/jschardet": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-1.6.0.tgz", - "integrity": "sha512-xYuhvQ7I9PDJIGBWev9xm0+SMSed3ZDBAmvVjbFR1ZRLAF+vlXcQu6cRI9uAlj81rzikElRVteehwV7DuX2ZmQ==", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" - }, - "node_modules/jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/latest-version": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-1.0.1.tgz", - "integrity": "sha512-HERbxp4SBlmI380+eM0B0u4nxjfTaPeydIMzl9+9UQ4nSu3xMWKlX9WoT34e4wy7VWe67c53Nv9qPVjS8fHKgg==", - "dependencies": { - "package-json": "^1.0.0" - }, - "bin": { - "latest-version": "cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha512-9mDDwqVIma6OZX79ZlDACZl8sBm0TEnkf99zV3iMA4GzkIT/9hiqP5mY0HoT1iNLCrKc/R1HByV+yJfRWVJryQ==" - }, - "node_modules/lodash._arrayeach": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz", - "integrity": "sha512-Mn7HidOVcl3mkQtbPsuKR0Fj0N6Q6DQB77CtYncZcJc0bx5qv2q4Gl6a0LC1AN+GSxpnBDNnK3CKEm9XNA4zqQ==" - }, - "node_modules/lodash._baseassign": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", - "integrity": "sha512-t3N26QR2IdSN+gqSy9Ds9pBu/J1EAFEshKlUHpJG3rvyJOYgcELIxcIeKKfZk7sjOz11cFfzJRsyFry/JyabJQ==", - "dependencies": { - "lodash._basecopy": "^3.0.0", - "lodash.keys": "^3.0.0" - } - }, - "node_modules/lodash._basecopy": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", - "integrity": "sha512-rFR6Vpm4HeCK1WPGvjZSJ+7yik8d8PVUdCJx5rT2pogG4Ve/2ZS7kfmO5l5T2o5V2mqlNIfSF5MZlr1+xOoYQQ==" - }, - "node_modules/lodash._baseeach": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash._baseeach/-/lodash._baseeach-3.0.4.tgz", - "integrity": "sha512-IqUZ9MQo2UT1XPGuBntInqTOlc+oV+bCo0kMp+yuKGsfvRSNgUW0YjWVZUrG/gs+8z/Eyuc0jkJjOBESt9BXxg==", - "dependencies": { - "lodash.keys": "^3.0.0" - } - }, - "node_modules/lodash._bindcallback": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", - "integrity": "sha512-2wlI0JRAGX8WEf4Gm1p/mv/SZ+jLijpj0jyaE/AXeuQphzCgD8ZQW4oSpoN8JAopujOFGU3KMuq7qfHBWlGpjQ==" - }, - "node_modules/lodash._createassigner": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz", - "integrity": "sha512-LziVL7IDnJjQeeV95Wvhw6G28Z8Q6da87LWKOPWmzBLv4u6FAT/x5v00pyGW0u38UoogNF2JnD3bGgZZDaNEBw==", - "dependencies": { - "lodash._bindcallback": "^3.0.0", - "lodash._isiterateecall": "^3.0.0", - "lodash.restparam": "^3.0.0" - } - }, - "node_modules/lodash._getnative": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha512-RrL9VxMEPyDMHOd9uFbvMe8X55X16/cGM5IgOKgRElQZutpX89iS6vwl64duTV1/16w5JY7tuFNXqoekmh1EmA==" - }, - "node_modules/lodash._isiterateecall": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", - "integrity": "sha512-De+ZbrMu6eThFti/CSzhRvTKMgQToLxbij58LMfM8JnYDNSOjkjTCIaa8ixglOeGh2nyPlakbt5bJWJ7gvpYlQ==" - }, - "node_modules/lodash.assign": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-3.2.0.tgz", - "integrity": "sha512-/VVxzgGBmbphasTg51FrztxQJ/VgAUpol6zmJuSVSGcNg4g7FA4z7rQV8Ovr9V3vFBNWZhvKWHfpAytjTVUfFA==", - "dependencies": { - "lodash._baseassign": "^3.0.0", - "lodash._createassigner": "^3.0.0", - "lodash.keys": "^3.0.0" - } - }, - "node_modules/lodash.defaults": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-3.1.2.tgz", - "integrity": "sha512-X7135IXFQt5JDFnYxOVAzVz+kFvwDn3N8DJYf+nrz/mMWEuSu7+OL6rWqsk3+VR1T4TejFCSu5isBJOLSID2bg==", - "dependencies": { - "lodash.assign": "^3.0.0", - "lodash.restparam": "^3.0.0" - } - }, - "node_modules/lodash.foreach": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-3.0.3.tgz", - "integrity": "sha512-PA7Lp7pe2HMJBoB1vELegEIF3waUFnM0fWDKJVYolwZ4zHh6WTmnq0xmzfQksD66gx2quhDNyBdyaE2T8/DP3Q==", - "dependencies": { - "lodash._arrayeach": "^3.0.0", - "lodash._baseeach": "^3.0.0", - "lodash._bindcallback": "^3.0.0", - "lodash.isarray": "^3.0.0" - } - }, - "node_modules/lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" - }, - "node_modules/lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha512-JwObCrNJuT0Nnbuecmqr5DgtuBppuCvGD9lxjFpAzwnVtdGoDQ1zig+5W8k5/6Gcn0gZ3936HDAlGd28i7sOGQ==" - }, - "node_modules/lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha512-CuBsapFjcubOGMn3VD+24HOAPxM79tH+V6ivJL3CHYjtrawauDJHUk//Yew9Hvc6e9rbCrURGk8z6PC+8WJBfQ==", - "dependencies": { - "lodash._getnative": "^3.0.0", - "lodash.isarguments": "^3.0.0", - "lodash.isarray": "^3.0.0" - } - }, - "node_modules/lodash.restparam": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", - "integrity": "sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==" - }, - "node_modules/longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha512-k+yt5n3l48JU4k8ftnKG6V7u32wyH2NfKzeMto9F/QRE0amxy/LayxwlvjjkZEIzqR+19IrtFO8p5kB9QaYUFg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", "dependencies": { "tslib": "^2.0.3" } }, - "node_modules/lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", "bin": { "mime": "cli.js" }, "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" + "node": ">=4.0.0" } }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -1317,51 +535,27 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/nested-error-stacks": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-1.0.2.tgz", - "integrity": "sha512-o32anp9JA7oezPOFSfG2BBXSdHepOm5FpJvwxHWDtfJ3Bg3xdi68S6ijPlEOfUg6quxZWyvJM+8fHk1yMDKspA==", - "dependencies": { - "inherits": "~2.0.1" - } + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "node_modules/nodemon": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", - "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", + "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", + "license": "MIT", "dependencies": { "chokidar": "^3.5.2", "debug": "^4", @@ -1385,112 +579,11 @@ "url": "https://opencollective.com/nodemon" } }, - "node_modules/nodemon/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/nodemon/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/nodemon/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "dependencies": { - "boolbase": "~1.0.0" - } - }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "engines": { - "node": "*" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "deprecated": "This package is no longer supported.", - "dependencies": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "node_modules/package-json": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-1.2.0.tgz", - "integrity": "sha512-knDtirWWqKVJrLY3gEBLflVvueTMpyjbAwX/9j/EKi2DsjNemp5voS8cyKyGh57SNaMJNhNRZbIaWdneOcLU1g==", - "dependencies": { - "got": "^3.2.0", - "registry-url": "^3.0.0" - }, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1499,6 +592,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "license": "MIT", "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -1508,20 +602,17 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "license": "MIT", "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" - }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -1529,155 +620,17 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", - "dependencies": { - "pinkie": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "dependencies": { - "asap": "~2.0.3" - } - }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" - }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", - "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", - "engines": { - "node": ">=0.6.0", - "teleport": ">=0.2.0" - } - }, - "node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/read-all-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz", - "integrity": "sha512-DI1drPHbmBcUDWrJ7ull/F2Qb8HkwBncVx8/RpKYFSIACYaVRQReISYPdZz/mt1y1+qMCOrfReTopERmaxtP6w==", - "dependencies": { - "pinkie-promise": "^2.0.0", - "readable-stream": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-all-stream/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/read-all-stream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/read-all-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/read-all-stream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "license": "MIT" }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -1685,122 +638,20 @@ "node": ">=8.10.0" } }, - "node_modules/registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", - "dependencies": { - "rc": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "license": "MIT", "engines": { "node": ">= 0.10" } }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/repeating": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-1.1.3.tgz", - "integrity": "sha512-Nh30JLeMHdoI+AsQ5eblhZ7YlTsM9wiJQe/AHIunlK3KWzvXhXb36IJ7K1IOeRjIOtzMjdUHjwXUFxKJoPTSOg==", - "dependencies": { - "is-finite": "^1.0.0" - }, - "bin": { - "repeating": "cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/right-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha512-yqINtL/G7vs2v+dFIZmFUDbnVyFUJFKd6gK22Kgo6R4jfJGFtisKyncWDDULgjfqf4ASQuIQyjJ7XZ+3aWpsAg==", - "dependencies": { - "align-text": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -1808,29 +659,11 @@ "node": ">=10" } }, - "node_modules/semver-diff": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", - "integrity": "sha512-gL8F8L4ORwsS0+iQ34yCYv///jsOq0ZL7WP55d1HnJ32o7tyFYEFQZQA22mrLIacZdU6xecaBBZ+uEiffGNyXw==", - "dependencies": { - "semver": "^5.0.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/semver-diff/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "bin": { - "semver": "bin/semver" - } - }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "license": "MIT", "dependencies": { "semver": "^7.5.3" }, @@ -1838,18 +671,11 @@ "node": ">=10" } }, - "node_modules/slide": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", - "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", - "engines": { - "node": "*" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -1858,113 +684,29 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" - }, - "node_modules/sshpk": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", - "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stream-shift": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", - "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==" - }, - "node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" - }, - "node_modules/string-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz", - "integrity": "sha512-MNCACnufWUf3pQ57O5WTBMkKhzYIaKEcUioO0XHrTMafrbBaNk4IyDOLHBv5xbXO0jLLdsYWeFjpjG2hVHRDtw==", - "dependencies": { - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/svgo": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-0.6.6.tgz", - "integrity": "sha512-C5A1r5SjFesNoKsmc+kWBxmB04iBGH2D/nFy8HJaME9+SyZKcmqcN8QG+GwxIc7D2+JWhaaW7uaM9+XwfplTEQ==", - "deprecated": "This SVGO version is no longer supported. Upgrade to v2.x.x.", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", "dependencies": { - "coa": "~1.0.1", - "colors": "~1.1.2", - "csso": "~2.0.0", - "js-yaml": "~3.6.0", - "mkdirp": "~0.5.1", - "sax": "~1.2.1", - "whet.extend": "~0.9.9" - }, - "bin": { - "svgo": "bin/svgo" + "has-flag": "^3.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, "node_modules/terser": { - "version": "5.34.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.34.0.tgz", - "integrity": "sha512-y5NUX+U9HhVsK/zihZwoq4r9dICLyV2jXGOriDAVOeKhq3LKVjgJbGO90FisozXLlJfvjHqgckGmJFBb9KYoWQ==", + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", + "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", + "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -1981,28 +723,14 @@ "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "node_modules/then-fs": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/then-fs/-/then-fs-2.0.0.tgz", - "integrity": "sha512-5ffcBcU+vFUCYDNi/o507IqjqrTkuGsLVZ1Fp50hwgZRY7ufVFa9jFfTy5uZ2QnSKacKigWKeaXkOqLa4DsjLw==", - "dependencies": { - "promise": ">=3.2 <8" - } - }, - "node_modules/timed-out": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-2.0.0.tgz", - "integrity": "sha512-pqqJOi1rF5zNs/ps4vmbE4SFCrM4iR7LW+GHAsHqO/EumqbIWceioevYLM5xZRgQSH6gFgL9J/uB7EcJhQ9niQ==", - "engines": { - "node": ">=0.10.0" - } + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -2014,196 +742,46 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "license": "ISC", "bin": { "nodetouch": "bin/nodetouch.js" } }, - "node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" - }, - "node_modules/uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha512-qLq/4y2pjcU3vhlhseXGGJ7VbFO4pBANu0kwl8VCa9KEI0V8VfZIx2Fy3w01iSTA/pGwKZSmu/+I4etLNDdt5w==", - "dependencies": { - "source-map": "~0.5.1", - "yargs": "~3.10.0" - }, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - }, - "optionalDependencies": { - "uglify-to-browserify": "~1.0.0" - } - }, - "node_modules/uglify-js/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha512-vb2s1lYx2xBtUgy+ta+b2J/GLVUR+wmpINwHePmPRhOsIVCG2wDzKJ0n14GslH1BifsqVzSOwQhRaCAsZ/nI4Q==", - "optional": true + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "license": "MIT" }, - "node_modules/update-notifier": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-0.5.0.tgz", - "integrity": "sha512-zOGOlUKDAgDlLHLv7Oiszz3pSj8fKlSJ3i0u49sEakjXUEVJ6DMjo/Mh/B6mg2eOALvRTJkd0kbChcipQoYCng==", + "node_modules/valid-data-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", + "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/web-resource-inliner": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-7.0.0.tgz", + "integrity": "sha512-NlfnGF8MY9ZUwFjyq3vOUBx7KwF8bmE+ywR781SB0nWB6MoMxN4BA8gtgP1KGTZo/O/AyWJz7HZpR704eaj4mg==", + "license": "MIT", "dependencies": { - "chalk": "^1.0.0", - "configstore": "^1.0.0", - "is-npm": "^1.0.0", - "latest-version": "^1.0.0", - "repeating": "^1.1.2", - "semver-diff": "^2.0.0", - "string-length": "^1.0.0" + "ansi-colors": "^4.1.1", + "escape-goat": "^3.0.0", + "htmlparser2": "^5.0.0", + "mime": "^2.4.6", + "valid-data-url": "^3.0.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "engines": [ - "node >=0.6.0" - ], - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "node_modules/verror/node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" - }, - "node_modules/whet.extend": { - "version": "0.9.9", - "resolved": "https://registry.npmjs.org/whet.extend/-/whet.extend-0.9.9.tgz", - "integrity": "sha512-mmIPAft2vTgEILgPeZFqE/wWh24SEsR/k+N9fJ3Jxrz44iDFy9aemCxdksfURSHYFCLmvs/d/7Iso5XjPpNfrA==", - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha512-1pTPQDKTdd61ozlKGNCjhNRd+KPmgLSGa3mZTHoOliaGcESD8G1PXhh7c1fgiPjVbNVfgy2Faw4BI8/m0cC8Mg==", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/write-file-atomic": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", - "integrity": "sha512-SdrHoC/yVBPpV0Xq/mUZQIpW2sWXAShb/V4pomcJXh92RuaO+f3UTWItiR3Px+pLnV2PvC2/bfn5cwr5X6Vfxw==", - "dependencies": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "slide": "^1.1.5" - } - }, - "node_modules/xdg-basedir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-2.0.0.tgz", - "integrity": "sha512-NF1pPn594TaRSUO/HARoB4jK8I+rWgcpVlpQCK6/6o5PHyLUt2CSiDrpUZbQ6rROck+W2EwF8mBJcTs+W98J9w==", - "dependencies": { - "os-homedir": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha512-QFzUah88GAGy9lyDKGBqZdkYApt63rCXYBGYnEP4xDJPXNqXXnBDACnbrXnViV6jRSqAePwrATi2i8mfYm4L1A==", - "dependencies": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.0" + "node": ">=10.0.0" } } } diff --git a/package.json b/package.json index fbcb8ed8..2d7ad2de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wled", - "version": "0.15.0-dev", + "version": "0.16.0-alpha", "description": "Tools for WLED project", "main": "tools/cdata.js", "directories": { @@ -14,21 +14,21 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/Aircoookie/WLED.git" + "url": "git+https://github.com/wled-dev/WLED.git" }, "author": "", "license": "ISC", "bugs": { - "url": "https://github.com/Aircoookie/WLED/issues" + "url": "https://github.com/wled-dev/WLED/issues" }, - "homepage": "https://github.com/Aircoookie/WLED#readme", + "homepage": "https://github.com/wled-dev/WLED#readme", "dependencies": { "clean-css": "^5.3.3", "html-minifier-terser": "^7.2.0", - "inliner": "^1.13.1", - "nodemon": "^3.1.7" + "web-resource-inliner": "^7.0.0", + "nodemon": "^3.1.9" }, "engines": { "node": ">=20.0.0" } -} +} \ No newline at end of file diff --git a/pio-scripts/build_ui.py b/pio-scripts/build_ui.py index f3688a5d..eb7a01b3 100644 --- a/pio-scripts/build_ui.py +++ b/pio-scripts/build_ui.py @@ -1,3 +1,21 @@ -Import('env') +Import("env") +import shutil -env.Execute("npm run build") \ No newline at end of file +node_ex = shutil.which("node") +# Check if Node.js is installed and present in PATH if it failed, abort the build +if node_ex is None: + print('\x1b[0;31;43m' + 'Node.js is not installed or missing from PATH html css js will not be processed check https://kno.wled.ge/advanced/compiling-wled/' + '\x1b[0m') + exitCode = env.Execute("null") + exit(exitCode) +else: + # Install the necessary node packages for the pre-build asset bundling script + print('\x1b[6;33;42m' + 'Installing node packages' + '\x1b[0m') + env.Execute("npm ci") + + # Call the bundling script + exitCode = env.Execute("npm run build") + + # If it failed, abort the build + if (exitCode): + print('\x1b[0;31;43m' + 'npm run build fails check https://kno.wled.ge/advanced/compiling-wled/' + '\x1b[0m') + exit(exitCode) diff --git a/pio-scripts/load_usermods.py b/pio-scripts/load_usermods.py new file mode 100644 index 00000000..146cb1f8 --- /dev/null +++ b/pio-scripts/load_usermods.py @@ -0,0 +1,103 @@ +Import('env') +from collections import deque +from pathlib import Path # For OS-agnostic path manipulation +from click import secho +from SCons.Script import Exit +from platformio.builder.tools.piolib import LibBuilderBase + +usermod_dir = Path(env["PROJECT_DIR"]).resolve() / "usermods" + +# Utility functions +def find_usermod(mod: str) -> Path: + """Locate this library in the usermods folder. + We do this to avoid needing to rename a bunch of folders; + this could be removed later + """ + # Check name match + mp = usermod_dir / mod + if mp.exists(): + return mp + mp = usermod_dir / f"{mod}_v2" + if mp.exists(): + return mp + mp = usermod_dir / f"usermod_v2_{mod}" + if mp.exists(): + return mp + raise RuntimeError(f"Couldn't locate module {mod} in usermods directory!") + +def is_wled_module(dep: LibBuilderBase) -> bool: + """Returns true if the specified library is a wled module + """ + return usermod_dir in Path(dep.src_dir).parents or str(dep.name).startswith("wled-") + +## Script starts here +# Process usermod option +usermods = env.GetProjectOption("custom_usermods","") + +# Handle "all usermods" case +if usermods == '*': + usermods = [f.name for f in usermod_dir.iterdir() if f.is_dir() and f.joinpath('library.json').exists()] +else: + usermods = usermods.split() + +if usermods: + # Inject usermods in to project lib_deps + symlinks = [f"symlink://{find_usermod(mod).resolve()}" for mod in usermods] + env.GetProjectConfig().set("env:" + env['PIOENV'], 'lib_deps', env.GetProjectOption('lib_deps') + symlinks) + +# Utility function for assembling usermod include paths +def cached_add_includes(dep, dep_cache: set, includes: deque): + """ Add dep's include paths to includes if it's not in the cache """ + if dep not in dep_cache: + dep_cache.add(dep) + for include in dep.get_include_dirs(): + if include not in includes: + includes.appendleft(include) + if usermod_dir not in Path(dep.src_dir).parents: + # Recurse, but only for NON-usermods + for subdep in dep.depbuilders: + cached_add_includes(subdep, dep_cache, includes) + +# Monkey-patch ConfigureProjectLibBuilder to mark up the dependencies +# Save the old value +old_ConfigureProjectLibBuilder = env.ConfigureProjectLibBuilder + +# Our new wrapper +def wrapped_ConfigureProjectLibBuilder(xenv): + # Call the wrapped function + result = old_ConfigureProjectLibBuilder.clone(xenv)() + + # Fix up include paths + # In PlatformIO >=6.1.17, this could be done prior to ConfigureProjectLibBuilder + wled_dir = xenv["PROJECT_SRC_DIR"] + # Build a list of dependency include dirs + # TODO: Find out if this is the order that PlatformIO/SCons puts them in?? + processed_deps = set() + extra_include_dirs = deque() # Deque used for fast prepend + for dep in result.depbuilders: + cached_add_includes(dep, processed_deps, extra_include_dirs) + + broken_usermods = [] + for dep in result.depbuilders: + if is_wled_module(dep): + # Add the wled folder to the include path + dep.env.PrependUnique(CPPPATH=str(wled_dir)) + # Add WLED's own dependencies + for dir in extra_include_dirs: + dep.env.PrependUnique(CPPPATH=str(dir)) + # Enforce that libArchive is not set; we must link them directly to the executable + if dep.lib_archive: + broken_usermods.append(dep) + + if broken_usermods: + broken_usermods = [usermod.name for usermod in broken_usermods] + secho( + f"ERROR: libArchive=false is missing on usermod(s) {' '.join(broken_usermods)} -- modules will not compile in correctly", + fg="red", + err=True) + Exit(1) + + return result + +# Apply the wrapper +env.AddMethod(wrapped_ConfigureProjectLibBuilder, "ConfigureProjectLibBuilder") diff --git a/pio-scripts/validate_modules.py b/pio-scripts/validate_modules.py new file mode 100644 index 00000000..f8c9a599 --- /dev/null +++ b/pio-scripts/validate_modules.py @@ -0,0 +1,92 @@ +import re +from pathlib import Path # For OS-agnostic path manipulation +from typing import Iterable +from click import secho +from SCons.Script import Action, Exit +from platformio.builder.tools.piolib import LibBuilderBase + + +def is_wled_module(env, dep: LibBuilderBase) -> bool: + """Returns true if the specified library is a wled module + """ + usermod_dir = Path(env["PROJECT_DIR"]).resolve() / "usermods" + return usermod_dir in Path(dep.src_dir).parents or str(dep.name).startswith("wled-") + + +def read_lines(p: Path): + """ Read in the contents of a file for analysis """ + with p.open("r", encoding="utf-8", errors="ignore") as f: + return f.readlines() + + +def check_map_file_objects(map_file: list[str], dirs: Iterable[str]) -> set[str]: + """ Identify which dirs contributed to the final build + + Returns the (sub)set of dirs that are found in the output ELF + """ + # Pattern to match symbols in object directories + # Join directories into alternation + usermod_dir_regex = "|".join([re.escape(dir) for dir in dirs]) + # Matches nonzero address, any size, and any path in a matching directory + object_path_regex = re.compile(r"0x0*[1-9a-f][0-9a-f]*\s+0x[0-9a-f]+\s+\S+[/\\](" + usermod_dir_regex + r")[/\\]\S+\.o") + + found = set() + for line in map_file: + matches = object_path_regex.findall(line) + for m in matches: + found.add(m) + return found + + +def count_usermod_objects(map_file: list[str]) -> int: + """ Returns the number of usermod objects in the usermod list """ + # Count the number of entries in the usermods table section + return len([x for x in map_file if ".dtors.tbl.usermods.1" in x]) + + +def validate_map_file(source, target, env): + """ Validate that all modules appear in the output build """ + build_dir = Path(env.subst("$BUILD_DIR")) + map_file_path = build_dir / env.subst("${PROGNAME}.map") + + if not map_file_path.exists(): + secho(f"ERROR: Map file not found: {map_file_path}", fg="red", err=True) + Exit(1) + + # Identify the WLED module source directories + module_lib_builders = [builder for builder in env.GetLibBuilders() if is_wled_module(env, builder)] + + if env.GetProjectOption("custom_usermods","") == "*": + # All usermods build; filter non-platform-OK modules + module_lib_builders = [builder for builder in module_lib_builders if env.IsCompatibleLibBuilder(builder)] + else: + incompatible_builders = [builder for builder in module_lib_builders if not env.IsCompatibleLibBuilder(builder)] + if incompatible_builders: + secho( + f"ERROR: Modules {[b.name for b in incompatible_builders]} are not compatible with this platform!", + fg="red", + err=True) + Exit(1) + + # Extract the values we care about + modules = {Path(builder.build_dir).name: builder.name for builder in module_lib_builders} + secho(f"INFO: {len(modules)} libraries linked as WLED optional/user modules") + + # Now parse the map file + map_file_contents = read_lines(map_file_path) + usermod_object_count = count_usermod_objects(map_file_contents) + secho(f"INFO: {usermod_object_count} usermod object entries") + + confirmed_modules = check_map_file_objects(map_file_contents, modules.keys()) + missing_modules = [modname for mdir, modname in modules.items() if mdir not in confirmed_modules] + if missing_modules: + secho( + f"ERROR: No object files from {missing_modules} found in linked output!", + fg="red", + err=True) + Exit(1) + return None + +Import("env") +env.Append(LINKFLAGS=[env.subst("-Wl,--Map=${BUILD_DIR}/${PROGNAME}.map")]) +env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", Action(validate_map_file, cmdstr='Checking linked optional modules (usermods) in map file')) diff --git a/platformio.ini b/platformio.ini index fe02213f..9bdf58d3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -10,7 +10,7 @@ # ------------------------------------------------------------------------------ # CI/release binaries -default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, nodemcuv2_160, esp8266_2m_160, esp01_1m_full_160, nodemcuv2_compat, esp8266_2m_compat, esp01_1m_full_compat, esp32dev, esp32_eth, lolin_s2_mini, esp32c3dev, esp32s3dev_16MB_opi, esp32s3dev_8MB_opi, esp32s3_4M_qspi, esp32_wrover +default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, nodemcuv2_160, esp8266_2m_160, esp01_1m_full_160, nodemcuv2_compat, esp8266_2m_compat, esp01_1m_full_compat, esp32dev, esp32dev_V4, esp32_eth, lolin_s2_mini, esp32c3dev, esp32s3dev_16MB_opi, esp32s3dev_8MB_opi, esp32s3_4M_qspi, esp32_wrover, usermods src_dir = ./wled00 data_dir = ./wled00/data @@ -114,7 +114,9 @@ extra_scripts = post:pio-scripts/output_bins.py post:pio-scripts/strip-floats.py pre:pio-scripts/user_config_copy.py + pre:pio-scripts/load_usermods.py pre:pio-scripts/build_ui.py + post:pio-scripts/validate_modules.py ;; double-check the build output usermods ; post:pio-scripts/obj-dump.py ;; convenience script to create a disassembly dump of the firmware (hardcore debugging) # ------------------------------------------------------------------------------ @@ -138,9 +140,9 @@ lib_compat_mode = strict lib_deps = fastled/FastLED @ 3.6.0 IRremoteESP8266 @ 2.8.2 - makuna/NeoPixelBus @ 2.8.0 + makuna/NeoPixelBus @ 2.8.3 #https://github.com/makuna/NeoPixelBus.git#CoreShaderBeta - https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.2.1 + https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.0 # for I2C interface ;Wire # ESP-NOW library @@ -157,25 +159,18 @@ lib_deps = ;adafruit/Adafruit BMP280 Library @ 2.1.0 ;adafruit/Adafruit CCS811 Library @ 1.0.4 ;adafruit/Adafruit Si7021 Library @ 1.4.0 - #For ADS1115 sensor uncomment following - ;adafruit/Adafruit BusIO @ 1.13.2 - ;adafruit/Adafruit ADS1X15 @ 2.4.0 #For MAX1704x Lipo Monitor / Fuel Gauge uncomment following ; https://github.com/adafruit/Adafruit_BusIO @ 1.14.5 ; https://github.com/adafruit/Adafruit_MAX1704X @ 1.0.2 #For MPU6050 IMU uncomment follwoing ;electroniccats/MPU6050 @1.0.1 - # For -D USERMOD_ANIMARTRIX - # CC BY-NC 3.0 licensed effects by Stefan Petrick, include this usermod only if you accept the terms! - ;https://github.com/netmindz/animartrix.git#18bf17389e57c69f11bc8d04ebe1d215422c7fb7 # SHT85 ;robtillaart/SHT85@~0.3.3 - # Audioreactive usermod - ;kosme/arduinoFFT @ 2.0.1 extra_scripts = ${scripts_defaults.extra_scripts} [esp8266] +build_unflags = ${common.build_unflags} build_flags = -DESP8266 -DFP_IN_IROM @@ -235,20 +230,30 @@ lib_deps_compat = IRremoteESP8266 @ 2.8.2 makuna/NeoPixelBus @ 2.7.9 https://github.com/blazoncek/QuickESPNow.git#optional-debug - https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.2.1 + https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.0 +[esp32_all_variants] +lib_deps = + willmmiles/AsyncTCP @ 1.3.1 + bitbank2/AnimatedGIF@^1.4.7 + https://github.com/Aircoookie/GifDecoder#bc3af18 +build_flags = + -D CONFIG_ASYNC_TCP_USE_WDT=0 + -D WLED_ENABLE_GIF [esp32] #platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.2.3/platform-espressif32-2.0.2.3.zip platform = espressif32@3.5.0 platform_packages = framework-arduinoespressif32 @ https://github.com/Aircoookie/arduino-esp32.git#1.0.6.4 +build_unflags = ${common.build_unflags} build_flags = -g -DARDUINO_ARCH_ESP32 #-DCONFIG_LITTLEFS_FOR_IDF_3_2 - -D CONFIG_ASYNC_TCP_USE_WDT=0 #use LITTLEFS library by lorol in ESP32 core 1.x.x instead of built-in in 2.x.x -D LOROL_LITTLEFS ; -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 + ${esp32_all_variants.build_flags} + tiny_partitions = tools/WLED_ESP32_2MB_noOTA.csv default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv extended_partitions = tools/WLED_ESP32_4MB_700k_FS.csv @@ -257,12 +262,13 @@ large_partitions = tools/WLED_ESP32_8MB.csv extreme_partitions = tools/WLED_ESP32_16MB_9MB_FS.csv lib_deps = https://github.com/lorol/LITTLEFS.git - https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 + ${esp32_all_variants.lib_deps} ${env.lib_deps} -# additional build flags for audioreactive -AR_build_flags = -D USERMOD_AUDIOREACTIVE - -D sqrt_internal=sqrtf ;; -fsingle-precision-constant ;; forces ArduinoFFT to use float math (2x faster) -AR_lib_deps = kosme/arduinoFFT @ 2.0.1 +board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs +# additional build flags for audioreactive - must be applied globally +AR_build_flags = ;; -fsingle-precision-constant ;; forces ArduinoFFT to use float math (2x faster) +AR_lib_deps = ;; for pre-usermod-library platformio_override compatibility + [esp32_idf_V4] ;; experimental build environment for ESP32 using ESP-IDF 4.4.x / arduino-esp32 v2.0.5 @@ -270,69 +276,78 @@ AR_lib_deps = kosme/arduinoFFT @ 2.0.1 ;; ;; please note that you can NOT update existing ESP32 installs with a "V4" build. Also updating by OTA will not work properly. ;; You need to completely erase your device (esptool erase_flash) first, then install the "V4" build from VSCode+platformio. -platform = espressif32@ ~6.3.2 -platform_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) + +;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) +platform = https://github.com/tasmota/platform-espressif32/releases/download/2023.06.02/platform-espressif32.zip ;; Tasmota Arduino Core 2.0.9 with IPv6 support, based on IDF 4.4.4 +build_unflags = ${common.build_unflags} build_flags = -g -Wshadow=compatible-local ;; emit warning in case a local variable "shadows" another local one -DARDUINO_ARCH_ESP32 -DESP32 - -D CONFIG_ASYNC_TCP_USE_WDT=0 -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 + ${esp32_all_variants.build_flags} + -D WLED_ENABLE_DMX_INPUT lib_deps = - https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 + ${esp32_all_variants.lib_deps} + https://github.com/someweisguy/esp_dmx.git#47db25d ${env.lib_deps} +board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs [esp32s2] ;; generic definitions for all ESP32-S2 boards -platform = espressif32@ ~6.3.2 -platform_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) +platform = ${esp32_idf_V4.platform} +build_unflags = ${common.build_unflags} build_flags = -g -DARDUINO_ARCH_ESP32 -DARDUINO_ARCH_ESP32S2 -DCONFIG_IDF_TARGET_ESP32S2=1 - -D CONFIG_ASYNC_TCP_USE_WDT=0 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=0 -DCO -DARDUINO_USB_MODE=0 ;; this flag is mandatory for ESP32-S2 ! ;; please make sure that the following flags are properly set (to 0 or 1) by your board.json, or included in your custom platformio_override.ini entry: ;; ARDUINO_USB_CDC_ON_BOOT + ${esp32_all_variants.build_flags} lib_deps = - https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 + ${esp32_all_variants.lib_deps} ${env.lib_deps} +board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs [esp32c3] ;; generic definitions for all ESP32-C3 boards -platform = espressif32@ ~6.3.2 -platform_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) +platform = ${esp32_idf_V4.platform} +build_unflags = ${common.build_unflags} build_flags = -g -DARDUINO_ARCH_ESP32 -DARDUINO_ARCH_ESP32C3 -DCONFIG_IDF_TARGET_ESP32C3=1 - -D CONFIG_ASYNC_TCP_USE_WDT=0 -DCO -DARDUINO_USB_MODE=1 ;; this flag is mandatory for ESP32-C3 ;; please make sure that the following flags are properly set (to 0 or 1) by your board.json, or included in your custom platformio_override.ini entry: ;; ARDUINO_USB_CDC_ON_BOOT + ${esp32_all_variants.build_flags} lib_deps = - https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 + ${esp32_all_variants.lib_deps} ${env.lib_deps} +board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs +board_build.flash_mode = qio [esp32s3] ;; generic definitions for all ESP32-S3 boards -platform = espressif32@ ~6.3.2 -platform_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) +platform = ${esp32_idf_V4.platform} +build_unflags = ${common.build_unflags} build_flags = -g -DESP32 -DARDUINO_ARCH_ESP32 -DARDUINO_ARCH_ESP32S3 -DCONFIG_IDF_TARGET_ESP32S3=1 - -D CONFIG_ASYNC_TCP_USE_WDT=0 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_DFU_ON_BOOT=0 -DCO ;; please make sure that the following flags are properly set (to 0 or 1) by your board.json, or included in your custom platformio_override.ini entry: ;; ARDUINO_USB_MODE, ARDUINO_USB_CDC_ON_BOOT + ${esp32_all_variants.build_flags} lib_deps = - https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 + ${esp32_all_variants.lib_deps} ${env.lib_deps} +board_build.partitions = ${esp32.large_partitions} ;; default partioning for 8MB flash - can be overridden in build envs # ------------------------------------------------------------------------------ @@ -346,6 +361,7 @@ platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266\" #-DWLED_DISABLE_2D + -D WLED_DISABLE_PARTICLESYSTEM2D lib_deps = ${esp8266.lib_deps} monitor_filters = esp8266_exception_decoder @@ -355,13 +371,15 @@ extends = env:nodemcuv2 platform = ${esp8266.platform_compat} platform_packages = ${esp8266.platform_packages_compat} build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP8266_compat\" #-DWLED_DISABLE_2D + -D WLED_DISABLE_PARTICLESYSTEM2D ;; lib_deps = ${esp8266.lib_deps_compat} ;; experimental - use older NeoPixelBus 2.7.9 [env:nodemcuv2_160] extends = env:nodemcuv2 board_build.f_cpu = 160000000L build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_160\" #-DWLED_DISABLE_2D - -D USERMOD_AUDIOREACTIVE + -D WLED_DISABLE_PARTICLESYSTEM2D +custom_usermods = audioreactive [env:esp8266_2m] board = esp_wroom_02 @@ -370,6 +388,8 @@ platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP02\" + -D WLED_DISABLE_PARTICLESYSTEM2D + -D WLED_DISABLE_PARTICLESYSTEM1D lib_deps = ${esp8266.lib_deps} [env:esp8266_2m_compat] @@ -378,12 +398,16 @@ extends = env:esp8266_2m platform = ${esp8266.platform_compat} platform_packages = ${esp8266.platform_packages_compat} build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP02_compat\" #-DWLED_DISABLE_2D + -D WLED_DISABLE_PARTICLESYSTEM1D + -D WLED_DISABLE_PARTICLESYSTEM2D [env:esp8266_2m_160] extends = env:esp8266_2m board_build.f_cpu = 160000000L build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP02_160\" - -D USERMOD_AUDIOREACTIVE + -D WLED_DISABLE_PARTICLESYSTEM1D + -D WLED_DISABLE_PARTICLESYSTEM2D +custom_usermods = audioreactive [env:esp01_1m_full] board = esp01_1m @@ -393,6 +417,8 @@ board_build.ldscript = ${common.ldscript_1m128k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP01\" -D WLED_DISABLE_OTA ; -D WLED_USE_REAL_MATH ;; may fix wrong sunset/sunrise times, at the cost of 7064 bytes FLASH and 975 bytes RAM + -D WLED_DISABLE_PARTICLESYSTEM1D + -D WLED_DISABLE_PARTICLESYSTEM2D lib_deps = ${esp8266.lib_deps} [env:esp01_1m_full_compat] @@ -401,35 +427,47 @@ extends = env:esp01_1m_full platform = ${esp8266.platform_compat} platform_packages = ${esp8266.platform_packages_compat} build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP01_compat\" -D WLED_DISABLE_OTA #-DWLED_DISABLE_2D + -D WLED_DISABLE_PARTICLESYSTEM1D + -D WLED_DISABLE_PARTICLESYSTEM2D [env:esp01_1m_full_160] extends = env:esp01_1m_full board_build.f_cpu = 160000000L build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP01_160\" -D WLED_DISABLE_OTA - -D USERMOD_AUDIOREACTIVE ; -D WLED_USE_REAL_MATH ;; may fix wrong sunset/sunrise times, at the cost of 7064 bytes FLASH and 975 bytes RAM + -D WLED_DISABLE_PARTICLESYSTEM1D + -D WLED_DISABLE_PARTICLESYSTEM2D +custom_usermods = audioreactive [env:esp32dev] board = esp32dev platform = ${esp32.platform} platform_packages = ${esp32.platform_packages} +custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"ESP32\" #-D WLED_DISABLE_BROWNOUT_DET - ${esp32.AR_build_flags} lib_deps = ${esp32.lib_deps} - ${esp32.AR_lib_deps} monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.default_partitions} +[env:esp32dev_V4] +board = esp32dev +platform = ${esp32_idf_V4.platform} +build_unflags = ${common.build_unflags} +custom_usermods = audioreactive +build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_V4\" #-D WLED_DISABLE_BROWNOUT_DET +lib_deps = ${esp32_idf_V4.lib_deps} +monitor_filters = esp32_exception_decoder +board_build.partitions = ${esp32.default_partitions} +board_build.flash_mode = dio + [env:esp32dev_8M] board = esp32dev platform = ${esp32_idf_V4.platform} -platform_packages = ${esp32_idf_V4.platform_packages} +custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_8M\" #-D WLED_DISABLE_BROWNOUT_DET - ${esp32.AR_build_flags} lib_deps = ${esp32_idf_V4.lib_deps} - ${esp32.AR_lib_deps} monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.large_partitions} board_upload.flash_size = 8MB @@ -440,12 +478,10 @@ board_upload.maximum_size = 8388608 [env:esp32dev_16M] board = esp32dev platform = ${esp32_idf_V4.platform} -platform_packages = ${esp32_idf_V4.platform_packages} +custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_16M\" #-D WLED_DISABLE_BROWNOUT_DET - ${esp32.AR_build_flags} lib_deps = ${esp32_idf_V4.lib_deps} - ${esp32.AR_lib_deps} monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.extreme_partitions} board_upload.flash_size = 16MB @@ -457,11 +493,10 @@ board_build.flash_mode = dio ;board = esp32dev ;platform = ${esp32.platform} ;platform_packages = ${esp32.platform_packages} +;custom_usermods = audioreactive ;build_unflags = ${common.build_unflags} ;build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"ESP32_audioreactive\" #-D WLED_DISABLE_BROWNOUT_DET -; ${esp32.AR_build_flags} ;lib_deps = ${esp32.lib_deps} -; ${esp32.AR_lib_deps} ;monitor_filters = esp32_exception_decoder ;board_build.partitions = ${esp32.default_partitions} ;; board_build.f_flash = 80000000L @@ -472,34 +507,30 @@ board = esp32-poe platform = ${esp32.platform} platform_packages = ${esp32.platform_packages} upload_speed = 921600 +custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"ESP32_Ethernet\" -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1 ; -D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only - ${esp32.AR_build_flags} lib_deps = ${esp32.lib_deps} - ${esp32.AR_lib_deps} board_build.partitions = ${esp32.default_partitions} [env:esp32_wrover] extends = esp32_idf_V4 platform = ${esp32_idf_V4.platform} -platform_packages = ${esp32_idf_V4.platform_packages} board = ttgo-t7-v14-mini32 board_build.f_flash = 80000000L board_build.flash_mode = qio board_build.partitions = ${esp32.extended_partitions} +custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_WROVER\" -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue ;; Older ESP32 (rev.<3) need a PSRAM fix (increases static RAM used) https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/external-ram.html -D DATA_PINS=25 - ${esp32.AR_build_flags} lib_deps = ${esp32_idf_V4.lib_deps} - ${esp32.AR_lib_deps} - + [env:esp32c3dev] extends = esp32c3 platform = ${esp32c3.platform} -platform_packages = ${esp32c3.platform_packages} framework = arduino board = esp32-c3-devkitm-1 board_build.partitions = ${esp32.default_partitions} @@ -517,17 +548,15 @@ lib_deps = ${esp32c3.lib_deps} board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support board_build.arduino.memory_type = qio_opi ;; use with PSRAM: 8MB or 16MB platform = ${esp32s3.platform} -platform_packages = ${esp32s3.platform_packages} upload_speed = 921600 +custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_16MB_opi\" -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 ;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") -DBOARD_HAS_PSRAM - ${esp32.AR_build_flags} lib_deps = ${esp32s3.lib_deps} - ${esp32.AR_lib_deps} board_build.partitions = ${esp32.extreme_partitions} board_upload.flash_size = 16MB board_upload.maximum_size = 16777216 @@ -540,17 +569,15 @@ monitor_filters = esp32_exception_decoder board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support board_build.arduino.memory_type = qio_opi ;; use with PSRAM: 8MB or 16MB platform = ${esp32s3.platform} -platform_packages = ${esp32s3.platform_packages} upload_speed = 921600 +custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_8MB_opi\" -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 ;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") -DBOARD_HAS_PSRAM - ${esp32.AR_build_flags} lib_deps = ${esp32s3.lib_deps} - ${esp32.AR_lib_deps} board_build.partitions = ${esp32.large_partitions} board_build.f_flash = 80000000L board_build.flash_mode = qio @@ -560,10 +587,10 @@ monitor_filters = esp32_exception_decoder ;; For ESP32-S3 WROOM-2, a.k.a. ESP32-S3 DevKitC-1 v1.1 ;; with >= 16MB FLASH and >= 8MB PSRAM (memory_type: opi_opi) platform = ${esp32s3.platform} -platform_packages = ${esp32s3.platform_packages} board = esp32s3camlcd ;; this is the only standard board with "opi_opi" board_build.arduino.memory_type = opi_opi upload_speed = 921600 +custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_WROOM-2\" -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 @@ -573,10 +600,8 @@ build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME= -D LEDPIN=38 -D DATA_PINS=38 ;; buildin WS2812b LED -D BTNPIN=0 -D RLYPIN=16 -D IRPIN=17 -D AUDIOPIN=-1 -D WLED_DEBUG - ${esp32.AR_build_flags} -D SR_DMTYPE=1 -D I2S_SDPIN=13 -D I2S_CKPIN=14 -D I2S_WSPIN=15 -D MCLK_PIN=4 ;; I2S mic lib_deps = ${esp32s3.lib_deps} - ${esp32.AR_lib_deps} board_build.partitions = ${esp32.extreme_partitions} board_upload.flash_size = 16MB @@ -587,17 +612,15 @@ monitor_filters = esp32_exception_decoder ;; ESP32-S3, with 4MB FLASH and <= 4MB PSRAM (memory_type: qio_qspi) board = lolin_s3_mini ;; -S3 mini, 4MB flash 2MB PSRAM platform = ${esp32s3.platform} -platform_packages = ${esp32s3.platform_packages} upload_speed = 921600 +custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_4M_qspi\" -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") -DBOARD_HAS_PSRAM -DLOLIN_WIFI_FIX ; seems to work much better with this -D WLED_WATCHDOG_TIMEOUT=0 - ${esp32.AR_build_flags} lib_deps = ${esp32s3.lib_deps} - ${esp32.AR_lib_deps} board_build.partitions = ${esp32.default_partitions} board_build.f_flash = 80000000L board_build.flash_mode = qio @@ -605,11 +628,11 @@ monitor_filters = esp32_exception_decoder [env:lolin_s2_mini] platform = ${esp32s2.platform} -platform_packages = ${esp32s2.platform_packages} board = lolin_s2_mini board_build.partitions = ${esp32.default_partitions} board_build.flash_mode = qio board_build.f_flash = 80000000L +custom_usermods = audioreactive build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s2.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S2\" -DARDUINO_USB_CDC_ON_BOOT=1 @@ -618,7 +641,6 @@ build_flags = ${common.build_flags} ${esp32s2.build_flags} -D WLED_RELEASE_NAME= -DBOARD_HAS_PSRAM -DLOLIN_WIFI_FIX ; seems to work much better with this -D WLED_WATCHDOG_TIMEOUT=0 - -D CONFIG_ASYNC_TCP_USE_WDT=0 -D DATA_PINS=16 -D HW_PIN_SCL=35 -D HW_PIN_SDA=33 @@ -626,6 +648,17 @@ build_flags = ${common.build_flags} ${esp32s2.build_flags} -D WLED_RELEASE_NAME= -D HW_PIN_DATASPI=11 -D HW_PIN_MISOSPI=9 ; -D STATUSLED=15 - ${esp32.AR_build_flags} lib_deps = ${esp32s2.lib_deps} - ${esp32.AR_lib_deps} + + +[env:usermods] +board = esp32dev +platform = ${esp32_idf_V4.platform} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_USERMODS\" + -DTOUCH_CS=9 +lib_deps = ${esp32_idf_V4.lib_deps} +monitor_filters = esp32_exception_decoder +board_build.flash_mode = dio +custom_usermods = * ; Expands to all usermods in usermods folder +board_build.partitions = ${esp32.extreme_partitions} ; We're gonna need a bigger boat diff --git a/platformio_override.sample.ini b/platformio_override.sample.ini index a0120897..7151e7e3 100644 --- a/platformio_override.sample.ini +++ b/platformio_override.sample.ini @@ -5,7 +5,7 @@ # Please visit documentation: https://docs.platformio.org/page/projectconf.html [platformio] -default_envs = WLED_tasmota_1M # define as many as you need +default_envs = WLED_generic8266_1M, esp32dev_V4_dio80 # put the name(s) of your own build environment here. You can define as many as you need #---------- # SAMPLE @@ -28,13 +28,13 @@ lib_deps = ${esp8266.lib_deps} ; robtillaart/SHT85@~0.3.3 ; ;gmag11/QuickESPNow @ ~0.7.0 # will also load QuickDebug ; https://github.com/blazoncek/QuickESPNow.git#optional-debug ;; exludes debug library -; ${esp32.AR_lib_deps} ;; used for USERMOD_AUDIOREACTIVE ; bitbank2/PNGdec@^1.0.1 ;; used for POV display uncomment following +; ${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} ; -; *** To use the below defines/overrides, copy and paste each onto it's own line just below build_flags in the section above. +; *** To use the below defines/overrides, copy and paste each onto its own line just below build_flags in the section above. ; ; Set a release name that may be used to distinguish required binary for flashing ; -D WLED_RELEASE_NAME=\"ESP32_MULTI_USREMODS\" @@ -141,7 +141,8 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} ; -D PIR_SENSOR_MAX_SENSORS=2 # max allowable sensors (uses OR logic for triggering) ; ; Use Audioreactive usermod and configure I2S microphone -; -D USERMOD_AUDIOREACTIVE +; ${esp32.AR_build_flags} ;; default flags required to properly configure ArduinoFFT +; ;; don't forget to add ArduinoFFT to your libs_deps: ${esp32.AR_lib_deps} ; -D AUDIOPIN=-1 ; -D DMTYPE=1 # 0-analog/disabled, 1-I2S generic, 2-ES7243, 3-SPH0645, 4-I2S+mclk, 5-I2S PDM ; -D I2S_SDPIN=36 @@ -157,17 +158,22 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} ; -D USERMOD_POV_DISPLAY ; Use built-in or custom LED as a status indicator (assumes LED is connected to GPIO16) ; -D STATUSLED=16 -; +; ; set the name of the module - make sure there is a quote-backslash-quote before the name and a backslash-quote-quote after the name ; -D SERVERNAME="\"WLED\"" -; +; ; set the number of LEDs -; -D DEFAULT_LED_COUNT=30 +; -D PIXEL_COUNTS=30 ; or this for multiple outputs ; -D PIXEL_COUNTS=30,30 ; ; set the default LED type -; -D DEFAULT_LED_TYPE=22 # see const.h (TYPE_xxxx) +; -D LED_TYPES=22 # see const.h (TYPE_xxxx) +; or this for multiple outputs +; -D LED_TYPES=TYPE_SK6812_RGBW,TYPE_WS2812_RGB +; +; set default color order of your led strip +; -D DEFAULT_LED_COLOR_ORDER=COL_ORDER_GRB ; ; set milliampere limit when using ESP power pin (or inadequate PSU) to power LEDs ; -D ABL_MILLIAMPS_DEFAULT=850 @@ -175,9 +181,6 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} ; ; enable IR by setting remote type ; -D IRTYPE=0 # 0 Remote disabled | 1 24-key RGB | 2 24-key with CT | 3 40-key blue | 4 40-key RGB | 5 21-key RGB | 6 6-key black | 7 9-key red | 8 JSON remote -; -; set default color order of your led strip -; -D DEFAULT_LED_COLOR_ORDER=COL_ORDER_GRB ; ; use PSRAM on classic ESP32 rev.1 (rev.3 or above has no issues) ; -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue # needed only for classic ESP32 rev.1 @@ -236,14 +239,13 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} -D DATA_PINS=1 -D WLE lib_deps = ${esp8266.lib_deps} [env:esp32dev_qio80] +extends = env:esp32dev # we want to extend the existing esp32dev environment (and define only updated options) board = esp32dev -platform = ${esp32.platform} -platform_packages = ${esp32.platform_packages} -build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32.build_flags} #-D WLED_DISABLE_BROWNOUT_DET + ${esp32.AR_build_flags} ;; optional - includes USERMOD_AUDIOREACTIVE lib_deps = ${esp32.lib_deps} + ${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE monitor_filters = esp32_exception_decoder -board_build.partitions = ${esp32.default_partitions} board_build.f_flash = 80000000L board_build.flash_mode = qio @@ -251,26 +253,25 @@ board_build.flash_mode = qio ;; experimental ESP32 env using ESP-IDF V4.4.x ;; Warning: this build environment is not stable!! ;; please erase your device before installing. +extends = esp32_idf_V4 # based on newer "esp-idf V4" platform environment board = esp32dev -platform = ${esp32_idf_V4.platform} -platform_packages = ${esp32_idf_V4.platform_packages} -build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} #-D WLED_DISABLE_BROWNOUT_DET + ${esp32.AR_build_flags} ;; includes USERMOD_AUDIOREACTIVE lib_deps = ${esp32_idf_V4.lib_deps} + ${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE monitor_filters = esp32_exception_decoder -board_build.partitions = ${esp32_idf_V4.default_partitions} +board_build.partitions = ${esp32.default_partitions} ;; if you get errors about "out of program space", change this to ${esp32.extended_partitions} or even ${esp32.big_partitions} board_build.f_flash = 80000000L board_build.flash_mode = dio [env:esp32s2_saola] +extends = esp32s2 board = esp32-s2-saola-1 platform = ${esp32s2.platform} platform_packages = ${esp32s2.platform_packages} framework = arduino -board_build.partitions = tools/WLED_ESP32_4MB_1MB_FS.csv board_build.flash_mode = qio upload_speed = 460800 -build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s2.build_flags} ;-DLOLIN_WIFI_FIX ;; try this in case Wifi does not work -DARDUINO_USB_CDC_ON_BOOT=1 @@ -279,7 +280,7 @@ lib_deps = ${esp32s2.lib_deps} [env:esp32s3dev_8MB_PSRAM_qspi] ;; ESP32-TinyS3 development board, with 8MB FLASH and PSRAM (memory_type: qio_qspi) extends = env:esp32s3dev_8MB_PSRAM_opi -;board = um_tinys3 ; -> needs workaround from https://github.com/Aircoookie/WLED/pull/2905#issuecomment-1328049860 +;board = um_tinys3 ; -> needs workaround from https://github.com/wled-dev/WLED/pull/2905#issuecomment-1328049860 board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support board_build.arduino.memory_type = qio_qspi ;; use with PSRAM: 2MB or 4MB @@ -307,7 +308,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_USE_SHOJO_PCB +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_USE_SHOJO_PCB ;; NB: WLED_USE_SHOJO_PCB is not used anywhere in the source code. Not sure why its needed. lib_deps = ${esp8266.lib_deps} [env:d1_mini_debug] @@ -362,35 +363,48 @@ board_upload.flash_size = 2MB board_upload.maximum_size = 2097152 [env:wemos_shield_esp32] +extends = esp32 ;; use default esp32 platform board = esp32dev -platform = ${esp32.platform} -platform_packages = ${esp32.platform_packages} upload_speed = 460800 -build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32.build_flags} + -D WLED_RELEASE_NAME=\"ESP32_wemos_shield\" -D DATA_PINS=16 -D RLYPIN=19 -D BTNPIN=17 -D IRPIN=18 - -D UWLED_USE_MY_CONFIG + -UWLED_USE_MY_CONFIG -D USERMOD_DALLASTEMPERATURE -D USERMOD_FOUR_LINE_DISPLAY -D TEMPERATURE_PIN=23 - -D USERMOD_AUDIOREACTIVE + ${esp32.AR_build_flags} ;; includes USERMOD_AUDIOREACTIVE lib_deps = ${esp32.lib_deps} - OneWire@~2.3.5 - olikraus/U8g2 @ ^2.28.8 - https://github.com/blazoncek/arduinoFFT.git + OneWire@~2.3.5 ;; needed for USERMOD_DALLASTEMPERATURE + olikraus/U8g2 @ ^2.28.8 ;; needed for USERMOD_FOUR_LINE_DISPLAY + ${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE board_build.partitions = ${esp32.default_partitions} -[env:m5atom] -board = esp32dev -build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags} ${esp32.build_flags} -D DATA_PINS=27 -D BTNPIN=39 +[env:esp32_pico-D4] +extends = esp32 ;; use default esp32 platform +board = pico32 ;; pico32-D4 is different from the standard esp32dev + ;; hardware details from https://github.com/srg74/WLED-ESP32-pico +build_flags = ${common.build_flags} ${esp32.build_flags} + -D WLED_RELEASE_NAME=\"pico32-D4\" -D SERVERNAME='"WLED-pico32"' + -D WLED_DISABLE_ADALIGHT ;; no serial-to-USB chip on this board - better to disable serial protocols + -D DATA_PINS=2,18 ;; LED pins + -D RLYPIN=19 -D BTNPIN=0 -D IRPIN=-1 ;; no default pin for IR + ${esp32.AR_build_flags} ;; include USERMOD_AUDIOREACTIVE + -D UM_AUDIOREACTIVE_ENABLE ;; enable AR by default + ;; Audioreactive settings for on-board microphone (ICS-43432) + -D SR_DMTYPE=1 -D I2S_SDPIN=25 -D I2S_WSPIN=15 -D I2S_CKPIN=14 + -D SR_SQUELCH=5 -D SR_GAIN=30 lib_deps = ${esp32.lib_deps} -platform = ${esp32.platform} -platform_packages = ${esp32.platform_packages} + ${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE board_build.partitions = ${esp32.default_partitions} +board_build.f_flash = 80000000L + +[env:m5atom] +extends = env:esp32dev # we want to extend the existing esp32dev environment (and define only updated options) +build_flags = ${common.build_flags} ${esp32.build_flags} -D DATA_PINS=27 -D BTNPIN=39 [env:sp501e] board = esp_wroom_02 @@ -413,7 +427,7 @@ platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=-1 -D RLYPIN=-1 -D DATA_PINS=4,12,14,13,5 - -D DEFAULT_LED_TYPE=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0 + -D LED_TYPES=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0 lib_deps = ${esp8266.lib_deps} [env:Athom_15w_RGBCW] ;15w bulb @@ -423,7 +437,7 @@ platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=-1 -D RLYPIN=-1 -D DATA_PINS=4,12,14,5,13 - -D DEFAULT_LED_TYPE=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0 -D WLED_USE_IC_CCT + -D LED_TYPES=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0 -D WLED_USE_IC_CCT lib_deps = ${esp8266.lib_deps} [env:Athom_3Pin_Controller] ;small controller with only data @@ -489,17 +503,15 @@ lib_deps = ${esp8266.lib_deps} # EleksTube-IPS # ------------------------------------------------------------------------------ [env:elekstube_ips] +extends = esp32 ;; use default esp32 platform board = esp32dev -platform = ${esp32.platform} -platform_packages = ${esp32.platform_packages} upload_speed = 921600 +custom_usermods = ${env:esp32dev.custom_usermods} RTC EleksTube_IPS build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_DISABLE_BROWNOUT_DET -D WLED_DISABLE_INFRARED - -D USERMOD_RTC - -D USERMOD_ELEKSTUBE_IPS -D DATA_PINS=12 -D RLYPIN=27 -D BTNPIN=34 - -D DEFAULT_LED_COUNT=6 + -D PIXEL_COUNTS=6 # Display config -D ST7789_DRIVER -D TFT_WIDTH=135 @@ -513,10 +525,17 @@ build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_DISABLE_BROWNOU -D SPI_FREQUENCY=40000000 -D USER_SETUP_LOADED monitor_filters = esp32_exception_decoder -lib_deps = - ${esp32.lib_deps} - TFT_eSPI @ ^2.3.70 -board_build.partitions = ${esp32.default_partitions} + +# ------------------------------------------------------------------------------ +# Usermod examples +# ------------------------------------------------------------------------------ + +# 433MHz RF remote example for esp32dev +[env:esp32dev_usermod_RF433] +extends = env:esp32dev +build_flags = ${env:esp32dev.build_flags} -D USERMOD_RF433 +lib_deps = ${env:esp32dev.lib_deps} + sui77/rc-switch @ 2.6.4 [env:esp32dev_hub75] diff --git a/readme.md b/readme.md index 8c9a0880..c0d24cff 100644 --- a/readme.md +++ b/readme.md @@ -1,12 +1,12 @@

- - + + - +

diff --git a/requirements.in b/requirements.in index 7c715125..a74bd418 100644 --- a/requirements.in +++ b/requirements.in @@ -1 +1 @@ -platformio +platformio>=6.1.17 diff --git a/requirements.txt b/requirements.txt index 666122aa..7a393786 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,28 +1,26 @@ # -# This file is autogenerated by pip-compile with Python 3.12 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile +# pip-compile requirements.in # ajsonrpc==1.2.0 # via platformio -anyio==4.6.0 +anyio==4.8.0 # via starlette -bottle==0.13.1 +bottle==0.13.2 # via platformio -certifi==2024.8.30 +certifi==2025.1.31 # via requests -charset-normalizer==3.3.2 +charset-normalizer==3.4.1 # via requests -click==8.1.7 +click==8.1.8 # via # platformio # uvicorn colorama==0.4.6 - # via - # click - # platformio -h11==0.14.0 + # via platformio +h11==0.16.0 # via # uvicorn # wsproto @@ -30,29 +28,31 @@ idna==3.10 # via # anyio # requests -marshmallow==3.22.0 +marshmallow==3.26.1 # via platformio -packaging==24.1 +packaging==24.2 # via marshmallow -platformio==6.1.16 +platformio==6.1.17 # via -r requirements.in -pyelftools==0.31 +pyelftools==0.32 # via platformio pyserial==3.5 # via platformio -requests==2.32.3 +requests==2.32.4 # via platformio semantic-version==2.10.0 # via platformio sniffio==1.3.1 # via anyio -starlette==0.39.1 +starlette==0.45.3 # via platformio tabulate==0.9.0 # via platformio -urllib3==2.2.3 +typing-extensions==4.12.2 + # via anyio +urllib3==2.3.0 # via requests -uvicorn==0.30.6 +uvicorn==0.34.0 # via platformio wsproto==1.2.0 # via platformio diff --git a/tools/AutoCubeMap.xlsx b/tools/AutoCubeMap.xlsx new file mode 100644 index 00000000..b3f5cee2 Binary files /dev/null and b/tools/AutoCubeMap.xlsx differ diff --git a/tools/cdata.js b/tools/cdata.js index c5d3c6aa..e02047e8 100644 --- a/tools/cdata.js +++ b/tools/cdata.js @@ -17,7 +17,7 @@ const fs = require("node:fs"); const path = require("path"); -const inliner = require("inliner"); +const inline = require("web-resource-inliner"); const zlib = require("node:zlib"); const CleanCSS = require("clean-css"); const minifyHtml = require("html-minifier-terser").minify; @@ -89,7 +89,7 @@ function adoptVersionAndRepo(html) { repoUrl = repoUrl.replace(/^git\+/, ""); repoUrl = repoUrl.replace(/\.git$/, ""); html = html.replaceAll("https://github.com/atuline/WLED", repoUrl); - html = html.replaceAll("https://github.com/Aircoookie/WLED", repoUrl); + html = html.replaceAll("https://github.com/wled-dev/WLED", repoUrl); } let version = packageJson.version; if (version) { @@ -128,21 +128,26 @@ async function minify(str, type = "plain") { async function writeHtmlGzipped(sourceFile, resultFile, page) { console.info("Reading " + sourceFile); - new inliner(sourceFile, async function (error, html) { - if (error) throw error; + inline.html({ + fileContent: fs.readFileSync(sourceFile, "utf8"), + relativeTo: path.dirname(sourceFile), + strict: true, + }, + async function (error, html) { + if (error) throw error; - html = adoptVersionAndRepo(html); - const originalLength = html.length; - html = await minify(html, "html-minify"); - const result = zlib.gzipSync(html, { level: zlib.constants.Z_BEST_COMPRESSION }); - console.info("Minified and compressed " + sourceFile + " from " + originalLength + " to " + result.length + " bytes"); - const array = hexdump(result); - let src = singleHeader; - src += `const uint16_t PAGE_${page}_L = ${result.length};\n`; - src += `const uint8_t PAGE_${page}[] PROGMEM = {\n${array}\n};\n\n`; - console.info("Writing " + resultFile); - fs.writeFileSync(resultFile, src); - }); + html = adoptVersionAndRepo(html); + const originalLength = html.length; + html = await minify(html, "html-minify"); + const result = zlib.gzipSync(html, { level: zlib.constants.Z_BEST_COMPRESSION }); + console.info("Minified and compressed " + sourceFile + " from " + originalLength + " to " + result.length + " bytes"); + const array = hexdump(result); + let src = singleHeader; + src += `const uint16_t PAGE_${page}_L = ${result.length};\n`; + src += `const uint8_t PAGE_${page}[] PROGMEM = {\n${array}\n};\n\n`; + console.info("Writing " + resultFile); + fs.writeFileSync(resultFile, src); + }); } async function specToChunk(srcDir, s) { diff --git a/tools/stress_test.sh b/tools/stress_test.sh index d7c344c5..d86c5086 100644 --- a/tools/stress_test.sh +++ b/tools/stress_test.sh @@ -27,6 +27,7 @@ read -a JSON_TINY_TARGETS <<< $(replicate "json/nodes") read -a JSON_SMALL_TARGETS <<< $(replicate "json/info") read -a JSON_LARGE_TARGETS <<< $(replicate "json/si") read -a JSON_LARGER_TARGETS <<< $(replicate "json/fxdata") +read -a INDEX_TARGETS <<< $(replicate "") # Expand target URLS to full arguments for curl TARGETS=(${TARGET_STR[@]}) diff --git a/usermods/ADS1115_v2/usermod_ads1115.h b/usermods/ADS1115_v2/ADS1115_v2.cpp similarity index 98% rename from usermods/ADS1115_v2/usermod_ads1115.h rename to usermods/ADS1115_v2/ADS1115_v2.cpp index 5e2b4b27..bbf457f4 100644 --- a/usermods/ADS1115_v2/usermod_ads1115.h +++ b/usermods/ADS1115_v2/ADS1115_v2.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" #include #include @@ -252,4 +250,7 @@ class ADS1115Usermod : public Usermod { int16_t results = ads.getLastConversionResults(); readings[activeChannel] = ads.computeVolts(results); } -}; \ No newline at end of file +}; + +static ADS1115Usermod ads1115_v2; +REGISTER_USERMOD(ads1115_v2); \ No newline at end of file diff --git a/usermods/ADS1115_v2/library.json b/usermods/ADS1115_v2/library.json new file mode 100644 index 00000000..5e5d7e45 --- /dev/null +++ b/usermods/ADS1115_v2/library.json @@ -0,0 +1,8 @@ +{ + "name": "ADS1115_v2", + "build": { "libArchive": false }, + "dependencies": { + "Adafruit BusIO": "https://github.com/adafruit/Adafruit_BusIO#1.13.2", + "Adafruit ADS1X15": "https://github.com/adafruit/Adafruit_ADS1X15#2.4.0" + } +} diff --git a/usermods/ADS1115_v2/readme.md b/usermods/ADS1115_v2/readme.md index 44092bc8..7397fc2e 100644 --- a/usermods/ADS1115_v2/readme.md +++ b/usermods/ADS1115_v2/readme.md @@ -6,5 +6,5 @@ Configuration is performed via the Usermod menu. There are no parameters to set ## Installation -Add the build flag `-D USERMOD_ADS1115` to your platformio environment. -Uncomment libraries with comment `#For ADS1115 sensor uncomment following` +Add 'ADS1115' to `custom_usermods` in your platformio environment. + diff --git a/usermods/AHT10_v2/usermod_aht10.h b/usermods/AHT10_v2/AHT10_v2.cpp similarity index 98% rename from usermods/AHT10_v2/usermod_aht10.h rename to usermods/AHT10_v2/AHT10_v2.cpp index b5dc1841..f88bee1d 100644 --- a/usermods/AHT10_v2/usermod_aht10.h +++ b/usermods/AHT10_v2/AHT10_v2.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" #include @@ -54,12 +52,6 @@ private: _lastTemperature = 0; } - ~UsermodAHT10() - { - delete _aht; - _aht = nullptr; - } - #ifndef WLED_DISABLE_MQTT void mqttInitialize() { @@ -322,6 +314,15 @@ public: _initDone = true; return configComplete; } + + ~UsermodAHT10() + { + delete _aht; + _aht = nullptr; + } }; -const char UsermodAHT10::_name[] PROGMEM = "AHTxx"; \ No newline at end of file +const char UsermodAHT10::_name[] PROGMEM = "AHTxx"; + +static UsermodAHT10 aht10_v2; +REGISTER_USERMOD(aht10_v2); \ No newline at end of file diff --git a/usermods/AHT10_v2/README.md b/usermods/AHT10_v2/README.md index 69fab467..d84c1c6a 100644 --- a/usermods/AHT10_v2/README.md +++ b/usermods/AHT10_v2/README.md @@ -22,15 +22,9 @@ Dependencies, These must be added under `lib_deps` in your `platform.ini` (or `p # Compiling -To enable, compile with `USERMOD_AHT10` defined (e.g. in `platformio_override.ini`) +To enable, add 'AHT10' to `custom_usermods` in your platformio encrionment (e.g. in `platformio_override.ini`) ```ini [env:aht10_example] extends = env:esp32dev -build_flags = - ${common.build_flags} ${esp32.build_flags} - -D USERMOD_AHT10 - ; -D USERMOD_AHT10_DEBUG ; -- add a debug status to the info modal -lib_deps = - ${esp32.lib_deps} - enjoyneering/AHT10@~1.1.0 +custom_usermods = ${env:esp32dev.custom_usermods} AHT10 ``` diff --git a/usermods/AHT10_v2/library.json b/usermods/AHT10_v2/library.json new file mode 100644 index 00000000..fa6c2a6f --- /dev/null +++ b/usermods/AHT10_v2/library.json @@ -0,0 +1,7 @@ +{ + "name": "AHT10_v2", + "build": { "libArchive": false }, + "dependencies": { + "enjoyneering/AHT10":"~1.1.0" + } +} diff --git a/usermods/AHT10_v2/platformio_override.ini b/usermods/AHT10_v2/platformio_override.ini index 30240f22..74dcd659 100644 --- a/usermods/AHT10_v2/platformio_override.ini +++ b/usermods/AHT10_v2/platformio_override.ini @@ -2,8 +2,4 @@ extends = env:esp32dev build_flags = ${common.build_flags} ${esp32.build_flags} - -D USERMOD_AHT10 ; -D USERMOD_AHT10_DEBUG ; -- add a debug status to the info modal -lib_deps = - ${esp32.lib_deps} - enjoyneering/AHT10@~1.1.0 \ No newline at end of file diff --git a/usermods/Analog_Clock/Analog_Clock.h b/usermods/Analog_Clock/Analog_Clock.cpp similarity index 96% rename from usermods/Analog_Clock/Analog_Clock.h rename to usermods/Analog_Clock/Analog_Clock.cpp index 9d82f767..d3a2b73b 100644 --- a/usermods/Analog_Clock/Analog_Clock.h +++ b/usermods/Analog_Clock/Analog_Clock.cpp @@ -1,4 +1,3 @@ -#pragma once #include "wled.h" /* @@ -103,9 +102,9 @@ private: void secondsEffectSineFade(int16_t secondLed, Toki::Time const& time) { uint32_t ms = time.ms % 1000; uint8_t b0 = (cos8_t(ms * 64 / 1000) - 128) * 2; - setPixelColor(secondLed, gamma32(scale32(secondColor, b0))); + setPixelColor(secondLed, scale32(secondColor, b0)); uint8_t b1 = (sin8_t(ms * 64 / 1000) - 128) * 2; - setPixelColor(inc(secondLed, 1, secondsSegment), gamma32(scale32(secondColor, b1))); + setPixelColor(inc(secondLed, 1, secondsSegment), scale32(secondColor, b1)); } static inline uint32_t qadd32(uint32_t c1, uint32_t c2) { @@ -192,7 +191,7 @@ public: // for (uint16_t i = 1; i < secondsTrail + 1; ++i) { // uint16_t trailLed = dec(secondLed, i, secondsSegment); // uint8_t trailBright = 255 / (secondsTrail + 1) * (secondsTrail - i + 1); - // setPixelColor(trailLed, gamma32(scale32(secondColor, trailBright))); + // setPixelColor(trailLed, scale32(secondColor, trailBright)); // } } @@ -254,3 +253,7 @@ public: return USERMOD_ID_ANALOG_CLOCK; } }; + + +static AnalogClockUsermod analog_clock; +REGISTER_USERMOD(analog_clock); \ No newline at end of file diff --git a/usermods/Analog_Clock/library.json b/usermods/Analog_Clock/library.json new file mode 100644 index 00000000..f76cf426 --- /dev/null +++ b/usermods/Analog_Clock/library.json @@ -0,0 +1,4 @@ +{ + "name": "Analog_Clock", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/Animated_Staircase/Animated_Staircase.h b/usermods/Animated_Staircase/Animated_Staircase.cpp similarity index 99% rename from usermods/Animated_Staircase/Animated_Staircase.h rename to usermods/Animated_Staircase/Animated_Staircase.cpp index 54a9b333..2d2d27cf 100644 --- a/usermods/Animated_Staircase/Animated_Staircase.h +++ b/usermods/Animated_Staircase/Animated_Staircase.cpp @@ -7,7 +7,6 @@ * * See the accompanying README.md file for more info. */ -#pragma once #include "wled.h" class Animated_Staircase : public Usermod { @@ -562,3 +561,7 @@ const char Animated_Staircase::_bottomEcho_pin[] PROGMEM = "bottomEch const char Animated_Staircase::_topEchoCm[] PROGMEM = "top-dist-cm"; const char Animated_Staircase::_bottomEchoCm[] PROGMEM = "bottom-dist-cm"; const char Animated_Staircase::_togglePower[] PROGMEM = "toggle-on-off"; + + +static Animated_Staircase animated_staircase; +REGISTER_USERMOD(animated_staircase); \ No newline at end of file diff --git a/usermods/Animated_Staircase/README.md b/usermods/Animated_Staircase/README.md index 2ad66b5a..263ac806 100644 --- a/usermods/Animated_Staircase/README.md +++ b/usermods/Animated_Staircase/README.md @@ -1,4 +1,5 @@ # Usermod Animated Staircase + This usermod makes your staircase look cool by illuminating it with an animation. It uses PIR or ultrasonic sensors at the top and bottom of your stairs to: @@ -11,14 +12,15 @@ The Animated Staircase can be controlled by the WLED API. Change settings such a speed, on/off time and distance by sending an HTTP request, see below. ## WLED integration + To include this usermod in your WLED setup, you have to be able to [compile WLED from source](https://kno.wled.ge/advanced/compiling-wled/). Before compiling, you have to make the following modifications: -Edit `usermods_list.cpp`: -1. Open `wled00/usermods_list.cpp` -2. add `#include "../usermods/Animated_Staircase/Animated_Staircase.h"` to the top of the file -3. add `UsermodManager::add(new Animated_Staircase());` to the end of the `void registerUsermods()` function. +Edit your environment in `platformio_override.ini` + +1. Open `platformio_override.ini` +2. add `Animated_Staircase` to the `custom_usermods` line for your environment You can configure usermod using the Usermods settings page. Please enter GPIO pins for PIR or ultrasonic sensors (trigger and echo). @@ -26,10 +28,10 @@ If you use PIR sensor enter -1 for echo pin. Maximum distance for ultrasonic sensor can be configured as the time needed for an echo (see below). ## Hardware installation + 1. Attach the LED strip to each step of the stairs. 2. Connect the ESP8266 pin D4 or ESP32 pin D2 to the first LED data pin at the bottom step. -3. Connect the data-out pin at the end of each strip per step to the data-in pin on the - next step, creating one large virtual LED strip. +3. Connect the data-out pin at the end of each strip per step to the data-in pin on the next step, creating one large virtual LED strip. 4. Mount sensors of choice at the bottom and top of the stairs and connect them to the ESP. 5. To make sure all LEDs get enough power and have your staircase lighted evenly, power each step from one side, using at least AWG14 or 2.5mm^2 cable. Don't connect them serial as you @@ -38,24 +40,23 @@ Maximum distance for ultrasonic sensor can be configured as the time needed for You _may_ need to use 10k pull-down resistors on the selected PIR pins, depending on the sensor. ## WLED configuration -1. In the WLED UI, configure a segment for each step. The lowest step of the stairs is the - lowest segment id. -2. Save your segments into a preset. -3. Ideally, add the preset in the config > LED setup menu to the "apply - preset **n** at boot" setting. + +1. In the WLED UI, configure a segment for each step. The lowest step of the stairs is the lowest segment id. +2. Save your segments into a preset. +3. Ideally, add the preset in the config > LED setup menu to the "apply preset **n** at boot" setting. ## Changing behavior through API + The Staircase settings can be changed through the WLED JSON api. **NOTE:** We are using [curl](https://curl.se/) to send HTTP POSTs to the WLED API. If you're using Windows and want to use the curl commands, replace the `\` with a `^` or remove them and put everything on one line. - | Setting | Description | Default | |------------------|---------------------------------------------------------------|---------| | enabled | Enable or disable the usermod | true | -| bottom-sensor | Manually trigger a down to up animation via API | false | +| bottom-sensor | Manually trigger a down to up animation via API | false | | top-sensor | Manually trigger an up to down animation via API | false | @@ -75,6 +76,7 @@ The staircase settings and sensor states are inside the WLED "state" element: ``` ### Enable/disable the usermod + By disabling the usermod you will be able to keep the LED's on, independent from the sensor activity. This enables you to play with the lights without the usermod switching them on or off. @@ -91,6 +93,7 @@ To enable the usermod again, use `"enabled":true`. Alternatively you can use _Usermod_ Settings page where you can change other parameters as well. ### Changing animation parameters and detection range of the ultrasonic HC-SR04 sensor + Using _Usermod_ Settings page you can define different usermod parameters, including sensor pins, delay between segment activation etc. When an ultrasonic sensor is enabled you can enter maximum detection distance in centimeters separately for top and bottom sensors. @@ -100,6 +103,7 @@ distances creates delays in the WLED software, _might_ introduce timing hiccups a less responsive web interface. It is therefore advised to keep the detection distance as short as possible. ### Animation triggering through the API + In addition to activation by one of the stair sensors, you can also trigger the animation manually via the API. To simulate triggering the bottom sensor, use: @@ -116,15 +120,19 @@ curl -X POST -H "Content-Type: application/json" \ -d '{"staircase":{"top-sensor":true}}' \ xxx.xxx.xxx.xxx/json/state ``` + **MQTT** You can publish a message with either `up` or `down` on topic `/swipe` to trigger animation. You can also use `on` or `off` for enabling or disabling the usermod. -Have fun with this usermod.
-www.rolfje.com +Have fun with this usermod + +`www.rolfje.com` Modifications @blazoncek ## Change log + 2021-04 -* Adaptation for runtime configuration. + +- Adaptation for runtime configuration. diff --git a/usermods/Animated_Staircase/library.json b/usermods/Animated_Staircase/library.json new file mode 100644 index 00000000..015b15ce --- /dev/null +++ b/usermods/Animated_Staircase/library.json @@ -0,0 +1,4 @@ +{ + "name": "Animated_Staircase", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/BH1750_v2/BH1750_v2.cpp b/usermods/BH1750_v2/BH1750_v2.cpp new file mode 100644 index 00000000..3800e915 --- /dev/null +++ b/usermods/BH1750_v2/BH1750_v2.cpp @@ -0,0 +1,186 @@ +// force the compiler to show a warning to confirm that this file is included +#warning **** Included USERMOD_BH1750 **** + +#include "wled.h" +#include "BH1750_v2.h" + +#ifdef WLED_DISABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + +static bool checkBoundSensor(float newValue, float prevValue, float maxDiff) +{ + return isnan(prevValue) || newValue <= prevValue - maxDiff || newValue >= prevValue + maxDiff || (newValue == 0.0 && prevValue > 0.0); +} + +void Usermod_BH1750::_mqttInitialize() +{ + mqttLuminanceTopic = String(mqttDeviceTopic) + F("/brightness"); + + if (HomeAssistantDiscovery) _createMqttSensor(F("Brightness"), mqttLuminanceTopic, F("Illuminance"), F(" lx")); +} + +// Create an MQTT Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop. +void Usermod_BH1750::_createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement) +{ + String t = String(F("homeassistant/sensor/")) + mqttClientID + F("/") + name + F("/config"); + + StaticJsonDocument<600> doc; + + doc[F("name")] = String(serverDescription) + " " + name; + doc[F("state_topic")] = topic; + doc[F("unique_id")] = String(mqttClientID) + name; + if (unitOfMeasurement != "") + doc[F("unit_of_measurement")] = unitOfMeasurement; + if (deviceClass != "") + doc[F("device_class")] = deviceClass; + doc[F("expire_after")] = 1800; + + JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device + device[F("name")] = serverDescription; + device[F("identifiers")] = "wled-sensor-" + String(mqttClientID); + device[F("manufacturer")] = F(WLED_BRAND); + device[F("model")] = F(WLED_PRODUCT_NAME); + device[F("sw_version")] = versionString; + + String temp; + serializeJson(doc, temp); + DEBUG_PRINTLN(t); + DEBUG_PRINTLN(temp); + + mqtt->publish(t.c_str(), 0, true, temp.c_str()); +} + +void Usermod_BH1750::setup() +{ + if (i2c_scl<0 || i2c_sda<0) { enabled = false; return; } + sensorFound = lightMeter.begin(); + initDone = true; +} + +void Usermod_BH1750::loop() +{ + if ((!enabled) || strip.isUpdating()) + return; + + unsigned long now = millis(); + + // check to see if we are due for taking a measurement + // lastMeasurement will not be updated until the conversion + // is complete the the reading is finished + if (now - lastMeasurement < minReadingInterval) + { + return; + } + + bool shouldUpdate = now - lastSend > maxReadingInterval; + + float lux = lightMeter.readLightLevel(); + lastMeasurement = millis(); + getLuminanceComplete = true; + + if (shouldUpdate || checkBoundSensor(lux, lastLux, offset)) + { + lastLux = lux; + lastSend = millis(); + + if (WLED_MQTT_CONNECTED) + { + if (!mqttInitialized) + { + _mqttInitialize(); + mqttInitialized = true; + } + mqtt->publish(mqttLuminanceTopic.c_str(), 0, true, String(lux).c_str()); + DEBUG_PRINTLN(F("Brightness: ") + String(lux) + F("lx")); + } + else + { + DEBUG_PRINTLN(F("Missing MQTT connection. Not publishing data")); + } + } +} + + +void Usermod_BH1750::addToJsonInfo(JsonObject &root) +{ + JsonObject user = root[F("u")]; + if (user.isNull()) + user = root.createNestedObject(F("u")); + + JsonArray lux_json = user.createNestedArray(F("Luminance")); + if (!enabled) { + lux_json.add(F("disabled")); + } else if (!sensorFound) { + // if no sensor + lux_json.add(F("BH1750 ")); + lux_json.add(F("Not Found")); + } else if (!getLuminanceComplete) { + // if we haven't read the sensor yet, let the user know + // that we are still waiting for the first measurement + lux_json.add((USERMOD_BH1750_FIRST_MEASUREMENT_AT - millis()) / 1000); + lux_json.add(F(" sec until read")); + return; + } else { + lux_json.add(lastLux); + lux_json.add(F(" lx")); + } +} + +// (called from set.cpp) stores persistent properties to cfg.json +void Usermod_BH1750::addToConfig(JsonObject &root) +{ + // we add JSON object. + JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname + top[FPSTR(_enabled)] = enabled; + top[FPSTR(_maxReadInterval)] = maxReadingInterval; + top[FPSTR(_minReadInterval)] = minReadingInterval; + top[FPSTR(_HomeAssistantDiscovery)] = HomeAssistantDiscovery; + top[FPSTR(_offset)] = offset; + + DEBUG_PRINTLN(F("BH1750 config saved.")); +} + +// called before setup() to populate properties from values stored in cfg.json +bool Usermod_BH1750::readFromConfig(JsonObject &root) +{ + // we look for JSON object. + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) + { + DEBUG_PRINT(FPSTR(_name)); + DEBUG_PRINT(F("BH1750")); + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + bool configComplete = !top.isNull(); + + configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled, false); + configComplete &= getJsonValue(top[FPSTR(_maxReadInterval)], maxReadingInterval, 10000); //ms + configComplete &= getJsonValue(top[FPSTR(_minReadInterval)], minReadingInterval, 500); //ms + configComplete &= getJsonValue(top[FPSTR(_HomeAssistantDiscovery)], HomeAssistantDiscovery, false); + configComplete &= getJsonValue(top[FPSTR(_offset)], offset, 1); + + DEBUG_PRINT(FPSTR(_name)); + if (!initDone) { + DEBUG_PRINTLN(F(" config loaded.")); + } else { + DEBUG_PRINTLN(F(" config (re)loaded.")); + } + + return configComplete; + +} + + +// strings to reduce flash memory usage (used more than twice) +const char Usermod_BH1750::_name[] PROGMEM = "BH1750"; +const char Usermod_BH1750::_enabled[] PROGMEM = "enabled"; +const char Usermod_BH1750::_maxReadInterval[] PROGMEM = "max-read-interval-ms"; +const char Usermod_BH1750::_minReadInterval[] PROGMEM = "min-read-interval-ms"; +const char Usermod_BH1750::_HomeAssistantDiscovery[] PROGMEM = "HomeAssistantDiscoveryLux"; +const char Usermod_BH1750::_offset[] PROGMEM = "offset-lx"; + + +static Usermod_BH1750 bh1750_v2; +REGISTER_USERMOD(bh1750_v2); \ No newline at end of file diff --git a/usermods/BH1750_v2/BH1750_v2.h b/usermods/BH1750_v2/BH1750_v2.h new file mode 100644 index 00000000..22f51ce9 --- /dev/null +++ b/usermods/BH1750_v2/BH1750_v2.h @@ -0,0 +1,92 @@ + +#pragma once +#include "wled.h" +#include + +#ifdef WLED_DISABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + +// the max frequency to check photoresistor, 10 seconds +#ifndef USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL +#define USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL 10000 +#endif + +// the min frequency to check photoresistor, 500 ms +#ifndef USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL +#define USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL 500 +#endif + +// how many seconds after boot to take first measurement, 10 seconds +#ifndef USERMOD_BH1750_FIRST_MEASUREMENT_AT +#define USERMOD_BH1750_FIRST_MEASUREMENT_AT 10000 +#endif + +// only report if difference grater than offset value +#ifndef USERMOD_BH1750_OFFSET_VALUE +#define USERMOD_BH1750_OFFSET_VALUE 1 +#endif + +class Usermod_BH1750 : public Usermod +{ +private: + int8_t offset = USERMOD_BH1750_OFFSET_VALUE; + + unsigned long maxReadingInterval = USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL; + unsigned long minReadingInterval = USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL; + unsigned long lastMeasurement = UINT32_MAX - (USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL - USERMOD_BH1750_FIRST_MEASUREMENT_AT); + unsigned long lastSend = UINT32_MAX - (USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL - USERMOD_BH1750_FIRST_MEASUREMENT_AT); + // flag to indicate we have finished the first readLightLevel call + // allows this library to report to the user how long until the first + // measurement + bool getLuminanceComplete = false; + + // flag set at startup + bool enabled = true; + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _maxReadInterval[]; + static const char _minReadInterval[]; + static const char _offset[]; + static const char _HomeAssistantDiscovery[]; + + bool initDone = false; + bool sensorFound = false; + + // Home Assistant and MQTT + String mqttLuminanceTopic; + bool mqttInitialized = false; + bool HomeAssistantDiscovery = true; // Publish Home Assistant Discovery messages + + BH1750 lightMeter; + float lastLux = -1000; + + // set up Home Assistant discovery entries + void _mqttInitialize(); + + // Create an MQTT Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop. + void _createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement); + +public: + void setup(); + void loop(); + inline float getIlluminance() { + return (float)lastLux; + } + + void addToJsonInfo(JsonObject &root); + + // (called from set.cpp) stores persistent properties to cfg.json + void addToConfig(JsonObject &root); + + // called before setup() to populate properties from values stored in cfg.json + bool readFromConfig(JsonObject &root); + + inline uint16_t getId() + { + return USERMOD_ID_BH1750; + } + +}; diff --git a/usermods/BH1750_v2/library.json b/usermods/BH1750_v2/library.json new file mode 100644 index 00000000..4e32099b --- /dev/null +++ b/usermods/BH1750_v2/library.json @@ -0,0 +1,7 @@ +{ + "name": "BH1750_v2", + "build": { "libArchive": false }, + "dependencies": { + "claws/BH1750":"^1.2.0" + } +} diff --git a/usermods/BH1750_v2/readme.md b/usermods/BH1750_v2/readme.md index 6e6c693d..9f599107 100644 --- a/usermods/BH1750_v2/readme.md +++ b/usermods/BH1750_v2/readme.md @@ -4,43 +4,40 @@ This usermod will read from an ambient light sensor like the BH1750. The luminance is displayed in both the Info section of the web UI, as well as published to the `/luminance` MQTT topic if enabled. ## Dependencies + - Libraries - `claws/BH1750 @^1.2.0` - - This must be added under `lib_deps` in your `platformio.ini` (or `platformio_override.ini`). - Data is published over MQTT - make sure you've enabled the MQTT sync interface. ## Compilation -To enable, compile with `USERMOD_BH1750` defined (e.g. in `platformio_override.ini`) -```ini -[env:usermod_BH1750_d1_mini] -extends = env:d1_mini -build_flags = - ${common.build_flags_esp8266} - -D USERMOD_BH1750 -lib_deps = - ${esp8266.lib_deps} - claws/BH1750 @ ^1.2.0 -``` +To enable, compile with `BH1750` in `custom_usermods` (e.g. in `platformio_override.ini`) ### Configuration Options + The following settings can be set at compile-time but are configurable on the usermod menu (except First Measurement time): -* `USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL` - the max number of milliseconds between measurements, defaults to 10000ms -* `USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL` - the min number of milliseconds between measurements, defaults to 500ms -* `USERMOD_BH1750_OFFSET_VALUE` - the offset value to report on, defaults to 1 -* `USERMOD_BH1750_FIRST_MEASUREMENT_AT` - the number of milliseconds after boot to take first measurement, defaults to 10000 ms + +- `USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL` - the max number of milliseconds between measurements, defaults to 10000ms +- `USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL` - the min number of milliseconds between measurements, defaults to 500ms +- `USERMOD_BH1750_OFFSET_VALUE` - the offset value to report on, defaults to 1 +- `USERMOD_BH1750_FIRST_MEASUREMENT_AT` - the number of milliseconds after boot to take first measurement, defaults to 10000 ms In addition, the Usermod screen allows you to: + - enable/disable the usermod - Enable Home Assistant Discovery of usermod - Configure the SCL/SDA pins ## API + The following method is available to interact with the usermod from other code modules: + - `getIlluminance` read the brightness from the sensor ## Change Log + Jul 2022 + - Added Home Assistant Discovery - Implemented PinManager to register pins - Made pins configurable in usermod menu diff --git a/usermods/BH1750_v2/usermod_bh1750.h b/usermods/BH1750_v2/usermod_bh1750.h deleted file mode 100644 index 2a2bd463..00000000 --- a/usermods/BH1750_v2/usermod_bh1750.h +++ /dev/null @@ -1,252 +0,0 @@ -// force the compiler to show a warning to confirm that this file is included -#warning **** Included USERMOD_BH1750 **** - -#ifndef WLED_ENABLE_MQTT -#error "This user mod requires MQTT to be enabled." -#endif - -#pragma once - -#include "wled.h" -#include - -// the max frequency to check photoresistor, 10 seconds -#ifndef USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL -#define USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL 10000 -#endif - -// the min frequency to check photoresistor, 500 ms -#ifndef USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL -#define USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL 500 -#endif - -// how many seconds after boot to take first measurement, 10 seconds -#ifndef USERMOD_BH1750_FIRST_MEASUREMENT_AT -#define USERMOD_BH1750_FIRST_MEASUREMENT_AT 10000 -#endif - -// only report if difference grater than offset value -#ifndef USERMOD_BH1750_OFFSET_VALUE -#define USERMOD_BH1750_OFFSET_VALUE 1 -#endif - -class Usermod_BH1750 : public Usermod -{ -private: - int8_t offset = USERMOD_BH1750_OFFSET_VALUE; - - unsigned long maxReadingInterval = USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL; - unsigned long minReadingInterval = USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL; - unsigned long lastMeasurement = UINT32_MAX - (USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL - USERMOD_BH1750_FIRST_MEASUREMENT_AT); - unsigned long lastSend = UINT32_MAX - (USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL - USERMOD_BH1750_FIRST_MEASUREMENT_AT); - // flag to indicate we have finished the first readLightLevel call - // allows this library to report to the user how long until the first - // measurement - bool getLuminanceComplete = false; - - // flag set at startup - bool enabled = true; - - // strings to reduce flash memory usage (used more than twice) - static const char _name[]; - static const char _enabled[]; - static const char _maxReadInterval[]; - static const char _minReadInterval[]; - static const char _offset[]; - static const char _HomeAssistantDiscovery[]; - - bool initDone = false; - bool sensorFound = false; - - // Home Assistant and MQTT - String mqttLuminanceTopic; - bool mqttInitialized = false; - bool HomeAssistantDiscovery = true; // Publish Home Assistant Discovery messages - - BH1750 lightMeter; - float lastLux = -1000; - - bool checkBoundSensor(float newValue, float prevValue, float maxDiff) - { - return isnan(prevValue) || newValue <= prevValue - maxDiff || newValue >= prevValue + maxDiff || (newValue == 0.0 && prevValue > 0.0); - } - - // set up Home Assistant discovery entries - void _mqttInitialize() - { - mqttLuminanceTopic = String(mqttDeviceTopic) + F("/brightness"); - - if (HomeAssistantDiscovery) _createMqttSensor(F("Brightness"), mqttLuminanceTopic, F("Illuminance"), F(" lx")); - } - - // Create an MQTT Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop. - void _createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement) - { - String t = String(F("homeassistant/sensor/")) + mqttClientID + F("/") + name + F("/config"); - - StaticJsonDocument<600> doc; - - doc[F("name")] = String(serverDescription) + " " + name; - doc[F("state_topic")] = topic; - doc[F("unique_id")] = String(mqttClientID) + name; - if (unitOfMeasurement != "") - doc[F("unit_of_measurement")] = unitOfMeasurement; - if (deviceClass != "") - doc[F("device_class")] = deviceClass; - doc[F("expire_after")] = 1800; - - JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device - device[F("name")] = serverDescription; - device[F("identifiers")] = "wled-sensor-" + String(mqttClientID); - device[F("manufacturer")] = F(WLED_BRAND); - device[F("model")] = F(WLED_PRODUCT_NAME); - device[F("sw_version")] = versionString; - - String temp; - serializeJson(doc, temp); - DEBUG_PRINTLN(t); - DEBUG_PRINTLN(temp); - - mqtt->publish(t.c_str(), 0, true, temp.c_str()); - } - -public: - void setup() - { - if (i2c_scl<0 || i2c_sda<0) { enabled = false; return; } - sensorFound = lightMeter.begin(); - initDone = true; - } - - void loop() - { - if ((!enabled) || strip.isUpdating()) - return; - - unsigned long now = millis(); - - // check to see if we are due for taking a measurement - // lastMeasurement will not be updated until the conversion - // is complete the the reading is finished - if (now - lastMeasurement < minReadingInterval) - { - return; - } - - bool shouldUpdate = now - lastSend > maxReadingInterval; - - float lux = lightMeter.readLightLevel(); - lastMeasurement = millis(); - getLuminanceComplete = true; - - if (shouldUpdate || checkBoundSensor(lux, lastLux, offset)) - { - lastLux = lux; - lastSend = millis(); -#ifndef WLED_DISABLE_MQTT - if (WLED_MQTT_CONNECTED) - { - if (!mqttInitialized) - { - _mqttInitialize(); - mqttInitialized = true; - } - mqtt->publish(mqttLuminanceTopic.c_str(), 0, true, String(lux).c_str()); - DEBUG_PRINTLN(F("Brightness: ") + String(lux) + F("lx")); - } - else - { - DEBUG_PRINTLN(F("Missing MQTT connection. Not publishing data")); - } -#endif - } - } - - inline float getIlluminance() { - return (float)lastLux; - } - - void addToJsonInfo(JsonObject &root) - { - JsonObject user = root[F("u")]; - if (user.isNull()) - user = root.createNestedObject(F("u")); - - JsonArray lux_json = user.createNestedArray(F("Luminance")); - if (!enabled) { - lux_json.add(F("disabled")); - } else if (!sensorFound) { - // if no sensor - lux_json.add(F("BH1750 ")); - lux_json.add(F("Not Found")); - } else if (!getLuminanceComplete) { - // if we haven't read the sensor yet, let the user know - // that we are still waiting for the first measurement - lux_json.add((USERMOD_BH1750_FIRST_MEASUREMENT_AT - millis()) / 1000); - lux_json.add(F(" sec until read")); - return; - } else { - lux_json.add(lastLux); - lux_json.add(F(" lx")); - } - } - - // (called from set.cpp) stores persistent properties to cfg.json - void addToConfig(JsonObject &root) - { - // we add JSON object. - JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname - top[FPSTR(_enabled)] = enabled; - top[FPSTR(_maxReadInterval)] = maxReadingInterval; - top[FPSTR(_minReadInterval)] = minReadingInterval; - top[FPSTR(_HomeAssistantDiscovery)] = HomeAssistantDiscovery; - top[FPSTR(_offset)] = offset; - - DEBUG_PRINTLN(F("BH1750 config saved.")); - } - - // called before setup() to populate properties from values stored in cfg.json - bool readFromConfig(JsonObject &root) - { - // we look for JSON object. - JsonObject top = root[FPSTR(_name)]; - if (top.isNull()) - { - DEBUG_PRINT(FPSTR(_name)); - DEBUG_PRINT(F("BH1750")); - DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); - return false; - } - bool configComplete = !top.isNull(); - - configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled, false); - configComplete &= getJsonValue(top[FPSTR(_maxReadInterval)], maxReadingInterval, 10000); //ms - configComplete &= getJsonValue(top[FPSTR(_minReadInterval)], minReadingInterval, 500); //ms - configComplete &= getJsonValue(top[FPSTR(_HomeAssistantDiscovery)], HomeAssistantDiscovery, false); - configComplete &= getJsonValue(top[FPSTR(_offset)], offset, 1); - - DEBUG_PRINT(FPSTR(_name)); - if (!initDone) { - DEBUG_PRINTLN(F(" config loaded.")); - } else { - DEBUG_PRINTLN(F(" config (re)loaded.")); - } - - return configComplete; - - } - - uint16_t getId() - { - return USERMOD_ID_BH1750; - } - -}; - -// strings to reduce flash memory usage (used more than twice) -const char Usermod_BH1750::_name[] PROGMEM = "BH1750"; -const char Usermod_BH1750::_enabled[] PROGMEM = "enabled"; -const char Usermod_BH1750::_maxReadInterval[] PROGMEM = "max-read-interval-ms"; -const char Usermod_BH1750::_minReadInterval[] PROGMEM = "min-read-interval-ms"; -const char Usermod_BH1750::_HomeAssistantDiscovery[] PROGMEM = "HomeAssistantDiscoveryLux"; -const char Usermod_BH1750::_offset[] PROGMEM = "offset-lx"; diff --git a/usermods/BME280_v2/usermod_bme280.h b/usermods/BME280_v2/BME280_v2.cpp similarity index 96% rename from usermods/BME280_v2/usermod_bme280.h rename to usermods/BME280_v2/BME280_v2.cpp index 9168f422..dd585904 100644 --- a/usermods/BME280_v2/usermod_bme280.h +++ b/usermods/BME280_v2/BME280_v2.cpp @@ -1,17 +1,15 @@ // force the compiler to show a warning to confirm that this file is included #warning **** Included USERMOD_BME280 version 2.0 **** -#ifndef WLED_ENABLE_MQTT -#error "This user mod requires MQTT to be enabled." -#endif - -#pragma once - #include "wled.h" #include #include // BME280 sensor #include // BME280 extended measurements +#ifdef WLED_DISABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + class UsermodBME280 : public Usermod { private: @@ -241,7 +239,7 @@ public: // from the UI and values read from sensor, then publish to broker if (temperature != lastTemperature || PublishAlways) { - publishMqtt("temperature", String(temperature, TemperatureDecimals).c_str()); + publishMqtt("temperature", String(temperature, (unsigned) TemperatureDecimals).c_str()); } lastTemperature = temperature; // Update last sensor temperature for next loop @@ -254,17 +252,17 @@ public: if (humidity != lastHumidity || PublishAlways) { - publishMqtt("humidity", String(humidity, HumidityDecimals).c_str()); + publishMqtt("humidity", String(humidity, (unsigned) HumidityDecimals).c_str()); } if (heatIndex != lastHeatIndex || PublishAlways) { - publishMqtt("heat_index", String(heatIndex, TemperatureDecimals).c_str()); + publishMqtt("heat_index", String(heatIndex, (unsigned) TemperatureDecimals).c_str()); } if (dewPoint != lastDewPoint || PublishAlways) { - publishMqtt("dew_point", String(dewPoint, TemperatureDecimals).c_str()); + publishMqtt("dew_point", String(dewPoint, (unsigned) TemperatureDecimals).c_str()); } lastHumidity = humidity; @@ -281,7 +279,7 @@ public: if (pressure != lastPressure || PublishAlways) { - publishMqtt("pressure", String(pressure, PressureDecimals).c_str()); + publishMqtt("pressure", String(pressure, (unsigned) PressureDecimals).c_str()); } lastPressure = pressure; @@ -479,3 +477,7 @@ public: const char UsermodBME280::_name[] PROGMEM = "BME280/BMP280"; const char UsermodBME280::_enabled[] PROGMEM = "enabled"; + + +static UsermodBME280 bme280_v2; +REGISTER_USERMOD(bme280_v2); \ No newline at end of file diff --git a/usermods/BME280_v2/README.md b/usermods/BME280_v2/README.md index a4fc229a..0daea582 100644 --- a/usermods/BME280_v2/README.md +++ b/usermods/BME280_v2/README.md @@ -22,7 +22,6 @@ Dependencies - Libraries - `BME280@~3.0.0` (by [finitespace](https://github.com/finitespace/BME280)) - `Wire` - - These must be added under `lib_deps` in your `platform.ini` (or `platform_override.ini`). - Data is published over MQTT - make sure you've enabled the MQTT sync interface. - This usermod also writes to serial (GPIO1 on ESP8266). Please make sure nothing else is listening to the serial TX pin or your board will get confused by log messages! @@ -40,17 +39,11 @@ Methods also exist to read the read/calculated values from other WLED modules th # Compiling -To enable, compile with `USERMOD_BME280` defined (e.g. in `platformio_override.ini`) +To enable, add `BME280_v2` to your `custom_usermods` (e.g. in `platformio_override.ini`) ```ini [env:usermod_bme280_d1_mini] extends = env:d1_mini -build_flags = - ${common.build_flags_esp8266} - -D USERMOD_BME280 -lib_deps = - ${esp8266.lib_deps} - BME280@~3.0.0 - Wire +custom_usermods = ${env:d1_mini.custom_usermods} BME280_v2 ``` diff --git a/usermods/BME280_v2/library.json b/usermods/BME280_v2/library.json new file mode 100644 index 00000000..cfdfe1ba --- /dev/null +++ b/usermods/BME280_v2/library.json @@ -0,0 +1,7 @@ +{ + "name": "BME280_v2", + "build": { "libArchive": false }, + "dependencies": { + "finitespace/BME280":"~3.0.0" + } +} \ No newline at end of file diff --git a/usermods/BME68X_v2/BME68X_v2.cpp b/usermods/BME68X_v2/BME68X_v2.cpp new file mode 100644 index 00000000..63bada5a --- /dev/null +++ b/usermods/BME68X_v2/BME68X_v2.cpp @@ -0,0 +1,1114 @@ +/** + * @file usermod_BMW68X.cpp + * @author Gabriel A. Sieben (GeoGab) + * @brief Usermod for WLED to implement the BME680/BME688 sensor + * @version 1.0.2 + * @date 28 March 2025 + */ + + #define UMOD_DEVICE "ESP32" // NOTE - Set your hardware here + #define HARDWARE_VERSION "1.0" // NOTE - Set your hardware version here + #define UMOD_BME680X_SW_VERSION "1.0.2" // NOTE - Version of the User Mod + #define CALIB_FILE_NAME "/BME680X-Calib.hex" // NOTE - Calibration file name + #define UMOD_NAME "BME680X" // NOTE - User module name + #define UMOD_DEBUG_NAME "UM-BME680X: " // NOTE - Debug print module name addon + + #define ESC "\033" + #define ESC_CSI ESC "[" + #define ESC_STYLE_RESET ESC_CSI "0m" + #define ESC_CURSOR_COLUMN(n) ESC_CSI #n "G" + + #define ESC_FGCOLOR_BLACK ESC_CSI "30m" + #define ESC_FGCOLOR_RED ESC_CSI "31m" + #define ESC_FGCOLOR_GREEN ESC_CSI "32m" + #define ESC_FGCOLOR_YELLOW ESC_CSI "33m" + #define ESC_FGCOLOR_BLUE ESC_CSI "34m" + #define ESC_FGCOLOR_MAGENTA ESC_CSI "35m" + #define ESC_FGCOLOR_CYAN ESC_CSI "36m" + #define ESC_FGCOLOR_WHITE ESC_CSI "37m" + #define ESC_FGCOLOR_DEFAULT ESC_CSI "39m" + + /* Debug Print Special Text */ + #define INFO_COLUMN ESC_CURSOR_COLUMN(60) + #define GOGAB_OK INFO_COLUMN "[" ESC_FGCOLOR_GREEN "OK" ESC_STYLE_RESET "]" + #define GOGAB_FAIL INFO_COLUMN "[" ESC_FGCOLOR_RED "FAIL" ESC_STYLE_RESET "]" + #define GOGAB_WARN INFO_COLUMN "[" ESC_FGCOLOR_YELLOW "WARN" ESC_STYLE_RESET "]" + #define GOGAB_DONE INFO_COLUMN "[" ESC_FGCOLOR_CYAN "DONE" ESC_STYLE_RESET "]" + + #include "bsec.h" // Bosch sensor library + #include "wled.h" + #include + + /* UsermodBME68X class definition */ + class UsermodBME68X : public Usermod { + + public: + /* Public: Functions */ + uint16_t getId(); + void loop(); // Loop of the user module called by wled main in loop + void setup(); // Setup of the user module called by wled main + void addToConfig(JsonObject& root); // Extends the settings/user module settings page to include the user module requirements. The settings are written from the wled core to the configuration file. + void appendConfigData(); // Adds extra info to the config page of weld + bool readFromConfig(JsonObject& root); // Reads config values + void addToJsonInfo(JsonObject& root); // Adds user module info to the weld info page + + /* Wled internal functions which can be used by the core or other user mods */ + inline float getTemperature(); // Get Temperature in the selected scale of °C or °F + inline float getHumidity(); // ... + inline float getPressure(); + inline float getGasResistance(); + inline float getAbsoluteHumidity(); + inline float getDewPoint(); + inline float getIaq(); + inline float getStaticIaq(); + inline float getCo2(); + inline float getVoc(); + inline float getGasPerc(); + inline uint8_t getIaqAccuracy(); + inline uint8_t getStaticIaqAccuracy(); + inline uint8_t getCo2Accuracy(); + inline uint8_t getVocAccuracy(); + inline uint8_t getGasPercAccuracy(); + inline bool getStabStatus(); + inline bool getRunInStatus(); + + private: + /* Private: Functions */ + void HomeAssistantDiscovery(); + void MQTT_PublishHASensor(const String& name, const String& deviceClass, const String& unitOfMeasurement, const int8_t& digs, const uint8_t& option = 0); + void MQTT_publish(const char* topic, const float& value, const int8_t& dig); + void onMqttConnect(bool sessionPresent); + void checkIaqSensorStatus(); + void InfoHelper(JsonObject& root, const char* name, const float& sensorvalue, const int8_t& decimals, const char* unit); + void InfoHelper(JsonObject& root, const char* name, const String& sensorvalue, const bool& status); + void loadState(); + void saveState(); + void getValues(); + + /*** V A R I A B L E s & C O N S T A N T s ***/ + /* Private: Settings of Usermod BME68X */ + struct settings_t { + bool enabled; // true if user module is active + byte I2cadress; // Depending on the manufacturer, the BME680 has the address 0x76 or 0x77 + uint8_t Interval; // Interval of reading sensor data in seconds + uint16_t MaxAge; // Force the publication of the value of a sensor after these defined seconds at the latest + bool pubAcc; // Publish the accuracy values + bool publishSensorState; // Publisch the sensor calibration state + bool publishAfterCalibration ; // The IAQ/CO2/VOC/GAS value are only valid after the sensor has been calibrated. If this switch is active, the values are only sent after calibration + bool PublischChange; // Publish values even when they have not changed + bool PublishIAQVerbal; // Publish Index of Air Quality (IAQ) classification Verbal + bool PublishStaticIAQVerbal; // Publish Static Index of Air Quality (Static IAQ) Verbal + byte tempScale; // 0 -> Use Celsius, 1-> Use Fahrenheit + float tempOffset; // Temperature Offset + bool HomeAssistantDiscovery; // Publish Home Assistant Device Information + bool pauseOnActiveWled ; // If this is set to true, the user mod ist not executed while wled is active + + /* Decimal Places (-1 means inactive) */ + struct decimals_t { + int8_t temperature; + int8_t humidity; + int8_t pressure; + int8_t gasResistance; + int8_t absHumidity; + int8_t drewPoint; + int8_t iaq; + int8_t staticIaq; + int8_t co2; + int8_t Voc; + int8_t gasPerc; + } decimals; + } settings; + + /* Private: Flags */ + struct flags_t { + bool InitSuccessful = false; // Initialation was un-/successful + bool MqttInitialized = false; // MQTT Initialation done flag (first MQTT Connect) + bool SaveState = false; // Save the calibration data flag + bool DeleteCaibration = false; // If set the calib file will be deleted on the next round + } flags; + + /* Private: Measurement timers */ + struct timer_t { + long actual; // Actual time stamp + long lastRun; // Last measurement time stamp + } timer; + + /* Private: Various variables */ + String stringbuff; // General string stringbuff buffer + char charbuffer[128]; // General char stringbuff buffer + String InfoPageStatusLine; // Shown on the info page of WLED + String tempScale; // °C or °F + uint8_t bsecState[BSEC_MAX_STATE_BLOB_SIZE]; // Calibration data array + uint16_t stateUpdateCounter; // Save state couter + static const uint8_t bsec_config_iaq[]; // Calibration Buffer + Bsec iaqSensor; // Sensor variable + + /* Private: Sensor values */ + struct values_t { + float temperature; // Temp [°C] (Sensor-compensated) + float humidity; // Relative humidity [%] (Sensor-compensated) + float pressure; // raw pressure [hPa] + float gasResistance; // raw gas restistance [Ohm] + float absHumidity; // UserMod calculated: Absolute Humidity [g/m³] + float drewPoint; // UserMod calculated: drew point [°C/°F] + float iaq; // IAQ (Indoor Air Quallity) + float staticIaq; // Satic IAQ + float co2; // CO2 [PPM] + float Voc; // VOC in [PPM] + float gasPerc; // Gas Percentage in [%] + uint8_t iaqAccuracy; // IAQ accuracy - IAQ Accuracy = 1 means value is inaccurate, IAQ Accuracy = 2 means sensor is being calibrated, IAQ Accuracy = 3 means sensor successfully calibrated. + uint8_t staticIaqAccuracy; // Static IAQ accuracy + uint8_t co2Accuracy; // co2 accuracy + uint8_t VocAccuracy; // voc accuracy + uint8_t gasPercAccuracy; // Gas percentage accuracy + bool stabStatus; // Indicates if the sensor is undergoing initial stabilization during its first use after production + bool runInStatus; // Indicates when the sensor is ready after after switch-on + } valuesA, valuesB, *ValuesPtr, *PrevValuesPtr, *swap; // Data Scructur A, Data Structur B, Pointers to switch between data channel A & B + + struct cvalues_t { + String iaqVerbal; // IAQ verbal + String staticIaqVerbal; // Static IAQ verbal + + } cvalues; + + /* Private: Sensor settings */ + bsec_virtual_sensor_t sensorList[13] = { + BSEC_OUTPUT_IAQ, // Index for Air Quality estimate [0-500] Index for Air Quality (IAQ) gives an indication of the relative change in ambient TVOCs detected by BME680. + BSEC_OUTPUT_STATIC_IAQ, // Unscaled Index for Air Quality estimate + BSEC_OUTPUT_CO2_EQUIVALENT, // CO2 equivalent estimate [ppm] + BSEC_OUTPUT_BREATH_VOC_EQUIVALENT, // Breath VOC concentration estimate [ppm] + BSEC_OUTPUT_RAW_TEMPERATURE, // Temperature sensor signal [degrees Celsius] Temperature directly measured by BME680 in degree Celsius. This value is cross-influenced by the sensor heating and device specific heating. + BSEC_OUTPUT_RAW_PRESSURE, // Pressure sensor signal [Pa] Pressure directly measured by the BME680 in Pa. + BSEC_OUTPUT_RAW_HUMIDITY, // Relative humidity sensor signal [%] Relative humidity directly measured by the BME680 in %. This value is cross-influenced by the sensor heating and device specific heating. + BSEC_OUTPUT_RAW_GAS, // Gas sensor signal [Ohm] Gas resistance measured directly by the BME680 in Ohm.The resistance value changes due to varying VOC concentrations (the higher the concentration of reducing VOCs, the lower the resistance and vice versa). + BSEC_OUTPUT_STABILIZATION_STATUS, // Gas sensor stabilization status [boolean] Indicates initial stabilization status of the gas sensor element: stabilization is ongoing (0) or stabilization is finished (1). + BSEC_OUTPUT_RUN_IN_STATUS, // Gas sensor run-in status [boolean] Indicates power-on stabilization status of the gas sensor element: stabilization is ongoing (0) or stabilization is finished (1) + BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE, // Sensor heat compensated temperature [degrees Celsius] Temperature measured by BME680 which is compensated for the influence of sensor (heater) in degree Celsius. The self heating introduced by the heater is depending on the sensor operation mode and the sensor supply voltage. + BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY, // Sensor heat compensated humidity [%] Relative measured by BME680 which is compensated for the influence of sensor (heater) in %. It converts the ::BSEC_INPUT_HUMIDITY from temperature ::BSEC_INPUT_TEMPERATURE to temperature ::BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE. + BSEC_OUTPUT_GAS_PERCENTAGE // Percentage of min and max filtered gas value [%] + }; + + /*** V A R I A B L E s & C O N S T A N T s ***/ + /* Public: strings to reduce flash memory usage (used more than twice) */ + static const char _enabled[]; + static const char _hadtopic[]; + + /* Public: Settings Strings*/ + static const char _nameI2CAdr[]; + static const char _nameInterval[]; + static const char _nameMaxAge[]; + static const char _namePubAc[]; + static const char _namePubSenState[]; + static const char _namePubAfterCalib[]; + static const char _namePublishChange[]; + static const char _nameTempScale[]; + static const char _nameTempOffset[]; + static const char _nameHADisc[]; + static const char _nameDelCalib[]; + + /* Public: Sensor names / Sensor short names */ + static const char _nameTemp[]; + static const char _nameHum[]; + static const char _namePress[]; + static const char _nameGasRes[]; + static const char _nameAHum[]; + static const char _nameDrewP[]; + static const char _nameIaq[]; + static const char _nameIaqAc[]; + static const char _nameIaqVerb[]; + static const char _nameStaticIaq[]; + static const char _nameStaticIaqVerb[]; + static const char _nameStaticIaqAc[]; + static const char _nameCo2[]; + static const char _nameCo2Ac[]; + static const char _nameVoc[]; + static const char _nameVocAc[]; + static const char _nameComGasAc[]; + static const char _nameGasPer[]; + static const char _nameGasPerAc[]; + static const char _namePauseOnActWL[]; + + static const char _nameStabStatus[]; + static const char _nameRunInStatus[]; + + /* Public: Sensor Units */ + static const char _unitTemp[]; + static const char _unitHum[]; + static const char _unitPress[]; + static const char _unitGasres[]; + static const char _unitAHum[]; + static const char _unitDrewp[]; + static const char _unitIaq[]; + static const char _unitStaticIaq[]; + static const char _unitCo2[]; + static const char _unitVoc[]; + static const char _unitGasPer[]; + static const char _unitNone[]; + + static const char _unitCelsius[]; + static const char _unitFahrenheit[]; + }; // UsermodBME68X class definition End + + /*** Setting C O N S T A N T S ***/ + /* Private: Settings Strings*/ + const char UsermodBME68X::_enabled[] PROGMEM = "Enabled"; + const char UsermodBME68X::_hadtopic[] PROGMEM = "homeassistant/sensor/"; + + const char UsermodBME68X::_nameI2CAdr[] PROGMEM = "i2C Address"; + const char UsermodBME68X::_nameInterval[] PROGMEM = "Interval"; + const char UsermodBME68X::_nameMaxAge[] PROGMEM = "Max Age"; + const char UsermodBME68X::_namePublishChange[] PROGMEM = "Pub changes only"; + const char UsermodBME68X::_namePubAc[] PROGMEM = "Pub Accuracy"; + const char UsermodBME68X::_namePubSenState[] PROGMEM = "Pub Calib State"; + const char UsermodBME68X::_namePubAfterCalib[] PROGMEM = "Pub After Calib"; + const char UsermodBME68X::_nameTempScale[] PROGMEM = "Temp Scale"; + const char UsermodBME68X::_nameTempOffset[] PROGMEM = "Temp Offset"; + const char UsermodBME68X::_nameHADisc[] PROGMEM = "HA Discovery"; + const char UsermodBME68X::_nameDelCalib[] PROGMEM = "Del Calibration Hist"; + const char UsermodBME68X::_namePauseOnActWL[] PROGMEM = "Pause while WLED active"; + + /* Private: Sensor names / Sensor short name */ + const char UsermodBME68X::_nameTemp[] PROGMEM = "Temperature"; + const char UsermodBME68X::_nameHum[] PROGMEM = "Humidity"; + const char UsermodBME68X::_namePress[] PROGMEM = "Pressure"; + const char UsermodBME68X::_nameGasRes[] PROGMEM = "Gas-Resistance"; + const char UsermodBME68X::_nameAHum[] PROGMEM = "Absolute-Humidity"; + const char UsermodBME68X::_nameDrewP[] PROGMEM = "Drew-Point"; + const char UsermodBME68X::_nameIaq[] PROGMEM = "IAQ"; + const char UsermodBME68X::_nameIaqVerb[] PROGMEM = "IAQ-Verbal"; + const char UsermodBME68X::_nameStaticIaq[] PROGMEM = "Static-IAQ"; + const char UsermodBME68X::_nameStaticIaqVerb[] PROGMEM = "Static-IAQ-Verbal"; + const char UsermodBME68X::_nameCo2[] PROGMEM = "CO2"; + const char UsermodBME68X::_nameVoc[] PROGMEM = "VOC"; + const char UsermodBME68X::_nameGasPer[] PROGMEM = "Gas-Percentage"; + const char UsermodBME68X::_nameIaqAc[] PROGMEM = "IAQ-Accuracy"; + const char UsermodBME68X::_nameStaticIaqAc[] PROGMEM = "Static-IAQ-Accuracy"; + const char UsermodBME68X::_nameCo2Ac[] PROGMEM = "CO2-Accuracy"; + const char UsermodBME68X::_nameVocAc[] PROGMEM = "VOC-Accuracy"; + const char UsermodBME68X::_nameGasPerAc[] PROGMEM = "Gas-Percentage-Accuracy"; + const char UsermodBME68X::_nameStabStatus[] PROGMEM = "Stab-Status"; + const char UsermodBME68X::_nameRunInStatus[] PROGMEM = "Run-In-Status"; + + /* Private Units */ + const char UsermodBME68X::_unitTemp[] PROGMEM = " "; // NOTE - Is set with the selectable temperature unit + const char UsermodBME68X::_unitHum[] PROGMEM = "%"; + const char UsermodBME68X::_unitPress[] PROGMEM = "hPa"; + const char UsermodBME68X::_unitGasres[] PROGMEM = "kΩ"; + const char UsermodBME68X::_unitAHum[] PROGMEM = "g/m³"; + const char UsermodBME68X::_unitDrewp[] PROGMEM = " "; // NOTE - Is set with the selectable temperature unit + const char UsermodBME68X::_unitIaq[] PROGMEM = " "; // No unit + const char UsermodBME68X::_unitStaticIaq[] PROGMEM = " "; // No unit + const char UsermodBME68X::_unitCo2[] PROGMEM = "ppm"; + const char UsermodBME68X::_unitVoc[] PROGMEM = "ppm"; + const char UsermodBME68X::_unitGasPer[] PROGMEM = "%"; + const char UsermodBME68X::_unitNone[] PROGMEM = ""; + + const char UsermodBME68X::_unitCelsius[] PROGMEM = "°C"; // Symbol for Celsius + const char UsermodBME68X::_unitFahrenheit[] PROGMEM = "°F"; // Symbol for Fahrenheit + + /* Load Sensor Settings */ + const uint8_t UsermodBME68X::bsec_config_iaq[] = { + #include "config/generic_33v_3s_28d/bsec_iaq.txt" // Allow 28 days for calibration because the WLED module normally stays in the same place anyway + }; + + + /************************************************************************************************************/ + /********************************************* M A I N C O D E *********************************************/ + /************************************************************************************************************/ + + /** + * @brief Called by WLED: Setup of the usermod + */ + void UsermodBME68X::setup() { + DEBUG_PRINTLN(F(UMOD_DEBUG_NAME ESC_FGCOLOR_CYAN "Initialize" ESC_STYLE_RESET)); + + /* Check, if i2c is activated */ + if (i2c_scl < 0 || i2c_sda < 0) { + settings.enabled = false; // Disable usermod once i2c is not running + DEBUG_PRINTLN(F(UMOD_DEBUG_NAME "I2C is not activated. Please activate I2C first." GOGAB_FAIL)); + return; + } + + flags.InitSuccessful = true; // Will be set to false on need + + /* Set data structure pointers */ + ValuesPtr = &valuesA; + PrevValuesPtr = &valuesB; + + /* Init Library*/ + iaqSensor.begin(settings.I2cadress, Wire); // BME68X_I2C_ADDR_LOW + stringbuff = "BSEC library version " + String(iaqSensor.version.major) + "." + String(iaqSensor.version.minor) + "." + String(iaqSensor.version.major_bugfix) + "." + String(iaqSensor.version.minor_bugfix); + DEBUG_PRINT(F(UMOD_NAME)); + DEBUG_PRINTLN(F(stringbuff.c_str())); + + /* Init Sensor*/ + iaqSensor.setConfig(bsec_config_iaq); + iaqSensor.updateSubscription(sensorList, 13, BSEC_SAMPLE_RATE_LP); + iaqSensor.setTPH(BME68X_OS_2X, BME68X_OS_16X, BME68X_OS_1X); // Set the temperature, Pressure and Humidity over-sampling + iaqSensor.setTemperatureOffset(settings.tempOffset); // set the temperature offset in degree Celsius + loadState(); // Load the old calibration data + checkIaqSensorStatus(); // Check the sensor status + // HomeAssistantDiscovery(); + DEBUG_PRINTLN(F(INFO_COLUMN GOGAB_DONE)); + } + + /** + * @brief Called by WLED: Main loop called by WLED + * + */ + void UsermodBME68X::loop() { + if (!settings.enabled || strip.isUpdating() || !flags.InitSuccessful) return; // Leave if not enabled or string is updating or init failed + + if (settings.pauseOnActiveWled && strip.getBrightness()) return; // Workarround Known Issue: handing led update - Leave once pause on activ wled is active and wled is active + + timer.actual = millis(); // Timer to fetch new temperature, humidity and pressure data at intervals + + if (timer.actual - timer.lastRun >= settings.Interval * 1000) { + timer.lastRun = timer.actual; + + /* Get the sonsor measurments and publish them */ + if (iaqSensor.run()) { // iaqSensor.run() + getValues(); // Get the new values + + if (ValuesPtr->temperature != PrevValuesPtr->temperature || !settings.PublischChange) { // NOTE - negative dig means inactive + MQTT_publish(_nameTemp, ValuesPtr->temperature, settings.decimals.temperature); + } + if (ValuesPtr->humidity != PrevValuesPtr->humidity || !settings.PublischChange) { + MQTT_publish(_nameHum, ValuesPtr->humidity, settings.decimals.humidity); + } + if (ValuesPtr->pressure != PrevValuesPtr->pressure || !settings.PublischChange) { + MQTT_publish(_namePress, ValuesPtr->pressure, settings.decimals.humidity); + } + if (ValuesPtr->gasResistance != PrevValuesPtr->gasResistance || !settings.PublischChange) { + MQTT_publish(_nameGasRes, ValuesPtr->gasResistance, settings.decimals.gasResistance); + } + if (ValuesPtr->absHumidity != PrevValuesPtr->absHumidity || !settings.PublischChange) { + MQTT_publish(_nameAHum, PrevValuesPtr->absHumidity, settings.decimals.absHumidity); + } + if (ValuesPtr->drewPoint != PrevValuesPtr->drewPoint || !settings.PublischChange) { + MQTT_publish(_nameDrewP, PrevValuesPtr->drewPoint, settings.decimals.drewPoint); + } + if (ValuesPtr->iaq != PrevValuesPtr->iaq || !settings.PublischChange) { + MQTT_publish(_nameIaq, ValuesPtr->iaq, settings.decimals.iaq); + if (settings.pubAcc) MQTT_publish(_nameIaqAc, ValuesPtr->iaqAccuracy, 0); + if (settings.decimals.iaq>-1) { + if (settings.PublishIAQVerbal) { + if (ValuesPtr->iaq <= 50) cvalues.iaqVerbal = F("Excellent"); + else if (ValuesPtr->iaq <= 100) cvalues.iaqVerbal = F("Good"); + else if (ValuesPtr->iaq <= 150) cvalues.iaqVerbal = F("Lightly polluted"); + else if (ValuesPtr->iaq <= 200) cvalues.iaqVerbal = F("Moderately polluted"); + else if (ValuesPtr->iaq <= 250) cvalues.iaqVerbal = F("Heavily polluted"); + else if (ValuesPtr->iaq <= 350) cvalues.iaqVerbal = F("Severely polluted"); + else cvalues.iaqVerbal = F("Extremely polluted"); + snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, _nameIaqVerb); + if (WLED_MQTT_CONNECTED) mqtt->publish(charbuffer, 0, false, cvalues.iaqVerbal.c_str()); + } + } + } + if (ValuesPtr->staticIaq != PrevValuesPtr->staticIaq || !settings.PublischChange) { + MQTT_publish(_nameStaticIaq, ValuesPtr->staticIaq, settings.decimals.staticIaq); + if (settings.pubAcc) MQTT_publish(_nameStaticIaqAc, ValuesPtr->staticIaqAccuracy, 0); + if (settings.decimals.staticIaq>-1) { + if (settings.PublishIAQVerbal) { + if (ValuesPtr->staticIaq <= 50) cvalues.staticIaqVerbal = F("Excellent"); + else if (ValuesPtr->staticIaq <= 100) cvalues.staticIaqVerbal = F("Good"); + else if (ValuesPtr->staticIaq <= 150) cvalues.staticIaqVerbal = F("Lightly polluted"); + else if (ValuesPtr->staticIaq <= 200) cvalues.staticIaqVerbal = F("Moderately polluted"); + else if (ValuesPtr->staticIaq <= 250) cvalues.staticIaqVerbal = F("Heavily polluted"); + else if (ValuesPtr->staticIaq <= 350) cvalues.staticIaqVerbal = F("Severely polluted"); + else cvalues.staticIaqVerbal = F("Extremely polluted"); + snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, _nameStaticIaqVerb); + if (WLED_MQTT_CONNECTED) mqtt->publish(charbuffer, 0, false, cvalues.staticIaqVerbal.c_str()); + } + } + } + if (ValuesPtr->co2 != PrevValuesPtr->co2 || !settings.PublischChange) { + MQTT_publish(_nameCo2, ValuesPtr->co2, settings.decimals.co2); + if (settings.pubAcc) MQTT_publish(_nameCo2Ac, ValuesPtr->co2Accuracy, 0); + } + if (ValuesPtr->Voc != PrevValuesPtr->Voc || !settings.PublischChange) { + MQTT_publish(_nameVoc, ValuesPtr->Voc, settings.decimals.Voc); + if (settings.pubAcc) MQTT_publish(_nameVocAc, ValuesPtr->VocAccuracy, 0); + } + if (ValuesPtr->gasPerc != PrevValuesPtr->gasPerc || !settings.PublischChange) { + MQTT_publish(_nameGasPer, ValuesPtr->gasPerc, settings.decimals.gasPerc); + if (settings.pubAcc) MQTT_publish(_nameGasPerAc, ValuesPtr->gasPercAccuracy, 0); + } + + /**** Publish Sensor State Entrys *****/ + if ((ValuesPtr->stabStatus != PrevValuesPtr->stabStatus || !settings.PublischChange) && settings.publishSensorState) MQTT_publish(_nameStabStatus, ValuesPtr->stabStatus, 0); + if ((ValuesPtr->runInStatus != PrevValuesPtr->runInStatus || !settings.PublischChange) && settings.publishSensorState) MQTT_publish(_nameRunInStatus, ValuesPtr->runInStatus, 0); + + /* Check accuracies - if accurasy level 3 is reached -> save calibration data */ + if ((ValuesPtr->iaqAccuracy != PrevValuesPtr->iaqAccuracy) && ValuesPtr->iaqAccuracy == 3) flags.SaveState = true; // Save after calibration / recalibration + if ((ValuesPtr->staticIaqAccuracy != PrevValuesPtr->staticIaqAccuracy) && ValuesPtr->staticIaqAccuracy == 3) flags.SaveState = true; + if ((ValuesPtr->co2Accuracy != PrevValuesPtr->co2Accuracy) && ValuesPtr->co2Accuracy == 3) flags.SaveState = true; + if ((ValuesPtr->VocAccuracy != PrevValuesPtr->VocAccuracy) && ValuesPtr->VocAccuracy == 3) flags.SaveState = true; + if ((ValuesPtr->gasPercAccuracy != PrevValuesPtr->gasPercAccuracy) && ValuesPtr->gasPercAccuracy == 3) flags.SaveState = true; + + if (flags.SaveState) saveState(); // Save if the save state flag is set + } + } + } + + /** + * @brief Retrieves the sensor data and truncates it to the requested decimal places + * + */ + void UsermodBME68X::getValues() { + /* Swap the point to the data structures */ + swap = PrevValuesPtr; + PrevValuesPtr = ValuesPtr; + ValuesPtr = swap; + + /* Float Values */ + ValuesPtr->temperature = roundf(iaqSensor.temperature * powf(10, settings.decimals.temperature)) / powf(10, settings.decimals.temperature); + ValuesPtr->humidity = roundf(iaqSensor.humidity * powf(10, settings.decimals.humidity)) / powf(10, settings.decimals.humidity); + ValuesPtr->pressure = roundf(iaqSensor.pressure * powf(10, settings.decimals.pressure)) / powf(10, settings.decimals.pressure) /100; // Pa 2 hPa + ValuesPtr->gasResistance = roundf(iaqSensor.gasResistance * powf(10, settings.decimals.gasResistance)) /powf(10, settings.decimals.gasResistance) /1000; // Ohm 2 KOhm + ValuesPtr->iaq = roundf(iaqSensor.iaq * powf(10, settings.decimals.iaq)) / powf(10, settings.decimals.iaq); + ValuesPtr->staticIaq = roundf(iaqSensor.staticIaq * powf(10, settings.decimals.staticIaq)) / powf(10, settings.decimals.staticIaq); + ValuesPtr->co2 = roundf(iaqSensor.co2Equivalent * powf(10, settings.decimals.co2)) / powf(10, settings.decimals.co2); + ValuesPtr->Voc = roundf(iaqSensor.breathVocEquivalent * powf(10, settings.decimals.Voc)) / powf(10, settings.decimals.Voc); + ValuesPtr->gasPerc = roundf(iaqSensor.gasPercentage * powf(10, settings.decimals.gasPerc)) / powf(10, settings.decimals.gasPerc); + + /* Calculate Absolute Humidity [g/m³] */ + if (settings.decimals.absHumidity>-1) { + const float mw = 18.01534; // molar mass of water g/mol + const float r = 8.31447215; // Universal gas constant J/mol/K + ValuesPtr->absHumidity = (6.112 * powf(2.718281828, (17.67 * ValuesPtr->temperature) / (ValuesPtr->temperature + 243.5)) * ValuesPtr->humidity * mw) / ((273.15 + ValuesPtr->temperature) * r); // in ppm + } + /* Calculate Drew Point (C°) */ + if (settings.decimals.drewPoint>-1) { + ValuesPtr->drewPoint = (243.5 * (log( ValuesPtr->humidity / 100) + ((17.67 * ValuesPtr->temperature) / (243.5 + ValuesPtr->temperature))) / (17.67 - log(ValuesPtr->humidity / 100) - ((17.67 * ValuesPtr->temperature) / (243.5 + ValuesPtr->temperature)))); + } + + /* Convert to Fahrenheit when selected */ + if (settings.tempScale) { // settings.tempScale = 0 => Celsius, = 1 => Fahrenheit + ValuesPtr->temperature = ValuesPtr->temperature * 1.8 + 32; // Value stored in Fahrenheit + ValuesPtr->drewPoint = ValuesPtr->drewPoint * 1.8 + 32; + } + + /* Integer Values */ + ValuesPtr->iaqAccuracy = iaqSensor.iaqAccuracy; + ValuesPtr->staticIaqAccuracy = iaqSensor.staticIaqAccuracy; + ValuesPtr->co2Accuracy = iaqSensor.co2Accuracy; + ValuesPtr->VocAccuracy = iaqSensor.breathVocAccuracy; + ValuesPtr->gasPercAccuracy = iaqSensor.gasPercentageAccuracy; + ValuesPtr->stabStatus = iaqSensor.stabStatus; + ValuesPtr->runInStatus = iaqSensor.runInStatus; + } + + + /** + * @brief Sends the current sensor data via MQTT + * @param topic Suptopic of the sensor as const char + * @param value Current sensor value as float + */ + void UsermodBME68X::MQTT_publish(const char* topic, const float& value, const int8_t& dig) { + if (dig<0) return; + if (WLED_MQTT_CONNECTED) { + snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, topic); + mqtt->publish(charbuffer, 0, false, String(value, dig).c_str()); + } + } + + /** + * @brief Called by WLED: Initialize the MQTT parts when the connection to the MQTT server is established. + * @param bool Session Present + */ + void UsermodBME68X::onMqttConnect(bool sessionPresent) { + DEBUG_PRINTLN(UMOD_DEBUG_NAME "OnMQTTConnect event fired"); + HomeAssistantDiscovery(); + + if (!flags.MqttInitialized) { + flags.MqttInitialized=true; + DEBUG_PRINTLN(UMOD_DEBUG_NAME "MQTT first connect"); + } + } + + + /** + * @brief MQTT initialization to generate the mqtt topic strings. This initialization also creates the HomeAssistat device configuration (HA Discovery), which home assinstant automatically evaluates to create a device. + */ + void UsermodBME68X::HomeAssistantDiscovery() { + if (!settings.HomeAssistantDiscovery || !flags.InitSuccessful || !settings.enabled) return; // Leave once HomeAssistant Discovery is inactive + + DEBUG_PRINTLN(UMOD_DEBUG_NAME ESC_FGCOLOR_CYAN "Creating HomeAssistant Discovery Mqtt-Entrys" ESC_STYLE_RESET); + + /* Sensor Values */ + MQTT_PublishHASensor(_nameTemp, "TEMPERATURE", tempScale.c_str(), settings.decimals.temperature ); // Temperature + MQTT_PublishHASensor(_namePress, "ATMOSPHERIC_PRESSURE", _unitPress, settings.decimals.pressure ); // Pressure + MQTT_PublishHASensor(_nameHum, "HUMIDITY", _unitHum, settings.decimals.humidity ); // Humidity + MQTT_PublishHASensor(_nameGasRes, "GAS", _unitGasres, settings.decimals.gasResistance ); // There is no device class for resistance in HA yet: https://developers.home-assistant.io/docs/core/entity/sensor/ + MQTT_PublishHASensor(_nameAHum, "HUMIDITY", _unitAHum, settings.decimals.absHumidity ); // Absolute Humidity + MQTT_PublishHASensor(_nameDrewP, "TEMPERATURE", tempScale.c_str(), settings.decimals.drewPoint ); // Drew Point + MQTT_PublishHASensor(_nameIaq, "AQI", _unitIaq, settings.decimals.iaq ); // IAQ + MQTT_PublishHASensor(_nameIaqVerb, "", _unitNone, settings.PublishIAQVerbal, 2); // IAQ Verbal / Set Option 2 (text sensor) + MQTT_PublishHASensor(_nameStaticIaq, "AQI", _unitNone, settings.decimals.staticIaq ); // Static IAQ + MQTT_PublishHASensor(_nameStaticIaqVerb, "", _unitNone, settings.PublishStaticIAQVerbal, 2); // IAQ Verbal / Set Option 2 (text sensor + MQTT_PublishHASensor(_nameCo2, "CO2", _unitCo2, settings.decimals.co2 ); // CO2 + MQTT_PublishHASensor(_nameVoc, "VOLATILE_ORGANIC_COMPOUNDS", _unitVoc, settings.decimals.Voc ); // VOC + MQTT_PublishHASensor(_nameGasPer, "AQI", _unitGasPer, settings.decimals.gasPerc ); // Gas % + + /* Accuracys - switched off once publishAccuracy=0 or the main value is switched of by digs set to a negative number */ + MQTT_PublishHASensor(_nameIaqAc, "AQI", _unitNone, settings.pubAcc - 1 + settings.decimals.iaq * settings.pubAcc, 1); // Option 1: Diagnostics Sektion + MQTT_PublishHASensor(_nameStaticIaqAc, "", _unitNone, settings.pubAcc - 1 + settings.decimals.staticIaq * settings.pubAcc, 1); + MQTT_PublishHASensor(_nameCo2Ac, "", _unitNone, settings.pubAcc - 1 + settings.decimals.co2 * settings.pubAcc, 1); + MQTT_PublishHASensor(_nameVocAc, "", _unitNone, settings.pubAcc - 1 + settings.decimals.Voc * settings.pubAcc, 1); + MQTT_PublishHASensor(_nameGasPerAc, "", _unitNone, settings.pubAcc - 1 + settings.decimals.gasPerc * settings.pubAcc, 1); + + MQTT_PublishHASensor(_nameStabStatus, "", _unitNone, settings.publishSensorState - 1, 1); + MQTT_PublishHASensor(_nameRunInStatus, "", _unitNone, settings.publishSensorState - 1, 1); + + DEBUG_PRINTLN(UMOD_DEBUG_NAME GOGAB_DONE); + } + + /** + * @brief These MQTT entries are responsible for the Home Assistant Discovery of the sensors. HA is shown here where to look for the sensor data. This entry therefore only needs to be sent once. + * Important note: In order to find everything that is sent from this device to Home Assistant via MQTT under the same device name, the "device/identifiers" entry must be the same. + * I use the MQTT device name here. If other user mods also use the HA Discovery, it is recommended to set the identifier the same. Otherwise you would have several devices, + * even though it is one device. I therefore only use the MQTT client name set in WLED here. + * @param name Name of the sensor + * @param topic Topic of the live sensor data + * @param unitOfMeasurement Unit of the measurment + * @param digs Number of decimal places + * @param option Set to true if the sensor is part of diagnostics (dafault 0) + */ + void UsermodBME68X::MQTT_PublishHASensor(const String& name, const String& deviceClass, const String& unitOfMeasurement, const int8_t& digs, const uint8_t& option) { + DEBUG_PRINT(UMOD_DEBUG_NAME "\t" + name); + + snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, name.c_str()); // Current values will be posted here + String basetopic = String(_hadtopic) + mqttClientID + F("/") + name + F("/config"); // This is the place where Home Assinstant Discovery will check for new devices + + if (digs < 0) { // if digs are set to -1 -> entry deactivated + /* Delete MQTT Entry */ + if (WLED_MQTT_CONNECTED) { + mqtt->publish(basetopic.c_str(), 0, true, ""); // Send emty entry to delete + DEBUG_PRINTLN(INFO_COLUMN "deleted"); + } + } else { + /* Create all the necessary HAD MQTT entrys - see: https://www.home-assistant.io/integrations/sensor.mqtt/#configuration-variables */ + DynamicJsonDocument jdoc(700); // json document + // See: https://www.home-assistant.io/integrations/mqtt/ + JsonObject avail = jdoc.createNestedObject(F("avty")); // 'avty': 'availability' + avail[F("topic")] = mqttDeviceTopic + String("/status"); // An MQTT topic subscribed to receive availability (online/offline) updates. + avail[F("payload_available")] = "online"; + avail[F("payload_not_available")] = "offline"; + JsonObject device = jdoc.createNestedObject(F("device")); // Information about the device this sensor is a part of to tie it into the device registry. Only works when unique_id is set. At least one of identifiers or connections must be present to identify the device. + device[F("name")] = serverDescription; + device[F("identifiers")] = String(mqttClientID); + device[F("manufacturer")] = F("WLED"); + device[F("model")] = UMOD_DEVICE; + device[F("sw_version")] = versionString; + device[F("hw_version")] = F(HARDWARE_VERSION); + + if (deviceClass != "") jdoc[F("device_class")] = deviceClass; // The type/class of the sensor to set the icon in the frontend. The device_class can be null + if (option == 1) jdoc[F("entity_category")] = "diagnostic"; // Option 1: The category of the entity | When set, the entity category must be diagnostic for sensors. + if (option == 2) jdoc[F("mode")] = "text"; // Option 2: Set text mode | + jdoc[F("expire_after")] = 1800; // If set, it defines the number of seconds after the sensor’s state expires, if it’s not updated. After expiry, the sensor’s state becomes unavailable. Default the sensors state never expires. + jdoc[F("name")] = name; // The name of the MQTT sensor. Without server/module/device name. The device name will be added by HomeAssinstant anyhow + if (unitOfMeasurement != "") jdoc[F("state_class")] = "measurement"; // NOTE: This entry is missing in some other usermods. But it is very important. Because only with this entry, you can use statistics (such as statistical graphs). + jdoc[F("state_topic")] = charbuffer; // The MQTT topic subscribed to receive sensor values. If device_class, state_class, unit_of_measurement or suggested_display_precision is set, and a numeric value is expected, an empty value '' will be ignored and will not update the state, a 'null' value will set the sensor to an unknown state. The device_class can be null. + jdoc[F("unique_id")] = String(mqttClientID) + "-" + name; // An ID that uniquely identifies this sensor. If two sensors have the same unique ID, Home Assistant will raise an exception. + if (unitOfMeasurement != "") jdoc[F("unit_of_measurement")] = unitOfMeasurement; // Defines the units of measurement of the sensor, if any. The unit_of_measurement can be null. + + DEBUG_PRINTF(" (%d bytes)", jdoc.memoryUsage()); + + stringbuff = ""; // clear string buffer + serializeJson(jdoc, stringbuff); // JSON to String + + if (WLED_MQTT_CONNECTED) { // Check if MQTT Connected, otherwise it will crash the 8266 + mqtt->publish(basetopic.c_str(), 0, true, stringbuff.c_str()); // Publish the HA discovery sensor entry + DEBUG_PRINTLN(INFO_COLUMN "published"); + } + } + } + + /** + * @brief Called by WLED: Publish Sensor Information to Info Page + * @param JsonObject Pointer + */ + void UsermodBME68X::addToJsonInfo(JsonObject& root) { + //DEBUG_PRINTLN(F(UMOD_DEBUG_NAME "Add to info event")); + JsonObject user = root[F("u")]; + + if (user.isNull()) + user = root.createNestedObject(F("u")); + + if (!flags.InitSuccessful) { + // Init was not seccessful - let the user know + JsonArray temperature_json = user.createNestedArray(F("BME68X Sensor")); + temperature_json.add(F("not found")); + JsonArray humidity_json = user.createNestedArray(F("BMW68x Reason")); + humidity_json.add(InfoPageStatusLine); + } + else if (!settings.enabled) { + JsonArray temperature_json = user.createNestedArray(F("BME68X Sensor")); + temperature_json.add(F("disabled")); + } + else { + InfoHelper(user, _nameTemp, ValuesPtr->temperature, settings.decimals.temperature, tempScale.c_str()); + InfoHelper(user, _nameHum, ValuesPtr->humidity, settings.decimals.humidity, _unitHum); + InfoHelper(user, _namePress, ValuesPtr->pressure, settings.decimals.pressure, _unitPress); + InfoHelper(user, _nameGasRes, ValuesPtr->gasResistance, settings.decimals.gasResistance, _unitGasres); + InfoHelper(user, _nameAHum, ValuesPtr->absHumidity, settings.decimals.absHumidity, _unitAHum); + InfoHelper(user, _nameDrewP, ValuesPtr->drewPoint, settings.decimals.drewPoint, tempScale.c_str()); + InfoHelper(user, _nameIaq, ValuesPtr->iaq, settings.decimals.iaq, _unitIaq); + InfoHelper(user, _nameIaqVerb, cvalues.iaqVerbal, settings.PublishIAQVerbal); + InfoHelper(user, _nameStaticIaq, ValuesPtr->staticIaq, settings.decimals.staticIaq, _unitStaticIaq); + InfoHelper(user, _nameStaticIaqVerb,cvalues.staticIaqVerbal, settings.PublishStaticIAQVerbal); + InfoHelper(user, _nameCo2, ValuesPtr->co2, settings.decimals.co2, _unitCo2); + InfoHelper(user, _nameVoc, ValuesPtr->Voc, settings.decimals.Voc, _unitVoc); + InfoHelper(user, _nameGasPer, ValuesPtr->gasPerc, settings.decimals.gasPerc, _unitGasPer); + + if (settings.pubAcc) { + if (settings.decimals.iaq >= 0) InfoHelper(user, _nameIaqAc, ValuesPtr->iaqAccuracy, 0, " "); + if (settings.decimals.staticIaq >= 0) InfoHelper(user, _nameStaticIaqAc, ValuesPtr->staticIaqAccuracy, 0, " "); + if (settings.decimals.co2 >= 0) InfoHelper(user, _nameCo2Ac, ValuesPtr->co2Accuracy, 0, " "); + if (settings.decimals.Voc >= 0) InfoHelper(user, _nameVocAc, ValuesPtr->VocAccuracy, 0, " "); + if (settings.decimals.gasPerc >= 0) InfoHelper(user, _nameGasPerAc, ValuesPtr->gasPercAccuracy, 0, " "); + } + + if (settings.publishSensorState) { + InfoHelper(user, _nameStabStatus, ValuesPtr->stabStatus, 0, " "); + InfoHelper(user, _nameRunInStatus, ValuesPtr->runInStatus, 0, " "); + } + } + } + + /** + * @brief Info Page helper function + * @param root JSON object + * @param name Name of the sensor as char + * @param sensorvalue Value of the sensor as float + * @param decimals Decimal places of the value + * @param unit Unit of the sensor + */ + void UsermodBME68X::InfoHelper(JsonObject& root, const char* name, const float& sensorvalue, const int8_t& decimals, const char* unit) { + if (decimals > -1) { + JsonArray sub_json = root.createNestedArray(name); + sub_json.add(roundf(sensorvalue * powf(10, decimals)) / powf(10, decimals)); + sub_json.add(unit); + } + } + + /** + * @brief Info Page helper function (overload) + * @param root JSON object + * @param name Name of the sensor + * @param sensorvalue Value of the sensor as string + * @param status Status of the value (active/inactive) + */ + void UsermodBME68X::InfoHelper(JsonObject& root, const char* name, const String& sensorvalue, const bool& status) { + if (status) { + JsonArray sub_json = root.createNestedArray(name); + sub_json.add(sensorvalue); + } + } + + /** + * @brief Called by WLED: Adds the usermodul neends on the config page for user modules + * @param JsonObject Pointer + * + * @see Usermod::addToConfig() + * @see UsermodManager::addToConfig() + */ + void UsermodBME68X::addToConfig(JsonObject& root) { + DEBUG_PRINT(F(UMOD_DEBUG_NAME "Creating configuration pages content: ")); + + JsonObject top = root.createNestedObject(FPSTR(UMOD_NAME)); + /* general settings */ + top[FPSTR(_enabled)] = settings.enabled; + top[FPSTR(_nameI2CAdr)] = settings.I2cadress; + top[FPSTR(_nameInterval)] = settings.Interval; + top[FPSTR(_namePublishChange)] = settings.PublischChange; + top[FPSTR(_namePubAc)] = settings.pubAcc; + top[FPSTR(_namePubSenState)] = settings.publishSensorState; + top[FPSTR(_nameTempScale)] = settings.tempScale; + top[FPSTR(_nameTempOffset)] = settings.tempOffset; + top[FPSTR(_nameHADisc)] = settings.HomeAssistantDiscovery; + top[FPSTR(_namePauseOnActWL)] = settings.pauseOnActiveWled; + top[FPSTR(_nameDelCalib)] = flags.DeleteCaibration; + + /* Digs */ + JsonObject sensors_json = top.createNestedObject("Sensors"); + sensors_json[FPSTR(_nameTemp)] = settings.decimals.temperature; + sensors_json[FPSTR(_nameHum)] = settings.decimals.humidity; + sensors_json[FPSTR(_namePress)] = settings.decimals.pressure; + sensors_json[FPSTR(_nameGasRes)] = settings.decimals.gasResistance; + sensors_json[FPSTR(_nameAHum)] = settings.decimals.absHumidity; + sensors_json[FPSTR(_nameDrewP)] = settings.decimals.drewPoint; + sensors_json[FPSTR(_nameIaq)] = settings.decimals.iaq; + sensors_json[FPSTR(_nameIaqVerb)] = settings.PublishIAQVerbal; + sensors_json[FPSTR(_nameStaticIaq)] = settings.decimals.staticIaq; + sensors_json[FPSTR(_nameStaticIaqVerb)] = settings.PublishStaticIAQVerbal; + sensors_json[FPSTR(_nameCo2)] = settings.decimals.co2; + sensors_json[FPSTR(_nameVoc)] = settings.decimals.Voc; + sensors_json[FPSTR(_nameGasPer)] = settings.decimals.gasPerc; + + DEBUG_PRINTLN(F(GOGAB_OK)); + } + + /** + * @brief Called by WLED: Add dropdown and additional infos / structure + * @see Usermod::appendConfigData() + * @see UsermodManager::appendConfigData() + */ + void UsermodBME68X::appendConfigData() { + // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'read interval [seconds]');"), UMOD_NAME, _nameInterval); oappend(charbuffer); + // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'only if value changes');"), UMOD_NAME, _namePublishChange); oappend(charbuffer); + // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'maximum age of a message in seconds');"), UMOD_NAME, _nameMaxAge); oappend(charbuffer); + // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'Gas related values are only published after the gas sensor has been calibrated');"), UMOD_NAME, _namePubAfterCalib); oappend(charbuffer); + // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'*) Set to minus to deactivate (all sensors)');"), UMOD_NAME, _nameTemp); oappend(charbuffer); + + /* Dropdown for Celsius/Fahrenheit*/ + oappend(F("dd=addDropdown('")); + oappend(UMOD_NAME); + oappend(F("','")); + oappend(_nameTempScale); + oappend(F("');")); + oappend(F("addOption(dd,'Celsius',0);")); + oappend(F("addOption(dd,'Fahrenheit',1);")); + + /* i²C Address*/ + oappend(F("dd=addDropdown('")); + oappend(UMOD_NAME); + oappend(F("','")); + oappend(_nameI2CAdr); + oappend(F("');")); + oappend(F("addOption(dd,'0x76',0x76);")); + oappend(F("addOption(dd,'0x77',0x77);")); + } + + /** + * @brief Called by WLED: Read Usermod Config Settings default settings values could be set here (or below using the 3-argument getJsonValue()) + * instead of in the class definition or constructor setting them inside readFromConfig() is slightly more robust, handling the rare but + * plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) + * This is called whenever WLED boots and loads cfg.json, or when the UM config + * page is saved. Will properly re-instantiate the SHT class upon type change and + * publish HA discovery after enabling. + * NOTE: Here are the default settings of the user module + * @param JsonObject Pointer + * @return bool + * @see Usermod::readFromConfig() + * @see UsermodManager::readFromConfig() + */ + bool UsermodBME68X::readFromConfig(JsonObject& root) { + DEBUG_PRINT(F(UMOD_DEBUG_NAME "Reading configuration: ")); + + JsonObject top = root[FPSTR(UMOD_NAME)]; + bool configComplete = !top.isNull(); + + /* general settings */ /* DEFAULTS */ + configComplete &= getJsonValue(top[FPSTR(_enabled)], settings.enabled, 1 ); // Usermod enabled per default + configComplete &= getJsonValue(top[FPSTR(_nameI2CAdr)], settings.I2cadress, 0x77 ); // Defalut IC2 adress set to 0x77 (some modules are set to 0x76) + configComplete &= getJsonValue(top[FPSTR(_nameInterval)], settings.Interval, 1 ); // Executed every second + configComplete &= getJsonValue(top[FPSTR(_namePublishChange)], settings.PublischChange, false ); // Publish changed values only + configComplete &= getJsonValue(top[FPSTR(_nameTempScale)], settings.tempScale, 0 ); // Temp sale set to Celsius (1=Fahrenheit) + configComplete &= getJsonValue(top[FPSTR(_nameTempOffset)], settings.tempOffset, 0 ); // Temp offset is set to 0 (Celsius) + configComplete &= getJsonValue(top[FPSTR(_namePubSenState)], settings.publishSensorState, 1 ); // Publish the sensor states + configComplete &= getJsonValue(top[FPSTR(_namePubAc)], settings.pubAcc, 1 ); // Publish accuracy values + configComplete &= getJsonValue(top[FPSTR(_nameHADisc)], settings.HomeAssistantDiscovery, true ); // Activate HomeAssistant Discovery (this Module will be shown as MQTT device in HA) + configComplete &= getJsonValue(top[FPSTR(_namePauseOnActWL)], settings.pauseOnActiveWled, false ); // Pause on active WLED not activated per default + configComplete &= getJsonValue(top[FPSTR(_nameDelCalib)], flags.DeleteCaibration, false ); // IF checked the calibration file will be delete when the save button is pressed + + /* Decimal places */ /* no of digs / -1 means deactivated */ + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameTemp)], settings.decimals.temperature, 1 ); // One decimal places + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameHum)], settings.decimals.humidity, 1 ); + configComplete &= getJsonValue(top["Sensors"][FPSTR(_namePress)], settings.decimals.pressure, 0 ); // Zero decimal places + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameGasRes)], settings.decimals.gasResistance, -1 ); // deavtivated + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameDrewP)], settings.decimals.drewPoint, 1 ); + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameAHum)], settings.decimals.absHumidity, 1 ); + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameIaq)], settings.decimals.iaq, 0 ); // Index for Air Quality Number is active + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameIaqVerb)], settings.PublishIAQVerbal, -1 ); // deactivated - Index for Air Quality (IAQ) verbal classification + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameStaticIaq)], settings.decimals.staticIaq, 0 ); // activated - Static IAQ is better than IAQ for devices that are not moved + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameStaticIaqVerb)], settings.PublishStaticIAQVerbal, 0 ); // activated + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameCo2)], settings.decimals.co2, 0 ); + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameVoc)], settings.decimals.Voc, 0 ); + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameGasPer)], settings.decimals.gasPerc, 0 ); + + DEBUG_PRINTLN(F(GOGAB_OK)); + + /* Set the selected temperature unit */ + if (settings.tempScale) { + tempScale = F(_unitFahrenheit); + } + else { + tempScale = F(_unitCelsius); + } + + if (flags.DeleteCaibration) { + DEBUG_PRINT(F(UMOD_DEBUG_NAME "Deleting Calibration File")); + flags.DeleteCaibration = false; + if (WLED_FS.remove(CALIB_FILE_NAME)) { + DEBUG_PRINTLN(F(GOGAB_OK)); + } + else { + DEBUG_PRINTLN(F(GOGAB_FAIL)); + } + } + + if (settings.Interval < 1) settings.Interval = 1; // Correct interval on need (A number less than 1 is not permitted) + iaqSensor.setTemperatureOffset(settings.tempOffset); // Set Temp Offset + + return configComplete; + } + + /** + * @brief Called by WLED: Retunrs the user modul id number + * + * @return uint16_t User module number + */ + uint16_t UsermodBME68X::getId() { + return USERMOD_ID_BME68X; + } + + + /** + * @brief Returns the current temperature in the scale which is choosen in settings + * @return Temperature value (°C or °F as choosen in settings) + */ + inline float UsermodBME68X::getTemperature() { + return ValuesPtr->temperature; + } + + /** + * @brief Returns the current humidity + * @return Humididty value (%) + */ + inline float UsermodBME68X::getHumidity() { + return ValuesPtr->humidity; + } + + /** + * @brief Returns the current pressure + * @return Pressure value (hPa) + */ + inline float UsermodBME68X::getPressure() { + return ValuesPtr->pressure; + } + + /** + * @brief Returns the current gas resistance + * @return Gas resistance value (kΩ) + */ + inline float UsermodBME68X::getGasResistance() { + return ValuesPtr->gasResistance; + } + + /** + * @brief Returns the current absolute humidity + * @return Absolute humidity value (g/m³) + */ + inline float UsermodBME68X::getAbsoluteHumidity() { + return ValuesPtr->absHumidity; + } + + /** + * @brief Returns the current dew point + * @return Dew point (°C or °F as choosen in settings) + */ + inline float UsermodBME68X::getDewPoint() { + return ValuesPtr->drewPoint; + } + + /** + * @brief Returns the current iaq (Indoor Air Quallity) + * @return Iaq value (0-500) + */ + inline float UsermodBME68X::getIaq() { + return ValuesPtr->iaq; + } + + /** + * @brief Returns the current static iaq (Indoor Air Quallity) (NOTE: Static iaq is the better choice than iaq for fixed devices such as the wled module) + * @return Static iaq value (float) + */ + inline float UsermodBME68X::getStaticIaq() { + return ValuesPtr->staticIaq; + } + + /** + * @brief Returns the current co2 + * @return Co2 value (ppm) + */ + inline float UsermodBME68X::getCo2() { + return ValuesPtr->co2; + } + + /** + * @brief Returns the current voc (Breath VOC concentration estimate [ppm]) + * @return Voc value (ppm) + */ + inline float UsermodBME68X::getVoc() { + return ValuesPtr->Voc; + } + + /** + * @brief Returns the current gas percentage + * @return Gas percentage value (%) + */ + inline float UsermodBME68X::getGasPerc() { + return ValuesPtr->gasPerc; + } + + /** + * @brief Returns the current iaq accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated) + * @return Iaq accuracy value (0-3) + */ + inline uint8_t UsermodBME68X::getIaqAccuracy() { + return ValuesPtr->iaqAccuracy ; + } + + /** + * @brief Returns the current static iaq accuracy accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated) + * @return Static iaq accuracy value (0-3) + */ + inline uint8_t UsermodBME68X::getStaticIaqAccuracy() { + return ValuesPtr->staticIaqAccuracy; + } + + /** + * @brief Returns the current co2 accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated) + * @return Co2 accuracy value (0-3) + */ + inline uint8_t UsermodBME68X::getCo2Accuracy() { + return ValuesPtr->co2Accuracy; + } + + /** + * @brief Returns the current voc accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated) + * @return Voc accuracy value (0-3) + */ + inline uint8_t UsermodBME68X::getVocAccuracy() { + return ValuesPtr->VocAccuracy; + } + + /** + * @brief Returns the current gas percentage accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated) + * @return Gas percentage accuracy value (0-3) + */ + inline uint8_t UsermodBME68X::getGasPercAccuracy() { + return ValuesPtr->gasPercAccuracy; + } + + /** + * @brief Returns the current stab status. + * Indicates when the sensor is ready after after switch-on + * @return stab status value (0 = switched on / 1 = stabilized) + */ + inline bool UsermodBME68X::getStabStatus() { + return ValuesPtr->stabStatus; + } + + /** + * @brief Returns the current run in status. + * Indicates if the sensor is undergoing initial stabilization during its first use after production + * @return Tun status accuracy value (0 = switched on first time / 1 = stabilized) + */ + inline bool UsermodBME68X::getRunInStatus() { + return ValuesPtr->runInStatus; + } + + + /** + * @brief Checks whether the library and the sensor are running. + */ + void UsermodBME68X::checkIaqSensorStatus() { + + if (iaqSensor.bsecStatus != BSEC_OK) { + InfoPageStatusLine = "BSEC Library "; + DEBUG_PRINT(UMOD_DEBUG_NAME + InfoPageStatusLine); + flags.InitSuccessful = false; + if (iaqSensor.bsecStatus < BSEC_OK) { + InfoPageStatusLine += " Error Code : " + String(iaqSensor.bsecStatus); + DEBUG_PRINTLN(GOGAB_FAIL); + } + else { + InfoPageStatusLine += " Warning Code : " + String(iaqSensor.bsecStatus); + DEBUG_PRINTLN(GOGAB_WARN); + } + } + else { + InfoPageStatusLine = "Sensor BME68X "; + DEBUG_PRINT(UMOD_DEBUG_NAME + InfoPageStatusLine); + + if (iaqSensor.bme68xStatus != BME68X_OK) { + flags.InitSuccessful = false; + if (iaqSensor.bme68xStatus < BME68X_OK) { + InfoPageStatusLine += "error code: " + String(iaqSensor.bme68xStatus); + DEBUG_PRINTLN(GOGAB_FAIL); + } + else { + InfoPageStatusLine += "warning code: " + String(iaqSensor.bme68xStatus); + DEBUG_PRINTLN(GOGAB_WARN); + } + } + else { + InfoPageStatusLine += F("OK"); + DEBUG_PRINTLN(GOGAB_OK); + } + } + } + + /** + * @brief Loads the calibration data from the file system of the device + */ + void UsermodBME68X::loadState() { + if (WLED_FS.exists(CALIB_FILE_NAME)) { + DEBUG_PRINT(F(UMOD_DEBUG_NAME "Read the calibration file: ")); + File file = WLED_FS.open(CALIB_FILE_NAME, FILE_READ); + if (!file) { + DEBUG_PRINTLN(GOGAB_FAIL); + } + else { + file.read(bsecState, BSEC_MAX_STATE_BLOB_SIZE); + file.close(); + DEBUG_PRINTLN(GOGAB_OK); + iaqSensor.setState(bsecState); + } + } + else { + DEBUG_PRINTLN(F(UMOD_DEBUG_NAME "Calibration file not found.")); + } + } + + /** + * @brief Saves the calibration data from the file system of the device + */ + void UsermodBME68X::saveState() { + DEBUG_PRINT(F(UMOD_DEBUG_NAME "Write the calibration file ")); + File file = WLED_FS.open(CALIB_FILE_NAME, FILE_WRITE); + if (!file) { + DEBUG_PRINTLN(GOGAB_FAIL); + } + else { + iaqSensor.getState(bsecState); + file.write(bsecState, BSEC_MAX_STATE_BLOB_SIZE); + file.close(); + stateUpdateCounter++; + DEBUG_PRINTF("(saved %d times)" GOGAB_OK "\n", stateUpdateCounter); + flags.SaveState = false; // Clear save state flag + + char contbuffer[30]; + + /* Timestamp */ + time_t curr_time; + tm* curr_tm; + time(&curr_time); + curr_tm = localtime(&curr_time); + + snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, UMOD_NAME "/Calib Last Run"); + strftime(contbuffer, 30, "%d %B %Y - %T", curr_tm); + if (WLED_MQTT_CONNECTED) mqtt->publish(charbuffer, 0, false, contbuffer); + + snprintf(contbuffer, 30, "%d", stateUpdateCounter); + snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, UMOD_NAME "/Calib Count"); + if (WLED_MQTT_CONNECTED) mqtt->publish(charbuffer, 0, false, contbuffer); + } + } + + + static UsermodBME68X bme68x_v2; + REGISTER_USERMOD(bme68x_v2); \ No newline at end of file diff --git a/usermods/BME68X_v2/README.md b/usermods/BME68X_v2/README.md index 72ae25a5..ee2670aa 100644 --- a/usermods/BME68X_v2/README.md +++ b/usermods/BME68X_v2/README.md @@ -1,65 +1,70 @@ # Usermod BME68X -This usermod was developed for a BME680/BME68X sensor. The BME68X is not compatible with the BME280/BMP280 chip. It has its own library. The original 'BSEC Software Library' from Bosch was used to develop the code. The measured values are displayed on the WLED info page. + +This usermod was developed for a BME680/BME68X sensor. The BME68X is not compatible with the BME280/BMP280 chip. It has its own library. The original 'BSEC Software Library' from Bosch was used to develop the code. The measured values are displayed on the WLED info page.

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

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

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

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

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

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

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

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

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

diff --git a/usermods/Cronixie/usermod_cronixie.h b/usermods/Cronixie/Cronixie.cpp similarity index 98% rename from usermods/Cronixie/usermod_cronixie.h rename to usermods/Cronixie/Cronixie.cpp index 671c5d13..e0a3bbee 100644 --- a/usermods/Cronixie/usermod_cronixie.h +++ b/usermods/Cronixie/Cronixie.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" class UsermodCronixie : public Usermod { @@ -249,7 +247,7 @@ class UsermodCronixie : public Usermod { if (backlight && _digitOut[i] <11) { - uint32_t col = gamma32(strip.getSegment(0).colors[1]); + uint32_t col = strip.getSegment(0).colors[1]; for (uint16_t j=o; j< o+10; j++) { if (j != excl) strip.setPixelColor(j, col); } @@ -299,4 +297,7 @@ class UsermodCronixie : public Usermod { { return USERMOD_ID_CRONIXIE; } -}; \ No newline at end of file +}; + +static UsermodCronixie cronixie; +REGISTER_USERMOD(cronixie); \ No newline at end of file diff --git a/usermods/Cronixie/library.json b/usermods/Cronixie/library.json new file mode 100644 index 00000000..4a1b6988 --- /dev/null +++ b/usermods/Cronixie/library.json @@ -0,0 +1,4 @@ +{ + "name": "Cronixie", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/Cronixie/readme.md b/usermods/Cronixie/readme.md index 1eeac8ed..38efdbab 100644 --- a/usermods/Cronixie/readme.md +++ b/usermods/Cronixie/readme.md @@ -4,5 +4,5 @@ This usermod supports driving the Cronixie M and L clock kits by Diamex. ## Installation -Compile and upload after adding `-D USERMOD_CRONIXIE` to `build_flags` of your PlatformIO environment. +Compile and upload after adding `Cronixie` to `custom_usermods` of your PlatformIO environment. Make sure the Auto Brightness Limiter is enabled at 420mA (!) and configure 60 WS281x LEDs. \ No newline at end of file diff --git a/usermods/DHT/usermod_dht.h b/usermods/DHT/DHT.cpp similarity index 98% rename from usermods/DHT/usermod_dht.h rename to usermods/DHT/DHT.cpp index 05a7267b..2ed3dd0a 100644 --- a/usermods/DHT/usermod_dht.h +++ b/usermods/DHT/DHT.cpp @@ -1,7 +1,5 @@ -#pragma once - #include "wled.h" -#ifndef WLED_ENABLE_MQTT +#ifdef WLED_DISABLE_MQTT #error "This user mod requires MQTT to be enabled." #endif @@ -245,3 +243,7 @@ class UsermodDHT : public Usermod { } }; + + +static UsermodDHT dht; +REGISTER_USERMOD(dht); \ No newline at end of file diff --git a/usermods/DHT/library.json b/usermods/DHT/library.json new file mode 100644 index 00000000..7b0dc361 --- /dev/null +++ b/usermods/DHT/library.json @@ -0,0 +1,7 @@ +{ + "name": "DHT", + "build": { "libArchive": false}, + "dependencies": { + "DHT_nonblocking":"https://github.com/alwynallan/DHT_nonblocking" + } +} diff --git a/usermods/DHT/platformio_override.ini b/usermods/DHT/platformio_override.ini index d192f043..6ec2fb99 100644 --- a/usermods/DHT/platformio_override.ini +++ b/usermods/DHT/platformio_override.ini @@ -1,6 +1,5 @@ ; Options ; ------- -; USERMOD_DHT - define this to have this user mod included wled00\usermods_list.cpp ; USERMOD_DHT_DHTTYPE - DHT model: 11, 21, 22 for DHT11, DHT21, or DHT22, defaults to 22/DHT22 ; USERMOD_DHT_PIN - pin to which DTH is connected, defaults to Q2 pin on QuinLed Dig-Uno's board ; USERMOD_DHT_CELSIUS - define this to report temperatures in degrees celsious, otherwise fahrenheit will be reported @@ -11,13 +10,11 @@ [env:d1_mini_usermod_dht_C] extends = env:d1_mini -build_flags = ${env:d1_mini.build_flags} -D USERMOD_DHT -D USERMOD_DHT_CELSIUS -lib_deps = ${env:d1_mini.lib_deps} - https://github.com/alwynallan/DHT_nonblocking +custom_usermods = ${env:d1_mini.custom_usermods} DHT +build_flags = ${env:d1_mini.build_flags} -D USERMOD_DHT_CELSIUS [env:custom32_LEDPIN_16_usermod_dht_C] extends = env:custom32_LEDPIN_16 -build_flags = ${env:custom32_LEDPIN_16.build_flags} -D USERMOD_DHT -D USERMOD_DHT_CELSIUS -D USERMOD_DHT_STATS -lib_deps = ${env.lib_deps} - https://github.com/alwynallan/DHT_nonblocking +custom_usermods = ${env:custom32_LEDPIN_16.custom_usermods} DHT +build_flags = ${env:custom32_LEDPIN_16.build_flags} -D USERMOD_DHT_CELSIUS -D USERMOD_DHT_STATS diff --git a/usermods/DHT/readme.md b/usermods/DHT/readme.md index 6089ffbf..9080b9b2 100644 --- a/usermods/DHT/readme.md +++ b/usermods/DHT/readme.md @@ -15,7 +15,6 @@ Copy the example `platformio_override.ini` to the root directory. This file sho ### Define Your Options -* `USERMOD_DHT` - define this to include this user mod wled00\usermods_list.cpp * `USERMOD_DHT_DHTTYPE` - DHT model: 11, 21, 22 for DHT11, DHT21, or DHT22, defaults to 22/DHT22 * `USERMOD_DHT_PIN` - pin to which DTH is connected, defaults to Q2 pin on QuinLed Dig-Uno's board * `USERMOD_DHT_CELSIUS` - define this to report temperatures in degrees Celsius, otherwise Fahrenheit will be reported diff --git a/usermods/EXAMPLE/library.json b/usermods/EXAMPLE/library.json new file mode 100644 index 00000000..d0dc2f88 --- /dev/null +++ b/usermods/EXAMPLE/library.json @@ -0,0 +1,5 @@ +{ + "name": "EXAMPLE", + "build": { "libArchive": false }, + "dependencies": {} +} diff --git a/usermods/EXAMPLE_v2/readme.md b/usermods/EXAMPLE/readme.md similarity index 64% rename from usermods/EXAMPLE_v2/readme.md rename to usermods/EXAMPLE/readme.md index 8917a1fb..ee8a2282 100644 --- a/usermods/EXAMPLE_v2/readme.md +++ b/usermods/EXAMPLE/readme.md @@ -4,7 +4,6 @@ In this usermod file you can find the documentation on how to take advantage of ## Installation -Copy `usermod_v2_example.h` to the wled00 directory. -Uncomment the corresponding lines in `usermods_list.cpp` and compile! +Add `EXAMPLE` to `custom_usermods` in your PlatformIO environment and compile! _(You shouldn't need to actually install this, it does nothing useful)_ diff --git a/usermods/EXAMPLE_v2/usermod_v2_example.h b/usermods/EXAMPLE/usermod_v2_example.cpp similarity index 99% rename from usermods/EXAMPLE_v2/usermod_v2_example.h rename to usermods/EXAMPLE/usermod_v2_example.cpp index df05f3e3..fc50833e 100644 --- a/usermods/EXAMPLE_v2/usermod_v2_example.h +++ b/usermods/EXAMPLE/usermod_v2_example.cpp @@ -1,10 +1,8 @@ -#pragma once - #include "wled.h" /* * Usermods allow you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality * * This is an example for a v2 usermod. * v2 usermods are class inheritance based and can (but don't have to) implement more functions, each of them is shown in this example. @@ -404,3 +402,6 @@ void MyExampleUsermod::publishMqtt(const char* state, bool retain) } #endif } + +static MyExampleUsermod example_usermod; +REGISTER_USERMOD(example_usermod); diff --git a/usermods/EleksTube_IPS/usermod_elekstube_ips.h b/usermods/EleksTube_IPS/EleksTube_IPS.cpp similarity index 98% rename from usermods/EleksTube_IPS/usermod_elekstube_ips.h rename to usermods/EleksTube_IPS/EleksTube_IPS.cpp index 0f7d92e7..e8637b0f 100644 --- a/usermods/EleksTube_IPS/usermod_elekstube_ips.h +++ b/usermods/EleksTube_IPS/EleksTube_IPS.cpp @@ -1,4 +1,3 @@ -#pragma once #include "TFTs.h" #include "wled.h" @@ -156,3 +155,7 @@ class ElekstubeIPSUsermod : public Usermod { const char ElekstubeIPSUsermod::_name[] PROGMEM = "EleksTubeIPS"; const char ElekstubeIPSUsermod::_tubeSeg[] PROGMEM = "tubeSegment"; const char ElekstubeIPSUsermod::_digitOffset[] PROGMEM = "digitOffset"; + + +static ElekstubeIPSUsermod elekstube_ips; +REGISTER_USERMOD(elekstube_ips); \ No newline at end of file diff --git a/usermods/EleksTube_IPS/library.json.disabled b/usermods/EleksTube_IPS/library.json.disabled new file mode 100644 index 00000000..d143638e --- /dev/null +++ b/usermods/EleksTube_IPS/library.json.disabled @@ -0,0 +1,8 @@ +{ + "name:": "EleksTube_IPS", + "build": { "libArchive": false }, + "dependencies": { + "TFT_eSPI" : "2.5.33" + } +} +# Seems to add 300kb to the RAM requirement??? diff --git a/usermods/Enclosure_with_OLED_temp_ESP07/usermod.cpp b/usermods/Enclosure_with_OLED_temp_ESP07/usermod.cpp index 823ad747..d51ddb57 100644 --- a/usermods/Enclosure_with_OLED_temp_ESP07/usermod.cpp +++ b/usermods/Enclosure_with_OLED_temp_ESP07/usermod.cpp @@ -1,11 +1,12 @@ -#ifndef WLED_ENABLE_MQTT -#error "This user mod requires MQTT to be enabled." -#endif - #include "wled.h" #include #include // from https://github.com/olikraus/u8g2/ #include //Dallastemperature sensor + +#ifdef WLED_DISABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + //The SCL and SDA pins are defined here. //Lolin32 boards use SCL=5 SDA=4 #define U8X8_PIN_SCL 5 diff --git a/usermods/Enclosure_with_OLED_temp_ESP07/usermod_bme280.cpp b/usermods/Enclosure_with_OLED_temp_ESP07/usermod_bme280.cpp index 29a4332d..ce3c659e 100644 --- a/usermods/Enclosure_with_OLED_temp_ESP07/usermod_bme280.cpp +++ b/usermods/Enclosure_with_OLED_temp_ESP07/usermod_bme280.cpp @@ -1,13 +1,13 @@ -#ifndef WLED_ENABLE_MQTT -#error "This user mod requires MQTT to be enabled." -#endif - #include "wled.h" #include #include // from https://github.com/olikraus/u8g2/ #include #include //BME280 sensor +#ifdef WLED_DISABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + void UpdateBME280Data(); #define Celsius // Show temperature measurement in Celsius otherwise is in Fahrenheit diff --git a/usermods/Fix_unreachable_netservices_v2/library.json b/usermods/Fix_unreachable_netservices_v2/library.json new file mode 100644 index 00000000..4d1dbfc8 --- /dev/null +++ b/usermods/Fix_unreachable_netservices_v2/library.json @@ -0,0 +1,4 @@ +{ + "name": "Fix_unreachable_netservices_v2", + "platforms": ["espressif8266"] +} diff --git a/usermods/Fix_unreachable_netservices_v2/readme.md b/usermods/Fix_unreachable_netservices_v2/readme.md index 07d64bc6..9f3889eb 100644 --- a/usermods/Fix_unreachable_netservices_v2/readme.md +++ b/usermods/Fix_unreachable_netservices_v2/readme.md @@ -30,41 +30,6 @@ The usermod supports the following state changes: ## Installation -1. Copy the file `usermod_Fix_unreachable_netservices.h` to the `wled00` directory. -2. Register the usermod by adding `#include "usermod_Fix_unreachable_netservices.h"` in the top and `registerUsermod(new FixUnreachableNetServices());` in the bottom of `usermods_list.cpp`. - -Example **usermods_list.cpp**: - -```cpp -#include "wled.h" -/* - * Register your v2 usermods here! - * (for v1 usermods using just usermod.cpp, you can ignore this file) - */ - -/* - * Add/uncomment your usermod filename here (and once more below) - * || || || - * \/ \/ \/ - */ -//#include "usermod_v2_example.h" -//#include "usermod_temperature.h" -//#include "usermod_v2_empty.h" -#include "usermod_Fix_unreachable_netservices.h" - -void registerUsermods() -{ - /* - * Add your usermod class name here - * || || || - * \/ \/ \/ - */ - //UsermodManager::add(new MyExampleUsermod()); - //UsermodManager::add(new UsermodTemperature()); - //UsermodManager::add(new UsermodRenameMe()); - UsermodManager::add(new FixUnreachableNetServices()); - -} -``` +1. Add `Fix_unreachable_netservices` to `custom_usermods` in your PlatformIO environment. Hopefully I can help someone with that - @gegu diff --git a/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.h b/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.cpp similarity index 96% rename from usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.h rename to usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.cpp index 3d441e59..7fb8e979 100644 --- a/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.h +++ b/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.cpp @@ -1,12 +1,4 @@ -#pragma once - #include "wled.h" -#if defined(ESP32) -#warning "Usermod FixUnreachableNetServices works only with ESP8266 builds" -class FixUnreachableNetServices : public Usermod -{ -}; -#endif #if defined(ESP8266) #include @@ -16,7 +8,7 @@ class FixUnreachableNetServices : public Usermod * By this procedure the net services of WLED remains accessible in some problematic WLAN environments. * * Usermods allow you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality * * v2 usermods are class inheritance based and can (but don't have to) implement more functions, each of them is shown in this example. * Multiple v2 usermods can be added to one compilation easily. @@ -168,4 +160,11 @@ Delay @@ -210,12 +208,6 @@ private: } } - ~UsermodINA226() - { - delete _ina226; - _ina226 = nullptr; - } - #ifndef WLED_DISABLE_MQTT void mqttInitialize() { @@ -551,6 +543,17 @@ public: _initDone = true; return configComplete; } + + ~UsermodINA226() + { + delete _ina226; + _ina226 = nullptr; + } + }; const char UsermodINA226::_name[] PROGMEM = "INA226"; + + +static UsermodINA226 ina226_v2; +REGISTER_USERMOD(ina226_v2); \ No newline at end of file diff --git a/usermods/INA226_v2/README.md b/usermods/INA226_v2/README.md index b1e69161..dfa9fc00 100644 --- a/usermods/INA226_v2/README.md +++ b/usermods/INA226_v2/README.md @@ -22,13 +22,6 @@ The following settings can be configured in the Usermod Menu: - **MqttPublishAlways**: Publish always, regardless if there is a change. - **MqttHomeAssistantDiscovery**: Enable Home Assistant discovery. -## Dependencies - -These must be added under `lib_deps` in your `platform.ini` (or `platform_override.ini`). - -- Libraries - - `wollewald/INA226_WE@~1.2.9` (by [wollewald](https://registry.platformio.org/libraries/wollewald/INA226_WE)) - - `Wire` ## Understanding Samples and Conversion Times @@ -62,16 +55,12 @@ For detailed programming information and register configurations, refer to the [ ## Compiling -To enable, compile with `USERMOD_INA226` defined (e.g. in `platformio_override.ini`). +To enable, compile with `INA226` in `custom_usermods` (e.g. in `platformio_override.ini`). ```ini [env:ina226_example] extends = env:esp32dev -build_flags = - ${common.build_flags} ${esp32.build_flags} - -D USERMOD_INA226 +custom_usermods = ${env:esp32dev.custom_usermods} INA226 +build_flags = ${env:esp32dev.build_flags} ; -D USERMOD_INA226_DEBUG ; -- add a debug status to the info modal -lib_deps = - ${esp32.lib_deps} - wollewald/INA226_WE@~1.2.9 ``` \ No newline at end of file diff --git a/usermods/INA226_v2/library.json b/usermods/INA226_v2/library.json new file mode 100644 index 00000000..34fcd368 --- /dev/null +++ b/usermods/INA226_v2/library.json @@ -0,0 +1,7 @@ +{ + "name": "INA226_v2", + "build": { "libArchive": false }, + "dependencies": { + "wollewald/INA226_WE":"~1.2.9" + } +} diff --git a/usermods/INA226_v2/platformio_override.ini b/usermods/INA226_v2/platformio_override.ini index 885b2dd1..9968cbf7 100644 --- a/usermods/INA226_v2/platformio_override.ini +++ b/usermods/INA226_v2/platformio_override.ini @@ -1,9 +1,6 @@ [env:ina226_example] extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} INA226_v2 build_flags = - ${common.build_flags} ${esp32.build_flags} - -D USERMOD_INA226 + ${env:esp32dev.build_flags} ; -D USERMOD_INA226_DEBUG ; -- add a debug status to the info modal -lib_deps = - ${esp32.lib_deps} - wollewald/INA226_WE@~1.2.9 \ No newline at end of file diff --git a/usermods/Internal_Temperature_v2/usermod_internal_temperature.h b/usermods/Internal_Temperature_v2/Internal_Temperature_v2.cpp similarity index 98% rename from usermods/Internal_Temperature_v2/usermod_internal_temperature.h rename to usermods/Internal_Temperature_v2/Internal_Temperature_v2.cpp index 6d4d4577..7c30985e 100644 --- a/usermods/Internal_Temperature_v2/usermod_internal_temperature.h +++ b/usermods/Internal_Temperature_v2/Internal_Temperature_v2.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" class InternalTemperatureUsermod : public Usermod @@ -193,4 +191,7 @@ void InternalTemperatureUsermod::publishMqtt(const char *state, bool retain) mqtt->publish(subuf, 0, retain, state); } #endif -} \ No newline at end of file +} + +static InternalTemperatureUsermod internal_temperature_v2; +REGISTER_USERMOD(internal_temperature_v2); \ No newline at end of file diff --git a/usermods/Internal_Temperature_v2/library.json b/usermods/Internal_Temperature_v2/library.json new file mode 100644 index 00000000..b1826ab4 --- /dev/null +++ b/usermods/Internal_Temperature_v2/library.json @@ -0,0 +1,4 @@ +{ + "name": "Internal_Temperature_v2", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/Internal_Temperature_v2/readme.md b/usermods/Internal_Temperature_v2/readme.md index d574f3ab..68bac8b0 100644 --- a/usermods/Internal_Temperature_v2/readme.md +++ b/usermods/Internal_Temperature_v2/readme.md @@ -23,8 +23,7 @@ ## Installation -- Add a build flag `-D USERMOD_INTERNAL_TEMPERATURE` to your `platformio.ini` (or `platformio_override.ini`). - +- Add `Internal_Temperature` to `custom_usermods` in your `platformio.ini` (or `platformio_override.ini`). ## 📝 Change Log diff --git a/usermods/LD2410_v2/usermod_ld2410.h b/usermods/LD2410_v2/LD2410_v2.cpp similarity index 98% rename from usermods/LD2410_v2/usermod_ld2410.h rename to usermods/LD2410_v2/LD2410_v2.cpp index 4d96b32f..095da12f 100644 --- a/usermods/LD2410_v2/usermod_ld2410.h +++ b/usermods/LD2410_v2/LD2410_v2.cpp @@ -1,14 +1,10 @@ -#warning **** Included USERMOD_LD2410 **** - -#ifndef WLED_ENABLE_MQTT -#error "This user mod requires MQTT to be enabled." -#endif - -#pragma once - #include "wled.h" #include +#ifdef WLED_DISABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + class LD2410Usermod : public Usermod { private: @@ -235,3 +231,7 @@ void LD2410Usermod::publishMqtt(const char* topic, const char* state, bool retai } #endif } + + +static LD2410Usermod ld2410_v2; +REGISTER_USERMOD(ld2410_v2); \ No newline at end of file diff --git a/usermods/LD2410_v2/library.json b/usermods/LD2410_v2/library.json new file mode 100644 index 00000000..757ec404 --- /dev/null +++ b/usermods/LD2410_v2/library.json @@ -0,0 +1,7 @@ +{ + "name": "LD2410_v2", + "build": { "libArchive": false }, + "dependencies": { + "ncmreynolds/ld2410":"^0.1.3" + } +} \ No newline at end of file diff --git a/usermods/LD2410_v2/readme.md b/usermods/LD2410_v2/readme.md index 40463d4b..ea85ab82 100644 --- a/usermods/LD2410_v2/readme.md +++ b/usermods/LD2410_v2/readme.md @@ -10,21 +10,15 @@ The movement and presence state are displayed in both the Info section of the we ## Dependencies - Libraries - `ncmreynolds/ld2410@^0.1.3` - - This must be added under `lib_deps` in your `platformio.ini` (or `platformio_override.ini`). - Data is published over MQTT - make sure you've enabled the MQTT sync interface. ## Compilation -To enable, compile with `USERMOD_LD2410` defined (e.g. in `platformio_override.ini`) +To enable, compile with `LD2140` in `custom_usermods` (e.g. in `platformio_override.ini`) ```ini [env:usermod_USERMOD_LD2410_esp32dev] extends = env:esp32dev -build_flags = - ${common.build_flags_esp32} - -D USERMOD_LD2410 -lib_deps = - ${esp32.lib_deps} - ncmreynolds/ld2410@^0.1.3 +custom_usermods = ${env:esp32dev.custom_usermods} LD2140 ``` ### Configuration Options diff --git a/usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h b/usermods/LDR_Dusk_Dawn_v2/LDR_Dusk_Dawn_v2.cpp similarity index 98% rename from usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h rename to usermods/LDR_Dusk_Dawn_v2/LDR_Dusk_Dawn_v2.cpp index 03f4c078..9c5c835e 100644 --- a/usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h +++ b/usermods/LDR_Dusk_Dawn_v2/LDR_Dusk_Dawn_v2.cpp @@ -1,4 +1,3 @@ -#pragma once #include "wled.h" #ifndef ARDUINO_ARCH_ESP32 @@ -151,3 +150,7 @@ class LDR_Dusk_Dawn_v2 : public Usermod { }; const char LDR_Dusk_Dawn_v2::_name[] PROGMEM = "LDR_Dusk_Dawn_v2"; + + +static LDR_Dusk_Dawn_v2 ldr_dusk_dawn_v2; +REGISTER_USERMOD(ldr_dusk_dawn_v2); \ No newline at end of file diff --git a/usermods/LDR_Dusk_Dawn_v2/README.md b/usermods/LDR_Dusk_Dawn_v2/README.md index 5e33518a..8fe86a36 100644 --- a/usermods/LDR_Dusk_Dawn_v2/README.md +++ b/usermods/LDR_Dusk_Dawn_v2/README.md @@ -2,13 +2,14 @@ This usermod will obtain readings from a Light Dependent Resistor (LDR) and will turn on/off specific presets based on those readings. This is useful for exterior lighting situations where you want the lights to only be on when it is dark out. # Installation -Add "-D USERMOD_LDR_DUSK_DAWN" to your platformio.ini [common] build_flags and build. +Add "LDR_Dusk_Dawn" to your platformio.ini environment's custom_usermods and build. Example: ``` -[common] -build_flags = - -D USERMOD_LDR_DUSK_DAWN # Enable LDR Dusk Dawn Usermod +[env:usermod_LDR_Dusk_Dawn_esp32dev] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} + LDR_Dusk_Dawn # Enable LDR Dusk Dawn Usermod ``` # Usermod Settings diff --git a/usermods/LDR_Dusk_Dawn_v2/library.json b/usermods/LDR_Dusk_Dawn_v2/library.json new file mode 100644 index 00000000..709967ea --- /dev/null +++ b/usermods/LDR_Dusk_Dawn_v2/library.json @@ -0,0 +1,4 @@ +{ + "name": "LDR_Dusk_Dawn_v2", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/MAX17048_v2/usermod_max17048.h b/usermods/MAX17048_v2/MAX17048_v2.cpp similarity index 97% rename from usermods/MAX17048_v2/usermod_max17048.h rename to usermods/MAX17048_v2/MAX17048_v2.cpp index c3a2664a..520f1a7b 100644 --- a/usermods/MAX17048_v2/usermod_max17048.h +++ b/usermods/MAX17048_v2/MAX17048_v2.cpp @@ -1,8 +1,6 @@ // force the compiler to show a warning to confirm that this file is included #warning **** Included USERMOD_MAX17048 V2.0 **** -#pragma once - #include "wled.h" #include "Adafruit_MAX1704X.h" @@ -37,8 +35,8 @@ class Usermod_MAX17048 : public Usermod { unsigned long lastSend = UINT32_MAX - (USERMOD_MAX17048_MAX_MONITOR_INTERVAL - USERMOD_MAX17048_FIRST_MONITOR_AT); - uint8_t VoltageDecimals = 3; // Number of decimal places in published voltage values - uint8_t PercentDecimals = 1; // Number of decimal places in published percent values + unsigned VoltageDecimals = 3; // Number of decimal places in published voltage values + unsigned PercentDecimals = 1; // Number of decimal places in published percent values // string that are used multiple time (this will save some flash memory) static const char _name[]; @@ -279,3 +277,7 @@ const char Usermod_MAX17048::_enabled[] PROGMEM = "enabled"; const char Usermod_MAX17048::_maxReadInterval[] PROGMEM = "max-read-interval-ms"; const char Usermod_MAX17048::_minReadInterval[] PROGMEM = "min-read-interval-ms"; const char Usermod_MAX17048::_HomeAssistantDiscovery[] PROGMEM = "HomeAssistantDiscovery"; + + +static Usermod_MAX17048 max17048_v2; +REGISTER_USERMOD(max17048_v2); \ No newline at end of file diff --git a/usermods/MAX17048_v2/library.json b/usermods/MAX17048_v2/library.json new file mode 100644 index 00000000..a9ae1543 --- /dev/null +++ b/usermods/MAX17048_v2/library.json @@ -0,0 +1,7 @@ +{ + "name": "MAX17048_v2", + "build": { "libArchive": false}, + "dependencies": { + "Adafruit_MAX1704X":"https://github.com/adafruit/Adafruit_MAX1704X#1.0.2" + } +} diff --git a/usermods/MAX17048_v2/readme.md b/usermods/MAX17048_v2/readme.md index 958e6def..df42f989 100644 --- a/usermods/MAX17048_v2/readme.md +++ b/usermods/MAX17048_v2/readme.md @@ -5,26 +5,16 @@ This usermod reads information from an Adafruit MAX17048 and outputs the follow ## Dependencies -Libraries: -- `Adafruit_BusIO@~1.14.5` (by [adafruit](https://github.com/adafruit/Adafruit_BusIO)) -- `Adafruit_MAX1704X@~1.0.2` (by [adafruit](https://github.com/adafruit/Adafruit_MAX1704X)) - -These must be added under `lib_deps` in your `platform.ini` (or `platform_override.ini`). Data is published over MQTT - make sure you've enabled the MQTT sync interface. ## Compilation +Add "MAX17048_v2" to your platformio.ini environment's custom_usermods and build. To enable, compile with `USERMOD_MAX17048` define in the build_flags (e.g. in `platformio.ini` or `platformio_override.ini`) such as in the example below: ```ini [env:usermod_max17048_d1_mini] extends = env:d1_mini -build_flags = - ${common.build_flags_esp8266} - -D USERMOD_MAX17048 -lib_deps = - ${esp8266.lib_deps} - https://github.com/adafruit/Adafruit_BusIO @ 1.14.5 - https://github.com/adafruit/Adafruit_MAX1704X @ 1.0.2 +custom_usermods = ${env:d1_mini.custom_usermods} MAX17048_v2 ``` ### Configuration Options diff --git a/usermods/MY9291/usermode_MY9291.h b/usermods/MY9291/MY9291.cpp similarity index 94% rename from usermods/MY9291/usermode_MY9291.h rename to usermods/MY9291/MY9291.cpp index 66bbc34c..3881ffd0 100644 --- a/usermods/MY9291/usermode_MY9291.h +++ b/usermods/MY9291/MY9291.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" #include "MY92xx.h" @@ -42,4 +40,7 @@ class MY9291Usermod : public Usermod { uint16_t getId() { return USERMOD_ID_MY9291; } -}; \ No newline at end of file +}; + +static MY9291Usermod my9291; +REGISTER_USERMOD(my9291); \ No newline at end of file diff --git a/usermods/MY9291/library.json b/usermods/MY9291/library.json new file mode 100644 index 00000000..9c3a33d4 --- /dev/null +++ b/usermods/MY9291/library.json @@ -0,0 +1,5 @@ +{ + "name": "MY9291", + "build": { "libArchive": false }, + "platforms": ["espressif8266"] +} \ No newline at end of file diff --git a/usermods/PIR_sensor_switch/PIR_Highlight_Standby b/usermods/PIR_sensor_switch/PIR_Highlight_Standby index 152388e8..4ca32bf4 100644 --- a/usermods/PIR_sensor_switch/PIR_Highlight_Standby +++ b/usermods/PIR_sensor_switch/PIR_Highlight_Standby @@ -42,7 +42,7 @@ * * * Usermods allow you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality * * v2 usermods are class inheritance based and can (but don't have to) implement more functions, each of them is shown in this example. * Multiple v2 usermods can be added to one compilation easily. diff --git a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h b/usermods/PIR_sensor_switch/PIR_sensor_switch.cpp similarity index 96% rename from usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h rename to usermods/PIR_sensor_switch/PIR_sensor_switch.cpp index 0deda181..6f09ce5b 100644 --- a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h +++ b/usermods/PIR_sensor_switch/PIR_sensor_switch.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" #ifndef PIR_SENSOR_PIN @@ -26,7 +24,7 @@ * Maintained by: @blazoncek * * Usermods allow you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality * * v2 usermods are class inheritance based and can (but don't have to) implement more functions, each of them is shown in this example. * Multiple v2 usermods can be added to one compilation easily. @@ -571,3 +569,7 @@ bool PIRsensorSwitch::readFromConfig(JsonObject &root) // use "return !top["newestParameter"].isNull();" when updating Usermod with new features return !(pins.isNull() || pins.size() != PIR_SENSOR_MAX_SENSORS); } + + +static PIRsensorSwitch pir_sensor_switch; +REGISTER_USERMOD(pir_sensor_switch); \ No newline at end of file diff --git a/usermods/PIR_sensor_switch/library.json b/usermods/PIR_sensor_switch/library.json new file mode 100644 index 00000000..b3cbcbbf --- /dev/null +++ b/usermods/PIR_sensor_switch/library.json @@ -0,0 +1,4 @@ +{ + "name": "PIR_sensor_switch", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/PIR_sensor_switch/readme.md b/usermods/PIR_sensor_switch/readme.md index fac5419f..2b889748 100644 --- a/usermods/PIR_sensor_switch/readme.md +++ b/usermods/PIR_sensor_switch/readme.md @@ -5,7 +5,7 @@ This usermod-v2 modification allows the connection of a PIR sensor to switch on _Story:_ I use the PIR Sensor to automatically turn on the WLED analog clock in my home office room when I am there. -The LED strip is switched [using a relay](https://github.com/Aircoookie/WLED/wiki/Control-a-relay-with-WLED) to keep the power consumption low when it is switched off. +The LED strip is switched [using a relay](https://kno.wled.ge/features/relay-control/) to keep the power consumption low when it is switched off. ## Web interface @@ -25,7 +25,7 @@ You can also use usermod's off timer instead of sensor's. In such case rotate th **NOTE:** Usermod has been included in master branch of WLED so it can be compiled in directly just by defining `-D USERMOD_PIRSWITCH` and optionally `-D PIR_SENSOR_PIN=16` to override default pin. You can also change the default off time by adding `-D PIR_SENSOR_OFF_SEC=30`. -## API to enable/disable the PIR sensor from outside. For example from another usermod: +## API to enable/disable the PIR sensor from outside. For example from another usermod To query or change the PIR sensor state the methods `bool PIRsensorEnabled()` and `void EnablePIRsensor(bool enable)` are available. @@ -33,15 +33,16 @@ When the PIR sensor state changes an MQTT message is broadcasted with topic `wle Usermod can also be configured to send just the MQTT message but not change WLED state using settings page as well as responding to motion only at night (assuming NTP and latitude/longitude are set to determine sunrise/sunset times). -### There are two options to get access to the usermod instance: +### There are two options to get access to the usermod instance -1. Include `usermod_PIR_sensor_switch.h` **before** you include other usermods in `usermods_list.cpp' +_1._ Include `usermod_PIR_sensor_switch.h` **before** you include other usermods in `usermods_list.cpp' or -2. Use `#include "usermod_PIR_sensor_switch.h"` at the top of the `usermod.h` where you need it. +_2._ Use `#include "usermod_PIR_sensor_switch.h"` at the top of the `usermod.h` where you need it. **Example usermod.h :** + ```cpp #include "wled.h" @@ -79,25 +80,30 @@ Usermod can be configured via the Usermods settings page. * `override` - override PIR input when WLED state is changed using UI * `domoticz-idx` - Domoticz virtual switch ID (used with MQTT `domoticz/in`) - Have fun - @gegu & @blazoncek ## Change log + 2021-04 + * Adaptation for runtime configuration. 2021-11 + * Added information about dynamic configuration options * Added option to temporary enable/disable usermod from WLED UI (Info dialog) 2022-11 + * Added compile time option for off timer. * Added Home Assistant autodiscovery MQTT broadcast. * Updated info on compiling. 2023-?? + * Override option * Domoticz virtual switch ID (used with MQTT `domoticz/in`) 2024-02 -* Added compile time option to expand number of PIR sensors (they are logically ORed) `-D PIR_SENSOR_MAX_SENSORS=3` \ No newline at end of file + +* Added compile time option to expand number of PIR sensors (they are logically ORed) `-D PIR_SENSOR_MAX_SENSORS=3` diff --git a/usermods/PWM_fan/usermod_PWM_fan.h b/usermods/PWM_fan/PWM_fan.cpp similarity index 98% rename from usermods/PWM_fan/usermod_PWM_fan.h rename to usermods/PWM_fan/PWM_fan.cpp index c3ef24fe..a0939f08 100644 --- a/usermods/PWM_fan/usermod_PWM_fan.h +++ b/usermods/PWM_fan/PWM_fan.cpp @@ -1,10 +1,14 @@ -#pragma once +#include "wled.h" -#if !defined(USERMOD_DALLASTEMPERATURE) && !defined(USERMOD_SHT) +#if defined(USERMOD_DALLASTEMPERATURE) +#include "UsermodTemperature.h" +#elif defined(USERMOD_SHT) +#include "ShtUsermod.h" +#else #error The "PWM fan" usermod requires "Dallas Temeprature" or "SHT" usermod to function properly. #endif -#include "wled.h" + // PWM & tacho code curtesy of @KlausMu // https://github.com/KlausMu/esp32-fan-controller/tree/main/src @@ -397,3 +401,7 @@ const char PWMFanUsermod::_maxPWMValuePct[] PROGMEM = "max-PWM-percent"; const char PWMFanUsermod::_IRQperRotation[] PROGMEM = "IRQs-per-rotation"; const char PWMFanUsermod::_speed[] PROGMEM = "speed"; const char PWMFanUsermod::_lock[] PROGMEM = "lock"; + + +static PWMFanUsermod pwm_fan; +REGISTER_USERMOD(pwm_fan); \ No newline at end of file diff --git a/usermods/PWM_fan/library.json b/usermods/PWM_fan/library.json new file mode 100644 index 00000000..8ae3d7fd --- /dev/null +++ b/usermods/PWM_fan/library.json @@ -0,0 +1,7 @@ +{ + "name": "PWM_fan", + "build": { + "libArchive": false, + "extraScript": "setup_deps.py" + } +} \ No newline at end of file diff --git a/usermods/PWM_fan/readme.md b/usermods/PWM_fan/readme.md index 6a44acf3..872bbd9b 100644 --- a/usermods/PWM_fan/readme.md +++ b/usermods/PWM_fan/readme.md @@ -11,8 +11,8 @@ If the _tachometer_ is supported, the current speed (in RPM) will be displayed o ## Installation -Add the compile-time option `-D USERMOD_PWM_FAN` to your `platformio.ini` (or `platformio_override.ini`) or use `#define USERMOD_PWM_FAN` in `myconfig.h`. -You will also need `-D USERMOD_DALLASTEMPERATURE`. +Add the `PWM_fan` to `custom_usermods` in your `platformio.ini` (or `platformio_override.ini`) +You will also need `Temperature` or `sht`. ### Define Your Options @@ -40,6 +40,9 @@ If the fan speed is unlocked, it will revert to temperature controlled speed on ## Change Log 2021-10 + * First public release + 2022-05 + * Added JSON API call to allow changing of speed diff --git a/usermods/PWM_fan/setup_deps.py b/usermods/PWM_fan/setup_deps.py new file mode 100644 index 00000000..11879079 --- /dev/null +++ b/usermods/PWM_fan/setup_deps.py @@ -0,0 +1,12 @@ +from platformio.package.meta import PackageSpec +Import('env') + + +libs = [PackageSpec(lib).name for lib in env.GetProjectOption("lib_deps",[])] +# Check for dependencies +if "Temperature" in libs: + env.Append(CPPDEFINES=[("USERMOD_DALLASTEMPERATURE")]) +elif "sht" in libs: + env.Append(CPPDEFINES=[("USERMOD_SHT")]) +elif "PWM_fan" in libs: # The script can be run if this module was previously selected + raise RuntimeError("PWM_fan usermod requires Temperature or sht to be enabled") diff --git a/usermods/RTC/usermod_rtc.h b/usermods/RTC/RTC.cpp similarity index 96% rename from usermods/RTC/usermod_rtc.h rename to usermods/RTC/RTC.cpp index 42965e3a..2b9c3b4f 100644 --- a/usermods/RTC/usermod_rtc.h +++ b/usermods/RTC/RTC.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "src/dependencies/time/DS1307RTC.h" #include "wled.h" @@ -48,4 +46,7 @@ class RTCUsermod : public Usermod { { return USERMOD_ID_RTC; } -}; \ No newline at end of file +}; + +static RTCUsermod rtc; +REGISTER_USERMOD(rtc); \ No newline at end of file diff --git a/usermods/RTC/library.json b/usermods/RTC/library.json new file mode 100644 index 00000000..688dfc2d --- /dev/null +++ b/usermods/RTC/library.json @@ -0,0 +1,4 @@ +{ + "name": "RTC", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/SN_Photoresistor/SN_Photoresistor.cpp b/usermods/SN_Photoresistor/SN_Photoresistor.cpp new file mode 100644 index 00000000..ffd78c0f --- /dev/null +++ b/usermods/SN_Photoresistor/SN_Photoresistor.cpp @@ -0,0 +1,146 @@ +#include "wled.h" +#include "SN_Photoresistor.h" + +//Pin defaults for QuinLed Dig-Uno (A0) +#ifndef PHOTORESISTOR_PIN +#define PHOTORESISTOR_PIN A0 +#endif + +static bool checkBoundSensor(float newValue, float prevValue, float maxDiff) +{ + return isnan(prevValue) || newValue <= prevValue - maxDiff || newValue >= prevValue + maxDiff; +} + +uint16_t Usermod_SN_Photoresistor::getLuminance() +{ + // http://forum.arduino.cc/index.php?topic=37555.0 + // https://forum.arduino.cc/index.php?topic=185158.0 + float volts = analogRead(PHOTORESISTOR_PIN) * (referenceVoltage / adcPrecision); + float amps = volts / resistorValue; + float lux = amps * 1000000 * 2.0; + + lastMeasurement = millis(); + getLuminanceComplete = true; + return uint16_t(lux); +} + +void Usermod_SN_Photoresistor::setup() +{ + // set pinmode + pinMode(PHOTORESISTOR_PIN, INPUT); +} + +void Usermod_SN_Photoresistor::loop() +{ + if (disabled || strip.isUpdating()) + return; + + unsigned long now = millis(); + + // check to see if we are due for taking a measurement + // lastMeasurement will not be updated until the conversion + // is complete the the reading is finished + if (now - lastMeasurement < readingInterval) + { + return; + } + + uint16_t currentLDRValue = getLuminance(); + if (checkBoundSensor(currentLDRValue, lastLDRValue, offset)) + { + lastLDRValue = currentLDRValue; + +#ifndef WLED_DISABLE_MQTT + if (WLED_MQTT_CONNECTED) + { + char subuf[45]; + strcpy(subuf, mqttDeviceTopic); + strcat_P(subuf, PSTR("/luminance")); + mqtt->publish(subuf, 0, true, String(lastLDRValue).c_str()); + } + else + { + DEBUG_PRINTLN(F("Missing MQTT connection. Not publishing data")); + } + } +#endif +} + + +void Usermod_SN_Photoresistor::addToJsonInfo(JsonObject &root) +{ + JsonObject user = root[F("u")]; + if (user.isNull()) + user = root.createNestedObject(F("u")); + + JsonArray lux = user.createNestedArray(F("Luminance")); + + if (!getLuminanceComplete) + { + // if we haven't read the sensor yet, let the user know + // that we are still waiting for the first measurement + lux.add((USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT - millis()) / 1000); + lux.add(F(" sec until read")); + return; + } + + lux.add(lastLDRValue); + lux.add(F(" lux")); +} + + +/** + * addToConfig() (called from set.cpp) stores persistent properties to cfg.json + */ +void Usermod_SN_Photoresistor::addToConfig(JsonObject &root) +{ + // we add JSON object. + JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname + top[FPSTR(_enabled)] = !disabled; + top[FPSTR(_readInterval)] = readingInterval / 1000; + top[FPSTR(_referenceVoltage)] = referenceVoltage; + top[FPSTR(_resistorValue)] = resistorValue; + top[FPSTR(_adcPrecision)] = adcPrecision; + top[FPSTR(_offset)] = offset; + + DEBUG_PRINTLN(F("Photoresistor config saved.")); +} + +/** +* readFromConfig() is called before setup() to populate properties from values stored in cfg.json +*/ +bool Usermod_SN_Photoresistor::readFromConfig(JsonObject &root) +{ + // we look for JSON object. + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINT(FPSTR(_name)); + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + disabled = !(top[FPSTR(_enabled)] | !disabled); + readingInterval = (top[FPSTR(_readInterval)] | readingInterval/1000) * 1000; // convert to ms + referenceVoltage = top[FPSTR(_referenceVoltage)] | referenceVoltage; + resistorValue = top[FPSTR(_resistorValue)] | resistorValue; + adcPrecision = top[FPSTR(_adcPrecision)] | adcPrecision; + offset = top[FPSTR(_offset)] | offset; + DEBUG_PRINT(FPSTR(_name)); + DEBUG_PRINTLN(F(" config (re)loaded.")); + + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return true; +} + + +// strings to reduce flash memory usage (used more than twice) +const char Usermod_SN_Photoresistor::_name[] PROGMEM = "Photoresistor"; +const char Usermod_SN_Photoresistor::_enabled[] PROGMEM = "enabled"; +const char Usermod_SN_Photoresistor::_readInterval[] PROGMEM = "read-interval-s"; +const char Usermod_SN_Photoresistor::_referenceVoltage[] PROGMEM = "supplied-voltage"; +const char Usermod_SN_Photoresistor::_resistorValue[] PROGMEM = "resistor-value"; +const char Usermod_SN_Photoresistor::_adcPrecision[] PROGMEM = "adc-precision"; +const char Usermod_SN_Photoresistor::_offset[] PROGMEM = "offset"; + +static Usermod_SN_Photoresistor sn_photoresistor; +REGISTER_USERMOD(sn_photoresistor); \ No newline at end of file diff --git a/usermods/SN_Photoresistor/SN_Photoresistor.h b/usermods/SN_Photoresistor/SN_Photoresistor.h new file mode 100644 index 00000000..87836c0e --- /dev/null +++ b/usermods/SN_Photoresistor/SN_Photoresistor.h @@ -0,0 +1,90 @@ +#pragma once +#include "wled.h" + +// the frequency to check photoresistor, 10 seconds +#ifndef USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL +#define USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL 10000 +#endif + +// how many seconds after boot to take first measurement, 10 seconds +#ifndef USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT +#define USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT 10000 +#endif + +// supplied voltage +#ifndef USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE +#define USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE 5 +#endif + +// 10 bits +#ifndef USERMOD_SN_PHOTORESISTOR_ADC_PRECISION +#define USERMOD_SN_PHOTORESISTOR_ADC_PRECISION 1024.0f +#endif + +// resistor size 10K hms +#ifndef USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE +#define USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE 10000.0f +#endif + +// only report if difference grater than offset value +#ifndef USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE +#define USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE 5 +#endif + +class Usermod_SN_Photoresistor : public Usermod +{ +private: + float referenceVoltage = USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE; + float resistorValue = USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE; + float adcPrecision = USERMOD_SN_PHOTORESISTOR_ADC_PRECISION; + int8_t offset = USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE; + + unsigned long readingInterval = USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL; + // set last reading as "40 sec before boot", so first reading is taken after 20 sec + unsigned long lastMeasurement = UINT32_MAX - (USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL - USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT); + // flag to indicate we have finished the first getTemperature call + // allows this library to report to the user how long until the first + // measurement + bool getLuminanceComplete = false; + uint16_t lastLDRValue = 65535; + + // flag set at startup + bool disabled = false; + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _readInterval[]; + static const char _referenceVoltage[]; + static const char _resistorValue[]; + static const char _adcPrecision[]; + static const char _offset[]; + + uint16_t getLuminance(); + +public: + void setup(); + void loop(); + + uint16_t getLastLDRValue() + { + return lastLDRValue; + } + + void addToJsonInfo(JsonObject &root); + + uint16_t getId() + { + return USERMOD_ID_SN_PHOTORESISTOR; + } + + /** + * addToConfig() (called from set.cpp) stores persistent properties to cfg.json + */ + void addToConfig(JsonObject &root); + + /** + * readFromConfig() is called before setup() to populate properties from values stored in cfg.json + */ + bool readFromConfig(JsonObject &root); +}; diff --git a/usermods/SN_Photoresistor/library.json b/usermods/SN_Photoresistor/library.json new file mode 100644 index 00000000..c896644f --- /dev/null +++ b/usermods/SN_Photoresistor/library.json @@ -0,0 +1,4 @@ +{ + "name": "SN_Photoresistor", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/SN_Photoresistor/usermod_sn_photoresistor.h b/usermods/SN_Photoresistor/usermod_sn_photoresistor.h deleted file mode 100644 index 45cdb66a..00000000 --- a/usermods/SN_Photoresistor/usermod_sn_photoresistor.h +++ /dev/null @@ -1,212 +0,0 @@ -#pragma once - -#include "wled.h" - -//Pin defaults for QuinLed Dig-Uno (A0) -#ifndef PHOTORESISTOR_PIN -#define PHOTORESISTOR_PIN A0 -#endif - -// the frequency to check photoresistor, 10 seconds -#ifndef USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL -#define USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL 10000 -#endif - -// how many seconds after boot to take first measurement, 10 seconds -#ifndef USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT -#define USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT 10000 -#endif - -// supplied voltage -#ifndef USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE -#define USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE 5 -#endif - -// 10 bits -#ifndef USERMOD_SN_PHOTORESISTOR_ADC_PRECISION -#define USERMOD_SN_PHOTORESISTOR_ADC_PRECISION 1024.0f -#endif - -// resistor size 10K hms -#ifndef USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE -#define USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE 10000.0f -#endif - -// only report if difference grater than offset value -#ifndef USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE -#define USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE 5 -#endif - -class Usermod_SN_Photoresistor : public Usermod -{ -private: - float referenceVoltage = USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE; - float resistorValue = USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE; - float adcPrecision = USERMOD_SN_PHOTORESISTOR_ADC_PRECISION; - int8_t offset = USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE; - - unsigned long readingInterval = USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL; - // set last reading as "40 sec before boot", so first reading is taken after 20 sec - unsigned long lastMeasurement = UINT32_MAX - (USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL - USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT); - // flag to indicate we have finished the first getTemperature call - // allows this library to report to the user how long until the first - // measurement - bool getLuminanceComplete = false; - uint16_t lastLDRValue = -1000; - - // flag set at startup - bool disabled = false; - - // strings to reduce flash memory usage (used more than twice) - static const char _name[]; - static const char _enabled[]; - static const char _readInterval[]; - static const char _referenceVoltage[]; - static const char _resistorValue[]; - static const char _adcPrecision[]; - static const char _offset[]; - - bool checkBoundSensor(float newValue, float prevValue, float maxDiff) - { - return isnan(prevValue) || newValue <= prevValue - maxDiff || newValue >= prevValue + maxDiff; - } - - uint16_t getLuminance() - { - // http://forum.arduino.cc/index.php?topic=37555.0 - // https://forum.arduino.cc/index.php?topic=185158.0 - float volts = analogRead(PHOTORESISTOR_PIN) * (referenceVoltage / adcPrecision); - float amps = volts / resistorValue; - float lux = amps * 1000000 * 2.0; - - lastMeasurement = millis(); - getLuminanceComplete = true; - return uint16_t(lux); - } - -public: - void setup() - { - // set pinmode - pinMode(PHOTORESISTOR_PIN, INPUT); - } - - void loop() - { - if (disabled || strip.isUpdating()) - return; - - unsigned long now = millis(); - - // check to see if we are due for taking a measurement - // lastMeasurement will not be updated until the conversion - // is complete the the reading is finished - if (now - lastMeasurement < readingInterval) - { - return; - } - - uint16_t currentLDRValue = getLuminance(); - if (checkBoundSensor(currentLDRValue, lastLDRValue, offset)) - { - lastLDRValue = currentLDRValue; - -#ifndef WLED_DISABLE_MQTT - if (WLED_MQTT_CONNECTED) - { - char subuf[45]; - strcpy(subuf, mqttDeviceTopic); - strcat_P(subuf, PSTR("/luminance")); - mqtt->publish(subuf, 0, true, String(lastLDRValue).c_str()); - } - else - { - DEBUG_PRINTLN(F("Missing MQTT connection. Not publishing data")); - } - } -#endif - } - - uint16_t getLastLDRValue() - { - return lastLDRValue; - } - - void addToJsonInfo(JsonObject &root) - { - JsonObject user = root[F("u")]; - if (user.isNull()) - user = root.createNestedObject(F("u")); - - JsonArray lux = user.createNestedArray(F("Luminance")); - - if (!getLuminanceComplete) - { - // if we haven't read the sensor yet, let the user know - // that we are still waiting for the first measurement - lux.add((USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT - millis()) / 1000); - lux.add(F(" sec until read")); - return; - } - - lux.add(lastLDRValue); - lux.add(F(" lux")); - } - - uint16_t getId() - { - return USERMOD_ID_SN_PHOTORESISTOR; - } - - /** - * addToConfig() (called from set.cpp) stores persistent properties to cfg.json - */ - void addToConfig(JsonObject &root) - { - // we add JSON object. - JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname - top[FPSTR(_enabled)] = !disabled; - top[FPSTR(_readInterval)] = readingInterval / 1000; - top[FPSTR(_referenceVoltage)] = referenceVoltage; - top[FPSTR(_resistorValue)] = resistorValue; - top[FPSTR(_adcPrecision)] = adcPrecision; - top[FPSTR(_offset)] = offset; - - DEBUG_PRINTLN(F("Photoresistor config saved.")); - } - - /** - * readFromConfig() is called before setup() to populate properties from values stored in cfg.json - */ - bool readFromConfig(JsonObject &root) - { - // we look for JSON object. - JsonObject top = root[FPSTR(_name)]; - if (top.isNull()) { - DEBUG_PRINT(FPSTR(_name)); - DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); - return false; - } - - disabled = !(top[FPSTR(_enabled)] | !disabled); - readingInterval = (top[FPSTR(_readInterval)] | readingInterval/1000) * 1000; // convert to ms - referenceVoltage = top[FPSTR(_referenceVoltage)] | referenceVoltage; - resistorValue = top[FPSTR(_resistorValue)] | resistorValue; - adcPrecision = top[FPSTR(_adcPrecision)] | adcPrecision; - offset = top[FPSTR(_offset)] | offset; - DEBUG_PRINT(FPSTR(_name)); - DEBUG_PRINTLN(F(" config (re)loaded.")); - - // use "return !top["newestParameter"].isNull();" when updating Usermod with new features - return true; - } -}; - -// strings to reduce flash memory usage (used more than twice) -const char Usermod_SN_Photoresistor::_name[] PROGMEM = "Photoresistor"; -const char Usermod_SN_Photoresistor::_enabled[] PROGMEM = "enabled"; -const char Usermod_SN_Photoresistor::_readInterval[] PROGMEM = "read-interval-s"; -const char Usermod_SN_Photoresistor::_referenceVoltage[] PROGMEM = "supplied-voltage"; -const char Usermod_SN_Photoresistor::_resistorValue[] PROGMEM = "resistor-value"; -const char Usermod_SN_Photoresistor::_adcPrecision[] PROGMEM = "adc-precision"; -const char Usermod_SN_Photoresistor::_offset[] PROGMEM = "offset"; diff --git a/usermods/SN_Photoresistor/usermods_list.cpp b/usermods/SN_Photoresistor/usermods_list.cpp deleted file mode 100644 index a2c6ca16..00000000 --- a/usermods/SN_Photoresistor/usermods_list.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include "wled.h" -/* - * Register your v2 usermods here! - */ -#ifdef USERMOD_SN_PHOTORESISTOR -#include "../usermods/SN_Photoresistor/usermod_sn_photoresistor.h" -#endif - -void registerUsermods() -{ -#ifdef USERMOD_SN_PHOTORESISTOR - UsermodManager::add(new Usermod_SN_Photoresistor()); -#endif -} \ No newline at end of file diff --git a/usermods/ST7789_display/README.md b/usermods/ST7789_display/README.md index ebaae492..7dd3b599 100644 --- a/usermods/ST7789_display/README.md +++ b/usermods/ST7789_display/README.md @@ -27,19 +27,6 @@ This usermod enables display of the following: ### Platformio.ini changes -In the `platformio.ini` file, uncomment the `TFT_eSPI` line within the [common] section, under `lib_deps`: - -```ini -# platformio.ini -... -[common] -... -lib_deps = - ... - #For use of the TTGO T-Display ESP32 Module with integrated TFT display uncomment the following line - #TFT_eSPI -... -``` In the `platformio.ini` file, you must change the environment setup to build for just the esp32dev platform as follows: diff --git a/usermods/ST7789_display/ST7789_display.h b/usermods/ST7789_display/ST7789_display.cpp similarity index 99% rename from usermods/ST7789_display/ST7789_display.h rename to usermods/ST7789_display/ST7789_display.cpp index 65f4cae5..c596baec 100644 --- a/usermods/ST7789_display/ST7789_display.h +++ b/usermods/ST7789_display/ST7789_display.cpp @@ -1,8 +1,6 @@ // Credits to @mrVanboy, @gwaland and my dearest friend @westward // Also for @spiff72 for usermod TTGO-T-Display // 210217 -#pragma once - #include "wled.h" #include #include @@ -410,4 +408,7 @@ class St7789DisplayUsermod : public Usermod { //More methods can be added in the future, this example will then be extended. //Your usermod will remain compatible as it does not need to implement all methods from the Usermod base class! -}; \ No newline at end of file +}; + +static name. st7789_display; +REGISTER_USERMOD(st7789_display); \ No newline at end of file diff --git a/usermods/ST7789_display/library.json.disabled b/usermods/ST7789_display/library.json.disabled new file mode 100644 index 00000000..725e20a6 --- /dev/null +++ b/usermods/ST7789_display/library.json.disabled @@ -0,0 +1,4 @@ +{ + "name:": "ST7789_display", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/Si7021_MQTT_HA/usermod_si7021_mqtt_ha.h b/usermods/Si7021_MQTT_HA/Si7021_MQTT_HA.cpp similarity index 98% rename from usermods/Si7021_MQTT_HA/usermod_si7021_mqtt_ha.h rename to usermods/Si7021_MQTT_HA/Si7021_MQTT_HA.cpp index 9f027382..7845658a 100644 --- a/usermods/Si7021_MQTT_HA/usermod_si7021_mqtt_ha.h +++ b/usermods/Si7021_MQTT_HA/Si7021_MQTT_HA.cpp @@ -1,9 +1,3 @@ -#ifndef WLED_ENABLE_MQTT -#error "This user mod requires MQTT to be enabled." -#endif - -#pragma once - // this is remixed from usermod_v2_SensorsToMqtt.h (sensors_to_mqtt usermod) // and usermod_multi_relay.h (multi_relay usermod) @@ -11,7 +5,11 @@ #include #include // EnvironmentCalculations::HeatIndex(), ::DewPoint(), ::AbsoluteHumidity() -Adafruit_Si7021 si7021; +#ifdef WLED_DISABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + +static Adafruit_Si7021 si7021; class Si7021_MQTT_HA : public Usermod { @@ -229,3 +227,7 @@ const char Si7021_MQTT_HA::_name[] PROGMEM = "Si7021 MQTT (Hom const char Si7021_MQTT_HA::_enabled[] PROGMEM = "enabled"; const char Si7021_MQTT_HA::_sendAdditionalSensors[] PROGMEM = "Send Dew Point, Abs. Humidity and Heat Index"; const char Si7021_MQTT_HA::_haAutoDiscovery[] PROGMEM = "Home Assistant MQTT Auto-Discovery"; + + +static Si7021_MQTT_HA si7021_mqtt_ha; +REGISTER_USERMOD(si7021_mqtt_ha); \ No newline at end of file diff --git a/usermods/Si7021_MQTT_HA/library.json b/usermods/Si7021_MQTT_HA/library.json new file mode 100644 index 00000000..e3d7635e --- /dev/null +++ b/usermods/Si7021_MQTT_HA/library.json @@ -0,0 +1,8 @@ +{ + "name": "Si7021_MQTT_HA", + "build": { "libArchive": false }, + "dependencies": { + "finitespace/BME280":"3.0.0", + "adafruit/Adafruit Si7021 Library" : "1.5.3" + } +} \ No newline at end of file diff --git a/usermods/TTGO-T-Display/usermod.cpp b/usermods/TTGO-T-Display/usermod.cpp index cbba0777..d8dcb299 100644 --- a/usermods/TTGO-T-Display/usermod.cpp +++ b/usermods/TTGO-T-Display/usermod.cpp @@ -1,7 +1,7 @@ /* * This file allows you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality * EEPROM bytes 2750+ are reserved for your custom use case. (if you extend #define EEPSIZE in const.h) * bytes 2400+ are currently unused, but might be used for future wled features */ diff --git a/usermods/Temperature/usermod_temperature.h b/usermods/Temperature/Temperature.cpp similarity index 78% rename from usermods/Temperature/usermod_temperature.h rename to usermods/Temperature/Temperature.cpp index 178bc05a..a2e0ea91 100644 --- a/usermods/Temperature/usermod_temperature.h +++ b/usermods/Temperature/Temperature.cpp @@ -1,114 +1,7 @@ -#pragma once - -#include "wled.h" -#include "OneWire.h" - -//Pin defaults for QuinLed Dig-Uno if not overriden -#ifndef TEMPERATURE_PIN - #ifdef ARDUINO_ARCH_ESP32 - #define TEMPERATURE_PIN 18 - #else //ESP8266 boards - #define TEMPERATURE_PIN 14 - #endif -#endif - -// the frequency to check temperature, 1 minute -#ifndef USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL -#define USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL 60000 -#endif +#include "UsermodTemperature.h" static uint16_t mode_temperature(); -class UsermodTemperature : public Usermod { - - private: - - bool initDone = false; - OneWire *oneWire; - // GPIO pin used for sensor (with a default compile-time fallback) - int8_t temperaturePin = TEMPERATURE_PIN; - // measurement unit (true==°C, false==°F) - bool degC = true; - // using parasite power on the sensor - bool parasite = false; - int8_t parasitePin = -1; - // how often do we read from sensor? - unsigned long readingInterval = USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL; - // set last reading as "40 sec before boot", so first reading is taken after 20 sec - unsigned long lastMeasurement = UINT32_MAX - USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL; - // last time requestTemperatures was called - // used to determine when we can read the sensors temperature - // we have to wait at least 93.75 ms after requestTemperatures() is called - unsigned long lastTemperaturesRequest; - float temperature; - // indicates requestTemperatures has been called but the sensor measurement is not complete - bool waitingForConversion = false; - // flag set at startup if DS18B20 sensor not found, avoids trying to keep getting - // temperature if flashed to a board without a sensor attached - byte sensorFound; - - bool enabled = true; - - bool HApublished = false; - int16_t idx = -1; // Domoticz virtual sensor idx - - // strings to reduce flash memory usage (used more than twice) - static const char _name[]; - static const char _enabled[]; - static const char _readInterval[]; - static const char _parasite[]; - static const char _parasitePin[]; - static const char _domoticzIDX[]; - static const char _sensor[]; - static const char _temperature[]; - static const char _Temperature[]; - static const char _data_fx[]; - - //Dallas sensor quick (& dirty) reading. Credit to - Author: Peter Scargill, August 17th, 2013 - float readDallas(); - void requestTemperatures(); - void readTemperature(); - bool findSensor(); -#ifndef WLED_DISABLE_MQTT - void publishHomeAssistantAutodiscovery(); -#endif - - static UsermodTemperature* _instance; // to overcome nonstatic getTemperatureC() method and avoid UsermodManager::lookup(USERMOD_ID_TEMPERATURE); - - public: - - UsermodTemperature() { _instance = this; } - static UsermodTemperature *getInstance() { return UsermodTemperature::_instance; } - - /* - * API calls te enable data exchange between WLED modules - */ - inline float getTemperatureC() { return temperature; } - inline float getTemperatureF() { return temperature * 1.8f + 32.0f; } - float getTemperature(); - const char *getTemperatureUnit(); - uint16_t getId() override { return USERMOD_ID_TEMPERATURE; } - - void setup() override; - void loop() override; - //void connected() override; -#ifndef WLED_DISABLE_MQTT - void onMqttConnect(bool sessionPresent) override; -#endif - //void onUpdateBegin(bool init) override; - - //bool handleButton(uint8_t b) override; - //void handleOverlayDraw() override; - - void addToJsonInfo(JsonObject& root) override; - //void addToJsonState(JsonObject &root) override; - //void readFromJsonState(JsonObject &root) override; - void addToConfig(JsonObject &root) override; - bool readFromConfig(JsonObject &root) override; - - void appendConfigData() override; -}; - //Dallas sensor quick (& dirty) reading. Credit to - Author: Peter Scargill, August 17th, 2013 float UsermodTemperature::readDallas() { byte data[9]; @@ -471,3 +364,7 @@ static uint16_t mode_temperature() { SEGMENT.fill(SEGMENT.color_from_palette(i, false, false, 255)); return FRAMETIME; } + + +static UsermodTemperature temperature; +REGISTER_USERMOD(temperature); \ No newline at end of file diff --git a/usermods/Temperature/UsermodTemperature.h b/usermods/Temperature/UsermodTemperature.h new file mode 100644 index 00000000..2517a2b8 --- /dev/null +++ b/usermods/Temperature/UsermodTemperature.h @@ -0,0 +1,108 @@ +#pragma once +#include "wled.h" +#include "OneWire.h" + +//Pin defaults for QuinLed Dig-Uno if not overriden +#ifndef TEMPERATURE_PIN + #ifdef ARDUINO_ARCH_ESP32 + #define TEMPERATURE_PIN 18 + #else //ESP8266 boards + #define TEMPERATURE_PIN 14 + #endif +#endif + +// the frequency to check temperature, 1 minute +#ifndef USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL +#define USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL 60000 +#endif + +class UsermodTemperature : public Usermod { + + private: + + bool initDone = false; + OneWire *oneWire; + // GPIO pin used for sensor (with a default compile-time fallback) + int8_t temperaturePin = TEMPERATURE_PIN; + // measurement unit (true==°C, false==°F) + bool degC = true; + // using parasite power on the sensor + bool parasite = false; + int8_t parasitePin = -1; + // how often do we read from sensor? + unsigned long readingInterval = USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL; + // set last reading as "40 sec before boot", so first reading is taken after 20 sec + unsigned long lastMeasurement = UINT32_MAX - USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL; + // last time requestTemperatures was called + // used to determine when we can read the sensors temperature + // we have to wait at least 93.75 ms after requestTemperatures() is called + unsigned long lastTemperaturesRequest; + float temperature; + // indicates requestTemperatures has been called but the sensor measurement is not complete + bool waitingForConversion = false; + // flag set at startup if DS18B20 sensor not found, avoids trying to keep getting + // temperature if flashed to a board without a sensor attached + byte sensorFound; + + bool enabled = true; + + bool HApublished = false; + int16_t idx = -1; // Domoticz virtual sensor idx + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _readInterval[]; + static const char _parasite[]; + static const char _parasitePin[]; + static const char _domoticzIDX[]; + static const char _sensor[]; + static const char _temperature[]; + static const char _Temperature[]; + static const char _data_fx[]; + + //Dallas sensor quick (& dirty) reading. Credit to - Author: Peter Scargill, August 17th, 2013 + float readDallas(); + void requestTemperatures(); + void readTemperature(); + bool findSensor(); +#ifndef WLED_DISABLE_MQTT + void publishHomeAssistantAutodiscovery(); +#endif + + static UsermodTemperature* _instance; // to overcome nonstatic getTemperatureC() method and avoid UsermodManager::lookup(USERMOD_ID_TEMPERATURE); + + public: + + UsermodTemperature() { _instance = this; } + static UsermodTemperature *getInstance() { return UsermodTemperature::_instance; } + + /* + * API calls te enable data exchange between WLED modules + */ + inline float getTemperatureC() { return temperature; } + inline float getTemperatureF() { return temperature * 1.8f + 32.0f; } + float getTemperature(); + const char *getTemperatureUnit(); + uint16_t getId() override { return USERMOD_ID_TEMPERATURE; } + + void setup() override; + void loop() override; + //void connected() override; +#ifndef WLED_DISABLE_MQTT + void onMqttConnect(bool sessionPresent) override; +#endif + //void onUpdateBegin(bool init) override; + + //bool handleButton(uint8_t b) override; + //void handleOverlayDraw() override; + + void addToJsonInfo(JsonObject& root) override; + //void addToJsonState(JsonObject &root) override; + //void readFromJsonState(JsonObject &root) override; + void addToConfig(JsonObject &root) override; + bool readFromConfig(JsonObject &root) override; + + void appendConfigData() override; +}; + diff --git a/usermods/Temperature/library.json b/usermods/Temperature/library.json new file mode 100644 index 00000000..5439bc13 --- /dev/null +++ b/usermods/Temperature/library.json @@ -0,0 +1,7 @@ +{ + "name": "Temperature", + "build": { "libArchive": false}, + "dependencies": { + "paulstoffregen/OneWire":"~2.3.8" + } +} diff --git a/usermods/Temperature/platformio_override.ini b/usermods/Temperature/platformio_override.ini index cc86367f..a53b5974 100644 --- a/usermods/Temperature/platformio_override.ini +++ b/usermods/Temperature/platformio_override.ini @@ -1,10 +1,5 @@ ; Options ; ------- -; USERMOD_DALLASTEMPERATURE - define this to have this user mod included wled00\usermods_list.cpp ; USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL - the number of milliseconds between measurements, defaults to 60 seconds ; -[env:d1_mini_usermod_dallas_temperature_C] -extends = env:d1_mini -build_flags = ${common.build_flags_esp8266} -D USERMOD_DALLASTEMPERATURE -lib_deps = ${env.lib_deps} - paulstoffregen/OneWire@~2.3.8 \ No newline at end of file + diff --git a/usermods/Temperature/readme.md b/usermods/Temperature/readme.md index 3d65be7e..b09495fe 100644 --- a/usermods/Temperature/readme.md +++ b/usermods/Temperature/readme.md @@ -11,11 +11,19 @@ Maintained by @blazoncek ## Installation -Copy the example `platformio_override.ini` to the root directory. This file should be placed in the same directory as `platformio.ini`. +Add `Temperature` to `custom_usermods` in your platformio_override.ini. + +Example **platformio_override.ini**: + +```ini +[env:usermod_temperature_esp32dev] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} + Temperature +``` ### Define Your Options -* `USERMOD_DALLASTEMPERATURE` - enables this user mod wled00/usermods_list.cpp * `USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL` - number of milliseconds between measurements, defaults to 60000 ms (60s) All parameters can be configured at runtime via the Usermods settings page, including pin, temperature in degrees Celsius or Fahrenheit and measurement interval. @@ -25,43 +33,25 @@ All parameters can be configured at runtime via the Usermods settings page, incl * [QuinLED-Dig-Uno](https://quinled.info/2018/09/15/quinled-dig-uno/) - Project link * [Srg74-WLED-Wemos-shield](https://github.com/srg74/WLED-wemos-shield) - another great DIY WLED board -### PlatformIO requirements - -If you are using `platformio_override.ini`, you should be able to refresh the task list and see your custom task, for example `env:d1_mini_usermod_dallas_temperature_C`. - -If you are not using `platformio_override.ini`, you might have to uncomment `OneWire@~2.3.5 under` `[common]` section in `platformio.ini`: - -```ini -# platformio.ini -... -[platformio] -... -; default_envs = esp07 -default_envs = d1_mini -... -[common] -... -lib_deps = - ... - #For Dallas sensor uncomment following - paulstoffregen/OneWire @ ~2.3.8 -``` - ## Change Log -2020-09-12 +2020-09-12 + * Changed to use async non-blocking implementation * Do not report erroneous low temperatures to MQTT * Disable plugin if temperature sensor not detected * Report the number of seconds until the first read in the info screen instead of sensor error 2021-04 + * Adaptation for runtime configuration. 2023-05 + * Rewrite to conform to newer recommendations. * Recommended @blazoncek fork of OneWire for ESP32 to avoid Sensor error 2024-09 + * Update OneWire to version 2.3.8, which includes stickbreaker's and garyd9's ESP32 fixes: - blazoncek's fork is no longer needed \ No newline at end of file + blazoncek's fork is no longer needed diff --git a/usermods/TetrisAI_v2/usermod_v2_tetrisai.h b/usermods/TetrisAI_v2/TetrisAI_v2.cpp similarity index 99% rename from usermods/TetrisAI_v2/usermod_v2_tetrisai.h rename to usermods/TetrisAI_v2/TetrisAI_v2.cpp index 0f7039da..b51250b2 100644 --- a/usermods/TetrisAI_v2/usermod_v2_tetrisai.h +++ b/usermods/TetrisAI_v2/TetrisAI_v2.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" #include "FX.h" #include "fcn_declare.h" @@ -250,3 +248,7 @@ public: return USERMOD_ID_TETRISAI; } }; + + +static TetrisAIUsermod tetrisai_v2; +REGISTER_USERMOD(tetrisai_v2); \ No newline at end of file diff --git a/usermods/TetrisAI_v2/library.json b/usermods/TetrisAI_v2/library.json new file mode 100644 index 00000000..54aa22d3 --- /dev/null +++ b/usermods/TetrisAI_v2/library.json @@ -0,0 +1,4 @@ +{ + "name": "TetrisAI_v2", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/TetrisAI_v2/readme.md b/usermods/TetrisAI_v2/readme.md index b56f801a..5ac80289 100644 --- a/usermods/TetrisAI_v2/readme.md +++ b/usermods/TetrisAI_v2/readme.md @@ -6,7 +6,7 @@ Version 1.0 ## Installation -Just activate the usermod with `-D USERMOD_TETRISAI` and the effect will become available under the name 'Tetris AI'. If you are running out of flash memory, use a different memory layout (e.g. [WLED_ESP32_4MB_256KB_FS.csv](https://github.com/Aircoookie/WLED/blob/main/tools/WLED_ESP32_4MB_256KB_FS.csv)). +Just activate the usermod with `-D USERMOD_TETRISAI` and the effect will become available under the name 'Tetris AI'. If you are running out of flash memory, use a different memory layout (e.g. [WLED_ESP32_4MB_256KB_FS.csv](https://github.com/wled-dev/WLED/blob/main/tools/WLED_ESP32_4MB_256KB_FS.csv)). If needed simply add to `platformio_override.ini` (or `platformio_override.ini`): diff --git a/usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h b/usermods/VL53L0X_gestures/VL53L0X_gestures.cpp similarity index 98% rename from usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h rename to usermods/VL53L0X_gestures/VL53L0X_gestures.cpp index fe6b958f..af3220b3 100644 --- a/usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h +++ b/usermods/VL53L0X_gestures/VL53L0X_gestures.cpp @@ -13,8 +13,6 @@ * lib_deps = ${env.lib_deps} * pololu/VL53L0X @ ^1.3.0 */ -#pragma once - #include "wled.h" #include @@ -126,4 +124,7 @@ class UsermodVL53L0XGestures : public Usermod { { return USERMOD_ID_VL53L0X; } -}; \ No newline at end of file +}; + +static UsermodVL53L0XGestures vl53l0x_gestures; +REGISTER_USERMOD(vl53l0x_gestures); \ No newline at end of file diff --git a/usermods/VL53L0X_gestures/library.json b/usermods/VL53L0X_gestures/library.json new file mode 100644 index 00000000..08f5921c --- /dev/null +++ b/usermods/VL53L0X_gestures/library.json @@ -0,0 +1,7 @@ +{ + "name": "VL53L0X_gestures", + "build": { "libArchive": false}, + "dependencies": { + "pololu/VL53L0X" : "^1.3.0" + } +} diff --git a/usermods/VL53L0X_gestures/readme.md b/usermods/VL53L0X_gestures/readme.md index a230b1a6..04c4a4aa 100644 --- a/usermods/VL53L0X_gestures/readme.md +++ b/usermods/VL53L0X_gestures/readme.md @@ -10,20 +10,4 @@ Useful for controlling strips when you want to avoid touching anything. 1. Attach VL53L0X sensor to i2c pins according to default pins for your board. 2. Add `-D USERMOD_VL53L0X_GESTURES` to your build flags at platformio.ini (plaformio_override.ini) for needed environment. -In my case, for example: `build_flags = ${env.build_flags} -D USERMOD_VL53L0X_GESTURES` -3. Add "pololu/VL53L0X" dependency below to `lib_deps` like this: -```ini -lib_deps = ${env.lib_deps} - pololu/VL53L0X @ ^1.3.0 -``` -My entire `platformio_override.ini` for example (for nodemcu board): -```ini -[platformio] -default_envs = nodemcuv2 - -[env:nodemcuv2] -build_flags = ${env.build_flags} -D USERMOD_VL53L0X_GESTURES -lib_deps = ${env.lib_deps} - pololu/VL53L0X @ ^1.3.0 -``` diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.cpp similarity index 99% rename from usermods/audioreactive/audio_reactive.h rename to usermods/audioreactive/audio_reactive.cpp index 9c463e0a..06268560 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.cpp @@ -1,4 +1,3 @@ -#pragma once #include "wled.h" @@ -19,7 +18,7 @@ /* * Usermods allow you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality * * This is an audioreactive v2 usermod. * .... @@ -66,11 +65,14 @@ static bool udpSyncConnected = false; // UDP connection status -> true i // audioreactive variables #ifdef ARDUINO_ARCH_ESP32 + #ifndef SR_AGC // Automatic gain control mode + #define SR_AGC 0 // default mode = off + #endif static float micDataReal = 0.0f; // MicIn data with full 24bit resolution - lowest 8bit after decimal point static float multAgc = 1.0f; // sample * multAgc = sampleAgc. Our AGC multiplier static float sampleAvg = 0.0f; // Smoothed Average sample - sampleAvg < 1 means "quiet" (simple noise gate) static float sampleAgc = 0.0f; // Smoothed AGC sample -static uint8_t soundAgc = 0; // Automagic gain control: 0 - none, 1 - normal, 2 - vivid, 3 - lazy (config value) +static uint8_t soundAgc = SR_AGC; // Automatic gain control: 0 - off, 1 - normal, 2 - vivid, 3 - lazy (config value) #endif //static float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample static float FFT_MajorPeak = 1.0f; // FFT: strongest (peak) frequency @@ -870,7 +872,7 @@ class AudioReactive : public Usermod { const int AGC_preset = (soundAgc > 0)? (soundAgc-1): 0; // make sure the _compiler_ knows this value will not change while we are inside the function #ifdef WLED_DISABLE_SOUND - micIn = inoise8(millis(), millis()); // Simulated analog read + micIn = perlin8(millis(), millis()); // Simulated analog read micDataReal = micIn; #else #ifdef ARDUINO_ARCH_ESP32 @@ -1737,7 +1739,7 @@ class AudioReactive : public Usermod { } void onStateChange(uint8_t callMode) override { - if (initDone && enabled && addPalettes && palettes==0 && strip.customPalettes.size()<10) { + if (initDone && enabled && addPalettes && palettes==0 && customPalettes.size()<10) { // if palettes were removed during JSON call re-add them createAudioPalettes(); } @@ -1967,20 +1969,20 @@ class AudioReactive : public Usermod { void AudioReactive::removeAudioPalettes(void) { DEBUG_PRINTLN(F("Removing audio palettes.")); while (palettes>0) { - strip.customPalettes.pop_back(); + customPalettes.pop_back(); DEBUG_PRINTLN(palettes); palettes--; } - DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(strip.customPalettes.size()); + DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(customPalettes.size()); } void AudioReactive::createAudioPalettes(void) { - DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(strip.customPalettes.size()); + DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(customPalettes.size()); if (palettes) return; DEBUG_PRINTLN(F("Adding audio palettes.")); for (int i=0; i= palettes) lastCustPalette -= palettes; for (int pal=0; pal= 12 && wakePin <= 15) || (wakePin >= 25 && wakePin <= 27) || (wakePin >= 32 && wakePin <= 39)) { + return true; + } + #endif + #if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32S2) //ESP32 S3 & S3: GPIOs 0-21 can be used for wake-up + if (wakePin <= 21) { + return true; + } + #endif + #ifdef CONFIG_IDF_TARGET_ESP32C3 // ESP32 C3: GPIOs 0-5 can be used for wake-up + if (wakePin <= 5) { + return true; + } + #endif + DEBUG_PRINTLN(F("Error: unsupported deep sleep wake-up pin")); + return false; + } + + public: + + inline void enable(bool enable) { enabled = enable; } // Enable/Disable the usermod + inline bool isEnabled() { return enabled; } //Get usermod enabled/disabled state + + // setup is called at boot (or in this case after every exit of sleep mode) + void setup() { + //TODO: if the de-init of RTC pins is required to do it could be done here + //rtc_gpio_deinit(wakeupPin); + initDone = true; + } + + void loop() { + if (!enabled || !offMode) { // disabled or LEDs are on + lastLoopTime = 0; // reset timer + return; + } + + if (sleepDelay > 0) { + if(lastLoopTime == 0) lastLoopTime = millis(); // initialize + if (millis() - lastLoopTime < sleepDelay * 1000) { + return; // wait until delay is over + } + } + + if(powerup == false && delaycounter) { // delay sleep in case a preset is being loaded and turnOnAtBoot is disabled (handleIO() does enable offMode temporarily in this case) + delaycounter--; + if(delaycounter == 2 && offMode) { // force turn on, no matter the settings (device is bricked if user set sleepDelay=0, no bootup preset and turnOnAtBoot=false) + if (briS == 0) bri = 10; // turn on at low brightness + else bri = briS; + strip.setBrightness(bri); // needed to make handleIO() not turn off LEDs (really? does not help in bootup preset) + offMode = false; + applyPresetWithFallback(0, CALL_MODE_INIT, FX_MODE_STATIC, 0); // try to apply preset 0, fallback to static + if (rlyPin >= 0) { + digitalWrite(rlyPin, (rlyMde ? HIGH : LOW)); // turn relay on TODO: this should be done by wled, what function to call? + } + } + return; + } + + DEBUG_PRINTLN(F("DeepSleep UM: entering deep sleep...")); + powerup = false; // turn leds on in all subsequent bootups (overrides Turn LEDs on after power up/reset' at reboot) + if(!pin_is_valid(wakeupPin)) return; + esp_err_t halerror = ESP_OK; + pinMode(wakeupPin, INPUT); // make sure GPIO is input with pullup/pulldown disabled + esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); //disable all wake-up sources (just in case) + + if(wakeupAfter) + esp_sleep_enable_timer_wakeup((uint64_t)wakeupAfter * (uint64_t)1e6); //sleep for x seconds + + #if defined(CONFIG_IDF_TARGET_ESP32C3) // ESP32 C3 + if(noPull) + gpio_sleep_set_pull_mode((gpio_num_t)wakeupPin, GPIO_FLOATING); + else { // enable pullup/pulldown resistor + if(wakeWhenHigh) + gpio_sleep_set_pull_mode((gpio_num_t)wakeupPin, GPIO_PULLDOWN_ONLY); + else + gpio_sleep_set_pull_mode((gpio_num_t)wakeupPin, GPIO_PULLUP_ONLY); + } + if(wakeWhenHigh) + halerror = esp_deep_sleep_enable_gpio_wakeup(1<(0 = never)');")); + oappend(SET_F("addInfo('DeepSleep:delaySleep',1,'seconds (0 = sleep at powerup)');")); // first string is suffix, second string is prefix + } + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() { + return USERMOD_ID_DEEP_SLEEP; + } + +}; + +// add more strings here to reduce flash memory usage +const char DeepSleepUsermod::_name[] PROGMEM = "DeepSleep"; +const char DeepSleepUsermod::_enabled[] PROGMEM = "enabled"; + +static DeepSleepUsermod deep_sleep; +REGISTER_USERMOD(deep_sleep); \ No newline at end of file diff --git a/usermods/deep_sleep/library.json b/usermods/deep_sleep/library.json new file mode 100644 index 00000000..82e32c99 --- /dev/null +++ b/usermods/deep_sleep/library.json @@ -0,0 +1,4 @@ +{ + "name": "deep_sleep", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/deep_sleep/readme.md b/usermods/deep_sleep/readme.md new file mode 100644 index 00000000..807757f8 --- /dev/null +++ b/usermods/deep_sleep/readme.md @@ -0,0 +1,84 @@ +# Deep Sleep usermod + +This usermod unleashes the low power capabilities of th ESP: when you power off your LEDs (using the UI power button or a macro) the ESP will be put into deep sleep mode, reducing power consumption to a minimum. +During deep sleep the ESP is shut down completely: no WiFi, no CPU, no outputs. The only way to wake it up is to use an external signal or a button. Once it wakes from deep sleep it reboots so ***make sure to use a boot-up preset.*** + +# A word of warning + +When you disable the WLED option 'Turn LEDs on after power up/reset' and 'DelaySleep' is set to zero the ESP will go into deep sleep directly after power-up and only start WLED after it has been woken up. +If the ESP can not be awoken from deep sleep due to a wrong configuration it has to be factory reset, disabling sleep at power-up. There is no other way to wake it up. + +# Power Consumption in deep sleep + +The current drawn by the ESP in deep sleep mode depends on the type and is in the range of 5uA-20uA (as in micro Amperes): +- ESP32: 10uA +- ESP32 S3: 8uA +- ESP32 S2: 20uA +- ESP32 C3: 5uA +- ESP8266: 20uA (not supported in this usermod) + +However, there is usually additional components on a controller that increase the value: +- Power LED: the power LED on a ESP board draws 500uA - 1mA +- LDO: the voltage regulator also draws idle current. Depending on the type used this can be around 50uA up to 10mA (LM1117). Special low power LDOs with very low idle currents do exist +- Digital LEDs: WS2812 for example draw a current of about 1mA per LED. To make good use of this usermod it is required to power them off using MOSFETs or a Relay + +For lowest power consumption, remove the Power LED and make sure your board does not use an LM1117. On a ESP32 C3 Supermini with the power LED removed (no other modifications) powered through the 5V pin I measured a current draw of 50uA in deep sleep. + +# Useable GPIOs + +The GPIOs that can be used to wake the ESP from deep sleep are limited. Only pins connected to the internal RTC unit can be used: + +- ESP32: GPIO 0, 2, 4, 12-15, 25-39 +- ESP32 S3: GPIO 0-21 +- ESP32 S2: GPIO 0-21 +- ESP32 C3: GPIO 0-5 +- ESP8266 is not supported in this usermod + +You can however use the selected wake-up pin normally in WLED, it only gets activated as a wake-up pin when your LEDs are powered down. + +# Limitations + +To keep this usermod simple and easy to use, it is a very basic implementation of the low-power capabilities provided by the ESP. If you need more advanced control you are welcome to implement your own version based on this usermod. + +## Usermod installation + +Add `deep_sleep` to `custom_usermods` in your platformio.ini. Settings can be changed in the usermod config UI. + +### Define Settings + +There are five parameters you can set: + +- GPIO: the pin to use for wake-up +- WakeWhen High/Low: the pin state that triggers the wake-up +- Pull-up/down disable: enable or disable the internal pullup resistors during sleep (does not affect normal use while running) +- Wake after: if set larger than 0, ESP will automatically wake-up after this many seconds (Turn LEDs on after power up/reset is overriden, it will always turn on) +- Delay sleep: if set larger than 0, ESP will not go to sleep for this many seconds after you power it off. Timer is reset when switched back on during this time. + +To override the default settings, place the `#define` in wled.h or add `-D DEEPSLEEP_xxx` to your platformio_override.ini build flags + +* `DEEPSLEEP_WAKEUPPIN x` - define the pin to be used for wake-up, see list of useable pins above. The pin can be used normally as a button pin in WLED. +* `DEEPSLEEP_WAKEWHENHIGH` - if defined, wakes up when pin goes high (default is low) +* `DEEPSLEEP_DISABLEPULL` - if defined, internal pullup/pulldown is disabled in deep sleep (default is ebnabled) +* `DEEPSLEEP_WAKEUPINTERVAL` - number of seconds after which a wake-up happens automatically, sooner if button is pressed. 0 = never. accuracy is about 2% +* `DEEPSLEEP_DELAY` - delay between power-off and sleep + +example for env build flags: + `-D USERMOD_DEEP_SLEEP` + `-D DEEPSLEEP_WAKEUPPIN=4` + `-D DEEPSLEEP_DISABLEPULL=0` ;enable pull-up/down resistors by default + `-D DEEPSLEEP_WAKEUPINTERVAL=43200` ;wake up after 12 hours (or when button is pressed) + +### Hardware Setup + +To wake from deep-sleep an external trigger signal on the configured GPIO is required. When using timed-only wake-up, use a GPIO that has an on-board pull-up resistor (GPIO0 on most boards). When using push-buttons it is highly recommended to use an external pull-up resistor: not all IO's on all devices have properly working internal resistors. + +Using sensors like PIR, IR, touch sensors or any other sensor with a digital output can be used instead of a button. + +now go on and save some power +@dedehai + +## Change log +2024-09 +* Initial version +2024-10 +* Changed from #define configuration to UI configuration \ No newline at end of file diff --git a/usermods/mpu6050_imu/library.json b/usermods/mpu6050_imu/library.json new file mode 100644 index 00000000..d2a0f70a --- /dev/null +++ b/usermods/mpu6050_imu/library.json @@ -0,0 +1,7 @@ +{ + "name": "mpu6050_imu", + "build": { "libArchive": false}, + "dependencies": { + "electroniccats/MPU6050":"1.0.1" + } +} diff --git a/usermods/mpu6050_imu/usermod_mpu6050_imu.h b/usermods/mpu6050_imu/mpu6050_imu.cpp similarity index 99% rename from usermods/mpu6050_imu/usermod_mpu6050_imu.h rename to usermods/mpu6050_imu/mpu6050_imu.cpp index f04578fe..6df5d64e 100644 --- a/usermods/mpu6050_imu/usermod_mpu6050_imu.h +++ b/usermods/mpu6050_imu/mpu6050_imu.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" /* This driver reads quaternion data from the MPU6060 and adds it to the JSON @@ -105,7 +103,7 @@ class MPU6050Driver : public Usermod { VectorInt16 aaReal; // [x, y, z] gravity-free accel sensor measurements VectorInt16 aaWorld; // [x, y, z] world-frame accel sensor measurements VectorFloat gravity; // [x, y, z] gravity vector - uint32 sample_count; + uint32_t sample_count; // Usermod output um_data_t um_data; @@ -446,3 +444,7 @@ const char MPU6050Driver::_z_acc_bias[] PROGMEM = "z_acc_bias"; const char MPU6050Driver::_x_gyro_bias[] PROGMEM = "x_gyro_bias"; const char MPU6050Driver::_y_gyro_bias[] PROGMEM = "y_gyro_bias"; const char MPU6050Driver::_z_gyro_bias[] PROGMEM = "z_gyro_bias"; + + +static MPU6050Driver mpu6050_imu; +REGISTER_USERMOD(mpu6050_imu); \ No newline at end of file diff --git a/usermods/mpu6050_imu/readme.md b/usermods/mpu6050_imu/readme.md index c324738d..87c43913 100644 --- a/usermods/mpu6050_imu/readme.md +++ b/usermods/mpu6050_imu/readme.md @@ -16,26 +16,6 @@ As a memento to a long trip I was on, I built an icosahedron globe. I put lights I wanted to integrate an IMU to allow either on-board, or off-board effects that would react to the globes orientation. See the blog post on building it or a video demo . -## Adding Dependencies - -I2Cdev and MPU6050 must be installed. - -To install them, add electroniccats/MPU6050@1.0.1 to lib_deps in the platformio.ini file. - -For example: - -``` -lib_deps = - FastLED@3.3.2 - NeoPixelBus@2.5.7 - ESPAsyncTCP@1.2.0 - ESPAsyncUDP@697c75a025 - AsyncTCP@1.0.3 - Esp Async WebServer@1.2.0 - IRremoteESP8266@2.7.3 - electroniccats/MPU6050@1.0.1 -``` - ## Wiring The connections needed to the MPU6050 are as follows: @@ -74,18 +54,13 @@ to the info object ## Usermod installation -1. Copy the file `usermod_mpu6050_imu.h` to the `wled00` directory. -2. Register the usermod by adding `#include "usermod_mpu6050_imu.h"` in the top and `registerUsermod(new MPU6050Driver());` in the bottom of `usermods_list.cpp`. +Add `mpu6050_imu` to `custom_usermods` in your platformio_override.ini. -Example **usermods_list.cpp**: +Example **platformio_override.ini**: -```cpp -#include "wled.h" - -#include "usermod_mpu6050_imu.h" - -void registerUsermods() -{ - UsermodManager::add(new MPU6050Driver()); -} +```ini +[env:usermod_mpu6050_imu_esp32dev] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} + mpu6050_imu ``` diff --git a/usermods/mqtt_switch_v2/README.md b/usermods/mqtt_switch_v2/README.md deleted file mode 100644 index 382f72d0..00000000 --- a/usermods/mqtt_switch_v2/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# DEPRECATION NOTICE -This usermod is deprecated and no longer maintained. It will be removed in a future WLED release. Please use usermod multi_relay which has more features. - - -# MQTT controllable switches -This usermod allows controlling switches (e.g. relays) via MQTT. - -## Usermod installation - -1. Copy the file `usermod_mqtt_switch.h` to the `wled00` directory. -2. Register the usermod by adding `#include "usermod_mqtt_switch.h"` in the top and `registerUsermod(new UsermodMqttSwitch());` in the bottom of `usermods_list.cpp`. - - -Example `usermods_list.cpp`: - -``` -#include "wled.h" -#include "usermod_mqtt_switch.h" - -void registerUsermods() -{ - UsermodManager::add(new UsermodMqttSwitch()); -} -``` - -## Define pins -Add a define for MQTTSWITCHPINS to platformio_override.ini. -The following example defines 3 switches connected to the GPIO pins 13, 5 and 2: - -``` -[env:livingroom] -board = esp12e -platform = ${common.platform_wled_default} -board_build.ldscript = ${common.ldscript_4m1m} -build_flags = ${common.build_flags_esp8266} - -D DATA_PINS=3 - -D BTNPIN=4 - -D RLYPIN=12 - -D RLYMDE=1 - -D STATUSPIN=15 - -D MQTTSWITCHPINS="13, 5, 2" -``` - -Pins can be inverted by setting `MQTTSWITCHINVERT`. For example `-D MQTTSWITCHINVERT="false, false, true"` would invert the switch on pin 2 in the previous example. - -The default state after booting before any MQTT message can be set by `MQTTSWITCHDEFAULTS`. For example `-D MQTTSWITCHDEFAULTS="ON, OFF, OFF"` would power on the switch on pin 13 and power off switches on pins 5 and 2. - -## MQTT topics -This usermod listens on `[mqttDeviceTopic]/switch/0/set` (where 0 is replaced with the index of the switch) for commands. Anything starting with `ON` turns on the switch, everything else turns it off. -Feedback about the current state is provided at `[mqttDeviceTopic]/switch/0/state`. - -### Home Assistant auto-discovery -Auto-discovery information is automatically published and you shouldn't have to do anything to register the switches in Home Assistant. - diff --git a/usermods/mqtt_switch_v2/usermod_mqtt_switch.h b/usermods/mqtt_switch_v2/usermod_mqtt_switch.h deleted file mode 100644 index 67dfc9cc..00000000 --- a/usermods/mqtt_switch_v2/usermod_mqtt_switch.h +++ /dev/null @@ -1,159 +0,0 @@ -#pragma once - -#warning "This usermod is deprecated and no longer maintained. It will be removed in a future WLED release. Please use usermod multi_relay which has more features." - -#include "wled.h" -#ifndef WLED_ENABLE_MQTT -#error "This user mod requires MQTT to be enabled." -#endif - -#ifndef MQTTSWITCHPINS -#error "Please define MQTTSWITCHPINS in platformio_override.ini. e.g. -D MQTTSWITCHPINS="12, 0, 2" " -// The following define helps Eclipse's C++ parser but is never used in production due to the #error statement on the line before -#define MQTTSWITCHPINS 12, 0, 2 -#endif - -// Default behavior: All outputs active high -#ifndef MQTTSWITCHINVERT -#define MQTTSWITCHINVERT -#endif - -// Default behavior: All outputs off -#ifndef MQTTSWITCHDEFAULTS -#define MQTTSWITCHDEFAULTS -#endif - -static const uint8_t switchPins[] = { MQTTSWITCHPINS }; -//This is a hack to get the number of pins defined by the user -#define NUM_SWITCH_PINS (sizeof(switchPins)) -static const bool switchInvert[NUM_SWITCH_PINS] = { MQTTSWITCHINVERT}; -//Make settings in config file more readable -#define ON 1 -#define OFF 0 -static const bool switchDefaults[NUM_SWITCH_PINS] = { MQTTSWITCHDEFAULTS}; -#undef ON -#undef OFF - -class UsermodMqttSwitch: public Usermod -{ -private: - bool mqttInitialized; - bool switchState[NUM_SWITCH_PINS]; - -public: - UsermodMqttSwitch() : - mqttInitialized(false) - { - } - - void setup() - { - for (int pinNr = 0; pinNr < NUM_SWITCH_PINS; pinNr++) { - setState(pinNr, switchDefaults[pinNr]); - pinMode(switchPins[pinNr], OUTPUT); - } - } - - void loop() - { - if (!mqttInitialized) { - mqttInit(); - return; // Try again in next loop iteration - } - } - - void mqttInit() - { - if (!mqtt) - return; - mqtt->onMessage( - std::bind(&UsermodMqttSwitch::onMqttMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, - std::placeholders::_5, std::placeholders::_6)); - mqtt->onConnect(std::bind(&UsermodMqttSwitch::onMqttConnect, this, std::placeholders::_1)); - mqttInitialized = true; - } - - void onMqttConnect(bool sessionPresent); - - void onMqttMessage(char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total); - void updateState(uint8_t pinNr); - - void setState(uint8_t pinNr, bool active) - { - if (pinNr > NUM_SWITCH_PINS) - return; - switchState[pinNr] = active; - digitalWrite((char) switchPins[pinNr], (char) (switchInvert[pinNr] ? !active : active)); - updateState(pinNr); - } -}; - -inline void UsermodMqttSwitch::onMqttConnect(bool sessionPresent) -{ - if (mqttDeviceTopic[0] == 0) - return; - - for (int pinNr = 0; pinNr < NUM_SWITCH_PINS; pinNr++) { - char buf[128]; - StaticJsonDocument<1024> json; - sprintf(buf, "%s Switch %d", serverDescription, pinNr + 1); - json[F("name")] = buf; - - sprintf(buf, "%s/switch/%d", mqttDeviceTopic, pinNr); - json["~"] = buf; - strcat(buf, "/set"); - mqtt->subscribe(buf, 0); - - json[F("stat_t")] = "~/state"; - json[F("cmd_t")] = "~/set"; - json[F("pl_off")] = F("OFF"); - json[F("pl_on")] = F("ON"); - - char uid[16]; - sprintf(uid, "%s_sw%d", escapedMac.c_str(), pinNr); - json[F("unique_id")] = uid; - - strcpy(buf, mqttDeviceTopic); - strcat(buf, "/status"); - json[F("avty_t")] = buf; - json[F("pl_avail")] = F("online"); - json[F("pl_not_avail")] = F("offline"); - //TODO: dev - sprintf(buf, "homeassistant/switch/%s/config", uid); - char json_str[1024]; - size_t payload_size = serializeJson(json, json_str); - mqtt->publish(buf, 0, true, json_str, payload_size); - updateState(pinNr); - } -} - -inline void UsermodMqttSwitch::onMqttMessage(char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) -{ - //Note: Payload is not necessarily null terminated. Check "len" instead. - for (int pinNr = 0; pinNr < NUM_SWITCH_PINS; pinNr++) { - char buf[64]; - sprintf(buf, "%s/switch/%d/set", mqttDeviceTopic, pinNr); - if (strcmp(topic, buf) == 0) { - //Any string starting with "ON" is interpreted as ON, everything else as OFF - setState(pinNr, len >= 2 && payload[0] == 'O' && payload[1] == 'N'); - break; - } - } -} - -inline void UsermodMqttSwitch::updateState(uint8_t pinNr) -{ - if (!mqttInitialized) - return; - - if (pinNr > NUM_SWITCH_PINS) - return; - - char buf[64]; - sprintf(buf, "%s/switch/%d/state", mqttDeviceTopic, pinNr); - if (switchState[pinNr]) { - mqtt->publish(buf, 0, false, "ON"); - } else { - mqtt->publish(buf, 0, false, "OFF"); - } -} diff --git a/usermods/multi_relay/library.json b/usermods/multi_relay/library.json new file mode 100644 index 00000000..a5e5c693 --- /dev/null +++ b/usermods/multi_relay/library.json @@ -0,0 +1,4 @@ +{ + "name": "multi_relay", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/multi_relay/usermod_multi_relay.h b/usermods/multi_relay/multi_relay.cpp similarity index 99% rename from usermods/multi_relay/usermod_multi_relay.h rename to usermods/multi_relay/multi_relay.cpp index c4446c7a..f8b1f566 100644 --- a/usermods/multi_relay/usermod_multi_relay.h +++ b/usermods/multi_relay/multi_relay.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" #define COUNT_OF(x) ((sizeof(x)/sizeof(0[x])) / ((size_t)(!(sizeof(x) % sizeof(0[x]))))) @@ -842,3 +840,7 @@ const char MultiRelay::_pcfAddress[] PROGMEM = "PCF8574-address"; const char MultiRelay::_switch[] PROGMEM = "switch"; const char MultiRelay::_toggle[] PROGMEM = "toggle"; const char MultiRelay::_Command[] PROGMEM = "/command"; + + +static MultiRelay multi_relay; +REGISTER_USERMOD(multi_relay); \ No newline at end of file diff --git a/usermods/multi_relay/readme.md b/usermods/multi_relay/readme.md index eaa069ae..543809d8 100644 --- a/usermods/multi_relay/readme.md +++ b/usermods/multi_relay/readme.md @@ -41,9 +41,7 @@ When a relay is switched, a message is published: ## Usermod installation -1. Register the usermod by adding `#include "../usermods/multi_relay/usermod_multi_relay.h"` at the top and `UsermodManager::add(new MultiRelay());` at the bottom of `usermods_list.cpp`. -or -2. Use `#define USERMOD_MULTI_RELAY` in wled.h or `-D USERMOD_MULTI_RELAY` in your platformio.ini +Add `multi_relay` to the `custom_usermods` of your platformio.ini environment. You can override the default maximum number of relays (which is 4) by defining MULTI_RELAY_MAX_RELAYS. @@ -65,38 +63,6 @@ The following definitions should be a list of values (maximum number of entries ``` These 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`) -Example **usermods_list.cpp**: - -```cpp -#include "wled.h" -/* - * Register your v2 usermods here! - * (for v1 usermods using just usermod.cpp, you can ignore this file) - */ - -/* - * Add/uncomment your usermod filename here (and once more below) - * || || || - * \/ \/ \/ - */ -//#include "usermod_v2_example.h" -//#include "usermod_temperature.h" -#include "../usermods/usermod_multi_relay.h" - -void registerUsermods() -{ - /* - * Add your usermod class name here - * || || || - * \/ \/ \/ - */ - //UsermodManager::add(new MyExampleUsermod()); - //UsermodManager::add(new UsermodTemperature()); - UsermodManager::add(new MultiRelay()); - -} -``` - ## Configuration Usermod can be configured via the Usermods settings page. diff --git a/usermods/photoresistor_sensor_mqtt_v1/usermod.cpp b/usermods/photoresistor_sensor_mqtt_v1/usermod.cpp index fff7118f..bbbefc10 100644 --- a/usermods/photoresistor_sensor_mqtt_v1/usermod.cpp +++ b/usermods/photoresistor_sensor_mqtt_v1/usermod.cpp @@ -1,7 +1,7 @@ #include "wled.h" /* * This v1 usermod file allows you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality * EEPROM bytes 2750+ are reserved for your custom use case. (if you extend #define EEPSIZE in const.h) * If you just need 8 bytes, use 2551-2559 (you do not need to increase EEPSIZE) * diff --git a/usermods/pixels_dice_tray/library.json b/usermods/pixels_dice_tray/library.json new file mode 100644 index 00000000..ac1a7a07 --- /dev/null +++ b/usermods/pixels_dice_tray/library.json @@ -0,0 +1,8 @@ +{ + "name": "pixels_dice_tray", + "build": { "libArchive": false}, + "dependencies": { + "arduino-pixels-dice":"https://github.com/axlan/arduino-pixels-dice.git", + "BLE":"*" + } +} diff --git a/usermods/pixels_dice_tray/pixels_dice_tray.h b/usermods/pixels_dice_tray/pixels_dice_tray.cpp similarity index 99% rename from usermods/pixels_dice_tray/pixels_dice_tray.h rename to usermods/pixels_dice_tray/pixels_dice_tray.cpp index 61348ebb..73457c67 100644 --- a/usermods/pixels_dice_tray/pixels_dice_tray.h +++ b/usermods/pixels_dice_tray/pixels_dice_tray.cpp @@ -1,5 +1,3 @@ -#pragma once - #include // https://github.com/axlan/arduino-pixels-dice #include "wled.h" @@ -533,3 +531,7 @@ class PixelsDiceTrayUsermod : public Usermod { // extended. Your usermod will remain compatible as it does not need to // implement all methods from the Usermod base class! }; + + +static PixelsDiceTrayUsermod pixels_dice_tray; +REGISTER_USERMOD(pixels_dice_tray); \ No newline at end of file diff --git a/usermods/pixels_dice_tray/platformio_override.ini.sample b/usermods/pixels_dice_tray/platformio_override.ini.sample index b712f8b2..6b4fa776 100644 --- a/usermods/pixels_dice_tray/platformio_override.ini.sample +++ b/usermods/pixels_dice_tray/platformio_override.ini.sample @@ -102,7 +102,7 @@ lib_deps = ${esp32s3.lib_deps} # parallel. Also not clear exactly what difference between the ESP32 and the # ESP32S3 would be causing this, though they do run different BLE versions. # May be related to some of the issues discussed in: -# https://github.com/Aircoookie/WLED/issues/1382 +# https://github.com/wled-dev/WLED/issues/1382 ; [env:esp32dev_dice] ; extends = env:esp32dev ; build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=ESP32 diff --git a/usermods/pov_display/library.json.disabled b/usermods/pov_display/library.json.disabled new file mode 100644 index 00000000..2dd944a8 --- /dev/null +++ b/usermods/pov_display/library.json.disabled @@ -0,0 +1,7 @@ +{ + "name:": "pov_display", + "build": { "libArchive": false}, + "dependencies": { + "bitbank2/PNGdec":"^1.0.3" + } +} diff --git a/usermods/pov_display/usermod_pov_display.h b/usermods/pov_display/pov_display.cpp similarity index 96% rename from usermods/pov_display/usermod_pov_display.h rename to usermods/pov_display/pov_display.cpp index b1fc0dba..b2b91f7d 100644 --- a/usermods/pov_display/usermod_pov_display.h +++ b/usermods/pov_display/pov_display.cpp @@ -1,4 +1,3 @@ -#pragma once #include "wled.h" #include @@ -83,3 +82,7 @@ class PovDisplayUsermod : public Usermod void connected() {} }; + + +static PovDisplayUsermod pov_display; +REGISTER_USERMOD(pov_display); \ No newline at end of file diff --git a/usermods/project_cars_shiftlight/readme.md b/usermods/project_cars_shiftlight/readme.md index 433da430..338936a8 100644 --- a/usermods/project_cars_shiftlight/readme.md +++ b/usermods/project_cars_shiftlight/readme.md @@ -1,11 +1,11 @@ -### Shift Light for Project Cars +# Shift Light for Project Cars Turn your WLED lights into a rev light and shift indicator for Project Cars. It's easy to use. -1. Make sure your WLED device and your PC/console are on the same network and can talk to each other +_1._ Make sure your WLED device and your PC/console are on the same network and can talk to each other -2. Go to the gameplay settings menu in PCARS and enable UDP. There are 9 numbers you can choose from. This is the refresh rate. The lower the number, the better. However, you might run into problems at faster rates. +_2._ Go to the gameplay settings menu in PCARS and enable UDP. There are 9 numbers you can choose from. This is the refresh rate. The lower the number, the better. However, you might run into problems at faster rates. | Number | Updates/Second | | ------ | -------------- | @@ -19,5 +19,5 @@ It's easy to use. | 8 | 05 | | 9 | 1 | -3. Once you enter a race, WLED should automatically shift to PCARS mode. -4. Done. +_3._ Once you enter a race, WLED should automatically shift to PCARS mode. +_4._ Done. diff --git a/usermods/pwm_outputs/library.json b/usermods/pwm_outputs/library.json new file mode 100644 index 00000000..a01068bd --- /dev/null +++ b/usermods/pwm_outputs/library.json @@ -0,0 +1,4 @@ +{ + "name": "pwm_outputs", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/pwm_outputs/usermod_pwm_outputs.h b/usermods/pwm_outputs/pwm_outputs.cpp similarity index 98% rename from usermods/pwm_outputs/usermod_pwm_outputs.h rename to usermods/pwm_outputs/pwm_outputs.cpp index 09232f04..d94f1d84 100644 --- a/usermods/pwm_outputs/usermod_pwm_outputs.h +++ b/usermods/pwm_outputs/pwm_outputs.cpp @@ -1,4 +1,3 @@ -#pragma once #include "wled.h" #ifndef ESP32 @@ -219,3 +218,7 @@ class PwmOutputsUsermod : public Usermod { const char PwmOutputsUsermod::USERMOD_NAME[] PROGMEM = "PwmOutputs"; const char PwmOutputsUsermod::PWM_STATE_NAME[] PROGMEM = "pwm"; + + +static PwmOutputsUsermod pwm_outputs; +REGISTER_USERMOD(pwm_outputs); \ No newline at end of file diff --git a/usermods/quinled-an-penta/library.json b/usermods/quinled-an-penta/library.json new file mode 100644 index 00000000..fca9b0e3 --- /dev/null +++ b/usermods/quinled-an-penta/library.json @@ -0,0 +1,8 @@ +{ + "name": "quinled-an-penta", + "build": { "libArchive": false}, + "dependencies": { + "olikraus/U8g2":"~2.28.8", + "robtillaart/SHT85":"~0.3.3" + } +} diff --git a/usermods/quinled-an-penta/quinled-an-penta.h b/usermods/quinled-an-penta/quinled-an-penta.cpp similarity index 99% rename from usermods/quinled-an-penta/quinled-an-penta.h rename to usermods/quinled-an-penta/quinled-an-penta.cpp index e4467203..a3b452bf 100644 --- a/usermods/quinled-an-penta/quinled-an-penta.h +++ b/usermods/quinled-an-penta/quinled-an-penta.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "U8g2lib.h" #include "SHT85.h" #include "Wire.h" @@ -752,4 +750,7 @@ const unsigned char QuinLEDAnPentaUsermod::quinLedLogo[] PROGMEM = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, -}; \ No newline at end of file +}; + +static QuinLEDAnPentaUsermod quinled_an_penta; +REGISTER_USERMOD(quinled_an_penta); \ No newline at end of file diff --git a/usermods/quinled-an-penta/readme.md b/usermods/quinled-an-penta/readme.md index c1260d91..db1f72c4 100644 --- a/usermods/quinled-an-penta/readme.md +++ b/usermods/quinled-an-penta/readme.md @@ -5,29 +5,6 @@ The (un)official usermod to get the best out of the QuinLED-An-Penta (https://qu * "u8g2" by olikraus, v2.28 or higher: https://github.com/olikraus/u8g2 * "SHT85" by Rob Tillaart, v0.2 or higher: https://github.com/RobTillaart/SHT85 -## Usermod installation -Simply copy the below block (build task) to your `platformio_override.ini` and compile WLED using this new build task. Or use an existing one, add the buildflag `-D QUINLED_AN_PENTA` and the below library dependencies. - -ESP32 (**without** ethernet): -``` -[env:custom_esp32dev_usermod_quinled_an_penta] -extends = env:esp32dev -build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32 -D QUINLED_AN_PENTA -lib_deps = ${esp32.lib_deps} - olikraus/U8g2@~2.28.8 - robtillaart/SHT85@~0.2.0 -``` - -ESP32 (**with** ethernet): -``` -[env:custom_esp32dev_usermod_quinled_an_penta] -extends = env:esp32dev -build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_Ethernet -D WLED_USE_ETHERNET -D QUINLED_AN_PENTA -lib_deps = ${esp32.lib_deps} - olikraus/U8g2@~2.28.8 - robtillaart/SHT85@~0.2.0 -``` - ## Some words about the (optional) OLED This mod has been optimized for an SSD1306 driven 128x64 OLED. Using a smaller OLED or an OLED using a different driver will result in unexpected results. I highly recommend using these "two color monochromatic OLEDs", which have the first 16 pixels in a different color than the other 48, e.g. a yellow/blue OLED. diff --git a/usermods/readme.md b/usermods/readme.md index 8aa8d6ab..eefb64db 100644 --- a/usermods/readme.md +++ b/usermods/readme.md @@ -1,4 +1,4 @@ -### Usermods +# Usermods This folder serves as a repository for usermods (custom `usermod.cpp` files)! @@ -6,11 +6,11 @@ If you have created a usermod you believe is useful (for example to support a pa In order for other people to be able to have fun with your usermod, please keep these points in mind: -- Create a folder in this folder with a descriptive name (for example `usermod_ds18b20_temp_sensor_mqtt`) -- Include your custom files -- If your usermod requires changes to other WLED files, please write a `readme.md` outlining the steps one needs to take -- Create a pull request! -- If your feature is useful for the majority of WLED users, I will consider adding it to the base code! +* Create a folder in this folder with a descriptive name (for example `usermod_ds18b20_temp_sensor_mqtt`) +* Include your custom files +* If your usermod requires changes to other WLED files, please write a `readme.md` outlining the steps one needs to take +* Create a pull request! +* If your feature is useful for the majority of WLED users, I will consider adding it to the base code! While I do my best to not break too much, keep in mind that as WLED is updated, usermods might break. I am not actively maintaining any usermod in this directory, that is your responsibility as the creator of the usermod. diff --git a/usermods/rgb-rotary-encoder/library.json b/usermods/rgb-rotary-encoder/library.json new file mode 100644 index 00000000..fa8a65d1 --- /dev/null +++ b/usermods/rgb-rotary-encoder/library.json @@ -0,0 +1,7 @@ +{ + "name": "rgb-rotary-encoder", + "build": { "libArchive": false}, + "dependencies": { + "lennarthennigs/ESP Rotary":"^2.1.1" + } +} diff --git a/usermods/rgb-rotary-encoder/readme.md b/usermods/rgb-rotary-encoder/readme.md index ba5aad4d..abd8a812 100644 --- a/usermods/rgb-rotary-encoder/readme.md +++ b/usermods/rgb-rotary-encoder/readme.md @@ -8,30 +8,6 @@ https://user-images.githubusercontent.com/3090131/124680599-0180ab80-dec7-11eb-9 The actual / original code that controls the LED modes is from Adam Zeloof. I take no credit for it. I ported it to WLED, which involved replacing the LED library he used, (because WLED already has one, so no need to add another one) plus the rotary encoder library because it was not compatible with ESP, only Arduino. It was quite a bit more work than I hoped, but I got there eventually :) -## Requirements -* "ESP Rotary" by Lennart Hennigs, v1.5.0 or higher: https://github.com/LennartHennigs/ESPRotary - -## Usermod installation -Simply copy the below block (build task) to your `platformio_override.ini` and compile WLED using this new build task. Or use an existing one and add the buildflag `-D RGB_ROTARY_ENCODER`. - -ESP32: -``` -[env:custom_esp32dev_usermod_rgb_encoder_board] -extends = env:esp32dev -build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32 -D RGB_ROTARY_ENCODER -lib_deps = ${esp32.lib_deps} - lennarthennigs/ESP Rotary@^1.5.0 -``` - -ESP8266 / D1 Mini: -``` -[env:custom_d1_mini_usermod_rgb_encoder_board] -extends = env:d1_mini -build_flags = ${common.build_flags_esp8266} -D RGB_ROTARY_ENCODER -lib_deps = ${esp8266.lib_deps} - lennarthennigs/ESP Rotary@^1.5.0 -``` - ## How to connect the board to your ESP We'll need (minimum) three or (maximum) four GPIOs for the board: * "ea": reports the encoder direction diff --git a/usermods/rgb-rotary-encoder/rgb-rotary-encoder.h b/usermods/rgb-rotary-encoder/rgb-rotary-encoder.cpp similarity index 99% rename from usermods/rgb-rotary-encoder/rgb-rotary-encoder.h rename to usermods/rgb-rotary-encoder/rgb-rotary-encoder.cpp index b06037f5..7f9614b7 100644 --- a/usermods/rgb-rotary-encoder/rgb-rotary-encoder.h +++ b/usermods/rgb-rotary-encoder/rgb-rotary-encoder.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "ESPRotary.h" #include #include "wled.h" @@ -340,4 +338,7 @@ const char RgbRotaryEncoderUsermod::_ebIo[] PROGMEM = "eb-pin"; const char RgbRotaryEncoderUsermod::_ledMode[] PROGMEM = "LED-Mode"; const char RgbRotaryEncoderUsermod::_ledBrightness[] PROGMEM = "LED-Brightness"; const char RgbRotaryEncoderUsermod::_stepsPerClick[] PROGMEM = "Steps-per-Click"; -const char RgbRotaryEncoderUsermod::_incrementPerClick[] PROGMEM = "Increment-per-Click"; \ No newline at end of file +const char RgbRotaryEncoderUsermod::_incrementPerClick[] PROGMEM = "Increment-per-Click"; + +static RgbRotaryEncoderUsermod rgb_rotary_encoder; +REGISTER_USERMOD(rgb_rotary_encoder); \ No newline at end of file diff --git a/usermods/sd_card/library.json b/usermods/sd_card/library.json new file mode 100644 index 00000000..33e8f98f --- /dev/null +++ b/usermods/sd_card/library.json @@ -0,0 +1,4 @@ +{ + "name": "sd_card", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/sd_card/usermod_sd_card.h b/usermods/sd_card/sd_card.cpp similarity index 99% rename from usermods/sd_card/usermod_sd_card.h rename to usermods/sd_card/sd_card.cpp index da1999d9..4e68b97a 100644 --- a/usermods/sd_card/usermod_sd_card.h +++ b/usermods/sd_card/sd_card.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" // SD connected via MMC / SPI @@ -240,4 +238,7 @@ void listDir( const char * dirname, uint8_t levels){ } } -#endif \ No newline at end of file +#endif + +static UsermodSdCard sd_card; +REGISTER_USERMOD(sd_card); \ No newline at end of file diff --git a/usermods/sensors_to_mqtt/library.json b/usermods/sensors_to_mqtt/library.json new file mode 100644 index 00000000..977053da --- /dev/null +++ b/usermods/sensors_to_mqtt/library.json @@ -0,0 +1,10 @@ +{ + "name": "sensors_to_mqtt", + "build": { "libArchive": false}, + "dependencies": { + "adafruit/Adafruit BMP280 Library":"2.6.8", + "adafruit/Adafruit CCS811 Library":"1.1.3", + "adafruit/Adafruit Si7021 Library":"1.5.3", + "adafruit/Adafruit Unified Sensor":"^1.1.15" + } +} diff --git a/usermods/sensors_to_mqtt/readme.md b/usermods/sensors_to_mqtt/readme.md index d427d3e1..06162793 100644 --- a/usermods/sensors_to_mqtt/readme.md +++ b/usermods/sensors_to_mqtt/readme.md @@ -60,25 +60,6 @@ SCL_PIN = 5; SDA_PIN = 4; ``` -## Enable in WLED - -1. Copy `usermod_v2_SensorsToMqtt.h` into the `wled00` directory. -2. Add to `build_flags` in platformio.ini: - -``` - -D USERMOD_SENSORSTOMQTT -``` - -3. And add to `lib_deps` in platformio.ini: - -``` - adafruit/Adafruit BMP280 Library @ 2.1.0 - adafruit/Adafruit CCS811 Library @ 1.0.4 - adafruit/Adafruit Si7021 Library @ 1.4.0 -``` - -The #ifdefs in `usermods_list.cpp` should do the rest - # Credits - Aircoookie for making WLED diff --git a/usermods/sensors_to_mqtt/usermod_v2_SensorsToMqtt.h b/usermods/sensors_to_mqtt/sensors_to_mqtt.cpp similarity index 95% rename from usermods/sensors_to_mqtt/usermod_v2_SensorsToMqtt.h rename to usermods/sensors_to_mqtt/sensors_to_mqtt.cpp index 9b5bd8c8..5f7da97a 100644 --- a/usermods/sensors_to_mqtt/usermod_v2_SensorsToMqtt.h +++ b/usermods/sensors_to_mqtt/sensors_to_mqtt.cpp @@ -1,9 +1,3 @@ -#ifndef WLED_ENABLE_MQTT -#error "This user mod requires MQTT to be enabled." -#endif - -#pragma once - #include "wled.h" #include #include @@ -11,9 +5,13 @@ #include #include -Adafruit_BMP280 bmp; -Adafruit_Si7021 si7021; -Adafruit_CCS811 ccs811; +#ifdef WLED_DISABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + +static Adafruit_BMP280 bmp; +static Adafruit_Si7021 si7021; +static Adafruit_CCS811 ccs811; class UserMod_SensorsToMQTT : public Usermod { @@ -23,7 +21,7 @@ private: float SensorPressure = 0; float SensorTemperature = 0; float SensorHumidity = 0; - char *SensorIaq = "Unknown"; + const char *SensorIaq = "Unknown"; String mqttTemperatureTopic = ""; String mqttHumidityTopic = ""; String mqttPressureTopic = ""; @@ -117,7 +115,7 @@ private: /** * Credits: Bouke_Regnerus @ https://community.home-assistant.io/t/example-indoor-air-quality-text-sensor-using-ccs811-sensor/125854 */ - char *_getIaqIndex(float humidity, int tvoc, int eco2) + const char *_getIaqIndex(float humidity, int tvoc, int eco2) { int iaq_index = 0; @@ -216,6 +214,7 @@ private: { return "Excellent"; } + return "Unknown"; } public: @@ -276,3 +275,7 @@ public: } } }; + + +static UserMod_SensorsToMQTT sensors_to_mqtt; +REGISTER_USERMOD(sensors_to_mqtt); \ No newline at end of file diff --git a/usermods/seven_segment_display/library.json b/usermods/seven_segment_display/library.json new file mode 100644 index 00000000..f78aad87 --- /dev/null +++ b/usermods/seven_segment_display/library.json @@ -0,0 +1,4 @@ +{ + "name": "seven_segment_display", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/seven_segment_display/usermod_v2_seven_segment_display.h b/usermods/seven_segment_display/seven_segment_display.cpp similarity index 99% rename from usermods/seven_segment_display/usermod_v2_seven_segment_display.h rename to usermods/seven_segment_display/seven_segment_display.cpp index 20fef15d..13a6306b 100644 --- a/usermods/seven_segment_display/usermod_v2_seven_segment_display.h +++ b/usermods/seven_segment_display/seven_segment_display.cpp @@ -1,11 +1,9 @@ -#ifndef WLED_ENABLE_MQTT +#include "wled.h" + +#ifdef WLED_DISABLE_MQTT #error "This user mod requires MQTT to be enabled." #endif -#pragma once - -#include "wled.h" - class SevenSegmentDisplay : public Usermod { @@ -498,4 +496,7 @@ const char SevenSegmentDisplay::_str_timeEnabled[] PROGMEM = "timeEnabled"; const char SevenSegmentDisplay::_str_scrollSpd[] PROGMEM = "scrollSpd"; const char SevenSegmentDisplay::_str_displayMask[] PROGMEM = "displayMask"; const char SevenSegmentDisplay::_str_displayMsg[] PROGMEM = "displayMsg"; -const char SevenSegmentDisplay::_str_sevenSeg[] PROGMEM = "sevenSeg"; \ No newline at end of file +const char SevenSegmentDisplay::_str_sevenSeg[] PROGMEM = "sevenSeg"; + +static SevenSegmentDisplay seven_segment_display; +REGISTER_USERMOD(seven_segment_display); \ No newline at end of file diff --git a/usermods/seven_segment_display_reloaded/library.json b/usermods/seven_segment_display_reloaded/library.json new file mode 100644 index 00000000..1b7d0687 --- /dev/null +++ b/usermods/seven_segment_display_reloaded/library.json @@ -0,0 +1,7 @@ +{ + "name": "seven_segment_display_reloaded", + "build": { + "libArchive": false, + "extraScript": "setup_deps.py" + } +} \ No newline at end of file diff --git a/usermods/seven_segment_display_reloaded/readme.md b/usermods/seven_segment_display_reloaded/readme.md index a3398c3e..94788df7 100644 --- a/usermods/seven_segment_display_reloaded/readme.md +++ b/usermods/seven_segment_display_reloaded/readme.md @@ -9,7 +9,7 @@ Very loosely based on the existing usermod "seven segment display". Add the compile-time option `-D USERMOD_SSDR` to your `platformio.ini` (or `platformio_override.ini`) or use `#define USERMOD_SSDR` in `my_config.h`. -For the auto brightness option, the usermod SN_Photoresistor has to be installed as well. See SN_Photoresistor/readme.md for instructions. +For the auto brightness option, the usermod SN_Photoresistor or BH1750_V2 has to be installed as well. See SN_Photoresistor/readme.md or BH1750_V2/readme.md for instructions. ## Settings All settings can be controlled via the usermod settings page. @@ -28,10 +28,10 @@ Enables the blinking colon(s) if they are defined Shows the leading zero of the hour if it exists (i.e. shows `07` instead of `7`) ### enable-auto-brightness -Enables the auto brightness feature. Can be used only when the usermod SN_Photoresistor is installed. +Enables the auto brightness feature. Can be used only when the usermods SN_Photoresistor or BH1750_V2 are installed. ### auto-brightness-min / auto-brightness-max -The lux value calculated from usermod SN_Photoresistor will be mapped to the values defined here. +The lux value calculated from usermod SN_Photoresistor or BH1750_V2 will be mapped to the values defined here. The mapping, 0 - 1000 lux, will be mapped to auto-brightness-min and auto-brightness-max WLED current protection will override the calculated value if it is too high. diff --git a/usermods/seven_segment_display_reloaded/setup_deps.py b/usermods/seven_segment_display_reloaded/setup_deps.py new file mode 100644 index 00000000..1c51accc --- /dev/null +++ b/usermods/seven_segment_display_reloaded/setup_deps.py @@ -0,0 +1,10 @@ +from platformio.package.meta import PackageSpec +Import('env') + + +libs = [PackageSpec(lib).name for lib in env.GetProjectOption("lib_deps",[])] +# Check for partner usermods +if "SN_Photoresistor" in libs: + env.Append(CPPDEFINES=[("USERMOD_SN_PHOTORESISTOR")]) +if any(mod in ("BH1750_v2", "BH1750") for mod in libs): + env.Append(CPPDEFINES=[("USERMOD_BH1750")]) diff --git a/usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h b/usermods/seven_segment_display_reloaded/seven_segment_display_reloaded.cpp similarity index 94% rename from usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h rename to usermods/seven_segment_display_reloaded/seven_segment_display_reloaded.cpp index 1436f8fc..893e061b 100644 --- a/usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h +++ b/usermods/seven_segment_display_reloaded/seven_segment_display_reloaded.cpp @@ -1,10 +1,14 @@ -#ifndef WLED_ENABLE_MQTT -#error "This user mod requires MQTT to be enabled." +#include "wled.h" +#ifdef USERMOD_SN_PHOTORESISTOR + #include "SN_Photoresistor.h" +#endif +#ifdef USERMOD_BH1750 + #include "BH1750_v2.h" #endif -#pragma once - -#include "wled.h" +#ifdef WLED_DISABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif class UsermodSSDR : public Usermod { @@ -97,6 +101,11 @@ private: #else void* ptr = nullptr; #endif +#ifdef USERMOD_BH1750 + Usermod_BH1750* bh1750 = nullptr; +#else + void* bh1750 = nullptr; +#endif void _overlaySevenSegmentDraw() { int displayMaskLen = static_cast(umSSDRDisplayMask.length()); @@ -387,6 +396,9 @@ public: #ifdef USERMOD_SN_PHOTORESISTOR ptr = (Usermod_SN_Photoresistor*) UsermodManager::lookup(USERMOD_ID_SN_PHOTORESISTOR); #endif + #ifdef USERMOD_BH1750 + bh1750 = (Usermod_BH1750*) UsermodManager::lookup(USERMOD_ID_BH1750); + #endif DEBUG_PRINTLN(F("Setup done")); } @@ -410,6 +422,20 @@ public: umSSDRLastRefresh = millis(); } #endif + #ifdef USERMOD_BH1750 + if(bri != 0 && umSSDREnableLDR && (millis() - umSSDRLastRefresh > umSSDRResfreshTime)) { + if (bh1750 != nullptr) { + float lux = bh1750->getIlluminance(); + uint16_t brightness = map(lux, 0, 1000, umSSDRBrightnessMin, umSSDRBrightnessMax); + if (bri != brightness) { + DEBUG_PRINTF("Adjusting brightness based on lux value: %.2f lx, new brightness: %d\n", lux, brightness); + bri = brightness; + stateUpdated(1); + } + } + umSSDRLastRefresh = millis(); + } + #endif } void handleOverlayDraw() { @@ -571,3 +597,7 @@ const char UsermodSSDR::_str_years[] PROGMEM = "LED-Numbers-Year"; const char UsermodSSDR::_str_ldrEnabled[] PROGMEM = "enable-auto-brightness"; const char UsermodSSDR::_str_minBrightness[] PROGMEM = "auto-brightness-min"; const char UsermodSSDR::_str_maxBrightness[] PROGMEM = "auto-brightness-max"; + + +static UsermodSSDR seven_segment_display_reloaded; +REGISTER_USERMOD(seven_segment_display_reloaded); \ No newline at end of file diff --git a/usermods/sht/ShtUsermod.h b/usermods/sht/ShtUsermod.h new file mode 100644 index 00000000..5dd83f46 --- /dev/null +++ b/usermods/sht/ShtUsermod.h @@ -0,0 +1,71 @@ +#pragma once +#include "wled.h" + +#ifdef WLED_DISABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + +#define USERMOD_SHT_TYPE_SHT30 0 +#define USERMOD_SHT_TYPE_SHT31 1 +#define USERMOD_SHT_TYPE_SHT35 2 +#define USERMOD_SHT_TYPE_SHT85 3 + +class SHT; + +class ShtUsermod : public Usermod +{ + private: + bool enabled = false; // Is usermod enabled or not + bool firstRunDone = false; // Remembers if the first config load run had been done + bool initDone = false; // Remembers if the mod has been completely initialised + bool haMqttDiscovery = false; // Is MQTT discovery enabled or not + bool haMqttDiscoveryDone = false; // Remembers if we already published the HA discovery topics + + // SHT vars + SHT *shtTempHumidSensor = nullptr; // Instance of SHT lib + byte shtType = 0; // SHT sensor type to be used. Default: SHT30 + byte unitOfTemp = 0; // Temperature unit to be used. Default: Celsius (0 = Celsius, 1 = Fahrenheit) + bool shtInitDone = false; // Remembers if SHT sensor has been initialised + bool shtReadDataSuccess = false; // Did we have a successful data read and is a valid temperature and humidity available? + const byte shtI2cAddress = 0x44; // i2c address of the sensor. 0x44 is the default for all SHT sensors. Change this, if needed + unsigned long shtLastTimeUpdated = 0; // Remembers when we read data the last time + bool shtDataRequested = false; // Reading data is done async. This remembers if we asked the sensor to read data + float shtCurrentTempC = 0.0f; // Last read temperature in Celsius + float shtCurrentHumidity = 0.0f; // Last read humidity in RH% + + + void initShtTempHumiditySensor(); + void cleanupShtTempHumiditySensor(); + void cleanup(); + inline bool isShtReady() { return shtInitDone; } // Checks if the SHT sensor has been initialised. + + void publishTemperatureAndHumidityViaMqtt(); + void publishHomeAssistantAutodiscovery(); + void appendDeviceToMqttDiscoveryMessage(JsonDocument& root); + + public: + // Strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _shtType[]; + static const char _unitOfTemp[]; + static const char _haMqttDiscovery[]; + + void setup(); + void loop(); + void onMqttConnect(bool sessionPresent); + void appendConfigData(); + void addToConfig(JsonObject &root); + bool readFromConfig(JsonObject &root); + void addToJsonInfo(JsonObject& root); + + bool isEnabled() { return enabled; } + + float getTemperature(); + float getTemperatureC() { return roundf(shtCurrentTempC * 10.0f) / 10.0f; } + float getTemperatureF() { return (getTemperatureC() * 1.8f) + 32.0f; } + float getHumidity() { return roundf(shtCurrentHumidity * 10.0f) / 10.0f; } + const char* getUnitString(); + + uint16_t getId() { return USERMOD_ID_SHT; } +}; diff --git a/usermods/sht/library.json b/usermods/sht/library.json new file mode 100644 index 00000000..0916e9a3 --- /dev/null +++ b/usermods/sht/library.json @@ -0,0 +1,7 @@ +{ + "name": "sht", + "build": { "libArchive": false }, + "dependencies": { + "robtillaart/SHT85": "~0.3.3" + } +} \ No newline at end of file diff --git a/usermods/sht/readme.md b/usermods/sht/readme.md index 0337805b..4470c883 100644 --- a/usermods/sht/readme.md +++ b/usermods/sht/readme.md @@ -1,40 +1,43 @@ # SHT + Usermod to support various SHT i2c sensors like the SHT30, SHT31, SHT35 and SHT85 ## Requirements -* "SHT85" by Rob Tillaart, v0.2 or higher: https://github.com/RobTillaart/SHT85 + +* "SHT85" by Rob Tillaart, v0.2 or higher: ## Usermod installation -Simply copy the below block (build task) to your `platformio_override.ini` and compile WLED using this new build task. Or use an existing one, add the buildflag `-D USERMOD_SHT` and the below library dependencies. + +Simply copy the below block (build task) to your `platformio_override.ini` and compile WLED using this new build task. Or use an existing one, add the custom_usermod `sht`. ESP32: -``` + +```ini [env:custom_esp32dev_usermod_sht] extends = env:esp32dev -build_flags = ${common.build_flags_esp32} - -D USERMOD_SHT -lib_deps = ${esp32.lib_deps} - robtillaart/SHT85@~0.3.3 +custom_usermods = ${env:esp32dev.custom_usermods} sht ``` ESP8266: -``` + +```ini [env:custom_d1_mini_usermod_sht] extends = env:d1_mini -build_flags = ${common.build_flags_esp8266} - -D USERMOD_SHT -lib_deps = ${esp8266.lib_deps} - robtillaart/SHT85@~0.3.3 +custom_usermods = ${env:d1_mini.custom_usermods} sht ``` ## MQTT Discovery for Home Assistant + If you're using Home Assistant and want to have the temperature and humidity available as entities in HA, you can tick the "Add-To-Home-Assistant-MQTT-Discovery" option in the usermod settings. If you have an MQTT broker configured under "Sync Settings" and it is connected, the mod will publish the auto discovery message to your broker and HA will instantly find it and create an entity each for the temperature and humidity. ### Publishing readings via MQTT + Regardless of having MQTT discovery ticked or not, the mod will always report temperature and humidity to the WLED MQTT topic of that instance, if you have a broker configured and it's connected. ## Configuration + Navigate to the "Config" and then to the "Usermods" section. If you compiled WLED with `-D USERMOD_SHT`, you will see the config for it there: + * SHT-Type: * What it does: Select the SHT sensor type you want to use * Possible values: SHT30, SHT31, SHT35, SHT85 @@ -49,8 +52,11 @@ Navigate to the "Config" and then to the "Usermods" section. If you compiled WLE * Default: Disabled ## Change log + 2022-12 + * First implementation. ## Credits -ezcGman | Andy: Find me on the Intermit.Tech (QuinLED) Discord server: https://discord.gg/WdbAauG + +ezcGman | Andy: Find me on the Intermit.Tech (QuinLED) Discord server: diff --git a/usermods/sht/usermod_sht.h b/usermods/sht/sht.cpp similarity index 79% rename from usermods/sht/usermod_sht.h rename to usermods/sht/sht.cpp index f10c78a2..e1eb9e88 100644 --- a/usermods/sht/usermod_sht.h +++ b/usermods/sht/sht.cpp @@ -1,74 +1,6 @@ -#ifndef WLED_ENABLE_MQTT -#error "This user mod requires MQTT to be enabled." -#endif - -#pragma once - +#include "ShtUsermod.h" #include "SHT85.h" -#define USERMOD_SHT_TYPE_SHT30 0 -#define USERMOD_SHT_TYPE_SHT31 1 -#define USERMOD_SHT_TYPE_SHT35 2 -#define USERMOD_SHT_TYPE_SHT85 3 - -class ShtUsermod : public Usermod -{ - private: - bool enabled = false; // Is usermod enabled or not - bool firstRunDone = false; // Remembers if the first config load run had been done - bool initDone = false; // Remembers if the mod has been completely initialised - bool haMqttDiscovery = false; // Is MQTT discovery enabled or not - bool haMqttDiscoveryDone = false; // Remembers if we already published the HA discovery topics - - // SHT vars - SHT *shtTempHumidSensor = nullptr; // Instance of SHT lib - byte shtType = 0; // SHT sensor type to be used. Default: SHT30 - byte unitOfTemp = 0; // Temperature unit to be used. Default: Celsius (0 = Celsius, 1 = Fahrenheit) - bool shtInitDone = false; // Remembers if SHT sensor has been initialised - bool shtReadDataSuccess = false; // Did we have a successful data read and is a valid temperature and humidity available? - const byte shtI2cAddress = 0x44; // i2c address of the sensor. 0x44 is the default for all SHT sensors. Change this, if needed - unsigned long shtLastTimeUpdated = 0; // Remembers when we read data the last time - bool shtDataRequested = false; // Reading data is done async. This remembers if we asked the sensor to read data - float shtCurrentTempC = 0.0f; // Last read temperature in Celsius - float shtCurrentHumidity = 0.0f; // Last read humidity in RH% - - - void initShtTempHumiditySensor(); - void cleanupShtTempHumiditySensor(); - void cleanup(); - inline bool isShtReady() { return shtInitDone; } // Checks if the SHT sensor has been initialised. - - void publishTemperatureAndHumidityViaMqtt(); - void publishHomeAssistantAutodiscovery(); - void appendDeviceToMqttDiscoveryMessage(JsonDocument& root); - - public: - // Strings to reduce flash memory usage (used more than twice) - static const char _name[]; - static const char _enabled[]; - static const char _shtType[]; - static const char _unitOfTemp[]; - static const char _haMqttDiscovery[]; - - void setup(); - void loop(); - void onMqttConnect(bool sessionPresent); - void appendConfigData(); - void addToConfig(JsonObject &root); - bool readFromConfig(JsonObject &root); - void addToJsonInfo(JsonObject& root); - - bool isEnabled() { return enabled; } - - float getTemperature(); - float getTemperatureC() { return roundf(shtCurrentTempC * 10.0f) / 10.0f; } - float getTemperatureF() { return (getTemperatureC() * 1.8f) + 32.0f; } - float getHumidity() { return roundf(shtCurrentHumidity * 10.0f) / 10.0f; } - const char* getUnitString(); - - uint16_t getId() { return USERMOD_ID_SHT; } -}; - // Strings to reduce flash memory usage (used more than twice) const char ShtUsermod::_name[] PROGMEM = "SHT-Sensor"; const char ShtUsermod::_enabled[] PROGMEM = "Enabled"; @@ -477,4 +409,7 @@ float ShtUsermod::getTemperature() { */ const char* ShtUsermod::getUnitString() { return unitOfTemp ? "°F" : "°C"; -} \ No newline at end of file +} + +static ShtUsermod sht; +REGISTER_USERMOD(sht); diff --git a/usermods/smartnest/library.json b/usermods/smartnest/library.json new file mode 100644 index 00000000..3e9ea63a --- /dev/null +++ b/usermods/smartnest/library.json @@ -0,0 +1,4 @@ +{ + "name": "smartnest", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/smartnest/usermod_smartnest.h b/usermods/smartnest/smartnest.cpp similarity index 97% rename from usermods/smartnest/usermod_smartnest.h rename to usermods/smartnest/smartnest.cpp index 92d524c8..d0cb92dc 100644 --- a/usermods/smartnest/usermod_smartnest.h +++ b/usermods/smartnest/smartnest.cpp @@ -1,11 +1,9 @@ -#ifndef WLED_ENABLE_MQTT +#include "wled.h" + +#ifdef WLED_DISABLE_MQTT #error "This user mod requires MQTT to be enabled." #endif -#pragma once - -#include "wled.h" - class Smartnest : public Usermod { private: @@ -49,7 +47,7 @@ private: void setColor(int r, int g, int b) { - strip.setColor(0, r, g, b); + strip.getMainSegment().setColor(0, RGBW32(r, g, b, 0)); stateUpdated(CALL_MODE_DIRECT_CHANGE); char msg[18] {}; sprintf(msg, "rgb(%d,%d,%d)", r, g, b); @@ -203,3 +201,7 @@ public: } } }; + + +static Smartnest smartnest; +REGISTER_USERMOD(smartnest); \ No newline at end of file diff --git a/usermods/stairway_wipe_basic/library.json b/usermods/stairway_wipe_basic/library.json new file mode 100644 index 00000000..f7d353b5 --- /dev/null +++ b/usermods/stairway_wipe_basic/library.json @@ -0,0 +1,4 @@ +{ + "name": "stairway_wipe_basic", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h b/usermods/stairway_wipe_basic/stairway_wipe_basic.cpp similarity index 96% rename from usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h rename to usermods/stairway_wipe_basic/stairway_wipe_basic.cpp index 707479df..cddd655d 100644 --- a/usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h +++ b/usermods/stairway_wipe_basic/stairway_wipe_basic.cpp @@ -2,7 +2,7 @@ /* * Usermods allow you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality * * This is Stairway-Wipe as a v2 usermod. * @@ -126,3 +126,7 @@ void setup() { //More methods can be added in the future, this example will then be extended. //Your usermod will remain compatible as it does not need to implement all methods from the Usermod base class! }; + + +static StairwayWipeUsermod stairway_wipe_basic; +REGISTER_USERMOD(stairway_wipe_basic); \ No newline at end of file diff --git a/usermods/stairway_wipe_basic/wled06_usermod.ino b/usermods/stairway_wipe_basic/wled06_usermod.ino deleted file mode 100644 index dc2159ee..00000000 --- a/usermods/stairway_wipe_basic/wled06_usermod.ino +++ /dev/null @@ -1,111 +0,0 @@ -/* - * This file allows you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality - * EEPROM bytes 2750+ are reserved for your custom use case. (if you extend #define EEPSIZE in wled_eeprom.h) - * bytes 2400+ are currently ununsed, but might be used for future wled features - */ - -//Use userVar0 and userVar1 (API calls &U0=,&U1=, uint16_t) - -byte wipeState = 0; //0: inactive 1: wiping 2: solid -unsigned long timeStaticStart = 0; -uint16_t previousUserVar0 = 0; - -//comment this out if you want the turn off effect to be just fading out instead of reverse wipe -#define STAIRCASE_WIPE_OFF - -//gets called once at boot. Do all initialization that doesn't depend on network here -void userSetup() -{ - //setup PIR sensor here, if needed -} - -//gets called every time WiFi is (re-)connected. Initialize own network interfaces here -void userConnected() -{ - -} - -//loop. You can use "if (WLED_CONNECTED)" to check for successful connection -void userLoop() -{ - //userVar0 (U0 in HTTP API): - //has to be set to 1 if movement is detected on the PIR that is the same side of the staircase as the ESP8266 - //has to be set to 2 if movement is detected on the PIR that is the opposite side - //can be set to 0 if no movement is detected. Otherwise LEDs will turn off after a configurable timeout (userVar1 seconds) - - if (userVar0 > 0) - { - if ((previousUserVar0 == 1 && userVar0 == 2) || (previousUserVar0 == 2 && userVar0 == 1)) wipeState = 3; //turn off if other PIR triggered - previousUserVar0 = userVar0; - - if (wipeState == 0) { - startWipe(); - wipeState = 1; - } else if (wipeState == 1) { //wiping - uint32_t cycleTime = 360 + (255 - effectSpeed)*75; //this is how long one wipe takes (minus 25 ms to make sure we switch in time) - if (millis() + strip.timebase > (cycleTime - 25)) { //wipe complete - effectCurrent = FX_MODE_STATIC; - timeStaticStart = millis(); - colorUpdated(CALL_MODE_NOTIFICATION); - wipeState = 2; - } - } else if (wipeState == 2) { //static - if (userVar1 > 0) //if U1 is not set, the light will stay on until second PIR or external command is triggered - { - if (millis() - timeStaticStart > userVar1*1000) wipeState = 3; - } - } else if (wipeState == 3) { //switch to wipe off - #ifdef STAIRCASE_WIPE_OFF - effectCurrent = FX_MODE_COLOR_WIPE; - strip.timebase = 360 + (255 - effectSpeed)*75 - millis(); //make sure wipe starts fully lit - colorUpdated(CALL_MODE_NOTIFICATION); - wipeState = 4; - #else - turnOff(); - #endif - } else { //wiping off - if (millis() + strip.timebase > (725 + (255 - effectSpeed)*150)) turnOff(); //wipe complete - } - } else { - wipeState = 0; //reset for next time - if (previousUserVar0) { - #ifdef STAIRCASE_WIPE_OFF - userVar0 = previousUserVar0; - wipeState = 3; - #else - turnOff(); - #endif - } - previousUserVar0 = 0; - } -} - -void startWipe() -{ - bri = briLast; //turn on - transitionDelayTemp = 0; //no transition - effectCurrent = FX_MODE_COLOR_WIPE; - strip.resetTimebase(); //make sure wipe starts from beginning - - //set wipe direction - Segment& seg = strip.getSegment(0); - bool doReverse = (userVar0 == 2); - seg.setOption(1, doReverse); - - colorUpdated(CALL_MODE_NOTIFICATION); -} - -void turnOff() -{ - #ifdef STAIRCASE_WIPE_OFF - transitionDelayTemp = 0; //turn off immediately after wipe completed - #else - transitionDelayTemp = 4000; //fade out slowly - #endif - bri = 0; - stateUpdated(CALL_MODE_NOTIFICATION); - wipeState = 0; - userVar0 = 0; - previousUserVar0 = 0; -} diff --git a/usermods/user_fx/README.md b/usermods/user_fx/README.md new file mode 100644 index 00000000..8dc1d128 --- /dev/null +++ b/usermods/user_fx/README.md @@ -0,0 +1,4 @@ +# Usermod user FX + +This Usermod is a common place to put various user's LED effects. + diff --git a/usermods/user_fx/library.json b/usermods/user_fx/library.json new file mode 100644 index 00000000..83f6358b --- /dev/null +++ b/usermods/user_fx/library.json @@ -0,0 +1,4 @@ +{ + "name": "user_fx", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp new file mode 100644 index 00000000..7d8fc308 --- /dev/null +++ b/usermods/user_fx/user_fx.cpp @@ -0,0 +1,116 @@ +#include "wled.h" + +// for information how FX metadata strings work see https://kno.wled.ge/interfaces/json-api/#effect-metadata + +// static effect, used if an effect fails to initialize +static uint16_t mode_static(void) { + SEGMENT.fill(SEGCOLOR(0)); + return strip.isOffRefreshRequired() ? FRAMETIME : 350; +} + +///////////////////////// +// User FX functions // +///////////////////////// + +// Diffusion Fire: fire effect intended for 2D setups smaller than 16x16 +static uint16_t mode_diffusionfire(void) { + if (!strip.isMatrix || !SEGMENT.is2D()) + return mode_static(); // not a 2D set-up + + const int cols = SEG_W; + const int rows = SEG_H; + const auto XY = [&](int x, int y) { return x + y * cols; }; + + const uint8_t refresh_hz = map(SEGMENT.speed, 0, 255, 20, 80); + const unsigned refresh_ms = 1000 / refresh_hz; + const int16_t diffusion = map(SEGMENT.custom1, 0, 255, 0, 100); + const uint8_t spark_rate = SEGMENT.intensity; + const uint8_t turbulence = SEGMENT.custom2; + + unsigned dataSize = SEGMENT.length(); // allocate persistent data for heat value for each pixel + if (!SEGENV.allocateData(dataSize)) + return mode_static(); // allocation failed + + if (SEGENV.call == 0) { + SEGMENT.fill(BLACK); + SEGENV.step = 0; + } + + if ((strip.now - SEGENV.step) >= refresh_ms) { + uint8_t tmp_row[cols]; + SEGENV.step = strip.now; + // scroll up + for (unsigned y = 1; y < rows; y++) + for (unsigned x = 0; x < cols; x++) { + unsigned src = XY(x, y); + unsigned dst = XY(x, y - 1); + SEGMENT.data[dst] = SEGMENT.data[src]; + } + + if (hw_random8() > turbulence) { + // create new sparks at bottom row + for (unsigned x = 0; x < cols; x++) { + uint8_t p = hw_random8(); + if (p < spark_rate) { + unsigned dst = XY(x, rows - 1); + SEGMENT.data[dst] = 255; + } + } + } + + // diffuse + for (unsigned y = 0; y < rows; y++) { + for (unsigned x = 0; x < cols; x++) { + unsigned v = SEGMENT.data[XY(x, y)]; + if (x > 0) { + v += SEGMENT.data[XY(x - 1, y)]; + } + if (x < (cols - 1)) { + v += SEGMENT.data[XY(x + 1, y)]; + } + tmp_row[x] = min(255, (int)(v * 100 / (300 + diffusion))); + } + + for (unsigned x = 0; x < cols; x++) { + SEGMENT.data[XY(x, y)] = tmp_row[x]; + if (SEGMENT.check1) { + uint32_t color = ColorFromPalette(SEGPALETTE, tmp_row[x], 255, LINEARBLEND_NOWRAP); + SEGMENT.setPixelColorXY(x, y, color); + } else { + uint32_t color = SEGCOLOR(0); + SEGMENT.setPixelColorXY(x, y, color_fade(color, tmp_row[x])); + } + } + } + } + return FRAMETIME; +} +static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spark rate,Diffusion Speed,Turbulence,,Use palette;;Color;;2;pal=35"; + + +///////////////////// +// UserMod Class // +///////////////////// + +class UserFxUsermod : public Usermod { + private: + public: + void setup() override { + strip.addEffect(255, &mode_diffusionfire, _data_FX_MODE_DIFFUSIONFIRE); + + //////////////////////////////////////// + // add your effect function(s) here // + //////////////////////////////////////// + + // use id=255 for all custom user FX (the final id is assigned when adding the effect) + + // strip.addEffect(255, &mode_your_effect, _data_FX_MODE_YOUR_EFFECT); + // strip.addEffect(255, &mode_your_effect2, _data_FX_MODE_YOUR_EFFECT2); + // strip.addEffect(255, &mode_your_effect3, _data_FX_MODE_YOUR_EFFECT3); + } + void loop() override {} // nothing to do in the loop + uint16_t getId() override { return USERMOD_ID_USER_FX; } +}; + +static UserFxUsermod user_fx; +REGISTER_USERMOD(user_fx); diff --git a/usermods/usermod_rotary_brightness_color/README.md b/usermods/usermod_rotary_brightness_color/README.md index 06a3a0f8..feb778dd 100644 --- a/usermods/usermod_rotary_brightness_color/README.md +++ b/usermods/usermod_rotary_brightness_color/README.md @@ -37,6 +37,7 @@ Open Usermod Settings in WLED to change settings: No special requirements. ## Change Log - -2021-07 -* Upgraded to work with the latest WLED code, and make settings configurable in Usermod Settings +- 2021-07
+Upgraded to work with the latest WLED code, and make settings configurable in Usermod Settings +- 2025-03
+Upgraded to work with the latest WLED code diff --git a/usermods/usermod_rotary_brightness_color/library.json b/usermods/usermod_rotary_brightness_color/library.json new file mode 100644 index 00000000..4f7a146a --- /dev/null +++ b/usermods/usermod_rotary_brightness_color/library.json @@ -0,0 +1,4 @@ +{ + "name": "usermod_rotary_brightness_color", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.h b/usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.cpp similarity index 91% rename from usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.h rename to usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.cpp index 85a9a160..0a485152 100644 --- a/usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.h +++ b/usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" //v2 usermod that allows to change brightness and color using a rotary encoder, @@ -95,9 +93,9 @@ public: } else { - fastled_col.red = col[0]; - fastled_col.green = col[1]; - fastled_col.blue = col[2]; + fastled_col.red = colPri[0]; + fastled_col.green = colPri[1]; + fastled_col.blue = colPri[2]; prim_hsv = rgb2hsv_approximate(fastled_col); new_val = (int16_t)prim_hsv.h + fadeAmount; if (new_val > 255) @@ -106,9 +104,9 @@ public: new_val += 255; // roll-over if smaller than 0 prim_hsv.h = (byte)new_val; hsv2rgb_rainbow(prim_hsv, fastled_col); - col[0] = fastled_col.red; - col[1] = fastled_col.green; - col[2] = fastled_col.blue; + colPri[0] = fastled_col.red; + colPri[1] = fastled_col.green; + colPri[2] = fastled_col.blue; } } else if (Enc_B == LOW) @@ -120,9 +118,9 @@ public: } else { - fastled_col.red = col[0]; - fastled_col.green = col[1]; - fastled_col.blue = col[2]; + fastled_col.red = colPri[0]; + fastled_col.green = colPri[1]; + fastled_col.blue = colPri[2]; prim_hsv = rgb2hsv_approximate(fastled_col); new_val = (int16_t)prim_hsv.h - fadeAmount; if (new_val > 255) @@ -131,9 +129,9 @@ public: new_val += 255; // roll-over if smaller than 0 prim_hsv.h = (byte)new_val; hsv2rgb_rainbow(prim_hsv, fastled_col); - col[0] = fastled_col.red; - col[1] = fastled_col.green; - col[2] = fastled_col.blue; + colPri[0] = fastled_col.red; + colPri[1] = fastled_col.green; + colPri[2] = fastled_col.blue; } } //call for notifier -> 0: init 1: direct change 2: button 3: notification 4: nightlight 5: other (No notification) @@ -187,3 +185,7 @@ public: return configComplete; } }; + + +static RotaryEncoderBrightnessColor usermod_rotary_brightness_color; +REGISTER_USERMOD(usermod_rotary_brightness_color); \ No newline at end of file diff --git a/usermods/usermod_v2_HttpPullLightControl/library.json b/usermods/usermod_v2_HttpPullLightControl/library.json new file mode 100644 index 00000000..870753b9 --- /dev/null +++ b/usermods/usermod_v2_HttpPullLightControl/library.json @@ -0,0 +1,4 @@ +{ + "name": "usermod_v2_HttpPullLightControl", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/usermod_v2_HttpPullLightControl/readme.md b/usermods/usermod_v2_HttpPullLightControl/readme.md index eb56d505..d86ece4d 100644 --- a/usermods/usermod_v2_HttpPullLightControl/readme.md +++ b/usermods/usermod_v2_HttpPullLightControl/readme.md @@ -5,7 +5,7 @@ The `usermod_v2_HttpPullLightControl` is a custom user module for WLED that enab ## Features * Configure the URL endpoint (only support HTTP for now, no HTTPS) and polling interval via the WLED user interface. -* All options from the JSON API are supported (since v0.0.3). See: https://kno.wled.ge/interfaces/json-api/ +* All options from the JSON API are supported (since v0.0.3). See: [https://kno.wled.ge/interfaces/json-api/](https://kno.wled.ge/interfaces/json-api/) * The ability to control the brightness of all lights and the state (on/off) and color of individual lights remotely. * Start or stop an effect and when you run the same effect when its's already running, it won't restart. * The ability to control all these settings per segment. @@ -13,13 +13,15 @@ The `usermod_v2_HttpPullLightControl` is a custom user module for WLED that enab * Unique ID generation based on the device's MAC address and a configurable salt value, appended to the request URL for identification. ## Configuration + * Enable the `usermod_v2_HttpPullLightControl` via the WLED user interface. * Specify the URL endpoint and polling interval. ## JSON Format and examples + * The module sends a GET request to the configured URL, appending a unique identifier as a query parameter: `https://www.example.com/mycustompage.php?id=xxxxxxxx` where xxxxxxx is a 40 character long SHA1 hash of the MAC address combined with a given salt. -* Response Format (since v0.0.3) it is eactly the same as the WLED JSON API, see: https://kno.wled.ge/interfaces/json-api/ +* Response Format (since v0.0.3) it is eactly the same as the WLED JSON API, see: [https://kno.wled.ge/interfaces/json-api/](https://kno.wled.ge/interfaces/json-api/) After getting the URL (it can be a static file like static.json or a mylogic.php which gives a dynamic response), the response is read and parsed to WLED. * An example of a response to set the individual lights: 0 to RED, 12 to Green and 14 to BLUE. Remember that is will SET lights, you might want to set all the others to black. @@ -58,48 +60,51 @@ After getting the URL (it can be a static file like static.json or a mylogic.php }` * Or use the following example to start an effect, but first we UNFREEZE (frz=false) the segment because it was frozen by individual light control in the previous examples (28=Chase effect, Speed=180m Intensity=128). The three color slots are the slots you see under the color wheel and used by the effect. RED, Black, White in this case. + +```json `{ "seg": { - "frz": false, - "fx": 28, - "sx": 200, - "ix": 128, - "col": [ - "FF0000", - "000000", - "FFFFFF" - ] - } + "frz": false, + "fx": 28, + "sx": 200, + "ix": 128, + "col": [ + "FF0000", + "000000", + "FFFFFF" + ] + } }` - +``` ## Installation 1. Add `usermod_v2_HttpPullLightControl` to your WLED project following the instructions provided in the WLED documentation. 2. Compile by setting the build_flag: -D USERMOD_HTTP_PULL_LIGHT_CONTROL and upload to your ESP32/ESP8266! 3. There are several compile options which you can put in your platformio.ini or platformio_override.ini: -- -DUSERMOD_HTTP_PULL_LIGHT_CONTROL ;To Enable the usermod -- -DHTTP_PULL_LIGHT_CONTROL_URL="\"http://mydomain.com/json-response.php\"" ; The URL which will be requested all the time to set the lights/effects -- -DHTTP_PULL_LIGHT_CONTROL_SALT="\"my_very-S3cret_C0de\"" ; A secret SALT which will help by making the ID more safe -- -DHTTP_PULL_LIGHT_CONTROL_INTERVAL=30 ; The interval at which the URL is requested in seconds -- -DHTTP_PULL_LIGHT_CONTROL_HIDE_SALT ; Do you want to Hide the SALT in the User Interface? If yes, Set this flag. Note that the salt can now only be set via the above -DHTTP_PULL_LIGHT_CONTROL_SALT= setting -- -DWLED_AP_SSID="\"Christmas Card\"" ; These flags are not just for my Usermod but you probably want to set them -- -DWLED_AP_PASS="\"christmas\"" -- -DWLED_OTA_PASS="\"otapw-secret\"" -- -DMDNS_NAME="\"christmascard\"" -- -DSERVERNAME="\"CHRISTMASCARD\"" -- -D ABL_MILLIAMPS_DEFAULT=450 -- -D DEFAULT_LED_COUNT=60 ; For a LED Ring of 60 LEDs -- -D BTNPIN=41 ; The M5Stack Atom S3 Lite has a button on GPIO41 -- -D DATA_PINS=2 ; The M5Stack Atom S3 Lite has a Grove connector on the front, we use this GPIO2 -- -D STATUSLED=35 ; The M5Stack Atom S3 Lite has a Multi-Color LED on GPIO35, although I didnt managed to control it -- -D IRPIN=4 ; The M5Stack Atom S3 Lite has a IR LED on GPIO4 +* -DUSERMOD_HTTP_PULL_LIGHT_CONTROL ;To Enable the usermod +* -DHTTP_PULL_LIGHT_CONTROL_URL="\"`http://mydomain.com/json-response.php`\"" ; The URL which will be requested all the time to set the lights/effects +* -DHTTP_PULL_LIGHT_CONTROL_SALT="\"my_very-S3cret_C0de\"" ; A secret SALT which will help by making the ID more safe +* -DHTTP_PULL_LIGHT_CONTROL_INTERVAL=30 ; The interval at which the URL is requested in seconds +* -DHTTP_PULL_LIGHT_CONTROL_HIDE_SALT ; Do you want to Hide the SALT in the User Interface? If yes, Set this flag. Note that the salt can now only be set via the above -DHTTP_PULL_LIGHT_CONTROL_SALT= setting -- -D DEBUG=1 ; Set these DEBUG flags ONLY if you want to debug and read out Serial (using Visual Studio Code - Serial Monitor) -- -DDEBUG_LEVEL=5 -- -DWLED_DEBUG +* -DWLED_AP_SSID="\"Christmas Card\"" ; These flags are not just for my Usermod but you probably want to set them +* -DWLED_AP_PASS="\"christmas\"" +* -DWLED_OTA_PASS="\"otapw-secret\"" +* -DMDNS_NAME="\"christmascard\"" +* -DSERVERNAME="\"CHRISTMASCARD\"" +* -D ABL_MILLIAMPS_DEFAULT=450 +* -D DEFAULT_LED_COUNT=60 ; For a LED Ring of 60 LEDs +* -D BTNPIN=41 ; The M5Stack Atom S3 Lite has a button on GPIO41 +* -D DATA_PINS=2 ; The M5Stack Atom S3 Lite has a Grove connector on the front, we use this GPIO2 +* -D STATUSLED=35 ; The M5Stack Atom S3 Lite has a Multi-Color LED on GPIO35, although I didnt managed to control it +* -D IRPIN=4 ; The M5Stack Atom S3 Lite has a IR LED on GPIO4 + +* -D DEBUG=1 ; Set these DEBUG flags ONLY if you want to debug and read out Serial (using Visual Studio Code - Serial Monitor) +* -DDEBUG_LEVEL=5 +* -DWLED_DEBUG ## Use Case: Interactive Christmas Cards @@ -107,4 +112,4 @@ Imagine distributing interactive Christmas cards embedded with a tiny ESP32 and Your server keeps track of how many cards are active at any given time. If all 20 cards are active, your server instructs each card to light up all of its LEDs. However, if only 4 cards are active, your server instructs each card to light up only 4 LEDs. This creates a real-time interactive experience, symbolizing the collective spirit of the holiday season. Each lit LED represents a friend who's thinking about the others, and the visual feedback creates a sense of connection among the group, despite the physical distance. -This setup demonstrates a unique way to blend traditional holiday sentiments with modern technology, offering an engaging and memorable experience. \ No newline at end of file +This setup demonstrates a unique way to blend traditional holiday sentiments with modern technology, offering an engaging and memorable experience. diff --git a/usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.cpp b/usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.cpp index 854cc206..44a2726e 100644 --- a/usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.cpp +++ b/usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.cpp @@ -4,6 +4,9 @@ const char HttpPullLightControl::_name[] PROGMEM = "HttpPullLightControl"; const char HttpPullLightControl::_enabled[] PROGMEM = "Enable"; +static HttpPullLightControl http_pull_usermod; +REGISTER_USERMOD(http_pull_usermod); + void HttpPullLightControl::setup() { //Serial.begin(115200); @@ -297,10 +300,10 @@ void HttpPullLightControl::handleResponse(String& responseStr) { // Check for valid JSON, otherwise we brick the program runtime if (jsonStr[0] == '{' || jsonStr[0] == '[') { // Attempt to deserialize the JSON response - DeserializationError error = deserializeJson(doc, jsonStr); + DeserializationError error = deserializeJson(*pDoc, jsonStr); if (error == DeserializationError::Ok) { // Get JSON object from th doc - JsonObject obj = doc.as(); + JsonObject obj = pDoc->as(); // Parse the object throuhg deserializeState (use CALL_MODE_NO_NOTIFY or OR CALL_MODE_DIRECT_CHANGE) deserializeState(obj, CALL_MODE_NO_NOTIFY); } else { diff --git a/usermods/usermod_v2_RF433/library.json b/usermods/usermod_v2_RF433/library.json new file mode 100644 index 00000000..d8de29b8 --- /dev/null +++ b/usermods/usermod_v2_RF433/library.json @@ -0,0 +1,7 @@ +{ + "name": "usermod_v2_RF433", + "build": { "libArchive": false }, + "dependencies": { + "sui77/rc-switch":"2.6.4" + } +} \ No newline at end of file diff --git a/usermods/usermod_v2_RF433/readme.md b/usermods/usermod_v2_RF433/readme.md new file mode 100644 index 00000000..43919f11 --- /dev/null +++ b/usermods/usermod_v2_RF433/readme.md @@ -0,0 +1,18 @@ +# RF433 remote usermod + +Usermod for controlling WLED using a generic 433 / 315MHz remote and simple 3-pin receiver +See for compatibility details + +## Build + +- Create a `platformio_override.ini` file at the root of the wled source directory if not already present +- Copy the `433MHz RF remote example for esp32dev` section from `platformio_override.sample.ini` into it +- Duplicate/adjust for other boards + +## Usage + +- Connect receiver to a free pin +- Set pin in Config->Usermods +- Info pane will show the last received button code +- Upload the remote433.json sample file in this folder to the ESP with the file editor at [http://\[wled-ip\]/edit](http://ip/edit) +- Edit as necessary, the key is the button number retrieved from the info pane, and the "cmd" can be either an [HTTP API](https://kno.wled.ge/interfaces/http-api/) or a [JSON API](https://kno.wled.ge/interfaces/json-api/) command. \ No newline at end of file diff --git a/usermods/usermod_v2_RF433/remote433.json b/usermods/usermod_v2_RF433/remote433.json new file mode 100644 index 00000000..d5d930a8 --- /dev/null +++ b/usermods/usermod_v2_RF433/remote433.json @@ -0,0 +1,34 @@ +{ + "13985576": { + "cmnt": "Toggle Power using HTTP API", + "cmd": "T=2" + }, + "3670817": { + "cmnt": "Force Power ON using HTTP API", + "cmd": "T=1" + }, + "13985572": { + "cmnt": "Set brightness to 200 using JSON API", + "cmd": {"bri":200} + }, + "3670818": { + "cmnt": "Run Preset 1 using JSON API", + "cmd": {"ps":1} + }, + "13985570": { + "cmnt": "Increase brightness by 40 using HTTP API", + "cmd": "A=~40" + }, + "13985569": { + "cmnt": "Decrease brightness by 40 using HTTP API", + "cmd": "A=~-40" + }, + "7608836": { + "cmnt": "Start 1min timer using JSON API", + "cmd": {"nl":{"on":true,"dur":1,"mode":0}} + }, + "7608840": { + "cmnt": "Select random effect on all segments using JSON API", + "cmd": {"seg":{"fx":"r"}} + } +} \ No newline at end of file diff --git a/usermods/usermod_v2_RF433/usermod_v2_RF433.cpp b/usermods/usermod_v2_RF433/usermod_v2_RF433.cpp new file mode 100644 index 00000000..9ac6c416 --- /dev/null +++ b/usermods/usermod_v2_RF433/usermod_v2_RF433.cpp @@ -0,0 +1,183 @@ +#include "wled.h" +#include "Arduino.h" +#include + +#define RF433_BUSWAIT_TIMEOUT 24 + +class RF433Usermod : public Usermod +{ +private: + RCSwitch mySwitch = RCSwitch(); + unsigned long lastCommand = 0; + unsigned long lastTime = 0; + + bool modEnabled = true; + int8_t receivePin = -1; + + static const char _modName[]; + static const char _modEnabled[]; + static const char _receivePin[]; + + bool initDone = false; + +public: + + void setup() + { + mySwitch.disableReceive(); + if (modEnabled) + { + mySwitch.enableReceive(receivePin); + } + initDone = true; + } + + /* + * connected() is called every time the WiFi is (re)connected + * Use it to initialize network interfaces + */ + void connected() + { + } + + void loop() + { + if (!modEnabled || strip.isUpdating()) + return; + + if (mySwitch.available()) + { + unsigned long receivedCommand = mySwitch.getReceivedValue(); + mySwitch.resetAvailable(); + + // Discard duplicates, limit long press repeat + if (lastCommand == receivedCommand && millis() - lastTime < 800) + return; + + lastCommand = receivedCommand; + lastTime = millis(); + + DEBUG_PRINT(F("RF433 Receive: ")); + DEBUG_PRINTLN(receivedCommand); + + if(!remoteJson433(receivedCommand)) + DEBUG_PRINTLN(F("RF433: unknown button")); + } + } + + // Add last received button to info pane + void addToJsonInfo(JsonObject &root) + { + if (!initDone) + return; // prevent crash on boot applyPreset() + JsonObject user = root["u"]; + if (user.isNull()) + user = root.createNestedObject("u"); + + JsonArray switchArr = user.createNestedArray("RF433 Last Received"); // name + switchArr.add(lastCommand); + } + + void addToConfig(JsonObject &root) + { + JsonObject top = root.createNestedObject(FPSTR(_modName)); // usermodname + top[FPSTR(_modEnabled)] = modEnabled; + JsonArray pinArray = top.createNestedArray("pin"); + pinArray.add(receivePin); + + DEBUG_PRINTLN(F(" config saved.")); + } + + bool readFromConfig(JsonObject &root) + { + JsonObject top = root[FPSTR(_modName)]; + if (top.isNull()) + { + DEBUG_PRINT(FPSTR(_modName)); + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + getJsonValue(top[FPSTR(_modEnabled)], modEnabled); + getJsonValue(top["pin"][0], receivePin); + + DEBUG_PRINTLN(F("config (re)loaded.")); + + // Redo init on update + if(initDone) + setup(); + + return true; + } + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() + { + return USERMOD_ID_RF433; + } + + // this function follows the same principle as decodeIRJson() / remoteJson() + bool remoteJson433(int button) + { + char objKey[14]; + bool parsed = false; + + if (!requestJSONBufferLock(22)) return false; + + sprintf_P(objKey, PSTR("\"%d\":"), button); + + unsigned long start = millis(); + while (strip.isUpdating() && millis()-start < RF433_BUSWAIT_TIMEOUT) yield(); // wait for strip to finish updating, accessing FS during sendout causes glitches + + // attempt to read command from remote.json + readObjectFromFile(PSTR("/remote433.json"), objKey, pDoc); + JsonObject fdo = pDoc->as(); + if (fdo.isNull()) { + // the received button does not exist + releaseJSONBufferLock(); + return parsed; + } + + String cmdStr = fdo["cmd"].as(); + JsonObject jsonCmdObj = fdo["cmd"]; //object + + if (jsonCmdObj.isNull()) // we could also use: fdo["cmd"].is() + { + // HTTP API command + String apireq = "win"; apireq += '&'; // reduce flash string usage + if (!cmdStr.startsWith(apireq)) cmdStr = apireq + cmdStr; // if no "win&" prefix + if (!irApplyToAllSelected && cmdStr.indexOf(F("SS="))<0) { + char tmp[10]; + sprintf_P(tmp, PSTR("&SS=%d"), strip.getMainSegmentId()); + cmdStr += tmp; + } + fdo.clear(); // clear JSON buffer (it is no longer needed) + handleSet(nullptr, cmdStr, false); // no stateUpdated() call here + stateUpdated(CALL_MODE_BUTTON); + parsed = true; + } else { + // command is JSON object + if (jsonCmdObj[F("psave")].isNull()) + deserializeState(jsonCmdObj, CALL_MODE_BUTTON_PRESET); + else { + uint8_t psave = jsonCmdObj[F("psave")].as(); + char pname[33]; + sprintf_P(pname, PSTR("IR Preset %d"), psave); + fdo.clear(); + if (psave > 0 && psave < 251) savePreset(psave, pname, fdo); + } + parsed = true; + } + releaseJSONBufferLock(); + return parsed; + } +}; + +const char RF433Usermod::_modName[] PROGMEM = "RF433 Remote"; +const char RF433Usermod::_modEnabled[] PROGMEM = "Enabled"; +const char RF433Usermod::_receivePin[] PROGMEM = "RX Pin"; + +static RF433Usermod usermod_v2_RF433; +REGISTER_USERMOD(usermod_v2_RF433); diff --git a/usermods/usermod_v2_animartrix/library.json b/usermods/usermod_v2_animartrix/library.json new file mode 100644 index 00000000..667572ba --- /dev/null +++ b/usermods/usermod_v2_animartrix/library.json @@ -0,0 +1,7 @@ +{ + "name": "animartrix", + "build": { "libArchive": false }, + "dependencies": { + "Animartrix": "https://github.com/netmindz/animartrix.git#b172586" + } +} diff --git a/usermods/usermod_v2_animartrix/readme.md b/usermods/usermod_v2_animartrix/readme.md index 42d463c5..f0ff60a7 100644 --- a/usermods/usermod_v2_animartrix/readme.md +++ b/usermods/usermod_v2_animartrix/readme.md @@ -6,9 +6,5 @@ CC BY-NC 3.0 licensed effects by Stefan Petrick, include this usermod only if yo ## Installation -Please uncomment the two references to ANIMartRIX in your platform.ini - -lib_dep to a version of https://github.com/netmindz/animartrix.git -and the build_flags -D USERMOD_ANIMARTRIX - +Add 'animartrix' to 'custom_usermods' in your platformio_override.ini. diff --git a/usermods/usermod_v2_animartrix/usermod_v2_animartrix.h b/usermods/usermod_v2_animartrix/usermod_v2_animartrix.cpp similarity index 99% rename from usermods/usermod_v2_animartrix/usermod_v2_animartrix.h rename to usermods/usermod_v2_animartrix/usermod_v2_animartrix.cpp index d91cf6c9..d2968f2f 100644 --- a/usermods/usermod_v2_animartrix/usermod_v2_animartrix.h +++ b/usermods/usermod_v2_animartrix/usermod_v2_animartrix.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" #include @@ -452,5 +450,6 @@ class AnimartrixUsermod : public Usermod { }; - +static AnimartrixUsermod animartrix_module("Animartrix", false); +REGISTER_USERMOD(animartrix_module); diff --git a/usermods/usermod_v2_auto_save/library.json b/usermods/usermod_v2_auto_save/library.json new file mode 100644 index 00000000..127767eb --- /dev/null +++ b/usermods/usermod_v2_auto_save/library.json @@ -0,0 +1,4 @@ +{ + "name": "auto_save", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/usermod_v2_auto_save/readme.md b/usermods/usermod_v2_auto_save/readme.md index f54d87a7..ce15d8c2 100644 --- a/usermods/usermod_v2_auto_save/readme.md +++ b/usermods/usermod_v2_auto_save/readme.md @@ -2,6 +2,7 @@ v2 Usermod to automatically save settings to preset number AUTOSAVE_PRESET_NUM after a change to any of: + * brightness * effect speed * effect intensity @@ -19,7 +20,7 @@ Note: WLED doesn't respect the brightness of the preset being auto loaded, so th ## Installation -Copy and update the example `platformio_override.ini.sample` +Copy and update the example `platformio_override.ini.sample` from the Rotary Encoder UI usermode folder to the root directory of your particular build. This file should be placed in the same directory as `platformio.ini`. @@ -50,6 +51,9 @@ Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. ## Change Log 2021-02 + * First public release + 2021-04 + * Adaptation for runtime configuration. diff --git a/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.cpp similarity index 99% rename from usermods/usermod_v2_auto_save/usermod_v2_auto_save.h rename to usermods/usermod_v2_auto_save/usermod_v2_auto_save.cpp index a257413b..1b97ea94 100644 --- a/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h +++ b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" // v2 Usermod to automatically save settings @@ -275,3 +273,6 @@ const char AutoSaveUsermod::_autoSaveEnabled[] PROGMEM = "enabled"; const char AutoSaveUsermod::_autoSaveAfterSec[] PROGMEM = "autoSaveAfterSec"; const char AutoSaveUsermod::_autoSavePreset[] PROGMEM = "autoSavePreset"; const char AutoSaveUsermod::_autoSaveApplyOnBoot[] PROGMEM = "autoSaveApplyOnBoot"; + +static AutoSaveUsermod autosave; +REGISTER_USERMOD(autosave); diff --git a/usermods/usermod_v2_brightness_follow_sun/README.md b/usermods/usermod_v2_brightness_follow_sun/README.md new file mode 100644 index 00000000..cbd87a55 --- /dev/null +++ b/usermods/usermod_v2_brightness_follow_sun/README.md @@ -0,0 +1,35 @@ +# Update Brightness Follow Sun + +This UserMod can set brightness by mapping [minimum-maximum-minimum] from [sunrise-suntop-sunset], I use this UserMod to adjust the brightness of my plant growth light (pwm led), and I think it will make my plants happy. + +This UserMod will adjust brightness from sunrise to sunset, reaching maximum brightness at the zenith of the sun. It can also maintain the lowest brightness within 0-6 hours before sunrise and after sunset according to the settings. + +## Installation + +define `USERMOD_BRIGHTNESS_FOLLOW_SUN` e.g. `#define USERMOD_BRIGHTNESS_FOLLOW_SUN` in my_config.h + +or add `-D USERMOD_BRIGHTNESS_FOLLOW_SUN` to `build_flags` in platformio_override.ini + +### Options + +Open Usermod Settings in WLED to change settings: + +`Enable` - When checked `Enable`, turn on the `Brightness Follow Sun` Usermod, which will automatically turn on the lights, adjust the brightness, and turn off the lights. If you need to completely turn off the lights, please unchecked `Enable`. + +`Update Interval Sec` - The unit is seconds, and the brightness will be automatically refreshed according to the set parameters. + +`Min Brightness` - set brightness by map of min-max-min : sunrise-suntop-sunset + +`Max Brightness` - It needs to be set to a value greater than `Min Brightness`, otherwise it will always remain at `Min Brightness`. + +`Relax Hour` - The unit is in hours, with an effective range of 0-6. According to the settings, maintain the lowest brightness for 0-6 hours before sunrise and after sunset. + +### PlatformIO requirements + +No special requirements. + +### Change Log + +2025-01-02 + +* init diff --git a/usermods/usermod_v2_brightness_follow_sun/library.json b/usermods/usermod_v2_brightness_follow_sun/library.json new file mode 100644 index 00000000..dec00e55 --- /dev/null +++ b/usermods/usermod_v2_brightness_follow_sun/library.json @@ -0,0 +1,4 @@ +{ + "name": "brightness_follow_sun", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/usermod_v2_brightness_follow_sun/usermod_v2_brightness_follow_sun.cpp b/usermods/usermod_v2_brightness_follow_sun/usermod_v2_brightness_follow_sun.cpp new file mode 100644 index 00000000..ff97cba4 --- /dev/null +++ b/usermods/usermod_v2_brightness_follow_sun/usermod_v2_brightness_follow_sun.cpp @@ -0,0 +1,131 @@ +#include "wled.h" + +//v2 usermod that allows to change brightness and color using a rotary encoder, +//change between modes by pressing a button (many encoders have one included) +class UsermodBrightnessFollowSun : public Usermod +{ +private: + static const char _name[]; + static const char _enabled[]; + static const char _update_interval[]; + static const char _min_bri[]; + static const char _max_bri[]; + static const char _relax_hour[]; + +private: + bool enabled = false; //WLEDMM + unsigned long update_interval = 60; + unsigned long update_interval_ms = 60000; + int min_bri = 1; + int max_bri = 255; + float relax_hour = 0; + int relaxSec = 0; + unsigned long lastUMRun = 0; +public: + + void setup() {}; + + float mapFloat(float inputValue, float inMin, float inMax, float outMin, float outMax) { + if (inMax == inMin) + return outMin; + + inputValue = constrain(inputValue, inMin, inMax); + + return ((inputValue - inMin) * (outMax - outMin) / (inMax - inMin)) + outMin; + } + + uint16_t getId() override + { + return USERMOD_ID_BRIGHTNESS_FOLLOW_SUN; + } + + void update() + { + if (sunrise == 0 || sunset == 0 || localTime == 0) + return; + + int curSec = elapsedSecsToday(localTime); + int sunriseSec = elapsedSecsToday(sunrise); + int sunsetSec = elapsedSecsToday(sunset); + int sunMiddleSec = sunriseSec + (sunsetSec-sunriseSec)/2; + + int relaxSecH = sunriseSec-relaxSec; + int relaxSecE = sunsetSec+relaxSec; + + int briSet = 0; + if (curSec >= relaxSecH && curSec <= relaxSecE) { + float timeMapToAngle = curSec < sunMiddleSec ? + mapFloat(curSec, sunriseSec, sunMiddleSec, 0, M_PI/2.0) : + mapFloat(curSec, sunMiddleSec, sunsetSec, M_PI/2.0, M_PI); + float sinValue = sin_t(timeMapToAngle); + briSet = min_bri + (max_bri-min_bri)*sinValue; + } + + bri = briSet; + stateUpdated(CALL_MODE_DIRECT_CHANGE); +} + + void loop() override + { + if (!enabled || strip.isUpdating()) + return; + + if (millis() - lastUMRun < update_interval_ms) + return; + lastUMRun = millis(); + + update(); + } + + void addToConfig(JsonObject& root) + { + JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname + + top[FPSTR(_enabled)] = enabled; + top[FPSTR(_update_interval)] = update_interval; + top[FPSTR(_min_bri)] = min_bri; + top[FPSTR(_max_bri)] = max_bri; + top[FPSTR(_relax_hour)] = relax_hour; + } + + bool readFromConfig(JsonObject& root) + { + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINTF("[%s] No config found. (Using defaults.)\n", _name); + return false; + } + + bool configComplete = true; + + configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled, false); + configComplete &= getJsonValue(top[FPSTR(_update_interval)], update_interval, 60); + configComplete &= getJsonValue(top[FPSTR(_min_bri)], min_bri, 1); + configComplete &= getJsonValue(top[FPSTR(_max_bri)], max_bri, 255); + configComplete &= getJsonValue(top[FPSTR(_relax_hour)], relax_hour, 0); + + update_interval = constrain(update_interval, 1, SECS_PER_HOUR); + min_bri = constrain(min_bri, 1, 255); + max_bri = constrain(max_bri, 1, 255); + relax_hour = constrain(relax_hour, 0, 6); + + update_interval_ms = update_interval*1000; + relaxSec = SECS_PER_HOUR*relax_hour; + + lastUMRun = 0; + update(); + + return configComplete; + } +}; + + +const char UsermodBrightnessFollowSun::_name[] PROGMEM = "Brightness Follow Sun"; +const char UsermodBrightnessFollowSun::_enabled[] PROGMEM = "Enabled"; +const char UsermodBrightnessFollowSun::_update_interval[] PROGMEM = "Update Interval Sec"; +const char UsermodBrightnessFollowSun::_min_bri[] PROGMEM = "Min Brightness"; +const char UsermodBrightnessFollowSun::_max_bri[] PROGMEM = "Max Brightness"; +const char UsermodBrightnessFollowSun::_relax_hour[] PROGMEM = "Relax Hour"; + +static UsermodBrightnessFollowSun usermod_brightness_follow_sun; +REGISTER_USERMOD(usermod_brightness_follow_sun); diff --git a/usermods/usermod_v2_four_line_display_ALT/4LD_wled_fonts.c b/usermods/usermod_v2_four_line_display_ALT/4LD_wled_fonts.h similarity index 99% rename from usermods/usermod_v2_four_line_display_ALT/4LD_wled_fonts.c rename to usermods/usermod_v2_four_line_display_ALT/4LD_wled_fonts.h index 5495f919..0fb5f3bb 100644 --- a/usermods/usermod_v2_four_line_display_ALT/4LD_wled_fonts.c +++ b/usermods/usermod_v2_four_line_display_ALT/4LD_wled_fonts.h @@ -1,7 +1,5 @@ -#pragma once - //WLED custom fonts, curtesy of @Benji (https://github.com/Proto-molecule) - +#pragma once /* Fontname: wled_logo_akemi_4x4 diff --git a/usermods/usermod_v2_four_line_display_ALT/library.json b/usermods/usermod_v2_four_line_display_ALT/library.json new file mode 100644 index 00000000..b1644822 --- /dev/null +++ b/usermods/usermod_v2_four_line_display_ALT/library.json @@ -0,0 +1,8 @@ +{ + "name": "four_line_display_ALT", + "build": { "libArchive": false }, + "dependencies": { + "U8g2": "~2.34.4", + "Wire": "" + } +} \ No newline at end of file diff --git a/usermods/usermod_v2_four_line_display_ALT/platformio_override.sample.ini b/usermods/usermod_v2_four_line_display_ALT/platformio_override.sample.ini index e5963745..f4fa8c9d 100644 --- a/usermods/usermod_v2_four_line_display_ALT/platformio_override.sample.ini +++ b/usermods/usermod_v2_four_line_display_ALT/platformio_override.sample.ini @@ -1,18 +1,11 @@ [platformio] -default_envs = esp32dev +default_envs = esp32dev_fld -[env:esp32dev] -board = esp32dev -platform = ${esp32.platform} -build_unflags = ${common.build_unflags} +[env:esp32dev_fld] +extends = env:esp32dev_V4 +custom_usermods = ${env:esp32dev_V4.custom_usermods} four_line_display_ALT build_flags = - ${common.build_flags_esp32} - -D USERMOD_FOUR_LINE_DISPLAY + ${env:esp32dev_V4.build_flags} -D FLD_TYPE=SH1106 -D I2CSCLPIN=27 -D I2CSDAPIN=26 - -lib_deps = - ${esp32.lib_deps} - U8g2@~2.34.4 - Wire diff --git a/usermods/usermod_v2_four_line_display_ALT/readme.md b/usermods/usermod_v2_four_line_display_ALT/readme.md index 39bb5d28..663c93a4 100644 --- a/usermods/usermod_v2_four_line_display_ALT/readme.md +++ b/usermods/usermod_v2_four_line_display_ALT/readme.md @@ -5,6 +5,7 @@ This usermod could be used in compination with `usermod_v2_rotary_encoder_ui_ALT ## Functionalities Press the encoder to cycle through the options: + * Brightness * Speed * Intensity @@ -35,15 +36,15 @@ These options are configurable in Config > Usermods * `enabled` - enable/disable usermod * `type` - display type in numeric format - * 1 = I2C SSD1306 128x32 - * 2 = I2C SH1106 128x32 - * 3 = I2C SSD1306 128x64 (4 double-height lines) - * 4 = I2C SSD1305 128x32 - * 5 = I2C SSD1305 128x64 (4 double-height lines) - * 6 = SPI SSD1306 128x32 - * 7 = SPI SSD1306 128x64 (4 double-height lines) - * 8 = SPI SSD1309 128x64 (4 double-height lines) - * 9 = I2C SSD1309 128x64 (4 double-height lines) + * 1 = I2C SSD1306 128x32 + * 2 = I2C SH1106 128x32 + * 3 = I2C SSD1306 128x64 (4 double-height lines) + * 4 = I2C SSD1305 128x32 + * 5 = I2C SSD1305 128x64 (4 double-height lines) + * 6 = SPI SSD1306 128x32 + * 7 = SPI SSD1306 128x64 (4 double-height lines) + * 8 = SPI SSD1309 128x64 (4 double-height lines) + * 9 = I2C SSD1309 128x64 (4 double-height lines) * `pin` - GPIO pins used for display; SPI displays can use SCK, MOSI, CS, DC & RST * `flip` - flip/rotate display 180° * `contrast` - set display contrast (higher contrast may reduce display lifetime) @@ -53,7 +54,6 @@ These options are configurable in Config > Usermods * `showSeconds` - Show seconds on the clock display * `i2c-freq-kHz` - I2C clock frequency in kHz (may help reduce dropped frames, range: 400-3400) - ### PlatformIO requirements Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. @@ -61,4 +61,5 @@ Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. ## Change Log 2021-10 + * First public release diff --git a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display.h b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display.h new file mode 100644 index 00000000..4fc963b9 --- /dev/null +++ b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display.h @@ -0,0 +1,315 @@ +#include "wled.h" +#undef U8X8_NO_HW_I2C // borrowed from WLEDMM: we do want I2C hardware drivers - if possible +#include // from https://github.com/olikraus/u8g2/ + +#pragma once + +#ifndef FLD_ESP32_NO_THREADS + #define FLD_ESP32_USE_THREADS // comment out to use 0.13.x behaviour without parallel update task - slower, but more robust. May delay other tasks like LEDs or audioreactive!! +#endif + +#ifndef FLD_PIN_CS + #define FLD_PIN_CS 15 +#endif + +#ifdef ARDUINO_ARCH_ESP32 + #ifndef FLD_PIN_DC + #define FLD_PIN_DC 19 + #endif + #ifndef FLD_PIN_RESET + #define FLD_PIN_RESET 26 + #endif +#else + #ifndef FLD_PIN_DC + #define FLD_PIN_DC 12 + #endif + #ifndef FLD_PIN_RESET + #define FLD_PIN_RESET 16 + #endif +#endif + +#ifndef FLD_TYPE + #ifndef FLD_SPI_DEFAULT + #define FLD_TYPE SSD1306 + #else + #define FLD_TYPE SSD1306_SPI + #endif +#endif + +// When to time out to the clock or blank the screen +// if SLEEP_MODE_ENABLED. +#define SCREEN_TIMEOUT_MS 60*1000 // 1 min + +// Minimum time between redrawing screen in ms +#define REFRESH_RATE_MS 1000 + +// Extra char (+1) for null +#define LINE_BUFFER_SIZE 16+1 +#define MAX_JSON_CHARS 19+1 +#define MAX_MODE_LINE_SPACE 13+1 + +typedef enum { + NONE = 0, + SSD1306, // U8X8_SSD1306_128X32_UNIVISION_HW_I2C + SH1106, // U8X8_SH1106_128X64_WINSTAR_HW_I2C + SSD1306_64, // U8X8_SSD1306_128X64_NONAME_HW_I2C + SSD1305, // U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C + SSD1305_64, // U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C + SSD1306_SPI, // U8X8_SSD1306_128X32_NONAME_HW_SPI + SSD1306_SPI64, // U8X8_SSD1306_128X64_NONAME_HW_SPI + SSD1309_SPI64, // U8X8_SSD1309_128X64_NONAME0_4W_HW_SPI + SSD1309_64 // U8X8_SSD1309_128X64_NONAME0_HW_I2C +} DisplayType; + +class FourLineDisplayUsermod : public Usermod { + #if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + public: + FourLineDisplayUsermod() { if (!instance) instance = this; } + static FourLineDisplayUsermod* getInstance(void) { return instance; } + #endif + + private: + + static FourLineDisplayUsermod *instance; + bool initDone = false; + volatile bool drawing = false; + volatile bool lockRedraw = false; + + // HW interface & configuration + U8X8 *u8x8 = nullptr; // pointer to U8X8 display object + + #ifndef FLD_SPI_DEFAULT + int8_t ioPin[3] = {-1, -1, -1}; // I2C pins: SCL, SDA + uint32_t ioFrequency = 400000; // in Hz (minimum is 100000, baseline is 400000 and maximum should be 3400000) + #else + int8_t ioPin[3] = {FLD_PIN_CS, FLD_PIN_DC, FLD_PIN_RESET}; // custom SPI pins: CS, DC, RST + uint32_t ioFrequency = 1000000; // in Hz (minimum is 500kHz, baseline is 1MHz and maximum should be 20MHz) + #endif + + DisplayType type = FLD_TYPE; // display type + bool flip = false; // flip display 180° + uint8_t contrast = 10; // screen contrast + uint8_t lineHeight = 1; // 1 row or 2 rows + uint16_t refreshRate = REFRESH_RATE_MS; // in ms + uint32_t screenTimeout = SCREEN_TIMEOUT_MS; // in ms + bool sleepMode = true; // allow screen sleep? + bool clockMode = false; // display clock + bool showSeconds = true; // display clock with seconds + bool enabled = true; + bool contrastFix = false; + + // Next variables hold the previous known values to determine if redraw is + // required. + String knownSsid = apSSID; + IPAddress knownIp = IPAddress(4, 3, 2, 1); + uint8_t knownBrightness = 0; + uint8_t knownEffectSpeed = 0; + uint8_t knownEffectIntensity = 0; + uint8_t knownMode = 0; + uint8_t knownPalette = 0; + uint8_t knownMinute = 99; + uint8_t knownHour = 99; + byte brightness100; + byte fxspeed100; + byte fxintensity100; + bool knownnightlight = nightlightActive; + bool wificonnected = interfacesInited; + bool powerON = true; + + bool displayTurnedOff = false; + unsigned long nextUpdate = 0; + unsigned long lastRedraw = 0; + unsigned long overlayUntil = 0; + + // Set to 2 or 3 to mark lines 2 or 3. Other values ignored. + byte markLineNum = 255; + byte markColNum = 255; + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _contrast[]; + static const char _refreshRate[]; + static const char _screenTimeOut[]; + static const char _flip[]; + static const char _sleepMode[]; + static const char _clockMode[]; + static const char _showSeconds[]; + static const char _busClkFrequency[]; + static const char _contrastFix[]; + + // If display does not work or looks corrupted check the + // constructor reference: + // https://github.com/olikraus/u8g2/wiki/u8x8setupcpp + // or check the gallery: + // https://github.com/olikraus/u8g2/wiki/gallery + + // some displays need this to properly apply contrast + void setVcomh(bool highContrast); + void startDisplay(); + + /** + * Wrappers for screen drawing + */ + void setFlipMode(uint8_t mode); + void setContrast(uint8_t contrast); + void drawString(uint8_t col, uint8_t row, const char *string, bool ignoreLH=false); + void draw2x2String(uint8_t col, uint8_t row, const char *string); + void drawGlyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font, bool ignoreLH=false); + void draw2x2Glyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font); + void draw2x2GlyphIcons(); + uint8_t getCols(); + void clear(); + void setPowerSave(uint8_t save); + void center(String &line, uint8_t width); + + /** + * Display the current date and time in large characters + * on the middle rows. Based 24 or 12 hour depending on + * the useAMPM configuration. + */ + void showTime(); + + /** + * Enable sleep (turn the display off) or clock mode. + */ + void sleepOrClock(bool enabled); + + public: + + // gets called once at boot. Do all initialization that doesn't depend on + // network here + void setup() override; + + // gets called every time WiFi is (re-)connected. Initialize own network + // interfaces here + void connected() override; + + /** + * Da loop. + */ + void loop() override; + + //function to update lastredraw + inline void updateRedrawTime() { lastRedraw = millis(); } + + /** + * Redraw the screen (but only if things have changed + * or if forceRedraw). + */ + void redraw(bool forceRedraw); + + void updateBrightness(); + void updateSpeed(); + void updateIntensity(); + void drawStatusIcons(); + + /** + * marks the position of the arrow showing + * the current setting being changed + * pass line and colum info + */ + void setMarkLine(byte newMarkLineNum, byte newMarkColNum); + + //Draw the arrow for the current setting being changed + void drawArrow(); + + //Display the current effect or palette (desiredEntry) + // on the appropriate line (row). + void showCurrentEffectOrPalette(int inputEffPal, const char *qstring, uint8_t row); + + /** + * If there screen is off or in clock is displayed, + * this will return true. This allows us to throw away + * the first input from the rotary encoder but + * to wake up the screen. + */ + bool wakeDisplay(); + + /** + * Allows you to show one line and a glyph as overlay for a period of time. + * Clears the screen and prints. + * Used in Rotary Encoder usermod. + */ + void overlay(const char* line1, long showHowLong, byte glyphType); + + /** + * Allows you to show Akemi WLED logo overlay for a period of time. + * Clears the screen and prints. + */ + void overlayLogo(long showHowLong); + + /** + * Allows you to show two lines as overlay for a period of time. + * Clears the screen and prints. + * Used in Auto Save usermod + */ + void overlay(const char* line1, const char* line2, long showHowLong); + + void networkOverlay(const char* line1, long showHowLong); + + /** + * handleButton() can be used to override default button behaviour. Returning true + * will prevent button working in a default way. + * Replicating button.cpp + */ + bool handleButton(uint8_t b); + + void onUpdateBegin(bool init) override; + + /* + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. + * Below it is shown how this could be used for e.g. a light sensor + */ + //void addToJsonInfo(JsonObject& root) override; + + /* + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + //void addToJsonState(JsonObject& root) override; + + /* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + //void readFromJsonState(JsonObject& root) override; + + void appendConfigData() override; + + /* + * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. + * It will be called by WLED when settings are actually saved (for example, LED settings are saved) + * If you want to force saving the current state, use serializeConfig() in your loop(). + * + * CAUTION: serializeConfig() will initiate a filesystem write operation. + * It might cause the LEDs to stutter and will cause flash wear if called too often. + * Use it sparingly and always in the loop, never in network callbacks! + * + * addToConfig() will also not yet add your setting to one of the settings pages automatically. + * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually. + * + * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! + */ + void addToConfig(JsonObject& root) override; + + /* + * readFromConfig() can be used to read back the custom settings you added with addToConfig(). + * This is called by WLED when settings are loaded (currently this only happens once immediately after boot) + * + * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), + * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. + * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) + */ + bool readFromConfig(JsonObject& root) override; + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() override { + return USERMOD_ID_FOUR_LINE_DISP; + } + }; + \ No newline at end of file diff --git a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.cpp similarity index 78% rename from usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h rename to usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.cpp index 684dd86e..36a8b029 100644 --- a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h +++ b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.cpp @@ -1,13 +1,5 @@ -#pragma once - -#include "wled.h" -#undef U8X8_NO_HW_I2C // borrowed from WLEDMM: we do want I2C hardware drivers - if possible -#include // from https://github.com/olikraus/u8g2/ -#include "4LD_wled_fonts.c" - -#ifndef FLD_ESP32_NO_THREADS - #define FLD_ESP32_USE_THREADS // comment out to use 0.13.x behaviour without parallel update task - slower, but more robust. May delay other tasks like LEDs or audioreactive!! -#endif +#include "usermod_v2_four_line_display.h" +#include "4LD_wled_fonts.h" // // Inspired by the usermod_v2_four_line_display @@ -22,330 +14,18 @@ // // Make sure to enable NTP and set your time zone in WLED Config | Time. // -// REQUIREMENT: You must add the following requirements to -// REQUIREMENT: "lib_deps" within platformio.ini / platformio_override.ini -// REQUIREMENT: * U8g2 (the version already in platformio.ini is fine) -// REQUIREMENT: * Wire -// // If display does not work or looks corrupted check the // constructor reference: // https://github.com/olikraus/u8g2/wiki/u8x8setupcpp // or check the gallery: // https://github.com/olikraus/u8g2/wiki/gallery -#ifndef FLD_PIN_CS - #define FLD_PIN_CS 15 -#endif - -#ifdef ARDUINO_ARCH_ESP32 - #ifndef FLD_PIN_DC - #define FLD_PIN_DC 19 - #endif - #ifndef FLD_PIN_RESET - #define FLD_PIN_RESET 26 - #endif -#else - #ifndef FLD_PIN_DC - #define FLD_PIN_DC 12 - #endif - #ifndef FLD_PIN_RESET - #define FLD_PIN_RESET 16 - #endif -#endif - -#ifndef FLD_TYPE - #ifndef FLD_SPI_DEFAULT - #define FLD_TYPE SSD1306 - #else - #define FLD_TYPE SSD1306_SPI - #endif -#endif - -// When to time out to the clock or blank the screen -// if SLEEP_MODE_ENABLED. -#define SCREEN_TIMEOUT_MS 60*1000 // 1 min - -// Minimum time between redrawing screen in ms -#define REFRESH_RATE_MS 1000 - -// Extra char (+1) for null -#define LINE_BUFFER_SIZE 16+1 -#define MAX_JSON_CHARS 19+1 -#define MAX_MODE_LINE_SPACE 13+1 - #ifdef ARDUINO_ARCH_ESP32 static TaskHandle_t Display_Task = nullptr; void DisplayTaskCode(void * parameter); #endif - -typedef enum { - NONE = 0, - SSD1306, // U8X8_SSD1306_128X32_UNIVISION_HW_I2C - SH1106, // U8X8_SH1106_128X64_WINSTAR_HW_I2C - SSD1306_64, // U8X8_SSD1306_128X64_NONAME_HW_I2C - SSD1305, // U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C - SSD1305_64, // U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C - SSD1306_SPI, // U8X8_SSD1306_128X32_NONAME_HW_SPI - SSD1306_SPI64, // U8X8_SSD1306_128X64_NONAME_HW_SPI - SSD1309_SPI64, // U8X8_SSD1309_128X64_NONAME0_4W_HW_SPI - SSD1309_64 // U8X8_SSD1309_128X64_NONAME0_HW_I2C -} DisplayType; - - -class FourLineDisplayUsermod : public Usermod { -#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) - public: - FourLineDisplayUsermod() { if (!instance) instance = this; } - static FourLineDisplayUsermod* getInstance(void) { return instance; } -#endif - - private: - - static FourLineDisplayUsermod *instance; - bool initDone = false; - volatile bool drawing = false; - volatile bool lockRedraw = false; - - // HW interface & configuration - U8X8 *u8x8 = nullptr; // pointer to U8X8 display object - - #ifndef FLD_SPI_DEFAULT - int8_t ioPin[3] = {-1, -1, -1}; // I2C pins: SCL, SDA - uint32_t ioFrequency = 400000; // in Hz (minimum is 100000, baseline is 400000 and maximum should be 3400000) - #else - int8_t ioPin[3] = {FLD_PIN_CS, FLD_PIN_DC, FLD_PIN_RESET}; // custom SPI pins: CS, DC, RST - uint32_t ioFrequency = 1000000; // in Hz (minimum is 500kHz, baseline is 1MHz and maximum should be 20MHz) - #endif - - DisplayType type = FLD_TYPE; // display type - bool flip = false; // flip display 180° - uint8_t contrast = 10; // screen contrast - uint8_t lineHeight = 1; // 1 row or 2 rows - uint16_t refreshRate = REFRESH_RATE_MS; // in ms - uint32_t screenTimeout = SCREEN_TIMEOUT_MS; // in ms - bool sleepMode = true; // allow screen sleep? - bool clockMode = false; // display clock - bool showSeconds = true; // display clock with seconds - bool enabled = true; - bool contrastFix = false; - - // Next variables hold the previous known values to determine if redraw is - // required. - String knownSsid = apSSID; - IPAddress knownIp = IPAddress(4, 3, 2, 1); - uint8_t knownBrightness = 0; - uint8_t knownEffectSpeed = 0; - uint8_t knownEffectIntensity = 0; - uint8_t knownMode = 0; - uint8_t knownPalette = 0; - uint8_t knownMinute = 99; - uint8_t knownHour = 99; - byte brightness100; - byte fxspeed100; - byte fxintensity100; - bool knownnightlight = nightlightActive; - bool wificonnected = interfacesInited; - bool powerON = true; - - bool displayTurnedOff = false; - unsigned long nextUpdate = 0; - unsigned long lastRedraw = 0; - unsigned long overlayUntil = 0; - - // Set to 2 or 3 to mark lines 2 or 3. Other values ignored. - byte markLineNum = 255; - byte markColNum = 255; - - // strings to reduce flash memory usage (used more than twice) - static const char _name[]; - static const char _enabled[]; - static const char _contrast[]; - static const char _refreshRate[]; - static const char _screenTimeOut[]; - static const char _flip[]; - static const char _sleepMode[]; - static const char _clockMode[]; - static const char _showSeconds[]; - static const char _busClkFrequency[]; - static const char _contrastFix[]; - - // If display does not work or looks corrupted check the - // constructor reference: - // https://github.com/olikraus/u8g2/wiki/u8x8setupcpp - // or check the gallery: - // https://github.com/olikraus/u8g2/wiki/gallery - - // some displays need this to properly apply contrast - void setVcomh(bool highContrast); - void startDisplay(); - - /** - * Wrappers for screen drawing - */ - void setFlipMode(uint8_t mode); - void setContrast(uint8_t contrast); - void drawString(uint8_t col, uint8_t row, const char *string, bool ignoreLH=false); - void draw2x2String(uint8_t col, uint8_t row, const char *string); - void drawGlyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font, bool ignoreLH=false); - void draw2x2Glyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font); - void draw2x2GlyphIcons(); - uint8_t getCols(); - void clear(); - void setPowerSave(uint8_t save); - void center(String &line, uint8_t width); - - /** - * Display the current date and time in large characters - * on the middle rows. Based 24 or 12 hour depending on - * the useAMPM configuration. - */ - void showTime(); - - /** - * Enable sleep (turn the display off) or clock mode. - */ - void sleepOrClock(bool enabled); - - public: - - // gets called once at boot. Do all initialization that doesn't depend on - // network here - void setup() override; - - // gets called every time WiFi is (re-)connected. Initialize own network - // interfaces here - void connected() override; - - /** - * Da loop. - */ - void loop() override; - - //function to update lastredraw - inline void updateRedrawTime() { lastRedraw = millis(); } - - /** - * Redraw the screen (but only if things have changed - * or if forceRedraw). - */ - void redraw(bool forceRedraw); - - void updateBrightness(); - void updateSpeed(); - void updateIntensity(); - void drawStatusIcons(); - - /** - * marks the position of the arrow showing - * the current setting being changed - * pass line and colum info - */ - void setMarkLine(byte newMarkLineNum, byte newMarkColNum); - - //Draw the arrow for the current setting being changed - void drawArrow(); - - //Display the current effect or palette (desiredEntry) - // on the appropriate line (row). - void showCurrentEffectOrPalette(int inputEffPal, const char *qstring, uint8_t row); - - /** - * If there screen is off or in clock is displayed, - * this will return true. This allows us to throw away - * the first input from the rotary encoder but - * to wake up the screen. - */ - bool wakeDisplay(); - - /** - * Allows you to show one line and a glyph as overlay for a period of time. - * Clears the screen and prints. - * Used in Rotary Encoder usermod. - */ - void overlay(const char* line1, long showHowLong, byte glyphType); - - /** - * Allows you to show Akemi WLED logo overlay for a period of time. - * Clears the screen and prints. - */ - void overlayLogo(long showHowLong); - - /** - * Allows you to show two lines as overlay for a period of time. - * Clears the screen and prints. - * Used in Auto Save usermod - */ - void overlay(const char* line1, const char* line2, long showHowLong); - - void networkOverlay(const char* line1, long showHowLong); - - /** - * handleButton() can be used to override default button behaviour. Returning true - * will prevent button working in a default way. - * Replicating button.cpp - */ - bool handleButton(uint8_t b); - - void onUpdateBegin(bool init) override; - - /* - * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. - * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. - * Below it is shown how this could be used for e.g. a light sensor - */ - //void addToJsonInfo(JsonObject& root) override; - - /* - * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - //void addToJsonState(JsonObject& root) override; - - /* - * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - //void readFromJsonState(JsonObject& root) override; - - void appendConfigData() override; - - /* - * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. - * It will be called by WLED when settings are actually saved (for example, LED settings are saved) - * If you want to force saving the current state, use serializeConfig() in your loop(). - * - * CAUTION: serializeConfig() will initiate a filesystem write operation. - * It might cause the LEDs to stutter and will cause flash wear if called too often. - * Use it sparingly and always in the loop, never in network callbacks! - * - * addToConfig() will also not yet add your setting to one of the settings pages automatically. - * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually. - * - * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! - */ - void addToConfig(JsonObject& root) override; - - /* - * readFromConfig() can be used to read back the custom settings you added with addToConfig(). - * This is called by WLED when settings are loaded (currently this only happens once immediately after boot) - * - * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), - * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. - * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) - */ - bool readFromConfig(JsonObject& root) override; - - /* - * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). - * This could be used in the future for the system to determine whether your usermod is installed. - */ - uint16_t getId() override { - return USERMOD_ID_FOUR_LINE_DISP; - } -}; - // strings to reduce flash memory usage (used more than twice) const char FourLineDisplayUsermod::_name[] PROGMEM = "4LineDisplay"; const char FourLineDisplayUsermod::_enabled[] PROGMEM = "enabled"; @@ -1386,3 +1066,7 @@ bool FourLineDisplayUsermod::readFromConfig(JsonObject& root) { // use "return !top["newestParameter"].isNull();" when updating Usermod with new features return !top[FPSTR(_contrastFix)].isNull(); } + + +static FourLineDisplayUsermod usermod_v2_four_line_display_alt; +REGISTER_USERMOD(usermod_v2_four_line_display_alt); diff --git a/usermods/usermod_v2_klipper_percentage/library.json b/usermods/usermod_v2_klipper_percentage/library.json new file mode 100644 index 00000000..962dda14 --- /dev/null +++ b/usermods/usermod_v2_klipper_percentage/library.json @@ -0,0 +1,4 @@ +{ + "name": "usermod_v2_klipper_percentage", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/usermod_v2_klipper_percentage/usermod_v2_klipper_percentage.h b/usermods/usermod_v2_klipper_percentage/usermod_v2_klipper_percentage.cpp similarity index 97% rename from usermods/usermod_v2_klipper_percentage/usermod_v2_klipper_percentage.h rename to usermods/usermod_v2_klipper_percentage/usermod_v2_klipper_percentage.cpp index bd4170dd..71c5c45f 100644 --- a/usermods/usermod_v2_klipper_percentage/usermod_v2_klipper_percentage.h +++ b/usermods/usermod_v2_klipper_percentage/usermod_v2_klipper_percentage.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" class klipper_percentage : public Usermod @@ -219,4 +217,7 @@ public: } }; const char klipper_percentage::_name[] PROGMEM = "Klipper_Percentage"; -const char klipper_percentage::_enabled[] PROGMEM = "enabled"; \ No newline at end of file +const char klipper_percentage::_enabled[] PROGMEM = "enabled"; + +static klipper_percentage usermod_v2_klipper_percentage; +REGISTER_USERMOD(usermod_v2_klipper_percentage); \ No newline at end of file diff --git a/usermods/usermod_v2_ping_pong_clock/library.json b/usermods/usermod_v2_ping_pong_clock/library.json new file mode 100644 index 00000000..4b272eca --- /dev/null +++ b/usermods/usermod_v2_ping_pong_clock/library.json @@ -0,0 +1,4 @@ +{ + "name": "usermod_v2_ping_pong_clock", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/usermod_v2_ping_pong_clock/usermod_v2_ping_pong_clock.h b/usermods/usermod_v2_ping_pong_clock/usermod_v2_ping_pong_clock.cpp similarity index 97% rename from usermods/usermod_v2_ping_pong_clock/usermod_v2_ping_pong_clock.h rename to usermods/usermod_v2_ping_pong_clock/usermod_v2_ping_pong_clock.cpp index 40ff675c..c6632b53 100644 --- a/usermods/usermod_v2_ping_pong_clock/usermod_v2_ping_pong_clock.h +++ b/usermods/usermod_v2_ping_pong_clock/usermod_v2_ping_pong_clock.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" class PingPongClockUsermod : public Usermod @@ -117,3 +115,7 @@ public: } }; + + +static PingPongClockUsermod usermod_v2_ping_pong_clock; +REGISTER_USERMOD(usermod_v2_ping_pong_clock); \ No newline at end of file diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/library.json b/usermods/usermod_v2_rotary_encoder_ui_ALT/library.json new file mode 100644 index 00000000..7c828d08 --- /dev/null +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/library.json @@ -0,0 +1,7 @@ +{ + "name": "rotary_encoder_ui_ALT", + "build": { + "libArchive": false, + "extraScript": "setup_deps.py" + } +} \ No newline at end of file diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.sample.ini b/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.sample.ini index 8a88fd6b..2511d2fa 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.sample.ini +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.sample.ini @@ -1,13 +1,11 @@ [platformio] -default_envs = esp32dev +default_envs = esp32dev_re -[env:esp32dev] -board = esp32dev -platform = ${esp32.platform} -build_unflags = ${common.build_unflags} +[env:esp32dev_re] +extends = env:esp32dev_V4 +custom_usermods = ${env:esp32dev_V4.custom_usermods} rotary_encoder_ui_ALT build_flags = - ${common.build_flags_esp32} - -D USERMOD_ROTARY_ENCODER_UI + ${env:esp32dev_V4.build_flags} -D USERMOD_ROTARY_ENCODER_GPIO=INPUT -D ENCODER_DT_PIN=21 -D ENCODER_CLK_PIN=23 diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md b/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md index c46e8766..cb6150a4 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md @@ -5,6 +5,7 @@ This usermod supports the UI of the `usermod_v2_rotary_encoder_ui_ALT`. ## Functionalities Press the encoder to cycle through the options: + * Brightness * Speed * Intensity @@ -25,10 +26,6 @@ Copy the example `platformio_override.sample.ini` to the root directory of your ### Define Your Options -* `USERMOD_ROTARY_ENCODER_UI` - define this to have this user mod included wled00\usermods_list.cpp -* `USERMOD_FOUR_LINE_DISPLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp - also tells this usermod that the display is available - (see the Four Line Display usermod `readme.md` for more details) * `ENCODER_DT_PIN` - defaults to 18 * `ENCODER_CLK_PIN` - defaults to 5 * `ENCODER_SW_PIN` - defaults to 19 @@ -43,4 +40,5 @@ No special requirements. ## Change Log 2021-10 + * First public release diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/setup_deps.py b/usermods/usermod_v2_rotary_encoder_ui_ALT/setup_deps.py new file mode 100644 index 00000000..ed579bc1 --- /dev/null +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/setup_deps.py @@ -0,0 +1,8 @@ +from platformio.package.meta import PackageSpec +Import('env') + +libs = [PackageSpec(lib).name for lib in env.GetProjectOption("lib_deps",[])] +# Check for partner usermod +# Allow both "usermod_v2" and unqualified syntax +if any(mod in ("four_line_display_ALT", "usermod_v2_four_line_display_ALT") for mod in libs): + env.Append(CPPDEFINES=[("USERMOD_FOUR_LINE_DISPLAY")]) diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp similarity index 96% rename from usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h rename to usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp index 383c1193..02bb08c9 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" // @@ -29,6 +27,10 @@ // * display network (long press buttion) // +#ifdef USERMOD_FOUR_LINE_DISPLAY +#include "usermod_v2_four_line_display.h" +#endif + #ifdef USERMOD_MODE_SORT #error "Usermod Mode Sort is no longer required. Remove -D USERMOD_MODE_SORT from platformio.ini" #endif @@ -398,20 +400,20 @@ void RotaryEncoderUIUsermod::sortModesAndPalettes() { modes_alpha_indexes = re_initIndexArray(strip.getModeCount()); re_sortModes(modes_qstrings, modes_alpha_indexes, strip.getModeCount(), MODE_SORT_SKIP_COUNT); - DEBUG_PRINT(F("Sorting palettes: ")); DEBUG_PRINT(strip.getPaletteCount()); DEBUG_PRINT('/'); DEBUG_PRINTLN(strip.customPalettes.size()); - palettes_qstrings = re_findModeStrings(JSON_palette_names, strip.getPaletteCount()); - palettes_alpha_indexes = re_initIndexArray(strip.getPaletteCount()); - if (strip.customPalettes.size()) { - for (int i=0; i> 5; + currentCCT = (approximateKelvinFromRGB(RGBW32(colPri[0], colPri[1], colPri[2], colPri[3])) - 1900) >> 5; if (!initDone) sortModesAndPalettes(); @@ -700,7 +702,7 @@ void RotaryEncoderUIUsermod::findCurrentEffectAndPalette() { effectPaletteIndex = 0; DEBUG_PRINTLN(effectPalette); - for (unsigned i = 0; i < strip.getPaletteCount()+strip.customPalettes.size(); i++) { + for (unsigned i = 0; i < getPaletteCount()+customPalettes.size(); i++) { if (palettes_alpha_indexes[i] == effectPalette) { effectPaletteIndex = i; DEBUG_PRINTLN(F("Found palette.")); @@ -890,7 +892,7 @@ void RotaryEncoderUIUsermod::changePalette(bool increase) { } display->updateRedrawTime(); #endif - effectPaletteIndex = max(min((unsigned)(increase ? effectPaletteIndex+1 : effectPaletteIndex-1), strip.getPaletteCount()+strip.customPalettes.size()-1), 0U); + effectPaletteIndex = max(min((unsigned)(increase ? effectPaletteIndex+1 : effectPaletteIndex-1), getPaletteCount()+customPalettes.size()-1), 0U); effectPalette = palettes_alpha_indexes[effectPaletteIndex]; stateChanged = true; if (applyToAll) { @@ -920,17 +922,17 @@ void RotaryEncoderUIUsermod::changeHue(bool increase){ display->updateRedrawTime(); #endif currentHue1 = max(min((increase ? currentHue1+fadeAmount : currentHue1-fadeAmount), 255), 0); - colorHStoRGB(currentHue1*256, currentSat1, col); + colorHStoRGB(currentHue1*256, currentSat1, colPri); stateChanged = true; if (applyToAll) { for (unsigned i=0; iupdateRedrawTime(); #endif currentSat1 = max(min((increase ? currentSat1+fadeAmount : currentSat1-fadeAmount), 255), 0); - colorHStoRGB(currentHue1*256, currentSat1, col); + colorHStoRGB(currentHue1*256, currentSat1, colPri); if (applyToAll) { for (unsigned i=0; i #include "wled.h" @@ -124,4 +122,7 @@ class WireguardUsermod : public Usermod { int endpoint_port = 0; bool is_enabled = false; unsigned long lastTime = 0; -}; \ No newline at end of file +}; + +static WireguardUsermod wireguard; +REGISTER_USERMOD(wireguard); \ No newline at end of file diff --git a/usermods/wizlights/library.json b/usermods/wizlights/library.json new file mode 100644 index 00000000..0bfc097c --- /dev/null +++ b/usermods/wizlights/library.json @@ -0,0 +1,4 @@ +{ + "name": "wizlights", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/wizlights/wizlights.h b/usermods/wizlights/wizlights.cpp similarity index 98% rename from usermods/wizlights/wizlights.h rename to usermods/wizlights/wizlights.cpp index 08d20493..3ac756b1 100644 --- a/usermods/wizlights/wizlights.h +++ b/usermods/wizlights/wizlights.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" #include @@ -156,3 +154,7 @@ class WizLightsUsermod : public Usermod { uint16_t getId(){return USERMOD_ID_WIZLIGHTS;} }; + + +static WizLightsUsermod wizlights; +REGISTER_USERMOD(wizlights); \ No newline at end of file diff --git a/usermods/word-clock-matrix/library.json b/usermods/word-clock-matrix/library.json new file mode 100644 index 00000000..7bc3919d --- /dev/null +++ b/usermods/word-clock-matrix/library.json @@ -0,0 +1,4 @@ +{ + "name": "word-clock-matrix", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/usermods/word-clock-matrix/usermod_word_clock_matrix.h b/usermods/word-clock-matrix/usermod_word_clock_matrix.h deleted file mode 100644 index 506c1275..00000000 --- a/usermods/word-clock-matrix/usermod_word_clock_matrix.h +++ /dev/null @@ -1,338 +0,0 @@ -#pragma once - -#include "wled.h" - -/* - * Things to do... - * Turn on ntp clock 24h format - * 64 LEDS - */ - - -class WordClockMatrix : public Usermod -{ -private: - unsigned long lastTime = 0; - uint8_t minuteLast = 99; - int dayBrightness = 128; - int nightBrightness = 16; - -public: - void setup() - { - Serial.println("Hello from my usermod!"); - - //saveMacro(14, "A=128", false); - //saveMacro(15, "A=64", false); - //saveMacro(16, "A=16", false); - - //saveMacro(1, "&FX=0&R=255&G=255&B=255", false); - - //strip.getSegment(1).setOption(SEG_OPTION_SELECTED, true); - - //select first two segments (background color + FX settable) - WS2812FX::Segment &seg = strip.getSegment(0); - seg.colors[0] = ((0 << 24) | ((0 & 0xFF) << 16) | ((0 & 0xFF) << 8) | ((0 & 0xFF))); - strip.getSegment(0).setOption(0, false); - strip.getSegment(0).setOption(2, false); - //other segments are text - for (int i = 1; i < 10; i++) - { - WS2812FX::Segment &seg = strip.getSegment(i); - seg.colors[0] = ((0 << 24) | ((0 & 0xFF) << 16) | ((190 & 0xFF) << 8) | ((180 & 0xFF))); - strip.getSegment(i).setOption(0, true); - strip.setBrightness(64); - } - } - - void connected() - { - Serial.println("Connected to WiFi!"); - } - - void selectWordSegments(bool state) - { - for (int i = 1; i < 10; i++) - { - //WS2812FX::Segment &seg = strip.getSegment(i); - strip.getSegment(i).setOption(0, state); - // strip.getSegment(1).setOption(SEG_OPTION_SELECTED, true); - //seg.mode = 12; - //seg.palette = 1; - //strip.setBrightness(255); - } - strip.getSegment(0).setOption(0, !state); - } - - void hourChime() - { - //strip.resetSegments(); - selectWordSegments(true); - colorUpdated(CALL_MODE_FX_CHANGED); - savePreset(13, false); - selectWordSegments(false); - //strip.getSegment(0).setOption(0, true); - strip.getSegment(0).setOption(2, true); - applyPreset(12); - colorUpdated(CALL_MODE_FX_CHANGED); - } - - void displayTime(byte hour, byte minute) - { - bool isToHour = false; //true if minute > 30 - strip.setSegment(0, 0, 64); // background - strip.setSegment(1, 0, 2); //It is - - strip.setSegment(2, 0, 0); - strip.setSegment(3, 0, 0); //disable minutes - strip.setSegment(4, 0, 0); //past - strip.setSegment(6, 0, 0); //to - strip.setSegment(8, 0, 0); //disable o'clock - - if (hour < 24) //valid time, display - { - if (minute == 30) - { - strip.setSegment(2, 3, 6); //half - strip.setSegment(3, 0, 0); //minutes - } - else if (minute == 15 || minute == 45) - { - strip.setSegment(3, 0, 0); //minutes - } - else if (minute == 10) - { - //strip.setSegment(5, 6, 8); //ten - } - else if (minute == 5) - { - //strip.setSegment(5, 16, 18); //five - } - else if (minute == 0) - { - strip.setSegment(3, 0, 0); //minutes - //hourChime(); - } - else - { - strip.setSegment(3, 18, 22); //minutes - } - - //past or to? - if (minute == 0) - { //full hour - strip.setSegment(3, 0, 0); //disable minutes - strip.setSegment(4, 0, 0); //disable past - strip.setSegment(6, 0, 0); //disable to - strip.setSegment(8, 60, 64); //o'clock - } - else if (minute > 34) - { - //strip.setSegment(6, 22, 24); //to - //minute = 60 - minute; - isToHour = true; - } - else - { - //strip.setSegment(4, 24, 27); //past - //isToHour = false; - } - } - - //byte minuteRem = minute %10; - - if (minute <= 4) - { - strip.setSegment(3, 0, 0); //nothing - strip.setSegment(5, 0, 0); //nothing - strip.setSegment(6, 0, 0); //nothing - strip.setSegment(8, 60, 64); //o'clock - } - else if (minute <= 9) - { - strip.setSegment(5, 16, 18); // five past - strip.setSegment(4, 24, 27); //past - } - else if (minute <= 14) - { - strip.setSegment(5, 6, 8); // ten past - strip.setSegment(4, 24, 27); //past - } - else if (minute <= 19) - { - strip.setSegment(5, 8, 12); // quarter past - strip.setSegment(3, 0, 0); //minutes - strip.setSegment(4, 24, 27); //past - } - else if (minute <= 24) - { - strip.setSegment(5, 12, 16); // twenty past - strip.setSegment(4, 24, 27); //past - } - else if (minute <= 29) - { - strip.setSegment(5, 12, 18); // twenty-five past - strip.setSegment(4, 24, 27); //past - } - else if (minute <= 34) - { - strip.setSegment(5, 3, 6); // half past - strip.setSegment(3, 0, 0); //minutes - strip.setSegment(4, 24, 27); //past - } - else if (minute <= 39) - { - strip.setSegment(5, 12, 18); // twenty-five to - strip.setSegment(6, 22, 24); //to - } - else if (minute <= 44) - { - strip.setSegment(5, 12, 16); // twenty to - strip.setSegment(6, 22, 24); //to - } - else if (minute <= 49) - { - strip.setSegment(5, 8, 12); // quarter to - strip.setSegment(3, 0, 0); //minutes - strip.setSegment(6, 22, 24); //to - } - else if (minute <= 54) - { - strip.setSegment(5, 6, 8); // ten to - strip.setSegment(6, 22, 24); //to - } - else if (minute <= 59) - { - strip.setSegment(5, 16, 18); // five to - strip.setSegment(6, 22, 24); //to - } - - //hours - if (hour > 23) - return; - if (isToHour) - hour++; - if (hour > 12) - hour -= 12; - if (hour == 0) - hour = 12; - - switch (hour) - { - case 1: - strip.setSegment(7, 27, 29); - break; //one - case 2: - strip.setSegment(7, 35, 37); - break; //two - case 3: - strip.setSegment(7, 29, 32); - break; //three - case 4: - strip.setSegment(7, 32, 35); - break; //four - case 5: - strip.setSegment(7, 37, 40); - break; //five - case 6: - strip.setSegment(7, 43, 45); - break; //six - case 7: - strip.setSegment(7, 40, 43); - break; //seven - case 8: - strip.setSegment(7, 45, 48); - break; //eight - case 9: - strip.setSegment(7, 48, 50); - break; //nine - case 10: - strip.setSegment(7, 54, 56); - break; //ten - case 11: - strip.setSegment(7, 50, 54); - break; //eleven - case 12: - strip.setSegment(7, 56, 60); - break; //twelve - } - - selectWordSegments(true); - applyMacro(1); - } - - void timeOfDay() - { - // NOT USED: use timed macros instead - //Used to set brightness dependant of time of day - lights dimmed at night - - //monday to thursday and sunday - - if ((weekday(localTime) == 6) | (weekday(localTime) == 7)) - { - if ((hour(localTime) > 0) | (hour(localTime) < 8)) - { - strip.setBrightness(nightBrightness); - } - else - { - strip.setBrightness(dayBrightness); - } - } - else - { - if ((hour(localTime) < 6) | (hour(localTime) >= 22)) - { - strip.setBrightness(nightBrightness); - } - else - { - strip.setBrightness(dayBrightness); - } - } - } - - //loop. You can use "if (WLED_CONNECTED)" to check for successful connection - void loop() - { - - if (millis() - lastTime > 1000) { - //Serial.println("I'm alive!"); - Serial.println(hour(localTime)); - lastTime = millis(); - } - - - if (minute(localTime) != minuteLast) - { - updateLocalTime(); - //timeOfDay(); - minuteLast = minute(localTime); - displayTime(hour(localTime), minute(localTime)); - if (minute(localTime) == 0) - { - hourChime(); - } - if (minute(localTime) == 1) - { - //turn off background segment; - strip.getSegment(0).setOption(2, false); - //applyPreset(13); - } - } - } - - void addToConfig(JsonObject& root) - { - JsonObject modName = root.createNestedObject("id"); - modName[F("mdns")] = "wled-word-clock"; - modName[F("name")] = "WLED WORD CLOCK"; - } - - uint16_t getId() - { - return USERMOD_ID_WORD_CLOCK_MATRIX; - } - - -}; diff --git a/usermods/word-clock-matrix/word-clock-matrix.cpp b/usermods/word-clock-matrix/word-clock-matrix.cpp index 67c5b1e4..24f69aad 100644 --- a/usermods/word-clock-matrix/word-clock-matrix.cpp +++ b/usermods/word-clock-matrix/word-clock-matrix.cpp @@ -1,305 +1,340 @@ #include "wled.h" + /* - * This v1 usermod file allows you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality - * EEPROM bytes 2750+ are reserved for your custom use case. (if you extend #define EEPSIZE in const.h) - * If you just need 8 bytes, use 2551-2559 (you do not need to increase EEPSIZE) - * - * Consider the v2 usermod API if you need a more advanced feature set! + * Things to do... + * Turn on ntp clock 24h format + * 64 LEDS */ -uint8_t minuteLast = 99; -int dayBrightness = 128; -int nightBrightness = 16; - -//Use userVar0 and userVar1 (API calls &U0=,&U1=, uint16_t) - -//gets called once at boot. Do all initialization that doesn't depend on network here -void userSetup() +class WordClockMatrix : public Usermod { -saveMacro(14, "A=128", false); -saveMacro(15, "A=64", false); -saveMacro(16, "A=16", false); +private: + unsigned long lastTime = 0; + uint8_t minuteLast = 99; + int dayBrightness = 128; + int nightBrightness = 16; -saveMacro(1, "&FX=0&R=255&G=255&B=255", false); - -//strip.getSegment(1).setOption(SEG_OPTION_SELECTED, true); - - //select first two segments (background color + FX settable) - Segment &seg = strip.getSegment(0); - seg.colors[0] = ((0 << 24) | ((0 & 0xFF) << 16) | ((0 & 0xFF) << 8) | ((0 & 0xFF))); - strip.getSegment(0).setOption(0, false); - strip.getSegment(0).setOption(2, false); - //other segments are text - for (int i = 1; i < 10; i++) +public: + void setup() { - Segment &seg = strip.getSegment(i); - seg.colors[0] = ((0 << 24) | ((0 & 0xFF) << 16) | ((190 & 0xFF) << 8) | ((180 & 0xFF))); - strip.getSegment(i).setOption(0, true); - strip.setBrightness(128); + Serial.println("Hello from my usermod!"); + + //saveMacro(14, "A=128", false); + //saveMacro(15, "A=64", false); + //saveMacro(16, "A=16", false); + + //saveMacro(1, "&FX=0&R=255&G=255&B=255", false); + + //strip.getSegment(1).setOption(SEG_OPTION_SELECTED, true); + + //select first two segments (background color + FX settable) + Segment &seg = strip.getSegment(0); + seg.colors[0] = ((0 << 24) | ((0 & 0xFF) << 16) | ((0 & 0xFF) << 8) | ((0 & 0xFF))); + strip.getSegment(0).setOption(0, false); + strip.getSegment(0).setOption(2, false); + //other segments are text + for (int i = 1; i < 10; i++) + { + Segment &text_seg = strip.getSegment(i); + text_seg.colors[0] = ((0 << 24) | ((0 & 0xFF) << 16) | ((190 & 0xFF) << 8) | ((180 & 0xFF))); + strip.getSegment(i).setOption(0, true); + strip.setBrightness(64); + } } -} -//gets called every time WiFi is (re-)connected. Initialize own network interfaces here -void userConnected() -{ -} - -void selectWordSegments(bool state) -{ - for (int i = 1; i < 10; i++) + void connected() { - //Segment &seg = strip.getSegment(i); - strip.getSegment(i).setOption(0, state); - // strip.getSegment(1).setOption(SEG_OPTION_SELECTED, true); - //seg.mode = 12; - //seg.palette = 1; - //strip.setBrightness(255); + Serial.println("Connected to WiFi!"); } - strip.getSegment(0).setOption(0, !state); -} -void hourChime() -{ - //strip.resetSegments(); - selectWordSegments(true); - colorUpdated(CALL_MODE_FX_CHANGED); - //savePreset(255); - selectWordSegments(false); - //strip.getSegment(0).setOption(0, true); - strip.getSegment(0).setOption(2, true); - applyPreset(12); - colorUpdated(CALL_MODE_FX_CHANGED); -} - -void displayTime(byte hour, byte minute) -{ - bool isToHour = false; //true if minute > 30 - strip.setSegment(0, 0, 64); // background - strip.setSegment(1, 0, 2); //It is - - strip.setSegment(2, 0, 0); - strip.setSegment(3, 0, 0); //disable minutes - strip.setSegment(4, 0, 0); //past - strip.setSegment(6, 0, 0); //to - strip.setSegment(8, 0, 0); //disable o'clock - - if (hour < 24) //valid time, display + void selectWordSegments(bool state) { - if (minute == 30) + for (int i = 1; i < 10; i++) { - strip.setSegment(2, 3, 6); //half - strip.setSegment(3, 0, 0); //minutes + //WS2812FX::Segment &seg = strip.getSegment(i); + strip.getSegment(i).setOption(0, state); + // strip.getSegment(1).setOption(SEG_OPTION_SELECTED, true); + //seg.mode = 12; + //seg.palette = 1; + //strip.setBrightness(255); } - else if (minute == 15 || minute == 45) + strip.getSegment(0).setOption(0, !state); + } + + void hourChime() + { + //strip.resetSegments(); + selectWordSegments(true); + colorUpdated(CALL_MODE_FX_CHANGED); + savePreset(13); + selectWordSegments(false); + //strip.getSegment(0).setOption(0, true); + strip.getSegment(0).setOption(2, true); + applyPreset(12); + colorUpdated(CALL_MODE_FX_CHANGED); + } + + void displayTime(byte hour, byte minute) + { + bool isToHour = false; //true if minute > 30 + strip.getSegment(0).setGeometry(0, 64); // background + strip.getSegment(1).setGeometry(0, 2); //It is + + strip.getSegment(2).setGeometry(0, 0); + strip.getSegment(3).setGeometry(0, 0); //disable minutes + strip.getSegment(4).setGeometry(0, 0); //past + strip.getSegment(6).setGeometry(0, 0); //to + strip.getSegment(8).setGeometry(0, 0); //disable o'clock + + if (hour < 24) //valid time, display { - strip.setSegment(3, 0, 0); //minutes + if (minute == 30) + { + strip.getSegment(2).setGeometry(3, 6); //half + strip.getSegment(3).setGeometry(0, 0); //minutes + } + else if (minute == 15 || minute == 45) + { + strip.getSegment(3).setGeometry(0, 0); //minutes + } + else if (minute == 10) + { + //strip.getSegment(5).setGeometry(6, 8); //ten + } + else if (minute == 5) + { + //strip.getSegment(5).setGeometry(16, 18); //five + } + else if (minute == 0) + { + strip.getSegment(3).setGeometry(0, 0); //minutes + //hourChime(); + } + else + { + strip.getSegment(3).setGeometry(18, 22); //minutes + } + + //past or to? + if (minute == 0) + { //full hour + strip.getSegment(3).setGeometry(0, 0); //disable minutes + strip.getSegment(4).setGeometry(0, 0); //disable past + strip.getSegment(6).setGeometry(0, 0); //disable to + strip.getSegment(8).setGeometry(60, 64); //o'clock + } + else if (minute > 34) + { + //strip.getSegment(6).setGeometry(22, 24); //to + //minute = 60 - minute; + isToHour = true; + } + else + { + //strip.getSegment(4).setGeometry(24, 27); //past + //isToHour = false; + } } - else if (minute == 10) + + //byte minuteRem = minute %10; + + if (minute <= 4) { - //strip.setSegment(5, 6, 8); //ten + strip.getSegment(3).setGeometry(0, 0); //nothing + strip.getSegment(5).setGeometry(0, 0); //nothing + strip.getSegment(6).setGeometry(0, 0); //nothing + strip.getSegment(8).setGeometry(60, 64); //o'clock } - else if (minute == 5) + else if (minute <= 9) { - //strip.setSegment(5, 16, 18); //five + strip.getSegment(5).setGeometry(16, 18); // five past + strip.getSegment(4).setGeometry(24, 27); //past } - else if (minute == 0) + else if (minute <= 14) { - strip.setSegment(3, 0, 0); //minutes - //hourChime(); + strip.getSegment(5).setGeometry(6, 8); // ten past + strip.getSegment(4).setGeometry(24, 27); //past + } + else if (minute <= 19) + { + strip.getSegment(5).setGeometry(8, 12); // quarter past + strip.getSegment(3).setGeometry(0, 0); //minutes + strip.getSegment(4).setGeometry(24, 27); //past + } + else if (minute <= 24) + { + strip.getSegment(5).setGeometry(12, 16); // twenty past + strip.getSegment(4).setGeometry(24, 27); //past + } + else if (minute <= 29) + { + strip.getSegment(5).setGeometry(12, 18); // twenty-five past + strip.getSegment(4).setGeometry(24, 27); //past + } + else if (minute <= 34) + { + strip.getSegment(5).setGeometry(3, 6); // half past + strip.getSegment(3).setGeometry(0, 0); //minutes + strip.getSegment(4).setGeometry(24, 27); //past + } + else if (minute <= 39) + { + strip.getSegment(5).setGeometry(12, 18); // twenty-five to + strip.getSegment(6).setGeometry(22, 24); //to + } + else if (minute <= 44) + { + strip.getSegment(5).setGeometry(12, 16); // twenty to + strip.getSegment(6).setGeometry(22, 24); //to + } + else if (minute <= 49) + { + strip.getSegment(5).setGeometry(8, 12); // quarter to + strip.getSegment(3).setGeometry(0, 0); //minutes + strip.getSegment(6).setGeometry(22, 24); //to + } + else if (minute <= 54) + { + strip.getSegment(5).setGeometry(6, 8); // ten to + strip.getSegment(6).setGeometry(22, 24); //to + } + else if (minute <= 59) + { + strip.getSegment(5).setGeometry(16, 18); // five to + strip.getSegment(6).setGeometry(22, 24); //to + } + + //hours + if (hour > 23) + return; + if (isToHour) + hour++; + if (hour > 12) + hour -= 12; + if (hour == 0) + hour = 12; + + switch (hour) + { + case 1: + strip.getSegment(7).setGeometry(27, 29); + break; //one + case 2: + strip.getSegment(7).setGeometry(35, 37); + break; //two + case 3: + strip.getSegment(7).setGeometry(29, 32); + break; //three + case 4: + strip.getSegment(7).setGeometry(32, 35); + break; //four + case 5: + strip.getSegment(7).setGeometry(37, 40); + break; //five + case 6: + strip.getSegment(7).setGeometry(43, 45); + break; //six + case 7: + strip.getSegment(7).setGeometry(40, 43); + break; //seven + case 8: + strip.getSegment(7).setGeometry(45, 48); + break; //eight + case 9: + strip.getSegment(7).setGeometry(48, 50); + break; //nine + case 10: + strip.getSegment(7).setGeometry(54, 56); + break; //ten + case 11: + strip.getSegment(7).setGeometry(50, 54); + break; //eleven + case 12: + strip.getSegment(7).setGeometry(56, 60); + break; //twelve + } + + selectWordSegments(true); + applyPreset(1); + } + + void timeOfDay() + { + // NOT USED: use timed macros instead + //Used to set brightness dependant of time of day - lights dimmed at night + + //monday to thursday and sunday + + if ((weekday(localTime) == 6) | (weekday(localTime) == 7)) + { + if ((hour(localTime) > 0) | (hour(localTime) < 8)) + { + strip.setBrightness(nightBrightness); + } + else + { + strip.setBrightness(dayBrightness); + } } else { - strip.setSegment(3, 18, 22); //minutes + if ((hour(localTime) < 6) | (hour(localTime) >= 22)) + { + strip.setBrightness(nightBrightness); + } + else + { + strip.setBrightness(dayBrightness); + } } + } - //past or to? - if (minute == 0) - { //full hour - strip.setSegment(3, 0, 0); //disable minutes - strip.setSegment(4, 0, 0); //disable past - strip.setSegment(6, 0, 0); //disable to - strip.setSegment(8, 60, 64); //o'clock - } - else if (minute > 34) + //loop. You can use "if (WLED_CONNECTED)" to check for successful connection + void loop() + { + + if (millis() - lastTime > 1000) { + //Serial.println("I'm alive!"); + Serial.println(hour(localTime)); + lastTime = millis(); + } + + + if (minute(localTime) != minuteLast) { - //strip.setSegment(6, 22, 24); //to - //minute = 60 - minute; - isToHour = true; - } - else - { - //strip.setSegment(4, 24, 27); //past - //isToHour = false; - } - } - else - { //temperature display - } - - //byte minuteRem = minute %10; - - if (minute <= 4) - { - strip.setSegment(3, 0, 0); //nothing - strip.setSegment(5, 0, 0); //nothing - strip.setSegment(6, 0, 0); //nothing - strip.setSegment(8, 60, 64); //o'clock - } - else if (minute <= 9) - { - strip.setSegment(5, 16, 18); // five past - strip.setSegment(4, 24, 27); //past - } - else if (minute <= 14) - { - strip.setSegment(5, 6, 8); // ten past - strip.setSegment(4, 24, 27); //past - } - else if (minute <= 19) - { - strip.setSegment(5, 8, 12); // quarter past - strip.setSegment(3, 0, 0); //minutes - strip.setSegment(4, 24, 27); //past - } - else if (minute <= 24) - { - strip.setSegment(5, 12, 16); // twenty past - strip.setSegment(4, 24, 27); //past - } - else if (minute <= 29) - { - strip.setSegment(5, 12, 18); // twenty-five past - strip.setSegment(4, 24, 27); //past - } - else if (minute <= 34) - { - strip.setSegment(5, 3, 6); // half past - strip.setSegment(3, 0, 0); //minutes - strip.setSegment(4, 24, 27); //past - } - else if (minute <= 39) - { - strip.setSegment(5, 12, 18); // twenty-five to - strip.setSegment(6, 22, 24); //to - } - else if (minute <= 44) - { - strip.setSegment(5, 12, 16); // twenty to - strip.setSegment(6, 22, 24); //to - } - else if (minute <= 49) - { - strip.setSegment(5, 8, 12); // quarter to - strip.setSegment(3, 0, 0); //minutes - strip.setSegment(6, 22, 24); //to - } - else if (minute <= 54) - { - strip.setSegment(5, 6, 8); // ten to - strip.setSegment(6, 22, 24); //to - } - else if (minute <= 59) - { - strip.setSegment(5, 16, 18); // five to - strip.setSegment(6, 22, 24); //to - } - - //hours - if (hour > 23) - return; - if (isToHour) - hour++; - if (hour > 12) - hour -= 12; - if (hour == 0) - hour = 12; - - switch (hour) - { - case 1: - strip.setSegment(7, 27, 29); - break; //one - case 2: - strip.setSegment(7, 35, 37); - break; //two - case 3: - strip.setSegment(7, 29, 32); - break; //three - case 4: - strip.setSegment(7, 32, 35); - break; //four - case 5: - strip.setSegment(7, 37, 40); - break; //five - case 6: - strip.setSegment(7, 43, 45); - break; //six - case 7: - strip.setSegment(7, 40, 43); - break; //seven - case 8: - strip.setSegment(7, 45, 48); - break; //eight - case 9: - strip.setSegment(7, 48, 50); - break; //nine - case 10: - strip.setSegment(7, 54, 56); - break; //ten - case 11: - strip.setSegment(7, 50, 54); - break; //eleven - case 12: - strip.setSegment(7, 56, 60); - break; //twelve - } - -selectWordSegments(true); -applyMacro(1); -} - -void timeOfDay() { -// NOT USED: use timed macros instead - //Used to set brightness dependant of time of day - lights dimmed at night - - //monday to thursday and sunday - - if ((weekday(localTime) == 6) | (weekday(localTime) == 7)) { - if (hour(localTime) > 0 | hour(localTime) < 8) { - strip.setBrightness(nightBrightness); - } - else { - strip.setBrightness(dayBrightness); - } - } - else { - if (hour(localTime) < 6 | hour(localTime) >= 22) { - strip.setBrightness(nightBrightness); - } - else { - strip.setBrightness(dayBrightness); - } - } -} - -//loop. You can use "if (WLED_CONNECTED)" to check for successful connection -void userLoop() -{ - if (minute(localTime) != minuteLast) - { - updateLocalTime(); - //timeOfDay(); - minuteLast = minute(localTime); - displayTime(hour(localTime), minute(localTime)); - if (minute(localTime) == 0){ - hourChime(); - } - if (minute(localTime) == 1){ - //turn off background segment; + updateLocalTime(); + //timeOfDay(); + minuteLast = minute(localTime); + displayTime(hour(localTime), minute(localTime)); + if (minute(localTime) == 0) + { + hourChime(); + } + if (minute(localTime) == 1) + { + //turn off background segment; strip.getSegment(0).setOption(2, false); - //applyPreset(255); + //applyPreset(13); + } } } -} + + void addToConfig(JsonObject& root) + { + JsonObject modName = root.createNestedObject("id"); + modName[F("mdns")] = "wled-word-clock"; + modName[F("name")] = "WLED WORD CLOCK"; + } + + uint16_t getId() + { + return 500; + } + + +}; + + +static WordClockMatrix word_clock_matrix; +REGISTER_USERMOD(word_clock_matrix); \ No newline at end of file diff --git a/wled00/FX.cpp b/wled00/FX.cpp index fd2118fd..4a364ea6 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -14,14 +14,60 @@ #include "FX.h" #include "fcn_declare.h" -#define IBN 5100 +#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) + #include "FXparticleSystem.h" + #ifdef ESP8266 + #if !defined(WLED_DISABLE_PARTICLESYSTEM2D) && !defined(WLED_DISABLE_PARTICLESYSTEM1D) + #error ESP8266 does not support 1D and 2D particle systems simultaneously. Please disable one of them. + #endif + #endif +#else + #define WLED_PS_DONT_REPLACE_FX +#endif + ////////////// + // DEV INFO // + ////////////// +/* + information for FX metadata strings: https://kno.wled.ge/interfaces/json-api/#effect-metadata + + Audio Reactive: use the following code to pass usermod variables to effect + + uint8_t *binNum = (uint8_t*)&SEGENV.aux1, *maxVol = (uint8_t*)(&SEGENV.aux1+1); // just in case assignment + bool samplePeak = false; + float FFT_MajorPeak = 1.0; + uint8_t *fftResult = nullptr; + float *fftBin = nullptr; + um_data_t *um_data = getAudioData(); + volumeSmth = *(float*) um_data->u_data[0]; + volumeRaw = *(float*) um_data->u_data[1]; + fftResult = (uint8_t*) um_data->u_data[2]; + samplePeak = *(uint8_t*) um_data->u_data[3]; + FFT_MajorPeak = *(float*) um_data->u_data[4]; + my_magnitude = *(float*) um_data->u_data[5]; + maxVol = (uint8_t*) um_data->u_data[6]; // requires UI element (SEGMENT.customX?), changes source element + binNum = (uint8_t*) um_data->u_data[7]; // requires UI element (SEGMENT.customX?), changes source element + fftBin = (float*) um_data->u_data[8]; +*/ + +#define IBN 5100 // paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) #define PALETTE_SOLID_WRAP (strip.paletteBlend == 1 || strip.paletteBlend == 3) #define PALETTE_MOVING_WRAP !(strip.paletteBlend == 2 || (strip.paletteBlend == 0 && SEGMENT.speed == 0)) #define indexToVStrip(index, stripNr) ((index) | (int((stripNr)+1)<<16)) +// a few constants needed for AudioReactive effects +// for 22Khz sampling +#define MAX_FREQUENCY 11025 // sample frequency / 2 (as per Nyquist criterion) +#define MAX_FREQ_LOG10 4.04238f // log10(MAX_FREQUENCY) +// for 20Khz sampling +//#define MAX_FREQUENCY 10240 +//#define MAX_FREQ_LOG10 4.0103f +// for 10Khz sampling +//#define MAX_FREQUENCY 5120 +//#define MAX_FREQ_LOG10 3.71f + // effect utility functions uint8_t sin_gap(uint16_t in) { if (in & 0x100) return 0; @@ -104,7 +150,7 @@ uint16_t blink(uint32_t color1, uint32_t color2, bool strobe, bool do_palette) { uint32_t color = on ? color1 : color2; if (color == color1 && do_palette) { - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } } else SEGMENT.fill(color); @@ -155,7 +201,7 @@ static const char _data_FX_MODE_STROBE_RAINBOW[] PROGMEM = "Strobe Rainbow@!;,!; * if (bool rev == true) then LEDs are turned off in reverse order */ uint16_t color_wipe(bool rev, bool useRandomColors) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); uint32_t cycleTime = 750 + (255 - SEGMENT.speed)*150; uint32_t perc = strip.now % cycleTime; unsigned prog = (perc * 65535) / cycleTime; @@ -169,7 +215,7 @@ uint16_t color_wipe(bool rev, bool useRandomColors) { if (useRandomColors) { if (SEGENV.call == 0) { - SEGENV.aux0 = random8(); + SEGENV.aux0 = hw_random8(); SEGENV.step = 3; } if (SEGENV.step == 1) { //if flag set, change to new random color @@ -183,8 +229,7 @@ uint16_t color_wipe(bool rev, bool useRandomColors) { } unsigned ledIndex = (prog * SEGLEN) >> 15; - unsigned rem = 0; - rem = (prog * SEGLEN) * 2; //mod 0xFFFF + uint16_t rem = (prog * SEGLEN) * 2; //mod 0xFFFF by truncating rem /= (SEGMENT.intensity +1); if (rem > 255) rem = 255; @@ -200,7 +245,7 @@ uint16_t color_wipe(bool rev, bool useRandomColors) { } else { SEGMENT.setPixelColor(index, back? col0 : col1); - if (i == ledIndex) SEGMENT.setPixelColor(index, color_blend(back? col0 : col1, back? col1 : col0, rem)); + if (i == ledIndex) SEGMENT.setPixelColor(index, color_blend(back? col0 : col1, back? col1 : col0, uint8_t(rem))); } } return FRAMETIME; @@ -261,7 +306,7 @@ uint16_t mode_random_color(void) { } if (SEGENV.call == 0) { - SEGENV.aux0 = random8(); + SEGENV.aux0 = hw_random8(); SEGENV.step = 2; } if (it != SEGENV.step) //new color @@ -271,7 +316,7 @@ uint16_t mode_random_color(void) { SEGENV.step = it; } - SEGMENT.fill(color_blend(SEGMENT.color_wheel(SEGENV.aux1), SEGMENT.color_wheel(SEGENV.aux0), fade)); + SEGMENT.fill(color_blend(SEGMENT.color_wheel(SEGENV.aux1), SEGMENT.color_wheel(SEGENV.aux0), uint8_t(fade))); return FRAMETIME; } static const char _data_FX_MODE_RANDOM_COLOR[] PROGMEM = "Random Colors@!,Fade time;;!;01"; @@ -286,25 +331,25 @@ uint16_t mode_dynamic(void) { if(SEGENV.call == 0) { //SEGMENT.fill(BLACK); - for (int i = 0; i < SEGLEN; i++) SEGENV.data[i] = random8(); + for (unsigned i = 0; i < SEGLEN; i++) SEGENV.data[i] = hw_random8(); } uint32_t cycleTime = 50 + (255 - SEGMENT.speed)*15; uint32_t it = strip.now / cycleTime; if (it != SEGENV.step && SEGMENT.speed != 0) //new color { - for (int i = 0; i < SEGLEN; i++) { - if (random8() <= SEGMENT.intensity) SEGENV.data[i] = random8(); // random color index + for (unsigned i = 0; i < SEGLEN; i++) { + if (hw_random8() <= SEGMENT.intensity) SEGENV.data[i] = hw_random8(); // random color index } SEGENV.step = it; } if (SEGMENT.check1) { - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.blendPixelColor(i, SEGMENT.color_wheel(SEGENV.data[i]), 16); } } else { - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_wheel(SEGENV.data[i])); } } @@ -338,8 +383,8 @@ uint16_t mode_breath(void) { var = sin16_t(counter) / 103; //close to parabolic in range 0-8192, max val. 23170 } - unsigned lum = 30 + var; - for (int i = 0; i < SEGLEN; i++) { + uint8_t lum = 30 + var; + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), lum)); } @@ -353,9 +398,9 @@ static const char _data_FX_MODE_BREATH[] PROGMEM = "Breathe@!;!,!;!;01"; */ uint16_t mode_fade(void) { unsigned counter = (strip.now * ((SEGMENT.speed >> 3) +10)); - unsigned lum = triwave16(counter) >> 8; + uint8_t lum = triwave16(counter) >> 8; - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), lum)); } @@ -368,7 +413,7 @@ static const char _data_FX_MODE_FADE[] PROGMEM = "Fade@!;!,!;!;01"; * Scan mode parent function */ uint16_t scan(bool dual) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); uint32_t cycleTime = 750 + (255 - SEGMENT.speed)*150; uint32_t perc = strip.now % cycleTime; int prog = (perc * 65535) / cycleTime; @@ -421,7 +466,7 @@ uint16_t mode_rainbow(void) { counter = counter >> 8; if (SEGMENT.intensity < 128){ - SEGMENT.fill(color_blend(SEGMENT.color_wheel(counter),WHITE,128-SEGMENT.intensity)); + SEGMENT.fill(color_blend(SEGMENT.color_wheel(counter),WHITE,uint8_t(128-SEGMENT.intensity))); } else { SEGMENT.fill(SEGMENT.color_wheel(counter)); } @@ -438,7 +483,7 @@ uint16_t mode_rainbow_cycle(void) { unsigned counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF; counter = counter >> 8; - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { //intensity/29 = 0 (1/16) 1 (1/8) 2 (1/4) 3 (1/2) 4 (1) 5 (2) 6 (4) 7 (8) 8 (16) uint8_t index = (i * (16 << (SEGMENT.intensity /29)) / SEGLEN) + counter; SEGMENT.setPixelColor(i, SEGMENT.color_wheel(index)); @@ -458,7 +503,7 @@ static uint16_t running(uint32_t color1, uint32_t color2, bool theatre = false) uint32_t it = strip.now / cycleTime; bool usePalette = color1 == SEGCOLOR(0); - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { uint32_t col = color2; if (usePalette) color1 = SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0); if (theatre) { @@ -505,7 +550,7 @@ static uint16_t running_base(bool saw, bool dual=false) { unsigned x_scale = SEGMENT.intensity >> 2; uint32_t counter = (strip.now * SEGMENT.speed) >> 9; - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { unsigned a = i*x_scale - counter; if (saw) { a &= 0xFF; @@ -523,7 +568,7 @@ static uint16_t running_base(bool saw, bool dual=false) { unsigned b = (SEGLEN-1-i)*x_scale - counter; uint8_t t = sin_gap(b); uint32_t cb = color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 2), t); - ca = color_blend(ca, cb, 127); + ca = color_blend(ca, cb, uint8_t(127)); } SEGMENT.setPixelColor(i, ca); } @@ -575,7 +620,7 @@ uint16_t mode_twinkle(void) { if (SEGENV.aux0 >= maxOn) { SEGENV.aux0 = 0; - SEGENV.aux1 = random16(); //new seed for our PRNG + SEGENV.aux1 = hw_random(); //new seed for our PRNG } SEGENV.aux0++; SEGENV.step = it; @@ -600,45 +645,39 @@ static const char _data_FX_MODE_TWINKLE[] PROGMEM = "Twinkle@!,!;!,!;!;;m12=0"; * Dissolve function */ uint16_t dissolve(uint32_t color) { - unsigned dataSize = (SEGLEN+7) >> 3; //1 bit per LED + unsigned dataSize = sizeof(uint32_t) * SEGLEN; if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + uint32_t* pixels = reinterpret_cast(SEGENV.data); if (SEGENV.call == 0) { - memset(SEGMENT.data, 0xFF, dataSize); // start by fading pixels up + for (unsigned i = 0; i < SEGLEN; i++) pixels[i] = SEGCOLOR(1); SEGENV.aux0 = 1; } - for (int j = 0; j <= SEGLEN / 15; j++) { - if (random8() <= SEGMENT.intensity) { + for (unsigned j = 0; j <= SEGLEN / 15; j++) { + if (hw_random8() <= SEGMENT.intensity) { for (size_t times = 0; times < 10; times++) { //attempt to spawn a new pixel 10 times - unsigned i = random16(SEGLEN); - unsigned index = i >> 3; - unsigned bitNum = i & 0x07; - bool fadeUp = bitRead(SEGENV.data[index], bitNum); + unsigned i = hw_random16(SEGLEN); if (SEGENV.aux0) { //dissolve to primary/palette - if (fadeUp) { - if (color == SEGCOLOR(0)) { - SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); - } else { - SEGMENT.setPixelColor(i, color); - } - bitWrite(SEGENV.data[index], bitNum, false); + if (pixels[i] == SEGCOLOR(1)) { + pixels[i] = color == SEGCOLOR(0) ? SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0) : color; break; //only spawn 1 new pixel per frame per 50 LEDs } } else { //dissolve to secondary - if (!fadeUp) { - SEGMENT.setPixelColor(i, SEGCOLOR(1)); break; - bitWrite(SEGENV.data[index], bitNum, true); + if (pixels[i] != SEGCOLOR(1)) { + pixels[i] = SEGCOLOR(1); + break; } } } } } + // fix for #4401 + for (unsigned i = 0; i < SEGLEN; i++) SEGMENT.setPixelColor(i, pixels[i]); if (SEGENV.step > (255 - SEGMENT.speed) + 15U) { SEGENV.aux0 = !SEGENV.aux0; SEGENV.step = 0; - memset(SEGMENT.data, (SEGENV.aux0 ? 0xFF : 0), dataSize); // switch fading } else { SEGENV.step++; } @@ -651,7 +690,7 @@ uint16_t dissolve(uint32_t color) { * Blink several LEDs on and then off */ uint16_t mode_dissolve(void) { - return dissolve(SEGMENT.check1 ? SEGMENT.color_wheel(random8()) : SEGCOLOR(0)); + return dissolve(SEGMENT.check1 ? SEGMENT.color_wheel(hw_random8()) : SEGCOLOR(0)); } static const char _data_FX_MODE_DISSOLVE[] PROGMEM = "Dissolve@Repeat speed,Dissolve speed,,,,Random;!,!;!"; @@ -660,7 +699,7 @@ static const char _data_FX_MODE_DISSOLVE[] PROGMEM = "Dissolve@Repeat speed,Diss * Blink several LEDs on and then off in random colors */ uint16_t mode_dissolve_random(void) { - return dissolve(SEGMENT.color_wheel(random8())); + return dissolve(SEGMENT.color_wheel(hw_random8())); } static const char _data_FX_MODE_DISSOLVE_RANDOM[] PROGMEM = "Dissolve Rnd@Repeat speed,Dissolve speed;,!;!"; @@ -670,14 +709,14 @@ static const char _data_FX_MODE_DISSOLVE_RANDOM[] PROGMEM = "Dissolve Rnd@Repeat * Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/ */ uint16_t mode_sparkle(void) { - if (!SEGMENT.check2) for(int i = 0; i < SEGLEN; i++) { + if (!SEGMENT.check2) for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); } uint32_t cycleTime = 10 + (255 - SEGMENT.speed)*2; uint32_t it = strip.now / cycleTime; if (it != SEGENV.step) { - SEGENV.aux0 = random16(SEGLEN); // aux0 stores the random led index + SEGENV.aux0 = hw_random16(SEGLEN); // aux0 stores the random led index SEGENV.step = it; } @@ -692,13 +731,13 @@ static const char _data_FX_MODE_SPARKLE[] PROGMEM = "Sparkle@!,,,,,,Overlay;!,!; * Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/ */ uint16_t mode_flash_sparkle(void) { - if (!SEGMENT.check2) for (int i = 0; i < SEGLEN; i++) { + if (!SEGMENT.check2) for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } if (strip.now - SEGENV.aux0 > SEGENV.step) { - if(random8((255-SEGMENT.intensity) >> 4) == 0) { - SEGMENT.setPixelColor(random16(SEGLEN), SEGCOLOR(1)); //flash + if(hw_random8((255-SEGMENT.intensity) >> 4) == 0) { + SEGMENT.setPixelColor(hw_random16(SEGLEN), SEGCOLOR(1)); //flash } SEGENV.step = strip.now; SEGENV.aux0 = 255-SEGMENT.speed; @@ -713,14 +752,15 @@ static const char _data_FX_MODE_FLASH_SPARKLE[] PROGMEM = "Sparkle Dark@!,!,,,,, * Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/ */ uint16_t mode_hyper_sparkle(void) { - if (!SEGMENT.check2) for (int i = 0; i < SEGLEN; i++) { + if (!SEGMENT.check2) for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } if (strip.now - SEGENV.aux0 > SEGENV.step) { - if (random8((255-SEGMENT.intensity) >> 4) == 0) { - for (int i = 0; i < max(1, SEGLEN/3); i++) { - SEGMENT.setPixelColor(random16(SEGLEN), SEGCOLOR(1)); + if (hw_random8((255-SEGMENT.intensity) >> 4) == 0) { + int len = max(1, (int)SEGLEN/3); + for (int i = 0; i < len; i++) { + SEGMENT.setPixelColor(hw_random16(SEGLEN), SEGCOLOR(1)); } } SEGENV.step = strip.now; @@ -735,7 +775,7 @@ static const char _data_FX_MODE_HYPER_SPARKLE[] PROGMEM = "Sparkle+@!,!,,,,,Over * Strobe effect with different strobe count and pause, controlled by speed. */ uint16_t mode_multi_strobe(void) { - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); } @@ -762,57 +802,45 @@ static const char _data_FX_MODE_MULTI_STROBE[] PROGMEM = "Strobe Mega@!,!;!,!;!; /* - * Android loading circle + * Android loading circle, refactored by @dedehai */ uint16_t mode_android(void) { - - for (int i = 0; i < SEGLEN; i++) { - SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); - } - - if (SEGENV.aux1 > (SEGMENT.intensity*SEGLEN)/255) - { - SEGENV.aux0 = 1; - } else - { - if (SEGENV.aux1 < 2) SEGENV.aux0 = 0; - } - - unsigned a = SEGENV.step & 0xFFFFU; - - if (SEGENV.aux0 == 0) - { - if (SEGENV.call %3 == 1) {a++;} - else {SEGENV.aux1++;} - } else - { - a++; - if (SEGENV.call %3 != 1) SEGENV.aux1--; - } - - if (a >= SEGLEN) a = 0; - - if (a + SEGENV.aux1 < SEGLEN) - { - for (unsigned i = a; i < a+SEGENV.aux1; i++) { - SEGMENT.setPixelColor(i, SEGCOLOR(0)); - } - } else - { - for (unsigned i = a; i < SEGLEN; i++) { - SEGMENT.setPixelColor(i, SEGCOLOR(0)); - } - for (unsigned i = 0; i < SEGENV.aux1 - (SEGLEN -a); i++) { - SEGMENT.setPixelColor(i, SEGCOLOR(0)); + if (!SEGENV.allocateData(sizeof(uint32_t))) return mode_static(); + uint32_t* counter = reinterpret_cast(SEGENV.data); + unsigned size = SEGENV.aux1 >> 1; // upper 15 bit + unsigned shrinking = SEGENV.aux1 & 0x01; // lowest bit + if(strip.now >= SEGENV.step) { + SEGENV.step = strip.now + 3 + ((8 * (uint32_t)(255 - SEGMENT.speed)) / SEGLEN); + if (size > (SEGMENT.intensity * SEGLEN) / 255) + shrinking = 1; + else if (size < 2) + shrinking = 0; + if (!shrinking) { // growing + if ((*counter % 3) == 1) + SEGENV.aux0++; // advance start position + else + size++; + } else { // shrinking + SEGENV.aux0++; + if ((*counter % 3) != 1) + size--; } + SEGENV.aux1 = size << 1 | shrinking; // save back + (*counter)++; + if (SEGENV.aux0 >= SEGLEN) SEGENV.aux0 = 0; } - SEGENV.step = a; - - return 3 + ((8 * (uint32_t)(255 - SEGMENT.speed)) / SEGLEN); + uint32_t start = SEGENV.aux0; + uint32_t end = (SEGENV.aux0 + size) % SEGLEN; + for (unsigned i = 0; i < SEGLEN; i++) { + if ((start < end && i >= start && i < end) || (start >= end && (i >= start || i < end))) + SEGMENT.setPixelColor(i, SEGCOLOR(0)); + else + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); + } + return FRAMETIME; } static const char _data_FX_MODE_ANDROID[] PROGMEM = "Android@!,Width;!,!;!;;m12=1"; //vertical - /* * color chase function. * color1 = background color @@ -980,11 +1008,11 @@ static const char _data_FX_MODE_COLORFUL[] PROGMEM = "Colorful@!,Saturation;1,2, * Emulates a traffic light. */ uint16_t mode_traffic_light(void) { - if (SEGLEN == 1) return mode_static(); - for (int i=0; i < SEGLEN; i++) + if (SEGLEN <= 1) return mode_static(); + for (unsigned i=0; i < SEGLEN; i++) SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); uint32_t mdelay = 500; - for (int i = 0; i < SEGLEN-2 ; i+=3) + for (unsigned i = 0; i < SEGLEN-2 ; i+=3) { switch (SEGENV.aux0) { @@ -1013,10 +1041,10 @@ static const char _data_FX_MODE_TRAFFIC_LIGHT[] PROGMEM = "Traffic Light@!,US st */ #define FLASH_COUNT 4 uint16_t mode_chase_flash(void) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); unsigned flash_step = SEGENV.call % ((FLASH_COUNT * 2) + 1); - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } @@ -1043,7 +1071,7 @@ static const char _data_FX_MODE_CHASE_FLASH[] PROGMEM = "Chase Flash@!;Bg,Fx;!"; * Prim flashes running, followed by random color. */ uint16_t mode_chase_flash_random(void) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); unsigned flash_step = SEGENV.call % ((FLASH_COUNT * 2) + 1); for (int i = 0; i < SEGENV.aux1; i++) { @@ -1090,14 +1118,14 @@ static const char _data_FX_MODE_RUNNING_COLOR[] PROGMEM = "Chase 2@!,Width;!,!;! uint16_t mode_running_random(void) { uint32_t cycleTime = 25 + (3 * (uint32_t)(255 - SEGMENT.speed)); uint32_t it = strip.now / cycleTime; - if (SEGENV.call == 0) SEGENV.aux0 = random16(); // random seed for PRNG on start + if (SEGENV.call == 0) SEGENV.aux0 = hw_random(); // random seed for PRNG on start unsigned zoneSize = ((255-SEGMENT.intensity) >> 4) +1; uint16_t PRNG16 = SEGENV.aux0; unsigned z = it % zoneSize; bool nzone = (!z && it != SEGENV.aux1); - for (unsigned i=SEGLEN-1; i > 0; i--) { + for (int i=SEGLEN-1; i >= 0; i--) { if (nzone || z >= zoneSize) { unsigned lastrand = PRNG16 >> 8; int16_t diff = 0; @@ -1125,7 +1153,7 @@ static const char _data_FX_MODE_RUNNING_RANDOM[] PROGMEM = "Stream@!,Zone size;; * K.I.T.T. */ uint16_t mode_larson_scanner(void) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); const unsigned speed = FRAMETIME * map(SEGMENT.speed, 0, 255, 96, 2); // map into useful range const unsigned pixels = SEGLEN / speed; // how many pixels to advance per frame @@ -1178,12 +1206,11 @@ uint16_t mode_dual_larson_scanner(void){ } static const char _data_FX_MODE_DUAL_LARSON_SCANNER[] PROGMEM = "Scanner Dual@!,Trail,Delay,,,Dual,Bi-delay;!,!,!;!;;m12=0,c1=0"; - /* * Firing comets from one end. "Lighthouse" */ uint16_t mode_comet(void) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); unsigned counter = (strip.now * ((SEGMENT.speed >>2) +1)) & 0xFFFF; unsigned index = (counter * SEGLEN) >> 16; if (SEGENV.call == 0) SEGENV.aux0 = index; @@ -1206,14 +1233,13 @@ uint16_t mode_comet(void) { } static const char _data_FX_MODE_COMET[] PROGMEM = "Lighthouse@!,Fade rate;!,!;!"; - /* * Fireworks function. */ uint16_t mode_fireworks() { - if (SEGLEN == 1) return mode_static(); - const uint16_t width = SEGMENT.is2D() ? SEGMENT.virtualWidth() : SEGMENT.virtualLength(); - const uint16_t height = SEGMENT.virtualHeight(); + if (SEGLEN <= 1) return mode_static(); + const uint16_t width = SEGMENT.is2D() ? SEG_W : SEGLEN; + const uint16_t height = SEG_H; if (SEGENV.call == 0) { SEGENV.aux0 = UINT16_MAX; @@ -1235,11 +1261,11 @@ uint16_t mode_fireworks() { } for (int i=0; i> 1)) == 0) { - uint16_t index = random16(width*height); + if (hw_random8(129 - (SEGMENT.intensity >> 1)) == 0) { + uint16_t index = hw_random16(width*height); x = index % width; y = index / width; - uint32_t col = SEGMENT.color_from_palette(random8(), false, false, 0); + uint32_t col = SEGMENT.color_from_palette(hw_random8(), false, false, 0); if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(x, y, col); else SEGMENT.setPixelColor(index, col); SEGENV.aux1 = SEGENV.aux0; // old spark @@ -1250,12 +1276,11 @@ uint16_t mode_fireworks() { } static const char _data_FX_MODE_FIREWORKS[] PROGMEM = "Fireworks@,Frequency;!,!;!;12;ix=192,pal=11"; - //Twinkling LEDs running. Inspired by https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Rain.h uint16_t mode_rain() { - if (SEGLEN == 1) return mode_static(); - const unsigned width = SEGMENT.virtualWidth(); - const unsigned height = SEGMENT.virtualHeight(); + if (SEGLEN <= 1) return mode_static(); + const unsigned width = SEG_W; + const unsigned height = SEG_H; SEGENV.step += FRAMETIME; if (SEGENV.call && SEGENV.step > SPEED_FORMULA_L) { SEGENV.step = 1; @@ -1269,7 +1294,7 @@ uint16_t mode_rain() { } else { //shift all leds left uint32_t ctemp = SEGMENT.getPixelColor(0); - for (int i = 0; i < SEGLEN - 1; i++) { + for (unsigned i = 0; i < SEGLEN - 1; i++) { SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); } SEGMENT.setPixelColor(SEGLEN -1, ctemp); // wrap around @@ -1285,7 +1310,6 @@ uint16_t mode_rain() { } static const char _data_FX_MODE_RAIN[] PROGMEM = "Rain@!,Spawning rate;!,!;!;12;ix=128,pal=0"; - /* * Fire flicker function */ @@ -1300,8 +1324,8 @@ uint16_t mode_fire_flicker(void) { byte b = (SEGCOLOR(0) ); byte lum = (SEGMENT.palette == 0) ? MAX(w, MAX(r, MAX(g, b))) : 255; lum /= (((256-SEGMENT.intensity)/16)+1); - for (int i = 0; i < SEGLEN; i++) { - byte flicker = random8(lum); + for (unsigned i = 0; i < SEGLEN; i++) { + byte flicker = hw_random8(lum); if (SEGMENT.palette == 0) { SEGMENT.setPixelColor(i, MAX(r - flicker, 0), MAX(g - flicker, 0), MAX(b - flicker, 0), MAX(w - flicker, 0)); } else { @@ -1319,7 +1343,7 @@ static const char _data_FX_MODE_FIRE_FLICKER[] PROGMEM = "Fire Flicker@!,!;!;!;0 * Gradient run base function */ uint16_t gradient_base(bool loading) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); uint16_t counter = strip.now * ((SEGMENT.speed >> 2) + 1); uint16_t pp = (counter * SEGLEN) >> 16; if (SEGENV.call == 0) pp = 0; @@ -1329,14 +1353,14 @@ uint16_t gradient_base(bool loading) { int p1 = pp-SEGLEN; int p2 = pp+SEGLEN; - for (int i = 0; i < SEGLEN; i++) { + for (int i = 0; i < (int)SEGLEN; i++) { if (loading) { val = abs(((i>pp) ? p2:pp) - i); } else { val = min(abs(pp-i),min(abs(p1-i),abs(p2-i))); } val = (brd > val) ? (val * 255) / brd : 255; - SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(0), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1), val)); + SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(0), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1), uint8_t(val))); } return FRAMETIME; @@ -1360,16 +1384,19 @@ uint16_t mode_loading(void) { } static const char _data_FX_MODE_LOADING[] PROGMEM = "Loading@!,Fade;!,!;!;;ix=16"; - -//American Police Light with all LEDs Red and Blue -uint16_t police_base(uint32_t color1, uint32_t color2) { - if (SEGLEN == 1) return mode_static(); +/* + * Two dots running + */ +uint16_t mode_two_dots() { + if (SEGLEN <= 1) return mode_static(); unsigned delay = 1 + (FRAMETIME<<3) / SEGLEN; // longer segments should change faster uint32_t it = strip.now / map(SEGMENT.speed, 0, 255, delay<<4, delay); unsigned offset = it % SEGLEN; - unsigned width = ((SEGLEN*(SEGMENT.intensity+1))>>9); //max width is half the strip if (!width) width = 1; + if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(2)); + const uint32_t color1 = SEGCOLOR(0); + const uint32_t color2 = (SEGCOLOR(1) == SEGCOLOR(2)) ? color1 : SEGCOLOR(1); for (unsigned i = 0; i < width; i++) { unsigned indexR = (offset + i) % SEGLEN; unsigned indexB = (offset + i + (SEGLEN>>1)) % SEGLEN; @@ -1378,23 +1405,6 @@ uint16_t police_base(uint32_t color1, uint32_t color2) { } return FRAMETIME; } - - -//Police Lights Red and Blue -//uint16_t mode_police() -//{ -// SEGMENT.fill(SEGCOLOR(1)); -// return police_base(RED, BLUE); -//} -//static const char _data_FX_MODE_POLICE[] PROGMEM = "Police@!,Width;,Bg;0"; - - -//Police Lights with custom colors -uint16_t mode_two_dots() { - if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(2)); - uint32_t color2 = (SEGCOLOR(1) == SEGCOLOR(2)) ? SEGCOLOR(0) : SEGCOLOR(1); - return police_base(SEGCOLOR(0), color2); -} static const char _data_FX_MODE_TWO_DOTS[] PROGMEM = "Two Dots@!,Dot size,,,,,Overlay;1,2,Bg;!"; @@ -1414,7 +1424,7 @@ typedef struct Flasher { uint16_t mode_fairy() { //set every pixel to a 'random' color from palette (using seed so it doesn't change between frames) uint16_t PRNG16 = 5100 + strip.getCurrSegmentId(); - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; //next 'random' number SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(PRNG16 >> 8, false, false, 0)); } @@ -1441,16 +1451,16 @@ uint16_t mode_fairy() { if (z == zones-1) flashersInZone = numFlashers-(flashersInZone*(zones-1)); for (unsigned f = firstFlasher; f < firstFlasher + flashersInZone; f++) { - unsigned stateTime = now16 - flashers[f].stateStart; + unsigned stateTime = uint16_t(now16 - flashers[f].stateStart); //random on/off time reached, switch state if (stateTime > flashers[f].stateDur * 10) { flashers[f].stateOn = !flashers[f].stateOn; if (flashers[f].stateOn) { - flashers[f].stateDur = 12 + random8(12 + ((255 - SEGMENT.speed) >> 2)); //*10, 250ms to 1250ms + flashers[f].stateDur = 12 + hw_random8(12 + ((255 - SEGMENT.speed) >> 2)); //*10, 250ms to 1250ms } else { - flashers[f].stateDur = 20 + random8(6 + ((255 - SEGMENT.speed) >> 2)); //*10, 250ms to 1250ms + flashers[f].stateDur = 20 + hw_random8(6 + ((255 - SEGMENT.speed) >> 2)); //*10, 250ms to 1250ms } - //flashers[f].stateDur = 51 + random8(2 + ((255 - SEGMENT.speed) >> 1)); + //flashers[f].stateDur = 51 + hw_random8(2 + ((255 - SEGMENT.speed) >> 1)); flashers[f].stateStart = now16; if (stateTime < 255) { flashers[f].stateStart -= 255 -stateTime; //start early to get correct bri @@ -1470,7 +1480,7 @@ uint16_t mode_fairy() { unsigned globalPeakBri = 255 - ((avgFlasherBri * MAX_SHIMMER) >> 8); //183-255, suitable for 1/5th of LEDs flashers for (unsigned f = firstFlasher; f < firstFlasher + flashersInZone; f++) { - unsigned bri = (flasherBri[f - firstFlasher] * globalPeakBri) / 255; + uint8_t bri = (flasherBri[f - firstFlasher] * globalPeakBri) / 255; PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; //next 'random' number unsigned flasherPos = f*flasherDistance; SEGMENT.setPixelColor(flasherPos, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(PRNG16 >> 8, false, false, 0), bri)); @@ -1499,29 +1509,29 @@ uint16_t mode_fairytwinkle() { unsigned riseFallTime = 400 + (255-SEGMENT.speed)*3; unsigned maxDur = riseFallTime/100 + ((255 - SEGMENT.intensity) >> 2) + 13 + ((255 - SEGMENT.intensity) >> 1); - for (int f = 0; f < SEGLEN; f++) { - unsigned stateTime = now16 - flashers[f].stateStart; + for (unsigned f = 0; f < SEGLEN; f++) { + uint16_t stateTime = now16 - flashers[f].stateStart; //random on/off time reached, switch state if (stateTime > flashers[f].stateDur * 100) { flashers[f].stateOn = !flashers[f].stateOn; bool init = !flashers[f].stateDur; if (flashers[f].stateOn) { - flashers[f].stateDur = riseFallTime/100 + ((255 - SEGMENT.intensity) >> 2) + random8(12 + ((255 - SEGMENT.intensity) >> 1)) +1; + flashers[f].stateDur = riseFallTime/100 + ((255 - SEGMENT.intensity) >> 2) + hw_random8(12 + ((255 - SEGMENT.intensity) >> 1)) +1; } else { - flashers[f].stateDur = riseFallTime/100 + random8(3 + ((255 - SEGMENT.speed) >> 6)) +1; + flashers[f].stateDur = riseFallTime/100 + hw_random8(3 + ((255 - SEGMENT.speed) >> 6)) +1; } flashers[f].stateStart = now16; stateTime = 0; if (init) { flashers[f].stateStart -= riseFallTime; //start lit - flashers[f].stateDur = riseFallTime/100 + random8(12 + ((255 - SEGMENT.intensity) >> 1)) +5; //fire up a little quicker + flashers[f].stateDur = riseFallTime/100 + hw_random8(12 + ((255 - SEGMENT.intensity) >> 1)) +5; //fire up a little quicker stateTime = riseFallTime; } } if (flashers[f].stateOn && flashers[f].stateDur > maxDur) flashers[f].stateDur = maxDur; //react more quickly on intensity change if (stateTime > riseFallTime) stateTime = riseFallTime; //for flasher brightness calculation, fades in first 255 ms of state unsigned fadeprog = 255 - ((stateTime * 255) / riseFallTime); - unsigned flasherBri = (flashers[f].stateOn) ? 255-gamma8(fadeprog) : gamma8(fadeprog); + uint8_t flasherBri = (flashers[f].stateOn) ? 255-gamma8(fadeprog) : gamma8(fadeprog); unsigned lastR = PRNG16; unsigned diff = 0; while (diff < 0x4000) { //make sure colors of two adjacent LEDs differ enough @@ -1544,7 +1554,7 @@ uint16_t tricolor_chase(uint32_t color1, uint32_t color2) { unsigned width = (1 + (SEGMENT.intensity>>4)); // value of 1-16 for each colour unsigned index = it % (width*3); - for (int i = 0; i < SEGLEN; i++, index++) { + for (unsigned i = 0; i < SEGLEN; i++, index++) { if (index > (width*3)-1) index = 0; uint32_t color = color1; @@ -1582,13 +1592,13 @@ uint16_t mode_icu(void) { SEGMENT.setPixelColor(dest + SEGLEN/space, col); if(SEGENV.aux0 == dest) { // pause between eye movements - if(random8(6) == 0) { // blink once in a while + if(hw_random8(6) == 0) { // blink once in a while SEGMENT.setPixelColor(dest, SEGCOLOR(1)); SEGMENT.setPixelColor(dest + SEGLEN/space, SEGCOLOR(1)); return 200; } - SEGENV.aux0 = random16(SEGLEN-SEGLEN/space); - return 1000 + random16(2000); + SEGENV.aux0 = hw_random16(SEGLEN-SEGLEN/space); + return 1000 + hw_random16(2000); } if(SEGENV.aux0 > SEGENV.step) { @@ -1617,7 +1627,7 @@ uint16_t mode_tricolor_wipe(void) { unsigned ledIndex = (prog * SEGLEN * 3) >> 16; unsigned ledOffset = ledIndex; - for (int i = 0; i < SEGLEN; i++) + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 2)); } @@ -1690,7 +1700,7 @@ uint16_t mode_tricolor_fade(void) { } static const char _data_FX_MODE_TRICOLOR_FADE[] PROGMEM = "Tri Fade@!;1,2,3;!"; - +#ifdef WLED_PS_DONT_REPLACE_FX /* * Creates random comets * Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/MultiComet.h @@ -1718,7 +1728,7 @@ uint16_t mode_multi_comet(void) { } comets[i]++; } else { - if(!random16(SEGLEN)) { + if(!hw_random16(SEGLEN)) { comets[i] = 0; } } @@ -1729,6 +1739,7 @@ uint16_t mode_multi_comet(void) { } static const char _data_FX_MODE_MULTI_COMET[] PROGMEM = "Multi Comet@!,Fade;!,!;!;1"; #undef MAX_COMETS +#endif // WLED_PS_DONT_REPLACE_FX /* * Running random pixels ("Stream 2") @@ -1745,7 +1756,7 @@ uint16_t mode_random_chase(void) { uint32_t color = SEGENV.step; random16_set_seed(SEGENV.aux0); - for (unsigned i = SEGLEN -1; i > 0; i--) { + for (int i = SEGLEN -1; i >= 0; i--) { uint8_t r = random8(6) != 0 ? (color >> 16 & 0xFF) : random8(); uint8_t g = random8(6) != 0 ? (color >> 8 & 0xFF) : random8(); uint8_t b = random8(6) != 0 ? (color & 0xFF) : random8(); @@ -1798,24 +1809,24 @@ uint16_t mode_oscillate(void) { // if the counter has increased, move the oscillator by the random step if (it != SEGENV.step) oscillators[i].pos += oscillators[i].dir * oscillators[i].speed; oscillators[i].size = SEGLEN/(3+SEGMENT.intensity/8); - if((oscillators[i].dir == -1) && (oscillators[i].pos <= 0)) { + if((oscillators[i].dir == -1) && (oscillators[i].pos > SEGLEN << 1)) { // use integer overflow oscillators[i].pos = 0; oscillators[i].dir = 1; // make bigger steps for faster speeds - oscillators[i].speed = SEGMENT.speed > 100 ? random8(2, 4):random8(1, 3); + oscillators[i].speed = SEGMENT.speed > 100 ? hw_random8(2, 4):hw_random8(1, 3); } if((oscillators[i].dir == 1) && (oscillators[i].pos >= (SEGLEN - 1))) { oscillators[i].pos = SEGLEN - 1; oscillators[i].dir = -1; - oscillators[i].speed = SEGMENT.speed > 100 ? random8(2, 4):random8(1, 3); + oscillators[i].speed = SEGMENT.speed > 100 ? hw_random8(2, 4):hw_random8(1, 3); } } for (unsigned i = 0; i < SEGLEN; i++) { uint32_t color = BLACK; for (unsigned j = 0; j < numOscillators; j++) { - if(i >= (unsigned)oscillators[j].pos - oscillators[j].size && i <= oscillators[j].pos + oscillators[j].size) { - color = (color == BLACK) ? SEGCOLOR(j) : color_blend(color, SEGCOLOR(j), 128); + if((int)i >= (int)oscillators[j].pos - oscillators[j].size && i <= oscillators[j].pos + oscillators[j].size) { + color = (color == BLACK) ? SEGCOLOR(j) : color_blend(color, SEGCOLOR(j), uint8_t(128)); } } SEGMENT.setPixelColor(i, color); @@ -1829,14 +1840,14 @@ static const char _data_FX_MODE_OSCILLATE[] PROGMEM = "Oscillate"; //TODO uint16_t mode_lightning(void) { - if (SEGLEN == 1) return mode_static(); - unsigned ledstart = random16(SEGLEN); // Determine starting location of flash - unsigned ledlen = 1 + random16(SEGLEN -ledstart); // Determine length of flash (not to go beyond NUM_LEDS-1) - uint8_t bri = 255/random8(1, 3); + if (SEGLEN <= 1) return mode_static(); + unsigned ledstart = hw_random16(SEGLEN); // Determine starting location of flash + unsigned ledlen = 1 + hw_random16(SEGLEN -ledstart); // Determine length of flash (not to go beyond NUM_LEDS-1) + uint8_t bri = 255/hw_random8(1, 3); if (SEGENV.aux1 == 0) //init, leader flash { - SEGENV.aux1 = random8(4, 4 + SEGMENT.intensity/20); //number of flashes + SEGENV.aux1 = hw_random8(4, 4 + SEGMENT.intensity/20); //number of flashes SEGENV.aux1 *= 2; bri = 52; //leader has lower brightness @@ -1853,15 +1864,15 @@ uint16_t mode_lightning(void) { SEGENV.aux1--; SEGENV.step = strip.now; - //return random8(4, 10); // each flash only lasts one frame/every 24ms... originally 4-10 milliseconds + //return hw_random8(4, 10); // each flash only lasts one frame/every 24ms... originally 4-10 milliseconds } else { if (strip.now - SEGENV.step > SEGENV.aux0) { SEGENV.aux1--; if (SEGENV.aux1 < 2) SEGENV.aux1 = 0; - SEGENV.aux0 = (50 + random8(100)); //delay between flashes + SEGENV.aux0 = (50 + hw_random8(100)); //delay between flashes if (SEGENV.aux1 == 2) { - SEGENV.aux0 = (random8(255 - SEGMENT.speed) * 100); // delay between strikes + SEGENV.aux0 = (hw_random8(255 - SEGMENT.speed) * 100); // delay between strikes } SEGENV.step = strip.now; } @@ -1870,52 +1881,77 @@ uint16_t mode_lightning(void) { } static const char _data_FX_MODE_LIGHTNING[] PROGMEM = "Lightning@!,!,,,,,Overlay;!,!;!"; - -// Pride2015 -// Animated, ever-changing rainbows. -// by Mark Kriegsman: https://gist.github.com/kriegsman/964de772d64c502760e5 -uint16_t mode_pride_2015(void) { +// combined function from original pride and colorwaves +uint16_t mode_colorwaves_pride_base(bool isPride2015) { unsigned duration = 10 + SEGMENT.speed; unsigned sPseudotime = SEGENV.step; unsigned sHue16 = SEGENV.aux0; - uint8_t sat8 = beatsin88_t( 87, 220, 250); - uint8_t brightdepth = beatsin88_t( 341, 96, 224); - unsigned brightnessthetainc16 = beatsin88_t( 203, (25 * 256), (40 * 256)); + uint8_t sat8 = isPride2015 ? beatsin88_t(87, 220, 250) : 255; + unsigned brightdepth = beatsin88_t(341, 96, 224); + unsigned brightnessthetainc16 = beatsin88_t(203, (25 * 256), (40 * 256)); unsigned msmultiplier = beatsin88_t(147, 23, 60); - unsigned hue16 = sHue16;//gHue * 256; - unsigned hueinc16 = beatsin88_t(113, 1, 3000); + unsigned hue16 = sHue16; + unsigned hueinc16 = isPride2015 ? beatsin88_t(113, 1, 3000) : + beatsin88_t(113, 60, 300) * SEGMENT.intensity * 10 / 255; sPseudotime += duration * msmultiplier; - sHue16 += duration * beatsin88_t( 400, 5,9); + sHue16 += duration * beatsin88_t(400, 5, 9); unsigned brightnesstheta16 = sPseudotime; - for (unsigned i = 0 ; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { hue16 += hueinc16; - uint8_t hue8 = hue16 >> 8; + uint8_t hue8; - brightnesstheta16 += brightnessthetainc16; - unsigned b16 = sin16_t( brightnesstheta16 ) + 32768; + if (isPride2015) { + hue8 = hue16 >> 8; + } else { + unsigned h16_128 = hue16 >> 7; + hue8 = (h16_128 & 0x100) ? (255 - (h16_128 >> 1)) : (h16_128 >> 1); + } + brightnesstheta16 += brightnessthetainc16; + unsigned b16 = sin16_t(brightnesstheta16) + 32768; unsigned bri16 = (uint32_t)((uint32_t)b16 * (uint32_t)b16) / 65536; uint8_t bri8 = (uint32_t)(((uint32_t)bri16) * brightdepth) / 65536; bri8 += (255 - brightdepth); - CRGB newcolor = CHSV(hue8, sat8, bri8); - SEGMENT.blendPixelColor(i, newcolor, 64); + if (isPride2015) { + CRGBW newcolor = CRGB(CHSV(hue8, sat8, bri8)); + newcolor.color32 = gamma32inv(newcolor.color32); + SEGMENT.blendPixelColor(i, newcolor, 64); + } else { + SEGMENT.blendPixelColor(i, SEGMENT.color_from_palette(hue8, false, PALETTE_SOLID_WRAP, 0, bri8), 128); + } } + SEGENV.step = sPseudotime; SEGENV.aux0 = sHue16; return FRAMETIME; } + +// Pride2015 +// Animated, ever-changing rainbows. +// by Mark Kriegsman: https://gist.github.com/kriegsman/964de772d64c502760e5 +uint16_t mode_pride_2015(void) { + return mode_colorwaves_pride_base(true); +} static const char _data_FX_MODE_PRIDE_2015[] PROGMEM = "Pride 2015@!;;"; +// ColorWavesWithPalettes by Mark Kriegsman: https://gist.github.com/kriegsman/8281905786e8b2632aeb +// This function draws color waves with an ever-changing, +// widely-varying set of parameters, using a color palette. +uint16_t mode_colorwaves() { + return mode_colorwaves_pride_base(false); +} +static const char _data_FX_MODE_COLORWAVES[] PROGMEM = "Colorwaves@!,Hue;!;!;;pal=26"; + //eight colored dots, weaving in and out of sync with each other uint16_t mode_juggle(void) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); SEGMENT.fadeToBlackBy(192 - (3*SEGMENT.intensity/4)); CRGB fastled_col; @@ -1923,7 +1959,7 @@ uint16_t mode_juggle(void) { for (int i = 0; i < 8; i++) { int index = 0 + beatsin88_t((16 + SEGMENT.speed)*(i + 7), 0, SEGLEN -1); fastled_col = CRGB(SEGMENT.getPixelColor(index)); - fastled_col |= (SEGMENT.palette==0)?CHSV(dothue, 220, 255):ColorFromPalette(SEGPALETTE, dothue, 255); + fastled_col |= (SEGMENT.palette==0)?CHSV(dothue, 220, 255):CRGB(ColorFromPalette(SEGPALETTE, dothue, 255)); SEGMENT.setPixelColor(index, fastled_col); dothue += 32; } @@ -1956,8 +1992,8 @@ uint16_t mode_palette() { constexpr float (*cosFunction)(float) = &cos_t; #endif const bool isMatrix = strip.isMatrix; - const int cols = SEGMENT.virtualWidth(); - const int rows = isMatrix ? SEGMENT.virtualHeight() : strip.getActiveSegmentsNum(); + const int cols = SEG_W; + const int rows = isMatrix ? SEG_H : strip.getActiveSegmentsNum(); const int inputShift = SEGMENT.speed; const int inputSize = SEGMENT.intensity; @@ -2003,7 +2039,7 @@ uint16_t mode_palette() { const mathType sourceX = xtSinTheta + ytCosTheta + centerX; // The computation was scaled just right so that the result should always be in range [0, maxXOut], but enforce this anyway // to account for imprecision. Then scale it so that the range is [0, 255], which we can use with the palette. - int colorIndex = (std::min(std::max(sourceX, mathType(0)), maxXOut * sInt16Scale) * 255) / (sInt16Scale * maxXOut); + int colorIndex = (std::min(std::max(sourceX, mathType(0)), maxXOut * sInt16Scale) * wideMathType(255)) / (sInt16Scale * maxXOut); // inputSize determines by how much we want to scale the palette: // values < 128 display a fraction of a palette, // values > 128 display multiple palettes. @@ -2030,7 +2066,7 @@ uint16_t mode_palette() { } static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Shift,Size,Rotation,,,Animate Shift,Animate Rotation,Anamorphic;;!;12;ix=112,c1=0,o1=1,o2=0,o3=1"; - +#ifdef WLED_PS_DONT_REPLACE_FX // WLED limitation: Analog Clock overlay will NOT work when Fire2012 is active // Fire2012 by Mark Kriegsman, July 2012 // as part of "Five Elements" shown here: http://youtu.be/knWiGsmgycY @@ -2060,7 +2096,7 @@ static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Shift,Size,Rotation // feel of your fire: COOLING (used in step 1 above) (Speed = COOLING), and SPARKING (used // in step 3 above) (Effect Intensity = Sparking). uint16_t mode_fire_2012() { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); const unsigned strips = SEGMENT.nrOfVStrips(); if (!SEGENV.allocateData(strips * SEGLEN)) return mode_static(); //allocation failed byte* heat = SEGENV.data; @@ -2070,11 +2106,11 @@ uint16_t mode_fire_2012() { struct virtualStrip { static void runStrip(uint16_t stripNr, byte* heat, uint32_t it) { - const uint8_t ignition = max(3,SEGLEN/10); // ignition area: 10% of segment length or minimum 3 pixels + const uint8_t ignition = MAX(3,SEGLEN/10); // ignition area: 10% of segment length or minimum 3 pixels // Step 1. Cool down every cell a little - for (int i = 0; i < SEGLEN; i++) { - uint8_t cool = (it != SEGENV.step) ? random8((((20 + SEGMENT.speed/3) * 16) / SEGLEN)+2) : random8(4); + for (unsigned i = 0; i < SEGLEN; i++) { + uint8_t cool = (it != SEGENV.step) ? hw_random8((((20 + SEGMENT.speed/3) * 16) / SEGLEN)+2) : hw_random8(4); uint8_t minTemp = (i> 8; - unsigned h16_128 = hue16 >> 7; - if ( h16_128 & 0x100) { - hue8 = 255 - (h16_128 >> 1); - } else { - hue8 = h16_128 >> 1; - } - - brightnesstheta16 += brightnessthetainc16; - unsigned b16 = sin16_t(brightnesstheta16) + 32768; - - unsigned bri16 = (uint32_t)((uint32_t)b16 * (uint32_t)b16) / 65536; - uint8_t bri8 = (uint32_t)(((uint32_t)bri16) * brightdepth) / 65536; - bri8 += (255 - brightdepth); - - SEGMENT.blendPixelColor(i, SEGMENT.color_from_palette(hue8, false, PALETTE_SOLID_WRAP, 0, bri8), 128); // 50/50 mix - } - SEGENV.step = sPseudotime; - SEGENV.aux0 = sHue16; - - return FRAMETIME; -} -static const char _data_FX_MODE_COLORWAVES[] PROGMEM = "Colorwaves@!,Hue;!;!"; - +static const char _data_FX_MODE_FIRE_2012[] PROGMEM = "Fire 2012@Cooling,Spark rate,,2D Blur,Boost;;!;1;pal=35,sx=64,ix=160,m12=1,c2=128"; // bars +#endif // WLED_PS_DONT_REPLACE_FX // colored stripes pulsing at a defined Beats-Per-Minute (BPM) uint16_t mode_bpm() { uint32_t stp = (strip.now / 20) & 0xFF; uint8_t beat = beatsin8_t(SEGMENT.speed, 64, 255); - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(stp + (i * 2), false, PALETTE_SOLID_WRAP, 0, beat - stp + (i * 10))); } @@ -2179,9 +2169,9 @@ static const char _data_FX_MODE_BPM[] PROGMEM = "Bpm@!;!;!;;sx=64"; uint16_t mode_fillnoise8() { - if (SEGENV.call == 0) SEGENV.step = random16(12345); - for (int i = 0; i < SEGLEN; i++) { - unsigned index = inoise8(i * SEGLEN, SEGENV.step + i * SEGLEN); + if (SEGENV.call == 0) SEGENV.step = hw_random(); + for (unsigned i = 0; i < SEGLEN; i++) { + unsigned index = perlin8(i * SEGLEN, SEGENV.step + i * SEGLEN); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } SEGENV.step += beatsin8_t(SEGMENT.speed, 1, 6); //10,1,4 @@ -2195,13 +2185,13 @@ uint16_t mode_noise16_1() { unsigned scale = 320; // the "zoom factor" for the noise SEGENV.step += (1 + SEGMENT.speed/16); - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { unsigned shift_x = beatsin8_t(11); // the x position of the noise field swings @ 17 bpm unsigned shift_y = SEGENV.step/42; // the y position becomes slowly incremented unsigned real_x = (i + shift_x) * scale; // the x position of the noise field swings @ 17 bpm unsigned real_y = (i + shift_y) * scale; // the y position becomes slowly incremented uint32_t real_z = SEGENV.step; // the z position becomes quickly incremented - unsigned noise = inoise16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down + unsigned noise = perlin16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down unsigned index = sin8_t(noise * 3); // map LED color based on noise data SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); @@ -2209,17 +2199,17 @@ uint16_t mode_noise16_1() { return FRAMETIME; } -static const char _data_FX_MODE_NOISE16_1[] PROGMEM = "Noise 1@!;!;!"; +static const char _data_FX_MODE_NOISE16_1[] PROGMEM = "Noise 1@!;!;!;;pal=20"; uint16_t mode_noise16_2() { unsigned scale = 1000; // the "zoom factor" for the noise SEGENV.step += (1 + (SEGMENT.speed >> 1)); - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { unsigned shift_x = SEGENV.step >> 6; // x as a function of time uint32_t real_x = (i + shift_x) * scale; // calculate the coordinates within the noise field - unsigned noise = inoise16(real_x, 0, 4223) >> 8; // get the noise data and scale it down + unsigned noise = perlin16(real_x, 0, 4223) >> 8; // get the noise data and scale it down unsigned index = sin8_t(noise * 3); // map led color based on noise data SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0, noise)); @@ -2227,20 +2217,20 @@ uint16_t mode_noise16_2() { return FRAMETIME; } -static const char _data_FX_MODE_NOISE16_2[] PROGMEM = "Noise 2@!;!;!"; +static const char _data_FX_MODE_NOISE16_2[] PROGMEM = "Noise 2@!;!;!;;pal=43"; uint16_t mode_noise16_3() { unsigned scale = 800; // the "zoom factor" for the noise SEGENV.step += (1 + SEGMENT.speed); - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { unsigned shift_x = 4223; // no movement along x and y unsigned shift_y = 1234; uint32_t real_x = (i + shift_x) * scale; // calculate the coordinates within the noise field uint32_t real_y = (i + shift_y) * scale; // based on the precalculated positions uint32_t real_z = SEGENV.step*8; - unsigned noise = inoise16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down + unsigned noise = perlin16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down unsigned index = sin8_t(noise * 3); // map led color based on noise data SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0, noise)); @@ -2248,19 +2238,19 @@ uint16_t mode_noise16_3() { return FRAMETIME; } -static const char _data_FX_MODE_NOISE16_3[] PROGMEM = "Noise 3@!;!;!"; +static const char _data_FX_MODE_NOISE16_3[] PROGMEM = "Noise 3@!;!;!;;pal=35"; //https://github.com/aykevl/ledstrip-spark/blob/master/ledstrip.ino uint16_t mode_noise16_4() { uint32_t stp = (strip.now * SEGMENT.speed) >> 7; - for (int i = 0; i < SEGLEN; i++) { - int index = inoise16(uint32_t(i) << 12, stp); + for (unsigned i = 0; i < SEGLEN; i++) { + int index = perlin16(uint32_t(i) << 12, stp); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } return FRAMETIME; } -static const char _data_FX_MODE_NOISE16_4[] PROGMEM = "Noise 4@!;!;!"; +static const char _data_FX_MODE_NOISE16_4[] PROGMEM = "Noise 4@!;!;!;;pal=26"; //based on https://gist.github.com/kriegsman/5408ecd397744ba0393e @@ -2268,46 +2258,45 @@ uint16_t mode_colortwinkle() { unsigned dataSize = (SEGLEN+7) >> 3; //1 bit per LED if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed - CRGB fastled_col, prev; + CRGBW col, prev; fract8 fadeUpAmount = strip.getBrightness()>28 ? 8 + (SEGMENT.speed>>2) : 68-strip.getBrightness(); fract8 fadeDownAmount = strip.getBrightness()>28 ? 8 + (SEGMENT.speed>>3) : 68-strip.getBrightness(); - for (int i = 0; i < SEGLEN; i++) { - fastled_col = SEGMENT.getPixelColor(i); - prev = fastled_col; + for (unsigned i = 0; i < SEGLEN; i++) { + CRGBW cur = SEGMENT.getPixelColor(i); + prev = cur; unsigned index = i >> 3; unsigned bitNum = i & 0x07; bool fadeUp = bitRead(SEGENV.data[index], bitNum); if (fadeUp) { - CRGB incrementalColor = fastled_col; - incrementalColor.nscale8_video(fadeUpAmount); - fastled_col += incrementalColor; + CRGBW incrementalColor = color_fade(cur, fadeUpAmount, true); + col = color_add(cur, incrementalColor); - if (fastled_col.red == 255 || fastled_col.green == 255 || fastled_col.blue == 255) { + if (col.r == 255 || col.g == 255 || col.b == 255) { bitWrite(SEGENV.data[index], bitNum, false); } - SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); - if (SEGMENT.getPixelColor(i) == RGBW32(prev.r, prev.g, prev.b, 0)) { //fix "stuck" pixels - fastled_col += fastled_col; - SEGMENT.setPixelColor(i, fastled_col); + if (cur == prev) { //fix "stuck" pixels + color_add(col, col); + SEGMENT.setPixelColor(i, col); } - } else { - fastled_col.nscale8(255 - fadeDownAmount); - SEGMENT.setPixelColor(i, fastled_col); + else SEGMENT.setPixelColor(i, col); + } + else { + col = color_fade(cur, 255 - fadeDownAmount); + SEGMENT.setPixelColor(i, col); } } for (unsigned j = 0; j <= SEGLEN / 50; j++) { - if (random8() <= SEGMENT.intensity) { + if (hw_random8() <= SEGMENT.intensity) { for (unsigned times = 0; times < 5; times++) { //attempt to spawn a new pixel 5 times - int i = random16(SEGLEN); + int i = hw_random16(SEGLEN); if (SEGMENT.getPixelColor(i) == 0) { - fastled_col = ColorFromPalette(SEGPALETTE, random8(), 64, NOBLEND); unsigned index = i >> 3; unsigned bitNum = i & 0x07; bitWrite(SEGENV.data[index], bitNum, true); - SEGMENT.setPixelColor(i, fastled_col); + SEGMENT.setPixelColor(i, ColorFromPalette(SEGPALETTE, hw_random8(), 64, NOBLEND)); break; //only spawn 1 new pixel per frame per 50 LEDs } } @@ -2325,7 +2314,7 @@ uint16_t mode_lake() { int wave2 = beatsin8_t(sp +1, -64,64); int wave3 = beatsin8_t(sp +2, 0,80); - for (int i = 0; i < SEGLEN; i++) + for (unsigned i = 0; i < SEGLEN; i++) { int index = cos8_t((i*15)+ wave1)/2 + cubicwave8((i*23)+ wave2)/2; uint8_t lum = (index > wave3) ? index - wave3 : 0; @@ -2337,99 +2326,80 @@ uint16_t mode_lake() { static const char _data_FX_MODE_LAKE[] PROGMEM = "Lake@!;Fx;!"; -// meteor effect +// meteor effect & meteor smooth (merged by @dedehai) // send a meteor from begining to to the end of the strip with a trail that randomly decays. // adapted from https://www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/#LEDStripEffectMeteorRain uint16_t mode_meteor() { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); if (!SEGENV.allocateData(SEGLEN)) return mode_static(); //allocation failed - + const bool meteorSmooth = SEGMENT.check3; byte* trail = SEGENV.data; const unsigned meteorSize = 1 + SEGLEN / 20; // 5% - unsigned counter = strip.now * ((SEGMENT.speed >> 2) +8); - uint16_t in = counter * SEGLEN >> 16; - - const int max = SEGMENT.palette==5 ? 239 : 255; // "* Colors only" palette blends end with start - // fade all leds to colors[1] in LEDs one step - for (int i = 0; i < SEGLEN; i++) { - if (random8() <= 255 - SEGMENT.intensity) { - int meteorTrailDecay = 128 + random8(127); - trail[i] = scale8(trail[i], meteorTrailDecay); - int index = trail[i]; - int idx = 255; - int bri = SEGMENT.palette==35 || SEGMENT.palette==36 ? 255 : trail[i]; - if (!SEGMENT.check1) { - idx = 0; - index = map(i,0,SEGLEN,0,max); - bri = trail[i]; - } - uint32_t col = SEGMENT.color_from_palette(index, false, false, idx, bri); // full brightness for Fire - SEGMENT.setPixelColor(i, col); - } + uint16_t meteorstart; + if(meteorSmooth) meteorstart = map((SEGENV.step >> 6 & 0xFF), 0, 255, 0, SEGLEN -1); + else { + unsigned counter = strip.now * ((SEGMENT.speed >> 2) + 8); + meteorstart = (counter * SEGLEN) >> 16; } - // draw meteor - for (unsigned j = 0; j < meteorSize; j++) { - int index = (in + j) % SEGLEN; - int idx = 255; - int i = trail[index] = max; - if (!SEGMENT.check1) { - i = map(index,0,SEGLEN,0,max); - idx = 0; - } - uint32_t col = SEGMENT.color_from_palette(i, false, false, idx, 255); // full brightness - SEGMENT.setPixelColor(index, col); - } - - return FRAMETIME; -} -static const char _data_FX_MODE_METEOR[] PROGMEM = "Meteor@!,Trail,,,,Gradient;!;!;1"; - - -// smooth meteor effect -// send a meteor from begining to to the end of the strip with a trail that randomly decays. -// adapted from https://www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/#LEDStripEffectMeteorRain -uint16_t mode_meteor_smooth() { - if (SEGLEN == 1) return mode_static(); - if (!SEGENV.allocateData(SEGLEN)) return mode_static(); //allocation failed - - byte* trail = SEGENV.data; - - const unsigned meteorSize = 1+ SEGLEN / 20; // 5% - uint16_t in = map((SEGENV.step >> 6 & 0xFF), 0, 255, 0, SEGLEN -1); - const int max = SEGMENT.palette==5 || !SEGMENT.check1 ? 240 : 255; // fade all leds to colors[1] in LEDs one step for (unsigned i = 0; i < SEGLEN; i++) { - if (/*trail[i] != 0 &&*/ random8() <= 255 - SEGMENT.intensity) { - int change = trail[i] + 4 - random8(24); //change each time between -20 and +4 - trail[i] = constrain(change, 0, max); - uint32_t col = SEGMENT.check1 ? SEGMENT.color_from_palette(i, true, false, 0, trail[i]) : SEGMENT.color_from_palette(trail[i], false, true, 255); + uint32_t col; + if (hw_random8() <= 255 - SEGMENT.intensity) { + if(meteorSmooth) { + if (trail[i] > 0) { + int change = trail[i] + 4 - hw_random8(24); //change each time between -20 and +4 + trail[i] = constrain(change, 0, max); + } + col = SEGMENT.check1 ? SEGMENT.color_from_palette(i, true, false, 0, trail[i]) : SEGMENT.color_from_palette(trail[i], false, true, 255); + } + else { + trail[i] = scale8(trail[i], 128 + hw_random8(127)); + int index = trail[i]; + int idx = 255; + int bri = SEGMENT.palette==35 || SEGMENT.palette==36 ? 255 : trail[i]; + if (!SEGMENT.check1) { + idx = 0; + index = map(i,0,SEGLEN,0,max); + bri = trail[i]; + } + col = SEGMENT.color_from_palette(index, false, false, idx, bri); // full brightness for Fire + } SEGMENT.setPixelColor(i, col); } } // draw meteor for (unsigned j = 0; j < meteorSize; j++) { - unsigned index = in + j; - if (index >= SEGLEN) { - index -= SEGLEN; + unsigned index = (meteorstart + j) % SEGLEN; + if(meteorSmooth) { + trail[index] = max; + uint32_t col = SEGMENT.check1 ? SEGMENT.color_from_palette(index, true, false, 0, trail[index]) : SEGMENT.color_from_palette(trail[index], false, true, 255); + SEGMENT.setPixelColor(index, col); + } + else{ + int idx = 255; + int i = trail[index] = max; + if (!SEGMENT.check1) { + i = map(index,0,SEGLEN,0,max); + idx = 0; + } + uint32_t col = SEGMENT.color_from_palette(i, false, false, idx, 255); // full brightness + SEGMENT.setPixelColor(index, col); } - trail[index] = max; - uint32_t col = SEGMENT.check1 ? SEGMENT.color_from_palette(index, true, false, 0, trail[index]) : SEGMENT.color_from_palette(trail[index], false, true, 255); - SEGMENT.setPixelColor(index, col); } SEGENV.step += SEGMENT.speed +1; return FRAMETIME; } -static const char _data_FX_MODE_METEOR_SMOOTH[] PROGMEM = "Meteor Smooth@!,Trail,,,,Gradient;;!;1"; +static const char _data_FX_MODE_METEOR[] PROGMEM = "Meteor@!,Trail,,,,Gradient,,Smooth;;!;1"; //Railway Crossing / Christmas Fairy lights uint16_t mode_railway() { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); unsigned dur = (256 - SEGMENT.speed) * 40; uint16_t rampdur = (dur * SEGMENT.intensity) >> 8; if (SEGENV.step > dur) @@ -2445,7 +2415,7 @@ uint16_t mode_railway() { if (p0 < 255) pos = p0; } if (SEGENV.aux0) pos = 255 - pos; - for (int i = 0; i < SEGLEN; i += 2) + for (unsigned i = 0; i < SEGLEN; i += 2) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(255 - pos, false, false, 255)); // do not use color 1 or 2, always use palette if (i < SEGLEN -1) @@ -2456,7 +2426,7 @@ uint16_t mode_railway() { SEGENV.step += FRAMETIME; return FRAMETIME; } -static const char _data_FX_MODE_RAILWAY[] PROGMEM = "Railway@!,Smoothness;1,2;!"; +static const char _data_FX_MODE_RAILWAY[] PROGMEM = "Railway@!,Smoothness;1,2;!;;pal=3"; //Water ripple @@ -2475,8 +2445,8 @@ typedef struct Ripple { #else #define MAX_RIPPLES 100 #endif -static uint16_t ripple_base() { - unsigned maxRipples = min(1 + (SEGLEN >> 2), MAX_RIPPLES); // 56 max for 16 segment ESP8266 +static uint16_t ripple_base(uint8_t blurAmount = 0) { + unsigned maxRipples = min(1 + (int)(SEGLEN >> 2), MAX_RIPPLES); // 56 max for 16 segment ESP8266 unsigned dataSize = sizeof(ripple) * maxRipples; if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed @@ -2508,7 +2478,7 @@ static uint16_t ripple_base() { int left = rippleorigin - propI -1; int right = rippleorigin + propI +2; for (int v = 0; v < 4; v++) { - unsigned mag = scale8(cubicwave8((propF>>2) + v * 64), amp); + uint8_t mag = scale8(cubicwave8((propF>>2) + v * 64), amp); SEGMENT.setPixelColor(left + v, color_blend(SEGMENT.getPixelColor(left + v), col, mag)); // TODO SEGMENT.setPixelColor(right - v, color_blend(SEGMENT.getPixelColor(right - v), col, mag)); // TODO } @@ -2516,42 +2486,45 @@ static uint16_t ripple_base() { ripplestate += rippledecay; ripples[i].state = (ripplestate > 254) ? 0 : ripplestate; } else {//randomly create new wave - if (random16(IBN + 10000) <= (SEGMENT.intensity >> (SEGMENT.is2D()*3))) { + if (hw_random16(IBN + 10000) <= (SEGMENT.intensity >> (SEGMENT.is2D()*3))) { ripples[i].state = 1; - ripples[i].pos = SEGMENT.is2D() ? ((random8(SEGENV.virtualWidth())<<8) | (random8(SEGENV.virtualHeight()))) : random16(SEGLEN); - ripples[i].color = random8(); //color + ripples[i].pos = SEGMENT.is2D() ? ((hw_random8(SEG_W)<<8) | (hw_random8(SEG_H))) : hw_random16(SEGLEN); + ripples[i].color = hw_random8(); //color } } } - + SEGMENT.blur(blurAmount); return FRAMETIME; } #undef MAX_RIPPLES uint16_t mode_ripple(void) { - if (SEGLEN == 1) return mode_static(); - if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); - else SEGMENT.fade_out(250); - return ripple_base(); + if (SEGLEN <= 1) return mode_static(); + if(SEGMENT.custom1 || SEGMENT.check2) // blur or overlay + SEGMENT.fade_out(250); + else + SEGMENT.fill(SEGCOLOR(1)); + + return ripple_base(SEGMENT.custom1>>1); } -static const char _data_FX_MODE_RIPPLE[] PROGMEM = "Ripple@!,Wave #,,,,,Overlay;,!;!;12"; +static const char _data_FX_MODE_RIPPLE[] PROGMEM = "Ripple@!,Wave #,Blur,,,,Overlay;,!;!;12;c1=0"; uint16_t mode_ripple_rainbow(void) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); if (SEGENV.call ==0) { - SEGENV.aux0 = random8(); - SEGENV.aux1 = random8(); + SEGENV.aux0 = hw_random8(); + SEGENV.aux1 = hw_random8(); } if (SEGENV.aux0 == SEGENV.aux1) { - SEGENV.aux1 = random8(); + SEGENV.aux1 = hw_random8(); } else if (SEGENV.aux1 > SEGENV.aux0) { SEGENV.aux0++; } else { SEGENV.aux0--; } - SEGMENT.fill(color_blend(SEGMENT.color_wheel(SEGENV.aux0),BLACK,235)); + SEGMENT.fill(color_blend(SEGMENT.color_wheel(SEGENV.aux0),BLACK,uint8_t(235))); return ripple_base(); } static const char _data_FX_MODE_RIPPLE_RAINBOW[] PROGMEM = "Ripple Rainbow@!,Wave #;;!;12"; @@ -2646,7 +2619,7 @@ static uint16_t twinklefox_base(bool cat) unsigned backgroundBrightness = bg.getAverageLight(); - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; // next 'random' number unsigned myclockoffset16= PRNG16; // use that number as clock offset @@ -2666,15 +2639,15 @@ static uint16_t twinklefox_base(bool cat) if (deltabright >= 32 || (!bg)) { // If the new pixel is significantly brighter than the background color, // use the new color. - SEGMENT.setPixelColor(i, c.red, c.green, c.blue); + SEGMENT.setPixelColor(i, c); } else if (deltabright > 0) { // If the new pixel is just slightly brighter than the background color, // mix a blend of the new color and the background color - SEGMENT.setPixelColor(i, color_blend(RGBW32(bg.r,bg.g,bg.b,0), RGBW32(c.r,c.g,c.b,0), deltabright * 8)); + SEGMENT.setPixelColor(i, color_blend(RGBW32(bg.r,bg.g,bg.b,0), RGBW32(c.r,c.g,c.b,0), uint8_t(deltabright * 8))); } else { // if the new pixel is not at all brighter than the background color, // just use the background color. - SEGMENT.setPixelColor(i, bg.r, bg.g, bg.b); + SEGMENT.setPixelColor(i, bg); } } return FRAMETIME; @@ -2717,9 +2690,9 @@ uint16_t mode_halloween_eyes() uint32_t blinkEndTime; }; - if (SEGLEN == 1) return mode_static(); - const unsigned maxWidth = strip.isMatrix ? SEGMENT.virtualWidth() : SEGLEN; - const unsigned HALLOWEEN_EYE_SPACE = MAX(2, strip.isMatrix ? SEGMENT.virtualWidth()>>4: SEGLEN>>5); + if (SEGLEN <= 1) return mode_static(); + const unsigned maxWidth = strip.isMatrix ? SEG_W : SEGLEN; + const unsigned HALLOWEEN_EYE_SPACE = MAX(2, strip.isMatrix ? SEG_W>>4: SEGLEN>>5); const unsigned HALLOWEEN_EYE_WIDTH = HALLOWEEN_EYE_SPACE/2; unsigned eyeLength = (2*HALLOWEEN_EYE_WIDTH) + HALLOWEEN_EYE_SPACE; if (eyeLength >= maxWidth) return mode_static(); //bail if segment too short @@ -2740,10 +2713,10 @@ uint16_t mode_halloween_eyes() // - select a duration // - immediately switch to eyes on state. - data.startPos = random16(0, maxWidth - eyeLength - 1); - data.color = random8(); - if (strip.isMatrix) SEGMENT.offset = random16(SEGMENT.virtualHeight()-1); // a hack: reuse offset since it is not used in matrices - duration = 128u + random16(SEGMENT.intensity*64u); + data.startPos = hw_random16(0, maxWidth - eyeLength - 1); + data.color = hw_random8(); + if (strip.isMatrix) SEGMENT.offset = hw_random16(SEG_H-1); // a hack: reuse offset since it is not used in matrices + duration = 128u + hw_random16(SEGMENT.intensity*64u); data.duration = duration; data.state = eyeState::on; [[fallthrough]]; @@ -2766,15 +2739,15 @@ uint16_t mode_halloween_eyes() const uint32_t eyeColor = SEGMENT.color_from_palette(data.color, false, false, 0); uint32_t c = eyeColor; if (fadeInAnimationState < 256u) { - c = color_blend(backgroundColor, eyeColor, fadeInAnimationState); + c = color_blend(backgroundColor, eyeColor, uint8_t(fadeInAnimationState)); } else if (elapsedTime > minimumOnTimeBegin) { const uint32_t remainingTime = (elapsedTime >= duration) ? 0u : (duration - elapsedTime); if (remainingTime > minimumOnTimeEnd) { - if (random8() < 4u) + if (hw_random8() < 4u) { c = backgroundColor; data.state = eyeState::blink; - data.blinkEndTime = strip.now + random8(8, 128); + data.blinkEndTime = strip.now + hw_random8(8, 128); } } } @@ -2808,7 +2781,7 @@ uint16_t mode_halloween_eyes() // - immediately switch to eyes-off state const unsigned eyeOffTimeBase = SEGMENT.speed*128u; - duration = eyeOffTimeBase + random16(eyeOffTimeBase); + duration = eyeOffTimeBase + hw_random16(eyeOffTimeBase); data.duration = duration; data.state = eyeState::off; [[fallthrough]]; @@ -2860,7 +2833,7 @@ uint16_t mode_static_pattern() bool drawingLit = true; unsigned cnt = 0; - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, (drawingLit) ? SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0) : SEGCOLOR(1)); cnt++; if (cnt >= ((drawingLit) ? lit : unlit)) { @@ -2880,7 +2853,7 @@ uint16_t mode_tri_static_pattern() unsigned currSeg = 0; unsigned currSegCount = 0; - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { if ( currSeg % 3 == 0 ) { SEGMENT.setPixelColor(i, SEGCOLOR(0)); } else if( currSeg % 3 == 1) { @@ -2902,7 +2875,7 @@ static const char _data_FX_MODE_TRI_STATIC_PATTERN[] PROGMEM = "Solid Pattern Tr static uint16_t spots_base(uint16_t threshold) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); unsigned maxZones = SEGLEN >> 2; @@ -2919,7 +2892,7 @@ static uint16_t spots_base(uint16_t threshold) if (wave > threshold) { unsigned index = 0 + pos + i; unsigned s = (wave - threshold)*255 / (0xFFFF - threshold); - SEGMENT.setPixelColor(index, color_blend(SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0), SEGCOLOR(1), 255-s)); + SEGMENT.setPixelColor(index, color_blend(SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0), SEGCOLOR(1), uint8_t(255-s))); } } } @@ -2946,7 +2919,6 @@ uint16_t mode_spots_fade() } static const char _data_FX_MODE_SPOTS_FADE[] PROGMEM = "Spots Fade@Spread,Width,,,,,Overlay;!,!;!"; - //each needs 12 bytes typedef struct Ball { unsigned long lastBounceTime; @@ -2958,7 +2930,7 @@ typedef struct Ball { * Bouncing Balls Effect */ uint16_t mode_bouncing_balls(void) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); //allocate segment data const unsigned strips = SEGMENT.nrOfVStrips(); // adapt for 2D const size_t maxNumBalls = 16; @@ -2998,7 +2970,7 @@ uint16_t mode_bouncing_balls(void) { balls[i].lastBounceTime = time; if (balls[i].impactVelocity < 0.015f) { - float impactVelocityStart = sqrtf(-2.0f * gravity) * random8(5,11)/10.0f; // randomize impact velocity + float impactVelocityStart = sqrtf(-2.0f * gravity) * hw_random8(5,11)/10.0f; // randomize impact velocity balls[i].impactVelocity = impactVelocityStart; } } else if (balls[i].height > 1.0f) { @@ -3030,11 +3002,11 @@ uint16_t mode_bouncing_balls(void) { } static const char _data_FX_MODE_BOUNCINGBALLS[] PROGMEM = "Bouncing Balls@Gravity,# of balls,,,,,Overlay;!,!,!;!;1;m12=1"; //bar - +#ifdef WLED_PS_DONT_REPLACE_FX /* * bouncing balls on a track track Effect modified from Aircoookie's bouncing balls * Courtesy of pjhatch (https://github.com/pjhatch) - * https://github.com/Aircoookie/WLED/pull/1039 + * https://github.com/wled-dev/WLED/pull/1039 */ // modified for balltrack mode typedef struct RollingBall { @@ -3061,10 +3033,10 @@ static uint16_t rolling_balls(void) { SEGMENT.fill(hasCol2 ? BLACK : SEGCOLOR(1)); // start clean for (unsigned i = 0; i < maxNumBalls; i++) { balls[i].lastBounceUpdate = strip.now; - balls[i].velocity = 20.0f * float(random16(1000, 10000))/10000.0f; // number from 1 to 10 - if (random8()<128) balls[i].velocity = -balls[i].velocity; // 50% chance of reverse direction - balls[i].height = (float(random16(0, 10000)) / 10000.0f); // from 0. to 1. - balls[i].mass = (float(random16(1000, 10000)) / 10000.0f); // from .1 to 1. + balls[i].velocity = 20.0f * float(hw_random16(1000, 10000))/10000.0f; // number from 1 to 10 + if (hw_random8()<128) balls[i].velocity = -balls[i].velocity; // 50% chance of reverse direction + balls[i].height = (float(hw_random16(0, 10000)) / 10000.0f); // from 0. to 1. + balls[i].mass = (float(hw_random16(1000, 10000)) / 10000.0f); // from .1 to 1. } } @@ -3080,7 +3052,7 @@ static uint16_t rolling_balls(void) { float thisHeight = balls[i].height + balls[i].velocity * timeSinceLastUpdate; // this method keeps higher resolution // test if intensity level was increased and some balls are way off the track then put them back if (thisHeight < -0.5f || thisHeight > 1.5f) { - thisHeight = balls[i].height = (float(random16(0, 10000)) / 10000.0f); // from 0. to 1. + thisHeight = balls[i].height = (float(hw_random16(0, 10000)) / 10000.0f); // from 0. to 1. balls[i].lastBounceUpdate = strip.now; } // check if reached ends of the strip @@ -3129,14 +3101,14 @@ static uint16_t rolling_balls(void) { return FRAMETIME; } -static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of balls,,,,Collisions,Overlay,Trails;!,!,!;!;1;m12=1"; //bar - +static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of balls,,,,Collide,Overlay,Trails;!,!,!;!;1;m12=1"; //bar +#endif // WLED_PS_DONT_REPLACE_FX /* * Sinelon stolen from FASTLED examples */ static uint16_t sinelon_base(bool dual, bool rainbow=false) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); SEGMENT.fade_out(SEGMENT.intensity); unsigned pos = beatsin16_t(SEGMENT.speed/10,0,SEGLEN-1); if (SEGENV.call == 0) SEGENV.aux0 = pos; @@ -3190,7 +3162,7 @@ static const char _data_FX_MODE_SINELON_RAINBOW[] PROGMEM = "Sinelon Rainbow@!,T // utility function that will add random glitter to SEGMENT void glitter_base(uint8_t intensity, uint32_t col = ULTRAWHITE) { - if (intensity > random8()) SEGMENT.setPixelColor(random16(SEGLEN), col); + if (intensity > hw_random8()) SEGMENT.setPixelColor(hw_random16(SEGLEN), col); } //Glitter with palette background, inspired by https://gist.github.com/kriegsman/062e10f7f07ba8518af6 @@ -3213,7 +3185,7 @@ uint16_t mode_glitter() glitter_base(SEGMENT.intensity, SEGCOLOR(2) ? SEGCOLOR(2) : ULTRAWHITE); return FRAMETIME; } -static const char _data_FX_MODE_GLITTER[] PROGMEM = "Glitter@!,!,,,,,Overlay;,,Glitter color;!;;pal=0,m12=0"; //pixels +static const char _data_FX_MODE_GLITTER[] PROGMEM = "Glitter@!,!,,,,,Overlay;,,Glitter color;!;;pal=11,m12=0"; //pixels //Solid colour background with glitter (can be replaced by Glitter) @@ -3225,7 +3197,6 @@ uint16_t mode_solid_glitter() } static const char _data_FX_MODE_SOLID_GLITTER[] PROGMEM = "Solid Glitter@,!;Bg,,Glitter color;;;m12=0"; - //each needs 20 bytes //Spark type is used for popcorn, 1D fireworks, and drip typedef struct Spark { @@ -3241,7 +3212,7 @@ typedef struct Spark { * modified from https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Popcorn.h */ uint16_t mode_popcorn(void) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); //allocate segment data unsigned strips = SEGMENT.nrOfVStrips(); unsigned usablePopcorns = maxNumPopcorn; @@ -3262,23 +3233,23 @@ uint16_t mode_popcorn(void) { unsigned numPopcorn = SEGMENT.intensity * usablePopcorns / 255; if (numPopcorn == 0) numPopcorn = 1; - for(unsigned i = 0; i < numPopcorn; i++) { + for (unsigned i = 0; i < numPopcorn; i++) { if (popcorn[i].pos >= 0.0f) { // if kernel is active, update its position popcorn[i].pos += popcorn[i].vel; popcorn[i].vel += gravity; } else { // if kernel is inactive, randomly pop it - if (random8() < 2) { // POP!!! + if (hw_random8() < 2) { // POP!!! popcorn[i].pos = 0.01f; - unsigned peakHeight = 128 + random8(128); //0-255 + unsigned peakHeight = 128 + hw_random8(128); //0-255 peakHeight = (peakHeight * (SEGLEN -1)) >> 8; popcorn[i].vel = sqrtf(-2.0f * gravity * peakHeight); if (SEGMENT.palette) { - popcorn[i].colIndex = random8(); + popcorn[i].colIndex = hw_random8(); } else { - byte col = random8(0, NUM_COLORS); + byte col = hw_random8(0, NUM_COLORS); if (!SEGCOLOR(2) || !SEGCOLOR(col)) col = 0; popcorn[i].colIndex = col; } @@ -3301,7 +3272,6 @@ uint16_t mode_popcorn(void) { } static const char _data_FX_MODE_POPCORN[] PROGMEM = "Popcorn@!,!,,,,,Overlay;!,!,!;!;;m12=1"; //bar - //values close to 100 produce 5Hz flicker, which looks very candle-y //Inspired by https://github.com/avanhanegem/ArduinoCandleEffectNeoPixel //and https://cpldcpu.wordpress.com/2016/01/05/reverse-engineering-a-real-candle/ @@ -3310,7 +3280,7 @@ uint16_t candle(bool multi) { if (multi && SEGLEN > 1) { //allocate segment data - unsigned dataSize = max(1, SEGLEN -1) *3; //max. 1365 pixels (ESP8266) + unsigned dataSize = max(1, (int)SEGLEN -1) *3; //max. 1365 pixels (ESP8266) if (!SEGENV.allocateData(dataSize)) return candle(false); //allocation failed } @@ -3340,7 +3310,7 @@ uint16_t candle(bool multi) s = SEGENV.data[d]; s_target = SEGENV.data[d+1]; fadeStep = SEGENV.data[d+2]; } if (fadeStep == 0) { //init vals - s = 128; s_target = 130 + random8(4); fadeStep = 1; + s = 128; s_target = 130 + hw_random8(4); fadeStep = 1; } bool newTarget = false; @@ -3353,8 +3323,8 @@ uint16_t candle(bool multi) } if (newTarget) { - s_target = random8(rndval) + random8(rndval); //between 0 and rndval*2 -2 = 252 - if (s_target < (rndval >> 1)) s_target = (rndval >> 1) + random8(rndval); + s_target = hw_random8(rndval) + hw_random8(rndval); //between 0 and rndval*2 -2 = 252 + if (s_target < (rndval >> 1)) s_target = (rndval >> 1) + hw_random8(rndval); unsigned offset = (255 - valrange); s_target += offset; @@ -3365,12 +3335,12 @@ uint16_t candle(bool multi) } if (i > 0) { - SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), s)); + SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), uint8_t(s))); SEGENV.data[d] = s; SEGENV.data[d+1] = s_target; SEGENV.data[d+2] = fadeStep; } else { - for (int j = 0; j < SEGLEN; j++) { - SEGMENT.setPixelColor(j, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(j, true, PALETTE_SOLID_WRAP, 0), s)); + for (unsigned j = 0; j < SEGLEN; j++) { + SEGMENT.setPixelColor(j, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(j, true, PALETTE_SOLID_WRAP, 0), uint8_t(s))); } SEGENV.aux0 = s; SEGENV.aux1 = s_target; SEGENV.step = fadeStep; @@ -3394,7 +3364,7 @@ uint16_t mode_candle_multi() } static const char _data_FX_MODE_CANDLE_MULTI[] PROGMEM = "Candle Multi@!,!;!,!;!;;sx=96,ix=224,pal=0"; - +#ifdef WLED_PS_DONT_REPLACE_FX /* / Fireworks in starburst effect / based on the video: https://www.reddit.com/r/arduino/comments/c3sd46/i_made_this_fireworks_effect_for_my_led_strips/ @@ -3416,7 +3386,7 @@ typedef struct particle { } star; uint16_t mode_starburst(void) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); unsigned maxData = FAIR_DATA_PER_SEG; //ESP8266: 256 ESP32: 640 unsigned segs = strip.getActiveSegmentsNum(); if (segs <= (strip.getMaxSegments() /2)) maxData *= 2; //ESP8266: 512 if <= 8 segs ESP32: 1280 if <= 16 segs @@ -3440,19 +3410,19 @@ uint16_t mode_starburst(void) { for (unsigned j = 0; j < numStars; j++) { // speed to adjust chance of a burst, max is nearly always. - if (random8((144-(SEGMENT.speed >> 1))) == 0 && stars[j].birth == 0) + if (hw_random8((144-(SEGMENT.speed >> 1))) == 0 && stars[j].birth == 0) { // Pick a random color and location. - unsigned startPos = random16(SEGLEN-1); - float multiplier = (float)(random8())/255.0f * 1.0f; + unsigned startPos = hw_random16(SEGLEN-1); + float multiplier = (float)(hw_random8())/255.0f * 1.0f; - stars[j].color = CRGB(SEGMENT.color_wheel(random8())); + stars[j].color = CRGB(SEGMENT.color_wheel(hw_random8())); stars[j].pos = startPos; - stars[j].vel = maxSpeed * (float)(random8())/255.0f * multiplier; + stars[j].vel = maxSpeed * (float)(hw_random8())/255.0f * multiplier; stars[j].birth = it; stars[j].last = it; // more fragments means larger burst effect - int num = random8(3,6 + (SEGMENT.intensity >> 5)); + int num = hw_random8(3,6 + (SEGMENT.intensity >> 5)); for (int i=0; i < STARBURST_MAX_FRAG; i++) { if (i < num) stars[j].fragment[i] = startPos; @@ -3488,7 +3458,7 @@ uint16_t mode_starburst(void) { float age = it-stars[j].birth; if (age < particleIgnition) { - c = CRGB(color_blend(WHITE, RGBW32(c.r,c.g,c.b,0), 254.5f*((age / particleIgnition)))); + c = CRGB(color_blend(WHITE, RGBW32(c.r,c.g,c.b,0), uint8_t(254.5f*((age / particleIgnition))))); } else { // Figure out how much to fade and shrink the star based on // its age relative to its lifetime @@ -3499,8 +3469,7 @@ uint16_t mode_starburst(void) { } else { age -= particleIgnition; fade = (age / particleFadeTime); // Fading star - byte f = 254.5f*fade; - c = CRGB(color_blend(RGBW32(c.r,c.g,c.b,0), SEGCOLOR(1), f)); + c = CRGB(color_blend(RGBW32(c.r,c.g,c.b,0), SEGCOLOR(1), uint8_t(254.5f*fade))); } } @@ -3512,13 +3481,13 @@ uint16_t mode_starburst(void) { if (stars[j].fragment[i] > 0) { float loc = stars[j].fragment[i]; if (mirrored) loc -= (loc-stars[j].pos)*2; - int start = loc - particleSize; - int end = loc + particleSize; + unsigned start = loc - particleSize; + unsigned end = loc + particleSize; if (start < 0) start = 0; if (start == end) end++; if (end > SEGLEN) end = SEGLEN; - for (int p = start; p < end; p++) { - SEGMENT.setPixelColor(p, c.r, c.g, c.b); + for (unsigned p = start; p < end; p++) { + SEGMENT.setPixelColor(p, c); } } } @@ -3527,8 +3496,9 @@ uint16_t mode_starburst(void) { } #undef STARBURST_MAX_FRAG static const char _data_FX_MODE_STARBURST[] PROGMEM = "Fireworks Starburst@Chance,Fragments,,,,,Overlay;,!;!;;pal=11,m12=0"; +#endif // WLED_PS_DONT_REPLACE_FX - + #ifdef WLED_PS_DONT_REPLACE_FX /* * Exploding fireworks effect * adapted from: http://www.anirama.com/1000leds/1d-fireworks/ @@ -3536,9 +3506,9 @@ static const char _data_FX_MODE_STARBURST[] PROGMEM = "Fireworks Starburst@Chanc */ uint16_t mode_exploding_fireworks(void) { - if (SEGLEN == 1) return mode_static(); - const int cols = SEGMENT.is2D() ? SEGMENT.virtualWidth() : 1; - const int rows = SEGMENT.is2D() ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + if (SEGLEN <= 1) return mode_static(); + const int cols = SEGMENT.is2D() ? SEG_W : 1; + const int rows = SEGMENT.is2D() ? SEG_H : SEGLEN; //allocate segment data unsigned maxData = FAIR_DATA_PER_SEG; //ESP8266: 256 ESP32: 640 @@ -3569,11 +3539,11 @@ uint16_t mode_exploding_fireworks(void) if (SEGENV.aux0 < 2) { //FLARE if (SEGENV.aux0 == 0) { //init flare flare->pos = 0; - flare->posX = SEGMENT.is2D() ? random16(2,cols-3) : (SEGMENT.intensity > random8()); // will enable random firing side on 1D - unsigned peakHeight = 75 + random8(180); //0-255 + flare->posX = SEGMENT.is2D() ? hw_random16(2,cols-3) : (SEGMENT.intensity > hw_random8()); // will enable random firing side on 1D + unsigned peakHeight = 75 + hw_random8(180); //0-255 peakHeight = (peakHeight * (rows -1)) >> 8; flare->vel = sqrtf(-2.0f * gravity * peakHeight); - flare->velX = SEGMENT.is2D() ? (random8(9)-4)/64.0f : 0; // no X velocity on 1D + flare->velX = SEGMENT.is2D() ? (hw_random8(9)-4)/64.0f : 0; // no X velocity on 1D flare->col = 255; //brightness SEGENV.aux0 = 1; } @@ -3601,7 +3571,7 @@ uint16_t mode_exploding_fireworks(void) * Explosion happens where the flare ended. * Size is proportional to the height. */ - unsigned nSparks = flare->pos + random8(4); + unsigned nSparks = flare->pos + hw_random8(4); nSparks = std::max(nSparks, 4U); // This is not a standard constrain; numSparks is not guaranteed to be at least 4 nSparks = std::min(nSparks, numSparks); @@ -3610,12 +3580,12 @@ uint16_t mode_exploding_fireworks(void) for (unsigned i = 1; i < nSparks; i++) { sparks[i].pos = flare->pos; sparks[i].posX = flare->posX; - sparks[i].vel = (float(random16(20001)) / 10000.0f) - 0.9f; // from -0.9 to 1.1 + sparks[i].vel = (float(hw_random16(20001)) / 10000.0f) - 0.9f; // from -0.9 to 1.1 sparks[i].vel *= rows<32 ? 0.5f : 1; // reduce velocity for smaller strips - sparks[i].velX = SEGMENT.is2D() ? (float(random16(20001)) / 10000.0f) - 1.0f : 0; // from -1 to 1 + sparks[i].velX = SEGMENT.is2D() ? (float(hw_random16(20001)) / 10000.0f) - 1.0f : 0; // from -1 to 1 sparks[i].col = 345;//abs(sparks[i].vel * 750.0); // set colors before scaling velocity to keep them bright //sparks[i].col = constrain(sparks[i].col, 0, 345); - sparks[i].colIndex = random8(); + sparks[i].colIndex = hw_random8(); sparks[i].vel *= flare->pos/rows; // proportional to height sparks[i].velX *= SEGMENT.is2D() ? flare->posX/cols : 0; // proportional to width sparks[i].vel *= -gravity *50; @@ -3637,23 +3607,23 @@ uint16_t mode_exploding_fireworks(void) if (SEGMENT.is2D() && !(sparks[i].posX >= 0 && sparks[i].posX < cols)) continue; unsigned prog = sparks[i].col; uint32_t spColor = (SEGMENT.palette) ? SEGMENT.color_wheel(sparks[i].colIndex) : SEGCOLOR(0); - CRGB c = CRGB::Black; //HeatColor(sparks[i].col); + CRGBW c = BLACK; //HeatColor(sparks[i].col); if (prog > 300) { //fade from white to spark color - c = CRGB(color_blend(spColor, WHITE, (prog - 300)*5)); + c = color_blend(spColor, WHITE, uint8_t((prog - 300)*5)); } else if (prog > 45) { //fade from spark color to black - c = CRGB(color_blend(BLACK, spColor, prog - 45)); + c = color_blend(BLACK, spColor, uint8_t(prog - 45)); unsigned cooling = (300 - prog) >> 5; c.g = qsub8(c.g, cooling); c.b = qsub8(c.b, cooling * 2); } - if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(int(sparks[i].posX), rows - int(sparks[i].pos) - 1, c.red, c.green, c.blue); - else SEGMENT.setPixelColor(int(sparks[i].posX) ? rows - int(sparks[i].pos) - 1 : int(sparks[i].pos), c.red, c.green, c.blue); + if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(int(sparks[i].posX), rows - int(sparks[i].pos) - 1, c); + else SEGMENT.setPixelColor(int(sparks[i].posX) ? rows - int(sparks[i].pos) - 1 : int(sparks[i].pos), c); } } if (SEGMENT.check3) SEGMENT.blur(16); *dying_gravity *= .8f; // as sparks burn out they fall slower } else { - SEGENV.aux0 = 6 + random8(10); //wait for this many frames + SEGENV.aux0 = 6 + hw_random8(10); //wait for this many frames } } else { SEGENV.aux0--; @@ -3665,8 +3635,8 @@ uint16_t mode_exploding_fireworks(void) return FRAMETIME; } #undef MAX_SPARKS -static const char _data_FX_MODE_EXPLODING_FIREWORKS[] PROGMEM = "Fireworks 1D@Gravity,Firing side,,,,,,Blur;!,!;!;12;pal=11,ix=128"; - +static const char _data_FX_MODE_EXPLODING_FIREWORKS[] PROGMEM = "Fireworks 1D@Gravity,Firing side;!,!;!;12;pal=11,ix=128"; +#endif // WLED_PS_DONT_REPLACE_FX /* * Drip Effect @@ -3674,7 +3644,7 @@ static const char _data_FX_MODE_EXPLODING_FIREWORKS[] PROGMEM = "Fireworks 1D@Gr */ uint16_t mode_drip(void) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); //allocate segment data unsigned strips = SEGMENT.nrOfVStrips(); const int maxNumDrops = 4; @@ -3690,7 +3660,7 @@ uint16_t mode_drip(void) unsigned numDrops = 1 + (SEGMENT.intensity >> 6); // 255>>6 = 3 float gravity = -0.0005f - (SEGMENT.speed/50000.0f); - gravity *= max(1, SEGLEN-1); + gravity *= max(1, (int)SEGLEN-1); int sourcedrop = 12; for (unsigned j=0;j255) drops[j].col=255; - SEGMENT.setPixelColor(indexToVStrip(uint16_t(drops[j].pos), stripNr), color_blend(BLACK,SEGCOLOR(0),drops[j].col)); + SEGMENT.setPixelColor(indexToVStrip(uint16_t(drops[j].pos), stripNr), color_blend(BLACK,SEGCOLOR(0),uint8_t(drops[j].col))); drops[j].col += map(SEGMENT.speed, 0, 255, 1, 6); // swelling - if (random8() < drops[j].col/10) { // random drop + if (hw_random8() < drops[j].col/10) { // random drop drops[j].colIndex=2; //fall drops[j].col=255; } @@ -3720,12 +3690,12 @@ uint16_t mode_drip(void) drops[j].vel += gravity; // gravity is negative for (int i=1;i<7-drops[j].colIndex;i++) { // some minor math so we don't expand bouncing droplets - unsigned pos = constrain(uint16_t(drops[j].pos) +i, 0, SEGLEN-1); //this is BAD, returns a pos >= SEGLEN occasionally - SEGMENT.setPixelColor(indexToVStrip(pos, stripNr), color_blend(BLACK,SEGCOLOR(0),drops[j].col/i)); //spread pixel with fade while falling + unsigned pos = constrain(unsigned(drops[j].pos) +i, 0, SEGLEN-1); //this is BAD, returns a pos >= SEGLEN occasionally + SEGMENT.setPixelColor(indexToVStrip(pos, stripNr), color_blend(BLACK,SEGCOLOR(0),uint8_t(drops[j].col/i))); //spread pixel with fade while falling } if (drops[j].colIndex > 2) { // during bounce, some water is on the floor - SEGMENT.setPixelColor(indexToVStrip(0, stripNr), color_blend(SEGCOLOR(0),BLACK,drops[j].col)); + SEGMENT.setPixelColor(indexToVStrip(0, stripNr), color_blend(SEGCOLOR(0),BLACK,uint8_t(drops[j].col))); } } else { // we hit bottom if (drops[j].colIndex > 2) { // already hit once, so back to forming @@ -3754,7 +3724,6 @@ uint16_t mode_drip(void) } static const char _data_FX_MODE_DRIP[] PROGMEM = "Drip@Gravity,# of drips,,,,,Overlay;!,!;!;;m12=1"; //bar - /* * Tetris or Stacking (falling bricks) Effect * by Blaz Kristan (AKA blazoncek) (https://github.com/blazoncek, https://blaz.at/home) @@ -3770,7 +3739,7 @@ typedef struct Tetris { } tetris; uint16_t mode_tetrix(void) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); unsigned strips = SEGMENT.nrOfVStrips(); // allow running on virtual strips (columns in 2D segment) unsigned dataSize = sizeof(tetris); if (!SEGENV.allocateData(dataSize * strips)) return mode_static(); //allocation failed @@ -3794,17 +3763,17 @@ uint16_t mode_tetrix(void) { // speed calculation: a single brick should reach bottom of strip in X seconds // if the speed is set to 1 this should take 5s and at 255 it should take 0.25s // as this is dependant on SEGLEN it should be taken into account and the fact that effect runs every FRAMETIME s - int speed = SEGMENT.speed ? SEGMENT.speed : random8(1,255); + int speed = SEGMENT.speed ? SEGMENT.speed : hw_random8(1,255); speed = map(speed, 1, 255, 5000, 250); // time taken for full (SEGLEN) drop drop->speed = float(SEGLEN * FRAMETIME) / float(speed); // set speed drop->pos = SEGLEN; // start at end of segment (no need to subtract 1) - if (!SEGMENT.check1) drop->col = random8(0,15)<<4; // limit color choices so there is enough HUE gap + if (!SEGMENT.check1) drop->col = hw_random8(0,15)<<4; // limit color choices so there is enough HUE gap drop->step = 1; // drop state (0 init, 1 forming, 2 falling) - drop->brick = (SEGMENT.intensity ? (SEGMENT.intensity>>5)+1 : random8(1,5)) * (1+(SEGLEN>>6)); // size of brick + drop->brick = (SEGMENT.intensity ? (SEGMENT.intensity>>5)+1 : hw_random8(1,5)) * (1+(SEGLEN>>6)); // size of brick } if (drop->step == 1) { // forming - if (random8()>>6) { // random drop + if (hw_random8()>>6) { // random drop drop->step = 2; // fall } } @@ -3813,8 +3782,8 @@ uint16_t mode_tetrix(void) { if (drop->pos > drop->stack) { // fall until top of stack drop->pos -= drop->speed; // may add gravity as: speed += gravity if (int(drop->pos) < int(drop->stack)) drop->pos = drop->stack; - for (int i = int(drop->pos); i < SEGLEN; i++) { - uint32_t col = ipos)+drop->brick ? SEGMENT.color_from_palette(drop->col, false, false, 0) : SEGCOLOR(1); + for (unsigned i = unsigned(drop->pos); i < SEGLEN; i++) { + uint32_t col = i < unsigned(drop->pos)+drop->brick ? SEGMENT.color_from_palette(drop->col, false, false, 0) : SEGCOLOR(1); SEGMENT.setPixelColor(indexToVStrip(i, stripNr), col); } } else { // we hit bottom @@ -3828,7 +3797,7 @@ uint16_t mode_tetrix(void) { drop->brick = 0; // reset brick size (no more growing) if (drop->step > strip.now) { // allow fading of virtual strip - for (int i = 0; i < SEGLEN; i++) SEGMENT.blendPixelColor(indexToVStrip(i, stripNr), SEGCOLOR(1), 25); // 10% blend + for (unsigned i = 0; i < SEGLEN; i++) SEGMENT.blendPixelColor(indexToVStrip(i, stripNr), SEGCOLOR(1), 25); // 10% blend } else { drop->stack = 0; // reset brick stack size drop->step = 0; // proceed with next brick @@ -3853,7 +3822,7 @@ static const char _data_FX_MODE_TETRIX[] PROGMEM = "Tetrix@!,Width,,,,One color; uint16_t mode_plasma(void) { // initialize phases on start if (SEGENV.call == 0) { - SEGENV.aux0 = random8(0,2); // add a bit of randomness + SEGENV.aux0 = hw_random8(0,2); // add a bit of randomness } unsigned thisPhase = beatsin8_t(6+SEGENV.aux0,-64,64); unsigned thatPhase = beatsin8_t(7+SEGENV.aux0,-64,64); @@ -3885,7 +3854,7 @@ uint16_t mode_percent(void) { if (SEGMENT.speed == 255) size = 255; if (percent <= 100) { - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { if (i < SEGENV.aux1) { if (SEGMENT.check1) SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(map(percent,0,100,0,255), false, false, 0)); @@ -3897,7 +3866,7 @@ uint16_t mode_percent(void) { } } } else { - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { if (i < (SEGLEN - SEGENV.aux1)) { SEGMENT.setPixelColor(i, SEGCOLOR(1)); } @@ -3947,8 +3916,8 @@ uint16_t mode_heartbeat(void) { SEGENV.step = strip.now; } - for (int i = 0; i < SEGLEN; i++) { - SEGMENT.setPixelColor(i, color_blend(SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), SEGCOLOR(1), 255 - (SEGENV.aux1 >> 8))); + for (unsigned i = 0; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, color_blend(SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), SEGCOLOR(1), uint8_t(255 - (SEGENV.aux1 >> 8)))); } return FRAMETIME; @@ -3981,7 +3950,7 @@ static const char _data_FX_MODE_HEARTBEAT[] PROGMEM = "Heartbeat@!,!;!,!;!;01;m1 // Modified for WLED, based on https://github.com/FastLED/FastLED/blob/master/examples/Pacifica/Pacifica.ino // // Add one layer of waves into the led array -static CRGB pacifica_one_layer(uint16_t i, CRGBPalette16& p, uint16_t cistart, uint16_t wavescale, uint8_t bri, uint16_t ioff) +static CRGB pacifica_one_layer(uint16_t i, const CRGBPalette16& p, uint16_t cistart, uint16_t wavescale, uint8_t bri, uint16_t ioff) { unsigned ci = cistart; unsigned waveangle = ioff; @@ -3993,7 +3962,7 @@ static CRGB pacifica_one_layer(uint16_t i, CRGBPalette16& p, uint16_t cistart, u ci += (cs * i); unsigned sindex16 = sin16_t(ci) + 32768; unsigned sindex8 = scale16(sindex16, 240); - return ColorFromPalette(p, sindex8, bri, LINEARBLEND); + return CRGB(ColorFromPalette(p, sindex8, bri, LINEARBLEND)); } uint16_t mode_pacifica() @@ -4041,7 +4010,7 @@ uint16_t mode_pacifica() unsigned basethreshold = beatsin8_t( 9, 55, 65); unsigned wave = beat8( 7 ); - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { CRGB c = CRGB(2, 6, 10); // Render each of four layers, with different scales and speeds, that vary over time c += pacifica_one_layer(i, pacifica_palette_1, sCIStart1, beatsin16_t(3, 11 * 256, 14 * 256), beatsin8_t(10, 70, 130), 0-beat16(301)); @@ -4077,7 +4046,7 @@ static const char _data_FX_MODE_PACIFICA[] PROGMEM = "Pacifica@!,Angle;;!;;pal=5 * Mode simulates a gradual sunrise */ uint16_t mode_sunrise() { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); //speed 0 - static sun //speed 1 - 60: sunrise time in minutes //speed 60 - 120 : sunset time in minutes - 60; @@ -4104,15 +4073,12 @@ uint16_t mode_sunrise() { if (SEGMENT.speed > 60) stage = 0xFFFF - stage; //sunset } - for (int i = 0; i <= SEGLEN/2; i++) + for (unsigned i = 0; i <= SEGLEN/2; i++) { - //default palette is Fire - uint32_t c = SEGMENT.color_from_palette(0, false, true, 255); //background - + //default palette is Fire unsigned wave = triwave16((i * stage) / SEGLEN); - wave = (wave >> 8) + ((wave * SEGMENT.intensity) >> 15); - + uint32_t c; if (wave > 240) { //clipped, full white sun c = SEGMENT.color_from_palette( 240, false, true, 255); } else { //transition @@ -4124,7 +4090,7 @@ uint16_t mode_sunrise() { return FRAMETIME; } -static const char _data_FX_MODE_SUNRISE[] PROGMEM = "Sunrise@Time [min],Width;;!;;sx=60"; +static const char _data_FX_MODE_SUNRISE[] PROGMEM = "Sunrise@Time [min],Width;;!;;pal=35,sx=60"; /* @@ -4140,14 +4106,14 @@ static uint16_t phased_base(uint8_t moder) { // We're making si unsigned index = strip.now/64; // Set color rotation speed *phase += SEGMENT.speed/32.0; // You can change the speed of the wave. AKA SPEED (was .4) - for (int i = 0; i < SEGLEN; i++) { - if (moder == 1) modVal = (inoise8(i*10 + i*10) /16); // Let's randomize our mod length with some Perlin noise. + for (unsigned i = 0; i < SEGLEN; i++) { + if (moder == 1) modVal = (perlin8(i*10 + i*10) /16); // Let's randomize our mod length with some Perlin noise. unsigned val = (i+1) * allfreq; // This sets the frequency of the waves. The +1 makes sure that led 0 is used. if (modVal == 0) modVal = 1; val += *phase * (i % modVal +1) /2; // This sets the varying phase change of the waves. By Andrew Tuline. unsigned b = cubicwave8(val); // Now we make an 8 bit sinewave. b = (b > cutOff) ? (b - cutOff) : 0; // A ternary operator to cutoff the light. - SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(index, false, false, 0), b)); + SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(index, false, false, 0), uint8_t(b))); index += 256 / SEGLEN; if (SEGLEN > 256) index ++; // Correction for segments longer than 256 LEDs } @@ -4172,7 +4138,7 @@ uint16_t mode_twinkleup(void) { // A very short twinkle routine unsigned prevSeed = random16_get_seed(); // save seed so we can restore it at the end of the function random16_set_seed(535); // The randomizer needs to be re-set each time through the loop in order for the same 'random' numbers to be the same each time through. - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { unsigned ranstart = random8(); // The starting value (aka brightness) for each pixel. Must be consistent each time through the loop for this to work. unsigned pixBri = sin8_t(ranstart + 16 * strip.now/(256-SEGMENT.speed)); if (random8() > SEGMENT.intensity) pixBri = 0; @@ -4200,21 +4166,18 @@ uint16_t mode_noisepal(void) { // Slow noise { SEGENV.step = strip.now; - unsigned baseI = random8(); - palettes[1] = CRGBPalette16(CHSV(baseI+random8(64), 255, random8(128,255)), CHSV(baseI+128, 255, random8(128,255)), CHSV(baseI+random8(92), 192, random8(128,255)), CHSV(baseI+random8(92), 255, random8(128,255))); + unsigned baseI = hw_random8(); + palettes[1] = CRGBPalette16(CHSV(baseI+hw_random8(64), 255, hw_random8(128,255)), CHSV(baseI+128, 255, hw_random8(128,255)), CHSV(baseI+hw_random8(92), 192, hw_random8(128,255)), CHSV(baseI+hw_random8(92), 255, hw_random8(128,255))); } - CRGB color; - //EVERY_N_MILLIS(10) { //(don't have to time this, effect function is only called every 24ms) nblendPaletteTowardPalette(palettes[0], palettes[1], 48); // Blend towards the target palette over 48 iterations. if (SEGMENT.palette > 0) palettes[0] = SEGPALETTE; - for (int i = 0; i < SEGLEN; i++) { - unsigned index = inoise8(i*scale, SEGENV.aux0+i*scale); // Get a value from the noise function. I'm using both x and y axis. - color = ColorFromPalette(palettes[0], index, 255, LINEARBLEND); // Use the my own palette. - SEGMENT.setPixelColor(i, color.red, color.green, color.blue); + for (unsigned i = 0; i < SEGLEN; i++) { + unsigned index = perlin8(i*scale, SEGENV.aux0+i*scale); // Get a value from the noise function. I'm using both x and y axis. + SEGMENT.setPixelColor(i, ColorFromPalette(palettes[0], index, 255, LINEARBLEND)); // Use my own palette. } SEGENV.aux0 += beatsin8_t(10,1,4); // Moving along the distance. Vary it a bit with a sine wave. @@ -4235,8 +4198,8 @@ uint16_t mode_sinewave(void) { // Adjustable sinewave. By Andrew Tul SEGENV.step += SEGMENT.speed/16; // Speed of animation. unsigned freq = SEGMENT.intensity/4;//SEGMENT.fft2/8; // Frequency of the signal. - for (int i = 0; i < SEGLEN; i++) { // For each of the LED's in the strand, set a brightness based on a wave as follows: - int pixBri = cubicwave8((i*freq)+SEGENV.step);//qsuba(cubicwave8((i*freq)+SEGENV.step), (255-SEGMENT.intensity)); // qsub sets a minimum value called thiscutoff. If < thiscutoff, then bright = 0. Otherwise, bright = 128 (as defined in qsub).. + for (unsigned i = 0; i < SEGLEN; i++) { // For each of the LED's in the strand, set a brightness based on a wave as follows: + uint8_t pixBri = cubicwave8((i*freq)+SEGENV.step);//qsuba(cubicwave8((i*freq)+SEGENV.step), (255-SEGMENT.intensity)); // qsub sets a minimum value called thiscutoff. If < thiscutoff, then bright = 0. Otherwise, bright = 128 (as defined in qsub).. //setPixCol(i, i*colorIndex/255, pixBri); SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i*colorIndex/255, false, PALETTE_SOLID_WRAP, 0), pixBri)); } @@ -4290,7 +4253,7 @@ static const char _data_FX_MODE_FLOW[] PROGMEM = "Flow@!,Zones;;!;;m12=1"; //ver */ uint16_t mode_chunchun(void) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); SEGMENT.fade_out(254); // add a bit of trail unsigned counter = strip.now * (6 + (SEGMENT.speed >> 4)); unsigned numBirds = 2 + (SEGLEN >> 3); // 2 + 1/8 of a segment @@ -4301,25 +4264,13 @@ uint16_t mode_chunchun(void) counter -= span; unsigned megumin = sin16_t(counter) + 0x8000; unsigned bird = uint32_t(megumin * SEGLEN) >> 16; - uint32_t c = SEGMENT.color_from_palette((i * 255)/ numBirds, false, false, 0); // no palette wrapping bird = constrain(bird, 0U, SEGLEN-1U); - SEGMENT.setPixelColor(bird, c); + SEGMENT.setPixelColor(bird, SEGMENT.color_from_palette((i * 255)/ numBirds, false, false, 0)); // no palette wrapping } return FRAMETIME; } static const char _data_FX_MODE_CHUNCHUN[] PROGMEM = "Chunchun@!,Gap size;!,!;!"; - -//13 bytes -typedef struct Spotlight { - float speed; - uint8_t colorIdx; - int16_t position; - unsigned long lastUpdateTime; - uint8_t width; - uint8_t type; -} spotlight; - #define SPOT_TYPE_SOLID 0 #define SPOT_TYPE_GRADIENT 1 #define SPOT_TYPE_2X_GRADIENT 2 @@ -4333,6 +4284,17 @@ typedef struct Spotlight { #define SPOT_MAX_COUNT 49 //Number of simultaneous waves #endif +#ifdef WLED_PS_DONT_REPLACE_FX +//13 bytes +typedef struct Spotlight { + float speed; + uint8_t colorIdx; + int16_t position; + unsigned long lastUpdateTime; + uint8_t width; + uint8_t type; +} spotlight; + /* * Spotlights moving back and forth that cast dancing shadows. * Shine this through tree branches/leaves or other close-up objects that cast @@ -4342,7 +4304,7 @@ typedef struct Spotlight { */ uint16_t mode_dancing_shadows(void) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); unsigned numSpotlights = map(SEGMENT.intensity, 0, 255, 2, SPOT_MAX_COUNT); // 49 on 32 segment ESP32, 17 on 16 segment ESP8266 bool initialize = SEGENV.aux0 != numSpotlights; SEGENV.aux0 = numSpotlights; @@ -4367,21 +4329,21 @@ uint16_t mode_dancing_shadows(void) spotlights[i].lastUpdateTime = time; } - respawn = (spotlights[i].speed > 0.0 && spotlights[i].position > (SEGLEN + 2)) + respawn = (spotlights[i].speed > 0.0 && spotlights[i].position > (int)(SEGLEN + 2)) || (spotlights[i].speed < 0.0 && spotlights[i].position < -(spotlights[i].width + 2)); } if (initialize || respawn) { - spotlights[i].colorIdx = random8(); - spotlights[i].width = random8(1, 10); + spotlights[i].colorIdx = hw_random8(); + spotlights[i].width = hw_random8(1, 10); - spotlights[i].speed = 1.0/random8(4, 50); + spotlights[i].speed = 1.0/hw_random8(4, 50); if (initialize) { - spotlights[i].position = random16(SEGLEN); - spotlights[i].speed *= random8(2) ? 1.0 : -1.0; + spotlights[i].position = hw_random16(SEGLEN); + spotlights[i].speed *= hw_random8(2) ? 1.0 : -1.0; } else { - if (random8(2)) { + if (hw_random8(2)) { spotlights[i].position = SEGLEN + spotlights[i].width; spotlights[i].speed *= -1.0; }else { @@ -4390,14 +4352,14 @@ uint16_t mode_dancing_shadows(void) } spotlights[i].lastUpdateTime = time; - spotlights[i].type = random8(SPOT_TYPES_COUNT); + spotlights[i].type = hw_random8(SPOT_TYPES_COUNT); } uint32_t color = SEGMENT.color_from_palette(spotlights[i].colorIdx, false, false, 255); int start = spotlights[i].position; if (spotlights[i].width <= 1) { - if (start >= 0 && start < SEGLEN) { + if (start >= 0 && start < (int)SEGLEN) { SEGMENT.blendPixelColor(start, color, 128); } } else { @@ -4456,7 +4418,7 @@ uint16_t mode_dancing_shadows(void) return FRAMETIME; } static const char _data_FX_MODE_DANCING_SHADOWS[] PROGMEM = "Dancing Shadows@!,# of shadows;!;!"; - +#endif // WLED_PS_DONT_REPLACE_FX /* Imitates a washing machine, rotating same waves forward, then pause, then backward. @@ -4467,7 +4429,7 @@ uint16_t mode_washing_machine(void) { SEGENV.step += (speed * 2048) / (512 - SEGMENT.speed); - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { uint8_t col = sin8_t(((SEGMENT.intensity / 25 + 1) * 255 * i / SEGLEN) + (SEGENV.step >> 7)); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(col, false, PALETTE_SOLID_WRAP, 3)); } @@ -4477,6 +4439,24 @@ uint16_t mode_washing_machine(void) { static const char _data_FX_MODE_WASHING_MACHINE[] PROGMEM = "Washing Machine@!,!;;!"; +/* + Image effect + Draws a .gif image from filesystem on the matrix/strip +*/ +uint16_t mode_image(void) { + #ifndef WLED_ENABLE_GIF + return mode_static(); + #else + renderImageToSegment(SEGMENT); + return FRAMETIME; + #endif + // if (status != 0 && status != 254 && status != 255) { + // Serial.print("GIF renderer return: "); + // Serial.println(status); + // } +} +static const char _data_FX_MODE_IMAGE[] PROGMEM = "Image@!,;;;12;sx=128"; + /* Blends random colors across palette Modified, originally by Mark Kriegsman https://gist.github.com/kriegsman/1f7ccbbfa492a73c015e @@ -4486,7 +4466,7 @@ uint16_t mode_blends(void) { unsigned dataSize = sizeof(uint32_t) * (pixelLen + 1); // max segment length of 56 pixels on 16 segment ESP8266 if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed uint32_t* pixels = reinterpret_cast(SEGENV.data); - unsigned blendSpeed = map(SEGMENT.intensity, 0, UINT8_MAX, 10, 128); + uint8_t blendSpeed = map(SEGMENT.intensity, 0, UINT8_MAX, 10, 128); unsigned shift = (strip.now * ((SEGMENT.speed >> 3) +1)) >> 8; for (unsigned i = 0; i < pixelLen; i++) { @@ -4549,10 +4529,10 @@ uint16_t mode_tv_simulator(void) { // create a new sceene if (((strip.now - tvSimulator->sceeneStart) >= tvSimulator->sceeneDuration) || SEGENV.aux1 == 0) { tvSimulator->sceeneStart = strip.now; // remember the start of the new sceene - tvSimulator->sceeneDuration = random16(60* 250* colorSpeed, 60* 750 * colorSpeed); // duration of a "movie sceene" which has similar colors (5 to 15 minutes with max speed slider) - tvSimulator->sceeneColorHue = random16( 0, 768); // random start color-tone for the sceene - tvSimulator->sceeneColorSat = random8 ( 100, 130 + colorIntensity); // random start color-saturation for the sceene - tvSimulator->sceeneColorBri = random8 ( 200, 240); // random start color-brightness for the sceene + tvSimulator->sceeneDuration = hw_random16(60* 250* colorSpeed, 60* 750 * colorSpeed); // duration of a "movie sceene" which has similar colors (5 to 15 minutes with max speed slider) + tvSimulator->sceeneColorHue = hw_random16( 0, 768); // random start color-tone for the sceene + tvSimulator->sceeneColorSat = hw_random8 ( 100, 130 + colorIntensity); // random start color-saturation for the sceene + tvSimulator->sceeneColorBri = hw_random8 ( 200, 240); // random start color-brightness for the sceene SEGENV.aux1 = 1; SEGENV.aux0 = 0; } @@ -4560,16 +4540,16 @@ uint16_t mode_tv_simulator(void) { // slightly change the color-tone in this sceene if (SEGENV.aux0 == 0) { // hue change in both directions - j = random8(4 * colorIntensity); - hue = (random8() < 128) ? ((j < tvSimulator->sceeneColorHue) ? tvSimulator->sceeneColorHue - j : 767 - tvSimulator->sceeneColorHue - j) : // negative + j = hw_random8(4 * colorIntensity); + hue = (hw_random8() < 128) ? ((j < tvSimulator->sceeneColorHue) ? tvSimulator->sceeneColorHue - j : 767 - tvSimulator->sceeneColorHue - j) : // negative ((j + tvSimulator->sceeneColorHue) < 767 ? tvSimulator->sceeneColorHue + j : tvSimulator->sceeneColorHue + j - 767) ; // positive // saturation - j = random8(2 * colorIntensity); + j = hw_random8(2 * colorIntensity); sat = (tvSimulator->sceeneColorSat - j) < 0 ? 0 : tvSimulator->sceeneColorSat - j; // brightness - j = random8(100); + j = hw_random8(100); bri = (tvSimulator->sceeneColorBri - j) < 0 ? 0 : tvSimulator->sceeneColorBri - j; // calculate R,G,B from HSV @@ -4595,9 +4575,9 @@ uint16_t mode_tv_simulator(void) { SEGENV.aux0 = 1; // randomize total duration and fade duration for the actual color - tvSimulator->totalTime = random16(250, 2500); // Semi-random pixel-to-pixel time - tvSimulator->fadeTime = random16(0, tvSimulator->totalTime); // Pixel-to-pixel transition time - if (random8(10) < 3) tvSimulator->fadeTime = 0; // Force scene cut 30% of time + tvSimulator->totalTime = hw_random16(250, 2500); // Semi-random pixel-to-pixel time + tvSimulator->fadeTime = hw_random16(0, tvSimulator->totalTime); // Pixel-to-pixel transition time + if (hw_random8(10) < 3) tvSimulator->fadeTime = 0; // Force scene cut 30% of time tvSimulator->startTime = strip.now; } // end of initialization @@ -4617,7 +4597,7 @@ uint16_t mode_tv_simulator(void) { } // set strip color - for (i = 0; i < SEGLEN; i++) { + for (i = 0; i < (int)SEGLEN; i++) { SEGMENT.setPixelColor(i, r >> 8, g >> 8, b >> 8); // Quantize to 8-bit } @@ -4662,15 +4642,15 @@ class AuroraWave { public: void init(uint32_t segment_length, CRGB color) { - ttl = random16(500, 1501); + ttl = hw_random16(500, 1501); basecolor = color; - basealpha = random8(60, 101) / (float)100; + basealpha = hw_random8(60, 101) / (float)100; age = 0; - width = random16(segment_length / 20, segment_length / W_WIDTH_FACTOR); //half of width to make math easier + width = hw_random16(segment_length / 20, segment_length / W_WIDTH_FACTOR); //half of width to make math easier if (!width) width = 1; - center = random8(101) / (float)100 * segment_length; - goingleft = random8(0, 2) == 0; - speed_factor = (random8(10, 31) / (float)100 * W_MAX_SPEED / 255); + center = hw_random8(101) / (float)100 * segment_length; + goingleft = hw_random8(0, 2) == 0; + speed_factor = (hw_random8(10, 31) / (float)100 * W_MAX_SPEED / 255); alive = true; } @@ -4755,7 +4735,7 @@ uint16_t mode_aurora(void) { waves = reinterpret_cast(SEGENV.data); for (int i = 0; i < SEGENV.aux1; i++) { - waves[i].init(SEGLEN, CRGB(SEGMENT.color_from_palette(random8(), false, false, random8(0, 3)))); + waves[i].init(SEGLEN, CRGB(SEGMENT.color_from_palette(hw_random8(), false, false, hw_random8(0, 3)))); } } else { waves = reinterpret_cast(SEGENV.data); @@ -4767,7 +4747,7 @@ uint16_t mode_aurora(void) { if(!(waves[i].stillAlive())) { //If a wave dies, reinitialize it starts over. - waves[i].init(SEGLEN, CRGB(SEGMENT.color_from_palette(random8(), false, false, random8(0, 3)))); + waves[i].init(SEGLEN, CRGB(SEGMENT.color_from_palette(hw_random8(), false, false, hw_random8(0, 3)))); } } @@ -4776,7 +4756,7 @@ uint16_t mode_aurora(void) { if (SEGCOLOR(1)) backlight++; if (SEGCOLOR(2)) backlight++; //Loop through LEDs to determine color - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { CRGB mixedRgb = CRGB(backlight, backlight, backlight); //For each LED we must check each wave if it is "active" at this position. @@ -4804,10 +4784,10 @@ static const char _data_FX_MODE_AURORA[] PROGMEM = "Aurora@!,!;1,2,3;!;;sx=24,pa // 16 bit perlinmove. Use Perlin Noise instead of sinewaves for movement. By Andrew Tuline. // Controls are speed, # of pixels, faderate. uint16_t mode_perlinmove(void) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); SEGMENT.fade_out(255-SEGMENT.custom1); for (int i = 0; i < SEGMENT.intensity/16 + 1; i++) { - unsigned locn = inoise16(strip.now*128/(260-SEGMENT.speed)+i*15000, strip.now*128/(260-SEGMENT.speed)); // Get a new pixel location from moving noise. + unsigned locn = perlin16(strip.now*128/(260-SEGMENT.speed)+i*15000, strip.now*128/(260-SEGMENT.speed)); // Get a new pixel location from moving noise. unsigned pixloc = map(locn, 50*256, 192*256, 0, SEGLEN-1); // Map that to the length of the strand, and ensure we don't go over. SEGMENT.setPixelColor(pixloc, SEGMENT.color_from_palette(pixloc%255, false, PALETTE_SOLID_WRAP, 0)); } @@ -4823,7 +4803,7 @@ static const char _data_FX_MODE_PERLINMOVE[] PROGMEM = "Perlin Move@!,# of pixel // Uses beatsin8() + phase shifting. By: Andrew Tuline uint16_t mode_wavesins(void) { - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { uint8_t bri = sin8_t(strip.now/4 + i * SEGMENT.intensity); uint8_t index = beatsin8_t(SEGMENT.speed, SEGMENT.custom1, SEGMENT.custom1+SEGMENT.custom2, 0, i * (SEGMENT.custom3<<3)); // custom3 is reduced resolution slider //SEGMENT.setPixelColor(i, ColorFromPalette(SEGPALETTE, index, bri, LINEARBLEND)); @@ -4838,38 +4818,37 @@ static const char _data_FX_MODE_WAVESINS[] PROGMEM = "Wavesins@!,Brightness vari ////////////////////////////// // Flow Stripe // ////////////////////////////// -// By: ldirko https://editor.soulmatelights.com/gallery/392-flow-led-stripe , modifed by: Andrew Tuline +// By: ldirko https://editor.soulmatelights.com/gallery/392-flow-led-stripe , modifed by: Andrew Tuline, fixed by @DedeHai uint16_t mode_FlowStripe(void) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); const int hl = SEGLEN * 10 / 13; uint8_t hue = strip.now / (SEGMENT.speed+1); uint32_t t = strip.now / (SEGMENT.intensity/8+1); - for (int i = 0; i < SEGLEN; i++) { - int c = (abs(i - hl) / hl) * 127; + for (unsigned i = 0; i < SEGLEN; i++) { + int c = ((abs((int)i - hl) * 127) / hl); c = sin8_t(c); c = sin8_t(c / 2 + t); byte b = sin8_t(c + t/8); - SEGMENT.setPixelColor(i, CHSV(b + hue, 255, 255)); + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(b + hue, false, true, 3)); } return FRAMETIME; } // mode_FlowStripe() -static const char _data_FX_MODE_FLOWSTRIPE[] PROGMEM = "Flow Stripe@Hue speed,Effect speed;;"; +static const char _data_FX_MODE_FLOWSTRIPE[] PROGMEM = "Flow Stripe@Hue speed,Effect speed;;!;pal=11"; #ifndef WLED_DISABLE_2D /////////////////////////////////////////////////////////////////////////////// //*************************** 2D routines *********************************** -#define XY(x,y) SEGMENT.XY(x,y) // Black hole uint16_t mode_2DBlackHole(void) { // By: Stepko https://editor.soulmatelights.com/gallery/1012 , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; int x, y; SEGMENT.fadeToBlackBy(16 + (SEGMENT.speed>>3)); // create fading trails @@ -4902,8 +4881,8 @@ static const char _data_FX_MODE_2DBLACKHOLE[] PROGMEM = "Black Hole@Fade rate,Ou uint16_t mode_2DColoredBursts() { // By: ldirko https://editor.soulmatelights.com/gallery/819-colored-bursts , modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; if (SEGENV.call == 0) { SEGENV.aux0 = 0; // start with red hue @@ -4915,13 +4894,13 @@ uint16_t mode_2DColoredBursts() { // By: ldirko https://editor.so byte numLines = SEGMENT.intensity/16 + 1; SEGENV.aux0++; // hue - SEGMENT.fadeToBlackBy(40); + SEGMENT.fadeToBlackBy(40 - SEGMENT.check2 * 8); for (size_t i = 0; i < numLines; i++) { byte x1 = beatsin8_t(2 + SEGMENT.speed/16, 0, (cols - 1)); byte x2 = beatsin8_t(1 + SEGMENT.speed/16, 0, (rows - 1)); byte y1 = beatsin8_t(5 + SEGMENT.speed/16, 0, (cols - 1), 0, i * 24); byte y2 = beatsin8_t(3 + SEGMENT.speed/16, 0, (rows - 1), 0, i * 48 + 64); - CRGB color = ColorFromPalette(SEGPALETTE, i * 255 / numLines + (SEGENV.aux0&0xFF), 255, LINEARBLEND); + uint32_t color = ColorFromPalette(SEGPALETTE, i * 255 / numLines + (SEGENV.aux0&0xFF), 255, LINEARBLEND); byte xsteps = abs8(x1 - y1) + 1; byte ysteps = abs8(x2 - y2) + 1; @@ -4941,11 +4920,11 @@ uint16_t mode_2DColoredBursts() { // By: ldirko https://editor.so SEGMENT.setPixelColorXY(y1, y2, DARKSLATEGRAY); } } - if (SEGMENT.custom3) SEGMENT.blur(SEGMENT.custom3/2); + SEGMENT.blur(SEGMENT.custom3>>1, SEGMENT.check2); return FRAMETIME; } // mode_2DColoredBursts() -static const char _data_FX_MODE_2DCOLOREDBURSTS[] PROGMEM = "Colored Bursts@Speed,# of lines,,,Blur,Gradient,,Dots;;!;2;c3=16"; +static const char _data_FX_MODE_2DCOLOREDBURSTS[] PROGMEM = "Colored Bursts@Speed,# of lines,,,Blur,Gradient,Smear,Dots;;!;2;c3=16"; ///////////////////// @@ -4954,20 +4933,19 @@ static const char _data_FX_MODE_2DCOLOREDBURSTS[] PROGMEM = "Colored Bursts@Spee uint16_t mode_2Ddna(void) { // dna originally by by ldirko at https://pastebin.com/pCkkkzcs. Updated by Preyy. WLED conversion by Andrew Tuline. if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; SEGMENT.fadeToBlackBy(64); for (int i = 0; i < cols; i++) { SEGMENT.setPixelColorXY(i, beatsin8_t(SEGMENT.speed/8, 0, rows-1, 0, i*4 ), ColorFromPalette(SEGPALETTE, i*5+strip.now/17, beatsin8_t(5, 55, 255, 0, i*10), LINEARBLEND)); SEGMENT.setPixelColorXY(i, beatsin8_t(SEGMENT.speed/8, 0, rows-1, 0, i*4+128), ColorFromPalette(SEGPALETTE, i*5+128+strip.now/17, beatsin8_t(5, 55, 255, 0, i*10+128), LINEARBLEND)); } - SEGMENT.blur(SEGMENT.intensity>>3); + SEGMENT.blur(SEGMENT.intensity / (8 - (SEGMENT.check1 * 2)), SEGMENT.check1); return FRAMETIME; } // mode_2Ddna() -static const char _data_FX_MODE_2DDNA[] PROGMEM = "DNA@Scroll speed,Blur;;!;2"; - +static const char _data_FX_MODE_2DDNA[] PROGMEM = "DNA@Scroll speed,Blur,,,,Smear;;!;2;ix=0"; ///////////////////////// // 2D DNA Spiral // @@ -4975,8 +4953,8 @@ static const char _data_FX_MODE_2DDNA[] PROGMEM = "DNA@Scroll speed,Blur;;!;2"; uint16_t mode_2DDNASpiral() { // By: ldirko https://editor.soulmatelights.com/gallery/512-dna-spiral-variation , modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); @@ -5010,10 +4988,11 @@ uint16_t mode_2DDNASpiral() { // By: ldirko https://editor.soulma SEGMENT.setPixelColorXY(x1, i, WHITE); } } + SEGMENT.blur(((uint16_t)SEGMENT.custom1 * 3) / (6 + SEGMENT.check1), SEGMENT.check1); return FRAMETIME; } // mode_2DDNASpiral() -static const char _data_FX_MODE_2DDNASPIRAL[] PROGMEM = "DNA Spiral@Scroll speed,Y frequency;;!;2"; +static const char _data_FX_MODE_2DDNASPIRAL[] PROGMEM = "DNA Spiral@Scroll speed,Y frequency,Blur,,,Smear;;!;2;c1=0"; ///////////////////////// @@ -5022,8 +5001,8 @@ static const char _data_FX_MODE_2DDNASPIRAL[] PROGMEM = "DNA Spiral@Scroll speed uint16_t mode_2DDrift() { // By: Stepko https://editor.soulmatelights.com/gallery/884-drift , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; const int colsCenter = (cols>>1) + (cols%2); const int rowsCenter = (rows>>1) + (rows%2); @@ -5039,11 +5018,11 @@ uint16_t mode_2DDrift() { // By: Stepko https://editor.soulmateli SEGMENT.setPixelColorXY(colsCenter + mySin, rowsCenter + myCos, ColorFromPalette(SEGPALETTE, (i * 20) + t_20, 255, LINEARBLEND)); if (SEGMENT.check1) SEGMENT.setPixelColorXY(colsCenter + myCos, rowsCenter + mySin, ColorFromPalette(SEGPALETTE, (i * 20) + t_20, 255, LINEARBLEND)); } - SEGMENT.blur(SEGMENT.intensity>>3); + SEGMENT.blur(SEGMENT.intensity>>(3 - SEGMENT.check2), SEGMENT.check2); return FRAMETIME; } // mode_2DDrift() -static const char _data_FX_MODE_2DDRIFT[] PROGMEM = "Drift@Rotation speed,Blur amount,,,,Twin;;!;2"; +static const char _data_FX_MODE_2DDRIFT[] PROGMEM = "Drift@Rotation speed,Blur,,,,Twin,Smear;;!;2;ix=0"; ////////////////////////// @@ -5052,8 +5031,8 @@ static const char _data_FX_MODE_2DDRIFT[] PROGMEM = "Drift@Rotation speed,Blur a uint16_t mode_2Dfirenoise(void) { // firenoise2d. By Andrew Tuline. Yet another short routine. if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); @@ -5063,15 +5042,15 @@ uint16_t mode_2Dfirenoise(void) { // firenoise2d. By Andrew Tuline unsigned yscale = SEGMENT.speed*8; unsigned indexx = 0; + //CRGBPalette16 pal = SEGMENT.check1 ? SEGPALETTE : SEGMENT.loadPalette(pal, 35); CRGBPalette16 pal = SEGMENT.check1 ? SEGPALETTE : CRGBPalette16(CRGB::Black, CRGB::Black, CRGB::Black, CRGB::Black, CRGB::Red, CRGB::Red, CRGB::Red, CRGB::DarkOrange, CRGB::DarkOrange,CRGB::DarkOrange, CRGB::Orange, CRGB::Orange, CRGB::Yellow, CRGB::Orange, CRGB::Yellow, CRGB::Yellow); - for (int j=0; j < cols; j++) { for (int i=0; i < rows; i++) { - indexx = inoise8(j*yscale*rows/255, i*xscale+strip.now/4); // We're moving along our Perlin map. - SEGMENT.setPixelColorXY(j, i, ColorFromPalette(pal, min(i*(indexx)>>4, 255U), i*255/cols, LINEARBLEND)); // With that value, look up the 8 bit colour palette value and assign it to the current LED. + indexx = perlin8(j*yscale*rows/255, i*xscale+strip.now/4); // We're moving along our Perlin map. + SEGMENT.setPixelColorXY(j, i, ColorFromPalette(pal, min(i*indexx/11, 225U), i*255/rows, LINEARBLEND)); // With that value, look up the 8 bit colour palette value and assign it to the current LED. } // for i } // for j @@ -5086,20 +5065,19 @@ static const char _data_FX_MODE_2DFIRENOISE[] PROGMEM = "Firenoise@X scale,Y sca uint16_t mode_2DFrizzles(void) { // By: Stepko https://editor.soulmatelights.com/gallery/640-color-frizzles , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; - SEGMENT.fadeToBlackBy(16); + SEGMENT.fadeToBlackBy(16 + SEGMENT.check1 * 10); for (size_t i = 8; i > 0; i--) { SEGMENT.addPixelColorXY(beatsin8_t(SEGMENT.speed/8 + i, 0, cols - 1), beatsin8_t(SEGMENT.intensity/8 - i, 0, rows - 1), ColorFromPalette(SEGPALETTE, beatsin8_t(12, 0, 255), 255, LINEARBLEND)); } - SEGMENT.blur(SEGMENT.custom1>>3); - + SEGMENT.blur(SEGMENT.custom1 >> (3 + SEGMENT.check1), SEGMENT.check1); return FRAMETIME; } // mode_2DFrizzles() -static const char _data_FX_MODE_2DFRIZZLES[] PROGMEM = "Frizzles@X frequency,Y frequency,Blur;;!;2"; +static const char _data_FX_MODE_2DFRIZZLES[] PROGMEM = "Frizzles@X frequency,Y frequency,Blur,,,Smear;;!;2"; /////////////////////////////////////////// @@ -5113,8 +5091,9 @@ typedef struct ColorCount { uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://natureofcode.com/book/chapter-7-cellular-automata/ and https://github.com/DougHaber/nlife-color if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; + const auto XY = [&](int x, int y) { return (x%cols) + (y%rows) * cols; }; const unsigned dataSize = sizeof(CRGB) * SEGMENT.length(); // using width*height prevents reallocation if mirroring is enabled const int crcBufferLen = 2; //(SEGMENT.width() + SEGMENT.height())*71/100; // roughly sqrt(2)/2 for better repetition detection (Ewowi) @@ -5127,15 +5106,14 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: if (SEGENV.call == 0 || strip.now - SEGMENT.step > 3000) { SEGENV.step = strip.now; SEGENV.aux0 = 0; - //random16_set_seed(millis()>>2); //seed the random generator //give the leds random state and colors (based on intensity, colors from palette or all posible colors are chosen) for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) { - unsigned state = random8()%2; + unsigned state = hw_random8()%2; if (state == 0) SEGMENT.setPixelColorXY(x,y, backgroundColor); else - SEGMENT.setPixelColorXY(x,y, SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 255)); + SEGMENT.setPixelColorXY(x,y, SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 255)); } for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) prevLeds[XY(x,y)] = CRGB::Black; @@ -5170,7 +5148,7 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: neighbors++; bool colorFound = false; int k; - for (k=0; k<9 && colorsCount[i].count != 0; k++) + for (k=0; k<9 && colorsCount[k].count != 0; k++) if (colorsCount[k].color == prevLeds[xy]) { colorsCount[k].count++; colorFound = true; @@ -5190,9 +5168,9 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: for (int i=0; i<9 && colorsCount[i].count != 0; i++) if (colorsCount[i].count > dominantColorCount.count) dominantColorCount = colorsCount[i]; // assign the dominant color w/ a bit of randomness to avoid "gliders" - if (dominantColorCount.count > 0 && random8(128)) SEGMENT.setPixelColorXY(x,y, dominantColorCount.color); - } else if ((col == bgc) && (neighbors == 2) && !random8(128)) { // Mutation - SEGMENT.setPixelColorXY(x,y, SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 255)); + if (dominantColorCount.count > 0 && hw_random8(128)) SEGMENT.setPixelColorXY(x,y, dominantColorCount.color); + } else if ((col == bgc) && (neighbors == 2) && !hw_random8(128)) { // Mutation + SEGMENT.setPixelColorXY(x,y, SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 255)); } // else do nothing! } //x,y @@ -5219,8 +5197,8 @@ static const char _data_FX_MODE_2DGAMEOFLIFE[] PROGMEM = "Game Of Life@!;!,!;!;2 uint16_t mode_2DHiphotic() { // By: ldirko https://editor.soulmatelights.com/gallery/810 , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; const uint32_t a = strip.now / ((SEGMENT.custom3>>1)+1); for (int x = 0; x < cols; x++) { @@ -5251,8 +5229,8 @@ typedef struct Julia { uint16_t mode_2DJulia(void) { // An animated Julia set by Andrew Tuline. if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; if (!SEGENV.allocateData(sizeof(julia))) return mode_static(); Julia* julias = reinterpret_cast(SEGENV.data); @@ -5344,11 +5322,12 @@ uint16_t mode_2DJulia(void) { // An animated Julia set } y += dy; } -// SEGMENT.blur(64); + if(SEGMENT.check1) + SEGMENT.blur(100, true); return FRAMETIME; } // mode_2DJulia() -static const char _data_FX_MODE_2DJULIA[] PROGMEM = "Julia@,Max iterations per pixel,X center,Y center,Area size;!;!;2;ix=24,c1=128,c2=128,c3=16"; +static const char _data_FX_MODE_2DJULIA[] PROGMEM = "Julia@,Max iterations per pixel,X center,Y center,Area size, Blur;!;!;2;ix=24,c1=128,c2=128,c3=16"; ////////////////////////////// @@ -5357,8 +5336,8 @@ static const char _data_FX_MODE_2DJULIA[] PROGMEM = "Julia@,Max iterations per p uint16_t mode_2DLissajous(void) { // By: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; SEGMENT.fadeToBlackBy(SEGMENT.intensity); uint_fast16_t phase = (strip.now * (1 + SEGENV.custom3)) /32; // allow user to control rotation speed @@ -5373,10 +5352,11 @@ uint16_t mode_2DLissajous(void) { // By: Andrew Tuline ylocn = (rows < 2) ? 1 : (map(2*ylocn, 0,511, 0,2*(rows-1)) +1) /2; // "rows > 1" is needed to avoid div/0 in map() SEGMENT.setPixelColorXY((uint8_t)xlocn, (uint8_t)ylocn, SEGMENT.color_from_palette(strip.now/100+i, false, PALETTE_SOLID_WRAP, 0)); } + SEGMENT.blur(SEGMENT.custom1 >> (1 + SEGMENT.check1 * 3), SEGMENT.check1); return FRAMETIME; } // mode_2DLissajous() -static const char _data_FX_MODE_2DLISSAJOUS[] PROGMEM = "Lissajous@X frequency,Fade rate,,,Speed;!;!;2;c3=15"; +static const char _data_FX_MODE_2DLISSAJOUS[] PROGMEM = "Lissajous@X frequency,Fade rate,Blur,,Speed,Smear;!;!;2;c1=0"; /////////////////////// @@ -5385,8 +5365,9 @@ static const char _data_FX_MODE_2DLISSAJOUS[] PROGMEM = "Lissajous@X frequency,F uint16_t mode_2Dmatrix(void) { // Matrix2D. By Jeremy Williams. Adapted by Andrew Tuline & improved by merkisoft and ewowi, and softhack007. if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; + const auto XY = [&](int x, int y) { return (x%cols) + (y%rows) * cols; }; unsigned dataSize = (SEGMENT.length()+7) >> 3; //1 bit per LED for trails if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed @@ -5434,8 +5415,8 @@ uint16_t mode_2Dmatrix(void) { // Matrix2D. By Jeremy Williams. } // spawn new falling code - if (random8() <= SEGMENT.intensity || emptyScreen) { - uint8_t spawnX = random8(cols); + if (hw_random8() <= SEGMENT.intensity || emptyScreen) { + uint8_t spawnX = hw_random8(cols); SEGMENT.setPixelColorXY(spawnX, 0, spawnColor); // update hint for next run unsigned index = XY(spawnX, 0) >> 3; @@ -5455,17 +5436,17 @@ static const char _data_FX_MODE_2DMATRIX[] PROGMEM = "Matrix@!,Spawning rate,Tra uint16_t mode_2Dmetaballs(void) { // Metaballs by Stefan Petrick. Cannot have one of the dimensions be 2 or less. Adapted by Andrew Tuline. if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; float speed = 0.25f * (1+(SEGMENT.speed>>6)); // get some 2 random moving points - int x2 = map(inoise8(strip.now * speed, 25355, 685), 0, 255, 0, cols-1); - int y2 = map(inoise8(strip.now * speed, 355, 11685), 0, 255, 0, rows-1); + int x2 = map(perlin8(strip.now * speed, 25355, 685), 0, 255, 0, cols-1); + int y2 = map(perlin8(strip.now * speed, 355, 11685), 0, 255, 0, rows-1); - int x3 = map(inoise8(strip.now * speed, 55355, 6685), 0, 255, 0, cols-1); - int y3 = map(inoise8(strip.now * speed, 25355, 22685), 0, 255, 0, rows-1); + int x3 = map(perlin8(strip.now * speed, 55355, 6685), 0, 255, 0, cols-1); + int y3 = map(perlin8(strip.now * speed, 25355, 22685), 0, 255, 0, rows-1); // and one Lissajou function int x1 = beatsin8_t(23 * speed, 0, cols-1); @@ -5477,15 +5458,15 @@ uint16_t mode_2Dmetaballs(void) { // Metaballs by Stefan Petrick. Cannot have // and add them together with weightening unsigned dx = abs(x - x1); unsigned dy = abs(y - y1); - unsigned dist = 2 * sqrt16((dx * dx) + (dy * dy)); + unsigned dist = 2 * sqrt32_bw((dx * dx) + (dy * dy)); dx = abs(x - x2); dy = abs(y - y2); - dist += sqrt16((dx * dx) + (dy * dy)); + dist += sqrt32_bw((dx * dx) + (dy * dy)); dx = abs(x - x3); dy = abs(y - y3); - dist += sqrt16((dx * dx) + (dy * dy)); + dist += sqrt32_bw((dx * dx) + (dy * dy)); // inverse result int color = dist ? 1000 / dist : 255; @@ -5514,14 +5495,14 @@ static const char _data_FX_MODE_2DMETABALLS[] PROGMEM = "Metaballs@!;;!;2"; uint16_t mode_2Dnoise(void) { // By Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; const unsigned scale = SEGMENT.intensity+2; for (int y = 0; y < rows; y++) { for (int x = 0; x < cols; x++) { - uint8_t pixelHue8 = inoise8(x * scale, y * scale, strip.now / (16 - SEGMENT.speed/16)); + uint8_t pixelHue8 = perlin8(x * scale, y * scale, strip.now / (16 - SEGMENT.speed/16)); SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, pixelHue8)); } } @@ -5537,16 +5518,16 @@ static const char _data_FX_MODE_2DNOISE[] PROGMEM = "Noise2D@!,Scale;;!;2"; uint16_t mode_2DPlasmaball(void) { // By: Stepko https://editor.soulmatelights.com/gallery/659-plasm-ball , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; SEGMENT.fadeToBlackBy(SEGMENT.custom1>>2); uint_fast32_t t = (strip.now * 8) / (256 - SEGMENT.speed); // optimized to avoid float for (int i = 0; i < cols; i++) { - unsigned thisVal = inoise8(i * 30, t, t); + unsigned thisVal = perlin8(i * 30, t, t); unsigned thisMax = map(thisVal, 0, 255, 0, cols-1); for (int j = 0; j < rows; j++) { - unsigned thisVal_ = inoise8(t, j * 30, t); + unsigned thisVal_ = perlin8(t, j * 30, t); unsigned thisMax_ = map(thisVal_, 0, 255, 0, rows-1); int x = (i + thisMax_ - cols / 2); int y = (j + thisMax - cols / 2); @@ -5571,16 +5552,12 @@ static const char _data_FX_MODE_2DPLASMABALL[] PROGMEM = "Plasma Ball@Speed,,Fad //////////////////////////////// // 2D Polar Lights // //////////////////////////////// -//static float fmap(const float x, const float in_min, const float in_max, const float out_min, const float out_max) { -// return (out_max - out_min) * (x - in_min) / (in_max - in_min) + out_min; -//} -uint16_t mode_2DPolarLights(void) { // By: Kostyantyn Matviyevskyy https://editor.soulmatelights.com/gallery/762-polar-lights , Modified by: Andrew Tuline + +uint16_t mode_2DPolarLights(void) { // By: Kostyantyn Matviyevskyy https://editor.soulmatelights.com/gallery/762-polar-lights , Modified by: Andrew Tuline & @dedehai (palette support) if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); - - CRGBPalette16 auroraPalette = {0x000000, 0x003300, 0x006600, 0x009900, 0x00cc00, 0x00ff00, 0x33ff00, 0x66ff00, 0x99ff00, 0xccff00, 0xffff00, 0xffcc00, 0xff9900, 0xff6600, 0xff3300, 0xff0000}; + const int cols = SEG_W; + const int rows = SEG_H; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); @@ -5589,37 +5566,22 @@ uint16_t mode_2DPolarLights(void) { // By: Kostyantyn Matviyevskyy https float adjustHeight = (float)map(rows, 8, 32, 28, 12); // maybe use mapf() ??? unsigned adjScale = map(cols, 8, 64, 310, 63); -/* - if (SEGENV.aux1 != SEGMENT.custom1/12) { // Hacky palette rotation. We need that black. - SEGENV.aux1 = SEGMENT.custom1/12; - for (int i = 0; i < 16; i++) { - long ilk; - ilk = (long)currentPalette[i].r << 16; - ilk += (long)currentPalette[i].g << 8; - ilk += (long)currentPalette[i].b; - ilk = (ilk << SEGENV.aux1) | (ilk >> (24 - SEGENV.aux1)); - currentPalette[i].r = ilk >> 16; - currentPalette[i].g = ilk >> 8; - currentPalette[i].b = ilk; - } - } -*/ unsigned _scale = map(SEGMENT.intensity, 0, 255, 30, adjScale); int _speed = map(SEGMENT.speed, 0, 255, 128, 16); for (int x = 0; x < cols; x++) { for (int y = 0; y < rows; y++) { SEGENV.step++; - SEGMENT.setPixelColorXY(x, y, ColorFromPalette(auroraPalette, - qsub8( - inoise8((SEGENV.step%2) + x * _scale, y * 16 + SEGENV.step % 16, SEGENV.step / _speed), - fabsf((float)rows / 2.0f - (float)y) * adjustHeight))); + uint8_t palindex = qsub8(perlin8((SEGENV.step%2) + x * _scale, y * 16 + SEGENV.step % 16, SEGENV.step / _speed), fabsf((float)rows / 2.0f - (float)y) * adjustHeight); + uint8_t palbrightness = palindex; + if(SEGMENT.check1) palindex = 255 - palindex; //flip palette + SEGMENT.setPixelColorXY(x, y, SEGMENT.color_from_palette(palindex, false, false, 255, palbrightness)); } } return FRAMETIME; } // mode_2DPolarLights() -static const char _data_FX_MODE_2DPOLARLIGHTS[] PROGMEM = "Polar Lights@!,Scale;;;2"; +static const char _data_FX_MODE_2DPOLARLIGHTS[] PROGMEM = "Polar Lights@!,Scale,,,,Flip Palette;;!;2;pal=71"; ///////////////////////// @@ -5628,8 +5590,8 @@ static const char _data_FX_MODE_2DPOLARLIGHTS[] PROGMEM = "Polar Lights@!,Scale; uint16_t mode_2DPulser(void) { // By: ldirko https://editor.soulmatelights.com/gallery/878-pulse-test , modifed by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; SEGMENT.fadeToBlackBy(8 - (SEGMENT.intensity>>5)); uint32_t a = strip.now / (18 - SEGMENT.speed / 16); @@ -5650,14 +5612,14 @@ static const char _data_FX_MODE_2DPULSER[] PROGMEM = "Pulser@!,Blur;;!;2"; uint16_t mode_2DSindots(void) { // By: ldirko https://editor.soulmatelights.com/gallery/597-sin-dots , modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); } - SEGMENT.fadeToBlackBy(SEGMENT.custom1>>3); + SEGMENT.fadeToBlackBy((SEGMENT.custom1>>3) + (SEGMENT.check1 * 24)); byte t1 = strip.now / (257 - SEGMENT.speed); // 20; byte t2 = sin8_t(t1) / 4 * 2; @@ -5666,11 +5628,11 @@ uint16_t mode_2DSindots(void) { // By: ldirko http int y = sin8_t(t2 + i * SEGMENT.intensity/8)*(rows-1)/255; // max index now 255x15/255=15! SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, i * 255 / 13, 255, LINEARBLEND)); } - SEGMENT.blur(SEGMENT.custom2>>3); + SEGMENT.blur(SEGMENT.custom2 >> (3 + SEGMENT.check1), SEGMENT.check1); return FRAMETIME; } // mode_2DSindots() -static const char _data_FX_MODE_2DSINDOTS[] PROGMEM = "Sindots@!,Dot distance,Fade rate,Blur;;!;2"; +static const char _data_FX_MODE_2DSINDOTS[] PROGMEM = "Sindots@!,Dot distance,Fade rate,Blur,,Smear;;!;2;"; ////////////////////////////// @@ -5681,12 +5643,12 @@ uint16_t mode_2Dsquaredswirl(void) { // By: Mark Kriegsman. https://g // Modifed by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; const uint8_t kBorderWidth = 2; - SEGMENT.fadeToBlackBy(24); + SEGMENT.fadeToBlackBy(1 + SEGMENT.intensity / 5); SEGMENT.blur(SEGMENT.custom3>>1); // Use two out-of-sync sine waves @@ -5703,7 +5665,7 @@ uint16_t mode_2Dsquaredswirl(void) { // By: Mark Kriegsman. https://g return FRAMETIME; } // mode_2Dsquaredswirl() -static const char _data_FX_MODE_2DSQUAREDSWIRL[] PROGMEM = "Squared Swirl@,,,,Blur;;!;2"; +static const char _data_FX_MODE_2DSQUAREDSWIRL[] PROGMEM = "Squared Swirl@,Fade,,,Blur;;!;2"; ////////////////////////////// @@ -5712,8 +5674,8 @@ static const char _data_FX_MODE_2DSQUAREDSWIRL[] PROGMEM = "Squared Swirl@,,,,Bl uint16_t mode_2DSunradiation(void) { // By: ldirko https://editor.soulmatelights.com/gallery/599-sun-radiation , modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; if (!SEGENV.allocateData(sizeof(byte)*(cols+2)*(rows+2))) return mode_static(); //allocation failed byte *bump = reinterpret_cast(SEGENV.data); @@ -5727,7 +5689,8 @@ uint16_t mode_2DSunradiation(void) { // By: ldirko https://edi uint8_t someVal = SEGMENT.speed/4; // Was 25. for (int j = 0; j < (rows + 2); j++) { for (int i = 0; i < (cols + 2); i++) { - byte col = (inoise8_raw(i * someVal, j * someVal, t)) / 2; + //byte col = (inoise8_raw(i * someVal, j * someVal, t)) / 2; + byte col = ((int16_t)perlin8(i * someVal, j * someVal, t) - 0x7F) / 3; bump[index++] = col; } } @@ -5762,8 +5725,8 @@ static const char _data_FX_MODE_2DSUNRADIATION[] PROGMEM = "Sun Radiation@Varian uint16_t mode_2Dtartan(void) { // By: Elliott Kember https://editor.soulmatelights.com/gallery/3-tartan , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); @@ -5801,17 +5764,17 @@ static const char _data_FX_MODE_2DTARTAN[] PROGMEM = "Tartan@X scale,Y scale,,,S uint16_t mode_2Dspaceships(void) { //// Space ships by stepko (c)05.02.21 [https://editor.soulmatelights.com/gallery/639-space-ships], adapted by Blaz Kristan (AKA blazoncek) if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; uint32_t tb = strip.now >> 12; // every ~4s if (tb > SEGENV.step) { int dir = ++SEGENV.aux0; - dir += (int)random8(3)-1; + dir += (int)hw_random8(3)-1; if (dir > 7) SEGENV.aux0 = 0; else if (dir < 0) SEGENV.aux0 = 7; else SEGENV.aux0 = dir; - SEGENV.step = tb + random8(4); + SEGENV.step = tb + hw_random8(4); } SEGMENT.fadeToBlackBy(map(SEGMENT.speed, 0, 255, 248, 16)); @@ -5820,7 +5783,7 @@ uint16_t mode_2Dspaceships(void) { //// Space ships by stepko (c)05.02.21 [ht for (size_t i = 0; i < 8; i++) { int x = beatsin8_t(12 + i, 2, cols - 3); int y = beatsin8_t(15 + i, 2, rows - 3); - CRGB color = ColorFromPalette(SEGPALETTE, beatsin8_t(12 + i, 0, 255), 255); + uint32_t color = ColorFromPalette(SEGPALETTE, beatsin8_t(12 + i, 0, 255), 255); SEGMENT.addPixelColorXY(x, y, color); if (cols > 24 || rows > 24) { SEGMENT.addPixelColorXY(x+1, y, color); @@ -5829,23 +5792,23 @@ uint16_t mode_2Dspaceships(void) { //// Space ships by stepko (c)05.02.21 [ht SEGMENT.addPixelColorXY(x, y-1, color); } } - SEGMENT.blur(SEGMENT.intensity>>3); + SEGMENT.blur(SEGMENT.intensity >> 3, SEGMENT.check1); return FRAMETIME; } -static const char _data_FX_MODE_2DSPACESHIPS[] PROGMEM = "Spaceships@!,Blur;;!;2"; +static const char _data_FX_MODE_2DSPACESHIPS[] PROGMEM = "Spaceships@!,Blur,,,,Smear;;!;2"; ///////////////////////// // 2D Crazy Bees // ///////////////////////// -//// Crazy bees by stepko (c)12.02.21 [https://editor.soulmatelights.com/gallery/651-crazy-bees], adapted by Blaz Kristan (AKA blazoncek) +//// Crazy bees by stepko (c)12.02.21 [https://editor.soulmatelights.com/gallery/651-crazy-bees], adapted by Blaz Kristan (AKA blazoncek), improved by @dedehai #define MAX_BEES 5 uint16_t mode_2Dcrazybees(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; byte n = MIN(MAX_BEES, (rows * cols) / 256 + 1); @@ -5879,14 +5842,14 @@ uint16_t mode_2Dcrazybees(void) { if (strip.now > SEGENV.step) { SEGENV.step = strip.now + (FRAMETIME * 16 / ((SEGMENT.speed>>4)+1)); - - SEGMENT.fadeToBlackBy(32); - + SEGMENT.fadeToBlackBy(32 + ((SEGMENT.check1*SEGMENT.intensity) / 25)); + SEGMENT.blur(SEGMENT.intensity / (2 + SEGMENT.check1 * 9), SEGMENT.check1); for (size_t i = 0; i < n; i++) { - SEGMENT.addPixelColorXY(bee[i].aimX + 1, bee[i].aimY, CHSV(bee[i].hue, 255, 255)); - SEGMENT.addPixelColorXY(bee[i].aimX, bee[i].aimY + 1, CHSV(bee[i].hue, 255, 255)); - SEGMENT.addPixelColorXY(bee[i].aimX - 1, bee[i].aimY, CHSV(bee[i].hue, 255, 255)); - SEGMENT.addPixelColorXY(bee[i].aimX, bee[i].aimY - 1, CHSV(bee[i].hue, 255, 255)); + uint32_t flowerCcolor = SEGMENT.color_from_palette(bee[i].hue, false, true, 255); + SEGMENT.addPixelColorXY(bee[i].aimX + 1, bee[i].aimY, flowerCcolor); + SEGMENT.addPixelColorXY(bee[i].aimX, bee[i].aimY + 1, flowerCcolor); + SEGMENT.addPixelColorXY(bee[i].aimX - 1, bee[i].aimY, flowerCcolor); + SEGMENT.addPixelColorXY(bee[i].aimX, bee[i].aimY - 1, flowerCcolor); if (bee[i].posX != bee[i].aimX || bee[i].posY != bee[i].aimY) { SEGMENT.setPixelColorXY(bee[i].posX, bee[i].posY, CRGB(CHSV(bee[i].hue, 60, 255))); int error2 = bee[i].error * 2; @@ -5902,13 +5865,13 @@ uint16_t mode_2Dcrazybees(void) { bee[i].aimed(cols, rows); } } - SEGMENT.blur(SEGMENT.intensity>>4); } return FRAMETIME; } -static const char _data_FX_MODE_2DCRAZYBEES[] PROGMEM = "Crazy Bees@!,Blur;;;2"; +static const char _data_FX_MODE_2DCRAZYBEES[] PROGMEM = "Crazy Bees@!,Blur,,,,Smear;;!;2;pal=11,ix=0"; #undef MAX_BEES +#ifdef WLED_PS_DONT_REPLACE_FX ///////////////////////// // 2D Ghost Rider // ///////////////////////// @@ -5917,8 +5880,8 @@ static const char _data_FX_MODE_2DCRAZYBEES[] PROGMEM = "Crazy Bees@!,Blur;;;2"; uint16_t mode_2Dghostrider(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; typedef struct Lighter { int16_t gPosX; @@ -5941,9 +5904,8 @@ uint16_t mode_2Dghostrider(void) { if (SEGENV.aux0 != cols || SEGENV.aux1 != rows) { SEGENV.aux0 = cols; SEGENV.aux1 = rows; - //random16_set_seed(strip.now); - lighter->angleSpeed = random8(0,20) - 10; - lighter->gAngle = random16(); + lighter->angleSpeed = hw_random8(0,20) - 10; + lighter->gAngle = hw_random16(); lighter->Vspeed = 5; lighter->gPosX = (cols/2) * 10; lighter->gPosY = (rows/2) * 10; @@ -5971,7 +5933,7 @@ uint16_t mode_2Dghostrider(void) { if (lighter->gPosY < 0) lighter->gPosY = (rows - 1) * 10; if (lighter->gPosY > (rows - 1) * 10) lighter->gPosY = 0; for (size_t i = 0; i < maxLighters; i++) { - lighter->time[i] += random8(5, 20); + lighter->time[i] += hw_random8(5, 20); if (lighter->time[i] >= 255 || (lighter->lightersPosX[i] <= 0) || (lighter->lightersPosX[i] >= (cols - 1) * 10) || @@ -5982,7 +5944,7 @@ uint16_t mode_2Dghostrider(void) { if (lighter->reg[i]) { lighter->lightersPosY[i] = lighter->gPosY; lighter->lightersPosX[i] = lighter->gPosX; - lighter->Angle[i] = lighter->gAngle + ((int)random8(20) - 10); + lighter->Angle[i] = lighter->gAngle + ((int)hw_random8(20) - 10); lighter->time[i] = 0; lighter->reg[i] = false; } else { @@ -6007,8 +5969,8 @@ static const char _data_FX_MODE_2DGHOSTRIDER[] PROGMEM = "Ghost Rider@Fade rate, uint16_t mode_2Dfloatingblobs(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; typedef struct Blob { float x[MAX_BLOBS], y[MAX_BLOBS]; @@ -6028,12 +5990,12 @@ uint16_t mode_2Dfloatingblobs(void) { SEGENV.aux1 = rows; //SEGMENT.fill(BLACK); for (size_t i = 0; i < MAX_BLOBS; i++) { - blob->r[i] = random8(1, cols>8 ? (cols/4) : 2); - blob->sX[i] = (float) random8(3, cols) / (float)(256 - SEGMENT.speed); // speed x - blob->sY[i] = (float) random8(3, rows) / (float)(256 - SEGMENT.speed); // speed y - blob->x[i] = random8(0, cols-1); - blob->y[i] = random8(0, rows-1); - blob->color[i] = random8(); + blob->r[i] = hw_random8(1, cols>8 ? (cols/4) : 2); + blob->sX[i] = (float) hw_random8(3, cols) / (float)(256 - SEGMENT.speed); // speed x + blob->sY[i] = (float) hw_random8(3, rows) / (float)(256 - SEGMENT.speed); // speed y + blob->x[i] = hw_random8(0, cols-1); + blob->y[i] = hw_random8(0, rows-1); + blob->color[i] = hw_random8(); blob->grow[i] = (blob->r[i] < 1.f); if (blob->sX[i] == 0) blob->sX[i] = 1; if (blob->sY[i] == 0) blob->sY[i] = 1; @@ -6072,19 +6034,19 @@ uint16_t mode_2Dfloatingblobs(void) { else blob->y[i] += blob->sY[i]; // bounce x if (blob->x[i] < 0.01f) { - blob->sX[i] = (float)random8(3, cols) / (256 - SEGMENT.speed); + blob->sX[i] = (float)hw_random8(3, cols) / (256 - SEGMENT.speed); blob->x[i] = 0.01f; } else if (blob->x[i] > (float)cols - 1.01f) { - blob->sX[i] = (float)random8(3, cols) / (256 - SEGMENT.speed); + blob->sX[i] = (float)hw_random8(3, cols) / (256 - SEGMENT.speed); blob->sX[i] = -blob->sX[i]; blob->x[i] = (float)cols - 1.01f; } // bounce y if (blob->y[i] < 0.01f) { - blob->sY[i] = (float)random8(3, rows) / (256 - SEGMENT.speed); + blob->sY[i] = (float)hw_random8(3, rows) / (256 - SEGMENT.speed); blob->y[i] = 0.01f; } else if (blob->y[i] > (float)rows - 1.01f) { - blob->sY[i] = (float)random8(3, rows) / (256 - SEGMENT.speed); + blob->sY[i] = (float)hw_random8(3, rows) / (256 - SEGMENT.speed); blob->sY[i] = -blob->sY[i]; blob->y[i] = (float)rows - 1.01f; } @@ -6097,7 +6059,7 @@ uint16_t mode_2Dfloatingblobs(void) { } static const char _data_FX_MODE_2DBLOBS[] PROGMEM = "Blobs@!,# blobs,Blur,Trail;!;!;2;c1=8"; #undef MAX_BLOBS - +#endif // WLED_PS_DONT_REPLACE_FX //////////////////////////// // 2D Scrolling text // @@ -6105,8 +6067,8 @@ static const char _data_FX_MODE_2DBLOBS[] PROGMEM = "Blobs@!,# blobs,Blur,Trail; uint16_t mode_2Dscrollingtext(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; unsigned letterWidth, rotLW; unsigned letterHeight, rotLH; @@ -6119,7 +6081,8 @@ uint16_t mode_2Dscrollingtext(void) { case 5: letterWidth = 5; letterHeight = 12; break; } // letters are rotated - if (((SEGMENT.custom3+1)>>3) % 2) { + const int8_t rotate = map(SEGMENT.custom3, 0, 31, -2, 2); + if (rotate == 1 || rotate == -1) { rotLH = letterWidth; rotLW = letterHeight; } else { @@ -6145,13 +6108,24 @@ uint16_t mode_2Dscrollingtext(void) { if (!strlen(text)) { // fallback if empty segment name: display date and time sprintf_P(text, PSTR("%s %d, %d %d:%02d%s"), monthShortStr(month(localTime)), day(localTime), year(localTime), AmPmHour, minute(localTime), sec); } else { + if (text[0] == '#') for (auto &c : text) c = std::toupper(c); if (!strncmp_P(text,PSTR("#DATE"),5)) sprintf_P(text, zero?PSTR("%02d.%02d.%04d"):PSTR("%d.%d.%d"), day(localTime), month(localTime), year(localTime)); else if (!strncmp_P(text,PSTR("#DDMM"),5)) sprintf_P(text, zero?PSTR("%02d.%02d") :PSTR("%d.%d"), day(localTime), month(localTime)); else if (!strncmp_P(text,PSTR("#MMDD"),5)) sprintf_P(text, zero?PSTR("%02d/%02d") :PSTR("%d/%d"), month(localTime), day(localTime)); else if (!strncmp_P(text,PSTR("#TIME"),5)) sprintf_P(text, zero?PSTR("%02d:%02d%s") :PSTR("%2d:%02d%s"), AmPmHour, minute(localTime), sec); else if (!strncmp_P(text,PSTR("#HHMM"),5)) sprintf_P(text, zero?PSTR("%02d:%02d") :PSTR("%d:%02d"), AmPmHour, minute(localTime)); - else if (!strncmp_P(text,PSTR("#HH"),3)) sprintf_P(text, zero?PSTR("%02d") :PSTR("%d"), AmPmHour); - else if (!strncmp_P(text,PSTR("#MM"),3)) sprintf_P(text, zero?PSTR("%02d") :PSTR("%d"), minute(localTime)); + else if (!strncmp_P(text,PSTR("#HH"),3)) sprintf (text, zero? ("%02d") : ("%d"), AmPmHour); + else if (!strncmp_P(text,PSTR("#MM"),3)) sprintf (text, zero? ("%02d") : ("%d"), minute(localTime)); + else if (!strncmp_P(text,PSTR("#SS"),3)) sprintf (text, ("%02d") , second(localTime)); + else if (!strncmp_P(text,PSTR("#DD"),3)) sprintf (text, zero? ("%02d") : ("%d"), day(localTime)); + else if (!strncmp_P(text,PSTR("#DAY"),4)) sprintf (text, ("%s") , dayShortStr(day(localTime))); + else if (!strncmp_P(text,PSTR("#DDDD"),5)) sprintf (text, ("%s") , dayStr(day(localTime))); + else if (!strncmp_P(text,PSTR("#DAYL"),5)) sprintf (text, ("%s") , dayStr(day(localTime))); + else if (!strncmp_P(text,PSTR("#MO"),3)) sprintf (text, zero? ("%02d") : ("%d"), month(localTime)); + else if (!strncmp_P(text,PSTR("#MON"),4)) sprintf (text, ("%s") , monthShortStr(month(localTime))); + else if (!strncmp_P(text,PSTR("#MMMM"),5)) sprintf (text, ("%s") , monthStr(month(localTime))); + else if (!strncmp_P(text,PSTR("#YY"),3)) sprintf (text, ("%02d") , year(localTime)%100); + else if (!strncmp_P(text,PSTR("#YYYY"),5)) sprintf_P(text, zero?PSTR("%04d") : ("%d"), year(localTime)); } const int numberOfLetters = strlen(text); @@ -6180,34 +6154,39 @@ uint16_t mode_2Dscrollingtext(void) { SEGENV.step = strip.now + map(SEGMENT.speed, 0, 255, 250, 50); // shift letters every ~250ms to ~50ms } - if (!SEGMENT.check2) SEGMENT.fade_out(255 - (SEGMENT.custom1>>4)); // trail + SEGMENT.fade_out(255 - (SEGMENT.custom1>>4)); // trail + uint32_t col1 = SEGMENT.color_from_palette(SEGENV.aux1, false, PALETTE_SOLID_WRAP, 0); + uint32_t col2 = BLACK; + // if gradient is selected and palette is default (0) drawCharacter() uses gradient from SEGCOLOR(0) to SEGCOLOR(2) + // otherwise col2 == BLACK means use currently selected palette for gradient + // if gradient is not selected set both colors the same + if (SEGMENT.check1) { // use gradient + if (SEGMENT.palette == 0) { // use colors for gradient + col1 = SEGCOLOR(0); + col2 = SEGCOLOR(2); + } + } else col2 = col1; // force characters to use single color (from palette) for (int i = 0; i < numberOfLetters; i++) { int xoffset = int(cols) - int(SEGENV.aux0) + rotLW*i; if (xoffset + rotLW < 0) continue; // don't draw characters off-screen - uint32_t col1 = SEGMENT.color_from_palette(SEGENV.aux1, false, PALETTE_SOLID_WRAP, 0); - uint32_t col2 = BLACK; - if (SEGMENT.check1 && SEGMENT.palette == 0) { - col1 = SEGCOLOR(0); - col2 = SEGCOLOR(2); - } - SEGMENT.drawCharacter(text[i], xoffset, yoffset, letterWidth, letterHeight, col1, col2, map(SEGMENT.custom3, 0, 31, -2, 2)); + SEGMENT.drawCharacter(text[i], xoffset, yoffset, letterWidth, letterHeight, col1, col2, rotate); } return FRAMETIME; } -static const char _data_FX_MODE_2DSCROLLTEXT[] PROGMEM = "Scrolling Text@!,Y Offset,Trail,Font size,Rotate,Gradient,Overlay,Reverse;!,!,Gradient;!;2;ix=128,c1=0,rev=0,mi=0,rY=0,mY=0"; +static const char _data_FX_MODE_2DSCROLLTEXT[] PROGMEM = "Scrolling Text@!,Y Offset,Trail,Font size,Rotate,Gradient,,Reverse;!,!,Gradient;!;2;ix=128,c1=0,rev=0,mi=0,rY=0,mY=0"; //////////////////////////// // 2D Drift Rose // //////////////////////////// -//// Drift Rose by stepko (c)2021 [https://editor.soulmatelights.com/gallery/1369-drift-rose-pattern], adapted by Blaz Kristan (AKA blazoncek) +//// Drift Rose by stepko (c)2021 [https://editor.soulmatelights.com/gallery/1369-drift-rose-pattern], adapted by Blaz Kristan (AKA blazoncek) improved by @dedehai uint16_t mode_2Ddriftrose(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; const float CX = (cols-cols%2)/2.f - .5f; const float CY = (rows-rows%2)/2.f - .5f; @@ -6218,13 +6197,14 @@ uint16_t mode_2Ddriftrose(void) { float angle = radians(i * 10); uint32_t x = (CX + (sin_t(angle) * (beatsin8_t(i, 0, L*2)-L))) * 255.f; uint32_t y = (CY + (cos_t(angle) * (beatsin8_t(i, 0, L*2)-L))) * 255.f; - SEGMENT.wu_pixel(x, y, CHSV(i * 10, 255, 255)); + if(SEGMENT.palette == 0) SEGMENT.wu_pixel(x, y, CHSV(i * 10, 255, 255)); + else SEGMENT.wu_pixel(x, y, ColorFromPalette(SEGPALETTE, i * 10)); } - SEGMENT.blur(SEGMENT.intensity>>4); + SEGMENT.blur(SEGMENT.intensity >> 4, SEGMENT.check1); return FRAMETIME; } -static const char _data_FX_MODE_2DDRIFTROSE[] PROGMEM = "Drift Rose@Fade,Blur;;;2"; +static const char _data_FX_MODE_2DDRIFTROSE[] PROGMEM = "Drift Rose@Fade,Blur,,,,Smear;;!;2;pal=11"; ///////////////////////////// // 2D PLASMA ROTOZOOMER // @@ -6233,8 +6213,8 @@ static const char _data_FX_MODE_2DDRIFTROSE[] PROGMEM = "Drift Rose@Fade,Blur;;; uint16_t mode_2Dplasmarotozoom() { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; unsigned dataSize = SEGMENT.length() + sizeof(float); if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed @@ -6280,52 +6260,12 @@ static const char _data_FX_MODE_2DPLASMAROTOZOOM[] PROGMEM = "Rotozoomer@!,Scale /////////////////////////////////////////////////////////////////////////////// -/* use the following code to pass AudioReactive usermod variables to effect - - uint8_t *binNum = (uint8_t*)&SEGENV.aux1, *maxVol = (uint8_t*)(&SEGENV.aux1+1); // just in case assignment - bool samplePeak = false; - float FFT_MajorPeak = 1.0; - uint8_t *fftResult = nullptr; - float *fftBin = nullptr; - um_data_t *um_data; - if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - volumeSmth = *(float*) um_data->u_data[0]; - volumeRaw = *(float*) um_data->u_data[1]; - fftResult = (uint8_t*) um_data->u_data[2]; - samplePeak = *(uint8_t*) um_data->u_data[3]; - FFT_MajorPeak = *(float*) um_data->u_data[4]; - my_magnitude = *(float*) um_data->u_data[5]; - maxVol = (uint8_t*) um_data->u_data[6]; // requires UI element (SEGMENT.customX?), changes source element - binNum = (uint8_t*) um_data->u_data[7]; // requires UI element (SEGMENT.customX?), changes source element - fftBin = (float*) um_data->u_data[8]; - } else { - // add support for no audio data - um_data = simulateSound(SEGMENT.soundSim); - } -*/ - - -// a few constants needed for AudioReactive effects - -// for 22Khz sampling -#define MAX_FREQUENCY 11025 // sample frequency / 2 (as per Nyquist criterion) -#define MAX_FREQ_LOG10 4.04238f // log10(MAX_FREQUENCY) - -// for 20Khz sampling -//#define MAX_FREQUENCY 10240 -//#define MAX_FREQ_LOG10 4.0103f - -// for 10Khz sampling -//#define MAX_FREQUENCY 5120 -//#define MAX_FREQ_LOG10 3.71f - - ///////////////////////////////// // * Ripple Peak // ///////////////////////////////// uint16_t mode_ripplepeak(void) { // * Ripple peak. By Andrew Tuline. // This currently has no controls. - #define maxsteps 16 // Case statement wouldn't allow a variable. + #define MAXSTEPS 16 // Case statement wouldn't allow a variable. unsigned maxRipples = 16; unsigned dataSize = sizeof(Ripple) * maxRipples; @@ -6343,7 +6283,6 @@ uint16_t mode_ripplepeak(void) { // * Ripple peak. By Andrew Tuli // printUmData(); if (SEGENV.call == 0) { - SEGENV.aux0 = 255; SEGMENT.custom1 = *binNum; SEGMENT.custom2 = *maxVol * 2; } @@ -6362,29 +6301,29 @@ uint16_t mode_ripplepeak(void) { // * Ripple peak. By Andrew Tuli break; case 255: // Initialize ripple variables. - ripples[i].pos = random16(SEGLEN); + ripples[i].pos = hw_random16(SEGLEN); #ifdef ESP32 if (FFT_MajorPeak > 1) // log10(0) is "forbidden" (throws exception) ripples[i].color = (int)(log10f(FFT_MajorPeak)*128); else ripples[i].color = 0; #else - ripples[i].color = random8(); + ripples[i].color = hw_random8(); #endif ripples[i].state = 0; break; case 0: - SEGMENT.setPixelColor(ripples[i].pos, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(ripples[i].color, false, PALETTE_SOLID_WRAP, 0), SEGENV.aux0)); + SEGMENT.setPixelColor(ripples[i].pos, SEGMENT.color_from_palette(ripples[i].color, false, PALETTE_SOLID_WRAP, 0)); ripples[i].state++; break; - case maxsteps: // At the end of the ripples. 254 is an inactive mode. + case MAXSTEPS: // At the end of the ripples. 254 is an inactive mode. ripples[i].state = 254; break; default: // Middle of the ripples. - SEGMENT.setPixelColor((ripples[i].pos + ripples[i].state + SEGLEN) % SEGLEN, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(ripples[i].color, false, PALETTE_SOLID_WRAP, 0), SEGENV.aux0/ripples[i].state*2)); - SEGMENT.setPixelColor((ripples[i].pos - ripples[i].state + SEGLEN) % SEGLEN, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(ripples[i].color, false, PALETTE_SOLID_WRAP, 0), SEGENV.aux0/ripples[i].state*2)); + SEGMENT.setPixelColor((ripples[i].pos + ripples[i].state + SEGLEN) % SEGLEN, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(ripples[i].color, false, PALETTE_SOLID_WRAP, 0), uint8_t(2*255/ripples[i].state))); + SEGMENT.setPixelColor((ripples[i].pos - ripples[i].state + SEGLEN) % SEGLEN, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(ripples[i].color, false, PALETTE_SOLID_WRAP, 0), uint8_t(2*255/ripples[i].state))); ripples[i].state++; // Next step. break; } // switch step @@ -6403,8 +6342,8 @@ static const char _data_FX_MODE_RIPPLEPEAK[] PROGMEM = "Ripple Peak@Fade rate,Ma uint16_t mode_2DSwirl(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); @@ -6442,8 +6381,8 @@ static const char _data_FX_MODE_2DSWIRL[] PROGMEM = "Swirl@!,Sensitivity,Blur;,B uint16_t mode_2DWaverly(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; @@ -6452,10 +6391,10 @@ uint16_t mode_2DWaverly(void) { long t = strip.now / 2; for (int i = 0; i < cols; i++) { - unsigned thisVal = (1 + SEGMENT.intensity/64) * inoise8(i * 45 , t , t)/2; + unsigned thisVal = (1 + SEGMENT.intensity/64) * perlin8(i * 45 , t , t)/2; // use audio if available if (um_data) { - thisVal /= 32; // reduce intensity of inoise8() + thisVal /= 32; // reduce intensity of perlin8() thisVal *= volumeSmth; } int thisMax = map(thisVal, 0, 512, 0, rows); @@ -6482,7 +6421,10 @@ typedef struct Gravity { /////////////////////// // * GRAVCENTER // /////////////////////// -uint16_t mode_gravcenter(void) { // Gravcenter. By Andrew Tuline. +// Gravcenter effects By Andrew Tuline. +// Gravcenter base function for Gravcenter (0), Gravcentric (1), Gravimeter (2), Gravfreq (3) (merged by @dedehai) + +uint16_t mode_gravcenter_base(unsigned mode) { if (SEGLEN == 1) return mode_static(); const unsigned dataSize = sizeof(gravity); @@ -6492,83 +6434,92 @@ uint16_t mode_gravcenter(void) { // Gravcenter. By Andrew Tuline. um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; - //SEGMENT.fade_out(240); - SEGMENT.fade_out(251); // 30% + if(mode == 1) SEGMENT.fade_out(253); // //Gravcentric + else if(mode == 2) SEGMENT.fade_out(249); // Gravimeter + else if(mode == 3) SEGMENT.fade_out(250); // Gravfreq + else SEGMENT.fade_out(251); // Gravcenter + float mySampleAvg; + int tempsamp; float segmentSampleAvg = volumeSmth * (float)SEGMENT.intensity / 255.0f; - segmentSampleAvg *= 0.125; // divide by 8, to compensate for later "sensitivity" upscaling - float mySampleAvg = mapf(segmentSampleAvg*2.0, 0, 32, 0, (float)SEGLEN/2.0f); // map to pixels available in current segment - int tempsamp = constrain(mySampleAvg, 0, SEGLEN/2); // Keep the sample from overflowing. + if(mode == 2) { //Gravimeter + segmentSampleAvg *= 0.25; // divide by 4, to compensate for later "sensitivity" upscaling + mySampleAvg = mapf(segmentSampleAvg*2.0, 0, 64, 0, (SEGLEN-1)); // map to pixels availeable in current segment + tempsamp = constrain(mySampleAvg,0,SEGLEN-1); // Keep the sample from overflowing. + } + else { // Gravcenter or Gravcentric or Gravfreq + segmentSampleAvg *= 0.125f; // divide by 8, to compensate for later "sensitivity" upscaling + mySampleAvg = mapf(segmentSampleAvg*2.0, 0.0f, 32.0f, 0.0f, (float)SEGLEN/2.0f); // map to pixels availeable in current segment + tempsamp = constrain(mySampleAvg, 0, SEGLEN/2); // Keep the sample from overflowing. + } + uint8_t gravity = 8 - SEGMENT.speed/32; - - for (int i=0; i= gravcen->topLED) gravcen->topLED = tempsamp-offset; + else if (gravcen->gravityCounter % gravity == 0) gravcen->topLED--; + + if(mode == 1) { //Gravcentric + for (int i=0; itopLED >= 0) { + SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, CRGB::Gray); + SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, CRGB::Gray); + } } - - if (tempsamp >= gravcen->topLED) - gravcen->topLED = tempsamp-1; - else if (gravcen->gravityCounter % gravity == 0) - gravcen->topLED--; - - if (gravcen->topLED >= 0) { - SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0)); - SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0)); + else if(mode == 2) { //Gravimeter + for (int i=0; itopLED > 0) { + SEGMENT.setPixelColor(gravcen->topLED, SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0)); + } } + else if(mode == 3) { //Gravfreq + for (int i=0; iu_data[4]; // used in mode 3: Gravfreq + if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; + uint8_t index = (log10f(FFT_MajorPeak) - (MAX_FREQ_LOG10 - 1.78f)) * 255; + SEGMENT.setPixelColor(i+SEGLEN/2, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); + SEGMENT.setPixelColor(SEGLEN/2-i-1, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); + } + if (gravcen->topLED >= 0) { + SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, CRGB::Gray); + SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, CRGB::Gray); + } + } + else { //Gravcenter + for (int i=0; itopLED >= 0) { + SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0)); + SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0)); + } + } gravcen->gravityCounter = (gravcen->gravityCounter + 1) % gravity; return FRAMETIME; -} // mode_gravcenter() -static const char _data_FX_MODE_GRAVCENTER[] PROGMEM = "Gravcenter@Rate of fall,Sensitivity;!,!;!;1v;ix=128,m12=2,si=0"; // Circle, Beatsin +} +uint16_t mode_gravcenter(void) { // Gravcenter. By Andrew Tuline. + return mode_gravcenter_base(0); +} +static const char _data_FX_MODE_GRAVCENTER[] PROGMEM = "Gravcenter@Rate of fall,Sensitivity;!,!;!;1v;ix=128,m12=2,si=0"; // Circle, Beatsin /////////////////////// // * GRAVCENTRIC // /////////////////////// -uint16_t mode_gravcentric(void) { // Gravcentric. By Andrew Tuline. - if (SEGLEN == 1) return mode_static(); - - unsigned dataSize = sizeof(gravity); - if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed - Gravity* gravcen = reinterpret_cast(SEGENV.data); - - um_data_t *um_data = getAudioData(); - float volumeSmth = *(float*) um_data->u_data[0]; - - // printUmData(); - - //SEGMENT.fade_out(240); - //SEGMENT.fade_out(240); // twice? really? - SEGMENT.fade_out(253); // 50% - - float segmentSampleAvg = volumeSmth * (float)SEGMENT.intensity / 255.0f; - segmentSampleAvg *= 0.125f; // divide by 8, to compensate for later "sensitivity" upscaling - - float mySampleAvg = mapf(segmentSampleAvg*2.0, 0.0f, 32.0f, 0.0f, (float)SEGLEN/2.0f); // map to pixels availeable in current segment - int tempsamp = constrain(mySampleAvg, 0, SEGLEN/2); // Keep the sample from overflowing. - uint8_t gravity = 8 - SEGMENT.speed/32; - - for (int i=0; i= gravcen->topLED) - gravcen->topLED = tempsamp-1; - else if (gravcen->gravityCounter % gravity == 0) - gravcen->topLED--; - - if (gravcen->topLED >= 0) { - SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, CRGB::Gray); - SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, CRGB::Gray); - } - gravcen->gravityCounter = (gravcen->gravityCounter + 1) % gravity; - - return FRAMETIME; -} // mode_gravcentric() +uint16_t mode_gravcentric(void) { // Gravcentric. By Andrew Tuline. + return mode_gravcenter_base(1); +} static const char _data_FX_MODE_GRAVCENTRIC[] PROGMEM = "Gravcentric@Rate of fall,Sensitivity;!,!;!;1v;ix=128,m12=3,si=0"; // Corner, Beatsin @@ -6576,45 +6527,20 @@ static const char _data_FX_MODE_GRAVCENTRIC[] PROGMEM = "Gravcentric@Rate of fal // * GRAVIMETER // /////////////////////// uint16_t mode_gravimeter(void) { // Gravmeter. By Andrew Tuline. - if (SEGLEN == 1) return mode_static(); - - unsigned dataSize = sizeof(gravity); - if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed - Gravity* gravcen = reinterpret_cast(SEGENV.data); - - um_data_t *um_data = getAudioData(); - float volumeSmth = *(float*) um_data->u_data[0]; - - //SEGMENT.fade_out(240); - SEGMENT.fade_out(249); // 25% - - float segmentSampleAvg = volumeSmth * (float)SEGMENT.intensity / 255.0; - segmentSampleAvg *= 0.25; // divide by 4, to compensate for later "sensitivity" upscaling - - float mySampleAvg = mapf(segmentSampleAvg*2.0, 0, 64, 0, (SEGLEN-1)); // map to pixels availeable in current segment - int tempsamp = constrain(mySampleAvg,0,SEGLEN-1); // Keep the sample from overflowing. - uint8_t gravity = 8 - SEGMENT.speed/32; - - for (int i=0; i= gravcen->topLED) - gravcen->topLED = tempsamp; - else if (gravcen->gravityCounter % gravity == 0) - gravcen->topLED--; - - if (gravcen->topLED > 0) { - SEGMENT.setPixelColor(gravcen->topLED, SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0)); - } - gravcen->gravityCounter = (gravcen->gravityCounter + 1) % gravity; - - return FRAMETIME; -} // mode_gravimeter() + return mode_gravcenter_base(2); +} static const char _data_FX_MODE_GRAVIMETER[] PROGMEM = "Gravimeter@Rate of fall,Sensitivity;!,!;!;1v;ix=128,m12=2,si=0"; // Circle, Beatsin +/////////////////////// +// ** Gravfreq // +/////////////////////// +uint16_t mode_gravfreq(void) { // Gravfreq. By Andrew Tuline. + return mode_gravcenter_base(3); +} +static const char _data_FX_MODE_GRAVFREQ[] PROGMEM = "Gravfreq@Rate of fall,Sensitivity;!,!;!;1f;ix=128,m12=0,si=0"; // Pixels, Beatsin + + ////////////////////// // * JUGGLES // ////////////////////// @@ -6623,7 +6549,7 @@ uint16_t mode_juggles(void) { // Juggles. By Andrew Tuline. float volumeSmth = *(float*) um_data->u_data[0]; SEGMENT.fade_out(224); // 6.25% - unsigned my_sampleAgc = fmax(fmin(volumeSmth, 255.0), 0); + uint8_t my_sampleAgc = fmax(fmin(volumeSmth, 255.0), 0); for (size_t i=0; iSEGLEN/2) maxLen = SEGLEN/2; - for (int i=(SEGLEN/2-maxLen); i<(SEGLEN/2+maxLen); i++) { - uint8_t index = inoise8(i*volumeSmth+SEGENV.aux0, SEGENV.aux1+i*volumeSmth); // Get a value from the noise function. I'm using both x and y axis. + for (unsigned i=(SEGLEN/2-maxLen); i<(SEGLEN/2+maxLen); i++) { + 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. SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } @@ -6710,13 +6644,12 @@ uint16_t mode_noisefire(void) { // Noisefire. By Andrew Tuline. if (SEGENV.call == 0) SEGMENT.fill(BLACK); - for (int i = 0; i < SEGLEN; i++) { - unsigned index = inoise8(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. + for (unsigned i = 0; i < SEGLEN; i++) { + 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. index = (255 - i*256/SEGLEN) * index/(256-SEGMENT.intensity); // Now we need to scale index so that it gets blacker as we get close to one of the ends. // This is a simple y=mx+b equation that's been scaled. index/128 is another scaling. - CRGB color = ColorFromPalette(myPal, index, volumeSmth*2, LINEARBLEND); // Use the my own palette. - SEGMENT.setPixelColor(i, color); + SEGMENT.setPixelColor(i, ColorFromPalette(myPal, index, volumeSmth*2, LINEARBLEND)); // Use my own palette. } return FRAMETIME; @@ -6738,12 +6671,12 @@ uint16_t mode_noisemeter(void) { // Noisemeter. By Andrew Tuline. SEGMENT.fade_out(fadeRate); float tmpSound2 = volumeRaw * 2.0 * (float)SEGMENT.intensity / 255.0; - int maxLen = mapf(tmpSound2, 0, 255, 0, SEGLEN); // map to pixels availeable in current segment // Still a bit too sensitive. - if (maxLen <0) maxLen = 0; - if (maxLen >SEGLEN) maxLen = SEGLEN; + unsigned maxLen = mapf(tmpSound2, 0, 255, 0, SEGLEN); // map to pixels availeable in current segment // Still a bit too sensitive. + if (maxLen < 0) maxLen = 0; + if (maxLen > SEGLEN) maxLen = SEGLEN; - for (int i=0; ithisphase) & 0xFF)/2; thisbright += cos8_t(((i*(97 +(5*SEGMENT.speed/32)))+plasmoip->thatphase) & 0xFF)/2; // Let's munge the brightness a bit and animate it all with the phases. @@ -6822,72 +6755,54 @@ uint16_t mode_plasmoid(void) { // Plasmoid. By Andrew Tuline. static const char _data_FX_MODE_PLASMOID[] PROGMEM = "Plasmoid@Phase,# of pixels;!,!;!;01v;sx=128,ix=128,m12=0,si=0"; // Pixels, Beatsin -/////////////////////// -// * PUDDLEPEAK // -/////////////////////// -// Andrew's crappy peak detector. If I were 40+ years younger, I'd learn signal processing. -uint16_t mode_puddlepeak(void) { // Puddlepeak. By Andrew Tuline. - if (SEGLEN == 1) return mode_static(); - +////////////////////// +// * PUDDLES // +////////////////////// +// Puddles/Puddlepeak By Andrew Tuline. Merged by @dedehai +uint16_t mode_puddles_base(bool peakdetect) { + if (SEGLEN <= 1) return mode_static(); unsigned size = 0; - uint8_t fadeVal = map(SEGMENT.speed,0,255, 224, 254); - unsigned pos = random16(SEGLEN); // Set a random starting position. + uint8_t fadeVal = map(SEGMENT.speed, 0, 255, 224, 254); + unsigned pos = hw_random16(SEGLEN); // Set a random starting position. + SEGMENT.fade_out(fadeVal); um_data_t *um_data = getAudioData(); + int volumeRaw = *(int16_t*)um_data->u_data[1]; uint8_t samplePeak = *(uint8_t*)um_data->u_data[3]; uint8_t *maxVol = (uint8_t*)um_data->u_data[6]; uint8_t *binNum = (uint8_t*)um_data->u_data[7]; float volumeSmth = *(float*) um_data->u_data[0]; - if (SEGENV.call == 0) { - SEGMENT.custom1 = *binNum; - SEGMENT.custom2 = *maxVol * 2; + if(peakdetect) { // puddles peak + *binNum = SEGMENT.custom1; // Select a bin. + *maxVol = SEGMENT.custom2 / 2; // Our volume comparator. + if (samplePeak == 1) { + size = volumeSmth * SEGMENT.intensity /256 /4 + 1; // Determine size of the flash based on the volume. + if (pos+size>= SEGLEN) size = SEGLEN - pos; + } } - - *binNum = SEGMENT.custom1; // Select a bin. - *maxVol = SEGMENT.custom2 / 2; // Our volume comparator. - - SEGMENT.fade_out(fadeVal); - - if (samplePeak == 1) { - size = volumeSmth * SEGMENT.intensity /256 /4 + 1; // Determine size of the flash based on the volume. - if (pos+size>= SEGLEN) size = SEGLEN - pos; + else { // puddles + if (volumeRaw > 1) { + size = volumeRaw * SEGMENT.intensity /256 /8 + 1; // Determine size of the flash based on the volume. + if (pos+size >= SEGLEN) size = SEGLEN - pos; + } } - - for (unsigned i=0; iu_data[1]; - - if (volumeRaw > 1) { - size = volumeRaw * SEGMENT.intensity /256 /8 + 1; // Determine size of the flash based on the volume. - if (pos+size >= SEGLEN) size = SEGLEN - pos; - } - + for (unsigned i=0; i SPEED_FORMULA_L) { - unsigned segLoc = random16(SEGLEN); - SEGMENT.setPixelColor(segLoc, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(2*fftResult[SEGENV.aux0%16]*240/max(1, SEGLEN-1), false, PALETTE_SOLID_WRAP, 0), 2*fftResult[SEGENV.aux0%16])); + unsigned segLoc = hw_random16(SEGLEN); + SEGMENT.setPixelColor(segLoc, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(2*fftResult[SEGENV.aux0%16]*240/max(1, (int)SEGLEN-1), false, PALETTE_SOLID_WRAP, 0), uint8_t(2*fftResult[SEGENV.aux0%16]))); ++(SEGENV.aux0) %= 16; // make sure it doesn't cross 16 SEGENV.step = 1; - SEGMENT.blur(SEGMENT.intensity); + SEGMENT.blur(SEGMENT.intensity); // note: blur > 210 results in a alternating pattern, this could be fixed by mapping but some may like it (very old bug) } return FRAMETIME; @@ -6993,7 +6902,7 @@ static const char _data_FX_MODE_DJLIGHT[] PROGMEM = "DJ Light@Speed;;;01f;m12=2, // ** Freqmap // //////////////////// uint16_t mode_freqmap(void) { // Map FFT_MajorPeak to SEGLEN. Would be better if a higher framerate. - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); // Start frequency = 60 Hz and log10(60) = 1.78 // End frequency = MAX_FREQUENCY in Hz and lo10(MAX_FREQUENCY) = MAX_FREQ_LOG10 @@ -7009,11 +6918,11 @@ uint16_t mode_freqmap(void) { // Map FFT_MajorPeak to SEGLEN. int locn = (log10f((float)FFT_MajorPeak) - 1.78f) * (float)SEGLEN/(MAX_FREQ_LOG10 - 1.78f); // log10 frequency range is from 1.78 to 3.71. Let's scale to SEGLEN. if (locn < 1) locn = 0; // avoid underflow - if (locn >=SEGLEN) locn = SEGLEN-1; + if (locn >= (int)SEGLEN) locn = SEGLEN-1; unsigned pixCol = (log10f(FFT_MajorPeak) - 1.78f) * 255.0f/(MAX_FREQ_LOG10 - 1.78f); // Scale log10 of frequency values to the 255 colour index. if (FFT_MajorPeak < 61.0f) pixCol = 0; // handle underflow - unsigned bright = (int)my_magnitude; + uint8_t bright = (uint8_t)my_magnitude; SEGMENT.setPixelColor(locn, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(SEGMENT.intensity+pixCol, false, PALETTE_SOLID_WRAP, 0), bright)); @@ -7099,8 +7008,8 @@ uint16_t mode_freqpixels(void) { // Freqpixel. By Andrew Tuline. uint8_t pixCol = (log10f(FFT_MajorPeak) - 1.78f) * 255.0f/(MAX_FREQ_LOG10 - 1.78f); // Scale log10 of frequency values to the 255 colour index. if (FFT_MajorPeak < 61.0f) pixCol = 0; // handle underflow for (int i=0; i < SEGMENT.intensity/32+1; i++) { - unsigned locn = random16(0,SEGLEN); - SEGMENT.setPixelColor(locn, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(SEGMENT.intensity+pixCol, false, PALETTE_SOLID_WRAP, 0), (int)my_magnitude)); + unsigned locn = hw_random16(0,SEGLEN); + SEGMENT.setPixelColor(locn, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(SEGMENT.intensity+pixCol, false, PALETTE_SOLID_WRAP, 0), (uint8_t)my_magnitude)); } return FRAMETIME; @@ -7162,8 +7071,8 @@ uint16_t mode_freqwave(void) { // Freqwave. By Andreas Pleschun // shift the pixels one pixel outwards // if SEGLEN equals 1 these loops won't execute - for (int i = SEGLEN - 1; i > SEGLEN/2; i--) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i-1)); //move to the left - for (int i = 0; i < SEGLEN/2; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // move to the right + for (unsigned i = SEGLEN - 1; i > SEGLEN/2; i--) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i-1)); //move to the left + for (unsigned i = 0; i < SEGLEN/2; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // move to the right } return FRAMETIME; @@ -7171,54 +7080,6 @@ uint16_t mode_freqwave(void) { // Freqwave. By Andreas Pleschun static const char _data_FX_MODE_FREQWAVE[] PROGMEM = "Freqwave@Speed,Sound effect,Low bin,High bin,Pre-amp;;;01f;m12=2,si=0"; // Circle, Beatsin -/////////////////////// -// ** Gravfreq // -/////////////////////// -uint16_t mode_gravfreq(void) { // Gravfreq. By Andrew Tuline. - if (SEGLEN == 1) return mode_static(); - unsigned dataSize = sizeof(gravity); - if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed - Gravity* gravcen = reinterpret_cast(SEGENV.data); - - um_data_t *um_data = getAudioData(); - float FFT_MajorPeak = *(float*)um_data->u_data[4]; - float volumeSmth = *(float*)um_data->u_data[0]; - if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; // log10(0) is "forbidden" (throws exception) - - SEGMENT.fade_out(250); - - float segmentSampleAvg = volumeSmth * (float)SEGMENT.intensity / 255.0f; - segmentSampleAvg *= 0.125f; // divide by 8, to compensate for later "sensitivity" upscaling - - float mySampleAvg = mapf(segmentSampleAvg*2.0f, 0,32, 0, (float)SEGLEN/2.0f); // map to pixels availeable in current segment - int tempsamp = constrain(mySampleAvg,0,SEGLEN/2); // Keep the sample from overflowing. - uint8_t gravity = 8 - SEGMENT.speed/32; - - for (int i=0; i= gravcen->topLED) - gravcen->topLED = tempsamp-1; - else if (gravcen->gravityCounter % gravity == 0) - gravcen->topLED--; - - if (gravcen->topLED >= 0) { - SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, CRGB::Gray); - SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, CRGB::Gray); - } - gravcen->gravityCounter = (gravcen->gravityCounter + 1) % gravity; - - return FRAMETIME; -} // mode_gravfreq() -static const char _data_FX_MODE_GRAVFREQ[] PROGMEM = "Gravfreq@Rate of fall,Sensitivity;!,!;!;1f;ix=128,m12=0,si=0"; // Pixels, Beatsin - - ////////////////////// // ** Noisemove // ////////////////////// @@ -7231,15 +7092,15 @@ uint16_t mode_noisemove(void) { // Noisemove. By: Andrew Tuli uint8_t numBins = map(SEGMENT.intensity,0,255,0,16); // Map slider to fftResult bins. for (int i=0; i(SEGENV.data); //array of previous bar heights per frequency band @@ -7386,8 +7255,8 @@ static const char _data_FX_MODE_2DGEQ[] PROGMEM = "GEQ@Fade speed,Ripple decay,# uint16_t mode_2DFunkyPlank(void) { // Written by ??? Adapted by Will Tatam. if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; int NUMB_BANDS = map(SEGMENT.custom1, 0, 255, 1, 16); int barWidth = (cols / NUMB_BANDS); @@ -7474,8 +7343,8 @@ static uint8_t akemi[] PROGMEM = { uint16_t mode_2DAkemi(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; unsigned counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF; counter = counter >> 8; @@ -7523,7 +7392,7 @@ uint16_t mode_2DAkemi(void) { unsigned band = map(x, 0, max(xMax,4), 0, 15); // map 0..cols/8 to 16 GEQ bands band = constrain(band, 0, 15); int barHeight = map(fftResult[band], 0, 255, 0, 17*rows/32); - CRGB color = CRGB(SEGMENT.color_from_palette((band * 35), false, PALETTE_SOLID_WRAP, 0)); + uint32_t color = SEGMENT.color_from_palette((band * 35), false, PALETTE_SOLID_WRAP, 0); for (int y=0; y < barHeight; y++) { SEGMENT.setPixelColorXY(x, rows/2-y, color); @@ -7539,29 +7408,32 @@ static const char _data_FX_MODE_2DAKEMI[] PROGMEM = "Akemi@Color speed,Dance;Hea // Distortion waves - ldirko // https://editor.soulmatelights.com/gallery/1089-distorsion-waves -// adapted for WLED by @blazoncek +// adapted for WLED by @blazoncek, improvements by @dedehai uint16_t mode_2Ddistortionwaves() { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; uint8_t speed = SEGMENT.speed/32; uint8_t scale = SEGMENT.intensity/32; - - uint8_t w = 2; + if(SEGMENT.check2) scale += 192 / (cols+rows); // zoom out some more. note: not changing scale slider for backwards compatibility unsigned a = strip.now/32; unsigned a2 = a/2; unsigned a3 = a/3; + unsigned colsScaled = cols * scale; + unsigned rowsScaled = rows * scale; + + unsigned cx = beatsin16_t(10-speed,0,colsScaled); + unsigned cy = beatsin16_t(12-speed,0,rowsScaled); + unsigned cx1 = beatsin16_t(13-speed,0,colsScaled); + unsigned cy1 = beatsin16_t(15-speed,0,rowsScaled); + unsigned cx2 = beatsin16_t(17-speed,0,colsScaled); + unsigned cy2 = beatsin16_t(14-speed,0,rowsScaled); + + byte rdistort, gdistort, bdistort; - unsigned cx = beatsin8_t(10-speed,0,cols-1)*scale; - unsigned cy = beatsin8_t(12-speed,0,rows-1)*scale; - unsigned cx1 = beatsin8_t(13-speed,0,cols-1)*scale; - unsigned cy1 = beatsin8_t(15-speed,0,rows-1)*scale; - unsigned cx2 = beatsin8_t(17-speed,0,cols-1)*scale; - unsigned cy2 = beatsin8_t(14-speed,0,rows-1)*scale; - unsigned xoffs = 0; for (int x = 0; x < cols; x++) { xoffs += scale; @@ -7570,65 +7442,134 @@ uint16_t mode_2Ddistortionwaves() { for (int y = 0; y < rows; y++) { yoffs += scale; - byte rdistort = cos8_t((cos8_t(((x<<3)+a )&255)+cos8_t(((y<<3)-a2)&255)+a3 )&255)>>1; - byte gdistort = cos8_t((cos8_t(((x<<3)-a2)&255)+cos8_t(((y<<3)+a3)&255)+a+32 )&255)>>1; - byte bdistort = cos8_t((cos8_t(((x<<3)+a3)&255)+cos8_t(((y<<3)-a) &255)+a2+64)&255)>>1; + if(SEGMENT.check3) { + // alternate mode from original code + rdistort = cos8_t (((x+y)*8+a2)&255)>>1; + gdistort = cos8_t (((x+y)*8+a3+32)&255)>>1; + bdistort = cos8_t (((x+y)*8+a+64)&255)>>1; + } else { + rdistort = cos8_t((cos8_t(((x<<3)+a )&255)+cos8_t(((y<<3)-a2)&255)+a3 )&255)>>1; + gdistort = cos8_t((cos8_t(((x<<3)-a2)&255)+cos8_t(((y<<3)+a3)&255)+a+32 )&255)>>1; + bdistort = cos8_t((cos8_t(((x<<3)+a3)&255)+cos8_t(((y<<3)-a) &255)+a2+64)&255)>>1; + } - byte valueR = rdistort+ w* (a- ( ((xoffs - cx) * (xoffs - cx) + (yoffs - cy) * (yoffs - cy))>>7 )); - byte valueG = gdistort+ w* (a2-( ((xoffs - cx1) * (xoffs - cx1) + (yoffs - cy1) * (yoffs - cy1))>>7 )); - byte valueB = bdistort+ w* (a3-( ((xoffs - cx2) * (xoffs - cx2) + (yoffs - cy2) * (yoffs - cy2))>>7 )); + byte valueR = rdistort + ((a- ( ((xoffs - cx) * (xoffs - cx) + (yoffs - cy) * (yoffs - cy))>>7 ))<<1); + byte valueG = gdistort + ((a2-( ((xoffs - cx1) * (xoffs - cx1) + (yoffs - cy1) * (yoffs - cy1))>>7 ))<<1); + byte valueB = bdistort + ((a3-( ((xoffs - cx2) * (xoffs - cx2) + (yoffs - cy2) * (yoffs - cy2))>>7 ))<<1); valueR = gamma8(cos8_t(valueR)); valueG = gamma8(cos8_t(valueG)); valueB = gamma8(cos8_t(valueB)); - SEGMENT.setPixelColorXY(x, y, RGBW32(valueR, valueG, valueB, 0)); + if(SEGMENT.palette == 0) { + // use RGB values (original color mode) + SEGMENT.setPixelColorXY(x, y, RGBW32(valueR, valueG, valueB, 0)); + } else { + // use palette + uint8_t brightness = (valueR + valueG + valueB) / 3; + if(SEGMENT.check1) { // map brightness to palette index + SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, brightness, 255, LINEARBLEND_NOWRAP)); + } else { + // color mapping: calculate hue from pixel color, map it to palette index + CHSV hsvclr = rgb2hsv_approximate(CRGB(valueR>>2, valueG>>2, valueB>>2)); // scale colors down to not saturate for better hue extraction + SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, hsvclr.h, brightness)); + } + } } } + // palette mode and not filling: smear-blur to cover up palette wrapping artefacts + if(!SEGMENT.check1 && SEGMENT.palette) + SEGMENT.blur(200, true); + return FRAMETIME; } -static const char _data_FX_MODE_2DDISTORTIONWAVES[] PROGMEM = "Distortion Waves@!,Scale;;;2"; +static const char _data_FX_MODE_2DDISTORTIONWAVES[] PROGMEM = "Distortion Waves@!,Scale,,,,Fill,Zoom,Alt;;!;2;pal=0"; //Soap //@Stepko //Idea from https://www.youtube.com/watch?v=DiHBgITrZck&ab_channel=StefanPetrick -// adapted for WLED by @blazoncek +// adapted for WLED by @blazoncek, optimization by @dedehai +static void soapPixels(bool isRow, uint8_t *noise3d, CRGB *pixels) { + const int cols = SEG_W; + const int rows = SEG_H; + const auto XY = [&](int x, int y) { return x + y * cols; }; + const auto abs = [](int x) { return x<0 ? -x : x; }; + const int tRC = isRow ? rows : cols; // transpose if isRow + const int tCR = isRow ? cols : rows; // transpose if isRow + const int amplitude = max(1, (tCR - 8) >> 3) * (1 + (SEGMENT.custom1 >> 5)); + const int shift = 0; //(128 - SEGMENT.custom2)*2; + + CRGB ledsbuff[tCR]; + + for (int i = 0; i < tRC; i++) { + int amount = ((int)noise3d[isRow ? i*cols : i] - 128) * amplitude + shift; // use first row/column: XY(0,i)/XY(i,0) + int delta = abs(amount) >> 8; + int fraction = abs(amount) & 255; + for (int j = 0; j < tCR; j++) { + int zD, zF; + if (amount < 0) { + zD = j - delta; + zF = zD - 1; + } else { + zD = j + delta; + zF = zD + 1; + } + int yA = abs(zD)%tCR; + int yB = abs(zF)%tCR; + int xA = i; + int xB = i; + if (isRow) { + std::swap(xA,yA); + std::swap(xB,yB); + } + const int indxA = XY(xA,yA); + const int indxB = XY(xB,yB); + CRGB PixelA; + CRGB PixelB; + if ((zD >= 0) && (zD < tCR)) PixelA = pixels[indxA]; + else PixelA = ColorFromPalette(SEGPALETTE, ~noise3d[indxA]*3); + if ((zF >= 0) && (zF < tCR)) PixelB = pixels[indxB]; + else PixelB = ColorFromPalette(SEGPALETTE, ~noise3d[indxB]*3); + ledsbuff[j] = (PixelA.nscale8(ease8InOutApprox(255 - fraction))) + (PixelB.nscale8(ease8InOutApprox(fraction))); + } + for (int j = 0; j < tCR; j++) { + CRGB c = ledsbuff[j]; + if (isRow) std::swap(j,i); + SEGMENT.setPixelColorXY(i, j, pixels[XY(i,j)] = c); + if (isRow) std::swap(j,i); + } + } +} + uint16_t mode_2Dsoap() { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; + const auto XY = [&](int x, int y) { return x + y * cols; }; - const size_t dataSize = SEGMENT.width() * SEGMENT.height() * sizeof(uint8_t); // prevent reallocation if mirrored or grouped + const size_t segSize = SEGMENT.width() * SEGMENT.height(); // prevent reallocation if mirrored or grouped + const size_t dataSize = segSize * (sizeof(uint8_t) + sizeof(CRGB)); // pixels and noise if (!SEGENV.allocateData(dataSize + sizeof(uint32_t)*3)) return mode_static(); //allocation failed - uint8_t *noise3d = reinterpret_cast(SEGENV.data); - uint32_t *noise32_x = reinterpret_cast(SEGENV.data + dataSize); - uint32_t *noise32_y = reinterpret_cast(SEGENV.data + dataSize + sizeof(uint32_t)); - uint32_t *noise32_z = reinterpret_cast(SEGENV.data + dataSize + sizeof(uint32_t)*2); + uint8_t *noise3d = reinterpret_cast(SEGENV.data); + CRGB *pixels = reinterpret_cast(SEGENV.data + segSize * sizeof(uint8_t)); + uint32_t *noisecoord = reinterpret_cast(SEGENV.data + dataSize); // x, y, z coordinates const uint32_t scale32_x = 160000U/cols; const uint32_t scale32_y = 160000U/rows; const uint32_t mov = MIN(cols,rows)*(SEGMENT.speed+2)/2; const uint8_t smoothness = MIN(250,SEGMENT.intensity); // limit as >250 produces very little changes - // init - if (SEGENV.call == 0) { - *noise32_x = random16(); - *noise32_y = random16(); - *noise32_z = random16(); - } else { - *noise32_x += mov; - *noise32_y += mov; - *noise32_z += mov; - } + if (SEGENV.call == 0) for (int i = 0; i < 3; i++) noisecoord[i] = hw_random(); // init + else for (int i = 0; i < 3; i++) noisecoord[i] += mov; for (int i = 0; i < cols; i++) { int32_t ioffset = scale32_x * (i - cols / 2); for (int j = 0; j < rows; j++) { int32_t joffset = scale32_y * (j - rows / 2); - uint8_t data = inoise16(*noise32_x + ioffset, *noise32_y + joffset, *noise32_z) >> 8; + uint8_t data = perlin16(noisecoord[0] + ioffset, noisecoord[1] + joffset, noisecoord[2]) >> 8; noise3d[XY(i,j)] = scale8(noise3d[XY(i,j)], smoothness) + scale8(data, 255 - smoothness); } } @@ -7643,64 +7584,12 @@ uint16_t mode_2Dsoap() { } } - int zD; - int zF; - int amplitude; - int shiftX = 0; //(SEGMENT.custom1 - 128) / 4; - int shiftY = 0; //(SEGMENT.custom2 - 128) / 4; - CRGB ledsbuff[MAX(cols,rows)]; - - amplitude = (cols >= 16) ? (cols-8)/8 : 1; - for (int y = 0; y < rows; y++) { - int amount = ((int)noise3d[XY(0,y)] - 128) * 2 * amplitude + 256*shiftX; - int delta = abs(amount) >> 8; - int fraction = abs(amount) & 255; - for (int x = 0; x < cols; x++) { - if (amount < 0) { - zD = x - delta; - zF = zD - 1; - } else { - zD = x + delta; - zF = zD + 1; - } - CRGB PixelA = CRGB::Black; - if ((zD >= 0) && (zD < cols)) PixelA = SEGMENT.getPixelColorXY(zD, y); - else PixelA = ColorFromPalette(SEGPALETTE, ~noise3d[XY(abs(zD),y)]*3); - CRGB PixelB = CRGB::Black; - if ((zF >= 0) && (zF < cols)) PixelB = SEGMENT.getPixelColorXY(zF, y); - else PixelB = ColorFromPalette(SEGPALETTE, ~noise3d[XY(abs(zF),y)]*3); - ledsbuff[x] = (PixelA.nscale8(ease8InOutApprox(255 - fraction))) + (PixelB.nscale8(ease8InOutApprox(fraction))); - } - for (int x = 0; x < cols; x++) SEGMENT.setPixelColorXY(x, y, ledsbuff[x]); - } - - amplitude = (rows >= 16) ? (rows-8)/8 : 1; - for (int x = 0; x < cols; x++) { - int amount = ((int)noise3d[XY(x,0)] - 128) * 2 * amplitude + 256*shiftY; - int delta = abs(amount) >> 8; - int fraction = abs(amount) & 255; - for (int y = 0; y < rows; y++) { - if (amount < 0) { - zD = y - delta; - zF = zD - 1; - } else { - zD = y + delta; - zF = zD + 1; - } - CRGB PixelA = CRGB::Black; - if ((zD >= 0) && (zD < rows)) PixelA = SEGMENT.getPixelColorXY(x, zD); - else PixelA = ColorFromPalette(SEGPALETTE, ~noise3d[XY(x,abs(zD))]*3); - CRGB PixelB = CRGB::Black; - if ((zF >= 0) && (zF < rows)) PixelB = SEGMENT.getPixelColorXY(x, zF); - else PixelB = ColorFromPalette(SEGPALETTE, ~noise3d[XY(x,abs(zF))]*3); - ledsbuff[y] = (PixelA.nscale8(ease8InOutApprox(255 - fraction))) + (PixelB.nscale8(ease8InOutApprox(fraction))); - } - for (int y = 0; y < rows; y++) SEGMENT.setPixelColorXY(x, y, ledsbuff[y]); - } + soapPixels(true, noise3d, pixels); // rows + soapPixels(false, noise3d, pixels); // cols return FRAMETIME; } -static const char _data_FX_MODE_2DSOAP[] PROGMEM = "Soap@!,Smoothness;;!;2"; +static const char _data_FX_MODE_2DSOAP[] PROGMEM = "Soap@!,Smoothness,Density;;!;2;pal=11"; //Idea from https://www.youtube.com/watch?v=HsA-6KIbgto&ab_channel=GreatScott%21 @@ -7710,8 +7599,9 @@ static const char _data_FX_MODE_2DSOAP[] PROGMEM = "Soap@!,Smoothness;;!;2"; uint16_t mode_2Doctopus() { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; + const auto XY = [&](int x, int y) { return (x%cols) + (y%rows) * cols; }; const uint8_t mapp = 180 / MAX(cols,rows); typedef struct { @@ -7753,8 +7643,7 @@ uint16_t mode_2Doctopus() { //CRGB c = CHSV(SEGENV.step / 2 - radius, 255, sin8_t(sin8_t((angle * 4 - radius) / 4 + SEGENV.step) + radius - SEGENV.step * 2 + angle * (SEGMENT.custom3/3+1))); unsigned intensity = sin8_t(sin8_t((angle * 4 - radius) / 4 + SEGENV.step/2) + radius - SEGENV.step + angle * (SEGMENT.custom3/4+1)); intensity = map((intensity*intensity) & 0xFFFF, 0, 65535, 0, 255); // add a bit of non-linearity for cleaner display - CRGB c = ColorFromPalette(SEGPALETTE, SEGENV.step / 2 - radius, intensity); - SEGMENT.setPixelColorXY(x, y, c); + SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, SEGENV.step / 2 - radius, intensity)); } } return FRAMETIME; @@ -7764,27 +7653,2899 @@ static const char _data_FX_MODE_2DOCTOPUS[] PROGMEM = "Octopus@!,,Offset X,Offse //Waving Cell //@Stepko (https://editor.soulmatelights.com/gallery/1704-wavingcells) -// adapted for WLED by @blazoncek +// adapted for WLED by @blazoncek, improvements by @dedehai uint16_t mode_2Dwavingcell() { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const int cols = SEGMENT.virtualWidth(); - const int rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; - uint32_t t = strip.now/(257-SEGMENT.speed); - uint8_t aX = SEGMENT.custom1/16 + 9; - uint8_t aY = SEGMENT.custom2/16 + 1; - uint8_t aZ = SEGMENT.custom3 + 1; - for (int x = 0; x < cols; x++) for (int y = 0; y >3; + uint32_t aX = SEGMENT.custom1/16 + 9; + uint32_t aY = SEGMENT.custom2/16 + 1; + uint32_t aZ = SEGMENT.custom3 + 1; + for (int x = 0; x < cols; x++) { + for (int y = 0; y < rows; y++) { + uint32_t wave = sin8_t((x * aX) + sin8_t((((y<<8) + t) * aY)>>8)) + cos8_t(y * aZ); // bit shifts to increase temporal resolution + uint8_t colorIndex = wave + (t>>(8-(SEGMENT.check2*3))); + SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, colorIndex)); + } + } + SEGMENT.blur(SEGMENT.intensity); + return FRAMETIME; +} +static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,Blur,Amplitude 1,Amplitude 2,Amplitude 3,,Flow;;!;2;ix=0"; + +#ifndef WLED_DISABLE_PARTICLESYSTEM2D + +/* + Particle System Vortex + Particles sprayed from center with a rotating spray + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +#define NUMBEROFSOURCES 8 +uint16_t mode_particlevortex(void) { + if (SEGLEN == 1) + return mode_static(); + ParticleSystem2D *PartSys = nullptr; + uint32_t i, j; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) + return mode_static(); // allocation failed + #ifdef ESP8266 + PartSys->setMotionBlur(180); + #else + PartSys->setMotionBlur(130); + #endif + for (i = 0; i < min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); i++) { + PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center + PartSys->sources[i].source.y = (PartSys->maxY + 1) >> 1; // center + PartSys->sources[i].maxLife = 900; + PartSys->sources[i].minLife = 800; + } + PartSys->setKillOutOfBounds(true); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + uint32_t spraycount = min(PartSys->numSources, (uint32_t)(1 + (SEGMENT.custom1 >> 5))); // number of sprays to display, 1-8 + #ifdef ESP8266 + for (i = 1; i < 4; i++) { // need static particles in the center to reduce blinking (would be black every other frame without this hack), just set them there fixed + int partindex = (int)PartSys->usedParticles - (int)i; + if (partindex >= 0) { + PartSys->particles[partindex].x = (PartSys->maxX + 1) >> 1; // center + PartSys->particles[partindex].y = (PartSys->maxY + 1) >> 1; // center + PartSys->particles[partindex].sat = 230; + PartSys->particles[partindex].ttl = 256; //keep alive + } + } + #endif + + if (SEGMENT.check1) + PartSys->setSmearBlur(90); // enable smear blur + else + PartSys->setSmearBlur(0); // disable smear blur + + // update colors of the sprays + for (i = 0; i < spraycount; i++) { + uint32_t coloroffset = 0xFF / spraycount; + PartSys->sources[i].source.hue = coloroffset * i; + } + + // set rotation direction and speed + // can use direction flag to determine current direction + bool direction = SEGMENT.check2; //no automatic direction change, set it to flag + int32_t currentspeed = (int32_t)SEGENV.step; // make a signed integer out of step + + if (SEGMENT.custom2 > 0) { // automatic direction change enabled + uint32_t changeinterval = 1040 - ((uint32_t)SEGMENT.custom2 << 2); + direction = SEGENV.aux1 & 0x01; //set direction according to flag + + if (SEGMENT.check3) // random interval + changeinterval = 20 + changeinterval + hw_random16(changeinterval); + + if (SEGMENT.call % changeinterval == 0) { //flip direction on next frame + SEGENV.aux1 |= 0x02; // set the update flag (for random interval update) + if (direction) + SEGENV.aux1 &= ~0x01; // clear the direction flag + else + SEGENV.aux1 |= 0x01; // set the direction flag + } + } + + int32_t targetspeed = (direction ? 1 : -1) * (SEGMENT.speed << 3); + int32_t speeddiff = targetspeed - currentspeed; + int32_t speedincrement = speeddiff / 50; + + if (speedincrement == 0) { //if speeddiff is not zero, make the increment at least 1 so it reaches target speed + if (speeddiff < 0) + speedincrement = -1; + else if (speeddiff > 0) + speedincrement = 1; + } + + currentspeed += speedincrement; + SEGENV.aux0 += currentspeed; + SEGENV.step = (uint32_t)currentspeed; //save it back + + uint16_t angleoffset = 0xFFFF / spraycount; // angle offset for an even distribution + uint32_t skip = PS_P_HALFRADIUS / (SEGMENT.intensity + 1) + 1; // intensity is emit speed, emit less on low speeds + if (SEGMENT.call % skip == 0) { + j = hw_random16(spraycount); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. + for (i = 0; i < spraycount; i++) { // emit one particle per spray (if available) + PartSys->sources[j].var = (SEGMENT.custom3 >> 1); //update speed variation + #ifdef ESP8266 + if (SEGMENT.call & 0x01) // every other frame, do not emit to save particles + #endif + PartSys->angleEmit(PartSys->sources[j], SEGENV.aux0 + angleoffset * j, (SEGMENT.intensity >> 2)+1); + j = (j + 1) % spraycount; + } + } + PartSys->update(); //update all particles and render to frame + return FRAMETIME; +} +#undef NUMBEROFSOURCES +static const char _data_FX_MODE_PARTICLEVORTEX[] PROGMEM = "PS Vortex@Rotation Speed,Particle Speed,Arms,Flip,Nozzle,Smear,Direction,Random Flip;;!;2;pal=27,c1=200,c2=0,c3=0"; + +/* + Particle Fireworks + Rockets shoot up and explode in a random color, sometimes in a defined pattern + by DedeHai (Damian Schneider) +*/ +#define NUMBEROFSOURCES 8 + +uint16_t mode_particlefireworks(void) { + ParticleSystem2D *PartSys = nullptr; + uint32_t numRockets; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) + return mode_static(); // allocation failed + + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setWallHardness(120); // ground bounce is fixed + numRockets = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); + for (uint32_t j = 0; j < numRockets; j++) { + PartSys->sources[j].source.ttl = 500 * j; // first rocket starts immediately, others follow soon + PartSys->sources[j].source.vy = -1; // at negative speed, no particles are emitted and if rocket dies, it will be relaunched + } + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + numRockets = map(SEGMENT.speed, 0 , 255, 4, min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES)); + + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceY(SEGMENT.check2); + PartSys->setGravity(map(SEGMENT.custom3, 0, 31, SEGMENT.check2 ? 1 : 0, 10)); // if bounded, set gravity to minimum of 1 or they will bounce at top + PartSys->setMotionBlur(map(SEGMENT.custom2, 0, 255, 0, 245)); // anable motion blur + + // update the rockets, set the speed state + for (uint32_t j = 0; j < numRockets; j++) { + PartSys->applyGravity(PartSys->sources[j].source); + PartSys->particleMoveUpdate(PartSys->sources[j].source, PartSys->sources[j].sourceFlags); + if (PartSys->sources[j].source.ttl == 0) { + if (PartSys->sources[j].source.vy > 0) { // rocket has died and is moving up. stop it so it will explode (is handled in the code below) + PartSys->sources[j].source.vy = 0; + } + else if (PartSys->sources[j].source.vy < 0) { // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it + PartSys->sources[j].source.y = PS_P_RADIUS; // start from bottom + PartSys->sources[j].source.x = (PartSys->maxX >> 2) + hw_random(PartSys->maxX >> 1); // centered half + PartSys->sources[j].source.vy = (SEGMENT.custom3) + hw_random16(SEGMENT.custom1 >> 3) + 5; // rocket speed TODO: need to adjust for segment height + PartSys->sources[j].source.vx = hw_random16(7) - 3; // not perfectly straight up + PartSys->sources[j].source.sat = 30; // low saturation -> exhaust is off-white + PartSys->sources[j].source.ttl = hw_random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // set fuse time + PartSys->sources[j].maxLife = 40; // exhaust particle life + PartSys->sources[j].minLife = 10; + PartSys->sources[j].vx = 0; // emitting speed + PartSys->sources[j].vy = -5; // emitting speed + PartSys->sources[j].var = 4; // speed variation around vx,vy (+/- var) + } + } + } + // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time + uint32_t emitparticles, frequency, baseangle, hueincrement; // number of particles to emit for each rocket's state + // variables for circular explosions + [[maybe_unused]] int32_t speed, currentspeed, speedvariation, percircle; + int32_t counter = 0; + [[maybe_unused]] uint16_t angle; + [[maybe_unused]] unsigned angleincrement; + bool circularexplosion = false; + + // emit particles for each rocket + for (uint32_t j = 0; j < numRockets; j++) { + // determine rocket state by its speed: + if (PartSys->sources[j].source.vy > 0) { // moving up, emit exhaust + emitparticles = 1; + } + else if (PartSys->sources[j].source.vy < 0) { // falling down, standby time + emitparticles = 0; + } + else { // speed is zero, explode! + PartSys->sources[j].source.hue = hw_random16(); // random color + PartSys->sources[j].source.sat = hw_random16(55) + 200; + PartSys->sources[j].maxLife = 200; + PartSys->sources[j].minLife = 100; + PartSys->sources[j].source.ttl = hw_random16((2000 - ((uint32_t)SEGMENT.speed << 2))) + 550 - (SEGMENT.speed << 1); // standby time til next launch + PartSys->sources[j].var = ((SEGMENT.intensity >> 4) + 5); // speed variation around vx,vy (+/- var) + PartSys->sources[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch + #ifdef ESP8266 + emitparticles = hw_random16(SEGMENT.intensity >> 3) + (SEGMENT.intensity >> 3) + 5; // defines the size of the explosion + #else + emitparticles = hw_random16(SEGMENT.intensity >> 2) + (SEGMENT.intensity >> 2) + 5; // defines the size of the explosion + #endif + + if (random16() & 1) { // 50% chance for circular explosion + circularexplosion = true; + speed = 2 + hw_random16(3) + ((SEGMENT.intensity >> 6)); + currentspeed = speed; + angleincrement = 2730 + hw_random16(5461); // minimum 15° + random(30°) + angle = hw_random16(); // random start angle + baseangle = angle; // save base angle for modulation + percircle = 0xFFFF / angleincrement + 1; // number of particles to make complete circles + hueincrement = hw_random16() & 127; // &127 is equivalent to %128 + int circles = 1 + hw_random16(3) + ((SEGMENT.intensity >> 6)); + frequency = hw_random16() & 127; // modulation frequency (= "waves per circle"), x.4 fixed point + emitparticles = percircle * circles; + PartSys->sources[j].var = angle & 1; // 0 or 1 variation, angle is random + } + } + uint32_t i; + for (i = 0; i < emitparticles; i++) { + if (circularexplosion) { + int32_t sineMod = 0xEFFF + sin16_t((uint16_t)(((angle * frequency) >> 4) + baseangle)); // shifted to positive values + currentspeed = (speed/2 + ((sineMod * speed) >> 16)) >> 1; // sine modulation on speed based on emit angle + PartSys->angleEmit(PartSys->sources[j], angle, currentspeed); // note: compiler warnings can be ignored, variables are set just above + counter++; + if (counter > percircle) { // full circle completed, increase speed + counter = 0; + speed += 3 + ((SEGMENT.intensity >> 6)); // increase speed to form a second wave + PartSys->sources[j].source.hue += hueincrement; // new color for next circle + PartSys->sources[j].source.sat = 100 + hw_random16(156); + } + angle += angleincrement; // set angle for next particle + } + else { // random explosion or exhaust + PartSys->sprayEmit(PartSys->sources[j]); + if ((j % 3) == 0) { + PartSys->sources[j].source.hue = hw_random16(); // random color for each particle (this is also true for exhaust, but that is white anyways) + } + } + } + if (i == 0) // no particles emitted, this rocket is falling + PartSys->sources[j].source.y = 1000; // reset position so gravity wont pull it to the ground and bounce it (vy MUST stay negative until relaunch) + circularexplosion = false; // reset for next rocket + } + if (SEGMENT.check3) { // fast speed, move particles twice + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i], nullptr, nullptr); + } + } + PartSys->update(); // update and render + return FRAMETIME; +} +#undef NUMBEROFSOURCES +static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,Blur,Gravity,Cylinder,Ground,Fast;;!;2;pal=11,ix=50,c1=40,c2=0,c3=12"; + +/* + Particle Volcano + Particles are sprayed from below, spray moves back and forth if option is set + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +#define NUMBEROFSOURCES 1 +uint16_t mode_particlevolcano(void) { + ParticleSystem2D *PartSys = nullptr; + PSsettings2D volcanosettings; + volcanosettings.asByte = 0b00000100; // PS settings for volcano movement: bounceX is enabled + uint8_t numSprays; // note: so far only one tested but more is possible + uint32_t i = 0; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, no additional data needed + return mode_static(); // allocation failed or not 2D + + PartSys->setBounceY(true); + PartSys->setGravity(); // enable with default gforce + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setMotionBlur(230); // anable motion blur + + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); // number of sprays + for (i = 0; i < numSprays; i++) { + PartSys->sources[i].source.hue = hw_random16(); + PartSys->sources[i].source.x = PartSys->maxX / (numSprays + 1) * (i + 1); // distribute evenly + PartSys->sources[i].maxLife = 300; // lifetime in frames + PartSys->sources[i].minLife = 250; + PartSys->sources[i].sourceFlags.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[i].sourceFlags.perpetual = true; // source never dies + } + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); // number of volcanoes + + // change source emitting color from time to time, emit one particle per spray + if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) { // every nth frame, cycle color and emit particles (and update the sources) + for (i = 0; i < numSprays; i++) { + PartSys->sources[i].source.y = PS_P_RADIUS + 5; // reset to just above the lower edge that is allowed for bouncing particles, if zero, particles already 'bounce' at start and loose speed. + PartSys->sources[i].source.vy = 0; //reset speed (so no extra particlesettin is required to keep the source 'afloat') + PartSys->sources[i].source.hue++; // = hw_random16(); //change hue of spray source (note: random does not look good) + PartSys->sources[i].source.vx = PartSys->sources[i].source.vx > 0 ? (SEGMENT.custom1 >> 2) : -(SEGMENT.custom1 >> 2); // set moving speed but keep the direction given by PS + PartSys->sources[i].vy = SEGMENT.speed >> 2; // emitting speed (upwards) + PartSys->sources[i].vx = 0; + PartSys->sources[i].var = SEGMENT.custom3 >> 1; // emiting variation = nozzle size (custom 3 goes from 0-31) + PartSys->sprayEmit(PartSys->sources[i]); + PartSys->setWallHardness(255); // full hardness for source bounce + PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &volcanosettings); //move the source + } + } + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setColorByAge(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setWallHardness(SEGMENT.custom2); + + if (SEGMENT.check3) // collisions enabled + PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); + + PartSys->update(); // update and render + return FRAMETIME; +} +#undef NUMBEROFSOURCES +static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Speed,Intensity,Move,Bounce,Spread,AgeColor,Walls,Collide;;!;2;pal=35,sx=100,ix=190,c1=0,c2=160,c3=6,o1=1"; + +/* + Particle Fire + realistic fire effect using particles. heat based and using perlin-noise for wind + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particlefire(void) { + ParticleSystem2D *PartSys = nullptr; + uint32_t i; // index variable + uint32_t numFlames; // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, SEGMENT.virtualWidth(), 4)) //maximum number of source (PS may limit based on segment size); need 4 additional bytes for time keeping (uint32_t lastcall) + return mode_static(); // allocation failed or not 2D + SEGENV.aux0 = hw_random16(); // aux0 is wind position (index) in the perlin noise + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check2); + PartSys->setMotionBlur(SEGMENT.check1 * 170); // anable/disable motion blur + PartSys->setSmearBlur(!SEGMENT.check1 * 60); // enable smear blur if motion blur is not enabled + + uint32_t firespeed = max((uint8_t)100, SEGMENT.speed); //limit speed to 100 minimum, reduce frame rate to make it slower (slower speeds than 100 do not look nice) + if (SEGMENT.speed < 100) { //slow, limit FPS + uint32_t *lastcall = reinterpret_cast(PartSys->PSdataEnd); + uint32_t period = strip.now - *lastcall; + if (period < (uint32_t)map(SEGMENT.speed, 0, 99, 50, 10)) { // limit to 90FPS - 20FPS + SEGMENT.call--; //skipping a frame, decrement the counter (on call0, this is never executed as lastcall is 0, so its fine to not check if >0) + //still need to render the frame or flickering will occur in transitions + PartSys->updateFire(SEGMENT.intensity, true); // render the fire without updating particles (render only) + return FRAMETIME; //do not update this frame + } + *lastcall = strip.now; + } + + uint32_t spread = (PartSys->maxX >> 5) * (SEGMENT.custom3 + 1); //fire around segment center (in subpixel points) + numFlames = min((uint32_t)PartSys->numSources, (4 + ((spread / PS_P_RADIUS) << 1))); // number of flames used depends on spread with, good value is (fire width in pixel) * 2 + uint32_t percycle = (numFlames * 2) / 3; // maximum number of particles emitted per cycle (TODO: for ESP826 maybe use flames/2) + + // update the flame sprays: + for (i = 0; i < numFlames; i++) { + if (SEGMENT.call & 1 && PartSys->sources[i].source.ttl > 0) { // every second frame + PartSys->sources[i].source.ttl--; + } else { // flame source is dead: initialize new flame: set properties of source + PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread >> 1) + hw_random(spread); // change flame position: distribute randomly on chosen width + PartSys->sources[i].source.y = -(PS_P_RADIUS << 2); // set the source below the frame + PartSys->sources[i].source.ttl = 20 + hw_random16((SEGMENT.custom1 * SEGMENT.custom1) >> 8) / (1 + (firespeed >> 5)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed + PartSys->sources[i].maxLife = hw_random16(SEGMENT.virtualHeight() >> 1) + 16; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height + PartSys->sources[i].minLife = PartSys->sources[i].maxLife >> 1; + PartSys->sources[i].vx = hw_random16(5) - 2; // emitting speed (sideways) + PartSys->sources[i].vy = (SEGMENT.virtualHeight() >> 1) + (firespeed >> 4) + (SEGMENT.custom1 >> 4); // emitting speed (upwards) + PartSys->sources[i].var = 2 + hw_random16(2 + (firespeed >> 4)); // speed variation around vx,vy (+/- var) + } + } + + if (SEGMENT.call % 3 == 0) { // update noise position and add wind + SEGENV.aux0++; // position in the perlin noise matrix for wind generation + if (SEGMENT.call % 10 == 0) + SEGENV.aux1++; // move in noise y direction so noise does not repeat as often + // add wind force to all particles + int8_t windspeed = ((int16_t)(perlin8(SEGENV.aux0, SEGENV.aux1) - 127) * SEGMENT.custom2) >> 7; + PartSys->applyForce(windspeed, 0); + } + SEGENV.step++; + + if (SEGMENT.check3) { //add turbulance (parameters and algorithm found by experimentation) + if (SEGMENT.call % map(firespeed, 0, 255, 4, 15) == 0) { + for (i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].y < PartSys->maxY / 4) { // do not apply turbulance everywhere -> bottom quarter seems a good balance + int32_t curl = ((int32_t)perlin8(PartSys->particles[i].x, PartSys->particles[i].y, SEGENV.step << 4) - 127); + PartSys->particles[i].vx += (curl * (firespeed + 10)) >> 9; + } + } + } + } + + // emit faster sparks at first flame position, amount and speed mostly dependends on intensity + if(hw_random8() < 10 + (SEGMENT.intensity >> 2)) { + for (i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl == 0) { // find a dead particle + PartSys->particles[i].ttl = hw_random16(SEGMENT.virtualHeight()) + 30; + PartSys->particles[i].x = PartSys->sources[0].source.x; + PartSys->particles[i].y = PartSys->sources[0].source.y; + PartSys->particles[i].vx = PartSys->sources[0].source.vx; + PartSys->particles[i].vy = (SEGMENT.virtualHeight() >> 1) + (firespeed >> 4) + ((30 + (SEGMENT.intensity >> 1) + SEGMENT.custom1) >> 4); // emitting speed (upwards) + break; // emit only one particle + } + } + } + + uint8_t j = hw_random16(); // start with a random flame (so each flame gets the chance to emit a particle if available particles is smaller than number of flames) + for (i = 0; i < percycle; i++) { + j = (j + 1) % numFlames; + PartSys->flameEmit(PartSys->sources[j]); + } + + PartSys->updateFire(SEGMENT.intensity, false); // update and render the fire return FRAMETIME; } -static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,,Amplitude 1,Amplitude 2,Amplitude 3;;!;2"; +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Flame Height,Wind,Spread,Smooth,Cylinder,Turbulence;;!;2;pal=35,sx=110,c1=110,c2=50,c3=31,o1=1"; + +/* + PS Ballpit: particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce + sliders control falling speed, intensity (number of particles spawned), inter-particle collision hardness (0 means no particle collisions) and render saturation + this is quite versatile, can be made to look like rain or snow or confetti etc. + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particlepit(void) { + ParticleSystem2D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 0, 0, true, false)) // init + return mode_static(); // allocation failed or not 2D + PartSys->setKillOutOfBounds(true); + PartSys->setGravity(); // enable with default gravity + PartSys->setUsedParticles(170); // use 75% of available particles + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setBounceY(SEGMENT.check3); + PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)150)); // limit to 100 min (if collisions are disabled, still want bouncy) + if (SEGMENT.custom2 > 0) + PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); + + uint32_t i; + if (SEGMENT.call % (128 - (SEGMENT.intensity >> 1)) == 0 && SEGMENT.intensity > 0) { // every nth frame emit particles, stop emitting if set to zero + for (i = 0; i < PartSys->usedParticles; i++) { // emit particles + if (PartSys->particles[i].ttl == 0) { // find a dead particle + // emit particle at random position over the top of the matrix (random16 is not random enough) + PartSys->particles[i].ttl = 1500 - (SEGMENT.speed << 2) + hw_random16(500); // if speed is higher, make them die sooner + PartSys->particles[i].x = hw_random(PartSys->maxX); //random(PartSys->maxX >> 1) + (PartSys->maxX >> 2); + PartSys->particles[i].y = (PartSys->maxY << 1); // particles appear somewhere above the matrix, maximum is double the height + PartSys->particles[i].vx = (int16_t)hw_random16(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); // side speed is +/- + PartSys->particles[i].vy = map(SEGMENT.speed, 0, 255, -5, -100); // downward speed + PartSys->particles[i].hue = hw_random16(); // set random color + PartSys->particleFlags[i].collide = true; // enable collision for particle + PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7; + // set particle size + if (SEGMENT.custom1 == 255) { + PartSys->setParticleSize(1); // set global size to 1 for advanced rendering (no single pixel particles) + PartSys->advPartProps[i].size = hw_random16(SEGMENT.custom1); // set each particle to random size + } else { + PartSys->setParticleSize(SEGMENT.custom1); // set global size + PartSys->advPartProps[i].size = 0; // use global size + } + break; // emit only one particle per round + } + } + } + + uint32_t frictioncoefficient = 1 + SEGMENT.check1; //need more friction if wrapX is set, see below note + if (SEGMENT.speed < 50) // for low speeds, apply more friction + frictioncoefficient = 50 - SEGMENT.speed; + + if (SEGMENT.call % 6 == 0)// (3 + max(3, (SEGMENT.speed >> 2))) == 0) // note: if friction is too low, hard particles uncontrollably 'wander' left and right if wrapX is enabled + PartSys->applyFriction(frictioncoefficient); + + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Size,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=220,c1=120,c2=130,c3=31,o3=1"; + +/* + Particle Waterfall + Uses palette for particle color, spray source at top emitting particles, many config options + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particlewaterfall(void) { + ParticleSystem2D *PartSys = nullptr; + uint8_t numSprays; + uint32_t i = 0; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 12)) // init, request 12 sources, no additional data needed + return mode_static(); // allocation failed or not 2D + + PartSys->setGravity(); // enable with default gforce + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setMotionBlur(190); // anable motion blur + PartSys->setSmearBlur(30); // enable 2D blurring (smearing) + for (i = 0; i < PartSys->numSources; i++) { + PartSys->sources[i].source.hue = i*90; + PartSys->sources[i].sourceFlags.collide = true; // seeded particles will collide + #ifdef ESP8266 + PartSys->sources[i].maxLife = 250; // lifetime in frames (ESP8266 has less particles, make them short lived to keep the water flowing) + PartSys->sources[i].minLife = 100; + #else + PartSys->sources[i].maxLife = 400; // lifetime in frames + PartSys->sources[i].minLife = 150; + #endif + } + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check1); // cylinder + PartSys->setBounceX(SEGMENT.check2); // walls + PartSys->setBounceY(SEGMENT.check3); // ground + PartSys->setWallHardness(SEGMENT.custom2); + numSprays = min((int32_t)PartSys->numSources, max(PartSys->maxXpixel / 6, (int32_t)2)); // number of sprays depends on segment width + if (SEGMENT.custom2 > 0) // collisions enabled + PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness + else { + PartSys->enableParticleCollisions(false); + PartSys->setWallHardness(120); // set hardness (for ground bounce) to fixed value if not using collisions + } + + for (i = 0; i < numSprays; i++) { + PartSys->sources[i].source.hue += 1 + hw_random16(SEGMENT.custom1>>1); // change hue of spray source + } + + if (SEGMENT.call % (12 - (SEGMENT.intensity >> 5)) == 0 && SEGMENT.intensity > 0) { // every nth frame, emit particles, do not emit if intensity is zero + for (i = 0; i < numSprays; i++) { + PartSys->sources[i].vy = -SEGMENT.speed >> 3; // emitting speed, down + //PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (PartSys->maxXpixel - numSprays * 2) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position + PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (PartSys->maxXpixel - numSprays) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position + PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS * ((i<<2) + 4)); // source y position, few pixels above the top to increase spreading before entering the matrix + PartSys->sources[i].var = (SEGMENT.custom1 >> 3); // emiting variation 0-32 + PartSys->sprayEmit(PartSys->sources[i]); + } + } + + if (SEGMENT.call % 20 == 0) + PartSys->applyFriction(1); // add just a tiny amount of friction to help smooth things + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "PS Waterfall@Speed,Intensity,Variation,Collide,Position,Cylinder,Walls,Ground;;!;2;pal=9,sx=15,ix=200,c1=32,c2=160,o3=1"; + +/* + Particle Box, applies gravity to particles in either a random direction or random but only downwards (sloshing) + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particlebox(void) { + ParticleSystem2D *PartSys = nullptr; + uint32_t i; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1)) // init + return mode_static(); // allocation failed or not 2D + PartSys->setBounceX(true); + PartSys->setBounceY(true); + SEGENV.aux0 = hw_random16(); // position in perlin noise + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setParticleSize(SEGMENT.custom3<<3); + PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); // wall hardness is 200 or more + PartSys->enableParticleCollisions(true, max(2, (int)SEGMENT.custom2)); // enable collisions and set particle collision hardness + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 2, 153)); // 1% - 60% + // add in new particles if amount has changed + for (i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl < 260) { // initialize handed over particles and dead particles + PartSys->particles[i].ttl = 260; // full brigthness + PartSys->particles[i].x = hw_random16(PartSys->maxX); + PartSys->particles[i].y = hw_random16(PartSys->maxY); + PartSys->particles[i].hue = hw_random8(); // make it colorful + PartSys->particleFlags[i].perpetual = true; // never die + PartSys->particleFlags[i].collide = true; // all particles colllide + break; // only spawn one particle per frame for less chaotic transitions + } + } + + if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0 && SEGMENT.speed > 0) { // how often the force is applied depends on speed setting + int32_t xgravity; + int32_t ygravity; + int32_t increment = (SEGMENT.speed >> 6) + 1; + + if (SEGMENT.check2) { // washing machine + int speed = tristate_square8(strip.now >> 7, 90, 15) / ((400 - SEGMENT.speed) >> 3); + SEGENV.aux0 += speed; + if (speed == 0) SEGENV.aux0 = 190; //down (= 270°) + } + else + SEGENV.aux0 -= increment; + + if (SEGMENT.check1) { // random, use perlin noise + xgravity = ((int16_t)perlin8(SEGENV.aux0) - 127); + ygravity = ((int16_t)perlin8(SEGENV.aux0 + 10000) - 127); + // scale the gravity force + xgravity = (xgravity * SEGMENT.custom1) / 128; + ygravity = (ygravity * SEGMENT.custom1) / 128; + } + else { // go in a circle + xgravity = ((int32_t)(SEGMENT.custom1) * cos16_t(SEGENV.aux0 << 8)) / 0xFFFF; + ygravity = ((int32_t)(SEGMENT.custom1) * sin16_t(SEGENV.aux0 << 8)) / 0xFFFF; + } + if (SEGMENT.check3) { // sloshing, y force is always downwards + if (ygravity > 0) + ygravity = -ygravity; + } + + PartSys->applyForce(xgravity, ygravity); + } + + if ((SEGMENT.call & 0x0F) == 0) // every 16th frame + PartSys->applyFriction(1); + + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@!,Particles,Tilt,Hardness,Size,Random,Washing Machine,Sloshing;;!;2;pal=53,ix=50,c3=1,o1=1"; + +/* + Fuzzy Noise: Perlin noise 'gravity' mapping as in particles on 'noise hills' viewed from above + calculates slope gradient at the particle positions and applies 'downhill' force, resulting in a fuzzy perlin noise display + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleperlin(void) { + ParticleSystem2D *PartSys = nullptr; + uint32_t i; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1, 0, true)) // init with 1 source and advanced properties + return mode_static(); // allocation failed or not 2D + + PartSys->setKillOutOfBounds(true); // should never happen, but lets make sure there are no stray particles + PartSys->setMotionBlur(230); // anable motion blur + PartSys->setBounceY(true); + SEGENV.aux0 = rand(); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceX(!SEGMENT.check1); + PartSys->setWallHardness(SEGMENT.custom1); // wall hardness + PartSys->enableParticleCollisions(SEGMENT.check3, SEGMENT.custom1); // enable collisions and set particle collision hardness + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 128)); // min is 10%, max is 50% + PartSys->setSmearBlur(SEGMENT.check2 * 15); // enable 2D blurring (smearing) + + // apply 'gravity' from a 2D perlin noise map + SEGENV.aux0 += 1 + (SEGMENT.speed >> 5); // noise z-position + // update position in noise + for (i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl == 0) { // revive dead particles (do not keep them alive forever, they can clump up, need to reseed) + PartSys->particles[i].ttl = hw_random16(500) + 200; + PartSys->particles[i].x = hw_random(PartSys->maxX); + PartSys->particles[i].y = hw_random(PartSys->maxY); + PartSys->particleFlags[i].collide = true; // particle colllides + } + uint32_t scale = 16 - ((31 - SEGMENT.custom3) >> 1); + uint16_t xnoise = PartSys->particles[i].x / scale; // position in perlin noise, scaled by slider + uint16_t ynoise = PartSys->particles[i].y / scale; + int16_t baseheight = perlin8(xnoise, ynoise, SEGENV.aux0); // noise value at particle position + PartSys->particles[i].hue = baseheight; // color particles to perlin noise value + if (SEGMENT.call % 8 == 0) { // do not apply the force every frame, is too chaotic + int8_t xslope = (baseheight + (int16_t)perlin8(xnoise - 10, ynoise, SEGENV.aux0)); + int8_t yslope = (baseheight + (int16_t)perlin8(xnoise, ynoise - 10, SEGENV.aux0)); + PartSys->applyForce(i, xslope, yslope); + } + } + + if (SEGMENT.call % (16 - (SEGMENT.custom2 >> 4)) == 0) + PartSys->applyFriction(2); + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,Smear,Collide;;!;2;pal=64,sx=50,ix=200,c1=130,c2=30,c3=5,o3=1"; + +/* + Particle smashing down like meteors and exploding as they hit the ground, has many parameters to play with + by DedeHai (Damian Schneider) +*/ +#define NUMBEROFSOURCES 8 +uint16_t mode_particleimpact(void) { + ParticleSystem2D *PartSys = nullptr; + uint32_t i = 0; + uint32_t numMeteors; + PSsettings2D meteorsettings; + meteorsettings.asByte = 0b00101000; // PS settings for meteors: bounceY and gravity enabled + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, no additional data needed + return mode_static(); // allocation failed or not 2D + PartSys->setKillOutOfBounds(true); + PartSys->setGravity(); // enable default gravity + PartSys->setBounceY(true); // always use ground bounce + PartSys->setWallRoughness(220); // high roughness + numMeteors = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); + for (i = 0; i < numMeteors; i++) { + PartSys->sources[i].source.ttl = hw_random16(10 * i); // set initial delay for meteors + PartSys->sources[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched + } + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setMotionBlur(SEGMENT.custom3<<3); + uint8_t hardness = map(SEGMENT.custom2, 0, 255, PS_P_MINSURFACEHARDNESS - 2, 255); + PartSys->setWallHardness(hardness); + PartSys->enableParticleCollisions(SEGMENT.check3, hardness); // enable collisions and set particle collision hardness + numMeteors = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); + uint32_t emitparticles; // number of particles to emit for each rocket's state + + for (i = 0; i < numMeteors; i++) { + // determine meteor state by its speed: + if ( PartSys->sources[i].source.vy < 0) // moving down, emit sparks + emitparticles = 1; + else if ( PartSys->sources[i].source.vy > 0) // moving up means meteor is on 'standby' + emitparticles = 0; + else { // speed is zero, explode! + PartSys->sources[i].source.vy = 10; // set source speed positive so it goes into timeout and launches again + emitparticles = map(SEGMENT.intensity, 0, 255, 10, hw_random16(PartSys->usedParticles>>2)); // defines the size of the explosion + } + for (int e = emitparticles; e > 0; e--) { + PartSys->sprayEmit(PartSys->sources[i]); + } + } + + // update the meteors, set the speed state + for (i = 0; i < numMeteors; i++) { + if (PartSys->sources[i].source.ttl) { + PartSys->sources[i].source.ttl--; // note: this saves an if statement, but moving down particles age twice + if (PartSys->sources[i].source.vy < 0) { // move down + PartSys->applyGravity(PartSys->sources[i].source); + PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &meteorsettings); + + // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) + if (PartSys->sources[i].source.y < PS_P_RADIUS<<1) { // reached the bottom pixel on its way down + PartSys->sources[i].source.vy = 0; // set speed zero so it will explode + PartSys->sources[i].source.vx = 0; + PartSys->sources[i].sourceFlags.collide = true; + #ifdef ESP8266 + PartSys->sources[i].maxLife = 900; + PartSys->sources[i].minLife = 100; + #else + PartSys->sources[i].maxLife = 1250; + PartSys->sources[i].minLife = 250; + #endif + PartSys->sources[i].source.ttl = hw_random16((768 - (SEGMENT.speed << 1))) + 40; // standby time til next launch (in frames) + PartSys->sources[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y + PartSys->sources[i].var = (SEGMENT.custom1 >> 2); // speed variation around vx,vy (+/- var) + } + } + } + else if (PartSys->sources[i].source.vy > 0) { // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it + // reinitialize meteor + PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS << 2); // start 4 pixels above the top + PartSys->sources[i].source.x = hw_random(PartSys->maxX); + PartSys->sources[i].source.vy = -hw_random16(30) - 30; // meteor downward speed + PartSys->sources[i].source.vx = hw_random16(50) - 25; // TODO: make this dependent on position so they do not move out of frame + PartSys->sources[i].source.hue = hw_random16(); // random color + PartSys->sources[i].source.ttl = 500; // long life, will explode at bottom + PartSys->sources[i].sourceFlags.collide = false; // trail particles will not collide + PartSys->sources[i].maxLife = 300; // spark particle life + PartSys->sources[i].minLife = 100; + PartSys->sources[i].vy = -9; // emitting speed (down) + PartSys->sources[i].var = 3; // speed variation around vx,vy (+/- var) + } + } + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl > 5) PartSys->particles[i].ttl -= 5; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan + } + + PartSys->update(); // update and render + return FRAMETIME; +} +#undef NUMBEROFSOURCES +static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,!,Force,Hardness,Blur,Cylinder,Walls,Collide;;!;2;pal=0,sx=32,ix=85,c1=70,c2=130,c3=0,o3=1"; + +/* + Particle Attractor, a particle attractor sits in the matrix center, a spray bounces around and seeds particles + uses inverse square law like in planetary motion + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleattractor(void) { + ParticleSystem2D *PartSys = nullptr; + PSsettings2D sourcesettings; + sourcesettings.asByte = 0b00001100; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) + PSparticleFlags attractorFlags; + attractorFlags.asByte = 0; // no flags set + PSparticle *attractor; // particle pointer to the attractor + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1, sizeof(PSparticle), true)) // init using 1 source and advanced particle settings + return mode_static(); // allocation failed or not 2D + PartSys->sources[0].source.hue = hw_random16(); + PartSys->sources[0].source.vx = -7; // will collied with wall and get random bounce direction + PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide + PartSys->sources[0].sourceFlags.perpetual = true; //source does not age + #ifdef ESP8266 + PartSys->sources[0].maxLife = 200; // lifetime in frames (ESP8266 has less particles) + PartSys->sources[0].minLife = 30; + #else + PartSys->sources[0].maxLife = 350; // lifetime in frames + PartSys->sources[0].minLife = 50; + #endif + PartSys->sources[0].var = 4; // emiting variation + PartSys->setWallHardness(255); //bounce forever + PartSys->setWallRoughness(200); //randomize wall bounce + } + else { + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + } + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + attractor = reinterpret_cast(PartSys->PSdataEnd); + + PartSys->setColorByAge(SEGMENT.check1); + PartSys->setParticleSize(SEGMENT.custom1 >> 1); //set size globally + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 190)); + + if (SEGMENT.custom2 > 0) // collisions enabled + PartSys->enableParticleCollisions(true, map(SEGMENT.custom2, 1, 255, 120, 255)); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); + + if (SEGMENT.call == 0) { + attractor->vx = PartSys->sources[0].source.vy; // set to spray movemement but reverse x and y + attractor->vy = PartSys->sources[0].source.vx; + } + + // set attractor properties + attractor->ttl = 100; // never dies + if (SEGMENT.check2) { + if ((SEGMENT.call % 3) == 0) // move slowly + PartSys->particleMoveUpdate(*attractor, attractorFlags, &sourcesettings); // move the attractor + } + else { + attractor->x = PartSys->maxX >> 1; // set to center + attractor->y = PartSys->maxY >> 1; + } + + if (SEGMENT.call % 5 == 0) + PartSys->sources[0].source.hue++; + + SEGENV.aux0 += 256; // emitting angle, one full turn in 255 frames (0xFFFF is 360°) + if (SEGMENT.call % 2 == 0) // alternate direction of emit + PartSys->angleEmit(PartSys->sources[0], SEGENV.aux0, 12); + else + PartSys->angleEmit(PartSys->sources[0], SEGENV.aux0 + 0x7FFF, 12); // emit at 180° as well + // apply force + uint32_t strength = SEGMENT.speed; + #ifdef USERMOD_AUDIOREACTIVE + um_data_t *um_data; + if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // AR active, do not use simulated data + uint32_t volumeSmth = (uint32_t)(*(float*) um_data->u_data[0]); // 0-255 + strength = (SEGMENT.speed * volumeSmth) >> 8; + } + #endif + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->pointAttractor(i, *attractor, strength, SEGMENT.check3); + } + if (SEGMENT.call % (33 - SEGMENT.custom3) == 0) + PartSys->applyFriction(2); + PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags, &sourcesettings); // move the source + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Size,Collide,Friction,AgeColor,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=2,c2=0"; + +/* + Particle Spray, just a particle spray with many parameters + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particlespray(void) { + ParticleSystem2D *PartSys = nullptr; + const uint8_t hardness = 200; // collision hardness is fixed + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1)) // init, no additional data needed + return mode_static(); // allocation failed or not 2D + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setBounceY(true); + PartSys->setMotionBlur(200); // anable motion blur + PartSys->setSmearBlur(10); // anable motion blur + PartSys->sources[0].source.hue = hw_random16(); + PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[0].var = 3; + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setBounceX(!SEGMENT.check2); + PartSys->setWrapX(SEGMENT.check2); + PartSys->setWallHardness(hardness); + PartSys->setGravity(8 * SEGMENT.check1); // enable gravity if checked (8 is default strength) + //numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays + + if (SEGMENT.check3) // collisions enabled + PartSys->enableParticleCollisions(true, hardness); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); + + //position according to sliders + PartSys->sources[0].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX); + PartSys->sources[0].source.y = map(SEGMENT.custom2, 0, 255, 0, PartSys->maxY); + uint16_t angle = (256 - (((int32_t)SEGMENT.custom3 + 1) << 3)) << 8; + + #ifdef USERMOD_AUDIOREACTIVE + um_data_t *um_data; + if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // get AR data, do not use simulated data + uint32_t volumeSmth = (uint8_t)(*(float*) um_data->u_data[0]); //0 to 255 + uint32_t volumeRaw = *(int16_t*)um_data->u_data[1]; //0 to 255 + PartSys->sources[0].minLife = 30; + + if (SEGMENT.call % 20 == 0 || SEGMENT.call % (11 - volumeSmth / 25) == 0) { // defines interval of particle emit + PartSys->sources[0].maxLife = (volumeSmth >> 1) + (SEGMENT.intensity >> 1); // lifetime in frames + PartSys->sources[0].var = 1 + ((volumeRaw * SEGMENT.speed) >> 12); + uint32_t emitspeed = (SEGMENT.speed >> 2) + (volumeRaw >> 3); + PartSys->sources[0].source.hue += volumeSmth/30; + PartSys->angleEmit(PartSys->sources[0], angle, emitspeed); + } + } + else { //no AR data, fall back to normal mode + // change source properties + if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) { // every nth frame, cycle color and emit particles + PartSys->sources[0].maxLife = 300 + SEGMENT.intensity; // lifetime in frames + PartSys->sources[0].minLife = 150 + SEGMENT.intensity; + PartSys->sources[0].source.hue++; // = hw_random16(); //change hue of spray source + PartSys->angleEmit(PartSys->sources[0], angle, SEGMENT.speed >> 2); + } + } + #else + // change source properties + if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) { // every nth frame, cycle color and emit particles + PartSys->sources[0].maxLife = 300; // lifetime in frames. note: could be done in init part, but AR moderequires this to be dynamic + PartSys->sources[0].minLife = 100; + PartSys->sources[0].source.hue++; // = hw_random16(); //change hue of spray source + // PartSys->sources[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) + // spray[j].source.hue = hw_random16(); //set random color for each particle (using palette) + PartSys->angleEmit(PartSys->sources[0], angle, SEGMENT.speed >> 2); + } + #endif + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "PS Spray@Speed,!,Left/Right,Up/Down,Angle,Gravity,Cylinder/Square,Collide;;!;2v;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21"; + + +/* + Particle base Graphical Equalizer + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleGEQ(void) { + ParticleSystem2D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1)) + return mode_static(); // allocation failed or not 2D + PartSys->setKillOutOfBounds(true); + PartSys->setUsedParticles(170); // use 2/3 of available particles + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + uint32_t i; + // set particle system properties + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setBounceY(SEGMENT.check3); + //PartSys->enableParticleCollisions(false); + PartSys->setWallHardness(SEGMENT.custom2); + PartSys->setGravity(SEGMENT.custom3 << 2); // set gravity strength + + um_data_t *um_data = getAudioData(); + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + + //map the bands into 16 positions on x axis, emit some particles according to frequency loudness + i = 0; + uint32_t binwidth = (PartSys->maxX + 1)>>4; //emit poisition variation for one bin (+/-) is equal to width/16 (for 16 bins) + uint32_t threshold = 300 - SEGMENT.intensity; + uint32_t emitparticles = 0; + + for (uint32_t bin = 0; bin < 16; bin++) { + uint32_t xposition = binwidth*bin + (binwidth>>1); // emit position according to frequency band + uint8_t emitspeed = ((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 9; // emit speed according to loudness of band (127 max!) + emitparticles = 0; + + if (fftResult[bin] > threshold) { + emitparticles = 1;// + (fftResult[bin]>>6); + } + else if (fftResult[bin] > 0) { // band has low volue + uint32_t restvolume = ((threshold - fftResult[bin])>>2) + 2; + if (hw_random16() % restvolume == 0) + emitparticles = 1; + } + + while (i < PartSys->usedParticles && emitparticles > 0) { // emit particles if there are any left, low frequencies take priority + if (PartSys->particles[i].ttl == 0) { // find a dead particle + //set particle properties TODO: could also use the spray... + PartSys->particles[i].ttl = 20 + map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + hw_random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames + PartSys->particles[i].x = xposition + hw_random16(binwidth) - (binwidth>>1); // position randomly, deviating half a bin width + PartSys->particles[i].y = PS_P_RADIUS; // start at the bottom (PS_P_RADIUS is minimum position a particle is fully in frame) + PartSys->particles[i].vx = hw_random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation: +/- custom1/4 + PartSys->particles[i].vy = emitspeed; + PartSys->particles[i].hue = (bin<<4) + hw_random16(17) - 8; // color from palette according to bin + emitparticles--; + } + i++; + } + } + + PartSys->update(); // update and render + return FRAMETIME; +} + +static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS GEQ 2D@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0"; + +/* + Particle rotating GEQ + Particles sprayed from center with rotating spray + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +#define NUMBEROFSOURCES 16 +uint16_t mode_particlecenterGEQ(void) { + ParticleSystem2D *PartSys = nullptr; + uint8_t numSprays; + uint32_t i; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, request 16 sources + return mode_static(); // allocation failed or not 2D + + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); + for (i = 0; i < numSprays; i++) { + PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center + PartSys->sources[i].source.y = (PartSys->maxY + 1) >> 1; // center + PartSys->sources[i].source.hue = i * 16; // even color distribution + PartSys->sources[i].maxLife = 400; + PartSys->sources[i].minLife = 200; + } + PartSys->setKillOutOfBounds(true); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); + + um_data_t *um_data = getAudioData(); + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + uint32_t threshold = 300 - SEGMENT.intensity; + + if (SEGMENT.check2) + SEGENV.aux0 += SEGMENT.custom1 << 2; + else + SEGENV.aux0 -= SEGMENT.custom1 << 2; + + uint16_t angleoffset = (uint16_t)0xFFFF / (uint16_t)numSprays; + uint32_t j = hw_random16(numSprays); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. + for (i = 0; i < numSprays; i++) { + if (SEGMENT.call % (32 - (SEGMENT.custom2 >> 3)) == 0 && SEGMENT.custom2 > 0) + PartSys->sources[j].source.hue += 1 + (SEGMENT.custom2 >> 4); + + PartSys->sources[j].var = SEGMENT.custom3 >> 2; + int8_t emitspeed = 5 + (((uint32_t)fftResult[j] * ((uint32_t)SEGMENT.speed + 20)) >> 10); // emit speed according to loudness of band + uint16_t emitangle = j * angleoffset + SEGENV.aux0; + + uint32_t emitparticles = 0; + if (fftResult[j] > threshold) + emitparticles = 1; + else if (fftResult[j] > 0) { // band has low value + uint32_t restvolume = ((threshold - fftResult[j]) >> 2) + 2; + if (hw_random16() % restvolume == 0) + emitparticles = 1; + } + if (emitparticles) + PartSys->angleEmit(PartSys->sources[j], emitangle, emitspeed); + + j = (j + 1) % numSprays; + } + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLECIRCULARGEQ[] PROGMEM = "PS GEQ Nova@Speed,Intensity,Rotation Speed,Color Change,Nozzle,,Direction;;!;2f;pal=13,ix=180,c1=0,c2=0,c3=8"; + +/* + Particle replacement of Ghost Rider by DedeHai (Damian Schneider), original FX by stepko adapted by Blaz Kristan (AKA blazoncek) +*/ +#define MAXANGLESTEP 2200 //32767 means 180° +uint16_t mode_particleghostrider(void) { + ParticleSystem2D *PartSys = nullptr; + PSsettings2D ghostsettings; + ghostsettings.asByte = 0b0000011; //enable wrapX and wrapY + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1)) // init, no additional data needed + return mode_static(); // allocation failed or not 2D + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->sources[0].maxLife = 260; // lifetime in frames + PartSys->sources[0].minLife = 250; + PartSys->sources[0].source.x = hw_random16(PartSys->maxX); + PartSys->sources[0].source.y = hw_random16(PartSys->maxY); + SEGENV.step = hw_random16(MAXANGLESTEP) - (MAXANGLESTEP>>1); // angle increment + } + else { + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + } + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + if (SEGMENT.intensity > 0) { // spiraling + if (SEGENV.aux1) { + SEGENV.step += SEGMENT.intensity>>3; + if ((int32_t)SEGENV.step > MAXANGLESTEP) + SEGENV.aux1 = 0; + } + else { + SEGENV.step -= SEGMENT.intensity>>3; + if ((int32_t)SEGENV.step < -MAXANGLESTEP) + SEGENV.aux1 = 1; + } + } + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(SEGMENT.custom1); + PartSys->sources[0].var = SEGMENT.custom3 >> 1; + + // color by age (PS 'color by age' always starts with hue = 255, don't want that here) + if (SEGMENT.check1) { + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->particles[i].hue = PartSys->sources[0].source.hue + (PartSys->particles[i].ttl<<2); + } + } + + // enable/disable walls + ghostsettings.bounceX = SEGMENT.check2; + ghostsettings.bounceY = SEGMENT.check2; + + SEGENV.aux0 += (int32_t)SEGENV.step; // step is angle increment + uint16_t emitangle = SEGENV.aux0 + 32767; // +180° + int32_t speed = map(SEGMENT.speed, 0, 255, 12, 64); + PartSys->sources[0].source.vx = ((int32_t)cos16_t(SEGENV.aux0) * speed) / (int32_t)32767; + PartSys->sources[0].source.vy = ((int32_t)sin16_t(SEGENV.aux0) * speed) / (int32_t)32767; + PartSys->sources[0].source.ttl = 500; // source never dies (note: setting 'perpetual' is not needed if replenished each frame) + PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags, &ghostsettings); + // set head (steal one of the particles) + PartSys->particles[PartSys->usedParticles-1].x = PartSys->sources[0].source.x; + PartSys->particles[PartSys->usedParticles-1].y = PartSys->sources[0].source.y; + PartSys->particles[PartSys->usedParticles-1].ttl = 255; + PartSys->particles[PartSys->usedParticles-1].sat = 0; //white + // emit two particles + PartSys->angleEmit(PartSys->sources[0], emitangle, speed); + PartSys->angleEmit(PartSys->sources[0], emitangle, speed); + if (SEGMENT.call % (11 - (SEGMENT.custom2 / 25)) == 0) { // every nth frame, cycle color and emit particles + PartSys->sources[0].source.hue++; + } + if (SEGMENT.custom2 > 190) //fast color change + PartSys->sources[0].source.hue += (SEGMENT.custom2 - 190) >> 2; + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEGHOSTRIDER[] PROGMEM = "PS Ghost Rider@Speed,Spiral,Blur,Color Cycle,Spread,AgeColor,Walls;;!;2;pal=1,sx=70,ix=0,c1=220,c2=30,c3=21,o1=1"; + +/* + PS Blobs: large particles bouncing around, changing size and form + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleblobs(void) { + ParticleSystem2D *PartSys = nullptr; + + if (SEGMENT.call == 0) { + if (!initParticleSystem2D(PartSys, 0, 0, true, true)) //init, no additional bytes, advanced size & size control + return mode_static(); // allocation failed or not 2D + PartSys->setBounceX(true); + PartSys->setBounceY(true); + PartSys->setWallHardness(255); + PartSys->setWallRoughness(255); + PartSys->setCollisionHardness(255); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 128)); // minimum 10%, maximum 50% of available particles (note: PS ensures at least 1) + PartSys->enableParticleCollisions(SEGMENT.check2); + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // update particles + if (SEGENV.aux0 != SEGMENT.speed || PartSys->particles[i].ttl == 0) { // speed changed or dead + PartSys->particles[i].vx = (int8_t)hw_random16(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); // +/- speed/4 + PartSys->particles[i].vy = (int8_t)hw_random16(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); + } + if (SEGENV.aux1 != SEGMENT.custom1 || PartSys->particles[i].ttl == 0) // size changed or dead + PartSys->advPartSize[i].maxsize = 60 + (SEGMENT.custom1 >> 1) + hw_random16((SEGMENT.custom1 >> 2)); // set each particle to slightly randomized size + + //PartSys->particles[i].perpetual = SEGMENT.check2; //infinite life if set + if (PartSys->particles[i].ttl == 0) { // find dead particle, renitialize + PartSys->particles[i].ttl = 300 + hw_random16(((uint16_t)SEGMENT.custom2 << 3) + 100); + PartSys->particles[i].x = hw_random(PartSys->maxX); + PartSys->particles[i].y = hw_random16(PartSys->maxY); + PartSys->particles[i].hue = hw_random16(); // set random color + PartSys->particleFlags[i].collide = true; // enable collision for particle + PartSys->advPartProps[i].size = 0; // start out small + PartSys->advPartSize[i].asymmetry = hw_random16(220); + PartSys->advPartSize[i].asymdir = hw_random16(255); + // set advanced size control properties + PartSys->advPartSize[i].grow = true; + PartSys->advPartSize[i].growspeed = 1 + hw_random16(9); + PartSys->advPartSize[i].shrinkspeed = 1 + hw_random16(9); + PartSys->advPartSize[i].wobblespeed = 1 + hw_random16(3); + } + //PartSys->advPartSize[i].asymmetry++; + PartSys->advPartSize[i].pulsate = SEGMENT.check3; + PartSys->advPartSize[i].wobble = SEGMENT.check1; + } + SEGENV.aux0 = SEGMENT.speed; //write state back + SEGENV.aux1 = SEGMENT.custom1; + + #ifdef USERMOD_AUDIOREACTIVE + um_data_t *um_data; + if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // get AR data, do not use simulated data + uint8_t volumeSmth = (uint8_t)(*(float*)um_data->u_data[0]); + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // update particles + if (SEGMENT.check3) //pulsate selected + PartSys->advPartProps[i].size = volumeSmth; + } + } + #endif + + PartSys->setMotionBlur(((SEGMENT.custom3) << 3) + 7); + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs,Size,Life,Blur,Wobble,Collide,Pulsate;;!;2v;sx=30,ix=64,c1=200,c2=130,c3=0,o3=1"; + +/* + Particle Galaxy, particles spiral like in a galaxy + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particlegalaxy(void) { + ParticleSystem2D *PartSys = nullptr; + PSsettings2D sourcesettings; + sourcesettings.asByte = 0b00001100; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1, 0, true)) // init using 1 source and advanced particle settings + return mode_static(); // allocation failed or not 2D + PartSys->sources[0].source.vx = -4; // will collide with wall and get random bounce direction + PartSys->sources[0].source.x = PartSys->maxX >> 1; // start in the center + PartSys->sources[0].source.y = PartSys->maxY >> 1; + PartSys->sources[0].sourceFlags.perpetual = true; //source does not age + PartSys->sources[0].maxLife = 4000; // lifetime in frames + PartSys->sources[0].minLife = 800; + PartSys->sources[0].source.hue = hw_random16(); // start with random color + PartSys->setWallHardness(255); //bounce forever + PartSys->setWallRoughness(200); //randomize wall bounce + } + else { + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + } + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + uint8_t particlesize = SEGMENT.custom1; + if(SEGMENT.check3) + particlesize = SEGMENT.custom1 ? 1 : 0; // set size to 0 (single pixel) or 1 (quad pixel) so motion blur works and adds streaks + PartSys->setParticleSize(particlesize); // set size globally + PartSys->setMotionBlur(250 * SEGMENT.check3); // adds trails to single/quad pixel particles, no effect if size > 1 + + if ((SEGMENT.call % ((33 - SEGMENT.custom3) >> 1)) == 0) // change hue of emitted particles + PartSys->sources[0].source.hue+=2; + + if (hw_random8() < (10 + (SEGMENT.intensity >> 1))) // 5%-55% chance to emit a particle in this frame + PartSys->sprayEmit(PartSys->sources[0]); + + if ((SEGMENT.call & 0x3) == 0) // every 4th frame, move the emitter + PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags, &sourcesettings); + + // move alive particles in a spiral motion (or almost straight in fast starfield mode) + int32_t centerx = PartSys->maxX >> 1; // center of matrix in subpixel coordinates + int32_t centery = PartSys->maxY >> 1; + if (SEGMENT.check2) { // starfield mode + PartSys->setKillOutOfBounds(true); + PartSys->sources[0].var = 7; // emiting variation + PartSys->sources[0].source.x = centerx; // set emitter to center + PartSys->sources[0].source.y = centery; + } + else { + PartSys->setKillOutOfBounds(false); + PartSys->sources[0].var = 1; // emiting variation + } + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { //check all particles + if (PartSys->particles[i].ttl == 0) continue; //skip dead particles + // (dx/dy): vector pointing from particle to center + int32_t dx = centerx - PartSys->particles[i].x; + int32_t dy = centery - PartSys->particles[i].y; + //speed towards center: + int32_t distance = sqrt32_bw(dx * dx + dy * dy); // absolute distance to center + if (distance < 20) distance = 20; // avoid division by zero, keep a minimum + int32_t speedfactor; + if (SEGMENT.check2) { // starfield mode + speedfactor = 1 + (1 + (SEGMENT.speed >> 1)) * distance; // speed increases towards edge + //apply velocity + PartSys->particles[i].x += (-speedfactor * dx) / 400000 - (dy >> 6); + PartSys->particles[i].y += (-speedfactor * dy) / 400000 + (dx >> 6); + } + else { + speedfactor = 2 + (((50 + SEGMENT.speed) << 6) / distance); // speed increases towards center + // rotate clockwise + int32_t tempVx = (-speedfactor * dy); // speed is orthogonal to center vector + int32_t tempVy = (speedfactor * dx); + //add speed towards center to make particles spiral in + int vxc = (dx << 9) / (distance - 19); // subtract value from distance to make the pull-in force a bit stronger (helps on faster speeds) + int vyc = (dy << 9) / (distance - 19); + //apply velocity + PartSys->particles[i].x += (tempVx + vxc) / 1024; // note: cannot use bit shift as that causes asymmetric rounding + PartSys->particles[i].y += (tempVy + vyc) / 1024; + + if (distance < 128) { // close to center + if (PartSys->particles[i].ttl > 3) + PartSys->particles[i].ttl -= 4; //age fast + PartSys->particles[i].sat = distance << 1; // turn white towards center + } + } + if(SEGMENT.custom3 == 31) // color by age but mapped to 1024 as particles have a long life, since age is random, this gives more or less random colors + PartSys->particles[i].hue = PartSys->particles[i].ttl >> 2; + else if(SEGMENT.custom3 == 0) // color by distance + PartSys->particles[i].hue = map(distance, 20, (PartSys->maxX + PartSys->maxY) >> 2, 0, 180); // color by distance to center + } + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEGALAXY[] PROGMEM = "PS Galaxy@!,!,Size,,Color,,Starfield,Trace;;!;2;pal=59,sx=80,c1=2,c3=4"; + +#endif //WLED_DISABLE_PARTICLESYSTEM2D #endif // WLED_DISABLE_2D +/////////////////////////// +// 1D Particle System FX // +/////////////////////////// + +#ifndef WLED_DISABLE_PARTICLESYSTEM1D +/* + Particle version of Drip and Rain + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleDrip(void) { + ParticleSystem1D *PartSys = nullptr; + //uint8_t numSprays; + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 4)) // init + return mode_static(); // allocation failed or single pixel + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->sources[0].source.hue = hw_random16(); + SEGENV.aux1 = 0xFFFF; // invalidate + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setBounce(true); + PartSys->setWallHardness(50); + + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setGravity(SEGMENT.custom3 >> 1); // set gravity (8 is default strength) + PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering + + if (SEGMENT.check2) { //collisions enabled + PartSys->enableParticleCollisions(true); //enable, full hardness + } + else + PartSys->enableParticleCollisions(false); + + PartSys->sources[0].sourceFlags.collide = false; //drops do not collide + + if (SEGMENT.check1) { //rain mode, emit at random position, short life (3-8 seconds at 50fps) + if (SEGMENT.custom1 == 0) //splash disabled, do not bounce raindrops + PartSys->setBounce(false); + PartSys->sources[0].var = 5; + PartSys->sources[0].v = -(8 + (SEGMENT.speed >> 2)); //speed + var must be < 128, inverted speed (=down) + // lifetime in frames + PartSys->sources[0].minLife = 30; + PartSys->sources[0].maxLife = 200; + PartSys->sources[0].source.x = hw_random(PartSys->maxX); //random emit position + } + else { //drip + PartSys->sources[0].var = 0; + PartSys->sources[0].v = -(SEGMENT.speed >> 1); //speed + var must be < 128, inverted speed (=down) + PartSys->sources[0].minLife = 3000; + PartSys->sources[0].maxLife = 3000; + PartSys->sources[0].source.x = PartSys->maxX - PS_P_RADIUS_1D; + } + + if (SEGENV.aux1 != SEGMENT.intensity) //slider changed + SEGENV.aux0 = 1; //must not be zero or "% 0" happens below which crashes on ESP32 + + SEGENV.aux1 = SEGMENT.intensity; // save state + + // every nth frame emit a particle + if (SEGMENT.call % SEGENV.aux0 == 0) { + int32_t interval = 300 / ((SEGMENT.intensity) + 1); + SEGENV.aux0 = interval + hw_random(interval + 5); + // if (SEGMENT.check1) // rain mode + // PartSys->sources[0].source.hue = 0; + // else + PartSys->sources[0].source.hue = hw_random8(); //set random color TODO: maybe also not random but color cycling? need another slider or checkmark for this. + PartSys->sprayEmit(PartSys->sources[0]); + } + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { //check all particles + if (PartSys->particles[i].ttl && PartSys->particleFlags[i].collide == false) { // use collision flag to identify splash particles + if (SEGMENT.custom1 > 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D << 1)) { //splash enabled and reached bottom + PartSys->particles[i].ttl = 0; //kill origin particle + PartSys->sources[0].maxLife = 80; + PartSys->sources[0].minLife = 20; + PartSys->sources[0].var = 10 + (SEGMENT.custom1 >> 3); + PartSys->sources[0].v = 0; + PartSys->sources[0].source.hue = PartSys->particles[i].hue; + PartSys->sources[0].source.x = PS_P_RADIUS_1D; + PartSys->sources[0].sourceFlags.collide = true; //splashes do collide if enabled + for (int j = 0; j < 2 + (SEGMENT.custom1 >> 2); j++) { + PartSys->sprayEmit(PartSys->sources[0]); + } + } + } + + if (SEGMENT.check1) { //rain mode, fade hue to max + if (PartSys->particles[i].hue < 245) + PartSys->particles[i].hue += 8; + } + //increase speed on high settings by calling the move function twice + if (SEGMENT.speed > 200) + PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]); + } + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEDRIP[] PROGMEM = "PS DripDrop@Speed,!,Splash,Blur,Gravity,Rain,PushSplash,Smooth;,!;!;1;pal=0,sx=150,ix=25,c1=220,c2=30,c3=21"; + + +/* + Particle Replacement for "Bbouncing Balls by Aircoookie" + Also replaces rolling balls and juggle (and maybe popcorn) + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particlePinball(void) { + ParticleSystem1D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init + return mode_static(); // allocation failed or is single pixel + PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[0].source.x = PS_P_RADIUS_1D; //emit at bottom + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return + SEGENV.aux0 = 1; + SEGENV.aux1 = 5000; //set out of range to ensure uptate on first call + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + //uint32_t hardness = 240 + (SEGMENT.custom1>>4); + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setGravity(map(SEGMENT.custom3, 0 , 31, 0 , 16)); // set gravity (8 is default strength) + PartSys->setBounce(SEGMENT.custom3); // disables bounce if no gravity is used + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->enableParticleCollisions(SEGMENT.check1, 255); // enable collisions and set particle collision to high hardness + PartSys->setUsedParticles(SEGMENT.intensity); + PartSys->setColorByPosition(SEGMENT.check3); + + bool updateballs = false; + if (SEGENV.aux1 != SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1 + PartSys->usedParticles) { // user settings change or more particles are available + SEGENV.step = SEGMENT.call; // reset delay + updateballs = true; + PartSys->sources[0].maxLife = SEGMENT.custom3 ? 5000 : 0xFFFF; // maximum lifetime in frames/2 (very long if not using gravity, this is enough to travel 4000 pixels at min speed) + PartSys->sources[0].minLife = PartSys->sources[0].maxLife >> 1; + } + + if (SEGMENT.check2) { //rolling balls + PartSys->setGravity(0); + PartSys->setWallHardness(255); + int speedsum = 0; + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->particles[i].ttl = 260; // keep particles alive + if (updateballs) { //speed changed or particle is dead, set particle properties + PartSys->particleFlags[i].collide = true; + if (PartSys->particles[i].x == 0) { // still at initial position (when not switching from a PS) + PartSys->particles[i].x = hw_random16(PartSys->maxX); // random initial position for all particles + PartSys->particles[i].vx = (hw_random16() & 0x01) ? 1 : -1; // random initial direction + } + PartSys->particles[i].hue = hw_random8(); //set ball colors to random + PartSys->advPartProps[i].sat = 255; + PartSys->advPartProps[i].size = SEGMENT.custom1; + } + speedsum += abs(PartSys->particles[i].vx); + } + int32_t avgSpeed = speedsum / PartSys->usedParticles; + int32_t setSpeed = 2 + (SEGMENT.speed >> 3); + if (avgSpeed < setSpeed) { // if balls are slow, speed up some of them at random to keep the animation going + for (int i = 0; i < setSpeed - avgSpeed; i++) { + int idx = hw_random16(PartSys->usedParticles); + PartSys->particles[idx].vx += PartSys->particles[idx].vx >= 0 ? 1 : -1; // add 1, keep direction + } + } + else if (avgSpeed > setSpeed + 8) // if avg speed is too high, apply friction to slow them down + PartSys->applyFriction(1); + } + else { //bouncing balls + PartSys->setWallHardness(220); + PartSys->sources[0].var = SEGMENT.speed >> 3; + int32_t newspeed = 2 + (SEGMENT.speed >> 1) - (SEGMENT.speed >> 3); + PartSys->sources[0].v = newspeed; + //check for balls that are 'laying on the ground' and remove them + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].vx == 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D + SEGMENT.custom1)) + PartSys->particles[i].ttl = 0; + if (updateballs) { + PartSys->advPartProps[i].size = SEGMENT.custom1; + if (SEGMENT.custom3 == 0) //gravity off, update speed + PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction + } + } + + // every nth frame emit a ball + if (SEGMENT.call > SEGENV.step) { + int interval = 260 - ((int)SEGMENT.intensity); + SEGENV.step += interval + hw_random16(interval); + PartSys->sources[0].source.hue = hw_random16(); //set ball color + PartSys->sources[0].sat = 255; + PartSys->sources[0].size = SEGMENT.custom1; + PartSys->sprayEmit(PartSys->sources[0]); + } + } + SEGENV.aux1 = SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1 + PartSys->usedParticles; + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]); // double the speed + } + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PSPINBALL[] PROGMEM = "PS Pinball@Speed,!,Size,Blur,Gravity,Collide,Rolling,Position Color;,!;!;1;pal=0,ix=220,c2=0,c3=8,o1=1"; + +/* + Particle Replacement for original Dancing Shadows: + "Spotlights moving back and forth that cast dancing shadows. + Shine this through tree branches/leaves or other close-up objects that cast + interesting shadows onto a ceiling or tarp. + By Steve Pomeroy @xxv" + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleDancingShadows(void) { + ParticleSystem1D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1)) // init, one source + return mode_static(); // allocation failed or is single pixel + PartSys->sources[0].maxLife = 1000; //set long life (kill out of bounds is done in custom way) + PartSys->sources[0].minLife = PartSys->sources[0].maxLife; + } + else { + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + } + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(SEGMENT.custom1); + if (SEGMENT.check1) + PartSys->setSmearBlur(120); // enable smear blur + else + PartSys->setSmearBlur(0); // disable smear blur + PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering + PartSys->setColorByPosition(SEGMENT.check2); // color fixed by position + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, 255)); // set percentage of particles to use + + uint32_t deadparticles = 0; + //kill out of bounds and moving away plus change color + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (((SEGMENT.call & 0x07) == 0) && PartSys->particleFlags[i].outofbounds) { //check if out of bounds particle move away from strip, only update every 8th frame + if ((int32_t)PartSys->particles[i].vx * PartSys->particles[i].x > 0) PartSys->particles[i].ttl = 0; //particle is moving away, kill it + } + PartSys->particleFlags[i].perpetual = true; //particles do not age + if (SEGMENT.call % (32 / (1 + (SEGMENT.custom2 >> 3))) == 0) + PartSys->particles[i].hue += 2 + (SEGMENT.custom2 >> 5); + //note: updating speed on the fly is not accurately possible, since it is unknown which particles are assigned to which spot + if (SEGENV.aux0 != SEGMENT.speed) { //speed changed + //update all particle speed by setting them to current value + PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? SEGMENT.speed >> 3 : -SEGMENT.speed >> 3; + } + if (PartSys->particles[i].ttl == 0) deadparticles++; // count dead particles + } + SEGENV.aux0 = SEGMENT.speed; + + //generate a spotlight: generates particles just outside of view + if (deadparticles > 5 && (SEGMENT.call & 0x03) == 0) { + //random color, random type + uint32_t type = hw_random16(SPOT_TYPES_COUNT); + int8_t speed = 2 + hw_random16(2 + (SEGMENT.speed >> 1)) + (SEGMENT.speed >> 4); + int32_t width = hw_random16(1, 10); + uint32_t ttl = 300; //ttl is particle brightness (below perpetual is set so it does not age, i.e. ttl stays at this value) + int32_t position; + //choose random start position, left and right from the segment + if (hw_random() & 0x01) { + position = PartSys->maxXpixel; + speed = -speed; + } + else + position = -width; + + PartSys->sources[0].v = speed; //emitted particle speed + PartSys->sources[0].source.hue = hw_random8(); //random spotlight color + for (int32_t i = 0; i < width; i++) { + if (width > 1) { + switch (type) { + case SPOT_TYPE_SOLID: + //nothing to do + break; + + case SPOT_TYPE_GRADIENT: + ttl = cubicwave8(map(i, 0, width - 1, 0, 255)); + ttl = ttl*ttl >> 8; //make gradient more pronounced + break; + + case SPOT_TYPE_2X_GRADIENT: + ttl = cubicwave8(2 * map(i, 0, width - 1, 0, 255)); + ttl = ttl*ttl >> 8; + break; + + case SPOT_TYPE_2X_DOT: + if (i > 0) position++; //skip one pixel + i++; + break; + + case SPOT_TYPE_3X_DOT: + if (i > 0) position += 2; //skip two pixels + i+=2; + break; + + case SPOT_TYPE_4X_DOT: + if (i > 0) position += 3; //skip three pixels + i+=3; + break; + } + } + //emit particle + //set the particle source position: + PartSys->sources[0].source.x = position * PS_P_RADIUS_1D; + uint32_t partidx = PartSys->sprayEmit(PartSys->sources[0]); + PartSys->particles[partidx].ttl = ttl; + position++; //do the next pixel + } + } + + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEDANCINGSHADOWS[] PROGMEM = "PS Dancing Shadows@Speed,!,Blur,Color Cycle,,Smear,Position Color,Smooth;,!;!;1;sx=100,ix=180,c1=0,c2=0"; + +/* + Particle Fireworks 1D replacement + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleFireworks1D(void) { + ParticleSystem1D *PartSys = nullptr; + uint8_t *forcecounter; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 4, 150, 4, true)) // init advanced particle system + if (!initParticleSystem1D(PartSys, 4, 150, 4, true)) // init advanced particle system + return mode_static(); // allocation failed or is single pixel + PartSys->setKillOutOfBounds(true); + PartSys->sources[0].sourceFlags.custom1 = 1; // set rocket state to standby + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + forcecounter = PartSys->PSdataEnd; + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + int32_t gravity = (1 + (SEGMENT.speed >> 3)); // gravity value used for rocket speed calculation + PartSys->setGravity(SEGMENT.speed ? gravity : 0); // set gravity + + if (PartSys->sources[0].sourceFlags.custom1 == 1) { // rocket is on standby + PartSys->sources[0].source.ttl--; + if (PartSys->sources[0].source.ttl == 0) { // time is up, relaunch + + if (hw_random8() < SEGMENT.custom1) // randomly choose direction according to slider, fire at start of segment if true + SEGENV.aux0 = 1; + else + SEGENV.aux0 = 0; + + PartSys->sources[0].sourceFlags.custom1 = 0; //flag used for rocket state + PartSys->sources[0].source.hue = hw_random16(); // different color for each launch + PartSys->sources[0].var = 10; // emit variation + PartSys->sources[0].v = -10; // emit speed + PartSys->sources[0].minLife = 30; + PartSys->sources[0].maxLife = SEGMENT.check2 ? 400 : 60; + PartSys->sources[0].source.x = 0; // start from bottom + uint32_t speed = sqrt((gravity * ((PartSys->maxX >> 2) + hw_random16(PartSys->maxX >> 1))) >> 4); // set speed such that rocket explods in frame + PartSys->sources[0].source.vx = min(speed, (uint32_t)127); + PartSys->sources[0].source.ttl = 4000; + PartSys->sources[0].sat = 30; // low saturation exhaust + PartSys->sources[0].size = SEGMENT.check3; // single or double pixel rendering + PartSys->sources[0].sourceFlags.reversegrav = false ; // normal gravity + + if (SEGENV.aux0) { // inverted rockets launch from end + PartSys->sources[0].sourceFlags.reversegrav = true; + PartSys->sources[0].source.x = PartSys->maxX; // start from top + PartSys->sources[0].source.vx = -PartSys->sources[0].source.vx; // revert direction + PartSys->sources[0].v = -PartSys->sources[0].v; // invert exhaust emit speed + } + } + } + else { // rocket is launched + int32_t rocketgravity = -gravity; + int32_t currentspeed = PartSys->sources[0].source.vx; + if (SEGENV.aux0) { // negative speed rocket + rocketgravity = -rocketgravity; + currentspeed = -currentspeed; + } + PartSys->applyForce(PartSys->sources[0].source, rocketgravity, forcecounter[0]); + PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags); + PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags); // increase rocket speed by calling the move function twice, also ages twice + uint32_t rocketheight = SEGENV.aux0 ? PartSys->maxX - PartSys->sources[0].source.x : PartSys->sources[0].source.x; + + if (currentspeed < 0 && PartSys->sources[0].source.ttl > 50) // reached apogee + PartSys->sources[0].source.ttl = min((uint32_t)50, rocketheight >> (PS_P_RADIUS_SHIFT_1D + 3)); // alive for a few more frames + + if (PartSys->sources[0].source.ttl < 2) { // explode + PartSys->sources[0].sourceFlags.custom1 = 1; // set standby state + PartSys->sources[0].var = 5 + ((((PartSys->maxX >> 1) + rocketheight) * (200 + SEGMENT.intensity)) / (PartSys->maxX << 2)); // set explosion particle speed + PartSys->sources[0].minLife = 600; + PartSys->sources[0].maxLife = 1300; + PartSys->sources[0].source.ttl = 100 + hw_random16(64 - (SEGMENT.speed >> 2)); // standby time til next launch + PartSys->sources[0].sat = SEGMENT.custom3 < 16 ? 10 + (SEGMENT.custom3 << 4) : 255; //color saturation + PartSys->sources[0].size = SEGMENT.check3 ? hw_random16(SEGMENT.intensity) : 0; // random particle size in explosion + uint32_t explosionsize = 8 + (PartSys->maxXpixel >> 2) + (PartSys->sources[0].source.x >> (PS_P_RADIUS_SHIFT_1D - 1)); + explosionsize += hw_random16((explosionsize * SEGMENT.intensity) >> 8); + for (uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles + int idx = PartSys->sprayEmit(PartSys->sources[0]); // emit a particle + if(SEGMENT.custom3 > 23) { + if(SEGMENT.custom3 == 31) { // highest slider value + PartSys->setColorByAge(SEGMENT.check1); // color by age if colorful mode is enabled + PartSys->setColorByPosition(!SEGMENT.check1); // color by position otherwise + } + else { // if custom3 is set to high value (but not highest), set particle color by initial speed + PartSys->particles[idx].hue = map(abs(PartSys->particles[idx].vx), 0, PartSys->sources[0].var, 0, 16 + hw_random16(200)); // set hue according to speed, use random amount of palette width + PartSys->particles[idx].hue += PartSys->sources[0].source.hue; // add hue offset of the rocket (random starting color) + } + } + else { + if (SEGMENT.check1) // colorful mode + PartSys->sources[0].source.hue = hw_random16(); //random color for each particle + } + } + } + } + if ((SEGMENT.call & 0x01) == 0 && PartSys->sources[0].sourceFlags.custom1 == false && PartSys->sources[0].source.ttl > 50) // every second frame and not in standby and not about to explode + PartSys->sprayEmit(PartSys->sources[0]); // emit exhaust particle + + if ((SEGMENT.call & 0x03) == 0) // every fourth frame + PartSys->applyFriction(1); // apply friction to all particles + + PartSys->update(); // update and render + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl > 10) PartSys->particles[i].ttl -= 10; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan + else PartSys->particles[i].ttl = 0; + } + return FRAMETIME; +} +static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur,Color,Colorful,Trail,Smooth;,!;!;1;c2=30,o1=1"; + +/* + Particle based Sparkle effect + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleSparkler(void) { + ParticleSystem1D *PartSys = nullptr; + uint32_t numSparklers; + PSsettings1D sparklersettings; + sparklersettings.asByte = 0; // PS settings for sparkler (set below) + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 16, 128 ,0, true)) // init, no additional data needed + return mode_static(); // allocation failed or is single pixel + } else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + + sparklersettings.wrap = !SEGMENT.check2; + sparklersettings.bounce = SEGMENT.check2; // note: bounce always takes priority over wrap + + numSparklers = PartSys->numSources; + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur/overlay + //PartSys->setSmearBlur(SEGMENT.custom2); // anable smearing blur + + for (uint32_t i = 0; i < numSparklers; i++) { + PartSys->sources[i].source.hue = hw_random16(); + PartSys->sources[i].var = 0; // sparks stationary + PartSys->sources[i].minLife = 150 + SEGMENT.intensity; + PartSys->sources[i].maxLife = 250 + (SEGMENT.intensity << 1); + int32_t speed = SEGMENT.speed >> 1; + if (SEGMENT.check1) // sparks move (slide option) + PartSys->sources[i].var = SEGMENT.intensity >> 3; + PartSys->sources[i].source.vx = PartSys->sources[i].source.vx > 0 ? speed : -speed; // update speed, do not change direction + PartSys->sources[i].source.ttl = 400; // replenish its life (setting it perpetual uses more code) + PartSys->sources[i].sat = SEGMENT.custom1; // color saturation + PartSys->sources[i].size = SEGMENT.check3 ? 120 : 0; + if (SEGMENT.speed == 255) // random position at highest speed setting + PartSys->sources[i].source.x = hw_random16(PartSys->maxX); + else + PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &sparklersettings); //move sparkler + } + + numSparklers = min(1 + (SEGMENT.custom3 >> 1), (int)numSparklers); // set used sparklers, 1 to 16 + + if (SEGENV.aux0 != SEGMENT.custom3) { //number of used sparklers changed, redistribute + for (uint32_t i = 1; i < numSparklers; i++) { + PartSys->sources[i].source.x = (PartSys->sources[0].source.x + (PartSys->maxX / numSparklers) * i ) % PartSys->maxX; //distribute evenly + } + } + SEGENV.aux0 = SEGMENT.custom3; + + for (uint32_t i = 0; i < numSparklers; i++) { + if (hw_random() % (((271 - SEGMENT.intensity) >> 4)) == 0) + PartSys->sprayEmit(PartSys->sources[i]); //emit a particle + } + + PartSys->update(); // update and render + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl > (64 - (SEGMENT.intensity >> 2))) PartSys->particles[i].ttl -= (64 - (SEGMENT.intensity >> 2)); //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan + else PartSys->particles[i].ttl = 0; + } + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_SPARKLER[] PROGMEM = "PS Sparkler@Move,!,Saturation,Blur,Sparklers,Slide,Bounce,Large;,!;!;1;pal=0,sx=255,c1=0,c2=0,c3=6"; + +/* + Particle based Hourglass, particles falling at defined intervals + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleHourglass(void) { + ParticleSystem1D *PartSys = nullptr; + constexpr int positionOffset = PS_P_RADIUS_1D / 2;; // resting position offset + bool* direction; + uint32_t* settingTracker; + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 0, 255, 8, false)) // init + return mode_static(); // allocation failed or is single pixel + PartSys->setBounce(true); + PartSys->setWallHardness(100); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + settingTracker = reinterpret_cast(PartSys->PSdataEnd); //assign data pointer + direction = reinterpret_cast(PartSys->PSdataEnd + 4); //assign data pointer + PartSys->setUsedParticles(1 + ((SEGMENT.intensity * 255) >> 8)); + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setGravity(map(SEGMENT.custom3, 0, 31, 1, 30)); + PartSys->enableParticleCollisions(true, 32); // hardness value found by experimentation on different settings + + uint32_t colormode = SEGMENT.custom1 >> 5; // 0-7 + + if (SEGMENT.intensity != *settingTracker) { // initialize + *settingTracker = SEGMENT.intensity; + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->particleFlags[i].reversegrav = true; // resting particles dont fall + *direction = 0; // down + SEGENV.aux1 = 1; // initialize below + } + SEGENV.aux0 = PartSys->usedParticles - 1; // initial state, start with highest number particle + } + + // calculate target position depending on direction + auto calcTargetPos = [&](size_t i) { + return PartSys->particleFlags[i].reversegrav ? + PartSys->maxX - i * PS_P_RADIUS_1D - positionOffset + : (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionOffset; + }; + + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // check if particle reached target position after falling + if (PartSys->particleFlags[i].fixed == false && abs(PartSys->particles[i].vx) < 5) { + int32_t targetposition = calcTargetPos(i); + bool closeToTarget = abs(targetposition - PartSys->particles[i].x) < 3 * PS_P_RADIUS_1D; + if (closeToTarget) { // close to target and slow speed + PartSys->particles[i].x = targetposition; // set exact position + PartSys->particleFlags[i].fixed = true; // pin particle + } + } + if (colormode == 7) + PartSys->setColorByPosition(true); // color fixed by position + else { + PartSys->setColorByPosition(false); + uint8_t basehue = ((SEGMENT.custom1 & 0x1F) << 3); // use 5 LSBs to select color + switch(colormode) { + case 0: PartSys->particles[i].hue = 120; break; // fixed at 120, if flip is activated, this can make red and green (use palette 34) + case 1: PartSys->particles[i].hue = basehue; break; // fixed selectable color + case 2: // 2 colors inverleaved (same code as 3) + case 3: PartSys->particles[i].hue = ((SEGMENT.custom1 & 0x1F) << 1) + (i % colormode)*74; break; // interleved colors (every 2 or 3 particles) + case 4: PartSys->particles[i].hue = basehue + (i * 255) / PartSys->usedParticles; break; // gradient palette colors + case 5: PartSys->particles[i].hue = basehue + (i * 1024) / PartSys->usedParticles; break; // multi gradient palette colors + case 6: PartSys->particles[i].hue = i + (strip.now >> 3); break; // disco! moving color gradient + default: break; + } + } + if (SEGMENT.check1 && !PartSys->particleFlags[i].reversegrav) // flip color when fallen + PartSys->particles[i].hue += 120; + } + + // re-order particles in case collisions flipped particles (highest number index particle is on the "bottom") + for (uint32_t i = 0; i < PartSys->usedParticles - 1; i++) { + if (PartSys->particles[i].x < PartSys->particles[i+1].x && PartSys->particleFlags[i].fixed == false && PartSys->particleFlags[i+1].fixed == false) { + std::swap(PartSys->particles[i].x, PartSys->particles[i+1].x); + } + } + + + if (SEGENV.aux1 == 1) { // last countdown call before dropping starts, reset all particles + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->particleFlags[i].collide = true; + PartSys->particleFlags[i].perpetual = true; + PartSys->particles[i].ttl = 260; + PartSys->particles[i].x = calcTargetPos(i); + PartSys->particleFlags[i].fixed = true; + } + } + + if (SEGENV.aux1 == 0) { // countdown passed, run + if (strip.now >= SEGENV.step) { // drop a particle, do not drop more often than every second frame or particles tangle up quite badly + // set next drop time + if (SEGMENT.check3 && *direction) // fast reset + SEGENV.step = strip.now + 100; // drop one particle every 100ms + else // normal interval + SEGENV.step = strip.now + max(20, SEGMENT.speed * 20); // map speed slider from 0.1s to 5s + if (SEGENV.aux0 < PartSys->usedParticles) { + PartSys->particleFlags[SEGENV.aux0].reversegrav = *direction; // let this particle fall or rise + PartSys->particleFlags[SEGENV.aux0].fixed = false; // unpin + } + else { // overflow + *direction = !(*direction); // flip direction + SEGENV.aux1 = SEGMENT.virtualLength() + 100; // set countdown + } + if (*direction == 0) // down, start dropping the highest number particle + SEGENV.aux0--; // next particle + else + SEGENV.aux0++; + } + } + else if (SEGMENT.check2) // auto reset + SEGENV.aux1--; // countdown + + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Interval,!,Color,Blur,Gravity,Colorflip,Start,Fast Reset;,!;!;1;pal=34,sx=50,ix=200,c1=140,c2=80,c3=4,o1=1,o2=1,o3=1"; + +/* + Particle based Spray effect (like a volcano, possible replacement for popcorn) + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particle1Dspray(void) { + ParticleSystem1D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1)) + return mode_static(); // allocation failed or is single pixel + PartSys->setKillOutOfBounds(true); + PartSys->setWallHardness(150); + PartSys->setParticleSize(1); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setBounce(SEGMENT.check2); + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + int32_t gravity = -((int32_t)SEGMENT.custom3 - 16); // gravity setting, 0-15 is positive (down), 17 - 31 is negative (up) + PartSys->setGravity(abs(gravity)); // use reversgrav setting to invert gravity (for proper 'floor' and out of bounce handling) + + PartSys->sources[0].source.hue = SEGMENT.aux0; // hw_random16(); + PartSys->sources[0].var = 20; + PartSys->sources[0].minLife = 200; + PartSys->sources[0].maxLife = 400; + PartSys->sources[0].source.x = map(SEGMENT.custom1, 0 , 255, 0, PartSys->maxX); // spray position + PartSys->sources[0].v = map(SEGMENT.speed, 0 , 255, -127 + PartSys->sources[0].var, 127 - PartSys->sources[0].var); // particle emit speed + PartSys->sources[0].sourceFlags.reversegrav = gravity < 0 ? true : false; + + if (hw_random() % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) { + PartSys->sprayEmit(PartSys->sources[0]); // emit a particle + SEGMENT.aux0++; // increment hue + } + + //update color settings + PartSys->setColorByAge(SEGMENT.check1); // overruled by 'color by position' + PartSys->setColorByPosition(SEGMENT.check3); + for (uint i = 0; i < PartSys->usedParticles; i++) { + PartSys->particleFlags[i].reversegrav = PartSys->sources[0].sourceFlags.reversegrav; // update gravity direction + } + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS Spray 1D@Speed(+/-),!,Position,Blur,Gravity(+/-),AgeColor,Bounce,Position Color;,!;!;1;sx=200,ix=220,c1=0,c2=0"; + +/* + Particle based balance: particles move back and forth (1D pendent to 2D particle box) + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleBalance(void) { + ParticleSystem1D *PartSys = nullptr; + uint32_t i; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 128)) // init, no additional data needed, use half of max particles + return mode_static(); // allocation failed or is single pixel + PartSys->setParticleSize(1); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(SEGMENT.custom2); // enable motion blur + PartSys->setBounce(!SEGMENT.check2); + PartSys->setWrap(SEGMENT.check2); + uint8_t hardness = SEGMENT.custom1 > 0 ? map(SEGMENT.custom1, 0, 255, 50, 250) : 200; // set hardness, make the walls hard if collisions are disabled + PartSys->enableParticleCollisions(SEGMENT.custom1, hardness); // enable collisions if custom1 > 0 + PartSys->setWallHardness(200); + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, 255)); + if (PartSys->usedParticles > SEGENV.aux1) { // more particles, reinitialize + for (i = 0; i < PartSys->usedParticles; i++) { + PartSys->particles[i].x = i * PS_P_RADIUS_1D; + PartSys->particles[i].ttl = 300; + PartSys->particleFlags[i].perpetual = true; + PartSys->particleFlags[i].collide = true; + } + } + SEGENV.aux1 = PartSys->usedParticles; + + // re-order particles in case collisions flipped particles + for (i = 0; i < PartSys->usedParticles - 1; i++) { + if (PartSys->particles[i].x > PartSys->particles[i+1].x) { + if (SEGMENT.check2) { // check for wrap around + if (PartSys->particles[i].x - PartSys->particles[i+1].x > 3 * PS_P_RADIUS_1D) + continue; + } + std::swap(PartSys->particles[i].x, PartSys->particles[i+1].x); + } + } + + if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0) { // how often the force is applied depends on speed setting + int32_t xgravity; + int32_t increment = (SEGMENT.speed >> 6) + 1; + SEGENV.aux0 += increment; + if (SEGMENT.check3) // random, use perlin noise + xgravity = ((int16_t)perlin8(SEGENV.aux0) - 128); + else // sinusoidal + xgravity = (int16_t)cos8(SEGENV.aux0) - 128;//((int32_t)(SEGMENT.custom3 << 2) * cos8(SEGENV.aux0) + // scale the force + xgravity = (xgravity * ((SEGMENT.custom3+1) << 2)) / 128; // xgravity: -127 to +127 + PartSys->applyForce(xgravity); + } + + uint32_t randomindex = hw_random16(PartSys->usedParticles); + PartSys->particles[randomindex].vx = ((int32_t)PartSys->particles[randomindex].vx * 200) / 255; // apply friction to random particle to reduce clumping (without collisions) + + //if (SEGMENT.check2 && (SEGMENT.call & 0x07) == 0) // no walls, apply friction to smooth things out + if ((SEGMENT.call & 0x0F) == 0 && SEGMENT.custom3 > 4) // apply friction every 16th frame to smooth things out (except for low tilt) + PartSys->applyFriction(1); // apply friction to all particles + + //update colors + PartSys->setColorByPosition(SEGMENT.check1); + if (!SEGMENT.check1) { + for (i = 0; i < PartSys->usedParticles; i++) { + PartSys->particles[i].hue = (1024 * i) / PartSys->usedParticles; // color by particle index + } + } + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Hardness,Blur,Tilt,Position Color,Wrap,Random;,!;!;1;pal=18,c2=0,c3=4,o1=1"; + +/* +Particle based Chase effect +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleChase(void) { + ParticleSystem1D *PartSys = nullptr; + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 255, 2, true)) // init + return mode_static(); // allocation failed or is single pixel + SEGENV.aux0 = 0xFFFF; // invalidate + *PartSys->PSdataEnd = 1; // huedir + *(PartSys->PSdataEnd + 1) = 1; // sizedir + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setColorByPosition(SEGMENT.check3); + PartSys->setMotionBlur(7 + ((SEGMENT.custom3) << 3)); // anable motion blur + uint32_t numParticles = 1 + map(SEGMENT.intensity, 0, 255, 2, 255 / (1 + (SEGMENT.custom1 >> 6))); // depends on intensity and particle size (custom1), minimum 1 + numParticles = min(numParticles, PartSys->usedParticles); // limit to available particles + int32_t huestep = 1 + ((((uint32_t)SEGMENT.custom2 << 19) / numParticles) >> 16); // hue increment + uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3; + if (SEGENV.aux0 != settingssum) { // settings changed changed, update + if (SEGMENT.check1) + SEGENV.step = PartSys->advPartProps[0].size / 2 + (PartSys->maxX / numParticles); + else + SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 5)) / numParticles; // spacing between particles + for (int32_t i = 0; i < (int32_t)PartSys->usedParticles; i++) { + PartSys->advPartProps[i].sat = 255; + PartSys->particles[i].x = (i - 1) * SEGENV.step; // distribute evenly (starts out of frame for i=0) + PartSys->particles[i].vx = SEGMENT.speed >> 2; + PartSys->advPartProps[i].size = SEGMENT.custom1; + if (SEGMENT.custom2 < 255) + PartSys->particles[i].hue = i * huestep; // gradient distribution + else + PartSys->particles[i].hue = hw_random16(); + } + SEGENV.aux0 = settingssum; + } + + if(SEGMENT.check1) { + huestep = 1 + (max((int)huestep, 3) * ((int(sin16_t(strip.now * 3) + 32767))) >> 15); // changes gradient spread (scale hue step) + } + + // wrap around (cannot use particle system wrap if distributing colors manually, it also wraps rendering which does not look good) + for (int32_t i = (int32_t)PartSys->usedParticles - 1; i >= 0; i--) { // check from the back, last particle wraps first, multiple particles can overrun per frame + if (PartSys->particles[i].x > PartSys->maxX + PS_P_RADIUS_1D + PartSys->advPartProps[i].size) { // wrap it around + uint32_t nextindex = (i + 1) % PartSys->usedParticles; + PartSys->particles[i].x = PartSys->particles[nextindex].x - (int)SEGENV.step; + if(SEGMENT.check1) // playful mode, vary size + PartSys->advPartProps[i].size = max(1 + (SEGMENT.custom1 >> 1), ((int(sin16_t(strip.now << 1) + 32767)) >> 8)); // cycle size + if (SEGMENT.custom2 < 255) + PartSys->particles[i].hue = PartSys->particles[nextindex].hue - huestep; + else + PartSys->particles[i].hue = hw_random16(); + } + PartSys->particles[i].ttl = 300; // reset ttl, cannot use perpetual because memmanager can change pointer at any time + } + + if (SEGMENT.check1) { // playful mode, changes hue, size, speed, density dynamically + int8_t* huedir = reinterpret_cast(PartSys->PSdataEnd); //assign data pointer + int8_t* stepdir = reinterpret_cast(PartSys->PSdataEnd + 1); + if(*stepdir == 0) *stepdir = 1; // initialize directions + if(*huedir == 0) *huedir = 1; + if (SEGENV.step >= (PartSys->advPartProps[0].size + PS_P_RADIUS_1D * 4) + PartSys->maxX / numParticles) + *stepdir = -1; // increase density (decrease space between particles) + else if (SEGENV.step <= (PartSys->advPartProps[0].size >> 1) + ((PartSys->maxX / numParticles))) + *stepdir = 1; // decrease density + if (SEGENV.aux1 > 512) + *huedir = -1; + else if (SEGENV.aux1 < 50) + *huedir = 1; + if (SEGMENT.call % (1024 / (1 + (SEGMENT.speed >> 2))) == 0) + SEGENV.aux1 += *huedir; + int8_t globalhuestep = 0; // global hue increment + if (SEGMENT.call % (1 + (int(sin16_t(strip.now) + 32767) >> 12)) == 0) + globalhuestep = 2; // global hue change to add some color variation + if ((SEGMENT.call & 0x1F) == 0) + SEGENV.step += *stepdir; // change density + for(uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->particles[i].hue -= globalhuestep; // shift global hue (both directions) + PartSys->particles[i].vx = 1 + (SEGMENT.speed >> 2) + ((int32_t(sin16_t(strip.now >> 1) + 32767) * (SEGMENT.speed >> 2)) >> 16); + } + } + + PartSys->setParticleSize(SEGMENT.custom1); // if custom1 == 0 this sets rendering size to one pixel + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@!,Density,Size,Hue,Blur,Playful,,Position Color;,!;!;1;pal=11,sx=50,c2=5,c3=0"; + +/* + Particle Fireworks Starburst replacement (smoother rendering, more settings) + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleStarburst(void) { + ParticleSystem1D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 200, 0, true)) // init + return mode_static(); // allocation failed or is single pixel + PartSys->setKillOutOfBounds(true); + PartSys->enableParticleCollisions(true, 200); + PartSys->sources[0].source.ttl = 1; // set initial stanby time + PartSys->sources[0].sat = 0; // emitted particles start out white + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setGravity(SEGMENT.check1 * 8); // enable gravity + + if (PartSys->sources[0].source.ttl-- == 0) { // stanby time elapsed TODO: make it a timer? + uint32_t explosionsize = 4 + hw_random16(SEGMENT.intensity >> 2); + PartSys->sources[0].source.hue = hw_random16(); + PartSys->sources[0].var = 10 + (explosionsize << 1); + PartSys->sources[0].minLife = 250; + PartSys->sources[0].maxLife = 300; + PartSys->sources[0].source.x = hw_random(PartSys->maxX); //random explosion position + PartSys->sources[0].source.ttl = 10 + hw_random16(255 - SEGMENT.speed); + PartSys->sources[0].size = SEGMENT.custom1; // Fragment size + PartSys->setParticleSize(SEGMENT.custom1); // enable advanced size rendering + PartSys->sources[0].sourceFlags.collide = SEGMENT.check3; + for (uint32_t e = 0; e < explosionsize; e++) { // emit particles + if (SEGMENT.check2) + PartSys->sources[0].source.hue = hw_random16(); //random color for each particle + PartSys->sprayEmit(PartSys->sources[0]); //emit a particle + } + } + //shrink all particles + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->advPartProps[i].size) + PartSys->advPartProps[i].size--; + if (PartSys->advPartProps[i].sat < 251) + PartSys->advPartProps[i].sat += 1 + (SEGMENT.custom3 >> 2); //note: it should be >> 3, the >> 2 creates overflows resulting in blinking if custom3 > 27, which is a bonus feature + } + + if (SEGMENT.call % 5 == 0) { + PartSys->applyFriction(1); //slow down particles + } + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PS_STARBURST[] PROGMEM = "PS Starburst@Chance,Fragments,Size,Blur,Cooling,Gravity,Colorful,Push;,!;!;1;pal=52,sx=150,ix=150,c1=120,c2=0,c3=21"; + +/* + Particle based 1D GEQ effect, each frequency bin gets an emitter, distributed over the strip + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particle1DGEQ(void) { + ParticleSystem1D *PartSys = nullptr; + uint32_t numSources; + uint32_t i; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 16, 255, 0, true)) // init, no additional data needed + return mode_static(); // allocation failed or is single pixel + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + numSources = PartSys->numSources; + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + + uint32_t spacing = PartSys->maxX / numSources; + for (i = 0; i < numSources; i++) { + PartSys->sources[i].source.hue = i * 16; // hw_random16(); //TODO: make adjustable, maybe even colorcycle? + PartSys->sources[i].var = SEGMENT.speed >> 2; + PartSys->sources[i].minLife = 180 + (SEGMENT.intensity >> 1); + PartSys->sources[i].maxLife = 240 + SEGMENT.intensity; + PartSys->sources[i].sat = 255; + PartSys->sources[i].size = SEGMENT.custom1; + PartSys->sources[i].source.x = (spacing >> 1) + spacing * i; //distribute evenly + } + + for (i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl > 20) PartSys->particles[i].ttl -= 20; //ttl is linked to brightness, this allows to use higher brightness but still a short lifespan + else PartSys->particles[i].ttl = 0; + } + + um_data_t *um_data = getAudioData(); + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + + //map the bands into 16 positions on x axis, emit some particles according to frequency loudness + i = 0; + uint32_t bin = hw_random16(numSources); //current bin , start with random one to distribute available particles fairly + uint32_t threshold = 300 - SEGMENT.intensity; + + for (i = 0; i < numSources; i++) { + bin++; + bin = bin % numSources; + uint32_t emitparticle = 0; + // uint8_t emitspeed = ((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 10; // emit speed according to loudness of band (127 max!) + if (fftResult[bin] > threshold) { + emitparticle = 1; + } + else if (fftResult[bin] > 0) { // band has low volue + uint32_t restvolume = ((threshold - fftResult[bin]) >> 2) + 2; + if (hw_random() % restvolume == 0) { + emitparticle = 1; + } + } + + if (emitparticle) + PartSys->sprayEmit(PartSys->sources[bin]); + } + //TODO: add color control? + + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_1D_GEQ[] PROGMEM = "PS GEQ 1D@Speed,!,Size,Blur,,,,;,!;!;1f;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1"; + +/* + Particle based Fire effect + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleFire1D(void) { + ParticleSystem1D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 5)) // init + return mode_static(); // allocation failed or is single pixel + PartSys->setKillOutOfBounds(true); + PartSys->setParticleSize(1); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(128 + (SEGMENT.custom2 >> 1)); // enable motion blur + PartSys->setColorByAge(true); + uint32_t emitparticles = 1; + uint32_t j = hw_random16(); + for (uint i = 0; i < 3; i++) { // 3 base flames + if (PartSys->sources[i].source.ttl > 50) + PartSys->sources[i].source.ttl -= 10; // TODO: in 2D making the source fade out slow results in much smoother flames, need to check if it can be done the same + else + PartSys->sources[i].source.ttl = 100 + hw_random16(200); + } + for (uint i = 0; i < PartSys->numSources; i++) { + j = (j + 1) % PartSys->numSources; + PartSys->sources[j].source.x = 0; + PartSys->sources[j].var = 2 + (SEGMENT.speed >> 4); + // base flames + if (j > 2) { + PartSys->sources[j].minLife = 150 + SEGMENT.intensity + (j << 2); // TODO: in 2D, min life is maxlife/2 and that looks very nice + PartSys->sources[j].maxLife = 200 + SEGMENT.intensity + (j << 3); + PartSys->sources[j].v = (SEGMENT.speed >> (2 + (j << 1))); + if (emitparticles) { + emitparticles--; + PartSys->sprayEmit(PartSys->sources[j]); // emit a particle + } + } + else { + PartSys->sources[j].minLife = PartSys->sources[j].source.ttl + SEGMENT.intensity; + PartSys->sources[j].maxLife = PartSys->sources[j].minLife + 50; + PartSys->sources[j].v = SEGMENT.speed >> 2; + if (SEGENV.call & 0x01) // every second frame + PartSys->sprayEmit(PartSys->sources[j]); // emit a particle + } + } + + for (uint i = 0; i < PartSys->usedParticles; i++) { + PartSys->particles[i].x += PartSys->particles[i].ttl >> 7; // 'hot' particles are faster, apply some extra velocity + if (PartSys->particles[i].ttl > 3 + ((255 - SEGMENT.custom1) >> 1)) + PartSys->particles[i].ttl -= map(SEGMENT.custom1, 0, 255, 1, 3); // age faster + } + + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_FIRE1D[] PROGMEM = "PS Fire 1D@!,!,Cooling,Blur;,!;!;1;pal=35,sx=100,ix=50,c1=80,c2=100,c3=28,o1=1,o2=1"; + +/* + Particle based AR effect, swoop particles along the strip with selected frequency loudness + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particle1DsonicStream(void) { + ParticleSystem1D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 255, 0, true)) // init, no additional data needed + return mode_static(); // allocation failed or is single pixel + PartSys->setKillOutOfBounds(true); + PartSys->sources[0].source.x = 0; // at start + //PartSys->sources[1].source.x = PartSys->maxX; // at end + PartSys->sources[0].var = 0;//SEGMENT.custom1 >> 3; + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(20 + (SEGMENT.custom2 >> 1)); // anable motion blur + PartSys->setSmearBlur(200); // smooth out the edges + PartSys->sources[0].v = 5 + (SEGMENT.speed >> 2); + + // FFT processing + um_data_t *um_data = getAudioData(); + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + uint32_t loudness; + uint32_t baseBin = SEGMENT.custom3 >> 1; // 0 - 15 map(SEGMENT.custom3, 0, 31, 0, 14); + + loudness = fftResult[baseBin];// + fftResult[baseBin + 1]; + if (baseBin > 12) + loudness = loudness << 2; // double loudness for high frequencies (better detecion) + + uint32_t threshold = 140 - (SEGMENT.intensity >> 1); + if (SEGMENT.check2) { // enable low pass filter for dynamic threshold + SEGMENT.step = (SEGMENT.step * 31500 + loudness * (32768 - 31500)) >> 15; // low pass filter for simple beat detection: add average to base threshold + threshold = 20 + (threshold >> 1) + SEGMENT.step; // add average to threshold + } + + // color + uint32_t hueincrement = (SEGMENT.custom1 >> 3); // 0-31 + PartSys->sources[0].sat = SEGMENT.custom1 > 0 ? 255 : 0; // color slider at zero: set to white + PartSys->setColorByPosition(SEGMENT.custom1 == 255); + + // particle manipulation + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->sources[0].sourceFlags.perpetual == false) { // age faster if not perpetual + if (PartSys->particles[i].ttl > 2) { + PartSys->particles[i].ttl -= 2; //ttl is linked to brightness, this allows to use higher brightness but still a short lifespan + } + else PartSys->particles[i].ttl = 0; + } + if (SEGMENT.check1) { // modulate colors by mid frequencies + int mids = sqrt32_bw((int)fftResult[5] + (int)fftResult[6] + (int)fftResult[7] + (int)fftResult[8] + (int)fftResult[9] + (int)fftResult[10]); // average the mids, bin 5 is ~500Hz, bin 10 is ~2kHz (see audio_reactive.h) + PartSys->particles[i].hue += (mids * perlin8(PartSys->particles[i].x << 2, SEGMENT.step << 2)) >> 9; // color by perlin noise from mid frequencies + } + } + + if (loudness > threshold) { + SEGMENT.aux0 += hueincrement; // change color + PartSys->sources[0].minLife = 100 + (((unsigned)SEGMENT.intensity * loudness * loudness) >> 13); + PartSys->sources[0].maxLife = PartSys->sources[0].minLife; + PartSys->sources[0].source.hue = SEGMENT.aux0; + PartSys->sources[0].size = SEGMENT.speed; + if (PartSys->particles[SEGMENT.aux1].x > 3 * PS_P_RADIUS_1D || PartSys->particles[SEGMENT.aux1].ttl == 0) { // only emit if last particle is far enough away or dead + int partindex = PartSys->sprayEmit(PartSys->sources[0]); // emit a particle + if (partindex >= 0) SEGMENT.aux1 = partindex; // track last emitted particle + } + } + else loudness = 0; // required for push mode + + PartSys->update(); // update and render (needs to be done before manipulation for initial particle spacing to be right) + + if (SEGMENT.check3) { // push mode + PartSys->sources[0].sourceFlags.perpetual = true; // emitted particles dont age + PartSys->applyFriction(1); //slow down particles + int32_t movestep = (((int)SEGMENT.speed + 2) * loudness) >> 10; + if (movestep) { + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl) { + PartSys->particles[i].x += movestep; // push particles + PartSys->particles[i].vx = 10 + (SEGMENT.speed >> 4) ; // give particles some speed for smooth movement (friction will slow them down) + } + } + } + } + else { + PartSys->sources[0].sourceFlags.perpetual = false; // emitted particles age + // move all particles (again) to allow faster speeds + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].vx == 0) + PartSys->particles[i].vx = PartSys->sources[0].v; // move static particles (after disabling push mode) + PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i], nullptr, &PartSys->advPartProps[i]); + } + } + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_SONICSTREAM[] PROGMEM = "PS Sonic Stream@!,!,Color,Blur,Bin,Mod,Filter,Push;,!;!;1f;c3=0,o2=1"; + + +/* + Particle based AR effect, creates exploding particles on beats + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particle1DsonicBoom(void) { + ParticleSystem1D *PartSys = nullptr; + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 255, 0, true)) // init, no additional data needed + return mode_static(); // allocation failed or is single pixel + PartSys->setKillOutOfBounds(true); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(180 * SEGMENT.check3); + PartSys->setSmearBlur(64 * SEGMENT.check3); + PartSys->sources[0].var = map(SEGMENT.speed, 0, 255, 10, 127); + + // FFT processing + um_data_t *um_data = getAudioData(); + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + uint32_t loudness; + uint32_t baseBin = SEGMENT.custom3 >> 1; // 0 - 15 map(SEGMENT.custom3, 0, 31, 0, 14); + loudness = fftResult[baseBin];// + fftResult[baseBin + 1]; + + if (baseBin > 12) + loudness = loudness << 2; // double loudness for high frequencies (better detecion) + uint32_t threshold = 150 - (SEGMENT.intensity >> 1); + if (SEGMENT.check2) { // enable low pass filter for dynamic threshold + SEGMENT.step = (SEGMENT.step * 31500 + loudness * (32768 - 31500)) >> 15; // low pass filter for simple beat detection: add average to base threshold + threshold = 20 + (threshold >> 1) + SEGMENT.step; // add average to threshold + } + + // particle manipulation + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (SEGMENT.check1) { // modulate colors by mid frequencies + int mids = sqrt32_bw((int)fftResult[5] + (int)fftResult[6] + (int)fftResult[7] + (int)fftResult[8] + (int)fftResult[9] + (int)fftResult[10]); // average the mids, bin 5 is ~500Hz, bin 10 is ~2kHz (see audio_reactive.h) + PartSys->particles[i].hue += (mids * perlin8(PartSys->particles[i].x << 2, SEGMENT.step << 2)) >> 9; // color by perlin noise from mid frequencies + } + if (PartSys->particles[i].ttl > 16) { + PartSys->particles[i].ttl -= 16; //ttl is linked to brightness, this allows to use higher brightness but still a (very) short lifespan + } + } + + if (loudness > threshold) { + if (SEGMENT.aux1 == 0) { // edge detected, code only runs once per "beat" + // update position + if (SEGMENT.custom2 < 128) // fixed position + PartSys->sources[0].source.x = map(SEGMENT.custom2, 0, 127, 0, PartSys->maxX); + else if (SEGMENT.custom2 < 255) { // advances on each "beat" + int32_t step = PartSys->maxX / (((270 - SEGMENT.custom2) >> 3)); // step: 2 - 33 steps for full segment width + PartSys->sources[0].source.x = (PartSys->sources[0].source.x + step) % PartSys->maxX; + if (PartSys->sources[0].source.x < step) // align to be symmetrical by making the first position half a step from start + PartSys->sources[0].source.x = step >> 1; + } + else // position set to max, use random postion per beat + PartSys->sources[0].source.x = hw_random(PartSys->maxX); + + // update color + //PartSys->setColorByPosition(SEGMENT.custom1 == 255); // color slider at max: particle color by position + PartSys->sources[0].sat = SEGMENT.custom1 > 0 ? 255 : 0; // color slider at zero: set to white + if (SEGMENT.custom1 == 255) // emit color by position + SEGMENT.aux0 = map(PartSys->sources[0].source.x , 0, PartSys->maxX, 0, 255); + else if (SEGMENT.custom1 > 0) + SEGMENT.aux0 += (SEGMENT.custom1 >> 1); // change emit color per "beat" + } + SEGMENT.aux1 = 1; // track edge detection + + PartSys->sources[0].minLife = 200; + PartSys->sources[0].maxLife = PartSys->sources[0].minLife + (((unsigned)SEGMENT.intensity * loudness * loudness) >> 13); + PartSys->sources[0].source.hue = SEGMENT.aux0; + PartSys->sources[0].size = 1; //SEGMENT.speed>>3; + uint32_t explosionsize = 4 + (PartSys->maxXpixel >> 2); + explosionsize = hw_random16((explosionsize * loudness) >> 10); + for (uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles + PartSys->sprayEmit(PartSys->sources[0]); // emit a particle + } + } + else + SEGMENT.aux1 = 0; // reset edge detection + + PartSys->update(); // update and render (needs to be done before manipulation for initial particle spacing to be right) + return FRAMETIME; +} +static const char _data_FX_MODE_PS_SONICBOOM[] PROGMEM = "PS Sonic Boom@!,!,Color,Position,Bin,Mod,Filter,Blur;,!;!;1f;c2=63,c3=0,o2=1"; + +/* +Particles bound by springs +by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleSpringy(void) { + ParticleSystem1D *PartSys = nullptr; + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init + return mode_static(); // allocation failed or is single pixel + SEGENV.aux0 = SEGENV.aux1 = 0xFFFF; // invalidate settings + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(220 * SEGMENT.check1); // anable motion blur + PartSys->setSmearBlur(50); // smear a little + PartSys->setUsedParticles(map(SEGMENT.custom1, 0, 255, 30 >> SEGMENT.check2, 255 >> (SEGMENT.check2*2))); // depends on density and particle size + // PartSys->enableParticleCollisions(true, 140); // enable particle collisions, can not be set too hard or impulses will not strech the springs if soft. + int32_t springlength = PartSys->maxX / (PartSys->usedParticles); // spring length (spacing between particles) + int32_t springK = map(SEGMENT.speed, 0, 255, 5, 35); // spring constant (stiffness) + + uint32_t settingssum = SEGMENT.custom1 + SEGMENT.check2; + if (SEGENV.aux0 != settingssum) { // number of particles changed, update distribution + for (int32_t i = 0; i < (int32_t)PartSys->usedParticles; i++) { + PartSys->advPartProps[i].sat = 255; // full saturation + //PartSys->particleFlags[i].collide = true; // enable collision for particles + PartSys->particles[i].x = (i+1) * ((PartSys->maxX) / (PartSys->usedParticles)); // distribute + //PartSys->particles[i].vx = 0; //reset speed + PartSys->advPartProps[i].size = SEGMENT.check2 ? 190 : 2; // set size, small or big + } + SEGENV.aux0 = settingssum; + } + int dxlimit = (2 + ((255 - SEGMENT.speed) >> 5)) * springlength; // limit for spring length to avoid overstretching + + int springforce[PartSys->usedParticles]; // spring forces + memset(springforce, 0, PartSys->usedParticles * sizeof(int32_t)); // reset spring forces + + // calculate spring forces and limit particle positions + if (PartSys->particles[0].x < -springlength) + PartSys->particles[0].x = -springlength; // limit the spring length + else if (PartSys->particles[0].x > dxlimit) + PartSys->particles[0].x = dxlimit; // limit the spring length + springforce[0] += ((springlength >> 1) - (PartSys->particles[0].x)) * springK; // first particle anchors to x=0 + + for (uint32_t i = 1; i < PartSys->usedParticles; i++) { + // reorder particles if they are out of order to prevent chaos + if (PartSys->particles[i].x < PartSys->particles[i-1].x) + std::swap(PartSys->particles[i].x, PartSys->particles[i-1].x); // swap particle positions to maintain order + int dx = PartSys->particles[i].x - PartSys->particles[i-1].x; // distance, always positive + if (dx > dxlimit) { // limit the spring length + PartSys->particles[i].x = PartSys->particles[i-1].x + dxlimit; + dx = dxlimit; + } + int dxleft = (springlength - dx); // offset from spring resting position + springforce[i] += dxleft * springK; + springforce[i-1] -= dxleft * springK; + if (i == (PartSys->usedParticles - 1)) { + if (PartSys->particles[i].x >= PartSys->maxX + springlength) + PartSys->particles[i].x = PartSys->maxX + springlength; + int dxright = (springlength >> 1) - (PartSys->maxX - PartSys->particles[i].x); // last particle anchors to x=maxX + springforce[i] -= dxright * springK; + } + } + // apply spring forces to particles + bool dampenoscillations = (SEGMENT.call % (9 - (SEGMENT.speed >> 5))) == 0; // dampen oscillation if particles are slow, more damping on stiffer springs + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + springforce[i] = springforce[i] / 64; // scale spring force (cannot use shifts because of negative values) + int maxforce = 120; // limit spring force + springforce[i] = springforce[i] > maxforce ? maxforce : springforce[i] < -maxforce ? -maxforce : springforce[i]; // limit spring force + PartSys->applyForce(PartSys->particles[i], springforce[i], PartSys->advPartProps[i].forcecounter); + //dampen slow particles to avoid persisting oscillations on higher stiffness + if (dampenoscillations) { + if (abs(PartSys->particles[i].vx) < 3 && abs(springforce[i]) < (springK >> 2)) + PartSys->particles[i].vx = (PartSys->particles[i].vx * 254) / 256; // take out some energy + } + PartSys->particles[i].ttl = 300; // reset ttl, cannot use perpetual + } + + if (SEGMENT.call % ((65 - ((SEGMENT.intensity * (1 + (SEGMENT.speed>>3))) >> 7))) == 0) // more damping for higher stiffness + PartSys->applyFriction((SEGMENT.intensity >> 2)); + + // add a small resetting force so particles return to resting position even under high damping + for (uint32_t i = 1; i < PartSys->usedParticles - 1; i++) { + int restposition = (springlength >> 1) + i * springlength; // resting position + int dx = restposition - PartSys->particles[i].x; // distance, always positive + PartSys->applyForce(PartSys->particles[i], dx > 0 ? 1 : (dx < 0 ? -1 : 0), PartSys->advPartProps[i].forcecounter); + } + + // Modes + if (SEGMENT.check3) { // use AR, custom 3 becomes frequency band to use, applies velocity to center particle according to loudness + um_data_t *um_data = getAudioData(); + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + uint32_t baseBin = map(SEGMENT.custom3, 0, 31, 0, 14); + uint32_t loudness = fftResult[baseBin] + fftResult[baseBin+1]; + uint32_t threshold = 80; //150 - (SEGMENT.intensity >> 1); + if (loudness > threshold) { + int offset = (PartSys->maxX >> 1) - PartSys->particles[PartSys->usedParticles>>1].x; // offset from center + if (abs(offset) < PartSys->maxX >> 5) // push particle around in center sector + PartSys->particles[PartSys->usedParticles>>1].vx = ((PartSys->particles[PartSys->usedParticles>>1].vx > 0 ? 1 : -1)) * (loudness >> 3); + } + } + else{ + if (SEGMENT.custom3 <= 10) { // periodic pulse: 0-5 apply at start, 6-10 apply at center + if (strip.now > SEGMENT.step) { + int speed = (SEGMENT.custom3 > 5) ? (SEGMENT.custom3 - 6) : SEGMENT.custom3; + SEGMENT.step = strip.now + 7500 - ((SEGMENT.speed << 3) + (speed << 10)); + int amplitude = 40 + (SEGMENT.custom1 >> 2); + int index = (SEGMENT.custom3 > 5) ? (PartSys->usedParticles / 2) : 0; // center or start particle + PartSys->particles[index].vx += amplitude; + } + } + else if (SEGMENT.custom3 <= 30) { // sinusoidal wave: 11-20 apply at start, 21-30 apply at center + int index = (SEGMENT.custom3 > 20) ? (PartSys->usedParticles / 2) : 0; // center or start particle + int restposition = 0; + if (index > 0) restposition = PartSys->maxX >> 1; // center + //int amplitude = 5 + (SEGMENT.speed >> 3) + (SEGMENT.custom1 >> 2); // amplitude depends on density + int amplitude = 5 + (SEGMENT.custom1 >> 2); // amplitude depends on density + int speed = SEGMENT.custom3 - 10 - (index ? 10 : 0); // map 11-20 and 21-30 to 1-10 + int phase = strip.now * ((1 + (SEGMENT.speed >> 4)) * speed); + if (SEGMENT.check2) amplitude <<= 1; // double amplitude for XL particles + //PartSys->applyForce(PartSys->particles[index], (sin16_t(phase) * amplitude) >> 15, PartSys->advPartProps[index].forcecounter); // apply acceleration + PartSys->particles[index].x = restposition + ((sin16_t(phase) * amplitude) >> 12); // apply position + } + else { + if (hw_random16() < 656) { // ~1% chance to add a pulse + int amplitude = 60; + if (SEGMENT.check2) amplitude <<= 1; // double amplitude for XL particles + PartSys->particles[PartSys->usedParticles >> 1].vx += hw_random16(amplitude << 1) - amplitude; // apply acceleration + } + } + } + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (SEGMENT.custom2 == 255) { // map speed to hue + int speedclr = ((int8_t(abs(PartSys->particles[i].vx))) >> 2) << 4; // scale for greater color variation, dump small values to avoid flickering + //int speed = PartSys->particles[i].vx << 2; // +/- 512 + if (speedclr > 240) speedclr = 240; // limit color to non-wrapping part of palette + PartSys->particles[i].hue = speedclr; + } + else if (SEGMENT.custom2 > 0) + PartSys->particles[i].hue = i * (SEGMENT.custom2 >> 2); // gradient distribution + else { + // map hue to particle density + int deviation; + if (i == 0) // First particle: measure density based on distance to anchor point + deviation = springlength/2 - PartSys->particles[i].x; + else if (i == PartSys->usedParticles - 1) // Last particle: measure density based on distance to right boundary + deviation = springlength/2 - (PartSys->maxX - PartSys->particles[i].x); + else { + // Middle particles: average of compression/expansion from both sides + int leftDx = PartSys->particles[i].x - PartSys->particles[i-1].x; + int rightDx = PartSys->particles[i+1].x - PartSys->particles[i].x; + int avgDistance = (leftDx + rightDx) >> 1; + if (avgDistance < 0) avgDistance = 0; // avoid negative distances (not sure why this happens) + deviation = (springlength - avgDistance); + } + deviation = constrain(deviation, -127, 112); // limit deviation to -127..112 (do not go intwo wrapping part of palette) + PartSys->particles[i].hue = 127 + deviation; // map density to hue + } + } + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PS_SPRINGY[] PROGMEM = "PS Springy@Stiffness,Damping,Density,Hue,Mode,Smear,XL,AR;,!;!;1f;pal=54,c2=0,c3=23"; + +#endif // WLED_DISABLE_PARTICLESYSTEM1D ////////////////////////////////////////////////////////////////////////////////////////// // mode data @@ -7803,7 +10564,7 @@ uint8_t WS2812FX::addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name) _mode[id] = mode_fn; _modeData[id] = mode_name; return id; - } else if(_mode.size() < 255) { // 255 is reserved for indicating the effect wasn't added + } else if (_mode.size() < 255) { // 255 is reserved for indicating the effect wasn't added _mode.push_back(mode_fn); _modeData.push_back(mode_name); if (_modeCount < _mode.size()) _modeCount++; @@ -7843,7 +10604,6 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_TWINKLE, &mode_twinkle, _data_FX_MODE_TWINKLE); addEffect(FX_MODE_DISSOLVE, &mode_dissolve, _data_FX_MODE_DISSOLVE); addEffect(FX_MODE_DISSOLVE_RANDOM, &mode_dissolve_random, _data_FX_MODE_DISSOLVE_RANDOM); - addEffect(FX_MODE_SPARKLE, &mode_sparkle, _data_FX_MODE_SPARKLE); addEffect(FX_MODE_FLASH_SPARKLE, &mode_flash_sparkle, _data_FX_MODE_FLASH_SPARKLE); addEffect(FX_MODE_HYPER_SPARKLE, &mode_hyper_sparkle, _data_FX_MODE_HYPER_SPARKLE); addEffect(FX_MODE_STROBE, &mode_strobe, _data_FX_MODE_STROBE); @@ -7864,34 +10624,31 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_AURORA, &mode_aurora, _data_FX_MODE_AURORA); addEffect(FX_MODE_RUNNING_RANDOM, &mode_running_random, _data_FX_MODE_RUNNING_RANDOM); addEffect(FX_MODE_LARSON_SCANNER, &mode_larson_scanner, _data_FX_MODE_LARSON_SCANNER); - addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET); - addEffect(FX_MODE_FIREWORKS, &mode_fireworks, _data_FX_MODE_FIREWORKS); addEffect(FX_MODE_RAIN, &mode_rain, _data_FX_MODE_RAIN); + addEffect(FX_MODE_PRIDE_2015, &mode_pride_2015, _data_FX_MODE_PRIDE_2015); + addEffect(FX_MODE_COLORWAVES, &mode_colorwaves, _data_FX_MODE_COLORWAVES); + addEffect(FX_MODE_FIREWORKS, &mode_fireworks, _data_FX_MODE_FIREWORKS); addEffect(FX_MODE_TETRIX, &mode_tetrix, _data_FX_MODE_TETRIX); addEffect(FX_MODE_FIRE_FLICKER, &mode_fire_flicker, _data_FX_MODE_FIRE_FLICKER); addEffect(FX_MODE_GRADIENT, &mode_gradient, _data_FX_MODE_GRADIENT); addEffect(FX_MODE_LOADING, &mode_loading, _data_FX_MODE_LOADING); - addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); - addEffect(FX_MODE_FAIRY, &mode_fairy, _data_FX_MODE_FAIRY); addEffect(FX_MODE_TWO_DOTS, &mode_two_dots, _data_FX_MODE_TWO_DOTS); addEffect(FX_MODE_FAIRYTWINKLE, &mode_fairytwinkle, _data_FX_MODE_FAIRYTWINKLE); addEffect(FX_MODE_RUNNING_DUAL, &mode_running_dual, _data_FX_MODE_RUNNING_DUAL); - + #ifdef WLED_ENABLE_GIF + addEffect(FX_MODE_IMAGE, &mode_image, _data_FX_MODE_IMAGE); + #endif addEffect(FX_MODE_TRICOLOR_CHASE, &mode_tricolor_chase, _data_FX_MODE_TRICOLOR_CHASE); addEffect(FX_MODE_TRICOLOR_WIPE, &mode_tricolor_wipe, _data_FX_MODE_TRICOLOR_WIPE); addEffect(FX_MODE_TRICOLOR_FADE, &mode_tricolor_fade, _data_FX_MODE_TRICOLOR_FADE); addEffect(FX_MODE_LIGHTNING, &mode_lightning, _data_FX_MODE_LIGHTNING); addEffect(FX_MODE_ICU, &mode_icu, _data_FX_MODE_ICU); - addEffect(FX_MODE_MULTI_COMET, &mode_multi_comet, _data_FX_MODE_MULTI_COMET); addEffect(FX_MODE_DUAL_LARSON_SCANNER, &mode_dual_larson_scanner, _data_FX_MODE_DUAL_LARSON_SCANNER); addEffect(FX_MODE_RANDOM_CHASE, &mode_random_chase, _data_FX_MODE_RANDOM_CHASE); addEffect(FX_MODE_OSCILLATE, &mode_oscillate, _data_FX_MODE_OSCILLATE); - addEffect(FX_MODE_PRIDE_2015, &mode_pride_2015, _data_FX_MODE_PRIDE_2015); addEffect(FX_MODE_JUGGLE, &mode_juggle, _data_FX_MODE_JUGGLE); addEffect(FX_MODE_PALETTE, &mode_palette, _data_FX_MODE_PALETTE); - addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012); - addEffect(FX_MODE_COLORWAVES, &mode_colorwaves, _data_FX_MODE_COLORWAVES); addEffect(FX_MODE_BPM, &mode_bpm, _data_FX_MODE_BPM); addEffect(FX_MODE_FILLNOISE8, &mode_fillnoise8, _data_FX_MODE_FILLNOISE8); addEffect(FX_MODE_NOISE16_1, &mode_noise16_1, _data_FX_MODE_NOISE16_1); @@ -7901,7 +10658,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_COLORTWINKLE, &mode_colortwinkle, _data_FX_MODE_COLORTWINKLE); addEffect(FX_MODE_LAKE, &mode_lake, _data_FX_MODE_LAKE); addEffect(FX_MODE_METEOR, &mode_meteor, _data_FX_MODE_METEOR); - addEffect(FX_MODE_METEOR_SMOOTH, &mode_meteor_smooth, _data_FX_MODE_METEOR_SMOOTH); + //addEffect(FX_MODE_METEOR_SMOOTH, &mode_meteor_smooth, _data_FX_MODE_METEOR_SMOOTH); // merged with mode_meteor addEffect(FX_MODE_RAILWAY, &mode_railway, _data_FX_MODE_RAILWAY); addEffect(FX_MODE_RIPPLE, &mode_ripple, _data_FX_MODE_RIPPLE); addEffect(FX_MODE_TWINKLEFOX, &mode_twinklefox, _data_FX_MODE_TWINKLEFOX); @@ -7911,23 +10668,31 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_TRI_STATIC_PATTERN, &mode_tri_static_pattern, _data_FX_MODE_TRI_STATIC_PATTERN); addEffect(FX_MODE_SPOTS, &mode_spots, _data_FX_MODE_SPOTS); addEffect(FX_MODE_SPOTS_FADE, &mode_spots_fade, _data_FX_MODE_SPOTS_FADE); + addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET); + #ifdef WLED_PS_DONT_REPLACE_FX + addEffect(FX_MODE_MULTI_COMET, &mode_multi_comet, _data_FX_MODE_MULTI_COMET); + addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); + addEffect(FX_MODE_SPARKLE, &mode_sparkle, _data_FX_MODE_SPARKLE); addEffect(FX_MODE_GLITTER, &mode_glitter, _data_FX_MODE_GLITTER); - addEffect(FX_MODE_CANDLE, &mode_candle, _data_FX_MODE_CANDLE); + addEffect(FX_MODE_SOLID_GLITTER, &mode_solid_glitter, _data_FX_MODE_SOLID_GLITTER); addEffect(FX_MODE_STARBURST, &mode_starburst, _data_FX_MODE_STARBURST); + addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); + addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012); addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS); + #endif + addEffect(FX_MODE_CANDLE, &mode_candle, _data_FX_MODE_CANDLE); addEffect(FX_MODE_BOUNCINGBALLS, &mode_bouncing_balls, _data_FX_MODE_BOUNCINGBALLS); + addEffect(FX_MODE_POPCORN, &mode_popcorn, _data_FX_MODE_POPCORN); + addEffect(FX_MODE_DRIP, &mode_drip, _data_FX_MODE_DRIP); addEffect(FX_MODE_SINELON, &mode_sinelon, _data_FX_MODE_SINELON); addEffect(FX_MODE_SINELON_DUAL, &mode_sinelon_dual, _data_FX_MODE_SINELON_DUAL); addEffect(FX_MODE_SINELON_RAINBOW, &mode_sinelon_rainbow, _data_FX_MODE_SINELON_RAINBOW); - addEffect(FX_MODE_POPCORN, &mode_popcorn, _data_FX_MODE_POPCORN); - addEffect(FX_MODE_DRIP, &mode_drip, _data_FX_MODE_DRIP); addEffect(FX_MODE_PLASMA, &mode_plasma, _data_FX_MODE_PLASMA); addEffect(FX_MODE_PERCENT, &mode_percent, _data_FX_MODE_PERCENT); addEffect(FX_MODE_RIPPLE_RAINBOW, &mode_ripple_rainbow, _data_FX_MODE_RIPPLE_RAINBOW); addEffect(FX_MODE_HEARTBEAT, &mode_heartbeat, _data_FX_MODE_HEARTBEAT); addEffect(FX_MODE_PACIFICA, &mode_pacifica, _data_FX_MODE_PACIFICA); addEffect(FX_MODE_CANDLE_MULTI, &mode_candle_multi, _data_FX_MODE_CANDLE_MULTI); - addEffect(FX_MODE_SOLID_GLITTER, &mode_solid_glitter, _data_FX_MODE_SOLID_GLITTER); addEffect(FX_MODE_SUNRISE, &mode_sunrise, _data_FX_MODE_SUNRISE); addEffect(FX_MODE_PHASED, &mode_phased, _data_FX_MODE_PHASED); addEffect(FX_MODE_TWINKLEUP, &mode_twinkleup, _data_FX_MODE_TWINKLEUP); @@ -7935,10 +10700,8 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_SINEWAVE, &mode_sinewave, _data_FX_MODE_SINEWAVE); addEffect(FX_MODE_PHASEDNOISE, &mode_phased_noise, _data_FX_MODE_PHASEDNOISE); addEffect(FX_MODE_FLOW, &mode_flow, _data_FX_MODE_FLOW); - addEffect(FX_MODE_CHUNCHUN, &mode_chunchun, _data_FX_MODE_CHUNCHUN); - addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); + addEffect(FX_MODE_CHUNCHUN, &mode_chunchun, _data_FX_MODE_CHUNCHUN); addEffect(FX_MODE_WASHING_MACHINE, &mode_washing_machine, _data_FX_MODE_WASHING_MACHINE); - addEffect(FX_MODE_BLENDS, &mode_blends, _data_FX_MODE_BLENDS); addEffect(FX_MODE_TV_SIMULATOR, &mode_tv_simulator, _data_FX_MODE_TV_SIMULATOR); addEffect(FX_MODE_DYNAMIC_SMOOTH, &mode_dynamic_smooth, _data_FX_MODE_DYNAMIC_SMOOTH); @@ -7955,27 +10718,20 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_NOISEMETER, &mode_noisemeter, _data_FX_MODE_NOISEMETER); addEffect(FX_MODE_FREQWAVE, &mode_freqwave, _data_FX_MODE_FREQWAVE); addEffect(FX_MODE_FREQMATRIX, &mode_freqmatrix, _data_FX_MODE_FREQMATRIX); - addEffect(FX_MODE_WATERFALL, &mode_waterfall, _data_FX_MODE_WATERFALL); addEffect(FX_MODE_FREQPIXELS, &mode_freqpixels, _data_FX_MODE_FREQPIXELS); - addEffect(FX_MODE_NOISEFIRE, &mode_noisefire, _data_FX_MODE_NOISEFIRE); addEffect(FX_MODE_PUDDLEPEAK, &mode_puddlepeak, _data_FX_MODE_PUDDLEPEAK); addEffect(FX_MODE_NOISEMOVE, &mode_noisemove, _data_FX_MODE_NOISEMOVE); - addEffect(FX_MODE_PERLINMOVE, &mode_perlinmove, _data_FX_MODE_PERLINMOVE); addEffect(FX_MODE_RIPPLEPEAK, &mode_ripplepeak, _data_FX_MODE_RIPPLEPEAK); - addEffect(FX_MODE_FREQMAP, &mode_freqmap, _data_FX_MODE_FREQMAP); addEffect(FX_MODE_GRAVCENTER, &mode_gravcenter, _data_FX_MODE_GRAVCENTER); addEffect(FX_MODE_GRAVCENTRIC, &mode_gravcentric, _data_FX_MODE_GRAVCENTRIC); addEffect(FX_MODE_GRAVFREQ, &mode_gravfreq, _data_FX_MODE_GRAVFREQ); addEffect(FX_MODE_DJLIGHT, &mode_DJLight, _data_FX_MODE_DJLIGHT); - addEffect(FX_MODE_BLURZ, &mode_blurz, _data_FX_MODE_BLURZ); - addEffect(FX_MODE_FLOWSTRIPE, &mode_FlowStripe, _data_FX_MODE_FLOWSTRIPE); - addEffect(FX_MODE_WAVESINS, &mode_wavesins, _data_FX_MODE_WAVESINS); addEffect(FX_MODE_ROCKTAVES, &mode_rocktaves, _data_FX_MODE_ROCKTAVES); @@ -7984,16 +10740,17 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DPLASMAROTOZOOM, &mode_2Dplasmarotozoom, _data_FX_MODE_2DPLASMAROTOZOOM); addEffect(FX_MODE_2DSPACESHIPS, &mode_2Dspaceships, _data_FX_MODE_2DSPACESHIPS); addEffect(FX_MODE_2DCRAZYBEES, &mode_2Dcrazybees, _data_FX_MODE_2DCRAZYBEES); + + #ifdef WLED_PS_DONT_REPLACE_FX addEffect(FX_MODE_2DGHOSTRIDER, &mode_2Dghostrider, _data_FX_MODE_2DGHOSTRIDER); addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS); + #endif + addEffect(FX_MODE_2DSCROLLTEXT, &mode_2Dscrollingtext, _data_FX_MODE_2DSCROLLTEXT); addEffect(FX_MODE_2DDRIFTROSE, &mode_2Ddriftrose, _data_FX_MODE_2DDRIFTROSE); addEffect(FX_MODE_2DDISTORTIONWAVES, &mode_2Ddistortionwaves, _data_FX_MODE_2DDISTORTIONWAVES); - addEffect(FX_MODE_2DGEQ, &mode_2DGEQ, _data_FX_MODE_2DGEQ); // audio - addEffect(FX_MODE_2DNOISE, &mode_2Dnoise, _data_FX_MODE_2DNOISE); - addEffect(FX_MODE_2DFIRENOISE, &mode_2Dfirenoise, _data_FX_MODE_2DFIRENOISE); addEffect(FX_MODE_2DSQUAREDSWIRL, &mode_2Dsquaredswirl, _data_FX_MODE_2DSQUAREDSWIRL); @@ -8002,15 +10759,12 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DMATRIX, &mode_2Dmatrix, _data_FX_MODE_2DMATRIX); addEffect(FX_MODE_2DMETABALLS, &mode_2Dmetaballs, _data_FX_MODE_2DMETABALLS); addEffect(FX_MODE_2DFUNKYPLANK, &mode_2DFunkyPlank, _data_FX_MODE_2DFUNKYPLANK); // audio - addEffect(FX_MODE_2DPULSER, &mode_2DPulser, _data_FX_MODE_2DPULSER); - addEffect(FX_MODE_2DDRIFT, &mode_2DDrift, _data_FX_MODE_2DDRIFT); addEffect(FX_MODE_2DWAVERLY, &mode_2DWaverly, _data_FX_MODE_2DWAVERLY); // audio addEffect(FX_MODE_2DSUNRADIATION, &mode_2DSunradiation, _data_FX_MODE_2DSUNRADIATION); addEffect(FX_MODE_2DCOLOREDBURSTS, &mode_2DColoredBursts, _data_FX_MODE_2DCOLOREDBURSTS); addEffect(FX_MODE_2DJULIA, &mode_2DJulia, _data_FX_MODE_2DJULIA); - addEffect(FX_MODE_2DGAMEOFLIFE, &mode_2Dgameoflife, _data_FX_MODE_2DGAMEOFLIFE); addEffect(FX_MODE_2DTARTAN, &mode_2Dtartan, _data_FX_MODE_2DTARTAN); addEffect(FX_MODE_2DPOLARLIGHTS, &mode_2DPolarLights, _data_FX_MODE_2DPOLARLIGHTS); @@ -8018,7 +10772,6 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DLISSAJOUS, &mode_2DLissajous, _data_FX_MODE_2DLISSAJOUS); addEffect(FX_MODE_2DFRIZZLES, &mode_2DFrizzles, _data_FX_MODE_2DFRIZZLES); addEffect(FX_MODE_2DPLASMABALL, &mode_2DPlasmaball, _data_FX_MODE_2DPLASMABALL); - addEffect(FX_MODE_2DHIPHOTIC, &mode_2DHiphotic, _data_FX_MODE_2DHIPHOTIC); addEffect(FX_MODE_2DSINDOTS, &mode_2DSindots, _data_FX_MODE_2DSINDOTS); addEffect(FX_MODE_2DDNASPIRAL, &mode_2DDNASpiral, _data_FX_MODE_2DDNASPIRAL); @@ -8026,8 +10779,44 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DSOAP, &mode_2Dsoap, _data_FX_MODE_2DSOAP); addEffect(FX_MODE_2DOCTOPUS, &mode_2Doctopus, _data_FX_MODE_2DOCTOPUS); addEffect(FX_MODE_2DWAVINGCELL, &mode_2Dwavingcell, _data_FX_MODE_2DWAVINGCELL); - addEffect(FX_MODE_2DAKEMI, &mode_2DAkemi, _data_FX_MODE_2DAKEMI); // audio + +#ifndef WLED_DISABLE_PARTICLESYSTEM2D + addEffect(FX_MODE_PARTICLEVOLCANO, &mode_particlevolcano, _data_FX_MODE_PARTICLEVOLCANO); + addEffect(FX_MODE_PARTICLEFIRE, &mode_particlefire, _data_FX_MODE_PARTICLEFIRE); + addEffect(FX_MODE_PARTICLEFIREWORKS, &mode_particlefireworks, _data_FX_MODE_PARTICLEFIREWORKS); + addEffect(FX_MODE_PARTICLEVORTEX, &mode_particlevortex, _data_FX_MODE_PARTICLEVORTEX); + addEffect(FX_MODE_PARTICLEPERLIN, &mode_particleperlin, _data_FX_MODE_PARTICLEPERLIN); + addEffect(FX_MODE_PARTICLEPIT, &mode_particlepit, _data_FX_MODE_PARTICLEPIT); + addEffect(FX_MODE_PARTICLEBOX, &mode_particlebox, _data_FX_MODE_PARTICLEBOX); + addEffect(FX_MODE_PARTICLEATTRACTOR, &mode_particleattractor, _data_FX_MODE_PARTICLEATTRACTOR); // 872 bytes + addEffect(FX_MODE_PARTICLEIMPACT, &mode_particleimpact, _data_FX_MODE_PARTICLEIMPACT); + addEffect(FX_MODE_PARTICLEWATERFALL, &mode_particlewaterfall, _data_FX_MODE_PARTICLEWATERFALL); + addEffect(FX_MODE_PARTICLESPRAY, &mode_particlespray, _data_FX_MODE_PARTICLESPRAY); + addEffect(FX_MODE_PARTICLESGEQ, &mode_particleGEQ, _data_FX_MODE_PARTICLEGEQ); + addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECIRCULARGEQ); + addEffect(FX_MODE_PARTICLEGHOSTRIDER, &mode_particleghostrider, _data_FX_MODE_PARTICLEGHOSTRIDER); + addEffect(FX_MODE_PARTICLEBLOBS, &mode_particleblobs, _data_FX_MODE_PARTICLEBLOBS); + addEffect(FX_MODE_PARTICLEGALAXY, &mode_particlegalaxy, _data_FX_MODE_PARTICLEGALAXY); +#endif // WLED_DISABLE_PARTICLESYSTEM2D #endif // WLED_DISABLE_2D +#ifndef WLED_DISABLE_PARTICLESYSTEM1D +addEffect(FX_MODE_PSDRIP, &mode_particleDrip, _data_FX_MODE_PARTICLEDRIP); +addEffect(FX_MODE_PSPINBALL, &mode_particlePinball, _data_FX_MODE_PSPINBALL); //potential replacement for: bouncing balls, rollingballs, popcorn +addEffect(FX_MODE_PSDANCINGSHADOWS, &mode_particleDancingShadows, _data_FX_MODE_PARTICLEDANCINGSHADOWS); +addEffect(FX_MODE_PSFIREWORKS1D, &mode_particleFireworks1D, _data_FX_MODE_PS_FIREWORKS1D); +addEffect(FX_MODE_PSSPARKLER, &mode_particleSparkler, _data_FX_MODE_PS_SPARKLER); +addEffect(FX_MODE_PSHOURGLASS, &mode_particleHourglass, _data_FX_MODE_PS_HOURGLASS); +addEffect(FX_MODE_PS1DSPRAY, &mode_particle1Dspray, _data_FX_MODE_PS_1DSPRAY); +addEffect(FX_MODE_PSBALANCE, &mode_particleBalance, _data_FX_MODE_PS_BALANCE); +addEffect(FX_MODE_PSCHASE, &mode_particleChase, _data_FX_MODE_PS_CHASE); +addEffect(FX_MODE_PSSTARBURST, &mode_particleStarburst, _data_FX_MODE_PS_STARBURST); +addEffect(FX_MODE_PS1DGEQ, &mode_particle1DGEQ, _data_FX_MODE_PS_1D_GEQ); +addEffect(FX_MODE_PSFIRE1D, &mode_particleFire1D, _data_FX_MODE_PS_FIRE1D); +addEffect(FX_MODE_PS1DSONICSTREAM, &mode_particle1DsonicStream, _data_FX_MODE_PS_SONICSTREAM); +addEffect(FX_MODE_PS1DSONICBOOM, &mode_particle1DsonicBoom, _data_FX_MODE_PS_SONICBOOM); +addEffect(FX_MODE_PS1DSPRINGY, &mode_particleSpringy, _data_FX_MODE_PS_SPRINGY); +#endif // WLED_DISABLE_PARTICLESYSTEM1D + } diff --git a/wled00/FX.h b/wled00/FX.h old mode 100644 new mode 100755 index 0ba33303..1b9bbaac --- a/wled00/FX.h +++ b/wled00/FX.h @@ -1,3 +1,4 @@ +#pragma once /* WS2812FX.h - Library for WS2812 LED effects. Harm Aldick - 2016 @@ -8,14 +9,34 @@ Adapted from code originally licensed under the MIT license Modified for WLED + + Segment class/struct (c) 2022 Blaz Kristan (@blazoncek) */ #ifndef WS2812FX_h #define WS2812FX_h #include +#include "wled.h" -#include "const.h" +#ifdef WLED_DEBUG + // enable additional debug output + #if defined(WLED_DEBUG_HOST) + #include "net_debug.h" + #define DEBUGOUT NetDebug + #else + #define DEBUGOUT Serial + #endif + #define DEBUGFX_PRINT(x) DEBUGOUT.print(x) + #define DEBUGFX_PRINTLN(x) DEBUGOUT.println(x) + #define DEBUGFX_PRINTF(x...) DEBUGOUT.printf(x) + #define DEBUGFX_PRINTF_P(x...) DEBUGOUT.printf_P(x) +#else + #define DEBUGFX_PRINT(x) + #define DEBUGFX_PRINTLN(x) + #define DEBUGFX_PRINTF(x...) + #define DEBUGFX_PRINTF_P(x...) +#endif #define FASTLED_INTERNAL //remove annoying pragma messages #define USE_GET_MILLISECOND_TIMER @@ -42,6 +63,9 @@ #define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b)))) #endif +extern bool realtimeRespectLedMaps; // used in getMappedPixelIndex() +extern byte realtimeMode; // used in getMappedPixelIndex() + /* Not used in all effects yet */ #define WLED_FPS 42 #define FRAMETIME_FIXED (1000/WLED_FPS) @@ -67,32 +91,31 @@ /* each segment uses 82 bytes of SRAM memory, so if you're application fails because of insufficient memory, decreasing MAX_NUM_SEGMENTS may help */ #ifdef ESP8266 - #define MAX_NUM_SEGMENTS 16 + #define MAX_NUM_SEGMENTS 16 /* How much data bytes all segments combined may allocate */ #define MAX_SEGMENT_DATA 5120 +#elif defined(CONFIG_IDF_TARGET_ESP32S2) + #define MAX_NUM_SEGMENTS 20 + #define MAX_SEGMENT_DATA (MAX_NUM_SEGMENTS*512) // 10k by default (S2 is short on free RAM) #else - #ifndef MAX_NUM_SEGMENTS - #define MAX_NUM_SEGMENTS 32 - #endif - #if defined(ARDUINO_ARCH_ESP32S2) - #define MAX_SEGMENT_DATA MAX_NUM_SEGMENTS*768 // 24k by default (S2 is short on free RAM) - #else - #define MAX_SEGMENT_DATA MAX_NUM_SEGMENTS*1280 // 40k by default - #endif + #define MAX_NUM_SEGMENTS 32 // warning: going beyond 32 may consume too much RAM for stable operation + #define MAX_SEGMENT_DATA (MAX_NUM_SEGMENTS*1280) // 40k by default #endif /* How much data bytes each segment should max allocate to leave enough space for other segments, assuming each segment uses the same amount of data. 256 for ESP8266, 640 for ESP32. */ -#define FAIR_DATA_PER_SEG (MAX_SEGMENT_DATA / strip.getMaxSegments()) +#define FAIR_DATA_PER_SEG (MAX_SEGMENT_DATA / WS2812FX::getMaxSegments()) + +#define MIN_SHOW_DELAY (_frametime < 16 ? 8 : 15) #define NUM_COLORS 3 /* number of colors per segment */ -#define SEGMENT strip._segments[strip.getCurrSegmentId()] -#define SEGENV strip._segments[strip.getCurrSegmentId()] -//#define SEGCOLOR(x) strip._segments[strip.getCurrSegmentId()].currentColor(x, strip._segments[strip.getCurrSegmentId()].colors[x]) -//#define SEGLEN strip._segments[strip.getCurrSegmentId()].virtualLength() -#define SEGCOLOR(x) strip.segColor(x) /* saves us a few kbytes of code */ +#define SEGMENT (*strip._currentSegment) +#define SEGENV (*strip._currentSegment) +#define SEGCOLOR(x) Segment::getCurrentColor(x) #define SEGPALETTE Segment::getCurrentPalette() -#define SEGLEN strip._virtualSegmentLength /* saves us a few kbytes of code */ +#define SEGLEN Segment::vLength() +#define SEG_W Segment::vWidth() +#define SEG_H Segment::vHeight() #define SPEED_FORMULA_L (5U + (50U*(255U - SEGMENT.speed))/SEGLEN) // some common colors @@ -138,10 +161,10 @@ #define FX_MODE_RAINBOW 8 #define FX_MODE_RAINBOW_CYCLE 9 #define FX_MODE_SCAN 10 -#define FX_MODE_DUAL_SCAN 11 +#define FX_MODE_DUAL_SCAN 11 // candidate for removal (use Scan) #define FX_MODE_FADE 12 #define FX_MODE_THEATER_CHASE 13 -#define FX_MODE_THEATER_CHASE_RAINBOW 14 +#define FX_MODE_THEATER_CHASE_RAINBOW 14 // candidate for removal (use Theater) #define FX_MODE_RUNNING_LIGHTS 15 #define FX_MODE_SAW 16 #define FX_MODE_TWINKLE 17 @@ -164,7 +187,7 @@ #define FX_MODE_COLORFUL 34 #define FX_MODE_TRAFFIC_LIGHT 35 #define FX_MODE_COLOR_SWEEP_RANDOM 36 -#define FX_MODE_RUNNING_COLOR 37 +#define FX_MODE_RUNNING_COLOR 37 // candidate for removal (use Theater) #define FX_MODE_AURORA 38 #define FX_MODE_RUNNING_RANDOM 39 #define FX_MODE_LARSON_SCANNER 40 @@ -179,8 +202,8 @@ #define FX_MODE_FAIRY 49 //was Police All prior to 0.13.0-b6 (use "Two Dots" with Red/Blue and full intensity) #define FX_MODE_TWO_DOTS 50 #define FX_MODE_FAIRYTWINKLE 51 //was Two Areas prior to 0.13.0-b6 (use "Two Dots" with full intensity) -#define FX_MODE_RUNNING_DUAL 52 -// #define FX_MODE_HALLOWEEN 53 // removed in 0.14! +#define FX_MODE_RUNNING_DUAL 52 // candidate for removal (use Running) +#define FX_MODE_IMAGE 53 #define FX_MODE_TRICOLOR_CHASE 54 #define FX_MODE_TRICOLOR_WIPE 55 #define FX_MODE_TRICOLOR_FADE 56 @@ -204,7 +227,7 @@ #define FX_MODE_COLORTWINKLE 74 #define FX_MODE_LAKE 75 #define FX_MODE_METEOR 76 -#define FX_MODE_METEOR_SMOOTH 77 +//#define FX_MODE_METEOR_SMOOTH 77 // replaced by Meteor #define FX_MODE_RAILWAY 78 #define FX_MODE_RIPPLE 79 #define FX_MODE_TWINKLEFOX 80 @@ -220,16 +243,16 @@ #define FX_MODE_EXPLODING_FIREWORKS 90 #define FX_MODE_BOUNCINGBALLS 91 #define FX_MODE_SINELON 92 -#define FX_MODE_SINELON_DUAL 93 -#define FX_MODE_SINELON_RAINBOW 94 +#define FX_MODE_SINELON_DUAL 93 // candidate for removal (use sinelon) +#define FX_MODE_SINELON_RAINBOW 94 // candidate for removal (use sinelon) #define FX_MODE_POPCORN 95 #define FX_MODE_DRIP 96 #define FX_MODE_PLASMA 97 #define FX_MODE_PERCENT 98 -#define FX_MODE_RIPPLE_RAINBOW 99 +#define FX_MODE_RIPPLE_RAINBOW 99 // candidate for removal (use ripple) #define FX_MODE_HEARTBEAT 100 #define FX_MODE_PACIFICA 101 -#define FX_MODE_CANDLE_MULTI 102 +#define FX_MODE_CANDLE_MULTI 102 // candidate for removal (use candle with multi select) #define FX_MODE_SOLID_GLITTER 103 // candidate for removal (use glitter) #define FX_MODE_SUNRISE 104 #define FX_MODE_PHASED 105 @@ -319,7 +342,68 @@ #define FX_MODE_ROCKTAVES 185 #define FX_MODE_2DAKEMI 186 -#define MODE_COUNT 187 +#define FX_MODE_PARTICLEVOLCANO 187 +#define FX_MODE_PARTICLEFIRE 188 +#define FX_MODE_PARTICLEFIREWORKS 189 +#define FX_MODE_PARTICLEVORTEX 190 +#define FX_MODE_PARTICLEPERLIN 191 +#define FX_MODE_PARTICLEPIT 192 +#define FX_MODE_PARTICLEBOX 193 +#define FX_MODE_PARTICLEATTRACTOR 194 +#define FX_MODE_PARTICLEIMPACT 195 +#define FX_MODE_PARTICLEWATERFALL 196 +#define FX_MODE_PARTICLESPRAY 197 +#define FX_MODE_PARTICLESGEQ 198 +#define FX_MODE_PARTICLECENTERGEQ 199 +#define FX_MODE_PARTICLEGHOSTRIDER 200 +#define FX_MODE_PARTICLEBLOBS 201 +#define FX_MODE_PSDRIP 202 +#define FX_MODE_PSPINBALL 203 +#define FX_MODE_PSDANCINGSHADOWS 204 +#define FX_MODE_PSFIREWORKS1D 205 +#define FX_MODE_PSSPARKLER 206 +#define FX_MODE_PSHOURGLASS 207 +#define FX_MODE_PS1DSPRAY 208 +#define FX_MODE_PSBALANCE 209 +#define FX_MODE_PSCHASE 210 +#define FX_MODE_PSSTARBURST 211 +#define FX_MODE_PS1DGEQ 212 +#define FX_MODE_PSFIRE1D 213 +#define FX_MODE_PS1DSONICSTREAM 214 +#define FX_MODE_PS1DSONICBOOM 215 +#define FX_MODE_PS1DSPRINGY 216 +#define FX_MODE_PARTICLEGALAXY 217 +#define MODE_COUNT 218 + + +#define BLEND_STYLE_FADE 0x00 // universal +#define BLEND_STYLE_FAIRY_DUST 0x01 // universal +#define BLEND_STYLE_SWIPE_RIGHT 0x02 // 1D or 2D +#define BLEND_STYLE_SWIPE_LEFT 0x03 // 1D or 2D +#define BLEND_STYLE_OUTSIDE_IN 0x04 // 1D or 2D +#define BLEND_STYLE_INSIDE_OUT 0x05 // 1D or 2D +#define BLEND_STYLE_SWIPE_UP 0x06 // 2D +#define BLEND_STYLE_SWIPE_DOWN 0x07 // 2D +#define BLEND_STYLE_OPEN_H 0x08 // 2D +#define BLEND_STYLE_OPEN_V 0x09 // 2D +#define BLEND_STYLE_SWIPE_TL 0x0A // 2D +#define BLEND_STYLE_SWIPE_TR 0x0B // 2D +#define BLEND_STYLE_SWIPE_BR 0x0C // 2D +#define BLEND_STYLE_SWIPE_BL 0x0D // 2D +#define BLEND_STYLE_CIRCULAR_OUT 0x0E // 2D +#define BLEND_STYLE_CIRCULAR_IN 0x0F // 2D +// as there are many push variants to optimise if statements they are groupped together +#define BLEND_STYLE_PUSH_RIGHT 0x10 // 1D or 2D (& 0b00010000) +#define BLEND_STYLE_PUSH_LEFT 0x11 // 1D or 2D (& 0b00010000) +#define BLEND_STYLE_PUSH_UP 0x12 // 2D (& 0b00010000) +#define BLEND_STYLE_PUSH_DOWN 0x13 // 2D (& 0b00010000) +#define BLEND_STYLE_PUSH_TL 0x14 // 2D (& 0b00010000) +#define BLEND_STYLE_PUSH_TR 0x15 // 2D (& 0b00010000) +#define BLEND_STYLE_PUSH_BR 0x16 // 2D (& 0b00010000) +#define BLEND_STYLE_PUSH_BL 0x17 // 2D (& 0b00010000) +#define BLEND_STYLE_PUSH_MASK 0x10 +#define BLEND_STYLE_COUNT 18 + typedef enum mapping1D2D { M12_Pixels = 0, @@ -329,172 +413,200 @@ typedef enum mapping1D2D { M12_sPinwheel = 4 } mapping1D2D_t; -// segment, 80 bytes -typedef struct Segment { +class WS2812FX; + +// segment, 76 bytes +class Segment { public: - uint16_t start; // start index / start X coordinate 2D (left) - uint16_t stop; // stop index / stop X coordinate 2D (right); segment is invalid if stop == 0 - uint16_t offset; - uint8_t speed; - uint8_t intensity; - uint8_t palette; - uint8_t mode; + uint32_t colors[NUM_COLORS]; + uint16_t start; // start index / start X coordinate 2D (left) + uint16_t stop; // stop index / stop X coordinate 2D (right); segment is invalid if stop == 0 + uint16_t startY; // start Y coodrinate 2D (top); there should be no more than 255 rows + uint16_t stopY; // stop Y coordinate 2D (bottom); there should be no more than 255 rows + uint16_t offset; // offset for 1D effects (effect will wrap around) union { - uint16_t options; //bit pattern: msb first: [transposed mirrorY reverseY] transitional (tbd) paused needspixelstate mirrored on reverse selected + mutable uint16_t options; //bit pattern: msb first: [transposed mirrorY reverseY] transitional (tbd) paused needspixelstate mirrored on reverse selected struct { - bool selected : 1; // 0 : selected - bool reverse : 1; // 1 : reversed - bool on : 1; // 2 : is On - bool mirror : 1; // 3 : mirrored - bool freeze : 1; // 4 : paused/frozen - bool reset : 1; // 5 : indicates that Segment runtime requires reset - bool reverse_y : 1; // 6 : reversed Y (2D) - bool mirror_y : 1; // 7 : mirrored Y (2D) - bool transpose : 1; // 8 : transposed (2D, swapped X & Y) - uint8_t map1D2D : 3; // 9-11 : mapping for 1D effect on 2D (0-use as strip, 1-expand vertically, 2-circular/arc, 3-rectangular/corner, ...) - uint8_t soundSim : 2; // 12-13 : 0-3 sound simulation types ("soft" & "hard" or "on"/"off") - uint8_t set : 2; // 14-15 : 0-3 UI segment sets/groups + mutable bool selected : 1; // 0 : selected + bool reverse : 1; // 1 : reversed + mutable bool on : 1; // 2 : is On + bool mirror : 1; // 3 : mirrored + mutable bool freeze : 1; // 4 : paused/frozen + mutable bool reset : 1; // 5 : indicates that Segment runtime requires reset + bool reverse_y : 1; // 6 : reversed Y (2D) + bool mirror_y : 1; // 7 : mirrored Y (2D) + bool transpose : 1; // 8 : transposed (2D, swapped X & Y) + uint8_t map1D2D : 3; // 9-11 : mapping for 1D effect on 2D (0-use as strip, 1-expand vertically, 2-circular/arc, 3-rectangular/corner, ...) + uint8_t soundSim : 2; // 12-13 : 0-3 sound simulation types ("soft" & "hard" or "on"/"off") + mutable uint8_t set : 2; // 14-15 : 0-3 UI segment sets/groups }; }; uint8_t grouping, spacing; - uint8_t opacity; - uint32_t colors[NUM_COLORS]; - uint8_t cct; //0==1900K, 255==10091K - uint8_t custom1, custom2; // custom FX parameters/sliders + uint8_t opacity, cct; // 0==1900K, 255==10091K + // effect data + uint8_t mode; + uint8_t palette; + uint8_t speed; + uint8_t intensity; + uint8_t custom1, custom2; // custom FX parameters/sliders struct { uint8_t custom3 : 5; // reduced range slider (0-31) bool check1 : 1; // checkmark 1 bool check2 : 1; // checkmark 2 bool check3 : 1; // checkmark 3 + //uint8_t blendMode : 4; // segment blending modes: top, bottom, add, subtract, difference, multiply, divide, lighten, darken, screen, overlay, hardlight, softlight, dodge, burn }; - uint8_t startY; // start Y coodrinate 2D (top); there should be no more than 255 rows - uint8_t stopY; // stop Y coordinate 2D (bottom); there should be no more than 255 rows - // note: two bytes of padding are added here - char *name; + uint8_t blendMode; // segment blending modes: top, bottom, add, subtract, difference, multiply, divide, lighten, darken, screen, overlay, hardlight, softlight, dodge, burn + char *name; // segment name // runtime data - unsigned long next_time; // millis() of next update - uint32_t step; // custom "step" var - uint32_t call; // call counter - uint16_t aux0; // custom var - uint16_t aux1; // custom var + mutable unsigned long next_time; // millis() of next update + mutable uint32_t step; // custom "step" var + mutable uint32_t call; // call counter + mutable uint16_t aux0; // custom var + mutable uint16_t aux1; // custom var byte *data; // effect data pointer + static uint16_t maxWidth, maxHeight; // these define matrix width & height (max. segment dimensions) - typedef struct TemporarySegmentData { - uint16_t _optionsT; - uint32_t _colorT[NUM_COLORS]; - uint8_t _speedT; - uint8_t _intensityT; - uint8_t _custom1T, _custom2T; // custom FX parameters/sliders - struct { - uint8_t _custom3T : 5; // reduced range slider (0-31) - bool _check1T : 1; // checkmark 1 - bool _check2T : 1; // checkmark 2 - bool _check3T : 1; // checkmark 3 - }; - uint16_t _aux0T; - uint16_t _aux1T; - uint32_t _stepT; - uint32_t _callT; - uint8_t *_dataT; - uint16_t _dataLenT; - TemporarySegmentData() - : _dataT(nullptr) // just in case... - , _dataLenT(0) - {} - } tmpsegd_t; - private: + uint32_t *pixels; // pixel data + unsigned _dataLen; + uint8_t _default_palette; // palette number that gets assigned to pal0 union { - uint8_t _capabilities; + mutable uint8_t _capabilities; // determines segment capabilities in terms of what is available: RGB, W, CCT, manual W, etc. struct { bool _isRGB : 1; bool _hasW : 1; bool _isCCT : 1; bool _manualW : 1; - uint8_t _reserved : 4; }; }; - uint16_t _dataLen; - static uint16_t _usedSegmentData; - // perhaps this should be per segment, not static + // static variables are use to speed up effect calculations by stashing common pre-calculated values + static unsigned _usedSegmentData; // amount of data used by all segments + static unsigned _vLength; // 1D dimension used for current effect + static unsigned _vWidth, _vHeight; // 2D dimensions used for current effect + static uint32_t _currentColors[NUM_COLORS]; // colors used for current effect (faster access from effect functions) static CRGBPalette16 _currentPalette; // palette used for current effect (includes transition, used in color_from_palette()) static CRGBPalette16 _randomPalette; // actual random palette static CRGBPalette16 _newRandomPalette; // target random palette - static uint16_t _lastPaletteChange; // last random palette change time in millis()/1000 - static uint16_t _lastPaletteBlend; // blend palette according to set Transition Delay in millis()%0xFFFF - static uint16_t _transitionprogress; // current transition progress 0 - 0xFFFF - #ifndef WLED_DISABLE_MODE_BLEND + static uint16_t _lastPaletteChange; // last random palette change time (in seconds) + static uint16_t _nextPaletteBlend; // next due time for random palette morph (in millis()) static bool _modeBlend; // mode/effect blending semaphore - #endif + // clipping rectangle used for blending + static uint16_t _clipStart, _clipStop; + static uint8_t _clipStartY, _clipStopY; - // transition data, valid only if transitional==true, holds values during transition (72 bytes) + // transition data, holds values during transition (76 bytes/28 bytes) struct Transition { - #ifndef WLED_DISABLE_MODE_BLEND - tmpsegd_t _segT; // previous segment environment - uint8_t _modeT; // previous mode/effect - #else - uint32_t _colorT[NUM_COLORS]; + Segment *_oldSegment; // previous segment environment (may be nullptr if effect did not change) + unsigned long _start; // must accommodate millis() + uint32_t _colors[NUM_COLORS]; // current colors + #ifndef WLED_SAVE_RAM + CRGBPalette16 _palT; // temporary palette (slowly being morphed from old to new) #endif - uint8_t _briT; // temporary brightness - uint8_t _cctT; // temporary CCT - CRGBPalette16 _palT; // temporary palette - uint8_t _prevPaletteBlends; // number of previous palette blends (there are max 255 blends possible) - unsigned long _start; // must accommodate millis() - uint16_t _dur; + uint16_t _dur; // duration of transition in ms + uint16_t _progress; // transition progress (0-65535); pre-calculated from _start & _dur in updateTransitionProgress() + uint8_t _prevPaletteBlends; // number of previous palette blends (there are max 255 blends possible) + uint8_t _palette, _bri, _cct; // palette ID, brightness and CCT at the start of transition (brightness will be 0 if segment was off) Transition(uint16_t dur=750) - : _palT(CRGBPalette16(CRGB::Black)) - , _prevPaletteBlends(0) - , _start(millis()) - , _dur(dur) + : _oldSegment(nullptr) + , _start(millis()) + , _colors{0,0,0} + #ifndef WLED_SAVE_RAM + , _palT(CRGBPalette16(CRGB::Black)) + #endif + , _dur(dur) + , _progress(0) + , _prevPaletteBlends(0) + , _palette(0) + , _bri(0) + , _cct(0) {} + ~Transition() { + //DEBUGFX_PRINTF_P(PSTR("-- Destroying transition: %p\n"), this); + if (_oldSegment) delete _oldSegment; + } } *_t; + protected: + + inline static unsigned getUsedSegmentData() { return Segment::_usedSegmentData; } + inline static void addUsedSegmentData(int len) { Segment::_usedSegmentData += len; } + + inline uint32_t *getPixels() const { return pixels; } + inline void setPixelColorRaw(unsigned i, uint32_t c) const { pixels[i] = c; } + inline uint32_t getPixelColorRaw(unsigned i) const { return pixels[i]; }; + #ifndef WLED_DISABLE_2D + inline void setPixelColorXYRaw(unsigned x, unsigned y, uint32_t c) const { auto XY = [](unsigned X, unsigned Y){ return X + Y*Segment::vWidth(); }; pixels[XY(x,y)] = c; } + inline uint32_t getPixelColorXYRaw(unsigned x, unsigned y) const { auto XY = [](unsigned X, unsigned Y){ return X + Y*Segment::vWidth(); }; return pixels[XY(x,y)]; }; + #endif + void resetIfRequired(); // sets all SEGENV variables to 0 and clears data buffer + CRGBPalette16 &loadPalette(CRGBPalette16 &tgt, uint8_t pal); + + // transition functions + void stopTransition(); // ends transition mode by destroying transition structure (does nothing if not in transition) + void updateTransitionProgress() const; // sets transition progress (0-65535) based on time passed since transition start + inline void handleTransition() { + updateTransitionProgress(); + if (isInTransition() && progress() == 0xFFFFU) stopTransition(); + } + inline uint16_t progress() const { return isInTransition() ? _t->_progress : 0xFFFFU; } // relies on handleTransition()/updateTransitionProgress() to update progression variable + inline Segment *getOldSegment() const { return isInTransition() ? _t->_oldSegment : nullptr; } + + inline static void modeBlend(bool blend) { Segment::_modeBlend = blend; } + inline static void setClippingRect(int startX, int stopX, int startY = 0, int stopY = 1) { _clipStart = startX; _clipStop = stopX; _clipStartY = startY; _clipStopY = stopY; }; + inline static bool isPreviousMode() { return Segment::_modeBlend; } // needed for determining CCT/opacity during non-BLEND_STYLE_FADE transition + + static void handleRandomPalette(); + public: - Segment(uint16_t sStart=0, uint16_t sStop=30) : - start(sStart), - stop(sStop), - offset(0), - speed(DEFAULT_SPEED), - intensity(DEFAULT_INTENSITY), - palette(0), - mode(DEFAULT_MODE), - options(SELECTED | SEGMENT_ON), - grouping(1), - spacing(0), - opacity(255), - colors{DEFAULT_COLOR,BLACK,BLACK}, - cct(127), - custom1(DEFAULT_C1), - custom2(DEFAULT_C2), - custom3(DEFAULT_C3), - check1(false), - check2(false), - check3(false), - startY(0), - stopY(1), - name(nullptr), - next_time(0), - step(0), - call(0), - aux0(0), - aux1(0), - data(nullptr), - _capabilities(0), - _dataLen(0), - _t(nullptr) + Segment(uint16_t sStart=0, uint16_t sStop=30, uint16_t sStartY = 0, uint16_t sStopY = 1) + : colors{DEFAULT_COLOR,BLACK,BLACK} + , start(sStart) + , stop(sStop > sStart ? sStop : sStart+1) // minimum length is 1 + , startY(sStartY) + , stopY(sStopY > sStartY ? sStopY : sStartY+1) // minimum height is 1 + , offset(0) + , options(SELECTED | SEGMENT_ON) + , grouping(1) + , spacing(0) + , opacity(255) + , cct(127) + , mode(DEFAULT_MODE) + , palette(0) + , speed(DEFAULT_SPEED) + , intensity(DEFAULT_INTENSITY) + , custom1(DEFAULT_C1) + , custom2(DEFAULT_C2) + , custom3(DEFAULT_C3) + , check1(false) + , check2(false) + , check3(false) + , blendMode(0) + , name(nullptr) + , next_time(0) + , step(0) + , call(0) + , aux0(0) + , aux1(0) + , data(nullptr) + , _dataLen(0) + , _default_palette(6) + , _capabilities(0) + , _t(nullptr) { - #ifdef WLED_DEBUG - //Serial.printf("-- Creating segment: %p\n", this); - #endif - } - - Segment(uint16_t sStartX, uint16_t sStopX, uint16_t sStartY, uint16_t sStopY) : Segment(sStartX, sStopX) { - startY = sStartY; - stopY = sStopY; + DEBUGFX_PRINTF_P(PSTR("-- Creating segment: %p [%d,%d:%d,%d]\n"), this, (int)start, (int)stop, (int)startY, (int)stopY); + // allocate render buffer (always entire segment) + pixels = static_cast(d_calloc(sizeof(uint32_t), length())); // error handling is also done in isActive() + if (!pixels) { + DEBUGFX_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!")); + extern byte errorFlag; + errorFlag = ERR_NORAM_PX; + stop = 0; // mark segment as inactive/invalid + } } Segment(const Segment &orig); // copy constructor @@ -502,60 +614,63 @@ typedef struct Segment { ~Segment() { #ifdef WLED_DEBUG - //Serial.printf("-- Destroying segment: %p", this); - //if (name) Serial.printf(" %s (%p)", name, name); - //if (data) Serial.printf(" %d->(%p)", (int)_dataLen, data); - //Serial.println(); + DEBUGFX_PRINTF_P(PSTR("-- Destroying segment: %p [%d,%d:%d,%d]"), this, (int)start, (int)stop, (int)startY, (int)stopY); + if (name) DEBUGFX_PRINTF_P(PSTR(" %s (%p)"), name, name); + if (data) DEBUGFX_PRINTF_P(PSTR(" %u->(%p)"), _dataLen, data); + DEBUGFX_PRINTF_P(PSTR(" T[%p]"), _t); + DEBUGFX_PRINTLN(); #endif - if (name) { delete[] name; name = nullptr; } - stopTransition(); + clearName(); deallocateData(); + d_free(pixels); } Segment& operator= (const Segment &orig); // copy assignment Segment& operator= (Segment &&orig) noexcept; // move assignment #ifdef WLED_DEBUG - size_t getSize() const { return sizeof(Segment) + (data?_dataLen:0) + (name?strlen(name):0) + (_t?sizeof(Transition):0); } + size_t getSize() const { return sizeof(Segment) + (data?_dataLen:0) + (name?strlen(name):0) + (_t?sizeof(Transition):0) + (pixels?length()*sizeof(uint32_t):0); } #endif - inline bool getOption(uint8_t n) const { return ((options >> n) & 0x01); } - inline bool isSelected() const { return selected; } - inline bool isInTransition() const { return _t != nullptr; } - inline bool isActive() const { return stop > start; } - inline bool is2D() const { return (width()>1 && height()>1); } - inline bool hasRGB() const { return _isRGB; } - inline bool hasWhite() const { return _hasW; } - inline bool isCCT() const { return _isCCT; } - inline uint16_t width() const { return isActive() ? (stop - start) : 0; } // segment width in physical pixels (length if 1D) - inline uint16_t height() const { return stopY - startY; } // segment height (if 2D) in physical pixels (it *is* always >=1) - inline uint16_t length() const { return width() * height(); } // segment length (count) in physical pixels - inline uint16_t groupLength() const { return grouping + spacing; } + inline bool getOption(uint8_t n) const { return ((options >> n) & 0x01); } + inline bool isSelected() const { return selected; } + inline bool isInTransition() const { return _t != nullptr; } + inline bool isActive() const { return stop > start && pixels; } + inline bool hasRGB() const { return _isRGB; } + inline bool hasWhite() const { return _hasW; } + inline bool isCCT() const { return _isCCT; } + inline uint16_t width() const { return stop > start ? (stop - start) : 0; }// segment width in physical pixels (length if 1D) + inline uint16_t height() const { return stopY - startY; } // segment height (if 2D) in physical pixels (it *is* always >=1) + inline uint16_t length() const { return width() * height(); } // segment length (count) in physical pixels + inline uint16_t groupLength() const { return grouping + spacing; } inline uint8_t getLightCapabilities() const { return _capabilities; } + inline void deactivate() { setGeometry(0,0); } + inline Segment &clearName() { d_free(name); name = nullptr; return *this; } + inline Segment &setName(const String &name) { return setName(name.c_str()); } - inline static uint16_t getUsedSegmentData() { return _usedSegmentData; } - inline static void addUsedSegmentData(int len) { _usedSegmentData += len; } - #ifndef WLED_DISABLE_MODE_BLEND - inline static void modeBlend(bool blend) { _modeBlend = blend; } - #endif - static void handleRandomPalette(); + inline static unsigned vLength() { return Segment::_vLength; } + inline static unsigned vWidth() { return Segment::_vWidth; } + inline static unsigned vHeight() { return Segment::_vHeight; } + inline static uint32_t getCurrentColor(unsigned i) { return Segment::_currentColors[i= 0 && i < length()) setPixelColorRaw(i,col); } #ifdef WLED_USE_AA_PIXELS - void setPixelColor(float i, uint32_t c, bool aa = true); - inline void setPixelColor(float i, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0, bool aa = true) { setPixelColor(i, RGBW32(r,g,b,w), aa); } - inline void setPixelColor(float i, CRGB c, bool aa = true) { setPixelColor(i, RGBW32(c.r,c.g,c.b,0), aa); } + void setPixelColor(float i, uint32_t c, bool aa = true) const; + inline void setPixelColor(float i, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0, bool aa = true) const { setPixelColor(i, RGBW32(r,g,b,w), aa); } + inline void setPixelColor(float i, CRGB c, bool aa = true) const { setPixelColor(i, RGBW32(c.r,c.g,c.b,0), aa); } #endif + [[gnu::hot]] bool isPixelClipped(int i) const; [[gnu::hot]] uint32_t getPixelColor(int i) const; // 1D support functions (some implement 2D as well) - void blur(uint8_t, bool smear = false); - void fill(uint32_t c); - void fade_out(uint8_t r); - void fadeToBlackBy(uint8_t fadeBy); - inline void blendPixelColor(int n, uint32_t color, uint8_t blend) { setPixelColor(n, color_blend(getPixelColor(n), color, blend)); } - inline void blendPixelColor(int n, CRGB c, uint8_t blend) { blendPixelColor(n, RGBW32(c.r,c.g,c.b,0), blend); } - inline void addPixelColor(int n, uint32_t color, bool fast = false) { setPixelColor(n, color_add(getPixelColor(n), color, fast)); } - inline void addPixelColor(int n, byte r, byte g, byte b, byte w = 0, bool fast = false) { addPixelColor(n, RGBW32(r,g,b,w), fast); } - inline void addPixelColor(int n, CRGB c, bool fast = false) { addPixelColor(n, RGBW32(c.r,c.g,c.b,0), fast); } - inline void fadePixelColor(uint16_t n, uint8_t fade) { setPixelColor(n, color_fade(getPixelColor(n), fade, true)); } - [[gnu::hot]] uint32_t color_from_palette(uint16_t, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri = 255) const; + void blur(uint8_t, bool smear = false) const; + void clear() const { fill(BLACK); } // clear segment + void fill(uint32_t c) const; + void fade_out(uint8_t r) const; + void fadeToSecondaryBy(uint8_t fadeBy) const; + void fadeToBlackBy(uint8_t fadeBy) const; + inline void blendPixelColor(int n, uint32_t color, uint8_t blend) const { setPixelColor(n, color_blend(getPixelColor(n), color, blend)); } + inline void blendPixelColor(int n, CRGB c, uint8_t blend) const { blendPixelColor(n, RGBW32(c.r,c.g,c.b,0), blend); } + inline void addPixelColor(int n, uint32_t color, bool preserveCR = true) const { setPixelColor(n, color_add(getPixelColor(n), color, preserveCR)); } + inline void addPixelColor(int n, byte r, byte g, byte b, byte w = 0, bool preserveCR = true) const + { addPixelColor(n, RGBW32(r,g,b,w), preserveCR); } + inline void addPixelColor(int n, CRGB c, bool preserveCR = true) const { addPixelColor(n, RGBW32(c.r,c.g,c.b,0), preserveCR); } + inline void fadePixelColor(uint16_t n, uint8_t fade) const { setPixelColor(n, color_fade(getPixelColor(n), fade, true)); } + [[gnu::hot]] uint32_t color_from_palette(uint16_t, bool mapping, bool moving, uint8_t mcol, uint8_t pbri = 255) const; [[gnu::hot]] uint32_t color_wheel(uint8_t pos) const; - - // 2D Blur: shortcuts for bluring columns or rows only (50% faster than full 2D blur) - inline void blurCols(fract8 blur_amount, bool smear = false) { // blur all columns - const unsigned cols = virtualWidth(); - for (unsigned k = 0; k < cols; k++) blurCol(k, blur_amount, smear); - } - inline void blurRows(fract8 blur_amount, bool smear = false) { // blur all rows - const unsigned rows = virtualHeight(); - for ( unsigned i = 0; i < rows; i++) blurRow(i, blur_amount, smear); - } - // 2D matrix - [[gnu::hot]] unsigned virtualWidth() const; // segment width in virtual pixels (accounts for groupping and spacing) - [[gnu::hot]] unsigned virtualHeight() const; // segment height in virtual pixels (accounts for groupping and spacing) - inline unsigned nrOfVStrips() const { // returns number of virtual vertical strips in 2D matrix (used to expand 1D effects into 2D) + unsigned virtualWidth() const; // segment width in virtual pixels (accounts for groupping and spacing) + unsigned virtualHeight() const; // segment height in virtual pixels (accounts for groupping and spacing) + inline unsigned nrOfVStrips() const { // returns number of virtual vertical strips in 2D matrix (used to expand 1D effects into 2D) #ifndef WLED_DISABLE_2D return (is2D() && map1D2D == M12_pBar) ? virtualWidth() : 1; #else @@ -627,69 +724,70 @@ typedef struct Segment { #endif } #ifndef WLED_DISABLE_2D - [[gnu::hot]] uint16_t XY(int x, int y); // support function to get relative index within segment - [[gnu::hot]] void setPixelColorXY(int x, int y, uint32_t c); // set relative pixel within segment with color - inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) { setPixelColorXY(int(x), int(y), c); } - inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) { setPixelColorXY(x, y, RGBW32(r,g,b,w)); } - inline void setPixelColorXY(int x, int y, CRGB c) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); } - inline void setPixelColorXY(unsigned x, unsigned y, CRGB c) { setPixelColorXY(int(x), int(y), RGBW32(c.r,c.g,c.b,0)); } + inline bool is2D() const { return (width()>1 && height()>1); } + [[gnu::hot]] void setPixelColorXY(int x, int y, uint32_t c) const; // set relative pixel within segment with color + inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) const { setPixelColorXY(int(x), int(y), c); } + inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) const { setPixelColorXY(x, y, RGBW32(r,g,b,w)); } + inline void setPixelColorXY(int x, int y, CRGB c) const { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); } + inline void setPixelColorXY(unsigned x, unsigned y, CRGB c) const { setPixelColorXY(int(x), int(y), RGBW32(c.r,c.g,c.b,0)); } #ifdef WLED_USE_AA_PIXELS - void setPixelColorXY(float x, float y, uint32_t c, bool aa = true); - inline void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) { setPixelColorXY(x, y, RGBW32(r,g,b,w), aa); } - inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), aa); } + void setPixelColorXY(float x, float y, uint32_t c, bool aa = true) const; + inline void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) const { setPixelColorXY(x, y, RGBW32(r,g,b,w), aa); } + inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) const { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), aa); } #endif + [[gnu::hot]] bool isPixelXYClipped(int x, int y) const; [[gnu::hot]] uint32_t getPixelColorXY(int x, int y) const; // 2D support functions - inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t color, uint8_t blend) { setPixelColorXY(x, y, color_blend(getPixelColorXY(x,y), color, blend)); } - inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) { blendPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), blend); } - inline void addPixelColorXY(int x, int y, uint32_t color, bool fast = false) { setPixelColorXY(x, y, color_add(getPixelColorXY(x,y), color, fast)); } - inline void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool fast = false) { addPixelColorXY(x, y, RGBW32(r,g,b,w), fast); } - inline void addPixelColorXY(int x, int y, CRGB c, bool fast = false) { addPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), fast); } - inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) { setPixelColorXY(x, y, color_fade(getPixelColorXY(x,y), fade, true)); } - void box_blur(unsigned r = 1U, bool smear = false); // 2D box blur - void blur2D(uint8_t blur_amount, bool smear = false); - void blurRow(uint32_t row, fract8 blur_amount, bool smear = false); - void blurCol(uint32_t col, fract8 blur_amount, bool smear = false); - void moveX(int8_t delta, bool wrap = false); - void moveY(int8_t delta, bool wrap = false); - void move(uint8_t dir, uint8_t delta, bool wrap = false); - void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false); - inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) { drawCircle(cx, cy, radius, RGBW32(c.r,c.g,c.b,0), soft); } - void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false); - inline void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) { fillCircle(cx, cy, radius, RGBW32(c.r,c.g,c.b,0), soft); } - void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft = false); - inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c, bool soft = false) { drawLine(x0, y0, x1, y1, RGBW32(c.r,c.g,c.b,0), soft); } // automatic inline - void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t col2 = 0, int8_t rotate = 0); - inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c) { drawCharacter(chr, x, y, w, h, RGBW32(c.r,c.g,c.b,0)); } // automatic inline - inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2, int8_t rotate = 0) { drawCharacter(chr, x, y, w, h, RGBW32(c.r,c.g,c.b,0), RGBW32(c2.r,c2.g,c2.b,0), rotate); } // automatic inline - void wu_pixel(uint32_t x, uint32_t y, CRGB c); - inline void blur2d(fract8 blur_amount) { blur(blur_amount); } - inline void fill_solid(CRGB c) { fill(RGBW32(c.r,c.g,c.b,0)); } + inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t color, uint8_t blend) const { setPixelColorXY(x, y, color_blend(getPixelColorXY(x,y), color, blend)); } + inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) const { blendPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), blend); } + inline void addPixelColorXY(int x, int y, uint32_t color, bool preserveCR = true) const { setPixelColorXY(x, y, color_add(getPixelColorXY(x,y), color, preserveCR)); } + inline void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool preserveCR = true) + { addPixelColorXY(x, y, RGBW32(r,g,b,w), preserveCR); } + inline void addPixelColorXY(int x, int y, CRGB c, bool preserveCR = true) const { addPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), preserveCR); } + inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) const { setPixelColorXY(x, y, color_fade(getPixelColorXY(x,y), fade, true)); } + inline void blurCols(fract8 blur_amount, bool smear = false) const { blur2D(0, blur_amount, smear); } // blur all columns (50% faster than full 2D blur) + inline void blurRows(fract8 blur_amount, bool smear = false) const { blur2D(blur_amount, 0, smear); } // blur all rows (50% faster than full 2D blur) + //void box_blur(unsigned r = 1U, bool smear = false); // 2D box blur + void blur2D(uint8_t blur_x, uint8_t blur_y, bool smear = false) const; + void moveX(int delta, bool wrap = false) const; + void moveY(int delta, bool wrap = false) const; + void move(unsigned dir, unsigned delta, bool wrap = false) const; + void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) const; + void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) const; + void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft = false) const; + void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t col2 = 0, int8_t rotate = 0) const; + void wu_pixel(uint32_t x, uint32_t y, CRGB c) const; + inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) const { drawCircle(cx, cy, radius, RGBW32(c.r,c.g,c.b,0), soft); } + inline void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) const { fillCircle(cx, cy, radius, RGBW32(c.r,c.g,c.b,0), soft); } + inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c, bool soft = false) const { drawLine(x0, y0, x1, y1, RGBW32(c.r,c.g,c.b,0), soft); } // automatic inline + inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2 = CRGB::Black, int8_t rotate = 0) const { drawCharacter(chr, x, y, w, h, RGBW32(c.r,c.g,c.b,0), RGBW32(c2.r,c2.g,c2.b,0), rotate); } // automatic inline + inline void fill_solid(CRGB c) const { fill(RGBW32(c.r,c.g,c.b,0)); } #else - inline uint16_t XY(uint16_t x, uint16_t y) { return x; } - inline void setPixelColorXY(int x, int y, uint32_t c) { setPixelColor(x, c); } - inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) { setPixelColor(int(x), c); } - inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) { setPixelColor(x, RGBW32(r,g,b,w)); } - inline void setPixelColorXY(int x, int y, CRGB c) { setPixelColor(x, RGBW32(c.r,c.g,c.b,0)); } - inline void setPixelColorXY(unsigned x, unsigned y, CRGB c) { setPixelColor(int(x), RGBW32(c.r,c.g,c.b,0)); } + inline constexpr bool is2D() const { return false; } + inline void setPixelColorXY(int x, int y, uint32_t c) const { setPixelColor(x, c); } + inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) const { setPixelColor(int(x), c); } + inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) const { setPixelColor(x, RGBW32(r,g,b,w)); } + inline void setPixelColorXY(int x, int y, CRGB c) const { setPixelColor(x, RGBW32(c.r,c.g,c.b,0)); } + inline void setPixelColorXY(unsigned x, unsigned y, CRGB c) const { setPixelColor(int(x), RGBW32(c.r,c.g,c.b,0)); } #ifdef WLED_USE_AA_PIXELS - inline void setPixelColorXY(float x, float y, uint32_t c, bool aa = true) { setPixelColor(x, c, aa); } + inline void setPixelColorXY(float x, float y, uint32_t c, bool aa = true) const { setPixelColor(x, c, aa); } inline void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) { setPixelColor(x, RGBW32(r,g,b,w), aa); } - inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) { setPixelColor(x, RGBW32(c.r,c.g,c.b,0), aa); } + inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) const { setPixelColor(x, RGBW32(c.r,c.g,c.b,0), aa); } #endif - inline uint32_t getPixelColorXY(int x, int y) { return getPixelColor(x); } - inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t c, uint8_t blend) { blendPixelColor(x, c, blend); } - inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) { blendPixelColor(x, RGBW32(c.r,c.g,c.b,0), blend); } - inline void addPixelColorXY(int x, int y, uint32_t color, bool fast = false) { addPixelColor(x, color, fast); } - inline void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool fast = false) { addPixelColor(x, RGBW32(r,g,b,w), fast); } - inline void addPixelColorXY(int x, int y, CRGB c, bool fast = false) { addPixelColor(x, RGBW32(c.r,c.g,c.b,0), fast); } - inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) { fadePixelColor(x, fade); } - inline void box_blur(unsigned i, bool vertical, fract8 blur_amount) {} - inline void blur2D(uint8_t blur_amount, bool smear = false) {} - inline void blurRow(uint32_t row, fract8 blur_amount, bool smear = false) {} - inline void blurCol(uint32_t col, fract8 blur_amount, bool smear = false) {} - inline void moveX(int8_t delta, bool wrap = false) {} - inline void moveY(int8_t delta, bool wrap = false) {} + inline bool isPixelXYClipped(int x, int y) const { return isPixelClipped(x); } + inline uint32_t getPixelColorXY(int x, int y) const { return getPixelColor(x); } + inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t c, uint8_t blend) const { blendPixelColor(x, c, blend); } + inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) const { blendPixelColor(x, RGBW32(c.r,c.g,c.b,0), blend); } + inline void addPixelColorXY(int x, int y, uint32_t color, bool saturate = false) const { addPixelColor(x, color, saturate); } + inline void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool saturate = false) const { addPixelColor(x, RGBW32(r,g,b,w), saturate); } + inline void addPixelColorXY(int x, int y, CRGB c, bool saturate = false) const { addPixelColor(x, RGBW32(c.r,c.g,c.b,0), saturate); } + inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) const { fadePixelColor(x, fade); } + //inline void box_blur(unsigned i, bool vertical, fract8 blur_amount) {} + inline void blur2D(uint8_t blur_x, uint8_t blur_y, bool smear = false) {} + inline void blurCols(fract8 blur_amount, bool smear = false) { blur(blur_amount, smear); } // blur all columns (50% faster than full 2D blur) + inline void blurRows(fract8 blur_amount, bool smear = false) {} + inline void moveX(int delta, bool wrap = false) {} + inline void moveY(int delta, bool wrap = false) {} inline void move(uint8_t dir, uint8_t delta, bool wrap = false) {} inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) {} inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) {} @@ -698,15 +796,14 @@ typedef struct Segment { inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft = false) {} inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c, bool soft = false) {} inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t = 0, int8_t = 0) {} - inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB color) {} inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2, int8_t rotate = 0) {} inline void wu_pixel(uint32_t x, uint32_t y, CRGB c) {} #endif -} segment; -//static int segSize = sizeof(Segment); + friend class WS2812FX; +}; -// main "strip" class -class WS2812FX { // 96 bytes +// main "strip" class (104 bytes) +class WS2812FX { typedef uint16_t (*mode_ptr)(); // pointer to mode function typedef void (*show_callback)(); // pre show callback typedef struct ModeData { @@ -716,20 +813,13 @@ class WS2812FX { // 96 bytes ModeData(uint8_t id, uint16_t (*fcn)(void), const char *data) : _id(id), _fcn(fcn), _data(data) {} } mode_data_t; - static WS2812FX* instance; - public: WS2812FX() : - paletteFade(0), paletteBlend(0), - cctBlending(0), now(millis()), timebase(0), isMatrix(false), -#ifndef WLED_DISABLE_2D - panels(1), -#endif #ifdef WLED_AUTOSEGMENTS autoSegments(true), #else @@ -737,31 +827,28 @@ class WS2812FX { // 96 bytes #endif correctWB(false), cctFromRgb(false), - // semi-private (just obscured) used in effect functions through macros - _colors_t{0,0,0}, - _virtualSegmentLength(0), // true private variables + _pixels(nullptr), _suspend(false), - _length(DEFAULT_LED_COUNT), _brightness(DEFAULT_BRIGHTNESS), + _length(DEFAULT_LED_COUNT), _transitionDur(750), - _targetFps(WLED_FPS), _frametime(FRAMETIME_FIXED), - _cumulativeFps(50 << FPS_CALC_SHIFT), + _cumulativeFps(WLED_FPS << FPS_CALC_SHIFT), + _targetFps(WLED_FPS), _isServicing(false), _isOffRefreshRequired(false), _hasWhiteChannel(false), _triggered(false), + _segment_index(0), + _mainSegment(0), _modeCount(MODE_COUNT), _callback(nullptr), customMappingTable(nullptr), customMappingSize(0), _lastShow(0), - _lastServiceShow(0), - _segment_index(0), - _mainSegment(0) + _lastServiceShow(0) { - WS2812FX::instance = this; _mode.reserve(_modeCount); // allocate memory to prevent initial fragmentation (does not increase size()) _modeData.reserve(_modeCount); // allocate memory to prevent initial fragmentation (does not increase size()) if (_mode.capacity() <= 1 || _modeData.capacity() <= 1) _modeCount = 1; // memory allocation failed only show Solid @@ -769,124 +856,110 @@ class WS2812FX { // 96 bytes } ~WS2812FX() { - if (customMappingTable) delete[] customMappingTable; + d_free(_pixels); + d_free(customMappingTable); _mode.clear(); _modeData.clear(); _segments.clear(); #ifndef WLED_DISABLE_2D panel.clear(); #endif - customPalettes.clear(); } - static WS2812FX* getInstance() { return instance; } - void #ifdef WLED_DEBUG printSize(), // prints memory usage for strip components #endif finalizeInit(), // initialises strip components service(), // executes effect functions when due and calls strip.show() - setMode(uint8_t segid, uint8_t m), // sets effect/mode for given segment (high level API) - setColor(uint8_t slot, uint32_t c), // sets color (in slot) for given segment (high level API) setCCT(uint16_t k), // sets global CCT (either in relative 0-255 value or in K) setBrightness(uint8_t b, bool direct = false), // sets strip brightness setRange(uint16_t i, uint16_t i2, uint32_t col), // used for clock overlay purgeSegments(), // removes inactive segments from RAM (may incure penalty and memory fragmentation but reduces vector footprint) - setSegment(uint8_t n, uint16_t start, uint16_t stop, uint8_t grouping = 1, uint8_t spacing = 0, uint16_t offset = UINT16_MAX, uint16_t startY=0, uint16_t stopY=1), - setMainSegmentId(uint8_t n), + setMainSegmentId(unsigned n = 0), resetSegments(), // marks all segments for reset makeAutoSegments(bool forceReset = false), // will create segments based on configured outputs fixInvalidSegments(), // fixes incorrect segment configuration - setPixelColor(unsigned n, uint32_t c), // paints absolute strip pixel with index n and color c + blendSegment(const Segment &topSegment) const, // blends topSegment into pixels show(), // initiates LED output - setTargetFps(uint8_t fps), - setupEffectData(); // add default effects to the list; defined in FX.cpp + setTargetFps(unsigned fps), + setupEffectData(), // add default effects to the list; defined in FX.cpp + waitForIt(); // wait until frame is over (service() has finished or time for 1 frame has passed) - inline void resetTimebase() { timebase = 0UL - millis(); } - inline void restartRuntime() { for (Segment &seg : _segments) { seg.markForReset().resetIfRequired(); } } - inline void setTransitionMode(bool t) { for (Segment &seg : _segments) seg.startTransition(t ? _transitionDur : 0); } - inline void setColor(uint8_t slot, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) { setColor(slot, RGBW32(r,g,b,w)); } - inline void setPixelColor(unsigned n, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) { setPixelColor(n, RGBW32(r,g,b,w)); } - inline void setPixelColor(unsigned n, CRGB c) { setPixelColor(n, c.red, c.green, c.blue); } - inline void fill(uint32_t c) { for (unsigned i = 0; i < getLengthTotal(); i++) setPixelColor(i, c); } // fill whole strip with color (inline) + void setRealtimePixelColor(unsigned i, uint32_t c); + inline void setPixelColor(unsigned n, uint32_t c) const { if (n < getLengthTotal()) _pixels[n] = c; } // paints absolute strip pixel with index n and color c + inline void resetTimebase() { timebase = 0UL - millis(); } + inline void setPixelColor(unsigned n, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) const + { setPixelColor(n, RGBW32(r,g,b,w)); } + inline void setPixelColor(unsigned n, CRGB c) const { setPixelColor(n, c.red, c.green, c.blue); } + inline void fill(uint32_t c) const { for (size_t i = 0; i < getLengthTotal(); i++) setPixelColor(i, c); } // fill whole strip with color (inline) inline void trigger() { _triggered = true; } // Forces the next frame to be computed on all active segments. inline void setShowCallback(show_callback cb) { _callback = cb; } inline void setTransition(uint16_t t) { _transitionDur = t; } // sets transition time (in ms) - inline void appendSegment(const Segment &seg = Segment()) { if (_segments.size() < getMaxSegments()) _segments.push_back(seg); } + inline void appendSegment(uint16_t sStart=0, uint16_t sStop=30, uint16_t sStartY = 0, uint16_t sStopY = 1) + { if (_segments.size() < getMaxSegments()) _segments.emplace_back(sStart,sStop,sStartY,sStopY); } inline void suspend() { _suspend = true; } // will suspend (and canacel) strip.service() execution inline void resume() { _suspend = false; } // will resume strip.service() execution - bool - paletteFade, - checkSegmentAlignment(), - hasRGBWBus() const, - hasCCTBus() const, - isUpdating() const, // return true if the strip is being sent pixel updates - deserializeMap(uint8_t n=0); + void restartRuntime(); + void setTransitionMode(bool t); + bool checkSegmentAlignment() const; + bool hasRGBWBus() const; + bool hasCCTBus() const; + bool deserializeMap(unsigned n = 0); + + inline bool isUpdating() const { return !BusManager::canAllShow(); } // return true if the strip is being sent pixel updates inline bool isServicing() const { return _isServicing; } // returns true if strip.service() is executing inline bool hasWhiteChannel() const { return _hasWhiteChannel; } // returns true if strip contains separate white chanel inline bool isOffRefreshRequired() const { return _isOffRefreshRequired; } // returns true if strip requires regular updates (i.e. TM1814 chipset) inline bool isSuspended() const { return _suspend; } // returns true if strip.service() execution is suspended inline bool needsUpdate() const { return _triggered; } // returns true if strip received a trigger() request - uint8_t - paletteBlend, - cctBlending, - getActiveSegmentsNum() const, - getFirstSelectedSegId() const, - getLastActiveSegmentId() const, - getActiveSegsLightCapabilities(bool selectedOnly = false) const, - addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name); // add effect to the list; defined in FX.cpp; + uint8_t paletteBlend; + uint8_t getActiveSegmentsNum() const; + uint8_t getFirstSelectedSegId() const; + uint8_t getLastActiveSegmentId() const; + uint8_t getActiveSegsLightCapabilities(bool selectedOnly = false) const; + uint8_t addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name); // add effect to the list; defined in FX.cpp; inline uint8_t getBrightness() const { return _brightness; } // returns current strip brightness - inline uint8_t getMaxSegments() const { return MAX_NUM_SEGMENTS; } // returns maximum number of supported segments (fixed value) + inline static constexpr unsigned getMaxSegments() { return MAX_NUM_SEGMENTS; } // returns maximum number of supported segments (fixed value) inline uint8_t getSegmentsNum() const { return _segments.size(); } // returns currently present segments inline uint8_t getCurrSegmentId() const { return _segment_index; } // returns current segment index (only valid while strip.isServicing()) inline uint8_t getMainSegmentId() const { return _mainSegment; } // returns main segment index - inline uint8_t getPaletteCount() const { return 13 + GRADIENT_PALETTE_COUNT + customPalettes.size(); } inline uint8_t getTargetFps() const { return _targetFps; } // returns rough FPS value for las 2s interval inline uint8_t getModeCount() const { return _modeCount; } // returns number of registered modes/effects - uint16_t - getLengthPhysical() const, - getLengthTotal() const, // will include virtual/nonexistent pixels in matrix - getFps() const, - getMappedPixelIndex(uint16_t index) const; + uint16_t getLengthPhysical() const; + uint16_t getLengthTotal() const; // will include virtual/nonexistent pixels in matrix + inline uint16_t getFps() const { return (millis() - _lastShow > 2000) ? 0 : (FPS_MULTIPLIER * _cumulativeFps) >> FPS_CALC_SHIFT; } // Returns the refresh rate of the LED strip (_cumulativeFps is stored in fixed point) inline uint16_t getFrameTime() const { return _frametime; } // returns amount of time a frame should take (in ms) inline uint16_t getMinShowDelay() const { return MIN_FRAME_DELAY; } // returns minimum amount of time strip.service() can be delayed (constant) inline uint16_t getLength() const { return _length; } // returns actual amount of LEDs on a strip (2D matrix may have less LEDs than W*H) inline uint16_t getTransition() const { return _transitionDur; } // returns currently set transition time (in ms) + inline uint16_t getMappedPixelIndex(uint16_t index) const { // convert logical address to physical + if (index < customMappingSize && (realtimeMode == REALTIME_MODE_INACTIVE || realtimeRespectLedMaps)) index = customMappingTable[index]; + return index; + }; unsigned long now, timebase; - uint32_t getPixelColor(unsigned) const; + inline uint32_t getPixelColor(unsigned n) const { return (n < getLengthTotal()) ? _pixels[n] : 0; } // returns color of pixel n + inline uint32_t getLastShow() const { return _lastShow; } // returns millis() timestamp of last strip.show() call - inline uint32_t getLastShow() const { return _lastShow; } // returns millis() timestamp of last strip.show() call - inline uint32_t segColor(uint8_t i) const { return _colors_t[i]; } // returns currently valid color (for slot i) AKA SEGCOLOR(); may be blended between two colors while in transition + const char *getModeData(unsigned id = 0) const { return (id && id < _modeCount) ? _modeData[id] : PSTR("Solid"); } + inline const char **getModeDataSrc() { return &(_modeData[0]); } // vectors use arrays for underlying data - const char * - getModeData(uint8_t id = 0) const { return (id && id<_modeCount) ? _modeData[id] : PSTR("Solid"); } - - const char ** - getModeDataSrc() { return &(_modeData[0]); } // vectors use arrays for underlying data - - Segment& getSegment(uint8_t id); + Segment& getSegment(unsigned id); inline Segment& getFirstSelectedSeg() { return _segments[getFirstSelectedSegId()]; } // returns reference to first segment that is "selected" inline Segment& getMainSegment() { return _segments[getMainSegmentId()]; } // returns reference to main segment inline Segment* getSegments() { return &(_segments[0]); } // returns pointer to segment vector structure (warning: use carefully) // 2D support (panels) - bool - isMatrix; #ifndef WLED_DISABLE_2D - #define WLED_MAX_PANELS 18 - uint8_t - panels; - - typedef struct panel_t { + struct Panel { uint16_t xOffset; // x offset relative to the top left of matrix in LEDs uint16_t yOffset; // y offset relative to the top left of matrix in LEDs uint8_t width; // width of the panel @@ -900,55 +973,48 @@ class WS2812FX { // 96 bytes bool serpentine : 1; // is serpentine? }; }; - panel_t() - : xOffset(0) - , yOffset(0) - , width(8) - , height(8) - , options(0) + Panel() + : xOffset(0) + , yOffset(0) + , width(8) + , height(8) + , options(0) {} - } Panel; + }; std::vector panel; #endif void setUpMatrix(); // sets up automatic matrix ledmap from panel configuration - // outsmart the compiler :) by correctly overloading - inline void setPixelColorXY(int x, int y, uint32_t c) { setPixelColor((unsigned)(y * Segment::maxWidth + x), c); } - inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) { setPixelColorXY(x, y, RGBW32(r,g,b,w)); } - inline void setPixelColorXY(int x, int y, CRGB c) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); } - - inline uint32_t getPixelColorXY(int x, int y) const { return getPixelColor(isMatrix ? y * Segment::maxWidth + x : x); } + inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) const { setPixelColor(y * Segment::maxWidth + x, c); } + inline void setPixelColorXY(unsigned x, unsigned y, byte r, byte g, byte b, byte w = 0) const { setPixelColorXY(x, y, RGBW32(r,g,b,w)); } + inline void setPixelColorXY(unsigned x, unsigned y, CRGB c) const { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); } + inline uint32_t getPixelColorXY(unsigned x, unsigned y) const { return getPixelColor(y * Segment::maxWidth + x); } // end 2D support - void loadCustomPalettes(); // loads custom palettes from JSON - std::vector customPalettes; // TODO: move custom palettes out of WS2812FX class - + bool isMatrix; struct { bool autoSegments : 1; bool correctWB : 1; bool cctFromRgb : 1; }; - // using public variables to reduce code size increase due to inline function getSegment() (with bounds checking) - // and color transitions - uint32_t _colors_t[3]; // color used for effect (includes transition) - uint16_t _virtualSegmentLength; - - std::vector _segments; - friend class Segment; + Segment *_currentSegment; private: + uint32_t *_pixels; + std::vector _segments; + volatile bool _suspend; - uint16_t _length; uint8_t _brightness; + uint16_t _length; uint16_t _transitionDur; - uint8_t _targetFps; uint16_t _frametime; uint16_t _cumulativeFps; + uint8_t _targetFps; // will require only 1 byte struct { @@ -958,6 +1024,9 @@ class WS2812FX { // 96 bytes bool _triggered : 1; }; + uint8_t _segment_index; + uint8_t _mainSegment; + uint8_t _modeCount; std::vector _mode; // SRAM footprint: 4 bytes per element std::vector _modeData; // mode (effect) name and its slider control data array @@ -970,8 +1039,7 @@ class WS2812FX { // 96 bytes unsigned long _lastShow; unsigned long _lastServiceShow; - uint8_t _segment_index; - uint8_t _mainSegment; + friend class Segment; }; extern const char JSON_mode_names[]; diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index 7c1ae366..9a3c6fbe 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -8,7 +8,6 @@ Parts of the code adapted from WLED Sound Reactive */ #include "wled.h" -#include "FX.h" #include "palettes.h" // setUpMatrix() - constructs ledmap array from matrix of panels with WxH pixels @@ -26,8 +25,7 @@ void WS2812FX::setUpMatrix() { // calculate width dynamically because it may have gaps Segment::maxWidth = 1; Segment::maxHeight = 1; - for (size_t i = 0; i < panel.size(); i++) { - Panel &p = panel[i]; + for (const Panel &p : panel) { if (p.xOffset + p.width > Segment::maxWidth) { Segment::maxWidth = p.xOffset + p.width; } @@ -37,21 +35,24 @@ void WS2812FX::setUpMatrix() { } // safety check - if (Segment::maxWidth * Segment::maxHeight > MAX_LEDS || Segment::maxWidth <= 1 || Segment::maxHeight <= 1) { + if (Segment::maxWidth * Segment::maxHeight > MAX_LEDS || Segment::maxWidth > 255 || Segment::maxHeight > 255 || Segment::maxWidth <= 1 || Segment::maxHeight <= 1) { DEBUG_PRINTLN(F("2D Bounds error.")); isMatrix = false; Segment::maxWidth = _length; Segment::maxHeight = 1; - panels = 0; panel.clear(); // release memory allocated by panels + panel.shrink_to_fit(); // release memory if allocated resetSegments(); return; } + suspend(); + waitForIt(); + customMappingSize = 0; // prevent use of mapping if anything goes wrong - if (customMappingTable) delete[] customMappingTable; - customMappingTable = new uint16_t[getLengthTotal()]; + d_free(customMappingTable); + customMappingTable = static_cast(d_malloc(sizeof(uint16_t)*getLengthTotal())); // prefer to not use SPI RAM if (customMappingTable) { customMappingSize = getLengthTotal(); @@ -68,7 +69,7 @@ void WS2812FX::setUpMatrix() { // content of the file is just raw JSON array in the form of [val1,val2,val3,...] // there are no other "key":"value" pairs in it // allowed values are: -1 (missing pixel/no LED attached), 0 (inactive/unused pixel), 1 (active/used pixel) - char fileName[32]; strcpy_P(fileName, PSTR("/2d-gaps.json")); // reduce flash footprint + char fileName[32]; strcpy_P(fileName, PSTR("/2d-gaps.json")); bool isFile = WLED_FS.exists(fileName); size_t gapSize = 0; int8_t *gapTable = nullptr; @@ -85,7 +86,7 @@ void WS2812FX::setUpMatrix() { JsonArray map = pDoc->as(); gapSize = map.size(); if (!map.isNull() && gapSize >= matrixSize) { // not an empty map - gapTable = new int8_t[gapSize]; + gapTable = static_cast(p_malloc(gapSize)); if (gapTable) for (size_t i = 0; i < gapSize; i++) { gapTable[i] = constrain(map[i], -1, 1); } @@ -96,8 +97,7 @@ void WS2812FX::setUpMatrix() { } unsigned x, y, pix=0; //pixel - for (size_t pan = 0; pan < panel.size(); pan++) { - Panel &p = panel[pan]; + for (const Panel &p : panel) { unsigned h = p.vertical ? p.height : p.width; unsigned v = p.vertical ? p.width : p.height; for (size_t j = 0; j < v; j++){ @@ -113,7 +113,8 @@ void WS2812FX::setUpMatrix() { } // delete gap array as we no longer need it - if (gapTable) delete[] gapTable; + p_free(gapTable); + resume(); #ifdef WLED_DEBUG DEBUG_PRINT(F("Matrix ledmap:")); @@ -126,7 +127,6 @@ void WS2812FX::setUpMatrix() { } else { // memory allocation error DEBUG_PRINTLN(F("ERROR 2D LED map allocation error.")); isMatrix = false; - panels = 0; panel.clear(); Segment::maxWidth = _length; Segment::maxHeight = 1; @@ -144,76 +144,61 @@ void WS2812FX::setUpMatrix() { /////////////////////////////////////////////////////////// #ifndef WLED_DISABLE_2D - -// XY(x,y) - gets pixel index within current segment (often used to reference leds[] array element) -uint16_t IRAM_ATTR_YN Segment::XY(int x, int y) -{ - unsigned width = virtualWidth(); // segment width in logical pixels (can be 0 if segment is inactive) - unsigned height = virtualHeight(); // segment height in logical pixels (is always >= 1) - return isActive() ? (x%width) + (y%height) * width : 0; +// pixel is clipped if it falls outside clipping range +// if clipping start > stop the clipping range is inverted +bool IRAM_ATTR_YN Segment::isPixelXYClipped(int x, int y) const { + if (blendingStyle != BLEND_STYLE_FADE && isInTransition() && _clipStart != _clipStop) { + const bool invertX = _clipStart > _clipStop; + const bool invertY = _clipStartY > _clipStopY; + const int cStartX = invertX ? _clipStop : _clipStart; + const int cStopX = invertX ? _clipStart : _clipStop; + const int cStartY = invertY ? _clipStopY : _clipStartY; + const int cStopY = invertY ? _clipStartY : _clipStopY; + if (blendingStyle == BLEND_STYLE_FAIRY_DUST) { + const unsigned width = cStopX - cStartX; // assumes full segment width (faster than virtualWidth()) + const unsigned len = width * (cStopY - cStartY); // assumes full segment height (faster than virtualHeight()) + if (len < 2) return false; + const unsigned shuffled = hashInt(x + y * width) % len; + const unsigned pos = (shuffled * 0xFFFFU) / len; + return progress() <= pos; + } + if (blendingStyle == BLEND_STYLE_CIRCULAR_IN || blendingStyle == BLEND_STYLE_CIRCULAR_OUT) { + const int cx = (cStopX-cStartX+1) / 2; + const int cy = (cStopY-cStartY+1) / 2; + const bool out = (blendingStyle == BLEND_STYLE_CIRCULAR_OUT); + const unsigned prog = out ? progress() : 0xFFFFU - progress(); + int radius2 = max(cx, cy) * prog / 0xFFFF; + radius2 = 2 * radius2 * radius2; + if (radius2 == 0) return out; + const int dx = x - cx; + const int dy = y - cy; + const bool outside = dx * dx + dy * dy > radius2; + return out ? outside : !outside; + } + bool xInside = (x >= cStartX && x < cStopX); if (invertX) xInside = !xInside; + bool yInside = (y >= cStartY && y < cStopY); if (invertY) yInside = !yInside; + const bool clip = blendingStyle == BLEND_STYLE_OUTSIDE_IN ? xInside || yInside : xInside && yInside; + return !clip; + } + return false; } -void IRAM_ATTR_YN Segment::setPixelColorXY(int x, int y, uint32_t col) +void IRAM_ATTR_YN Segment::setPixelColorXY(int x, int y, uint32_t col) const { if (!isActive()) return; // not active - if ((unsigned)x >= virtualWidth() || (unsigned)y >= virtualHeight() || x<0 || y<0) return; // if pixel would fall out of virtual segment just exit - - uint8_t _bri_t = currentBri(); - if (_bri_t < 255) { - col = color_fade(col, _bri_t); - } - - if (reverse ) x = virtualWidth() - x - 1; - if (reverse_y) y = virtualHeight() - y - 1; - if (transpose) { std::swap(x,y); } // swap X & Y if segment transposed - - x *= groupLength(); // expand to physical pixels - y *= groupLength(); // expand to physical pixels - - int W = width(); - int H = height(); - if (x >= W || y >= H) return; // if pixel would fall out of segment just exit - - uint32_t tmpCol = col; - for (int j = 0; j < grouping; j++) { // groupping vertically - for (int g = 0; g < grouping; g++) { // groupping horizontally - int xX = (x+g), yY = (y+j); - if (xX >= W || yY >= H) continue; // we have reached one dimension's end - -#ifndef WLED_DISABLE_MODE_BLEND - // if blending modes, blend with underlying pixel - if (_modeBlend) tmpCol = color_blend(strip.getPixelColorXY(start + xX, startY + yY), col, 0xFFFFU - progress(), true); -#endif - - strip.setPixelColorXY(start + xX, startY + yY, tmpCol); - - if (mirror) { //set the corresponding horizontally mirrored pixel - if (transpose) strip.setPixelColorXY(start + xX, startY + height() - yY - 1, tmpCol); - else strip.setPixelColorXY(start + width() - xX - 1, startY + yY, tmpCol); - } - if (mirror_y) { //set the corresponding vertically mirrored pixel - if (transpose) strip.setPixelColorXY(start + width() - xX - 1, startY + yY, tmpCol); - else strip.setPixelColorXY(start + xX, startY + height() - yY - 1, tmpCol); - } - if (mirror_y && mirror) { //set the corresponding vertically AND horizontally mirrored pixel - strip.setPixelColorXY(start + width() - xX - 1, startY + height() - yY - 1, tmpCol); - } - } - } + if (x >= (int)vWidth() || y >= (int)vHeight() || x < 0 || y < 0) return; // if pixel would fall out of virtual segment just exit + setPixelColorXYRaw(x, y, col); } #ifdef WLED_USE_AA_PIXELS // anti-aliased version of setPixelColorXY() -void Segment::setPixelColorXY(float x, float y, uint32_t col, bool aa) +void Segment::setPixelColorXY(float x, float y, uint32_t col, bool aa) const { if (!isActive()) return; // not active if (x<0.0f || x>1.0f || y<0.0f || y>1.0f) return; // not normalized - const unsigned cols = virtualWidth(); - const unsigned rows = virtualHeight(); - - float fX = x * (cols-1); - float fY = y * (rows-1); + float fX = x * (vWidth()-1); + float fY = y * (vHeight()-1); if (aa) { unsigned xL = roundf(fX-0.49f); unsigned xR = roundf(fX+0.49f); @@ -251,138 +236,74 @@ void Segment::setPixelColorXY(float x, float y, uint32_t col, bool aa) // returns RGBW values of pixel uint32_t IRAM_ATTR_YN Segment::getPixelColorXY(int x, int y) const { if (!isActive()) return 0; // not active - if ((unsigned)x >= virtualWidth() || (unsigned)y >= virtualHeight() || x<0 || y<0) return 0; // if pixel would fall out of virtual segment just exit - if (reverse ) x = virtualWidth() - x - 1; - if (reverse_y) y = virtualHeight() - y - 1; - if (transpose) { std::swap(x,y); } // swap X & Y if segment transposed - x *= groupLength(); // expand to physical pixels - y *= groupLength(); // expand to physical pixels - if (x >= width() || y >= height()) return 0; - return strip.getPixelColorXY(start + x, startY + y); + if (x >= (int)vWidth() || y >= (int)vHeight() || x<0 || y<0) return 0; // if pixel would fall out of virtual segment just exit + return getPixelColorXYRaw(x,y); } -// blurRow: perform a blur on a row of a rectangular matrix -void Segment::blurRow(uint32_t row, fract8 blur_amount, bool smear){ - if (!isActive() || blur_amount == 0) return; // not active - const unsigned cols = virtualWidth(); - const unsigned rows = virtualHeight(); - - if (row >= rows) return; - // blur one row - uint8_t keep = smear ? 255 : 255 - blur_amount; - uint8_t seep = blur_amount >> 1; - uint32_t carryover = BLACK; - uint32_t lastnew; +// 2D blurring, can be asymmetrical +void Segment::blur2D(uint8_t blur_x, uint8_t blur_y, bool smear) const { + if (!isActive()) return; // not active + const unsigned cols = vWidth(); + const unsigned rows = vHeight(); + const auto XY = [&](unsigned x, unsigned y){ return x + y*cols; }; + uint32_t lastnew; // not necessary to initialize lastnew and last, as both will be initialized by the first loop iteration uint32_t last; - uint32_t curnew = BLACK; - for (unsigned x = 0; x < cols; x++) { - uint32_t cur = getPixelColorXY(x, row); - uint32_t part = color_fade(cur, seep); - curnew = color_fade(cur, keep); - if (x > 0) { - if (carryover) - curnew = color_add(curnew, carryover, true); - uint32_t prev = color_add(lastnew, part, true); - if (last != prev) // optimization: only set pixel if color has changed - setPixelColorXY(x - 1, row, prev); - } else // first pixel - setPixelColorXY(x, row, curnew); - lastnew = curnew; - last = cur; // save original value for comparison on next iteration - carryover = part; - } - setPixelColorXY(cols-1, row, curnew); // set last pixel -} - -// blurCol: perform a blur on a column of a rectangular matrix -void Segment::blurCol(uint32_t col, fract8 blur_amount, bool smear) { - if (!isActive() || blur_amount == 0) return; // not active - const unsigned cols = virtualWidth(); - const unsigned rows = virtualHeight(); - - if (col >= cols) return; - // blur one column - uint8_t keep = smear ? 255 : 255 - blur_amount; - uint8_t seep = blur_amount >> 1; - uint32_t carryover = BLACK; - uint32_t lastnew; - uint32_t last; - uint32_t curnew = BLACK; - for (unsigned y = 0; y < rows; y++) { - uint32_t cur = getPixelColorXY(col, y); - uint32_t part = color_fade(cur, seep); - curnew = color_fade(cur, keep); - if (y > 0) { - if (carryover) - curnew = color_add(curnew, carryover, true); - uint32_t prev = color_add(lastnew, part, true); - if (last != prev) // optimization: only set pixel if color has changed - setPixelColorXY(col, y - 1, prev); - } else // first pixel - setPixelColorXY(col, y, curnew); - lastnew = curnew; - last = cur; //save original value for comparison on next iteration - carryover = part; - } - setPixelColorXY(col, rows - 1, curnew); -} - -void Segment::blur2D(uint8_t blur_amount, bool smear) { - if (!isActive() || blur_amount == 0) return; // not active - const unsigned cols = virtualWidth(); - const unsigned rows = virtualHeight(); - - const uint8_t keep = smear ? 255 : 255 - blur_amount; - const uint8_t seep = blur_amount >> (1 + smear); - uint32_t lastnew; - uint32_t last; - for (unsigned row = 0; row < rows; row++) { - uint32_t carryover = BLACK; - uint32_t curnew = BLACK; - for (unsigned x = 0; x < cols; x++) { - uint32_t cur = getPixelColorXY(x, row); - uint32_t part = color_fade(cur, seep); - curnew = color_fade(cur, keep); - if (x > 0) { - if (carryover) curnew = color_add(curnew, carryover, true); - uint32_t prev = color_add(lastnew, part, true); - // optimization: only set pixel if color has changed - if (last != prev) setPixelColorXY(x - 1, row, prev); - } else setPixelColorXY(x, row, curnew); // first pixel - lastnew = curnew; - last = cur; // save original value for comparison on next iteration - carryover = part; + if (blur_x) { + const uint8_t keepx = smear ? 255 : 255 - blur_x; + const uint8_t seepx = blur_x >> 1; + for (unsigned row = 0; row < rows; row++) { // blur rows (x direction) + uint32_t carryover = BLACK; + uint32_t curnew = BLACK; + for (unsigned x = 0; x < cols; x++) { + uint32_t cur = getPixelColorRaw(XY(x, row)); + uint32_t part = color_fade(cur, seepx); + curnew = color_fade(cur, keepx); + if (x > 0) { + if (carryover) curnew = color_add(curnew, carryover); + uint32_t prev = color_add(lastnew, part); + // optimization: only set pixel if color has changed + if (last != prev) setPixelColorRaw(XY(x - 1, row), prev); + } else setPixelColorRaw(XY(x, row), curnew); // first pixel + lastnew = curnew; + last = cur; // save original value for comparison on next iteration + carryover = part; + } + setPixelColorRaw(XY(cols-1, row), curnew); // set last pixel } - setPixelColorXY(cols-1, row, curnew); // set last pixel } - for (unsigned col = 0; col < cols; col++) { - uint32_t carryover = BLACK; - uint32_t curnew = BLACK; - for (unsigned y = 0; y < rows; y++) { - uint32_t cur = getPixelColorXY(col, y); - uint32_t part = color_fade(cur, seep); - curnew = color_fade(cur, keep); - if (y > 0) { - if (carryover) curnew = color_add(curnew, carryover, true); - uint32_t prev = color_add(lastnew, part, true); - // optimization: only set pixel if color has changed - if (last != prev) setPixelColorXY(col, y - 1, prev); - } else setPixelColorXY(col, y, curnew); // first pixel - lastnew = curnew; - last = cur; //save original value for comparison on next iteration - carryover = part; + if (blur_y) { + const uint8_t keepy = smear ? 255 : 255 - blur_y; + const uint8_t seepy = blur_y >> 1; + for (unsigned col = 0; col < cols; col++) { + uint32_t carryover = BLACK; + uint32_t curnew = BLACK; + for (unsigned y = 0; y < rows; y++) { + uint32_t cur = getPixelColorRaw(XY(col, y)); + uint32_t part = color_fade(cur, seepy); + curnew = color_fade(cur, keepy); + if (y > 0) { + if (carryover) curnew = color_add(curnew, carryover); + uint32_t prev = color_add(lastnew, part); + // optimization: only set pixel if color has changed + if (last != prev) setPixelColorRaw(XY(col, y - 1), prev); + } else setPixelColorRaw(XY(col, y), curnew); // first pixel + lastnew = curnew; + last = cur; //save original value for comparison on next iteration + carryover = part; + } + setPixelColorRaw(XY(col, rows - 1), curnew); } - setPixelColorXY(col, rows - 1, curnew); } } +/* // 2D Box blur void Segment::box_blur(unsigned radius, bool smear) { if (!isActive() || radius == 0) return; // not active if (radius > 3) radius = 3; const unsigned d = (1 + 2*radius) * (1 + 2*radius); // averaging divisor - const unsigned cols = virtualWidth(); - const unsigned rows = virtualHeight(); + const unsigned cols = vWidth(); + const unsigned rows = vHeight(); uint16_t *tmpRSum = new uint16_t[cols*rows]; uint16_t *tmpGSum = new uint16_t[cols*rows]; uint16_t *tmpBSum = new uint16_t[cols*rows]; @@ -448,40 +369,58 @@ void Segment::box_blur(unsigned radius, bool smear) { delete[] tmpBSum; delete[] tmpWSum; } - -void Segment::moveX(int8_t delta, bool wrap) { - if (!isActive()) return; // not active - const int cols = virtualWidth(); - const int rows = virtualHeight(); - if (!delta || abs(delta) >= cols) return; - uint32_t newPxCol[cols]; - for (int y = 0; y < rows; y++) { - if (delta > 0) { - for (int x = 0; x < cols-delta; x++) newPxCol[x] = getPixelColorXY((x + delta), y); - for (int x = cols-delta; x < cols; x++) newPxCol[x] = getPixelColorXY(wrap ? (x + delta) - cols : x, y); - } else { - for (int x = cols-1; x >= -delta; x--) newPxCol[x] = getPixelColorXY((x + delta), y); - for (int x = -delta-1; x >= 0; x--) newPxCol[x] = getPixelColorXY(wrap ? (x + delta) + cols : x, y); +*/ +void Segment::moveX(int delta, bool wrap) const { + if (!isActive() || !delta) return; // not active + const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) + const int vH = vHeight(); // segment height in logical pixels (is always >= 1) + const auto XY = [&](unsigned x, unsigned y){ return x + y*vW; }; + int absDelta = abs(delta); + if (absDelta >= vW) return; + uint32_t newPxCol[vW]; + int newDelta; + int stop = vW; + int start = 0; + if (wrap) newDelta = (delta + vW) % vW; // +cols in case delta < 0 + else { + if (delta < 0) start = absDelta; + stop = vW - absDelta; + newDelta = delta > 0 ? delta : 0; + } + for (int y = 0; y < vH; y++) { + for (int x = 0; x < stop; x++) { + int srcX = x + newDelta; + if (wrap) srcX %= vW; // Wrap using modulo when `wrap` is true + newPxCol[x] = getPixelColorRaw(XY(srcX, y)); } - for (int x = 0; x < cols; x++) setPixelColorXY(x, y, newPxCol[x]); + for (int x = 0; x < stop; x++) setPixelColorRaw(XY(x + start, y), newPxCol[x]); } } -void Segment::moveY(int8_t delta, bool wrap) { - if (!isActive()) return; // not active - const int cols = virtualWidth(); - const int rows = virtualHeight(); - if (!delta || abs(delta) >= rows) return; - uint32_t newPxCol[rows]; - for (int x = 0; x < cols; x++) { - if (delta > 0) { - for (int y = 0; y < rows-delta; y++) newPxCol[y] = getPixelColorXY(x, (y + delta)); - for (int y = rows-delta; y < rows; y++) newPxCol[y] = getPixelColorXY(x, wrap ? (y + delta) - rows : y); - } else { - for (int y = rows-1; y >= -delta; y--) newPxCol[y] = getPixelColorXY(x, (y + delta)); - for (int y = -delta-1; y >= 0; y--) newPxCol[y] = getPixelColorXY(x, wrap ? (y + delta) + rows : y); +void Segment::moveY(int delta, bool wrap) const { + if (!isActive() || !delta) return; // not active + const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) + const int vH = vHeight(); // segment height in logical pixels (is always >= 1) + const auto XY = [&](unsigned x, unsigned y){ return x + y*vW; }; + int absDelta = abs(delta); + if (absDelta >= vH) return; + uint32_t newPxCol[vH]; + int newDelta; + int stop = vH; + int start = 0; + if (wrap) newDelta = (delta + vH) % vH; // +rows in case delta < 0 + else { + if (delta < 0) start = absDelta; + stop = vH - absDelta; + newDelta = delta > 0 ? delta : 0; + } + for (int x = 0; x < vW; x++) { + for (int y = 0; y < stop; y++) { + int srcY = y + newDelta; + if (wrap) srcY %= vH; // Wrap using modulo when `wrap` is true + newPxCol[y] = getPixelColorRaw(XY(x, srcY)); } - for (int y = 0; y < rows; y++) setPixelColorXY(x, y, newPxCol[y]); + for (int y = 0; y < stop; y++) setPixelColorRaw(XY(x, y + start), newPxCol[y]); } } @@ -489,7 +428,7 @@ void Segment::moveY(int8_t delta, bool wrap) { // @param dir direction: 0=left, 1=left-up, 2=up, 3=right-up, 4=right, 5=right-down, 6=down, 7=left-down // @param delta number of pixels to move // @param wrap around -void Segment::move(uint8_t dir, uint8_t delta, bool wrap) { +void Segment::move(unsigned dir, unsigned delta, bool wrap) const { if (delta==0) return; switch (dir) { case 0: moveX( delta, wrap); break; @@ -503,35 +442,37 @@ void Segment::move(uint8_t dir, uint8_t delta, bool wrap) { } } -void Segment::drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, bool soft) { +void Segment::drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, bool soft) const { if (!isActive() || radius == 0) return; // not active if (soft) { // Xiaolin Wu’s algorithm - int rsq = radius*radius; + const int rsq = radius*radius; int x = 0; int y = radius; unsigned oldFade = 0; while (x < y) { float yf = sqrtf(float(rsq - x*x)); // needs to be floating point - unsigned fade = float(0xFFFF) * (ceilf(yf) - yf); // how much color to keep + uint8_t fade = float(0xFF) * (ceilf(yf) - yf); // how much color to keep if (oldFade > fade) y--; oldFade = fade; - setPixelColorXY(cx+x, cy+y, color_blend(col, getPixelColorXY(cx+x, cy+y), fade, true)); - setPixelColorXY(cx-x, cy+y, color_blend(col, getPixelColorXY(cx-x, cy+y), fade, true)); - setPixelColorXY(cx+x, cy-y, color_blend(col, getPixelColorXY(cx+x, cy-y), fade, true)); - setPixelColorXY(cx-x, cy-y, color_blend(col, getPixelColorXY(cx-x, cy-y), fade, true)); - setPixelColorXY(cx+y, cy+x, color_blend(col, getPixelColorXY(cx+y, cy+x), fade, true)); - setPixelColorXY(cx-y, cy+x, color_blend(col, getPixelColorXY(cx-y, cy+x), fade, true)); - setPixelColorXY(cx+y, cy-x, color_blend(col, getPixelColorXY(cx+y, cy-x), fade, true)); - setPixelColorXY(cx-y, cy-x, color_blend(col, getPixelColorXY(cx-y, cy-x), fade, true)); - setPixelColorXY(cx+x, cy+y-1, color_blend(getPixelColorXY(cx+x, cy+y-1), col, fade, true)); - setPixelColorXY(cx-x, cy+y-1, color_blend(getPixelColorXY(cx-x, cy+y-1), col, fade, true)); - setPixelColorXY(cx+x, cy-y+1, color_blend(getPixelColorXY(cx+x, cy-y+1), col, fade, true)); - setPixelColorXY(cx-x, cy-y+1, color_blend(getPixelColorXY(cx-x, cy-y+1), col, fade, true)); - setPixelColorXY(cx+y-1, cy+x, color_blend(getPixelColorXY(cx+y-1, cy+x), col, fade, true)); - setPixelColorXY(cx-y+1, cy+x, color_blend(getPixelColorXY(cx-y+1, cy+x), col, fade, true)); - setPixelColorXY(cx+y-1, cy-x, color_blend(getPixelColorXY(cx+y-1, cy-x), col, fade, true)); - setPixelColorXY(cx-y+1, cy-x, color_blend(getPixelColorXY(cx-y+1, cy-x), col, fade, true)); + int px, py; + for (uint8_t i = 0; i < 16; i++) { + int swaps = (i & 0x4 ? 1 : 0); // 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1 + int adj = (i < 8) ? 0 : 1; // 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1 + int dx = (i & 1) ? -1 : 1; // 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1 + int dy = (i & 2) ? -1 : 1; // 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1 + if (swaps) { + px = cx + (y - adj) * dx; + py = cy + x * dy; + } else { + px = cx + x * dx; + py = cy + (y - adj) * dy; + } + uint32_t pixCol = getPixelColorXY(px, py); + setPixelColorXY(px, py, adj ? + color_blend(pixCol, col, fade) : + color_blend(col, pixCol, fade)); + } x++; } } else { @@ -539,14 +480,12 @@ void Segment::drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, int d = 3 - (2*radius); int y = radius, x = 0; while (y >= x) { - setPixelColorXY(cx+x, cy+y, col); - setPixelColorXY(cx-x, cy+y, col); - setPixelColorXY(cx+x, cy-y, col); - setPixelColorXY(cx-x, cy-y, col); - setPixelColorXY(cx+y, cy+x, col); - setPixelColorXY(cx-y, cy+x, col); - setPixelColorXY(cx+y, cy-x, col); - setPixelColorXY(cx-y, cy-x, col); + for (int i = 0; i < 4; i++) { + int dx = (i & 1) ? -x : x; + int dy = (i & 2) ? -y : y; + setPixelColorXY(cx + dx, cy + dy, col); + setPixelColorXY(cx + dy, cy + dx, col); + } x++; if (d > 0) { y--; @@ -559,29 +498,29 @@ void Segment::drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, } // by stepko, taken from https://editor.soulmatelights.com/gallery/573-blobs -void Segment::fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, bool soft) { +void Segment::fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, bool soft) const { if (!isActive() || radius == 0) return; // not active + const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) + const int vH = vHeight(); // segment height in logical pixels (is always >= 1) // draw soft bounding circle if (soft) drawCircle(cx, cy, radius, col, soft); // fill it - const int cols = virtualWidth(); - const int rows = virtualHeight(); for (int y = -radius; y <= radius; y++) { for (int x = -radius; x <= radius; x++) { if (x * x + y * y <= radius * radius && - int(cx)+x>=0 && int(cy)+y>=0 && - int(cx)+x= 0 && int(cy)+y >= 0 && + int(cx)+x < vW && int(cy)+y < vH) setPixelColorXY(cx + x, cy + y, col); } } } //line function -void Segment::drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft) { +void Segment::drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft) const { if (!isActive()) return; // not active - const int cols = virtualWidth(); - const int rows = virtualHeight(); - if (x0 >= cols || x1 >= cols || y0 >= rows || y1 >= rows) return; + const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) + const int vH = vHeight(); // segment height in logical pixels (is always >= 1) + if (x0 >= vW || x1 >= vW || y0 >= vH || y1 >= vH) return; const int dx = abs(x1-x0), sx = x0 126) return; // only ASCII 32-126 supported chr -= 32; // align with font table entries - const int cols = virtualWidth(); - const int rows = virtualHeight(); const int font = w*h; - CRGB col = CRGB(color); - CRGBPalette16 grad = CRGBPalette16(col, col2 ? CRGB(col2) : col); + // if col2 == BLACK then use currently selected palette for gradient otherwise create gradient from color and col2 + CRGBPalette16 grad = col2 ? CRGBPalette16(CRGB(color), CRGB(col2)) : SEGPALETTE; // selected palette as gradient - //if (w<5 || w>6 || h!=8) return; for (int i = 0; i= cols || y0 < 0 || y0 >= rows) continue; // drawing off-screen + if (x0 < 0 || x0 >= (int)vWidth() || y0 < 0 || y0 >= (int)vHeight()) continue; // drawing off-screen if (((bits>>(j+(8-w))) & 0x01)) { // bit set - setPixelColorXY(x0, y0, col); + setPixelColorXYRaw(x0, y0, c.color32); } } } } #define WU_WEIGHT(a,b) ((uint8_t) (((a)*(b)+(a)+(b))>>8)) -void Segment::wu_pixel(uint32_t x, uint32_t y, CRGB c) { //awesome wu_pixel procedure by reddit u/sutaburosu +void Segment::wu_pixel(uint32_t x, uint32_t y, CRGB c) const { //awesome wu_pixel procedure by reddit u/sutaburosu if (!isActive()) return; // not active // extract the fractional parts and derive their inverses unsigned xx = x & 0xff, yy = y & 0xff, ix = 255 - xx, iy = 255 - yy; diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp old mode 100644 new mode 100755 index 88fec3d8..7d515810 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -10,7 +10,7 @@ Modified heavily for WLED */ #include "wled.h" -#include "FX.h" +#include "FXparticleSystem.h" // TODO: better define the required function (mem service) in FX.h? #include "palettes.h" /* @@ -29,68 +29,54 @@ 19, 18, 17, 16, 15, 20, 21, 22, 23, 24, 29, 28, 27, 26, 25]} */ -#ifndef PIXEL_COUNTS - #define PIXEL_COUNTS DEFAULT_LED_COUNT -#endif - -#ifndef DATA_PINS - #define DATA_PINS DEFAULT_LED_PIN -#endif - -#ifndef LED_TYPES - #define LED_TYPES DEFAULT_LED_TYPE -#endif - -#ifndef DEFAULT_LED_COLOR_ORDER - #define DEFAULT_LED_COLOR_ORDER COL_ORDER_GRB //default to GRB -#endif - - #if MAX_NUM_SEGMENTS < WLED_MAX_BUSSES #error "Max segments must be at least max number of busses!" #endif -static constexpr unsigned sumPinsRequired(const unsigned* current, size_t count) { - return (count > 0) ? (Bus::getNumberOfPins(*current) + sumPinsRequired(current+1,count-1)) : 0; -} - -static constexpr bool validatePinsAndTypes(const unsigned* types, unsigned numTypes, unsigned numPins ) { - // Pins provided < pins required -> always invalid - // Pins provided = pins required -> always valid - // Pins provided > pins required -> valid if excess pins are a product of last type pins since it will be repeated - return (sumPinsRequired(types, numTypes) > numPins) ? false : - (numPins - sumPinsRequired(types, numTypes)) % Bus::getNumberOfPins(types[numTypes-1]) == 0; -} - /////////////////////////////////////////////////////////////////////////////// // Segment class implementation /////////////////////////////////////////////////////////////////////////////// -uint16_t Segment::_usedSegmentData = 0U; // amount of RAM all segments use for their data[] -uint16_t Segment::maxWidth = DEFAULT_LED_COUNT; -uint16_t Segment::maxHeight = 1; - +unsigned Segment::_usedSegmentData = 0U; // amount of RAM all segments use for their data[] +uint16_t Segment::maxWidth = DEFAULT_LED_COUNT; +uint16_t Segment::maxHeight = 1; +unsigned Segment::_vLength = 0; +unsigned Segment::_vWidth = 0; +unsigned Segment::_vHeight = 0; +uint32_t Segment::_currentColors[NUM_COLORS] = {0,0,0}; CRGBPalette16 Segment::_currentPalette = CRGBPalette16(CRGB::Black); CRGBPalette16 Segment::_randomPalette = generateRandomPalette(); // was CRGBPalette16(DEFAULT_COLOR); CRGBPalette16 Segment::_newRandomPalette = generateRandomPalette(); // was CRGBPalette16(DEFAULT_COLOR); -uint16_t Segment::_lastPaletteChange = 0; // perhaps it should be per segment -uint16_t Segment::_lastPaletteBlend = 0; //in millis (lowest 16 bits only) -uint16_t Segment::_transitionprogress = 0xFFFF; +uint16_t Segment::_lastPaletteChange = 0; // in seconds; perhaps it should be per segment +uint16_t Segment::_nextPaletteBlend = 0; // in millis -#ifndef WLED_DISABLE_MODE_BLEND -bool Segment::_modeBlend = false; -#endif +bool Segment::_modeBlend = false; +uint16_t Segment::_clipStart = 0; +uint16_t Segment::_clipStop = 0; +uint8_t Segment::_clipStartY = 0; +uint8_t Segment::_clipStopY = 1; // copy constructor Segment::Segment(const Segment &orig) { //DEBUG_PRINTF_P(PSTR("-- Copy segment constructor: %p -> %p\n"), &orig, this); memcpy((void*)this, (void*)&orig, sizeof(Segment)); - _t = nullptr; // copied segment cannot be in transition + _t = nullptr; // copied segment cannot be in transition name = nullptr; data = nullptr; _dataLen = 0; - if (orig.name) { name = new char[strlen(orig.name)+1]; if (name) strcpy(name, orig.name); } + pixels = nullptr; + if (!stop) return; // nothing to do if segment is inactive/invalid + if (orig.name) { name = static_cast(d_malloc(strlen(orig.name)+1)); if (name) strcpy(name, orig.name); } if (orig.data) { if (allocateData(orig._dataLen)) memcpy(data, orig.data, orig._dataLen); } + if (orig.pixels) { + pixels = static_cast(d_malloc(sizeof(uint32_t) * orig.length())); + if (pixels) memcpy(pixels, orig.pixels, sizeof(uint32_t) * orig.length()); + else { + DEBUG_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!")); + errorFlag = ERR_NORAM_PX; + stop = 0; // mark segment as inactive/invalid + } + } else stop = 0; // mark segment as inactive/invalid } // move constructor @@ -101,6 +87,7 @@ Segment::Segment(Segment &&orig) noexcept { orig.name = nullptr; orig.data = nullptr; orig._dataLen = 0; + orig.pixels = nullptr; } // copy assignment @@ -108,17 +95,29 @@ Segment& Segment::operator= (const Segment &orig) { //DEBUG_PRINTF_P(PSTR("-- Copying segment: %p -> %p\n"), &orig, this); if (this != &orig) { // clean destination - if (name) { delete[] name; name = nullptr; } - stopTransition(); + if (name) { d_free(name); name = nullptr; } + if (_t) stopTransition(); // also erases _t deallocateData(); + d_free(pixels); // copy source memcpy((void*)this, (void*)&orig, sizeof(Segment)); // erase pointers to allocated data data = nullptr; _dataLen = 0; + pixels = nullptr; + if (!stop) return *this; // nothing to do if segment is inactive/invalid // copy source data - if (orig.name) { name = new char[strlen(orig.name)+1]; if (name) strcpy(name, orig.name); } + if (orig.name) { name = static_cast(d_malloc(strlen(orig.name)+1)); if (name) strcpy(name, orig.name); } if (orig.data) { if (allocateData(orig._dataLen)) memcpy(data, orig.data, orig._dataLen); } + if (orig.pixels) { + pixels = static_cast(d_malloc(sizeof(uint32_t) * orig.length())); + if (pixels) memcpy(pixels, orig.pixels, sizeof(uint32_t) * orig.length()); + else { + DEBUG_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!")); + errorFlag = ERR_NORAM_PX; + stop = 0; // mark segment as inactive/invalid + } + } else stop = 0; // mark segment as inactive/invalid } return *this; } @@ -127,48 +126,60 @@ Segment& Segment::operator= (const Segment &orig) { Segment& Segment::operator= (Segment &&orig) noexcept { //DEBUG_PRINTF_P(PSTR("-- Moving segment: %p -> %p\n"), &orig, this); if (this != &orig) { - if (name) { delete[] name; name = nullptr; } // free old name - stopTransition(); + if (name) { d_free(name); name = nullptr; } // free old name + if (_t) stopTransition(); // also erases _t deallocateData(); // free old runtime data + d_free(pixels); // free old pixel buffer + // move source data memcpy((void*)this, (void*)&orig, sizeof(Segment)); orig.name = nullptr; orig.data = nullptr; orig._dataLen = 0; - orig._t = nullptr; // old segment cannot be in transition + orig.pixels = nullptr; + orig._t = nullptr; // old segment cannot be in transition } return *this; } // allocates effect data buffer on heap and initialises (erases) it -bool IRAM_ATTR_YN Segment::allocateData(size_t len) { +bool Segment::allocateData(size_t len) { if (len == 0) return false; // nothing to do if (data && _dataLen >= len) { // already allocated enough (reduce fragmentation) - if (call == 0) memset(data, 0, len); // erase buffer if called during effect initialisation + if (call == 0) { + //DEBUG_PRINTF_P(PSTR("-- Clearing data (%d): %p\n"), len, this); + memset(data, 0, len); // erase buffer if called during effect initialisation + } return true; } - //DEBUG_PRINTF_P(PSTR("-- Allocating data (%d): %p\n", len, this); - deallocateData(); // if the old buffer was smaller release it first - if (Segment::getUsedSegmentData() + len > MAX_SEGMENT_DATA) { + //DEBUG_PRINTF_P(PSTR("-- Allocating data (%d): %p\n"), len, this); + if (Segment::getUsedSegmentData() + len - _dataLen > MAX_SEGMENT_DATA) { // not enough memory - DEBUG_PRINT(F("!!! Effect RAM depleted: ")); - DEBUG_PRINTF_P(PSTR("%d/%d !!!\n"), len, Segment::getUsedSegmentData()); + DEBUG_PRINTF_P(PSTR("!!! Not enough RAM: %d/%d !!!\n"), len, Segment::getUsedSegmentData()); errorFlag = ERR_NORAM; return false; } - // do not use SPI RAM on ESP32 since it is slow - data = (byte*)calloc(len, sizeof(byte)); - if (!data) { DEBUG_PRINTLN(F("!!! Allocation failed. !!!")); return false; } // allocation failed - Segment::addUsedSegmentData(len); - //DEBUG_PRINTF_P(PSTR("--- Allocated data (%p): %d/%d -> %p\n"), this, len, Segment::getUsedSegmentData(), data); - _dataLen = len; - return true; + // prefer DRAM over SPI RAM on ESP32 since it is slow + if (data) data = (byte*)d_realloc(data, len); + else data = (byte*)d_malloc(len); + if (data) { + memset(data, 0, len); // erase buffer + Segment::addUsedSegmentData(len - _dataLen); + _dataLen = len; + //DEBUG_PRINTF_P(PSTR("--- Allocated data (%p): %d/%d -> %p\n"), this, len, Segment::getUsedSegmentData(), data); + return true; + } + // allocation failed + DEBUG_PRINTLN(F("!!! Allocation failed. !!!")); + Segment::addUsedSegmentData(-_dataLen); // subtract original buffer size + errorFlag = ERR_NORAM; + return false; } -void IRAM_ATTR_YN Segment::deallocateData() { +void Segment::deallocateData() { if (!data) { _dataLen = 0; return; } - //DEBUG_PRINTF_P(PSTR("--- Released data (%p): %d/%d -> %p\n"), this, _dataLen, Segment::getUsedSegmentData(), data); if ((Segment::getUsedSegmentData() > 0) && (_dataLen > 0)) { // check that we don't have a dangling / inconsistent data pointer - free(data); + //DEBUG_PRINTF_P(PSTR("--- Released data (%p): %d/%d -> %p\n"), this, _dataLen, Segment::getUsedSegmentData(), data); + d_free(data); } else { DEBUG_PRINTF_P(PSTR("---- Released data (%p): inconsistent UsedSegmentData (%d/%d), cowardly refusing to free nothing.\n"), this, _dataLen, Segment::getUsedSegmentData()); } @@ -185,53 +196,49 @@ void IRAM_ATTR_YN Segment::deallocateData() { * may free that data buffer. */ void Segment::resetIfRequired() { - if (!reset) return; + if (!reset || !isActive()) return; //DEBUG_PRINTF_P(PSTR("-- Segment reset: %p\n"), this); if (data && _dataLen > 0) memset(data, 0, _dataLen); // prevent heap fragmentation (just erase buffer instead of deallocateData()) + if (pixels) for (size_t i = 0; i < length(); i++) pixels[i] = BLACK; // clear pixel buffer next_time = 0; step = 0; call = 0; aux0 = 0; aux1 = 0; reset = false; + #ifdef WLED_ENABLE_GIF + endImagePlayback(this); + #endif } CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { if (pal < 245 && pal > GRADIENT_PALETTE_COUNT+13) pal = 0; - if (pal > 245 && (strip.customPalettes.size() == 0 || 255U-pal > strip.customPalettes.size()-1)) pal = 0; // TODO remove strip dependency by moving customPalettes out of strip + if (pal > 245 && (customPalettes.size() == 0 || 255U-pal > customPalettes.size()-1)) pal = 0; //default palette. Differs depending on effect - if (pal == 0) switch (mode) { - case FX_MODE_FIRE_2012 : pal = 35; break; // heat palette - case FX_MODE_COLORWAVES : pal = 26; break; // landscape 33 - case FX_MODE_FILLNOISE8 : pal = 9; break; // ocean colors - case FX_MODE_NOISE16_1 : pal = 20; break; // Drywet - case FX_MODE_NOISE16_2 : pal = 43; break; // Blue cyan yellow - case FX_MODE_NOISE16_3 : pal = 35; break; // heat palette - case FX_MODE_NOISE16_4 : pal = 26; break; // landscape 33 - case FX_MODE_GLITTER : pal = 11; break; // rainbow colors - case FX_MODE_SUNRISE : pal = 35; break; // heat palette - case FX_MODE_RAILWAY : pal = 3; break; // prim + sec - case FX_MODE_2DSOAP : pal = 11; break; // rainbow colors - } + if (pal == 0) pal = _default_palette; // _default_palette is set in setMode() switch (pal) { case 0: //default palette. Exceptions for specific effects above - targetPalette = PartyColors_p; break; + targetPalette = PartyColors_p; + break; case 1: //randomly generated palette targetPalette = _randomPalette; //random palette is generated at intervals in handleRandomPalette() break; case 2: {//primary color only - CRGB prim = gamma32(colors[0]); - targetPalette = CRGBPalette16(prim); break;} + CRGB prim = colors[0]; + targetPalette = CRGBPalette16(prim); + break;} case 3: {//primary + secondary - CRGB prim = gamma32(colors[0]); - CRGB sec = gamma32(colors[1]); - targetPalette = CRGBPalette16(prim,prim,sec,sec); break;} + CRGB prim = colors[0]; + CRGB sec = colors[1]; + targetPalette = CRGBPalette16(prim,prim,sec,sec); + break;} case 4: {//primary + secondary + tertiary - CRGB prim = gamma32(colors[0]); - CRGB sec = gamma32(colors[1]); - CRGB ter = gamma32(colors[2]); - targetPalette = CRGBPalette16(ter,sec,prim); break;} + CRGB prim = colors[0]; + CRGB sec = colors[1]; + CRGB ter = colors[2]; + targetPalette = CRGBPalette16(ter,sec,prim); + break;} case 5: {//primary + secondary (+tertiary if not off), more distinct - CRGB prim = gamma32(colors[0]); - CRGB sec = gamma32(colors[1]); + CRGB prim = colors[0]; + CRGB sec = colors[1]; if (colors[2]) { - CRGB ter = gamma32(colors[2]); + CRGB ter = colors[2]; targetPalette = CRGBPalette16(prim,prim,prim,prim,prim,sec,sec,sec,sec,sec,ter,ter,ter,ter,ter,prim); } else { targetPalette = CRGBPalette16(prim,prim,prim,prim,prim,prim,prim,prim,sec,sec,sec,sec,sec,sec,sec,sec); @@ -239,7 +246,7 @@ CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { break;} default: //progmem palettes if (pal>245) { - targetPalette = strip.customPalettes[255-pal]; // we checked bounds above + targetPalette = customPalettes[255-pal]; // we checked bounds above } else if (pal < 13) { // palette 6 - 12, fastled palettes targetPalette = *fastledPalettes[pal-6]; } else { @@ -252,212 +259,151 @@ CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { return targetPalette; } -void Segment::startTransition(uint16_t dur) { - if (dur == 0) { - if (isInTransition()) _t->_dur = dur; // this will stop transition in next handleTransition() +// starting a transition has to occur before change so we get current values 1st +void Segment::startTransition(uint16_t dur, bool segmentCopy) { + if (dur == 0 || !isActive()) { + if (isInTransition()) _t->_dur = 0; return; } - if (isInTransition()) return; // already in transition no need to store anything - - // starting a transition has to occur before change so we get current values 1st - _t = new Transition(dur); // no previous transition running - if (!_t) return; // failed to allocate data - - //DEBUG_PRINTF_P(PSTR("-- Started transition: %p (%p)\n"), this, _t); - loadPalette(_t->_palT, palette); - _t->_briT = on ? opacity : 0; - _t->_cctT = cct; -#ifndef WLED_DISABLE_MODE_BLEND - if (modeBlending) { - swapSegenv(_t->_segT); - _t->_modeT = mode; - _t->_segT._dataLenT = 0; - _t->_segT._dataT = nullptr; - if (_dataLen > 0 && data) { - _t->_segT._dataT = (byte *)malloc(_dataLen); - if (_t->_segT._dataT) { - //DEBUG_PRINTF_P(PSTR("-- Allocated duplicate data (%d) for %p: %p\n"), _dataLen, this, _t->_segT._dataT); - memcpy(_t->_segT._dataT, data, _dataLen); - _t->_segT._dataLenT = _dataLen; + if (isInTransition()) { + if (segmentCopy && !_t->_oldSegment) { + // already in transition but segment copy requested and not yet created + _t->_oldSegment = new(std::nothrow) Segment(*this); // store/copy current segment settings + _t->_start = millis(); // restart countdown + _t->_dur = dur; + if (_t->_oldSegment) { + _t->_oldSegment->palette = _t->_palette; // restore original palette and colors (from start of transition) + for (unsigned i = 0; i < NUM_COLORS; i++) _t->_oldSegment->colors[i] = _t->_colors[i]; } + DEBUG_PRINTF_P(PSTR("-- Updated transition with segment copy: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); } - } else { - for (size_t i=0; i_segT._colorT[i] = colors[i]; + return; } -#else - for (size_t i=0; i_colorT[i] = colors[i]; -#endif + + // no previous transition running, start by allocating memory for segment copy + _t = new(std::nothrow) Transition(dur); + if (_t) { + _t->_bri = on ? opacity : 0; + _t->_cct = cct; + _t->_palette = palette; + #ifndef WLED_SAVE_RAM + loadPalette(_t->_palT, palette); + #endif + for (int i=0; i_colors[i] = colors[i]; + if (segmentCopy) _t->_oldSegment = new(std::nothrow) Segment(*this); // store/copy current segment settings + #ifdef WLED_DEBUG + if (_t->_oldSegment) { + DEBUG_PRINTF_P(PSTR("-- Started transition: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); + } else { + DEBUG_PRINTF_P(PSTR("-- Started transition without old segment: S=%p T(%p)\n"), this, _t); + } + #endif + }; } void Segment::stopTransition() { - if (isInTransition()) { - //DEBUG_PRINTF_P(PSTR("-- Stopping transition: %p\n"), this); - #ifndef WLED_DISABLE_MODE_BLEND - if (_t->_segT._dataT && _t->_segT._dataLenT > 0) { - //DEBUG_PRINTF_P(PSTR("-- Released duplicate data (%d) for %p: %p\n"), _t->_segT._dataLenT, this, _t->_segT._dataT); - free(_t->_segT._dataT); - _t->_segT._dataT = nullptr; - _t->_segT._dataLenT = 0; - } - #endif - delete _t; - _t = nullptr; - } + DEBUG_PRINTF_P(PSTR("-- Stopping transition: S=%p T(%p) O[%p]\n"), this, _t, _t->_oldSegment); + delete _t; + _t = nullptr; } -// transition progression between 0-65535 -inline void Segment::updateTransitionProgress() { - _transitionprogress = 0xFFFFU; +// sets transition progress variable (0-65535) based on time passed since transition start +void Segment::updateTransitionProgress() const { if (isInTransition()) { + _t->_progress = 0xFFFF; unsigned diff = millis() - _t->_start; - if (_t->_dur > 0 && diff < _t->_dur) _transitionprogress = diff * 0xFFFFU / _t->_dur; + if (_t->_dur > 0 && diff < _t->_dur) _t->_progress = diff * 0xFFFFU / _t->_dur; } } -#ifndef WLED_DISABLE_MODE_BLEND -void Segment::swapSegenv(tmpsegd_t &tmpSeg) { - //DEBUG_PRINTF_P(PSTR("-- Saving temp seg: %p->(%p) [%d->%p]\n"), this, &tmpSeg, _dataLen, data); - tmpSeg._optionsT = options; - for (size_t i=0; i_segT)) { - // swap SEGENV with transitional data - options = _t->_segT._optionsT; - for (size_t i=0; i_segT._colorT[i]; - speed = _t->_segT._speedT; - intensity = _t->_segT._intensityT; - custom1 = _t->_segT._custom1T; - custom2 = _t->_segT._custom2T; - custom3 = _t->_segT._custom3T; - check1 = _t->_segT._check1T; - check2 = _t->_segT._check2T; - check3 = _t->_segT._check3T; - aux0 = _t->_segT._aux0T; - aux1 = _t->_segT._aux1T; - step = _t->_segT._stepT; - call = _t->_segT._callT; - data = _t->_segT._dataT; - _dataLen = _t->_segT._dataLenT; - } -} - -void Segment::restoreSegenv(tmpsegd_t &tmpSeg) { - //DEBUG_PRINTF_P(PSTR("-- Restoring temp seg: %p->(%p) [%d->%p]\n"), &tmpSeg, this, _dataLen, data); - if (_t && &(_t->_segT) != &tmpSeg) { - // update possibly changed variables to keep old effect running correctly - _t->_segT._aux0T = aux0; - _t->_segT._aux1T = aux1; - _t->_segT._stepT = step; - _t->_segT._callT = call; - //if (_t->_segT._dataT != data) DEBUG_PRINTF_P(PSTR("--- data re-allocated: (%p) %p -> %p\n"), this, _t->_segT._dataT, data); - _t->_segT._dataT = data; - _t->_segT._dataLenT = _dataLen; - } - options = tmpSeg._optionsT; - for (size_t i=0; i_cctT : _t->_briT) * (0xFFFFU - prog); - return curBri / 0xFFFFU; + if (blendingStyle == BLEND_STYLE_FADE) return (cct * prog + (_t->_cct * (0xFFFFU - prog))) / 0xFFFFU; + //else return Segment::isPreviousMode() ? _t->_cct : cct; } - return (useCct ? cct : (on ? opacity : 0)); + return cct; } -uint8_t Segment::currentMode() const { -#ifndef WLED_DISABLE_MODE_BLEND +// will return segment's opacity during a transition (blending it with old in case of FADE transition) +uint8_t Segment::currentBri() const { unsigned prog = progress(); - if (modeBlending && prog < 0xFFFFU) return _t->_modeT; -#endif - return mode; + unsigned curBri = on ? opacity : 0; + if (prog < 0xFFFFU) { + // this will blend opacity in new mode if style is FADE (single effect call) + if (blendingStyle == BLEND_STYLE_FADE) curBri = (prog * curBri + _t->_bri * (0xFFFFU - prog)) / 0xFFFFU; + else curBri = Segment::isPreviousMode() ? _t->_bri : curBri; + } + return curBri; } -uint32_t IRAM_ATTR_YN Segment::currentColor(uint8_t slot) const { - if (slot >= NUM_COLORS) slot = 0; -#ifndef WLED_DISABLE_MODE_BLEND - return isInTransition() ? color_blend(_t->_segT._colorT[slot], colors[slot], progress(), true) : colors[slot]; -#else - return isInTransition() ? color_blend(_t->_colorT[slot], colors[slot], progress(), true) : colors[slot]; -#endif -} - -void Segment::setCurrentPalette() { - loadPalette(_currentPalette, palette); - unsigned prog = progress(); - if (strip.paletteFade && prog < 0xFFFFU) { +// pre-calculate drawing parameters for faster access (based on the idea from @softhack007 from MM fork) +// and blends colors and palettes if necessary +// prog is the progress of the transition (0-65535) and is passed to the function as it may be called in the context of old segment +// which does not have transition structure +void Segment::beginDraw(uint16_t prog) { + setDrawDimensions(); + // load colors into _currentColors + for (unsigned i = 0; i < NUM_COLORS; i++) _currentColors[i] = colors[i]; + // load palette into _currentPalette + loadPalette(Segment::_currentPalette, palette); + if (isInTransition() && prog < 0xFFFFU && blendingStyle == BLEND_STYLE_FADE) { + // blend colors + for (unsigned i = 0; i < NUM_COLORS; i++) _currentColors[i] = color_blend16(_t->_colors[i], colors[i], prog); // blend palettes // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in _dur time) // minimum blend time is 100ms maximum is 65535ms + #ifndef WLED_SAVE_RAM unsigned noOfBlends = ((255U * prog) / 0xFFFFU) - _t->_prevPaletteBlends; - for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, _currentPalette, 48); - _currentPalette = _t->_palT; // copy transitioning/temporary palette + for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, Segment::_currentPalette, 48); + Segment::_currentPalette = _t->_palT; // copy transitioning/temporary palette + #else + unsigned noOfBlends = ((255U * prog) / 0xFFFFU); + CRGBPalette16 tmpPalette; + loadPalette(tmpPalette, _t->_palette); + for (unsigned i = 0; i < noOfBlends; i++) nblendPaletteTowardPalette(tmpPalette, Segment::_currentPalette, 48); + Segment::_currentPalette = tmpPalette; // copy transitioning/temporary palette + #endif } } // relies on WS2812FX::service() to call it for each frame void Segment::handleRandomPalette() { + unsigned long now = millis(); + uint16_t now_s = now / 1000; // we only need seconds (and @dedehai hated shift >> 10) + now = (now_s)*1000 + (now % 1000); // ignore days (now is limited to 18 hours as now_s can only store 65535s ~ 18h 12min) + if (now_s < Segment::_lastPaletteChange) Segment::_lastPaletteChange = 0; // handle overflow (will cause 2*randomPaletteChangeTime glitch at most) // is it time to generate a new palette? - if ((uint16_t)((uint16_t)(millis() / 1000U) - _lastPaletteChange) > randomPaletteChangeTime){ - _newRandomPalette = useHarmonicRandomPalette ? generateHarmonicRandomPalette(_randomPalette) : generateRandomPalette(); - _lastPaletteChange = (uint16_t)(millis() / 1000U); - _lastPaletteBlend = (uint16_t)((uint16_t)millis() - 512); // starts blending immediately + if (now_s > Segment::_lastPaletteChange + randomPaletteChangeTime) { + Segment::_newRandomPalette = useHarmonicRandomPalette ? generateHarmonicRandomPalette(Segment::_randomPalette) : generateRandomPalette(); + Segment::_lastPaletteChange = now_s; + Segment::_nextPaletteBlend = now; // starts blending immediately } - - // if palette transitions is enabled, blend it according to Transition Time (if longer than minimum given by service calls) - if (strip.paletteFade) { - // assumes that 128 updates are sufficient to blend a palette, so shift by 7 (can be more, can be less) - // in reality there need to be 255 blends to fully blend two entirely different palettes - if ((uint16_t)((uint16_t)millis() - _lastPaletteBlend) < strip.getTransition() >> 7) return; // not yet time to fade, delay the update - _lastPaletteBlend = (uint16_t)millis(); - } - nblendPaletteTowardPalette(_randomPalette, _newRandomPalette, 48); + // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in strip.getTransition() time) + // if randomPaletteChangeTime is shorter than strip.getTransition() palette will never fully blend + unsigned frameTime = strip.getFrameTime(); // in ms [8-1000] + unsigned transitionTime = strip.getTransition(); // in ms [100-65535] + if ((uint16_t)now < Segment::_nextPaletteBlend || now > ((Segment::_lastPaletteChange*1000) + transitionTime + 2*frameTime)) return; // not yet time or past transition time, no need to blend + unsigned transitionFrames = frameTime > transitionTime ? 1 : transitionTime / frameTime; // i.e. 700ms/23ms = 30 or 20000ms/8ms = 2500 or 100ms/1000ms = 0 -> 1 + unsigned noOfBlends = transitionFrames > 255 ? 1 : (255 + (transitionFrames>>1)) / transitionFrames; // we do some rounding here + for (unsigned i = 0; i < noOfBlends; i++) nblendPaletteTowardPalette(Segment::_randomPalette, Segment::_newRandomPalette, 48); + Segment::_nextPaletteBlend = now + ((transitionFrames >> 8) * frameTime); // postpone next blend if necessary } -// segId is given when called from network callback, changes are queued if that segment is currently in its effect function -void Segment::setUp(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t ofs, uint16_t i1Y, uint16_t i2Y) { +// sets Segment geometry (length or width/height and grouping, spacing and offset as well as 2D mapping) +// strip must be suspended (strip.suspend()) before calling this function +// this function may call fill() to clear pixels if spacing or mapping changed (which requires setting _vWidth, _vHeight, _vLength or beginDraw()) +void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t ofs, uint16_t i1Y, uint16_t i2Y, uint8_t m12) { // return if neither bounds nor grouping have changed bool boundsUnchanged = (start == i1 && stop == i2); #ifndef WLED_DISABLE_2D - if (Segment::maxHeight>1) boundsUnchanged &= (startY == i1Y && stopY == i2Y); // 2D + boundsUnchanged &= (startY == i1Y && stopY == i2Y); // 2D #endif - if (boundsUnchanged - && (!grp || (grouping == grp && spacing == spc)) - && (ofs == UINT16_MAX || ofs == offset)) return; + boundsUnchanged &= (grouping == grp && spacing == spc); // changing grouping and/or spacing changes virtual segment length (painting dimensions) - stateChanged = true; // send UDP/WS broadcast - - if (stop) fill(BLACK); // turn old segment range off (clears pixels if changing spacing) + if (stop && (spc > 0 || m12 != map1D2D)) clear(); if (grp) { // prevent assignment of 0 grouping = grp; spacing = spc; @@ -466,34 +412,52 @@ void Segment::setUp(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t spacing = 0; } if (ofs < UINT16_MAX) offset = ofs; + map1D2D = constrain(m12, 0, 7); - DEBUG_PRINT(F("setUp segment: ")); DEBUG_PRINT(i1); - DEBUG_PRINT(','); DEBUG_PRINT(i2); - DEBUG_PRINT(F(" -> ")); DEBUG_PRINT(i1Y); - DEBUG_PRINT(','); DEBUG_PRINTLN(i2Y); - markForReset(); if (boundsUnchanged) return; + unsigned oldLength = length(); + + DEBUG_PRINTF_P(PSTR("Segment geometry: %d,%d -> %d,%d [%d,%d]\n"), (int)i1, (int)i2, (int)i1Y, (int)i2Y, (int)grp, (int)spc); + markForReset(); + startTransition(strip.getTransition()); // start transition prior to change (if segment is deactivated (start>stop) no transition will happen) + stateChanged = true; // send UDP/WS broadcast + // apply change immediately if (i2 <= i1) { //disable segment + d_free(pixels); + pixels = nullptr; stop = 0; return; } if (i1 < Segment::maxWidth || (i1 >= Segment::maxWidth*Segment::maxHeight && i1 < strip.getLengthTotal())) start = i1; // Segment::maxWidth equals strip.getLengthTotal() for 1D - stop = i2 > Segment::maxWidth*Segment::maxHeight ? MIN(i2,strip.getLengthTotal()) : (i2 > Segment::maxWidth ? Segment::maxWidth : MAX(1,i2)); + stop = i2 > Segment::maxWidth*Segment::maxHeight ? MIN(i2,strip.getLengthTotal()) : constrain(i2, 1, Segment::maxWidth); startY = 0; stopY = 1; #ifndef WLED_DISABLE_2D if (Segment::maxHeight>1) { // 2D if (i1Y < Segment::maxHeight) startY = i1Y; - stopY = i2Y > Segment::maxHeight ? Segment::maxHeight : MAX(1,i2Y); + stopY = constrain(i2Y, 1, Segment::maxHeight); } #endif // safety check if (start >= stop || startY >= stopY) { + d_free(pixels); + pixels = nullptr; stop = 0; return; } + // re-allocate FX render buffer + if (length() != oldLength) { + if (pixels) pixels = static_cast(d_realloc(pixels, sizeof(uint32_t) * length())); + else pixels = static_cast(d_malloc(sizeof(uint32_t) * length())); + if (!pixels) { + DEBUG_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!")); + errorFlag = ERR_NORAM_PX; + stop = 0; + return; + } + } refreshLightCapabilities(); } @@ -504,7 +468,8 @@ Segment &Segment::setColor(uint8_t slot, uint32_t c) { if (slot == 0 && c == BLACK) return *this; // on/off segment cannot have primary color black if (slot == 1 && c != BLACK) return *this; // on/off segment cannot have secondary color non black } - if (fadeTransition) startTransition(strip.getTransition()); // start transition prior to change + //DEBUG_PRINTF_P(PSTR("- Starting color transition: %d [0x%X]\n"), slot, c); + startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change colors[slot] = c; stateChanged = true; // send UDP/WS broadcast return *this; @@ -517,8 +482,8 @@ Segment &Segment::setCCT(uint16_t k) { k = (k - 1900) >> 5; } if (cct != k) { - //DEBUGFX_PRINTF_P(PSTR("- Starting CCT transition: %d\n"), k); - startTransition(strip.getTransition()); // start transition prior to change + //DEBUG_PRINTF_P(PSTR("- Starting CCT transition: %d\n"), k); + startTransition(strip.getTransition(), false); // start transition prior to change (no need to copy segment) cct = k; stateChanged = true; // send UDP/WS broadcast } @@ -527,8 +492,8 @@ Segment &Segment::setCCT(uint16_t k) { Segment &Segment::setOpacity(uint8_t o) { if (opacity != o) { - //DEBUGFX_PRINTF_P(PSTR("- Starting opacity transition: %d\n"), o); - startTransition(strip.getTransition()); // start transition prior to change + //DEBUG_PRINTF_P(PSTR("- Starting opacity transition: %d\n"), o); + startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change opacity = o; stateChanged = true; // send UDP/WS broadcast } @@ -536,11 +501,13 @@ Segment &Segment::setOpacity(uint8_t o) { } Segment &Segment::setOption(uint8_t n, bool val) { - bool prevOn = on; - if (fadeTransition && n == SEG_OPTION_ON && val != prevOn) startTransition(strip.getTransition()); // start transition prior to change + bool prev = (options >> n) & 0x01; + if (val == prev) return *this; + //DEBUG_PRINTF_P(PSTR("- Starting option transition: %d\n"), n); + if (n == SEG_OPTION_ON) startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change if (val) options |= 0x01 << n; else options &= ~(0x01 << n); - if (!(n == SEG_OPTION_SELECTED || n == SEG_OPTION_RESET)) stateChanged = true; // send UDP/WS broadcast + stateChanged = true; // send UDP/WS broadcast return *this; } @@ -550,13 +517,11 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) { if (fx >= strip.getModeCount()) fx = 0; // set solid mode // if we have a valid mode & is not reserved if (fx != mode) { -#ifndef WLED_DISABLE_MODE_BLEND - if (modeBlending) startTransition(strip.getTransition()); // set effect transitions -#endif + startTransition(strip.getTransition(), true); // set effect transitions (must create segment copy) mode = fx; + int sOpt; // load default values from effect string if (loadDefaults) { - int sOpt; sOpt = extractModeDefaults(fx, "sx"); speed = (sOpt >= 0) ? sOpt : DEFAULT_SPEED; sOpt = extractModeDefaults(fx, "ix"); intensity = (sOpt >= 0) ? sOpt : DEFAULT_INTENSITY; sOpt = extractModeDefaults(fx, "c1"); custom1 = (sOpt >= 0) ? sOpt : DEFAULT_C1; @@ -571,8 +536,11 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) { sOpt = extractModeDefaults(fx, "mi"); if (sOpt >= 0) mirror = (bool)sOpt; // NOTE: setting this option is a risky business sOpt = extractModeDefaults(fx, "rY"); if (sOpt >= 0) reverse_y = (bool)sOpt; sOpt = extractModeDefaults(fx, "mY"); if (sOpt >= 0) mirror_y = (bool)sOpt; // NOTE: setting this option is a risky business - sOpt = extractModeDefaults(fx, "pal"); if (sOpt >= 0) setPalette(sOpt); //else setPalette(0); } + sOpt = extractModeDefaults(fx, "pal"); // always extract 'pal' to set _default_palette + if (sOpt >= 0 && loadDefaults) setPalette(sOpt); + if (sOpt <= 0) sOpt = 6; // partycolors if zero or not set + _default_palette = sOpt; // _deault_palette is loaded into pal0 in loadPalette() (if selected) markForReset(); stateChanged = true; // send UDP/WS broadcast } @@ -581,24 +549,39 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) { Segment &Segment::setPalette(uint8_t pal) { if (pal < 245 && pal > GRADIENT_PALETTE_COUNT+13) pal = 0; // built in palettes - if (pal > 245 && (strip.customPalettes.size() == 0 || 255U-pal > strip.customPalettes.size()-1)) pal = 0; // custom palettes + if (pal > 245 && (customPalettes.size() == 0 || 255U-pal > customPalettes.size()-1)) pal = 0; // custom palettes if (pal != palette) { - if (strip.paletteFade) startTransition(strip.getTransition()); + //DEBUG_PRINTF_P(PSTR("- Starting palette transition: %d\n"), pal); + startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change (no need to copy segment) palette = pal; stateChanged = true; // send UDP/WS broadcast } return *this; } +Segment &Segment::setName(const char *newName) { + if (newName) { + const int newLen = min(strlen(newName), (size_t)WLED_MAX_SEGNAME_LEN); + if (newLen) { + if (name) name = static_cast(d_realloc(name, newLen+1)); + else name = static_cast(d_malloc(newLen+1)); + if (name) strlcpy(name, newName, newLen+1); + name[newLen] = 0; + return *this; + } + } + return clearName(); +} + // 2D matrix -unsigned IRAM_ATTR Segment::virtualWidth() const { +unsigned Segment::virtualWidth() const { unsigned groupLen = groupLength(); unsigned vWidth = ((transpose ? height() : width()) + groupLen - 1) / groupLen; if (mirror) vWidth = (vWidth + 1) /2; // divide by 2 if mirror, leave at least a single LED return vWidth; } -unsigned IRAM_ATTR Segment::virtualHeight() const { +unsigned Segment::virtualHeight() const { unsigned groupLen = groupLength(); unsigned vHeight = ((transpose ? width() : height()) + groupLen - 1) / groupLen; if (mirror_y) vHeight = (vHeight + 1) /2; // divide by 2 if mirror, leave at least a single LED @@ -607,42 +590,30 @@ unsigned IRAM_ATTR Segment::virtualHeight() const { // Constants for mapping mode "Pinwheel" #ifndef WLED_DISABLE_2D -constexpr int Pinwheel_Steps_Small = 72; // no holes up to 16x16 -constexpr int Pinwheel_Size_Small = 16; // larger than this -> use "Medium" -constexpr int Pinwheel_Steps_Medium = 192; // no holes up to 32x32 -constexpr int Pinwheel_Size_Medium = 32; // larger than this -> use "Big" -constexpr int Pinwheel_Steps_Big = 304; // no holes up to 50x50 -constexpr int Pinwheel_Size_Big = 50; // larger than this -> use "XL" -constexpr int Pinwheel_Steps_XL = 368; -constexpr float Int_to_Rad_Small = (DEG_TO_RAD * 360) / Pinwheel_Steps_Small; // conversion: from 0...72 to Radians -constexpr float Int_to_Rad_Med = (DEG_TO_RAD * 360) / Pinwheel_Steps_Medium; // conversion: from 0...192 to Radians -constexpr float Int_to_Rad_Big = (DEG_TO_RAD * 360) / Pinwheel_Steps_Big; // conversion: from 0...304 to Radians -constexpr float Int_to_Rad_XL = (DEG_TO_RAD * 360) / Pinwheel_Steps_XL; // conversion: from 0...368 to Radians - -constexpr int Fixed_Scale = 512; // fixpoint scaling factor (9bit for fraction) - -// Pinwheel helper function: pixel index to radians -static float getPinwheelAngle(int i, int vW, int vH) { - int maxXY = max(vW, vH); - if (maxXY <= Pinwheel_Size_Small) return float(i) * Int_to_Rad_Small; - if (maxXY <= Pinwheel_Size_Medium) return float(i) * Int_to_Rad_Med; - if (maxXY <= Pinwheel_Size_Big) return float(i) * Int_to_Rad_Big; - // else - return float(i) * Int_to_Rad_XL; -} +constexpr int Fixed_Scale = 16384; // fixpoint scaling factor (14bit for fraction) // Pinwheel helper function: matrix dimensions to number of rays static int getPinwheelLength(int vW, int vH) { - int maxXY = max(vW, vH); - if (maxXY <= Pinwheel_Size_Small) return Pinwheel_Steps_Small; - if (maxXY <= Pinwheel_Size_Medium) return Pinwheel_Steps_Medium; - if (maxXY <= Pinwheel_Size_Big) return Pinwheel_Steps_Big; - // else - return Pinwheel_Steps_XL; + // Returns multiple of 8, prevents over drawing + return (max(vW, vH) + 15) & ~7; +} +static void setPinwheelParameters(int i, int vW, int vH, int& startx, int& starty, int* cosVal, int* sinVal, bool getPixel = false) { + int steps = getPinwheelLength(vW, vH); + int baseAngle = ((0xFFFF + steps / 2) / steps); // 360° / steps, in 16 bit scale round to nearest integer + int rotate = 0; + if (getPixel) rotate = baseAngle / 2; // rotate by half a ray width when reading pixel color + for (int k = 0; k < 2; k++) // angular steps for two consecutive rays + { + int angle = (i + k) * baseAngle + rotate; + cosVal[k] = (cos16_t(angle) * Fixed_Scale) >> 15; // step per pixel in fixed point, cos16 output is -0x7FFF to +0x7FFF + sinVal[k] = (sin16_t(angle) * Fixed_Scale) >> 15; // using explicit bit shifts as dividing negative numbers is not equivalent (rounding error is acceptable) + } + startx = (vW * Fixed_Scale) / 2; // + cosVal[0] / 4; // starting position = center + 1/4 pixel (in fixed point) + starty = (vH * Fixed_Scale) / 2; // + sinVal[0] / 4; } #endif // 1D strip -uint16_t IRAM_ATTR Segment::virtualLength() const { +uint16_t Segment::virtualLength() const { #ifndef WLED_DISABLE_2D if (is2D()) { unsigned vW = virtualWidth(); @@ -656,7 +627,7 @@ uint16_t IRAM_ATTR Segment::virtualLength() const { vLen = max(vW,vH); // get the longest dimension break; case M12_pArc: - vLen = sqrt16(vH*vH + vW*vW); // use diagonal + vLen = sqrt32_bw(vH*vH + vW*vW); // use diagonal break; case M12_sPinwheel: vLen = getPinwheelLength(vW, vH); @@ -674,34 +645,65 @@ uint16_t IRAM_ATTR Segment::virtualLength() const { return vLength; } -void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col) -{ - if (!isActive()) return; // not active -#ifndef WLED_DISABLE_2D - int vStrip = i>>16; // hack to allow running on virtual strips (2D segment columns/rows) -#endif - i &= 0xFFFF; +// pixel is clipped if it falls outside clipping range +// if clipping start > stop the clipping range is inverted +bool IRAM_ATTR_YN Segment::isPixelClipped(int i) const { + if (blendingStyle != BLEND_STYLE_FADE && isInTransition() && _clipStart != _clipStop) { + bool invert = _clipStart > _clipStop; // ineverted start & stop + int start = invert ? _clipStop : _clipStart; + int stop = invert ? _clipStart : _clipStop; + if (blendingStyle == BLEND_STYLE_FAIRY_DUST) { + unsigned len = stop - start; + if (len < 2) return false; + unsigned shuffled = hashInt(i) % len; + unsigned pos = (shuffled * 0xFFFFU) / len; + return progress() <= pos; + } + const bool iInside = (i >= start && i < stop); + return !iInside ^ invert; // thanks @willmmiles (https://github.com/wled/WLED/pull/3877#discussion_r1554633876) + } + return false; +} - if (i >= virtualLength() || i<0) return; // if pixel would fall out of segment just exit +void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col) const +{ + if (!isActive() || i < 0) return; // not active or invalid index +#ifndef WLED_DISABLE_2D + int vStrip = 0; +#endif + const int vL = vLength(); + // if the 1D effect is using virtual strips "i" will have virtual strip id stored in upper 16 bits + // in such case "i" will be > virtualLength() + if (i >= vL) { + // check if this is a virtual strip + #ifndef WLED_DISABLE_2D + vStrip = i>>16; // hack to allow running on virtual strips (2D segment columns/rows) + i &= 0xFFFF; //truncate vstrip index + if (i >= vL) return; // if pixel would still fall out of segment just exit + #else + return; + #endif + } #ifndef WLED_DISABLE_2D if (is2D()) { - int vH = virtualHeight(); // segment height in logical pixels - int vW = virtualWidth(); + const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) + const int vH = vHeight(); // segment height in logical pixels (is always >= 1) + const auto XY = [&](unsigned x, unsigned y){ return x + y*vW;}; switch (map1D2D) { case M12_Pixels: // use all available pixels as a long strip - setPixelColorXY(i % vW, i / vW, col); + setPixelColorRaw(XY(i % vW, i / vW), col); break; case M12_pBar: // expand 1D effect vertically or have it play on virtual strips - if (vStrip>0) setPixelColorXY(vStrip - 1, vH - i - 1, col); - else for (int x = 0; x < vW; x++) setPixelColorXY(x, vH - i - 1, col); + if (vStrip > 0) setPixelColorRaw(XY(vStrip - 1, vH - i - 1), col); + else for (int x = 0; x < vW; x++) setPixelColorRaw(XY(x, vH - i - 1), col); break; case M12_pArc: // expand in circular fashion from center - if (i==0) - setPixelColorXY(0, 0, col); + if (i == 0) + setPixelColorRaw(XY(0, 0), col); else { float r = i; float step = HALF_PI / (2.8284f * r + 4); // we only need (PI/4)/(r/sqrt(2)+1) steps @@ -729,113 +731,123 @@ void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col) } break; case M12_pCorner: - for (int x = 0; x <= i; x++) setPixelColorXY(x, i, col); - for (int y = 0; y < i; y++) setPixelColorXY(i, y, col); + for (int x = 0; x <= i; x++) setPixelColorRaw(XY(x, i), col); + for (int y = 0; y < i; y++) setPixelColorRaw(XY(i, y), col); break; case M12_sPinwheel: { - // i = angle --> 0 - 296 (Big), 0 - 192 (Medium), 0 - 72 (Small) - float centerX = roundf((vW-1) / 2.0f); - float centerY = roundf((vH-1) / 2.0f); - float angleRad = getPinwheelAngle(i, vW, vH); // angle in radians - float cosVal = cos_t(angleRad); - float sinVal = sin_t(angleRad); + // Uses Bresenham's algorithm to place coordinates of two lines in arrays then draws between them + int startX, startY, cosVal[2], sinVal[2]; // in fixed point scale + setPinwheelParameters(i, vW, vH, startX, startY, cosVal, sinVal); - // avoid re-painting the same pixel - int lastX = INT_MIN; // impossible position - int lastY = INT_MIN; // impossible position - // draw line at angle, starting at center and ending at the segment edge - // we use fixed point math for better speed. Starting distance is 0.5 for better rounding - // int_fast16_t and int_fast32_t types changed to int, minimum bits commented - int posx = (centerX + 0.5f * cosVal) * Fixed_Scale; // X starting position in fixed point 18 bit - int posy = (centerY + 0.5f * sinVal) * Fixed_Scale; // Y starting position in fixed point 18 bit - int inc_x = cosVal * Fixed_Scale; // X increment per step (fixed point) 10 bit - int inc_y = sinVal * Fixed_Scale; // Y increment per step (fixed point) 10 bit + unsigned maxLineLength = max(vW, vH) + 2; // pixels drawn is always smaller than dx or dy, +1 pair for rounding errors + uint16_t lineCoords[2][maxLineLength]; // uint16_t to save ram + int lineLength[2] = {0}; - int32_t maxX = vW * Fixed_Scale; // X edge in fixedpoint - int32_t maxY = vH * Fixed_Scale; // Y edge in fixedpoint + static int prevRays[2] = {INT_MAX, INT_MAX}; // previous two ray numbers + int closestEdgeIdx = INT_MAX; // index of the closest edge pixel - // Odd rays start further from center if prevRay started at center. - static int prevRay = INT_MIN; // previous ray number - if ((i % 2 == 1) && (i - 1 == prevRay || i + 1 == prevRay)) { - int jump = min(vW/3, vH/3); // can add 2 if using medium pinwheel - posx += inc_x * jump; - posy += inc_y * jump; + for (int lineNr = 0; lineNr < 2; lineNr++) { + int x0 = startX; // x, y coordinates in fixed scale + int y0 = startY; + int x1 = (startX + (cosVal[lineNr] << 9)); // outside of grid + int y1 = (startY + (sinVal[lineNr] << 9)); // outside of grid + const int dx = abs(x1-x0), sx = x0= (unsigned)vW || (unsigned)y0 >= (unsigned)vH) { + closestEdgeIdx = min(closestEdgeIdx, idx-2); + break; // stop if outside of grid (exploit unsigned int overflow) + } + coordinates[idx++] = x0; + coordinates[idx++] = y0; + (*length)++; + // note: since endpoint is out of grid, no need to check if endpoint is reached + int e2 = 2 * err; + if (e2 >= dy) { err += dy; x0 += sx; } + if (e2 <= dx) { err += dx; y0 += sy; } + } } - prevRay = i; - // draw ray until we hit any edge - while ((posx >= 0) && (posy >= 0) && (posx < maxX) && (posy < maxY)) { - // scale down to integer (compiler will replace division with appropriate bitshift) - int x = posx / Fixed_Scale; - int y = posy / Fixed_Scale; - // set pixel - if (x != lastX || y != lastY) setPixelColorXY(x, y, col); // only paint if pixel position is different - lastX = x; - lastY = y; - // advance to next position - posx += inc_x; - posy += inc_y; + // fill up the shorter line with missing coordinates, so block filling works correctly and efficiently + int diff = lineLength[0] - lineLength[1]; + int longLineIdx = (diff > 0) ? 0 : 1; + int shortLineIdx = longLineIdx ? 0 : 1; + if (diff != 0) { + int idx = (lineLength[shortLineIdx] - 1) * 2; // last valid coordinate index + int lastX = lineCoords[shortLineIdx][idx++]; + int lastY = lineCoords[shortLineIdx][idx++]; + bool keepX = lastX == 0 || lastX == vW - 1; + for (int d = 0; d < abs(diff); d++) { + lineCoords[shortLineIdx][idx] = keepX ? lastX :lineCoords[longLineIdx][idx]; + idx++; + lineCoords[shortLineIdx][idx] = keepX ? lineCoords[longLineIdx][idx] : lastY; + idx++; + } } + + // draw and block-fill the line coordinates. Note: block filling only efficient if angle between lines is small + closestEdgeIdx += 2; + int max_i = getPinwheelLength(vW, vH) - 1; + bool drawFirst = !(prevRays[0] == i - 1 || (i == 0 && prevRays[0] == max_i)); // draw first line if previous ray was not adjacent including wrap + bool drawLast = !(prevRays[0] == i + 1 || (i == max_i && prevRays[0] == 0)); // same as above for last line + for (int idx = 0; idx < lineLength[longLineIdx] * 2;) { //!! should be long line idx! + int x1 = lineCoords[0][idx]; + int x2 = lineCoords[1][idx++]; + int y1 = lineCoords[0][idx]; + int y2 = lineCoords[1][idx++]; + int minX, maxX, minY, maxY; + (x1 < x2) ? (minX = x1, maxX = x2) : (minX = x2, maxX = x1); + (y1 < y2) ? (minY = y1, maxY = y2) : (minY = y2, maxY = y1); + + // fill the block between the two x,y points + bool alwaysDraw = (drawFirst && drawLast) || // No adjacent rays, draw all pixels + (idx > closestEdgeIdx) || // Edge pixels on uneven lines are always drawn + (i == 0 && idx == 2) || // Center pixel special case + (i == prevRays[1]); // Effect drawing twice in 1 frame + for (int x = minX; x <= maxX; x++) { + for (int y = minY; y <= maxY; y++) { + bool onLine1 = x == x1 && y == y1; + bool onLine2 = x == x2 && y == y2; + if ((alwaysDraw) || + (!onLine1 && (!onLine2 || drawLast)) || // Middle pixels and line2 if drawLast + (!onLine2 && (!onLine1 || drawFirst)) // Middle pixels and line1 if drawFirst + ) { + setPixelColorXY(x, y, col); + } + } + } + } + prevRays[1] = prevRays[0]; + prevRays[0] = i; break; } } return; - } else if (Segment::maxHeight!=1 && (width()==1 || height()==1)) { + } else if (Segment::maxHeight != 1 && (width() == 1 || height() == 1)) { if (start < Segment::maxWidth*Segment::maxHeight) { // we have a vertical or horizontal 1D segment (WARNING: virtual...() may be transposed) int x = 0, y = 0; - if (virtualHeight()>1) y = i; - if (virtualWidth() >1) x = i; + if (vHeight() > 1) y = i; + if (vWidth() > 1) x = i; setPixelColorXY(x, y, col); return; } } #endif - - unsigned len = length(); - uint8_t _bri_t = currentBri(); - if (_bri_t < 255) { - col = color_fade(col, _bri_t); - } - - // expand pixel (taking into account start, grouping, spacing [and offset]) - i = i * groupLength(); - if (reverse) { // is segment reversed? - if (mirror) { // is segment mirrored? - i = (len - 1) / 2 - i; //only need to index half the pixels - } else { - i = (len - 1) - i; - } - } - i += start; // starting pixel in a group - - uint32_t tmpCol = col; - // set all the pixels in the group - for (int j = 0; j < grouping; j++) { - unsigned indexSet = i + ((reverse) ? -j : j); - if (indexSet >= start && indexSet < stop) { - if (mirror) { //set the corresponding mirrored pixel - unsigned indexMir = stop - indexSet + start - 1; - indexMir += offset; // offset/phase - if (indexMir >= stop) indexMir -= len; // wrap -#ifndef WLED_DISABLE_MODE_BLEND - if (_modeBlend) tmpCol = color_blend(strip.getPixelColor(indexMir), col, 0xFFFFU - progress(), true); -#endif - strip.setPixelColor(indexMir, tmpCol); - } - indexSet += offset; // offset/phase - if (indexSet >= stop) indexSet -= len; // wrap -#ifndef WLED_DISABLE_MODE_BLEND - if (_modeBlend) tmpCol = color_blend(strip.getPixelColor(indexSet), col, 0xFFFFU - progress(), true); -#endif - strip.setPixelColor(indexSet, tmpCol); - } - } + setPixelColorRaw(i, col); } #ifdef WLED_USE_AA_PIXELS // anti-aliased normalized version of setPixelColor() -void Segment::setPixelColor(float i, uint32_t col, bool aa) +void Segment::setPixelColor(float i, uint32_t col, bool aa) const { if (!isActive()) return; // not active int vStrip = int(i/10.0f); // hack to allow running on virtual strips (2D segment columns/rows) @@ -870,146 +882,91 @@ void Segment::setPixelColor(float i, uint32_t col, bool aa) uint32_t IRAM_ATTR_YN Segment::getPixelColor(int i) const { - if (!isActive()) return 0; // not active + if (!isActive() || i < 0) return 0; // not active or invalid index + #ifndef WLED_DISABLE_2D - int vStrip = i>>16; -#endif + int vStrip = i>>16; // virtual strips are only relevant in Bar expansion mode i &= 0xFFFF; +#endif + if (i >= (int)vLength()) return 0; #ifndef WLED_DISABLE_2D if (is2D()) { - int vH = virtualHeight(); // segment height in logical pixels - int vW = virtualWidth(); + const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) + const int vH = vHeight(); // segment height in logical pixels (is always >= 1) + int x = 0, y = 0; switch (map1D2D) { case M12_Pixels: - return getPixelColorXY(i % vW, i / vW); + x = i % vW; + y = i / vW; break; case M12_pBar: - if (vStrip>0) return getPixelColorXY(vStrip - 1, vH - i -1); - else return getPixelColorXY(0, vH - i -1); + if (vStrip > 0) { x = vStrip - 1; y = vH - i - 1; } + else { y = vH - i - 1; }; break; case M12_pArc: - if (i >= vW && i >= vH) { - unsigned vI = sqrt16(i*i/2); - return getPixelColorXY(vI,vI); // use diagonal + if (i > vW && i > vH) { + x = y = sqrt32_bw(i*i/2); + break; // use diagonal } + // otherwise fallthrough case M12_pCorner: // use longest dimension - return vW>vH ? getPixelColorXY(i, 0) : getPixelColorXY(0, i); + if (vW > vH) x = i; + else y = i; break; - case M12_sPinwheel: + case M12_sPinwheel: { // not 100% accurate, returns pixel at outer edge - // i = angle --> 0 - 296 (Big), 0 - 192 (Medium), 0 - 72 (Small) - float centerX = roundf((vW-1) / 2.0f); - float centerY = roundf((vH-1) / 2.0f); - float angleRad = getPinwheelAngle(i, vW, vH); // angle in radians - float cosVal = cos_t(angleRad); - float sinVal = sin_t(angleRad); - - int posx = (centerX + 0.5f * cosVal) * Fixed_Scale; // X starting position in fixed point 18 bit - int posy = (centerY + 0.5f * sinVal) * Fixed_Scale; // Y starting position in fixed point 18 bit - int inc_x = cosVal * Fixed_Scale; // X increment per step (fixed point) 10 bit - int inc_y = sinVal * Fixed_Scale; // Y increment per step (fixed point) 10 bit - int32_t maxX = vW * Fixed_Scale; // X edge in fixedpoint - int32_t maxY = vH * Fixed_Scale; // Y edge in fixedpoint - - // trace ray from center until we hit any edge - to avoid rounding problems, we use the same method as in setPixelColor - int x = INT_MIN; - int y = INT_MIN; - while ((posx >= 0) && (posy >= 0) && (posx < maxX) && (posy < maxY)) { - // scale down to integer (compiler will replace division with appropriate bitshift) - x = posx / Fixed_Scale; - y = posy / Fixed_Scale; - // advance to next position - posx += inc_x; - posy += inc_y; + int cosVal[2], sinVal[2]; + setPinwheelParameters(i, vW, vH, x, y, cosVal, sinVal, true); + int maxX = (vW-1) * Fixed_Scale; + int maxY = (vH-1) * Fixed_Scale; + // trace ray from center until we hit any edge - to avoid rounding problems, we use fixed point coordinates + while ((x < maxX) && (y < maxY) && (x > Fixed_Scale) && (y > Fixed_Scale)) { + x += cosVal[0]; // advance to next position + y += sinVal[0]; } - return getPixelColorXY(x, y); + x /= Fixed_Scale; + y /= Fixed_Scale; break; } - return 0; + } + return getPixelColorXY(x, y); } #endif - - if (reverse) i = virtualLength() - i - 1; - i *= groupLength(); - i += start; - // offset/phase - i += offset; - if (i >= stop) i -= length(); - return strip.getPixelColor(i); + return getPixelColorRaw(i); } -uint8_t Segment::differs(Segment& b) const { - uint8_t d = 0; - if (start != b.start) d |= SEG_DIFFERS_BOUNDS; - if (stop != b.stop) d |= SEG_DIFFERS_BOUNDS; - if (offset != b.offset) d |= SEG_DIFFERS_GSO; - if (grouping != b.grouping) d |= SEG_DIFFERS_GSO; - if (spacing != b.spacing) d |= SEG_DIFFERS_GSO; - if (opacity != b.opacity) d |= SEG_DIFFERS_BRI; - if (mode != b.mode) d |= SEG_DIFFERS_FX; - if (speed != b.speed) d |= SEG_DIFFERS_FX; - if (intensity != b.intensity) d |= SEG_DIFFERS_FX; - if (palette != b.palette) d |= SEG_DIFFERS_FX; - if (custom1 != b.custom1) d |= SEG_DIFFERS_FX; - if (custom2 != b.custom2) d |= SEG_DIFFERS_FX; - if (custom3 != b.custom3) d |= SEG_DIFFERS_FX; - if (startY != b.startY) d |= SEG_DIFFERS_BOUNDS; - if (stopY != b.stopY) d |= SEG_DIFFERS_BOUNDS; - - //bit pattern: (msb first) - // set:2, sound:2, mapping:3, transposed, mirrorY, reverseY, [reset,] paused, mirrored, on, reverse, [selected] - if ((options & 0b1111111111011110U) != (b.options & 0b1111111111011110U)) d |= SEG_DIFFERS_OPT; - if ((options & 0x0001U) != (b.options & 0x0001U)) d |= SEG_DIFFERS_SEL; - for (unsigned i = 0; i < NUM_COLORS; i++) if (colors[i] != b.colors[i]) d |= SEG_DIFFERS_COL; - - return d; -} - -void Segment::refreshLightCapabilities() { +void Segment::refreshLightCapabilities() const { unsigned capabilities = 0; - unsigned segStartIdx = 0xFFFFU; - unsigned segStopIdx = 0; if (!isActive()) { _capabilities = 0; return; } - if (start < Segment::maxWidth * Segment::maxHeight) { - // we are withing 2D matrix (includes 1D segments) - for (int y = startY; y < stopY; y++) for (int x = start; x < stop; x++) { - unsigned index = strip.getMappedPixelIndex(x + Segment::maxWidth * y); // convert logical address to physical - if (index < 0xFFFFU) { - if (segStartIdx > index) segStartIdx = index; - if (segStopIdx < index) segStopIdx = index; + // we must traverse each pixel in segment to determine its capabilities (as pixel may be mapped) + for (unsigned y = startY; y < stopY; y++) for (unsigned x = start; x < stop; x++) { + unsigned index = x + Segment::maxWidth * y; + index = strip.getMappedPixelIndex(index); // convert logical address to physical + if (index == 0xFFFF) continue; // invalid/missing pixel + for (unsigned b = 0; b < BusManager::getNumBusses(); b++) { + const Bus *bus = BusManager::getBus(b); + if (!bus || !bus->isOk()) break; + if (bus->containsPixel(index)) { + if (bus->hasRGB() || (strip.cctFromRgb && bus->hasCCT())) capabilities |= SEG_CAPABILITY_RGB; + if (!strip.cctFromRgb && bus->hasCCT()) capabilities |= SEG_CAPABILITY_CCT; + if (strip.correctWB && (bus->hasRGB() || bus->hasCCT())) capabilities |= SEG_CAPABILITY_CCT; //white balance correction (CCT slider) + if (bus->hasWhite()) { + unsigned aWM = Bus::getGlobalAWMode() == AW_GLOBAL_DISABLED ? bus->getAutoWhiteMode() : Bus::getGlobalAWMode(); + bool whiteSlider = (aWM == RGBW_MODE_DUAL || aWM == RGBW_MODE_MANUAL_ONLY); // white slider allowed + // if auto white calculation from RGB is active (Accurate/Brighter), force RGB controls even if there are no RGB busses + if (!whiteSlider) capabilities |= SEG_CAPABILITY_RGB; + // if auto white calculation from RGB is disabled/optional (None/Dual), allow white channel adjustments + if ( whiteSlider) capabilities |= SEG_CAPABILITY_W; + } + break; } - if (segStartIdx == segStopIdx) segStopIdx++; // we only have 1 pixel segment - } - } else { - // we are on the strip located after the matrix - segStartIdx = start; - segStopIdx = stop; - } - - for (unsigned b = 0; b < BusManager::getNumBusses(); b++) { - Bus *bus = BusManager::getBus(b); - if (bus == nullptr || bus->getLength()==0) break; - if (!bus->isOk()) continue; - if (bus->getStart() >= segStopIdx) continue; - if (bus->getStart() + bus->getLength() <= segStartIdx) continue; - - if (bus->hasRGB() || (strip.cctFromRgb && bus->hasCCT())) capabilities |= SEG_CAPABILITY_RGB; - if (!strip.cctFromRgb && bus->hasCCT()) capabilities |= SEG_CAPABILITY_CCT; - if (strip.correctWB && (bus->hasRGB() || bus->hasCCT())) capabilities |= SEG_CAPABILITY_CCT; //white balance correction (CCT slider) - if (bus->hasWhite()) { - unsigned aWM = Bus::getGlobalAWMode() == AW_GLOBAL_DISABLED ? bus->getAutoWhiteMode() : Bus::getGlobalAWMode(); - bool whiteSlider = (aWM == RGBW_MODE_DUAL || aWM == RGBW_MODE_MANUAL_ONLY); // white slider allowed - // if auto white calculation from RGB is active (Accurate/Brighter), force RGB controls even if there are no RGB busses - if (!whiteSlider) capabilities |= SEG_CAPABILITY_RGB; - // if auto white calculation from RGB is disabled/optional (None/Dual), allow white channel adjustments - if ( whiteSlider) capabilities |= SEG_CAPABILITY_W; } } _capabilities = capabilities; @@ -1018,105 +975,86 @@ void Segment::refreshLightCapabilities() { /* * Fills segment with color */ -void Segment::fill(uint32_t c) { +void Segment::fill(uint32_t c) const { if (!isActive()) return; // not active - const int cols = is2D() ? virtualWidth() : virtualLength(); - const int rows = virtualHeight(); // will be 1 for 1D - for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) { - if (is2D()) setPixelColorXY(x, y, c); - else setPixelColor(x, c); - } + for (unsigned i = 0; i < length(); i++) setPixelColorRaw(i,c); // always fill all pixels (blending will take care of grouping, spacing and clipping) } /* * fade out function, higher rate = quicker fade + * fading is highly dependant on frame rate (higher frame rates, faster fading) + * each frame will fade at max 9% or as little as 0.8% */ -void Segment::fade_out(uint8_t rate) { +void Segment::fade_out(uint8_t rate) const { if (!isActive()) return; // not active - const int cols = is2D() ? virtualWidth() : virtualLength(); - const int rows = virtualHeight(); // will be 1 for 1D - - rate = (255-rate) >> 1; - float mappedRate = 1.0f / (float(rate) + 1.1f); - - uint32_t color = colors[1]; // SEGCOLOR(1); // target color - int w2 = W(color); - int r2 = R(color); - int g2 = G(color); - int b2 = B(color); - - for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) { - color = is2D() ? getPixelColorXY(x, y) : getPixelColor(x); + rate = (256-rate) >> 1; + const int mappedRate = 256 / (rate + 1); + for (unsigned j = 0; j < vLength(); j++) { + uint32_t color = getPixelColorRaw(j); if (color == colors[1]) continue; // already at target color - int w1 = W(color); - int r1 = R(color); - int g1 = G(color); - int b1 = B(color); - - int wdelta = (w2 - w1) * mappedRate; - int rdelta = (r2 - r1) * mappedRate; - int gdelta = (g2 - g1) * mappedRate; - int bdelta = (b2 - b1) * mappedRate; - - // if fade isn't complete, make sure delta is at least 1 (fixes rounding issues) - wdelta += (w2 == w1) ? 0 : (w2 > w1) ? 1 : -1; - rdelta += (r2 == r1) ? 0 : (r2 > r1) ? 1 : -1; - gdelta += (g2 == g1) ? 0 : (g2 > g1) ? 1 : -1; - bdelta += (b2 == b1) ? 0 : (b2 > b1) ? 1 : -1; - - if (is2D()) setPixelColorXY(x, y, r1 + rdelta, g1 + gdelta, b1 + bdelta, w1 + wdelta); - else setPixelColor(x, r1 + rdelta, g1 + gdelta, b1 + bdelta, w1 + wdelta); + for (int i = 0; i < 32; i += 8) { + uint8_t c2 = (colors[1]>>i); // get background channel + uint8_t c1 = (color>>i); // get foreground channel + // we can't use bitshift since we are using int + int delta = (c2 - c1) * mappedRate / 256; + // if fade isn't complete, make sure delta is at least 1 (fixes rounding issues) + if (delta == 0) delta += (c2 == c1) ? 0 : (c2 > c1) ? 1 : -1; + // stuff new value back into color + color &= ~(0xFF< 215 this function does not work properly (creates alternating pattern) */ -void Segment::blur(uint8_t blur_amount, bool smear) { +void Segment::blur(uint8_t blur_amount, bool smear) const { if (!isActive() || blur_amount == 0) return; // optimization: 0 means "don't blur" #ifndef WLED_DISABLE_2D if (is2D()) { // compatibility with 2D - blur2D(blur_amount, smear); + blur2D(blur_amount, blur_amount, smear); // symmetrical 2D blur //box_blur(map(blur_amount,1,255,1,3), smear); return; } #endif uint8_t keep = smear ? 255 : 255 - blur_amount; - uint8_t seep = blur_amount >> (1 + smear); - unsigned vlength = virtualLength(); + uint8_t seep = blur_amount >> 1; + unsigned vlength = vLength(); uint32_t carryover = BLACK; - uint32_t lastnew; + uint32_t lastnew; // not necessary to initialize lastnew and last, as both will be initialized by the first loop iteration uint32_t last; uint32_t curnew = BLACK; for (unsigned i = 0; i < vlength; i++) { - uint32_t cur = getPixelColor(i); + uint32_t cur = getPixelColorRaw(i); uint32_t part = color_fade(cur, seep); curnew = color_fade(cur, keep); if (i > 0) { - if (carryover) curnew = color_add(curnew, carryover, true); - uint32_t prev = color_add(lastnew, part, true); + if (carryover) curnew = color_add(curnew, carryover); + uint32_t prev = color_add(lastnew, part); // optimization: only set pixel if color has changed - if (last != prev) setPixelColor(i - 1, prev); - } else // first pixel - setPixelColor(i, curnew); + if (last != prev) setPixelColorRaw(i - 1, prev); + } else setPixelColorRaw(i, curnew); // first pixel lastnew = curnew; last = cur; // save original value for comparison on next iteration carryover = part; } - setPixelColor(vlength - 1, curnew); + setPixelColorRaw(vlength - 1, curnew); } /* @@ -1125,17 +1063,23 @@ void Segment::blur(uint8_t blur_amount, bool smear) { * Inspired by the Adafruit examples. */ uint32_t Segment::color_wheel(uint8_t pos) const { - if (palette) return color_from_palette(pos, false, true, 0); // perhaps "strip.paletteBlend < 2" should be better instead of "true" - uint8_t w = W(currentColor(0)); + if (palette) return color_from_palette(pos, false, false, 0); // never wrap palette + uint8_t w = W(getCurrentColor(0)); pos = 255 - pos; - if (pos < 85) { - return RGBW32((255 - pos * 3), 0, (pos * 3), w); - } else if(pos < 170) { - pos -= 85; - return RGBW32(0, (pos * 3), (255 - pos * 3), w); + if (useRainbowWheel) { + CRGB rgb; + hsv2rgb_rainbow(CHSV(pos, 255, 255), rgb); + return RGBW32(rgb.r, rgb.g, rgb.b, w); } else { - pos -= 170; - return RGBW32((pos * 3), (255 - pos * 3), 0, w); + if (pos < 85) { + return RGBW32((255 - pos * 3), 0, (pos * 3), w); + } else if (pos < 170) { + pos -= 85; + return RGBW32(0, (pos * 3), (255 - pos * 3), w); + } else { + pos -= 170; + return RGBW32((pos * 3), (255 - pos * 3), 0, w); + } } } @@ -1143,24 +1087,32 @@ uint32_t Segment::color_wheel(uint8_t pos) const { * Gets a single color from the currently selected palette. * @param i Palette Index (if mapping is true, the full palette will be _virtualSegmentLength long, if false, 255). Will wrap around automatically. * @param mapping if true, LED position in segment is considered for color - * @param wrap FastLED palettes will usually wrap back to the start smoothly. Set false to get a hard edge + * @param moving FastLED palettes will usually wrap back to the start smoothly. Set to true if effect has moving palette and you want wrap. * @param mcol If the default palette 0 is selected, return the standard color 0, 1 or 2 instead. If >2, Party palette is used instead * @param pbri Value to scale the brightness of the returned color by. Default is 255. (no scaling) * @returns Single color from palette */ -uint32_t Segment::color_from_palette(uint16_t i, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri) const { - uint32_t color = gamma32(currentColor(mcol)); - +uint32_t Segment::color_from_palette(uint16_t i, bool mapping, bool moving, uint8_t mcol, uint8_t pbri) const { + uint32_t color = getCurrentColor(mcol); // default palette or no RGB support on segment - if ((palette == 0 && mcol < NUM_COLORS) || !_isRGB) return (pbri == 255) ? color : color_fade(color, pbri, true); + if ((palette == 0 && mcol < NUM_COLORS) || !_isRGB) { + return color_fade(color, pbri, true); + } unsigned paletteIndex = i; - if (mapping && virtualLength() > 1) paletteIndex = (i*255)/(virtualLength() -1); - // paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) - if (!wrap && strip.paletteBlend != 3) paletteIndex = scale8(paletteIndex, 240); //cut off blend at palette "end" - CRGB fastled_col = ColorFromPalette(_currentPalette, paletteIndex, pbri, (strip.paletteBlend == 3)? NOBLEND:LINEARBLEND); // NOTE: paletteBlend should be global + if (mapping) paletteIndex = min((i*255)/vLength(), 255U); + // paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined/no interpolation of palette entries) + // ColorFromPalette interpolations are: NOBLEND, LINEARBLEND, LINEARBLEND_NOWRAP + TBlendType blend = NOBLEND; + switch (paletteBlend) { + case 0: blend = moving ? LINEARBLEND : LINEARBLEND_NOWRAP; break; + case 1: blend = LINEARBLEND; break; + case 2: blend = LINEARBLEND_NOWRAP; break; + } + CRGBW palcol = ColorFromPalette(_currentPalette, paletteIndex, pbri, blend); + palcol.w = W(color); - return RGBW32(fastled_col.r, fastled_col.g, fastled_col.b, W(color)); + return palcol.color32; } @@ -1180,105 +1132,51 @@ void WS2812FX::finalizeInit() { enumerateLedmaps(); _hasWhiteChannel = _isOffRefreshRequired = false; + BusManager::removeAll(); - //if busses failed to load, add default (fresh install, FS issue, ...) - if (BusManager::getNumBusses() == 0) { - DEBUG_PRINTLN(F("No busses, init default")); - constexpr unsigned defDataTypes[] = {LED_TYPES}; - constexpr unsigned defDataPins[] = {DATA_PINS}; - constexpr unsigned defCounts[] = {PIXEL_COUNTS}; - constexpr unsigned defNumTypes = ((sizeof defDataTypes) / (sizeof defDataTypes[0])); - constexpr unsigned defNumPins = ((sizeof defDataPins) / (sizeof defDataPins[0])); - constexpr unsigned defNumCounts = ((sizeof defCounts) / (sizeof defCounts[0])); - - static_assert(validatePinsAndTypes(defDataTypes, defNumTypes, defNumPins), - "The default pin list defined in DATA_PINS does not match the pin requirements for the default buses defined in LED_TYPES"); - - unsigned prevLen = 0; - unsigned pinsIndex = 0; - for (unsigned i = 0; i < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; i++) { - uint8_t defPin[OUTPUT_MAX_PINS]; - // if we have less types than requested outputs and they do not align, use last known type to set current type - unsigned dataType = defDataTypes[(i < defNumTypes) ? i : defNumTypes -1]; - unsigned busPins = Bus::getNumberOfPins(dataType); - - // if we need more pins than available all outputs have been configured - if (pinsIndex + busPins > defNumPins) break; - - // Assign all pins first so we can check for conflicts on this bus - for (unsigned j = 0; j < busPins && j < OUTPUT_MAX_PINS; j++) defPin[j] = defDataPins[pinsIndex + j]; - - for (unsigned j = 0; j < busPins && j < OUTPUT_MAX_PINS; j++) { - bool validPin = true; - // When booting without config (1st boot) we need to make sure GPIOs defined for LED output don't clash with hardware - // i.e. DEBUG (GPIO1), DMX (2), SPI RAM/FLASH (16&17 on ESP32-WROVER/PICO), read/only pins, etc. - // Pin should not be already allocated, read/only or defined for current bus - while (PinManager::isPinAllocated(defPin[j]) || !PinManager::isPinOk(defPin[j],true)) { - if (validPin) { - DEBUG_PRINTLN(F("Some of the provided pins cannot be used to configure this LED output.")); - defPin[j] = 1; // start with GPIO1 and work upwards - validPin = false; - } else if (defPin[j] < WLED_NUM_PINS) { - defPin[j]++; - } else { - DEBUG_PRINTLN(F("No available pins left! Can't configure output.")); - return; - } - // is the newly assigned pin already defined or used previously? - // try next in line until there are no clashes or we run out of pins - bool clash; - do { - clash = false; - // check for conflicts on current bus - for (const auto &pin : defPin) { - if (&pin != &defPin[j] && pin == defPin[j]) { - clash = true; - break; - } - } - // We already have a clash on current bus, no point checking next buses - if (!clash) { - // check for conflicts in defined pins - for (const auto &pin : defDataPins) { - if (pin == defPin[j]) { - clash = true; - break; - } - } - } - if (clash) defPin[j]++; - if (defPin[j] >= WLED_NUM_PINS) break; - } while (clash); - } - } - pinsIndex += busPins; - - unsigned start = prevLen; - // if we have less counts than pins and they do not align, use last known count to set current count - unsigned count = defCounts[(i < defNumCounts) ? i : defNumCounts -1]; - // analog always has length 1 - if (Bus::isPWM(dataType) || Bus::isOnOff(dataType)) count = 1; - prevLen += count; - BusConfig defCfg = BusConfig(dataType, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY, 0, useGlobalLedBuffer); - if (BusManager::add(defCfg) == -1) break; + unsigned digitalCount = 0; + #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) + // determine if it is sensible to use parallel I2S outputs on ESP32 (i.e. more than 5 outputs = 1 I2S + 4 RMT) + unsigned maxLedsOnBus = 0; + for (const auto &bus : busConfigs) { + if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type)) { + digitalCount++; + if (bus.count > maxLedsOnBus) maxLedsOnBus = bus.count; } } + DEBUG_PRINTF_P(PSTR("Maximum LEDs on a bus: %u\nDigital buses: %u\n"), maxLedsOnBus, digitalCount); + // we may remove 300 LEDs per bus limit when NeoPixelBus is updated beyond 2.9.0 + if (maxLedsOnBus <= 300 && useParallelI2S) BusManager::useParallelOutput(); // must call before creating buses + else useParallelI2S = false; // enforce single I2S + #endif + + // create buses/outputs + unsigned mem = 0; + digitalCount = 0; + for (const auto &bus : busConfigs) { + mem += bus.memUsage(Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) ? digitalCount++ : 0); // includes global buffer + if (mem <= MAX_LED_MEMORY) { + if (BusManager::add(bus) == -1) break; + } else DEBUG_PRINTF_P(PSTR("Out of LED memory! Bus %d (%d) #%u not created."), (int)bus.type, (int)bus.count, digitalCount); + } + busConfigs.clear(); + busConfigs.shrink_to_fit(); _length = 0; - for (int i=0; igetStart() + bus->getLength() > MAX_LEDS) break; + if (!bus || !bus->isOk() || bus->getStart() + bus->getLength() > MAX_LEDS) break; //RGBW mode is enabled if at least one of the strips is RGBW _hasWhiteChannel |= bus->hasWhite(); //refresh is required to remain off if at least one of the strips requires the refresh. _isOffRefreshRequired |= bus->isOffRefreshRequired() && !bus->isPWM(); // use refresh bit for phase shift with analog unsigned busEnd = bus->getStart() + bus->getLength(); if (busEnd > _length) _length = busEnd; - // This must be done after all buses have been created, as some kinds (parallel I2S) interact bus->begin(); + bus->setBrightness(bri); } + DEBUG_PRINTF_P(PSTR("Heap after buses: %d\n"), ESP.getFreeHeap()); Segment::maxWidth = _length; Segment::maxHeight = 1; @@ -1288,17 +1186,22 @@ void WS2812FX::finalizeInit() { loadCustomPalettes(); // (re)load all custom palettes DEBUG_PRINTLN(F("Loading custom ledmaps")); deserializeMap(); // (re)load default ledmap (will also setUpMatrix() if ledmap does not exist) + + // allocate frame buffer after matrix has been set up (gaps!) + if (_pixels) _pixels = static_cast(d_realloc(_pixels, getLengthTotal() * sizeof(uint32_t))); + else _pixels = static_cast(d_malloc(getLengthTotal() * sizeof(uint32_t))); + DEBUG_PRINTF_P(PSTR("strip buffer size: %uB\n"), getLengthTotal() * sizeof(uint32_t)); + + DEBUG_PRINTF_P(PSTR("Heap after strip init: %uB\n"), ESP.getFreeHeap()); } void WS2812FX::service() { unsigned long nowUp = millis(); // Be aware, millis() rolls over every 49 days now = nowUp + timebase; - if (_suspend) return; unsigned long elapsed = nowUp - _lastServiceShow; - - if (elapsed <= MIN_FRAME_DELAY) return; // keep wifi alive - no matter if triggered or unlimited - if ( !_triggered && (_targetFps != FPS_UNLIMITED)) { // unlimited mode = no frametime - if (elapsed < _frametime) return; // too early for service + if (_suspend || elapsed <= MIN_FRAME_DELAY) return; // keep wifi alive - no matter if triggered or unlimited + if (!_triggered && (_targetFps != FPS_UNLIMITED)) { // unlimited mode = no frametime + if (elapsed < _frametime) return; // too early for service } bool doShow = false; @@ -1306,10 +1209,10 @@ void WS2812FX::service() { _isServicing = true; _segment_index = 0; - for (segment &seg : _segments) { - if (_suspend) return; // immediately stop processing segments if suspend requested during service() + for (Segment &seg : _segments) { + if (_suspend) break; // immediately stop processing segments if suspend requested during service() - // process transition (mode changes in the middle of transition) + // process transition (also pre-calculates progress value) seg.handleTransition(); // reset the segment runtime data if needed seg.resetIfRequired(); @@ -1317,93 +1220,403 @@ void WS2812FX::service() { if (!seg.isActive()) continue; // last condition ensures all solid segments are updated at the same time - if (nowUp >= seg.next_time || _triggered || (doShow && seg.mode == FX_MODE_STATIC)) + if (nowUp > seg.next_time || _triggered || (doShow && seg.mode == FX_MODE_STATIC)) { doShow = true; unsigned frameDelay = FRAMETIME; if (!seg.freeze) { //only run effect function if not frozen int oldCCT = BusManager::getSegmentCCT(); // store original CCT value (actually it is not Segment based) - _virtualSegmentLength = seg.virtualLength(); //SEGLEN - _colors_t[0] = gamma32(seg.currentColor(0)); - _colors_t[1] = gamma32(seg.currentColor(1)); - _colors_t[2] = gamma32(seg.currentColor(2)); - seg.setCurrentPalette(); // load actual palette // when correctWB is true we need to correct/adjust RGB value according to desired CCT value, but it will also affect actual WW/CW ratio // when cctFromRgb is true we implicitly calculate WW and CW from RGB values if (cctFromRgb) BusManager::setSegmentCCT(-1); - else BusManager::setSegmentCCT(seg.currentBri(true), correctWB); + else BusManager::setSegmentCCT(seg.currentCCT(), correctWB); // Effect blending - // When two effects are being blended, each may have different segment data, this - // data needs to be saved first and then restored before running previous mode. - // The blending will largely depend on the effect behaviour since actual output (LEDs) may be - // overwritten by later effect. To enable seamless blending for every effect, additional LED buffer - // would need to be allocated for each effect and then blended together for each pixel. - [[maybe_unused]] uint8_t tmpMode = seg.currentMode(); // this will return old mode while in transition - frameDelay = (*_mode[seg.mode])(); // run new/current mode -#ifndef WLED_DISABLE_MODE_BLEND - if (modeBlending && seg.mode != tmpMode) { - Segment::tmpsegd_t _tmpSegData; - Segment::modeBlend(true); // set semaphore - seg.swapSegenv(_tmpSegData); // temporarily store new mode state (and swap it with transitional state) - _virtualSegmentLength = seg.virtualLength(); // update SEGLEN (mapping may have changed) - unsigned d2 = (*_mode[tmpMode])(); // run old mode - seg.restoreSegenv(_tmpSegData); // restore mode state (will also update transitional state) - frameDelay = min(frameDelay,d2); // use shortest delay - Segment::modeBlend(false); // unset semaphore - } -#endif + uint16_t prog = seg.progress(); + seg.beginDraw(prog); // set up parameters for get/setPixelColor() (will also blend colors and palette if blend style is FADE) + _currentSegment = &seg; // set current segment for effect functions (SEGMENT & SEGENV) + // workaround for on/off transition to respect blending style + frameDelay = (*_mode[seg.mode])(); // run new/current mode (needed for bri workaround) seg.call++; + // if segment is in transition and no old segment exists we don't need to run the old mode + // (blendSegments() takes care of On/Off transitions and clipping) + Segment *segO = seg.getOldSegment(); + if (segO && (seg.mode != segO->mode || blendingStyle != BLEND_STYLE_FADE)) { + Segment::modeBlend(true); // set semaphore for beginDraw() to blend colors and palette + segO->beginDraw(prog); // set up palette & colors (also sets draw dimensions), parent segment has transition progress + _currentSegment = segO; // set current segment + // workaround for on/off transition to respect blending style + frameDelay = min(frameDelay, (unsigned)(*_mode[segO->mode])()); // run old mode (needed for bri workaround; semaphore!!) + segO->call++; // increment old mode run counter + Segment::modeBlend(false); // unset semaphore + } if (seg.isInTransition() && frameDelay > FRAMETIME) frameDelay = FRAMETIME; // force faster updates during transition - BusManager::setSegmentCCT(oldCCT); // restore old CCT for ABL adjustments + BusManager::setSegmentCCT(oldCCT); // restore old CCT for ABL adjustments } seg.next_time = nowUp + frameDelay; } _segment_index++; } - _virtualSegmentLength = 0; - _isServicing = false; - _triggered = false; #ifdef WLED_DEBUG if ((_targetFps != FPS_UNLIMITED) && (millis() - nowUp > _frametime)) DEBUG_PRINTF_P(PSTR("Slow effects %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime); #endif - if (doShow) { + if (doShow && !_suspend) { yield(); Segment::handleRandomPalette(); // slowly transition random palette; move it into for loop when each segment has individual random palette - show(); _lastServiceShow = nowUp; // update timestamp, for precise FPS control + show(); } #ifdef WLED_DEBUG if ((_targetFps != FPS_UNLIMITED) && (millis() - nowUp > _frametime)) DEBUG_PRINTF_P(PSTR("Slow strip %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime); #endif + + _triggered = false; + _isServicing = false; } -void IRAM_ATTR WS2812FX::setPixelColor(unsigned i, uint32_t col) { - i = getMappedPixelIndex(i); - if (i >= _length) return; - BusManager::setPixelColor(i, col); +// https://en.wikipedia.org/wiki/Blend_modes but using a for top layer & b for bottom layer +static uint8_t _top (uint8_t a, uint8_t b) { return a; } +static uint8_t _bottom (uint8_t a, uint8_t b) { return b; } +static uint8_t _add (uint8_t a, uint8_t b) { unsigned t = a + b; return t > 255 ? 255 : t; } +static uint8_t _subtract (uint8_t a, uint8_t b) { return b > a ? (b - a) : 0; } +static uint8_t _difference(uint8_t a, uint8_t b) { return b > a ? (b - a) : (a - b); } +static uint8_t _average (uint8_t a, uint8_t b) { return (a + b) >> 1; } +#ifdef CONFIG_IDF_TARGET_ESP32C3 +static uint8_t _multiply (uint8_t a, uint8_t b) { return ((a * b) + 255) >> 8; } // faster than division on C3 but slightly less accurate +#else +static uint8_t _multiply (uint8_t a, uint8_t b) { return (a * b) / 255; } // origianl uses a & b in range [0,1] +#endif +static uint8_t _divide (uint8_t a, uint8_t b) { return a > b ? (b * 255) / a : 255; } +static uint8_t _lighten (uint8_t a, uint8_t b) { return a > b ? a : b; } +static uint8_t _darken (uint8_t a, uint8_t b) { return a < b ? a : b; } +static uint8_t _screen (uint8_t a, uint8_t b) { return 255 - _multiply(~a,~b); } // 255 - (255-a)*(255-b)/255 +static uint8_t _overlay (uint8_t a, uint8_t b) { return b < 128 ? 2 * _multiply(a,b) : (255 - 2 * _multiply(~a,~b)); } +static uint8_t _hardlight (uint8_t a, uint8_t b) { return a < 128 ? 2 * _multiply(a,b) : (255 - 2 * _multiply(~a,~b)); } +#ifdef CONFIG_IDF_TARGET_ESP32C3 +static uint8_t _softlight (uint8_t a, uint8_t b) { return (((b * b * (255 - 2 * a) + 255) >> 8) + 2 * a * b + 255) >> 8; } // Pegtop's formula (1 - 2a)b^2 + 2ab +#else +static uint8_t _softlight (uint8_t a, uint8_t b) { return (b * b * (255 - 2 * a) / 255 + 2 * a * b) / 255; } // Pegtop's formula (1 - 2a)b^2 + 2ab +#endif +static uint8_t _dodge (uint8_t a, uint8_t b) { return _divide(~a,b); } +static uint8_t _burn (uint8_t a, uint8_t b) { return ~_divide(a,~b); } + +void WS2812FX::blendSegment(const Segment &topSegment) const { + + typedef uint8_t(*FuncType)(uint8_t, uint8_t); + FuncType funcs[] = { + _top, _bottom, + _add, _subtract, _difference, _average, + _multiply, _divide, _lighten, _darken, _screen, _overlay, + _hardlight, _softlight, _dodge, _burn + }; + + const size_t blendMode = topSegment.blendMode < (sizeof(funcs) / sizeof(FuncType)) ? topSegment.blendMode : 0; + const auto func = funcs[blendMode]; // blendMode % (sizeof(funcs) / sizeof(FuncType)) + const auto blend = [&](uint32_t top, uint32_t bottom){ return RGBW32(func(R(top),R(bottom)), func(G(top),G(bottom)), func(B(top),B(bottom)), func(W(top),W(bottom))); }; + + const int length = topSegment.length(); // physical segment length (counts all pixels in 2D segment) + const int width = topSegment.width(); + const int height = topSegment.height(); + const auto XY = [](int x, int y){ return x + y*Segment::maxWidth; }; + const size_t matrixSize = Segment::maxWidth * Segment::maxHeight; + const size_t startIndx = XY(topSegment.start, topSegment.startY); + const size_t stopIndx = startIndx + length; + const unsigned progress = topSegment.progress(); + const unsigned progInv = 0xFFFFU - progress; + uint8_t opacity = topSegment.currentBri(); // returns transitioned opacity for style FADE + + Segment::setClippingRect(0, 0); // disable clipping by default + + const unsigned dw = (blendingStyle==BLEND_STYLE_OUTSIDE_IN ? progInv : progress) * width / 0xFFFFU + 1; + const unsigned dh = (blendingStyle==BLEND_STYLE_OUTSIDE_IN ? progInv : progress) * height / 0xFFFFU + 1; + const unsigned orgBS = blendingStyle; + if (width*height == 1) blendingStyle = BLEND_STYLE_FADE; // disable style for single pixel segments (use fade instead) + switch (blendingStyle) { + case BLEND_STYLE_CIRCULAR_IN: // (must set entire segment, see isPixelXYClipped()) + case BLEND_STYLE_CIRCULAR_OUT:// (must set entire segment, see isPixelXYClipped()) + case BLEND_STYLE_FAIRY_DUST: // fairy dust (must set entire segment, see isPixelXYClipped()) + Segment::setClippingRect(0, width, 0, height); + break; + case BLEND_STYLE_SWIPE_RIGHT: // left-to-right + case BLEND_STYLE_PUSH_RIGHT: // left-to-right + Segment::setClippingRect(0, dw, 0, height); + break; + case BLEND_STYLE_SWIPE_LEFT: // right-to-left + case BLEND_STYLE_PUSH_LEFT: // right-to-left + Segment::setClippingRect(width - dw, width, 0, height); + break; + case BLEND_STYLE_OUTSIDE_IN: // corners + Segment::setClippingRect((width + dw)/2, (width - dw)/2, (height + dh)/2, (height - dh)/2); // inverted!! + break; + case BLEND_STYLE_INSIDE_OUT: // outward + Segment::setClippingRect((width - dw)/2, (width + dw)/2, (height - dh)/2, (height + dh)/2); + break; + case BLEND_STYLE_SWIPE_DOWN: // top-to-bottom (2D) + case BLEND_STYLE_PUSH_DOWN: // top-to-bottom (2D) + Segment::setClippingRect(0, width, 0, dh); + break; + case BLEND_STYLE_SWIPE_UP: // bottom-to-top (2D) + case BLEND_STYLE_PUSH_UP: // bottom-to-top (2D) + Segment::setClippingRect(0, width, height - dh, height); + break; + case BLEND_STYLE_OPEN_H: // horizontal-outward (2D) same look as INSIDE_OUT on 1D + Segment::setClippingRect((width - dw)/2, (width + dw)/2, 0, height); + break; + case BLEND_STYLE_OPEN_V: // vertical-outward (2D) + Segment::setClippingRect(0, width, (height - dh)/2, (height + dh)/2); + break; + case BLEND_STYLE_SWIPE_TL: // TL-to-BR (2D) + case BLEND_STYLE_PUSH_TL: // TL-to-BR (2D) + Segment::setClippingRect(0, dw, 0, dh); + break; + case BLEND_STYLE_SWIPE_TR: // TR-to-BL (2D) + case BLEND_STYLE_PUSH_TR: // TR-to-BL (2D) + Segment::setClippingRect(width - dw, width, 0, dh); + break; + case BLEND_STYLE_SWIPE_BR: // BR-to-TL (2D) + case BLEND_STYLE_PUSH_BR: // BR-to-TL (2D) + Segment::setClippingRect(width - dw, width, height - dh, height); + break; + case BLEND_STYLE_SWIPE_BL: // BL-to-TR (2D) + case BLEND_STYLE_PUSH_BL: // BL-to-TR (2D) + Segment::setClippingRect(0, dw, height - dh, height); + break; + } + + if (isMatrix && stopIndx <= matrixSize) { +#ifndef WLED_DISABLE_2D + const int nCols = topSegment.virtualWidth(); + const int nRows = topSegment.virtualHeight(); + const Segment *segO = topSegment.getOldSegment(); + const int oCols = segO ? segO->virtualWidth() : nCols; + const int oRows = segO ? segO->virtualHeight() : nRows; + + const auto setMirroredPixel = [&](int x, int y, uint32_t c, uint8_t o) { + const int baseX = topSegment.start + x; + const int baseY = topSegment.startY + y; + size_t indx = XY(baseX, baseY); // absolute address on strip + _pixels[indx] = color_blend(_pixels[indx], blend(c, _pixels[indx]), o); + // Apply mirroring + if (topSegment.mirror || topSegment.mirror_y) { + const int mirrorX = topSegment.start + width - x - 1; + const int mirrorY = topSegment.startY + height - y - 1; + const size_t idxMX = XY(topSegment.transpose ? baseX : mirrorX, topSegment.transpose ? mirrorY : baseY); + const size_t idxMY = XY(topSegment.transpose ? mirrorX : baseX, topSegment.transpose ? baseY : mirrorY); + const size_t idxMM = XY(mirrorX, mirrorY); + if (topSegment.mirror) _pixels[idxMX] = color_blend(_pixels[idxMX], blend(c, _pixels[idxMX]), o); + if (topSegment.mirror_y) _pixels[idxMY] = color_blend(_pixels[idxMY], blend(c, _pixels[idxMY]), o); + if (topSegment.mirror && topSegment.mirror_y) _pixels[idxMM] = color_blend(_pixels[idxMM], blend(c, _pixels[idxMM]), o); + } + }; + + // if we blend using "push" style we need to "shift" canvas to left/right/up/down + unsigned offsetX = (blendingStyle == BLEND_STYLE_PUSH_UP || blendingStyle == BLEND_STYLE_PUSH_DOWN) ? 0 : progInv * nCols / 0xFFFFU; + unsigned offsetY = (blendingStyle == BLEND_STYLE_PUSH_LEFT || blendingStyle == BLEND_STYLE_PUSH_RIGHT) ? 0 : progInv * nRows / 0xFFFFU; + + // we only traverse new segment, not old one + for (int r = 0; r < nRows; r++) for (int c = 0; c < nCols; c++) { + const bool clipped = topSegment.isPixelXYClipped(c, r); + // if segment is in transition and pixel is clipped take old segment's pixel and opacity + const Segment *seg = clipped && segO ? segO : &topSegment; // pixel is never clipped for FADE + int vCols = seg == segO ? oCols : nCols; // old segment may have different dimensions + int vRows = seg == segO ? oRows : nRows; // old segment may have different dimensions + int x = c; + int y = r; + // if we blend using "push" style we need to "shift" canvas to left/right/up/down + switch (blendingStyle) { + case BLEND_STYLE_PUSH_RIGHT: x = (x + offsetX) % nCols; break; + case BLEND_STYLE_PUSH_LEFT: x = (x - offsetX + nCols) % nCols; break; + case BLEND_STYLE_PUSH_DOWN: y = (y + offsetY) % nRows; break; + case BLEND_STYLE_PUSH_UP: y = (y - offsetY + nRows) % nRows; break; + } + uint32_t c_a = BLACK; + if (x < vCols && y < vRows) c_a = seg->getPixelColorRaw(x + y*vCols); // will get clipped pixel from old segment or unclipped pixel from new segment + if (segO && blendingStyle == BLEND_STYLE_FADE && topSegment.mode != segO->mode && x < oCols && y < oRows) { + // we need to blend old segment using fade as pixels ae not clipped + c_a = color_blend16(c_a, segO->getPixelColorRaw(x + y*oCols), progInv); + } else if (blendingStyle != BLEND_STYLE_FADE) { + // workaround for On/Off transition + // (bri != briT) && !bri => from On to Off + // (bri != briT) && bri => from Off to On + if ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri)) c_a = BLACK; + } + // map it into frame buffer + x = c; // restore coordiates if we were PUSHing + y = r; + if (topSegment.reverse ) x = nCols - x - 1; + if (topSegment.reverse_y) y = nRows - y - 1; + if (topSegment.transpose) std::swap(x,y); // swap X & Y if segment transposed + // expand pixel + const unsigned groupLen = topSegment.groupLength(); + if (groupLen == 1) { + setMirroredPixel(x, y, c_a, opacity); + } else { + // handle grouping and spacing + x *= groupLen; // expand to physical pixels + y *= groupLen; // expand to physical pixels + const int maxX = std::min(x + topSegment.grouping, width); + const int maxY = std::min(y + topSegment.grouping, height); + while (y < maxY) { + int _x = x; + while (_x < maxX) setMirroredPixel(_x++, y, c_a, opacity); + y++; + } + } + } +#endif + } else { + const int nLen = topSegment.virtualLength(); + const Segment *segO = topSegment.getOldSegment(); + const int oLen = segO ? segO->virtualLength() : nLen; + + const auto setMirroredPixel = [&](int i, uint32_t c, uint8_t o) { + int indx = topSegment.start + i; + // Apply mirroring + if (topSegment.mirror) { + unsigned indxM = topSegment.stop - i - 1; + indxM += topSegment.offset; // offset/phase + if (indxM >= topSegment.stop) indxM -= length; // wrap + _pixels[indxM] = color_blend(_pixels[indxM], blend(c, _pixels[indxM]), o); + } + indx += topSegment.offset; // offset/phase + if (indx >= topSegment.stop) indx -= length; // wrap + _pixels[indx] = color_blend(_pixels[indx], blend(c, _pixels[indx]), o); + }; + + // if we blend using "push" style we need to "shift" canvas to left/right/ + unsigned offsetI = progInv * nLen / 0xFFFFU; + + for (int k = 0; k < nLen; k++) { + const bool clipped = topSegment.isPixelClipped(k); + // if segment is in transition and pixel is clipped take old segment's pixel and opacity + const Segment *seg = clipped && segO ? segO : &topSegment; // pixel is never clipped for FADE + const int vLen = seg == segO ? oLen : nLen; + int i = k; + // if we blend using "push" style we need to "shift" canvas to left or right + switch (blendingStyle) { + case BLEND_STYLE_PUSH_RIGHT: i = (i + offsetI) % nLen; break; + case BLEND_STYLE_PUSH_LEFT: i = (i - offsetI + nLen) % nLen; break; + } + uint32_t c_a = BLACK; + if (i < vLen) c_a = seg->getPixelColorRaw(i); // will get clipped pixel from old segment or unclipped pixel from new segment + if (segO && blendingStyle == BLEND_STYLE_FADE && topSegment.mode != segO->mode && i < oLen) { + // we need to blend old segment using fade as pixels are not clipped + c_a = color_blend16(c_a, segO->getPixelColorRaw(i), progInv); + } else if (blendingStyle != BLEND_STYLE_FADE) { + // workaround for On/Off transition + // (bri != briT) && !bri => from On to Off + // (bri != briT) && bri => from Off to On + if ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri)) c_a = BLACK; + } + // map into frame buffer + i = k; // restore index if we were PUSHing + if (topSegment.reverse) i = nLen - i - 1; // is segment reversed? + // expand pixel + i *= topSegment.groupLength(); + // set all the pixels in the group + const int maxI = std::min(i + topSegment.grouping, length); // make sure to not go beyond physical length + while (i < maxI) setMirroredPixel(i++, c_a, opacity); + } + } + + blendingStyle = orgBS; + Segment::setClippingRect(0, 0); // disable clipping for overlays } -uint32_t IRAM_ATTR WS2812FX::getPixelColor(unsigned i) const { - i = getMappedPixelIndex(i); - if (i >= _length) return 0; - return BusManager::getPixelColor(i); +// To disable brightness limiter we either set output max current to 0 or single LED current to 0 +static uint8_t estimateCurrentAndLimitBri(uint8_t brightness, uint32_t *pixels) { + unsigned milliAmpsMax = BusManager::ablMilliampsMax(); + if (milliAmpsMax > 0) { + unsigned milliAmpsTotal = 0; + unsigned avgMilliAmpsPerLED = 0; + unsigned lengthDigital = 0; + bool useWackyWS2815PowerModel = false; + + for (size_t i = 0; i < BusManager::getNumBusses(); i++) { + const Bus *bus = BusManager::getBus(i); + if (!(bus && bus->isDigital() && bus->isOk())) continue; + unsigned maPL = bus->getLEDCurrent(); + if (maPL == 0 || bus->getMaxCurrent() > 0) continue; // skip buses with 0 mA per LED or max current per bus defined (PP-ABL) + if (maPL == 255) { + useWackyWS2815PowerModel = true; + maPL = 12; // WS2815 uses 12mA per channel + } + avgMilliAmpsPerLED += maPL * bus->getLength(); + lengthDigital += bus->getLength(); + // sum up the usage of each LED on digital bus + uint32_t busPowerSum = 0; + for (unsigned j = 0; j < bus->getLength(); j++) { + uint32_t c = pixels[j + bus->getStart()]; + byte r = R(c), g = G(c), b = B(c), w = W(c); + if (useWackyWS2815PowerModel) { //ignore white component on WS2815 power calculation + busPowerSum += (max(max(r,g),b)) * 3; + } else { + busPowerSum += (r + g + b + w); + } + } + // RGBW led total output with white LEDs enabled is still 50mA, so each channel uses less + if (bus->hasWhite()) { + busPowerSum *= 3; + busPowerSum >>= 2; //same as /= 4 + } + // powerSum has all the values of channels summed (max would be getLength()*765 as white is excluded) so convert to milliAmps + milliAmpsTotal += (busPowerSum * maPL * brightness) / (765*255); + } + if (lengthDigital > 0) { + avgMilliAmpsPerLED /= lengthDigital; + + if (milliAmpsMax > MA_FOR_ESP && avgMilliAmpsPerLED > 0) { //0 mA per LED and too low numbers turn off calculation + unsigned powerBudget = (milliAmpsMax - MA_FOR_ESP); //80/120mA for ESP power + if (powerBudget > lengthDigital) { //each LED uses about 1mA in standby, exclude that from power budget + powerBudget -= lengthDigital; + } else { + powerBudget = 0; + } + if (milliAmpsTotal > powerBudget) { + //scale brightness down to stay in current limit + unsigned scaleB = powerBudget * 255 / milliAmpsTotal; + brightness = ((brightness * scaleB) >> 8) + 1; + } + } + } + } + return brightness; } void WS2812FX::show() { + unsigned long showNow = millis(); + size_t diff = showNow - _lastShow; + + size_t totalLen = getLengthTotal(); + if (realtimeMode == REALTIME_MODE_INACTIVE || useMainSegmentOnly || realtimeOverride > REALTIME_OVERRIDE_NONE) { + // clear frame buffer + for (size_t i = 0; i < totalLen; i++) _pixels[i] = BLACK; // memset(_pixels, 0, sizeof(uint32_t) * getLengthTotal()); + // blend all segments into (cleared) buffer + for (Segment &seg : _segments) if (seg.isActive() && (seg.on || seg.isInTransition())) { + blendSegment(seg); // blend segment's buffer into frame buffer + } + } + // avoid race condition, capture _callback value show_callback callback = _callback; - if (callback) callback(); - unsigned long showNow = millis(); + if (callback) callback(); // will call setPixelColor or setRealtimePixelColor + + // determine ABL brightness + uint8_t newBri = estimateCurrentAndLimitBri(_brightness, _pixels); + if (newBri != _brightness) BusManager::setBrightness(newBri); + + // paint actuall pixels + for (size_t i = 0; i < totalLen; i++) BusManager::setPixelColor(getMappedPixelIndex(i), realtimeMode && arlsDisableGammaCorrection ? _pixels[i] : gamma32(_pixels[i])); // some buses send asynchronously and this method will return before // all of the data has been sent. // See https://github.com/Makuna/NeoPixelBus/wiki/ESP32-NeoMethods#neoesp32rmt-methods BusManager::show(); - size_t diff = showNow - _lastShow; + // restore brightness for next frame + if (newBri != _brightness) BusManager::setBrightness(_brightness); if (diff > 0) { // skip calculation if no time has passed size_t fpsCurr = (1000 << FPS_CALC_SHIFT) / diff; // fixed point math @@ -1412,52 +1625,48 @@ void WS2812FX::show() { } } -/** - * Returns a true value if any of the strips are still being updated. - * On some hardware (ESP32), strip updates are done asynchronously. - */ -bool WS2812FX::isUpdating() const { - return !BusManager::canAllShow(); +void WS2812FX::setRealtimePixelColor(unsigned i, uint32_t c) { + if (useMainSegmentOnly) { + const Segment &seg = getMainSegment(); + if (seg.isActive() && i < seg.length()) seg.setPixelColorRaw(i, c); + } else { + setPixelColor(i, c); + } } -/** - * Returns the refresh rate of the LED strip. Useful for finding out whether a given setup is fast enough. - * Only updates on show() or is set to 0 fps if last show is more than 2 secs ago, so accuracy varies - */ -uint16_t WS2812FX::getFps() const { - if (millis() - _lastShow > 2000) return 0; - return (FPS_MULTIPLIER * _cumulativeFps) >> FPS_CALC_SHIFT; // _cumulativeFps is stored in fixed point +// reset all segments +void WS2812FX::restartRuntime() { + suspend(); + waitForIt(); + for (Segment &seg : _segments) seg.markForReset().resetIfRequired(); + resume(); } -void WS2812FX::setTargetFps(uint8_t fps) { +// start or stop transition for all segments +void WS2812FX::setTransitionMode(bool t) { + suspend(); + waitForIt(); + for (Segment &seg : _segments) seg.startTransition(t ? _transitionDur : 0); + resume(); +} + +// wait until frame is over (service() has finished or time for 1 frame has passed; yield() crashes on 8266) +void WS2812FX::waitForIt() { + unsigned long maxWait = millis() + getFrameTime(); + while (isServicing() && maxWait > millis()) delay(1); + #ifdef WLED_DEBUG + if (millis() >= maxWait) DEBUG_PRINTLN(F("Waited for strip to finish servicing.")); + #endif +}; + +void WS2812FX::setTargetFps(unsigned fps) { if (fps <= 250) _targetFps = fps; if (_targetFps > 0) _frametime = 1000 / _targetFps; else _frametime = MIN_FRAME_DELAY; // unlimited mode } -void WS2812FX::setMode(uint8_t segid, uint8_t m) { - if (segid >= _segments.size()) return; - - if (m >= getModeCount()) m = getModeCount() - 1; - - if (_segments[segid].mode != m) { - _segments[segid].setMode(m); // do not load defaults - } -} - -//applies to all active and selected segments -void WS2812FX::setColor(uint8_t slot, uint32_t c) { - if (slot >= NUM_COLORS) return; - - for (segment &seg : _segments) { - if (seg.isActive() && seg.isSelected()) { - seg.setColor(slot, c); - } - } -} - void WS2812FX::setCCT(uint16_t k) { - for (segment &seg : _segments) { + for (Segment &seg : _segments) { if (seg.isActive() && seg.isSelected()) { seg.setCCT(k); } @@ -1471,22 +1680,18 @@ void WS2812FX::setBrightness(uint8_t b, bool direct) { if (_brightness == b) return; _brightness = b; if (_brightness == 0) { //unfreeze all segments on power off - for (segment &seg : _segments) { - seg.freeze = false; - } + for (const Segment &seg : _segments) seg.freeze = false; // freeze is mutable } - // setting brightness with NeoPixelBusLg has no effect on already painted pixels, - // so we need to force an update to existing buffer BusManager::setBrightness(b); if (!direct) { unsigned long t = millis(); - if (_segments[0].next_time > t + 22 && t - _lastShow > MIN_FRAME_DELAY) trigger(); //apply brightness change immediately if no refresh soon + if (_segments[0].next_time > t + 22 && t - _lastShow > MIN_SHOW_DELAY) trigger(); //apply brightness change immediately if no refresh soon } } uint8_t WS2812FX::getActiveSegsLightCapabilities(bool selectedOnly) const { uint8_t totalLC = 0; - for (const segment &seg : _segments) { + for (const Segment &seg : _segments) { if (seg.isActive() && (!selectedOnly || seg.isSelected())) totalLC |= seg.getLightCapabilities(); } return totalLC; @@ -1494,7 +1699,7 @@ uint8_t WS2812FX::getActiveSegsLightCapabilities(bool selectedOnly) const { uint8_t WS2812FX::getFirstSelectedSegId() const { size_t i = 0; - for (const segment &seg : _segments) { + for (const Segment &seg : _segments) { if (seg.isActive() && seg.isSelected()) return i; i++; } @@ -1502,9 +1707,9 @@ uint8_t WS2812FX::getFirstSelectedSegId() const { return getMainSegmentId(); } -void WS2812FX::setMainSegmentId(uint8_t n) { - _mainSegment = 0; - if (n < _segments.size()) { +void WS2812FX::setMainSegmentId(unsigned n) { + _mainSegment = getLastActiveSegmentId(); + if (n < _segments.size() && _segments[n].isActive()) { // only set if segment is active _mainSegment = n; } return; @@ -1518,10 +1723,8 @@ uint8_t WS2812FX::getLastActiveSegmentId() const { } uint8_t WS2812FX::getActiveSegmentsNum() const { - uint8_t c = 0; - for (size_t i = 0; i < _segments.size(); i++) { - if (_segments[i].isActive()) c++; - } + unsigned c = 0; + for (const Segment &seg : _segments) if (seg.isActive()) c++; return c; } @@ -1532,13 +1735,7 @@ uint16_t WS2812FX::getLengthTotal() const { } uint16_t WS2812FX::getLengthPhysical() const { - unsigned len = 0; - for (size_t b = 0; b < BusManager::getNumBusses(); b++) { - Bus *bus = BusManager::getBus(b); - if (bus->isVirtual()) continue; //exclude non-physical network busses - len += bus->getLength(); - } - return len; + return BusManager::getTotalLength(true); } //used for JSON API info.leds.rgbw. Little practical use, deprecate with info.leds.rgbw. @@ -1546,8 +1743,8 @@ uint16_t WS2812FX::getLengthPhysical() const { //not influenced by auto-white mode, also true if white slider does not affect output white channel bool WS2812FX::hasRGBWBus() const { for (size_t b = 0; b < BusManager::getNumBusses(); b++) { - Bus *bus = BusManager::getBus(b); - if (bus == nullptr || bus->getLength()==0) break; + const Bus *bus = BusManager::getBus(b); + if (!bus || !bus->isOk()) break; if (bus->hasRGB() && bus->hasWhite()) return true; } return false; @@ -1556,8 +1753,8 @@ bool WS2812FX::hasRGBWBus() const { bool WS2812FX::hasCCTBus() const { if (cctFromRgb && !correctWB) return false; for (size_t b = 0; b < BusManager::getNumBusses(); b++) { - Bus *bus = BusManager::getBus(b); - if (bus == nullptr || bus->getLength()==0) break; + const Bus *bus = BusManager::getBus(b); + if (!bus || !bus->isOk()) break; if (bus->hasCCT()) return true; } return false; @@ -1578,32 +1775,14 @@ void WS2812FX::purgeSegments() { } } -Segment& WS2812FX::getSegment(uint8_t id) { +Segment& WS2812FX::getSegment(unsigned id) { return _segments[id >= _segments.size() ? getMainSegmentId() : id]; // vectors } -// sets new segment bounds, queues if that segment is currently running -void WS2812FX::setSegment(uint8_t segId, uint16_t i1, uint16_t i2, uint8_t grouping, uint8_t spacing, uint16_t offset, uint16_t startY, uint16_t stopY) { - if (segId >= getSegmentsNum()) { - if (i2 <= i1) return; // do not append empty/inactive segments - appendSegment(Segment(0, strip.getLengthTotal())); - segId = getSegmentsNum()-1; // segments are added at the end of list - } - suspend(); - _segments[segId].setUp(i1, i2, grouping, spacing, offset, startY, stopY); - resume(); - if (segId > 0 && segId == getSegmentsNum()-1 && i2 <= i1) _segments.pop_back(); // if last segment was deleted remove it from vector -} - void WS2812FX::resetSegments() { - _segments.clear(); // destructs all Segment as part of clearing - #ifndef WLED_DISABLE_2D - segment seg = isMatrix ? Segment(0, Segment::maxWidth, 0, Segment::maxHeight) : Segment(0, _length); - #else - segment seg = Segment(0, _length); - #endif - _segments.push_back(seg); - _segments.shrink_to_fit(); // just in case ... + _segments.clear(); // destructs all Segment as part of clearing + _segments.emplace_back(0, isMatrix ? Segment::maxWidth : _length, 0, isMatrix ? Segment::maxHeight : 1); + _segments.shrink_to_fit(); // just in case ... _mainSegment = 0; } @@ -1623,10 +1802,11 @@ void WS2812FX::makeAutoSegments(bool forceReset) { #endif for (size_t i = s; i < BusManager::getNumBusses(); i++) { - Bus* b = BusManager::getBus(i); + const Bus *bus = BusManager::getBus(i); + if (!bus || !bus->isOk()) break; - segStarts[s] = b->getStart(); - segStops[s] = segStarts[s] + b->getLength(); + segStarts[s] = bus->getStart(); + segStops[s] = segStarts[s] + bus->getLength(); #ifndef WLED_DISABLE_2D if (isMatrix && segStops[s] <= Segment::maxWidth*Segment::maxHeight) continue; // ignore buses comprising matrix @@ -1650,12 +1830,12 @@ void WS2812FX::makeAutoSegments(bool forceReset) { // there is always at least one segment (but we need to differentiate between 1D and 2D) #ifndef WLED_DISABLE_2D if (isMatrix) - _segments.push_back(Segment(0, Segment::maxWidth, 0, Segment::maxHeight)); + _segments.emplace_back(0, Segment::maxWidth, 0, Segment::maxHeight); else #endif - _segments.push_back(Segment(segStarts[0], segStops[0])); + _segments.emplace_back(segStarts[0], segStops[0]); for (size_t i = 1; i < s; i++) { - _segments.push_back(Segment(segStarts[i], segStops[i])); + _segments.emplace_back(segStarts[i], segStops[i]); } DEBUG_PRINTF_P(PSTR("%d auto segments created.\n"), _segments.size()); @@ -1666,15 +1846,9 @@ void WS2812FX::makeAutoSegments(bool forceReset) { else if (getActiveSegmentsNum() == 1) { size_t i = getLastActiveSegmentId(); #ifndef WLED_DISABLE_2D - _segments[i].start = 0; - _segments[i].stop = Segment::maxWidth; - _segments[i].startY = 0; - _segments[i].stopY = Segment::maxHeight; - _segments[i].grouping = 1; - _segments[i].spacing = 0; + _segments[i].setGeometry(0, Segment::maxWidth, 1, 0, 0xFFFF, 0, Segment::maxHeight); #else - _segments[i].start = 0; - _segments[i].stop = _length; + _segments[i].setGeometry(0, _length); #endif } } @@ -1706,17 +1880,18 @@ void WS2812FX::fixInvalidSegments() { // if any segments were deleted free memory purgeSegments(); // this is always called as the last step after finalizeInit(), update covered bus types - for (segment &seg : _segments) + for (const Segment &seg : _segments) seg.refreshLightCapabilities(); } //true if all segments align with a bus, or if a segment covers the total length //irrelevant in 2D set-up -bool WS2812FX::checkSegmentAlignment() { +bool WS2812FX::checkSegmentAlignment() const { bool aligned = false; - for (segment &seg : _segments) { + for (const Segment &seg : _segments) { for (unsigned b = 0; bisOk()) break; if (seg.start == bus->getStart() && seg.stop == bus->getStart() + bus->getLength()) aligned = true; } if (seg.start == 0 && seg.stop == _length) aligned = true; @@ -1743,59 +1918,9 @@ void WS2812FX::printSize() { } #endif -void WS2812FX::loadCustomPalettes() { - byte tcp[72]; //support gradient palettes with up to 18 entries - CRGBPalette16 targetPalette; - customPalettes.clear(); // start fresh - for (int index = 0; index<10; index++) { - char fileName[32]; - sprintf_P(fileName, PSTR("/palette%d.json"), index); - - StaticJsonDocument<1536> pDoc; // barely enough to fit 72 numbers - if (WLED_FS.exists(fileName)) { - DEBUG_PRINT(F("Reading palette from ")); - DEBUG_PRINTLN(fileName); - - if (readObjectFromFile(fileName, nullptr, &pDoc)) { - JsonArray pal = pDoc[F("palette")]; - if (!pal.isNull() && pal.size()>3) { // not an empty palette (at least 2 entries) - if (pal[0].is() && pal[1].is()) { - // we have an array of index & hex strings - size_t palSize = MIN(pal.size(), 36); - palSize -= palSize % 2; // make sure size is multiple of 2 - for (size_t i=0, j=0; i()<256; i+=2, j+=4) { - uint8_t rgbw[] = {0,0,0,0}; - tcp[ j ] = (uint8_t) pal[ i ].as(); // index - colorFromHexString(rgbw, pal[i+1].as()); // will catch non-string entires - for (size_t c=0; c<3; c++) tcp[j+1+c] = gamma8(rgbw[c]); // only use RGB component - DEBUG_PRINTF_P(PSTR("%d(%d) : %d %d %d\n"), i, int(tcp[j]), int(tcp[j+1]), int(tcp[j+2]), int(tcp[j+3])); - } - } else { - size_t palSize = MIN(pal.size(), 72); - palSize -= palSize % 4; // make sure size is multiple of 4 - for (size_t i=0; i()<256; i+=4) { - tcp[ i ] = (uint8_t) pal[ i ].as(); // index - tcp[i+1] = gamma8((uint8_t) pal[i+1].as()); // R - tcp[i+2] = gamma8((uint8_t) pal[i+2].as()); // G - tcp[i+3] = gamma8((uint8_t) pal[i+3].as()); // B - DEBUG_PRINTF_P(PSTR("%d(%d) : %d %d %d\n"), i, int(tcp[i]), int(tcp[i+1]), int(tcp[i+2]), int(tcp[i+3])); - } - } - customPalettes.push_back(targetPalette.loadDynamicGradientPalette(tcp)); - } else { - DEBUG_PRINTLN(F("Wrong palette format.")); - } - } - } else { - break; - } - } -} - -//load custom mapping table from JSON file (called from finalizeInit() or deserializeState()) -bool WS2812FX::deserializeMap(uint8_t n) { - // 2D support creates its own ledmap (on the fly) if a ledmap.json exists it will overwrite built one. - +// load custom mapping table from JSON file (called from finalizeInit() or deserializeState()) +// if this is a matrix set-up and default ledmap.json file does not exist, create mapping table using setUpMatrix() from panel information +bool WS2812FX::deserializeMap(unsigned n) { char fileName[32]; strcpy_P(fileName, PSTR("/ledmap")); if (n) sprintf(fileName +7, "%d", n); @@ -1807,54 +1932,89 @@ bool WS2812FX::deserializeMap(uint8_t n) { if (n == 0 || isFile) interfaceUpdateCallMode = CALL_MODE_WS_SEND; // schedule WS update (to inform UI) if (!isFile && n==0 && isMatrix) { + // 2D panel support creates its own ledmap (on the fly) if a ledmap.json does not exist setUpMatrix(); return false; } if (!isFile || !requestJSONBufferLock(7)) return false; - if (!readObjectFromFile(fileName, nullptr, pDoc)) { - DEBUG_PRINT(F("ERROR Invalid ledmap in ")); DEBUG_PRINTLN(fileName); + StaticJsonDocument<64> filter; + filter[F("width")] = true; + filter[F("height")] = true; + if (!readObjectFromFile(fileName, nullptr, pDoc, &filter)) { + DEBUG_PRINTF_P(PSTR("ERROR Invalid ledmap in %s\n"), fileName); releaseJSONBufferLock(); return false; // if file does not load properly then exit - } + } else + DEBUG_PRINTF_P(PSTR("Reading LED map from %s\n"), fileName); + + suspend(); + waitForIt(); JsonObject root = pDoc->as(); // if we are loading default ledmap (at boot) set matrix width and height from the ledmap (compatible with WLED MM ledmaps) - if (isMatrix && n == 0 && (!root[F("width")].isNull() || !root[F("height")].isNull())) { - Segment::maxWidth = min(max(root[F("width")].as(), 1), 128); - Segment::maxHeight = min(max(root[F("height")].as(), 1), 128); + if (n == 0 && (!root[F("width")].isNull() || !root[F("height")].isNull())) { + Segment::maxWidth = min(max(root[F("width")].as(), 1), 255); + Segment::maxHeight = min(max(root[F("height")].as(), 1), 255); + isMatrix = true; } - if (customMappingTable) delete[] customMappingTable; - customMappingTable = new uint16_t[getLengthTotal()]; + d_free(customMappingTable); + customMappingTable = static_cast(d_malloc(sizeof(uint16_t)*getLengthTotal())); // do not use SPI RAM if (customMappingTable) { - DEBUG_PRINT(F("Reading LED map from ")); DEBUG_PRINTLN(fileName); + DEBUG_PRINTF_P(PSTR("ledmap allocated: %uB\n"), sizeof(uint16_t)*getLengthTotal()); + File f = WLED_FS.open(fileName, "r"); + f.find("\"map\":["); + while (f.available()) { // f.position() < f.size() - 1 + char number[32]; + size_t numRead = f.readBytesUntil(',', number, sizeof(number)-1); // read a single number (may include array terminating "]" but not number separator ',') + number[numRead] = 0; + if (numRead > 0) { + char *end = strchr(number,']'); // we encountered end of array so stop processing if no digit found + bool foundDigit = (end == nullptr); + int i = 0; + if (end != nullptr) do { + if (number[i] >= '0' && number[i] <= '9') foundDigit = true; + if (foundDigit || &number[i++] == end) break; + } while (i < 32); + if (!foundDigit) break; + int index = atoi(number); + if (index < 0 || index > 16384) index = 0xFFFF; + customMappingTable[customMappingSize++] = index; + if (customMappingSize > getLengthTotal()) break; + } else break; // there was nothing to read, stop + } + currentLedmap = n; + f.close(); + + #ifdef WLED_DEBUG + DEBUG_PRINT(F("Loaded ledmap:")); + for (unsigned i=0; i 0); } -uint16_t IRAM_ATTR WS2812FX::getMappedPixelIndex(uint16_t index) const { - // convert logical address to physical - if (index < customMappingSize - && (realtimeMode == REALTIME_MODE_INACTIVE || realtimeRespectLedMaps)) index = customMappingTable[index]; - - return index; -} - - -WS2812FX* WS2812FX::instance = nullptr; const char JSON_mode_names[] PROGMEM = R"=====(["FX names moved"])====="; const char JSON_palette_names[] PROGMEM = R"=====([ @@ -1865,5 +2025,5 @@ const char JSON_palette_names[] PROGMEM = R"=====([ "Magenta","Magred","Yelmag","Yelblu","Orange & Teal","Tiamat","April Night","Orangery","C9","Sakura", "Aurora","Atlantica","C9 2","C9 New","Temperature","Aurora 2","Retro Clown","Candy","Toxy Reaf","Fairy Reaf", "Semi Blue","Pink Candy","Red Reaf","Aqua Flash","Yelblu Hot","Lite Light","Red Flash","Blink Red","Red Shift","Red Tide", -"Candy2" +"Candy2","Traffic Light" ])====="; diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp new file mode 100644 index 00000000..c07aec39 --- /dev/null +++ b/wled00/FXparticleSystem.cpp @@ -0,0 +1,1963 @@ +/* + FXparticleSystem.cpp + + Particle system with functions for particle generation, particle movement and particle rendering to RGB matrix. + by DedeHai (Damian Schneider) 2013-2024 + + Copyright (c) 2024 Damian Schneider + Licensed under the EUPL v. 1.2 or later +*/ + +#ifdef WLED_DISABLE_2D +#define WLED_DISABLE_PARTICLESYSTEM2D +#endif + +#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled +#include "FXparticleSystem.h" +// local shared functions (used both in 1D and 2D system) +static int32_t calcForce_dv(const int8_t force, uint8_t &counter); +static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, const bool wrap); // returns false if out of bounds by more than particleradius +static void fast_color_add(CRGB &c1, const CRGB &c2, uint8_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) +static void fast_color_scale(CRGB &c, const uint8_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 +//static CRGB *allocateCRGBbuffer(uint32_t length); +#endif + +#ifndef WLED_DISABLE_PARTICLESYSTEM2D +ParticleSystem2D::ParticleSystem2D(uint32_t width, uint32_t height, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced, bool sizecontrol) { + PSPRINTLN("\n ParticleSystem2D constructor"); + numSources = numberofsources; // number of sources allocated in init + numParticles = numberofparticles; // number of particles allocated in init + usedParticles = numParticles; // use all particles by default + advPartProps = nullptr; //make sure we start out with null pointers (just in case memory was not cleared) + advPartSize = nullptr; + setMatrixSize(width, height); + updatePSpointers(isadvanced, sizecontrol); // set the particle and sources pointer (call this before accessing sprays or particles) + setWallHardness(255); // set default wall hardness to max + setWallRoughness(0); // smooth walls by default + setGravity(0); //gravity disabled by default + setParticleSize(1); // 2x2 rendering size by default + motionBlur = 0; //no fading by default + smearBlur = 0; //no smearing by default + emitIndex = 0; + collisionStartIdx = 0; + + //initialize some default non-zero values most FX use + for (uint32_t i = 0; i < numParticles; i++) { + particles[i].sat = 255; // full saturation + } + for (uint32_t i = 0; i < numSources; i++) { + sources[i].source.sat = 255; //set saturation to max by default + sources[i].source.ttl = 1; //set source alive + sources[i].sourceFlags.asByte = 0; // all flags disabled + } + +} + +// update function applies gravity, moves the particles, handles collisions and renders the particles +void ParticleSystem2D::update(void) { + //apply gravity globally if enabled + if (particlesettings.useGravity) + applyGravity(); + + //update size settings before handling collisions + if (advPartSize) { + for (uint32_t i = 0; i < usedParticles; i++) { + if (updateSize(&advPartProps[i], &advPartSize[i]) == false) { // if particle shrinks to 0 size + particles[i].ttl = 0; // kill particle + } + } + } + + // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) + if (particlesettings.useCollisions) + handleCollisions(); + + //move all particles + for (uint32_t i = 0; i < usedParticles; i++) { + particleMoveUpdate(particles[i], particleFlags[i], nullptr, advPartProps ? &advPartProps[i] : nullptr); // note: splitting this into two loops is slower and uses more flash + } + + render(); +} + +// update function for fire animation +void ParticleSystem2D::updateFire(const uint8_t intensity,const bool renderonly) { + if (!renderonly) + fireParticleupdate(); + fireIntesity = intensity > 0 ? intensity : 1; // minimum of 1, zero checking is used in render function + render(); +} + +// set percentage of used particles as uint8_t i.e 127 means 50% for example +void ParticleSystem2D::setUsedParticles(uint8_t percentage) { + usedParticles = (numParticles * ((int)percentage+1)) >> 8; // number of particles to use (percentage is 0-255, 255 = 100%) + PSPRINT(" SetUsedpaticles: allocated particles: "); + PSPRINT(numParticles); + PSPRINT(" ,used particles: "); + PSPRINTLN(usedParticles); +} + +void ParticleSystem2D::setWallHardness(uint8_t hardness) { + wallHardness = hardness; +} + +void ParticleSystem2D::setWallRoughness(uint8_t roughness) { + wallRoughness = roughness; +} + +void ParticleSystem2D::setCollisionHardness(uint8_t hardness) { + collisionHardness = (int)hardness + 1; +} + +void ParticleSystem2D::setMatrixSize(uint32_t x, uint32_t y) { + maxXpixel = x - 1; // last physical pixel that can be drawn to + maxYpixel = y - 1; + maxX = x * PS_P_RADIUS - 1; // particle system boundary for movements + maxY = y * PS_P_RADIUS - 1; // this value is often needed (also by FX) to calculate positions +} + +void ParticleSystem2D::setWrapX(bool enable) { + particlesettings.wrapX = enable; +} + +void ParticleSystem2D::setWrapY(bool enable) { + particlesettings.wrapY = enable; +} + +void ParticleSystem2D::setBounceX(bool enable) { + particlesettings.bounceX = enable; +} + +void ParticleSystem2D::setBounceY(bool enable) { + particlesettings.bounceY = enable; +} + +void ParticleSystem2D::setKillOutOfBounds(bool enable) { + particlesettings.killoutofbounds = enable; +} + +void ParticleSystem2D::setColorByAge(bool enable) { + particlesettings.colorByAge = enable; +} + +void ParticleSystem2D::setMotionBlur(uint8_t bluramount) { + if (particlesize < 2) // only allow motion blurring on default particle sizes or advanced size (cannot combine motion blur with normal blurring used for particlesize, would require another buffer) + motionBlur = bluramount; +} + +void ParticleSystem2D::setSmearBlur(uint8_t bluramount) { + smearBlur = bluramount; +} + + +// render size using smearing (see blur function) +void ParticleSystem2D::setParticleSize(uint8_t size) { + particlesize = size; + particleHardRadius = PS_P_MINHARDRADIUS; // ~1 pixel + if (particlesize > 1) { + particleHardRadius = max(particleHardRadius, (uint32_t)particlesize); // radius used for wall collisions & particle collisions + motionBlur = 0; // disable motion blur if particle size is set + } + else if (particlesize == 0) + particleHardRadius = particleHardRadius >> 1; // single pixel particles have half the radius (i.e. 1/2 pixel) +} + +// enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable +// if enabled, gravity is applied to all particles in ParticleSystemUpdate() +// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results) +void ParticleSystem2D::setGravity(int8_t force) { + if (force) { + gforce = force; + particlesettings.useGravity = true; + } else { + particlesettings.useGravity = false; + } +} + +void ParticleSystem2D::enableParticleCollisions(bool enable, uint8_t hardness) { // enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is also disable + particlesettings.useCollisions = enable; + collisionHardness = (int)hardness + 1; +} + +// emit one particle with variation, returns index of emitted particle (or -1 if no particle emitted) +int32_t ParticleSystem2D::sprayEmit(const PSsource &emitter) { + bool success = false; + for (uint32_t i = 0; i < usedParticles; i++) { + emitIndex++; + if (emitIndex >= usedParticles) + emitIndex = 0; + if (particles[emitIndex].ttl == 0) { // find a dead particle + success = true; + particles[emitIndex].vx = emitter.vx + hw_random16(emitter.var << 1) - emitter.var; // random(-var, var) + particles[emitIndex].vy = emitter.vy + hw_random16(emitter.var << 1) - emitter.var; // random(-var, var) + particles[emitIndex].x = emitter.source.x; + particles[emitIndex].y = emitter.source.y; + particles[emitIndex].hue = emitter.source.hue; + particles[emitIndex].sat = emitter.source.sat; + particleFlags[emitIndex].collide = emitter.sourceFlags.collide; + particles[emitIndex].ttl = hw_random16(emitter.minLife, emitter.maxLife); + if (advPartProps) + advPartProps[emitIndex].size = emitter.size; + break; + } + } + if (success) + return emitIndex; + else + return -1; +} + +// Spray emitter for particles used for flames (particle TTL depends on source TTL) +void ParticleSystem2D::flameEmit(const PSsource &emitter) { + int emitIndex = sprayEmit(emitter); + if (emitIndex > 0) particles[emitIndex].ttl += emitter.source.ttl; +} + +// Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var +// angle = 0 means in positive x-direction (i.e. to the right) +int32_t ParticleSystem2D::angleEmit(PSsource &emitter, const uint16_t angle, const int32_t speed) { + emitter.vx = ((int32_t)cos16_t(angle) * speed) / (int32_t)32600; // cos16_t() and sin16_t() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding + emitter.vy = ((int32_t)sin16_t(angle) * speed) / (int32_t)32600; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + return sprayEmit(emitter); +} + +// particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 +// uses passed settings to set bounce or wrap, if useGravity is enabled, it will never bounce at the top and killoutofbounds is not applied over the top +void ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSparticleFlags &partFlags, PSsettings2D *options, PSadvancedParticle *advancedproperties) { + if (options == nullptr) + options = &particlesettings; //use PS system settings by default + + if (part.ttl > 0) { + if (!partFlags.perpetual) + part.ttl--; // age + if (options->colorByAge) + part.hue = min(part.ttl, (uint16_t)255); //set color to ttl + + int32_t renderradius = PS_P_HALFRADIUS; // used to check out of bounds + int32_t newX = part.x + (int32_t)part.vx; + int32_t newY = part.y + (int32_t)part.vy; + partFlags.outofbounds = false; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) note: moving this to checks below adds code and is not faster + + if (advancedproperties) { //using individual particle size? + setParticleSize(particlesize); // updates default particleHardRadius + if (advancedproperties->size > PS_P_MINHARDRADIUS) { + particleHardRadius += (advancedproperties->size - PS_P_MINHARDRADIUS); // update radius + renderradius = particleHardRadius; + } + } + // note: if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle does not go half out of view + if (options->bounceY) { + if ((newY < (int32_t)particleHardRadius) || ((newY > (int32_t)(maxY - particleHardRadius)) && !options->useGravity)) { // reached floor / ceiling + bounce(part.vy, part.vx, newY, maxY); + } + } + + if (!checkBoundsAndWrap(newY, maxY, renderradius, options->wrapY)) { // check out of bounds note: this must not be skipped. if gravity is enabled, particles will never bounce at the top + partFlags.outofbounds = true; + if (options->killoutofbounds) { + if (newY < 0) // if gravity is enabled, only kill particles below ground + part.ttl = 0; + else if (!options->useGravity) + part.ttl = 0; + } + } + + if (part.ttl) { //check x direction only if still alive + if (options->bounceX) { + if ((newX < (int32_t)particleHardRadius) || (newX > (int32_t)(maxX - particleHardRadius))) // reached a wall + bounce(part.vx, part.vy, newX, maxX); + } + else if (!checkBoundsAndWrap(newX, maxX, renderradius, options->wrapX)) { // check out of bounds + partFlags.outofbounds = true; + if (options->killoutofbounds) + part.ttl = 0; + } + } + + part.x = (int16_t)newX; // set new position + part.y = (int16_t)newY; // set new position + } +} + +// move function for fire particles +void ParticleSystem2D::fireParticleupdate() { + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[i].ttl > 0) + { + particles[i].ttl--; // age + int32_t newY = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter + int32_t newX = particles[i].x + (int32_t)particles[i].vx; + particleFlags[i].outofbounds = 0; // reset out of bounds flag note: moving this to checks below is not faster but adds code + // check if particle is out of bounds, wrap x around to other side if wrapping is enabled + // as fire particles start below the frame, lots of particles are out of bounds in y direction. to improve speed, only check x direction if y is not out of bounds + if (newY < -PS_P_HALFRADIUS) + particleFlags[i].outofbounds = 1; + else if (newY > int32_t(maxY + PS_P_HALFRADIUS)) // particle moved out at the top + particles[i].ttl = 0; + else // particle is in frame in y direction, also check x direction now Note: using checkBoundsAndWrap() is slower, only saves a few bytes + { + if ((newX < 0) || (newX > (int32_t)maxX)) { // handle out of bounds & wrap + if (particlesettings.wrapX) { + newX = newX % (maxX + 1); + if (newX < 0) // handle negative modulo + newX += maxX + 1; + } + else if ((newX < -PS_P_HALFRADIUS) || (newX > int32_t(maxX + PS_P_HALFRADIUS))) { //if fully out of view + particles[i].ttl = 0; + } + } + particles[i].x = newX; + } + particles[i].y = newY; + } + } +} + +// update advanced particle size control, returns false if particle shrinks to 0 size +bool ParticleSystem2D::updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize) { + if (advsize == nullptr) // safety check + return false; + // grow/shrink particle + int32_t newsize = advprops->size; + uint32_t counter = advsize->sizecounter; + uint32_t increment = 0; + // calculate grow speed using 0-8 for low speeds and 9-15 for higher speeds + if (advsize->grow) increment = advsize->growspeed; + else if (advsize->shrink) increment = advsize->shrinkspeed; + if (increment < 9) { // 8 means +1 every frame + counter += increment; + if (counter > 7) { + counter -= 8; + increment = 1; + } else + increment = 0; + advsize->sizecounter = counter; + } else { + increment = (increment - 8) << 1; // 9 means +2, 10 means +4 etc. 15 means +14 + } + + if (advsize->grow) { + if (newsize < advsize->maxsize) { + newsize += increment; + if (newsize >= advsize->maxsize) { + advsize->grow = false; // stop growing, shrink from now on if enabled + newsize = advsize->maxsize; // limit + if (advsize->pulsate) advsize->shrink = true; + } + } + } else if (advsize->shrink) { + if (newsize > advsize->minsize) { + newsize -= increment; + if (newsize <= advsize->minsize) { + if (advsize->minsize == 0) + return false; // particle shrunk to zero + advsize->shrink = false; // disable shrinking + newsize = advsize->minsize; // limit + if (advsize->pulsate) advsize->grow = true; + } + } + } + advprops->size = newsize; + // handle wobbling + if (advsize->wobble) { + advsize->asymdir += advsize->wobblespeed; // note: if need better wobblespeed control a counter is already in the struct + } + return true; +} + +// calculate x and y size for asymmetrical particles (advanced size control) +void ParticleSystem2D::getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize) { + if (advsize == nullptr) // if advsize is valid, also advanced properties pointer is valid (handled by updatePSpointers()) + return; + int32_t size = advprops->size; + int32_t asymdir = advsize->asymdir; + int32_t deviation = ((uint32_t)size * (uint32_t)advsize->asymmetry + 255) >> 8; // deviation from symmetrical size + // Calculate x and y size based on deviation and direction (0 is symmetrical, 64 is x, 128 is symmetrical, 192 is y) + if (asymdir < 64) { + deviation = (asymdir * deviation) >> 6; + } else if (asymdir < 192) { + deviation = ((128 - asymdir) * deviation) >> 6; + } else { + deviation = ((asymdir - 255) * deviation) >> 6; + } + // Calculate x and y size based on deviation, limit to 255 (rendering function cannot handle larger sizes) + xsize = min((size - deviation), (int32_t)255); + ysize = min((size + deviation), (int32_t)255);; +} + +// function to bounce a particle from a wall using set parameters (wallHardness and wallRoughness) +void ParticleSystem2D::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition) { + incomingspeed = -incomingspeed; + incomingspeed = (incomingspeed * wallHardness + 128) >> 8; // reduce speed as energy is lost on non-hard surface + if (position < (int32_t)particleHardRadius) + position = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better + else + position = maxposition - particleHardRadius; + if (wallRoughness) { + int32_t incomingspeed_abs = abs((int32_t)incomingspeed); + int32_t totalspeed = incomingspeed_abs + abs((int32_t)parallelspeed); + // transfer an amount of incomingspeed speed to parallel speed + int32_t donatespeed = ((hw_random16(incomingspeed_abs << 1) - incomingspeed_abs) * (int32_t)wallRoughness) / (int32_t)255; // take random portion of + or - perpendicular speed, scaled by roughness + parallelspeed = limitSpeed((int32_t)parallelspeed + donatespeed); + // give the remainder of the speed to perpendicular speed + donatespeed = int8_t(totalspeed - abs(parallelspeed)); // keep total speed the same + incomingspeed = incomingspeed > 0 ? donatespeed : -donatespeed; + } +} + +// apply a force in x,y direction to individual particle +// caller needs to provide a 8bit counter (for each particle) that holds its value between calls +// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results) +void ParticleSystem2D::applyForce(PSparticle &part, const int8_t xforce, const int8_t yforce, uint8_t &counter) { + // for small forces, need to use a delay counter + uint8_t xcounter = counter & 0x0F; // lower four bits + uint8_t ycounter = counter >> 4; // upper four bits + + // velocity increase + int32_t dvx = calcForce_dv(xforce, xcounter); + int32_t dvy = calcForce_dv(yforce, ycounter); + + // save counter values back + counter = xcounter & 0x0F; // write lower four bits, make sure not to write more than 4 bits + counter |= (ycounter << 4) & 0xF0; // write upper four bits + + // apply the force to particle + part.vx = limitSpeed((int32_t)part.vx + dvx); + part.vy = limitSpeed((int32_t)part.vy + dvy); +} + +// apply a force in x,y direction to individual particle using advanced particle properties +void ParticleSystem2D::applyForce(const uint32_t particleindex, const int8_t xforce, const int8_t yforce) { + if (advPartProps == nullptr) + return; // no advanced properties available + applyForce(particles[particleindex], xforce, yforce, advPartProps[particleindex].forcecounter); +} + +// apply a force in x,y direction to all particles +// force is in 3.4 fixed point notation (see above) +void ParticleSystem2D::applyForce(const int8_t xforce, const int8_t yforce) { + // for small forces, need to use a delay counter + uint8_t tempcounter; + // note: this is not the most computationally efficient way to do this, but it saves on duplicate code and is fast enough + for (uint32_t i = 0; i < usedParticles; i++) { + tempcounter = forcecounter; + applyForce(particles[i], xforce, yforce, tempcounter); + } + forcecounter = tempcounter; // save value back +} + +// apply a force in angular direction to single particle +// caller needs to provide a 8bit counter that holds its value between calls (if using single particles, a counter for each particle is needed) +// angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) +// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame (useful force range is +/- 127) +void ParticleSystem2D::applyAngleForce(PSparticle &part, const int8_t force, const uint16_t angle, uint8_t &counter) { + int8_t xforce = ((int32_t)force * cos16_t(angle)) / 32767; // force is +/- 127 + int8_t yforce = ((int32_t)force * sin16_t(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + applyForce(part, xforce, yforce, counter); +} + +void ParticleSystem2D::applyAngleForce(const uint32_t particleindex, const int8_t force, const uint16_t angle) { + if (advPartProps == nullptr) + return; // no advanced properties available + applyAngleForce(particles[particleindex], force, angle, advPartProps[particleindex].forcecounter); +} + +// apply a force in angular direction to all particles +// angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) +void ParticleSystem2D::applyAngleForce(const int8_t force, const uint16_t angle) { + int8_t xforce = ((int32_t)force * cos16_t(angle)) / 32767; // force is +/- 127 + int8_t yforce = ((int32_t)force * sin16_t(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + applyForce(xforce, yforce); +} + +// apply gravity to all particles using PS global gforce setting +// force is in 3.4 fixed point notation, see note above +// note: faster than apply force since direction is always down and counter is fixed for all particles +void ParticleSystem2D::applyGravity() { + int32_t dv = calcForce_dv(gforce, gforcecounter); + if (dv == 0) return; + for (uint32_t i = 0; i < usedParticles; i++) { + // Note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways + particles[i].vy = limitSpeed((int32_t)particles[i].vy - dv); + } +} + +// apply gravity to single particle using system settings (use this for sources) +// function does not increment gravity counter, if gravity setting is disabled, this cannot be used +void ParticleSystem2D::applyGravity(PSparticle &part) { + uint32_t counterbkp = gforcecounter; // backup PS gravity counter + int32_t dv = calcForce_dv(gforce, gforcecounter); + gforcecounter = counterbkp; //save it back + part.vy = limitSpeed((int32_t)part.vy - dv); +} + +// slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) +// note: a coefficient smaller than 0 will speed them up (this is a feature, not a bug), coefficient larger than 255 inverts the speed, so don't do that +void ParticleSystem2D::applyFriction(PSparticle &part, const int32_t coefficient) { + // note: not checking if particle is dead can be done by caller (or can be omitted) + #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) + int32_t friction = 256 - coefficient; + part.vx = ((int32_t)part.vx * friction + (((int32_t)part.vx >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts + part.vy = ((int32_t)part.vy * friction + (((int32_t)part.vy >> 31) & 0xFF)) >> 8; + #else // division is faster on ESP32, S2 and S3 + int32_t friction = 255 - coefficient; + part.vx = ((int32_t)part.vx * friction) / 255; + part.vy = ((int32_t)part.vy * friction) / 255; + #endif +} + +// apply friction to all particles +// note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways +void ParticleSystem2D::applyFriction(const int32_t coefficient) { + #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) + int32_t friction = 256 - coefficient; + for (uint32_t i = 0; i < usedParticles; i++) { + particles[i].vx = ((int32_t)particles[i].vx * friction + (((int32_t)particles[i].vx >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts + particles[i].vy = ((int32_t)particles[i].vy * friction + (((int32_t)particles[i].vy >> 31) & 0xFF)) >> 8; + } + #else // division is faster on ESP32, S2 and S3 + int32_t friction = 255 - coefficient; + for (uint32_t i = 0; i < usedParticles; i++) { + particles[i].vx = ((int32_t)particles[i].vx * friction) / 255; + particles[i].vy = ((int32_t)particles[i].vy * friction) / 255; + } + #endif +} + +// attracts a particle to an attractor particle using the inverse square-law +void ParticleSystem2D::pointAttractor(const uint32_t particleindex, PSparticle &attractor, const uint8_t strength, const bool swallow) { + if (advPartProps == nullptr) + return; // no advanced properties available + + // Calculate the distance between the particle and the attractor + int32_t dx = attractor.x - particles[particleindex].x; + int32_t dy = attractor.y - particles[particleindex].y; + + // Calculate the force based on inverse square law + int32_t distanceSquared = dx * dx + dy * dy; + if (distanceSquared < 8192) { + if (swallow) { // particle is close, age it fast so it fades out, do not attract further + if (particles[particleindex].ttl > 7) + particles[particleindex].ttl -= 8; + else { + particles[particleindex].ttl = 0; + return; + } + } + distanceSquared = 2 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance to avoid very high forces + } + + int32_t force = ((int32_t)strength << 16) / distanceSquared; + int8_t xforce = (force * dx) / 1024; // scale to a lower value, found by experimenting + int8_t yforce = (force * dy) / 1024; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + applyForce(particleindex, xforce, yforce); +} + +// render particles to the LED buffer (uses palette to render the 8bit particle color value) +// if wrap is set, particles half out of bounds are rendered to the other side of the matrix +// warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds +// firemode is only used for PS Fire FX +void ParticleSystem2D::render() { + CRGB baseRGB; + uint32_t brightness; // particle brightness, fades if dying + TBlendType blend = LINEARBLEND; // default color rendering: wrap palette + if (particlesettings.colorByAge) { + blend = LINEARBLEND_NOWRAP; + } + + if (motionBlur) { // motion-blurring active + for (int32_t y = 0; y <= maxYpixel; y++) { + int index = y * (maxXpixel + 1); + for (int32_t x = 0; x <= maxXpixel; x++) { + fast_color_scale(framebuffer[index], motionBlur); // note: could skip if only globalsmear is active but usually they are both active and scaling is fast enough + index++; + } + } + } + else { // no blurring: clear buffer + memset(framebuffer, 0, (maxXpixel+1) * (maxYpixel+1) * sizeof(CRGB)); + } + + // go over particles and render them to the buffer + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[i].ttl == 0 || particleFlags[i].outofbounds) + continue; + // generate RGB values for particle + if (fireIntesity) { // fire mode + brightness = (uint32_t)particles[i].ttl * (3 + (fireIntesity >> 5)) + 5; + brightness = min(brightness, (uint32_t)255); + baseRGB = ColorFromPaletteWLED(SEGPALETTE, brightness, 255, LINEARBLEND_NOWRAP); + } + else { + brightness = min((particles[i].ttl << 1), (int)255); + baseRGB = ColorFromPaletteWLED(SEGPALETTE, particles[i].hue, 255, blend); + if (particles[i].sat < 255) { + CHSV32 baseHSV; + rgb2hsv((uint32_t((byte(baseRGB.r) << 16) | (byte(baseRGB.g) << 8) | (byte(baseRGB.b)))), baseHSV); // convert to HSV + baseHSV.s = min(baseHSV.s, particles[i].sat); // set the saturation but don't increase it + uint32_t tempcolor; + hsv2rgb(baseHSV, tempcolor); // convert back to RGB + baseRGB = (CRGB)tempcolor; + } + } + brightness = gamma8(brightness); // apply gamma correction, used for gamma-inverted brightness distribution + renderParticle(i, brightness, baseRGB, particlesettings.wrapX, particlesettings.wrapY); + } + + // apply global size rendering + if (particlesize > 1) { + uint32_t passes = particlesize / 64 + 1; // number of blur passes, four passes max + uint32_t bluramount = particlesize; + uint32_t bitshift = 0; + for (uint32_t i = 0; i < passes; i++) { + if (i == 2) // for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) + bitshift = 1; + blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, bluramount << bitshift, bluramount << bitshift); + bluramount -= 64; + } + } + + // apply 2D blur to rendered frame + if (smearBlur) { + blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, smearBlur, smearBlur); + } + + // transfer the framebuffer to the segment + for (int y = 0; y <= maxYpixel; y++) { + int index = y * (maxXpixel + 1); // current row index for 1D buffer + for (int x = 0; x <= maxXpixel; x++) { + SEGMENT.setPixelColorXY(x, y, framebuffer[index++]); + } + } +} + +// calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer +__attribute__((optimize("O2"))) void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB& color, const bool wrapX, const bool wrapY) { + uint32_t size = particlesize; + if (advPartProps && advPartProps[particleindex].size > 0) // use advanced size properties (0 means use global size including single pixel rendering) + size = advPartProps[particleindex].size; + + if (size == 0) { // single pixel rendering + uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT; + uint32_t y = particles[particleindex].y >> PS_P_RADIUS_SHIFT; + if (x <= (uint32_t)maxXpixel && y <= (uint32_t)maxYpixel) { + fast_color_add(framebuffer[x + (maxYpixel - y) * (maxXpixel + 1)], color, brightness); + } + return; + } + uint8_t pxlbrightness[4]; // brightness values for the four pixels representing a particle + struct { + int32_t x,y; + } pixco[4]; // particle pixel coordinates, the order is bottom left [0], bottom right[1], top right [2], top left [3] (thx @blazoncek for improved readability struct) + bool pixelvalid[4] = {true, true, true, true}; // is set to false if pixel is out of bounds + + // add half a radius as the rendering algorithm always starts at the bottom left, this leaves things positive, so shifts can be used, then shift coordinate by a full pixel (x--/y-- below) + int32_t xoffset = particles[particleindex].x + PS_P_HALFRADIUS; + int32_t yoffset = particles[particleindex].y + PS_P_HALFRADIUS; + int32_t dx = xoffset & (PS_P_RADIUS - 1); // relativ particle position in subpixel space + int32_t dy = yoffset & (PS_P_RADIUS - 1); // modulo replaced with bitwise AND, as radius is always a power of 2 + int32_t x = (xoffset >> PS_P_RADIUS_SHIFT); // divide by PS_P_RADIUS which is 64, so can bitshift (compiler can not optimize integer) + int32_t y = (yoffset >> PS_P_RADIUS_SHIFT); + + // set the four raw pixel coordinates + pixco[1].x = pixco[2].x = x; // bottom right & top right + pixco[2].y = pixco[3].y = y; // top right & top left + x--; // shift by a full pixel here, this is skipped above to not do -1 and then +1 + y--; + pixco[0].x = pixco[3].x = x; // bottom left & top left + pixco[0].y = pixco[1].y = y; // bottom left & bottom right + + // calculate brightness values for all four pixels representing a particle using linear interpolation + // could check for out of frame pixels here but calculating them is faster (very few are out) + // precalculate values for speed optimization + int32_t precal1 = (int32_t)PS_P_RADIUS - dx; + int32_t precal2 = ((int32_t)PS_P_RADIUS - dy) * brightness; + int32_t precal3 = dy * brightness; + pxlbrightness[0] = (precal1 * precal2) >> PS_P_SURFACE; // bottom left value equal to ((PS_P_RADIUS - dx) * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE + pxlbrightness[1] = (dx * precal2) >> PS_P_SURFACE; // bottom right value equal to (dx * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE + pxlbrightness[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightness) >> PS_P_SURFACE + pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightness) >> PS_P_SURFACE + // adjust brightness such that distribution is linear after gamma correction: + // - scale brigthness with gamma correction (done in render()) + // - apply inverse gamma correction to brightness values + // - gamma is applied again in show() -> the resulting brightness distribution is linear but gamma corrected in total + pxlbrightness[0] = gamma8inv(pxlbrightness[0]); // use look-up-table for invers gamma + pxlbrightness[1] = gamma8inv(pxlbrightness[1]); + pxlbrightness[2] = gamma8inv(pxlbrightness[2]); + pxlbrightness[3] = gamma8inv(pxlbrightness[3]); + + if (advPartProps && advPartProps[particleindex].size > 1) { //render particle to a bigger size + CRGB renderbuffer[100]; // 10x10 pixel buffer + memset(renderbuffer, 0, sizeof(renderbuffer)); // clear buffer + //particle size to pixels: < 64 is 4x4, < 128 is 6x6, < 192 is 8x8, bigger is 10x10 + //first, render the pixel to the center of the renderbuffer, then apply 2D blurring + fast_color_add(renderbuffer[4 + (4 * 10)], color, pxlbrightness[0]); // oCrder is: bottom left, bottom right, top right, top left + fast_color_add(renderbuffer[5 + (4 * 10)], color, pxlbrightness[1]); + fast_color_add(renderbuffer[5 + (5 * 10)], color, pxlbrightness[2]); + fast_color_add(renderbuffer[4 + (5 * 10)], color, pxlbrightness[3]); + uint32_t rendersize = 2; // initialize render size, minimum is 4x4 pixels, it is incremented int he loop below to start with 4 + uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below) + uint32_t maxsize = advPartProps[particleindex].size; + uint32_t xsize = maxsize; + uint32_t ysize = maxsize; + if (advPartSize) { // use advanced size control + if (advPartSize[particleindex].asymmetry > 0) + getParticleXYsize(&advPartProps[particleindex], &advPartSize[particleindex], xsize, ysize); + maxsize = (xsize > ysize) ? xsize : ysize; // choose the bigger of the two + } + maxsize = maxsize/64 + 1; // number of blur passes depends on maxsize, four passes max + uint32_t bitshift = 0; + for (uint32_t i = 0; i < maxsize; i++) { + if (i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) + bitshift = 1; + rendersize += 2; + offset--; + blur2D(renderbuffer, rendersize, rendersize, xsize << bitshift, ysize << bitshift, offset, offset, true); + xsize = xsize > 64 ? xsize - 64 : 0; + ysize = ysize > 64 ? ysize - 64 : 0; + } + + // calculate origin coordinates to render the particle to in the framebuffer + uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; + uint32_t yfb_orig = y - (rendersize>>1) + 1 - offset; + uint32_t xfb, yfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked (spits a warning though) + + //note on y-axis flip: WLED has the y-axis defined from top to bottom, so y coordinates must be flipped. doing this in the buffer xfer clashes with 1D/2D combined rendering, which does not invert y + // transferring the 1D buffer in inverted fashion will flip the x-axis of overlaid 2D FX, so the y-axis flip is done here so the buffer is flipped in y, giving correct results + + // transfer particle renderbuffer to framebuffer + for (uint32_t xrb = offset; xrb < rendersize + offset; xrb++) { + xfb = xfb_orig + xrb; + if (xfb > (uint32_t)maxXpixel) { + if (wrapX) { // wrap x to the other side if required + if (xfb > (uint32_t)maxXpixel << 1) // xfb is "negative", handle it + xfb = (maxXpixel + 1) + (int32_t)xfb; // this always overflows to within bounds + else + xfb = xfb % (maxXpixel + 1); // note: without the above "negative" check, this works only for powers of 2 + } + else + continue; + } + + for (uint32_t yrb = offset; yrb < rendersize + offset; yrb++) { + yfb = yfb_orig + yrb; + if (yfb > (uint32_t)maxYpixel) { + if (wrapY) {// wrap y to the other side if required + if (yfb > (uint32_t)maxYpixel << 1) // yfb is "negative", handle it + yfb = (maxYpixel + 1) + (int32_t)yfb; // this always overflows to within bounds + else + yfb = yfb % (maxYpixel + 1); // note: without the above "negative" check, this works only for powers of 2 + } + else + continue; + } + fast_color_add(framebuffer[xfb + (maxYpixel - yfb) * (maxXpixel + 1)], renderbuffer[xrb + yrb * 10]); + } + } + } else { // standard rendering (2x2 pixels) + // check for out of frame pixels and wrap them if required: x,y is bottom left pixel coordinate of the particle + if (x < 0) { // left pixels out of frame + if (wrapX) { // wrap x to the other side if required + pixco[0].x = pixco[3].x = maxXpixel; + } else { + pixelvalid[0] = pixelvalid[3] = false; // out of bounds + } + } + else if (pixco[1].x > (int32_t)maxXpixel) { // right pixels, only has to be checked if left pixel is in frame + if (wrapX) { // wrap y to the other side if required + pixco[1].x = pixco[2].x = 0; + } else { + pixelvalid[1] = pixelvalid[2] = false; // out of bounds + } + } + + if (y < 0) { // bottom pixels out of frame + if (wrapY) { // wrap y to the other side if required + pixco[0].y = pixco[1].y = maxYpixel; + } else { + pixelvalid[0] = pixelvalid[1] = false; // out of bounds + } + } + else if (pixco[2].y > maxYpixel) { // top pixels + if (wrapY) { // wrap y to the other side if required + pixco[2].y = pixco[3].y = 0; + } else { + pixelvalid[2] = pixelvalid[3] = false; // out of bounds + } + } + for (uint32_t i = 0; i < 4; i++) { + if (pixelvalid[i]) + fast_color_add(framebuffer[pixco[i].x + (maxYpixel - pixco[i].y) * (maxXpixel + 1)], color, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left + } + } +} + +// detect collisions in an array of particles and handle them +// uses binning by dividing the frame into slices in x direction which is efficient if using gravity in y direction (but less efficient for FX that use forces in x direction) +// for code simplicity, no y slicing is done, making very tall matrix configurations less efficient +// note: also tested adding y slicing, it gives diminishing returns, some FX even get slower. FX not using gravity would benefit with a 10% FPS improvement +void ParticleSystem2D::handleCollisions() { + uint32_t collDistSq = particleHardRadius << 1; // distance is double the radius note: particleHardRadius is updated when setting global particle size + collDistSq = collDistSq * collDistSq; // square it for faster comparison (square is one operation) + // note: partices are binned in x-axis, assumption is that no more than half of the particles are in the same bin + // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) + constexpr int BIN_WIDTH = 6 * PS_P_RADIUS; // width of a bin in sub-pixels + int32_t overlap = particleHardRadius << 1; // overlap bins to include edge particles to neighbouring bins + if (advPartProps) //may be using individual particle size + overlap += 512; // add 2 * max radius (approximately) + uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 2); // assume no more than half of the particles are in the same bin, do not bin small amounts of particles + uint32_t numBins = (maxX + (BIN_WIDTH - 1)) / BIN_WIDTH; // number of bins in x direction + uint16_t binIndices[maxBinParticles]; // creat array on stack for indices, 2kB max for 1024 particles (ESP32_MAXPARTICLES/2) + uint32_t binParticleCount; // number of particles in the current bin + uint16_t nextFrameStartIdx = hw_random16(usedParticles); // index of the first particle in the next frame (set to fixed value if bin overflow) + uint32_t pidx = collisionStartIdx; //start index in case a bin is full, process remaining particles next frame + + // fill the binIndices array for this bin + for (uint32_t bin = 0; bin < numBins; bin++) { + binParticleCount = 0; // reset for this bin + int32_t binStart = bin * BIN_WIDTH - overlap; // note: first bin will extend to negative, but that is ok as out of bounds particles are ignored + int32_t binEnd = binStart + BIN_WIDTH + overlap; // note: last bin can be out of bounds, see above; + + // fill the binIndices array for this bin + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[pidx].ttl > 0) { // is alive + if (particles[pidx].x >= binStart && particles[pidx].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins) + if(particleFlags[pidx].outofbounds == 0 && particleFlags[pidx].collide) { // particle is in frame and does collide note: checking flags is quite slow and usually these are set, so faster to check here + if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame + nextFrameStartIdx = pidx; // bin overflow can only happen once as bin size is at least half of the particles (or half +1) + break; + } + binIndices[binParticleCount++] = pidx; + } + } + } + pidx++; + if (pidx >= usedParticles) pidx = 0; // wrap around + } + + for (uint32_t i = 0; i < binParticleCount; i++) { // go though all 'higher number' particles in this bin and see if any of those are in close proximity and if they are, make them collide + uint32_t idx_i = binIndices[i]; + for (uint32_t j = i + 1; j < binParticleCount; j++) { // check against higher number particles + uint32_t idx_j = binIndices[j]; + if (advPartProps) { //may be using individual particle size + setParticleSize(particlesize); // updates base particleHardRadius + collDistSq = (particleHardRadius << 1) + (((uint32_t)advPartProps[idx_i].size + (uint32_t)advPartProps[idx_j].size) >> 1); // collision distance note: not 100% clear why the >> 1 is needed, but it is. + collDistSq = collDistSq * collDistSq; // square it for faster comparison + } + int32_t dx = (particles[idx_j].x + particles[idx_j].vx) - (particles[idx_i].x + particles[idx_i].vx); // distance with lookahead + if (dx * dx < collDistSq) { // check x direction, if close, check y direction (squaring is faster than abs() or dual compare) + int32_t dy = (particles[idx_j].y + particles[idx_j].vy) - (particles[idx_i].y + particles[idx_i].vy); // distance with lookahead + if (dy * dy < collDistSq) // particles are close + collideParticles(particles[idx_i], particles[idx_j], dx, dy, collDistSq); + } + } + } + } + collisionStartIdx = nextFrameStartIdx; // set the start index for the next frame +} + +// handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS +// takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) +__attribute__((optimize("O2"))) void ParticleSystem2D::collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy, const uint32_t collDistSq) { + int32_t distanceSquared = dx * dx + dy * dy; + // Calculate relative velocity note: could zero check but that does not improve overall speed but deminish it as that is rarely the case and pushing is still required + int32_t relativeVx = (int32_t)particle2.vx - (int32_t)particle1.vx; + int32_t relativeVy = (int32_t)particle2.vy - (int32_t)particle1.vy; + + // if dx and dy are zero (i.e. same position) give them an offset, if speeds are also zero, also offset them (pushes particles apart if they are clumped before enabling collisions) + if (distanceSquared == 0) { + // Adjust positions based on relative velocity direction + dx = -1; + if (relativeVx < 0) // if true, particle2 is on the right side + dx = 1; + else if (relativeVx == 0) + relativeVx = 1; + + dy = -1; + if (relativeVy < 0) + dy = 1; + else if (relativeVy == 0) + relativeVy = 1; + + distanceSquared = 2; // 1 + 1 + } + + // Calculate dot product of relative velocity and relative distance + int32_t dotProduct = (dx * relativeVx + dy * relativeVy); // is always negative if moving towards each other + + if (dotProduct < 0) {// particles are moving towards each other + // integer math used to avoid floats. + // overflow check: dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit, dotproduct/distsquared ist 8b, multiplied by collisionhardness of 8bit. so a 16bit shift is ok, make it 15 to be sure no overflows happen + // note: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate! the trick is: only shift positive numers + // Calculate new velocities after collision + int32_t surfacehardness = 1 + max(collisionHardness, (int32_t)PS_P_MINSURFACEHARDNESS); // if particles are soft, the impulse must stay above a limit or collisions slip through at higher speeds, 170 seems to be a good value + int32_t impulse = (((((-dotProduct) << 15) / distanceSquared) * surfacehardness) >> 8); // note: inverting before bitshift corrects for asymmetry in right-shifts (is slightly faster) + + #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) + int32_t ximpulse = (impulse * dx + ((dx >> 31) & 32767)) >> 15; // note: extracting sign bit and adding rounding value to correct for asymmetry in right shifts + int32_t yimpulse = (impulse * dy + ((dy >> 31) & 32767)) >> 15; + #else + int32_t ximpulse = (impulse * dx) / 32767; + int32_t yimpulse = (impulse * dy) / 32767; + #endif + particle1.vx -= ximpulse; // note: impulse is inverted, so subtracting it + particle1.vy -= yimpulse; + particle2.vx += ximpulse; + particle2.vy += yimpulse; + + if (collisionHardness < PS_P_MINSURFACEHARDNESS && (SEGMENT.call & 0x07) == 0) { // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and stop sloshing around) + const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS); + // Note: could call applyFriction, but this is faster and speed is key here + #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) + particle1.vx = ((int32_t)particle1.vx * coeff + (((int32_t)particle1.vx >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts + particle1.vy = ((int32_t)particle1.vy * coeff + (((int32_t)particle1.vy >> 31) & 0xFF)) >> 8; + particle2.vx = ((int32_t)particle2.vx * coeff + (((int32_t)particle2.vx >> 31) & 0xFF)) >> 8; + particle2.vy = ((int32_t)particle2.vy * coeff + (((int32_t)particle2.vy >> 31) & 0xFF)) >> 8; + #else // division is faster on ESP32, S2 and S3 + particle1.vx = ((int32_t)particle1.vx * coeff) / 255; + particle1.vy = ((int32_t)particle1.vy * coeff) / 255; + particle2.vx = ((int32_t)particle2.vx * coeff) / 255; + particle2.vy = ((int32_t)particle2.vy * coeff) / 255; + #endif + } + + // particles have volume, push particles apart if they are too close + // tried lots of configurations, it works best if not moved but given a little velocity, it tends to oscillate less this way + // when hard pushing by offsetting position, they sink into each other under gravity + // a problem with giving velocity is, that on harder collisions, this adds up as it is not dampened enough, so add friction in the FX if required + if (distanceSquared < collDistSq && dotProduct > -250) { // too close and also slow, push them apart + int32_t notsorandom = dotProduct & 0x01; //dotprouct LSB should be somewhat random, so no need to calculate a random number + int32_t pushamount = 1 + ((250 + dotProduct) >> 6); // the closer dotproduct is to zero, the closer the particles are + int32_t push = 0; + if (dx < 0) // particle 1 is on the right + push = pushamount; + else if (dx > 0) + push = -pushamount; + else { // on the same x coordinate, shift it a little so they do not stack + if (notsorandom) + particle1.x++; // move it so pile collapses + else + particle1.x--; + } + particle1.vx += push; + push = 0; + if (dy < 0) + push = pushamount; + else if (dy > 0) + push = -pushamount; + else { // dy==0 + if (notsorandom) + particle1.y++; // move it so pile collapses + else + particle1.y--; + } + particle1.vy += push; + + // note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye + if (collisionHardness < 5) { // if they are very soft, stop slow particles completely to make them stick to each other + particle1.vx = 0; + particle1.vy = 0; + particle2.vx = 0; + particle2.vy = 0; + //push them apart + particle1.x += push; + particle1.y += push; + } + } + } +} + +// update size and pointers (memory location and size can change dynamically) +// note: do not access the PS class in FX befor running this function (or it messes up SEGENV.data) +void ParticleSystem2D::updateSystem(void) { + PSPRINTLN("updateSystem2D"); + setMatrixSize(SEGMENT.vWidth(), SEGMENT.vHeight()); + updatePSpointers(advPartProps != nullptr, advPartSize != nullptr); // update pointers to PS data, also updates availableParticles + PSPRINTLN("\n END update System2D, running FX..."); +} + +// set the pointers for the class (this only has to be done once and not on every FX call, only the class pointer needs to be reassigned to SEGENV.data every time) +// function returns the pointer to the next byte available for the FX (if it assigned more memory for other stuff using the above allocate function) +// FX handles the PSsources, need to tell this function how many there are +void ParticleSystem2D::updatePSpointers(bool isadvanced, bool sizecontrol) { + PSPRINTLN("updatePSpointers"); + // Note on memory alignment: + // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. + // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. + // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. + particles = reinterpret_cast(this + 1); // pointer to particles + particleFlags = reinterpret_cast(particles + numParticles); // pointer to particle flags + sources = reinterpret_cast(particleFlags + numParticles); // pointer to source(s) at data+sizeof(ParticleSystem2D) + framebuffer = reinterpret_cast(sources + numSources); // pointer to framebuffer + // align pointer after framebuffer + uintptr_t p = reinterpret_cast(framebuffer + (maxXpixel+1)*(maxYpixel+1)); + p = (p + 3) & ~0x03; // align to 4-byte boundary + PSdataEnd = reinterpret_cast(p); // pointer to first available byte after the PS for FX additional data + if (isadvanced) { + advPartProps = reinterpret_cast(PSdataEnd); + PSdataEnd = reinterpret_cast(advPartProps + numParticles); + if (sizecontrol) { + advPartSize = reinterpret_cast(PSdataEnd); + PSdataEnd = reinterpret_cast(advPartSize + numParticles); + } + } +#ifdef DEBUG_PS + Serial.printf_P(PSTR(" particles %p "), particles); + Serial.printf_P(PSTR(" sources %p "), sources); + Serial.printf_P(PSTR(" adv. props %p "), advPartProps); + Serial.printf_P(PSTR(" adv. ctrl %p "), advPartSize); + Serial.printf_P(PSTR("end %p\n"), PSdataEnd); + #endif + +} + +// blur a matrix in x and y direction, blur can be asymmetric in x and y +// for speed, 1D array and 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined +// to blur a subset of the buffer, change the xsize/ysize and set xstart/ystart to the desired starting coordinates (default start is 0/0) +// subset blurring only works on 10x10 buffer (single particle rendering), if other sizes are needed, buffer width must be passed as parameter +void blur2D(CRGB *colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, uint32_t xstart, uint32_t ystart, bool isparticle) { + CRGB seeppart, carryover; + uint32_t seep = xblur >> 1; + uint32_t width = xsize; // width of the buffer, used to calculate the index of the pixel + + if (isparticle) { //first and last row are always black in first pass of particle rendering + ystart++; + ysize--; + width = 10; // buffer size is 10x10 + } + + for (uint32_t y = ystart; y < ystart + ysize; y++) { + carryover = BLACK; + uint32_t indexXY = xstart + y * width; + for (uint32_t x = xstart; x < xstart + xsize; x++) { + seeppart = colorbuffer[indexXY]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if (x > 0) { + fast_color_add(colorbuffer[indexXY - 1], seeppart); + if (carryover) // note: check adds overhead but is faster on average + fast_color_add(colorbuffer[indexXY], carryover); + } + carryover = seeppart; + indexXY++; // next pixel in x direction + } + } + + if (isparticle) { // first and last row are now smeared + ystart--; + ysize++; + } + + seep = yblur >> 1; + for (uint32_t x = xstart; x < xstart + xsize; x++) { + carryover = BLACK; + uint32_t indexXY = x + ystart * width; + for (uint32_t y = ystart; y < ystart + ysize; y++) { + seeppart = colorbuffer[indexXY]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if (y > 0) { + fast_color_add(colorbuffer[indexXY - width], seeppart); + if (carryover) // note: check adds overhead but is faster on average + fast_color_add(colorbuffer[indexXY], carryover); + } + carryover = seeppart; + indexXY += width; // next pixel in y direction + } + } +} + +//non class functions to use for initialization +uint32_t calculateNumberOfParticles2D(uint32_t const pixels, const bool isadvanced, const bool sizecontrol) { + uint32_t numberofParticles = pixels; // 1 particle per pixel (for example 512 particles on 32x16) +#ifdef ESP8266 + uint32_t particlelimit = ESP8266_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 16x16 and 4k effect ram) +#elif ARDUINO_ARCH_ESP32S2 + uint32_t particlelimit = ESP32S2_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 32x32 and 24k effect ram) +#else + uint32_t particlelimit = ESP32_MAXPARTICLES; // maximum number of paticles allowed (based on two segments of 32x32 and 40k effect ram) +#endif + numberofParticles = max((uint32_t)4, min(numberofParticles, particlelimit)); // limit to 4 - particlelimit + if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount + numberofParticles = (numberofParticles * sizeof(PSparticle)) / (sizeof(PSparticle) + sizeof(PSadvancedParticle)); + if (sizecontrol) // advanced property array needs ram, reduce number of particles + numberofParticles /= 8; // if advanced size control is used, much fewer particles are needed note: if changing this number, adjust FX using this accordingly + + //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) + numberofParticles = (numberofParticles+3) & ~0x03; + return numberofParticles; +} + +uint32_t calculateNumberOfSources2D(uint32_t pixels, uint32_t requestedsources) { +#ifdef ESP8266 + int numberofSources = min((pixels) / 8, (uint32_t)requestedsources); + numberofSources = max(1, min(numberofSources, ESP8266_MAXSOURCES)); // limit +#elif ARDUINO_ARCH_ESP32S2 + int numberofSources = min((pixels) / 6, (uint32_t)requestedsources); + numberofSources = max(1, min(numberofSources, ESP32S2_MAXSOURCES)); // limit +#else + int numberofSources = min((pixels) / 4, (uint32_t)requestedsources); + numberofSources = max(1, min(numberofSources, ESP32_MAXSOURCES)); // limit +#endif + // make sure it is a multiple of 4 for proper memory alignment + numberofSources = (numberofSources+3) & ~0x03; + return numberofSources; +} + +//allocate memory for particle system class, particles, sprays plus additional memory requested by FX //TODO: add percentofparticles like in 1D to reduce memory footprint of some FX? +bool allocateParticleSystemMemory2D(uint32_t numparticles, uint32_t numsources, bool isadvanced, bool sizecontrol, uint32_t additionalbytes) { + PSPRINTLN("PS 2D alloc"); + PSPRINTLN("numparticles:" + String(numparticles) + " numsources:" + String(numsources) + " additionalbytes:" + String(additionalbytes)); + uint32_t requiredmemory = sizeof(ParticleSystem2D); + // functions above make sure numparticles is a multiple of 4 bytes (to avoid alignment issues) + requiredmemory += sizeof(PSparticleFlags) * numparticles; + requiredmemory += sizeof(PSparticle) * numparticles; + if (isadvanced) + requiredmemory += sizeof(PSadvancedParticle) * numparticles; + if (sizecontrol) + requiredmemory += sizeof(PSsizeControl) * numparticles; + requiredmemory += sizeof(PSsource) * numsources; + requiredmemory += sizeof(CRGB) * SEGMENT.virtualLength(); // virtualLength is witdh * height + requiredmemory += additionalbytes + 3; // add 3 to ensure there is room for stuffing bytes + //requiredmemory = (requiredmemory + 3) & ~0x03; // align memory block to next 4-byte boundary + PSPRINTLN("mem alloc: " + String(requiredmemory)); + return(SEGMENT.allocateData(requiredmemory)); +} + +// initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) +bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, uint32_t additionalbytes, bool advanced, bool sizecontrol) { + PSPRINT("PS 2D init "); + if (!strip.isMatrix) return false; // only for 2D + uint32_t cols = SEGMENT.virtualWidth(); + uint32_t rows = SEGMENT.virtualHeight(); + uint32_t pixels = cols * rows; + + uint32_t numparticles = calculateNumberOfParticles2D(pixels, advanced, sizecontrol); + PSPRINT(" segmentsize:" + String(cols) + " x " + String(rows)); + PSPRINT(" request numparticles:" + String(numparticles)); + uint32_t numsources = calculateNumberOfSources2D(pixels, requestedsources); + if (!allocateParticleSystemMemory2D(numparticles, numsources, advanced, sizecontrol, additionalbytes)) + { + DEBUG_PRINT(F("PS init failed: memory depleted")); + return false; + } + + PartSys = new (SEGENV.data) ParticleSystem2D(cols, rows, numparticles, numsources, advanced, sizecontrol); // particle system constructor + + PSPRINTLN("******init done, pointers:"); + return true; +} + +#endif // WLED_DISABLE_PARTICLESYSTEM2D + + +//////////////////////// +// 1D Particle System // +//////////////////////// +#ifndef WLED_DISABLE_PARTICLESYSTEM1D + +ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced) { + numSources = numberofsources; + numParticles = numberofparticles; // number of particles allocated in init + usedParticles = numParticles; // use all particles by default + advPartProps = nullptr; //make sure we start out with null pointers (just in case memory was not cleared) + //advPartSize = nullptr; + setSize(length); + updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) + setWallHardness(255); // set default wall hardness to max + setGravity(0); //gravity disabled by default + setParticleSize(0); // 1 pixel size by default + motionBlur = 0; //no fading by default + smearBlur = 0; //no smearing by default + emitIndex = 0; + collisionStartIdx = 0; + // initialize some default non-zero values most FX use + for (uint32_t i = 0; i < numSources; i++) { + sources[i].source.ttl = 1; //set source alive + sources[i].sourceFlags.asByte = 0; // all flags disabled + } + + if (isadvanced) { + for (uint32_t i = 0; i < numParticles; i++) { + advPartProps[i].sat = 255; // set full saturation + } + } +} + +// update function applies gravity, moves the particles, handles collisions and renders the particles +void ParticleSystem1D::update(void) { + //apply gravity globally if enabled + if (particlesettings.useGravity) //note: in 1D system, applying gravity after collisions also works but may be worse + applyGravity(); + + // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) + if (particlesettings.useCollisions) + handleCollisions(); + + //move all particles + for (uint32_t i = 0; i < usedParticles; i++) { + particleMoveUpdate(particles[i], particleFlags[i], nullptr, advPartProps ? &advPartProps[i] : nullptr); + } + + if (particlesettings.colorByPosition) { + uint32_t scale = (255 << 16) / maxX; // speed improvement: multiplication is faster than division + for (uint32_t i = 0; i < usedParticles; i++) { + particles[i].hue = (scale * particles[i].x) >> 16; // note: x is > 0 if not out of bounds + } + } + + render(); +} + +// set percentage of used particles as uint8_t i.e 127 means 50% for example +void ParticleSystem1D::setUsedParticles(const uint8_t percentage) { + usedParticles = (numParticles * ((int)percentage+1)) >> 8; // number of particles to use (percentage is 0-255, 255 = 100%) + PSPRINT(" SetUsedpaticles: allocated particles: "); + PSPRINT(numParticles); + PSPRINT(" ,used particles: "); + PSPRINTLN(usedParticles); +} + +void ParticleSystem1D::setWallHardness(const uint8_t hardness) { + wallHardness = hardness; +} + +void ParticleSystem1D::setSize(const uint32_t x) { + maxXpixel = x - 1; // last physical pixel that can be drawn to + maxX = x * PS_P_RADIUS_1D - 1; // particle system boundary for movements +} + +void ParticleSystem1D::setWrap(const bool enable) { + particlesettings.wrap = enable; +} + +void ParticleSystem1D::setBounce(const bool enable) { + particlesettings.bounce = enable; +} + +void ParticleSystem1D::setKillOutOfBounds(const bool enable) { + particlesettings.killoutofbounds = enable; +} + +void ParticleSystem1D::setColorByAge(const bool enable) { + particlesettings.colorByAge = enable; +} + +void ParticleSystem1D::setColorByPosition(const bool enable) { + particlesettings.colorByPosition = enable; +} + +void ParticleSystem1D::setMotionBlur(const uint8_t bluramount) { + motionBlur = bluramount; +} + +void ParticleSystem1D::setSmearBlur(const uint8_t bluramount) { + smearBlur = bluramount; +} + +// render size, 0 = 1 pixel, 1 = 2 pixel (interpolated), bigger sizes require adanced properties +void ParticleSystem1D::setParticleSize(const uint8_t size) { + particlesize = size > 0 ? 1 : 0; // TODO: add support for global sizes? see note above (motion blur) + particleHardRadius = PS_P_MINHARDRADIUS_1D >> (!particlesize); // 2 pixel sized particles or single pixel sized particles +} + +// enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable +// if enabled, gravity is applied to all particles in ParticleSystemUpdate() +// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results) +void ParticleSystem1D::setGravity(const int8_t force) { + if (force) { + gforce = force; + particlesettings.useGravity = true; + } + else + particlesettings.useGravity = false; +} + +void ParticleSystem1D::enableParticleCollisions(const bool enable, const uint8_t hardness) { + particlesettings.useCollisions = enable; + collisionHardness = hardness; +} + +// emit one particle with variation, returns index of last emitted particle (or -1 if no particle emitted) +int32_t ParticleSystem1D::sprayEmit(const PSsource1D &emitter) { + for (uint32_t i = 0; i < usedParticles; i++) { + emitIndex++; + if (emitIndex >= usedParticles) + emitIndex = 0; + if (particles[emitIndex].ttl == 0) { // find a dead particle + particles[emitIndex].vx = emitter.v + hw_random16(emitter.var << 1) - emitter.var; // random(-var,var) + particles[emitIndex].x = emitter.source.x; + particles[emitIndex].hue = emitter.source.hue; + particles[emitIndex].ttl = hw_random16(emitter.minLife, emitter.maxLife); + particleFlags[emitIndex].collide = emitter.sourceFlags.collide; // TODO: could just set all flags (asByte) but need to check if that breaks any of the FX + particleFlags[emitIndex].reversegrav = emitter.sourceFlags.reversegrav; + particleFlags[emitIndex].perpetual = emitter.sourceFlags.perpetual; + if (advPartProps) { + advPartProps[emitIndex].sat = emitter.sat; + advPartProps[emitIndex].size = emitter.size; + } + return emitIndex; + } + } + return -1; +} + +// particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 +// uses passed settings to set bounce or wrap, if useGravity is set, it will never bounce at the top and killoutofbounds is not applied over the top +void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSparticleFlags1D &partFlags, PSsettings1D *options, PSadvancedParticle1D *advancedproperties) { + if (options == nullptr) + options = &particlesettings; // use PS system settings by default + + if (part.ttl > 0) { + if (!partFlags.perpetual) + part.ttl--; // age + if (options->colorByAge) + part.hue = min(part.ttl, (uint16_t)255); // set color to ttl + + int32_t renderradius = PS_P_HALFRADIUS_1D; // used to check out of bounds, default for 2 pixel rendering + int32_t newX = part.x + (int32_t)part.vx; + partFlags.outofbounds = false; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) + + if (advancedproperties) { // using individual particle size? + if (advancedproperties->size > 1) + particleHardRadius = PS_P_MINHARDRADIUS_1D + (advancedproperties->size >> 1); + else // single pixel particles use half the collision distance for walls + particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; + renderradius = particleHardRadius; // note: for single pixel particles, it should be zero, but it does not matter as out of bounds checking is done in rendering function + } + + // if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view + if (options->bounce) { + if ((newX < (int32_t)particleHardRadius) || ((newX > (int32_t)(maxX - particleHardRadius)))) { // reached a wall + bool bouncethis = true; + if (options->useGravity) { + if (partFlags.reversegrav) { // skip bouncing at x = 0 + if (newX < (int32_t)particleHardRadius) + bouncethis = false; + } else if (newX > (int32_t)particleHardRadius) { // skip bouncing at x = max + bouncethis = false; + } + } + if (bouncethis) { + part.vx = -part.vx; // invert speed + part.vx = ((int32_t)part.vx * (int32_t)wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + if (newX < (int32_t)particleHardRadius) + newX = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better + else + newX = maxX - particleHardRadius; + } + } + } + + if (!checkBoundsAndWrap(newX, maxX, renderradius, options->wrap)) { // check out of bounds note: this must not be skipped or it can lead to crashes + partFlags.outofbounds = true; + if (options->killoutofbounds) { + bool killthis = true; + if (options->useGravity) { // if gravity is used, only kill below 'floor level' + if (partFlags.reversegrav) { // skip at x = 0, do not skip far out of bounds + if (newX < 0 || newX > maxX << 2) + killthis = false; + } else { // skip at x = max, do not skip far out of bounds + if (newX > 0 && newX < maxX << 2) + killthis = false; + } + } + if (killthis) + part.ttl = 0; + } + } + + if (!partFlags.fixed) + part.x = newX; // set new position + else + part.vx = 0; // set speed to zero. note: particle can get speed in collisions, if unfixed, it should not speed away + } +} + +// apply a force in x direction to individual particle (or source) +// caller needs to provide a 8bit counter (for each paticle) that holds its value between calls +// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame +void ParticleSystem1D::applyForce(PSparticle1D &part, const int8_t xforce, uint8_t &counter) { + int32_t dv = calcForce_dv(xforce, counter); // velocity increase + part.vx = limitSpeed((int32_t)part.vx + dv); // apply the force to particle +} + +// apply a force to all particles +// force is in 3.4 fixed point notation (see above) +void ParticleSystem1D::applyForce(const int8_t xforce) { + int32_t dv = calcForce_dv(xforce, forcecounter); // velocity increase + for (uint32_t i = 0; i < usedParticles; i++) { + particles[i].vx = limitSpeed((int32_t)particles[i].vx + dv); + } +} + +// apply gravity to all particles using PS global gforce setting +// gforce is in 3.4 fixed point notation, see note above +void ParticleSystem1D::applyGravity() { + int32_t dv_raw = calcForce_dv(gforce, gforcecounter); + for (uint32_t i = 0; i < usedParticles; i++) { + int32_t dv = dv_raw; + if (particleFlags[i].reversegrav) dv = -dv_raw; + // note: not checking if particle is dead is omitted as most are usually alive and if few are alive, rendering is fast anyways + particles[i].vx = limitSpeed((int32_t)particles[i].vx - dv); + } +} + +// apply gravity to single particle using system settings (use this for sources) +// function does not increment gravity counter, if gravity setting is disabled, this cannot be used +void ParticleSystem1D::applyGravity(PSparticle1D &part, PSparticleFlags1D &partFlags) { + uint32_t counterbkp = gforcecounter; + int32_t dv = calcForce_dv(gforce, gforcecounter); + if (partFlags.reversegrav) dv = -dv; + gforcecounter = counterbkp; //save it back + part.vx = limitSpeed((int32_t)part.vx - dv); +} + + +// slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) +// note: a coefficient smaller than 0 will speed them up (this is a feature, not a bug), coefficient larger than 255 inverts the speed, so don't do that +void ParticleSystem1D::applyFriction(int32_t coefficient) { + #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) + int32_t friction = 256 - coefficient; + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[i].ttl) + particles[i].vx = ((int32_t)particles[i].vx * friction + (((int32_t)particles[i].vx >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts + } + #else // division is faster on ESP32, S2 and S3 + int32_t friction = 255 - coefficient; + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[i].ttl) + particles[i].vx = ((int32_t)particles[i].vx * friction) / 255; + } + #endif +} + + +// render particles to the LED buffer (uses palette to render the 8bit particle color value) +// if wrap is set, particles half out of bounds are rendered to the other side of the matrix +// warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds +void ParticleSystem1D::render() { + CRGB baseRGB; + uint32_t brightness; // particle brightness, fades if dying + TBlendType blend = LINEARBLEND; // default color rendering: wrap palette + if (particlesettings.colorByAge || particlesettings.colorByPosition) { + blend = LINEARBLEND_NOWRAP; + } + + #ifdef ESP8266 // no local buffer on ESP8266 + if (motionBlur) + SEGMENT.fadeToBlackBy(255 - motionBlur); + else + SEGMENT.fill(BLACK); // clear the buffer before rendering to it + #else + if (motionBlur) { // blurring active + for (int32_t x = 0; x <= maxXpixel; x++) { + fast_color_scale(framebuffer[x], motionBlur); + } + } + else { // no blurring: clear buffer + memset(framebuffer, 0, (maxXpixel+1) * sizeof(CRGB)); + } + #endif + // go over particles and render them to the buffer + for (uint32_t i = 0; i < usedParticles; i++) { + if ( particles[i].ttl == 0 || particleFlags[i].outofbounds) + continue; + + // generate RGB values for particle + brightness = min(particles[i].ttl << 1, (int)255); + baseRGB = ColorFromPaletteWLED(SEGPALETTE, particles[i].hue, 255, blend); + + if (advPartProps) { //saturation is advanced property in 1D system + if (advPartProps[i].sat < 255) { + CHSV32 baseHSV; + rgb2hsv((uint32_t((byte(baseRGB.r) << 16) | (byte(baseRGB.g) << 8) | (byte(baseRGB.b)))), baseHSV); // convert to HSV + baseHSV.s = min(baseHSV.s, advPartProps[i].sat); // set the saturation but don't increase it + uint32_t tempcolor; + hsv2rgb(baseHSV, tempcolor); // convert back to RGB + baseRGB = (CRGB)tempcolor; + } + } + brightness = gamma8(brightness); // apply gamma correction, used for gamma-inverted brightness distribution + renderParticle(i, brightness, baseRGB, particlesettings.wrap); + } + // apply smear-blur to rendered frame + if (smearBlur) { + #ifdef ESP8266 + SEGMENT.blur(smearBlur, true); // no local buffer on ESP8266 + #else + blur1D(framebuffer, maxXpixel + 1, smearBlur, 0); + #endif + } + + // add background color + uint32_t bg_color = SEGCOLOR(1); + if (bg_color > 0) { //if not black + CRGB bg_color_crgb = bg_color; // convert to CRGB + for (int32_t i = 0; i <= maxXpixel; i++) { + #ifdef ESP8266 // no local buffer on ESP8266 + SEGMENT.addPixelColor(i, bg_color, true); + #else + fast_color_add(framebuffer[i], bg_color_crgb); + #endif + } + } + + #ifndef ESP8266 + // transfer the frame-buffer to segment + for (int x = 0; x <= maxXpixel; x++) { + SEGMENT.setPixelColor(x, framebuffer[x]); + } + #endif +} + +// calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer +__attribute__((optimize("O2"))) void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB &color, const bool wrap) { + uint32_t size = particlesize; + if (advPartProps) // use advanced size properties (1D system has no large size global rendering TODO: add large global rendering?) + size = advPartProps[particleindex].size; + + if (size == 0) { //single pixel particle, can be out of bounds as oob checking is made for 2-pixel particles (and updating it uses more code) + uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT_1D; + if (x <= (uint32_t)maxXpixel) { //by making x unsigned there is no need to check < 0 as it will overflow + #ifdef ESP8266 // no local buffer on ESP8266 + SEGMENT.addPixelColor(x, color.scale8(brightness), true); + #else + fast_color_add(framebuffer[x], color, brightness); + #endif + } + return; + } + //render larger particles + bool pxlisinframe[2] = {true, true}; + int32_t pxlbrightness[2]; + int32_t pixco[2]; // physical pixel coordinates of the two pixels representing a particle + + // add half a radius as the rendering algorithm always starts at the bottom left, this leaves things positive, so shifts can be used, then shift coordinate by a full pixel (x-- below) + int32_t xoffset = particles[particleindex].x + PS_P_HALFRADIUS_1D; + int32_t dx = xoffset & (PS_P_RADIUS_1D - 1); //relativ particle position in subpixel space, modulo replaced with bitwise AND + int32_t x = xoffset >> PS_P_RADIUS_SHIFT_1D; // divide by PS_P_RADIUS, bitshift of negative number stays negative -> checking below for x < 0 works (but does not when using division) + + // set the raw pixel coordinates + pixco[1] = x; // right pixel + x--; // shift by a full pixel here, this is skipped above to not do -1 and then +1 + pixco[0] = x; // left pixel + + //calculate the brightness values for both pixels using linear interpolation (note: in standard rendering out of frame pixels could be skipped but if checks add more clock cycles over all) + pxlbrightness[0] = (((int32_t)PS_P_RADIUS_1D - dx) * brightness) >> PS_P_SURFACE_1D; + pxlbrightness[1] = (dx * brightness) >> PS_P_SURFACE_1D; + // adjust brightness such that distribution is linear after gamma correction: + // - scale brigthness with gamma correction (done in render()) + // - apply inverse gamma correction to brightness values + // - gamma is applied again in show() -> the resulting brightness distribution is linear but gamma corrected in total + pxlbrightness[0] = gamma8inv(pxlbrightness[0]); // use look-up-table for invers gamma + pxlbrightness[1] = gamma8inv(pxlbrightness[1]); + + // check if particle has advanced size properties and buffer is available + if (advPartProps && advPartProps[particleindex].size > 1) { + CRGB renderbuffer[10]; // 10 pixel buffer + memset(renderbuffer, 0, sizeof(renderbuffer)); // clear buffer + //render particle to a bigger size + //particle size to pixels: 2 - 63 is 4 pixels, < 128 is 6pixels, < 192 is 8 pixels, bigger is 10 pixels + //first, render the pixel to the center of the renderbuffer, then apply 1D blurring + fast_color_add(renderbuffer[4], color, pxlbrightness[0]); + fast_color_add(renderbuffer[5], color, pxlbrightness[1]); + uint32_t rendersize = 2; // initialize render size, minimum is 4 pixels, it is incremented int he loop below to start with 4 + uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below) + uint32_t blurpasses = size/64 + 1; // number of blur passes depends on size, four passes max + uint32_t bitshift = 0; + for (uint32_t i = 0; i < blurpasses; i++) { + if (i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) + bitshift = 1; + rendersize += 2; + offset--; + blur1D(renderbuffer, rendersize, size << bitshift, offset); + size = size > 64 ? size - 64 : 0; + } + + // calculate origin coordinates to render the particle to in the framebuffer + uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; //note: using uint is fine + uint32_t xfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked + + // transfer particle renderbuffer to framebuffer + for (uint32_t xrb = offset; xrb < rendersize+offset; xrb++) { + xfb = xfb_orig + xrb; + if (xfb > (uint32_t)maxXpixel) { + if (wrap) { // wrap x to the other side if required + if (xfb > (uint32_t)maxXpixel << 1) // xfb is "negative" + xfb = (maxXpixel + 1) + (int32_t)xfb; // this always overflows to within bounds + else + xfb = xfb % (maxXpixel + 1); // note: without the above "negative" check, this works only for powers of 2 + } + else + continue; + } + #ifdef ESP8266 // no local buffer on ESP8266 + SEGMENT.addPixelColor(xfb, renderbuffer[xrb], true); + #else + fast_color_add(framebuffer[xfb], renderbuffer[xrb]); + #endif + } + } + else { // standard rendering (2 pixels per particle) + // check if any pixels are out of frame + if (x < 0) { // left pixels out of frame + if (wrap) // wrap x to the other side if required + pixco[0] = maxXpixel; + else + pxlisinframe[0] = false; // pixel is out of matrix boundaries, do not render + } + else if (pixco[1] > (int32_t)maxXpixel) { // right pixel, only has to be checkt if left pixel did not overflow + if (wrap) // wrap y to the other side if required + pixco[1] = 0; + else + pxlisinframe[1] = false; + } + for (uint32_t i = 0; i < 2; i++) { + if (pxlisinframe[i]) { + #ifdef ESP8266 // no local buffer on ESP8266 + SEGMENT.addPixelColor(pixco[i], color.scale8((uint8_t)pxlbrightness[i]), true); + #else + fast_color_add(framebuffer[pixco[i]], color, pxlbrightness[i]); + #endif + } + } + } + +} + +// detect collisions in an array of particles and handle them +void ParticleSystem1D::handleCollisions() { + uint32_t collisiondistance = particleHardRadius << 1; + // note: partices are binned by position, assumption is that no more than half of the particles are in the same bin + // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) + constexpr int BIN_WIDTH = 32 * PS_P_RADIUS_1D; // width of each bin, a compromise between speed and accuracy (larger bins are faster but collapse more) + int32_t overlap = particleHardRadius << 1; // overlap bins to include edge particles to neighbouring bins + if (advPartProps) //may be using individual particle size + overlap += 256; // add 2 * max radius (approximately) + uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 4); // do not bin small amounts, limit max to 1/4 of particles + uint32_t numBins = (maxX + (BIN_WIDTH - 1)) / BIN_WIDTH; // calculate number of bins + uint16_t binIndices[maxBinParticles]; // array to store indices of particles in a bin + uint32_t binParticleCount; // number of particles in the current bin + uint16_t nextFrameStartIdx = hw_random16(usedParticles); // index of the first particle in the next frame (set to fixed value if bin overflow) + uint32_t pidx = collisionStartIdx; //start index in case a bin is full, process remaining particles next frame + for (uint32_t bin = 0; bin < numBins; bin++) { + binParticleCount = 0; // reset for this bin + int32_t binStart = bin * BIN_WIDTH - overlap; // note: first bin will extend to negative, but that is ok as out of bounds particles are ignored + int32_t binEnd = binStart + BIN_WIDTH + overlap; // note: last bin can be out of bounds, see above + + // fill the binIndices array for this bin + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[pidx].ttl > 0) { // alivee + if (particles[pidx].x >= binStart && particles[pidx].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins) + if(particleFlags[pidx].outofbounds == 0 && particleFlags[pidx].collide) { // particle is in frame and does collide note: checking flags is quite slow and usually these are set, so faster to check here + if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame + nextFrameStartIdx = pidx; // bin overflow can only happen once as bin size is at least half of the particles (or half +1) + break; + } + binIndices[binParticleCount++] = pidx; + } + } + } + pidx++; + if (pidx >= usedParticles) pidx = 0; // wrap around + } + + for (uint32_t i = 0; i < binParticleCount; i++) { // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide + uint32_t idx_i = binIndices[i]; + for (uint32_t j = i + 1; j < binParticleCount; j++) { // check against higher number particles + uint32_t idx_j = binIndices[j]; + if (advPartProps) { // use advanced size properties + collisiondistance = (PS_P_MINHARDRADIUS_1D << particlesize) + ((advPartProps[idx_i].size + advPartProps[idx_j].size) >> 1); + } + int32_t dx = (particles[idx_j].x + particles[idx_j].vx) - (particles[idx_i].x + particles[idx_i].vx); // distance between particles with lookahead + uint32_t dx_abs = abs(dx); + if (dx_abs <= collisiondistance) { // collide if close + collideParticles(particles[idx_i], particleFlags[idx_i], particles[idx_j], particleFlags[idx_j], dx, dx_abs, collisiondistance); + } + } + } + } + collisionStartIdx = nextFrameStartIdx; // set the start index for the next frame +} +// handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS +// takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) +__attribute__((optimize("O2"))) void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, const int32_t dx, const uint32_t dx_abs, const uint32_t collisiondistance) { + int32_t dv = particle2.vx - particle1.vx; + int32_t dotProduct = (dx * dv); // is always negative if moving towards each other + + if (dotProduct < 0) { // particles are moving towards each other + uint32_t surfacehardness = max(collisionHardness, (int32_t)PS_P_MINSURFACEHARDNESS_1D); // if particles are soft, the impulse must stay above a limit or collisions slip through + // Calculate new velocities after collision note: not using dot product like in 2D as impulse is purely speed depnedent + #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) + int32_t impulse = ((dv * surfacehardness) + ((dv >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts + #else // division is faster on ESP32, S2 and S3 + int32_t impulse = (dv * surfacehardness) / 255; + #endif + particle1.vx += impulse; + particle2.vx -= impulse; + + // if one of the particles is fixed, transfer the impulse back so it bounces + if (particle1flags.fixed) + particle2.vx = -particle1.vx; + else if (particle2flags.fixed) + particle1.vx = -particle2.vx; + + if (collisionHardness < PS_P_MINSURFACEHARDNESS_1D && (SEGMENT.call & 0x07) == 0) { // if particles are soft, they become 'sticky' i.e. apply some friction + const uint32_t coeff = collisionHardness + (250 - PS_P_MINSURFACEHARDNESS_1D); + #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) + 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 + particle2.vx = ((int32_t)particle2.vx * coeff + (((int32_t)particle2.vx >> 31) & 0xFF)) >> 8; + #else // division is faster on ESP32, S2 and S3 + particle1.vx = ((int32_t)particle1.vx * coeff) / 255; + particle2.vx = ((int32_t)particle2.vx * coeff) / 255; + #endif + } + } + + if (dx_abs < (collisiondistance - 8) && abs(dv) < 5) { // overlapping and moving slowly + // particles have volume, push particles apart if they are too close + // behaviour is different than in 2D, we need pixel accurate stacking here, push the top particle + // note: like in 2D, pushing by a distance makes softer piles collapse, giving particles speed prevents that and looks nicer + int32_t pushamount = 1; + if (dx < 0) // particle2.x < particle1.x + pushamount = -pushamount; + particle1.vx -= pushamount; + particle2.vx += pushamount; + + if (dx_abs < collisiondistance >> 1) { // too close, force push particles so they dont collapse + pushamount = 1 + ((collisiondistance - dx_abs) >> 3); // note: push amount found by experimentation + + if (particle1.x < (maxX >> 1)) { // lower half, push particle with larger x in positive direction + if (dx < 0 && !particle1flags.fixed) { // particle2.x < particle1.x -> push particle 1 + particle1.vx++;// += pushamount; + particle1.x += pushamount; + } + else if (!particle2flags.fixed) { // particle1.x < particle2.x -> push particle 2 + particle2.vx++;// += pushamount; + particle2.x += pushamount; + } + } + else { // upper half, push particle with smaller x + if (dx < 0 && !particle2flags.fixed) { // particle2.x < particle1.x -> push particle 2 + particle2.vx--;// -= pushamount; + particle2.x -= pushamount; + } + else if (!particle1flags.fixed) { // particle1.x < particle2.x -> push particle 1 + particle1.vx--;// -= pushamount; + particle1.x -= pushamount; + } + } + } + } +} + +// update size and pointers (memory location and size can change dynamically) +// note: do not access the PS class in FX befor running this function (or it messes up SEGENV.data) +void ParticleSystem1D::updateSystem(void) { + setSize(SEGMENT.virtualLength()); // update size + updatePSpointers(advPartProps != nullptr); +} + +// set the pointers for the class (this only has to be done once and not on every FX call, only the class pointer needs to be reassigned to SEGENV.data every time) +// function returns the pointer to the next byte available for the FX (if it assigned more memory for other stuff using the above allocate function) +// FX handles the PSsources, need to tell this function how many there are +void ParticleSystem1D::updatePSpointers(bool isadvanced) { + // Note on memory alignment: + // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. + // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. + // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. + particles = reinterpret_cast(this + 1); // pointer to particles + particleFlags = reinterpret_cast(particles + numParticles); // pointer to particle flags + sources = reinterpret_cast(particleFlags + numParticles); // pointer to source(s) + #ifdef ESP8266 // no local buffer on ESP8266 + PSdataEnd = reinterpret_cast(sources + numSources); + #else + framebuffer = reinterpret_cast(sources + numSources); // pointer to framebuffer + // align pointer after framebuffer to 4bytes + uintptr_t p = reinterpret_cast(framebuffer + (maxXpixel+1)); // maxXpixel is SEGMENT.virtualLength() - 1 + p = (p + 3) & ~0x03; // align to 4-byte boundary + PSdataEnd = reinterpret_cast(p); // pointer to first available byte after the PS for FX additional data + #endif + if (isadvanced) { + advPartProps = reinterpret_cast(PSdataEnd); + PSdataEnd = reinterpret_cast(advPartProps + numParticles); // since numParticles is a multiple of 4, this is always aligned to 4 bytes. No need to add padding bytes here + } + #ifdef WLED_DEBUG_PS + PSPRINTLN(" PS Pointers: "); + PSPRINT(" PS : 0x"); + Serial.println((uintptr_t)this, HEX); + PSPRINT(" Particleflags : 0x"); + Serial.println((uintptr_t)particleFlags, HEX); + PSPRINT(" Particles : 0x"); + Serial.println((uintptr_t)particles, HEX); + PSPRINT(" Sources : 0x"); + Serial.println((uintptr_t)sources, HEX); + #endif +} + +//non class functions to use for initialization, fraction is uint8_t: 255 means 100% +uint32_t calculateNumberOfParticles1D(const uint32_t fraction, const bool isadvanced) { + uint32_t numberofParticles = SEGMENT.virtualLength(); // one particle per pixel (if possible) +#ifdef ESP8266 + uint32_t particlelimit = ESP8266_MAXPARTICLES_1D; // maximum number of paticles allowed +#elif ARDUINO_ARCH_ESP32S2 + uint32_t particlelimit = ESP32S2_MAXPARTICLES_1D; // maximum number of paticles allowed +#else + uint32_t particlelimit = ESP32_MAXPARTICLES_1D; // maximum number of paticles allowed +#endif + numberofParticles = min(numberofParticles, particlelimit); // limit to particlelimit + if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount + numberofParticles = (numberofParticles * sizeof(PSparticle1D)) / (sizeof(PSparticle1D) + sizeof(PSadvancedParticle1D)); + numberofParticles = (numberofParticles * (fraction + 1)) >> 8; // calculate fraction of particles + numberofParticles = numberofParticles < 20 ? 20 : numberofParticles; // 20 minimum + //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) + numberofParticles = (numberofParticles+3) & ~0x03; // note: with a separate particle buffer, this is probably unnecessary + PSPRINTLN(" calc numparticles:" + String(numberofParticles)) + return numberofParticles; +} + +uint32_t calculateNumberOfSources1D(const uint32_t requestedsources) { +#ifdef ESP8266 + int numberofSources = max(1, min((int)requestedsources,ESP8266_MAXSOURCES_1D)); // limit +#elif ARDUINO_ARCH_ESP32S2 + int numberofSources = max(1, min((int)requestedsources, ESP32S2_MAXSOURCES_1D)); // limit +#else + int numberofSources = max(1, min((int)requestedsources, ESP32_MAXSOURCES_1D)); // limit +#endif + // make sure it is a multiple of 4 for proper memory alignment (so minimum is acutally 4) + numberofSources = (numberofSources+3) & ~0x03; + return numberofSources; +} + +//allocate memory for particle system class, particles, sprays plus additional memory requested by FX +bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t numsources, const bool isadvanced, const uint32_t additionalbytes) { + uint32_t requiredmemory = sizeof(ParticleSystem1D); + // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) + requiredmemory += sizeof(PSparticleFlags1D) * numparticles; + requiredmemory += sizeof(PSparticle1D) * numparticles; + requiredmemory += sizeof(PSsource1D) * numsources; + #ifndef ESP8266 // no local buffer on ESP8266 + requiredmemory += sizeof(CRGB) * SEGMENT.virtualLength(); + #endif + requiredmemory += additionalbytes + 3; // add 3 to ensure room for stuffing bytes to make it 4 byte aligned + if (isadvanced) + requiredmemory += sizeof(PSadvancedParticle1D) * numparticles; + return(SEGMENT.allocateData(requiredmemory)); +} + +// initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) +// note: percentofparticles is in uint8_t, for example 191 means 75%, (deafaults to 255 or 100% meaning one particle per pixel), can be more than 100% (but not recommended, can cause out of memory) +bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedsources, const uint8_t fractionofparticles, const uint32_t additionalbytes, const bool advanced) { + if (SEGLEN == 1) return false; // single pixel not supported + uint32_t numparticles = calculateNumberOfParticles1D(fractionofparticles, advanced); + uint32_t numsources = calculateNumberOfSources1D(requestedsources); + if (!allocateParticleSystemMemory1D(numparticles, numsources, advanced, additionalbytes)) { + DEBUG_PRINT(F("PS init failed: memory depleted")); + return false; + } + PartSys = new (SEGENV.data) ParticleSystem1D(SEGMENT.virtualLength(), numparticles, numsources, advanced); // particle system constructor + return true; +} + +// blur a 1D buffer, sub-size blurring can be done using start and size +// for speed, 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined +// to blur a subset of the buffer, change the size and set start to the desired starting coordinates +void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, uint32_t start) +{ + CRGB seeppart, carryover; + uint32_t seep = blur >> 1; + carryover = BLACK; + for (uint32_t x = start; x < start + size; x++) { + seeppart = colorbuffer[x]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if (x > 0) { + fast_color_add(colorbuffer[x-1], seeppart); + if (carryover) // note: check adds overhead but is faster on average + fast_color_add(colorbuffer[x], carryover); // is black on first pass + } + carryover = seeppart; + } +} +#endif // WLED_DISABLE_PARTICLESYSTEM1D + +#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled + +////////////////////////////// +// Shared Utility Functions // +////////////////////////////// + +// calculate the delta speed (dV) value and update the counter for force calculation (is used several times, function saves on codesize) +// force is in 3.4 fixedpoint notation, +/-127 +static int32_t calcForce_dv(const int8_t force, uint8_t &counter) { + if (force == 0) + return 0; + // for small forces, need to use a delay counter + int32_t force_abs = abs(force); // absolute value (faster than lots of if's only 7 instructions) + int32_t dv = 0; + // for small forces, need to use a delay counter, apply force only if it overflows + if (force_abs < 16) { + counter += force_abs; + if (counter > 15) { + counter -= 16; + dv = force < 0 ? -1 : 1; // force is either 1 or -1 if it is small (zero force is handled above) + } + } + else + dv = force / 16; // MSBs, note: cannot use bitshift as dv can be negative + + return dv; +} + +// check if particle is out of bounds and wrap it around if required, returns false if out of bounds +static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, const bool wrap) { + if ((uint32_t)position > (uint32_t)max) { // check if particle reached an edge, cast to uint32_t to save negative checking (max is always positive) + if (wrap) { + position = position % (max + 1); // note: cannot optimize modulo, particles can be far out of bounds when wrap is enabled + if (position < 0) + position += max + 1; + } + else if (((position < -particleradius) || (position > max + particleradius))) // particle is leaving boundaries, out of bounds if it has fully left + return false; // out of bounds + } + return true; // particle is in bounds +} + +// fastled color adding is very inaccurate in color preservation (but it is fast) +// a better color add function is implemented in colors.cpp but it uses 32bit RGBW. to use it colors need to be shifted just to then be shifted back by that function, which is slow +// this is a fast version for RGB (no white channel, PS does not handle white) and with native CRGB including scaling of second color +// note: result is stored in c1, not using a return value is faster as the CRGB struct does not need to be copied upon return +// note2: function is mainly used to add scaled colors, so checking if one color is black is slower +// note3: scale is 255 when using blur, checking for that makes blur faster + __attribute__((optimize("O2"))) static void fast_color_add(CRGB &c1, const CRGB &c2, const uint8_t scale) { + uint32_t r, g, b; + if (scale < 255) { + r = c1.r + ((c2.r * scale) >> 8); + g = c1.g + ((c2.g * scale) >> 8); + b = c1.b + ((c2.b * scale) >> 8); + } else { + r = c1.r + c2.r; + g = c1.g + c2.g; + b = c1.b + c2.b; + } + + // note: this chained comparison is the fastest method for max of 3 values (faster than std:max() or using xor) + uint32_t max = (r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b); + if (max <= 255) { + c1.r = r; // save result to c1 + c1.g = g; + c1.b = b; + } else { + uint32_t newscale = (255U << 16) / max; + c1.r = (r * newscale) >> 16; + c1.g = (g * newscale) >> 16; + c1.b = (b * newscale) >> 16; + } +} + +// faster than fastled color scaling as it does in place scaling + __attribute__((optimize("O2"))) static void fast_color_scale(CRGB &c, const uint8_t scale) { + c.r = ((c.r * scale) >> 8); + c.g = ((c.g * scale) >> 8); + c.b = ((c.b * scale) >> 8); +} + +#endif // !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h new file mode 100644 index 00000000..d188ae23 --- /dev/null +++ b/wled00/FXparticleSystem.h @@ -0,0 +1,390 @@ +/* + FXparticleSystem.cpp + + Particle system with functions for particle generation, particle movement and particle rendering to RGB matrix. + by DedeHai (Damian Schneider) 2013-2024 + + Copyright (c) 2024 Damian Schneider + Licensed under the EUPL v. 1.2 or later +*/ + +#ifdef WLED_DISABLE_2D +#define WLED_DISABLE_PARTICLESYSTEM2D +#endif + +#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled + +#include +#include "wled.h" + +#define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8) +#define MAX_MEMIDLE 10 // max idle time (in frames) before memory is deallocated (if deallocated during an effect, it will crash!) + +//#define WLED_DEBUG_PS // note: enabling debug uses ~3k of flash + +#ifdef WLED_DEBUG_PS + #define PSPRINT(x) Serial.print(x) + #define PSPRINTLN(x) Serial.println(x) +#else + #define PSPRINT(x) + #define PSPRINTLN(x) +#endif + +// limit speed of particles (used in 1D and 2D) +static inline int32_t limitSpeed(const int32_t speed) { + return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); // note: this is slightly faster than using min/max at the cost of 50bytes of flash +} +#endif + +#ifndef WLED_DISABLE_PARTICLESYSTEM2D +// memory allocation +#define ESP8266_MAXPARTICLES 256 // enough up to 16x16 pixels +#define ESP8266_MAXSOURCES 24 +#define ESP32S2_MAXPARTICLES 1024 // enough up to 32x32 pixels +#define ESP32S2_MAXSOURCES 64 +#define ESP32_MAXPARTICLES 2048 // enough up to 64x32 pixels +#define ESP32_MAXSOURCES 128 + +// particle dimensions (subpixel division) +#define PS_P_RADIUS 64 // subpixel size, each pixel is divided by this for particle movement (must be a power of 2) +#define PS_P_HALFRADIUS (PS_P_RADIUS >> 1) +#define PS_P_RADIUS_SHIFT 6 // shift for RADIUS +#define PS_P_SURFACE 12 // shift: 2^PS_P_SURFACE = (PS_P_RADIUS)^2 +#define PS_P_MINHARDRADIUS 64 // minimum hard surface radius for collisions +#define PS_P_MINSURFACEHARDNESS 128 // minimum hardness used in collision impulse calculation, below this hardness, particles become sticky + +// struct for PS settings (shared for 1D and 2D class) +typedef union { + struct{ // one byte bit field for 2D settings + bool wrapX : 1; + bool wrapY : 1; + bool bounceX : 1; + bool bounceY : 1; + bool killoutofbounds : 1; // if set, out of bound particles are killed immediately + bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top + bool useCollisions : 1; + bool colorByAge : 1; // if set, particle hue is set by ttl value in render function + }; + byte asByte; // access as a byte, order is: LSB is first entry in the list above +} PSsettings2D; + +//struct for a single particle +typedef struct { // 10 bytes + int16_t x; // x position in particle system + int16_t y; // y position in particle system + uint16_t ttl; // time to live in frames + int8_t vx; // horizontal velocity + int8_t vy; // vertical velocity + uint8_t hue; // color hue + uint8_t sat; // particle color saturation +} PSparticle; + +//struct for particle flags note: this is separate from the particle struct to save memory (ram alignment) +typedef union { + struct { // 1 byte + bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area + bool collide : 1; // if set, particle takes part in collisions + bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) + bool custom1 : 1; // unused custom flags, can be used by FX to track particle states + bool custom2 : 1; + bool custom3 : 1; + bool custom4 : 1; + bool custom5 : 1; + }; + byte asByte; // access as a byte, order is: LSB is first entry in the list above +} PSparticleFlags; + +// struct for additional particle settings (option) +typedef struct { // 2 bytes + uint8_t size; // particle size, 255 means 10 pixels in diameter, 0 means use global size (including single pixel rendering) + uint8_t forcecounter; // counter for applying forces to individual particles +} PSadvancedParticle; + +// struct for advanced particle size control (option) +typedef struct { // 8 bytes + uint8_t asymmetry; // asymmetrical size (0=symmetrical, 255 fully asymmetric) + uint8_t asymdir; // direction of asymmetry, 64 is x, 192 is y (0 and 128 is symmetrical) + uint8_t maxsize; // target size for growing + uint8_t minsize; // target size for shrinking + uint8_t sizecounter : 4; // counters used for size contol (grow/shrink/wobble) + uint8_t wobblecounter : 4; + uint8_t growspeed : 4; + uint8_t shrinkspeed : 4; + uint8_t wobblespeed : 4; + bool grow : 1; // flags + bool shrink : 1; + bool pulsate : 1; // grows & shrinks & grows & ... + bool wobble : 1; // alternate x and y size +} PSsizeControl; + + +//struct for a particle source (20 bytes) +typedef struct { + uint16_t minLife; // minimum ttl of emittet particles + uint16_t maxLife; // maximum ttl of emitted particles + PSparticle source; // use a particle as the emitter source (speed, position, color) + PSparticleFlags sourceFlags; // flags for the source particle + int8_t var; // variation of emitted speed (adds random(+/- var) to speed) + int8_t vx; // emitting speed + int8_t vy; + uint8_t size; // particle size (advanced property), global size is added on top to this size +} PSsource; + +// class uses approximately 60 bytes +class ParticleSystem2D { +public: + ParticleSystem2D(const uint32_t width, const uint32_t height, const uint32_t numberofparticles, const uint32_t numberofsources, const bool isadvanced = false, const bool sizecontrol = false); // constructor + // note: memory is allcated in the FX function, no deconstructor needed + void update(void); //update the particles according to set options and render to the matrix + void updateFire(const uint8_t intensity, const bool renderonly); // update function for fire, if renderonly is set, particles are not updated (required to fix transitions with frameskips) + void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions + void particleMoveUpdate(PSparticle &part, PSparticleFlags &partFlags, PSsettings2D *options = NULL, PSadvancedParticle *advancedproperties = NULL); // move function + // particle emitters + int32_t sprayEmit(const PSsource &emitter); + void flameEmit(const PSsource &emitter); + int32_t angleEmit(PSsource& emitter, const uint16_t angle, const int32_t speed); + //particle physics + void applyGravity(PSparticle &part); // applies gravity to single particle (use this for sources) + [[gnu::hot]] void applyForce(PSparticle &part, const int8_t xforce, const int8_t yforce, uint8_t &counter); + [[gnu::hot]] void applyForce(const uint32_t particleindex, const int8_t xforce, const int8_t yforce); // use this for advanced property particles + void applyForce(const int8_t xforce, const int8_t yforce); // apply a force to all particles + void applyAngleForce(PSparticle &part, const int8_t force, const uint16_t angle, uint8_t &counter); + void applyAngleForce(const uint32_t particleindex, const int8_t force, const uint16_t angle); // use this for advanced property particles + void applyAngleForce(const int8_t force, const uint16_t angle); // apply angular force to all particles + void applyFriction(PSparticle &part, const int32_t coefficient); // apply friction to specific particle + void applyFriction(const int32_t coefficient); // apply friction to all used particles + void pointAttractor(const uint32_t particleindex, PSparticle &attractor, const uint8_t strength, const bool swallow); + // set options note: inlining the set function uses more flash so dont optimize + void setUsedParticles(const uint8_t percentage); // set the percentage of particles used in the system, 255=100% + void setCollisionHardness(const uint8_t hardness); // hardness for particle collisions (255 means full hard) + void setWallHardness(const uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set + void setWallRoughness(const uint8_t roughness); // wall roughness randomizes wall collisions + void setMatrixSize(const uint32_t x, const uint32_t y); + void setWrapX(const bool enable); + void setWrapY(const bool enable); + void setBounceX(const bool enable); + void setBounceY(const bool enable); + void setKillOutOfBounds(const bool enable); // if enabled, particles outside of matrix instantly die + void setSaturation(const uint8_t sat); // set global color saturation + void setColorByAge(const bool enable); + void setMotionBlur(const uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero + void setSmearBlur(const uint8_t bluramount); // enable 2D smeared blurring of full frame + void setParticleSize(const uint8_t size); + void setGravity(const int8_t force = 8); + void enableParticleCollisions(const bool enable, const uint8_t hardness = 255); + + PSparticle *particles; // pointer to particle array + PSparticleFlags *particleFlags; // pointer to particle flags array + PSsource *sources; // pointer to sources + PSadvancedParticle *advPartProps; // pointer to advanced particle properties (can be NULL) + PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL) + uint8_t* PSdataEnd; // points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data + int32_t maxX, maxY; // particle system size i.e. width-1 / height-1 in subpixels, Note: all "max" variables must be signed to compare to coordinates (which are signed) + int32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1 + uint32_t numSources; // number of sources + uint32_t usedParticles; // number of particles used in animation, is relative to 'numParticles' + //note: some variables are 32bit for speed and code size at the cost of ram + +private: + //rendering functions + void render(); + [[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB& color, const bool wrapX, const bool wrapY); + //paricle physics applied by system if flags are set + void applyGravity(); // applies gravity to all particles + void handleCollisions(); + [[gnu::hot]] void collideParticles(PSparticle &particle1, PSparticle &particle2, const int32_t dx, const int32_t dy, const uint32_t collDistSq); + void fireParticleupdate(); + //utility functions + void updatePSpointers(const bool isadvanced, const bool sizecontrol); // update the data pointers to current segment data space + bool updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control + void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize); + [[gnu::hot]] void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition); // bounce on a wall + // note: variables that are accessed often are 32bit for speed + CRGB *framebuffer; // local frame buffer for rendering + PSsettings2D particlesettings; // settings used when updating particles (can also used by FX to move sources), do not edit properties directly, use functions above + uint32_t numParticles; // total number of particles allocated by this system + uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster + int32_t collisionHardness; + uint32_t wallHardness; + uint32_t wallRoughness; // randomizes wall collisions + uint32_t particleHardRadius; // hard surface radius of a particle, used for collision detection (32bit for speed) + uint16_t collisionStartIdx; // particle array start index for collision detection + uint8_t fireIntesity = 0; // fire intensity, used for fire mode (flash use optimization, better than passing an argument to render function) + uint8_t forcecounter; // counter for globally applied forces + uint8_t gforcecounter; // counter for global gravity + int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) + // global particle properties for basic particles + uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels, 255 = 10 pixels (note: this is also added to individual sized particles, set to 0 or 1 for standard advanced particle rendering) + uint8_t motionBlur; // motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 + uint8_t smearBlur; // 2D smeared blurring of full frame +}; + +void blur2D(CRGB *colorbuffer, const uint32_t xsize, uint32_t ysize, const uint32_t xblur, const uint32_t yblur, const uint32_t xstart = 0, uint32_t ystart = 0, const bool isparticle = false); +// initialization functions (not part of class) +bool initParticleSystem2D(ParticleSystem2D *&PartSys, const uint32_t requestedsources, const uint32_t additionalbytes = 0, const bool advanced = false, const bool sizecontrol = false); +uint32_t calculateNumberOfParticles2D(const uint32_t pixels, const bool advanced, const bool sizecontrol); +uint32_t calculateNumberOfSources2D(const uint32_t pixels, const uint32_t requestedsources); +bool allocateParticleSystemMemory2D(const uint32_t numparticles, const uint32_t numsources, const bool advanced, const bool sizecontrol, const uint32_t additionalbytes); +#endif // WLED_DISABLE_PARTICLESYSTEM2D + +//////////////////////// +// 1D Particle System // +//////////////////////// +#ifndef WLED_DISABLE_PARTICLESYSTEM1D +// memory allocation +#define ESP8266_MAXPARTICLES_1D 320 +#define ESP8266_MAXSOURCES_1D 16 +#define ESP32S2_MAXPARTICLES_1D 1300 +#define ESP32S2_MAXSOURCES_1D 32 +#define ESP32_MAXPARTICLES_1D 2600 +#define ESP32_MAXSOURCES_1D 64 + +// particle dimensions (subpixel division) +#define PS_P_RADIUS_1D 32 // subpixel size, each pixel is divided by this for particle movement, if this value is changed, also change the shift defines (next two lines) +#define PS_P_HALFRADIUS_1D (PS_P_RADIUS_1D >> 1) +#define PS_P_RADIUS_SHIFT_1D 5 // 1 << PS_P_RADIUS_SHIFT = PS_P_RADIUS +#define PS_P_SURFACE_1D 5 // shift: 2^PS_P_SURFACE = PS_P_RADIUS_1D +#define PS_P_MINHARDRADIUS_1D 32 // minimum hard surface radius note: do not change or hourglass effect will be broken +#define PS_P_MINSURFACEHARDNESS_1D 120 // minimum hardness used in collision impulse calculation + +// struct for PS settings (shared for 1D and 2D class) +typedef union { + struct{ + // one byte bit field for 1D settings + bool wrap : 1; + bool bounce : 1; + bool killoutofbounds : 1; // if set, out of bound particles are killed immediately + bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top + bool useCollisions : 1; + bool colorByAge : 1; // if set, particle hue is set by ttl value in render function + bool colorByPosition : 1; // if set, particle hue is set by its position in the strip segment + bool unused : 1; + }; + byte asByte; // access as a byte, order is: LSB is first entry in the list above +} PSsettings1D; + +//struct for a single particle (8 bytes) +typedef struct { + int32_t x; // x position in particle system + uint16_t ttl; // time to live in frames + int8_t vx; // horizontal velocity + uint8_t hue; // color hue +} PSparticle1D; + +//struct for particle flags +typedef union { + struct { // 1 byte + bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area + bool collide : 1; // if set, particle takes part in collisions + bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) + bool reversegrav : 1; // if set, gravity is reversed on this particle + bool forcedirection : 1; // direction the force was applied, 1 is positive x-direction (used for collision stacking, similar to reversegrav) TODO: not used anymore, can be removed + bool fixed : 1; // if set, particle does not move (and collisions make other particles revert direction), + bool custom1 : 1; // unused custom flags, can be used by FX to track particle states + bool custom2 : 1; + }; + byte asByte; // access as a byte, order is: LSB is first entry in the list above +} PSparticleFlags1D; + +// struct for additional particle settings (optional) +typedef struct { + uint8_t sat; //color saturation + uint8_t size; // particle size, 255 means 10 pixels in diameter, this overrides global size setting + uint8_t forcecounter; +} PSadvancedParticle1D; + +//struct for a particle source (20 bytes) +typedef struct { + uint16_t minLife; // minimum ttl of emittet particles + uint16_t maxLife; // maximum ttl of emitted particles + PSparticle1D source; // use a particle as the emitter source (speed, position, color) + PSparticleFlags1D sourceFlags; // flags for the source particle + int8_t var; // variation of emitted speed (adds random(+/- var) to speed) + int8_t v; // emitting speed + uint8_t sat; // color saturation (advanced property) + uint8_t size; // particle size (advanced property) + // note: there is 3 bytes of padding added here +} PSsource1D; + +class ParticleSystem1D +{ +public: + ParticleSystem1D(const uint32_t length, const uint32_t numberofparticles, const uint32_t numberofsources, const bool isadvanced = false); // constructor + // note: memory is allcated in the FX function, no deconstructor needed + void update(void); //update the particles according to set options and render to the matrix + void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions + // particle emitters + int32_t sprayEmit(const PSsource1D &emitter); + void particleMoveUpdate(PSparticle1D &part, PSparticleFlags1D &partFlags, PSsettings1D *options = NULL, PSadvancedParticle1D *advancedproperties = NULL); // move function + //particle physics + [[gnu::hot]] void applyForce(PSparticle1D &part, const int8_t xforce, uint8_t &counter); //apply a force to a single particle + void applyForce(const int8_t xforce); // apply a force to all particles + void applyGravity(PSparticle1D &part, PSparticleFlags1D &partFlags); // applies gravity to single particle (use this for sources) + void applyFriction(const int32_t coefficient); // apply friction to all used particles + // set options + void setUsedParticles(const uint8_t percentage); // set the percentage of particles used in the system, 255=100% + void setWallHardness(const uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set + void setSize(const uint32_t x); //set particle system size (= strip length) + void setWrap(const bool enable); + void setBounce(const bool enable); + void setKillOutOfBounds(const bool enable); // if enabled, particles outside of matrix instantly die + // void setSaturation(uint8_t sat); // set global color saturation + void setColorByAge(const bool enable); + void setColorByPosition(const bool enable); + void setMotionBlur(const uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero + void setSmearBlur(const uint8_t bluramount); // enable 1D smeared blurring of full frame + void setParticleSize(const uint8_t size); //size 0 = 1 pixel, size 1 = 2 pixels, is overruled if advanced particle is used + void setGravity(int8_t force = 8); + void enableParticleCollisions(bool enable, const uint8_t hardness = 255); + + PSparticle1D *particles; // pointer to particle array + PSparticleFlags1D *particleFlags; // pointer to particle flags array + PSsource1D *sources; // pointer to sources + PSadvancedParticle1D *advPartProps; // pointer to advanced particle properties (can be NULL) + //PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL) + uint8_t* PSdataEnd; // points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data + int32_t maxX; // particle system size i.e. width-1, Note: all "max" variables must be signed to compare to coordinates (which are signed) + int32_t maxXpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 + uint32_t numSources; // number of sources + uint32_t usedParticles; // number of particles used in animation, is relative to 'numParticles' + +private: + //rendering functions + void render(void); + [[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB &color, const bool wrap); + + //paricle physics applied by system if flags are set + void applyGravity(); // applies gravity to all particles + void handleCollisions(); + [[gnu::hot]] void collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, const int32_t dx, const uint32_t dx_abs, const uint32_t collisiondistance); + + //utility functions + void updatePSpointers(const bool isadvanced); // update the data pointers to current segment data space + //void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control + [[gnu::hot]] void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition); // bounce on a wall + // note: variables that are accessed often are 32bit for speed + #ifndef ESP8266 + CRGB *framebuffer; // local frame buffer for rendering + #endif + PSsettings1D particlesettings; // settings used when updating particles + uint32_t numParticles; // total number of particles allocated by this system + uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster + int32_t collisionHardness; + uint32_t particleHardRadius; // hard surface radius of a particle, used for collision detection + uint32_t wallHardness; + uint8_t gforcecounter; // counter for global gravity + int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) + uint8_t forcecounter; // counter for globally applied forces + uint16_t collisionStartIdx; // particle array start index for collision detection + //global particle properties for basic particles + uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels, is overruled by advanced particle size + uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations + uint8_t smearBlur; // smeared blurring of full frame +}; + +bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedsources, const uint8_t fractionofparticles = 255, const uint32_t additionalbytes = 0, const bool advanced = false); +uint32_t calculateNumberOfParticles1D(const uint32_t fraction, const bool isadvanced); +uint32_t calculateNumberOfSources1D(const uint32_t requestedsources); +bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t numsources, const bool isadvanced, const uint32_t additionalbytes); +void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, uint32_t start); +#endif // WLED_DISABLE_PARTICLESYSTEM1D diff --git a/wled00/alexa.cpp b/wled00/alexa.cpp index b108f294..81b9ec34 100644 --- a/wled00/alexa.cpp +++ b/wled00/alexa.cpp @@ -126,10 +126,10 @@ void onAlexaChange(EspalexaDevice* dev) } else { colorKtoRGB(k, rgbw); } - strip.setColor(0, RGBW32(rgbw[0], rgbw[1], rgbw[2], rgbw[3])); + strip.getMainSegment().setColor(0, RGBW32(rgbw[0], rgbw[1], rgbw[2], rgbw[3])); } else { uint32_t color = dev->getRGB(); - strip.setColor(0, color); + strip.getMainSegment().setColor(0, color); } stateUpdated(CALL_MODE_ALEXA); } diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 9e4607a0..7c30d7fb 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -16,10 +16,14 @@ #define LEDC_MUTEX_UNLOCK() #endif #endif +#ifdef ESP8266 +#include "core_esp8266_waveform.h" +#endif #include "const.h" #include "pin_manager.h" -#include "bus_wrapper.h" #include "bus_manager.h" +#include "bus_wrapper.h" +#include // functions to get/set bits in an array - based on functions created by Brandon for GOL // toDo : make this a class that's completely defined in a header file @@ -30,19 +34,20 @@ bool getBitFromArray(const uint8_t* byteArray, size_t position) { // get bit val return (byteValue >> bitIndex) & 1; } extern bool cctICused; +extern bool useParallelI2S; void setBitInArray(uint8_t* byteArray, size_t position, bool value) { // set bit - with error handling for nullptr //if (byteArray == nullptr) return; size_t byteIndex = position / 8; unsigned bitIndex = position % 8; if (value) - byteArray[byteIndex] |= (1 << bitIndex); + byteArray[byteIndex] |= (1 << bitIndex); else byteArray[byteIndex] &= ~(1 << bitIndex); } size_t getBitArrayBytes(size_t num_bits) { // number of bytes needed for an array with num_bits bits - return (num_bits + 7) / 8; + return (num_bits + 7) / 8; } void setBitArray(uint8_t* byteArray, size_t numBits, bool value) { // set all bits to same value @@ -56,29 +61,30 @@ void setBitArray(uint8_t* byteArray, size_t numBits, bool value) { // set all b uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb); //udp.cpp -uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, byte *buffer, uint8_t bri=255, bool isRGBW=false); +uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const byte *buffer, uint8_t bri=255, bool isRGBW=false); -// enable additional debug output -#if defined(WLED_DEBUG_HOST) - #include "net_debug.h" - #define DEBUGOUT NetDebug +//util.cpp +// PSRAM allocation wrappers +#ifndef ESP8266 +extern "C" { + void *p_malloc(size_t); // prefer PSRAM over DRAM + void *p_calloc(size_t, size_t); // prefer PSRAM over DRAM + void *p_realloc(void *, size_t); // prefer PSRAM over DRAM + inline void p_free(void *ptr) { heap_caps_free(ptr); } + void *d_malloc(size_t); // prefer DRAM over PSRAM + void *d_calloc(size_t, size_t); // prefer DRAM over PSRAM + void *d_realloc(void *, size_t); // prefer DRAM over PSRAM + inline void d_free(void *ptr) { heap_caps_free(ptr); } +} #else - #define DEBUGOUT Serial -#endif - -#ifdef WLED_DEBUG - #ifndef ESP8266 - #include - #endif - #define DEBUG_PRINT(x) DEBUGOUT.print(x) - #define DEBUG_PRINTLN(x) DEBUGOUT.println(x) - #define DEBUG_PRINTF(x...) DEBUGOUT.printf(x) - #define DEBUG_PRINTF_P(x...) DEBUGOUT.printf_P(x) -#else - #define DEBUG_PRINT(x) - #define DEBUG_PRINTLN(x) - #define DEBUG_PRINTF(x...) - #define DEBUG_PRINTF_P(x...) +#define p_malloc malloc +#define p_calloc calloc +#define p_realloc realloc +#define p_free free +#define d_malloc malloc +#define d_calloc calloc +#define d_realloc realloc +#define d_free free #endif //color mangling macros @@ -89,19 +95,20 @@ uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, byte #define W(c) (byte((c) >> 24)) +static ColorOrderMap _colorOrderMap = {}; + bool ColorOrderMap::add(uint16_t start, uint16_t len, uint8_t colorOrder) { if (count() >= WLED_MAX_COLOR_ORDER_MAPPINGS || len == 0 || (colorOrder & 0x0F) > COL_ORDER_MAX) return false; // upper nibble contains W swap information _mappings.push_back({start,len,colorOrder}); + DEBUGBUS_PRINTF_P(PSTR("Bus: Add COM (%d,%d,%d)\n"), (int)start, (int)len, (int)colorOrder); return true; } uint8_t IRAM_ATTR ColorOrderMap::getPixelColorOrder(uint16_t pix, uint8_t defaultColorOrder) const { // upper nibble contains W swap information // when ColorOrderMap's upper nibble contains value >0 then swap information is used from it, otherwise global swap is used - for (unsigned i = 0; i < count(); i++) { - if (pix >= _mappings[i].start && pix < (_mappings[i].start + _mappings[i].len)) { - return _mappings[i].colorOrder | ((_mappings[i].colorOrder >> 4) ? 0 : (defaultColorOrder & 0xF0)); - } + for (const auto& map : _mappings) { + if (pix >= map.start && pix < (map.start + map.len)) return map.colorOrder | ((map.colorOrder >> 4) ? 0 : (defaultColorOrder & 0xF0)); } return defaultColorOrder; } @@ -144,56 +151,49 @@ uint32_t Bus::autoWhiteCalc(uint32_t c) const { return RGBW32(r, g, b, w); } -uint8_t *Bus::allocateData(size_t size) { - if (_data) free(_data); // should not happen, but for safety - return _data = (uint8_t *)(size>0 ? calloc(size, sizeof(uint8_t)) : nullptr); -} - -BusDigital::BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com) +BusDigital::BusDigital(const BusConfig &bc, uint8_t nr) : Bus(bc.type, bc.start, bc.autoWhite, bc.count, bc.reversed, (bc.refreshReq || bc.type == TYPE_TM1814)) , _skip(bc.skipAmount) //sacrificial pixels , _colorOrder(bc.colorOrder) , _milliAmpsPerLed(bc.milliAmpsPerLed) , _milliAmpsMax(bc.milliAmpsMax) -, _colorOrderMap(com) { - if (!isDigital(bc.type) || !bc.count) return; - if (!PinManager::allocatePin(bc.pins[0], true, PinOwner::BusDigital)) return; + DEBUGBUS_PRINTLN(F("Bus: Creating digital bus.")); + if (!isDigital(bc.type) || !bc.count) { DEBUGBUS_PRINTLN(F("Not digial or empty bus!")); return; } + if (!PinManager::allocatePin(bc.pins[0], true, PinOwner::BusDigital)) { DEBUGBUS_PRINTLN(F("Pin 0 allocated!")); return; } _frequencykHz = 0U; _pins[0] = bc.pins[0]; if (is2Pin(bc.type)) { if (!PinManager::allocatePin(bc.pins[1], true, PinOwner::BusDigital)) { cleanup(); + DEBUGBUS_PRINTLN(F("Pin 1 allocated!")); return; } _pins[1] = bc.pins[1]; _frequencykHz = bc.frequency ? bc.frequency : 2000U; // 2MHz clock if undefined } _iType = PolyBus::getI(bc.type, _pins, nr); - if (_iType == I_NONE) return; + if (_iType == I_NONE) { DEBUGBUS_PRINTLN(F("Incorrect iType!")); return; } _hasRgb = hasRGB(bc.type); _hasWhite = hasWhite(bc.type); _hasCCT = hasCCT(bc.type); - if (bc.doubleBuffer && !allocateData(bc.count * Bus::getNumberOfChannels(bc.type))) return; - //_buffering = bc.doubleBuffer; uint16_t lenToCreate = bc.count; if (bc.type == TYPE_WS2812_1CH_X3) lenToCreate = NUM_ICS_WS2812_1CH_3X(bc.count); // only needs a third of "RGB" LEDs for NeoPixelBus _busPtr = PolyBus::create(_iType, _pins, lenToCreate + _skip, nr); - _valid = (_busPtr != nullptr); - DEBUG_PRINTF_P(PSTR("%successfully inited strip %u (len %u) with type %u and pins %u,%u (itype %u). mA=%d/%d\n"), _valid?"S":"Uns", nr, bc.count, bc.type, _pins[0], is2Pin(bc.type)?_pins[1]:255, _iType, _milliAmpsPerLed, _milliAmpsMax); + _valid = (_busPtr != nullptr) && bc.count > 0; + DEBUGBUS_PRINTF_P(PSTR("Bus: %successfully inited #%u (len:%u, type:%u (RGB:%d, W:%d, CCT:%d), pins:%u,%u [itype:%u] mA=%d/%d)\n"), + _valid?"S":"Uns", + (int)nr, + (int)bc.count, + (int)bc.type, + (int)_hasRgb, (int)_hasWhite, (int)_hasCCT, + (unsigned)_pins[0], is2Pin(bc.type)?(unsigned)_pins[1]:255U, + (unsigned)_iType, + (int)_milliAmpsPerLed, (int)_milliAmpsMax + ); } -//fine tune power estimation constants for your setup -//you can set it to 0 if the ESP is powered by USB and the LEDs by external -#ifndef MA_FOR_ESP - #ifdef ESP8266 - #define MA_FOR_ESP 80 //how much mA does the ESP use (Wemos D1 about 80mA) - #else - #define MA_FOR_ESP 120 //how much mA does the ESP use (ESP32 about 120mA) - #endif -#endif - //DISCLAIMER //The following function attemps to calculate the current LED power usage, //and will limit the brightness to stay below a set amperage threshold. @@ -202,7 +202,7 @@ BusDigital::BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com) //I am NOT to be held liable for burned down garages or houses! // To disable brightness limiter we either set output max current to 0 or single LED current to 0 -uint8_t BusDigital::estimateCurrentAndLimitBri() { +uint8_t BusDigital::estimateCurrentAndLimitBri() const { bool useWackyWS2815PowerModel = false; byte actualMilliampsPerLed = _milliAmpsPerLed; @@ -215,7 +215,7 @@ uint8_t BusDigital::estimateCurrentAndLimitBri() { actualMilliampsPerLed = 12; // from testing an actual strip } - size_t powerBudget = (_milliAmpsMax - MA_FOR_ESP/BusManager::getNumBusses()); //80/120mA for ESP power + unsigned powerBudget = (_milliAmpsMax - MA_FOR_ESP/BusManager::getNumBusses()); //80/120mA for ESP power if (powerBudget > getLength()) { //each LED uses about 1mA in standby, exclude that from power budget powerBudget -= getLength(); } else { @@ -240,63 +240,26 @@ uint8_t BusDigital::estimateCurrentAndLimitBri() { } // powerSum has all the values of channels summed (max would be getLength()*765 as white is excluded) so convert to milliAmps - busPowerSum = (busPowerSum * actualMilliampsPerLed) / 765; - _milliAmpsTotal = busPowerSum * _bri / 255; + BusDigital::_milliAmpsTotal = (busPowerSum * actualMilliampsPerLed * _bri) / (765*255); uint8_t newBri = _bri; - if (busPowerSum * _bri / 255 > powerBudget) { //scale brightness down to stay in current limit - float scale = (float)(powerBudget * 255) / (float)(busPowerSum * _bri); - if (scale >= 1.0f) return _bri; - _milliAmpsTotal = ceilf((float)_milliAmpsTotal * scale); - uint8_t scaleB = min((int)(scale * 255), 255); - newBri = unsigned(_bri * scaleB) / 256 + 1; + if (BusDigital::_milliAmpsTotal > powerBudget) { + //scale brightness down to stay in current limit + unsigned scaleB = powerBudget * 255 / BusDigital::_milliAmpsTotal; + newBri = (_bri * scaleB) / 256 + 1; + BusDigital::_milliAmpsTotal = powerBudget; + //_milliAmpsTotal = (busPowerSum * actualMilliampsPerLed * newBri) / (765*255); } return newBri; } void BusDigital::show() { - _milliAmpsTotal = 0; + BusDigital::_milliAmpsTotal = 0; if (!_valid) return; uint8_t cctWW = 0, cctCW = 0; - unsigned newBri = estimateCurrentAndLimitBri(); // will fill _milliAmpsTotal + unsigned newBri = estimateCurrentAndLimitBri(); // will fill _milliAmpsTotal (TODO: could use PolyBus::CalcTotalMilliAmpere()) if (newBri < _bri) PolyBus::setBrightness(_busPtr, _iType, newBri); // limit brightness to stay within current limits - - if (_data) { - size_t channels = getNumberOfChannels(); - int16_t oldCCT = Bus::_cct; // temporarily save bus CCT - for (size_t i=0; i<_len; i++) { - size_t offset = i * channels; - unsigned co = _colorOrderMap.getPixelColorOrder(i+_start, _colorOrder); - uint32_t c; - if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs (_len is always a multiple of 3) - switch (i%3) { - case 0: c = RGBW32(_data[offset] , _data[offset+1], _data[offset+2], 0); break; - case 1: c = RGBW32(_data[offset-1], _data[offset] , _data[offset+1], 0); break; - case 2: c = RGBW32(_data[offset-2], _data[offset-1], _data[offset] , 0); break; - } - } else { - if (hasRGB()) c = RGBW32(_data[offset], _data[offset+1], _data[offset+2], hasWhite() ? _data[offset+3] : 0); - else c = RGBW32(0, 0, 0, _data[offset]); - } - if (hasCCT()) { - // unfortunately as a segment may span multiple buses or a bus may contain multiple segments and each segment may have different CCT - // we need to extract and appy CCT value for each pixel individually even though all buses share the same _cct variable - // TODO: there is an issue if CCT is calculated from RGB value (_cct==-1), we cannot do that with double buffer - Bus::_cct = _data[offset+channels-1]; - Bus::calculateCCT(c, cctWW, cctCW); - } - unsigned pix = i; - if (_reversed) pix = _len - pix -1; - pix += _skip; - PolyBus::setPixelColor(_busPtr, _iType, pix, c, co, (cctCW<<8) | cctWW); - } - #if !defined(STATUSLED) || STATUSLED>=0 - if (_skip) PolyBus::setPixelColor(_busPtr, _iType, 0, 0, _colorOrderMap.getPixelColorOrder(_start, _colorOrder)); // paint skipped pixels black - #endif - for (int i=1; i<_skip; i++) PolyBus::setPixelColor(_busPtr, _iType, i, 0, _colorOrderMap.getPixelColorOrder(_start, _colorOrder)); // paint skipped pixels black - Bus::_cct = oldCCT; - } else { if (newBri < _bri) { unsigned hwLen = _len; if (_type == TYPE_WS2812_1CH_X3) hwLen = NUM_ICS_WS2812_1CH_3X(_len); // only needs a third of "RGB" LEDs for NeoPixelBus @@ -307,8 +270,7 @@ void BusDigital::show() { PolyBus::setPixelColor(_busPtr, _iType, i, c, 0, (cctCW<<8) | cctWW); // repaint all pixels with new brightness } } - } - PolyBus::show(_busPtr, _iType, !_data); // faster if buffer consistency is not important (use !_buffering this causes 20% FPS drop) + PolyBus::show(_busPtr, _iType, false); // faster if buffer consistency is not important // restore bus brightness to its original value // this is done right after show, so this is only OK if LED updates are completed before show() returns // or async show has a separate buffer (ESP32 RMT and I2S are ok) @@ -335,85 +297,74 @@ void BusDigital::setStatusPixel(uint32_t c) { } } -void IRAM_ATTR BusDigital::setPixelColor(uint16_t pix, uint32_t c) { +void IRAM_ATTR BusDigital::setPixelColor(unsigned pix, uint32_t c) { if (!_valid) return; - uint8_t cctWW = 0, cctCW = 0; if (hasWhite()) c = autoWhiteCalc(c); if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT - if (_data) { - size_t offset = pix * getNumberOfChannels(); - if (hasRGB()) { - _data[offset++] = R(c); - _data[offset++] = G(c); - _data[offset++] = B(c); + if (_reversed) pix = _len - pix -1; + pix += _skip; + unsigned co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); + if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs + unsigned pOld = pix; + pix = IC_INDEX_WS2812_1CH_3X(pix); + uint32_t cOld = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, pix, co),_bri); + switch (pOld % 3) { // change only the single channel (TODO: this can cause loss because of get/set) + case 0: c = RGBW32(R(cOld), W(c) , B(cOld), 0); break; + case 1: c = RGBW32(W(c) , G(cOld), B(cOld), 0); break; + case 2: c = RGBW32(R(cOld), G(cOld), W(c) , 0); break; } - if (hasWhite()) _data[offset++] = W(c); - // unfortunately as a segment may span multiple buses or a bus may contain multiple segments and each segment may have different CCT - // we need to store CCT value for each pixel (if there is a color correction in play, convert K in CCT ratio) - if (hasCCT()) _data[offset] = Bus::_cct >= 1900 ? (Bus::_cct - 1900) >> 5 : (Bus::_cct < 0 ? 127 : Bus::_cct); // TODO: if _cct == -1 we simply ignore it - } else { - if (_reversed) pix = _len - pix -1; - pix += _skip; - unsigned co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); - if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs - unsigned pOld = pix; - pix = IC_INDEX_WS2812_1CH_3X(pix); - uint32_t cOld = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, pix, co),_bri); - switch (pOld % 3) { // change only the single channel (TODO: this can cause loss because of get/set) - case 0: c = RGBW32(R(cOld), W(c) , B(cOld), 0); break; - case 1: c = RGBW32(W(c) , G(cOld), B(cOld), 0); break; - case 2: c = RGBW32(R(cOld), G(cOld), W(c) , 0); break; - } - } - if (hasCCT()) Bus::calculateCCT(c, cctWW, cctCW); - PolyBus::setPixelColor(_busPtr, _iType, pix, c, co, (cctCW<<8) | cctWW); } + uint16_t wwcw = 0; + if (hasCCT()) { + uint8_t cctWW = 0, cctCW = 0; + Bus::calculateCCT(c, cctWW, cctCW); + wwcw = (cctCW<<8) | cctWW; + if (_type == TYPE_WS2812_WWA) c = RGBW32(cctWW, cctCW, 0, W(c)); + } + PolyBus::setPixelColor(_busPtr, _iType, pix, c, co, wwcw); } // returns original color if global buffering is enabled, else returns lossly restored color from bus -uint32_t IRAM_ATTR BusDigital::getPixelColor(uint16_t pix) const { +uint32_t IRAM_ATTR BusDigital::getPixelColor(unsigned pix) const { if (!_valid) return 0; - if (_data) { - size_t offset = pix * getNumberOfChannels(); - uint32_t c; - if (!hasRGB()) { - c = RGBW32(_data[offset], _data[offset], _data[offset], _data[offset]); - } else { - c = RGBW32(_data[offset], _data[offset+1], _data[offset+2], hasWhite() ? _data[offset+3] : 0); + if (_reversed) pix = _len - pix -1; + pix += _skip; + const unsigned co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); + uint32_t c = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, (_type==TYPE_WS2812_1CH_X3) ? IC_INDEX_WS2812_1CH_3X(pix) : pix, co),_bri); + if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs + unsigned r = R(c); + unsigned g = _reversed ? B(c) : G(c); // should G and B be switched if _reversed? + unsigned b = _reversed ? G(c) : B(c); + switch (pix % 3) { // get only the single channel + case 0: c = RGBW32(g, g, g, g); break; + case 1: c = RGBW32(r, r, r, r); break; + case 2: c = RGBW32(b, b, b, b); break; } - return c; - } else { - if (_reversed) pix = _len - pix -1; - pix += _skip; - unsigned co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); - uint32_t c = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, (_type==TYPE_WS2812_1CH_X3) ? IC_INDEX_WS2812_1CH_3X(pix) : pix, co),_bri); - if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs - unsigned r = R(c); - unsigned g = _reversed ? B(c) : G(c); // should G and B be switched if _reversed? - unsigned b = _reversed ? G(c) : B(c); - switch (pix % 3) { // get only the single channel - case 0: c = RGBW32(g, g, g, g); break; - case 1: c = RGBW32(r, r, r, r); break; - case 2: c = RGBW32(b, b, b, b); break; - } - } - return c; } + if (_type == TYPE_WS2812_WWA) { + uint8_t w = R(c) | G(c); + c = RGBW32(w, w, 0, w); + } + return c; } -uint8_t BusDigital::getPins(uint8_t* pinArray) const { +size_t BusDigital::getPins(uint8_t* pinArray) const { unsigned numPins = is2Pin(_type) + 1; if (pinArray) for (unsigned i = 0; i < numPins; i++) pinArray[i] = _pins[i]; return numPins; } +size_t BusDigital::getBusSize() const { + return sizeof(BusDigital) + (isOk() ? PolyBus::getDataSize(_busPtr, _iType) /*+ (_data ? _len * getNumberOfChannels() : 0)*/ : 0); +} + void BusDigital::setColorOrder(uint8_t colorOrder) { // upper nibble contains W swap information if ((colorOrder & 0x0F) > 5) return; _colorOrder = colorOrder; } -// credit @willmmiles & @netmindz https://github.com/Aircoookie/WLED/pull/4056 +// credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056 std::vector BusDigital::getLEDTypes() { return { {TYPE_WS2812_RGB, "D", PSTR("WS281x")}, @@ -429,8 +380,8 @@ std::vector BusDigital::getLEDTypes() { {TYPE_WS2805, "D", PSTR("WS2805 RGBCW")}, {TYPE_SM16825, "D", PSTR("SM16825 RGBCW")}, {TYPE_WS2812_1CH_X3, "D", PSTR("WS2811 White")}, - //{TYPE_WS2812_2CH_X3, "D", PSTR("WS2811 CCT")}, // not implemented - //{TYPE_WS2812_WWA, "D", PSTR("WS2811 WWA")}, // not implemented + //{TYPE_WS2812_2CH_X3, "D", PSTR("WS281x CCT")}, // not implemented + {TYPE_WS2812_WWA, "D", PSTR("WS281x WWA")}, // amber ignored {TYPE_WS2801, "2P", PSTR("WS2801")}, {TYPE_APA102, "2P", PSTR("APA102")}, {TYPE_LPD8806, "2P", PSTR("LPD8806")}, @@ -445,12 +396,11 @@ void BusDigital::begin() { } void BusDigital::cleanup() { - DEBUG_PRINTLN(F("Digital Cleanup.")); + DEBUGBUS_PRINTLN(F("Digital Cleanup.")); PolyBus::cleanup(_busPtr, _iType); _iType = I_NONE; _valid = false; _busPtr = nullptr; - if (_data != nullptr) freeData(); PinManager::deallocatePin(_pins[1], PinOwner::BusDigital); PinManager::deallocatePin(_pins[0], PinOwner::BusDigital); } @@ -474,18 +424,18 @@ void BusDigital::cleanup() { #else #ifdef SOC_LEDC_TIMER_BIT_WIDE_NUM // C6/H2/P4: 20 bit, S2/S3/C2/C3: 14 bit - #define MAX_BIT_WIDTH SOC_LEDC_TIMER_BIT_WIDE_NUM + #define MAX_BIT_WIDTH SOC_LEDC_TIMER_BIT_WIDE_NUM #else // ESP32: 20 bit (but in reality we would never go beyond 16 bit as the frequency would be to low) #define MAX_BIT_WIDTH 14 #endif #endif -BusPwm::BusPwm(BusConfig &bc) +BusPwm::BusPwm(const BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite, 1, bc.reversed, bc.refreshReq) // hijack Off refresh flag to indicate usage of dithering { if (!isPWM(bc.type)) return; - unsigned numPins = numPWMPins(bc.type); + const unsigned numPins = numPWMPins(bc.type); [[maybe_unused]] const bool dithering = _needsRefresh; _frequency = bc.frequency ? bc.frequency : WLED_PWM_FREQ; // duty cycle resolution (_depth) can be extracted from this formula: CLOCK_FREQUENCY > _frequency * 2^_depth @@ -493,44 +443,44 @@ BusPwm::BusPwm(BusConfig &bc) managed_pin_type pins[numPins]; for (unsigned i = 0; i < numPins; i++) pins[i] = {(int8_t)bc.pins[i], true}; - if (!PinManager::allocateMultiplePins(pins, numPins, PinOwner::BusPwm)) return; - -#ifdef ESP8266 - analogWriteRange((1<<_depth)-1); - analogWriteFreq(_frequency); -#else - // for 2 pin PWM CCT strip pinManager will make sure both LEDC channels are in the same speed group and sharing the same timer - _ledcStart = PinManager::allocateLedc(numPins); - if (_ledcStart == 255) { //no more free LEDC channels - PinManager::deallocateMultiplePins(pins, numPins, PinOwner::BusPwm); - return; - } - // if _needsRefresh is true (UI hack) we are using dithering (credit @dedehai & @zalatnaicsongor) - if (dithering) _depth = 12; // fixed 8 bit depth PWM with 4 bit dithering (ESP8266 has no hardware to support dithering) -#endif - - for (unsigned i = 0; i < numPins; i++) { - _pins[i] = bc.pins[i]; // store only after allocateMultiplePins() succeeded + if (PinManager::allocateMultiplePins(pins, numPins, PinOwner::BusPwm)) { #ifdef ESP8266 - pinMode(_pins[i], OUTPUT); + analogWriteRange((1<<_depth)-1); + analogWriteFreq(_frequency); #else - unsigned channel = _ledcStart + i; - ledcSetup(channel, _frequency, _depth - (dithering*4)); // with dithering _frequency doesn't really matter as resolution is 8 bit - ledcAttachPin(_pins[i], channel); - // LEDC timer reset credit @dedehai - uint8_t group = (channel / 8), timer = ((channel / 2) % 4); // same fromula as in ledcSetup() - ledc_timer_rst((ledc_mode_t)group, (ledc_timer_t)timer); // reset timer so all timers are almost in sync (for phase shift) + // for 2 pin PWM CCT strip pinManager will make sure both LEDC channels are in the same speed group and sharing the same timer + _ledcStart = PinManager::allocateLedc(numPins); + if (_ledcStart == 255) { //no more free LEDC channels + PinManager::deallocateMultiplePins(pins, numPins, PinOwner::BusPwm); + DEBUGBUS_PRINTLN(F("No more free LEDC channels!")); + return; + } + // if _needsRefresh is true (UI hack) we are using dithering (credit @dedehai & @zalatnaicsongor) + if (dithering) _depth = 12; // fixed 8 bit depth PWM with 4 bit dithering (ESP8266 has no hardware to support dithering) #endif + + for (unsigned i = 0; i < numPins; i++) { + _pins[i] = bc.pins[i]; // store only after allocateMultiplePins() succeeded + #ifdef ESP8266 + pinMode(_pins[i], OUTPUT); + #else + unsigned channel = _ledcStart + i; + ledcSetup(channel, _frequency, _depth - (dithering*4)); // with dithering _frequency doesn't really matter as resolution is 8 bit + ledcAttachPin(_pins[i], channel); + // LEDC timer reset credit @dedehai + uint8_t group = (channel / 8), timer = ((channel / 2) % 4); // same fromula as in ledcSetup() + ledc_timer_rst((ledc_mode_t)group, (ledc_timer_t)timer); // reset timer so all timers are almost in sync (for phase shift) + #endif + } + _hasRgb = hasRGB(bc.type); + _hasWhite = hasWhite(bc.type); + _hasCCT = hasCCT(bc.type); + _valid = true; } - _hasRgb = hasRGB(bc.type); - _hasWhite = hasWhite(bc.type); - _hasCCT = hasCCT(bc.type); - _data = _pwmdata; // avoid malloc() and use stack - _valid = true; - DEBUG_PRINTF_P(PSTR("%successfully inited PWM strip with type %u, frequency %u, bit depth %u and pins %u,%u,%u,%u,%u\n"), _valid?"S":"Uns", bc.type, _frequency, _depth, _pins[0], _pins[1], _pins[2], _pins[3], _pins[4]); + DEBUGBUS_PRINTF_P(PSTR("%successfully inited PWM strip with type %u, frequency %u, bit depth %u and pins %u,%u,%u,%u,%u\n"), _valid?"S":"Uns", bc.type, _frequency, _depth, _pins[0], _pins[1], _pins[2], _pins[3], _pins[4]); } -void BusPwm::setPixelColor(uint16_t pix, uint32_t c) { +void BusPwm::setPixelColor(unsigned pix, uint32_t c) { if (pix != 0 || !_valid) return; //only react to first pixel if (_type != TYPE_ANALOG_3CH) c = autoWhiteCalc(c); if (Bus::_cct >= 1900 && (_type == TYPE_ANALOG_3CH || _type == TYPE_ANALOG_4CH)) { @@ -567,7 +517,7 @@ void BusPwm::setPixelColor(uint16_t pix, uint32_t c) { } //does no index check -uint32_t BusPwm::getPixelColor(uint16_t pix) const { +uint32_t BusPwm::getPixelColor(unsigned pix) const { if (!_valid) return 0; // TODO getting the reverse from CCT is involved (a quick approximation when CCT blending is ste to 0 implemented) switch (_type) { @@ -589,50 +539,57 @@ uint32_t BusPwm::getPixelColor(uint16_t pix) const { void BusPwm::show() { if (!_valid) return; + const size_t numPins = getPins(); +#ifdef ESP8266 + const unsigned analogPeriod = F_CPU / _frequency; + const unsigned maxBri = analogPeriod; // compute to clock cycle accuracy + constexpr bool dithering = false; + constexpr unsigned bitShift = 8; // 256 clocks for dead time, ~3us at 80MHz +#else // if _needsRefresh is true (UI hack) we are using dithering (credit @dedehai & @zalatnaicsongor) - // https://github.com/Aircoookie/WLED/pull/4115 and https://github.com/zalatnaicsongor/WLED/pull/1) + // https://github.com/wled/WLED/pull/4115 and https://github.com/zalatnaicsongor/WLED/pull/1) const bool dithering = _needsRefresh; // avoid working with bitfield - const unsigned numPins = getPins(); - const unsigned maxBri = (1<<_depth); // possible values: 16384 (14), 8192 (13), 4096 (12), 2048 (11), 1024 (10), 512 (9) and 256 (8) - [[maybe_unused]] const unsigned bitShift = dithering * 4; // if dithering, _depth is 12 bit but LEDC channel is set to 8 bit (using 4 fractional bits) - - // use CIE brightness formula (cubic) to fit (or approximate linearity of) human eye perceived brightness - // the formula is based on 12 bit resolution as there is no need for greater precision + const unsigned maxBri = (1<<_depth); // possible values: 16384 (14), 8192 (13), 4096 (12), 2048 (11), 1024 (10), 512 (9) and 256 (8) + const unsigned bitShift = dithering * 4; // if dithering, _depth is 12 bit but LEDC channel is set to 8 bit (using 4 fractional bits) +#endif + // use CIE brightness formula (linear + cubic) to approximate human eye perceived brightness // see: https://en.wikipedia.org/wiki/Lightness - unsigned pwmBri = (unsigned)_bri * 100; // enlarge to use integer math for linear response - if (pwmBri < 2040) { - // linear response for values [0-20] - pwmBri = ((pwmBri << 12) + 115043) / 230087; //adding '0.5' before division for correct rounding - } else { - // cubic response for values [21-255] - pwmBri += 4080; - float temp = (float)pwmBri / 29580.0f; - temp = temp * temp * temp * (float)maxBri; - pwmBri = (unsigned)temp; // pwmBri is in range [0-maxBri] + unsigned pwmBri = _bri; + if (pwmBri < 21) { // linear response for values [0-20] + pwmBri = (pwmBri * maxBri + 2300 / 2) / 2300 ; // adding '0.5' before division for correct rounding, 2300 gives a good match to CIE curve + } else { // cubic response for values [21-255] + float temp = float(pwmBri + 41) / float(255 + 41); // 41 is to match offset & slope to linear part + temp = temp * temp * temp * (float)maxBri; + pwmBri = (unsigned)temp; // pwmBri is in range [0-maxBri] C } [[maybe_unused]] unsigned hPoint = 0; // phase shift (0 - maxBri) // we will be phase shifting every channel by previous pulse length (plus dead time if required) - // phase shifting is only mandatory when using H-bridge to drive reverse-polarity PWM CCT (2 wire) LED type + // phase shifting is only mandatory when using H-bridge to drive reverse-polarity PWM CCT (2 wire) LED type // CCT additive blending must be 0 (WW & CW will not overlap) otherwise signals *will* overlap // for all other cases it will just try to "spread" the load on PSU // Phase shifting requires that LEDC timers are synchronised (see setup()). For PWM CCT (and H-bridge) it is // also mandatory that both channels use the same timer (pinManager takes care of that). for (unsigned i = 0; i < numPins; i++) { - unsigned duty = (_data[i] * pwmBri) / 255; - #ifdef ESP8266 - if (_reversed) duty = maxBri - duty; - analogWrite(_pins[i], duty); - #else - int deadTime = 0; + unsigned duty = (_data[i] * pwmBri) / 255; + unsigned deadTime = 0; + if (_type == TYPE_ANALOG_2CH && Bus::getCCTBlend() == 0) { // add dead time between signals (when using dithering, two full 8bit pulses are required) deadTime = (1+dithering) << bitShift; // we only need to take care of shortening the signal at (almost) full brightness otherwise pulses may overlap - if (_bri >= 254 && duty >= maxBri / 2 && duty < maxBri) duty -= deadTime << 1; // shorten duty of larger signal except if full on - if (_reversed) deadTime = -deadTime; // need to invert dead time to make phaseshift go the opposite way so low signals dont overlap + if (_bri >= 254 && duty >= maxBri / 2 && duty < maxBri) { + duty -= deadTime << 1; // shorten duty of larger signal except if full on + } } - if (_reversed) duty = maxBri - duty; + if (_reversed) { + if (i) hPoint += duty; // align start at time zero + duty = maxBri - duty; + } + #ifdef ESP8266 + //stopWaveform(_pins[i]); // can cause the waveform to miss a cycle. instead we risk crossovers. + startWaveformClockCycles(_pins[i], duty, analogPeriod - duty, 0, i ? _pins[0] : -1, hPoint, false); + #else unsigned channel = _ledcStart + i; unsigned gr = channel/8; // high/low speed group unsigned ch = channel%8; // group channel @@ -641,20 +598,22 @@ void BusPwm::show() { LEDC.channel_group[gr].channel[ch].duty.duty = duty << ((!dithering)*4); // lowest 4 bits are used for dithering, shift by 4 bits if not using dithering LEDC.channel_group[gr].channel[ch].hpoint.hpoint = hPoint >> bitShift; // hPoint is at _depth resolution (needs shifting if dithering) ledc_update_duty((ledc_mode_t)gr, (ledc_channel_t)ch); - hPoint += duty + deadTime; // offset to cascade the signals - if (hPoint >= maxBri) hPoint = 0; // offset it out of bounds, reset #endif + + if (!_reversed) hPoint += duty; + hPoint += deadTime; // offset to cascade the signals + if (hPoint >= maxBri) hPoint -= maxBri; // offset is out of bounds, reset } } -uint8_t BusPwm::getPins(uint8_t* pinArray) const { +size_t BusPwm::getPins(uint8_t* pinArray) const { if (!_valid) return 0; unsigned numPins = numPWMPins(_type); if (pinArray) for (unsigned i = 0; i < numPins; i++) pinArray[i] = _pins[i]; return numPins; } -// credit @willmmiles & @netmindz https://github.com/Aircoookie/WLED/pull/4056 +// credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056 std::vector BusPwm::getLEDTypes() { return { {TYPE_ANALOG_1CH, "A", PSTR("PWM White")}, @@ -667,7 +626,7 @@ std::vector BusPwm::getLEDTypes() { } void BusPwm::deallocatePins() { - unsigned numPins = getPins(); + size_t numPins = getPins(); for (unsigned i = 0; i < numPins; i++) { PinManager::deallocatePin(_pins[i], PinOwner::BusPwm); if (!PinManager::isPinOk(_pins[i])) continue; @@ -683,9 +642,9 @@ void BusPwm::deallocatePins() { } -BusOnOff::BusOnOff(BusConfig &bc) +BusOnOff::BusOnOff(const BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite, 1, bc.reversed) -, _onoffdata(0) +, _data(0) { if (!Bus::isOnOff(bc.type)) return; @@ -698,45 +657,44 @@ BusOnOff::BusOnOff(BusConfig &bc) _hasRgb = false; _hasWhite = false; _hasCCT = false; - _data = &_onoffdata; // avoid malloc() and use stack _valid = true; - DEBUG_PRINTF_P(PSTR("%successfully inited On/Off strip with pin %u\n"), _valid?"S":"Uns", _pin); + DEBUGBUS_PRINTF_P(PSTR("%successfully inited On/Off strip with pin %u\n"), _valid?"S":"Uns", _pin); } -void BusOnOff::setPixelColor(uint16_t pix, uint32_t c) { +void BusOnOff::setPixelColor(unsigned pix, uint32_t c) { if (pix != 0 || !_valid) return; //only react to first pixel c = autoWhiteCalc(c); uint8_t r = R(c); uint8_t g = G(c); uint8_t b = B(c); uint8_t w = W(c); - _data[0] = bool(r|g|b|w) && bool(_bri) ? 0xFF : 0; + _data = bool(r|g|b|w) && bool(_bri) ? 0xFF : 0; } -uint32_t BusOnOff::getPixelColor(uint16_t pix) const { +uint32_t BusOnOff::getPixelColor(unsigned pix) const { if (!_valid) return 0; - return RGBW32(_data[0], _data[0], _data[0], _data[0]); + return RGBW32(_data, _data, _data, _data); } void BusOnOff::show() { if (!_valid) return; - digitalWrite(_pin, _reversed ? !(bool)_data[0] : (bool)_data[0]); + digitalWrite(_pin, _reversed ? !(bool)_data : (bool)_data); } -uint8_t BusOnOff::getPins(uint8_t* pinArray) const { +size_t BusOnOff::getPins(uint8_t* pinArray) const { if (!_valid) return 0; if (pinArray) pinArray[0] = _pin; return 1; } -// credit @willmmiles & @netmindz https://github.com/Aircoookie/WLED/pull/4056 +// credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056 std::vector BusOnOff::getLEDTypes() { return { {TYPE_ONOFF, "", PSTR("On/Off")}, }; } -BusNetwork::BusNetwork(BusConfig &bc) +BusNetwork::BusNetwork(const BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite, bc.count) , _broadcastLock(false) { @@ -759,11 +717,12 @@ BusNetwork::BusNetwork(BusConfig &bc) _hasCCT = false; _UDPchannels = _hasWhite + 3; _client = IPAddress(bc.pins[0],bc.pins[1],bc.pins[2],bc.pins[3]); - _valid = (allocateData(_len * _UDPchannels) != nullptr); - DEBUG_PRINTF_P(PSTR("%successfully inited virtual strip with type %u and IP %u.%u.%u.%u\n"), _valid?"S":"Uns", bc.type, bc.pins[0], bc.pins[1], bc.pins[2], bc.pins[3]); + _data = (uint8_t*)d_calloc(_len, _UDPchannels); + _valid = (_data != nullptr); + DEBUGBUS_PRINTF_P(PSTR("%successfully inited virtual strip with type %u and IP %u.%u.%u.%u\n"), _valid?"S":"Uns", bc.type, bc.pins[0], bc.pins[1], bc.pins[2], bc.pins[3]); } -void BusNetwork::setPixelColor(uint16_t pix, uint32_t c) { +void BusNetwork::setPixelColor(unsigned pix, uint32_t c) { if (!_valid || pix >= _len) return; if (_hasWhite) c = autoWhiteCalc(c); if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT @@ -774,7 +733,7 @@ void BusNetwork::setPixelColor(uint16_t pix, uint32_t c) { if (_hasWhite) _data[offset+3] = W(c); } -uint32_t BusNetwork::getPixelColor(uint16_t pix) const { +uint32_t BusNetwork::getPixelColor(unsigned pix) const { if (!_valid || pix >= _len) return 0; unsigned offset = pix * _UDPchannels; return RGBW32(_data[offset], _data[offset+1], _data[offset+2], (hasWhite() ? _data[offset+3] : 0)); @@ -787,12 +746,12 @@ void BusNetwork::show() { _broadcastLock = false; } -uint8_t BusNetwork::getPins(uint8_t* pinArray) const { +size_t BusNetwork::getPins(uint8_t* pinArray) const { if (pinArray) for (unsigned i = 0; i < 4; i++) pinArray[i] = _client[i]; return 4; } -// credit @willmmiles & @netmindz https://github.com/Aircoookie/WLED/pull/4056 +// credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056 std::vector BusNetwork::getLEDTypes() { return { {TYPE_NET_DDP_RGB, "N", PSTR("DDP RGB (network)")}, // should be "NNNN" to determine 4 "pin" fields @@ -803,14 +762,16 @@ std::vector BusNetwork::getLEDTypes() { //{TYPE_VIRTUAL_I2C_W, "V", PSTR("I2C White (virtual)")}, // allows setting I2C address in _pin[0] //{TYPE_VIRTUAL_I2C_CCT, "V", PSTR("I2C CCT (virtual)")}, // allows setting I2C address in _pin[0] //{TYPE_VIRTUAL_I2C_RGB, "VVV", PSTR("I2C RGB (virtual)")}, // allows setting I2C address in _pin[0] and 2 additional values in _pin[1] & _pin[2] - //{TYPE_USERMOD, "VVVVV", PSTR("Usermod (virtual)")}, // 5 data fields (see https://github.com/Aircoookie/WLED/pull/4123) + //{TYPE_USERMOD, "VVVVV", PSTR("Usermod (virtual)")}, // 5 data fields (see https://github.com/wled/WLED/pull/4123) }; } void BusNetwork::cleanup() { + DEBUGBUS_PRINTLN(F("Virtual Cleanup.")); + d_free(_data); + _data = nullptr; _type = I_NONE; _valid = false; - freeData(); } // *************************************************************************** @@ -819,7 +780,7 @@ void BusNetwork::cleanup() { #warning "HUB75 driver enabled (experimental)" #ifdef ESP8266 #error ESP8266 does not support HUB75 -#endif +#endif BusHub75Matrix::BusHub75Matrix(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite) { @@ -827,7 +788,7 @@ BusHub75Matrix::BusHub75Matrix(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWh _hasRgb = true; _hasWhite = false; - mxconfig.double_buff = false; // Use our own memory-optimised buffer rather than the driver's own double-buffer + mxconfig.double_buff = false; // Use our own memory-optimised buffer rather than the driver's own double-buffer // mxconfig.driver = HUB75_I2S_CFG::ICN2038S; // experimental - use specific shift register driver // mxconfig.driver = HUB75_I2S_CFG::FM6124; // try this driver in case you panel stays dark, or when colors look too pastel @@ -848,7 +809,7 @@ BusHub75Matrix::BusHub75Matrix(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWh } } else if (bc.type == TYPE_HUB75MATRIX_QS) { mxconfig.mx_width = min((u_int8_t) 64, bc.pins[0]) * 2; - mxconfig.mx_height = min((u_int8_t) 64, bc.pins[1]) / 2; + mxconfig.mx_height = min((u_int8_t) 64, bc.pins[1]) / 2; virtualDisp = new VirtualMatrixPanel((*display), 1, 1, bc.pins[0], bc.pins[1]); virtualDisp->setRotation(0); switch(bc.pins[1]) { @@ -868,7 +829,7 @@ BusHub75Matrix::BusHub75Matrix(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWh } else { DEBUG_PRINTLN("Unknown type"); return; - } + } #if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2)// classic esp32, or esp32-s2: reduce bitdepth for large panels if (mxconfig.mx_height >= 64) { @@ -903,7 +864,7 @@ BusHub75Matrix::BusHub75Matrix(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWh Can use a board like https://github.com/rorosaurus/esp32-hub75-driver */ - mxconfig.gpio = { 2, 15, 4, 16, 27, 17, 5, 18, 19, 21, 12, 26, 25, 22 }; + mxconfig.gpio = { 2, 15, 4, 16, 27, 17, 5, 18, 19, 21, 12, 26, 25, 22 }; #else DEBUG_PRINTLN("MatrixPanel_I2S_DMA - Default pins"); @@ -914,9 +875,9 @@ BusHub75Matrix::BusHub75Matrix(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWh https://esp32trinity.com/ https://www.electrodragon.com/product/rgb-matrix-panel-drive-interface-board-for-esp32-dma/ - + */ - mxconfig.gpio = { 25, 26, 27, 14, 12, 13, 23, 9, 5, 17, 18, 4, 15, 16 }; + mxconfig.gpio = { 25, 26, 27, 14, 12, 13, 23, 9, 5, 17, 18, 4, 15, 16 }; #endif @@ -985,7 +946,7 @@ BusHub75Matrix::BusHub75Matrix(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWh DEBUG_PRINTLN("MatrixPanel_I2S_DMA allocate memory"); _ledsDirty = (byte*) malloc(getBitArrayBytes(_len)); // create LEDs dirty bits DEBUG_PRINTLN("MatrixPanel_I2S_DMA allocate memory ok"); - + if (_ledsDirty == nullptr) { display->stopDMAoutput(); delete display; display = nullptr; @@ -1000,7 +961,7 @@ BusHub75Matrix::BusHub75Matrix(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWh _ledBuffer = (CRGB*) calloc(_len, sizeof(CRGB)); // create LEDs buffer (initialized to BLACK) } } - + if (_valid) { _panelWidth = virtualDisp ? virtualDisp->width() : display->width(); // cache width - it will never change @@ -1081,7 +1042,7 @@ void BusHub75Matrix::show(void) { size_t pix = 0; // running pixel index for (int y=0; ygetBusSize(); + #if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(ESP8266) + if (bus->isDigital() && !bus->is2Pin()) digitalCount++; + if (PolyBus::isParallelI2S1Output() && digitalCount > MAX_RMT) { + unsigned i2sCommonSize = 3 * bus->getLength() * bus->getNumberOfChannels() * (bus->is16bit()+1); + if (i2sCommonSize > maxI2S) maxI2S = i2sCommonSize; + busSize -= i2sCommonSize; + } + #endif + size += busSize; + } + return size + maxI2S; } -int BusManager::add(BusConfig &bc) { - if (getNumBusses() - getNumVirtualBusses() >= WLED_MAX_BUSSES) return -1; +int BusManager::add(const BusConfig &bc) { + DEBUGBUS_PRINTF_P(PSTR("Bus: Adding bus (p:%d v:%d)\n"), getNumBusses(), getNumVirtualBusses()); + unsigned digital = 0; + unsigned analog = 0; + unsigned twoPin = 0; + for (const auto &bus : busses) { + if (bus->isPWM()) analog += bus->getPins(); // number of analog channels used + if (bus->isDigital() && !bus->is2Pin()) digital++; + if (bus->is2Pin()) twoPin++; + } + if (digital > WLED_MAX_DIGITAL_CHANNELS || analog > WLED_MAX_ANALOG_CHANNELS) return -1; if (Bus::isVirtual(bc.type)) { - busses[numBusses] = new BusNetwork(bc); + busses.push_back(make_unique(bc)); #ifdef WLED_ENABLE_HUB75MATRIX } else if (Bus::isHub75(bc.type)) { - busses[numBusses] = new BusHub75Matrix(bc); + busses.push_back(make_unique(bc)); #endif } else if (Bus::isDigital(bc.type)) { - busses[numBusses] = new BusDigital(bc, numBusses, colorOrderMap); + busses.push_back(make_unique(bc, Bus::is2Pin(bc.type) ? twoPin : digital)); } else if (Bus::isOnOff(bc.type)) { - busses[numBusses] = new BusOnOff(bc); + busses.push_back(make_unique(bc)); } else { - busses[numBusses] = new BusPwm(bc); + busses.push_back(make_unique(bc)); } - return numBusses++; + return busses.size(); } // credit @willmmiles @@ -1199,7 +1185,7 @@ static String LEDTypesToJson(const std::vector& types) { return json; } -// credit @willmmiles & @netmindz https://github.com/Aircoookie/WLED/pull/4056 +// credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056 String BusManager::getLEDTypesJSONString() { String json = "["; json += LEDTypesToJson(BusDigital::getLEDTypes()); @@ -1216,18 +1202,20 @@ String BusManager::getLEDTypesJSONString() { } void BusManager::useParallelOutput() { - _parallelOutputs = 8; // hardcoded since we use NPB I2S x8 methods + DEBUGBUS_PRINTLN(F("Bus: Enabling parallel I2S.")); PolyBus::setParallelI2S1Output(); } +bool BusManager::hasParallelOutput() { + return PolyBus::isParallelI2S1Output(); +} + //do not call this method from system context (network callback) void BusManager::removeAll() { - DEBUG_PRINTLN(F("Removing all.")); + DEBUGBUS_PRINTLN(F("Removing all.")); //prevents crashes due to deleting busses while in use. while (!canAllShow()) yield(); - for (unsigned i = 0; i < numBusses; i++) delete busses[i]; - numBusses = 0; - _parallelOutputs = 1; + busses.clear(); PolyBus::setParallelI2S1Output(false); } @@ -1238,7 +1226,9 @@ void BusManager::removeAll() { void BusManager::esp32RMTInvertIdle() { bool idle_out; unsigned rmt = 0; - for (unsigned u = 0; u < numBusses(); u++) { + unsigned u = 0; + for (auto &bus : busses) { + if (bus->getLength()==0 || !bus->isDigital() || bus->is2Pin()) continue; #if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, only has 1 I2S but NPB does not support it ATM if (u > 1) return; rmt = u; @@ -1249,11 +1239,11 @@ void BusManager::esp32RMTInvertIdle() { if (u > 3) return; rmt = u; #else - if (u < _parallelOutputs) continue; - if (u >= _parallelOutputs + 8) return; // only 8 RMT channels - rmt = u - _parallelOutputs; + unsigned numI2S = !PolyBus::isParallelI2S1Output(); // if using parallel I2S, RMT is used 1st + if (numI2S > u) continue; + if (u > 7 + numI2S) return; + rmt = u - numI2S; #endif - if (busses[u]->getLength()==0 || !busses[u]->isDigital() || busses[u]->is2Pin()) continue; //assumes that bus number to rmt channel mapping stays 1:1 rmt_channel_t ch = static_cast(rmt); rmt_idle_level_t lvl; @@ -1262,6 +1252,7 @@ void BusManager::esp32RMTInvertIdle() { else if (lvl == RMT_IDLE_LEVEL_LOW) lvl = RMT_IDLE_LEVEL_HIGH; else continue; rmt_set_idle_level(ch, idle_out, lvl); + u++ } } #endif @@ -1270,12 +1261,12 @@ void BusManager::on() { #ifdef ESP8266 //Fix for turning off onboard LED breaking bus if (PinManager::getPinOwner(LED_BUILTIN) == PinOwner::BusDigital) { - for (unsigned i = 0; i < numBusses; i++) { + for (auto &bus : busses) { uint8_t pins[2] = {255,255}; - if (busses[i]->isDigital() && busses[i]->getPins(pins)) { + if (bus->isDigital() && bus->getPins(pins)) { if (pins[0] == LED_BUILTIN || pins[1] == LED_BUILTIN) { - BusDigital *bus = static_cast(busses[i]); - bus->begin(); + BusDigital &b = static_cast(*bus); + b.begin(); break; } } @@ -1292,7 +1283,7 @@ void BusManager::off() { // turn off built-in LED if strip is turned off // this will break digital bus so will need to be re-initialised on On if (PinManager::getPinOwner(LED_BUILTIN) == PinOwner::BusDigital) { - for (unsigned i = 0; i < numBusses; i++) if (busses[i]->isOffRefreshRequired()) return; + for (const auto &bus : busses) if (bus->isOffRefreshRequired()) return; pinMode(LED_BUILTIN, OUTPUT); digitalWrite(LED_BUILTIN, HIGH); } @@ -1303,31 +1294,17 @@ void BusManager::off() { } void BusManager::show() { - _milliAmpsUsed = 0; - for (unsigned i = 0; i < numBusses; i++) { - busses[i]->show(); - _milliAmpsUsed += busses[i]->getUsedCurrent(); - } - if (_milliAmpsUsed) _milliAmpsUsed += MA_FOR_ESP; -} - -void BusManager::setStatusPixel(uint32_t c) { - for (unsigned i = 0; i < numBusses; i++) { - busses[i]->setStatusPixel(c); + _gMilliAmpsUsed = 0; + for (auto &bus : busses) { + bus->show(); + _gMilliAmpsUsed += bus->getUsedCurrent(); } } -void IRAM_ATTR BusManager::setPixelColor(uint16_t pix, uint32_t c) { - for (unsigned i = 0; i < numBusses; i++) { - unsigned bstart = busses[i]->getStart(); - if (pix < bstart || pix >= bstart + busses[i]->getLength()) continue; - busses[i]->setPixelColor(pix - bstart, c); - } -} - -void BusManager::setBrightness(uint8_t b) { - for (unsigned i = 0; i < numBusses; i++) { - busses[i]->setBrightness(b); +void IRAM_ATTR BusManager::setPixelColor(unsigned pix, uint32_t c) { + for (auto &bus : busses) { + if (!bus->containsPixel(pix)) continue; + bus->setPixelColor(pix - bus->getStart(), c); } } @@ -1340,35 +1317,23 @@ void BusManager::setSegmentCCT(int16_t cct, bool allowWBCorrection) { Bus::setCCT(cct); } -uint32_t BusManager::getPixelColor(uint16_t pix) { - for (unsigned i = 0; i < numBusses; i++) { - unsigned bstart = busses[i]->getStart(); - if (!busses[i]->containsPixel(pix)) continue; - return busses[i]->getPixelColor(pix - bstart); +uint32_t BusManager::getPixelColor(unsigned pix) { + for (auto &bus : busses) { + if (!bus->containsPixel(pix)) continue; + return bus->getPixelColor(pix - bus->getStart()); } return 0; } bool BusManager::canAllShow() { - for (unsigned i = 0; i < numBusses; i++) { - if (!busses[i]->canShow()) return false; - } + for (const auto &bus : busses) if (!bus->canShow()) return false; return true; } -Bus* BusManager::getBus(uint8_t busNr) { - if (busNr >= numBusses) return nullptr; - return busses[busNr]; -} +ColorOrderMap& BusManager::getColorOrderMap() { return _colorOrderMap; } -//semi-duplicate of strip.getLengthTotal() (though that just returns strip._length, calculated in finalizeInit()) -uint16_t BusManager::getTotalLength() { - unsigned len = 0; - for (unsigned i=0; igetLength(); - return len; -} -bool PolyBus::useParallelI2S = false; +bool PolyBus::_useParallelI2S = false; // Bus static member definition int16_t Bus::_cct = -1; @@ -1377,9 +1342,6 @@ uint8_t Bus::_gAWM = 255; uint16_t BusDigital::_milliAmpsTotal = 0; -uint8_t BusManager::numBusses = 0; -Bus* BusManager::busses[WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES]; -ColorOrderMap BusManager::colorOrderMap = {}; -uint16_t BusManager::_milliAmpsUsed = 0; -uint16_t BusManager::_milliAmpsMax = ABL_MILLIAMPS_DEFAULT; -uint8_t BusManager::_parallelOutputs = 1; +std::vector> BusManager::busses; +uint16_t BusManager::_gMilliAmpsUsed = 0; +uint16_t BusManager::_gMilliAmpsMax = ABL_MILLIAMPS_DEFAULT; diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index f78a76a0..55d6d8d5 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -1,3 +1,4 @@ +#pragma once #ifndef BusManager_h #define BusManager_h @@ -13,7 +14,44 @@ */ #include "const.h" +#include "pin_manager.h" #include +#include + +#if __cplusplus >= 201402L +using std::make_unique; +#else +// Really simple C++11 shim for non-array case; implementation from cppreference.com +template +std::unique_ptr +make_unique(Args&&... args) +{ + return std::unique_ptr(new T(std::forward(args)...)); +} +#endif + +// enable additional debug output +#if defined(WLED_DEBUG_HOST) + #include "net_debug.h" + #define DEBUGOUT NetDebug +#else + #define DEBUGOUT Serial +#endif + +#ifdef WLED_DEBUG_BUS + #ifndef ESP8266 + #include + #endif + #define DEBUGBUS_PRINT(x) DEBUGOUT.print(x) + #define DEBUGBUS_PRINTLN(x) DEBUGOUT.println(x) + #define DEBUGBUS_PRINTF(x...) DEBUGOUT.printf(x) + #define DEBUGBUS_PRINTF_P(x...) DEBUGOUT.printf_P(x) +#else + #define DEBUGBUS_PRINT(x) + #define DEBUGBUS_PRINTLN(x) + #define DEBUGBUS_PRINTF(x...) + #define DEBUGBUS_PRINTF_P(x...) +#endif //colors.cpp uint16_t approximateKelvinFromRGB(uint32_t rgb); @@ -75,60 +113,59 @@ class Bus { : _type(type) , _bri(255) , _start(start) - , _len(len) + , _len(std::max(len,(uint16_t)1)) , _reversed(reversed) , _valid(false) , _needsRefresh(refresh) - , _data(nullptr) // keep data access consistent across all types of buses { _autoWhiteMode = Bus::hasWhite(type) ? aw : RGBW_MODE_MANUAL_ONLY; }; virtual ~Bus() {} //throw the bus under the bus - virtual void begin() {}; - virtual void show() = 0; - virtual bool canShow() const { return true; } - virtual void setStatusPixel(uint32_t c) {} - virtual void setPixelColor(uint16_t pix, uint32_t c) = 0; - virtual void setBrightness(uint8_t b) { _bri = b; }; - virtual void setColorOrder(uint8_t co) {} - virtual uint32_t getPixelColor(uint16_t pix) const { return 0; } - virtual uint8_t getPins(uint8_t* pinArray = nullptr) const { return 0; } - virtual uint16_t getLength() const { return isOk() ? _len : 0; } - virtual uint8_t getColorOrder() const { return COL_ORDER_RGB; } - virtual uint8_t skippedLeds() const { return 0; } - virtual uint16_t getFrequency() const { return 0U; } - virtual uint16_t getLEDCurrent() const { return 0; } - virtual uint16_t getUsedCurrent() const { return 0; } - virtual uint16_t getMaxCurrent() const { return 0; } + virtual void begin() {}; + virtual void show() = 0; + virtual bool canShow() const { return true; } + virtual void setStatusPixel(uint32_t c) {} + virtual void setPixelColor(unsigned pix, uint32_t c) = 0; + virtual void setBrightness(uint8_t b) { _bri = b; }; + virtual void setColorOrder(uint8_t co) {} + virtual uint32_t getPixelColor(unsigned pix) const { return 0; } + virtual size_t getPins(uint8_t* pinArray = nullptr) const { return 0; } + virtual uint16_t getLength() const { return isOk() ? _len : 0; } + virtual uint8_t getColorOrder() const { return COL_ORDER_RGB; } + virtual unsigned skippedLeds() const { return 0; } + virtual uint16_t getFrequency() const { return 0U; } + virtual uint16_t getLEDCurrent() const { return 0; } + virtual uint16_t getUsedCurrent() const { return 0; } + virtual uint16_t getMaxCurrent() const { return 0; } + virtual size_t getBusSize() const { return sizeof(Bus); } - inline bool hasRGB() const { return _hasRgb; } - inline bool hasWhite() const { return _hasWhite; } - inline bool hasCCT() const { return _hasCCT; } - inline bool isDigital() const { return isDigital(_type); } - inline bool is2Pin() const { return is2Pin(_type); } - inline bool isOnOff() const { return isOnOff(_type); } - inline bool isPWM() const { return isPWM(_type); } - inline bool isVirtual() const { return isVirtual(_type); } - inline bool isHub75() const { return isHub75(_type); } - inline bool is16bit() const { return is16bit(_type); } - inline bool mustRefresh() const { return mustRefresh(_type); } - inline void setReversed(bool reversed) { _reversed = reversed; } - inline void setStart(uint16_t start) { _start = start; } - inline void setAutoWhiteMode(uint8_t m) { if (m < 5) _autoWhiteMode = m; } - inline uint8_t getAutoWhiteMode() const { return _autoWhiteMode; } - inline uint8_t getNumberOfChannels() const { return hasWhite() + 3*hasRGB() + hasCCT(); } - inline uint16_t getStart() const { return _start; } - inline uint8_t getType() const { return _type; } - inline bool isOk() const { return _valid; } - inline bool isReversed() const { return _reversed; } - inline bool isOffRefreshRequired() const { return _needsRefresh; } - inline bool containsPixel(uint16_t pix) const { return pix >= _start && pix < _start + _len; } + inline bool hasRGB() const { return _hasRgb; } + inline bool hasWhite() const { return _hasWhite; } + inline bool hasCCT() const { return _hasCCT; } + inline bool isDigital() const { return isDigital(_type); } + inline bool is2Pin() const { return is2Pin(_type); } + inline bool isOnOff() const { return isOnOff(_type); } + inline bool isPWM() const { return isPWM(_type); } + inline bool isVirtual() const { return isVirtual(_type); } + inline bool is16bit() const { return is16bit(_type); } + inline bool mustRefresh() const { return mustRefresh(_type); } + inline void setReversed(bool reversed) { _reversed = reversed; } + inline void setStart(uint16_t start) { _start = start; } + inline void setAutoWhiteMode(uint8_t m) { if (m < 5) _autoWhiteMode = m; } + inline uint8_t getAutoWhiteMode() const { return _autoWhiteMode; } + inline size_t getNumberOfChannels() const { return hasWhite() + 3*hasRGB() + hasCCT(); } + inline uint16_t getStart() const { return _start; } + inline uint8_t getType() const { return _type; } + inline bool isOk() const { return _valid; } + inline bool isReversed() const { return _reversed; } + inline bool isOffRefreshRequired() const { return _needsRefresh; } + inline bool containsPixel(uint16_t pix) const { return pix >= _start && pix < _start + _len; } - static inline std::vector getLEDTypes() { return {{TYPE_NONE, "", PSTR("None")}}; } // not used. just for reference for derived classes - static constexpr uint8_t getNumberOfPins(uint8_t type) { return isVirtual(type) ? 4 : isPWM(type) ? numPWMPins(type) : is2Pin(type) + 1; } // credit @PaoloTK - static constexpr uint8_t getNumberOfChannels(uint8_t type) { return hasWhite(type) + 3*hasRGB(type) + hasCCT(type); } + static inline std::vector getLEDTypes() { return {{TYPE_NONE, "", PSTR("None")}}; } // not used. just for reference for derived classes + static constexpr size_t getNumberOfPins(uint8_t type) { return isVirtual(type) ? 4 : isPWM(type) ? numPWMPins(type) : is2Pin(type) + 1; } // credit @PaoloTK + static constexpr size_t getNumberOfChannels(uint8_t type) { return hasWhite(type) + 3*hasRGB(type) + hasCCT(type); } static constexpr bool hasRGB(uint8_t type) { return !((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || type == TYPE_ANALOG_1CH || type == TYPE_ANALOG_2CH || type == TYPE_ONOFF); } @@ -161,7 +198,7 @@ class Bus { static inline uint8_t getGlobalAWMode() { return _gAWM; } static inline void setCCT(int16_t cct) { _cct = cct; } static inline uint8_t getCCTBlend() { return _cctBlend; } - static inline void setCCTBlend(uint8_t b) { + static inline void setCCTBlend(uint8_t b) { _cctBlend = (std::min((int)b,100) * 127) / 100; //compile-time limiter for hardware that can't power both white channels at max #ifdef WLED_MAX_CCT_BLEND @@ -184,7 +221,6 @@ class Bus { bool _hasCCT;// : 1; //} __attribute__ ((packed)); uint8_t _autoWhiteMode; - uint8_t *_data; // global Auto White Calculation override static uint8_t _gAWM; // _cct has the following menaings (see calculateCCT() & BusManager::setSegmentCCT()): @@ -199,45 +235,43 @@ class Bus { static uint8_t _cctBlend; uint32_t autoWhiteCalc(uint32_t c) const; - uint8_t *allocateData(size_t size = 1); - void freeData() { if (_data != nullptr) free(_data); _data = nullptr; } }; class BusDigital : public Bus { public: - BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com); + BusDigital(const BusConfig &bc, uint8_t nr); ~BusDigital() { cleanup(); } void show() override; bool canShow() const override; void setBrightness(uint8_t b) override; void setStatusPixel(uint32_t c) override; - [[gnu::hot]] void setPixelColor(uint16_t pix, uint32_t c) override; + [[gnu::hot]] void setPixelColor(unsigned pix, uint32_t c) override; void setColorOrder(uint8_t colorOrder) override; - [[gnu::hot]] uint32_t getPixelColor(uint16_t pix) const override; + [[gnu::hot]] uint32_t getPixelColor(unsigned pix) const override; uint8_t getColorOrder() const override { return _colorOrder; } - uint8_t getPins(uint8_t* pinArray = nullptr) const override; - uint8_t skippedLeds() const override { return _skip; } + size_t getPins(uint8_t* pinArray = nullptr) const override; + unsigned skippedLeds() const override { return _skip; } uint16_t getFrequency() const override { return _frequencykHz; } uint16_t getLEDCurrent() const override { return _milliAmpsPerLed; } uint16_t getUsedCurrent() const override { return _milliAmpsTotal; } uint16_t getMaxCurrent() const override { return _milliAmpsMax; } + size_t getBusSize() const override; void begin() override; void cleanup(); static std::vector getLEDTypes(); private: - uint8_t _skip; - uint8_t _colorOrder; - uint8_t _pins[2]; - uint8_t _iType; + uint8_t _skip; + uint8_t _colorOrder; + uint8_t _pins[2]; + uint8_t _iType; uint16_t _frequencykHz; - uint8_t _milliAmpsPerLed; + uint8_t _milliAmpsPerLed; uint16_t _milliAmpsMax; - void * _busPtr; - const ColorOrderMap &_colorOrderMap; + void *_busPtr; static uint16_t _milliAmpsTotal; // is overwitten/recalculated on each show() @@ -252,27 +286,28 @@ class BusDigital : public Bus { return c; } - uint8_t estimateCurrentAndLimitBri(); + uint8_t estimateCurrentAndLimitBri() const; }; class BusPwm : public Bus { public: - BusPwm(BusConfig &bc); + BusPwm(const BusConfig &bc); ~BusPwm() { cleanup(); } - void setPixelColor(uint16_t pix, uint32_t c) override; - uint32_t getPixelColor(uint16_t pix) const override; //does no index check - uint8_t getPins(uint8_t* pinArray = nullptr) const override; + void setPixelColor(unsigned pix, uint32_t c) override; + uint32_t getPixelColor(unsigned pix) const override; //does no index check + size_t getPins(uint8_t* pinArray = nullptr) const override; uint16_t getFrequency() const override { return _frequency; } + size_t getBusSize() const override { return sizeof(BusPwm); } void show() override; - void cleanup() { deallocatePins(); } + inline void cleanup() { deallocatePins(); } static std::vector getLEDTypes(); private: uint8_t _pins[OUTPUT_MAX_PINS]; - uint8_t _pwmdata[OUTPUT_MAX_PINS]; + uint8_t _data[OUTPUT_MAX_PINS]; #ifdef ARDUINO_ARCH_ESP32 uint8_t _ledcStart; #endif @@ -285,34 +320,36 @@ class BusPwm : public Bus { class BusOnOff : public Bus { public: - BusOnOff(BusConfig &bc); + BusOnOff(const BusConfig &bc); ~BusOnOff() { cleanup(); } - void setPixelColor(uint16_t pix, uint32_t c) override; - uint32_t getPixelColor(uint16_t pix) const override; - uint8_t getPins(uint8_t* pinArray) const override; + void setPixelColor(unsigned pix, uint32_t c) override; + uint32_t getPixelColor(unsigned pix) const override; + size_t getPins(uint8_t* pinArray) const override; + size_t getBusSize() const override { return sizeof(BusOnOff); } void show() override; - void cleanup() { PinManager::deallocatePin(_pin, PinOwner::BusOnOff); } + inline void cleanup() { PinManager::deallocatePin(_pin, PinOwner::BusOnOff); } static std::vector getLEDTypes(); private: uint8_t _pin; - uint8_t _onoffdata; + uint8_t _data; }; class BusNetwork : public Bus { public: - BusNetwork(BusConfig &bc); + BusNetwork(const BusConfig &bc); ~BusNetwork() { cleanup(); } bool canShow() const override { return !_broadcastLock; } // this should be a return value from UDP routine if it is still sending data out - void setPixelColor(uint16_t pix, uint32_t c) override; - uint32_t getPixelColor(uint16_t pix) const override; - uint8_t getPins(uint8_t* pinArray = nullptr) const override; - void show() override; - void cleanup(); + [[gnu::hot]] void setPixelColor(unsigned pix, uint32_t c) override; + [[gnu::hot]] uint32_t getPixelColor(unsigned pix) const override; + size_t getPins(uint8_t* pinArray = nullptr) const override; + size_t getBusSize() const override { return sizeof(BusNetwork) + (isOk() ? _len * _UDPchannels : 0); } + void show() override; + void cleanup(); static std::vector getLEDTypes(); @@ -321,6 +358,7 @@ class BusNetwork : public Bus { uint8_t _UDPtype; uint8_t _UDPchannels; bool _broadcastLock; + uint8_t *_data; }; #ifdef WLED_ENABLE_HUB75MATRIX @@ -334,7 +372,7 @@ class BusHub75Matrix : public Bus { uint8_t getPins(uint8_t* pinArray) const override; void deallocatePins(); void cleanup(); - + ~BusHub75Matrix() { cleanup(); } @@ -348,7 +386,7 @@ class BusHub75Matrix : public Bus { unsigned _panelWidth = 0; CRGB *_ledBuffer = nullptr; byte *_ledsDirty = nullptr; - // workaround for missing constants on include path for non-MM + // workaround for missing constants on include path for non-MM uint32_t IS_BLACK = 0x000000; uint32_t IS_DARKGREY = 0x333333; const int PIN_COUNT = 14; @@ -367,19 +405,17 @@ struct BusConfig { uint8_t autoWhite; uint8_t pins[OUTPUT_MAX_PINS] = {255, 255, 255, 255, 255}; uint16_t frequency; - bool doubleBuffer; uint8_t milliAmpsPerLed; uint16_t milliAmpsMax; - BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U, bool dblBfr=false, uint8_t maPerLed=LED_MILLIAMPS_DEFAULT, uint16_t maMax=ABL_MILLIAMPS_DEFAULT) - : count(len) + BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U, uint8_t maPerLed=LED_MILLIAMPS_DEFAULT, uint16_t maMax=ABL_MILLIAMPS_DEFAULT) + : count(std::max(len,(uint16_t)1)) , start(pstart) , colorOrder(pcolorOrder) , reversed(rev) , skipAmount(skip) , autoWhite(aw) , frequency(clock_kHz) - , doubleBuffer(dblBfr) , milliAmpsPerLed(maPerLed) , milliAmpsMax(maMax) { @@ -387,6 +423,16 @@ struct BusConfig { type = busType & 0x7F; // bit 7 may be/is hacked to include refresh info (1=refresh in off state, 0=no refresh) size_t nPins = OUTPUT_MAX_PINS; for (size_t i = 0; i < nPins; i++) pins[i] = ppins[i]; + DEBUGBUS_PRINTF_P(PSTR("Bus: Config (%d-%d, type:%d, CO:%d, rev:%d, skip:%d, AW:%d kHz:%d, mA:%d/%d)\n"), + (int)start, (int)(start+len), + (int)type, + (int)colorOrder, + (int)reversed, + (int)skipAmount, + (int)autoWhite, + (int)frequency, + (int)milliAmpsPerLed, (int)milliAmpsMax + ); } //validates start and length and extends total if needed @@ -400,64 +446,73 @@ struct BusConfig { if (start + count > total) total = start + count; return true; } + + size_t memUsage(unsigned nr = 0) const; }; -class BusManager { - public: - BusManager() {}; +//fine tune power estimation constants for your setup +//you can set it to 0 if the ESP is powered by USB and the LEDs by external +#ifndef MA_FOR_ESP + #ifdef ESP8266 + #define MA_FOR_ESP 80 //how much mA does the ESP use (Wemos D1 about 80mA) + #else + #define MA_FOR_ESP 120 //how much mA does the ESP use (ESP32 about 120mA) + #endif +#endif - //utility to get the approx. memory usage of a given BusConfig - static uint32_t memUsage(BusConfig &bc); - static uint32_t memUsage(unsigned channels, unsigned count, unsigned buses = 1); - static uint16_t currentMilliamps() { return _milliAmpsUsed; } - static uint16_t ablMilliampsMax() { return _milliAmpsMax; } +namespace BusManager { - static int add(BusConfig &bc); - static void useParallelOutput(); // workaround for inaccessible PolyBus + extern std::vector> busses; + //extern std::vector busses; + extern uint16_t _gMilliAmpsUsed; + extern uint16_t _gMilliAmpsMax; - //do not call this method from system context (network callback) - static void removeAll(); + #ifdef ESP32_DATA_IDLE_HIGH + void esp32RMTInvertIdle() ; + #endif + inline size_t getNumVirtualBusses() { + size_t j = 0; + for (const auto &bus : busses) j += bus->isVirtual(); + return j; + } - static void on(); - static void off(); + size_t memUsage(); + inline uint16_t currentMilliamps() { return _gMilliAmpsUsed + MA_FOR_ESP; } + //inline uint16_t ablMilliampsMax() { unsigned sum = 0; for (auto &bus : busses) sum += bus->getMaxCurrent(); return sum; } + inline uint16_t ablMilliampsMax() { return _gMilliAmpsMax; } // used for compatibility reasons (and enabling virtual global ABL) + inline void setMilliampsMax(uint16_t max) { _gMilliAmpsMax = max;} - static void show(); - static bool canAllShow(); - static void setStatusPixel(uint32_t c); - [[gnu::hot]] static void setPixelColor(uint16_t pix, uint32_t c); - static void setBrightness(uint8_t b); - // for setSegmentCCT(), cct can only be in [-1,255] range; allowWBCorrection will convert it to K - // WARNING: setSegmentCCT() is a misleading name!!! much better would be setGlobalCCT() or just setCCT() - static void setSegmentCCT(int16_t cct, bool allowWBCorrection = false); - static inline void setMilliampsMax(uint16_t max) { _milliAmpsMax = max;} - static uint32_t getPixelColor(uint16_t pix); - static inline int16_t getSegmentCCT() { return Bus::getCCT(); } + void useParallelOutput(); // workaround for inaccessible PolyBus + bool hasParallelOutput(); // workaround for inaccessible PolyBus - static Bus* getBus(uint8_t busNr); + //do not call this method from system context (network callback) + void removeAll(); + int add(const BusConfig &bc); - //semi-duplicate of strip.getLengthTotal() (though that just returns strip._length, calculated in finalizeInit()) - static uint16_t getTotalLength(); - static inline uint8_t getNumBusses() { return numBusses; } - static String getLEDTypesJSONString(); + void on(); + void off(); - static inline ColorOrderMap& getColorOrderMap() { return colorOrderMap; } + [[gnu::hot]] void setPixelColor(unsigned pix, uint32_t c); + [[gnu::hot]] uint32_t getPixelColor(unsigned pix); + void show(); + bool canAllShow(); + inline void setStatusPixel(uint32_t c) { for (auto &bus : busses) bus->setStatusPixel(c);} + inline void setBrightness(uint8_t b) { for (auto &bus : busses) bus->setBrightness(b); } + // for setSegmentCCT(), cct can only be in [-1,255] range; allowWBCorrection will convert it to K + // WARNING: setSegmentCCT() is a misleading name!!! much better would be setGlobalCCT() or just setCCT() + void setSegmentCCT(int16_t cct, bool allowWBCorrection = false); + inline int16_t getSegmentCCT() { return Bus::getCCT(); } + inline Bus* getBus(size_t busNr) { return busNr < busses.size() ? busses[busNr].get() : nullptr; } + inline size_t getNumBusses() { return busses.size(); } - private: - static uint8_t numBusses; - static Bus* busses[WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES]; - static ColorOrderMap colorOrderMap; - static uint16_t _milliAmpsUsed; - static uint16_t _milliAmpsMax; - static uint8_t _parallelOutputs; - - #ifdef ESP32_DATA_IDLE_HIGH - static void esp32RMTInvertIdle() ; - #endif - static uint8_t getNumVirtualBusses() { - int j = 0; - for (int i=0; iisVirtual()) j++; - return j; - } + //semi-duplicate of strip.getLengthTotal() (though that just returns strip._length, calculated in finalizeInit()) + inline uint16_t getTotalLength(bool onlyPhysical = false) { + unsigned len = 0; + for (const auto &bus : busses) if (!(bus->isVirtual() && onlyPhysical)) len += bus->getLength(); + return len; + } + String getLEDTypesJSONString(); + ColorOrderMap& getColorOrderMap(); }; #endif diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index d2a18c9d..577aaeb8 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -1,23 +1,9 @@ +#pragma once #ifndef BusWrapper_h #define BusWrapper_h +//#define NPB_CONF_4STEP_CADENCE #include "NeoPixelBusLg.h" -#include "bus_manager.h" - -// temporary - these defines should actually be set in platformio.ini -// C3: I2S0 and I2S1 methods not supported (has one I2S bus) -// S2: I2S1 methods not supported (has one I2S bus) -// S3: I2S0 and I2S1 methods not supported yet (has two I2S buses) -// https://github.com/Makuna/NeoPixelBus/blob/b32f719e95ef3c35c46da5c99538017ef925c026/src/internal/Esp32_i2s.h#L4 -// https://github.com/Makuna/NeoPixelBus/blob/b32f719e95ef3c35c46da5c99538017ef925c026/src/internal/NeoEsp32RmtMethod.h#L857 - -#if !defined(WLED_NO_I2S0_PIXELBUS) && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3)) -#define WLED_NO_I2S0_PIXELBUS -#endif -#if !defined(WLED_NO_I2S1_PIXELBUS) && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2)) -#define WLED_NO_I2S1_PIXELBUS -#endif -// temporary end //Hardware SPI Pins #define P_8266_HS_MOSI 13 @@ -55,110 +41,98 @@ #define I_8266_DM_TM2_3 19 #define I_8266_BB_TM2_3 20 //UCS8903 (RGB) -#define I_8266_U0_UCS_3 49 -#define I_8266_U1_UCS_3 50 -#define I_8266_DM_UCS_3 51 -#define I_8266_BB_UCS_3 52 +#define I_8266_U0_UCS_3 21 +#define I_8266_U1_UCS_3 22 +#define I_8266_DM_UCS_3 23 +#define I_8266_BB_UCS_3 24 //UCS8904 (RGBW) -#define I_8266_U0_UCS_4 53 -#define I_8266_U1_UCS_4 54 -#define I_8266_DM_UCS_4 55 -#define I_8266_BB_UCS_4 56 +#define I_8266_U0_UCS_4 25 +#define I_8266_U1_UCS_4 26 +#define I_8266_DM_UCS_4 27 +#define I_8266_BB_UCS_4 28 //FW1906 GRBCW -#define I_8266_U0_FW6_5 66 -#define I_8266_U1_FW6_5 67 -#define I_8266_DM_FW6_5 68 -#define I_8266_BB_FW6_5 69 +#define I_8266_U0_FW6_5 29 +#define I_8266_U1_FW6_5 30 +#define I_8266_DM_FW6_5 31 +#define I_8266_BB_FW6_5 32 //ESP8266 APA106 -#define I_8266_U0_APA106_3 81 -#define I_8266_U1_APA106_3 82 -#define I_8266_DM_APA106_3 83 -#define I_8266_BB_APA106_3 84 +#define I_8266_U0_APA106_3 33 +#define I_8266_U1_APA106_3 34 +#define I_8266_DM_APA106_3 35 +#define I_8266_BB_APA106_3 36 //WS2805 (RGBCW) -#define I_8266_U0_2805_5 89 -#define I_8266_U1_2805_5 90 -#define I_8266_DM_2805_5 91 -#define I_8266_BB_2805_5 92 +#define I_8266_U0_2805_5 37 +#define I_8266_U1_2805_5 38 +#define I_8266_DM_2805_5 39 +#define I_8266_BB_2805_5 40 //TM1914 (RGB) -#define I_8266_U0_TM1914_3 99 -#define I_8266_U1_TM1914_3 100 -#define I_8266_DM_TM1914_3 101 -#define I_8266_BB_TM1914_3 102 +#define I_8266_U0_TM1914_3 41 +#define I_8266_U1_TM1914_3 42 +#define I_8266_DM_TM1914_3 43 +#define I_8266_BB_TM1914_3 44 //SM16825 (RGBCW) -#define I_8266_U0_SM16825_5 103 -#define I_8266_U1_SM16825_5 104 -#define I_8266_DM_SM16825_5 105 -#define I_8266_BB_SM16825_5 106 +#define I_8266_U0_SM16825_5 45 +#define I_8266_U1_SM16825_5 46 +#define I_8266_DM_SM16825_5 47 +#define I_8266_BB_SM16825_5 48 /*** ESP32 Neopixel methods ***/ //RGB -#define I_32_RN_NEO_3 21 -#define I_32_I0_NEO_3 22 -#define I_32_I1_NEO_3 23 +#define I_32_RN_NEO_3 1 +#define I_32_I2_NEO_3 2 //RGBW -#define I_32_RN_NEO_4 25 -#define I_32_I0_NEO_4 26 -#define I_32_I1_NEO_4 27 +#define I_32_RN_NEO_4 5 +#define I_32_I2_NEO_4 6 //400Kbps -#define I_32_RN_400_3 29 -#define I_32_I0_400_3 30 -#define I_32_I1_400_3 31 +#define I_32_RN_400_3 9 +#define I_32_I2_400_3 10 //TM1814 (RGBW) -#define I_32_RN_TM1_4 33 -#define I_32_I0_TM1_4 34 -#define I_32_I1_TM1_4 35 +#define I_32_RN_TM1_4 13 +#define I_32_I2_TM1_4 14 //TM1829 (RGB) -#define I_32_RN_TM2_3 36 -#define I_32_I0_TM2_3 37 -#define I_32_I1_TM2_3 38 +#define I_32_RN_TM2_3 17 +#define I_32_I2_TM2_3 18 //UCS8903 (RGB) -#define I_32_RN_UCS_3 57 -#define I_32_I0_UCS_3 58 -#define I_32_I1_UCS_3 59 +#define I_32_RN_UCS_3 21 +#define I_32_I2_UCS_3 22 //UCS8904 (RGBW) -#define I_32_RN_UCS_4 60 -#define I_32_I0_UCS_4 61 -#define I_32_I1_UCS_4 62 +#define I_32_RN_UCS_4 25 +#define I_32_I2_UCS_4 26 //FW1906 GRBCW -#define I_32_RN_FW6_5 63 -#define I_32_I0_FW6_5 64 -#define I_32_I1_FW6_5 65 +#define I_32_RN_FW6_5 29 +#define I_32_I2_FW6_5 30 //APA106 -#define I_32_RN_APA106_3 85 -#define I_32_I0_APA106_3 86 -#define I_32_I1_APA106_3 87 +#define I_32_RN_APA106_3 33 +#define I_32_I2_APA106_3 34 //WS2805 (RGBCW) -#define I_32_RN_2805_5 93 -#define I_32_I0_2805_5 94 -#define I_32_I1_2805_5 95 +#define I_32_RN_2805_5 37 +#define I_32_I2_2805_5 38 //TM1914 (RGB) -#define I_32_RN_TM1914_3 96 -#define I_32_I0_TM1914_3 97 -#define I_32_I1_TM1914_3 98 +#define I_32_RN_TM1914_3 41 +#define I_32_I2_TM1914_3 42 //SM16825 (RGBCW) -#define I_32_RN_SM16825_5 107 -#define I_32_I0_SM16825_5 108 -#define I_32_I1_SM16825_5 109 +#define I_32_RN_SM16825_5 45 +#define I_32_I2_SM16825_5 46 //APA102 -#define I_HS_DOT_3 39 //hardware SPI -#define I_SS_DOT_3 40 //soft SPI +#define I_HS_DOT_3 101 //hardware SPI +#define I_SS_DOT_3 102 //soft SPI //LPD8806 -#define I_HS_LPD_3 41 -#define I_SS_LPD_3 42 +#define I_HS_LPD_3 103 +#define I_SS_LPD_3 104 //WS2801 -#define I_HS_WS1_3 43 -#define I_SS_WS1_3 44 +#define I_HS_WS1_3 105 +#define I_SS_WS1_3 106 //P9813 -#define I_HS_P98_3 45 -#define I_SS_P98_3 46 +#define I_HS_P98_3 107 +#define I_SS_P98_3 108 //LPD6803 -#define I_HS_LPO_3 47 -#define I_SS_LPO_3 48 +#define I_HS_LPO_3 109 +#define I_SS_LPO_3 110 // In the following NeoGammaNullMethod can be replaced with NeoGammaWLEDMethod to perform Gamma correction implicitly @@ -230,66 +204,95 @@ /*** ESP32 Neopixel methods ***/ #ifdef ARDUINO_ARCH_ESP32 +// C3: I2S0 and I2S1 methods not supported (has one I2S bus) +// S2: I2S0 methods supported (single & parallel), I2S1 methods not supported (has one I2S bus) +// S3: I2S0 methods not supported, I2S1 supports LCD parallel methods (has two I2S buses) +// https://github.com/Makuna/NeoPixelBus/blob/b32f719e95ef3c35c46da5c99538017ef925c026/src/internal/Esp32_i2s.h#L4 +// https://github.com/Makuna/NeoPixelBus/blob/b32f719e95ef3c35c46da5c99538017ef925c026/src/internal/NeoEsp32RmtMethod.h#L857 +#if defined(CONFIG_IDF_TARGET_ESP32S3) + // S3 will always use LCD parallel output + typedef X8Ws2812xMethod X1Ws2812xMethod; + typedef X8Sk6812Method X1Sk6812Method; + typedef X8400KbpsMethod X1400KbpsMethod; + typedef X8800KbpsMethod X1800KbpsMethod; + typedef X8Tm1814Method X1Tm1814Method; + typedef X8Tm1829Method X1Tm1829Method; + typedef X8Apa106Method X1Apa106Method; + typedef X8Ws2805Method X1Ws2805Method; + typedef X8Tm1914Method X1Tm1914Method; +#elif defined(CONFIG_IDF_TARGET_ESP32S2) + // S2 will use I2S0 + typedef NeoEsp32I2s0Ws2812xMethod X1Ws2812xMethod; + typedef NeoEsp32I2s0Sk6812Method X1Sk6812Method; + typedef NeoEsp32I2s0400KbpsMethod X1400KbpsMethod; + typedef NeoEsp32I2s0800KbpsMethod X1800KbpsMethod; + typedef NeoEsp32I2s0Tm1814Method X1Tm1814Method; + typedef NeoEsp32I2s0Tm1829Method X1Tm1829Method; + typedef NeoEsp32I2s0Apa106Method X1Apa106Method; + typedef NeoEsp32I2s0Ws2805Method X1Ws2805Method; + typedef NeoEsp32I2s0Tm1914Method X1Tm1914Method; +#elif !defined(CONFIG_IDF_TARGET_ESP32C3) + // regular ESP32 will use I2S1 + typedef NeoEsp32I2s1Ws2812xMethod X1Ws2812xMethod; + typedef NeoEsp32I2s1Sk6812Method X1Sk6812Method; + typedef NeoEsp32I2s1400KbpsMethod X1400KbpsMethod; + typedef NeoEsp32I2s1800KbpsMethod X1800KbpsMethod; + typedef NeoEsp32I2s1Tm1814Method X1Tm1814Method; + typedef NeoEsp32I2s1Tm1829Method X1Tm1829Method; + typedef NeoEsp32I2s1Apa106Method X1Apa106Method; + typedef NeoEsp32I2s1Ws2805Method X1Ws2805Method; + typedef NeoEsp32I2s1Tm1914Method X1Tm1914Method; +#endif + //RGB -#define B_32_RN_NEO_3 NeoPixelBusLg -#define B_32_I0_NEO_3 NeoPixelBusLg -#define B_32_I1_NEO_3 NeoPixelBusLg -#define B_32_I1_NEO_3P NeoPixelBusLg // parallel I2S +#define B_32_RN_NEO_3 NeoPixelBusLg // ESP32, S2, S3, C3 +//#define B_32_IN_NEO_3 NeoPixelBusLg // ESP32 (dynamic I2S selection) +#define B_32_I2_NEO_3 NeoPixelBusLg // ESP32, S2, S3 (automatic I2S selection, see typedef above) +#define B_32_IP_NEO_3 NeoPixelBusLg // parallel I2S (ESP32, S2, S3) //RGBW #define B_32_RN_NEO_4 NeoPixelBusLg -#define B_32_I0_NEO_4 NeoPixelBusLg -#define B_32_I1_NEO_4 NeoPixelBusLg -#define B_32_I1_NEO_4P NeoPixelBusLg // parallel I2S +#define B_32_I2_NEO_4 NeoPixelBusLg +#define B_32_IP_NEO_4 NeoPixelBusLg // parallel I2S //400Kbps #define B_32_RN_400_3 NeoPixelBusLg -#define B_32_I0_400_3 NeoPixelBusLg -#define B_32_I1_400_3 NeoPixelBusLg -#define B_32_I1_400_3P NeoPixelBusLg // parallel I2S +#define B_32_I2_400_3 NeoPixelBusLg +#define B_32_IP_400_3 NeoPixelBusLg // parallel I2S //TM1814 (RGBW) #define B_32_RN_TM1_4 NeoPixelBusLg -#define B_32_I0_TM1_4 NeoPixelBusLg -#define B_32_I1_TM1_4 NeoPixelBusLg -#define B_32_I1_TM1_4P NeoPixelBusLg // parallel I2S +#define B_32_I2_TM1_4 NeoPixelBusLg +#define B_32_IP_TM1_4 NeoPixelBusLg // parallel I2S //TM1829 (RGB) #define B_32_RN_TM2_3 NeoPixelBusLg -#define B_32_I0_TM2_3 NeoPixelBusLg -#define B_32_I1_TM2_3 NeoPixelBusLg -#define B_32_I1_TM2_3P NeoPixelBusLg // parallel I2S +#define B_32_I2_TM2_3 NeoPixelBusLg +#define B_32_IP_TM2_3 NeoPixelBusLg // parallel I2S //UCS8903 #define B_32_RN_UCS_3 NeoPixelBusLg -#define B_32_I0_UCS_3 NeoPixelBusLg -#define B_32_I1_UCS_3 NeoPixelBusLg -#define B_32_I1_UCS_3P NeoPixelBusLg // parallel I2S +#define B_32_I2_UCS_3 NeoPixelBusLg +#define B_32_IP_UCS_3 NeoPixelBusLg // parallel I2S //UCS8904 #define B_32_RN_UCS_4 NeoPixelBusLg -#define B_32_I0_UCS_4 NeoPixelBusLg -#define B_32_I1_UCS_4 NeoPixelBusLg -#define B_32_I1_UCS_4P NeoPixelBusLg// parallel I2S +#define B_32_I2_UCS_4 NeoPixelBusLg +#define B_32_IP_UCS_4 NeoPixelBusLg// parallel I2S //APA106 #define B_32_RN_APA106_3 NeoPixelBusLg -#define B_32_I0_APA106_3 NeoPixelBusLg -#define B_32_I1_APA106_3 NeoPixelBusLg -#define B_32_I1_APA106_3P NeoPixelBusLg // parallel I2S +#define B_32_I2_APA106_3 NeoPixelBusLg +#define B_32_IP_APA106_3 NeoPixelBusLg // parallel I2S //FW1906 GRBCW #define B_32_RN_FW6_5 NeoPixelBusLg -#define B_32_I0_FW6_5 NeoPixelBusLg -#define B_32_I1_FW6_5 NeoPixelBusLg -#define B_32_I1_FW6_5P NeoPixelBusLg // parallel I2S +#define B_32_I2_FW6_5 NeoPixelBusLg +#define B_32_IP_FW6_5 NeoPixelBusLg // parallel I2S //WS2805 RGBWC #define B_32_RN_2805_5 NeoPixelBusLg -#define B_32_I0_2805_5 NeoPixelBusLg -#define B_32_I1_2805_5 NeoPixelBusLg -#define B_32_I1_2805_5P NeoPixelBusLg // parallel I2S +#define B_32_I2_2805_5 NeoPixelBusLg +#define B_32_IP_2805_5 NeoPixelBusLg // parallel I2S //TM1914 (RGB) #define B_32_RN_TM1914_3 NeoPixelBusLg -#define B_32_I0_TM1914_3 NeoPixelBusLg -#define B_32_I1_TM1914_3 NeoPixelBusLg -#define B_32_I1_TM1914_3P NeoPixelBusLg // parallel I2S +#define B_32_I2_TM1914_3 NeoPixelBusLg +#define B_32_IP_TM1914_3 NeoPixelBusLg // parallel I2S //Sm16825 (RGBWC) #define B_32_RN_SM16825_5 NeoPixelBusLg -#define B_32_I0_SM16825_5 NeoPixelBusLg -#define B_32_I1_SM16825_5 NeoPixelBusLg -#define B_32_I1_SM16825_5P NeoPixelBusLg // parallel I2S +#define B_32_I2_SM16825_5 NeoPixelBusLg +#define B_32_IP_SM16825_5 NeoPixelBusLg // parallel I2S #endif //APA102 @@ -328,11 +331,11 @@ //handles pointer type conversion for all possible bus types class PolyBus { private: - static bool useParallelI2S; + static bool _useParallelI2S; public: - static inline void setParallelI2S1Output(bool b = true) { useParallelI2S = b; } - static inline bool isParallelI2S1Output(void) { return useParallelI2S; } + static inline void setParallelI2S1Output(bool b = true) { _useParallelI2S = b; } + static inline bool isParallelI2S1Output(void) { return _useParallelI2S; } // initialize SPI bus speed for DotStar methods template @@ -436,34 +439,19 @@ class PolyBus { case I_32_RN_TM1914_3: beginTM1914(busPtr); break; case I_32_RN_SM16825_5: (static_cast(busPtr))->Begin(); break; // I2S1 bus or parellel buses - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_NEO_4: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_400_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_TM1_4: if (useParallelI2S) beginTM1814(busPtr); else beginTM1814(busPtr); break; - case I_32_I1_TM2_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_UCS_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_UCS_4: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_APA106_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - case I_32_I1_TM1914_3: if (useParallelI2S) beginTM1914(busPtr); else beginTM1914(busPtr); break; - case I_32_I1_SM16825_5: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; - #endif - // I2S0 bus - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: (static_cast(busPtr))->Begin(); break; - case I_32_I0_NEO_4: (static_cast(busPtr))->Begin(); break; - case I_32_I0_400_3: (static_cast(busPtr))->Begin(); break; - case I_32_I0_TM1_4: beginTM1814(busPtr); break; - case I_32_I0_TM2_3: (static_cast(busPtr))->Begin(); break; - case I_32_I0_UCS_3: (static_cast(busPtr))->Begin(); break; - case I_32_I0_UCS_4: (static_cast(busPtr))->Begin(); break; - case I_32_I0_FW6_5: (static_cast(busPtr))->Begin(); break; - case I_32_I0_APA106_3: (static_cast(busPtr))->Begin(); break; - case I_32_I0_2805_5: (static_cast(busPtr))->Begin(); break; - case I_32_I0_TM1914_3: beginTM1914(busPtr); break; - case I_32_I0_SM16825_5: (static_cast(busPtr))->Begin(); break; + #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_I2_NEO_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I2_NEO_4: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I2_400_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I2_TM1_4: if (_useParallelI2S) beginTM1814(busPtr); else beginTM1814(busPtr); break; + case I_32_I2_TM2_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I2_UCS_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I2_UCS_4: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I2_FW6_5: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I2_APA106_3: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I2_2805_5: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I2_TM1914_3: if (_useParallelI2S) beginTM1914(busPtr); else beginTM1914(busPtr); break; + case I_32_I2_SM16825_5: if (_useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; #endif // ESP32 can (and should, to avoid inadvertantly driving the chip select signal) specify the pins used for SPI, but only in begin() case I_HS_DOT_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; @@ -484,8 +472,8 @@ class PolyBus { #if defined(ARDUINO_ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3)) // NOTE: "channel" is only used on ESP32 (and its variants) for RMT channel allocation // since 0.15.0-b3 I2S1 is favoured for classic ESP32 and moved to position 0 (channel 0) so we need to subtract 1 for correct RMT allocation - if (useParallelI2S && channel > 7) channel -= 8; // accommodate parallel I2S1 which is used 1st on classic ESP32 - else if (channel > 0) channel--; // accommodate I2S1 which is used as 1st bus on classic ESP32 + if (!_useParallelI2S && channel > 0) channel--; // accommodate I2S1 which is used as 1st bus on classic ESP32 + // if user selected parallel I2S, RMT is used 1st (8 channels) followed by parallel I2S (8 channels) #endif void* busPtr = nullptr; switch (busType) { @@ -555,34 +543,19 @@ class PolyBus { case I_32_RN_TM1914_3: busPtr = new B_32_RN_TM1914_3(len, pins[0], (NeoBusChannel)channel); break; case I_32_RN_SM16825_5: busPtr = new B_32_RN_SM16825_5(len, pins[0], (NeoBusChannel)channel); break; // I2S1 bus or paralell buses - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: if (useParallelI2S) busPtr = new B_32_I1_NEO_3P(len, pins[0]); else busPtr = new B_32_I1_NEO_3(len, pins[0]); break; - case I_32_I1_NEO_4: if (useParallelI2S) busPtr = new B_32_I1_NEO_4P(len, pins[0]); else busPtr = new B_32_I1_NEO_4(len, pins[0]); break; - case I_32_I1_400_3: if (useParallelI2S) busPtr = new B_32_I1_400_3P(len, pins[0]); else busPtr = new B_32_I1_400_3(len, pins[0]); break; - case I_32_I1_TM1_4: if (useParallelI2S) busPtr = new B_32_I1_TM1_4P(len, pins[0]); else busPtr = new B_32_I1_TM1_4(len, pins[0]); break; - case I_32_I1_TM2_3: if (useParallelI2S) busPtr = new B_32_I1_TM2_3P(len, pins[0]); else busPtr = new B_32_I1_TM2_3(len, pins[0]); break; - case I_32_I1_UCS_3: if (useParallelI2S) busPtr = new B_32_I1_UCS_3P(len, pins[0]); else busPtr = new B_32_I1_UCS_3(len, pins[0]); break; - case I_32_I1_UCS_4: if (useParallelI2S) busPtr = new B_32_I1_UCS_4P(len, pins[0]); else busPtr = new B_32_I1_UCS_4(len, pins[0]); break; - case I_32_I1_APA106_3: if (useParallelI2S) busPtr = new B_32_I1_APA106_3P(len, pins[0]); else busPtr = new B_32_I1_APA106_3(len, pins[0]); break; - case I_32_I1_FW6_5: if (useParallelI2S) busPtr = new B_32_I1_FW6_5P(len, pins[0]); else busPtr = new B_32_I1_FW6_5(len, pins[0]); break; - case I_32_I1_2805_5: if (useParallelI2S) busPtr = new B_32_I1_2805_5P(len, pins[0]); else busPtr = new B_32_I1_2805_5(len, pins[0]); break; - case I_32_I1_TM1914_3: if (useParallelI2S) busPtr = new B_32_I1_TM1914_3P(len, pins[0]); else busPtr = new B_32_I1_TM1914_3(len, pins[0]); break; - case I_32_I1_SM16825_5: if (useParallelI2S) busPtr = new B_32_I1_SM16825_5P(len, pins[0]); else busPtr = new B_32_I1_SM16825_5(len, pins[0]); break; - #endif - // I2S0 bus - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: busPtr = new B_32_I0_NEO_3(len, pins[0]); break; - case I_32_I0_NEO_4: busPtr = new B_32_I0_NEO_4(len, pins[0]); break; - case I_32_I0_400_3: busPtr = new B_32_I0_400_3(len, pins[0]); break; - case I_32_I0_TM1_4: busPtr = new B_32_I0_TM1_4(len, pins[0]); break; - case I_32_I0_TM2_3: busPtr = new B_32_I0_TM2_3(len, pins[0]); break; - case I_32_I0_UCS_3: busPtr = new B_32_I0_UCS_3(len, pins[0]); break; - case I_32_I0_UCS_4: busPtr = new B_32_I0_UCS_4(len, pins[0]); break; - case I_32_I0_APA106_3: busPtr = new B_32_I0_APA106_3(len, pins[0]); break; - case I_32_I0_FW6_5: busPtr = new B_32_I0_FW6_5(len, pins[0]); break; - case I_32_I0_2805_5: busPtr = new B_32_I0_2805_5(len, pins[0]); break; - case I_32_I0_TM1914_3: busPtr = new B_32_I0_TM1914_3(len, pins[0]); break; - case I_32_I0_SM16825_5: busPtr = new B_32_I0_SM16825_5(len, pins[0]); break; + #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_I2_NEO_3: if (_useParallelI2S) busPtr = new B_32_IP_NEO_3(len, pins[0]); else busPtr = new B_32_I2_NEO_3(len, pins[0]); break; + case I_32_I2_NEO_4: if (_useParallelI2S) busPtr = new B_32_IP_NEO_4(len, pins[0]); else busPtr = new B_32_I2_NEO_4(len, pins[0]); break; + case I_32_I2_400_3: if (_useParallelI2S) busPtr = new B_32_IP_400_3(len, pins[0]); else busPtr = new B_32_I2_400_3(len, pins[0]); break; + case I_32_I2_TM1_4: if (_useParallelI2S) busPtr = new B_32_IP_TM1_4(len, pins[0]); else busPtr = new B_32_I2_TM1_4(len, pins[0]); break; + case I_32_I2_TM2_3: if (_useParallelI2S) busPtr = new B_32_IP_TM2_3(len, pins[0]); else busPtr = new B_32_I2_TM2_3(len, pins[0]); break; + case I_32_I2_UCS_3: if (_useParallelI2S) busPtr = new B_32_IP_UCS_3(len, pins[0]); else busPtr = new B_32_I2_UCS_3(len, pins[0]); break; + case I_32_I2_UCS_4: if (_useParallelI2S) busPtr = new B_32_IP_UCS_4(len, pins[0]); else busPtr = new B_32_I2_UCS_4(len, pins[0]); break; + case I_32_I2_APA106_3: if (_useParallelI2S) busPtr = new B_32_IP_APA106_3(len, pins[0]); else busPtr = new B_32_I2_APA106_3(len, pins[0]); break; + case I_32_I2_FW6_5: if (_useParallelI2S) busPtr = new B_32_IP_FW6_5(len, pins[0]); else busPtr = new B_32_I2_FW6_5(len, pins[0]); break; + case I_32_I2_2805_5: if (_useParallelI2S) busPtr = new B_32_IP_2805_5(len, pins[0]); else busPtr = new B_32_I2_2805_5(len, pins[0]); break; + case I_32_I2_TM1914_3: if (_useParallelI2S) busPtr = new B_32_IP_TM1914_3(len, pins[0]); else busPtr = new B_32_I2_TM1914_3(len, pins[0]); break; + case I_32_I2_SM16825_5: if (_useParallelI2S) busPtr = new B_32_IP_SM16825_5(len, pins[0]); else busPtr = new B_32_I2_SM16825_5(len, pins[0]); break; #endif #endif // for 2-wire: pins[1] is clk, pins[0] is dat. begin expects (len, clk, dat) @@ -669,34 +642,19 @@ class PolyBus { case I_32_RN_TM1914_3: (static_cast(busPtr))->Show(consistent); break; case I_32_RN_SM16825_5: (static_cast(busPtr))->Show(consistent); break; // I2S1 bus or paralell buses - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_NEO_4: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_400_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_TM1_4: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_TM2_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_UCS_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_UCS_4: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_APA106_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_TM1914_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_SM16825_5: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; - #endif - // I2S0 bus - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_NEO_4: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_400_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_TM1_4: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_TM2_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_UCS_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_UCS_4: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_APA106_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_FW6_5: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_2805_5: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_TM1914_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_I0_SM16825_5: (static_cast(busPtr))->Show(consistent); break; + #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_I2_NEO_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I2_NEO_4: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I2_400_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I2_TM1_4: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I2_TM2_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I2_UCS_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I2_UCS_4: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I2_APA106_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I2_FW6_5: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I2_2805_5: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I2_TM1914_3: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I2_SM16825_5: if (_useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; #endif #endif case I_HS_DOT_3: (static_cast(busPtr))->Show(consistent); break; @@ -743,6 +701,7 @@ class PolyBus { case I_8266_U0_UCS_4: return (static_cast(busPtr))->CanShow(); break; case I_8266_U1_UCS_4: return (static_cast(busPtr))->CanShow(); break; case I_8266_DM_UCS_4: return (static_cast(busPtr))->CanShow(); break; + case I_8266_BB_UCS_4: return (static_cast(busPtr))->CanShow(); break; case I_8266_U0_APA106_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_U1_APA106_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_DM_APA106_3: return (static_cast(busPtr))->CanShow(); break; @@ -779,34 +738,19 @@ class PolyBus { case I_32_RN_TM1914_3: return (static_cast(busPtr))->CanShow(); break; case I_32_RN_SM16825_5: return (static_cast(busPtr))->CanShow(); break; // I2S1 bus or paralell buses - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_NEO_4: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_400_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_TM1_4: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_TM2_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_UCS_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_UCS_4: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_APA106_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_FW6_5: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_2805_5: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_TM1914_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_SM16825_5: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; - #endif - // I2S0 bus - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_NEO_4: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_400_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_TM1_4: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_TM2_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_UCS_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_UCS_4: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_APA106_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_FW6_5: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_2805_5: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_TM1914_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_SM16825_5: return (static_cast(busPtr))->CanShow(); break; + #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_I2_NEO_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I2_NEO_4: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I2_400_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I2_TM1_4: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I2_TM2_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I2_UCS_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I2_UCS_4: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I2_APA106_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I2_FW6_5: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I2_2805_5: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I2_TM1914_3: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I2_SM16825_5: if (_useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; #endif #endif case I_HS_DOT_3: return (static_cast(busPtr))->CanShow(); break; @@ -823,7 +767,7 @@ class PolyBus { return true; } - static void setPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint32_t c, uint8_t co, uint16_t wwcw = 0) { + [[gnu::hot]] static void setPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint32_t c, uint8_t co, uint16_t wwcw = 0) { uint8_t r = c >> 16; uint8_t g = c >> 8; uint8_t b = c >> 0; @@ -916,34 +860,19 @@ class PolyBus { case I_32_RN_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_32_RN_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; // I2S1 bus or paralell buses - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I1_NEO_4: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_32_I1_400_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I1_TM1_4: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_32_I1_TM2_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I1_UCS_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - case I_32_I1_UCS_4: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - case I_32_I1_APA106_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); else (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); else (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_32_I1_TM1914_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I1_SM16825_5: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); else (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; - #endif - // I2S0 bus - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I0_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_32_I0_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I0_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_32_I0_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I0_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - case I_32_I0_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - case I_32_I0_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I0_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_32_I0_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_32_I0_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I0_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; + #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_I2_NEO_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_I2_NEO_4: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, col); break; + case I_32_I2_400_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_I2_TM1_4: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, col); break; + case I_32_I2_TM2_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_I2_UCS_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; + case I_32_I2_UCS_4: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; + case I_32_I2_APA106_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_I2_FW6_5: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); else (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_32_I2_2805_5: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); else (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_32_I2_TM1914_3: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_I2_SM16825_5: if (_useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); else (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; #endif #endif case I_HS_DOT_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; @@ -1027,34 +956,19 @@ class PolyBus { case I_32_RN_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; case I_32_RN_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; // I2S1 bus or paralell buses - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_NEO_4: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_400_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_TM1_4: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_TM2_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_UCS_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_UCS_4: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_APA106_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_TM1914_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_SM16825_5: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; - #endif - // I2S0 bus - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_400_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_2805_5: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I0_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; + #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_I2_NEO_3: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I2_NEO_4: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I2_400_3: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I2_TM1_4: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I2_TM2_3: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I2_UCS_3: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I2_UCS_4: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I2_APA106_3: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I2_FW6_5: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I2_2805_5: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I2_TM1914_3: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I2_SM16825_5: if (_useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; #endif #endif case I_HS_DOT_3: (static_cast(busPtr))->SetLuminance(b); break; @@ -1070,7 +984,7 @@ class PolyBus { } } - static uint32_t getPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint8_t co) { + [[gnu::hot]] static uint32_t getPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint8_t co) { RgbwColor col(0,0,0,0); switch (busType) { case I_NONE: break; @@ -1139,34 +1053,19 @@ class PolyBus { case I_32_RN_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_RN_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,max(c.WW,c.CW)/257); } break; // will not return original W // I2S1 bus or paralell buses - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I1_NEO_4: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I1_400_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I1_TM1_4: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I1_TM2_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I1_UCS_3: { Rgb48Color c = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,0); } break; - case I_32_I1_UCS_4: { Rgbw64Color c = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,c.W/257); } break; - case I_32_I1_APA106_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I1_FW6_5: { RgbwwColor c = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_32_I1_2805_5: { RgbwwColor c = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_32_I1_TM1914_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I1_SM16825_5: { Rgbww80Color c = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,max(c.WW,c.CW)/257); } break; // will not return original W - #endif - // I2S0 bus - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I0_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I0_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I0_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I0_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I0_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,0); } break; - case I_32_I0_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,c.W/257); } break; - case I_32_I0_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I0_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_32_I0_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - case I_32_I0_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I0_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,max(c.WW,c.CW)/257); } break; // will not return original W + #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_I2_NEO_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I2_NEO_4: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I2_400_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I2_TM1_4: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I2_TM2_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I2_UCS_3: { Rgb48Color c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,0); } break; + case I_32_I2_UCS_4: { Rgbw64Color c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,c.W/257); } break; + case I_32_I2_APA106_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I2_FW6_5: { RgbwwColor c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_32_I2_2805_5: { RgbwwColor c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_32_I2_TM1914_3: col = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I2_SM16825_5: { Rgbww80Color c = (_useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,max(c.WW,c.CW)/257); } break; // will not return original W #endif #endif case I_HS_DOT_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; @@ -1269,34 +1168,19 @@ class PolyBus { case I_32_RN_TM1914_3: delete (static_cast(busPtr)); break; case I_32_RN_SM16825_5: delete (static_cast(busPtr)); break; // I2S1 bus or paralell buses - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_NEO_4: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_400_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_TM1_4: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_TM2_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_UCS_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_UCS_4: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_APA106_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_FW6_5: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_2805_5: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_TM1914_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - case I_32_I1_SM16825_5: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; - #endif - // I2S0 bus - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: delete (static_cast(busPtr)); break; - case I_32_I0_NEO_4: delete (static_cast(busPtr)); break; - case I_32_I0_400_3: delete (static_cast(busPtr)); break; - case I_32_I0_TM1_4: delete (static_cast(busPtr)); break; - case I_32_I0_TM2_3: delete (static_cast(busPtr)); break; - case I_32_I0_UCS_3: delete (static_cast(busPtr)); break; - case I_32_I0_UCS_4: delete (static_cast(busPtr)); break; - case I_32_I0_APA106_3: delete (static_cast(busPtr)); break; - case I_32_I0_FW6_5: delete (static_cast(busPtr)); break; - case I_32_I0_2805_5: delete (static_cast(busPtr)); break; - case I_32_I0_TM1914_3: delete (static_cast(busPtr)); break; - case I_32_I0_SM16825_5: delete (static_cast(busPtr)); break; + #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_I2_NEO_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I2_NEO_4: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I2_400_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I2_TM1_4: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I2_TM2_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I2_UCS_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I2_UCS_4: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I2_APA106_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I2_FW6_5: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I2_2805_5: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I2_TM1914_3: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I2_SM16825_5: if (_useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; #endif #endif case I_HS_DOT_3: delete (static_cast(busPtr)); break; @@ -1312,8 +1196,178 @@ class PolyBus { } } + static unsigned getDataSize(void* busPtr, uint8_t busType) { + unsigned size = 0; + switch (busType) { + case I_NONE: break; + #ifdef ESP8266 + case I_8266_U0_NEO_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U1_NEO_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_DM_NEO_3: size = (static_cast(busPtr))->PixelsSize()*5; break; + case I_8266_BB_NEO_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U0_NEO_4: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U1_NEO_4: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_DM_NEO_4: size = (static_cast(busPtr))->PixelsSize()*5; break; + case I_8266_BB_NEO_4: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U0_400_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U1_400_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_DM_400_3: size = (static_cast(busPtr))->PixelsSize()*5; break; + case I_8266_BB_400_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U0_TM1_4: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U1_TM1_4: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_DM_TM1_4: size = (static_cast(busPtr))->PixelsSize()*5; break; + case I_8266_BB_TM1_4: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U0_TM2_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U1_TM2_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_DM_TM2_3: size = (static_cast(busPtr))->PixelsSize()*5; break; + case I_8266_BB_TM2_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U0_UCS_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U1_UCS_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_DM_UCS_3: size = (static_cast(busPtr))->PixelsSize()*5; break; + case I_8266_BB_UCS_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U0_UCS_4: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U1_UCS_4: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_DM_UCS_4: size = (static_cast(busPtr))->PixelsSize()*5; break; + case I_8266_BB_UCS_4: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U0_APA106_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U1_APA106_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_DM_APA106_3: size = (static_cast(busPtr))->PixelsSize()*5; break; + case I_8266_BB_APA106_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U0_FW6_5: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U1_FW6_5: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_DM_FW6_5: size = (static_cast(busPtr))->PixelsSize()*5; break; + case I_8266_BB_FW6_5: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U0_2805_5: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U1_2805_5: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_DM_2805_5: size = (static_cast(busPtr))->PixelsSize()*5; break; + case I_8266_BB_2805_5: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U0_TM1914_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U1_TM1914_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_DM_TM1914_3: size = (static_cast(busPtr))->PixelsSize()*5; break; + case I_8266_BB_TM1914_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U0_SM16825_5: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_U1_SM16825_5: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_8266_DM_SM16825_5: size = (static_cast(busPtr))->PixelsSize()*5; break; + case I_8266_BB_SM16825_5: size = (static_cast(busPtr))->PixelsSize()*2; break; + #endif + #ifdef ARDUINO_ARCH_ESP32 + // RMT buses (front + back + small system managed RMT) + case I_32_RN_NEO_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_32_RN_NEO_4: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_32_RN_400_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_32_RN_TM1_4: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_32_RN_TM2_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_32_RN_UCS_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_32_RN_UCS_4: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_32_RN_APA106_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_32_RN_FW6_5: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_32_RN_2805_5: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_32_RN_TM1914_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_32_RN_SM16825_5: size = (static_cast(busPtr))->PixelsSize()*2; break; + // I2S1 bus or paralell buses (front + DMA; DMA = front * cadence, aligned to 4 bytes) + #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_I2_NEO_3: size = (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; + case I_32_I2_NEO_4: size = (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; + case I_32_I2_400_3: size = (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; + case I_32_I2_TM1_4: size = (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; + case I_32_I2_TM2_3: size = (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; + case I_32_I2_UCS_3: size = (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; + case I_32_I2_UCS_4: size = (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; + case I_32_I2_APA106_3: size = (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; + case I_32_I2_FW6_5: size = (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; + case I_32_I2_2805_5: size = (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; + case I_32_I2_TM1914_3: size = (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; + case I_32_I2_SM16825_5: size = (_useParallelI2S) ? (static_cast(busPtr))->PixelsSize()*4 : (static_cast(busPtr))->PixelsSize()*4; break; + #endif + #endif + case I_HS_DOT_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_SS_DOT_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_HS_LPD_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_SS_LPD_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_HS_LPO_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_SS_LPO_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_HS_WS1_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_SS_WS1_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_HS_P98_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + case I_SS_P98_3: size = (static_cast(busPtr))->PixelsSize()*2; break; + } + return size; + } + + static unsigned memUsage(unsigned count, unsigned busType) { + unsigned size = count*3; // let's assume 3 channels, we will add count or 2*count below for 4 channels or 5 channels + switch (busType) { + case I_NONE: size = 0; break; + #ifdef ESP8266 + // UART methods have front + back buffers + small UART + case I_8266_U0_NEO_4: size = (size + count)*2; break; // 4 channels + case I_8266_U1_NEO_4: size = (size + count)*2; break; // 4 channels + case I_8266_BB_NEO_4: size = (size + count)*2; break; // 4 channels + case I_8266_U0_TM1_4: size = (size + count)*2; break; // 4 channels + case I_8266_U1_TM1_4: size = (size + count)*2; break; // 4 channels + case I_8266_BB_TM1_4: size = (size + count)*2; break; // 4 channels + case I_8266_U0_UCS_3: size *= 4; break; // 16 bit + case I_8266_U1_UCS_3: size *= 4; break; // 16 bit + case I_8266_BB_UCS_3: size *= 4; break; // 16 bit + case I_8266_U0_UCS_4: size = (size + count)*2*2; break; // 16 bit 4 channels + case I_8266_U1_UCS_4: size = (size + count)*2*2; break; // 16 bit 4 channels + case I_8266_BB_UCS_4: size = (size + count)*2*2; break; // 16 bit 4 channels + case I_8266_U0_FW6_5: size = (size + 2*count)*2; break; // 5 channels + case I_8266_U1_FW6_5: size = (size + 2*count)*2; break; // 5channels + case I_8266_BB_FW6_5: size = (size + 2*count)*2; break; // 5 channels + case I_8266_U0_2805_5: size = (size + 2*count)*2; break; // 5 channels + case I_8266_U1_2805_5: size = (size + 2*count)*2; break; // 5 channels + case I_8266_BB_2805_5: size = (size + 2*count)*2; break; // 5 channels + case I_8266_U0_SM16825_5: size = (size + 2*count)*2*2; break; // 16 bit 5 channels + case I_8266_U1_SM16825_5: size = (size + 2*count)*2*2; break; // 16 bit 5 channels + case I_8266_BB_SM16825_5: size = (size + 2*count)*2*2; break; // 16 bit 5 channels + // DMA methods have front + DMA buffer = ((1+(3+1)) * channels) + case I_8266_DM_NEO_3: size *= 5; break; + case I_8266_DM_NEO_4: size = (size + count)*5; break; + case I_8266_DM_400_3: size *= 5; break; + case I_8266_DM_TM1_4: size = (size + count)*5; break; + case I_8266_DM_TM2_3: size *= 5; break; + case I_8266_DM_UCS_3: size *= 2*5; break; + case I_8266_DM_UCS_4: size = (size + count)*2*5; break; + case I_8266_DM_APA106_3: size *= 5; break; + case I_8266_DM_FW6_5: size = (size + 2*count)*5; break; + case I_8266_DM_2805_5: size = (size + 2*count)*5; break; + case I_8266_DM_TM1914_3: size *= 5; break; + case I_8266_DM_SM16825_5: size = (size + 2*count)*2*5; break; + #endif + #ifdef ARDUINO_ARCH_ESP32 + // RMT buses (1x front and 1x back buffer) + case I_32_RN_NEO_4: size = (size + count)*2; break; + case I_32_RN_TM1_4: size = (size + count)*2; break; + case I_32_RN_UCS_3: size *= 2*2; break; + case I_32_RN_UCS_4: size = (size + count)*2*2; break; + case I_32_RN_FW6_5: size = (size + 2*count)*2; break; + case I_32_RN_2805_5: size = (size + 2*count)*2; break; + case I_32_RN_SM16825_5: size = (size + 2*count)*2*2; break; + // I2S1 bus or paralell buses (individual 1x front and 1 DMA (3x or 4x pixel count) or common back DMA buffers) + #ifndef CONFIG_IDF_TARGET_ESP32C3 + case I_32_I2_NEO_3: size *= 4; break; + case I_32_I2_NEO_4: size = (size + count)*4; break; + case I_32_I2_400_3: size *= 4; break; + case I_32_I2_TM1_4: size = (size + count)*4; break; + case I_32_I2_TM2_3: size *= 4; break; + case I_32_I2_UCS_3: size *= 2*4; break; + case I_32_I2_UCS_4: size = (size + count)*2*4; break; + case I_32_I2_APA106_3: size *= 4; break; + case I_32_I2_FW6_5: size = (size + 2*count)*4; break; + case I_32_I2_2805_5: size = (size + 2*count)*4; break; + case I_32_I2_TM1914_3: size *= 4; break; + case I_32_I2_SM16825_5: size = (size + 2*count)*2*4; break; + #endif + #endif + // everything else uses 2 buffers + default: size *= 2; break; + } + return size; + } + //gives back the internal type index (I_XX_XXX_X above) for the input - static uint8_t getI(uint8_t busType, uint8_t* pins, uint8_t num = 0) { + static uint8_t getI(uint8_t busType, const uint8_t* pins, uint8_t num = 0) { if (!Bus::isDigital(busType)) return I_NONE; if (Bus::is2Pin(busType)) { //SPI LED chips bool isHSPI = false; @@ -1372,26 +1426,33 @@ class PolyBus { uint8_t offset = 0; // 0 = RMT (num 1-8), 1 = I2S0 (used by Audioreactive), 2 = I2S1 #if defined(CONFIG_IDF_TARGET_ESP32S2) // ESP32-S2 only has 4 RMT channels - if (num > 4) return I_NONE; - if (num > 3) offset = 1; // only one I2S (use last to allow Audioreactive) + if (_useParallelI2S) { + if (num > 11) return I_NONE; + if (num > 3) offset = 1; // use x8 parallel I2S0 channels (use last to allow Audioreactive) + } else { + if (num > 4) return I_NONE; + if (num > 3) offset = 1; // only one I2S0 (use last to allow Audioreactive) + } #elif defined(CONFIG_IDF_TARGET_ESP32C3) // On ESP32-C3 only the first 2 RMT channels are usable for transmitting if (num > 1) return I_NONE; //if (num > 1) offset = 1; // I2S not supported yet (only 1 I2S) #elif defined(CONFIG_IDF_TARGET_ESP32S3) // On ESP32-S3 only the first 4 RMT channels are usable for transmitting - if (num > 3) return I_NONE; - //if (num > 3) offset = num -4; // I2S not supported yet + if (_useParallelI2S) { + if (num > 11) return I_NONE; + if (num > 3) offset = 1; // use x8 parallel I2S LCD channels + } else { + if (num > 3) return I_NONE; // do not use single I2S (as it is not supported) + } #else - // standard ESP32 has 8 RMT and 2 I2S channels - if (useParallelI2S) { - if (num > 16) return I_NONE; - if (num < 8) offset = 2; // prefer 8 parallel I2S1 channels - if (num == 16) offset = 1; + // standard ESP32 has 8 RMT and x1/x8 I2S1 channels + if (_useParallelI2S) { + if (num > 15) return I_NONE; + if (num > 7) offset = 1; // 8 RMT followed by 8 I2S } else { if (num > 9) return I_NONE; - if (num > 8) offset = 1; - if (num == 0) offset = 2; // prefer I2S1 for 1st bus (less flickering but more RAM needed) + if (num == 0) offset = 1; // prefer I2S1 for 1st bus (less flickering but more RAM needed) } #endif switch (busType) { diff --git a/wled00/button.cpp b/wled00/button.cpp index 4d6f954f..1c50200a 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -29,7 +29,7 @@ void shortPressAction(uint8_t b) #ifndef WLED_DISABLE_MQTT // publish MQTT message if (buttonPublishMqtt && WLED_MQTT_CONNECTED) { - char subuf[64]; + char subuf[MQTT_MAX_TOPIC_LEN + 32]; sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b); mqtt->publish(subuf, 0, false, "short"); } @@ -40,7 +40,7 @@ void longPressAction(uint8_t b) { if (!macroLongPress[b]) { switch (b) { - case 0: setRandomColor(col); colorUpdated(CALL_MODE_BUTTON); break; + case 0: setRandomColor(colPri); colorUpdated(CALL_MODE_BUTTON); break; case 1: if(buttonBriDirection) { if (bri == 255) break; // avoid unnecessary updates to brightness @@ -62,7 +62,7 @@ void longPressAction(uint8_t b) #ifndef WLED_DISABLE_MQTT // publish MQTT message if (buttonPublishMqtt && WLED_MQTT_CONNECTED) { - char subuf[64]; + char subuf[MQTT_MAX_TOPIC_LEN + 32]; sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b); mqtt->publish(subuf, 0, false, "long"); } @@ -74,7 +74,7 @@ void doublePressAction(uint8_t b) if (!macroDoublePress[b]) { switch (b) { //case 0: toggleOnOff(); colorUpdated(CALL_MODE_BUTTON); break; //instant short press on button 0 if no macro set - case 1: ++effectPalette %= strip.getPaletteCount(); colorUpdated(CALL_MODE_BUTTON); break; + case 1: ++effectPalette %= getPaletteCount(); colorUpdated(CALL_MODE_BUTTON); break; } } else { applyPreset(macroDoublePress[b], CALL_MODE_BUTTON_PRESET); @@ -83,19 +83,19 @@ void doublePressAction(uint8_t b) #ifndef WLED_DISABLE_MQTT // publish MQTT message if (buttonPublishMqtt && WLED_MQTT_CONNECTED) { - char subuf[64]; + char subuf[MQTT_MAX_TOPIC_LEN + 32]; sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b); mqtt->publish(subuf, 0, false, "double"); } #endif } -bool isButtonPressed(uint8_t i) +bool isButtonPressed(uint8_t b) { - if (btnPin[i]<0) return false; - unsigned pin = btnPin[i]; + if (btnPin[b]<0) return false; + unsigned pin = btnPin[b]; - switch (buttonType[i]) { + switch (buttonType[b]) { case BTN_TYPE_NONE: case BTN_TYPE_RESERVED: break; @@ -113,7 +113,7 @@ bool isButtonPressed(uint8_t i) #ifdef SOC_TOUCH_VERSION_2 //ESP32 S2 and S3 provide a function to check touch state (state is updated in interrupt) if (touchInterruptGetLastStatus(pin)) return true; #else - if (digitalPinToTouchChannel(btnPin[i]) >= 0 && touchRead(pin) <= touchThreshold) return true; + if (digitalPinToTouchChannel(btnPin[b]) >= 0 && touchRead(pin) <= touchThreshold) return true; #endif #endif break; @@ -151,7 +151,7 @@ void handleSwitch(uint8_t b) #ifndef WLED_DISABLE_MQTT // publish MQTT message if (buttonPublishMqtt && WLED_MQTT_CONNECTED) { - char subuf[64]; + char subuf[MQTT_MAX_TOPIC_LEN + 32]; if (buttonType[b] == BTN_TYPE_PIR_SENSOR) sprintf_P(subuf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)b); else sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b); mqtt->publish(subuf, 0, false, !buttonPressedBefore[b] ? "off" : "on"); @@ -226,11 +226,11 @@ void handleAnalog(uint8_t b) effectIntensity = aRead; } else if (macroDoublePress[b] == 247) { // selected palette - effectPalette = map(aRead, 0, 252, 0, strip.getPaletteCount()-1); - effectPalette = constrain(effectPalette, 0, strip.getPaletteCount()-1); // map is allowed to "overshoot", so we need to contrain the result + effectPalette = map(aRead, 0, 252, 0, getPaletteCount()-1); + effectPalette = constrain(effectPalette, 0, getPaletteCount()-1); // map is allowed to "overshoot", so we need to contrain the result } else if (macroDoublePress[b] == 200) { // primary color, hue, full saturation - colorHStoRGB(aRead*256,255,col); + colorHStoRGB(aRead*256,255,colPri); } else { // otherwise use "double press" for segment selection Segment& seg = strip.getSegment(macroDoublePress[b]); @@ -375,6 +375,7 @@ void handleIO() if (rlyPin>=0) { pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT); digitalWrite(rlyPin, rlyMde); + delay(50); // wait for relay to switch and power to stabilize } offMode = false; } diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index e043c343..fcdf4ff4 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -6,6 +6,35 @@ * The structure of the JSON is not to be considered an official API and may change without notice. */ +#ifndef PIXEL_COUNTS + #define PIXEL_COUNTS DEFAULT_LED_COUNT +#endif + +#ifndef DATA_PINS + #define DATA_PINS DEFAULT_LED_PIN +#endif + +#ifndef LED_TYPES + #define LED_TYPES DEFAULT_LED_TYPE +#endif + +#ifndef DEFAULT_LED_COLOR_ORDER + #define DEFAULT_LED_COLOR_ORDER COL_ORDER_GRB //default to GRB +#endif + +static constexpr unsigned sumPinsRequired(const unsigned* current, size_t count) { + return (count > 0) ? (Bus::getNumberOfPins(*current) + sumPinsRequired(current+1,count-1)) : 0; +} + +static constexpr bool validatePinsAndTypes(const unsigned* types, unsigned numTypes, unsigned numPins ) { + // Pins provided < pins required -> always invalid + // Pins provided = pins required -> always valid + // Pins provided > pins required -> valid if excess pins are a product of last type pins since it will be repeated + return (sumPinsRequired(types, numTypes) > numPins) ? false : + (numPins - sumPinsRequired(types, numTypes)) % Bus::getNumberOfPins(types[numTypes-1]) == 0; +} + + //simple macro for ArduinoJSON's or syntax #define CJSON(a,b) a = b | a @@ -24,7 +53,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonObject ethernet = doc[F("eth")]; CJSON(ethernetType, ethernet["type"]); // NOTE: Ethernet configuration takes priority over other use of pins - WLED::instance().initEthernet(); + initEthernet(); #endif JsonObject id = doc["id"]; @@ -38,8 +67,24 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonObject nw = doc["nw"]; #ifndef WLED_DISABLE_ESPNOW CJSON(enableESPNow, nw[F("espnow")]); - getStringFromJson(linked_remote, nw[F("linked_remote")], 13); - linked_remote[12] = '\0'; + linked_remotes.clear(); + JsonVariant lrem = nw[F("linked_remote")]; + if (!lrem.isNull()) { + if (lrem.is()) { + for (size_t i = 0; i < lrem.size(); i++) { + std::array entry{}; + getStringFromJson(entry.data(), lrem[i], 13); + entry[12] = '\0'; + linked_remotes.emplace_back(entry); + } + } + else { // legacy support for single MAC address in config + std::array entry{}; + getStringFromJson(entry.data(), lrem, 13); + entry[12] = '\0'; + linked_remotes.emplace_back(entry); + } + } #endif size_t n = 0; @@ -53,9 +98,11 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonArray sn = wifi["sn"]; char ssid[33] = ""; char pass[65] = ""; + char bssid[13] = ""; IPAddress nIP = (uint32_t)0U, nGW = (uint32_t)0U, nSN = (uint32_t)0x00FFFFFF; // little endian getStringFromJson(ssid, wifi[F("ssid")], 33); getStringFromJson(pass, wifi["psk"], 65); // password is not normally present but if it is, use it + getStringFromJson(bssid, wifi[F("bssid")], 13); for (size_t i = 0; i < 4; i++) { CJSON(nIP[i], ip[i]); CJSON(nGW[i], gw[i]); @@ -63,6 +110,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { } if (strlen(ssid) > 0) strlcpy(multiWiFi[n].clientSSID, ssid, 33); // this will keep old SSID intact if not present in JSON if (strlen(pass) > 0) strlcpy(multiWiFi[n].clientPass, pass, 65); // this will keep old password intact if not present in JSON + if (strlen(bssid) > 0) fillStr2MAC(multiWiFi[n].bssid, bssid); multiWiFi[n].staticIP = nIP; multiWiFi[n].staticGW = nGW; multiWiFi[n].staticSN = nSN; @@ -114,22 +162,25 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(strip.correctWB, hw_led["cct"]); CJSON(strip.cctFromRgb, hw_led[F("cr")]); CJSON(cctICused, hw_led[F("ic")]); - CJSON(strip.cctBlending, hw_led[F("cb")]); - Bus::setCCTBlend(strip.cctBlending); + uint8_t cctBlending = hw_led[F("cb")] | Bus::getCCTBlend(); + Bus::setCCTBlend(cctBlending); strip.setTargetFps(hw_led["fps"]); //NOP if 0, default 42 FPS - CJSON(useGlobalLedBuffer, hw_led[F("ld")]); + #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) + CJSON(useParallelI2S, hw_led[F("prl")]); + #endif #ifndef WLED_DISABLE_2D // 2D Matrix Settings JsonObject matrix = hw_led[F("matrix")]; if (!matrix.isNull()) { strip.isMatrix = true; - CJSON(strip.panels, matrix[F("mpc")]); + unsigned numPanels = matrix[F("mpc")] | 1; + numPanels = constrain(numPanels, 1, WLED_MAX_PANELS); strip.panel.clear(); JsonArray panels = matrix[F("panels")]; - int s = 0; + unsigned s = 0; if (!panels.isNull()) { - strip.panel.reserve(max(1U,min((size_t)strip.panels,(size_t)WLED_MAX_PANELS))); // pre-allocate memory for panels + strip.panel.reserve(numPanels); // pre-allocate default 8x8 panels for (JsonObject pnl : panels) { WS2812FX::Panel p; CJSON(p.bottomStart, pnl["b"]); @@ -141,58 +192,21 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(p.height, pnl["h"]); CJSON(p.width, pnl["w"]); strip.panel.push_back(p); - if (++s >= WLED_MAX_PANELS || s >= strip.panels) break; // max panels reached + if (++s >= numPanels) break; // max panels reached } - } else { - // fallback - WS2812FX::Panel p; - strip.panels = 1; - p.height = p.width = 8; - p.xOffset = p.yOffset = 0; - p.options = 0; - strip.panel.push_back(p); } - // cannot call strip.setUpMatrix() here due to already locked JSON buffer + strip.panel.shrink_to_fit(); // release unused memory (just in case) + // cannot call strip.deserializeLedmap()/strip.setUpMatrix() here due to already locked JSON buffer + //if (!fromFS) doInit2D = true; // if called at boot (fromFS==true), WLED::beginStrip() will take care of setting up matrix } #endif + DEBUG_PRINTF_P(PSTR("Heap before buses: %d\n"), ESP.getFreeHeap()); JsonArray ins = hw_led["ins"]; - - if (fromFS || !ins.isNull()) { - DEBUG_PRINTF_P(PSTR("Heap before buses: %d\n"), ESP.getFreeHeap()); + if (!ins.isNull()) { int s = 0; // bus iterator - if (fromFS) BusManager::removeAll(); // can't safely manipulate busses directly in network callback - unsigned mem = 0; - - // determine if it is sensible to use parallel I2S outputs on ESP32 (i.e. more than 5 outputs = 1 I2S + 4 RMT) - bool useParallel = false; - #if defined(ARDUINO_ARCH_ESP32) && !defined(ARDUINO_ARCH_ESP32S2) && !defined(ARDUINO_ARCH_ESP32S3) && !defined(ARDUINO_ARCH_ESP32C3) - unsigned digitalCount = 0; - unsigned maxLedsOnBus = 0; - unsigned maxChannels = 0; for (JsonObject elm : ins) { - unsigned type = elm["type"] | TYPE_WS2812_RGB; - unsigned len = elm["len"] | DEFAULT_LED_COUNT; - if (!Bus::isDigital(type)) continue; - if (!Bus::is2Pin(type)) { - digitalCount++; - unsigned channels = Bus::getNumberOfChannels(type); - if (len > maxLedsOnBus) maxLedsOnBus = len; - if (channels > maxChannels) maxChannels = channels; - } - } - DEBUG_PRINTF_P(PSTR("Maximum LEDs on a bus: %u\nDigital buses: %u\n"), maxLedsOnBus, digitalCount); - // we may remove 300 LEDs per bus limit when NeoPixelBus is updated beyond 2.9.0 - if (maxLedsOnBus <= 300 && digitalCount > 5) { - DEBUG_PRINTLN(F("Switching to parallel I2S.")); - useParallel = true; - BusManager::useParallelOutput(); - mem = BusManager::memUsage(maxChannels, maxLedsOnBus, 8); // use alternate memory calculation - } - #endif - - for (JsonObject elm : ins) { - if (s >= WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES) break; + if (s >= WLED_MAX_BUSSES) break; // only counts physical buses uint8_t pins[OUTPUT_MAX_PINS] = {255, 255, 255, 255, 255}; JsonArray pinArr = elm["pin"]; if (pinArr.size() == 0) continue; @@ -220,26 +234,104 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { maMax = 0; } ledType |= refresh << 7; // hack bit 7 to indicate strip requires off refresh - if (fromFS) { - BusConfig bc = BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, useGlobalLedBuffer, maPerLed, maMax); - if (useParallel && s < 8) { - // if for some unexplained reason the above pre-calculation was wrong, update - unsigned memT = BusManager::memUsage(bc); // includes x8 memory allocation for parallel I2S - if (memT > mem) mem = memT; // if we have unequal LED count use the largest - } else - mem += BusManager::memUsage(bc); // includes global buffer - if (mem <= MAX_LED_MEMORY) if (BusManager::add(bc) == -1) break; // finalization will be done in WLED::beginStrip() - } else { - if (busConfigs[s] != nullptr) delete busConfigs[s]; - busConfigs[s] = new BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, useGlobalLedBuffer, maPerLed, maMax); - doInitBusses = true; // finalization done in beginStrip() - } - s++; + + busConfigs.emplace_back(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, maPerLed, maMax); + doInitBusses = true; // finalization done in beginStrip() + if (!Bus::isVirtual(ledType)) s++; // have as many virtual buses as you want } - DEBUG_PRINTF_P(PSTR("LED buffer size: %uB\n"), mem); - DEBUG_PRINTF_P(PSTR("Heap after buses: %d\n"), ESP.getFreeHeap()); + } else if (fromFS) { + //if busses failed to load, add default (fresh install, FS issue, ...) + BusManager::removeAll(); + busConfigs.clear(); + + DEBUG_PRINTLN(F("No busses, init default")); + constexpr unsigned defDataTypes[] = {LED_TYPES}; + constexpr unsigned defDataPins[] = {DATA_PINS}; + constexpr unsigned defCounts[] = {PIXEL_COUNTS}; + constexpr unsigned defNumTypes = (sizeof(defDataTypes) / sizeof(defDataTypes[0])); + constexpr unsigned defNumPins = (sizeof(defDataPins) / sizeof(defDataPins[0])); + constexpr unsigned defNumCounts = (sizeof(defCounts) / sizeof(defCounts[0])); + + static_assert(validatePinsAndTypes(defDataTypes, defNumTypes, defNumPins), + "The default pin list defined in DATA_PINS does not match the pin requirements for the default buses defined in LED_TYPES"); + + unsigned mem = 0; + unsigned pinsIndex = 0; + unsigned digitalCount = 0; + for (unsigned i = 0; i < WLED_MAX_BUSSES; i++) { + uint8_t defPin[OUTPUT_MAX_PINS]; + // if we have less types than requested outputs and they do not align, use last known type to set current type + unsigned dataType = defDataTypes[(i < defNumTypes) ? i : defNumTypes -1]; + unsigned busPins = Bus::getNumberOfPins(dataType); + + // if we need more pins than available all outputs have been configured + if (pinsIndex + busPins > defNumPins) break; + + // Assign all pins first so we can check for conflicts on this bus + for (unsigned j = 0; j < busPins && j < OUTPUT_MAX_PINS; j++) defPin[j] = defDataPins[pinsIndex + j]; + + for (unsigned j = 0; j < busPins && j < OUTPUT_MAX_PINS; j++) { + bool validPin = true; + // When booting without config (1st boot) we need to make sure GPIOs defined for LED output don't clash with hardware + // i.e. DEBUG (GPIO1), DMX (2), SPI RAM/FLASH (16&17 on ESP32-WROVER/PICO), read/only pins, etc. + // Pin should not be already allocated, read/only or defined for current bus + while (PinManager::isPinAllocated(defPin[j]) || !PinManager::isPinOk(defPin[j],true)) { + if (validPin) { + DEBUG_PRINTLN(F("Some of the provided pins cannot be used to configure this LED output.")); + defPin[j] = 1; // start with GPIO1 and work upwards + validPin = false; + } else if (defPin[j] < WLED_NUM_PINS) { + defPin[j]++; + } else { + DEBUG_PRINTLN(F("No available pins left! Can't configure output.")); + break; + } + // is the newly assigned pin already defined or used previously? + // try next in line until there are no clashes or we run out of pins + bool clash; + do { + clash = false; + // check for conflicts on current bus + for (const auto &pin : defPin) { + if (&pin != &defPin[j] && pin == defPin[j]) { + clash = true; + break; + } + } + // We already have a clash on current bus, no point checking next buses + if (!clash) { + // check for conflicts in defined pins + for (const auto &pin : defDataPins) { + if (pin == defPin[j]) { + clash = true; + break; + } + } + } + if (clash) defPin[j]++; + if (defPin[j] >= WLED_NUM_PINS) break; + } while (clash); + } + } + pinsIndex += busPins; + + // if we have less counts than pins and they do not align, use last known count to set current count + unsigned count = defCounts[(i < defNumCounts) ? i : defNumCounts -1]; + unsigned start = 0; + // analog always has length 1 + if (Bus::isPWM(dataType) || Bus::isOnOff(dataType)) count = 1; + BusConfig defCfg = BusConfig(dataType, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY, 0); + mem += defCfg.memUsage(Bus::isDigital(dataType) && !Bus::is2Pin(dataType) ? digitalCount++ : 0); + if (mem > MAX_LED_MEMORY) { + DEBUG_PRINTF_P(PSTR("Out of LED memory! Bus %d (%d) #%u not created."), (int)dataType, (int)count, digitalCount); + break; + } + busConfigs.push_back(defCfg); // use push_back for simplification as we needed defCfg to calculate memory usage + doInitBusses = true; // finalization done in beginStrip() + } + DEBUG_PRINTF_P(PSTR("LED buffer size: %uB/%uB\n"), mem, BusManager::memUsage()); } - if (hw_led["rev"]) BusManager::getBus(0)->setReversed(true); //set 0.11 global reversed setting for first bus + if (hw_led["rev"] && BusManager::getNumBusses()) BusManager::getBus(0)->setReversed(true); //set 0.11 global reversed setting for first bus // read color order map configuration JsonArray hw_com = hw[F("com")]; @@ -326,30 +418,28 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { macroLongPress[s] = 0; macroDoublePress[s] = 0; } - } else { + } else if (fromFS) { // new install/missing configuration (button 0 has defaults) - if (fromFS) { - // relies upon only being called once with fromFS == true, which is currently true. - for (size_t s = 0; s < WLED_MAX_BUTTONS; s++) { - if (buttonType[s] == BTN_TYPE_NONE || btnPin[s] < 0 || !PinManager::allocatePin(btnPin[s], false, PinOwner::Button)) { - btnPin[s] = -1; - buttonType[s] = BTN_TYPE_NONE; - } - if (btnPin[s] >= 0) { - if (disablePullUp) { - pinMode(btnPin[s], INPUT); - } else { - #ifdef ESP32 - pinMode(btnPin[s], buttonType[s]==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP); - #else - pinMode(btnPin[s], INPUT_PULLUP); - #endif - } - } - macroButton[s] = 0; - macroLongPress[s] = 0; - macroDoublePress[s] = 0; + // relies upon only being called once with fromFS == true, which is currently true. + for (size_t s = 0; s < WLED_MAX_BUTTONS; s++) { + if (buttonType[s] == BTN_TYPE_NONE || btnPin[s] < 0 || !PinManager::allocatePin(btnPin[s], false, PinOwner::Button)) { + btnPin[s] = -1; + buttonType[s] = BTN_TYPE_NONE; } + if (btnPin[s] >= 0) { + if (disablePullUp) { + pinMode(btnPin[s], INPUT); + } else { + #ifdef ESP32 + pinMode(btnPin[s], buttonType[s]==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP); + #else + pinMode(btnPin[s], INPUT_PULLUP); + #endif + } + } + macroButton[s] = 0; + macroLongPress[s] = 0; + macroDoublePress[s] = 0; } } @@ -426,31 +516,28 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonObject light = doc[F("light")]; CJSON(briMultiplier, light[F("scale-bri")]); - CJSON(strip.paletteBlend, light[F("pal-mode")]); + CJSON(paletteBlend, light[F("pal-mode")]); CJSON(strip.autoSegments, light[F("aseg")]); + CJSON(useRainbowWheel, light[F("rw")]); - CJSON(gammaCorrectVal, light["gc"]["val"]); // default 2.8 + CJSON(gammaCorrectVal, light["gc"]["val"]); // default 2.2 float light_gc_bri = light["gc"]["bri"]; float light_gc_col = light["gc"]["col"]; if (light_gc_bri > 1.0f) gammaCorrectBri = true; else gammaCorrectBri = false; if (light_gc_col > 1.0f) gammaCorrectCol = true; else gammaCorrectCol = false; - if (gammaCorrectVal > 1.0f && gammaCorrectVal <= 3) { - if (gammaCorrectVal != 2.8f) NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); - } else { + if (gammaCorrectVal <= 1.0f || gammaCorrectVal > 3) { gammaCorrectVal = 1.0f; // no gamma correction gammaCorrectBri = false; gammaCorrectCol = false; } + NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); // fill look-up tables JsonObject light_tr = light["tr"]; - CJSON(fadeTransition, light_tr["mode"]); - CJSON(modeBlending, light_tr["fx"]); int tdd = light_tr["dur"] | -1; if (tdd >= 0) transitionDelay = transitionDelayDefault = tdd * 100; - strip.setTransition(fadeTransition ? transitionDelayDefault : 0); - CJSON(strip.paletteFade, light_tr["pal"]); + strip.setTransition(transitionDelayDefault); CJSON(randomPaletteChangeTime, light_tr[F("rpc")]); CJSON(useHarmonicRandomPalette, light_tr[F("hrp")]); @@ -523,6 +610,14 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { tdd = if_live[F("timeout")] | -1; if (tdd >= 0) realtimeTimeoutMs = tdd * 100; + + #ifdef WLED_ENABLE_DMX_INPUT + CJSON(dmxInputTransmitPin, if_live_dmx[F("inputRxPin")]); + CJSON(dmxInputReceivePin, if_live_dmx[F("inputTxPin")]); + CJSON(dmxInputEnablePin, if_live_dmx[F("inputEnablePin")]); + CJSON(dmxInputPort, if_live_dmx[F("dmxInputPort")]); + #endif + CJSON(arlsForceMaxBri, if_live[F("maxbri")]); CJSON(arlsDisableGammaCorrection, if_live[F("no-gc")]); // false CJSON(arlsOffset, if_live[F("offset")]); // 0 @@ -641,7 +736,9 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { if (pwdCorrect) { //only accept these values from cfg.json if ota is unlocked (else from wsec.json) CJSON(otaLock, ota[F("lock")]); CJSON(wifiLock, ota[F("lock-wifi")]); + #ifndef WLED_DISABLE_OTA CJSON(aOtaEnabled, ota[F("aota")]); + #endif getStringFromJson(otaPass, pwd, 33); //normally not present due to security } @@ -678,11 +775,10 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { static const char s_cfg_json[] PROGMEM = "/cfg.json"; void deserializeConfigFromFS() { - bool success = deserializeConfigSec(); + [[maybe_unused]] bool success = deserializeConfigSec(); #ifdef WLED_ADD_EEPROM_SUPPORT if (!success) { //if file does not exist, try reading from EEPROM deEEPSettings(); - return; } #endif @@ -691,23 +787,6 @@ void deserializeConfigFromFS() { DEBUG_PRINTLN(F("Reading settings from /cfg.json...")); success = readObjectFromFile(s_cfg_json, nullptr, pDoc); - if (!success) { // if file does not exist, optionally try reading from EEPROM and then save defaults to FS - releaseJSONBufferLock(); - #ifdef WLED_ADD_EEPROM_SUPPORT - deEEPSettings(); - #endif - - // save default values to /cfg.json - // call readFromConfig() with an empty object so that usermods can initialize to defaults prior to saving - JsonObject empty = JsonObject(); - UsermodManager::readFromConfig(empty); - serializeConfig(); - // init Ethernet (in case default type is set at compile time) - #ifdef WLED_USE_ETHERNET - WLED::instance().initEthernet(); - #endif - return; - } // NOTE: This routine deserializes *and* applies the configuration // Therefore, must also initialize ethernet from this function @@ -715,10 +794,10 @@ void deserializeConfigFromFS() { bool needsSave = deserializeConfig(root, true); releaseJSONBufferLock(); - if (needsSave) serializeConfig(); // usermods required new parameters + if (needsSave) serializeConfigToFS(); // usermods required new parameters } -void serializeConfig() { +void serializeConfigToFS() { serializeConfigSec(); DEBUG_PRINTLN(F("Writing settings to /cfg.json...")); @@ -727,6 +806,17 @@ void serializeConfig() { JsonObject root = pDoc->to(); + serializeConfig(root); + + File f = WLED_FS.open(FPSTR(s_cfg_json), "w"); + if (f) serializeJson(root, f); + f.close(); + releaseJSONBufferLock(); + + configNeedsWrite = false; +} + +void serializeConfig(JsonObject root) { JsonArray rev = root.createNestedArray("rev"); rev.add(1); //major settings revision rev.add(0); //minor settings revision @@ -744,7 +834,10 @@ void serializeConfig() { JsonObject nw = root.createNestedObject("nw"); #ifndef WLED_DISABLE_ESPNOW nw[F("espnow")] = enableESPNow; - nw[F("linked_remote")] = linked_remote; + JsonArray lrem = nw.createNestedArray(F("linked_remote")); + for (size_t i = 0; i < linked_remotes.size(); i++) { + lrem.add(linked_remotes[i].data()); + } #endif JsonArray nw_ins = nw.createNestedArray("ins"); @@ -752,6 +845,9 @@ void serializeConfig() { JsonObject wifi = nw_ins.createNestedObject(); wifi[F("ssid")] = multiWiFi[n].clientSSID; wifi[F("pskl")] = strlen(multiWiFi[n].clientPass); + char bssid[13]; + fillMAC2Str(bssid, multiWiFi[n].bssid); + wifi[F("bssid")] = bssid; JsonArray wifi_ip = wifi.createNestedArray("ip"); JsonArray wifi_gw = wifi.createNestedArray("gw"); JsonArray wifi_sn = wifi.createNestedArray("sn"); @@ -787,7 +883,7 @@ void serializeConfig() { wifi[F("txpwr")] = txPower; #endif -#ifdef WLED_USE_ETHERNET +#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) JsonObject ethernet = root.createNestedObject("eth"); ethernet["type"] = ethernetType; if (ethernetType != WLED_ETH_NONE && ethernetType < WLED_NUM_ETH_TYPES) { @@ -816,20 +912,22 @@ void serializeConfig() { JsonObject hw_led = hw.createNestedObject("led"); hw_led[F("total")] = strip.getLengthTotal(); //provided for compatibility on downgrade and per-output ABL hw_led[F("maxpwr")] = BusManager::ablMilliampsMax(); - hw_led[F("ledma")] = 0; // no longer used +// hw_led[F("ledma")] = 0; // no longer used hw_led["cct"] = strip.correctWB; hw_led[F("cr")] = strip.cctFromRgb; hw_led[F("ic")] = cctICused; - hw_led[F("cb")] = strip.cctBlending; + hw_led[F("cb")] = Bus::getCCTBlend(); hw_led["fps"] = strip.getTargetFps(); hw_led[F("rgbwm")] = Bus::getGlobalAWMode(); // global auto white mode override - hw_led[F("ld")] = useGlobalLedBuffer; + #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) + hw_led[F("prl")] = BusManager::hasParallelOutput(); + #endif #ifndef WLED_DISABLE_2D // 2D Matrix Settings if (strip.isMatrix) { JsonObject matrix = hw_led.createNestedObject(F("matrix")); - matrix[F("mpc")] = strip.panels; + matrix[F("mpc")] = strip.panel.size(); JsonArray panels = matrix.createNestedArray(F("panels")); for (size_t i = 0; i < strip.panel.size(); i++) { JsonObject pnl = panels.createNestedObject(); @@ -848,32 +946,42 @@ void serializeConfig() { JsonArray hw_led_ins = hw_led.createNestedArray("ins"); for (size_t s = 0; s < BusManager::getNumBusses(); s++) { - Bus *bus = BusManager::getBus(s); - if (!bus || bus->getLength()==0) break; + DEBUG_PRINTF_P(PSTR("Cfg: Saving bus #%u\n"), s); + const Bus *bus = BusManager::getBus(s); + if (!bus || !bus->isOk()) break; + DEBUG_PRINTF_P(PSTR(" (%d-%d, type:%d, CO:%d, rev:%d, skip:%d, AW:%d kHz:%d, mA:%d/%d)\n"), + (int)bus->getStart(), (int)(bus->getStart()+bus->getLength()), + (int)(bus->getType() & 0x7F), + (int)bus->getColorOrder(), + (int)bus->isReversed(), + (int)bus->skippedLeds(), + (int)bus->getAutoWhiteMode(), + (int)bus->getFrequency(), + (int)bus->getLEDCurrent(), (int)bus->getMaxCurrent() + ); JsonObject ins = hw_led_ins.createNestedObject(); ins["start"] = bus->getStart(); - ins["len"] = bus->getLength(); + ins["len"] = bus->getLength(); JsonArray ins_pin = ins.createNestedArray("pin"); uint8_t pins[5]; uint8_t nPins = bus->getPins(pins); for (int i = 0; i < nPins; i++) ins_pin.add(pins[i]); - ins[F("order")] = bus->getColorOrder(); - ins["rev"] = bus->isReversed(); - ins[F("skip")] = bus->skippedLeds(); - ins["type"] = bus->getType() & 0x7F; - ins["ref"] = bus->isOffRefreshRequired(); - ins[F("rgbwm")] = bus->getAutoWhiteMode(); - ins[F("freq")] = bus->getFrequency(); + ins[F("order")] = bus->getColorOrder(); + ins["rev"] = bus->isReversed(); + ins[F("skip")] = bus->skippedLeds(); + ins["type"] = bus->getType() & 0x7F; + ins["ref"] = bus->isOffRefreshRequired(); + ins[F("rgbwm")] = bus->getAutoWhiteMode(); + ins[F("freq")] = bus->getFrequency(); ins[F("maxpwr")] = bus->getMaxCurrent(); - ins[F("ledma")] = bus->getLEDCurrent(); + ins[F("ledma")] = bus->getLEDCurrent(); } JsonArray hw_com = hw.createNestedArray(F("com")); const ColorOrderMap& com = BusManager::getColorOrderMap(); for (size_t s = 0; s < com.count(); s++) { const ColorOrderMapEntry *entry = com.get(s); - if (!entry) break; - + if (!entry || !entry->len) break; JsonObject co = hw_com.createNestedObject(); co["start"] = entry->start; co["len"] = entry->len; @@ -929,8 +1037,9 @@ void serializeConfig() { JsonObject light = root.createNestedObject(F("light")); light[F("scale-bri")] = briMultiplier; - light[F("pal-mode")] = strip.paletteBlend; + light[F("pal-mode")] = paletteBlend; light[F("aseg")] = strip.autoSegments; + light[F("rw")] = useRainbowWheel; JsonObject light_gc = light.createNestedObject("gc"); light_gc["bri"] = (gammaCorrectBri) ? gammaCorrectVal : 1.0f; // keep compatibility @@ -938,10 +1047,7 @@ void serializeConfig() { light_gc["val"] = gammaCorrectVal; JsonObject light_tr = light.createNestedObject("tr"); - light_tr["mode"] = fadeTransition; - light_tr["fx"] = modeBlending; light_tr["dur"] = transitionDelayDefault / 100; - light_tr["pal"] = strip.paletteFade; light_tr[F("rpc")] = randomPaletteChangeTime; light_tr[F("hrp")] = useHarmonicRandomPalette; @@ -1002,6 +1108,12 @@ void serializeConfig() { if_live_dmx[F("addr")] = DMXAddress; if_live_dmx[F("dss")] = DMXSegmentSpacing; if_live_dmx["mode"] = DMXMode; + #ifdef WLED_ENABLE_DMX_INPUT + if_live_dmx[F("inputRxPin")] = dmxInputTransmitPin; + if_live_dmx[F("inputTxPin")] = dmxInputReceivePin; + if_live_dmx[F("inputEnablePin")] = dmxInputEnablePin; + if_live_dmx[F("dmxInputPort")] = dmxInputPort; + #endif if_live[F("timeout")] = realtimeTimeoutMs / 100; if_live[F("maxbri")] = arlsForceMaxBri; @@ -1103,7 +1215,9 @@ void serializeConfig() { ota[F("lock")] = otaLock; ota[F("lock-wifi")] = wifiLock; ota[F("pskl")] = strlen(otaPass); + #ifndef WLED_DISABLE_OTA ota[F("aota")] = aOtaEnabled; + #endif #ifdef WLED_ENABLE_DMX JsonObject dmx = root.createNestedObject("dmx"); @@ -1122,13 +1236,6 @@ void serializeConfig() { JsonObject usermods_settings = root.createNestedObject("um"); UsermodManager::addToConfig(usermods_settings); - - File f = WLED_FS.open(FPSTR(s_cfg_json), "w"); - if (f) serializeJson(root, f); - f.close(); - releaseJSONBufferLock(); - - doSerializeConfig = false; } @@ -1181,7 +1288,9 @@ bool deserializeConfigSec() { getStringFromJson(otaPass, ota[F("pwd")], 33); CJSON(otaLock, ota[F("lock")]); CJSON(wifiLock, ota[F("lock-wifi")]); + #ifndef WLED_DISABLE_OTA CJSON(aOtaEnabled, ota[F("aota")]); + #endif releaseJSONBufferLock(); return true; @@ -1221,10 +1330,12 @@ void serializeConfigSec() { ota[F("pwd")] = otaPass; ota[F("lock")] = otaLock; ota[F("lock-wifi")] = wifiLock; + #ifndef WLED_DISABLE_OTA ota[F("aota")] = aOtaEnabled; + #endif File f = WLED_FS.open(FPSTR(s_wsec_json), "w"); if (f) serializeJson(root, f); f.close(); releaseJSONBufferLock(); -} +} \ No newline at end of file diff --git a/wled00/colors.cpp b/wled00/colors.cpp index 478a0a27..ea5c9779 100644 --- a/wled00/colors.cpp +++ b/wled00/colors.cpp @@ -5,61 +5,58 @@ */ /* - * color blend function + * color blend function, based on FastLED blend function + * the calculation for each color is: result = (A*(amountOfA) + A + B*(amountOfB) + B) / 256 with amountOfA = 255 - amountOfB */ -uint32_t color_blend(uint32_t color1, uint32_t color2, uint16_t blend, bool b16) { - if (blend == 0) return color1; - unsigned blendmax = b16 ? 0xFFFF : 0xFF; - if (blend == blendmax) return color2; - unsigned shift = b16 ? 16 : 8; - - uint32_t w1 = W(color1); - uint32_t r1 = R(color1); - uint32_t g1 = G(color1); - uint32_t b1 = B(color1); - - uint32_t w2 = W(color2); - uint32_t r2 = R(color2); - uint32_t g2 = G(color2); - uint32_t b2 = B(color2); - - uint32_t w3 = ((w2 * blend) + (w1 * (blendmax - blend))) >> shift; - uint32_t r3 = ((r2 * blend) + (r1 * (blendmax - blend))) >> shift; - uint32_t g3 = ((g2 * blend) + (g1 * (blendmax - blend))) >> shift; - uint32_t b3 = ((b2 * blend) + (b1 * (blendmax - blend))) >> shift; - - return RGBW32(r3, g3, b3, w3); +uint32_t color_blend(uint32_t color1, uint32_t color2, uint8_t blend) { + // min / max blend checking is omitted: calls with 0 or 255 are rare, checking lowers overall performance + const uint32_t TWO_CHANNEL_MASK = 0x00FF00FF; // mask for R and B channels or W and G if negated (poorman's SIMD; https://github.com/wled/WLED/pull/4568#discussion_r1986587221) + uint32_t rb1 = color1 & TWO_CHANNEL_MASK; // extract R & B channels from color1 + uint32_t wg1 = (color1 >> 8) & TWO_CHANNEL_MASK; // extract W & G channels from color1 (shifted for multiplication later) + uint32_t rb2 = color2 & TWO_CHANNEL_MASK; // extract R & B channels from color2 + uint32_t wg2 = (color2 >> 8) & TWO_CHANNEL_MASK; // extract W & G channels from color2 (shifted for multiplication later) + uint32_t rb3 = ((((rb1 << 8) | rb2) + (rb2 * blend) - (rb1 * blend)) >> 8) & TWO_CHANNEL_MASK; // blend red and blue + uint32_t wg3 = ((((wg1 << 8) | wg2) + (wg2 * blend) - (wg1 * blend))) & ~TWO_CHANNEL_MASK; // negated mask for white and green + return rb3 | wg3; } /* * color add function that preserves ratio - * idea: https://github.com/Aircoookie/WLED/pull/2465 by https://github.com/Proto-molecule + * original idea: https://github.com/wled-dev/WLED/pull/2465 by https://github.com/Proto-molecule + * speed optimisations by @dedehai */ -uint32_t color_add(uint32_t c1, uint32_t c2, bool fast) +uint32_t color_add(uint32_t c1, uint32_t c2, bool preserveCR) { if (c1 == BLACK) return c2; if (c2 == BLACK) return c1; - if (fast) { - uint8_t r = R(c1); - uint8_t g = G(c1); - uint8_t b = B(c1); - uint8_t w = W(c1); - r = qadd8(r, R(c2)); - g = qadd8(g, G(c2)); - b = qadd8(b, B(c2)); - w = qadd8(w, W(c2)); - return RGBW32(r,g,b,w); + const uint32_t TWO_CHANNEL_MASK = 0x00FF00FF; // mask for R and B channels or W and G if negated + uint32_t rb = ( c1 & TWO_CHANNEL_MASK) + ( c2 & TWO_CHANNEL_MASK); // mask and add two colors at once + uint32_t wg = ((c1>>8) & TWO_CHANNEL_MASK) + ((c2>>8) & TWO_CHANNEL_MASK); + uint32_t r = rb >> 16; // extract single color values + uint32_t b = rb & 0xFFFF; + uint32_t w = wg >> 16; + uint32_t g = wg & 0xFFFF; + + if (preserveCR) { // preserve color ratios + uint32_t max = std::max(r,g); // check for overflow note + max = std::max(max,b); + max = std::max(max,w); + //unsigned max = r; // check for overflow note + //max = g > max ? g : max; + //max = b > max ? b : max; + //max = w > max ? w : max; + if (max > 255) { + const uint32_t scale = (uint32_t(255)<<8) / max; // division of two 8bit (shifted) values does not work -> use bit shifts and multiplaction instead + rb = ((rb * scale) >> 8) & TWO_CHANNEL_MASK; + wg = (wg * scale) & ~TWO_CHANNEL_MASK; + } else wg <<= 8; //shift white and green back to correct position + return rb | wg; } else { - uint32_t r = R(c1) + R(c2); - uint32_t g = G(c1) + G(c2); - uint32_t b = B(c1) + B(c2); - uint32_t w = W(c1) + W(c2); - unsigned max = r; - if (g > max) max = g; - if (b > max) max = b; - if (w > max) max = w; - if (max < 256) return RGBW32(r, g, b, w); - else return RGBW32(r * 255 / max, g * 255 / max, b * 255 / max, w * 255 / max); + r = r > 255 ? 255 : r; + g = g > 255 ? 255 : g; + b = b > 255 ? 255 : b; + w = w > 255 ? 255 : w; + return RGBW32(r,g,b,w); } } @@ -70,27 +67,56 @@ uint32_t color_add(uint32_t c1, uint32_t c2, bool fast) uint32_t color_fade(uint32_t c1, uint8_t amount, bool video) { - if (c1 == BLACK || amount + video == 0) return BLACK; + if (amount == 255) return c1; + if (c1 == BLACK || amount == 0) return BLACK; uint32_t scaledcolor; // color order is: W R G B from MSB to LSB - uint32_t r = R(c1); - uint32_t g = G(c1); - uint32_t b = B(c1); - uint32_t w = W(c1); uint32_t scale = amount; // 32bit for faster calculation - if (video) { - scaledcolor = (((r * scale) >> 8) + ((r && scale) ? 1 : 0)) << 16; - scaledcolor |= (((g * scale) >> 8) + ((g && scale) ? 1 : 0)) << 8; - scaledcolor |= ((b * scale) >> 8) + ((b && scale) ? 1 : 0); - scaledcolor |= (((w * scale) >> 8) + ((w && scale) ? 1 : 0)) << 24; - } else { - scaledcolor = ((r * scale) >> 8) << 16; - scaledcolor |= ((g * scale) >> 8) << 8; - scaledcolor |= (b * scale) >> 8; - scaledcolor |= ((w * scale) >> 8) << 24; + uint32_t addRemains = 0; + if (!video) scale++; // add one for correct scaling using bitshifts + else { // video scaling: make sure colors do not dim to zero if they started non-zero + addRemains = R(c1) ? 0x00010000 : 0; + addRemains |= G(c1) ? 0x00000100 : 0; + addRemains |= B(c1) ? 0x00000001 : 0; + addRemains |= W(c1) ? 0x01000000 : 0; } + const uint32_t TWO_CHANNEL_MASK = 0x00FF00FF; + uint32_t rb = (((c1 & TWO_CHANNEL_MASK) * scale) >> 8) & TWO_CHANNEL_MASK; // scale red and blue + uint32_t wg = (((c1 >> 8) & TWO_CHANNEL_MASK) * scale) & ~TWO_CHANNEL_MASK; // scale white and green + scaledcolor = (rb | wg) + addRemains; return scaledcolor; } +// 1:1 replacement of fastled function optimized for ESP, slightly faster, more accurate and uses less flash (~ -200bytes) +uint32_t ColorFromPaletteWLED(const CRGBPalette16& pal, unsigned index, uint8_t brightness, TBlendType blendType) +{ + if (blendType == LINEARBLEND_NOWRAP) { + index = (index * 0xF0) >> 8; // Blend range is affected by lo4 blend of values, remap to avoid wrapping + } + unsigned hi4 = byte(index) >> 4; + unsigned lo4 = (index & 0x0F); + const CRGB* entry = (CRGB*)&(pal[0]) + hi4; + unsigned red1 = entry->r; + unsigned green1 = entry->g; + unsigned blue1 = entry->b; + if (lo4 && blendType != NOBLEND) { + if (hi4 == 15) entry = &(pal[0]); + else ++entry; + unsigned f2 = (lo4 << 4); + unsigned f1 = 256 - f2; + red1 = (red1 * f1 + (unsigned)entry->r * f2) >> 8; // note: using color_blend() is 20% slower + green1 = (green1 * f1 + (unsigned)entry->g * f2) >> 8; + blue1 = (blue1 * f1 + (unsigned)entry->b * f2) >> 8; + } + if (brightness < 255) { // note: zero checking could be done to return black but that is hardly ever used so it is omitted + // actually color_fade(c1, brightness) + uint32_t scale = brightness + 1; // adjust for rounding (bitshift) + red1 = (red1 * scale) >> 8; // note: using color_fade() is 30% slower + green1 = (green1 * scale) >> 8; + blue1 = (blue1 * scale) >> 8; + } + return RGBW32(red1,green1,blue1,0); +} + void setRandomColor(byte* rgb) { lastRandomIndex = get_random_wheel_index(lastRandomIndex); @@ -101,95 +127,95 @@ void setRandomColor(byte* rgb) * generates a random palette based on harmonic color theory * takes a base palette as the input, it will choose one color of the base palette and keep it */ -CRGBPalette16 generateHarmonicRandomPalette(CRGBPalette16 &basepalette) +CRGBPalette16 generateHarmonicRandomPalette(const CRGBPalette16 &basepalette) { - CHSV palettecolors[4]; //array of colors for the new palette - uint8_t keepcolorposition = random8(4); //color position of current random palette to keep - palettecolors[keepcolorposition] = rgb2hsv_approximate(basepalette.entries[keepcolorposition*5]); //read one of the base colors of the current palette - palettecolors[keepcolorposition].hue += random8(10)-5; // +/- 5 randomness of base color - //generate 4 saturation and brightness value numbers - //only one saturation is allowed to be below 200 creating mostly vibrant colors - //only one brightness value number is allowed below 200, creating mostly bright palettes + CHSV palettecolors[4]; // array of colors for the new palette + uint8_t keepcolorposition = hw_random8(4); // color position of current random palette to keep + palettecolors[keepcolorposition] = rgb2hsv(basepalette.entries[keepcolorposition*5]); // read one of the base colors of the current palette + palettecolors[keepcolorposition].hue += hw_random8(10)-5; // +/- 5 randomness of base color + // generate 4 saturation and brightness value numbers + // only one saturation is allowed to be below 200 creating mostly vibrant colors + // only one brightness value number is allowed below 200, creating mostly bright palettes - for (int i = 0; i < 3; i++) { //generate three high values - palettecolors[i].saturation = random8(200,255); - palettecolors[i].value = random8(220,255); + for (int i = 0; i < 3; i++) { // generate three high values + palettecolors[i].saturation = hw_random8(200,255); + palettecolors[i].value = hw_random8(220,255); } - //allow one to be lower - palettecolors[3].saturation = random8(20,255); - palettecolors[3].value = random8(80,255); + // allow one to be lower + palettecolors[3].saturation = hw_random8(20,255); + palettecolors[3].value = hw_random8(80,255); - //shuffle the arrays + // shuffle the arrays for (int i = 3; i > 0; i--) { - std::swap(palettecolors[i].saturation, palettecolors[random8(i + 1)].saturation); - std::swap(palettecolors[i].value, palettecolors[random8(i + 1)].value); + std::swap(palettecolors[i].saturation, palettecolors[hw_random8(i + 1)].saturation); + std::swap(palettecolors[i].value, palettecolors[hw_random8(i + 1)].value); } - //now generate three new hues based off of the hue of the chosen current color + // now generate three new hues based off of the hue of the chosen current color uint8_t basehue = palettecolors[keepcolorposition].hue; - uint8_t harmonics[3]; //hues that are harmonic but still a little random - uint8_t type = random8(5); //choose a harmony type + uint8_t harmonics[3]; // hues that are harmonic but still a little random + uint8_t type = hw_random8(5); // choose a harmony type switch (type) { case 0: // analogous - harmonics[0] = basehue + random8(30, 50); - harmonics[1] = basehue + random8(10, 30); - harmonics[2] = basehue - random8(10, 30); + harmonics[0] = basehue + hw_random8(30, 50); + harmonics[1] = basehue + hw_random8(10, 30); + harmonics[2] = basehue - hw_random8(10, 30); break; case 1: // triadic - harmonics[0] = basehue + 113 + random8(15); - harmonics[1] = basehue + 233 + random8(15); - harmonics[2] = basehue - 7 + random8(15); + harmonics[0] = basehue + 113 + hw_random8(15); + harmonics[1] = basehue + 233 + hw_random8(15); + harmonics[2] = basehue - 7 + hw_random8(15); break; case 2: // split-complementary - harmonics[0] = basehue + 145 + random8(10); - harmonics[1] = basehue + 205 + random8(10); - harmonics[2] = basehue - 5 + random8(10); + harmonics[0] = basehue + 145 + hw_random8(10); + harmonics[1] = basehue + 205 + hw_random8(10); + harmonics[2] = basehue - 5 + hw_random8(10); break; - + case 3: // square - harmonics[0] = basehue + 85 + random8(10); - harmonics[1] = basehue + 175 + random8(10); - harmonics[2] = basehue + 265 + random8(10); + harmonics[0] = basehue + 85 + hw_random8(10); + harmonics[1] = basehue + 175 + hw_random8(10); + harmonics[2] = basehue + 265 + hw_random8(10); break; case 4: // tetradic - harmonics[0] = basehue + 80 + random8(20); - harmonics[1] = basehue + 170 + random8(20); - harmonics[2] = basehue - 15 + random8(30); + harmonics[0] = basehue + 80 + hw_random8(20); + harmonics[1] = basehue + 170 + hw_random8(20); + harmonics[2] = basehue - 15 + hw_random8(30); break; } - if (random8() < 128) { - //50:50 chance of shuffling hues or keep the color order + if (hw_random8() < 128) { + // 50:50 chance of shuffling hues or keep the color order for (int i = 2; i > 0; i--) { - std::swap(harmonics[i], harmonics[random8(i + 1)]); + std::swap(harmonics[i], harmonics[hw_random8(i + 1)]); } } - //now set the hues + // now set the hues int j = 0; for (int i = 0; i < 4; i++) { - if (i==keepcolorposition) continue; //skip the base color + if (i==keepcolorposition) continue; // skip the base color palettecolors[i].hue = harmonics[j]; j++; } bool makepastelpalette = false; - if (random8() < 25) { //~10% chance of desaturated 'pastel' colors + if (hw_random8() < 25) { // ~10% chance of desaturated 'pastel' colors makepastelpalette = true; } - //apply saturation & gamma correction + // apply saturation CRGB RGBpalettecolors[4]; for (int i = 0; i < 4; i++) { - if (makepastelpalette && palettecolors[i].saturation > 180) { + if (makepastelpalette && palettecolors[i].saturation > 180) { palettecolors[i].saturation -= 160; //desaturate all four colors - } + } RGBpalettecolors[i] = (CRGB)palettecolors[i]; //convert to RGB - RGBpalettecolors[i] = gamma32(((uint32_t)RGBpalettecolors[i]) & 0x00FFFFFFU); //strip alpha from CRGB + RGBpalettecolors[i] = ((uint32_t)RGBpalettecolors[i]) & 0x00FFFFFFU; //strip alpha from CRGB } return CRGBPalette16(RGBpalettecolors[0], @@ -198,36 +224,122 @@ CRGBPalette16 generateHarmonicRandomPalette(CRGBPalette16 &basepalette) RGBpalettecolors[3]); } -CRGBPalette16 generateRandomPalette() //generate fully random palette +CRGBPalette16 generateRandomPalette() // generate fully random palette { - return CRGBPalette16(CHSV(random8(), random8(160, 255), random8(128, 255)), - CHSV(random8(), random8(160, 255), random8(128, 255)), - CHSV(random8(), random8(160, 255), random8(128, 255)), - CHSV(random8(), random8(160, 255), random8(128, 255))); + return CRGBPalette16(CHSV(hw_random8(), hw_random8(160, 255), hw_random8(128, 255)), + CHSV(hw_random8(), hw_random8(160, 255), hw_random8(128, 255)), + CHSV(hw_random8(), hw_random8(160, 255), hw_random8(128, 255)), + CHSV(hw_random8(), hw_random8(160, 255), hw_random8(128, 255))); } -void colorHStoRGB(uint16_t hue, byte sat, byte* rgb) //hue, sat to rgb -{ - float h = ((float)hue)/10922.5f; // hue*6/65535 - float s = ((float)sat)/255.0f; - int i = int(h); - float f = h - i; - int p = int(255.0f * (1.0f-s)); - int q = int(255.0f * (1.0f-s*f)); - int t = int(255.0f * (1.0f-s*(1.0f-f))); - p = constrain(p, 0, 255); - q = constrain(q, 0, 255); - t = constrain(t, 0, 255); - switch (i%6) { - case 0: rgb[0]=255,rgb[1]=t, rgb[2]=p; break; - case 1: rgb[0]=q, rgb[1]=255,rgb[2]=p; break; - case 2: rgb[0]=p, rgb[1]=255,rgb[2]=t; break; - case 3: rgb[0]=p, rgb[1]=q, rgb[2]=255;break; - case 4: rgb[0]=t, rgb[1]=p, rgb[2]=255;break; - case 5: rgb[0]=255,rgb[1]=p, rgb[2]=q; break; +void loadCustomPalettes() { + byte tcp[72]; //support gradient palettes with up to 18 entries + CRGBPalette16 targetPalette; + customPalettes.clear(); // start fresh + for (int index = 0; index<10; index++) { + char fileName[32]; + sprintf_P(fileName, PSTR("/palette%d.json"), index); + + StaticJsonDocument<1536> pDoc; // barely enough to fit 72 numbers + if (WLED_FS.exists(fileName)) { + DEBUGFX_PRINTF_P(PSTR("Reading palette from %s\n"), fileName); + if (readObjectFromFile(fileName, nullptr, &pDoc)) { + JsonArray pal = pDoc[F("palette")]; + if (!pal.isNull() && pal.size()>3) { // not an empty palette (at least 2 entries) + memset(tcp, 255, sizeof(tcp)); + if (pal[0].is() && pal[1].is()) { + // we have an array of index & hex strings + size_t palSize = MIN(pal.size(), 36); + palSize -= palSize % 2; // make sure size is multiple of 2 + for (size_t i=0, j=0; i()<256; i+=2) { + uint8_t rgbw[] = {0,0,0,0}; + if (colorFromHexString(rgbw, pal[i+1].as())) { // will catch non-string entires + tcp[ j ] = (uint8_t) pal[ i ].as(); // index + for (size_t c=0; c<3; c++) tcp[j+1+c] = rgbw[c]; // only use RGB component + DEBUGFX_PRINTF_P(PSTR("%2u -> %3d [%3d,%3d,%3d]\n"), i, int(tcp[j]), int(tcp[j+1]), int(tcp[j+2]), int(tcp[j+3])); + j += 4; + } + } + } else { + size_t palSize = MIN(pal.size(), 72); + palSize -= palSize % 4; // make sure size is multiple of 4 + for (size_t i=0; i()<256; i+=4) { + tcp[ i ] = (uint8_t) pal[ i ].as(); // index + for (size_t c=0; c<3; c++) tcp[i+1+c] = (uint8_t) pal[i+1+c].as(); + DEBUGFX_PRINTF_P(PSTR("%2u -> %3d [%3d,%3d,%3d]\n"), i, int(tcp[i]), int(tcp[i+1]), int(tcp[i+2]), int(tcp[i+3])); + } + } + customPalettes.push_back(targetPalette.loadDynamicGradientPalette(tcp)); + } else { + DEBUGFX_PRINTLN(F("Wrong palette format.")); + } + } + } else { + break; + } } } +void hsv2rgb(const CHSV32& hsv, uint32_t& rgb) // convert HSV (16bit hue) to RGB (32bit with white = 0) +{ + unsigned int remainder, region, p, q, t; + unsigned int h = hsv.h; + unsigned int s = hsv.s; + unsigned int v = hsv.v; + if (s == 0) { + rgb = v << 16 | v << 8 | v; + return; + } + region = h / 10923; // 65536 / 6 = 10923 + remainder = (h - (region * 10923)) * 6; + p = (v * (255 - s)) >> 8; + q = (v * (255 - ((s * remainder) >> 16))) >> 8; + t = (v * (255 - ((s * (65535 - remainder)) >> 16))) >> 8; + switch (region) { + case 0: + rgb = v << 16 | t << 8 | p; break; + case 1: + rgb = q << 16 | v << 8 | p; break; + case 2: + rgb = p << 16 | v << 8 | t; break; + case 3: + rgb = p << 16 | q << 8 | v; break; + case 4: + rgb = t << 16 | p << 8 | v; break; + default: + rgb = v << 16 | p << 8 | q; break; + } +} + +void rgb2hsv(const uint32_t rgb, CHSV32& hsv) // convert RGB to HSV (16bit hue), much more accurate and faster than fastled version +{ + hsv.raw = 0; + int32_t r = (rgb>>16)&0xFF; + int32_t g = (rgb>>8)&0xFF; + int32_t b = rgb&0xFF; + int32_t minval, maxval, delta; + minval = min(r, g); + minval = min(minval, b); + maxval = max(r, g); + maxval = max(maxval, b); + if (maxval == 0) return; // black + hsv.v = maxval; + delta = maxval - minval; + hsv.s = (255 * delta) / maxval; + if (hsv.s == 0) return; // gray value + if (maxval == r) hsv.h = (10923 * (g - b)) / delta; + else if (maxval == g) hsv.h = 21845 + (10923 * (b - r)) / delta; + else hsv.h = 43690 + (10923 * (r - g)) / delta; +} + +void colorHStoRGB(uint16_t hue, byte sat, byte* rgb) { //hue, sat to rgb + uint32_t crgb; + hsv2rgb(CHSV32(hue, sat, 255), crgb); + rgb[0] = byte((crgb) >> 16); + rgb[1] = byte((crgb) >> 8); + rgb[2] = byte(crgb); +} + //get RGB values from color temperature in K (https://tannerhelland.com/2012/09/18/convert-temperature-rgb-algorithm-code.html) void colorKtoRGB(uint16_t kelvin, byte* rgb) //white spectrum to rgb, calc { @@ -332,7 +444,7 @@ void colorXYtoRGB(float x, float y, byte* rgb) //coordinates to rgb (https://www rgb[2] = byte(255.0f*b); } -void colorRGBtoXY(byte* rgb, float* xy) //rgb to coordinates (https://www.developers.meethue.com/documentation/color-conversions-rgb-xy) +void colorRGBtoXY(const byte* rgb, float* xy) //rgb to coordinates (https://www.developers.meethue.com/documentation/color-conversions-rgb-xy) { float X = rgb[0] * 0.664511f + rgb[1] * 0.154324f + rgb[2] * 0.162028f; float Y = rgb[0] * 0.283881f + rgb[1] * 0.668433f + rgb[2] * 0.047685f; @@ -343,7 +455,7 @@ void colorRGBtoXY(byte* rgb, float* xy) //rgb to coordinates (https://www.develo #endif // WLED_DISABLE_HUESYNC //RRGGBB / WWRRGGBB order for hex -void colorFromDecOrHexString(byte* rgb, char* in) +void colorFromDecOrHexString(byte* rgb, const char* in) { if (in[0] == 0) return; char first = in[0]; @@ -452,30 +564,17 @@ uint16_t approximateKelvinFromRGB(uint32_t rgb) { } } -//gamma 2.8 lookup table used for color correction -uint8_t NeoGammaWLEDMethod::gammaT[256] = { - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, - 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, - 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, - 10, 10, 11, 11, 11, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16, - 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 24, 24, 25, - 25, 26, 27, 27, 28, 29, 29, 30, 31, 32, 32, 33, 34, 35, 35, 36, - 37, 38, 39, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 50, - 51, 52, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 66, 67, 68, - 69, 70, 72, 73, 74, 75, 77, 78, 79, 81, 82, 83, 85, 86, 87, 89, - 90, 92, 93, 95, 96, 98, 99,101,102,104,105,107,109,110,112,114, - 115,117,119,120,122,124,126,127,129,131,133,135,137,138,140,142, - 144,146,148,150,152,154,156,158,160,162,164,167,169,171,173,175, - 177,180,182,184,186,189,191,193,196,198,200,203,205,208,210,213, - 215,218,220,223,225,228,231,233,236,239,241,244,247,249,252,255 }; +// gamma lookup tables used for color correction (filled on 1st use (cfg.cpp & set.cpp)) +uint8_t NeoGammaWLEDMethod::gammaT[256]; +uint8_t NeoGammaWLEDMethod::gammaT_inv[256]; -// re-calculates & fills gamma table +// re-calculates & fills gamma tables void NeoGammaWLEDMethod::calcGammaTable(float gamma) { + float gamma_inv = 1.0f / gamma; // inverse gamma for (size_t i = 0; i < 256; i++) { gammaT[i] = (int)(powf((float)i / 255.0f, gamma) * 255.0f + 0.5f); + gammaT_inv[i] = (int)(powf((float)i / 255.0f, gamma_inv) * 255.0f + 0.5f); } } @@ -499,3 +598,17 @@ uint32_t IRAM_ATTR_YN NeoGammaWLEDMethod::Correct32(uint32_t color) b = gammaT[b]; return RGBW32(r, g, b, w); } + +uint32_t IRAM_ATTR_YN NeoGammaWLEDMethod::inverseGamma32(uint32_t color) +{ + if (!gammaCorrectCol) return color; + uint8_t w = W(color); + uint8_t r = R(color); + uint8_t g = G(color); + uint8_t b = B(color); + w = gammaT_inv[w]; + r = gammaT_inv[r]; + g = gammaT_inv[g]; + b = gammaT_inv[b]; + return RGBW32(r, g, b, w); +} diff --git a/wled00/const.h b/wled00/const.h index bfd06aac..cca6d463 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -1,3 +1,4 @@ +#pragma once #ifndef WLED_CONST_H #define WLED_CONST_H @@ -5,7 +6,7 @@ * Readability defines and their associated numerical values + compile-time constants */ -#define GRADIENT_PALETTE_COUNT 58 +#define GRADIENT_PALETTE_COUNT 59 // You can define custom product info from build flags. // This is useful to allow API consumer to identify what type of WLED version @@ -37,69 +38,58 @@ #endif #ifndef WLED_MAX_USERMODS - #ifdef ESP8266 + #if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32S2) #define WLED_MAX_USERMODS 4 #else #define WLED_MAX_USERMODS 6 #endif #endif -#ifndef WLED_MAX_BUSSES - #ifdef ESP8266 - #define WLED_MAX_DIGITAL_CHANNELS 3 - #define WLED_MAX_ANALOG_CHANNELS 5 - #define WLED_MAX_BUSSES 4 // will allow 3 digital & 1 analog RGB - #define WLED_MIN_VIRTUAL_BUSSES 2 - #else - #define WLED_MAX_ANALOG_CHANNELS (LEDC_CHANNEL_MAX*LEDC_SPEED_MODE_MAX) - #if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, 6 LEDC, only has 1 I2S but NPB does not support it ATM - #define WLED_MAX_BUSSES 6 // will allow 2 digital & 2 analog RGB or 6 PWM white - #define WLED_MAX_DIGITAL_CHANNELS 2 - //#define WLED_MAX_ANALOG_CHANNELS 6 - #define WLED_MIN_VIRTUAL_BUSSES 3 - #elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, 8 LEDC, only has 1 I2S bus, supported in NPB - // the 5th bus (I2S) will prevent Audioreactive usermod from functioning (it is last used though) - #define WLED_MAX_BUSSES 7 // will allow 5 digital & 2 analog RGB - #define WLED_MAX_DIGITAL_CHANNELS 5 - //#define WLED_MAX_ANALOG_CHANNELS 8 - #define WLED_MIN_VIRTUAL_BUSSES 3 - #elif defined(CONFIG_IDF_TARGET_ESP32S3) // 4 RMT, 8 LEDC, has 2 I2S but NPB does not support them ATM - #define WLED_MAX_BUSSES 6 // will allow 4 digital & 2 analog RGB - #define WLED_MAX_DIGITAL_CHANNELS 4 - //#define WLED_MAX_ANALOG_CHANNELS 8 - #define WLED_MIN_VIRTUAL_BUSSES 4 - #else - // the last digital bus (I2S0) will prevent Audioreactive usermod from functioning - #define WLED_MAX_BUSSES 20 // will allow 17 digital & 3 analog RGB - #define WLED_MAX_DIGITAL_CHANNELS 17 - //#define WLED_MAX_ANALOG_CHANNELS 16 - #define WLED_MIN_VIRTUAL_BUSSES 4 - #endif - #endif +#ifdef ESP8266 + #define WLED_MAX_DIGITAL_CHANNELS 3 + #define WLED_MAX_ANALOG_CHANNELS 5 + #define WLED_MIN_VIRTUAL_BUSSES 3 // no longer used for bus creation but used to distinguish S2/S3 in UI #else - #ifdef ESP8266 - #if WLED_MAX_BUSSES > 5 - #error Maximum number of buses is 5. - #endif - #ifndef WLED_MAX_ANALOG_CHANNELS - #error You must also define WLED_MAX_ANALOG_CHANNELS. - #endif - #ifndef WLED_MAX_DIGITAL_CHANNELS - #error You must also define WLED_MAX_DIGITAL_CHANNELS. - #endif - #define WLED_MIN_VIRTUAL_BUSSES (5-WLED_MAX_BUSSES) - #else - #if WLED_MAX_BUSSES > 20 - #error Maximum number of buses is 20. - #endif - #ifndef WLED_MAX_ANALOG_CHANNELS - #error You must also define WLED_MAX_ANALOG_CHANNELS. - #endif - #ifndef WLED_MAX_DIGITAL_CHANNELS - #error You must also define WLED_MAX_DIGITAL_CHANNELS. - #endif - #define WLED_MIN_VIRTUAL_BUSSES (20-WLED_MAX_BUSSES) + #if !defined(LEDC_CHANNEL_MAX) || !defined(LEDC_SPEED_MODE_MAX) + #include "driver/ledc.h" // needed for analog/LEDC channel counts #endif + #define WLED_MAX_ANALOG_CHANNELS (LEDC_CHANNEL_MAX*LEDC_SPEED_MODE_MAX) + #if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, 6 LEDC, only has 1 I2S but NPB does not support it ATM + #define WLED_MAX_DIGITAL_CHANNELS 2 + //#define WLED_MAX_ANALOG_CHANNELS 6 + #define WLED_MIN_VIRTUAL_BUSSES 4 // no longer used for bus creation but used to distinguish S2/S3 in UI + #elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, 8 LEDC, only has 1 I2S bus, supported in NPB + // the 5th bus (I2S) will prevent Audioreactive usermod from functioning (it is last used though) + #define WLED_MAX_DIGITAL_CHANNELS 12 // x4 RMT + x1/x8 I2S0 + //#define WLED_MAX_ANALOG_CHANNELS 8 + #define WLED_MIN_VIRTUAL_BUSSES 4 // no longer used for bus creation but used to distinguish S2/S3 in UI + #elif defined(CONFIG_IDF_TARGET_ESP32S3) // 4 RMT, 8 LEDC, has 2 I2S but NPB supports parallel x8 LCD on I2S1 + #define WLED_MAX_DIGITAL_CHANNELS 12 // x4 RMT + x8 I2S-LCD + //#define WLED_MAX_ANALOG_CHANNELS 8 + #define WLED_MIN_VIRTUAL_BUSSES 6 // no longer used for bus creation but used to distinguish S2/S3 in UI + #else + // the last digital bus (I2S0) will prevent Audioreactive usermod from functioning + #define WLED_MAX_DIGITAL_CHANNELS 16 // x1/x8 I2S1 + x8 RMT + //#define WLED_MAX_ANALOG_CHANNELS 16 + #define WLED_MIN_VIRTUAL_BUSSES 6 // no longer used for bus creation but used to distinguish S2/S3 in UI + #endif +#endif +// WLED_MAX_BUSSES was used to define the size of busses[] array which is no longer needed +// instead it will help determine max number of buses that can be defined at compile time +#ifdef WLED_MAX_BUSSES + #undef WLED_MAX_BUSSES +#endif +#define WLED_MAX_BUSSES (WLED_MAX_DIGITAL_CHANNELS+WLED_MAX_ANALOG_CHANNELS) +static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); + +// Maximum number of pins per output. 5 for RGBCCT analog LEDs. +#define OUTPUT_MAX_PINS 5 + +// for pin manager +#ifdef ESP8266 +#define WLED_NUM_PINS (GPIO_PIN_COUNT+1) // somehow they forgot GPIO 16 (0-16==17) +#else +#define WLED_NUM_PINS (GPIO_PIN_COUNT) #endif #ifndef WLED_MAX_BUTTONS @@ -115,7 +105,7 @@ #endif #endif -#ifdef ESP8266 +#if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32S2) #define WLED_MAX_COLOR_ORDER_MAPPINGS 5 #else #define WLED_MAX_COLOR_ORDER_MAPPINGS 10 @@ -125,7 +115,7 @@ #undef WLED_MAX_LEDMAPS #endif #ifndef WLED_MAX_LEDMAPS - #ifdef ESP8266 + #if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32S2) #define WLED_MAX_LEDMAPS 10 #else #define WLED_MAX_LEDMAPS 16 @@ -147,6 +137,8 @@ #endif #endif +#define WLED_MAX_PANELS 18 // must not be more than 32 + //Usermod IDs #define USERMOD_ID_RESERVED 0 //Unused. Might indicate no usermod present #define USERMOD_ID_UNSPECIFIED 1 //Default value for a general user mod that does not specify a custom ID @@ -203,6 +195,10 @@ #define USERMOD_ID_LD2410 52 //Usermod "usermod_ld2410.h" #define USERMOD_ID_POV_DISPLAY 53 //Usermod "usermod_pov_display.h" #define USERMOD_ID_PIXELS_DICE_TRAY 54 //Usermod "pixels_dice_tray.h" +#define USERMOD_ID_DEEP_SLEEP 55 //Usermod "usermod_deep_sleep.h" +#define USERMOD_ID_RF433 56 //Usermod "usermod_v2_RF433.h" +#define USERMOD_ID_BRIGHTNESS_FOLLOW_SUN 57 //Usermod "usermod_v2_brightness_follow_sun.h" +#define USERMOD_ID_USER_FX 58 //Usermod "user_fx" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot @@ -248,6 +244,7 @@ #define REALTIME_MODE_ARTNET 6 #define REALTIME_MODE_TPM2NET 7 #define REALTIME_MODE_DDP 8 +#define REALTIME_MODE_DMX 9 //realtime override modes #define REALTIME_OVERRIDE_NONE 0 @@ -334,18 +331,6 @@ #define TYPE_NET_ARTNET_RGBW 89 //network ArtNet RGB bus (master broadcast bus, unused) #define TYPE_VIRTUAL_MAX 95 -/* -// old macros that have been moved to Bus class -#define IS_TYPE_VALID(t) ((t) > 15 && (t) < 128) -#define IS_DIGITAL(t) (((t) > 15 && (t) < 40) || ((t) > 47 && (t) < 64)) //digital are 16-39 and 48-63 -#define IS_2PIN(t) ((t) > 47 && (t) < 64) -#define IS_16BIT(t) ((t) == TYPE_UCS8903 || (t) == TYPE_UCS8904) -#define IS_ONOFF(t) ((t) == 40) -#define IS_PWM(t) ((t) > 40 && (t) < 46) //does not include on/Off type -#define NUM_PWM_PINS(t) ((t) - 40) //for analog PWM 41-45 only -#define IS_VIRTUAL(t) ((t) >= 80 && (t) < 96) //this was a poor choice a better would be 96-111 -*/ - //Color orders #define COL_ORDER_GRB 0 //GRB(w),defaut #define COL_ORDER_RGB 1 //common for WS2811 @@ -433,6 +418,7 @@ #define ERR_CONCURRENCY 2 // Conurrency (client active) #define ERR_NOBUF 3 // JSON buffer was not released in time, request cannot be handled at this time #define ERR_NOT_IMPL 4 // Not implemented +#define ERR_NORAM_PX 7 // not enough RAM for pixels #define ERR_NORAM 8 // effect RAM depleted #define ERR_JSON 9 // JSON parsing failed (input too large?) #define ERR_FS_BEGIN 10 // Could not init filesystem (no partition?) @@ -472,26 +458,29 @@ #define NTP_PACKET_SIZE 48 // size of NTP receive buffer #define NTP_MIN_PACKET_SIZE 48 // min expected size - NTP v4 allows for "extended information" appended to the standard fields -// Maximum number of pins per output. 5 for RGBCCT analog LEDs. -#define OUTPUT_MAX_PINS 5 - //maximum number of rendered LEDs - this does not have to match max. physical LEDs, e.g. if there are virtual busses #ifndef MAX_LEDS -#ifdef ESP8266 -#define MAX_LEDS 1664 //can't rely on memory limit to limit this to 1600 LEDs -#else -#define MAX_LEDS 8192 -#endif + #ifdef ESP8266 + #define MAX_LEDS 1536 //can't rely on memory limit to limit this to 1536 LEDs + #elif defined(CONFIG_IDF_TARGET_ESP32S2) + #define MAX_LEDS 2048 //due to memory constraints S2 + #elif defined(CONFIG_IDF_TARGET_ESP32C3) + #define MAX_LEDS 4096 + #else + #define MAX_LEDS 16384 + #endif #endif #ifndef MAX_LED_MEMORY #ifdef ESP8266 - #define MAX_LED_MEMORY 4000 + #define MAX_LED_MEMORY 4096 #else - #if defined(ARDUINO_ARCH_ESP32S2) || defined(ARDUINO_ARCH_ESP32C3) - #define MAX_LED_MEMORY 32000 + #if defined(ARDUINO_ARCH_ESP32S2) + #define MAX_LED_MEMORY 16384 + #elif defined(ARDUINO_ARCH_ESP32C3) + #define MAX_LED_MEMORY 32768 #else - #define MAX_LED_MEMORY 64000 + #define MAX_LED_MEMORY 65536 #endif #endif #endif @@ -563,8 +552,25 @@ #endif #endif -//#define MIN_HEAP_SIZE (8k for AsyncWebServer) -#define MIN_HEAP_SIZE 8192 +//#define MIN_HEAP_SIZE +#define MIN_HEAP_SIZE 2048 + +// Web server limits +#ifdef ESP8266 +// Minimum heap to consider handling a request +#define WLED_REQUEST_MIN_HEAP (8*1024) +// Estimated maximum heap required by any one request +#define WLED_REQUEST_HEAP_USAGE (6*1024) +#else +// ESP32 TCP stack needs much more RAM than ESP8266 +// Minimum heap remaining before queuing a request +#define WLED_REQUEST_MIN_HEAP (12*1024) +// Estimated maximum heap required by any one request +#define WLED_REQUEST_HEAP_USAGE (12*1024) +#endif +// Maximum number of requests in queue; absolute cap on web server resource usage. +// Websockets do not count against this limit. +#define WLED_REQUEST_MAX_QUEUE 6 // Maximum size of node map (list of other WLED instances) #ifdef ESP8266 diff --git a/wled00/data/common.js b/wled00/data/common.js index 9378ef07..658346e7 100644 --- a/wled00/data/common.js +++ b/wled00/data/common.js @@ -2,7 +2,7 @@ var d=document; var loc = false, locip, locproto = "http:"; function H(pg="") { window.open("https://kno.wled.ge/"+pg); } -function GH() { window.open("https://github.com/Aircoookie/WLED"); } +function GH() { window.open("https://github.com/wled-dev/WLED"); } function gId(c) { return d.getElementById(c); } // getElementById function cE(e) { return d.createElement(e); } // createElement function gEBCN(c) { return d.getElementsByClassName(c); } // getElementsByClassName @@ -16,7 +16,7 @@ function isI(n) { return n === +n && n === (n|0); } // isInteger function toggle(el) { gId(el).classList.toggle("hide"); gId('No'+el).classList.toggle("hide"); } function tooltip(cont=null) { d.querySelectorAll((cont?cont+" ":"")+"[title]").forEach((element)=>{ - element.addEventListener("mouseover", ()=>{ + element.addEventListener("pointerover", ()=>{ // save title element.setAttribute("data-title", element.getAttribute("title")); const tooltip = d.createElement("span"); @@ -41,7 +41,7 @@ function tooltip(cont=null) { tooltip.classList.add("visible"); }); - element.addEventListener("mouseout", ()=>{ + element.addEventListener("pointerout", ()=>{ d.querySelectorAll('.tooltip').forEach((tooltip)=>{ tooltip.classList.remove("visible"); d.body.removeChild(tooltip); diff --git a/wled00/data/index.css b/wled00/data/index.css index 31e2daa9..c92c884a 100644 --- a/wled00/data/index.css +++ b/wled00/data/index.css @@ -353,12 +353,12 @@ button { padding: 4px 0 0; } -#segutil, #segutil2, #segcont, #putil, #pcont, #pql, #fx, #palw, +#segutil, #segutil2, #segcont, #putil, #pcont, #pql, #fx, #palw, #bsp, .fnd { max-width: 280px; } -#putil, #segutil, #segutil2 { +#putil, #segutil, #segutil2, #bsp { min-height: 42px; margin: 0 auto; } diff --git a/wled00/data/index.htm b/wled00/data/index.htm index 8adec791..3716f7cc 100644 --- a/wled00/data/index.htm +++ b/wled00/data/index.htm @@ -6,6 +6,7 @@ + WLED @@ -128,7 +129,7 @@
- +

Color palette

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

Transition:  s

+

Transition:  s

+

@@ -340,7 +363,7 @@ diff --git a/wled00/data/index.js b/wled00/data/index.js index 1482c27b..295a3403 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -35,9 +35,10 @@ var cfg = { // [year, month (0 -> January, 11 -> December), day, duration in days, image url] var hol = [ [0, 11, 24, 4, "https://aircoookie.github.io/xmas.png"], // christmas - [0, 2, 17, 1, "https://images.alphacoders.com/491/491123.jpg"], // st. Patrick's day - [2025, 3, 20, 2, "https://aircoookie.github.io/easter.png"], // easter 2025 - [2024, 2, 31, 2, "https://aircoookie.github.io/easter.png"], // easter 2024 + [0, 2, 17, 1, "https://images.alphacoders.com/491/491123.jpg"], // st. Patrick's day + [2026, 3, 5, 2, "https://aircoookie.github.io/easter.png"], // easter 2026 + [2027, 2, 28, 2, "https://aircoookie.github.io/easter.png"], // easter 2027 + //[2028, 3, 16, 2, "https://aircoookie.github.io/easter.png"], // easter 2028 [0, 6, 4, 1, "https://images.alphacoders.com/516/516792.jpg"], // 4th of July [0, 0, 1, 1, "https://images.alphacoders.com/119/1198800.jpg"] // new year ]; @@ -57,7 +58,7 @@ function handleVisibilityChange() {if (!d.hidden && new Date () - lastUpdate > 3 function sCol(na, col) {d.documentElement.style.setProperty(na, col);} function gId(c) {return d.getElementById(c);} function gEBCN(c) {return d.getElementsByClassName(c);} -function isEmpty(o) {return Object.keys(o).length === 0;} +function isEmpty(o) {for (const i in o) return false; return true;} function isObj(i) {return (i && typeof i === 'object' && !Array.isArray(i));} function isNumeric(n) {return !isNaN(parseFloat(n)) && isFinite(n);} @@ -409,7 +410,9 @@ function pName(i) function isPlaylist(i) { - return pJson[i].playlist && pJson[i].playlist.ps; + if (isNumeric(i)) return pJson[i].playlist && pJson[i].playlist.ps; + if (isObj(i)) return i.playlist && i.playlist.ps; + return false; } function papiVal(i) @@ -677,8 +680,10 @@ function parseInfo(i) { isM = mw>0 && mh>0; if (!isM) { gId("filter2D").classList.add('hide'); + gId('bs').querySelectorAll('option[data-type="2D"]').forEach((o,i)=>{o.style.display='none';}); } else { gId("filter2D").classList.remove('hide'); + gId('bs').querySelectorAll('option[data-type="2D"]').forEach((o,i)=>{o.style.display='';}); } // if (i.noaudio) { // gId("filterVol").classList.add("hide"); @@ -773,8 +778,8 @@ function populateSegments(s) } let segp = `
`+ - ``+ - `
`+ + ``+ + `
`+ ``+ `
`+ `
`+ @@ -801,16 +806,38 @@ function populateSegments(s) ``+ `
`+ `
`; + let blend = `
Blend mode
`+ + `
`+ + `
`; let sndSim = `
Sound sim
`+ `
`+ `
`; cn += `
`+ ``+ `
`+ `&#x${inst.frz ? (li.live && li.liveseg==i?'e410':'e0e8') : 'e325'};`+ @@ -854,6 +881,7 @@ function populateSegments(s) ``+ ``+ `
`+ + blend + (!isMSeg ? rvXck : '') + (isMSeg&&stoY-staY>1&&stoX-staX>1 ? map2D : '') + (s.AudioReactive && s.AudioReactive.on ? "" : sndSim) + @@ -1414,7 +1442,7 @@ function makeWS() { ws = null; } ws.onopen = (e)=>{ - //ws.send("{'v':true}"); // unnecessary (https://github.com/Aircoookie/WLED/blob/master/wled00/ws.cpp#L18) + //ws.send("{'v':true}"); // unnecessary (https://github.com/wled/WLED/blob/master/wled00/ws.cpp#L18) wsRpt = 0; reqsLegal = true; } @@ -1437,43 +1465,37 @@ function readState(s,command=false) tr = s.transition; gId('tt').value = tr/10; + gId('bs').value = s.bs || 0; + if (tr===0) gId('bsp').classList.add('hide') + else gId('bsp').classList.remove('hide') populateSegments(s); - var selc=0; - var sellvl=0; // 0: selc is invalid, 1: selc is mainseg, 2: selc is first selected hasRGB = hasWhite = hasCCT = has2D = false; - segLmax = 0; - for (let i = 0; i < (s.seg||[]).length; i++) - { - if (sellvl == 0 && s.seg[i].id == s.mainseg) { - selc = i; - sellvl = 1; - } - if (s.seg[i].sel) { - if (sellvl < 2) selc = i; // get first selected segment - sellvl = 2; - let w = (s.seg[i].stop - s.seg[i].start); - let h = s.seg[i].stopY ? (s.seg[i].stopY - s.seg[i].startY) : 1; - let lc = lastinfo.leds.seglc[i]; + let i = {}; + // determine light capabilities from selected segments + for (let seg of (s.seg||[])) { + let w = (seg.stop - seg.start); + let h = seg.stopY ? (seg.stopY - seg.startY) : 1; + let lc = seg.lc; + if (w*h > segLmax) segLmax = w*h; + if (seg.sel) { + if (isEmpty(i) || (i.id == s.mainseg && !i.sel)) i = seg; // get first selected segment (and replace mainseg if it is not selected) hasRGB |= !!(lc & 0x01); hasWhite |= !!(lc & 0x02); hasCCT |= !!(lc & 0x04); has2D |= w > 1 && h > 1; - if (w*h > segLmax) segLmax = w*h; - } + } else if (isEmpty(i) && seg.id == s.mainseg) i = seg; // assign mainseg if no segments are selected } - var i=s.seg[selc]; - if (sellvl == 1) { - let lc = lastinfo.leds.seglc[selc]; - hasRGB = !!(lc & 0x01); - hasWhite = !!(lc & 0x02); - hasCCT = !!(lc & 0x04); - has2D = (i.stop - i.start) > 1 && (i.stopY ? (i.stopY - i.startY) : 1) > 1; - } - if (!i) { - showToast('No Segments!', true); + if (isEmpty(i)) { + showToast('No segments!', true); updateUI(); return true; + } else if (i.id == s.mainseg) { + // fallback if no segments are selected but we have mainseg + hasRGB |= !!(i.lc & 0x01); + hasWhite |= !!(i.lc & 0x02); + hasCCT |= !!(i.lc & 0x04); + has2D |= (i.stop - i.start) > 1 && (i.stopY ? (i.stopY - i.startY) : 1) > 1; } var cd = gId('csl').querySelectorAll("button"); @@ -1659,13 +1681,17 @@ function setEffectParameters(idx) paOnOff[0] = paOnOff[0].substring(0,dPos); } if (paOnOff.length>0 && paOnOff[0] != "!") text = paOnOff[0]; + gId("adPal").classList.remove("hide"); + if (lastinfo.cpalcount>0) gId("rmPal").classList.remove("hide"); } else { // disable palette list text += ' not used'; palw.style.display = "none"; + gId("adPal").classList.add("hide"); + gId("rmPal").classList.add("hide"); // Close palette dialog if not available - if (gId("palw").lastElementChild.tagName == "DIALOG") { - gId("palw").lastElementChild.close(); + if (palw.lastElementChild.tagName == "DIALOG") { + palw.lastElementChild.close(); } } pall.innerHTML = icon + text; @@ -1698,6 +1724,7 @@ function requestJson(command=null) var tn = parseInt(t.value*10); if (tn != tr) command.transition = tn; } + //command.bs = parseInt(gId('bs').value); req = JSON.stringify(command); if (req.length > 1340) useWs = false; // do not send very long requests over websocket if (req.length > 500 && lastinfo && lastinfo.arch == "esp8266") useWs = false; // esp8266 can only handle 500 bytes @@ -1879,7 +1906,7 @@ function makeSeg() function resetUtil(off=false) { gId('segutil').innerHTML = `
` - + '' + + '' + `
Add segment
` + '
' + `` @@ -1893,15 +1920,16 @@ function resetUtil(off=false) if (lSeg>2) d.querySelectorAll("#Segments .pop").forEach((e)=>{e.classList.remove("hide");}); } -function makePlSel(el, incPl=false) +function makePlSel(p, el) { var plSelContent = ""; delete pJson["0"]; // remove filler preset Object.entries(pJson).sort(cmpP).forEach((a)=>{ var n = a[1].n ? a[1].n : "Preset " + a[0]; + if (isPlaylist(a[1])) n += ' ▶'; // mark playlist if (cfg.comp.idsort) n = a[0] + ' ' + n; - if (!(!incPl && a[1].playlist && a[1].playlist.ps)) // skip playlists, sub-playlists not yet supported - plSelContent += ``; + // skip endless playlists and itself + if (!isPlaylist(a[1]) || (a[1].playlist.repeat > 0 && a[0]!=p)) plSelContent += ``; }); return plSelContent; } @@ -1926,21 +1954,23 @@ function refreshPlE(p) }); } -// p: preset ID, i: ps index +// p: preset ID, i: playlist item index function addPl(p,i) { - plJson[p].ps.splice(i+1,0,0); - plJson[p].dur.splice(i+1,0,plJson[p].dur[i]); - plJson[p].transition.splice(i+1,0,plJson[p].transition[i]); + const pl = plJson[p]; + pl.ps.splice(i+1,0,1); + pl.dur.splice(i+1,0,pl.dur[i]); + pl.transition.splice(i+1,0,pl.transition[i]); refreshPlE(p); } function delPl(p,i) { - if (plJson[p].ps.length < 2) return; - plJson[p].ps.splice(i,1); - plJson[p].dur.splice(i,1); - plJson[p].transition.splice(i,1); + const pl = plJson[p]; + if (pl.ps.length < 2) return; + pl.ps.splice(i,1); + pl.dur.splice(i,1); + pl.transition.splice(i,1); refreshPlE(p); } @@ -1957,6 +1987,13 @@ function pleDur(p,i,field) function pleTr(p,i,field) { + const du = gId(`pl${p}du${i}`); + const dv = parseFloat(du.value); + if (dv > 0) { + field.max = dv; + if (parseFloat(field.value) > dv) + field.value = du.value; + } if (field.validity.valid) plJson[p].transition[i] = Math.floor(field.value*10); } @@ -1976,6 +2013,17 @@ function plR(p) } } +function plM(p) +{ + const man = gId(`pl${p}manual`).checked; + plJson[p].dur.forEach((e,i)=>{ + const d = gId(`pl${p}du${i}`); + plJson[p].dur[i] = e = man ? 0 : 100; + d.value = e/10; // 10s default + d.readOnly = man; + }); +} + function makeP(i,pl) { var content = ""; @@ -1989,12 +2037,17 @@ function makeP(i,pl) r: false, end: 0 }; - var rep = plJson[i].repeat ? plJson[i].repeat : 0; + const rep = plJson[i].repeat ? plJson[i].repeat : 0; + const man = plJson[i].dur == 0; content = `
+
`; @@ -2078,25 +2131,26 @@ function makePUtil() function makePlEntry(p,i) { + const man = gId(`pl${p}manual`).checked; return `
- + - - + +
DurationDuration (0=inf.) Transition #${i+1}
ssss
@@ -2298,6 +2352,13 @@ function setSi(s) requestJson(obj); } +function setBm(s) +{ + var value = gId(`seg${s}bm`).selectedIndex; + var obj = {"seg": {"id": s, "bm": value}}; + requestJson(obj); +} + function setTp(s) { var tp = gId(`seg${s}tp`).checked; @@ -2649,28 +2710,28 @@ function fromRgb() var g = gId('sliderG').value; var b = gId('sliderB').value; setPicker(`rgb(${r},${g},${b})`); - let cd = gId('csl').children; // color slots - cd[csel].dataset.r = r; - cd[csel].dataset.g = g; - cd[csel].dataset.b = b; - setCSL(cd[csel]); + let cd = gId('csl').children[csel]; // color slots + cd.dataset.r = r; + cd.dataset.g = g; + cd.dataset.b = b; + setCSL(cd); } function fromW() { let w = gId('sliderW'); - let cd = gId('csl').children; // color slots - cd[csel].dataset.w = w.value; - setCSL(cd[csel]); + let cd = gId('csl').children[csel]; // color slots + cd.dataset.w = w.value; + setCSL(cd); updateTrail(w); } // sr 0: from RGB sliders, 1: from picker, 2: from hex function setColor(sr) { - var cd = gId('csl').children; // color slots - let cdd = cd[csel].dataset; - let w = 0, r,g,b; + var cd = gId('csl').children[csel]; // color slots + let cdd = cd.dataset; + let w = parseInt(cdd.w), r = parseInt(cdd.r), g = parseInt(cdd.g), b = parseInt(cdd.b); if (sr == 1 && isRgbBlack(cdd)) cpick.color.setChannel('hsv', 'v', 100); if (sr != 2 && hasWhite) w = parseInt(gId('sliderW').value); var col = cpick.color.rgb; @@ -2678,7 +2739,7 @@ function setColor(sr) cdd.g = g = hasRGB ? col.g : w; cdd.b = b = hasRGB ? col.b : w; cdd.w = w; - setCSL(cd[csel]); + setCSL(cd); var obj = {"seg": {"col": [[],[],[]]}}; obj.seg.col[csel] = [r, g, b, w]; requestJson(obj); @@ -2721,7 +2782,7 @@ setInterval(()=>{ gId('heart').style.color = `hsl(${hc}, 100%, 50%)`; }, 910); -function openGH() { window.open("https://github.com/Aircoookie/WLED/wiki"); } +function openGH() { window.open("https://github.com/wled/WLED/wiki"); } var cnfr = false; function cnfReset() @@ -3117,7 +3178,7 @@ function mergeDeep(target, ...sources) function tooltip(cont=null) { d.querySelectorAll((cont?cont+" ":"")+"[title]").forEach((element)=>{ - element.addEventListener("mouseover", ()=>{ + element.addEventListener("pointerover", ()=>{ // save title element.setAttribute("data-title", element.getAttribute("title")); const tooltip = d.createElement("span"); @@ -3142,7 +3203,7 @@ function tooltip(cont=null) tooltip.classList.add("visible"); }); - element.addEventListener("mouseout", ()=>{ + element.addEventListener("pointerout", ()=>{ d.querySelectorAll('.tooltip').forEach((tooltip)=>{ tooltip.classList.remove("visible"); d.body.removeChild(tooltip); diff --git a/wled00/data/settings_dmx.htm b/wled00/data/settings_dmx.htm index e800be6e..7458ec9f 100644 --- a/wled00/data/settings_dmx.htm +++ b/wled00/data/settings_dmx.htm @@ -6,7 +6,7 @@ DMX Settings @@ -151,6 +153,19 @@ Timeout: ms
Force max brightness:
Disable realtime gamma correction:
Realtime LED offset: +
+

Wired DMX Input Pins

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

Alexa Voice Assistant

diff --git a/wled00/data/settings_time.htm b/wled00/data/settings_time.htm index df054f41..ae29065e 100644 --- a/wled00/data/settings_time.htm +++ b/wled00/data/settings_time.htm @@ -168,8 +168,8 @@

Clock

Analog Clock overlay:
- First LED: Last LED:
- 12h LED:
+ First LED: Last LED:
+ 12h LED:
Show 5min marks:
Seconds (as trail):
Show clock overlay only if all LEDs are solid black:
diff --git a/wled00/data/settings_um.htm b/wled00/data/settings_um.htm index c2f0ffbf..b1505cac 100644 --- a/wled00/data/settings_um.htm +++ b/wled00/data/settings_um.htm @@ -13,7 +13,7 @@ function S() { getLoc(); // load settings and insert values into DOM - fetch(getURL('/cfg.json'), { + fetch(getURL('/json/cfg'), { method: 'get' }) .then(res => { diff --git a/wled00/data/settings_wifi.htm b/wled00/data/settings_wifi.htm index 30b6600a..d2d7c66c 100644 --- a/wled00/data/settings_wifi.htm +++ b/wled00/data/settings_wifi.htm @@ -47,7 +47,7 @@ scanLoops = 0; if (networks.length > 0) { - let cs = d.querySelectorAll("#wifi_entries input[type=text]"); + let cs = d.querySelectorAll("#wifi_entries input[type=text][name^=CS]"); for (let input of (cs||[])) { let found = false; let select = cE("select"); @@ -64,7 +64,7 @@ const option = cE("option"); option.setAttribute("value", networks[i].ssid); - option.textContent = `${networks[i].ssid} (${networks[i].rssi} dBm)`; + option.textContent = `${networks[i].ssid} (${networks[i].rssi} dBm)`; // [${networks[i].bssid.replaceAll(':','')}] if (networks[i].ssid === input.value) { option.setAttribute("selected", "selected"); @@ -109,12 +109,13 @@ gId("wifi_add").style.display = (i1) ? "inline":"none"; } - function addWiFi(ssid="",pass="",ip=0,gw=0,sn=0x00ffffff) { // little endian + function addWiFi(ssid="",pass="",bssid="",ip=0,gw=0,sn=0x00ffffff) { // little endian var i = gId("wifi_entries").childNodes.length; if (i >= maxNetworks) return; var b = `

Network name (SSID${i==0?", empty to not connect":""}):
0?"required":""}>
Network password:

+BSSID (optional):

Static IP (leave at 0.0.0.0 for DHCP)${i==0?"
Also used by Ethernet":""}:
...
Static gateway:
@@ -135,12 +136,52 @@ Static subnet mask:
getLoc(); loadJS(getURL('/settings/s.js?p=1'), false); // If we set async false, file is loaded and executed, then next statement is processed if (loc) d.Sf.action = getURL('/settings/wifi'); + setTimeout(tE, 500); // wait for DOM to load before calling tE() } + + var rC = 0; // remote count + // toggle visibility of ESP-NOW remote list based on checkbox state + function tE() { + // keep the hidden input with MAC addresses, only toggle visibility of the list UI + gId('rlc').style.display = d.Sf.RE.checked ? 'block' : 'none'; + } + // reset remotes: initialize empty list (called from xml.cpp) + function rstR() { + gId('rml').innerHTML = ''; // clear remote list + } + // add remote MAC to the list + function aR(id, mac) { + if (!/^[0-9A-F]{12}$/i.test(mac)) return; // check for valid hex string + let inputs = d.querySelectorAll("#rml input"); + for (let i of (inputs || [])) { + if (i.value === mac) return; + } + let l = gId('rml'), r = cE('div'), i = cE('input'); + i.type = 'text'; + i.name = id; + i.value = mac; + i.maxLength = 12; + i.minLength = 12; + //i.onchange = uR; + r.appendChild(i); + let b = cE('button'); + b.type = 'button'; + b.className = 'sml'; + b.innerText = '-'; + b.onclick = (e) => { + r.remove(); + }; + r.appendChild(b); + l.appendChild(r); + rC++; + gId('+').style.display = gId("rml").childElementCount < 10 ? 'inline' : 'none'; // can't append to list anymore, hide button + } + -
+

@@ -201,11 +242,16 @@ Static subnet mask:
This firmware build does not include ESP-NOW support.
- Enable ESP-NOW:
+ Enable ESP-NOW:
Listen for events over ESP-NOW
- Keep disabled if not using a remote or wireless sync, increases power consumption.
- Paired Remote MAC:
- Last device seen: None
+ Keep disabled if not using a remote or ESP-NOW sync, increases power consumption.
+
+ Last device seen: None +
+ Linked MACs (10 max):
+
+
+
diff --git a/wled00/data/update.htm b/wled00/data/update.htm index 23a6a866..96ba821e 100644 --- a/wled00/data/update.htm +++ b/wled00/data/update.htm @@ -17,9 +17,9 @@

WLED Software Update

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


diff --git a/wled00/dmx_input.cpp b/wled00/dmx_input.cpp new file mode 100644 index 00000000..59467590 --- /dev/null +++ b/wled00/dmx_input.cpp @@ -0,0 +1,280 @@ +#include "wled.h" + +#ifdef WLED_ENABLE_DMX_INPUT + +#ifdef ESP8266 +#error DMX input is only supported on ESP32 +#endif + +#include "dmx_input.h" +#include + +void rdmPersonalityChangedCb(dmx_port_t dmxPort, const rdm_header_t *header, + void *context) +{ + DMXInput *dmx = static_cast(context); + + if (!dmx) { + DEBUG_PRINTLN("DMX: Error: no context in rdmPersonalityChangedCb"); + return; + } + + if (header->cc == RDM_CC_SET_COMMAND_RESPONSE) { + const uint8_t personality = dmx_get_current_personality(dmx->inputPortNum); + DMXMode = std::min(DMX_MODE_PRESET, std::max(DMX_MODE_SINGLE_RGB, int(personality))); + configNeedsWrite = true; + DEBUG_PRINTF("DMX personality changed to to: %d\n", DMXMode); + } +} + +void rdmAddressChangedCb(dmx_port_t dmxPort, const rdm_header_t *header, + void *context) +{ + DMXInput *dmx = static_cast(context); + + if (!dmx) { + DEBUG_PRINTLN("DMX: Error: no context in rdmAddressChangedCb"); + return; + } + + if (header->cc == RDM_CC_SET_COMMAND_RESPONSE) { + const uint16_t addr = dmx_get_start_address(dmx->inputPortNum); + DMXAddress = std::min(512, int(addr)); + configNeedsWrite = true; + DEBUG_PRINTF("DMX start addr changed to: %d\n", DMXAddress); + } +} + +static dmx_config_t createConfig() +{ + dmx_config_t config; + config.pd_size = 255; + config.dmx_start_address = DMXAddress; + config.model_id = 0; + config.product_category = RDM_PRODUCT_CATEGORY_FIXTURE; + config.software_version_id = VERSION; + strcpy(config.device_label, "WLED_MM"); + + const std::string versionString = "WLED_V" + std::to_string(VERSION); + strncpy(config.software_version_label, versionString.c_str(), 32); + config.software_version_label[32] = '\0'; // zero termination in case versionString string was longer than 32 chars + + config.personalities[0].description = "SINGLE_RGB"; + config.personalities[0].footprint = 3; + config.personalities[1].description = "SINGLE_DRGB"; + config.personalities[1].footprint = 4; + config.personalities[2].description = "EFFECT"; + config.personalities[2].footprint = 15; + config.personalities[3].description = "MULTIPLE_RGB"; + config.personalities[3].footprint = std::min(512, int(strip.getLengthTotal()) * 3); + config.personalities[4].description = "MULTIPLE_DRGB"; + config.personalities[4].footprint = std::min(512, int(strip.getLengthTotal()) * 3 + 1); + config.personalities[5].description = "MULTIPLE_RGBW"; + config.personalities[5].footprint = std::min(512, int(strip.getLengthTotal()) * 4); + config.personalities[6].description = "EFFECT_W"; + config.personalities[6].footprint = 18; + config.personalities[7].description = "EFFECT_SEGMENT"; + config.personalities[7].footprint = std::min(512, strip.getSegmentsNum() * 15); + config.personalities[8].description = "EFFECT_SEGMENT_W"; + config.personalities[8].footprint = std::min(512, strip.getSegmentsNum() * 18); + config.personalities[9].description = "PRESET"; + config.personalities[9].footprint = 1; + + config.personality_count = 10; + // rdm personalities are numbered from 1, thus we can just set the DMXMode directly. + config.current_personality = DMXMode; + + return config; +} + +void dmxReceiverTask(void *context) +{ + DMXInput *instance = static_cast(context); + if (instance == nullptr) { + return; + } + + if (instance->installDriver()) { + while (true) { + instance->updateInternal(); + } + } +} + +bool DMXInput::installDriver() +{ + + const auto config = createConfig(); + DEBUG_PRINTF("DMX port: %u\n", inputPortNum); + if (!dmx_driver_install(inputPortNum, &config, DMX_INTR_FLAGS_DEFAULT)) { + DEBUG_PRINTF("Error: Failed to install dmx driver\n"); + return false; + } + + DEBUG_PRINTF("Listening for DMX on pin %u\n", rxPin); + DEBUG_PRINTF("Sending DMX on pin %u\n", txPin); + DEBUG_PRINTF("DMX enable pin is: %u\n", enPin); + dmx_set_pin(inputPortNum, txPin, rxPin, enPin); + + rdm_register_dmx_start_address(inputPortNum, rdmAddressChangedCb, this); + rdm_register_dmx_personality(inputPortNum, rdmPersonalityChangedCb, this); + initialized = true; + return true; +} + +void DMXInput::init(uint8_t rxPin, uint8_t txPin, uint8_t enPin, uint8_t inputPortNum) +{ + +#ifdef WLED_ENABLE_DMX_OUTPUT + //TODO add again once dmx output has been merged + // if(inputPortNum == dmxOutputPort) + // { + // DEBUG_PRINTF("DMXInput: Error: Input port == output port"); + // return; + // } +#endif + + if (inputPortNum <= (SOC_UART_NUM - 1) && inputPortNum > 0) { + this->inputPortNum = inputPortNum; + } + else { + DEBUG_PRINTF("DMXInput: Error: invalid inputPortNum: %d\n", inputPortNum); + return; + } + + if (rxPin > 0 && enPin > 0 && txPin > 0) { + + const managed_pin_type pins[] = { + {(int8_t)txPin, false}, // these are not used as gpio pins, thus isOutput is always false. + {(int8_t)rxPin, false}, + {(int8_t)enPin, false}}; + const bool pinsAllocated = PinManager::allocateMultiplePins(pins, 3, PinOwner::DMX_INPUT); + if (!pinsAllocated) { + DEBUG_PRINTF("DMXInput: Error: Failed to allocate pins for DMX_INPUT. Pins already in use:\n"); + DEBUG_PRINTF("rx in use by: %s\n", PinManager::getPinOwner(rxPin)); + DEBUG_PRINTF("tx in use by: %s\n", PinManager::getPinOwner(txPin)); + DEBUG_PRINTF("en in use by: %s\n", PinManager::getPinOwner(enPin)); + return; + } + + this->rxPin = rxPin; + this->txPin = txPin; + this->enPin = enPin; + + // put dmx receiver into seperate task because it should not be blocked + // pin to core 0 because wled is running on core 1 + xTaskCreatePinnedToCore(dmxReceiverTask, "DMX_RCV_TASK", 10240, this, 2, &task, 0); + if (!task) { + DEBUG_PRINTF("Error: Failed to create dmx rcv task"); + } + } + else { + DEBUG_PRINTLN("DMX input disabled due to rxPin, enPin or txPin not set"); + return; + } +} + +void DMXInput::updateInternal() +{ + if (!initialized) { + return; + } + + checkAndUpdateConfig(); + + dmx_packet_t packet; + unsigned long now = millis(); + if (dmx_receive(inputPortNum, &packet, DMX_TIMEOUT_TICK)) { + if (!packet.err) { + if(!connected) { + DEBUG_PRINTLN("DMX Input - connected"); + } + connected = true; + identify = isIdentifyOn(); + if (!packet.is_rdm) { + const std::lock_guard lock(dmxDataLock); + dmx_read(inputPortNum, dmxdata, packet.size); + } + } + else { + connected = false; + } + } + else { + if(connected) { + DEBUG_PRINTLN("DMX Input - disconnected"); + } + connected = false; + } +} + + +void DMXInput::update() +{ + if (identify) { + turnOnAllLeds(); + } + else if (connected) { + const std::lock_guard lock(dmxDataLock); + handleDMXData(1, 512, dmxdata, REALTIME_MODE_DMX, 0); + } +} + +void DMXInput::turnOnAllLeds() +{ + // TODO not sure if this is the correct way? + const uint16_t numPixels = strip.getLengthTotal(); + for (uint16_t i = 0; i < numPixels; ++i) + { + strip.setPixelColor(i, 255, 255, 255, 255); + } + strip.setBrightness(255, true); + strip.show(); +} + +void DMXInput::disable() +{ + if (initialized) { + dmx_driver_disable(inputPortNum); + } +} +void DMXInput::enable() +{ + if (initialized) { + dmx_driver_enable(inputPortNum); + } +} + +bool DMXInput::isIdentifyOn() const +{ + + uint8_t identify = 0; + const bool gotIdentify = rdm_get_identify_device(inputPortNum, &identify); + // gotIdentify should never be false because it is a default parameter in rdm + // but just in case we check for it anyway + return bool(identify) && gotIdentify; +} + +void DMXInput::checkAndUpdateConfig() +{ + + /** + * The global configuration variables are modified by the web interface. + * If they differ from the driver configuration, we have to update the driver + * configuration. + */ + + const uint8_t currentPersonality = dmx_get_current_personality(inputPortNum); + if (currentPersonality != DMXMode) { + DEBUG_PRINTF("DMX personality has changed from %d to %d\n", currentPersonality, DMXMode); + dmx_set_current_personality(inputPortNum, DMXMode); + } + + const uint16_t currentAddr = dmx_get_start_address(inputPortNum); + if (currentAddr != DMXAddress) { + DEBUG_PRINTF("DMX address has changed from %d to %d\n", currentAddr, DMXAddress); + dmx_set_start_address(inputPortNum, DMXAddress); + } +} + +#endif \ No newline at end of file diff --git a/wled00/dmx_input.h b/wled00/dmx_input.h new file mode 100644 index 00000000..29f015bd --- /dev/null +++ b/wled00/dmx_input.h @@ -0,0 +1,76 @@ +#pragma once +#include +#include +#include +#include + +/* + * Support for DMX/RDM input via serial (e.g. max485) on ESP32 + * ESP32 Library from: + * https://github.com/someweisguy/esp_dmx + */ +class DMXInput +{ +public: + void init(uint8_t rxPin, uint8_t txPin, uint8_t enPin, uint8_t inputPortNum); + void update(); + + /**disable dmx receiver (do this before disabling the cache)*/ + void disable(); + void enable(); + + /// True if dmx is currently connected + bool isConnected() const { return connected; } + +private: + /// @return true if rdm identify is active + bool isIdentifyOn() const; + + /** + * Checks if the global dmx config has changed and updates the changes in rdm + */ + void checkAndUpdateConfig(); + + /// overrides everything and turns on all leds + void turnOnAllLeds(); + + /// installs the dmx driver + /// @return false on fail + bool installDriver(); + + /// is called by the dmx receive task regularly to receive new dmx data + void updateInternal(); + + // is invoked whenver the dmx start address is changed via rdm + friend void rdmAddressChangedCb(dmx_port_t dmxPort, const rdm_header_t *header, + void *context); + + // is invoked whenever the personality is changed via rdm + friend void rdmPersonalityChangedCb(dmx_port_t dmxPort, const rdm_header_t *header, + void *context); + + /// The internal dmx task. + /// This is the main loop of the dmx receiver. It never returns. + friend void dmxReceiverTask(void * context); + + uint8_t inputPortNum = 255; + uint8_t rxPin = 255; + uint8_t txPin = 255; + uint8_t enPin = 255; + + /// is written to by the dmx receive task. + byte dmxdata[DMX_PACKET_SIZE]; + /// True once the dmx input has been initialized successfully + bool initialized = false; // true once init finished successfully + /// True if dmx is currently connected + std::atomic connected{false}; + std::atomic identify{false}; + /// Timestamp of the last time a dmx frame was received + unsigned long lastUpdate = 0; + + /// Taskhandle of the dmx task that is running in the background + TaskHandle_t task; + /// Guards access to dmxData + std::mutex dmxDataLock; + +}; diff --git a/wled00/dmx.cpp b/wled00/dmx_output.cpp similarity index 93% rename from wled00/dmx.cpp rename to wled00/dmx_output.cpp index dbe70f2a..eace2145 100644 --- a/wled00/dmx.cpp +++ b/wled00/dmx_output.cpp @@ -1,7 +1,7 @@ #include "wled.h" /* - * Support for DMX Output via MAX485. + * Support for DMX output via serial (e.g. MAX485). * Change the output pin in src/dependencies/ESPDMX.cpp, if needed (ESP8266) * Change the output pin in src/dependencies/SparkFunDMX.cpp, if needed (ESP32) * ESP8266 Library from: @@ -12,7 +12,7 @@ #ifdef WLED_ENABLE_DMX -void handleDMX() +void handleDMXOutput() { // don't act, when in DMX Proxy mode if (e131ProxyUniverse != 0) return; @@ -68,11 +68,14 @@ void handleDMX() dmx.update(); // update the DMX bus } -void initDMX() { +void initDMXOutput() { #if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2) dmx.init(512); // initialize with bus length #else dmx.initWrite(512); // initialize with bus length #endif } +#else +void initDMXOutput(){} +void handleDMXOutput() {} #endif diff --git a/wled00/e131.cpp b/wled00/e131.cpp index 7c074759..98cfe28f 100644 --- a/wled00/e131.cpp +++ b/wled00/e131.cpp @@ -38,7 +38,7 @@ void handleDDPPacket(e131_packet_t* p) { if (realtimeMode != REALTIME_MODE_DDP) ddpSeenPush = false; // just starting, no push yet realtimeLock(realtimeTimeoutMs, REALTIME_MODE_DDP); - if (!realtimeOverride || (realtimeMode && useMainSegmentOnly)) { + if (!realtimeOverride) { for (unsigned i = start; i < stop; i++, c += ddpChannelsPerLed) { setRealtimePixel(i, data[c], data[c+1], data[c+2], ddpChannelsPerLed >3 ? data[c+3] : 0); } @@ -115,6 +115,11 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ // update status info realtimeIP = clientIP; + + handleDMXData(uni, dmxChannels, e131_data, mde, previousUniverses); +} + +void handleDMXData(uint16_t uni, uint16_t dmxChannels, uint8_t* e131_data, uint8_t mde, uint8_t previousUniverses) { byte wChannel = 0; unsigned totalLen = strip.getLengthTotal(); unsigned availDMXLen = 0; @@ -129,7 +134,7 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ } // DMX data in Art-Net packet starts at index 0, for E1.31 at index 1 - if (protocol == P_ARTNET && dataOffset > 0) { + if (mde == REALTIME_MODE_ARTNET && dataOffset > 0) { dataOffset--; } @@ -144,7 +149,7 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ realtimeLock(realtimeTimeoutMs, mde); - if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; + if (realtimeOverride) return; wChannel = (availDMXLen > 3) ? e131_data[dataOffset+3] : 0; for (unsigned i = 0; i < totalLen; i++) @@ -156,7 +161,7 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ if (availDMXLen < 4) return; realtimeLock(realtimeTimeoutMs, mde); - if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; + if (realtimeOverride) return; wChannel = (availDMXLen > 4) ? e131_data[dataOffset+4] : 0; if (bri != e131_data[dataOffset+0]) { @@ -208,7 +213,7 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ else dataOffset = DMXAddress; // Modify address for Art-Net data - if (protocol == P_ARTNET && dataOffset > 0) + if (mde == REALTIME_MODE_ARTNET && dataOffset > 0) dataOffset--; // Skip out of universe addresses if (dataOffset > dmxChannels - dmxEffectChannels + 1) @@ -220,16 +225,16 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ if (e131_data[dataOffset+3] != seg.intensity) seg.intensity = e131_data[dataOffset+3]; if (e131_data[dataOffset+4] != seg.palette) seg.setPalette(e131_data[dataOffset+4]); - if ((e131_data[dataOffset+5] & 0b00000010) != seg.reverse_y) { seg.setOption(SEG_OPTION_REVERSED_Y, e131_data[dataOffset+5] & 0b00000010); } - if ((e131_data[dataOffset+5] & 0b00000100) != seg.mirror_y) { seg.setOption(SEG_OPTION_MIRROR_Y, e131_data[dataOffset+5] & 0b00000100); } - if ((e131_data[dataOffset+5] & 0b00001000) != seg.transpose) { seg.setOption(SEG_OPTION_TRANSPOSED, e131_data[dataOffset+5] & 0b00001000); } - if ((e131_data[dataOffset+5] & 0b00110000) / 8 != seg.map1D2D) { - seg.map1D2D = (e131_data[dataOffset+5] & 0b00110000) / 8; + if (bool(e131_data[dataOffset+5] & 0b00000010) != seg.reverse_y) { seg.reverse_y = bool(e131_data[dataOffset+5] & 0b00000010); } + if (bool(e131_data[dataOffset+5] & 0b00000100) != seg.mirror_y) { seg.mirror_y = bool(e131_data[dataOffset+5] & 0b00000100); } + if (bool(e131_data[dataOffset+5] & 0b00001000) != seg.transpose) { seg.transpose = bool(e131_data[dataOffset+5] & 0b00001000); } + if ((e131_data[dataOffset+5] & 0b00110000) >> 4 != seg.map1D2D) { + seg.map1D2D = (e131_data[dataOffset+5] & 0b00110000) >> 4; } // To maintain backwards compatibility with prior e1.31 values, reverse is fixed to mask 0x01000000 - if ((e131_data[dataOffset+5] & 0b01000000) != seg.reverse) { seg.setOption(SEG_OPTION_REVERSED, e131_data[dataOffset+5] & 0b01000000); } + if ((e131_data[dataOffset+5] & 0b01000000) != seg.reverse) { seg.reverse = bool(e131_data[dataOffset+5] & 0b01000000); } // To maintain backwards compatibility with prior e1.31 values, mirror is fixed to mask 0x10000000 - if ((e131_data[dataOffset+5] & 0b10000000) != seg.mirror) { seg.setOption(SEG_OPTION_MIRROR, e131_data[dataOffset+5] & 0b10000000); } + if ((e131_data[dataOffset+5] & 0b10000000) != seg.mirror) { seg.mirror = bool(e131_data[dataOffset+5] & 0b10000000); } uint32_t colors[3]; byte whites[3] = {0,0,0}; @@ -263,7 +268,7 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ case DMX_MODE_MULTIPLE_RGB: case DMX_MODE_MULTIPLE_RGBW: { - bool is4Chan = (DMXMode == DMX_MODE_MULTIPLE_RGBW); + const bool is4Chan = (DMXMode == DMX_MODE_MULTIPLE_RGBW); const unsigned dmxChannelsPerLed = is4Chan ? 4 : 3; const unsigned ledsPerUniverse = is4Chan ? MAX_4_CH_LEDS_PER_UNIVERSE : MAX_3_CH_LEDS_PER_UNIVERSE; uint8_t stripBrightness = bri; @@ -282,7 +287,7 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ } } else { // All subsequent universes start at the first channel. - dmxOffset = (protocol == P_ARTNET) ? 0 : 1; + dmxOffset = (mde == REALTIME_MODE_ARTNET) ? 0 : 1; const unsigned dimmerOffset = (DMXMode == DMX_MODE_MULTIPLE_DRGB) ? 1 : 0; unsigned ledsInFirstUniverse = (((MAX_CHANNELS_PER_UNIVERSE - DMXAddress) + dmxLenOffset) - dimmerOffset) / dmxChannelsPerLed; previousLeds = ledsInFirstUniverse + (previousUniverses - 1) * ledsPerUniverse; @@ -295,7 +300,7 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ } realtimeLock(realtimeTimeoutMs, mde); - if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; + if (realtimeOverride) return; if (ledsTotal > totalLen) { ledsTotal = totalLen; @@ -308,16 +313,9 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ } } - if (!is4Chan) { - for (unsigned i = previousLeds; i < ledsTotal; i++) { - setRealtimePixel(i, e131_data[dmxOffset], e131_data[dmxOffset+1], e131_data[dmxOffset+2], 0); - dmxOffset+=3; - } - } else { - for (unsigned i = previousLeds; i < ledsTotal; i++) { - setRealtimePixel(i, e131_data[dmxOffset], e131_data[dmxOffset+1], e131_data[dmxOffset+2], e131_data[dmxOffset+3]); - dmxOffset+=4; - } + for (unsigned i = previousLeds; i < ledsTotal; i++) { + setRealtimePixel(i, e131_data[dmxOffset], e131_data[dmxOffset+1], e131_data[dmxOffset+2], is4Chan ? e131_data[dmxOffset+3] : 0); + dmxOffset += dmxChannelsPerLed; } break; } @@ -520,7 +518,7 @@ void sendArtnetPollReply(ArtPollReply *reply, IPAddress ipAddress, uint16_t port reply->reply_sub_sw = (uint8_t)((portAddress >> 4) & 0x000F); reply->reply_sw_out[0] = (uint8_t)(portAddress & 0x000F); - snprintf_P((char *)reply->reply_node_report, sizeof(reply->reply_node_report)-1, PSTR("#0001 [%04u] OK - WLED v" TOSTRING(WLED_VERSION)), pollReplyCount); + snprintf_P((char *)reply->reply_node_report, sizeof(reply->reply_node_report)-1, PSTR("#0001 [%04u] OK - WLED v%s"), pollReplyCount, versionString); if (pollReplyCount < 9999) { pollReplyCount++; diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 3d8c27ac..d2f62870 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -1,3 +1,4 @@ +#pragma once #ifndef WLED_FCN_DECLARE_H #define WLED_FCN_DECLARE_H @@ -26,7 +27,8 @@ void IRAM_ATTR touchButtonISR(); bool deserializeConfig(JsonObject doc, bool fromFS = false); void deserializeConfigFromFS(); bool deserializeConfigSec(); -void serializeConfig(); +void serializeConfig(JsonObject doc); +void serializeConfigToFS(); void serializeConfigSec(); template @@ -52,6 +54,7 @@ bool getJsonValue(const JsonVariant& element, DestType& destination, const Defau typedef struct WiFiConfig { char clientSSID[33]; char clientPass[65]; + uint8_t bssid[6]; IPAddress staticIP; IPAddress staticGW; IPAddress staticSN; @@ -62,61 +65,163 @@ typedef struct WiFiConfig { { strncpy(clientSSID, ssid, 32); clientSSID[32] = 0; strncpy(clientPass, pass, 64); clientPass[64] = 0; + memset(bssid, 0, sizeof(bssid)); } } wifi_config; //colors.cpp +#define ColorFromPalette ColorFromPaletteWLED // override fastled version + +// CRGBW can be used to manipulate 32bit colors faster. However: if it is passed to functions, it adds overhead compared to a uint32_t color +// use with caution and pay attention to flash size. Usually converting a uint32_t to CRGBW to extract r, g, b, w values is slower than using bitshifts +// it can be useful to avoid back and forth conversions between uint32_t and fastled CRGB +struct CRGBW { + union { + uint32_t color32; // Access as a 32-bit value (0xWWRRGGBB) + struct { + uint8_t b; + uint8_t g; + uint8_t r; + uint8_t w; + }; + uint8_t raw[4]; // Access as an array in the order B, G, R, W + }; + + // Default constructor + inline CRGBW() __attribute__((always_inline)) = default; + + // Constructor from a 32-bit color (0xWWRRGGBB) + constexpr CRGBW(uint32_t color) __attribute__((always_inline)) : color32(color) {} + + // Constructor with r, g, b, w values + constexpr CRGBW(uint8_t red, uint8_t green, uint8_t blue, uint8_t white = 0) __attribute__((always_inline)) : b(blue), g(green), r(red), w(white) {} + + // Constructor from CRGB + constexpr CRGBW(CRGB rgb) __attribute__((always_inline)) : b(rgb.b), g(rgb.g), r(rgb.r), w(0) {} + + // Access as an array + inline const uint8_t& operator[] (uint8_t x) const __attribute__((always_inline)) { return raw[x]; } + + // Assignment from 32-bit color + inline CRGBW& operator=(uint32_t color) __attribute__((always_inline)) { color32 = color; return *this; } + + // Assignment from r, g, b, w + inline CRGBW& operator=(const CRGB& rgb) __attribute__((always_inline)) { b = rgb.b; g = rgb.g; r = rgb.r; w = 0; return *this; } + + // Conversion operator to uint32_t + inline operator uint32_t() const __attribute__((always_inline)) { + return color32; + } + /* + // Conversion operator to CRGB + inline operator CRGB() const __attribute__((always_inline)) { + return CRGB(r, g, b); + } + + CRGBW& scale32 (uint8_t scaledown) // 32bit math + { + if (color32 == 0) return *this; // 2 extra instructions, worth it if called a lot on black (which probably is true) adding check if scaledown is zero adds much more overhead as its 8bit + uint32_t scale = scaledown + 1; + uint32_t rb = (((color32 & 0x00FF00FF) * scale) >> 8) & 0x00FF00FF; // scale red and blue + uint32_t wg = (((color32 & 0xFF00FF00) >> 8) * scale) & 0xFF00FF00; // scale white and green + color32 = rb | wg; + return *this; + }*/ + +}; + +struct CHSV32 { // 32bit HSV color with 16bit hue for more accurate conversions + union { + struct { + uint16_t h; // hue + uint8_t s; // saturation + uint8_t v; // value + }; + uint32_t raw; // 32bit access + }; + inline CHSV32() __attribute__((always_inline)) = default; // default constructor + + /// Allow construction from hue, saturation, and value + /// @param ih input hue + /// @param is input saturation + /// @param iv input value + inline CHSV32(uint16_t ih, uint8_t is, uint8_t iv) __attribute__((always_inline)) // constructor from 16bit h, s, v + : h(ih), s(is), v(iv) {} + inline CHSV32(uint8_t ih, uint8_t is, uint8_t iv) __attribute__((always_inline)) // constructor from 8bit h, s, v + : h((uint16_t)ih << 8), s(is), v(iv) {} + inline CHSV32(const CHSV& chsv) __attribute__((always_inline)) // constructor from CHSV + : h((uint16_t)chsv.h << 8), s(chsv.s), v(chsv.v) {} + inline operator CHSV() const { return CHSV((uint8_t)(h >> 8), s, v); } // typecast to CHSV +}; // similar to NeoPixelBus NeoGammaTableMethod but allows dynamic changes (superseded by NPB::NeoGammaDynamicTableMethod) class NeoGammaWLEDMethod { public: [[gnu::hot]] static uint8_t Correct(uint8_t value); // apply Gamma to single channel [[gnu::hot]] static uint32_t Correct32(uint32_t color); // apply Gamma to RGBW32 color (WLED specific, not used by NPB) - static void calcGammaTable(float gamma); // re-calculates & fills gamma table + [[gnu::hot]] static uint32_t inverseGamma32(uint32_t color); // apply inverse Gamma to RGBW32 color + static void calcGammaTable(float gamma); // re-calculates & fills gamma tables static inline uint8_t rawGamma8(uint8_t val) { return gammaT[val]; } // get value from Gamma table (WLED specific, not used by NPB) + static inline uint8_t rawInverseGamma8(uint8_t val) { return gammaT_inv[val]; } // get value from inverse Gamma table (WLED specific, not used by NPB) private: static uint8_t gammaT[]; + static uint8_t gammaT_inv[]; }; #define gamma32(c) NeoGammaWLEDMethod::Correct32(c) #define gamma8(c) NeoGammaWLEDMethod::rawGamma8(c) -[[gnu::hot]] uint32_t color_blend(uint32_t,uint32_t,uint16_t,bool b16=false); -[[gnu::hot]] uint32_t color_add(uint32_t,uint32_t, bool fast=false); -[[gnu::hot]] uint32_t color_fade(uint32_t c1, uint8_t amount, bool video=false); -CRGBPalette16 generateHarmonicRandomPalette(CRGBPalette16 &basepalette); +#define gamma32inv(c) NeoGammaWLEDMethod::inverseGamma32(c) +#define gamma8inv(c) NeoGammaWLEDMethod::rawInverseGamma8(c) +[[gnu::hot, gnu::pure]] uint32_t color_blend(uint32_t c1, uint32_t c2 , uint8_t blend); +inline uint32_t color_blend16(uint32_t c1, uint32_t c2, uint16_t b) { return color_blend(c1, c2, b >> 8); }; +[[gnu::hot, gnu::pure]] uint32_t color_add(uint32_t, uint32_t, bool preserveCR = false); +[[gnu::hot, gnu::pure]] uint32_t color_fade(uint32_t c1, uint8_t amount, bool video=false); +[[gnu::hot, gnu::pure]] uint32_t ColorFromPaletteWLED(const CRGBPalette16 &pal, unsigned index, uint8_t brightness = (uint8_t)255U, TBlendType blendType = LINEARBLEND); +CRGBPalette16 generateHarmonicRandomPalette(const CRGBPalette16 &basepalette); CRGBPalette16 generateRandomPalette(); +void loadCustomPalettes(); +extern std::vector customPalettes; +inline size_t getPaletteCount() { return 13 + GRADIENT_PALETTE_COUNT + customPalettes.size(); } inline uint32_t colorFromRgbw(byte* rgbw) { return uint32_t((byte(rgbw[3]) << 24) | (byte(rgbw[0]) << 16) | (byte(rgbw[1]) << 8) | (byte(rgbw[2]))); } -void colorHStoRGB(uint16_t hue, byte sat, byte* rgb); //hue, sat to rgb +void hsv2rgb(const CHSV32& hsv, uint32_t& rgb); +void colorHStoRGB(uint16_t hue, byte sat, byte* rgb); +void rgb2hsv(const uint32_t rgb, CHSV32& hsv); +inline CHSV rgb2hsv(const CRGB c) { CHSV32 hsv; rgb2hsv((uint32_t((byte(c.r) << 16) | (byte(c.g) << 8) | (byte(c.b)))), hsv); return CHSV(hsv); } // CRGB to hsv void colorKtoRGB(uint16_t kelvin, byte* rgb); void colorCTtoRGB(uint16_t mired, byte* rgb); //white spectrum to rgb void colorXYtoRGB(float x, float y, byte* rgb); // only defined if huesync disabled TODO -void colorRGBtoXY(byte* rgb, float* xy); // only defined if huesync disabled TODO -void colorFromDecOrHexString(byte* rgb, char* in); +void colorRGBtoXY(const byte* rgb, float* xy); // only defined if huesync disabled TODO +void colorFromDecOrHexString(byte* rgb, const char* in); bool colorFromHexString(byte* rgb, const char* in); uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb); uint16_t approximateKelvinFromRGB(uint32_t rgb); void setRandomColor(byte* rgb); -//dmx.cpp -void initDMX(); -void handleDMX(); +//dmx_output.cpp +void initDMXOutput(); +void handleDMXOutput(); + +//dmx_input.cpp +void initDMXInput(); +void handleDMXInput(); //e131.cpp void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol); +void handleDMXData(uint16_t uni, uint16_t dmxChannels, uint8_t* e131_data, uint8_t mde, uint8_t previousUniverses); void handleArtnetPollReply(IPAddress ipAddress); void prepareArtnetPollReply(ArtPollReply* reply); void sendArtnetPollReply(ArtPollReply* reply, IPAddress ipAddress, uint16_t portAddress); //file.cpp bool handleFileRead(AsyncWebServerRequest*, String path); -bool writeObjectToFileUsingId(const char* file, uint16_t id, JsonDocument* content); -bool writeObjectToFile(const char* file, const char* key, JsonDocument* content); -bool readObjectFromFileUsingId(const char* file, uint16_t id, JsonDocument* dest); -bool readObjectFromFile(const char* file, const char* key, JsonDocument* dest); +bool writeObjectToFileUsingId(const char* file, uint16_t id, const JsonDocument* content); +bool writeObjectToFile(const char* file, const char* key, const JsonDocument* content); +bool readObjectFromFileUsingId(const char* file, uint16_t id, JsonDocument* dest, const JsonDocument* filter = nullptr); +bool readObjectFromFile(const char* file, const char* key, JsonDocument* dest, const JsonDocument* filter = nullptr); void updateFSInfo(); void closeFile(); -inline bool writeObjectToFileUsingId(const String &file, uint16_t id, JsonDocument* content) { return writeObjectToFileUsingId(file.c_str(), id, content); }; -inline bool writeObjectToFile(const String &file, const char* key, JsonDocument* content) { return writeObjectToFile(file.c_str(), key, content); }; -inline bool readObjectFromFileUsingId(const String &file, uint16_t id, JsonDocument* dest) { return readObjectFromFileUsingId(file.c_str(), id, dest); }; -inline bool readObjectFromFile(const String &file, const char* key, JsonDocument* dest) { return readObjectFromFile(file.c_str(), key, dest); }; +inline bool writeObjectToFileUsingId(const String &file, uint16_t id, const JsonDocument* content) { return writeObjectToFileUsingId(file.c_str(), id, content); }; +inline bool writeObjectToFile(const String &file, const char* key, const JsonDocument* content) { return writeObjectToFile(file.c_str(), key, content); }; +inline bool readObjectFromFileUsingId(const String &file, uint16_t id, JsonDocument* dest, const JsonDocument* filter = nullptr) { return readObjectFromFileUsingId(file.c_str(), id, dest); }; +inline bool readObjectFromFile(const String &file, const char* key, JsonDocument* dest, const JsonDocument* filter = nullptr) { return readObjectFromFile(file.c_str(), key, dest); }; //hue.cpp void handleHue(); @@ -126,6 +231,18 @@ void onHueConnect(void* arg, AsyncClient* client); void sendHuePoll(); void onHueData(void* arg, AsyncClient* client, void *data, size_t len); +//image_loader.cpp +class Segment; +#ifdef WLED_ENABLE_GIF +bool fileSeekCallback(unsigned long position); +unsigned long filePositionCallback(void); +int fileReadCallback(void); +int fileReadBlockCallback(void * buffer, int numberOfBytes); +int fileSizeCallback(void); +byte renderImageToSegment(Segment &seg); +void endImagePlayback(Segment* seg); +#endif + //improv.cpp enum ImprovRPCType { Command_Wifi = 0x01, @@ -151,15 +268,13 @@ void handleIR(); #include "ESPAsyncWebServer.h" #include "src/dependencies/json/ArduinoJson-v6.h" #include "src/dependencies/json/AsyncJson-v6.h" -#include "FX.h" -bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0); bool deserializeState(JsonObject root, byte callMode = CALL_MODE_DIRECT_CHANGE, byte presetId = 0); -void serializeSegment(JsonObject& root, Segment& seg, byte id, bool forPreset = false, bool segmentBounds = true); +void serializeSegment(const JsonObject& root, const Segment& seg, byte id, bool forPreset = false, bool segmentBounds = true); void serializeState(JsonObject root, bool forPreset = false, bool includeBri = true, bool segmentBounds = true, bool selectedSegmentsOnly = false); void serializeInfo(JsonObject root); -void serializeModeNames(JsonArray root); -void serializeModeData(JsonArray root); +void serializeModeNames(JsonArray arr); +void serializeModeData(JsonArray fxdata); void serveJson(AsyncWebServerRequest* request); #ifdef WLED_ENABLE_JSONLIVE bool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient = 0); @@ -167,8 +282,8 @@ bool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient = 0); //led.cpp void setValuesFromSegment(uint8_t s); -void setValuesFromMainSeg(); -void setValuesFromFirstSelectedSeg(); +#define setValuesFromMainSeg() setValuesFromSegment(strip.getMainSegmentId()) +#define setValuesFromFirstSelectedSeg() setValuesFromSegment(strip.getFirstSelectedSegId()) void toggleOnOff(); void applyBri(); void applyFinalBri(); @@ -218,6 +333,7 @@ void serializePlaylist(JsonObject obj); //presets.cpp const char *getPresetsFileName(bool persistent = true); +bool presetNeedsSaving(); void initPresetsFile(); void handlePresets(); bool applyPreset(byte index, byte callMode = CALL_MODE_DIRECT_CHANGE); @@ -230,7 +346,8 @@ void deletePreset(byte index); bool getPresetName(byte index, String& name); //remote.cpp -void handleRemote(uint8_t *data, size_t len); +void handleWiZdata(uint8_t *incomingData, size_t len); +void handleRemote(); //set.cpp bool isAsterisksOnly(const char* str, byte maxLen); @@ -239,7 +356,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply=tru //udp.cpp void notify(byte callMode, bool followUp=false); -uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, uint8_t *buffer, uint8_t bri=255, bool isRGBW=false); +uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const uint8_t* buffer, uint8_t bri=255, bool isRGBW=false); void realtimeLock(uint32_t timeoutMs, byte md = REALTIME_MODE_GENERIC); void exitRealtime(); void handleNotifications(); @@ -252,7 +369,12 @@ void espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rs #endif //network.cpp -int getSignalQuality(int rssi); +bool initEthernet(); // result is informational +int getSignalQuality(int rssi); +void fillMAC2Str(char *str, const uint8_t *mac); +void fillStr2MAC(uint8_t *mac, const char *str); +int findWiFi(bool doScan = false); +bool isWiFiConfigured(); void WiFiEvent(WiFiEvent_t event); //um_manager.cpp @@ -293,7 +415,7 @@ class Usermod { protected: um_data_t *um_data; // um_data should be allocated using new in (derived) Usermod's setup() or constructor public: - Usermod() { um_data = nullptr; } + Usermod() : um_data(nullptr) {}; virtual ~Usermod() { if (um_data) delete um_data; } virtual void setup() = 0; // pure virtual, has to be overriden virtual void loop() = 0; // pure virtual, has to be overriden @@ -329,40 +451,34 @@ class Usermod { #endif }; -class UsermodManager { - private: - static Usermod* ums[WLED_MAX_USERMODS]; - static byte numMods; - - public: - static void loop(); - static void handleOverlayDraw(); - static bool handleButton(uint8_t b); - static bool getUMData(um_data_t **um_data, uint8_t mod_id = USERMOD_ID_RESERVED); // USERMOD_ID_RESERVED will poll all usermods - static void setup(); - static void connected(); - static void appendConfigData(Print&); - static void addToJsonState(JsonObject& obj); - static void addToJsonInfo(JsonObject& obj); - static void readFromJsonState(JsonObject& obj); - static void addToConfig(JsonObject& obj); - static bool readFromConfig(JsonObject& obj); +namespace UsermodManager { + void loop(); + void handleOverlayDraw(); + bool handleButton(uint8_t b); + bool getUMData(um_data_t **um_data, uint8_t mod_id = USERMOD_ID_RESERVED); // USERMOD_ID_RESERVED will poll all usermods + void setup(); + void connected(); + void appendConfigData(Print&); + void addToJsonState(JsonObject& obj); + void addToJsonInfo(JsonObject& obj); + void readFromJsonState(JsonObject& obj); + void addToConfig(JsonObject& obj); + bool readFromConfig(JsonObject& obj); #ifndef WLED_DISABLE_MQTT - static void onMqttConnect(bool sessionPresent); - static bool onMqttMessage(char* topic, char* payload); + void onMqttConnect(bool sessionPresent); + bool onMqttMessage(char* topic, char* payload); #endif #ifndef WLED_DISABLE_ESPNOW - static bool onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len); + bool onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len); #endif - static void onUpdateBegin(bool); - static void onStateChange(uint8_t); - static bool add(Usermod* um); - static Usermod* lookup(uint16_t mod_id); - static inline byte getModCount() {return numMods;}; + void onUpdateBegin(bool); + void onStateChange(uint8_t); + Usermod* lookup(uint16_t mod_id); + size_t getModCount(); }; -//usermods_list.cpp -void registerUsermods(); +// Register usermods by building a static list via a linker section +#define REGISTER_USERMOD(x) Usermod* const um_##x __attribute__((__section__(".dtors.tbl.usermods.1"), used)) = &x //usermod.cpp void userSetup(); @@ -370,19 +486,28 @@ void userConnected(); void userLoop(); //util.cpp -int getNumVal(const String* req, uint16_t pos); -void parseNumber(const char* str, byte* val, byte minv=0, byte maxv=255); -bool getVal(JsonVariant elem, byte* val, byte minv=0, byte maxv=255); // getVal supports inc/decrementing and random ("X~Y(r|~[w][-][Z])" form) -bool getBoolVal(JsonVariant elem, bool dflt); -bool updateVal(const char* req, const char* key, byte* val, byte minv=0, byte maxv=255); +#ifdef ESP8266 +#define HW_RND_REGISTER RANDOM_REG32 +#else // ESP32 family +#include "soc/wdev_reg.h" +#define HW_RND_REGISTER REG_READ(WDEV_RND_REG) +#endif +#define inoise8 perlin8 // fastled legacy alias +#define inoise16 perlin16 // fastled legacy alias +#define hex2int(a) (((a)>='0' && (a)<='9') ? (a)-'0' : ((a)>='A' && (a)<='F') ? (a)-'A'+10 : ((a)>='a' && (a)<='f') ? (a)-'a'+10 : 0) +[[gnu::pure]] int getNumVal(const String &req, uint16_t pos); +void parseNumber(const char* str, byte &val, byte minv=0, byte maxv=255); +bool getVal(JsonVariant elem, byte &val, byte vmin=0, byte vmax=255); // getVal supports inc/decrementing and random ("X~Y(r|[w]~[-][Z])" form) +[[gnu::pure]] bool getBoolVal(const JsonVariant &elem, bool dflt); +bool updateVal(const char* req, const char* key, byte &val, byte minv=0, byte maxv=255); size_t printSetFormCheckbox(Print& settingsScript, const char* key, int val); size_t printSetFormValue(Print& settingsScript, const char* key, int val); size_t printSetFormValue(Print& settingsScript, const char* key, const char* val); size_t printSetFormIndex(Print& settingsScript, const char* key, int index); size_t printSetClassElementHTML(Print& settingsScript, const char* key, const int index, const char* val); void prepareHostname(char* hostname); -bool isAsterisksOnly(const char* str, byte maxLen); -bool requestJSONBufferLock(uint8_t module=255); +[[gnu::pure]] bool isAsterisksOnly(const char* str, byte maxLen); +bool requestJSONBufferLock(uint8_t moduleID=255); void releaseJSONBufferLock(); uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLen); uint8_t extractModeSlider(uint8_t mode, uint8_t slider, char *dest, uint8_t maxLen, uint8_t *var = nullptr); @@ -394,8 +519,58 @@ uint16_t beatsin16_t(accum88 beats_per_minute, uint16_t lowest = 0, uint16_t hig uint8_t beatsin8_t(accum88 beats_per_minute, uint8_t lowest = 0, uint8_t highest = 255, uint32_t timebase = 0, uint8_t phase_offset = 0); um_data_t* simulateSound(uint8_t simulationId); void enumerateLedmaps(); -uint8_t get_random_wheel_index(uint8_t pos); -float mapf(float x, float in_min, float in_max, float out_min, float out_max); +[[gnu::hot]] uint8_t get_random_wheel_index(uint8_t pos); +[[gnu::hot, gnu::pure]] float mapf(float x, float in_min, float in_max, float out_min, float out_max); +uint32_t hashInt(uint32_t s); +int32_t perlin1D_raw(uint32_t x, bool is16bit = false); +int32_t perlin2D_raw(uint32_t x, uint32_t y, bool is16bit = false); +int32_t perlin3D_raw(uint32_t x, uint32_t y, uint32_t z, bool is16bit = false); +uint16_t perlin16(uint32_t x); +uint16_t perlin16(uint32_t x, uint32_t y); +uint16_t perlin16(uint32_t x, uint32_t y, uint32_t z); +uint8_t perlin8(uint16_t x); +uint8_t perlin8(uint16_t x, uint16_t y); +uint8_t perlin8(uint16_t x, uint16_t y, uint16_t z); + +// fast (true) random numbers using hardware RNG, all functions return values in the range lowerlimit to upperlimit-1 +// note: for true random numbers with high entropy, do not call faster than every 200ns (5MHz) +// tests show it is still highly random reading it quickly in a loop (better than fastled PRNG) +// for 8bit and 16bit random functions: no limit check is done for best speed +// 32bit inputs are used for speed and code size, limits don't work if inverted or out of range +// inlining does save code size except for random(a,b) and 32bit random with limits +#define random hw_random // replace arduino random() +inline uint32_t hw_random() { return HW_RND_REGISTER; }; +uint32_t hw_random(uint32_t upperlimit); // not inlined for code size +int32_t hw_random(int32_t lowerlimit, int32_t upperlimit); +inline uint16_t hw_random16() { return HW_RND_REGISTER; }; +inline uint16_t hw_random16(uint32_t upperlimit) { return (hw_random16() * upperlimit) >> 16; }; // input range 0-65535 (uint16_t) +inline int16_t hw_random16(int32_t lowerlimit, int32_t upperlimit) { int32_t range = upperlimit - lowerlimit; return lowerlimit + hw_random16(range); }; // signed limits, use int16_t ranges +inline uint8_t hw_random8() { return HW_RND_REGISTER; }; +inline uint8_t hw_random8(uint32_t upperlimit) { return (hw_random8() * upperlimit) >> 8; }; // input range 0-255 +inline uint8_t hw_random8(uint32_t lowerlimit, uint32_t upperlimit) { uint32_t range = upperlimit - lowerlimit; return lowerlimit + hw_random8(range); }; // input range 0-255 + +// PSRAM allocation wrappers +#ifndef ESP8266 +extern "C" { + void *p_malloc(size_t); // prefer PSRAM over DRAM + void *p_calloc(size_t, size_t); // prefer PSRAM over DRAM + void *p_realloc(void *, size_t); // prefer PSRAM over DRAM + inline void p_free(void *ptr) { heap_caps_free(ptr); } + void *d_malloc(size_t); // prefer DRAM over PSRAM + void *d_calloc(size_t, size_t); // prefer DRAM over PSRAM + void *d_realloc(void *, size_t); // prefer DRAM over PSRAM + inline void d_free(void *ptr) { heap_caps_free(ptr); } +} +#else +#define p_malloc malloc +#define p_calloc calloc +#define p_realloc realloc +#define p_free free +#define d_malloc malloc +#define d_calloc calloc +#define d_realloc realloc +#define d_free free +#endif // RAII guard class for the JSON Buffer lock // Modeled after std::lock_guard @@ -438,6 +613,7 @@ float asin_t(float x); template T atan_t(T x); float floor_t(float x); float fmod_t(float num, float denom); +uint32_t sqrt32_bw(uint32_t x); #define sin_t sin_approx #define cos_t cos_approx #define tan_t tan_approx diff --git a/wled00/file.cpp b/wled00/file.cpp index bc346720..c1960e61 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -39,7 +39,7 @@ void closeFile() { uint32_t s = millis(); #endif f.close(); - DEBUGFS_PRINTF("took %d ms\n", millis() - s); + DEBUGFS_PRINTF("took %lu ms\n", millis() - s); doCloseFile = false; } @@ -69,14 +69,14 @@ static bool bufferedFind(const char *target, bool fromStart = true) { if(buf[count] == target[index]) { if(++index >= targetLen) { // return true if all chars in the target match f.seek((f.position() - bufsize) + count +1); - DEBUGFS_PRINTF("Found at pos %d, took %d ms", f.position(), millis() - s); + DEBUGFS_PRINTF("Found at pos %d, took %lu ms", f.position(), millis() - s); return true; } } count++; } } - DEBUGFS_PRINTF("No match, took %d ms\n", millis() - s); + DEBUGFS_PRINTF("No match, took %lu ms\n", millis() - s); return false; } @@ -111,7 +111,7 @@ static bool bufferedFindSpace(size_t targetLen, bool fromStart = true) { f.seek((f.position() - bufsize) + count +1 - targetLen); knownLargestSpace = MAX_SPACE; //there may be larger spaces after, so we don't know } - DEBUGFS_PRINTF("Found at pos %d, took %d ms", f.position(), millis() - s); + DEBUGFS_PRINTF("Found at pos %d, took %lu ms", f.position(), millis() - s); return true; } } else { @@ -125,7 +125,7 @@ static bool bufferedFindSpace(size_t targetLen, bool fromStart = true) { count++; } } - DEBUGFS_PRINTF("No match, took %d ms\n", millis() - s); + DEBUGFS_PRINTF("No match, took %lu ms\n", millis() - s); return false; } @@ -151,13 +151,13 @@ static bool bufferedFindObjectEnd() { if (buf[count] == '}') objDepth--; if (objDepth == 0) { f.seek((f.position() - bufsize) + count +1); - DEBUGFS_PRINTF("} at pos %d, took %d ms", f.position(), millis() - s); + DEBUGFS_PRINTF("} at pos %d, took %lu ms", f.position(), millis() - s); return true; } count++; } } - DEBUGFS_PRINTF("No match, took %d ms\n", millis() - s); + DEBUGFS_PRINTF("No match, took %lu ms\n", millis() - s); return false; } @@ -176,7 +176,7 @@ static void writeSpace(size_t l) if (knownLargestSpace < l) knownLargestSpace = l; } -bool appendObjectToFile(const char* key, JsonDocument* content, uint32_t s, uint32_t contentLen = 0) +static bool appendObjectToFile(const char* key, const JsonDocument* content, uint32_t s, uint32_t contentLen = 0) { #ifdef WLED_DEBUG_FS DEBUGFS_PRINTLN(F("Append")); @@ -203,7 +203,7 @@ bool appendObjectToFile(const char* key, JsonDocument* content, uint32_t s, uint if (f.position() > 2) f.write(','); //add comma if not first object f.print(key); serializeJson(*content, f); - DEBUGFS_PRINTF("Inserted, took %d ms (total %d)", millis() - s1, millis() - s); + DEBUGFS_PRINTF("Inserted, took %lu ms (total %lu)", millis() - s1, millis() - s); doCloseFile = true; return true; } @@ -251,18 +251,18 @@ bool appendObjectToFile(const char* key, JsonDocument* content, uint32_t s, uint f.write('}'); doCloseFile = true; - DEBUGFS_PRINTF("Appended, took %d ms (total %d)", millis() - s1, millis() - s); + DEBUGFS_PRINTF("Appended, took %lu ms (total %lu)", millis() - s1, millis() - s); return true; } -bool writeObjectToFileUsingId(const char* file, uint16_t id, JsonDocument* content) +bool writeObjectToFileUsingId(const char* file, uint16_t id, const JsonDocument* content) { char objKey[10]; sprintf(objKey, "\"%d\":", id); return writeObjectToFile(file, objKey, content); } -bool writeObjectToFile(const char* file, const char* key, JsonDocument* content) +bool writeObjectToFile(const char* file, const char* key, const JsonDocument* content) { uint32_t s = 0; //timing #ifdef WLED_DEBUG_FS @@ -321,19 +321,19 @@ bool writeObjectToFile(const char* file, const char* key, JsonDocument* content) } doCloseFile = true; - DEBUGFS_PRINTF("Replaced/deleted, took %d ms\n", millis() - s); + DEBUGFS_PRINTF("Replaced/deleted, took %lu ms\n", millis() - s); return true; } -bool readObjectFromFileUsingId(const char* file, uint16_t id, JsonDocument* dest) +bool readObjectFromFileUsingId(const char* file, uint16_t id, JsonDocument* dest, const JsonDocument* filter) { char objKey[10]; sprintf(objKey, "\"%d\":", id); - return readObjectFromFile(file, objKey, dest); + return readObjectFromFile(file, objKey, dest, filter); } //if the key is a nullptr, deserialize entire object -bool readObjectFromFile(const char* file, const char* key, JsonDocument* dest) +bool readObjectFromFile(const char* file, const char* key, JsonDocument* dest, const JsonDocument* filter) { if (doCloseFile) closeFile(); #ifdef WLED_DEBUG_FS @@ -352,10 +352,11 @@ bool readObjectFromFile(const char* file, const char* key, JsonDocument* dest) return false; } - deserializeJson(*dest, f); + if (filter) deserializeJson(*dest, f, DeserializationOption::Filter(*filter)); + else deserializeJson(*dest, f); f.close(); - DEBUGFS_PRINTF("Read, took %d ms\n", millis() - s); + DEBUGFS_PRINTF("Read, took %lu ms\n", millis() - s); return true; } @@ -391,7 +392,7 @@ static const uint8_t *getPresetCache(size_t &size) { if ((presetsModifiedTime != presetsCachedTime) || (presetsCachedValidate != cacheInvalidate)) { if (presetsCached) { - free(presetsCached); + p_free(presetsCached); presetsCached = nullptr; } } @@ -402,7 +403,7 @@ static const uint8_t *getPresetCache(size_t &size) { presetsCachedTime = presetsModifiedTime; presetsCachedValidate = cacheInvalidate; presetsCachedSize = 0; - presetsCached = (uint8_t*)ps_malloc(file.size() + 1); + presetsCached = (uint8_t*)p_malloc(file.size() + 1); if (presetsCached) { presetsCachedSize = file.size(); file.read(presetsCached, presetsCachedSize); @@ -418,7 +419,7 @@ static const uint8_t *getPresetCache(size_t &size) { #endif bool handleFileRead(AsyncWebServerRequest* request, String path){ - DEBUG_PRINT(F("WS FileRead: ")); DEBUG_PRINTLN(path); + DEBUGFS_PRINT(F("WS FileRead: ")); DEBUGFS_PRINTLN(path); if(path.endsWith("/")) path += "index.htm"; if(path.indexOf(F("sec")) > -1) return false; #ifdef ARDUINO_ARCH_ESP32 diff --git a/wled00/hue.cpp b/wled00/hue.cpp index 950c5489..d5fcb7cb 100644 --- a/wled00/hue.cpp +++ b/wled00/hue.cpp @@ -195,9 +195,9 @@ void onHueData(void* arg, AsyncClient* client, void *data, size_t len) { switch(hueColormode) { - case 1: if (hueX != hueXLast || hueY != hueYLast) colorXYtoRGB(hueX,hueY,col); hueXLast = hueX; hueYLast = hueY; break; - case 2: if (hueHue != hueHueLast || hueSat != hueSatLast) colorHStoRGB(hueHue,hueSat,col); hueHueLast = hueHue; hueSatLast = hueSat; break; - case 3: if (hueCt != hueCtLast) colorCTtoRGB(hueCt,col); hueCtLast = hueCt; break; + case 1: if (hueX != hueXLast || hueY != hueYLast) colorXYtoRGB(hueX,hueY,colPri); hueXLast = hueX; hueYLast = hueY; break; + case 2: if (hueHue != hueHueLast || hueSat != hueSatLast) colorHStoRGB(hueHue,hueSat,colPri); hueHueLast = hueHue; hueSatLast = hueSat; break; + case 3: if (hueCt != hueCtLast) colorCTtoRGB(hueCt,colPri); hueCtLast = hueCt; break; } } hueReceived = true; diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp new file mode 100644 index 00000000..aa4ae2e1 --- /dev/null +++ b/wled00/image_loader.cpp @@ -0,0 +1,144 @@ +#include "wled.h" + +#ifdef WLED_ENABLE_GIF + +#include "GifDecoder.h" + + +/* + * Functions to render images from filesystem to segments, used by the "Image" effect + */ + +File file; +char lastFilename[34] = "/"; +GifDecoder<320,320,12,true> decoder; +bool gifDecodeFailed = false; +unsigned long lastFrameDisplayTime = 0, currentFrameDelay = 0; + +bool fileSeekCallback(unsigned long position) { + return file.seek(position); +} + +unsigned long filePositionCallback(void) { + return file.position(); +} + +int fileReadCallback(void) { + return file.read(); +} + +int fileReadBlockCallback(void * buffer, int numberOfBytes) { + return file.read((uint8_t*)buffer, numberOfBytes); +} + +int fileSizeCallback(void) { + return file.size(); +} + +bool openGif(const char *filename) { + file = WLED_FS.open(filename, "r"); + + if (!file) return false; + return true; +} + +Segment* activeSeg; +uint16_t gifWidth, gifHeight; + +void screenClearCallback(void) { + activeSeg->fill(0); +} + +void updateScreenCallback(void) {} + +void drawPixelCallback(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { + // simple nearest-neighbor scaling + int16_t outY = y * activeSeg->height() / gifHeight; + int16_t outX = x * activeSeg->width() / gifWidth; + // set multiple pixels if upscaling + for (int16_t i = 0; i < (activeSeg->width()+(gifWidth-1)) / gifWidth; i++) { + for (int16_t j = 0; j < (activeSeg->height()+(gifHeight-1)) / gifHeight; j++) { + activeSeg->setPixelColorXY(outX + i, outY + j, gamma8(red), gamma8(green), gamma8(blue)); + } + } +} + +#define IMAGE_ERROR_NONE 0 +#define IMAGE_ERROR_NO_NAME 1 +#define IMAGE_ERROR_SEG_LIMIT 2 +#define IMAGE_ERROR_UNSUPPORTED_FORMAT 3 +#define IMAGE_ERROR_FILE_MISSING 4 +#define IMAGE_ERROR_DECODER_ALLOC 5 +#define IMAGE_ERROR_GIF_DECODE 6 +#define IMAGE_ERROR_FRAME_DECODE 7 +#define IMAGE_ERROR_WAITING 254 +#define IMAGE_ERROR_PREV 255 + +// renders an image (.gif only; .bmp and .fseq to be added soon) from FS to a segment +byte renderImageToSegment(Segment &seg) { + if (!seg.name) return IMAGE_ERROR_NO_NAME; + // disable during effect transition, causes flickering, multiple allocations and depending on image, part of old FX remaining + //if (seg.mode != seg.currentMode()) return IMAGE_ERROR_WAITING; + if (activeSeg && activeSeg != &seg) return IMAGE_ERROR_SEG_LIMIT; // only one segment at a time + activeSeg = &seg; + + if (strncmp(lastFilename +1, seg.name, 32) != 0) { // segment name changed, load new image + strncpy(lastFilename +1, seg.name, 32); + gifDecodeFailed = false; + if (strcmp(lastFilename + strlen(lastFilename) - 4, ".gif") != 0) { + gifDecodeFailed = true; + return IMAGE_ERROR_UNSUPPORTED_FORMAT; + } + if (file) file.close(); + openGif(lastFilename); + if (!file) { gifDecodeFailed = true; return IMAGE_ERROR_FILE_MISSING; } + decoder.setScreenClearCallback(screenClearCallback); + decoder.setUpdateScreenCallback(updateScreenCallback); + decoder.setDrawPixelCallback(drawPixelCallback); + decoder.setFileSeekCallback(fileSeekCallback); + decoder.setFilePositionCallback(filePositionCallback); + decoder.setFileReadCallback(fileReadCallback); + decoder.setFileReadBlockCallback(fileReadBlockCallback); + decoder.setFileSizeCallback(fileSizeCallback); + decoder.alloc(); + DEBUG_PRINTLN(F("Starting decoding")); + if(decoder.startDecoding() < 0) { gifDecodeFailed = true; return IMAGE_ERROR_GIF_DECODE; } + DEBUG_PRINTLN(F("Decoding started")); + } + + if (gifDecodeFailed) return IMAGE_ERROR_PREV; + if (!file) { gifDecodeFailed = true; return IMAGE_ERROR_FILE_MISSING; } + //if (!decoder) { gifDecodeFailed = true; return IMAGE_ERROR_DECODER_ALLOC; } + + // speed 0 = half speed, 128 = normal, 255 = full FX FPS + // TODO: 0 = 4x slow, 64 = 2x slow, 128 = normal, 192 = 2x fast, 255 = 4x fast + uint32_t wait = currentFrameDelay * 2 - seg.speed * currentFrameDelay / 128; + + // TODO consider handling this on FX level with a different frametime, but that would cause slow gifs to speed up during transitions + if (millis() - lastFrameDisplayTime < wait) return IMAGE_ERROR_WAITING; + + decoder.getSize(&gifWidth, &gifHeight); + + int result = decoder.decodeFrame(false); + if (result < 0) { gifDecodeFailed = true; return IMAGE_ERROR_FRAME_DECODE; } + + currentFrameDelay = decoder.getFrameDelay_ms(); + unsigned long tooSlowBy = (millis() - lastFrameDisplayTime) - wait; // if last frame was longer than intended, compensate + currentFrameDelay = tooSlowBy > currentFrameDelay ? 0 : currentFrameDelay - tooSlowBy; + lastFrameDisplayTime = millis(); + + return IMAGE_ERROR_NONE; +} + +void endImagePlayback(Segment *seg) { + DEBUG_PRINTLN(F("Image playback end called")); + if (!activeSeg || activeSeg != seg) return; + if (file) file.close(); + decoder.dealloc(); + gifDecodeFailed = false; + activeSeg = nullptr; + lastFilename[1] = '\0'; + DEBUG_PRINTLN(F("Image playback ended")); +} + +#endif \ No newline at end of file diff --git a/wled00/improv.cpp b/wled00/improv.cpp index 197148b2..0bc7a669 100644 --- a/wled00/improv.cpp +++ b/wled00/improv.cpp @@ -272,5 +272,5 @@ void parseWiFiCommand(char* rpcData) { improvActive = 2; forceReconnect = true; - serializeConfig(); + serializeConfigToFS(); } \ No newline at end of file diff --git a/wled00/ir.cpp b/wled00/ir.cpp index e4541cd9..b2fec76f 100644 --- a/wled00/ir.cpp +++ b/wled00/ir.cpp @@ -129,7 +129,7 @@ static void changeEffectSpeed(int8_t amount) } else { // if Effect == "solid Color", change the hue of the primary color Segment& sseg = irApplyToAllSelected ? strip.getFirstSelectedSeg() : strip.getMainSegment(); CRGB fastled_col = CRGB(sseg.colors[0]); - CHSV prim_hsv = rgb2hsv_approximate(fastled_col); + CHSV prim_hsv = rgb2hsv(fastled_col); int16_t new_val = (int16_t)prim_hsv.h + amount; if (new_val > 255) new_val -= 255; // roll-over if bigger than 255 if (new_val < 0) new_val += 255; // roll-over if smaller than 0 @@ -173,7 +173,7 @@ static void changeEffectIntensity(int8_t amount) } else { // if Effect == "solid Color", change the saturation of the primary color Segment& sseg = irApplyToAllSelected ? strip.getFirstSelectedSeg() : strip.getMainSegment(); CRGB fastled_col = CRGB(sseg.colors[0]); - CHSV prim_hsv = rgb2hsv_approximate(fastled_col); + CHSV prim_hsv = rgb2hsv(fastled_col); int16_t new_val = (int16_t) prim_hsv.s + amount; prim_hsv.s = (byte)constrain(new_val,0,255); // constrain to 0-255 hsv2rgb_rainbow(prim_hsv, fastled_col); @@ -425,8 +425,8 @@ static void decodeIR44(uint32_t code) case IR44_COLDWHITE2 : changeColor(COLOR_COLDWHITE2, 255); changeEffect(FX_MODE_STATIC); break; case IR44_REDPLUS : changeEffect(relativeChange(effectCurrent, 1, 0, strip.getModeCount() -1)); break; case IR44_REDMINUS : changeEffect(relativeChange(effectCurrent, -1, 0, strip.getModeCount() -1)); break; - case IR44_GREENPLUS : changePalette(relativeChange(effectPalette, 1, 0, strip.getPaletteCount() -1)); break; - case IR44_GREENMINUS : changePalette(relativeChange(effectPalette, -1, 0, strip.getPaletteCount() -1)); break; + case IR44_GREENPLUS : changePalette(relativeChange(effectPalette, 1, 0, getPaletteCount() -1)); break; + case IR44_GREENMINUS : changePalette(relativeChange(effectPalette, -1, 0, getPaletteCount() -1)); break; case IR44_BLUEPLUS : changeEffectIntensity( 16); break; case IR44_BLUEMINUS : changeEffectIntensity(-16); break; case IR44_QUICK : changeEffectSpeed( 16); break; @@ -435,7 +435,7 @@ static void decodeIR44(uint32_t code) case IR44_DIY2 : presetFallback(2, FX_MODE_BREATH, 0); break; case IR44_DIY3 : presetFallback(3, FX_MODE_FIRE_FLICKER, 0); break; case IR44_DIY4 : presetFallback(4, FX_MODE_RAINBOW, 0); break; - case IR44_DIY5 : presetFallback(5, FX_MODE_METEOR_SMOOTH, 0); break; + case IR44_DIY5 : presetFallback(5, FX_MODE_METEOR, 0); break; case IR44_DIY6 : presetFallback(6, FX_MODE_RAIN, 0); break; case IR44_AUTO : changeEffect(FX_MODE_STATIC); break; case IR44_FLASH : changeEffect(FX_MODE_PALETTE); break; @@ -484,7 +484,7 @@ static void decodeIR6(uint32_t code) case IR6_CHANNEL_UP: incBrightness(); break; case IR6_CHANNEL_DOWN: decBrightness(); break; case IR6_VOLUME_UP: changeEffect(relativeChange(effectCurrent, 1, 0, strip.getModeCount() -1)); break; - case IR6_VOLUME_DOWN: changePalette(relativeChange(effectPalette, 1, 0, strip.getPaletteCount() -1)); + case IR6_VOLUME_DOWN: changePalette(relativeChange(effectPalette, 1, 0, getPaletteCount() -1)); switch(lastIR6ColourIdx) { case 0: changeColor(COLOR_RED); break; case 1: changeColor(COLOR_REDDISH); break; @@ -530,7 +530,7 @@ static void decodeIR9(uint32_t code) /* This allows users to customize IR actions without the need to edit C code and compile. -From the https://github.com/Aircoookie/WLED/wiki/Infrared-Control page, download the starter +From the https://github.com/wled/WLED/wiki/Infrared-Control page, download the starter ir.json file that corresponds to the number of buttons on your remote. Many of the remotes with the same number of buttons emit the same codes, but will have different labels or colors. Once you edit the ir.json file, upload it to your controller @@ -593,7 +593,7 @@ static void decodeIRJson(uint32_t code) decBrightness(); } else if (cmdStr.startsWith(F("!presetF"))) { //!presetFallback uint8_t p1 = fdo["PL"] | 1; - uint8_t p2 = fdo["FX"] | random8(strip.getModeCount() -1); + uint8_t p2 = fdo["FX"] | hw_random8(strip.getModeCount() -1); uint8_t p3 = fdo["FP"] | 0; presetFallback(p1, p2, p3); } @@ -611,9 +611,15 @@ static void decodeIRJson(uint32_t code) handleSet(nullptr, cmdStr, false); // no stateUpdated() call here } } else { - // command is JSON object (TODO: currently will not handle irApplyToAllSelected correctly) - if (jsonCmdObj[F("psave")].isNull()) deserializeState(jsonCmdObj, CALL_MODE_BUTTON_PRESET); - else { + // command is JSON object + if (jsonCmdObj[F("psave")].isNull()) { + if (irApplyToAllSelected && jsonCmdObj["seg"].is()) { + JsonObject seg = jsonCmdObj["seg"][0]; // take 1st segment from array and use it to apply to all selected segments + seg.remove("id"); // remove segment ID if it exists + jsonCmdObj["seg"] = seg; // replace array with object + } + deserializeState(jsonCmdObj, CALL_MODE_BUTTON_PRESET); // **will call stateUpdated() with correct CALL_MODE** + } else { uint8_t psave = jsonCmdObj[F("psave")].as(); char pname[33]; sprintf_P(pname, PSTR("IR Preset %d"), psave); @@ -628,6 +634,7 @@ static void applyRepeatActions() { if (irEnabled == 8) { decodeIRJson(lastValidCode); + stateUpdated(CALL_MODE_BUTTON_PRESET); return; } else switch (lastRepeatableAction) { case ACTION_BRIGHT_UP : incBrightness(); stateUpdated(CALL_MODE_BUTTON); return; @@ -664,7 +671,7 @@ static void decodeIR(uint32_t code) if (irEnabled == 8) { // any remote configurable with ir.json file decodeIRJson(code); - stateUpdated(CALL_MODE_BUTTON); + stateUpdated(CALL_MODE_BUTTON_PRESET); return; } if (code > 0xFFFFFF) return; //invalid code diff --git a/wled00/json.cpp b/wled00/json.cpp index 5475aa8d..44146810 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -14,11 +14,62 @@ /* * JSON API (De)serialization */ +namespace { + typedef struct { + uint32_t colors[NUM_COLORS]; + uint16_t start; + uint16_t stop; + uint16_t offset; + uint16_t grouping; + uint16_t spacing; + uint16_t startY; + uint16_t stopY; + uint16_t options; + uint8_t mode; + uint8_t palette; + uint8_t opacity; + uint8_t speed; + uint8_t intensity; + uint8_t custom1; + uint8_t custom2; + uint8_t custom3; + bool check1; + bool check2; + bool check3; + } SegmentCopy; -bool deserializeSegment(JsonObject elem, byte it, byte presetId) + uint8_t differs(const Segment& b, const SegmentCopy& a) { + uint8_t d = 0; + if (a.start != b.start) d |= SEG_DIFFERS_BOUNDS; + if (a.stop != b.stop) d |= SEG_DIFFERS_BOUNDS; + if (a.offset != b.offset) d |= SEG_DIFFERS_GSO; + if (a.grouping != b.grouping) d |= SEG_DIFFERS_GSO; + if (a.spacing != b.spacing) d |= SEG_DIFFERS_GSO; + if (a.opacity != b.opacity) d |= SEG_DIFFERS_BRI; + if (a.mode != b.mode) d |= SEG_DIFFERS_FX; + if (a.speed != b.speed) d |= SEG_DIFFERS_FX; + if (a.intensity != b.intensity) d |= SEG_DIFFERS_FX; + if (a.palette != b.palette) d |= SEG_DIFFERS_FX; + if (a.custom1 != b.custom1) d |= SEG_DIFFERS_FX; + if (a.custom2 != b.custom2) d |= SEG_DIFFERS_FX; + if (a.custom3 != b.custom3) d |= SEG_DIFFERS_FX; + if (a.startY != b.startY) d |= SEG_DIFFERS_BOUNDS; + if (a.stopY != b.stopY) d |= SEG_DIFFERS_BOUNDS; + + //bit pattern: (msb first) + // set:2, sound:2, mapping:3, transposed, mirrorY, reverseY, [reset,] paused, mirrored, on, reverse, [selected] + if ((a.options & 0b1111111111011110U) != (b.options & 0b1111111111011110U)) d |= SEG_DIFFERS_OPT; + if ((a.options & 0x0001U) != (b.options & 0x0001U)) d |= SEG_DIFFERS_SEL; + for (unsigned i = 0; i < NUM_COLORS; i++) if (a.colors[i] != b.colors[i]) d |= SEG_DIFFERS_COL; + + return d; + } +} + +static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0) { byte id = elem["id"] | it; - if (id >= strip.getMaxSegments()) return false; + if (id >= WS2812FX::getMaxSegments()) return false; bool newSeg = false; int stop = elem["stop"] | -1; @@ -26,16 +77,37 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) // append segment if (id >= strip.getSegmentsNum()) { if (stop <= 0) return false; // ignore empty/inactive segments - strip.appendSegment(Segment(0, strip.getLengthTotal())); + strip.appendSegment(0, strip.getLengthTotal()); id = strip.getSegmentsNum()-1; // segments are added at the end of list newSeg = true; } //DEBUG_PRINTLN(F("-- JSON deserialize segment.")); Segment& seg = strip.getSegment(id); - //DEBUG_PRINTF_P(PSTR("-- Original segment: %p (%p)\n"), &seg, seg.data); - Segment prev = seg; //make a backup so we can tell if something changed (calling copy constructor) - //DEBUG_PRINTF_P(PSTR("-- Duplicate segment: %p (%p)\n"), &prev, prev.data); + // we do not want to make segment copy as it may use a lot of RAM (effect data and pixel buffer) + // so we will create a copy of segment options and compare it with original segment when done processing + SegmentCopy prev = { + {seg.colors[0], seg.colors[1], seg.colors[2]}, + seg.start, + seg.stop, + seg.offset, + seg.grouping, + seg.spacing, + seg.startY, + seg.stopY, + seg.options, + seg.mode, + seg.palette, + seg.opacity, + seg.speed, + seg.intensity, + seg.custom1, + seg.custom2, + seg.custom3, + seg.check1, + seg.check2, + seg.check3 + }; int start = elem["start"] | seg.start; if (stop < 0) { @@ -53,7 +125,7 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) elem.remove("rpt"); // remove for recursive call elem.remove("n"); // remove for recursive call unsigned len = stop - start; - for (size_t i=id+1; i= strip.getLengthTotal()) break; //TODO: add support for 2D @@ -67,46 +139,35 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) if (elem["n"]) { // name field exists - if (seg.name) { //clear old name - delete[] seg.name; - seg.name = nullptr; - } - const char * name = elem["n"].as(); - size_t len = 0; - if (name != nullptr) len = strlen(name); - if (len > 0) { - if (len > WLED_MAX_SEGNAME_LEN) len = WLED_MAX_SEGNAME_LEN; - seg.name = new char[len+1]; - if (seg.name) strlcpy(seg.name, name, WLED_MAX_SEGNAME_LEN+1); - } else { - // but is empty (already deleted above) - elem.remove("n"); - } + seg.setName(name); // will resolve empty and null correctly } else if (start != seg.start || stop != seg.stop) { // clearing or setting segment without name field - if (seg.name) { - delete[] seg.name; - seg.name = nullptr; - } + seg.clearName(); } - uint16_t grp = elem["grp"] | seg.grouping; - uint16_t spc = elem[F("spc")] | seg.spacing; - uint16_t of = seg.offset; - uint8_t soundSim = elem["si"] | seg.soundSim; - uint8_t map1D2D = elem["m12"] | seg.map1D2D; + uint16_t grp = elem["grp"] | seg.grouping; + uint16_t spc = elem[F("spc")] | seg.spacing; + uint16_t of = seg.offset; + uint8_t soundSim = elem["si"] | seg.soundSim; + uint8_t map1D2D = elem["m12"] | seg.map1D2D; + uint8_t set = elem[F("set")] | seg.set; + bool selected = getBoolVal(elem["sel"], seg.selected); + bool reverse = getBoolVal(elem["rev"], seg.reverse); + bool mirror = getBoolVal(elem["mi"] , seg.mirror); + #ifndef WLED_DISABLE_2D + bool reverse_y = getBoolVal(elem["rY"] , seg.reverse_y); + bool mirror_y = getBoolVal(elem["mY"] , seg.mirror_y); + bool transpose = getBoolVal(elem[F("tp")], seg.transpose); + #endif - if ((spc>0 && spc!=seg.spacing) || seg.map1D2D!=map1D2D) seg.fill(BLACK); // clear spacing gaps + // if segment's virtual dimensions change we need to restart effect (segment blending and PS rely on dimensions) + if (seg.mirror != mirror) seg.markForReset(); + #ifndef WLED_DISABLE_2D + if (seg.mirror_y != mirror_y || seg.transpose != transpose) seg.markForReset(); + #endif - seg.map1D2D = constrain(map1D2D, 0, 7); - seg.soundSim = constrain(soundSim, 0, 3); - - uint8_t set = elem[F("set")] | seg.set; - seg.set = constrain(set, 0, 3); - - int len = 1; - if (stop > start) len = stop - start; + int len = (stop > start) ? stop - start : 1; int offset = elem[F("of")] | INT32_MAX; if (offset != INT32_MAX) { int offsetAbs = abs(offset); @@ -117,7 +178,7 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) if (stop > start && of > len -1) of = len -1; // update segment (delete if necessary) - seg.setUp(start, stop, grp, spc, of, startY, stopY); // strip needs to be suspended for this to work without issues + seg.setGeometry(start, stop, grp, spc, of, startY, stopY, map1D2D); // strip needs to be suspended for this to work without issues if (newSeg) seg.refreshLightCapabilities(); // fix for #3403 @@ -127,8 +188,8 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) } byte segbri = seg.opacity; - if (getVal(elem["bri"], &segbri)) { - if (segbri > 0) seg.setOpacity(segbri); + if (getVal(elem["bri"], segbri)) { + if (segbri > 0) seg.setOpacity(segbri); // use transition seg.setOption(SEG_OPTION_ON, segbri); // use transition } @@ -184,13 +245,13 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) if (!colValid) continue; - seg.setColor(i, RGBW32(rgbw[0],rgbw[1],rgbw[2],rgbw[3])); + seg.setColor(i, RGBW32(rgbw[0],rgbw[1],rgbw[2],rgbw[3])); // use transition if (seg.mode == FX_MODE_STATIC) strip.trigger(); //instant refresh } } else { // non RGB & non White segment (usually On/Off bus) - seg.setColor(0, ULTRAWHITE); - seg.setColor(1, BLACK); + seg.setColor(0, ULTRAWHITE); // use transition + seg.setColor(1, BLACK); // use transition } } @@ -206,74 +267,70 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) } #endif + seg.set = constrain(set, 0, 3); + seg.soundSim = constrain(soundSim, 0, 3); + seg.selected = selected; + seg.reverse = reverse; + seg.mirror = mirror; #ifndef WLED_DISABLE_2D - bool reverse = seg.reverse; - bool mirror = seg.mirror; - #endif - seg.selected = getBoolVal(elem["sel"], seg.selected); - seg.reverse = getBoolVal(elem["rev"], seg.reverse); - seg.mirror = getBoolVal(elem["mi"] , seg.mirror); - #ifndef WLED_DISABLE_2D - bool reverse_y = seg.reverse_y; - bool mirror_y = seg.mirror_y; - seg.reverse_y = getBoolVal(elem["rY"] , seg.reverse_y); - seg.mirror_y = getBoolVal(elem["mY"] , seg.mirror_y); - seg.transpose = getBoolVal(elem[F("tp")], seg.transpose); - if (seg.is2D() && seg.map1D2D == M12_pArc && (reverse != seg.reverse || reverse_y != seg.reverse_y || mirror != seg.mirror || mirror_y != seg.mirror_y)) seg.fill(BLACK); // clear entire segment (in case of Arc 1D to 2D expansion) + seg.reverse_y = reverse_y; + seg.mirror_y = mirror_y; + seg.transpose = transpose; #endif byte fx = seg.mode; - if (getVal(elem["fx"], &fx, 0, strip.getModeCount())) { + if (getVal(elem["fx"], fx, 0, strip.getModeCount())) { if (!presetId && currentPlaylist>=0) unloadPlaylist(); - if (fx != seg.mode) seg.setMode(fx, elem[F("fxdef")]); + if (fx != seg.mode) seg.setMode(fx, elem[F("fxdef")]); // use transition (WARNING: may change map1D2D causing geometry change) } - getVal(elem["sx"], &seg.speed); - getVal(elem["ix"], &seg.intensity); + getVal(elem["sx"], seg.speed); + getVal(elem["ix"], seg.intensity); uint8_t pal = seg.palette; if (seg.getLightCapabilities() & 1) { // ignore palette for White and On/Off segments - if (getVal(elem["pal"], &pal, 0, strip.getPaletteCount())) seg.setPalette(pal); + if (getVal(elem["pal"], pal, 0, getPaletteCount())) seg.setPalette(pal); } - getVal(elem["c1"], &seg.custom1); - getVal(elem["c2"], &seg.custom2); + getVal(elem["c1"], seg.custom1); + getVal(elem["c2"], seg.custom2); uint8_t cust3 = seg.custom3; - getVal(elem["c3"], &cust3, 0, 31); // we can't pass reference to bitfield + getVal(elem["c3"], cust3, 0, 31); // we can't pass reference to bitfield seg.custom3 = constrain(cust3, 0, 31); seg.check1 = getBoolVal(elem["o1"], seg.check1); seg.check2 = getBoolVal(elem["o2"], seg.check2); seg.check3 = getBoolVal(elem["o3"], seg.check3); + uint8_t blend = seg.blendMode; + getVal(elem["bm"], blend, 0, 15); // we can't pass reference to bitfield + seg.blendMode = constrain(blend, 0, 15); + JsonArray iarr = elem[F("i")]; //set individual LEDs if (!iarr.isNull()) { - uint8_t oldMap1D2D = seg.map1D2D; - seg.map1D2D = M12_Pixels; // no mapping - // set brightness immediately and disable transition jsonTransitionOnce = true; - seg.stopTransition(); + if (seg.isInTransition()) seg.startTransition(0); // setting transition time to 0 will stop transition in next frame strip.setTransition(0); strip.setBrightness(scaledBri(bri), true); // freeze and init to black if (!seg.freeze) { seg.freeze = true; - seg.fill(BLACK); + seg.clear(); } - start = 0, stop = 0; - set = 0; //0 nothing set, 1 start set, 2 range set + unsigned iStart = 0, iStop = 0; + unsigned iSet = 0; //0 nothing set, 1 start set, 2 range set for (size_t i = 0; i < iarr.size(); i++) { - if(iarr[i].is()) { - if (!set) { - start = abs(iarr[i].as()); - set++; + if (iarr[i].is()) { + if (!iSet) { + iStart = abs(iarr[i].as()); + iSet++; } else { - stop = abs(iarr[i].as()); - set++; + iStop = abs(iarr[i].as()); + iSet++; } } else { //color uint8_t rgbw[] = {0,0,0,0}; @@ -289,17 +346,16 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) } } - if (set < 2 || stop <= start) stop = start + 1; - uint32_t c = gamma32(RGBW32(rgbw[0], rgbw[1], rgbw[2], rgbw[3])); - while (start < stop) seg.setPixelColor(start++, c); - set = 0; + if (iSet < 2 || iStop <= iStart) iStop = iStart + 1; + uint32_t c = RGBW32(rgbw[0], rgbw[1], rgbw[2], rgbw[3]); + while (iStart < iStop) seg.setRawPixelColor(iStart++, c); // sets pixel color without 1D->2D expansion, grouping or spacing + iSet = 0; } } - seg.map1D2D = oldMap1D2D; // restore mapping strip.trigger(); // force segment update } // send UDP/WS if segment options changed (except selection; will also deselect current preset) - if (seg.differs(prev) & 0x7F) stateChanged = true; + if (differs(seg, prev) & ~SEG_DIFFERS_SEL) stateChanged = true; return true; } @@ -315,7 +371,8 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) #endif bool onBefore = bri; - getVal(root["bri"], &bri); + getVal(root["bri"], bri); + if (bri != briOld) stateChanged = true; bool on = root["on"] | (bri > 0); if (!on != !bri) toggleOnOff(); @@ -338,21 +395,25 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) tr = root[F("transition")] | -1; if (tr >= 0) { transitionDelay = tr * 100; - if (fadeTransition) strip.setTransition(transitionDelay); + strip.setTransition(transitionDelay); } } + blendingStyle = root[F("bs")] | blendingStyle; + blendingStyle &= 0x1F; + // temporary transition (applies only once) tr = root[F("tt")] | -1; if (tr >= 0) { jsonTransitionOnce = true; - if (fadeTransition) strip.setTransition(tr * 100); + strip.setTransition(tr * 100); } tr = root[F("tb")] | -1; if (tr >= 0) strip.timebase = (unsigned long)tr - millis(); JsonObject nl = root["nl"]; + if (!nl.isNull()) stateChanged = true; nightlightActive = getBoolVal(nl["on"], nightlightActive); nightlightDelayMins = nl["dur"] | nightlightDelayMins; nightlightMode = nl["mode"] | nightlightMode; @@ -379,6 +440,7 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) if (realtimeOverride > 2) realtimeOverride = REALTIME_OVERRIDE_ALWAYS; if (realtimeMode && useMainSegmentOnly) { strip.getMainSegment().freeze = !realtimeOverride; + realtimeOverride = REALTIME_OVERRIDE_NONE; // ignore request for override if using main segment only } if (root.containsKey("live")) { @@ -393,35 +455,34 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) int it = 0; JsonVariant segVar = root["seg"]; - if (!segVar.isNull()) strip.suspend(); - if (segVar.is()) - { - int id = segVar["id"] | -1; - //if "seg" is not an array and ID not specified, apply to all selected/checked segments - if (id < 0) { - //apply all selected segments - //bool didSet = false; - for (size_t s = 0; s < strip.getSegmentsNum(); s++) { - Segment &sg = strip.getSegment(s); - if (sg.isActive() && sg.isSelected()) { - deserializeSegment(segVar, s, presetId); - //didSet = true; + if (!segVar.isNull()) { + // we may be called during strip.service() so we must not modify segments while effects are executing + strip.suspend(); + strip.waitForIt(); + if (segVar.is()) { + int id = segVar["id"] | -1; + //if "seg" is not an array and ID not specified, apply to all selected/checked segments + if (id < 0) { + //apply all selected segments + for (size_t s = 0; s < strip.getSegmentsNum(); s++) { + const Segment &sg = strip.getSegment(s); + if (sg.isActive() && sg.isSelected()) { + deserializeSegment(segVar, s, presetId); + } } + } else { + deserializeSegment(segVar, id, presetId); //apply only the segment with the specified ID } - //TODO: not sure if it is good idea to change first active but unselected segment - //if (!didSet) deserializeSegment(segVar, strip.getMainSegmentId(), presetId); } else { - deserializeSegment(segVar, id, presetId); //apply only the segment with the specified ID + size_t deleted = 0; + JsonArray segs = segVar.as(); + for (JsonObject elem : segs) { + if (deserializeSegment(elem, it++, presetId) && !elem["stop"].isNull() && elem["stop"]==0) deleted++; + } + if (strip.getSegmentsNum() > 3 && deleted >= strip.getSegmentsNum()/2U) strip.purgeSegments(); // batch deleting more than half segments } - } else { - size_t deleted = 0; - JsonArray segs = segVar.as(); - for (JsonObject elem : segs) { - if (deserializeSegment(elem, it++, presetId) && !elem["stop"].isNull() && elem["stop"]==0) deleted++; - } - if (strip.getSegmentsNum() > 3 && deleted >= strip.getSegmentsNum()/2U) strip.purgeSegments(); // batch deleting more than half segments + strip.resume(); } - strip.resume(); UsermodManager::readFromJsonState(root); @@ -454,7 +515,7 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) DEBUG_PRINTF_P(PSTR("Preset direct: %d\n"), currentPreset); } else if (!root["ps"].isNull()) { // we have "ps" call (i.e. from button or external API call) or "pd" that includes "ps" (i.e. from UI call) - if (root["win"].isNull() && getVal(root["ps"], &presetCycCurr, 1, 250) && presetCycCurr > 0 && presetCycCurr < 251 && presetCycCurr != currentPreset) { + if (root["win"].isNull() && getVal(root["ps"], presetCycCurr, 1, 250) && presetCycCurr > 0 && presetCycCurr < 251 && presetCycCurr != currentPreset) { DEBUG_PRINTF_P(PSTR("Preset select: %d\n"), presetCycCurr); // b) preset ID only or preset that does not change state (use embedded cycling limits if they exist in getVal()) applyPreset(presetCycCurr, callMode); // async load from file system (only preset ID was specified) @@ -470,11 +531,11 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) } if (root.containsKey(F("rmcpal")) && root[F("rmcpal")].as()) { - if (strip.customPalettes.size()) { + if (customPalettes.size()) { char fileName[32]; - sprintf_P(fileName, PSTR("/palette%d.json"), strip.customPalettes.size()-1); + sprintf_P(fileName, PSTR("/palette%d.json"), customPalettes.size()-1); if (WLED_FS.exists(fileName)) WLED_FS.remove(fileName); - strip.loadCustomPalettes(); + loadCustomPalettes(); } } @@ -493,13 +554,13 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) //if (restart) forceReconnect = true; } - stateUpdated(callMode); + if (stateChanged) stateUpdated(callMode); if (presetToRestore) currentPreset = presetToRestore; return stateResponse; } -void serializeSegment(JsonObject& root, Segment& seg, byte id, bool forPreset, bool segmentBounds) +static void serializeSegment(JsonObject& root, const Segment& seg, byte id, bool forPreset, bool segmentBounds) { root["id"] = id; if (segmentBounds) { @@ -522,6 +583,7 @@ void serializeSegment(JsonObject& root, Segment& seg, byte id, bool forPreset, b root["bri"] = (segbri) ? segbri : 255; root["cct"] = seg.cct; root[F("set")] = seg.set; + root["lc"] = seg.getLightCapabilities(); if (seg.name != nullptr) root["n"] = reinterpret_cast(seg.name); //not good practice, but decreases required JSON buffer else if (forPreset) root["n"] = ""; @@ -566,6 +628,7 @@ void serializeSegment(JsonObject& root, Segment& seg, byte id, bool forPreset, b root["o3"] = seg.check3; root["si"] = seg.soundSim; root["m12"] = seg.map1D2D; + root["bm"] = seg.blendMode; } void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segmentBounds, bool selectedSegmentsOnly) @@ -574,6 +637,7 @@ void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segme root["on"] = (bri > 0); root["bri"] = briLast; root[F("transition")] = transitionDelay/100; //in 100ms + root[F("bs")] = blendingStyle; } if (!forPreset) { @@ -604,7 +668,7 @@ void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segme root[F("mainseg")] = strip.getMainSegmentId(); JsonArray seg = root.createNestedArray("seg"); - for (size_t s = 0; s < strip.getMaxSegments(); s++) { + for (size_t s = 0; s < WS2812FX::getMaxSegments(); s++) { if (s >= strip.getSegmentsNum()) { if (forPreset && segmentBounds && !selectedSegmentsOnly) { //disable segments not part of preset JsonObject seg0 = seg.createNestedObject(); @@ -613,7 +677,7 @@ void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segme } else break; } - Segment &sg = strip.getSegment(s); + const Segment &sg = strip.getSegment(s); if (forPreset && selectedSegmentsOnly && !sg.isSelected()) continue; if (sg.isActive()) { JsonObject seg0 = seg.createNestedObject(); @@ -637,7 +701,7 @@ void serializeInfo(JsonObject root) leds[F("pwr")] = BusManager::currentMilliamps(); leds["fps"] = strip.getFps(); leds[F("maxpwr")] = BusManager::currentMilliamps()>0 ? BusManager::ablMilliampsMax() : 0; - leds[F("maxseg")] = strip.getMaxSegments(); + leds[F("maxseg")] = WS2812FX::getMaxSegments(); //leds[F("actseg")] = strip.getActiveSegmentsNum(); //leds[F("seglock")] = false; //might be used in the future to prevent modifications to segment config leds[F("bootps")] = bootPreset; @@ -651,13 +715,13 @@ void serializeInfo(JsonObject root) #endif unsigned totalLC = 0; - JsonArray lcarr = leds.createNestedArray(F("seglc")); + JsonArray lcarr = leds.createNestedArray(F("seglc")); // deprecated, use state.seg[].lc size_t nSegs = strip.getSegmentsNum(); for (size_t s = 0; s < nSegs; s++) { if (!strip.getSegment(s).isActive()) continue; unsigned lc = strip.getSegment(s).getLightCapabilities(); totalLC |= lc; - lcarr.add(lc); + lcarr.add(lc); // deprecated, use state.seg[].lc } leds["lc"] = totalLC; @@ -705,8 +769,8 @@ void serializeInfo(JsonObject root) #endif root[F("fxcount")] = strip.getModeCount(); - root[F("palcount")] = strip.getPaletteCount(); - root[F("cpalcount")] = strip.customPalettes.size(); //number of custom palettes + root[F("palcount")] = getPaletteCount(); + root[F("cpalcount")] = customPalettes.size(); //number of custom palettes JsonArray ledmaps = root.createNestedArray(F("maps")); for (size_t i=0; i maxPage) page = maxPage; int start = itemPerPage * page; int end = start + itemPerPage; - if (end > palettesCount + customPalettes) end = palettesCount + customPalettes; + if (end > palettesCount + customPalettesCount) end = palettesCount + customPalettesCount; root[F("m")] = maxPage; // inform caller how many pages there are JsonObject palettes = root.createNestedObject("p"); @@ -913,7 +977,7 @@ void serializePalettes(JsonObject root, int page) break; default: if (i >= palettesCount) - setPaletteColors(curPalette, strip.customPalettes[i - palettesCount]); + setPaletteColors(curPalette, customPalettes[i - palettesCount]); else if (i < 13) // palette 6 - 12, fastled palettes setPaletteColors(curPalette, *fastledPalettes[i-6]); else { @@ -1029,16 +1093,21 @@ class LockedJsonResponse: public AsyncJsonResponse { void serveJson(AsyncWebServerRequest* request) { - byte subJson = 0; + enum class json_target { + all, state, info, state_info, nodes, effects, palettes, fxdata, networks, config + }; + json_target subJson = json_target::all; + const String& url = request->url(); - if (url.indexOf("state") > 0) subJson = JSON_PATH_STATE; - else if (url.indexOf("info") > 0) subJson = JSON_PATH_INFO; - else if (url.indexOf("si") > 0) subJson = JSON_PATH_STATE_INFO; - else if (url.indexOf(F("nodes")) > 0) subJson = JSON_PATH_NODES; - else if (url.indexOf(F("eff")) > 0) subJson = JSON_PATH_EFFECTS; - else if (url.indexOf(F("palx")) > 0) subJson = JSON_PATH_PALETTES; - else if (url.indexOf(F("fxda")) > 0) subJson = JSON_PATH_FXDATA; - else if (url.indexOf(F("net")) > 0) subJson = JSON_PATH_NETWORKS; + if (url.indexOf("state") > 0) subJson = json_target::state; + else if (url.indexOf("info") > 0) subJson = json_target::info; + else if (url.indexOf("si") > 0) subJson = json_target::state_info; + else if (url.indexOf(F("nodes")) > 0) subJson = json_target::nodes; + else if (url.indexOf(F("eff")) > 0) subJson = json_target::effects; + else if (url.indexOf(F("palx")) > 0) subJson = json_target::palettes; + else if (url.indexOf(F("fxda")) > 0) subJson = json_target::fxdata; + else if (url.indexOf(F("net")) > 0) subJson = json_target::networks; + else if (url.indexOf(F("cfg")) > 0) subJson = json_target::config; #ifdef WLED_ENABLE_JSONLIVE else if (url.indexOf("live") > 0) { serveLiveLeds(request); @@ -1049,46 +1118,46 @@ void serveJson(AsyncWebServerRequest* request) request->send_P(200, FPSTR(CONTENT_TYPE_JSON), JSON_palette_names); return; } - else if (url.indexOf(F("cfg")) > 0 && handleFileRead(request, F("/cfg.json"))) { - return; - } else if (url.length() > 6) { //not just /json serveJsonError(request, 501, ERR_NOT_IMPL); return; } if (!requestJSONBufferLock(17)) { - serveJsonError(request, 503, ERR_NOBUF); + request->deferResponse(); return; } // releaseJSONBufferLock() will be called when "response" is destroyed (from AsyncWebServer) // make sure you delete "response" if no "request->send(response);" is made - LockedJsonResponse *response = new LockedJsonResponse(pDoc, subJson==JSON_PATH_FXDATA || subJson==JSON_PATH_EFFECTS); // will clear and convert JsonDocument into JsonArray if necessary + LockedJsonResponse *response = new LockedJsonResponse(pDoc, subJson==json_target::fxdata || subJson==json_target::effects); // will clear and convert JsonDocument into JsonArray if necessary JsonVariant lDoc = response->getRoot(); switch (subJson) { - case JSON_PATH_STATE: + case json_target::state: serializeState(lDoc); break; - case JSON_PATH_INFO: + case json_target::info: serializeInfo(lDoc); break; - case JSON_PATH_NODES: + case json_target::nodes: serializeNodes(lDoc); break; - case JSON_PATH_PALETTES: + case json_target::palettes: serializePalettes(lDoc, request->hasParam(F("page")) ? request->getParam(F("page"))->value().toInt() : 0); break; - case JSON_PATH_EFFECTS: + case json_target::effects: serializeModeNames(lDoc); break; - case JSON_PATH_FXDATA: + case json_target::fxdata: serializeModeData(lDoc); break; - case JSON_PATH_NETWORKS: + case json_target::networks: serializeNetworks(lDoc); break; - default: //all + case json_target::config: + serializeConfig(lDoc); break; + case json_target::state_info: + case json_target::all: JsonObject state = lDoc.createNestedObject("state"); serializeState(state); JsonObject info = lDoc.createNestedObject("info"); serializeInfo(info); - if (subJson != JSON_PATH_STATE_INFO) + if (subJson == json_target::all) { JsonArray effects = lDoc.createNestedArray(F("effects")); serializeModeNames(effects); // remove WLED-SR extensions from effect names diff --git a/wled00/led.cpp b/wled00/led.cpp index 9b97091e..43771f9d 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -4,15 +4,13 @@ * LED methods */ -void setValuesFromMainSeg() { setValuesFromSegment(strip.getMainSegmentId()); } -void setValuesFromFirstSelectedSeg() { setValuesFromSegment(strip.getFirstSelectedSegId()); } -void setValuesFromSegment(uint8_t s) -{ - Segment& seg = strip.getSegment(s); - col[0] = R(seg.colors[0]); - col[1] = G(seg.colors[0]); - col[2] = B(seg.colors[0]); - col[3] = W(seg.colors[0]); + // applies chosen setment properties to legacy values +void setValuesFromSegment(uint8_t s) { + const Segment& seg = strip.getSegment(s); + colPri[0] = R(seg.colors[0]); + colPri[1] = G(seg.colors[0]); + colPri[2] = B(seg.colors[0]); + colPri[3] = W(seg.colors[0]); colSec[0] = R(seg.colors[1]); colSec[1] = G(seg.colors[1]); colSec[2] = B(seg.colors[1]); @@ -24,25 +22,19 @@ void setValuesFromSegment(uint8_t s) } -// applies global legacy values (col, colSec, effectCurrent...) -// problem: if the first selected segment already has the value to be set, other selected segments are not updated -void applyValuesToSelectedSegs() -{ - // copy of first selected segment to tell if value was updated - unsigned firstSel = strip.getFirstSelectedSegId(); - Segment selsegPrev = strip.getSegment(firstSel); +// applies global legacy values (colPri, colSec, effectCurrent...) to each selected segment +void applyValuesToSelectedSegs() { for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); - if (i != firstSel && (!seg.isActive() || !seg.isSelected())) continue; - - if (effectSpeed != selsegPrev.speed) {seg.speed = effectSpeed; stateChanged = true;} - if (effectIntensity != selsegPrev.intensity) {seg.intensity = effectIntensity; stateChanged = true;} - if (effectPalette != selsegPrev.palette) {seg.setPalette(effectPalette);} - if (effectCurrent != selsegPrev.mode) {seg.setMode(effectCurrent);} - uint32_t col0 = RGBW32( col[0], col[1], col[2], col[3]); + if (!(seg.isActive() && seg.isSelected())) continue; + if (effectSpeed != seg.speed) {seg.speed = effectSpeed; stateChanged = true;} + if (effectIntensity != seg.intensity) {seg.intensity = effectIntensity; stateChanged = true;} + if (effectPalette != seg.palette) {seg.setPalette(effectPalette);} + if (effectCurrent != seg.mode) {seg.setMode(effectCurrent);} + uint32_t col0 = RGBW32(colPri[0], colPri[1], colPri[2], colPri[3]); uint32_t col1 = RGBW32(colSec[0], colSec[1], colSec[2], colSec[3]); - if (col0 != selsegPrev.colors[0]) {seg.setColor(0, col0);} - if (col1 != selsegPrev.colors[1]) {seg.setColor(1, col1);} + if (col0 != seg.colors[0]) {seg.setColor(0, col0);} + if (col1 != seg.colors[1]) {seg.setColor(1, col1);} } } @@ -71,10 +63,11 @@ byte scaledBri(byte in) } -//applies global brightness +//applies global temporary brightness (briT) to strip void applyBri() { - if (!realtimeMode || !arlsForceMaxBri) + if (realtimeOverride || !(realtimeMode && arlsForceMaxBri)) { + //DEBUG_PRINTF_P(PSTR("Applying strip brightness: %d (%d,%d)\n"), (int)briT, (int)bri, (int)briOld); strip.setBrightness(scaledBri(briT)); } } @@ -85,6 +78,7 @@ void applyFinalBri() { briOld = bri; briT = bri; applyBri(); + strip.trigger(); // force one last update } @@ -93,7 +87,7 @@ void applyFinalBri() { void stateUpdated(byte callMode) { //call for notifier -> 0: init 1: direct change 2: button 3: notification 4: nightlight 5: other (No notification) // 6: fx changed 7: hue 8: preset cycle 9: blynk 10: alexa 11: ws send only 12: button preset - setValuesFromFirstSelectedSeg(); + setValuesFromFirstSelectedSeg(); // a much better approach would be to use main segment: setValuesFromMainSeg() if (bri != briOld || stateChanged) { if (stateChanged) currentPreset = 0; //something changed, so we are no longer in the preset @@ -103,7 +97,6 @@ void stateUpdated(byte callMode) { //set flag to update ws and mqtt interfaceUpdateCallMode = callMode; - stateChanged = false; } else { if (nightlightActive && !nightlightActiveOld && callMode != CALL_MODE_NOTIFICATION && callMode != CALL_MODE_NO_NOTIFY) { notify(CALL_MODE_NIGHTLIGHT); @@ -111,10 +104,11 @@ void stateUpdated(byte callMode) { } } + unsigned long now = millis(); if (callMode != CALL_MODE_NO_NOTIFY && nightlightActive && (nightlightMode == NL_MODE_FADE || nightlightMode == NL_MODE_COLORFADE)) { briNlT = bri; - nightlightDelayMs -= (millis() - nightlightStartTime); - nightlightStartTime = millis(); + nightlightDelayMs -= (now - nightlightStartTime); + nightlightStartTime = now; } if (briT == 0) { if (callMode != CALL_MODE_NOTIFICATION) strip.resetTimebase(); //effect start from beginning @@ -128,43 +122,36 @@ void stateUpdated(byte callMode) { // notify usermods of state change UsermodManager::onStateChange(callMode); - if (fadeTransition) { - if (strip.getTransition() == 0) { - jsonTransitionOnce = false; - transitionActive = false; - applyFinalBri(); - strip.trigger(); - return; - } - - if (transitionActive) { - briOld = briT; - tperLast = 0; - } else - strip.setTransitionMode(true); // force all segments to transition mode - transitionActive = true; - transitionStartTime = millis(); - } else { + if (strip.getTransition() == 0) { + jsonTransitionOnce = false; + transitionActive = false; applyFinalBri(); strip.trigger(); + } else { + if (transitionActive) { + briOld = briT; + } else if (bri != briOld || stateChanged) + strip.setTransitionMode(true); // force all segments to transition mode + transitionActive = true; + transitionStartTime = now; } + stateChanged = false; } -void updateInterfaces(uint8_t callMode) -{ +void updateInterfaces(uint8_t callMode) { if (!interfaceUpdateCallMode || millis() - lastInterfaceUpdate < INTERFACE_UPDATE_COOLDOWN) return; sendDataWs(); lastInterfaceUpdate = millis(); - interfaceUpdateCallMode = 0; //disable further updates + interfaceUpdateCallMode = CALL_MODE_INIT; //disable further updates if (callMode == CALL_MODE_WS_SEND) return; #ifndef WLED_DISABLE_ALEXA if (espalexaDevice != nullptr && callMode != CALL_MODE_ALEXA) { espalexaDevice->setValue(bri); - espalexaDevice->setColor(col[0], col[1], col[2]); + espalexaDevice->setColor(colPri[0], colPri[1], colPri[2]); } #endif #ifndef WLED_DISABLE_MQTT @@ -173,28 +160,26 @@ void updateInterfaces(uint8_t callMode) } -void handleTransitions() -{ +void handleTransitions() { //handle still pending interface update updateInterfaces(interfaceUpdateCallMode); if (transitionActive && strip.getTransition() > 0) { - float tper = (millis() - transitionStartTime)/(float)strip.getTransition(); - if (tper >= 1.0f) { + int ti = millis() - transitionStartTime; + int tr = strip.getTransition(); + if (ti/tr) { strip.setTransitionMode(false); // stop all transitions // restore (global) transition time if not called from UDP notifier or single/temporary transition from JSON (also playlist) if (jsonTransitionOnce) strip.setTransition(transitionDelay); transitionActive = false; jsonTransitionOnce = false; - tperLast = 0; applyFinalBri(); return; } - if (tper - tperLast < 0.004f) return; // less than 1 bit change (1/255) - tperLast = tper; - briT = briOld + ((bri - briOld) * tper); - - applyBri(); + byte briTO = briT; + int deltaBri = (int)bri - (int)briOld; + briT = briOld + (deltaBri * ti / tr); + if (briTO != briT) applyBri(); } } @@ -206,8 +191,7 @@ void colorUpdated(byte callMode) { } -void handleNightlight() -{ +void handleNightlight() { unsigned long now = millis(); if (now < 100 && lastNlUpdate > 0) lastNlUpdate = 0; // take care of millis() rollover if (now - lastNlUpdate < 100) return; // allow only 10 NL updates per second @@ -221,7 +205,7 @@ void handleNightlight() nightlightDelayMs = (unsigned)(nightlightDelayMins*60000); nightlightActiveOld = true; briNlT = bri; - for (unsigned i=0; i<4; i++) colNlT[i] = col[i]; // remember starting color + for (unsigned i=0; i<4; i++) colNlT[i] = colPri[i]; // remember starting color if (nightlightMode == NL_MODE_SUN) { //save current @@ -229,8 +213,8 @@ void handleNightlight() colNlT[1] = effectSpeed; colNlT[2] = effectPalette; - strip.setMode(strip.getFirstSelectedSegId(), FX_MODE_STATIC); // make sure seg runtime is reset if it was in sunrise mode - effectCurrent = FX_MODE_SUNRISE; + strip.getFirstSelectedSeg().setMode(FX_MODE_STATIC); // make sure seg runtime is reset if it was in sunrise mode + effectCurrent = FX_MODE_SUNRISE; // colorUpdated() will take care of assigning that to all selected segments effectSpeed = nightlightDelayMins; effectPalette = 0; if (effectSpeed > 60) effectSpeed = 60; //currently limited to 60 minutes @@ -246,7 +230,7 @@ void handleNightlight() bri = briNlT + ((nightlightTargetBri - briNlT)*nper); if (nightlightMode == NL_MODE_COLORFADE) // color fading only is enabled with "NF=2" { - for (unsigned i=0; i<4; i++) col[i] = colNlT[i]+ ((colSec[i] - colNlT[i])*nper); // fading from actual color to secondary color + for (unsigned i=0; i<4; i++) colPri[i] = colNlT[i]+ ((colSec[i] - colNlT[i])*nper); // fading from actual color to secondary color } colorUpdated(CALL_MODE_NO_NOTIFY); } @@ -287,7 +271,6 @@ void handleNightlight() } //utility for FastLED to use our custom timer -uint32_t get_millisecond_timer() -{ +uint32_t get_millisecond_timer() { return strip.now; } diff --git a/wled00/lx_parser.cpp b/wled00/lx_parser.cpp index 7fd91ea0..1318686c 100644 --- a/wled00/lx_parser.cpp +++ b/wled00/lx_parser.cpp @@ -22,7 +22,7 @@ bool parseLx(int lxValue, byte* rgbw) } else if ((lxValue >= 200000000) && (lxValue <= 201006500)) { // Loxone Lumitech ok = true; - float tmpBri = floor((lxValue - 200000000) / 10000); ; + float tmpBri = floor((lxValue - 200000000) / 10000); uint16_t ct = (lxValue - 200000000) - (((uint8_t)tmpBri) * 10000); tmpBri *= 2.55f; diff --git a/wled00/mqtt.cpp b/wled00/mqtt.cpp index a476db87..62f29610 100644 --- a/wled00/mqtt.cpp +++ b/wled00/mqtt.cpp @@ -7,6 +7,10 @@ #ifndef WLED_DISABLE_MQTT #define MQTT_KEEP_ALIVE_TIME 60 // contact the MQTT broker every 60 seconds +#if MQTT_MAX_TOPIC_LEN > 32 +#warning "MQTT topics length > 32 is not recommended for compatibility with usermods!" +#endif + static void parseMQTTBriPayload(char* payload) { if (strstr(payload, "ON") || strstr(payload, "on") || strstr(payload, "true")) {bri = briLast; stateUpdated(CALL_MODE_DIRECT_CHANGE);} @@ -23,24 +27,24 @@ static void parseMQTTBriPayload(char* payload) static void onMqttConnect(bool sessionPresent) { //(re)subscribe to required topics - char subuf[38]; + char subuf[MQTT_MAX_TOPIC_LEN + 9]; if (mqttDeviceTopic[0] != 0) { - strlcpy(subuf, mqttDeviceTopic, 33); + strlcpy(subuf, mqttDeviceTopic, MQTT_MAX_TOPIC_LEN + 1); mqtt->subscribe(subuf, 0); strcat_P(subuf, PSTR("/col")); mqtt->subscribe(subuf, 0); - strlcpy(subuf, mqttDeviceTopic, 33); + strlcpy(subuf, mqttDeviceTopic, MQTT_MAX_TOPIC_LEN + 1); strcat_P(subuf, PSTR("/api")); mqtt->subscribe(subuf, 0); } if (mqttGroupTopic[0] != 0) { - strlcpy(subuf, mqttGroupTopic, 33); + strlcpy(subuf, mqttGroupTopic, MQTT_MAX_TOPIC_LEN + 1); mqtt->subscribe(subuf, 0); strcat_P(subuf, PSTR("/col")); mqtt->subscribe(subuf, 0); - strlcpy(subuf, mqttGroupTopic, 33); + strlcpy(subuf, mqttGroupTopic, MQTT_MAX_TOPIC_LEN + 1); strcat_P(subuf, PSTR("/api")); mqtt->subscribe(subuf, 0); } @@ -48,6 +52,13 @@ static void onMqttConnect(bool sessionPresent) UsermodManager::onMqttConnect(sessionPresent); DEBUG_PRINTLN(F("MQTT ready")); + +#ifndef USERMOD_SMARTNEST + strlcpy(subuf, mqttDeviceTopic, MQTT_MAX_TOPIC_LEN + 1); + strcat_P(subuf, PSTR("/status")); + mqtt->publish(subuf, 0, true, "online"); // retain message for a LWT +#endif + publishMqtt(); } @@ -64,8 +75,8 @@ static void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProp } if (index == 0) { // start (1st partial packet or the only packet) - if (payloadStr) delete[] payloadStr; // fail-safe: release buffer - payloadStr = new char[total+1]; // allocate new buffer + p_free(payloadStr); // release buffer if it exists + payloadStr = static_cast(p_malloc(total+1)); // allocate new buffer } if (payloadStr == nullptr) return; // buffer not allocated @@ -90,7 +101,7 @@ static void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProp } else { // Non-Wled Topic used here. Probably a usermod subscribed to this topic. UsermodManager::onMqttMessage(topic, payloadStr); - delete[] payloadStr; + p_free(payloadStr); payloadStr = nullptr; return; } @@ -99,7 +110,7 @@ static void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProp //Prefix is stripped from the topic at this point if (strcmp_P(topic, PSTR("/col")) == 0) { - colorFromDecOrHexString(col, payloadStr); + colorFromDecOrHexString(colPri, payloadStr); colorUpdated(CALL_MODE_DIRECT_CHANGE); } else if (strcmp_P(topic, PSTR("/api")) == 0) { if (requestJSONBufferLock(15)) { @@ -120,7 +131,7 @@ static void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProp // topmost topic (just wled/MAC) parseMQTTBriPayload(payloadStr); } - delete[] payloadStr; + p_free(payloadStr); payloadStr = nullptr; } @@ -158,27 +169,23 @@ void publishMqtt() #ifndef USERMOD_SMARTNEST char s[10]; - char subuf[48]; + char subuf[MQTT_MAX_TOPIC_LEN + 16]; sprintf_P(s, PSTR("%u"), bri); - strlcpy(subuf, mqttDeviceTopic, 33); + strlcpy(subuf, mqttDeviceTopic, MQTT_MAX_TOPIC_LEN + 1); strcat_P(subuf, PSTR("/g")); mqtt->publish(subuf, 0, retainMqttMsg, s); // optionally retain message (#2263) - sprintf_P(s, PSTR("#%06X"), (col[3] << 24) | (col[0] << 16) | (col[1] << 8) | (col[2])); - strlcpy(subuf, mqttDeviceTopic, 33); + sprintf_P(s, PSTR("#%06X"), (colPri[3] << 24) | (colPri[0] << 16) | (colPri[1] << 8) | (colPri[2])); + strlcpy(subuf, mqttDeviceTopic, MQTT_MAX_TOPIC_LEN + 1); strcat_P(subuf, PSTR("/c")); mqtt->publish(subuf, 0, retainMqttMsg, s); // optionally retain message (#2263) - strlcpy(subuf, mqttDeviceTopic, 33); - strcat_P(subuf, PSTR("/status")); - mqtt->publish(subuf, 0, true, "online"); // retain message for a LWT - // TODO: use a DynamicBufferList. Requires a list-read-capable MQTT client API. DynamicBuffer buf(1024); bufferPrint pbuf(buf.data(), buf.size()); XML_response(pbuf); - strlcpy(subuf, mqttDeviceTopic, 33); + strlcpy(subuf, mqttDeviceTopic, MQTT_MAX_TOPIC_LEN + 1); strcat_P(subuf, PSTR("/v")); mqtt->publish(subuf, 0, retainMqttMsg, buf.data(), pbuf.size()); // optionally retain message (#2263) #endif @@ -192,7 +199,8 @@ bool initMqtt() if (!mqttEnabled || mqttServer[0] == 0 || !WLED_CONNECTED) return false; if (mqtt == nullptr) { - mqtt = new AsyncMqttClient(); + void *ptr = p_malloc(sizeof(AsyncMqttClient)); + mqtt = new (ptr) AsyncMqttClient(); // use placement new (into PSRAM), client will never be deleted if (!mqtt) return false; mqtt->onMessage(onMqttMessage); mqtt->onConnect(onMqttConnect); @@ -211,7 +219,7 @@ bool initMqtt() if (mqttUser[0] && mqttPass[0]) mqtt->setCredentials(mqttUser, mqttPass); #ifndef USERMOD_SMARTNEST - strlcpy(mqttStatusTopic, mqttDeviceTopic, 33); + strlcpy(mqttStatusTopic, mqttDeviceTopic, MQTT_MAX_TOPIC_LEN + 1); strcat_P(mqttStatusTopic, PSTR("/status")); mqtt->setWill(mqttStatusTopic, 0, true, "offline"); // LWT message #endif diff --git a/wled00/network.cpp b/wled00/network.cpp index 3bd589f9..79209ff5 100644 --- a/wled00/network.cpp +++ b/wled00/network.cpp @@ -3,7 +3,7 @@ #include "wled_ethernet.h" -#ifdef WLED_USE_ETHERNET +#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) // The following six pins are neither configurable nor // can they be re-assigned through IOMUX / GPIO matrix. // See https://docs.espressif.com/projects/esp-idf/en/latest/esp32/hw-reference/esp32/get-started-ethernet-kit-v1.1.html#ip101gri-phy-interface @@ -146,6 +146,101 @@ const ethernet_settings ethernetBoards[] = { ETH_CLOCK_GPIO0_OUT // eth_clk_mode } }; + +bool initEthernet() +{ + static bool successfullyConfiguredEthernet = false; + + if (successfullyConfiguredEthernet) { + // DEBUG_PRINTLN(F("initE: ETH already successfully configured, ignoring")); + return false; + } + if (ethernetType == WLED_ETH_NONE) { + return false; + } + if (ethernetType >= WLED_NUM_ETH_TYPES) { + DEBUG_PRINTF_P(PSTR("initE: Ignoring attempt for invalid ethernetType (%d)\n"), ethernetType); + return false; + } + + DEBUG_PRINTF_P(PSTR("initE: Attempting ETH config: %d\n"), ethernetType); + + // Ethernet initialization should only succeed once -- else reboot required + ethernet_settings es = ethernetBoards[ethernetType]; + managed_pin_type pinsToAllocate[10] = { + // first six pins are non-configurable + esp32_nonconfigurable_ethernet_pins[0], + esp32_nonconfigurable_ethernet_pins[1], + esp32_nonconfigurable_ethernet_pins[2], + esp32_nonconfigurable_ethernet_pins[3], + esp32_nonconfigurable_ethernet_pins[4], + esp32_nonconfigurable_ethernet_pins[5], + { (int8_t)es.eth_mdc, true }, // [6] = MDC is output and mandatory + { (int8_t)es.eth_mdio, true }, // [7] = MDIO is bidirectional and mandatory + { (int8_t)es.eth_power, true }, // [8] = optional pin, not all boards use + { ((int8_t)0xFE), false }, // [9] = replaced with eth_clk_mode, mandatory + }; + // update the clock pin.... + if (es.eth_clk_mode == ETH_CLOCK_GPIO0_IN) { + pinsToAllocate[9].pin = 0; + pinsToAllocate[9].isOutput = false; + } else if (es.eth_clk_mode == ETH_CLOCK_GPIO0_OUT) { + pinsToAllocate[9].pin = 0; + pinsToAllocate[9].isOutput = true; + } else if (es.eth_clk_mode == ETH_CLOCK_GPIO16_OUT) { + pinsToAllocate[9].pin = 16; + pinsToAllocate[9].isOutput = true; + } else if (es.eth_clk_mode == ETH_CLOCK_GPIO17_OUT) { + pinsToAllocate[9].pin = 17; + pinsToAllocate[9].isOutput = true; + } else { + DEBUG_PRINTF_P(PSTR("initE: Failing due to invalid eth_clk_mode (%d)\n"), es.eth_clk_mode); + return false; + } + + if (!PinManager::allocateMultiplePins(pinsToAllocate, 10, PinOwner::Ethernet)) { + DEBUG_PRINTLN(F("initE: Failed to allocate ethernet pins")); + return false; + } + + /* + For LAN8720 the most correct way is to perform clean reset each time before init + applying LOW to power or nRST pin for at least 100 us (please refer to datasheet, page 59) + ESP_IDF > V4 implements it (150 us, lan87xx_reset_hw(esp_eth_phy_t *phy) function in + /components/esp_eth/src/esp_eth_phy_lan87xx.c, line 280) + but ESP_IDF < V4 does not. Lets do it: + [not always needed, might be relevant in some EMI situations at startup and for hot resets] + */ + #if ESP_IDF_VERSION_MAJOR==3 + if(es.eth_power>0 && es.eth_type==ETH_PHY_LAN8720) { + pinMode(es.eth_power, OUTPUT); + digitalWrite(es.eth_power, 0); + delayMicroseconds(150); + digitalWrite(es.eth_power, 1); + delayMicroseconds(10); + } + #endif + + if (!ETH.begin( + (uint8_t) es.eth_address, + (int) es.eth_power, + (int) es.eth_mdc, + (int) es.eth_mdio, + (eth_phy_type_t) es.eth_type, + (eth_clock_mode_t) es.eth_clk_mode + )) { + DEBUG_PRINTLN(F("initC: ETH.begin() failed")); + // de-allocate the allocated pins + for (managed_pin_type mpt : pinsToAllocate) { + PinManager::deallocatePin(mpt.pin, PinOwner::Ethernet); + } + return false; + } + + successfullyConfiguredEthernet = true; + DEBUG_PRINTLN(F("initC: *** Ethernet successfully configured! ***")); + return true; +} #endif @@ -170,19 +265,136 @@ int getSignalQuality(int rssi) } +void fillMAC2Str(char *str, const uint8_t *mac) { + sprintf_P(str, PSTR("%02x%02x%02x%02x%02x%02x"), MAC2STR(mac)); + byte nul = 0; + for (int i = 0; i < 6; i++) nul |= *mac++; // do we have 0 + if (!nul) str[0] = '\0'; // empty string +} + +void fillStr2MAC(uint8_t *mac, const char *str) { + for (int i = 0; i < 6; i++) *mac++ = 0; // clear + if (!str) return; // null string + uint64_t MAC = strtoull(str, nullptr, 16); + for (int i = 0; i < 6; i++) { *--mac = MAC & 0xFF; MAC >>= 8; } +} + + +// performs asynchronous scan for available networks (which may take couple of seconds to finish) +// returns configured WiFi ID with the strongest signal (or default if no configured networks available) +int findWiFi(bool doScan) { + if (multiWiFi.size() <= 1) { + DEBUG_PRINTF_P(PSTR("WiFi: Defaulf SSID (%s) used.\n"), multiWiFi[0].clientSSID); + return 0; + } + + int status = WiFi.scanComplete(); // complete scan may take as much as several seconds (usually <6s with not very crowded air) + + if (doScan || status == WIFI_SCAN_FAILED) { + DEBUG_PRINTF_P(PSTR("WiFi: Scan started. @ %lus\n"), millis()/1000); + WiFi.scanNetworks(true); // start scanning in asynchronous mode (will delete old scan) + } else if (status >= 0) { // status contains number of found networks (including duplicate SSIDs with different BSSID) + DEBUG_PRINTF_P(PSTR("WiFi: Found %d SSIDs. @ %lus\n"), status, millis()/1000); + int rssi = -9999; + int selected = selectedWiFi; + for (int o = 0; o < status; o++) { + DEBUG_PRINTF_P(PSTR(" SSID: %s (BSSID: %s) RSSI: %ddB\n"), WiFi.SSID(o).c_str(), WiFi.BSSIDstr(o).c_str(), WiFi.RSSI(o)); + for (unsigned n = 0; n < multiWiFi.size(); n++) + if (!strcmp(WiFi.SSID(o).c_str(), multiWiFi[n].clientSSID)) { + bool foundBSSID = memcmp(multiWiFi[n].bssid, WiFi.BSSID(o), 6) == 0; + // find the WiFi with the strongest signal (but keep priority of entry if signal difference is not big) + if (foundBSSID || (n < selected && WiFi.RSSI(o) > rssi-10) || WiFi.RSSI(o) > rssi) { + rssi = foundBSSID ? 0 : WiFi.RSSI(o); // RSSI is only ever negative + selected = n; + } + break; + } + } + DEBUG_PRINTF_P(PSTR("WiFi: Selected SSID: %s RSSI: %ddB\n"), multiWiFi[selected].clientSSID, rssi); + return selected; + } + //DEBUG_PRINT(F("WiFi scan running.")); + return status; // scan is still running or there was an error +} + + +bool isWiFiConfigured() { + return multiWiFi.size() > 1 || (strlen(multiWiFi[0].clientSSID) >= 1 && strcmp_P(multiWiFi[0].clientSSID, PSTR(DEFAULT_CLIENT_SSID)) != 0); +} + +#if defined(ESP8266) + #define ARDUINO_EVENT_WIFI_AP_STADISCONNECTED WIFI_EVENT_SOFTAPMODE_STADISCONNECTED + #define ARDUINO_EVENT_WIFI_AP_STACONNECTED WIFI_EVENT_SOFTAPMODE_STACONNECTED + #define ARDUINO_EVENT_WIFI_STA_GOT_IP WIFI_EVENT_STAMODE_GOT_IP + #define ARDUINO_EVENT_WIFI_STA_CONNECTED WIFI_EVENT_STAMODE_CONNECTED + #define ARDUINO_EVENT_WIFI_STA_DISCONNECTED WIFI_EVENT_STAMODE_DISCONNECTED +#elif defined(ARDUINO_ARCH_ESP32) && !defined(ESP_ARDUINO_VERSION_MAJOR) //ESP_IDF_VERSION_MAJOR==3 + // not strictly IDF v3 but Arduino core related + #define ARDUINO_EVENT_WIFI_AP_STADISCONNECTED SYSTEM_EVENT_AP_STADISCONNECTED + #define ARDUINO_EVENT_WIFI_AP_STACONNECTED SYSTEM_EVENT_AP_STACONNECTED + #define ARDUINO_EVENT_WIFI_STA_GOT_IP SYSTEM_EVENT_STA_GOT_IP + #define ARDUINO_EVENT_WIFI_STA_CONNECTED SYSTEM_EVENT_STA_CONNECTED + #define ARDUINO_EVENT_WIFI_STA_DISCONNECTED SYSTEM_EVENT_STA_DISCONNECTED + #define ARDUINO_EVENT_WIFI_AP_START SYSTEM_EVENT_AP_START + #define ARDUINO_EVENT_WIFI_AP_STOP SYSTEM_EVENT_AP_STOP + #define ARDUINO_EVENT_WIFI_SCAN_DONE SYSTEM_EVENT_SCAN_DONE + #define ARDUINO_EVENT_ETH_START SYSTEM_EVENT_ETH_START + #define ARDUINO_EVENT_ETH_CONNECTED SYSTEM_EVENT_ETH_CONNECTED + #define ARDUINO_EVENT_ETH_DISCONNECTED SYSTEM_EVENT_ETH_DISCONNECTED +#endif + //handle Ethernet connection event void WiFiEvent(WiFiEvent_t event) { switch (event) { -#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) - case SYSTEM_EVENT_ETH_START: - DEBUG_PRINTLN(F("ETH Started")); + case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED: + // AP client disconnected + if (--apClients == 0 && isWiFiConfigured()) forceReconnect = true; // no clients reconnect WiFi if awailable + DEBUG_PRINTF_P(PSTR("WiFi-E: AP Client Disconnected (%d) @ %lus.\n"), (int)apClients, millis()/1000); break; - case SYSTEM_EVENT_ETH_CONNECTED: + case ARDUINO_EVENT_WIFI_AP_STACONNECTED: + // AP client connected + apClients++; + DEBUG_PRINTF_P(PSTR("WiFi-E: AP Client Connected (%d) @ %lus.\n"), (int)apClients, millis()/1000); + break; + case ARDUINO_EVENT_WIFI_STA_GOT_IP: + DEBUG_PRINT(F("WiFi-E: IP address: ")); DEBUG_PRINTLN(Network.localIP()); + break; + case ARDUINO_EVENT_WIFI_STA_CONNECTED: + // followed by IDLE and SCAN_DONE + DEBUG_PRINTF_P(PSTR("WiFi-E: Connected! @ %lus\n"), millis()/1000); + wasConnected = true; + break; + case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: + if (wasConnected && interfacesInited) { + DEBUG_PRINTF_P(PSTR("WiFi-E: Disconnected! @ %lus\n"), millis()/1000); + if (interfacesInited && multiWiFi.size() > 1 && WiFi.scanComplete() >= 0) { + findWiFi(true); // reinit WiFi scan + forceReconnect = true; + } + interfacesInited = false; + } + break; + #ifdef ARDUINO_ARCH_ESP32 + case ARDUINO_EVENT_WIFI_SCAN_DONE: + // also triggered when connected to selected SSID + DEBUG_PRINTLN(F("WiFi-E: SSID scan completed.")); + break; + case ARDUINO_EVENT_WIFI_AP_START: + DEBUG_PRINTLN(F("WiFi-E: AP Started")); + break; + case ARDUINO_EVENT_WIFI_AP_STOP: + DEBUG_PRINTLN(F("WiFi-E: AP Stopped")); + break; + #if defined(WLED_USE_ETHERNET) + case ARDUINO_EVENT_ETH_START: + DEBUG_PRINTLN(F("ETH-E: Started")); + break; + case ARDUINO_EVENT_ETH_CONNECTED: { - DEBUG_PRINTLN(F("ETH Connected")); + DEBUG_PRINTLN(F("ETH-E: Connected")); if (!apActive) { - WiFi.disconnect(true); + WiFi.disconnect(true); // disable WiFi entirely } if (multiWiFi[0].staticIP != (uint32_t)0x00000000 && multiWiFi[0].staticGW != (uint32_t)0x00000000) { ETH.config(multiWiFi[0].staticIP, multiWiFi[0].staticGW, multiWiFi[0].staticSN, dnsAddress); @@ -196,18 +408,20 @@ void WiFiEvent(WiFiEvent_t event) showWelcomePage = false; break; } - case SYSTEM_EVENT_ETH_DISCONNECTED: - DEBUG_PRINTLN(F("ETH Disconnected")); + case ARDUINO_EVENT_ETH_DISCONNECTED: + DEBUG_PRINTLN(F("ETH-E: Disconnected")); // This doesn't really affect ethernet per se, // as it's only configured once. Rather, it // may be necessary to reconnect the WiFi when // ethernet disconnects, as a way to provide // alternative access to the device. + if (interfacesInited && WiFi.scanComplete() >= 0) findWiFi(true); // reinit WiFi scan forceReconnect = true; break; -#endif + #endif + #endif default: - DEBUG_PRINTF_P(PSTR("Network event: %d\n"), (int)event); + DEBUG_PRINTF_P(PSTR("WiFi-E: Event %d\n"), (int)event); break; } } diff --git a/wled00/ntp.cpp b/wled00/ntp.cpp index 8d44e634..12b698f4 100644 --- a/wled00/ntp.cpp +++ b/wled00/ntp.cpp @@ -224,7 +224,7 @@ void sendNTPPacket() ntpUdp.endPacket(); } -static bool isValidNtpResponse(byte * ntpPacket) { +static bool isValidNtpResponse(const byte* ntpPacket) { // Perform a few validity checks on the packet // based on https://github.com/taranais/NTPClient/blob/master/NTPClient.cpp if((ntpPacket[0] & 0b11000000) == 0b11000000) return false; //reject LI=UNSYNC diff --git a/wled00/overlay.cpp b/wled00/overlay.cpp index fcd0a40c..3f6e6312 100644 --- a/wled00/overlay.cpp +++ b/wled00/overlay.cpp @@ -90,9 +90,8 @@ void _overlayAnalogCountdown() void handleOverlayDraw() { UsermodManager::handleOverlayDraw(); if (analogClockSolidBlack) { - const Segment* segments = strip.getSegments(); for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { - const Segment& segment = segments[i]; + const Segment& segment = strip.getSegment(i); if (!segment.isActive()) continue; if (segment.mode > 0 || segment.colors[0] > 0) { return; diff --git a/wled00/palettes.h b/wled00/palettes.h old mode 100644 new mode 100755 index 1ead342b..a77540fd --- a/wled00/palettes.h +++ b/wled00/palettes.h @@ -1,499 +1,374 @@ -/* - * Color palettes for FastLED effects (65-73). - */ - -// From ColorWavesWithPalettes by Mark Kriegsman: https://gist.github.com/kriegsman/8281905786e8b2632aeb -// Unfortunately, these are stored in RAM! - -// Gradient palette "ib_jul01_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/ing/xmas/tn/ib_jul01.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 16 bytes of program space. - #ifndef PalettesWLED_h #define PalettesWLED_h -const byte ib_jul01_gp[] PROGMEM = { - 0, 194, 1, 1, - 94, 1, 29, 18, - 132, 57,131, 28, - 255, 113, 1, 1}; +/* + * WLED Color palettes + * + * Note: palettes imported from http://seaviewsensing.com/pub/cpt-city are gamma corrected using gammas (1.182, 1.0, 1.136) + * this is done to match colors of the palettes after applying the (default) global gamma of 2.2 to versions + * prior to WLED 0.16 which used pre-applied gammas of (2.6,2.2,2.5) for these palettes. + * Palettes from FastLED are intended to be used without gamma correction, an inverse gamma of 2.2 is applied to original colors + */ + +// Gradient palette "ib_jul01_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/ing/xmas/ib_jul01.c3g +const uint8_t ib_jul01_gp[] PROGMEM = { + 0, 226, 6, 12, + 94, 26, 96, 78, + 132, 130, 189, 94, + 255, 177, 3, 9}; // Gradient palette "es_vintage_57_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/vintage/tn/es_vintage_57.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 20 bytes of program space. - -const byte es_vintage_57_gp[] PROGMEM = { - 0, 2, 1, 1, - 53, 18, 1, 0, - 104, 69, 29, 1, - 153, 167,135, 10, - 255, 46, 56, 4}; - +// http://seaviewsensing.com/pub/cpt-city/es/vintage/es_vintage_57.c3g +const uint8_t es_vintage_57_gp[] PROGMEM = { + 0, 29, 8, 3, + 53, 76, 1, 0, + 104, 142, 96, 28, + 153, 211, 191, 61, + 255, 117, 129, 42}; // Gradient palette "es_vintage_01_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/vintage/tn/es_vintage_01.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 32 bytes of program space. - -const byte es_vintage_01_gp[] PROGMEM = { - 0, 4, 1, 1, - 51, 16, 0, 1, - 76, 97,104, 3, - 101, 255,131, 19, - 127, 67, 9, 4, - 153, 16, 0, 1, - 229, 4, 1, 1, - 255, 4, 1, 1}; - +// http://seaviewsensing.com/pub/cpt-city/es/vintage/es_vintage_01.c3g +const uint8_t es_vintage_01_gp[] PROGMEM = { + 0, 41, 18, 24, + 51, 73, 0, 22, + 76, 165, 170, 38, + 101, 255, 189, 80, + 127, 139, 56, 40, + 153, 73, 0, 22, + 229, 41, 18, 24, + 255, 41, 18, 24}; // Gradient palette "es_rivendell_15_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/rivendell/tn/es_rivendell_15.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 20 bytes of program space. - -const byte es_rivendell_15_gp[] PROGMEM = { - 0, 1, 14, 5, - 101, 16, 36, 14, - 165, 56, 68, 30, - 242, 150,156, 99, - 255, 150,156, 99}; - +// http://seaviewsensing.com/pub/cpt-city/es/rivendell/es_rivendell_15.c3g +const uint8_t es_rivendell_15_gp[] PROGMEM = { + 0, 24, 69, 44, + 101, 73, 105, 70, + 165, 129, 140, 97, + 242, 200, 204, 166, + 255, 200, 204, 166}; // Gradient palette "rgi_15_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/ds/rgi/tn/rgi_15.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 36 bytes of program space. -// Edited to be brighter - -const byte rgi_15_gp[] PROGMEM = { - 0, 4, 1, 70, - 31, 55, 1, 30, - 63, 255, 4, 7, - 95, 59, 2, 29, - 127, 11, 3, 50, - 159, 39, 8, 60, - 191, 112, 19, 40, - 223, 78, 11, 39, - 255, 29, 8, 59}; - +// http://seaviewsensing.com/pub/cpt-city/ds/rgi/rgi_15.c3g +const uint8_t rgi_15_gp[] PROGMEM = { + 0, 41, 14, 99, + 31, 128, 24, 74, + 63, 227, 34, 50, + 95, 132, 31, 76, + 127, 47, 29, 102, + 159, 109, 47, 101, + 191, 176, 66, 100, + 223, 129, 57, 104, + 255, 84, 48, 108}; // Gradient palette "retro2_16_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/ma/retro2/tn/retro2_16.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 8 bytes of program space. - -const byte retro2_16_gp[] PROGMEM = { - 0, 188,135, 1, - 255, 46, 7, 1}; - +// http://seaviewsensing.com/pub/cpt-city/ma/retro2/retro2_16.c3g +const uint8_t retro2_16_gp[] PROGMEM = { + 0, 222, 191, 8, + 255, 117, 52, 1}; // Gradient palette "Analogous_1_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/red/tn/Analogous_1.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 20 bytes of program space. - -const byte Analogous_1_gp[] PROGMEM = { - 0, 3, 0,255, - 63, 23, 0,255, - 127, 67, 0,255, - 191, 142, 0, 45, - 255, 255, 0, 0}; - +// http://seaviewsensing.com/pub/cpt-city/nd/red/Analogous_1.c3g +const uint8_t Analogous_1_gp[] PROGMEM = { + 0, 38, 0, 255, + 63, 86, 0, 255, + 127, 139, 0, 255, + 191, 196, 0, 117, + 255, 255, 0, 0}; // Gradient palette "es_pinksplash_08_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/pink_splash/tn/es_pinksplash_08.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 20 bytes of program space. - -const byte es_pinksplash_08_gp[] PROGMEM = { - 0, 126, 11,255, - 127, 197, 1, 22, - 175, 210,157,172, - 221, 157, 3,112, - 255, 157, 3,112}; - +// http://seaviewsensing.com/pub/cpt-city/es/pink_splash/es_pinksplash_08.c3g +const uint8_t es_pinksplash_08_gp[] PROGMEM = { + 0, 186, 63, 255, + 127, 227, 9, 85, + 175, 234, 205, 213, + 221, 205, 38, 176, + 255, 205, 38, 176, +}; // Gradient palette "es_ocean_breeze_036_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/ocean_breeze/tn/es_ocean_breeze_036.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 16 bytes of program space. - -const byte es_ocean_breeze_036_gp[] PROGMEM = { - 0, 1, 6, 7, - 89, 1, 99,111, - 153, 144,209,255, - 255, 0, 73, 82}; - +// http://seaviewsensing.com/pub/cpt-city/es/ocean_breeze/es_ocean_breeze_036.c3g +const uint8_t es_ocean_breeze_036_gp[] PROGMEM = { + 0, 16, 48, 51, + 89, 27, 166, 175, + 153, 197, 233, 255, + 255, 0, 145, 152}; // Gradient palette "departure_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/mjf/tn/departure.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 88 bytes of program space. - -const byte departure_gp[] PROGMEM = { - 0, 8, 3, 0, - 42, 23, 7, 0, - 63, 75, 38, 6, - 84, 169, 99, 38, - 106, 213,169,119, - 116, 255,255,255, - 138, 135,255,138, - 148, 22,255, 24, - 170, 0,255, 0, - 191, 0,136, 0, - 212, 0, 55, 0, - 255, 0, 55, 0}; - +// http://seaviewsensing.com/pub/cpt-city/mjf/departure.c3g +const uint8_t departure_gp[] PROGMEM = { + 0, 53, 34, 0, + 42, 86, 51, 0, + 63, 147, 108, 49, + 84, 212, 166, 108, + 106, 235, 212, 180, + 116, 255, 255, 255, + 138, 191, 255, 193, + 148, 84, 255, 88, + 170, 0, 255, 0, + 191, 0, 192, 0, + 212, 0, 128, 0, + 255, 0, 128, 0}; // Gradient palette "es_landscape_64_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/landscape/tn/es_landscape_64.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 36 bytes of program space. - -const byte es_landscape_64_gp[] PROGMEM = { - 0, 0, 0, 0, - 37, 2, 25, 1, - 76, 15,115, 5, - 127, 79,213, 1, - 128, 126,211, 47, - 130, 188,209,247, - 153, 144,182,205, - 204, 59,117,250, - 255, 1, 37,192}; - +// http://seaviewsensing.com/pub/cpt-city/es/landscape/es_landscape_64.c3g +const uint8_t es_landscape_64_gp[] PROGMEM = { + 0, 0, 0, 0, + 37, 31, 89, 19, + 76, 72, 178, 43, + 127, 150, 235, 5, + 128, 186, 234, 119, + 130, 222, 233, 252, + 153, 197, 219, 231, + 204, 132, 179, 253, + 255, 28, 107, 225}; // Gradient palette "es_landscape_33_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/landscape/tn/es_landscape_33.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 24 bytes of program space. - -const byte es_landscape_33_gp[] PROGMEM = { - 0, 1, 5, 0, - 19, 32, 23, 1, - 38, 161, 55, 1, - 63, 229,144, 1, - 66, 39,142, 74, - 255, 1, 4, 1}; - +// http://seaviewsensing.com/pub/cpt-city/es/landscape/es_landscape_33.c3g +const uint8_t es_landscape_33_gp[] PROGMEM = { + 0, 12, 45, 0, + 19, 101, 86, 2, + 38, 207, 128, 4, + 63, 243, 197, 18, + 66, 109, 196, 146, + 255, 5, 39, 7}; // Gradient palette "rainbowsherbet_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/ma/icecream/tn/rainbowsherbet.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 28 bytes of program space. - -const byte rainbowsherbet_gp[] PROGMEM = { - 0, 255, 33, 4, - 43, 255, 68, 25, - 86, 255, 7, 25, - 127, 255, 82,103, - 170, 255,255,242, - 209, 42,255, 22, - 255, 87,255, 65}; - +// http://seaviewsensing.com/pub/cpt-city/ma/icecream/rainbowsherbet.c3g +const uint8_t rainbowsherbet_gp[] PROGMEM = { + 0, 255, 102, 41, + 43, 255, 140, 90, + 86, 255, 51, 90, + 127, 255, 153, 169, + 170, 255, 255, 249, + 209, 113, 255, 85, + 255, 157, 255, 137}; // Gradient palette "gr65_hult_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/hult/tn/gr65_hult.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 24 bytes of program space. - -const byte gr65_hult_gp[] PROGMEM = { - 0, 247,176,247, - 48, 255,136,255, - 89, 220, 29,226, - 160, 7, 82,178, - 216, 1,124,109, - 255, 1,124,109}; - +// http://seaviewsensing.com/pub/cpt-city/hult/gr65_hult.c3g +const uint8_t gr65_hult_gp[] PROGMEM = { + 0, 251, 216, 252, + 48, 255, 192, 255, + 89, 239, 95, 241, + 160, 51, 153, 217, + 216, 24, 184, 174, + 255, 24, 184, 174}; // Gradient palette "gr64_hult_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/hult/tn/gr64_hult.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 32 bytes of program space. - -const byte gr64_hult_gp[] PROGMEM = { - 0, 1,124,109, - 66, 1, 93, 79, - 104, 52, 65, 1, - 130, 115,127, 1, - 150, 52, 65, 1, - 201, 1, 86, 72, - 239, 0, 55, 45, - 255, 0, 55, 45}; - +// http://seaviewsensing.com/pub/cpt-city/hult/gr64_hult.c3g +const uint8_t gr64_hult_gp[] PROGMEM = { + 0, 24, 184, 174, + 66, 8, 162, 150, + 104, 124, 137, 7, + 130, 178, 186, 22, + 150, 124, 137, 7, + 201, 6, 156, 144, + 239, 0, 128, 117, + 255, 0, 128, 117}; // Gradient palette "GMT_drywet_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gmt/tn/GMT_drywet.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 28 bytes of program space. - -const byte GMT_drywet_gp[] PROGMEM = { - 0, 47, 30, 2, - 42, 213,147, 24, - 84, 103,219, 52, - 127, 3,219,207, - 170, 1, 48,214, - 212, 1, 1,111, - 255, 1, 7, 33}; - +// http://seaviewsensing.com/pub/cpt-city/gmt/GMT_drywet.c3g +const uint8_t GMT_drywet_gp[] PROGMEM = { + 0, 119, 97, 33, + 42, 235, 199, 88, + 84, 169, 238, 124, + 127, 37, 238, 232, + 170, 7, 120, 236, + 212, 27, 1, 175, + 255, 4, 51, 101}; // Gradient palette "ib15_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/ing/general/tn/ib15.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 24 bytes of program space. - -const byte ib15_gp[] PROGMEM = { - 0, 113, 91,147, - 72, 157, 88, 78, - 89, 208, 85, 33, - 107, 255, 29, 11, - 141, 137, 31, 39, - 255, 59, 33, 89}; - +// http://seaviewsensing.com/pub/cpt-city/ing/general/ib15.c3g +const uint8_t ib15_gp[] PROGMEM = { + 0, 177, 160, 199, + 72, 205, 158, 149, + 89, 233, 155, 101, + 107, 255, 95, 63, + 141, 192, 98, 109, + 255, 132, 101, 159}; // Gradient palette "Tertiary_01_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/vermillion/tn/Tertiary_01.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 20 bytes of program space. - -const byte Tertiary_01_gp[] PROGMEM = { - 0, 0, 1,255, - 63, 3, 68, 45, - 127, 23,255, 0, - 191, 100, 68, 1, - 255, 255, 1, 4}; - +// http://seaviewsensing.com/pub/cpt-city/nd/vermillion/Tertiary_01.c3g +const uint8_t Tertiary_01_gp[] PROGMEM = { + 0, 0, 25, 255, + 63, 38, 140, 117, + 127, 86, 255, 0, + 191, 167, 140, 19, + 255, 255, 25, 41}; // Gradient palette "lava_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/neota/elem/tn/lava.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 52 bytes of program space. - -const byte lava_gp[] PROGMEM = { - 0, 0, 0, 0, - 46, 18, 0, 0, - 96, 113, 0, 0, - 108, 142, 3, 1, - 119, 175, 17, 1, - 146, 213, 44, 2, - 174, 255, 82, 4, - 188, 255,115, 4, - 202, 255,156, 4, - 218, 255,203, 4, - 234, 255,255, 4, - 244, 255,255, 71, - 255, 255,255,255}; - - -// Gradient palette "fierce_ice_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/neota/elem/tn/fierce-ice.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 28 bytes of program space. - -const byte fierce_ice_gp[] PROGMEM = { - 0, 0, 0, 0, - 59, 0, 9, 45, - 119, 0, 38,255, - 149, 3,100,255, - 180, 23,199,255, - 217, 100,235,255, - 255, 255,255,255}; +// http://seaviewsensing.com/pub/cpt-city/neota/elem/lava.c3g +const uint8_t lava_gp[] PROGMEM = { + 0, 0, 0, 0, + 46, 77, 0, 0, + 96, 177, 0, 0, + 108, 196, 38, 9, + 119, 215, 76, 19, + 146, 235, 115, 29, + 174, 255, 153, 41, + 188, 255, 178, 41, + 202, 255, 204, 41, + 218, 255, 230, 41, + 234, 255, 255, 41, + 244, 255, 255, 143, + 255, 255, 255, 255}; +// Gradient palette "fierce-ice_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/neota/elem/fierce-ice.c3g +const uint8_t fierce_ice_gp[] PROGMEM = { + 0, 0, 0, 0, + 59, 0, 51, 117, + 119, 0, 102, 255, + 149, 38, 153, 255, + 180, 86, 204, 255, + 217, 167, 230, 255, + 255, 255, 255, 255}; // Gradient palette "Colorfull_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Colorfull.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 44 bytes of program space. - -const byte Colorfull_gp[] PROGMEM = { - 0, 10, 85, 5, - 25, 29,109, 18, - 60, 59,138, 42, - 93, 83, 99, 52, - 106, 110, 66, 64, - 109, 123, 49, 65, - 113, 139, 35, 66, - 116, 192,117, 98, - 124, 255,255,137, - 168, 100,180,155, - 255, 22,121,174}; - +// http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Colorfull.c3g +const uint8_t Colorfull_gp[] PROGMEM = { + 0, 61, 155, 44, + 25, 95, 174, 77, + 60, 132, 193, 113, + 93, 154, 166, 125, + 106, 175, 138, 136, + 109, 183, 121, 137, + 113, 194, 104, 138, + 116, 225, 179, 165, + 124, 255, 255, 192, + 168, 167, 218, 203, + 255, 84, 182, 215}; // Gradient palette "Pink_Purple_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Pink_Purple.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 44 bytes of program space. - -const byte Pink_Purple_gp[] PROGMEM = { - 0, 19, 2, 39, - 25, 26, 4, 45, - 51, 33, 6, 52, - 76, 68, 62,125, - 102, 118,187,240, - 109, 163,215,247, - 114, 217,244,255, - 122, 159,149,221, - 149, 113, 78,188, - 183, 128, 57,155, - 255, 146, 40,123}; - +// http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Pink_Purple.c3g +const uint8_t Pink_Purple_gp[] PROGMEM = { + 0, 79, 32, 109, + 25, 90, 40, 117, + 51, 102, 48, 124, + 76, 141, 135, 185, + 102, 180, 222, 248, + 109, 208, 236, 252, + 114, 237, 250, 255, + 122, 206, 200, 239, + 149, 177, 149, 222, + 183, 187, 130, 203, + 255, 198, 111, 184}; // Gradient palette "Sunset_Real_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Sunset_Real.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 28 bytes of program space. - -const byte Sunset_Real_gp[] PROGMEM = { - 0, 120, 0, 0, - 22, 179, 22, 0, - 51, 255,104, 0, - 85, 167, 22, 18, - 135, 100, 0,103, - 198, 16, 0,130, - 255, 0, 0,160}; - +// http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Sunset_Real.c3g +const uint8_t Sunset_Real_gp[] PROGMEM = { + 0, 181, 0, 0, + 22, 218, 85, 0, + 51, 255, 170, 0, + 85, 211, 85, 77, + 135, 167, 0, 169, + 198, 73, 0, 188, + 255, 0, 0, 207}; // Gradient palette "Sunset_Yellow_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Sunset_Yellow.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 44 bytes of program space. - -const byte Sunset_Yellow_gp[] PROGMEM = { - 0, 10, 62,123, - 36, 56,130,103, - 87, 153,225, 85, - 100, 199,217, 68, - 107, 255,207, 54, - 115, 247,152, 57, - 120, 239,107, 61, - 128, 247,152, 57, - 180, 255,207, 54, - 223, 255,227, 48, - 255, 255,248, 42}; - +// http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Sunset_Yellow.c3g +const uint8_t Sunset_Yellow_gp[] PROGMEM = { + 0, 61, 135, 184, + 36, 129, 188, 169, + 87, 203, 241, 155, + 100, 228, 237, 141, + 107, 255, 232, 127, + 115, 251, 202, 130, + 120, 248, 172, 133, + 128, 251, 202, 130, + 180, 255, 232, 127, + 223, 255, 242, 120, + 255, 255, 252, 113}; // Gradient palette "Beech_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Beech.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 60 bytes of program space. - -const byte Beech_gp[] PROGMEM = { - 0, 255,252,214, - 12, 255,252,214, - 22, 255,252,214, - 26, 190,191,115, - 28, 137,141, 52, - 28, 112,255,205, - 50, 51,246,214, - 71, 17,235,226, - 93, 2,193,199, - 120, 0,156,174, - 133, 1,101,115, - 136, 1, 59, 71, - 136, 7,131,170, - 208, 1, 90,151, - 255, 0, 56,133}; - +// http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Beech.c3g +const uint8_t Beech_gp[] PROGMEM = { + 0, 255, 254, 236, + 12, 255, 254, 236, + 22, 255, 254, 236, + 26, 223, 224, 178, + 28, 192, 195, 124, + 28, 176, 255, 231, + 50, 123, 251, 236, + 71, 74, 246, 241, + 93, 33, 225, 228, + 120, 0, 204, 215, + 133, 4, 168, 178, + 136, 10, 132, 143, + 136, 51, 189, 212, + 208, 23, 159, 201, + 255, 0, 129, 190}; // Gradient palette "Another_Sunset_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Another_Sunset.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 32 bytes of program space. - -const byte Another_Sunset_gp[] PROGMEM = { - 0, 110, 49, 11, - 29, 55, 34, 10, - 68, 22, 22, 9, - 68, 239,124, 8, - 97, 220,156, 27, - 124, 203,193, 61, - 178, 33, 53, 56, - 255, 0, 1, 52}; - - - - +// http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Another_Sunset.c3g +const uint8_t Another_Sunset_gp[] PROGMEM = { + 0, 175, 121, 62, + 29, 128, 103, 60, + 68, 84, 84, 58, + 68, 248, 184, 55, + 97, 239, 204, 93, + 124, 230, 225, 133, + 178, 102, 125, 129, + 255, 0, 26, 125}; // Gradient palette "es_autumn_19_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/autumn/tn/es_autumn_19.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 52 bytes of program space. - -const byte es_autumn_19_gp[] PROGMEM = { - 0, 26, 1, 1, - 51, 67, 4, 1, - 84, 118, 14, 1, - 104, 137,152, 52, - 112, 113, 65, 1, - 122, 133,149, 59, - 124, 137,152, 52, - 135, 113, 65, 1, - 142, 139,154, 46, - 163, 113, 13, 1, - 204, 55, 3, 1, - 249, 17, 1, 1, - 255, 17, 1, 1}; - +// http://seaviewsensing.com/pub/cpt-city/es/autumn/es_autumn_19.c3g +const uint8_t es_autumn_19_gp[] PROGMEM = { + 0, 90, 14, 5, + 51, 139, 41, 13, + 84, 180, 70, 17, + 104, 192, 202, 125, + 112, 177, 137, 3, + 122, 190, 200, 131, + 124, 192, 202, 124, + 135, 177, 137, 3, + 142, 194, 203, 118, + 163, 177, 68, 17, + 204, 128, 35, 12, + 249, 74, 5, 2, + 255, 74, 5, 2}; // Gradient palette "BlacK_Blue_Magenta_White_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/basic/tn/BlacK_Blue_Magenta_White.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 28 bytes of program space. - -const byte BlacK_Blue_Magenta_White_gp[] PROGMEM = { - 0, 0, 0, 0, - 42, 0, 0, 45, - 84, 0, 0,255, - 127, 42, 0,255, - 170, 255, 0,255, - 212, 255, 55,255, - 255, 255,255,255}; - +// http://seaviewsensing.com/pub/cpt-city/nd/basic/BlacK_Blue_Magenta_White.c3g +const uint8_t BlacK_Blue_Magenta_White_gp[] PROGMEM = { + 0, 0, 0, 0, + 42, 0, 0, 117, + 84, 0, 0, 255, + 127, 113, 0, 255, + 170, 255, 0, 255, + 212, 255, 128, 255, + 255, 255, 255, 255}; // Gradient palette "BlacK_Magenta_Red_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/basic/tn/BlacK_Magenta_Red.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 20 bytes of program space. - -const byte BlacK_Magenta_Red_gp[] PROGMEM = { - 0, 0, 0, 0, - 63, 42, 0, 45, - 127, 255, 0,255, - 191, 255, 0, 45, - 255, 255, 0, 0}; - +// http://seaviewsensing.com/pub/cpt-city/nd/basic/BlacK_Magenta_Red.c3g +const uint8_t BlacK_Magenta_Red_gp[] PROGMEM = { + 0, 0, 0, 0, + 63, 113, 0, 117, + 127, 255, 0, 255, + 191, 255, 0, 117, + 255, 255, 0, 0}; // Gradient palette "BlacK_Red_Magenta_Yellow_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/basic/tn/BlacK_Red_Magenta_Yellow.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 28 bytes of program space. - -const byte BlacK_Red_Magenta_Yellow_gp[] PROGMEM = { - 0, 0, 0, 0, - 42, 42, 0, 0, - 84, 255, 0, 0, - 127, 255, 0, 45, - 170, 255, 0,255, - 212, 255, 55, 45, - 255, 255,255, 0}; - +// http://seaviewsensing.com/pub/cpt-city/nd/basic/BlacK_Red_Magenta_Yellow.c3g +const uint8_t BlacK_Red_Magenta_Yellow_gp[] PROGMEM = { + 0, 0, 0, 0, + 42, 113, 0, 0, + 84, 255, 0, 0, + 127, 255, 0, 117, + 170, 255, 0, 255, + 212, 255, 128, 117, + 255, 255, 255, 0}; // Gradient palette "Blue_Cyan_Yellow_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/basic/tn/Blue_Cyan_Yellow.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 20 bytes of program space. - -const byte Blue_Cyan_Yellow_gp[] PROGMEM = { - 0, 0, 0,255, - 63, 0, 55,255, - 127, 0,255,255, - 191, 42,255, 45, - 255, 255,255, 0}; - +// http://seaviewsensing.com/pub/cpt-city/nd/basic/Blue_Cyan_Yellow.c3g +const uint8_t Blue_Cyan_Yellow_gp[] PROGMEM = { + 0, 0, 0, 255, + 63, 0, 128, 255, + 127, 0, 255, 255, + 191, 113, 255, 117, + 255, 255, 255, 0}; //Custom palette by Aircoookie - const byte Orange_Teal_gp[] PROGMEM = { 0, 0,150, 92, 55, 0,150, 92, @@ -501,7 +376,6 @@ const byte Orange_Teal_gp[] PROGMEM = { 255, 255, 72, 0}; //Custom palette by Aircoookie - const byte Tiamat_gp[] PROGMEM = { 0, 1, 2, 14, //gc 33, 2, 5, 35, //gc from 47, 61,126 @@ -516,7 +390,6 @@ const byte Tiamat_gp[] PROGMEM = { 255, 255,249,255}; //Custom palette by Aircoookie - const byte April_Night_gp[] PROGMEM = { 0, 1, 5, 45, //deep blue 10, 1, 5, 45, @@ -584,282 +457,257 @@ const byte Atlantica_gp[] PROGMEM = { const byte C9_2_gp[] PROGMEM = { 0, 6, 126, 2, //green 45, 6, 126, 2, - 45, 4, 30, 114, //blue + 46, 4, 30, 114, //blue 90, 4, 30, 114, - 90, 255, 5, 0, //red + 91, 255, 5, 0, //red 135, 255, 5, 0, - 135, 196, 57, 2, //amber + 136, 196, 57, 2, //amber 180, 196, 57, 2, - 180, 137, 85, 2, //yellow + 181, 137, 85, 2, //yellow 255, 137, 85, 2}; //C9, but brighter and with a less purple blue const byte C9_new_gp[] PROGMEM = { 0, 255, 5, 0, //red 60, 255, 5, 0, - 60, 196, 57, 2, //amber (start 61?) + 61, 196, 57, 2, //amber (start 61?) 120, 196, 57, 2, - 120, 6, 126, 2, //green (start 126?) + 121, 6, 126, 2, //green (start 126?) 180, 6, 126, 2, - 180, 4, 30, 114, //blue (start 191?) + 181, 4, 30, 114, //blue (start 191?) 255, 4, 30, 114}; // Gradient palette "temperature_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/arendal/tn/temperature.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 144 bytes of program space. +// http://seaviewsensing.com/pub/cpt-city/arendal/temperature.c3g +const uint8_t temperature_gp[] PROGMEM = { + 0, 20, 92, 171, + 14, 15, 111, 186, + 28, 6, 142, 211, + 42, 2, 161, 227, + 56, 16, 181, 239, + 70, 38, 188, 201, + 84, 86, 204, 200, + 99, 139, 219, 176, + 113, 182, 229, 125, + 127, 196, 230, 63, + 141, 241, 240, 22, + 155, 254, 222, 30, + 170, 251, 199, 4, + 184, 247, 157, 9, + 198, 243, 114, 15, + 226, 213, 30, 29, + 240, 151, 38, 35, + 255, 151, 38, 35}; -const byte temperature_gp[] PROGMEM = { - 0, 1, 27,105, - 14, 1, 40,127, - 28, 1, 70,168, - 42, 1, 92,197, - 56, 1,119,221, - 70, 3,130,151, - 84, 23,156,149, - 99, 67,182,112, - 113, 121,201, 52, - 127, 142,203, 11, - 141, 224,223, 1, - 155, 252,187, 2, - 170, 247,147, 1, - 184, 237, 87, 1, - 198, 229, 43, 1, - 226, 171, 2, 2, - 240, 80, 3, 3, - 255, 80, 3, 3}; - - const byte Aurora2_gp[] PROGMEM = { - 0, 17, 177, 13, //Greenish - 64, 121, 242, 5, //Greenish - 128, 25, 173, 121, //Turquoise - 192, 250, 77, 127, //Pink - 255, 171, 101, 221 //Purple - }; - - // Gradient palette "bhw1_01_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_01.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 12 bytes of program space. - -const byte retro_clown_gp[] PROGMEM = { - 0, 227,101, 3, - 117, 194, 18, 19, - 255, 92, 8,192}; +// Gradient palette "bhw1_01_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_01.c3g +const uint8_t retro_clown_gp[] PROGMEM = { + 0, 242, 168, 38, + 117, 226, 78, 80, + 255, 161, 54, 225, +}; // Gradient palette "bhw1_04_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_04.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 20 bytes of program space. - -const byte candy_gp[] PROGMEM = { - 0, 229,227, 1, - 15, 227,101, 3, - 142, 40, 1, 80, - 198, 17, 1, 79, - 255, 0, 0, 45}; +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_04.c3g +const uint8_t candy_gp[] PROGMEM = { + 0, 243, 242, 23, + 15, 242, 168, 38, + 142, 111, 21, 151, + 198, 74, 22, 150, + 255, 0, 0, 117}; // Gradient palette "bhw1_05_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_05.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 8 bytes of program space. - -const byte toxy_reaf_gp[] PROGMEM = { - 0, 1,221, 53, - 255, 73, 3,178}; +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_05.c3g +const uint8_t toxy_reaf_gp[] PROGMEM = { + 0, 2, 239, 126, + 255, 145, 35, 217}; // Gradient palette "bhw1_06_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_06.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 16 bytes of program space. - -const byte fairy_reaf_gp[] PROGMEM = { - 0, 184, 1,128, - 160, 1,193,182, - 219, 153,227,190, - 255, 255,255,255}; +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_06.c3g +const uint8_t fairy_reaf_gp[] PROGMEM = { + 0, 220, 19, 187, + 160, 12, 225, 219, + 219, 203, 242, 223, + 255, 255, 255, 255}; // Gradient palette "bhw1_14_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_14.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 36 bytes of program space. - -const byte semi_blue_gp[] PROGMEM = { - 0, 0, 0, 0, - 12, 1, 1, 3, - 53, 8, 1, 22, - 80, 4, 6, 89, - 119, 2, 25,216, - 145, 7, 10, 99, - 186, 15, 2, 31, - 233, 2, 1, 5, - 255, 0, 0, 0}; +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_14.c3g +const uint8_t semi_blue_gp[] PROGMEM = { + 0, 0, 0, 0, + 12, 24, 4, 38, + 53, 55, 8, 84, + 80, 43, 48, 159, + 119, 31, 89, 237, + 145, 50, 59, 166, + 186, 71, 30, 98, + 233, 31, 15, 45, + 255, 0, 0, 0}; // Gradient palette "bhw1_three_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_three.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 32 bytes of program space. - -const byte pink_candy_gp[] PROGMEM = { - 0, 255,255,255, - 45, 7, 12,255, - 112, 227, 1,127, - 112, 227, 1,127, - 140, 255,255,255, - 155, 227, 1,127, - 196, 45, 1, 99, - 255, 255,255,255}; +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_three.c3g +const uint8_t pink_candy_gp[] PROGMEM = { + 0, 255, 255, 255, + 45, 50, 64, 255, + 112, 242, 16, 186, + 140, 255, 255, 255, + 155, 242, 16, 186, + 196, 116, 13, 166, + 255, 255, 255, 255}; // Gradient palette "bhw1_w00t_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_w00t.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 16 bytes of program space. - -const byte red_reaf_gp[] PROGMEM = { - 0, 3, 13, 43, - 104, 78,141,240, - 188, 255, 0, 0, - 255, 28, 1, 1}; - +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_w00t.c3g +const uint8_t red_reaf_gp[] PROGMEM = { + 0, 36, 68, 114, + 104, 149, 195, 248, + 188, 255, 0, 0, + 255, 94, 14, 9}; // Gradient palette "bhw2_23_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw2/tn/bhw2_23.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Red & Flash in SR -// Size: 28 bytes of program space. - -const byte aqua_flash_gp[] PROGMEM = { - 0, 0, 0, 0, - 66, 57,227,233, - 96, 255,255, 8, - 124, 255,255,255, - 153, 255,255, 8, - 188, 57,227,233, - 255, 0, 0, 0}; +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw2/bhw2_23.c3g +const uint8_t aqua_flash_gp[] PROGMEM = { + 0, 0, 0, 0, + 66, 130, 242, 245, + 96, 255, 255, 53, + 124, 255, 255, 255, + 153, 255, 255, 53, + 188, 130, 242, 245, + 255, 0, 0, 0}; // Gradient palette "bhw2_xc_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw2/tn/bhw2_xc.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// YBlue in SR -// Size: 28 bytes of program space. +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw2/bhw2_xc.c3g +const uint8_t yelblu_hot_gp[] PROGMEM = { + 0, 43, 30, 57, + 58, 73, 0, 119, + 122, 87, 0, 74, + 158, 197, 57, 22, + 183, 218, 117, 27, + 219, 239, 177, 32, + 255, 246, 247, 27, +}; -const byte yelblu_hot_gp[] PROGMEM = { - 0, 4, 2, 9, - 58, 16, 0, 47, - 122, 24, 0, 16, - 158, 144, 9, 1, - 183, 179, 45, 1, - 219, 220,114, 2, - 255, 234,237, 1}; - - // Gradient palette "bhw2_45_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw2/tn/bhw2_45.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 24 bytes of program space. - -const byte lite_light_gp[] PROGMEM = { - 0, 0, 0, 0, - 9, 1, 1, 1, - 40, 5, 5, 6, - 66, 5, 5, 6, - 101, 10, 1, 12, - 255, 0, 0, 0}; +// Gradient palette "bhw2_45_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw2/bhw2_45.c3g +const uint8_t lite_light_gp[] PROGMEM = { + 0, 0, 0, 0, + 9, 20, 21, 22, + 40, 46, 43, 49, + 66, 46, 43, 49, + 101, 61, 16, 65, + 255, 0, 0, 0}; // Gradient palette "bhw2_22_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw2/tn/bhw2_22.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Pink Plasma in SR -// Size: 20 bytes of program space. - -const byte red_flash_gp[] PROGMEM = { - 0, 0, 0, 0, - 99, 227, 1, 1, - 130, 249,199, 95, - 155, 227, 1, 1, - 255, 0, 0, 0}; +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw2/bhw2_22.c3g +const uint8_t red_flash_gp[] PROGMEM = { + 0, 0, 0, 0, + 99, 242, 12, 8, + 130, 253, 228, 163, + 155, 242, 12, 8, + 255, 0, 0, 0}; // Gradient palette "bhw3_40_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw3/tn/bhw3_40.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 32 bytes of program space. - -const byte blink_red_gp[] PROGMEM = { - 0, 1, 1, 1, - 43, 4, 1, 11, - 76, 10, 1, 3, - 109, 161, 4, 29, - 127, 255, 86,123, - 165, 125, 16,160, - 204, 35, 13,223, - 255, 18, 2, 18}; +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw3/bhw3_40.c3g +const uint8_t blink_red_gp[] PROGMEM = { + 0, 4, 7, 4, + 43, 40, 25, 62, + 76, 61, 15, 36, + 109, 207, 39, 96, + 127, 255, 156, 184, + 165, 185, 73, 207, + 204, 105, 66, 240, + 255, 77, 29, 78}; // Gradient palette "bhw3_52_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw3/tn/bhw3_52.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Yellow2Blue in SR -// Size: 28 bytes of program space. - -const byte red_shift_gp[] PROGMEM = { - 0, 31, 1, 27, - 45, 34, 1, 16, - 99, 137, 5, 9, - 132, 213,128, 10, - 175, 199, 22, 1, - 201, 199, 9, 6, - 255, 1, 0, 1}; +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw3/bhw3_52.c3g +const uint8_t red_shift_gp[] PROGMEM = { + 0, 98, 22, 93, + 45, 103, 22, 73, + 99, 192, 45, 56, + 132, 235, 187, 59, + 175, 228, 85, 26, + 201, 228, 56, 48, + 255, 2, 0, 2}; // Gradient palette "bhw4_097_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw4/tn/bhw4_097.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Yellow2Red in SR -// Size: 44 bytes of program space. - -const byte red_tide_gp[] PROGMEM = { - 0, 247, 5, 0, - 28, 255, 67, 1, - 43, 234, 88, 11, - 58, 234,176, 51, - 84, 229, 28, 1, - 114, 113, 12, 1, - 140, 255,225, 44, - 168, 113, 12, 1, - 196, 244,209, 88, - 216, 255, 28, 1, - 255, 53, 1, 1}; +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw4/bhw4_097.c3g +const uint8_t red_tide_gp[] PROGMEM = { + 0, 251, 46, 0, + 28, 255, 139, 25, + 43, 246, 158, 63, + 58, 246, 216, 123, + 84, 243, 94, 10, + 114, 177, 65, 11, + 140, 255, 241, 115, + 168, 177, 65, 11, + 196, 250, 233, 158, + 216, 255, 94, 6, + 255, 126, 8, 4}; // Gradient palette "bhw4_017_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw4/tn/bhw4_017.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 40 bytes of program space. +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw4/bhw4_017.c3g +const uint8_t candy2_gp[] PROGMEM = { + 0, 109, 102, 102, + 25, 42, 49, 71, + 48, 121, 96, 84, + 73, 241, 214, 26, + 89, 216, 104, 44, + 130, 42, 49, 71, + 163, 255, 177, 47, + 186, 241, 214, 26, + 211, 109, 102, 102, + 255, 20, 19, 13}; -const byte candy2_gp[] PROGMEM = { - 0, 39, 33, 34, - 25, 4, 6, 15, - 48, 49, 29, 22, - 73, 224,173, 1, - 89, 177, 35, 5, - 130, 4, 6, 15, - 163, 255,114, 6, - 186, 224,173, 1, - 211, 39, 33, 34, - 255, 1, 1, 1}; +const byte trafficlight_gp[] PROGMEM = { + 0, 0, 0, 0, //black + 85, 0, 255, 0, //green + 170, 255, 255, 0, //yellow + 255, 255, 0, 0}; //red + +const byte Aurora2_gp[] PROGMEM = { + 0, 17, 177, 13, //Greenish + 64, 121, 242, 5, //Greenish + 128, 25, 173, 121, //Turquoise + 192, 250, 77, 127, //Pink + 255, 171, 101, 221}; //Purple + +// FastLed palettes, corrected with inverse gamma of 2.2 to match original looks + +// Party colors +const TProgmemRGBPalette16 PartyColors_gc22 FL_PROGMEM = { + 0x9B00D5, 0xBD00B8, 0xDA0092, 0xF3005C, + 0xF45500, 0xDC8F00, 0xD5B400, 0xD5D500, + 0xD59B00, 0xEF6600, 0xF90044, 0xE10086, + 0xC400B0, 0xA300CF, 0x7600E8, 0x0032FC}; + +// Rainbow colors +const TProgmemRGBPalette16 RainbowColors_gc22 FL_PROGMEM = { + 0xFF0000, 0xEB7000, 0xD59B00, 0xD5BA00, + 0xD5D500, 0x9CEB00, 0x00FF00, 0x00EB70, + 0x00D59B, 0x009CD4, 0x0000FF, 0x7000EB, + 0x9B00D5, 0xBA00BB, 0xD5009B, 0xEB0072}; + +// Rainbow colors with alternatating stripes of black +const TProgmemRGBPalette16 RainbowStripeColors_gc22 FL_PROGMEM = { + 0xFF0000, 0x000000, 0xD59B00, 0x000000, + 0xD5D500, 0x000000, 0x00FF00, 0x000000, + 0x00D59B, 0x000000, 0x0000FF, 0x000000, + 0x9B00D5, 0x000000, 0xD5009B, 0x000000}; // array of fastled palettes (palette 6 - 12) const TProgmemRGBPalette16 *const fastledPalettes[] PROGMEM = { - &PartyColors_p, //06-00 Party + &PartyColors_gc22, //06-00 Party &CloudColors_p, //07-01 Cloud &LavaColors_p, //08-02 Lava &OceanColors_p, //09-03 Ocean &ForestColors_p, //10-04 Forest - &RainbowColors_p, //11-05 Rainbow - &RainbowStripeColors_p //12-06 Rainbow Bands + &RainbowColors_gc22, //11-05 Rainbow + &RainbowStripeColors_gc22 //12-06 Rainbow Bands }; // Single array of defined cpt-city color palettes. // This will let us programmatically choose one based on // a number, rather than having to activate each explicitly // by name every time. -const byte* const gGradientPalettes[] PROGMEM = { +const uint8_t* const gGradientPalettes[] PROGMEM = { Sunset_Real_gp, //13-00 Sunset es_rivendell_15_gp, //14-01 Rivendell es_ocean_breeze_036_gp, //15-02 Breeze @@ -917,7 +765,8 @@ const byte* const gGradientPalettes[] PROGMEM = { blink_red_gp, //67-54 Blink Red red_shift_gp, //68-55 Red Shift red_tide_gp, //69-56 Red Tide - candy2_gp //70-57 Candy2 + candy2_gp, //70-57 Candy2 + trafficlight_gp //71-58 Traffic Light }; #endif diff --git a/wled00/pin_manager.cpp b/wled00/pin_manager.cpp index ffbcad41..f95e421f 100644 --- a/wled00/pin_manager.cpp +++ b/wled00/pin_manager.cpp @@ -1,5 +1,5 @@ -#include "pin_manager.h" #include "wled.h" +#include "pin_manager.h" #ifdef ARDUINO_ARCH_ESP32 #ifdef bitRead @@ -13,6 +13,16 @@ #endif #endif +// Pin management state variables +#ifdef ESP8266 +static uint32_t pinAlloc = 0UL; // 1 bit per pin, we use first 17bits +#else +static uint64_t pinAlloc = 0ULL; // 1 bit per pin, we use 50 bits on ESP32-S3 +static uint16_t ledcAlloc = 0; // up to 16 LEDC channels (WLED_MAX_ANALOG_CHANNELS) +#endif +static uint8_t i2cAllocCount = 0; // allow multiple allocation of I2C bus pins but keep track of allocations +static uint8_t spiAllocCount = 0; // allow multiple allocation of SPI bus pins but keep track of allocations +static PinOwner ownerTag[WLED_NUM_PINS] = { PinOwner::None }; /// Actual allocation/deallocation routines bool PinManager::deallocatePin(byte gpio, PinOwner tag) @@ -137,7 +147,9 @@ bool PinManager::allocateMultiplePins(const int8_t * mptArray, byte arrayElement bool PinManager::allocatePin(byte gpio, bool output, PinOwner tag) { // HW I2C & SPI pins have to be allocated using allocateMultiplePins variant since there is always SCL/SDA pair - if (!isPinOk(gpio, output) || (gpio >= WLED_NUM_PINS) || tag==PinOwner::HW_I2C || tag==PinOwner::HW_SPI) { + // DMX_INPUT pins have to be allocated using allocateMultiplePins variant since there is always RX/TX/EN triple + if (!isPinOk(gpio, output) || (gpio >= WLED_NUM_PINS) || tag==PinOwner::HW_I2C || tag==PinOwner::HW_SPI + || tag==PinOwner::DMX_INPUT) { #ifdef WLED_DEBUG if (gpio < 255) { // 255 (-1) is the "not defined GPIO" if (!isPinOk(gpio, output)) { @@ -220,8 +232,20 @@ bool PinManager::isPinOk(byte gpio, bool output) // JTAG: GPIO39-42 are usually used for inline debugging // GPIO46 is input only and pulled down #else - if (gpio > 5 && gpio < 12) return false; //SPI flash pins - if (strncmp_P(PSTR("ESP32-PICO"), ESP.getChipModel(), 10) == 0 && (gpio == 16 || gpio == 17)) return false; // PICO-D4: gpio16+17 are in use for onboard SPI FLASH + + if ((strncmp_P(PSTR("ESP32-U4WDH"), ESP.getChipModel(), 11) == 0) || // this is the correct identifier, but.... + (strncmp_P(PSTR("ESP32-PICO-D2"), ESP.getChipModel(), 13) == 0)) { // https://github.com/espressif/arduino-esp32/issues/10683 + // this chip has 4 MB of internal Flash and different packaging, so available pins are different! + if (((gpio > 5) && (gpio < 9)) || (gpio == 11)) + return false; + } else { + // for classic ESP32 (non-mini) modules, these are the SPI flash pins + if (gpio > 5 && gpio < 12) return false; //SPI flash pins + } + + if (((strncmp_P(PSTR("ESP32-PICO"), ESP.getChipModel(), 10) == 0) || + (strncmp_P(PSTR("ESP32-U4WDH"), ESP.getChipModel(), 11) == 0)) + && (gpio == 16 || gpio == 17)) return false; // PICO-D4/U4WDH: gpio16+17 are in use for onboard SPI FLASH if (gpio == 16 || gpio == 17) return !psramFound(); //PSRAM pins on ESP32 (these are IO) #endif if (output) return digitalPinCanOutput(gpio); @@ -284,13 +308,3 @@ void PinManager::deallocateLedc(byte pos, byte channels) } } #endif - -#ifdef ESP8266 -uint32_t PinManager::pinAlloc = 0UL; -#else -uint64_t PinManager::pinAlloc = 0ULL; -uint16_t PinManager::ledcAlloc = 0; -#endif -uint8_t PinManager::i2cAllocCount = 0; -uint8_t PinManager::spiAllocCount = 0; -PinOwner PinManager::ownerTag[WLED_NUM_PINS] = { PinOwner::None }; diff --git a/wled00/pin_manager.h b/wled00/pin_manager.h index d87f409f..a488d24f 100644 --- a/wled00/pin_manager.h +++ b/wled00/pin_manager.h @@ -3,11 +3,12 @@ /* * Registers pins so there is no attempt for two interfaces to use the same pin */ -#include -#ifdef ARDUINO_ARCH_ESP32 -#include "driver/ledc.h" // needed for analog/LEDC channel counts + +#ifdef ESP8266 +#define WLED_NUM_PINS (GPIO_PIN_COUNT+1) // somehow they forgot GPIO 16 (0-16==17) +#else +#define WLED_NUM_PINS (GPIO_PIN_COUNT) #endif -#include "const.h" // for USERMOD_* values typedef struct PinManagerPinType { int8_t pin; @@ -29,15 +30,16 @@ enum struct PinOwner : uint8_t { Ethernet = 0x81, BusDigital = 0x82, BusOnOff = 0x83, - BusPwm = 0x84, // 'BusP' == PWM output using BusPwm - Button = 0x85, // 'Butn' == button from configuration - IR = 0x86, // 'IR' == IR receiver pin from configuration - Relay = 0x87, // 'Rly' == Relay pin from configuration - SPI_RAM = 0x88, // 'SpiR' == SPI RAM - DebugOut = 0x89, // 'Dbg' == debug output always IO1 - DMX = 0x8A, // 'DMX' == hard-coded to IO2 - HW_I2C = 0x8B, // 'I2C' == hardware I2C pins (4&5 on ESP8266, 21&22 on ESP32) - HW_SPI = 0x8C, // 'SPI' == hardware (V)SPI pins (13,14&15 on ESP8266, 5,18&23 on ESP32) + BusPwm = 0x84, // 'BusP' == PWM output using BusPwm + Button = 0x85, // 'Butn' == button from configuration + IR = 0x86, // 'IR' == IR receiver pin from configuration + Relay = 0x87, // 'Rly' == Relay pin from configuration + SPI_RAM = 0x88, // 'SpiR' == SPI RAM + DebugOut = 0x89, // 'Dbg' == debug output always IO1 + DMX = 0x8A, // 'DMX' == hard-coded to IO2 + HW_I2C = 0x8B, // 'I2C' == hardware I2C pins (4&5 on ESP8266, 21&22 on ESP32) + HW_SPI = 0x8C, // 'SPI' == hardware (V)SPI pins (13,14&15 on ESP8266, 5,18&23 on ESP32) + DMX_INPUT = 0x8D, // 'DMX_INPUT' == DMX input via serial HUB75 = 0x8E, // 'Hub75' == Hub75 driver // Use UserMod IDs from const.h here UM_Unspecified = USERMOD_ID_UNSPECIFIED, // 0x01 @@ -71,55 +73,40 @@ enum struct PinOwner : uint8_t { }; static_assert(0u == static_cast(PinOwner::None), "PinOwner::None must be zero, so default array initialization works as expected"); -class PinManager { - private: - #ifdef ESP8266 - #define WLED_NUM_PINS (GPIO_PIN_COUNT+1) // somehow they forgot GPIO 16 (0-16==17) - static uint32_t pinAlloc; // 1 bit per pin, we use first 17bits - #else - #define WLED_NUM_PINS (GPIO_PIN_COUNT) - static uint64_t pinAlloc; // 1 bit per pin, we use 50 bits on ESP32-S3 - static uint16_t ledcAlloc; // up to 16 LEDC channels (WLED_MAX_ANALOG_CHANNELS) - #endif - static uint8_t i2cAllocCount; // allow multiple allocation of I2C bus pins but keep track of allocations - static uint8_t spiAllocCount; // allow multiple allocation of SPI bus pins but keep track of allocations - static PinOwner ownerTag[WLED_NUM_PINS]; +namespace PinManager { + // De-allocates a single pin + bool deallocatePin(byte gpio, PinOwner tag); + // De-allocates multiple pins but only if all can be deallocated (PinOwner has to be specified) + bool deallocateMultiplePins(const uint8_t *pinArray, byte arrayElementCount, PinOwner tag); + bool deallocateMultiplePins(const managed_pin_type *pinArray, byte arrayElementCount, PinOwner tag); + // Allocates a single pin, with an owner tag. + // De-allocation requires the same owner tag (or override) + bool allocatePin(byte gpio, bool output, PinOwner tag); + // Allocates all the pins, or allocates none of the pins, with owner tag. + // Provided to simplify error condition handling in clients + // using more than one pin, such as I2C, SPI, rotary encoders, + // ethernet, etc.. + bool allocateMultiplePins(const managed_pin_type * mptArray, byte arrayElementCount, PinOwner tag ); + bool allocateMultiplePins(const int8_t * mptArray, byte arrayElementCount, PinOwner tag, boolean output); - public: - // De-allocates a single pin - static bool deallocatePin(byte gpio, PinOwner tag); - // De-allocates multiple pins but only if all can be deallocated (PinOwner has to be specified) - static bool deallocateMultiplePins(const uint8_t *pinArray, byte arrayElementCount, PinOwner tag); - static bool deallocateMultiplePins(const managed_pin_type *pinArray, byte arrayElementCount, PinOwner tag); - // Allocates a single pin, with an owner tag. - // De-allocation requires the same owner tag (or override) - static bool allocatePin(byte gpio, bool output, PinOwner tag); - // Allocates all the pins, or allocates none of the pins, with owner tag. - // Provided to simplify error condition handling in clients - // using more than one pin, such as I2C, SPI, rotary encoders, - // ethernet, etc.. - static bool allocateMultiplePins(const managed_pin_type * mptArray, byte arrayElementCount, PinOwner tag ); - static bool allocateMultiplePins(const int8_t * mptArray, byte arrayElementCount, PinOwner tag, boolean output); + [[deprecated("Replaced by three-parameter allocatePin(gpio, output, ownerTag), for improved debugging")]] + inline bool allocatePin(byte gpio, bool output = true) { return allocatePin(gpio, output, PinOwner::None); } + [[deprecated("Replaced by two-parameter deallocatePin(gpio, ownerTag), for improved debugging")]] + inline void deallocatePin(byte gpio) { deallocatePin(gpio, PinOwner::None); } + // will return true for reserved pins + bool isPinAllocated(byte gpio, PinOwner tag = PinOwner::None); + // will return false for reserved pins + bool isPinOk(byte gpio, bool output = true); - [[deprecated("Replaced by three-parameter allocatePin(gpio, output, ownerTag), for improved debugging")]] - static inline bool allocatePin(byte gpio, bool output = true) { return allocatePin(gpio, output, PinOwner::None); } - [[deprecated("Replaced by two-parameter deallocatePin(gpio, ownerTag), for improved debugging")]] - static inline void deallocatePin(byte gpio) { deallocatePin(gpio, PinOwner::None); } + bool isReadOnlyPin(byte gpio); - // will return true for reserved pins - static bool isPinAllocated(byte gpio, PinOwner tag = PinOwner::None); - // will return false for reserved pins - static bool isPinOk(byte gpio, bool output = true); - - static bool isReadOnlyPin(byte gpio); + PinOwner getPinOwner(byte gpio); - static PinOwner getPinOwner(byte gpio); - - #ifdef ARDUINO_ARCH_ESP32 - static byte allocateLedc(byte channels); - static void deallocateLedc(byte pos, byte channels); - #endif + #ifdef ARDUINO_ARCH_ESP32 + byte allocateLedc(byte channels); + void deallocateLedc(byte pos, byte channels); + #endif }; //extern PinManager pinManager; diff --git a/wled00/playlist.cpp b/wled00/playlist.cpp index 36235ab9..2e51503e 100644 --- a/wled00/playlist.cpp +++ b/wled00/playlist.cpp @@ -10,19 +10,19 @@ typedef struct PlaylistEntry { uint16_t tr; //Duration of the transition TO this entry (in tenths of seconds) } ple; -byte playlistRepeat = 1; //how many times to repeat the playlist (0 = infinitely) -byte playlistEndPreset = 0; //what preset to apply after playlist end (0 = stay on last preset) -byte playlistOptions = 0; //bit 0: shuffle playlist after each iteration. bits 1-7 TBD +static byte playlistRepeat = 1; //how many times to repeat the playlist (0 = infinitely) +static byte playlistEndPreset = 0; //what preset to apply after playlist end (0 = stay on last preset) +static byte playlistOptions = 0; //bit 0: shuffle playlist after each iteration. bits 1-7 TBD -PlaylistEntry *playlistEntries = nullptr; -byte playlistLen; //number of playlist entries -int8_t playlistIndex = -1; -uint16_t playlistEntryDur = 0; //duration of the current entry in tenths of seconds +static PlaylistEntry *playlistEntries = nullptr; +static byte playlistLen; //number of playlist entries +static int8_t playlistIndex = -1; +static uint16_t playlistEntryDur = 0; //duration of the current entry in tenths of seconds //values we need to keep about the parent playlist while inside sub-playlist -//int8_t parentPlaylistIndex = -1; -//byte parentPlaylistRepeat = 0; -//byte parentPlaylistPresetId = 0; //for re-loading +static int16_t parentPlaylistIndex = -1; +static byte parentPlaylistRepeat = 0; +static byte parentPlaylistPresetId = 0; //for re-loading void shufflePlaylist() { @@ -54,6 +54,12 @@ void unloadPlaylist() { int16_t loadPlaylist(JsonObject playlistObj, byte presetId) { + if (currentPlaylist > 0 && parentPlaylistPresetId > 0) return -1; // we are already in nested playlist, do nothing + if (currentPlaylist > 0) { + parentPlaylistIndex = playlistIndex; + parentPlaylistRepeat = playlistRepeat; + parentPlaylistPresetId = currentPlaylist; + } unloadPlaylist(); JsonArray presets = playlistObj["ps"]; @@ -61,7 +67,7 @@ int16_t loadPlaylist(JsonObject playlistObj, byte presetId) { if (playlistLen == 0) return -1; if (playlistLen > 100) playlistLen = 100; - playlistEntries = new PlaylistEntry[playlistLen]; + playlistEntries = new(std::nothrow) PlaylistEntry[playlistLen]; if (playlistEntries == nullptr) return -1; byte it = 0; @@ -79,7 +85,7 @@ int16_t loadPlaylist(JsonObject playlistObj, byte presetId) { } else { for (int dur : durations) { if (it >= playlistLen) break; - playlistEntries[it].dur = (dur > 1) ? dur : 100; + playlistEntries[it].dur = constrain(dur, 0, 65530); it++; } } @@ -117,6 +123,19 @@ int16_t loadPlaylist(JsonObject playlistObj, byte presetId) { shuffle = shuffle || playlistObj["r"]; if (shuffle) playlistOptions |= PL_OPTION_SHUFFLE; + if (parentPlaylistPresetId == 0 && parentPlaylistIndex > -1) { + // we are re-loading playlist when returning from nested playlist + playlistIndex = parentPlaylistIndex; + playlistRepeat = parentPlaylistRepeat; + parentPlaylistIndex = -1; + parentPlaylistRepeat = 0; + } else if (rep == 0) { + // endless playlist will never return to parent so erase parent information if it was called from it + parentPlaylistPresetId = 0; + parentPlaylistIndex = -1; + parentPlaylistRepeat = 0; + } + currentPlaylist = presetId; DEBUG_PRINTLN(F("Playlist loaded.")); return currentPlaylist; @@ -127,7 +146,7 @@ void handlePlaylist() { static unsigned long presetCycledTime = 0; if (currentPlaylist < 0 || playlistEntries == nullptr) return; -if (millis() - presetCycledTime > (100 * playlistEntryDur) || doAdvancePlaylist) { + if ((playlistEntryDur < UINT16_MAX && millis() - presetCycledTime > 100 * playlistEntryDur) || doAdvancePlaylist) { presetCycledTime = millis(); if (bri == 0 || nightlightActive) return; @@ -137,7 +156,10 @@ if (millis() - presetCycledTime > (100 * playlistEntryDur) || doAdvancePlaylist) if (!playlistIndex) { if (playlistRepeat == 1) { //stop if all repetitions are done unloadPlaylist(); - if (playlistEndPreset) applyPresetFromPlaylist(playlistEndPreset); + if (parentPlaylistPresetId > 0) { + applyPresetFromPlaylist(parentPlaylistPresetId); // reload previous playlist (unfortunately asynchronous) + parentPlaylistPresetId = 0; // reset previous playlist but do not reset Index or Repeat (they will be loaded & reset in loadPlaylist()) + } else if (playlistEndPreset) applyPresetFromPlaylist(playlistEndPreset); return; } if (playlistRepeat > 1) playlistRepeat--; // decrease repeat count on each index reset if not an endless playlist @@ -146,8 +168,8 @@ if (millis() - presetCycledTime > (100 * playlistEntryDur) || doAdvancePlaylist) } jsonTransitionOnce = true; - strip.setTransition(fadeTransition ? playlistEntries[playlistIndex].tr * 100 : 0); - playlistEntryDur = playlistEntries[playlistIndex].dur; + strip.setTransition(playlistEntries[playlistIndex].tr * 100); + playlistEntryDur = playlistEntries[playlistIndex].dur > 0 ? playlistEntries[playlistIndex].dur : UINT16_MAX; applyPresetFromPlaylist(playlistEntries[playlistIndex].preset); doAdvancePlaylist = false; } diff --git a/wled00/presets.cpp b/wled00/presets.cpp index 04474113..fed2c1ed 100644 --- a/wled00/presets.cpp +++ b/wled00/presets.cpp @@ -22,11 +22,16 @@ const char *getPresetsFileName(bool persistent) { return persistent ? presets_json : tmp_json; } +bool presetNeedsSaving() { + return presetToSave; +} + static void doSaveState() { bool persist = (presetToSave < 251); - unsigned long start = millis(); - while (strip.isUpdating() && millis()-start < (2*FRAMETIME_FIXED)+1) yield(); // wait 2 frames + unsigned long maxWait = millis() + strip.getFrameTime(); + while (strip.isUpdating() && millis() < maxWait) delay(1); // wait for strip to finish updating, accessing FS during sendout causes glitches + if (!requestJSONBufferLock(10)) return; initPresetsFile(); // just in case if someone deleted presets.json using /edit @@ -52,14 +57,10 @@ static void doSaveState() { */ #if defined(ARDUINO_ARCH_ESP32) if (!persist) { - if (tmpRAMbuffer!=nullptr) free(tmpRAMbuffer); + p_free(tmpRAMbuffer); size_t len = measureJson(*pDoc) + 1; - DEBUG_PRINTLN(len); // if possible use SPI RAM on ESP32 - if (psramSafe && psramFound()) - tmpRAMbuffer = (char*) ps_malloc(len); - else - tmpRAMbuffer = (char*) malloc(len); + tmpRAMbuffer = (char*)p_malloc(len); if (tmpRAMbuffer!=nullptr) { serializeJson(*pDoc, tmpRAMbuffer, len); } else { @@ -76,8 +77,8 @@ static void doSaveState() { // clean up saveLedmap = -1; presetToSave = 0; - delete[] saveName; - delete[] quickLoad; + p_free(saveName); + p_free(quickLoad); saveName = nullptr; quickLoad = nullptr; playlistSave = false; @@ -164,6 +165,11 @@ void handlePresets() DEBUG_PRINTF_P(PSTR("Applying preset: %u\n"), (unsigned)tmpPreset); + #if defined(ARDUINO_ARCH_ESP32S2) || defined(ARDUINO_ARCH_ESP32C3) + unsigned long maxWait = millis() + strip.getFrameTime(); + while (strip.isUpdating() && millis() < maxWait) delay(1); // wait for strip to finish updating, accessing FS during sendout causes glitches + #endif + #ifdef ARDUINO_ARCH_ESP32 if (tmpPreset==255 && tmpRAMbuffer!=nullptr) { deserializeJson(*pDoc,tmpRAMbuffer); @@ -197,7 +203,7 @@ void handlePresets() #if defined(ARDUINO_ARCH_ESP32) //Aircoookie recommended not to delete buffer if (tmpPreset==255 && tmpRAMbuffer!=nullptr) { - free(tmpRAMbuffer); + p_free(tmpRAMbuffer); tmpRAMbuffer = nullptr; } #endif @@ -211,8 +217,8 @@ void handlePresets() //called from handleSet(PS=) [network callback (sObj is empty), IR (irrational), deserializeState, UDP] and deserializeState() [network callback (filedoc!=nullptr)] void savePreset(byte index, const char* pname, JsonObject sObj) { - if (!saveName) saveName = new char[33]; - if (!quickLoad) quickLoad = new char[9]; + if (!saveName) saveName = static_cast(p_malloc(33)); + if (!quickLoad) quickLoad = static_cast(p_malloc(9)); if (!saveName || !quickLoad) return; if (index == 0 || (index > 250 && index < 255)) return; @@ -233,7 +239,7 @@ void savePreset(byte index, const char* pname, JsonObject sObj) if (!sObj[FPSTR(bootPS)].isNull()) { bootPreset = sObj[FPSTR(bootPS)] | bootPreset; sObj.remove(FPSTR(bootPS)); - doSerializeConfig = true; + configNeedsWrite = true; } if (sObj.size()==0 || sObj["o"].isNull()) { // no "o" means not a playlist or custom API call, saving of state is async (not immediately) @@ -258,13 +264,13 @@ void savePreset(byte index, const char* pname, JsonObject sObj) presetsModifiedTime = toki.second(); //unix time updateFSInfo(); } - delete[] saveName; - delete[] quickLoad; + p_free(saveName); + p_free(quickLoad); saveName = nullptr; quickLoad = nullptr; } else { // store playlist - // WARNING: playlist will be loaded in json.cpp after this call and will have repeat counter increased by 1 + // WARNING: playlist will be loaded in json.cpp after this call and will have repeat counter increased by 1 it will also be randomised if selected includeBri = true; // !sObj["on"].isNull(); playlistSave = true; } diff --git a/wled00/remote.cpp b/wled00/remote.cpp index 9bc5430c..14c3c0d0 100644 --- a/wled00/remote.cpp +++ b/wled00/remote.cpp @@ -1,6 +1,8 @@ #include "wled.h" #ifndef WLED_DISABLE_ESPNOW +#define ESPNOW_BUSWAIT_TIMEOUT 24 // one frame timeout to wait for bus to finish updating + #define NIGHT_MODE_DEACTIVATED -1 #define NIGHT_MODE_BRIGHTNESS 5 @@ -38,6 +40,7 @@ typedef struct WizMoteMessageStructure { static uint32_t last_seq = UINT32_MAX; static int brightnessBeforeNightMode = NIGHT_MODE_DEACTIVATED; +static int16_t ESPNowButton = -1; // set in callback if new button value is received // Pulled from the IR Remote logic but reduced to 10 steps with a constant of 3 static const byte brightnessSteps[] = { @@ -121,6 +124,9 @@ static bool remoteJson(int button) sprintf_P(objKey, PSTR("\"%d\":"), button); + unsigned long start = millis(); + while (strip.isUpdating() && millis()-start < ESPNOW_BUSWAIT_TIMEOUT) yield(); // wait for strip to finish updating, accessing FS during sendout causes glitches + // attempt to read command from remote.json readObjectFromFile(PSTR("/remote.json"), objKey, pDoc); JsonObject fdo = pDoc->as(); @@ -146,7 +152,7 @@ static bool remoteJson(int button) parsed = true; } else if (cmdStr.startsWith(F("!presetF"))) { //!presetFallback uint8_t p1 = fdo["PL"] | 1; - uint8_t p2 = fdo["FX"] | random8(strip.getModeCount() -1); + uint8_t p2 = fdo["FX"] | hw_random8(strip.getModeCount() -1); uint8_t p3 = fdo["FP"] | 0; presetWithFallback(p1, p2, p3); parsed = true; @@ -175,16 +181,10 @@ static bool remoteJson(int button) return parsed; } -// Callback function that will be executed when data is received -void handleRemote(uint8_t *incomingData, size_t len) { +// Callback function that will be executed when data is received from a linked remote +void handleWiZdata(uint8_t *incomingData, size_t len) { message_structure_t *incoming = reinterpret_cast(incomingData); - if (strcmp(last_signal_src, linked_remote) != 0) { - DEBUG_PRINT(F("ESP Now Message Received from Unlinked Sender: ")); - DEBUG_PRINTLN(last_signal_src); - return; - } - if (len != sizeof(message_structure_t)) { DEBUG_PRINTF_P(PSTR("Unknown incoming ESP Now message received of length %u\n"), len); return; @@ -202,8 +202,15 @@ void handleRemote(uint8_t *incomingData, size_t len) { DEBUG_PRINT(F("] button: ")); DEBUG_PRINTLN(incoming->button); - if (!remoteJson(incoming->button)) - switch (incoming->button) { + ESPNowButton = incoming->button; // save state, do not process in callback (can cause glitches) + last_seq = cur_seq; +} + +// process ESPNow button data (acesses FS, should not be called while update to avoid glitches) +void handleRemote() { + if(ESPNowButton >= 0) { + if (!remoteJson(ESPNowButton)) + switch (ESPNowButton) { case WIZMOTE_BUTTON_ON : setOn(); break; case WIZMOTE_BUTTON_OFF : setOff(); break; case WIZMOTE_BUTTON_ONE : presetWithFallback(1, FX_MODE_STATIC, 0); break; @@ -219,9 +226,10 @@ void handleRemote(uint8_t *incomingData, size_t len) { case WIZ_SMART_BUTTON_BRIGHT_DOWN : brightnessDown(); break; default: break; } - last_seq = cur_seq; + } + ESPNowButton = -1; } #else -void handleRemote(uint8_t *incomingData, size_t len) {} +void handleRemote() {} #endif diff --git a/wled00/set.cpp b/wled00/set.cpp index 72d1b396..c0ead554 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -23,11 +23,12 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) for (size_t n = 0; n < WLED_MAX_WIFI_COUNT; n++) { char cs[4] = "CS"; cs[2] = 48+n; cs[3] = 0; //client SSID char pw[4] = "PW"; pw[2] = 48+n; pw[3] = 0; //client password + char bs[4] = "BS"; bs[2] = 48+n; bs[3] = 0; //BSSID char ip[5] = "IP"; ip[2] = 48+n; ip[4] = 0; //IP address char gw[5] = "GW"; gw[2] = 48+n; gw[4] = 0; //GW address char sn[5] = "SN"; sn[2] = 48+n; sn[4] = 0; //subnet mask if (request->hasArg(cs)) { - if (n >= multiWiFi.size()) multiWiFi.push_back(WiFiConfig()); // expand vector by one + if (n >= multiWiFi.size()) multiWiFi.emplace_back(); // expand vector by one char oldSSID[33]; strcpy(oldSSID, multiWiFi[n].clientSSID); char oldPass[65]; strcpy(oldPass, multiWiFi[n].clientPass); @@ -39,6 +40,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) strlcpy(multiWiFi[n].clientPass, request->arg(pw).c_str(), 65); forceReconnect = true; } + fillStr2MAC(multiWiFi[n].bssid, request->arg(bs).c_str()); for (size_t i = 0; i < 4; i++) { ip[3] = 48+i; gw[3] = 48+i; @@ -89,13 +91,26 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) bool oldESPNow = enableESPNow; enableESPNow = request->hasArg(F("RE")); if (oldESPNow != enableESPNow) forceReconnect = true; - strlcpy(linked_remote, request->arg(F("RMAC")).c_str(), 13); - strlwr(linked_remote); //Normalize MAC format to lowercase + linked_remotes.clear(); // clear old remotes + for (size_t n = 0; n < 10; n++) { + char rm[4]; + snprintf(rm, sizeof(rm), "RM%d", n); // "RM0" to "RM9" + if (request->hasArg(rm)) { + const String& arg = request->arg(rm); + if (arg.isEmpty()) continue; + std::array mac{}; + strlcpy(mac.data(), request->arg(rm).c_str(), 13); + strlwr(mac.data()); + if (mac[0] != '\0') { + linked_remotes.emplace_back(mac); + } + } + } #endif - #ifdef WLED_USE_ETHERNET + #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) ethernetType = request->arg(F("ETH")).toInt(); - WLED::instance().initEthernet(); + initEthernet(); #endif } @@ -127,6 +142,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) unsigned length, start, maMax; uint8_t pins[OUTPUT_MAX_PINS] = {255, 255, 255, 255, 255}; + // this will set global ABL max current used when per-port ABL is not used unsigned ablMilliampsMax = request->arg(F("MA")).toInt(); BusManager::setMilliampsMax(ablMilliampsMax); @@ -134,15 +150,17 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) strip.correctWB = request->hasArg(F("CCT")); strip.cctFromRgb = request->hasArg(F("CR")); cctICused = request->hasArg(F("IC")); - strip.cctBlending = request->arg(F("CB")).toInt(); - Bus::setCCTBlend(strip.cctBlending); + uint8_t cctBlending = request->arg(F("CB")).toInt(); + Bus::setCCTBlend(cctBlending); Bus::setGlobalAWMode(request->arg(F("AW")).toInt()); strip.setTargetFps(request->arg(F("FR")).toInt()); - useGlobalLedBuffer = request->hasArg(F("LD")); + #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) + useParallelI2S = request->hasArg(F("PR")); + #endif bool busesChanged = false; - for (int s = 0; s < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; s++) { - int offset = s < 10 ? 48 : 55; + for (int s = 0; s < 36; s++) { // theoretical limit is 36 : "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + int offset = s < 10 ? '0' : 'A' - 10; char lp[4] = "L0"; lp[2] = offset+s; lp[3] = 0; //ascii 0-9 //strip data pin char lc[4] = "LC"; lc[2] = offset+s; lc[3] = 0; //strip length char co[4] = "CO"; co[2] = offset+s; co[3] = 0; //strip color order @@ -157,11 +175,11 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) char la[4] = "LA"; la[2] = offset+s; la[3] = 0; //LED mA char ma[4] = "MA"; ma[2] = offset+s; ma[3] = 0; //max mA if (!request->hasArg(lp)) { - DEBUG_PRINTF_P(PSTR("No data for %d\n"), s); + DEBUG_PRINTF_P(PSTR("# of buses: %d\n"), s+1); break; } for (int i = 0; i < 5; i++) { - lp[1] = offset+i; + lp[1] = '0'+i; if (!request->hasArg(lp)) break; pins[i] = (request->arg(lp).length() > 0) ? request->arg(lp).toInt() : 255; } @@ -203,13 +221,12 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) maMax = 0; } else { maPerLed = request->arg(la).toInt(); - maMax = request->arg(ma).toInt(); // if ABL is disabled this will be 0 + maMax = request->arg(ma).toInt() * request->hasArg(F("PPL")); // if PP-ABL is disabled maMax (per bus) must be 0 } type |= request->hasArg(rf) << 7; // off refresh override // actual finalization is done in WLED::loop() (removing old busses and adding new) // this may happen even before this loop is finished so we do "doInitBusses" after the loop - if (busConfigs[s] != nullptr) delete busConfigs[s]; - busConfigs[s] = new BusConfig(type, pins, start, length, colorOrder | (channelSwap<<4), request->hasArg(cv), skip, awmode, freq, useGlobalLedBuffer, maPerLed, maMax); + busConfigs.emplace_back(type, pins, start, length, colorOrder | (channelSwap<<4), request->hasArg(cv), skip, awmode, freq, maPerLed, maMax); busesChanged = true; } //doInitBusses = busesChanged; // we will do that below to ensure all input data is processed @@ -217,7 +234,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) // we will not bother with pre-allocating ColorOrderMappings vector BusManager::getColorOrderMap().reset(); for (int s = 0; s < WLED_MAX_COLOR_ORDER_MAPPINGS; s++) { - int offset = s < 10 ? 48 : 55; + int offset = s < 10 ? '0' : 'A' - 10; char xs[4] = "XS"; xs[2] = offset+s; xs[3] = 0; //start LED char xc[4] = "XC"; xc[2] = offset+s; xc[3] = 0; //strip length char xo[4] = "XO"; xo[2] = offset+s; xo[3] = 0; //color order @@ -256,7 +273,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) disablePullUp = (bool)request->hasArg(F("IP")); touchThreshold = request->arg(F("TT")).toInt(); for (int i = 0; i < WLED_MAX_BUTTONS; i++) { - int offset = i < 10 ? 48 : 55; + int offset = i < 10 ? '0' : 'A' - 10; char bt[4] = "BT"; bt[2] = offset+i; bt[3] = 0; // button pin (use A,B,C,... if WLED_MAX_BUTTONS>10) char be[4] = "BE"; be[2] = offset+i; be[3] = 0; // button type (use A,B,C,... if WLED_MAX_BUTTONS>10) int hw_btn_pin = request->arg(bt).toInt(); @@ -319,22 +336,19 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) gammaCorrectBri = request->hasArg(F("GB")); gammaCorrectCol = request->hasArg(F("GC")); gammaCorrectVal = request->arg(F("GV")).toFloat(); - if (gammaCorrectVal > 1.0f && gammaCorrectVal <= 3) - NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); - else { + if (gammaCorrectVal <= 1.0f || gammaCorrectVal > 3) { gammaCorrectVal = 1.0f; // no gamma correction gammaCorrectBri = false; gammaCorrectCol = false; } + NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); // fill look-up tables - fadeTransition = request->hasArg(F("TF")); - modeBlending = request->hasArg(F("EB")); t = request->arg(F("TD")).toInt(); if (t >= 0) transitionDelayDefault = t; - strip.paletteFade = request->hasArg(F("PF")); t = request->arg(F("TP")).toInt(); randomPaletteChangeTime = MIN(255,MAX(1,t)); useHarmonicRandomPalette = request->hasArg(F("TH")); + useRainbowWheel = request->hasArg(F("RW")); nightlightTargetBri = request->arg(F("TB")).toInt(); t = request->arg(F("TL")).toInt(); @@ -343,7 +357,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) nightlightMode = request->arg(F("TW")).toInt(); t = request->arg(F("PB")).toInt(); - if (t >= 0 && t < 4) strip.paletteBlend = t; + if (t >= 0 && t < 4) paletteBlend = t; t = request->arg(F("BF")).toInt(); if (t > 0) briMultiplier = t; @@ -359,7 +373,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) DEBUG_PRINTLN(F("Enumerating ledmaps")); enumerateLedmaps(); DEBUG_PRINTLN(F("Loading custom palettes")); - strip.loadCustomPalettes(); // (re)load all custom palettes + loadCustomPalettes(); // (re)load all custom palettes } //SYNC @@ -421,6 +435,14 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) t = request->arg(F("WO")).toInt(); if (t >= -255 && t <= 255) arlsOffset = t; +#ifdef WLED_ENABLE_DMX_INPUT + dmxInputTransmitPin = request->arg(F("IDMT")).toInt(); + dmxInputReceivePin = request->arg(F("IDMR")).toInt(); + dmxInputEnablePin = request->arg(F("IDME")).toInt(); + dmxInputPort = request->arg(F("IDMP")).toInt(); + if(dmxInputPort <= 0 || dmxInputPort > 2) dmxInputPort = 2; +#endif + #ifndef WLED_DISABLE_ALEXA alexaEnabled = request->hasArg(F("AL")); strlcpy(alexaInvocationName, request->arg(F("AI")).c_str(), 33); @@ -586,7 +608,9 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) { otaLock = request->hasArg(F("NO")); wifiLock = request->hasArg(F("OW")); + #ifndef WLED_DISABLE_OTA aOtaEnabled = request->hasArg(F("AO")); + #endif //createEditHandler(correctPIN && !otaLock); } } @@ -624,7 +648,10 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) //USERMODS if (subPage == SUBPAGE_UM) { - if (!requestJSONBufferLock(5)) return; + if (!requestJSONBufferLock(5)) { + request->deferResponse(); + return; + } // global I2C & SPI pins int8_t hw_sda_pin = !request->arg(F("SDA")).length() ? -1 : (int)request->arg(F("SDA")).toInt(); @@ -761,14 +788,14 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) if (subPage == SUBPAGE_2D) { strip.isMatrix = request->arg(F("SOMP")).toInt(); - strip.panel.clear(); // release memory if allocated + strip.panel.clear(); if (strip.isMatrix) { - strip.panels = MAX(1,MIN(WLED_MAX_PANELS,request->arg(F("MPC")).toInt())); - strip.panel.reserve(strip.panels); // pre-allocate memory - for (unsigned i=0; iarg(F("MPC")).toInt(), 1, WLED_MAX_PANELS); + strip.panel.reserve(panels); // pre-allocate memory + for (unsigned i=0; iarg(pO).toInt(); strip.panel.push_back(p); } - strip.setUpMatrix(); // will check limits - strip.makeAutoSegments(true); - strip.deserializeMap(); - } else { - Segment::maxWidth = strip.getLengthTotal(); - Segment::maxHeight = 1; } + strip.panel.shrink_to_fit(); // release unused memory + strip.deserializeMap(); // (re)load default ledmap (will also setUpMatrix() if ledmap does not exist) + strip.makeAutoSegments(true); // force re-creation of segments } #endif lastEditTime = millis(); // do not save if factory reset or LED settings (which are saved after LED re-init) - doSerializeConfig = subPage != SUBPAGE_LEDS && !(subPage == SUBPAGE_SEC && doReboot); - if (subPage == SUBPAGE_UM) doReboot = request->hasArg(F("RBT")); // prevent race condition on dual core system (set reboot here, after doSerializeConfig has been set) + configNeedsWrite = subPage != SUBPAGE_LEDS && !(subPage == SUBPAGE_SEC && doReboot); + if (subPage == SUBPAGE_UM) doReboot = request->hasArg(F("RBT")); // prevent race condition on dual core system (set reboot here, after configNeedsWrite has been set) #ifndef WLED_DISABLE_ALEXA if (subPage == SUBPAGE_SYNC) alexaInit(); #endif @@ -814,7 +838,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) //segment select (sets main segment) pos = req.indexOf(F("SM=")); if (pos > 0 && !realtimeMode) { - strip.setMainSegmentId(getNumVal(&req, pos)); + strip.setMainSegmentId(getNumVal(req, pos)); } byte selectedSeg = strip.getFirstSelectedSegId(); @@ -823,7 +847,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) pos = req.indexOf(F("SS=")); if (pos > 0) { - unsigned t = getNumVal(&req, pos); + unsigned t = getNumVal(req, pos); if (t < strip.getSegmentsNum()) { selectedSeg = t; singleSegment = true; @@ -833,14 +857,15 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) Segment& selseg = strip.getSegment(selectedSeg); pos = req.indexOf(F("SV=")); //segment selected if (pos > 0) { - unsigned t = getNumVal(&req, pos); + unsigned t = getNumVal(req, pos); if (t == 2) for (unsigned i = 0; i < strip.getSegmentsNum(); i++) strip.getSegment(i).selected = false; // unselect other segments selseg.selected = t; } // temporary values, write directly to segments, globals are updated by setValuesFromFirstSelectedSeg() - uint32_t col0 = selseg.colors[0]; - uint32_t col1 = selseg.colors[1]; + uint32_t col0 = selseg.colors[0]; + uint32_t col1 = selseg.colors[1]; + uint32_t col2 = selseg.colors[2]; byte colIn[4] = {R(col0), G(col0), B(col0), W(col0)}; byte colInSec[4] = {R(col1), G(col1), B(col1), W(col1)}; byte effectIn = selseg.mode; @@ -861,21 +886,23 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) uint16_t spcI = selseg.spacing; pos = req.indexOf(F("&S=")); //segment start if (pos > 0) { - startI = std::abs(getNumVal(&req, pos)); + startI = std::abs(getNumVal(req, pos)); } pos = req.indexOf(F("S2=")); //segment stop if (pos > 0) { - stopI = std::abs(getNumVal(&req, pos)); + stopI = std::abs(getNumVal(req, pos)); } pos = req.indexOf(F("GP=")); //segment grouping if (pos > 0) { - grpI = std::max(1,getNumVal(&req, pos)); + grpI = std::max(1,getNumVal(req, pos)); } pos = req.indexOf(F("SP=")); //segment spacing if (pos > 0) { - spcI = std::max(0,getNumVal(&req, pos)); + spcI = std::max(0,getNumVal(req, pos)); } - strip.setSegment(selectedSeg, startI, stopI, grpI, spcI, UINT16_MAX, startY, stopY); + strip.suspend(); // must suspend strip operations before changing geometry + selseg.setGeometry(startI, stopI, grpI, spcI, UINT16_MAX, startY, stopY, selseg.map1D2D); + strip.resume(); pos = req.indexOf(F("RV=")); //Segment reverse if (pos > 0) selseg.reverse = req.charAt(pos+3) != '0'; @@ -885,7 +912,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) pos = req.indexOf(F("SB=")); //Segment brightness/opacity if (pos > 0) { - byte segbri = getNumVal(&req, pos); + byte segbri = getNumVal(req, pos); selseg.setOption(SEG_OPTION_ON, segbri); // use transition if (segbri) { selseg.setOpacity(segbri); @@ -894,7 +921,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) pos = req.indexOf(F("SW=")); //segment power if (pos > 0) { - switch (getNumVal(&req, pos)) { + switch (getNumVal(req, pos)) { case 0: selseg.setOption(SEG_OPTION_ON, false); break; // use transition case 1: selseg.setOption(SEG_OPTION_ON, true); break; // use transition default: selseg.setOption(SEG_OPTION_ON, !selseg.on); break; // use transition @@ -902,16 +929,16 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) } pos = req.indexOf(F("PS=")); //saves current in preset - if (pos > 0) savePreset(getNumVal(&req, pos)); + if (pos > 0) savePreset(getNumVal(req, pos)); pos = req.indexOf(F("P1=")); //sets first preset for cycle - if (pos > 0) presetCycMin = getNumVal(&req, pos); + if (pos > 0) presetCycMin = getNumVal(req, pos); pos = req.indexOf(F("P2=")); //sets last preset for cycle - if (pos > 0) presetCycMax = getNumVal(&req, pos); + if (pos > 0) presetCycMax = getNumVal(req, pos); //apply preset - if (updateVal(req.c_str(), "PL=", &presetCycCurr, presetCycMin, presetCycMax)) { + if (updateVal(req.c_str(), "PL=", presetCycCurr, presetCycMin, presetCycMax)) { applyPreset(presetCycCurr); } @@ -919,25 +946,25 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) if (pos > 0) doAdvancePlaylist = true; //set brightness - updateVal(req.c_str(), "&A=", &bri); + updateVal(req.c_str(), "&A=", bri); - bool col0Changed = false, col1Changed = false; + bool col0Changed = false, col1Changed = false, col2Changed = false; //set colors - col0Changed |= updateVal(req.c_str(), "&R=", &colIn[0]); - col0Changed |= updateVal(req.c_str(), "&G=", &colIn[1]); - col0Changed |= updateVal(req.c_str(), "&B=", &colIn[2]); - col0Changed |= updateVal(req.c_str(), "&W=", &colIn[3]); + col0Changed |= updateVal(req.c_str(), "&R=", colIn[0]); + col0Changed |= updateVal(req.c_str(), "&G=", colIn[1]); + col0Changed |= updateVal(req.c_str(), "&B=", colIn[2]); + col0Changed |= updateVal(req.c_str(), "&W=", colIn[3]); - col1Changed |= updateVal(req.c_str(), "R2=", &colInSec[0]); - col1Changed |= updateVal(req.c_str(), "G2=", &colInSec[1]); - col1Changed |= updateVal(req.c_str(), "B2=", &colInSec[2]); - col1Changed |= updateVal(req.c_str(), "W2=", &colInSec[3]); + col1Changed |= updateVal(req.c_str(), "R2=", colInSec[0]); + col1Changed |= updateVal(req.c_str(), "G2=", colInSec[1]); + col1Changed |= updateVal(req.c_str(), "B2=", colInSec[2]); + col1Changed |= updateVal(req.c_str(), "W2=", colInSec[3]); #ifdef WLED_ENABLE_LOXONE //lox parser pos = req.indexOf(F("LX=")); // Lox primary color if (pos > 0) { - int lxValue = getNumVal(&req, pos); + int lxValue = getNumVal(req, pos); if (parseLx(lxValue, colIn)) { bri = 255; nightlightActive = false; //always disable nightlight when toggling @@ -946,7 +973,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) } pos = req.indexOf(F("LY=")); // Lox secondary color if (pos > 0) { - int lxValue = getNumVal(&req, pos); + int lxValue = getNumVal(req, pos); if(parseLx(lxValue, colInSec)) { bri = 255; nightlightActive = false; //always disable nightlight when toggling @@ -958,11 +985,11 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) //set hue pos = req.indexOf(F("HU=")); if (pos > 0) { - uint16_t temphue = getNumVal(&req, pos); + uint16_t temphue = getNumVal(req, pos); byte tempsat = 255; pos = req.indexOf(F("SA=")); if (pos > 0) { - tempsat = getNumVal(&req, pos); + tempsat = getNumVal(req, pos); } byte sec = req.indexOf(F("H2")); colorHStoRGB(temphue, tempsat, (sec>0) ? colInSec : colIn); @@ -973,12 +1000,11 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) pos = req.indexOf(F("&K=")); if (pos > 0) { byte sec = req.indexOf(F("K2")); - colorKtoRGB(getNumVal(&req, pos), (sec>0) ? colInSec : colIn); + colorKtoRGB(getNumVal(req, pos), (sec>0) ? colInSec : colIn); col0Changed |= (!sec); col1Changed |= sec; } //set color from HEX or 32bit DEC - byte tmpCol[4]; pos = req.indexOf(F("CL=")); if (pos > 0) { colorFromDecOrHexString(colIn, (char*)req.substring(pos + 3).c_str()); @@ -991,61 +1017,55 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) } pos = req.indexOf(F("C3=")); if (pos > 0) { + byte tmpCol[4]; colorFromDecOrHexString(tmpCol, (char*)req.substring(pos + 3).c_str()); - uint32_t col2 = RGBW32(tmpCol[0], tmpCol[1], tmpCol[2], tmpCol[3]); + col2 = RGBW32(tmpCol[0], tmpCol[1], tmpCol[2], tmpCol[3]); selseg.setColor(2, col2); // defined above (SS= or main) - if (!singleSegment) strip.setColor(2, col2); // will set color to all active & selected segments + col2Changed = true; } //set to random hue SR=0->1st SR=1->2nd pos = req.indexOf(F("SR")); if (pos > 0) { - byte sec = getNumVal(&req, pos); + byte sec = getNumVal(req, pos); setRandomColor(sec? colInSec : colIn); col0Changed |= (!sec); col1Changed |= sec; } + // apply colors to selected segment, and all selected segments if applicable + if (col0Changed) { + col0 = RGBW32(colIn[0], colIn[1], colIn[2], colIn[3]); + selseg.setColor(0, col0); + } + + if (col1Changed) { + col1 = RGBW32(colInSec[0], colInSec[1], colInSec[2], colInSec[3]); + selseg.setColor(1, col1); + } + //swap 2nd & 1st pos = req.indexOf(F("SC")); if (pos > 0) { - byte temp; - for (unsigned i=0; i<4; i++) { - temp = colIn[i]; - colIn[i] = colInSec[i]; - colInSec[i] = temp; - } + std::swap(col0,col1); col0Changed = col1Changed = true; } - // apply colors to selected segment, and all selected segments if applicable - if (col0Changed) { - uint32_t colIn0 = RGBW32(colIn[0], colIn[1], colIn[2], colIn[3]); - selseg.setColor(0, colIn0); - if (!singleSegment) strip.setColor(0, colIn0); // will set color to all active & selected segments - } - - if (col1Changed) { - uint32_t colIn1 = RGBW32(colInSec[0], colInSec[1], colInSec[2], colInSec[3]); - selseg.setColor(1, colIn1); - if (!singleSegment) strip.setColor(1, colIn1); // will set color to all active & selected segments - } - bool fxModeChanged = false, speedChanged = false, intensityChanged = false, paletteChanged = false; bool custom1Changed = false, custom2Changed = false, custom3Changed = false, check1Changed = false, check2Changed = false, check3Changed = false; // set effect parameters - if (updateVal(req.c_str(), "FX=", &effectIn, 0, strip.getModeCount()-1)) { + if (updateVal(req.c_str(), "FX=", effectIn, 0, strip.getModeCount()-1)) { if (request != nullptr) unloadPlaylist(); // unload playlist if changing FX using web request fxModeChanged = true; } - speedChanged = updateVal(req.c_str(), "SX=", &speedIn); - intensityChanged = updateVal(req.c_str(), "IX=", &intensityIn); - paletteChanged = updateVal(req.c_str(), "FP=", &paletteIn, 0, strip.getPaletteCount()-1); - custom1Changed = updateVal(req.c_str(), "X1=", &custom1In); - custom2Changed = updateVal(req.c_str(), "X2=", &custom2In); - custom3Changed = updateVal(req.c_str(), "X3=", &custom3In); - check1Changed = updateVal(req.c_str(), "M1=", &check1In); - check2Changed = updateVal(req.c_str(), "M2=", &check2In); - check3Changed = updateVal(req.c_str(), "M3=", &check3In); + speedChanged = updateVal(req.c_str(), "SX=", speedIn); + intensityChanged = updateVal(req.c_str(), "IX=", intensityIn); + paletteChanged = updateVal(req.c_str(), "FP=", paletteIn, 0, getPaletteCount()-1); + custom1Changed = updateVal(req.c_str(), "X1=", custom1In); + custom2Changed = updateVal(req.c_str(), "X2=", custom2In); + custom3Changed = updateVal(req.c_str(), "X3=", custom3In); + check1Changed = updateVal(req.c_str(), "M1=", check1In); + check2Changed = updateVal(req.c_str(), "M2=", check2In); + check3Changed = updateVal(req.c_str(), "M3=", check3In); stateChanged |= (fxModeChanged || speedChanged || intensityChanged || paletteChanged || custom1Changed || custom2Changed || custom3Changed || check1Changed || check2Changed || check3Changed); @@ -1057,6 +1077,9 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) if (speedChanged) seg.speed = speedIn; if (intensityChanged) seg.intensity = intensityIn; if (paletteChanged) seg.setPalette(paletteIn); + if (col0Changed) seg.setColor(0, col0); + if (col1Changed) seg.setColor(1, col1); + if (col2Changed) seg.setColor(2, col2); if (custom1Changed) seg.custom1 = custom1In; if (custom2Changed) seg.custom2 = custom2In; if (custom3Changed) seg.custom3 = custom3In; @@ -1068,13 +1091,13 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) //set advanced overlay pos = req.indexOf(F("OL=")); if (pos > 0) { - overlayCurrent = getNumVal(&req, pos); + overlayCurrent = getNumVal(req, pos); } //apply macro (deprecated, added for compatibility with pre-0.11 automations) pos = req.indexOf(F("&M=")); if (pos > 0) { - applyPreset(getNumVal(&req, pos) + 16); + applyPreset(getNumVal(req, pos) + 16); } //toggle send UDP direct notifications @@ -1093,7 +1116,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) pos = req.indexOf(F("&T=")); if (pos > 0) { nightlightActive = false; //always disable nightlight when toggling - switch (getNumVal(&req, pos)) + switch (getNumVal(req, pos)) { case 0: if (bri != 0){briLast = bri; bri = 0;} break; //off, only if it was previously on case 1: if (bri == 0) bri = briLast; break; //on, only if it was previously off @@ -1112,7 +1135,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) nightlightActive = false; } else { nightlightActive = true; - if (!aNlDef) nightlightDelayMins = getNumVal(&req, pos); + if (!aNlDef) nightlightDelayMins = getNumVal(req, pos); else nightlightDelayMins = nightlightDelayMinsDefault; nightlightStartTime = millis(); } @@ -1126,7 +1149,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) //set nightlight target brightness pos = req.indexOf(F("NT=")); if (pos > 0) { - nightlightTargetBri = getNumVal(&req, pos); + nightlightTargetBri = getNumVal(req, pos); nightlightActiveOld = false; //re-init } @@ -1134,35 +1157,36 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) pos = req.indexOf(F("NF=")); if (pos > 0) { - nightlightMode = getNumVal(&req, pos); + nightlightMode = getNumVal(req, pos); nightlightActiveOld = false; //re-init } if (nightlightMode > NL_MODE_SUN) nightlightMode = NL_MODE_SUN; pos = req.indexOf(F("TT=")); - if (pos > 0) transitionDelay = getNumVal(&req, pos); - if (fadeTransition) strip.setTransition(transitionDelay); + if (pos > 0) transitionDelay = getNumVal(req, pos); + strip.setTransition(transitionDelay); //set time (unix timestamp) pos = req.indexOf(F("ST=")); if (pos > 0) { - setTimeFromAPI(getNumVal(&req, pos)); + setTimeFromAPI(getNumVal(req, pos)); } //set countdown goal (unix timestamp) pos = req.indexOf(F("CT=")); if (pos > 0) { - countdownTime = getNumVal(&req, pos); + countdownTime = getNumVal(req, pos); if (countdownTime - toki.second() > 0) countdownOverTriggered = false; } pos = req.indexOf(F("LO=")); if (pos > 0) { - realtimeOverride = getNumVal(&req, pos); + realtimeOverride = getNumVal(req, pos); if (realtimeOverride > 2) realtimeOverride = REALTIME_OVERRIDE_ALWAYS; if (realtimeMode && useMainSegmentOnly) { strip.getMainSegment().freeze = !realtimeOverride; + realtimeOverride = REALTIME_OVERRIDE_NONE; // ignore request for override if using main segment only } } @@ -1175,16 +1199,16 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) pos = req.indexOf(F("U0=")); //user var 0 if (pos > 0) { - userVar0 = getNumVal(&req, pos); + userVar0 = getNumVal(req, pos); } pos = req.indexOf(F("U1=")); //user var 1 if (pos > 0) { - userVar1 = getNumVal(&req, pos); + userVar1 = getNumVal(req, pos); } // you can add more if you need - // global col[], effectCurrent, ... are updated in stateChanged() + // global colPri[], effectCurrent, ... are updated in stateChanged() if (!apply) return true; // when called by JSON API, do not call colorUpdated() here pos = req.indexOf(F("&NN")); //do not send UDP notifications this time diff --git a/wled00/src/dependencies/time/DS1307RTC.h b/wled00/src/dependencies/time/DS1307RTC.h index 551ae996..bc272701 100644 --- a/wled00/src/dependencies/time/DS1307RTC.h +++ b/wled00/src/dependencies/time/DS1307RTC.h @@ -7,6 +7,7 @@ #define DS1307RTC_h #include "TimeLib.h" +#include "Wire.h" // library interface description class DS1307RTC diff --git a/wled00/udp.cpp b/wled00/udp.cpp index 60774d70..f2fee535 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -6,7 +6,7 @@ #define UDP_SEG_SIZE 36 #define SEG_OFFSET (41) -#define WLEDPACKETSIZE (41+(MAX_NUM_SEGMENTS*UDP_SEG_SIZE)+0) +#define WLEDPACKETSIZE (41+(WS2812FX::getMaxSegments()*UDP_SEG_SIZE)+0) #define UDP_IN_MAXSIZE 1472 #define PRESUMED_NETWORK_DELAY 3 //how many ms could it take on avg to reach the receiver? This will be added to transmitted times @@ -55,7 +55,7 @@ void notify(byte callMode, bool followUp) //0: old 1: supports white 2: supports secondary color //3: supports FX intensity, 24 byte packet 4: supports transitionDelay 5: sup palette //6: supports timebase syncing, 29 byte packet 7: supports tertiary color 8: supports sys time sync, 36 byte packet - //9: supports sync groups, 37 byte packet 10: supports CCT, 39 byte packet 11: per segment options, variable packet length (40+MAX_NUM_SEGMENTS*3) + //9: supports sync groups, 37 byte packet 10: supports CCT, 39 byte packet 11: per segment options, variable packet length (40+WS2812FX::getMaxSegments()*3) //12: enhanced effect sliders, 2D & mapping options udpOut[11] = 12; col = mainseg.colors[1]; @@ -104,7 +104,7 @@ void notify(byte callMode, bool followUp) udpOut[40] = UDP_SEG_SIZE; //size of each loop iteration (one segment) size_t s = 0, nsegs = strip.getSegmentsNum(); for (size_t i = 0; i < nsegs; i++) { - Segment &selseg = strip.getSegment(i); + const Segment &selseg = strip.getSegment(i); if (!selseg.isActive()) continue; unsigned ofs = 41 + s*UDP_SEG_SIZE; //start of segment offset byte udpOut[0 +ofs] = s; @@ -177,7 +177,7 @@ void notify(byte callMode, bool followUp) memcpy(buffer.data + packetSize, &udpOut[41+i*UDP_SEG_SIZE], UDP_SEG_SIZE); packetSize += UDP_SEG_SIZE; if (packetSize + UDP_SEG_SIZE < bufferSize) continue; - DEBUG_PRINTF_P(PSTR("ESP-NOW sending packet: %d (%d)\n"), (int)buffer.packet, packetSize+3); + DEBUG_PRINTF_P(PSTR("ESP-NOW sending packet: %d (%u)\n"), (int)buffer.packet, packetSize+3); err = quickEspNow.send(ESPNOW_BROADCAST_ADDRESS, reinterpret_cast(&buffer), packetSize+3); buffer.packet++; packetSize = 0; @@ -206,7 +206,7 @@ void notify(byte callMode, bool followUp) notificationCount = followUp ? notificationCount + 1 : 0; } -void parseNotifyPacket(uint8_t *udpIn) { +static void parseNotifyPacket(const uint8_t *udpIn) { //ignore notification if received within a second after sending a notification ourselves if (millis() - notificationSentTime < 1000) return; if (udpIn[1] > 199) return; //do not receive custom versions @@ -225,21 +225,19 @@ void parseNotifyPacket(uint8_t *udpIn) { // set transition time before making any segment changes if (version > 3) { - if (fadeTransition) { - jsonTransitionOnce = true; - strip.setTransition(((udpIn[17] << 0) & 0xFF) + ((udpIn[18] << 8) & 0xFF00)); - } + jsonTransitionOnce = true; + strip.setTransition(((udpIn[17] << 0) & 0xFF) + ((udpIn[18] << 8) & 0xFF00)); } //apply colors from notification to main segment, only if not syncing full segments if ((receiveNotificationColor || !someSel) && (version < 11 || !receiveSegmentOptions)) { // primary color, only apply white if intented (version > 0) - strip.setColor(0, RGBW32(udpIn[3], udpIn[4], udpIn[5], (version > 0) ? udpIn[10] : 0)); + strip.getMainSegment().setColor(0, RGBW32(udpIn[3], udpIn[4], udpIn[5], (version > 0) ? udpIn[10] : 0)); if (version > 1) { - strip.setColor(1, RGBW32(udpIn[12], udpIn[13], udpIn[14], udpIn[15])); // secondary color + strip.getMainSegment().setColor(1, RGBW32(udpIn[12], udpIn[13], udpIn[14], udpIn[15])); // secondary color } if (version > 6) { - strip.setColor(2, RGBW32(udpIn[20], udpIn[21], udpIn[22], udpIn[23])); // tertiary color + strip.getMainSegment().setColor(2, RGBW32(udpIn[20], udpIn[21], udpIn[22], udpIn[23])); // tertiary color if (version > 9 && udpIn[37] < 255) { // valid CCT/Kelvin value unsigned cct = udpIn[38]; if (udpIn[37] > 0) { //Kelvin @@ -260,20 +258,21 @@ void parseNotifyPacket(uint8_t *udpIn) { // are we syncing bounds and slave has more active segments than master? if (receiveSegmentBounds && numSrcSegs < strip.getActiveSegmentsNum()) { DEBUG_PRINTLN(F("Removing excessive segments.")); - for (size_t i=strip.getSegmentsNum(); i>numSrcSegs; i--) { - if (strip.getSegment(i).isActive()) { - strip.setSegment(i-1,0,0); // delete segment - } + strip.suspend(); //should not be needed as UDP handling is not done in ISR callbacks but still added "just in case" + for (size_t i=strip.getSegmentsNum(); i>numSrcSegs && i>0; i--) { + Segment &seg = strip.getSegment(i-1); + if (seg.isActive()) seg.deactivate(); // delete segment } + strip.resume(); } size_t inactiveSegs = 0; - for (size_t i = 0; i < numSrcSegs && i < strip.getMaxSegments(); i++) { + for (size_t i = 0; i < numSrcSegs && i < WS2812FX::getMaxSegments(); i++) { unsigned ofs = 41 + i*udpIn[40]; //start of segment offset byte unsigned id = udpIn[0 +ofs]; DEBUG_PRINTF_P(PSTR("UDP segment received: %u\n"), id); if (id > strip.getSegmentsNum()) break; else if (id == strip.getSegmentsNum()) { - if (receiveSegmentBounds && id < strip.getMaxSegments()) strip.appendSegment(); + if (receiveSegmentBounds && id < WS2812FX::getMaxSegments()) strip.appendSegment(); else break; } DEBUG_PRINTF_P(PSTR("UDP segment check: %u\n"), id); @@ -300,7 +299,7 @@ void parseNotifyPacket(uint8_t *udpIn) { if (!receiveSegmentOptions) { DEBUG_PRINTF_P(PSTR("Set segment w/o options: %d [%d,%d;%d,%d]\n"), id, (int)start, (int)stop, (int)startY, (int)stopY); strip.suspend(); //should not be needed as UDP handling is not done in ISR callbacks but still added "just in case" - selseg.setUp(start, stop, selseg.grouping, selseg.spacing, offset, startY, stopY); + selseg.setGeometry(start, stop, selseg.grouping, selseg.spacing, offset, startY, stopY, selseg.map1D2D); strip.resume(); continue; // we do receive bounds, but not options } @@ -328,7 +327,7 @@ void parseNotifyPacket(uint8_t *udpIn) { // freeze, reset should never be synced // LSB to MSB: select, reverse, on, mirror, freeze, reset, reverse_y, mirror_y, transpose, map1d2d (3), ssim (2), set (2) DEBUG_PRINTF_P(PSTR("Apply options: %u\n"), id); - selseg.options = (selseg.options & 0b0000000000110001U) | (udpIn[28+ofs]<<8) | (udpIn[9 +ofs] & 0b11001110U); // ignore selected, freeze, reset + selseg.options = (selseg.options & 0b0000000000110001U) | ((uint16_t)udpIn[28+ofs]<<8) | (udpIn[9 +ofs] & 0b11001110U); // ignore selected, freeze, reset if (applyEffects) { DEBUG_PRINTF_P(PSTR("Apply sliders: %u\n"), id); selseg.custom1 = udpIn[29+ofs]; @@ -342,12 +341,12 @@ void parseNotifyPacket(uint8_t *udpIn) { if (receiveSegmentBounds) { DEBUG_PRINTF_P(PSTR("Set segment w/ options: %d [%d,%d;%d,%d]\n"), id, (int)start, (int)stop, (int)startY, (int)stopY); strip.suspend(); //should not be needed as UDP handling is not done in ISR callbacks but still added "just in case" - selseg.setUp(start, stop, udpIn[5+ofs], udpIn[6+ofs], offset, startY, stopY); + selseg.setGeometry(start, stop, udpIn[5+ofs], udpIn[6+ofs], offset, startY, stopY, selseg.map1D2D); strip.resume(); } else { DEBUG_PRINTF_P(PSTR("Set segment grouping: %d [%d,%d]\n"), id, (int)udpIn[5+ofs], (int)udpIn[6+ofs]); strip.suspend(); //should not be needed as UDP handling is not done in ISR callbacks but still added "just in case" - selseg.setUp(selseg.start, selseg.stop, udpIn[5+ofs], udpIn[6+ofs], selseg.offset, selseg.startY, selseg.stopY); + selseg.setGeometry(selseg.start, selseg.stop, udpIn[5+ofs], udpIn[6+ofs], selseg.offset, selseg.startY, selseg.stopY, selseg.map1D2D); strip.resume(); } } @@ -407,31 +406,26 @@ void parseNotifyPacket(uint8_t *udpIn) { stateUpdated(CALL_MODE_NOTIFICATION); } +// realtimeLock() is called from UDP notifications, JSON API or serial Ada void realtimeLock(uint32_t timeoutMs, byte md) { if (!realtimeMode && !realtimeOverride) { - unsigned stop, start; if (useMainSegmentOnly) { Segment& mainseg = strip.getMainSegment(); - start = mainseg.start; - stop = mainseg.stop; + mainseg.clear(); // clear entire segment (in case sender transmits less pixels) mainseg.freeze = true; - } else { - start = 0; - stop = strip.getLengthTotal(); - } - // clear strip/segment - for (size_t i = start; i < stop; i++) strip.setPixelColor(i,BLACK); - // if WLED was off and using main segment only, freeze non-main segments so they stay off - if (useMainSegmentOnly && bri == 0) { - for (size_t s=0; s < strip.getSegmentsNum(); s++) { - strip.getSegment(s).freeze = true; + // if WLED was off and using main segment only, freeze non-main segments so they stay off + if (bri == 0) { + for (size_t s = 0; s < strip.getSegmentsNum(); s++) strip.getSegment(s).freeze = true; } + } else { + // clear entire strip + strip.fill(BLACK); + } + // if strip is off (bri==0) and not already in RTM + if (briT == 0) { + strip.setBrightness(scaledBri(briLast), true); } - } - // if strip is off (bri==0) and not already in RTM - if (briT == 0 && !realtimeMode && !realtimeOverride) { - strip.setBrightness(scaledBri(briLast), true); } if (realtimeTimeout != UINT32_MAX) { @@ -453,6 +447,7 @@ void exitRealtime() { realtimeIP[0] = 0; if (useMainSegmentOnly) { // unfreeze live segment again strip.getMainSegment().freeze = false; + strip.trigger(); } else { strip.show(); // possible fix for #3589 } @@ -482,7 +477,8 @@ void handleNotifications() if (e131NewData && millis() - strip.getLastShow() > 15) { e131NewData = false; - strip.show(); + if (useMainSegmentOnly) strip.trigger(); + else strip.show(); } //unlock strip when realtime UDP times out @@ -509,15 +505,13 @@ void handleNotifications() uint8_t lbuf[packetSize]; rgbUdp.read(lbuf, packetSize); realtimeLock(realtimeTimeoutMs, REALTIME_MODE_HYPERION); - if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; - unsigned id = 0; + if (realtimeOverride) return; unsigned totalLen = strip.getLengthTotal(); - for (size_t i = 0; i < packetSize -2; i += 3) - { + for (size_t i = 0, id = 0; i < packetSize -2 && id < totalLen; i += 3, id++) { setRealtimePixel(id, lbuf[i], lbuf[i+1], lbuf[i+2], 0); - id++; if (id >= totalLen) break; } - if (!(realtimeMode && useMainSegmentOnly)) strip.show(); + if (useMainSegmentOnly) strip.trigger(); + else strip.show(); return; } } @@ -586,7 +580,7 @@ void handleNotifications() realtimeIP = (isSupp) ? notifier2Udp.remoteIP() : notifierUdp.remoteIP(); realtimeLock(realtimeTimeoutMs, REALTIME_MODE_TPM2NET); - if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; + if (realtimeOverride) return; tpmPacketCount++; //increment the packet count if (tpmPacketCount == 1) tpmPayloadFrameSize = (udpIn[2] << 8) + udpIn[3]; //save frame size for the whole payload if this is the first packet @@ -595,19 +589,13 @@ void handleNotifications() unsigned id = (tpmPayloadFrameSize/3)*(packetNum-1); //start LED unsigned totalLen = strip.getLengthTotal(); - for (size_t i = 6; i < tpmPayloadFrameSize + 4U; i += 3) - { - if (id < totalLen) - { - setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0); - id++; - } - else break; + for (size_t i = 6; i < tpmPayloadFrameSize + 4U && id < totalLen; i += 3, id++) { + setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0); } - if (tpmPacketCount == numPackets) //reset packet count and show if all packets were received - { + if (tpmPacketCount == numPackets) { //reset packet count and show if all packets were received tpmPacketCount = 0; - strip.show(); + if (useMainSegmentOnly) strip.trigger(); + else strip.show(); } return; } @@ -619,14 +607,13 @@ void handleNotifications() DEBUG_PRINTLN(realtimeIP); if (packetSize < 2) return; - if (udpIn[1] == 0) - { - realtimeTimeout = 0; + if (udpIn[1] == 0) { + realtimeTimeout = 0; // cancel realtime mode immediately return; } else { realtimeLock(udpIn[1]*1000 +1, REALTIME_MODE_UDP); } - if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; + if (realtimeOverride) return; unsigned totalLen = strip.getLengthTotal(); if (udpIn[0] == 1 && packetSize > 5) //warls @@ -637,42 +624,33 @@ void handleNotifications() } } else if (udpIn[0] == 2 && packetSize > 4) //drgb { - unsigned id = 0; - for (size_t i = 2; i < packetSize -2; i += 3) + for (size_t i = 2, id = 0; i < packetSize -2 && id < totalLen; i += 3, id++) { setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0); - - id++; if (id >= totalLen) break; } } else if (udpIn[0] == 3 && packetSize > 6) //drgbw { - unsigned id = 0; - for (size_t i = 2; i < packetSize -3; i += 4) + for (size_t i = 2, id = 0; i < packetSize -3 && id < totalLen; i += 4, id++) { setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3]); - - id++; if (id >= totalLen) break; } } else if (udpIn[0] == 4 && packetSize > 7) //dnrgb { unsigned id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00); - for (size_t i = 4; i < packetSize -2; i += 3) + for (size_t i = 4; i < packetSize -2 && id < totalLen; i += 3, id++) { - if (id >= totalLen) break; setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0); - id++; } } else if (udpIn[0] == 5 && packetSize > 8) //dnrgbw { unsigned id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00); - for (size_t i = 4; i < packetSize -2; i += 4) + for (size_t i = 4; i < packetSize -2 && id < totalLen; i += 4, id++) { - if (id >= totalLen) break; setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3]); - id++; } } - strip.show(); + if (useMainSegmentOnly) strip.trigger(); + else strip.show(); return; } @@ -697,20 +675,7 @@ void handleNotifications() void setRealtimePixel(uint16_t i, byte r, byte g, byte b, byte w) { unsigned pix = i + arlsOffset; - if (pix < strip.getLengthTotal()) { - if (!arlsDisableGammaCorrection && gammaCorrectCol) { - r = gamma8(r); - g = gamma8(g); - b = gamma8(b); - w = gamma8(w); - } - if (useMainSegmentOnly) { - Segment &seg = strip.getMainSegment(); - if (pixsetup(); } -void UsermodManager::connected() { for (unsigned i = 0; i < numMods; i++) ums[i]->connected(); } -void UsermodManager::loop() { for (unsigned i = 0; i < numMods; i++) ums[i]->loop(); } -void UsermodManager::handleOverlayDraw() { for (unsigned i = 0; i < numMods; i++) ums[i]->handleOverlayDraw(); } -void UsermodManager::appendConfigData(Print& dest) { for (unsigned i = 0; i < numMods; i++) ums[i]->appendConfigData(dest); } +void UsermodManager::setup() { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->setup(); } +void UsermodManager::connected() { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->connected(); } +void UsermodManager::loop() { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->loop(); } +void UsermodManager::handleOverlayDraw() { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->handleOverlayDraw(); } +void UsermodManager::appendConfigData(Print& dest) { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->appendConfigData(dest); } bool UsermodManager::handleButton(uint8_t b) { bool overrideIO = false; - for (unsigned i = 0; i < numMods; i++) { - if (ums[i]->handleButton(b)) overrideIO = true; + for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) { + if ((*mod)->handleButton(b)) overrideIO = true; } return overrideIO; } bool UsermodManager::getUMData(um_data_t **data, uint8_t mod_id) { - for (unsigned i = 0; i < numMods; i++) { - if (mod_id > 0 && ums[i]->getId() != mod_id) continue; // only get data form requested usermod if provided - if (ums[i]->getUMData(data)) return true; // if usermod does provide data return immediately (only one usermod can provide data at one time) + for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) { + if (mod_id > 0 && (*mod)->getId() != mod_id) continue; // only get data form requested usermod if provided + if ((*mod)->getUMData(data)) return true; // if usermod does provide data return immediately (only one usermod can provide data at one time) } return false; } -void UsermodManager::addToJsonState(JsonObject& obj) { for (unsigned i = 0; i < numMods; i++) ums[i]->addToJsonState(obj); } -void UsermodManager::addToJsonInfo(JsonObject& obj) { for (unsigned i = 0; i < numMods; i++) ums[i]->addToJsonInfo(obj); } -void UsermodManager::readFromJsonState(JsonObject& obj) { for (unsigned i = 0; i < numMods; i++) ums[i]->readFromJsonState(obj); } -void UsermodManager::addToConfig(JsonObject& obj) { for (unsigned i = 0; i < numMods; i++) ums[i]->addToConfig(obj); } +void UsermodManager::addToJsonState(JsonObject& obj) { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->addToJsonState(obj); } +void UsermodManager::addToJsonInfo(JsonObject& obj) { + auto um_id_list = obj.createNestedArray("um"); + for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) { + um_id_list.add((*mod)->getId()); + (*mod)->addToJsonInfo(obj); + } +} +void UsermodManager::readFromJsonState(JsonObject& obj) { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->readFromJsonState(obj); } +void UsermodManager::addToConfig(JsonObject& obj) { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->addToConfig(obj); } bool UsermodManager::readFromConfig(JsonObject& obj) { bool allComplete = true; - for (unsigned i = 0; i < numMods; i++) { - if (!ums[i]->readFromConfig(obj)) allComplete = false; + for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) { + if (!(*mod)->readFromConfig(obj)) allComplete = false; } return allComplete; } #ifndef WLED_DISABLE_MQTT -void UsermodManager::onMqttConnect(bool sessionPresent) { for (unsigned i = 0; i < numMods; i++) ums[i]->onMqttConnect(sessionPresent); } +void UsermodManager::onMqttConnect(bool sessionPresent) { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->onMqttConnect(sessionPresent); } bool UsermodManager::onMqttMessage(char* topic, char* payload) { - for (unsigned i = 0; i < numMods; i++) if (ums[i]->onMqttMessage(topic, payload)) return true; + for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) if ((*mod)->onMqttMessage(topic, payload)) return true; return false; } #endif #ifndef WLED_DISABLE_ESPNOW bool UsermodManager::onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len) { - for (unsigned i = 0; i < numMods; i++) if (ums[i]->onEspNowMessage(sender, payload, len)) return true; + for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) if ((*mod)->onEspNowMessage(sender, payload, len)) return true; return false; } #endif -void UsermodManager::onUpdateBegin(bool init) { for (unsigned i = 0; i < numMods; i++) ums[i]->onUpdateBegin(init); } // notify usermods that update is to begin -void UsermodManager::onStateChange(uint8_t mode) { for (unsigned i = 0; i < numMods; i++) ums[i]->onStateChange(mode); } // notify usermods that WLED state changed +void UsermodManager::onUpdateBegin(bool init) { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->onUpdateBegin(init); } // notify usermods that update is to begin +void UsermodManager::onStateChange(uint8_t mode) { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->onStateChange(mode); } // notify usermods that WLED state changed /* * Enables usermods to lookup another Usermod. */ Usermod* UsermodManager::lookup(uint16_t mod_id) { - for (unsigned i = 0; i < numMods; i++) { - if (ums[i]->getId() == mod_id) { - return ums[i]; + for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) { + if ((*mod)->getId() == mod_id) { + return *mod; } } return nullptr; } -bool UsermodManager::add(Usermod* um) -{ - if (numMods >= WLED_MAX_USERMODS || um == nullptr) return false; - ums[numMods++] = um; - return true; -} - -Usermod* UsermodManager::ums[WLED_MAX_USERMODS] = {nullptr}; -byte UsermodManager::numMods = 0; +size_t UsermodManager::getModCount() { return getCount(); }; /* Usermod v2 interface shim for oappend */ Print* Usermod::oappend_shim = nullptr; diff --git a/wled00/usermod.cpp b/wled00/usermod.cpp index da973823..40fda83b 100644 --- a/wled00/usermod.cpp +++ b/wled00/usermod.cpp @@ -1,7 +1,7 @@ #include "wled.h" /* * This v1 usermod file allows you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * See: https://github.com/wled-dev/WLED/wiki/Add-own-functionality * EEPROM bytes 2750+ are reserved for your custom use case. (if you extend #define EEPSIZE in const.h) * If you just need 8 bytes, use 2551-2559 (you do not need to increase EEPSIZE) * diff --git a/wled00/usermod_v2_empty.h b/wled00/usermod_v2_empty.h deleted file mode 100644 index 6537b56b..00000000 --- a/wled00/usermod_v2_empty.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include "wled.h" - -//This is an empty v2 usermod template. Please see the file usermod_v2_example.h in the EXAMPLE_v2 usermod folder for documentation on the functions you can use! - -class UsermodRenameMe : public Usermod { - private: - - public: - void setup() { - - } - - void loop() { - - } -}; \ No newline at end of file diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp deleted file mode 100644 index 36bd122a..00000000 --- a/wled00/usermods_list.cpp +++ /dev/null @@ -1,473 +0,0 @@ -#include "wled.h" -/* - * Register your v2 usermods here! - * (for v1 usermods using just usermod.cpp, you can ignore this file) - */ - -/* - * Add/uncomment your usermod filename here (and once more below) - * || || || - * \/ \/ \/ - */ -//#include "../usermods/EXAMPLE_v2/usermod_v2_example.h" - -#ifdef USERMOD_BATTERY - #include "../usermods/Battery/usermod_v2_Battery.h" -#endif - -#ifdef USERMOD_DALLASTEMPERATURE - #include "../usermods/Temperature/usermod_temperature.h" -#endif - -#ifdef USERMOD_SHT -#include "../usermods/sht/usermod_sht.h" -#endif - -#ifdef USERMOD_SN_PHOTORESISTOR - #include "../usermods/SN_Photoresistor/usermod_sn_photoresistor.h" -#endif - -#ifdef USERMOD_PWM_FAN - // requires DALLASTEMPERATURE or SHT included before it - #include "../usermods/PWM_fan/usermod_PWM_fan.h" -#endif - -#ifdef USERMOD_BUZZER - #include "../usermods/buzzer/usermod_v2_buzzer.h" -#endif - -#ifdef USERMOD_SENSORSTOMQTT - #include "../usermods/sensors_to_mqtt/usermod_v2_SensorsToMqtt.h" -#endif - -#ifdef USERMOD_PIRSWITCH - #include "../usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h" -#endif - -#ifdef USERMOD_BH1750 - #include "../usermods/BH1750_v2/usermod_BH1750.h" -#endif - -// BME280 v2 usermod. Define "USERMOD_BME280" in my_config.h -#ifdef USERMOD_BME280 - #include "../usermods/BME280_v2/usermod_bme280.h" -#endif - -#ifdef USERMOD_BME68X - #include "../usermods/BME68X_v2/usermod_bme68x.h" -#endif - - -#ifdef USERMOD_FOUR_LINE_DISPLAY - #include "../usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h" -#endif - -#ifdef USERMOD_ROTARY_ENCODER_UI - #include "../usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h" -#endif - -#ifdef USERMOD_AUTO_SAVE - #include "../usermods/usermod_v2_auto_save/usermod_v2_auto_save.h" -#endif - -#ifdef USERMOD_DHT - #include "../usermods/DHT/usermod_dht.h" -#endif - -#ifdef USERMOD_VL53L0X_GESTURES - #include "../usermods/VL53L0X_gestures/usermod_vl53l0x_gestures.h" -#endif - -#ifdef USERMOD_ANIMATED_STAIRCASE - #include "../usermods/Animated_Staircase/Animated_Staircase.h" -#endif - -#ifdef USERMOD_MULTI_RELAY - #include "../usermods/multi_relay/usermod_multi_relay.h" -#endif - -#ifdef USERMOD_RTC - #include "../usermods/RTC/usermod_rtc.h" -#endif - -#ifdef USERMOD_ELEKSTUBE_IPS - #include "../usermods/EleksTube_IPS/usermod_elekstube_ips.h" -#endif - -#ifdef USERMOD_ROTARY_ENCODER_BRIGHTNESS_COLOR - #include "../usermods/usermod_rotary_brightness_color/usermod_rotary_brightness_color.h" -#endif - -#ifdef RGB_ROTARY_ENCODER - #include "../usermods/rgb-rotary-encoder/rgb-rotary-encoder.h" -#endif - -#ifdef USERMOD_ST7789_DISPLAY - #include "../usermods/ST7789_display/ST7789_Display.h" -#endif - -#ifdef USERMOD_PIXELS_DICE_TRAY - #include "../usermods/pixels_dice_tray/pixels_dice_tray.h" -#endif - -#ifdef USERMOD_SEVEN_SEGMENT - #include "../usermods/seven_segment_display/usermod_v2_seven_segment_display.h" -#endif - -#ifdef USERMOD_SSDR - #include "../usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h" -#endif - -#ifdef USERMOD_CRONIXIE - #include "../usermods/Cronixie/usermod_cronixie.h" -#endif - -#ifdef QUINLED_AN_PENTA - #include "../usermods/quinled-an-penta/quinled-an-penta.h" -#endif - -#ifdef USERMOD_WIZLIGHTS - #include "../usermods/wizlights/wizlights.h" -#endif - -#ifdef USERMOD_WIREGUARD - #include "../usermods/wireguard/wireguard.h" -#endif - -#ifdef USERMOD_WORDCLOCK - #include "../usermods/usermod_v2_word_clock/usermod_v2_word_clock.h" -#endif - -#ifdef USERMOD_MY9291 - #include "../usermods/MY9291/usermode_MY9291.h" -#endif - -#ifdef USERMOD_SI7021_MQTT_HA - #include "../usermods/Si7021_MQTT_HA/usermod_si7021_mqtt_ha.h" -#endif - -#ifdef USERMOD_SMARTNEST - #include "../usermods/smartnest/usermod_smartnest.h" -#endif - -#ifdef USERMOD_AUDIOREACTIVE - #include "../usermods/audioreactive/audio_reactive.h" -#endif - -#ifdef USERMOD_ANALOG_CLOCK - #include "../usermods/Analog_Clock/Analog_Clock.h" -#endif - -#ifdef USERMOD_PING_PONG_CLOCK - #include "../usermods/usermod_v2_ping_pong_clock/usermod_v2_ping_pong_clock.h" -#endif - -#ifdef USERMOD_ADS1115 - #include "../usermods/ADS1115_v2/usermod_ads1115.h" -#endif - -#ifdef USERMOD_KLIPPER_PERCENTAGE - #include "../usermods/usermod_v2_klipper_percentage/usermod_v2_klipper_percentage.h" -#endif - -#ifdef USERMOD_BOBLIGHT - #include "../usermods/boblight/boblight.h" -#endif - -#ifdef USERMOD_ANIMARTRIX - #include "../usermods/usermod_v2_animartrix/usermod_v2_animartrix.h" -#endif - -#ifdef USERMOD_INTERNAL_TEMPERATURE - #include "../usermods/Internal_Temperature_v2/usermod_internal_temperature.h" -#endif - -#if defined(WLED_USE_SD_MMC) || defined(WLED_USE_SD_SPI) -// This include of SD.h and SD_MMC.h must happen here, else they won't be -// resolved correctly (when included in mod's header only) - #ifdef WLED_USE_SD_MMC - #include "SD_MMC.h" - #elif defined(WLED_USE_SD_SPI) - #include "SD.h" - #include "SPI.h" - #endif - #include "../usermods/sd_card/usermod_sd_card.h" -#endif - -#ifdef USERMOD_PWM_OUTPUTS - #include "../usermods/pwm_outputs/usermod_pwm_outputs.h" -#endif - -#ifdef USERMOD_HTTP_PULL_LIGHT_CONTROL - #include "../usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.h" -#endif - -#ifdef USERMOD_MPU6050_IMU - #include "../usermods/mpu6050_imu/usermod_mpu6050_imu.h" -#endif - -#ifdef USERMOD_MPU6050_IMU - #include "../usermods/mpu6050_imu/usermod_gyro_surge.h" -#endif - -#ifdef USERMOD_LDR_DUSK_DAWN - #include "../usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h" -#endif - -#ifdef USERMOD_POV_DISPLAY - #include "../usermods/pov_display/usermod_pov_display.h" -#endif - -#ifdef USERMOD_STAIRCASE_WIPE - #include "../usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h" -#endif - -#ifdef USERMOD_MAX17048 - #include "../usermods/MAX17048_v2/usermod_max17048.h" -#endif - -#ifdef USERMOD_TETRISAI - #include "../usermods/TetrisAI_v2/usermod_v2_tetrisai.h" -#endif - -#ifdef USERMOD_AHT10 - #include "../usermods/AHT10_v2/usermod_aht10.h" -#endif - -#ifdef USERMOD_INA226 - #include "../usermods/INA226_v2/usermod_ina226.h" -#endif - -#ifdef USERMOD_LD2410 -#include "../usermods/LD2410_v2/usermod_ld2410.h" -#endif - -void registerUsermods() -{ -/* - * Add your usermod class name here - * || || || - * \/ \/ \/ - */ - //UsermodManager::add(new MyExampleUsermod()); - - #ifdef USERMOD_BATTERY - UsermodManager::add(new UsermodBattery()); - #endif - - #ifdef USERMOD_DALLASTEMPERATURE - UsermodManager::add(new UsermodTemperature()); - #endif - - #ifdef USERMOD_SN_PHOTORESISTOR - UsermodManager::add(new Usermod_SN_Photoresistor()); - #endif - - #ifdef USERMOD_PWM_FAN - UsermodManager::add(new PWMFanUsermod()); - #endif - - #ifdef USERMOD_BUZZER - UsermodManager::add(new BuzzerUsermod()); - #endif - - #ifdef USERMOD_BH1750 - UsermodManager::add(new Usermod_BH1750()); - #endif - - #ifdef USERMOD_BME280 - UsermodManager::add(new UsermodBME280()); - #endif - - #ifdef USERMOD_BME68X - UsermodManager::add(new UsermodBME68X()); - #endif - - #ifdef USERMOD_SENSORSTOMQTT - UsermodManager::add(new UserMod_SensorsToMQTT()); - #endif - - #ifdef USERMOD_PIRSWITCH - UsermodManager::add(new PIRsensorSwitch()); - #endif - - #ifdef USERMOD_FOUR_LINE_DISPLAY - UsermodManager::add(new FourLineDisplayUsermod()); - #endif - - #ifdef USERMOD_ROTARY_ENCODER_UI - UsermodManager::add(new RotaryEncoderUIUsermod()); // can use USERMOD_FOUR_LINE_DISPLAY - #endif - - #ifdef USERMOD_AUTO_SAVE - UsermodManager::add(new AutoSaveUsermod()); // can use USERMOD_FOUR_LINE_DISPLAY - #endif - - #ifdef USERMOD_DHT - UsermodManager::add(new UsermodDHT()); - #endif - - #ifdef USERMOD_VL53L0X_GESTURES - UsermodManager::add(new UsermodVL53L0XGestures()); - #endif - - #ifdef USERMOD_ANIMATED_STAIRCASE - UsermodManager::add(new Animated_Staircase()); - #endif - - #ifdef USERMOD_MULTI_RELAY - UsermodManager::add(new MultiRelay()); - #endif - - #ifdef USERMOD_RTC - UsermodManager::add(new RTCUsermod()); - #endif - - #ifdef USERMOD_ELEKSTUBE_IPS - UsermodManager::add(new ElekstubeIPSUsermod()); - #endif - - #ifdef USERMOD_ROTARY_ENCODER_BRIGHTNESS_COLOR - UsermodManager::add(new RotaryEncoderBrightnessColor()); - #endif - - #ifdef RGB_ROTARY_ENCODER - UsermodManager::add(new RgbRotaryEncoderUsermod()); - #endif - - #ifdef USERMOD_ST7789_DISPLAY - UsermodManager::add(new St7789DisplayUsermod()); - #endif - - #ifdef USERMOD_PIXELS_DICE_TRAY - UsermodManager::add(new PixelsDiceTrayUsermod()); - #endif - - #ifdef USERMOD_SEVEN_SEGMENT - UsermodManager::add(new SevenSegmentDisplay()); - #endif - - #ifdef USERMOD_SSDR - UsermodManager::add(new UsermodSSDR()); - #endif - - #ifdef USERMOD_CRONIXIE - UsermodManager::add(new UsermodCronixie()); - #endif - - #ifdef QUINLED_AN_PENTA - UsermodManager::add(new QuinLEDAnPentaUsermod()); - #endif - - #ifdef USERMOD_WIZLIGHTS - UsermodManager::add(new WizLightsUsermod()); - #endif - - #ifdef USERMOD_WIREGUARD - UsermodManager::add(new WireguardUsermod()); - #endif - - #ifdef USERMOD_WORDCLOCK - UsermodManager::add(new WordClockUsermod()); - #endif - - #ifdef USERMOD_MY9291 - UsermodManager::add(new MY9291Usermod()); - #endif - - #ifdef USERMOD_SI7021_MQTT_HA - UsermodManager::add(new Si7021_MQTT_HA()); - #endif - - #ifdef USERMOD_SMARTNEST - UsermodManager::add(new Smartnest()); - #endif - - #ifdef USERMOD_AUDIOREACTIVE - UsermodManager::add(new AudioReactive()); - #endif - - #ifdef USERMOD_ANALOG_CLOCK - UsermodManager::add(new AnalogClockUsermod()); - #endif - - #ifdef USERMOD_PING_PONG_CLOCK - UsermodManager::add(new PingPongClockUsermod()); - #endif - - #ifdef USERMOD_ADS1115 - UsermodManager::add(new ADS1115Usermod()); - #endif - - #ifdef USERMOD_KLIPPER_PERCENTAGE - UsermodManager::add(new klipper_percentage()); - #endif - - #ifdef USERMOD_BOBLIGHT - UsermodManager::add(new BobLightUsermod()); - #endif - - #ifdef SD_ADAPTER - UsermodManager::add(new UsermodSdCard()); - #endif - - #ifdef USERMOD_PWM_OUTPUTS - UsermodManager::add(new PwmOutputsUsermod()); - #endif - - #ifdef USERMOD_SHT - UsermodManager::add(new ShtUsermod()); - #endif - - #ifdef USERMOD_ANIMARTRIX - UsermodManager::add(new AnimartrixUsermod("Animartrix", false)); - #endif - - #ifdef USERMOD_INTERNAL_TEMPERATURE - UsermodManager::add(new InternalTemperatureUsermod()); - #endif - - #ifdef USERMOD_HTTP_PULL_LIGHT_CONTROL - UsermodManager::add(new HttpPullLightControl()); - #endif - - #ifdef USERMOD_MPU6050_IMU - static MPU6050Driver mpu6050; UsermodManager::add(&mpu6050); - #endif - - #ifdef USERMOD_GYRO_SURGE - static GyroSurge gyro_surge; UsermodManager::add(&gyro_surge); - #endif - - #ifdef USERMOD_LDR_DUSK_DAWN - UsermodManager::add(new LDR_Dusk_Dawn_v2()); - #endif - - #ifdef USERMOD_STAIRCASE_WIPE - UsermodManager::add(new StairwayWipeUsermod()); - #endif - - #ifdef USERMOD_MAX17048 - UsermodManager::add(new Usermod_MAX17048()); - #endif - - #ifdef USERMOD_TETRISAI - UsermodManager::add(new TetrisAIUsermod()); - #endif - - #ifdef USERMOD_AHT10 - UsermodManager::add(new UsermodAHT10()); - #endif - - #ifdef USERMOD_INA226 - UsermodManager::add(new UsermodINA226()); - #endif - - #ifdef USERMOD_LD2410 - UsermodManager::add(new LD2410Usermod()); - #endif - - #ifdef USERMOD_POV_DISPLAY - UsermodManager::add(new PovDisplayUsermod()); - #endif -} diff --git a/wled00/util.cpp b/wled00/util.cpp index 41e3d6c2..97e1e3b0 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -4,17 +4,17 @@ //helper to get int value at a position in string -int getNumVal(const String* req, uint16_t pos) +int getNumVal(const String &req, uint16_t pos) { - return req->substring(pos+3).toInt(); + return req.substring(pos+3).toInt(); } //helper to get int value with in/decrementing support via ~ syntax -void parseNumber(const char* str, byte* val, byte minv, byte maxv) +void parseNumber(const char* str, byte &val, byte minv, byte maxv) { if (str == nullptr || str[0] == '\0') return; - if (str[0] == 'r') {*val = random8(minv,maxv?maxv:255); return;} // maxv for random cannot be 0 + if (str[0] == 'r') {val = hw_random8(minv,maxv?maxv:255); return;} // maxv for random cannot be 0 bool wrap = false; if (str[0] == 'w' && strlen(str) > 1) {str++; wrap = true;} if (str[0] == '~') { @@ -22,19 +22,19 @@ void parseNumber(const char* str, byte* val, byte minv, byte maxv) if (out == 0) { if (str[1] == '0') return; if (str[1] == '-') { - *val = (int)(*val -1) < (int)minv ? maxv : min((int)maxv,(*val -1)); //-1, wrap around + val = (int)(val -1) < (int)minv ? maxv : min((int)maxv,(val -1)); //-1, wrap around } else { - *val = (int)(*val +1) > (int)maxv ? minv : max((int)minv,(*val +1)); //+1, wrap around + val = (int)(val +1) > (int)maxv ? minv : max((int)minv,(val +1)); //+1, wrap around } } else { - if (wrap && *val == maxv && out > 0) out = minv; - else if (wrap && *val == minv && out < 0) out = maxv; + if (wrap && val == maxv && out > 0) out = minv; + else if (wrap && val == minv && out < 0) out = maxv; else { - out += *val; + out += val; if (out > maxv) out = maxv; if (out < minv) out = minv; } - *val = out; + val = out; } return; } else if (minv == maxv && minv == 0) { // limits "unset" i.e. both 0 @@ -49,14 +49,14 @@ void parseNumber(const char* str, byte* val, byte minv, byte maxv) } } } - *val = atoi(str); + val = atoi(str); } //getVal supports inc/decrementing and random ("X~Y(r|~[w][-][Z])" form) -bool getVal(JsonVariant elem, byte* val, byte vmin, byte vmax) { +bool getVal(JsonVariant elem, byte &val, byte vmin, byte vmax) { if (elem.is()) { if (elem < 0) return false; //ignore e.g. {"ps":-1} - *val = elem; + val = elem; return true; } else if (elem.is()) { const char* str = elem; @@ -73,7 +73,7 @@ bool getVal(JsonVariant elem, byte* val, byte vmin, byte vmax) { } -bool getBoolVal(JsonVariant elem, bool dflt) { +bool getBoolVal(const JsonVariant &elem, bool dflt) { if (elem.is() && elem.as()[0] == 't') { return !dflt; } else { @@ -82,7 +82,7 @@ bool getBoolVal(JsonVariant elem, bool dflt) { } -bool updateVal(const char* req, const char* key, byte* val, byte minv, byte maxv) +bool updateVal(const char* req, const char* key, byte &val, byte minv, byte maxv) { const char *v = strstr(req, key); if (v) v += strlen(key); @@ -150,8 +150,8 @@ bool isAsterisksOnly(const char* str, byte maxLen) } -//threading/network callback details: https://github.com/Aircoookie/WLED/pull/2336#discussion_r762276994 -bool requestJSONBufferLock(uint8_t module) +//threading/network callback details: https://github.com/wled-dev/WLED/pull/2336#discussion_r762276994 +bool requestJSONBufferLock(uint8_t moduleID) { if (pDoc == nullptr) { DEBUG_PRINTLN(F("ERROR: JSON buffer not allocated!")); @@ -175,14 +175,14 @@ bool requestJSONBufferLock(uint8_t module) #endif // If the lock is still held - by us, or by another task if (jsonBufferLock) { - DEBUG_PRINTF_P(PSTR("ERROR: Locking JSON buffer (%d) failed! (still locked by %d)\n"), module, jsonBufferLock); + DEBUG_PRINTF_P(PSTR("ERROR: Locking JSON buffer (%d) failed! (still locked by %d)\n"), moduleID, jsonBufferLock); #ifdef ARDUINO_ARCH_ESP32 xSemaphoreGiveRecursive(jsonBufferLockMutex); #endif return false; } - jsonBufferLock = module ? module : 255; + jsonBufferLock = moduleID ? moduleID : 255; DEBUG_PRINTF_P(PSTR("JSON buffer locked. (%d)\n"), jsonBufferLock); pDoc->clear(); return true; @@ -265,16 +265,16 @@ uint8_t extractModeSlider(uint8_t mode, uint8_t slider, char *dest, uint8_t maxL if (mode < strip.getModeCount()) { String lineBuffer = FPSTR(strip.getModeData(mode)); if (lineBuffer.length() > 0) { - unsigned start = lineBuffer.indexOf('@'); - unsigned stop = lineBuffer.indexOf(';', start); + int start = lineBuffer.indexOf('@'); // String::indexOf() returns an int, not an unsigned; -1 means "not found" + int stop = lineBuffer.indexOf(';', start); if (start>0 && stop>0) { String names = lineBuffer.substring(start, stop); // include @ - unsigned nameBegin = 1, nameEnd, nameDefault; + int nameBegin = 1, nameEnd, nameDefault; if (slider < 10) { for (size_t i=0; i<=slider; i++) { const char *tmpstr; dest[0] = '\0'; //clear dest buffer - if (nameBegin == 0) break; // there are no more names + if (nameBegin <= 0) break; // there are no more names nameEnd = names.indexOf(',', nameBegin); if (i == slider) { nameDefault = names.indexOf('=', nameBegin); // find default value @@ -470,13 +470,13 @@ um_data_t* simulateSound(uint8_t simulationId) for (int i = 0; i<16; i++) fftResult[i] = beatsin8_t(120 / (i+1), 0, 255); // fftResult[i] = (beatsin8_t(120, 0, 255) + (256/16 * i)) % 256; - volumeSmth = fftResult[8]; + volumeSmth = fftResult[8]; break; case UMS_WeWillRockYou: if (ms%2000 < 200) { - volumeSmth = random8(255); + volumeSmth = hw_random8(); for (int i = 0; i<5; i++) - fftResult[i] = random8(255); + fftResult[i] = hw_random8(); } else if (ms%2000 < 400) { volumeSmth = 0; @@ -484,9 +484,9 @@ um_data_t* simulateSound(uint8_t simulationId) fftResult[i] = 0; } else if (ms%2000 < 600) { - volumeSmth = random8(255); + volumeSmth = hw_random8(); for (int i = 5; i<11; i++) - fftResult[i] = random8(255); + fftResult[i] = hw_random8(); } else if (ms%2000 < 800) { volumeSmth = 0; @@ -494,9 +494,9 @@ um_data_t* simulateSound(uint8_t simulationId) fftResult[i] = 0; } else if (ms%2000 < 1000) { - volumeSmth = random8(255); + volumeSmth = hw_random8(); for (int i = 11; i<16; i++) - fftResult[i] = random8(255); + fftResult[i] = hw_random8(); } else { volumeSmth = 0; @@ -506,17 +506,17 @@ um_data_t* simulateSound(uint8_t simulationId) break; case UMS_10_13: for (int i = 0; i<16; i++) - fftResult[i] = inoise8(beatsin8_t(90 / (i+1), 0, 200)*15 + (ms>>10), ms>>3); - volumeSmth = fftResult[8]; + fftResult[i] = perlin8(beatsin8_t(90 / (i+1), 0, 200)*15 + (ms>>10), ms>>3); + volumeSmth = fftResult[8]; break; case UMS_14_3: for (int i = 0; i<16; i++) - fftResult[i] = inoise8(beatsin8_t(120 / (i+1), 10, 30)*10 + (ms>>14), ms>>3); + fftResult[i] = perlin8(beatsin8_t(120 / (i+1), 10, 30)*10 + (ms>>14), ms>>3); volumeSmth = fftResult[8]; break; } - samplePeak = random8() > 250; + samplePeak = hw_random8() > 250; FFT_MajorPeak = 21 + (volumeSmth*volumeSmth) / 8.0f; // walk thru full range of 21hz...8200hz maxVol = 31; // this gets feedback fro UI binNum = 8; // this gets feedback fro UI @@ -530,6 +530,8 @@ um_data_t* simulateSound(uint8_t simulationId) static const char s_ledmap_tmpl[] PROGMEM = "ledmap%d.json"; // enumerate all ledmapX.json files on FS and extract ledmap names if existing void enumerateLedmaps() { + StaticJsonDocument<64> filter; + filter["n"] = true; ledMaps = 1; for (size_t i=1; ias(); if (!root["n"].isNull()) { @@ -556,7 +558,7 @@ void enumerateLedmaps() { const char *name = root["n"].as(); if (name != nullptr) len = strlen(name); if (len > 0 && len < 33) { - ledmapNames[i-1] = new char[len+1]; + ledmapNames[i-1] = static_cast(malloc(len+1)); if (ledmapNames[i-1]) strlcpy(ledmapNames[i-1], name, 33); } } @@ -564,7 +566,7 @@ void enumerateLedmaps() { char tmp[33]; snprintf_P(tmp, 32, s_ledmap_tmpl, i); len = strlen(tmp); - ledmapNames[i-1] = new char[len+1]; + ledmapNames[i-1] = static_cast(malloc(len+1)); if (ledmapNames[i-1]) strlcpy(ledmapNames[i-1], tmp, 33); } } @@ -582,7 +584,7 @@ void enumerateLedmaps() { uint8_t get_random_wheel_index(uint8_t pos) { uint8_t r = 0, x = 0, y = 0, d = 0; while (d < 42) { - r = random8(); + r = hw_random8(); x = abs(pos - r); y = 255 - x; d = MIN(x, y); @@ -594,3 +596,262 @@ uint8_t get_random_wheel_index(uint8_t pos) { float mapf(float x, float in_min, float in_max, float out_min, float out_max) { return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; } + +uint32_t hashInt(uint32_t s) { + // borrowed from https://stackoverflow.com/questions/664014/what-integer-hash-function-are-good-that-accepts-an-integer-hash-key + s = ((s >> 16) ^ s) * 0x45d9f3b; + s = ((s >> 16) ^ s) * 0x45d9f3b; + return (s >> 16) ^ s; +} + +// 32 bit random number generator, inlining uses more code, use hw_random16() if speed is critical (see fcn_declare.h) +uint32_t hw_random(uint32_t upperlimit) { + uint32_t rnd = hw_random(); + uint64_t scaled = uint64_t(rnd) * uint64_t(upperlimit); + return scaled >> 32; +} + +int32_t hw_random(int32_t lowerlimit, int32_t upperlimit) { + if(lowerlimit >= upperlimit) { + return lowerlimit; + } + uint32_t diff = upperlimit - lowerlimit; + return hw_random(diff) + lowerlimit; +} + +#ifndef ESP8266 +void *p_malloc(size_t size) { + int caps1 = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; + int caps2 = MALLOC_CAP_DEFAULT | MALLOC_CAP_8BIT; + if (psramSafe) { + if (heap_caps_get_free_size(caps2) > 3*MIN_HEAP_SIZE && size < 512) std::swap(caps1, caps2); // use DRAM for small alloactions & when heap is plenty + return heap_caps_malloc_prefer(size, 2, caps1, caps2); // otherwise prefer PSRAM if it exists + } + return heap_caps_malloc(size, caps2); +} + +void *p_realloc(void *ptr, size_t size) { + int caps1 = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; + int caps2 = MALLOC_CAP_DEFAULT | MALLOC_CAP_8BIT; + if (psramSafe) { + if (heap_caps_get_free_size(caps2) > 3*MIN_HEAP_SIZE && size < 512) std::swap(caps1, caps2); // use DRAM for small alloactions & when heap is plenty + return heap_caps_realloc_prefer(ptr, size, 2, caps1, caps2); // otherwise prefer PSRAM if it exists + } + return heap_caps_realloc(ptr, size, caps2); +} + +void *p_calloc(size_t count, size_t size) { + int caps1 = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; + int caps2 = MALLOC_CAP_DEFAULT | MALLOC_CAP_8BIT; + if (psramSafe) { + if (heap_caps_get_free_size(caps2) > 3*MIN_HEAP_SIZE && size < 512) std::swap(caps1, caps2); // use DRAM for small alloactions & when heap is plenty + return heap_caps_calloc_prefer(count, size, 2, caps1, caps2); // otherwise prefer PSRAM if it exists + } + return heap_caps_calloc(count, size, caps2); +} + +void *d_malloc(size_t size) { + int caps1 = MALLOC_CAP_DEFAULT | MALLOC_CAP_8BIT; + int caps2 = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; + if (psramSafe) { + if (size > MIN_HEAP_SIZE) std::swap(caps1, caps2); // prefer PSRAM for large alloactions + return heap_caps_malloc_prefer(size, 2, caps1, caps2); // otherwise prefer DRAM + } + return heap_caps_malloc(size, caps1); +} + +void *d_realloc(void *ptr, size_t size) { + int caps1 = MALLOC_CAP_DEFAULT | MALLOC_CAP_8BIT; + int caps2 = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; + if (psramSafe) { + if (size > MIN_HEAP_SIZE) std::swap(caps1, caps2); // prefer PSRAM for large alloactions + return heap_caps_realloc_prefer(ptr, size, 2, caps1, caps2); // otherwise prefer DRAM + } + return heap_caps_realloc(ptr, size, caps1); +} + +void *d_calloc(size_t count, size_t size) { + int caps1 = MALLOC_CAP_DEFAULT | MALLOC_CAP_8BIT; + int caps2 = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; + if (psramSafe) { + if (size > MIN_HEAP_SIZE) std::swap(caps1, caps2); // prefer PSRAM for large alloactions + return heap_caps_calloc_prefer(count, size, 2, caps1, caps2); // otherwise prefer DRAM + } + return heap_caps_calloc(count, size, caps1); +} +#endif + +/* + * Fixed point integer based Perlin noise functions by @dedehai + * Note: optimized for speed and to mimic fastled inoise functions, not for accuracy or best randomness + */ +#define PERLIN_SHIFT 1 + +// calculate gradient for corner from hash value +static inline __attribute__((always_inline)) int32_t hashToGradient(uint32_t h) { + // using more steps yields more "detailed" perlin noise but looks less like the original fastled version (adjust PERLIN_SHIFT to compensate, also changes range and needs proper adustment) + // return (h & 0xFF) - 128; // use PERLIN_SHIFT 7 + // return (h & 0x0F) - 8; // use PERLIN_SHIFT 3 + // return (h & 0x07) - 4; // use PERLIN_SHIFT 2 + return (h & 0x03) - 2; // use PERLIN_SHIFT 1 -> closest to original fastled version +} + +// Gradient functions for 1D, 2D and 3D Perlin noise note: forcing inline produces smaller code and makes it 3x faster! +static inline __attribute__((always_inline)) int32_t gradient1D(uint32_t x0, int32_t dx) { + uint32_t h = x0 * 0x27D4EB2D; + h ^= h >> 15; + h *= 0x92C3412B; + h ^= h >> 13; + h ^= h >> 7; + return (hashToGradient(h) * dx) >> PERLIN_SHIFT; +} + +static inline __attribute__((always_inline)) int32_t gradient2D(uint32_t x0, int32_t dx, uint32_t y0, int32_t dy) { + uint32_t h = (x0 * 0x27D4EB2D) ^ (y0 * 0xB5297A4D); + h ^= h >> 15; + h *= 0x92C3412B; + h ^= h >> 13; + return (hashToGradient(h) * dx + hashToGradient(h>>PERLIN_SHIFT) * dy) >> (1 + PERLIN_SHIFT); +} + +static inline __attribute__((always_inline)) int32_t gradient3D(uint32_t x0, int32_t dx, uint32_t y0, int32_t dy, uint32_t z0, int32_t dz) { + // fast and good entropy hash from corner coordinates + uint32_t h = (x0 * 0x27D4EB2D) ^ (y0 * 0xB5297A4D) ^ (z0 * 0x1B56C4E9); + h ^= h >> 15; + h *= 0x92C3412B; + h ^= h >> 13; + return ((hashToGradient(h) * dx + hashToGradient(h>>(1+PERLIN_SHIFT)) * dy + hashToGradient(h>>(1 + 2*PERLIN_SHIFT)) * dz) * 85) >> (8 + PERLIN_SHIFT); // scale to 16bit, x*85 >> 8 = x/3 +} + +// fast cubic smoothstep: t*(3 - 2t²), optimized for fixed point, scaled to avoid overflows +static uint32_t smoothstep(const uint32_t t) { + uint32_t t_squared = (t * t) >> 16; + uint32_t factor = (3 << 16) - ((t << 1)); + return (t_squared * factor) >> 18; // scale to avoid overflows and give best resolution +} + +// simple linear interpolation for fixed-point values, scaled for perlin noise use +static inline int32_t lerpPerlin(int32_t a, int32_t b, int32_t t) { + return a + (((b - a) * t) >> 14); // match scaling with smoothstep to yield 16.16bit values +} + +// 1D Perlin noise function that returns a value in range of -24691 to 24689 +int32_t perlin1D_raw(uint32_t x, bool is16bit) { + // integer and fractional part coordinates + int32_t x0 = x >> 16; + int32_t x1 = x0 + 1; + if(is16bit) x1 = x1 & 0xFF; // wrap back to zero at 0xFF instead of 0xFFFF + + int32_t dx0 = x & 0xFFFF; + int32_t dx1 = dx0 - 0x10000; + // gradient values for the two corners + int32_t g0 = gradient1D(x0, dx0); + int32_t g1 = gradient1D(x1, dx1); + // interpolate and smooth function + int32_t tx = smoothstep(dx0); + int32_t noise = lerpPerlin(g0, g1, tx); + return noise; +} + +// 2D Perlin noise function that returns a value in range of -20633 to 20629 +int32_t perlin2D_raw(uint32_t x, uint32_t y, bool is16bit) { + int32_t x0 = x >> 16; + int32_t y0 = y >> 16; + int32_t x1 = x0 + 1; + int32_t y1 = y0 + 1; + + if(is16bit) { + x1 = x1 & 0xFF; // wrap back to zero at 0xFF instead of 0xFFFF + y1 = y1 & 0xFF; + } + + int32_t dx0 = x & 0xFFFF; + int32_t dy0 = y & 0xFFFF; + int32_t dx1 = dx0 - 0x10000; + int32_t dy1 = dy0 - 0x10000; + + int32_t g00 = gradient2D(x0, dx0, y0, dy0); + int32_t g10 = gradient2D(x1, dx1, y0, dy0); + int32_t g01 = gradient2D(x0, dx0, y1, dy1); + int32_t g11 = gradient2D(x1, dx1, y1, dy1); + + uint32_t tx = smoothstep(dx0); + uint32_t ty = smoothstep(dy0); + + int32_t nx0 = lerpPerlin(g00, g10, tx); + int32_t nx1 = lerpPerlin(g01, g11, tx); + + int32_t noise = lerpPerlin(nx0, nx1, ty); + return noise; +} + +// 3D Perlin noise function that returns a value in range of -16788 to 16381 +int32_t perlin3D_raw(uint32_t x, uint32_t y, uint32_t z, bool is16bit) { + int32_t x0 = x >> 16; + int32_t y0 = y >> 16; + int32_t z0 = z >> 16; + int32_t x1 = x0 + 1; + int32_t y1 = y0 + 1; + int32_t z1 = z0 + 1; + + if(is16bit) { + x1 = x1 & 0xFF; // wrap back to zero at 0xFF instead of 0xFFFF + y1 = y1 & 0xFF; + z1 = z1 & 0xFF; + } + + int32_t dx0 = x & 0xFFFF; + int32_t dy0 = y & 0xFFFF; + int32_t dz0 = z & 0xFFFF; + int32_t dx1 = dx0 - 0x10000; + int32_t dy1 = dy0 - 0x10000; + int32_t dz1 = dz0 - 0x10000; + + int32_t g000 = gradient3D(x0, dx0, y0, dy0, z0, dz0); + int32_t g001 = gradient3D(x0, dx0, y0, dy0, z1, dz1); + int32_t g010 = gradient3D(x0, dx0, y1, dy1, z0, dz0); + int32_t g011 = gradient3D(x0, dx0, y1, dy1, z1, dz1); + int32_t g100 = gradient3D(x1, dx1, y0, dy0, z0, dz0); + int32_t g101 = gradient3D(x1, dx1, y0, dy0, z1, dz1); + int32_t g110 = gradient3D(x1, dx1, y1, dy1, z0, dz0); + int32_t g111 = gradient3D(x1, dx1, y1, dy1, z1, dz1); + + uint32_t tx = smoothstep(dx0); + uint32_t ty = smoothstep(dy0); + uint32_t tz = smoothstep(dz0); + + int32_t nx0 = lerpPerlin(g000, g100, tx); + int32_t nx1 = lerpPerlin(g010, g110, tx); + int32_t nx2 = lerpPerlin(g001, g101, tx); + int32_t nx3 = lerpPerlin(g011, g111, tx); + int32_t ny0 = lerpPerlin(nx0, nx1, ty); + int32_t ny1 = lerpPerlin(nx2, nx3, ty); + + int32_t noise = lerpPerlin(ny0, ny1, tz); + return noise; +} + +// scaling functions for fastled replacement +uint16_t perlin16(uint32_t x) { + return ((perlin1D_raw(x) * 1159) >> 10) + 32803; //scale to 16bit and offset (fastled range: about 4838 to 60766) +} + +uint16_t perlin16(uint32_t x, uint32_t y) { + return ((perlin2D_raw(x, y) * 1537) >> 10) + 32725; //scale to 16bit and offset (fastled range: about 1748 to 63697) +} + +uint16_t perlin16(uint32_t x, uint32_t y, uint32_t z) { + return ((perlin3D_raw(x, y, z) * 1731) >> 10) + 33147; //scale to 16bit and offset (fastled range: about 4766 to 60840) +} + +uint8_t perlin8(uint16_t x) { + return (((perlin1D_raw((uint32_t)x << 8, true) * 1353) >> 10) + 32769) >> 8; //scale to 16 bit, offset, then scale to 8bit +} + +uint8_t perlin8(uint16_t x, uint16_t y) { + return (((perlin2D_raw((uint32_t)x << 8, (uint32_t)y << 8, true) * 1620) >> 10) + 32771) >> 8; //scale to 16 bit, offset, then scale to 8bit +} + +uint8_t perlin8(uint16_t x, uint16_t y, uint16_t z) { + return (((perlin3D_raw((uint32_t)x << 8, (uint32_t)y << 8, (uint32_t)z << 8, true) * 2015) >> 10) + 33168) >> 8; //scale to 16 bit, offset, then scale to 8bit +} \ No newline at end of file diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 394da678..63af7c2b 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -65,7 +65,10 @@ void WLED::loop() handleNotifications(); handleTransitions(); #ifdef WLED_ENABLE_DMX - handleDMX(); + handleDMXOutput(); + #endif + #ifdef WLED_ENABLE_DMX_INPUT + dmxInput.update(); #endif #ifdef WLED_DEBUG @@ -84,6 +87,9 @@ void WLED::loop() #ifndef WLED_DISABLE_INFRARED handleIR(); #endif + #ifndef WLED_DISABLE_ESPNOW + handleRemote(); + #endif #ifndef WLED_DISABLE_ALEXA handleAlexa(); #endif @@ -103,7 +109,6 @@ void WLED::loop() if (WLED_CONNECTED && aOtaEnabled && !otaLock && correctPIN) ArduinoOTA.handle(); #endif handleNightlight(); - handlePlaylist(); yield(); #ifndef WLED_DISABLE_HUESYNC @@ -111,6 +116,10 @@ void WLED::loop() yield(); #endif + if (!presetNeedsSaving()) { + handlePlaylist(); + yield(); + } handlePresets(); yield(); @@ -179,57 +188,19 @@ void WLED::loop() DEBUG_PRINTLN(F("Re-init busses.")); bool aligned = strip.checkSegmentAlignment(); //see if old segments match old bus(ses) BusManager::removeAll(); - unsigned mem = 0; - // determine if it is sensible to use parallel I2S outputs on ESP32 (i.e. more than 5 outputs = 1 I2S + 4 RMT) - bool useParallel = false; - #if defined(ARDUINO_ARCH_ESP32) && !defined(ARDUINO_ARCH_ESP32S2) && !defined(ARDUINO_ARCH_ESP32S3) && !defined(ARDUINO_ARCH_ESP32C3) - unsigned digitalCount = 0; - unsigned maxLedsOnBus = 0; - unsigned maxChannels = 0; - for (unsigned i = 0; i < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; i++) { - if (busConfigs[i] == nullptr) break; - if (!Bus::isDigital(busConfigs[i]->type)) continue; - if (!Bus::is2Pin(busConfigs[i]->type)) { - digitalCount++; - unsigned channels = Bus::getNumberOfChannels(busConfigs[i]->type); - if (busConfigs[i]->count > maxLedsOnBus) maxLedsOnBus = busConfigs[i]->count; - if (channels > maxChannels) maxChannels = channels; - } - } - DEBUG_PRINTF_P(PSTR("Maximum LEDs on a bus: %u\nDigital buses: %u\n"), maxLedsOnBus, digitalCount); - // we may remove 300 LEDs per bus limit when NeoPixelBus is updated beyond 2.9.0 - if (maxLedsOnBus <= 300 && digitalCount > 5) { - DEBUG_PRINTF_P(PSTR("Switching to parallel I2S.")); - useParallel = true; - BusManager::useParallelOutput(); - mem = BusManager::memUsage(maxChannels, maxLedsOnBus, 8); // use alternate memory calculation (hse to be used *after* useParallelOutput()) - } - #endif - // create buses/outputs - for (unsigned i = 0; i < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; i++) { - if (busConfigs[i] == nullptr || (!useParallel && i > 10)) break; - if (useParallel && i < 8) { - // if for some unexplained reason the above pre-calculation was wrong, update - unsigned memT = BusManager::memUsage(*busConfigs[i]); // includes x8 memory allocation for parallel I2S - if (memT > mem) mem = memT; // if we have unequal LED count use the largest - } else - mem += BusManager::memUsage(*busConfigs[i]); // includes global buffer - if (mem <= MAX_LED_MEMORY) BusManager::add(*busConfigs[i]); - delete busConfigs[i]; - busConfigs[i] = nullptr; - } - strip.finalizeInit(); // also loads default ledmap if present + strip.finalizeInit(); // will create buses and also load default ledmap if present BusManager::setBrightness(bri); // fix re-initialised bus' brightness #4005 if (aligned) strip.makeAutoSegments(); else strip.fixInvalidSegments(); - doSerializeConfig = true; + BusManager::setBrightness(bri); // fix re-initialised bus' brightness + configNeedsWrite = true; } if (loadLedmap >= 0) { strip.deserializeMap(loadLedmap); loadLedmap = -1; } yield(); - if (doSerializeConfig) serializeConfig(); + if (configNeedsWrite) serializeConfigToFS(); yield(); handleWs(); @@ -252,7 +223,7 @@ void WLED::loop() } #endif - if (doReboot && (!doInitBusses || !doSerializeConfig)) // if busses have to be inited & saved, wait until next iteration + if (doReboot && (!doInitBusses || !configNeedsWrite)) // if busses have to be inited & saved, wait until next iteration reset(); // DEBUG serial logging (every 30s) @@ -296,6 +267,7 @@ void WLED::loop() DEBUG_PRINTF_P(PSTR("Strip time[ms]:%u/%lu\n"), avgStripMillis/loops, maxStripMillis); } strip.printSize(); + server.printStatus(DEBUGOUT); loops = 0; maxLoopMillis = 0; maxUsermodMillis = 0; @@ -370,7 +342,6 @@ void WLED::setup() #else DEBUG_PRINTLN(F("arduino-esp32 v1.0.x\n")); // we can't say in more detail. #endif - DEBUG_PRINTF_P(PSTR("CPU: %s rev.%d, %d core(s), %d MHz.\n"), ESP.getChipModel(), (int)ESP.getChipRevision(), ESP.getChipCores(), ESP.getCpuFreqMHz()); DEBUG_PRINTF_P(PSTR("FLASH: %d MB, Mode %d "), (ESP.getFlashChipSize()/1024)/1024, (int)ESP.getFlashChipMode()); #ifdef WLED_DEBUG @@ -423,9 +394,6 @@ void WLED::setup() PinManager::allocatePin(2, true, PinOwner::DMX); #endif - DEBUG_PRINTLN(F("Registering usermods ...")); - registerUsermods(); - DEBUG_PRINTF_P(PSTR("heap %u\n"), ESP.getFreeHeap()); bool fsinit = false; @@ -495,7 +463,7 @@ void WLED::setup() #endif // fill in unique mdns default - if (strcmp(cmDNS, "x") == 0) sprintf_P(cmDNS, PSTR("wled-%*s"), 6, escapedMac.c_str() + 6); + if (strcmp(cmDNS, DEFAULT_MDNS_NAME) == 0) sprintf_P(cmDNS, PSTR("wled-%*s"), 6, escapedMac.c_str() + 6); #ifndef WLED_DISABLE_MQTT if (mqttDeviceTopic[0] == 0) sprintf_P(mqttDeviceTopic, PSTR("wled/%*s"), 6, escapedMac.c_str() + 6); if (mqttClientID[0] == 0) sprintf_P(mqttClientID, PSTR("WLED-%*s"), 6, escapedMac.c_str() + 6); @@ -523,7 +491,10 @@ void WLED::setup() } #endif #ifdef WLED_ENABLE_DMX - initDMX(); + initDMXOutput(); +#endif +#ifdef WLED_ENABLE_DMX_INPUT + dmxInput.init(dmxInputReceivePin, dmxInputTransmitPin, dmxInputEnablePin, dmxInputPort); #endif #ifdef WLED_ENABLE_ADALIGHT @@ -543,14 +514,8 @@ void WLED::setup() #endif // Seed FastLED random functions with an esp random value, which already works properly at this point. -#if defined(ARDUINO_ARCH_ESP32) - const uint32_t seed32 = esp_random(); -#elif defined(ARDUINO_ARCH_ESP8266) - const uint32_t seed32 = RANDOM_REG32; -#else - const uint32_t seed32 = random(std::numeric_limits::max()); -#endif - random16_set_seed((uint16_t)((seed32 & 0xFFFF) ^ (seed32 >> 16))); + const uint32_t seed32 = hw_random(); + random16_set_seed((uint16_t)seed32); #if WLED_WATCHDOG_TIMEOUT > 0 enableWatchdog(); @@ -564,10 +529,12 @@ void WLED::setup() void WLED::beginStrip() { // Initialize NeoPixel Strip and button + strip.setTransition(0); // temporarily prevent transitions to reduce segment copies strip.finalizeInit(); // busses created during deserializeConfig() if config existed strip.makeAutoSegments(); strip.setBrightness(0); strip.setShowCallback(handleOverlayDraw); + doInitBusses = false; if (turnOnAtBoot) { if (briS > 0) bri = briS; @@ -575,20 +542,23 @@ void WLED::beginStrip() } else { // fix for #3196 if (bootPreset > 0) { - bool oldTransition = fadeTransition; // workaround if transitions are enabled - fadeTransition = false; // ignore transitions temporarily - strip.setColor(0, BLACK); // set all segments black - fadeTransition = oldTransition; // restore transitions - col[0] = col[1] = col[2] = col[3] = 0; // needed for colorUpdated() + // set all segments black (no transition) + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { + Segment &seg = strip.getSegment(i); + if (seg.isActive()) seg.colors[0] = BLACK; + } + colPri[0] = colPri[1] = colPri[2] = colPri[3] = 0; // needed for colorUpdated() } briLast = briS; bri = 0; strip.fill(BLACK); strip.show(); } + colorUpdated(CALL_MODE_INIT); // will not send notification but will initiate transition if (bootPreset > 0) { applyPreset(bootPreset, CALL_MODE_INIT); } - colorUpdated(CALL_MODE_INIT); // will not send notification + + strip.setTransition(transitionDelayDefault); // restore transitions // init relay pin if (rlyPin >= 0) { @@ -636,150 +606,9 @@ void WLED::initAP(bool resetAP) apActive = true; } -bool WLED::initEthernet() -{ -#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) - - static bool successfullyConfiguredEthernet = false; - - if (successfullyConfiguredEthernet) { - // DEBUG_PRINTLN(F("initE: ETH already successfully configured, ignoring")); - return false; - } - if (ethernetType == WLED_ETH_NONE) { - return false; - } - if (ethernetType >= WLED_NUM_ETH_TYPES) { - DEBUG_PRINTF_P(PSTR("initE: Ignoring attempt for invalid ethernetType (%d)\n"), ethernetType); - return false; - } - - DEBUG_PRINTF_P(PSTR("initE: Attempting ETH config: %d\n"), ethernetType); - - // Ethernet initialization should only succeed once -- else reboot required - ethernet_settings es = ethernetBoards[ethernetType]; - managed_pin_type pinsToAllocate[10] = { - // first six pins are non-configurable - esp32_nonconfigurable_ethernet_pins[0], - esp32_nonconfigurable_ethernet_pins[1], - esp32_nonconfigurable_ethernet_pins[2], - esp32_nonconfigurable_ethernet_pins[3], - esp32_nonconfigurable_ethernet_pins[4], - esp32_nonconfigurable_ethernet_pins[5], - { (int8_t)es.eth_mdc, true }, // [6] = MDC is output and mandatory - { (int8_t)es.eth_mdio, true }, // [7] = MDIO is bidirectional and mandatory - { (int8_t)es.eth_power, true }, // [8] = optional pin, not all boards use - { ((int8_t)0xFE), false }, // [9] = replaced with eth_clk_mode, mandatory - }; - // update the clock pin.... - if (es.eth_clk_mode == ETH_CLOCK_GPIO0_IN) { - pinsToAllocate[9].pin = 0; - pinsToAllocate[9].isOutput = false; - } else if (es.eth_clk_mode == ETH_CLOCK_GPIO0_OUT) { - pinsToAllocate[9].pin = 0; - pinsToAllocate[9].isOutput = true; - } else if (es.eth_clk_mode == ETH_CLOCK_GPIO16_OUT) { - pinsToAllocate[9].pin = 16; - pinsToAllocate[9].isOutput = true; - } else if (es.eth_clk_mode == ETH_CLOCK_GPIO17_OUT) { - pinsToAllocate[9].pin = 17; - pinsToAllocate[9].isOutput = true; - } else { - DEBUG_PRINTF_P(PSTR("initE: Failing due to invalid eth_clk_mode (%d)\n"), es.eth_clk_mode); - return false; - } - - if (!PinManager::allocateMultiplePins(pinsToAllocate, 10, PinOwner::Ethernet)) { - DEBUG_PRINTLN(F("initE: Failed to allocate ethernet pins")); - return false; - } - - /* - For LAN8720 the most correct way is to perform clean reset each time before init - applying LOW to power or nRST pin for at least 100 us (please refer to datasheet, page 59) - ESP_IDF > V4 implements it (150 us, lan87xx_reset_hw(esp_eth_phy_t *phy) function in - /components/esp_eth/src/esp_eth_phy_lan87xx.c, line 280) - but ESP_IDF < V4 does not. Lets do it: - [not always needed, might be relevant in some EMI situations at startup and for hot resets] - */ - #if ESP_IDF_VERSION_MAJOR==3 - if(es.eth_power>0 && es.eth_type==ETH_PHY_LAN8720) { - pinMode(es.eth_power, OUTPUT); - digitalWrite(es.eth_power, 0); - delayMicroseconds(150); - digitalWrite(es.eth_power, 1); - delayMicroseconds(10); - } - #endif - - if (!ETH.begin( - (uint8_t) es.eth_address, - (int) es.eth_power, - (int) es.eth_mdc, - (int) es.eth_mdio, - (eth_phy_type_t) es.eth_type, - (eth_clock_mode_t) es.eth_clk_mode - )) { - DEBUG_PRINTLN(F("initC: ETH.begin() failed")); - // de-allocate the allocated pins - for (managed_pin_type mpt : pinsToAllocate) { - PinManager::deallocatePin(mpt.pin, PinOwner::Ethernet); - } - return false; - } - - successfullyConfiguredEthernet = true; - DEBUG_PRINTLN(F("initC: *** Ethernet successfully configured! ***")); - return true; -#else - return false; // Ethernet not enabled for build -#endif -} - -// performs asynchronous scan for available networks (which may take couple of seconds to finish) -// returns configured WiFi ID with the strongest signal (or default if no configured networks available) -int8_t WLED::findWiFi(bool doScan) { - if (multiWiFi.size() <= 1) { - DEBUG_PRINTLN(F("Defaulf WiFi used.")); - return 0; - } - - if (doScan) WiFi.scanDelete(); // restart scan - - int status = WiFi.scanComplete(); // complete scan may take as much as several seconds (usually <3s with not very crowded air) - - if (status == WIFI_SCAN_FAILED) { - DEBUG_PRINTLN(F("WiFi scan started.")); - WiFi.scanNetworks(true); // start scanning in asynchronous mode - } else if (status >= 0) { // status contains number of found networks - DEBUG_PRINT(F("WiFi scan completed: ")); DEBUG_PRINTLN(status); - int rssi = -9999; - unsigned selected = selectedWiFi; - for (int o = 0; o < status; o++) { - DEBUG_PRINT(F(" WiFi available: ")); DEBUG_PRINT(WiFi.SSID(o)); - DEBUG_PRINT(F(" RSSI: ")); DEBUG_PRINT(WiFi.RSSI(o)); DEBUG_PRINTLN(F("dB")); - for (unsigned n = 0; n < multiWiFi.size(); n++) - if (!strcmp(WiFi.SSID(o).c_str(), multiWiFi[n].clientSSID)) { - // find the WiFi with the strongest signal (but keep priority of entry if signal difference is not big) - if ((n < selected && WiFi.RSSI(o) > rssi-10) || WiFi.RSSI(o) > rssi) { - rssi = WiFi.RSSI(o); - selected = n; - } - break; - } - } - DEBUG_PRINT(F("Selected: ")); DEBUG_PRINT(multiWiFi[selected].clientSSID); - DEBUG_PRINT(F(" RSSI: ")); DEBUG_PRINT(rssi); DEBUG_PRINTLN(F("dB")); - return selected; - } - //DEBUG_PRINT(F("WiFi scan running.")); - return status; // scan is still running or there was an error -} - void WLED::initConnection() { DEBUG_PRINTF_P(PSTR("initConnection() called @ %lus.\n"), millis()/1000); - #ifdef WLED_ENABLE_WEBSOCKETS ws.onEvent(wsEvent); #endif @@ -793,6 +622,7 @@ void WLED::initConnection() #endif WiFi.disconnect(true); // close old connections + delay(5); // wait for hardware to be ready #ifdef ESP8266 WiFi.setPhyMode(force802_3g ? WIFI_PHY_MODE_11G : WIFI_PHY_MODE_11N); #endif @@ -808,6 +638,7 @@ void WLED::initConnection() if (!WLED_WIFI_CONFIGURED) { DEBUG_PRINTLN(F("No connection configured.")); if (!apActive) initAP(); // instantly go to ap mode + return; } else if (!apActive) { if (apBehavior == AP_BEHAVIOR_ALWAYS) { DEBUG_PRINTLN(F("Access point ALWAYS enabled.")); @@ -910,9 +741,6 @@ void WLED::initInterfaces() e131.begin(e131Multicast, e131Port, e131Universe, E131_MAX_UNIVERSE_COUNT); ddp.begin(false, DDP_DEFAULT_PORT); reconnectHue(); -#ifndef WLED_DISABLE_MQTT - initMqtt(); -#endif interfacesInited = true; wasConnected = true; } @@ -922,7 +750,9 @@ void WLED::handleConnection() static bool scanDone = true; static byte stacO = 0; const unsigned long now = millis(); + #ifdef WLED_DEBUG const unsigned long nowS = now/1000; + #endif const bool wifiConfigured = WLED_WIFI_CONFIGURED; // ignore connection handling if WiFi is configured and scan still running diff --git a/wled00/wled.h b/wled00/wled.h index 7c3ec82f..e4cab539 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -3,7 +3,6 @@ /* Main sketch, global variable declarations @title WLED project sketch - @version 0.15.0-dev @author Christian Schwinne */ @@ -16,7 +15,7 @@ // ESP8266-01 (blue) got too little storage space to work with WLED. 0.10.2 is the last release supporting this unit. // ESP8266-01 (black) has 1MB flash and can thus fit the whole program, although OTA update is not possible. Use 1M(128K SPIFFS). -// 2-step OTA may still be possible: https://github.com/Aircoookie/WLED/issues/2040#issuecomment-981111096 +// 2-step OTA may still be possible: https://github.com/wled-dev/WLED/issues/2040#issuecomment-981111096 // Uncomment some of the following lines to disable features: // Alternatively, with platformio pass your chosen flags to your custom build target in platformio_override.ini @@ -65,6 +64,9 @@ //This is generally a terrible idea, but improves boot success on boards with a 3.3v regulator + cap setup that can't provide 400mA peaks //#define WLED_DISABLE_BROWNOUT_DET +#include +#include + // Library inclusions. #include #ifdef ESP8266 @@ -145,6 +147,10 @@ #endif #endif +#ifdef WLED_ENABLE_DMX_INPUT + #include "dmx_input.h" +#endif + #include "src/dependencies/e131/ESPAsyncE131.h" #ifndef WLED_DISABLE_MQTT #include "src/dependencies/async-mqtt-client/AsyncMqttClient.h" @@ -214,6 +220,10 @@ using PSRAMDynamicJsonDocument = BasicJsonDocument; #define WLED_AP_PASS DEFAULT_AP_PASS #endif +#ifndef WLED_PIN + #define WLED_PIN "" +#endif + #ifndef SPIFFS_EDITOR_AIRCOOOKIE #error You are not using the Aircoookie fork of the ESPAsyncWebserver library.\ Using upstream puts your WiFi password at risk of being served by the filesystem.\ @@ -270,11 +280,15 @@ using PSRAMDynamicJsonDocument = BasicJsonDocument; // Global Variable definitions WLED_GLOBAL char versionString[] _INIT(TOSTRING(WLED_VERSION)); WLED_GLOBAL char releaseString[] _INIT(WLED_RELEASE_NAME); // must include the quotes when defining, e.g -D WLED_RELEASE_NAME=\"ESP32_MULTI_USREMODS\" -#define WLED_CODENAME "Kōsen" +#define WLED_CODENAME "Niji" // AP and OTA default passwords (for maximum security change them!) WLED_GLOBAL char apPass[65] _INIT(WLED_AP_PASS); +#ifdef WLED_OTA_PASS +WLED_GLOBAL char otaPass[33] _INIT(WLED_OTA_PASS); +#else WLED_GLOBAL char otaPass[33] _INIT(DEFAULT_OTA_PASS); +#endif // Hardware and pin config #ifndef BTNPIN @@ -356,7 +370,7 @@ WLED_GLOBAL wifi_options_t wifiOpt _INIT_N(({0, 1, false, AP_BEHAVIOR_BOOT_NO_CO #define noWifiSleep wifiOpt.noWifiSleep #define force802_3g wifiOpt.force802_3g #else -WLED_GLOBAL uint8_t selectedWiFi _INIT(0); +WLED_GLOBAL int8_t selectedWiFi _INIT(0); WLED_GLOBAL byte apChannel _INIT(1); // 2.4GHz WiFi AP channel (1-13) WLED_GLOBAL byte apHide _INIT(0); // hidden AP SSID WLED_GLOBAL byte apBehavior _INIT(AP_BEHAVIOR_BOOT_NO_CONN); // access point opens when no connection after boot by default @@ -368,15 +382,15 @@ WLED_GLOBAL bool noWifiSleep _INIT(false); WLED_GLOBAL bool force802_3g _INIT(false); #endif // WLED_SAVE_RAM #ifdef ARDUINO_ARCH_ESP32 - #if defined(LOLIN_WIFI_FIX) && (defined(ARDUINO_ARCH_ESP32C3) || defined(ARDUINO_ARCH_ESP32S2) || defined(ARDUINO_ARCH_ESP32S3)) + #if defined(LOLIN_WIFI_FIX) && (defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3)) WLED_GLOBAL uint8_t txPower _INIT(WIFI_POWER_8_5dBm); #else WLED_GLOBAL uint8_t txPower _INIT(WIFI_POWER_19_5dBm); #endif #endif -#define WLED_WIFI_CONFIGURED (strlen(multiWiFi[0].clientSSID) >= 1 && strcmp(multiWiFi[0].clientSSID, DEFAULT_CLIENT_SSID) != 0) +#define WLED_WIFI_CONFIGURED isWiFiConfigured() -#ifdef WLED_USE_ETHERNET +#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) #ifdef WLED_ETH_DEFAULT // default ethernet board type if specified WLED_GLOBAL int ethernetType _INIT(WLED_ETH_DEFAULT); // ethernet board type #else @@ -395,6 +409,9 @@ WLED_GLOBAL byte bootPreset _INIT(0); // save preset to load WLED_GLOBAL bool useGlobalLedBuffer _INIT(false); // double buffering disabled on ESP8266 #else WLED_GLOBAL bool useGlobalLedBuffer _INIT(true); // double buffering enabled on ESP32 + #ifndef CONFIG_IDF_TARGET_ESP32C3 +WLED_GLOBAL bool useParallelI2S _INIT(false); // parallel I2S for ESP32 + #endif #endif #ifdef WLED_USE_IC_CCT WLED_GLOBAL bool cctICused _INIT(true); // CCT IC used (Athom 15W bulbs) @@ -403,9 +420,9 @@ WLED_GLOBAL bool cctICused _INIT(false); // CCT IC used (Athom 15W bulb #endif WLED_GLOBAL bool gammaCorrectCol _INIT(true); // use gamma correction on colors WLED_GLOBAL bool gammaCorrectBri _INIT(false); // use gamma correction on brightness -WLED_GLOBAL float gammaCorrectVal _INIT(2.8f); // gamma correction value +WLED_GLOBAL float gammaCorrectVal _INIT(2.2f); // gamma correction value -WLED_GLOBAL byte col[] _INIT_N(({ 255, 160, 0, 0 })); // current RGB(W) primary color. col[] should be updated if you want to change the color. +WLED_GLOBAL byte colPri[] _INIT_N(({ 255, 160, 0, 0 })); // current RGB(W) primary color. colPri[] should be updated if you want to change the color. WLED_GLOBAL byte colSec[] _INIT_N(({ 0, 0, 0, 0 })); // current RGB(W) secondary color WLED_GLOBAL byte nightlightTargetBri _INIT(0); // brightness after nightlight is over @@ -460,7 +477,15 @@ WLED_GLOBAL bool arlsForceMaxBri _INIT(false); // enable to f WLED_GLOBAL uint16_t DMXStart _INIT(10); // start address of the first fixture WLED_GLOBAL uint16_t DMXStartLED _INIT(0); // LED from which DMX fixtures start #endif -WLED_GLOBAL uint16_t e131Universe _INIT(1); // settings for E1.31 (sACN) protocol (only DMX_MODE_MULTIPLE_* can span over consecutive universes) +#ifdef WLED_ENABLE_DMX_INPUT + WLED_GLOBAL int dmxInputTransmitPin _INIT(0); + WLED_GLOBAL int dmxInputReceivePin _INIT(0); + WLED_GLOBAL int dmxInputEnablePin _INIT(0); + WLED_GLOBAL int dmxInputPort _INIT(2); + WLED_GLOBAL DMXInput dmxInput; +#endif + +WLED_GLOBAL uint16_t e131Universe _INIT(1); // settings for E1.31 (sACN) protocol (only DMX_MODE_MULTIPLE_* can span over consequtive universes) WLED_GLOBAL uint16_t e131Port _INIT(5568); // DMX in port. E1.31 default is 5568, Art-Net is 6454 WLED_GLOBAL byte e131Priority _INIT(0); // E1.31 port priority (if != 0 priority handling is active) WLED_GLOBAL E131Priority highPriority _INIT(3); // E1.31 highest priority tracking, init = timeout in seconds @@ -483,10 +508,10 @@ WLED_GLOBAL unsigned long lastMqttReconnectAttempt _INIT(0); // used for other #endif WLED_GLOBAL AsyncMqttClient *mqtt _INIT(NULL); WLED_GLOBAL bool mqttEnabled _INIT(false); -WLED_GLOBAL char mqttStatusTopic[40] _INIT(""); // this must be global because of async handlers -WLED_GLOBAL char mqttDeviceTopic[MQTT_MAX_TOPIC_LEN+1] _INIT(""); // main MQTT topic (individual per device, default is wled/mac) -WLED_GLOBAL char mqttGroupTopic[MQTT_MAX_TOPIC_LEN+1] _INIT("wled/all"); // second MQTT topic (for example to group devices) -WLED_GLOBAL char mqttServer[MQTT_MAX_SERVER_LEN+1] _INIT(""); // both domains and IPs should work (no SSL) +WLED_GLOBAL char mqttStatusTopic[MQTT_MAX_TOPIC_LEN + 8] _INIT(""); // this must be global because of async handlers +WLED_GLOBAL char mqttDeviceTopic[MQTT_MAX_TOPIC_LEN + 1] _INIT(""); // main MQTT topic (individual per device, default is wled/mac) +WLED_GLOBAL char mqttGroupTopic[MQTT_MAX_TOPIC_LEN + 1] _INIT("wled/all"); // second MQTT topic (for example to group devices) +WLED_GLOBAL char mqttServer[MQTT_MAX_SERVER_LEN + 1] _INIT(""); // both domains and IPs should work (no SSL) WLED_GLOBAL char mqttUser[41] _INIT(""); // optional: username for MQTT auth WLED_GLOBAL char mqttPass[65] _INIT(""); // optional: password for MQTT auth WLED_GLOBAL char mqttClientID[41] _INIT(""); // override the client ID @@ -516,7 +541,8 @@ WLED_GLOBAL bool serialCanTX _INIT(false); WLED_GLOBAL bool enableESPNow _INIT(false); // global on/off for ESP-NOW WLED_GLOBAL byte statusESPNow _INIT(ESP_NOW_STATE_UNINIT); // state of ESP-NOW stack (0 uninitialised, 1 initialised, 2 error) WLED_GLOBAL bool useESPNowSync _INIT(false); // use ESP-NOW wireless technology for sync -WLED_GLOBAL char linked_remote[13] _INIT(""); // MAC of ESP-NOW remote (Wiz Mote) +//WLED_GLOBAL char linked_remote[13] _INIT(""); // MAC of ESP-NOW remote (Wiz Mote) +WLED_GLOBAL std::vector> linked_remotes; // MAC of ESP-NOW remotes (Wiz Mote) WLED_GLOBAL char last_signal_src[13] _INIT(""); // last seen ESP-NOW sender #endif @@ -536,7 +562,7 @@ WLED_GLOBAL byte currentTimezone _INIT(WLED_TIMEZONE); // Timezone ID. Refer WLED_GLOBAL int utcOffsetSecs _INIT(WLED_UTC_OFFSET); // Seconds to offset from UTC before timzone calculation WLED_GLOBAL byte overlayCurrent _INIT(0); // 0: no overlay 1: analog clock 2: was single-digit clock 3: was cronixie -WLED_GLOBAL byte overlayMin _INIT(0), overlayMax _INIT(DEFAULT_LED_COUNT - 1); // boundaries of overlay mode +WLED_GLOBAL uint16_t overlayMin _INIT(0), overlayMax _INIT(DEFAULT_LED_COUNT - 1); // boundaries of overlay mode WLED_GLOBAL byte analogClock12pixel _INIT(0); // The pixel in your strip where "midnight" would be WLED_GLOBAL bool analogClockSecondsTrail _INIT(false); // Display seconds as trail of LEDs instead of a single pixel @@ -556,11 +582,19 @@ WLED_GLOBAL byte macroLongPress[WLED_MAX_BUTTONS] _INIT({0}); WLED_GLOBAL byte macroDoublePress[WLED_MAX_BUTTONS] _INIT({0}); // Security CONFIG -WLED_GLOBAL bool otaLock _INIT(false); // prevents OTA firmware updates without password. ALWAYS enable if system exposed to any public networks -WLED_GLOBAL bool wifiLock _INIT(false); // prevents access to WiFi settings when OTA lock is enabled -WLED_GLOBAL bool aOtaEnabled _INIT(true); // ArduinoOTA allows easy updates directly from the IDE. Careful, it does not auto-disable when OTA lock is on -WLED_GLOBAL char settingsPIN[5] _INIT(""); // PIN for settings pages -WLED_GLOBAL bool correctPIN _INIT(true); +#ifdef WLED_OTA_PASS +WLED_GLOBAL bool otaLock _INIT(true); // prevents OTA firmware updates without password. ALWAYS enable if system exposed to any public networks +#else +WLED_GLOBAL bool otaLock _INIT(false); // prevents OTA firmware updates without password. ALWAYS enable if system exposed to any public networks +#endif +WLED_GLOBAL bool wifiLock _INIT(false); // prevents access to WiFi settings when OTA lock is enabled +#ifndef WLED_DISABLE_OTA +WLED_GLOBAL bool aOtaEnabled _INIT(true); // ArduinoOTA allows easy updates directly from the IDE. Careful, it does not auto-disable when OTA lock is on +#else +WLED_GLOBAL bool aOtaEnabled _INIT(false); // ArduinoOTA allows easy updates directly from the IDE. Careful, it does not auto-disable when OTA lock is on +#endif +WLED_GLOBAL char settingsPIN[5] _INIT(WLED_PIN); // PIN for settings pages +WLED_GLOBAL bool correctPIN _INIT(!strlen(settingsPIN)); WLED_GLOBAL unsigned long lastEditTime _INIT(0); WLED_GLOBAL uint16_t userVar0 _INIT(0), userVar1 _INIT(0); //available for use in usermod @@ -568,6 +602,7 @@ WLED_GLOBAL uint16_t userVar0 _INIT(0), userVar1 _INIT(0); //available for use i // internal global variable declarations // wifi WLED_GLOBAL bool apActive _INIT(false); +WLED_GLOBAL byte apClients _INIT(0); WLED_GLOBAL bool forceReconnect _INIT(false); WLED_GLOBAL unsigned long lastReconnectAttempt _INIT(0); WLED_GLOBAL bool interfacesInited _INIT(false); @@ -575,18 +610,19 @@ WLED_GLOBAL bool wasConnected _INIT(false); // color WLED_GLOBAL byte lastRandomIndex _INIT(0); // used to save last random color so the new one is not the same +WLED_GLOBAL std::vector customPalettes; // custom palettes +WLED_GLOBAL uint8_t paletteBlend _INIT(0); // determines blending and wrapping of palette: 0: blend, wrap if moving (SEGMENT.speed>0); 1: blend, always wrap; 2: blend, never wrap; 3: don't blend or wrap // transitions -WLED_GLOBAL bool fadeTransition _INIT(true); // enable crossfading brightness/color -WLED_GLOBAL bool modeBlending _INIT(true); // enable effect blending +WLED_GLOBAL uint8_t blendingStyle _INIT(0); // effect blending/transitionig style WLED_GLOBAL bool transitionActive _INIT(false); WLED_GLOBAL uint16_t transitionDelay _INIT(750); // global transition duration WLED_GLOBAL uint16_t transitionDelayDefault _INIT(750); // default transition time (stored in cfg.json) WLED_GLOBAL unsigned long transitionStartTime; -WLED_GLOBAL float tperLast _INIT(0.0f); // crossfade transition progress, 0.0f - 1.0f WLED_GLOBAL bool jsonTransitionOnce _INIT(false); // flag to override transitionDelay (playlist, JSON API: "live" & "seg":{"i"} & "tt") WLED_GLOBAL uint8_t randomPaletteChangeTime _INIT(5); // amount of time [s] between random palette changes (min: 1s, max: 255s) WLED_GLOBAL bool useHarmonicRandomPalette _INIT(true); // use *harmonic* random palette generation (nicer looking) or truly random +WLED_GLOBAL bool useRainbowWheel _INIT(false); // use "rainbow" color wheel instead of "spectrum" color wheel // nightlight WLED_GLOBAL bool nightlightActive _INIT(false); @@ -852,7 +888,7 @@ WLED_GLOBAL byte errorFlag _INIT(0); WLED_GLOBAL String messageHead, messageSub; WLED_GLOBAL byte optionType; -WLED_GLOBAL bool doSerializeConfig _INIT(false); // flag to initiate saving of config +WLED_GLOBAL bool configNeedsWrite _INIT(false); // flag to initiate saving of config WLED_GLOBAL bool doReboot _INIT(false); // flag to initiate reboot from async handlers WLED_GLOBAL bool psramSafe _INIT(true); // is it safe to use PSRAM (on ESP32 rev.1; compiler fix used "-mfix-esp32-psram-cache-issue") @@ -865,7 +901,7 @@ WLED_GLOBAL bool ledStatusState _INIT(false); // the current LED state #endif // server library objects -WLED_GLOBAL AsyncWebServer server _INIT_N(((80))); +WLED_GLOBAL AsyncWebServer server _INIT_N(((80, {0, WLED_REQUEST_MAX_QUEUE, WLED_REQUEST_MIN_HEAP, WLED_REQUEST_HEAP_USAGE}))); #ifdef WLED_ENABLE_WEBSOCKETS WLED_GLOBAL AsyncWebSocket ws _INIT_N((("/ws"))); #endif @@ -882,12 +918,11 @@ WLED_GLOBAL ESPAsyncE131 ddp _INIT_N(((handleE131Packet))); WLED_GLOBAL bool e131NewData _INIT(false); // led fx library object -WLED_GLOBAL BusManager busses _INIT(BusManager()); -WLED_GLOBAL WS2812FX strip _INIT(WS2812FX()); -WLED_GLOBAL BusConfig* busConfigs[WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES] _INIT({nullptr}); //temporary, to remember values from network callback until after -WLED_GLOBAL bool doInitBusses _INIT(false); -WLED_GLOBAL int8_t loadLedmap _INIT(-1); -WLED_GLOBAL uint8_t currentLedmap _INIT(0); +WLED_GLOBAL WS2812FX strip _INIT(WS2812FX()); +WLED_GLOBAL std::vector busConfigs; //temporary, to remember values from network callback until after +WLED_GLOBAL bool doInitBusses _INIT(false); +WLED_GLOBAL int8_t loadLedmap _INIT(-1); +WLED_GLOBAL uint8_t currentLedmap _INIT(0); #ifndef ESP8266 WLED_GLOBAL char *ledmapNames[WLED_MAX_LEDMAPS-1] _INIT_N(({nullptr})); #endif @@ -897,9 +932,6 @@ WLED_GLOBAL uint32_t ledMaps _INIT(0); // bitfield representation of available l WLED_GLOBAL uint16_t ledMaps _INIT(0); // bitfield representation of available ledmaps #endif -// Usermod manager -WLED_GLOBAL UsermodManager usermods _INIT(UsermodManager()); - // global I2C SDA pin (used for usermods) #ifndef I2CSDAPIN WLED_GLOBAL int8_t i2c_sda _INIT(-1); @@ -1040,11 +1072,9 @@ public: void beginStrip(); void handleConnection(); - bool initEthernet(); // result is informational void initAP(bool resetAP = false); void initConnection(); void initInterfaces(); - int8_t findWiFi(bool doScan = false); #if defined(STATUSLED) void handleStatusLED(); #endif diff --git a/wled00/wled_eeprom.cpp b/wled00/wled_eeprom.cpp old mode 100755 new mode 100644 index 57d82f7e..e1309c94 --- a/wled00/wled_eeprom.cpp +++ b/wled00/wled_eeprom.cpp @@ -2,12 +2,16 @@ #include #include "wled.h" +#if defined(WLED_ENABLE_MQTT) && MQTT_MAX_TOPIC_LEN < 32 +#error "MQTT topics length < 32 is not supported by the EEPROM module!" +#endif + /* * DEPRECATED, do not use for new settings * Only used to restore config from pre-0.11 installations using the deEEP() methods * * Methods to handle saving and loading to non-volatile memory - * EEPROM Map: https://github.com/Aircoookie/WLED/wiki/EEPROM-Map + * EEPROM Map: https://github.com/wled-dev/WLED/wiki/EEPROM-Map */ //eeprom Version code, enables default settings instead of 0 init on update @@ -220,8 +224,8 @@ void loadSettingsFromEEPROM() if (lastEEPROMversion > 7) { - strip.paletteFade = EEPROM.read(374); - strip.paletteBlend = EEPROM.read(382); + //strip.paletteFade = EEPROM.read(374); + paletteBlend = EEPROM.read(382); for (int i = 0; i < 8; ++i) { diff --git a/wled00/wled_math.cpp b/wled00/wled_math.cpp index a8ec5540..43c59308 100644 --- a/wled00/wled_math.cpp +++ b/wled00/wled_math.cpp @@ -220,3 +220,27 @@ float fmod_t(float num, float denom) { #endif return res; } + +// bit-wise integer square root calculation (exact) +uint32_t sqrt32_bw(uint32_t x) { + uint32_t res = 0; + uint32_t bit; + uint32_t num = x; // use 32bit for faster calculation + + if(num < 1 << 10) bit = 1 << 10; // speed optimization for small numbers < 32^2 + else if (num < 1 << 20) bit = 1 << 20; // speed optimization for medium numbers < 1024^2 + else bit = 1 << 30; // start with highest power of 4 <= 2^32 + + while (bit > num) bit >>= 2; // reduce iterations + + while (bit != 0) { + if (num >= res + bit) { + num -= res + bit; + res = (res >> 1) + bit; + } else { + res >>= 1; + } + bit >>= 2; + } + return res; +} diff --git a/wled00/wled_serial.cpp b/wled00/wled_serial.cpp index ad9bb141..a0e59c53 100644 --- a/wled00/wled_serial.cpp +++ b/wled00/wled_serial.cpp @@ -113,8 +113,8 @@ void handleSerial() //only send response if TX pin is unused for other purposes if (verboseResponse && serialCanTX) { pDoc->clear(); - JsonObject state = pDoc->createNestedObject("state"); - serializeState(state); + JsonObject stateDoc = pDoc->createNestedObject("state"); + serializeState(stateDoc); JsonObject info = pDoc->createNestedObject("info"); serializeInfo(info); diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index e8cbb41a..c7a4cc00 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -21,7 +21,7 @@ static const char s_accessdenied[] PROGMEM = "Access Denied"; static const char _common_js[] PROGMEM = "/common.js"; //Is this an IP? -static bool isIp(String str) { +static bool isIp(const String &str) { for (size_t i = 0; i < str.length(); i++) { int c = str.charAt(i); if (c != '.' && (c < '0' || c > '9')) { @@ -152,9 +152,9 @@ static String msgProcessor(const String& var) return String(); } -static void handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) { +static void handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool isFinal) { if (!correctPIN) { - if (final) request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_unlock_cfg)); + if (isFinal) request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_unlock_cfg)); return; } if (!index) { @@ -170,13 +170,13 @@ static void handleUpload(AsyncWebServerRequest *request, const String& filename, if (len) { request->_tempFile.write(data,len); } - if (final) { + if (isFinal) { request->_tempFile.close(); if (filename.indexOf(F("cfg.json")) >= 0) { // check for filename with or without slash doReboot = true; request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("Configuration restore successful.\nRebooting...")); } else { - if (filename.indexOf(F("palette")) >= 0 && filename.indexOf(F(".json")) >= 0) strip.loadCustomPalettes(); + if (filename.indexOf(F("palette")) >= 0 && filename.indexOf(F(".json")) >= 0) loadCustomPalettes(); request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("File Uploaded!")); } cacheInvalidate++; @@ -288,7 +288,7 @@ void initServer() bool isConfig = false; if (!requestJSONBufferLock(14)) { - serveJsonError(request, 503, ERR_NOBUF); + request->deferResponse(); return; } @@ -328,7 +328,7 @@ void initServer() interfaceUpdateCallMode = CALL_MODE_WS_SEND; // schedule WS update serveJson(request); return; //if JSON contains "v" } else { - doSerializeConfig = true; //serializeConfig(); //Save new settings to FS + configNeedsWrite = true; //Save new settings to FS } } request->send(200, CONTENT_TYPE_JSON, F("{\"success\":true}")); @@ -359,13 +359,12 @@ void initServer() server.on(F("/upload"), HTTP_POST, [](AsyncWebServerRequest *request) {}, [](AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, - size_t len, bool final) {handleUpload(request, filename, index, data, len, final);} + size_t len, bool isFinal) {handleUpload(request, filename, index, data, len, isFinal);} ); createEditHandler(correctPIN); static const char _update[] PROGMEM = "/update"; -#ifndef WLED_DISABLE_OTA //init ota page server.on(_update, HTTP_GET, [](AsyncWebServerRequest *request){ if (otaLock) { @@ -389,7 +388,7 @@ void initServer() serveMessage(request, 200, F("Update successful!"), F("Rebooting..."), 131); doReboot = true; } - },[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final){ + },[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){ if (!correctPIN || otaLock) return; if(!index){ DEBUG_PRINTLN(F("OTA Update Start")); @@ -406,7 +405,7 @@ void initServer() Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000); } if(!Update.hasError()) Update.write(data, len); - if(final){ + if(isFinal){ if(Update.end(true)){ DEBUG_PRINTLN(F("Update Success")); } else { @@ -419,12 +418,6 @@ void initServer() } } }); -#else - server.on(_update, HTTP_GET, [](AsyncWebServerRequest *request){ - serveMessage(request, 501, FPSTR(s_notimplemented), F("OTA updating is disabled in this build."), 254); - }); -#endif - #ifdef WLED_ENABLE_DMX server.on(F("/dmxmap"), HTTP_GET, [](AsyncWebServerRequest *request){ @@ -567,13 +560,14 @@ void serveSettings(AsyncWebServerRequest* request, bool post) { //else if (url.indexOf("/edit") >= 0) subPage = 10; else subPage = SUBPAGE_WELCOME; - if (!correctPIN && strlen(settingsPIN) > 0 && (subPage > 0 && subPage < 11)) { + bool pinRequired = !correctPIN && strlen(settingsPIN) > 0 && (subPage > (WLED_WIFI_CONFIGURED ? SUBPAGE_MENU : SUBPAGE_WIFI) && subPage < SUBPAGE_LOCK); + if (pinRequired) { originalSubPage = subPage; subPage = SUBPAGE_PINREQ; // require PIN } // if OTA locked or too frequent PIN entry requests fail hard - if ((subPage == SUBPAGE_WIFI && wifiLock && otaLock) || (post && !correctPIN && millis()-lastEditTime < PIN_RETRY_COOLDOWN)) + if ((subPage == SUBPAGE_WIFI && wifiLock && otaLock) || (post && pinRequired && millis()-lastEditTime < PIN_RETRY_COOLDOWN)) { serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_ota), 254); return; } @@ -609,7 +603,7 @@ void serveSettings(AsyncWebServerRequest* request, bool post) { if (!s2[0]) strcpy_P(s2, s_redirecting); bool redirectAfter9s = (subPage == SUBPAGE_WIFI || ((subPage == SUBPAGE_SEC || subPage == SUBPAGE_UM) && doReboot)); - serveMessage(request, (correctPIN ? 200 : 401), s, s2, redirectAfter9s ? 129 : (correctPIN ? 1 : 3)); + serveMessage(request, (!pinRequired ? 200 : 401), s, s2, redirectAfter9s ? 129 : (!pinRequired ? 1 : 3)); return; } } diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 7f9e1c5a..500bd200 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -11,7 +11,7 @@ void XML_response(Print& dest) dest.printf_P(PSTR("%d"), (nightlightActive && nightlightMode > NL_MODE_SET) ? briT : bri); for (int i = 0; i < 3; i++) { - dest.printf_P(PSTR("%d"), col[i]); + dest.printf_P(PSTR("%d"), colPri[i]); } for (int i = 0; i < 3; i++) { @@ -20,13 +20,13 @@ void XML_response(Print& dest) dest.printf_P(PSTR("%d%d%d%d%d%d%d%d%d%d%d%d%d%d%s%s%d"), notifyDirect, receiveGroups!=0, nightlightActive, nightlightMode > NL_MODE_SET, nightlightDelayMins, nightlightTargetBri, effectCurrent, effectSpeed, effectIntensity, effectPalette, - strip.hasWhiteChannel() ? col[3] : -1, colSec[3], currentPreset, currentPlaylist >= 0, + strip.hasWhiteChannel() ? colPri[3] : -1, colSec[3], currentPreset, currentPlaylist >= 0, serverDescription, realtimeMode ? PSTR(" (live)") : "", strip.getFirstSelectedSegId() ); } -static void extractPin(Print& settingsScript, JsonObject &obj, const char *key) { +static void extractPin(Print& settingsScript, const JsonObject &obj, const char *key) { if (obj[key].is()) { JsonArray pins = obj[key].as(); for (JsonVariant pv : pins) { @@ -38,7 +38,7 @@ static void extractPin(Print& settingsScript, JsonObject &obj, const char *key) } // print used pins by scanning JsonObject (1 level deep) -static void fillUMPins(Print& settingsScript, JsonObject &mods) +static void fillUMPins(Print& settingsScript, const JsonObject &mods) { for (JsonPair kv : mods) { // kv.key() is usermod name or subobject key @@ -110,7 +110,7 @@ void appendGPIOinfo(Print& settingsScript) { settingsScript.print(hardwareTX); // debug output (TX) pin firstPin = false; #endif - #ifdef WLED_USE_ETHERNET + #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) if (ethernetType != WLED_ETH_NONE && ethernetType < WLED_NUM_ETH_TYPES) { if (!firstPin) settingsScript.print(','); for (unsigned p=0; p 0) { //Have seen an ESP-NOW Remote printSetClassElementHTML(settingsScript,PSTR("rlid"),0,last_signal_src); - } else if (!enableESPNow) { - printSetClassElementHTML(settingsScript,PSTR("rlid"),0,(char*)F("(Enable ESP-NOW to listen)")); - } else { - printSetClassElementHTML(settingsScript,PSTR("rlid"),0,(char*)F("None")); } #endif } @@ -272,7 +275,7 @@ void getSettingsJS(byte subPage, Print& settingsScript) // set limits settingsScript.printf_P(PSTR("bLimits(%d,%d,%d,%d,%d,%d,%d,%d);"), WLED_MAX_BUSSES, - WLED_MIN_VIRTUAL_BUSSES, + WLED_MIN_VIRTUAL_BUSSES, // irrelevant, but kept to distinguish S2/S3 in UI MAX_LEDS_PER_BUS, MAX_LED_MEMORY, MAX_LEDS, @@ -285,16 +288,16 @@ void getSettingsJS(byte subPage, Print& settingsScript) printSetFormCheckbox(settingsScript,PSTR("CCT"),strip.correctWB); printSetFormCheckbox(settingsScript,PSTR("IC"),cctICused); printSetFormCheckbox(settingsScript,PSTR("CR"),strip.cctFromRgb); - printSetFormValue(settingsScript,PSTR("CB"),strip.cctBlending); + printSetFormValue(settingsScript,PSTR("CB"),Bus::getCCTBlend()); printSetFormValue(settingsScript,PSTR("FR"),strip.getTargetFps()); printSetFormValue(settingsScript,PSTR("AW"),Bus::getGlobalAWMode()); - printSetFormCheckbox(settingsScript,PSTR("LD"),useGlobalLedBuffer); + printSetFormCheckbox(settingsScript,PSTR("PR"),BusManager::hasParallelOutput()); // get it from bus manager not global variable unsigned sumMa = 0; - for (int s = 0; s < BusManager::getNumBusses(); s++) { - Bus* bus = BusManager::getBus(s); - if (bus == nullptr) continue; - int offset = s < 10 ? 48 : 55; + for (size_t s = 0; s < BusManager::getNumBusses(); s++) { + const Bus *bus = BusManager::getBus(s); + if (!bus || !bus->isOk()) break; // should not happen but for safety + int offset = s < 10 ? '0' : 'A' - 10; char lp[4] = "L0"; lp[2] = offset+s; lp[3] = 0; //ascii 0-9 //strip data pin char lc[4] = "LC"; lc[2] = offset+s; lc[3] = 0; //strip length char co[4] = "CO"; co[2] = offset+s; co[3] = 0; //strip color order @@ -312,8 +315,8 @@ void getSettingsJS(byte subPage, Print& settingsScript) uint8_t pins[OUTPUT_MAX_PINS]; int nPins = bus->getPins(pins); for (int i = 0; i < nPins; i++) { - lp[1] = offset+i; - if (PinManager::isPinOk(pins[i]) || bus->isVirtual() || bus->isHub75()) printSetFormValue(settingsScript,lp,pins[i]); + lp[1] = '0'+i; + if (PinManager::isPinOk(pins[i]) || bus->isVirtual()) printSetFormValue(settingsScript,lp,pins[i]); } printSetFormValue(settingsScript,lc,bus->getLength()); printSetFormValue(settingsScript,lt,bus->getType()); @@ -357,7 +360,7 @@ void getSettingsJS(byte subPage, Print& settingsScript) const ColorOrderMap& com = BusManager::getColorOrderMap(); for (int s = 0; s < com.count(); s++) { const ColorOrderMapEntry* entry = com.get(s); - if (entry == nullptr) break; + if (!entry || !entry->len) break; settingsScript.printf_P(PSTR("addCOM(%d,%d,%d);"), entry->start, entry->len, entry->colorOrder); } @@ -369,17 +372,15 @@ void getSettingsJS(byte subPage, Print& settingsScript) printSetFormCheckbox(settingsScript,PSTR("GB"),gammaCorrectBri); printSetFormCheckbox(settingsScript,PSTR("GC"),gammaCorrectCol); dtostrf(gammaCorrectVal,3,1,nS); printSetFormValue(settingsScript,PSTR("GV"),nS); - printSetFormCheckbox(settingsScript,PSTR("TF"),fadeTransition); - printSetFormCheckbox(settingsScript,PSTR("EB"),modeBlending); printSetFormValue(settingsScript,PSTR("TD"),transitionDelayDefault); - printSetFormCheckbox(settingsScript,PSTR("PF"),strip.paletteFade); printSetFormValue(settingsScript,PSTR("TP"),randomPaletteChangeTime); printSetFormCheckbox(settingsScript,PSTR("TH"),useHarmonicRandomPalette); printSetFormValue(settingsScript,PSTR("BF"),briMultiplier); printSetFormValue(settingsScript,PSTR("TB"),nightlightTargetBri); printSetFormValue(settingsScript,PSTR("TL"),nightlightDelayMinsDefault); printSetFormValue(settingsScript,PSTR("TW"),nightlightMode); - printSetFormIndex(settingsScript,PSTR("PB"),strip.paletteBlend); + printSetFormIndex(settingsScript,PSTR("PB"),paletteBlend); + printSetFormCheckbox(settingsScript,PSTR("RW"),useRainbowWheel); printSetFormValue(settingsScript,PSTR("RL"),rlyPin); printSetFormCheckbox(settingsScript,PSTR("RM"),rlyMde); printSetFormCheckbox(settingsScript,PSTR("RO"),rlyOpenDrain); @@ -436,6 +437,18 @@ void getSettingsJS(byte subPage, Print& settingsScript) printSetFormCheckbox(settingsScript,PSTR("ES"),e131SkipOutOfSequence); printSetFormCheckbox(settingsScript,PSTR("EM"),e131Multicast); printSetFormValue(settingsScript,PSTR("EU"),e131Universe); +#ifdef WLED_ENABLE_DMX + settingsScript.print(SET_F("hideNoDMX();")); // hide "not compiled in" message +#endif +#ifndef WLED_ENABLE_DMX_INPUT + settingsScript.print(SET_F("hideDMXInput();")); // hide "dmx input" settings +#else + settingsScript.print(SET_F("hideNoDMXInput();")); //hide "not compiled in" message + printSetFormValue(settingsScript,SET_F("IDMT"),dmxInputTransmitPin); + printSetFormValue(settingsScript,SET_F("IDMR"),dmxInputReceivePin); + printSetFormValue(settingsScript,SET_F("IDME"),dmxInputEnablePin); + printSetFormValue(settingsScript,SET_F("IDMP"),dmxInputPort); +#endif printSetFormValue(settingsScript,PSTR("DA"),DMXAddress); printSetFormValue(settingsScript,PSTR("XX"),DMXSegmentSpacing); printSetFormValue(settingsScript,PSTR("PY"),e131Priority); @@ -582,6 +595,10 @@ void getSettingsJS(byte subPage, Print& settingsScript) snprintf_P(tmp_buf,sizeof(tmp_buf),PSTR("WLED %s (build %d)"),versionString,VERSION); printSetClassElementHTML(settingsScript,PSTR("sip"),0,tmp_buf); settingsScript.printf_P(PSTR("sd=\"%s\";"), serverDescription); + #ifdef WLED_DISABLE_OTA + //hide settings if not compiled + settingsScript.print(F("toggle('aOTA');")); // hide ArduinoOTA checkbox + #endif } #ifdef WLED_ENABLE_DMX // include only if DMX is enabled @@ -653,16 +670,14 @@ void getSettingsJS(byte subPage, Print& settingsScript) #ifndef WLED_DISABLE_2D settingsScript.printf_P(PSTR("maxPanels=%d;resetPanels();"),WLED_MAX_PANELS); if (strip.isMatrix) { - if(strip.panels>0){ - printSetFormValue(settingsScript,PSTR("PW"),strip.panel[0].width); //Set generator Width and Height to first panel size for convenience - printSetFormValue(settingsScript,PSTR("PH"),strip.panel[0].height); - } - printSetFormValue(settingsScript,PSTR("MPC"),strip.panels); + printSetFormValue(settingsScript,PSTR("PW"),strip.panel.size()>0?strip.panel[0].width:8); //Set generator Width and Height to first panel size for convenience + printSetFormValue(settingsScript,PSTR("PH"),strip.panel.size()>0?strip.panel[0].height:8); + printSetFormValue(settingsScript,PSTR("MPC"),strip.panel.size()); // panels - for (unsigned i=0; i