diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 241acd79d..588641824 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -24,29 +24,30 @@ // 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": [], diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 138730058..a5c80f22d 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -37,4 +37,5 @@ jobs: prerelease: true body: ${{ steps.changelog.outputs.changelog }} files: | - ./*.bin \ No newline at end of file + *.bin + *.bin.gz diff --git a/CHANGELOG.md b/CHANGELOG.md index c570ac1f7..f591fc2b2 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/lib/ESP8266PWM/src/core_esp8266_waveform_phase.cpp b/lib/ESP8266PWM/src/core_esp8266_waveform_phase.cpp new file mode 100644 index 000000000..68cb9010e --- /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 78c7160d9..000000000 --- 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/platformio.ini b/platformio.ini index 1a239bf05..f0248d42a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -141,7 +141,7 @@ lib_deps = IRremoteESP8266 @ 2.8.2 makuna/NeoPixelBus @ 2.8.0 #https://github.com/makuna/NeoPixelBus.git#CoreShaderBeta - https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.2.1 + https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.0 # for I2C interface ;Wire # ESP-NOW library @@ -229,8 +229,16 @@ 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 @@ -240,10 +248,11 @@ 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 @@ -252,7 +261,7 @@ 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} 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 @@ -273,10 +282,12 @@ 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 @@ -288,14 +299,14 @@ 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 @@ -307,15 +318,16 @@ 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 @@ -326,13 +338,13 @@ build_flags = -g -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 @@ -514,7 +526,6 @@ build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME= upload_speed = 460800 build_unflags = ${common.build_unflags} lib_deps = ${esp32c3.lib_deps} -board_build.flash_mode = qio [env:esp32s3dev_16MB_opi] ;; ESP32-S3 development board, with 16MB FLASH and >= 8MB PSRAM (memory_type: qio_opi) @@ -614,7 +625,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 diff --git a/platformio_override.sample.ini b/platformio_override.sample.ini index cb5b43e7b..19b8c273a 100644 --- a/platformio_override.sample.ini +++ b/platformio_override.sample.ini @@ -529,3 +529,14 @@ monitor_filters = esp32_exception_decoder lib_deps = ${esp32.lib_deps} TFT_eSPI @ 2.5.33 ;; this is the last version that compiles with the WLED default framework - newer versions require platform = espressif32 @ ^6.3.2 + +# ------------------------------------------------------------------------------ +# 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 diff --git a/requirements.txt b/requirements.txt index 666122aa2..ee70cd689 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # -# 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 diff --git a/tools/AutoCubeMap.xlsx b/tools/AutoCubeMap.xlsx new file mode 100644 index 000000000..b3f5cee2a Binary files /dev/null and b/tools/AutoCubeMap.xlsx differ diff --git a/tools/stress_test.sh b/tools/stress_test.sh index d7c344c58..d86c50864 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/seven_segment_display_reloaded/readme.md b/usermods/seven_segment_display_reloaded/readme.md index a3398c3e5..94788df7e 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/seven_segment_display_reloaded.cpp b/usermods/seven_segment_display_reloaded/seven_segment_display_reloaded.cpp index 3e6877d5d..971a80c8d 100644 --- a/usermods/seven_segment_display_reloaded/seven_segment_display_reloaded.cpp +++ b/usermods/seven_segment_display_reloaded/seven_segment_display_reloaded.cpp @@ -95,6 +95,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()); @@ -385,6 +390,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")); } @@ -408,6 +416,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() { diff --git a/usermods/stairway_wipe_basic/wled06_usermod.ino b/usermods/stairway_wipe_basic/wled06_usermod.ino deleted file mode 100644 index dc2159ee9..000000000 --- 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/usermod_v2_RF433/library.json b/usermods/usermod_v2_RF433/library.json new file mode 100644 index 000000000..9ba2bdcf6 --- /dev/null +++ b/usermods/usermod_v2_RF433/library.json @@ -0,0 +1,6 @@ +{ + "name:": "usermod_v2_RF433", + "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 000000000..43919f11b --- /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 000000000..d5d930a81 --- /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 000000000..9ac6c416d --- /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/wled00/FX.cpp b/wled00/FX.cpp index 7bf054581..594822641 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -197,7 +197,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; @@ -410,7 +410,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; @@ -642,11 +642,12 @@ 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; } @@ -654,33 +655,26 @@ uint16_t dissolve(uint32_t color) { if (hw_random8() <= SEGMENT.intensity) { for (size_t times = 0; times < 10; times++) { //attempt to spawn a new pixel 10 times unsigned i = hw_random16(SEGLEN); - unsigned index = i >> 3; - unsigned bitNum = i & 0x07; - bool fadeUp = bitRead(SEGENV.data[index], bitNum); if (SEGENV.aux0) { //dissolve to primary/palette - if (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++; } @@ -1023,7 +1017,7 @@ 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(); + 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; @@ -1056,7 +1050,7 @@ 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 (unsigned i = 0; i < SEGLEN; i++) { @@ -1086,7 +1080,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++) { @@ -1168,7 +1162,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 @@ -1226,7 +1220,7 @@ static const char _data_FX_MODE_DUAL_LARSON_SCANNER[] PROGMEM = "Scanner Dual@!, * 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; @@ -1254,7 +1248,7 @@ static const char _data_FX_MODE_COMET[] PROGMEM = "Lighthouse@!,Fade rate;!,!;!" * Fireworks function. */ uint16_t mode_fireworks() { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); const uint16_t width = SEGMENT.is2D() ? SEG_W : SEGLEN; const uint16_t height = SEG_H; @@ -1296,7 +1290,7 @@ static const char _data_FX_MODE_FIREWORKS[] PROGMEM = "Fireworks@,Frequency;!,!; //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(); + if (SEGLEN <= 1) return mode_static(); const unsigned width = SEG_W; const unsigned height = SEG_H; SEGENV.step += FRAMETIME; @@ -1362,7 +1356,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; @@ -1407,7 +1401,7 @@ static const char _data_FX_MODE_LOADING[] PROGMEM = "Loading@!,Fade;!,!;!;;ix=16 * Two dots running */ uint16_t mode_two_dots() { - if (SEGLEN == 1) return mode_static(); + 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; @@ -1827,7 +1821,7 @@ 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 @@ -1843,7 +1837,7 @@ uint16_t mode_oscillate(void) { 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) { + 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)); } } @@ -1858,7 +1852,7 @@ static const char _data_FX_MODE_OSCILLATE[] PROGMEM = "Oscillate"; //TODO uint16_t mode_lightning(void) { - if (SEGLEN == 1) return mode_static(); + 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); @@ -1899,52 +1893,76 @@ 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) { + CRGB newcolor = CHSV(hue8, sat8, bri8); + 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; @@ -2089,7 +2107,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; @@ -2147,53 +2165,6 @@ uint16_t mode_fire_2012() { } 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 - -// 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() { - unsigned duration = 10 + SEGMENT.speed; - unsigned sPseudotime = SEGENV.step; - unsigned sHue16 = SEGENV.aux0; - - 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, 60, 300)*SEGMENT.intensity*10/255; // Use the Intensity Slider for the hues - - sPseudotime += duration * msmultiplier; - sHue16 += duration * beatsin88_t(400, 5, 9); - unsigned brightnesstheta16 = sPseudotime; - - for (unsigned i = 0 ; i < SEGLEN; i++) { - hue16 += hueinc16; - uint8_t hue8 = hue16 >> 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;!;!;;pal=26"; - - // colored stripes pulsing at a defined Beats-Per-Minute (BPM) uint16_t mode_bpm() { uint32_t stp = (strip.now / 20) & 0xFF; @@ -2369,7 +2340,7 @@ static const char _data_FX_MODE_LAKE[] PROGMEM = "Lake@!;Fx;!"; // 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; @@ -2436,7 +2407,7 @@ static const char _data_FX_MODE_METEOR[] PROGMEM = "Meteor@!,Trail,,,,Gradient,, //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) @@ -2537,7 +2508,7 @@ static uint16_t ripple_base(uint8_t blurAmount = 0) { uint16_t mode_ripple(void) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); if(SEGMENT.custom1 || SEGMENT.check2) // blur or overlay SEGMENT.fade_out(250); else @@ -2549,7 +2520,7 @@ static const char _data_FX_MODE_RIPPLE[] PROGMEM = "Ripple@!,Wave #,Blur,,,,Over uint16_t mode_ripple_rainbow(void) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); if (SEGENV.call ==0) { SEGENV.aux0 = hw_random8(); SEGENV.aux1 = hw_random8(); @@ -2727,7 +2698,7 @@ uint16_t mode_halloween_eyes() uint32_t blinkEndTime; }; - if (SEGLEN == 1) return mode_static(); + 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; @@ -2912,7 +2883,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; @@ -2968,7 +2939,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; @@ -3146,7 +3117,7 @@ static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of b * 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; @@ -3251,7 +3222,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; @@ -3426,7 +3397,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 @@ -3545,7 +3516,7 @@ static const char _data_FX_MODE_STARBURST[] PROGMEM = "Fireworks Starburst@Chanc */ uint16_t mode_exploding_fireworks(void) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); const int cols = SEGMENT.is2D() ? SEG_W : 1; const int rows = SEGMENT.is2D() ? SEG_H : SEGLEN; @@ -3683,7 +3654,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; @@ -3779,7 +3750,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 @@ -3990,7 +3961,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; @@ -4086,7 +4057,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; @@ -4293,7 +4264,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 @@ -4344,7 +4315,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; @@ -4479,6 +4450,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 @@ -4806,7 +4795,7 @@ 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. @@ -4842,7 +4831,7 @@ static const char _data_FX_MODE_WAVESINS[] PROGMEM = "Wavesins@!,Brightness vari ////////////////////////////// // By: ldirko https://editor.soulmatelights.com/gallery/392-flow-led-stripe , modifed by: Andrew Tuline uint16_t mode_FlowStripe(void) { - if (SEGLEN == 1) return mode_static(); + 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); @@ -5166,7 +5155,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; @@ -5475,15 +5464,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; @@ -6123,13 +6112,23 @@ 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("#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); @@ -6568,23 +6567,31 @@ static const char _data_FX_MODE_JUGGLES[] PROGMEM = "Juggles@!,# of balls;!,!;!; // * MATRIPIX // ////////////////////// uint16_t mode_matripix(void) { // Matripix. By Andrew Tuline. - if (SEGLEN == 1) return mode_static(); - // even with 1D effect we have to take logic for 2D segments for allocation as fill_solid() fills whole segment + // effect can work on single pixels, we just lose the shifting effect + unsigned dataSize = sizeof(uint32_t) * SEGLEN; + if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + uint32_t* pixels = reinterpret_cast(SEGENV.data); um_data_t *um_data = getAudioData(); int volumeRaw = *(int16_t*)um_data->u_data[1]; if (SEGENV.call == 0) { - SEGMENT.fill(BLACK); + for (unsigned i = 0; i < SEGLEN; i++) pixels[i] = BLACK; // may not be needed as resetIfRequired() clears buffer } uint8_t secondHand = micros()/(256-SEGMENT.speed)/500 % 16; if(SEGENV.aux0 != secondHand) { SEGENV.aux0 = secondHand; - uint8_t pixBri = volumeRaw * SEGMENT.intensity / 64; - for (unsigned i = 0; i < SEGLEN-1; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // shift left - SEGMENT.setPixelColor(SEGLEN-1, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0), pixBri)); + int pixBri = volumeRaw * SEGMENT.intensity / 64; + unsigned k = SEGLEN-1; + // loop will not execute if SEGLEN equals 1 + for (unsigned i = 0; i < k; i++) { + pixels[i] = pixels[i+1]; // shift left + SEGMENT.setPixelColor(i, pixels[i]); + } + pixels[k] = color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0), pixBri); + SEGMENT.setPixelColor(k, pixels[k]); } return FRAMETIME; @@ -6596,7 +6603,7 @@ static const char _data_FX_MODE_MATRIPIX[] PROGMEM = "Matripix@!,Brightness;!,!; // * MIDNOISE // ////////////////////// uint16_t mode_midnoise(void) { // Midnoise. By Andrew Tuline. - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); // Changing xdist to SEGENV.aux0 and ydist to SEGENV.aux1. um_data_t *um_data = getAudioData(); @@ -6687,7 +6694,7 @@ static const char _data_FX_MODE_NOISEMETER[] PROGMEM = "Noisemeter@Fade rate,Wid // * PIXELWAVE // ////////////////////// uint16_t mode_pixelwave(void) { // Pixelwave. By Andrew Tuline. - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); // even with 1D effect we have to take logic for 2D segments for allocation as fill_solid() fills whole segment if (SEGENV.call == 0) { @@ -6755,7 +6762,7 @@ static const char _data_FX_MODE_PLASMOID[] PROGMEM = "Plasmoid@Phase,# of pixels ////////////////////// // Puddles/Puddlepeak By Andrew Tuline. Merged by @dedehai uint16_t mode_puddles_base(bool peakdetect) { - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); unsigned size = 0; uint8_t fadeVal = map(SEGMENT.speed, 0, 255, 224, 254); unsigned pos = hw_random16(SEGLEN); // Set a random starting position. @@ -6805,7 +6812,7 @@ static const char _data_FX_MODE_PUDDLES[] PROGMEM = "Puddles@Fade rate,Puddle si // * PIXELS // ////////////////////// uint16_t mode_pixels(void) { // Pixels. By Andrew Tuline. - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); if (!SEGENV.allocateData(32*sizeof(uint8_t))) return mode_static(); //allocation failed uint8_t *myVals = reinterpret_cast(SEGENV.data); // Used to store a pile of samples because WLED frame rate and WLED sample rate are not synchronized. Frame rate is too low. @@ -6833,7 +6840,7 @@ static const char _data_FX_MODE_PIXELS[] PROGMEM = "Pixels@Fade rate,# of pixels // ** Blurz // ////////////////////// uint16_t mode_blurz(void) { // Blurz. By Andrew Tuline. - if (SEGLEN == 1) return mode_static(); + if (SEGLEN <= 1) return mode_static(); // even with 1D effect we have to take logic for 2D segments for allocation as fill_solid() fills whole segment um_data_t *um_data = getAudioData(); @@ -6897,7 +6904,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 @@ -7139,8 +7146,11 @@ static const char _data_FX_MODE_ROCKTAVES[] PROGMEM = "Rocktaves@;!,!;!;01f;m12= // Combines peak detection with FFT_MajorPeak and FFT_Magnitude. uint16_t mode_waterfall(void) { // Waterfall. By: Andrew Tuline // effect can work on single pixels, we just lose the shifting effect - - um_data_t *um_data = getAudioData(); + unsigned dataSize = sizeof(uint32_t) * SEGLEN; + if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + uint32_t* pixels = reinterpret_cast(SEGENV.data); + + um_data_t *um_data = getAudioData(); uint8_t samplePeak = *(uint8_t*)um_data->u_data[3]; float FFT_MajorPeak = *(float*) um_data->u_data[4]; uint8_t *maxVol = (uint8_t*)um_data->u_data[6]; @@ -7150,7 +7160,7 @@ uint16_t mode_waterfall(void) { // Waterfall. By: Andrew Tulin if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; // log10(0) is "forbidden" (throws exception) if (SEGENV.call == 0) { - SEGMENT.fill(BLACK); + for (unsigned i = 0; i < SEGLEN; i++) pixels[i] = BLACK; // may not be needed as resetIfRequired() clears buffer SEGENV.aux0 = 255; SEGMENT.custom1 = *binNum; SEGMENT.custom2 = *maxVol * 2; @@ -7167,13 +7177,18 @@ uint16_t mode_waterfall(void) { // Waterfall. By: Andrew Tulin uint8_t pixCol = (log10f(FFT_MajorPeak) - 2.26f) * 150; // 22Khz sampling - log10 frequency range is from 2.26 (182hz) to 3.967 (9260hz). Let's scale accordingly. if (FFT_MajorPeak < 182.0f) pixCol = 0; // handle underflow + unsigned k = SEGLEN-1; if (samplePeak) { - SEGMENT.setPixelColor(SEGLEN-1, CHSV(92,92,92)); + pixels[k] = (uint32_t)CRGB(CHSV(92,92,92)); } else { - SEGMENT.setPixelColor(SEGLEN-1, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(pixCol+SEGMENT.intensity, false, PALETTE_SOLID_WRAP, 0), (uint8_t)my_magnitude)); + pixels[k] = color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(pixCol+SEGMENT.intensity, false, PALETTE_SOLID_WRAP, 0), (uint8_t)my_magnitude); } + SEGMENT.setPixelColor(k, pixels[k]); // loop will not execute if SEGLEN equals 1 - for (unsigned i = 0; i < SEGLEN-1; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // shift left + for (unsigned i = 0; i < k; i++) { + pixels[i] = pixels[i+1]; // shift left + SEGMENT.setPixelColor(i, pixels[i]); + } } return FRAMETIME; @@ -7737,7 +7752,9 @@ void WS2812FX::setupEffectData() { 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); diff --git a/wled00/FX.h b/wled00/FX.h index 57df58549..3b1f8f8f1 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -79,9 +79,9 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #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) + #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 + #define MAX_SEGMENT_DATA (MAX_NUM_SEGMENTS*1280) // 40k by default #endif #endif @@ -184,7 +184,7 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #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_IMAGE 53 #define FX_MODE_TRICOLOR_CHASE 54 #define FX_MODE_TRICOLOR_WIPE 55 #define FX_MODE_TRICOLOR_FADE 56 @@ -325,6 +325,30 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define MODE_COUNT 187 + +#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_PINCH_OUT 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 +// 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, M12_pBar = 1, @@ -333,7 +357,7 @@ typedef enum mapping1D2D { M12_sPinwheel = 4 } mapping1D2D_t; -// segment, 80 bytes +// segment, 68 bytes typedef struct Segment { public: uint16_t start; // start index / start X coordinate 2D (left) @@ -436,6 +460,9 @@ typedef struct Segment { static uint16_t _transitionprogress; // current transition progress 0 - 0xFFFF #ifndef WLED_DISABLE_MODE_BLEND static bool _modeBlend; // mode/effect blending semaphore + // clipping + static uint16_t _clipStart, _clipStop; + static uint8_t _clipStartY, _clipStopY; #endif // transition data, valid only if transitional==true, holds values during transition (72 bytes) @@ -446,6 +473,7 @@ typedef struct Segment { #else uint32_t _colorT[NUM_COLORS]; #endif + uint8_t _palTid; // previous palette uint8_t _briT; // temporary brightness uint8_t _cctT; // temporary CCT CRGBPalette16 _palT; // temporary palette @@ -460,7 +488,7 @@ typedef struct Segment { {} } *_t; - [[gnu::hot]] void _setPixelColorXY_raw(int& x, int& y, uint32_t& col); // set pixel without mapping (internal use only) + [[gnu::hot]] void _setPixelColorXY_raw(const int& x, const int& y, uint32_t& col) const; // set pixel without mapping (internal use only) public: @@ -518,7 +546,7 @@ typedef struct Segment { //if (data) Serial.printf(" %d->(%p)", (int)_dataLen, data); //Serial.println(); #endif - if (name) { delete[] name; name = nullptr; } + if (name) { free(name); name = nullptr; } stopTransition(); deallocateData(); } @@ -534,7 +562,6 @@ typedef struct Segment { 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; } @@ -588,10 +615,10 @@ typedef struct Segment { inline void handleTransition() { updateTransitionProgress(); if (progress() == 0xFFFFU) stopTransition(); } #ifndef WLED_DISABLE_MODE_BLEND void swapSegenv(tmpsegd_t &tmpSegD); // copies segment data into specifed buffer, if buffer is not a transition buffer, segment data is overwritten from transition buffer - void restoreSegenv(tmpsegd_t &tmpSegD); // restores segment data from buffer, if buffer is not transition buffer, changed values are copied to transition buffer + void restoreSegenv(const tmpsegd_t &tmpSegD); // restores segment data from buffer, if buffer is not transition buffer, changed values are copied to transition buffer #endif [[gnu::hot]] void updateTransitionProgress(); // set current progression of transition - inline uint16_t progress() const { return _transitionprogress; }; // transition progression between 0-65535 + inline uint16_t progress() const { return Segment::_transitionprogress; } // transition progression between 0-65535 [[gnu::hot]] uint8_t currentBri(bool useCct = false) const; // current segment brightness/CCT (blended while in transition) uint8_t currentMode() const; // currently active effect/mode (while in transition) [[gnu::hot]] uint32_t currentColor(uint8_t slot) const; // currently active segment color (blended while in transition) @@ -599,15 +626,19 @@ typedef struct Segment { // 1D strip [[gnu::hot]] uint16_t virtualLength() const; - [[gnu::hot]] void setPixelColor(int n, uint32_t c); // set relative pixel within segment with color - inline void setPixelColor(unsigned n, uint32_t c) { setPixelColor(int(n), c); } - inline void setPixelColor(int n, byte r, byte g, byte b, byte w = 0) { setPixelColor(n, RGBW32(r,g,b,w)); } - inline void setPixelColor(int n, CRGB c) { setPixelColor(n, RGBW32(c.r,c.g,c.b,0)); } + [[gnu::hot]] void setPixelColor(int i, uint32_t c) const; // set relative pixel within segment with color + inline void setPixelColor(unsigned n, uint32_t c) const { setPixelColor(int(n), c); } + inline void setPixelColor(int n, byte r, byte g, byte b, byte w = 0) const { setPixelColor(n, RGBW32(r,g,b,w)); } + inline void setPixelColor(int n, CRGB c) const { setPixelColor(n, RGBW32(c.r,c.g,c.b,0)); } #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 + #ifndef WLED_DISABLE_MODE_BLEND + static inline void setClippingRect(int startX, int stopX, int startY = 0, int stopY = 1) { _clipStart = startX; _clipStop = stopX; _clipStartY = startY; _clipStopY = stopY; }; + #endif + 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); @@ -642,17 +673,19 @@ 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]] int XY(int x, int y) const; // support function to get relative index within segment + [[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)); } @@ -678,7 +711,8 @@ typedef struct Segment { void wu_pixel(uint32_t x, uint32_t y, CRGB c); inline void fill_solid(CRGB c) { fill(RGBW32(c.r,c.g,c.b,0)); } #else - inline uint16_t XY(int x, int y) { return x; } + inline constexpr bool is2D() const { return false; } + inline int XY(int x, int y) const { 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)); } @@ -689,6 +723,7 @@ typedef struct Segment { 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); } #endif + inline bool isPixelXYClipped(int x, int y) { return isPixelClipped(x); } 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); } @@ -733,9 +768,7 @@ class WS2812FX { // 96 bytes public: WS2812FX() : - paletteFade(0), paletteBlend(0), - cctBlending(0), now(millis()), timebase(0), isMatrix(false), @@ -778,7 +811,7 @@ class WS2812FX { // 96 bytes } ~WS2812FX() { - if (customMappingTable) delete[] customMappingTable; + if (customMappingTable) free(customMappingTable); _mode.clear(); _modeData.clear(); _segments.clear(); @@ -804,7 +837,7 @@ class WS2812FX { // 96 bytes 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 + setPixelColor(unsigned i, uint32_t c) const, // paints absolute strip pixel with index n and color c show(), // initiates LED output setTargetFps(unsigned fps), setupEffectData(); // add default effects to the list; defined in FX.cpp @@ -812,9 +845,9 @@ class WS2812FX { // 96 bytes 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 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) + 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 (unsigned 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) @@ -823,8 +856,7 @@ class WS2812FX { // 96 bytes inline void resume() { _suspend = false; } // will resume strip.service() execution bool - paletteFade, - checkSegmentAlignment(), + checkSegmentAlignment() const, hasRGBWBus() const, hasCCTBus() const, deserializeMap(unsigned n = 0); @@ -838,7 +870,6 @@ class WS2812FX { // 96 bytes uint8_t paletteBlend, - cctBlending, getActiveSegmentsNum() const, getFirstSelectedSegId() const, getLastActiveSegmentId() const, @@ -869,7 +900,7 @@ class WS2812FX { // 96 bytes }; unsigned long now, timebase; - uint32_t getPixelColor(unsigned) const; + uint32_t getPixelColor(unsigned i) const; inline uint32_t getLastShow() const { return _lastShow; } // returns millis() timestamp of last strip.show() call @@ -918,11 +949,11 @@ class WS2812FX { // 96 bytes 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 void setPixelColorXY(int x, int y, uint32_t c) const { setPixelColor((unsigned)(y * Segment::maxWidth + x), 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 uint32_t getPixelColorXY(int x, int y) const { return getPixelColor(isMatrix ? y * Segment::maxWidth + x : x); } + inline uint32_t getPixelColorXY(int x, int y) const { return getPixelColor(isMatrix ? y * Segment::maxWidth + x : x); } // end 2D support @@ -936,7 +967,7 @@ class WS2812FX { // 96 bytes }; std::vector _segments; - friend class Segment; + friend struct Segment; private: volatile bool _suspend; diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index f00e7147d..67624bac3 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -50,8 +50,8 @@ void WS2812FX::setUpMatrix() { customMappingSize = 0; // prevent use of mapping if anything goes wrong - if (customMappingTable) delete[] customMappingTable; - customMappingTable = new uint16_t[getLengthTotal()]; + if (customMappingTable) free(customMappingTable); + customMappingTable = static_cast(malloc(sizeof(uint16_t)*getLengthTotal())); if (customMappingTable) { customMappingSize = getLengthTotal(); @@ -68,7 +68,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 +85,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(malloc(gapSize)); if (gapTable) for (size_t i = 0; i < gapSize; i++) { gapTable[i] = constrain(map[i], -1, 1); } @@ -113,7 +113,7 @@ void WS2812FX::setUpMatrix() { } // delete gap array as we no longer need it - if (gapTable) delete[] gapTable; + if (gapTable) free(gapTable); #ifdef WLED_DEBUG DEBUG_PRINT(F("Matrix ledmap:")); @@ -146,7 +146,7 @@ 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) +int IRAM_ATTR_YN Segment::XY(int x, int y) const { 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) @@ -154,13 +154,13 @@ uint16_t IRAM_ATTR_YN Segment::XY(int x, int y) } // raw setColor function without checks (checks are done in setPixelColorXY()) -void IRAM_ATTR_YN Segment::_setPixelColorXY_raw(int& x, int& y, uint32_t& col) +void IRAM_ATTR_YN Segment::_setPixelColorXY_raw(const int& x, const int& y, uint32_t& col) const { const int baseX = start + x; const int baseY = startY + y; #ifndef WLED_DISABLE_MODE_BLEND // if blending modes, blend with underlying pixel - if (_modeBlend) col = color_blend16(strip.getPixelColorXY(baseX, baseY), col, 0xFFFFU - progress()); + if (_modeBlend && blendingStyle == BLEND_STYLE_FADE) col = color_blend16(strip.getPixelColorXY(baseX, baseY), col, 0xFFFFU - progress()); #endif strip.setPixelColorXY(baseX, baseY, col); @@ -179,14 +179,57 @@ void IRAM_ATTR_YN Segment::_setPixelColorXY_raw(int& x, int& y, uint32_t& col) } } -void IRAM_ATTR_YN Segment::setPixelColorXY(int x, int y, uint32_t col) +// pixel is clipped if it falls outside clipping range (_modeBlend==true) or is inside clipping range (_modeBlend==false) +// if clipping start > stop the clipping range is inverted +// _modeBlend==true -> old effect during transition +// _modeBlend==false -> new effect during transition +bool IRAM_ATTR_YN Segment::isPixelXYClipped(int x, int y) const { +#ifndef WLED_DISABLE_MODE_BLEND + if (_clipStart != _clipStop && blendingStyle != BLEND_STYLE_FADE) { + const bool invertX = _clipStart > _clipStop; + const bool invertY = _clipStartY > _clipStopY; + const int startX = invertX ? _clipStop : _clipStart; + const int stopX = invertX ? _clipStart : _clipStop; + const int startY = invertY ? _clipStopY : _clipStartY; + const int stopY = invertY ? _clipStartY : _clipStopY; + if (blendingStyle == BLEND_STYLE_FAIRY_DUST) { + const unsigned width = stopX - startX; // assumes full segment width (faster than virtualWidth()) + const unsigned len = width * (stopY - startY); // 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; + } + bool xInside = (x >= startX && x < stopX); if (invertX) xInside = !xInside; + bool yInside = (y >= startY && y < stopY); if (invertY) yInside = !yInside; + const bool clip = (invertX && invertY) ? !_modeBlend : _modeBlend; + if (xInside && yInside) return clip; // covers window & corners (inverted) + return !clip; + } +#endif + return false; +} + +void IRAM_ATTR_YN Segment::setPixelColorXY(int x, int y, uint32_t col) const { if (!isActive()) 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) - // negative values of x & y cast into unsigend will become very large values and will therefore be greater than vW/vH - if (unsigned(x) >= unsigned(vW) || unsigned(y) >= unsigned(vH)) return; // if pixel would fall out of virtual segment just exit + +#ifndef WLED_DISABLE_MODE_BLEND + unsigned prog = 0xFFFF - progress(); + if (!prog && !_modeBlend && (blendingStyle & BLEND_STYLE_PUSH_MASK)) { + unsigned dX = (blendingStyle == BLEND_STYLE_PUSH_UP || blendingStyle == BLEND_STYLE_PUSH_DOWN) ? 0 : prog * vW / 0xFFFF; + unsigned dY = (blendingStyle == BLEND_STYLE_PUSH_LEFT || blendingStyle == BLEND_STYLE_PUSH_RIGHT) ? 0 : prog * vH / 0xFFFF; + if (blendingStyle == BLEND_STYLE_PUSH_LEFT || blendingStyle == BLEND_STYLE_PUSH_TL || blendingStyle == BLEND_STYLE_PUSH_BL) x += dX; + else x -= dX; + if (blendingStyle == BLEND_STYLE_PUSH_DOWN || blendingStyle == BLEND_STYLE_PUSH_TL || blendingStyle == BLEND_STYLE_PUSH_TR) y -= dY; + else y += dY; + } +#endif + + if (x >= vW || y >= vH || x < 0 || y < 0 || isPixelXYClipped(x,y)) return; // if pixel would fall out of virtual segment just exit // if color is unscaled if (!_colorScaled) col = color_fade(col, _segBri); @@ -215,7 +258,7 @@ void IRAM_ATTR_YN Segment::setPixelColorXY(int x, int y, uint32_t 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 @@ -259,9 +302,24 @@ 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 + const int vW = vWidth(); const int vH = vHeight(); - if (unsigned(x) >= unsigned(vW) || unsigned(y) >= unsigned(vH)) return 0; // if pixel would fall out of virtual segment just exit + +#ifndef WLED_DISABLE_MODE_BLEND + unsigned prog = 0xFFFF - progress(); + if (!prog && !_modeBlend && (blendingStyle & BLEND_STYLE_PUSH_MASK)) { + unsigned dX = (blendingStyle == BLEND_STYLE_PUSH_UP || blendingStyle == BLEND_STYLE_PUSH_DOWN) ? 0 : prog * vW / 0xFFFF; + unsigned dY = (blendingStyle == BLEND_STYLE_PUSH_LEFT || blendingStyle == BLEND_STYLE_PUSH_RIGHT) ? 0 : prog * vH / 0xFFFF; + if (blendingStyle == BLEND_STYLE_PUSH_LEFT || blendingStyle == BLEND_STYLE_PUSH_TL || blendingStyle == BLEND_STYLE_PUSH_BL) x -= dX; + else x += dX; + if (blendingStyle == BLEND_STYLE_PUSH_DOWN || blendingStyle == BLEND_STYLE_PUSH_TL || blendingStyle == BLEND_STYLE_PUSH_TR) y -= dY; + else y += dY; + } +#endif + + if (x >= vW || y >= vH || x<0 || y<0 || isPixelXYClipped(x,y)) return 0; // if pixel would fall out of virtual segment just exit + if (reverse ) x = vW - x - 1; if (reverse_y) y = vH - y - 1; if (transpose) { std::swap(x,y); } // swap X & Y if segment transposed @@ -276,7 +334,7 @@ void Segment::blur2D(uint8_t blur_x, uint8_t blur_y, bool smear) { if (!isActive()) return; // not active const unsigned cols = vWidth(); const unsigned rows = vHeight(); - 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; if (blur_x) { const uint8_t keepx = smear ? 255 : 255 - blur_x; diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index b9a62bb2c..cf37a46c2 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -84,6 +84,10 @@ uint16_t Segment::_transitionprogress = 0xFFFF; #ifndef WLED_DISABLE_MODE_BLEND bool Segment::_modeBlend = false; +uint16_t Segment::_clipStart = 0; +uint16_t Segment::_clipStop = 0; +uint8_t Segment::_clipStartY = 0; +uint8_t Segment::_clipStopY = 1; #endif // copy constructor @@ -94,7 +98,7 @@ Segment::Segment(const Segment &orig) { name = nullptr; data = nullptr; _dataLen = 0; - if (orig.name) { name = new char[strlen(orig.name)+1]; if (name) strcpy(name, orig.name); } + if (orig.name) { name = static_cast(malloc(strlen(orig.name)+1)); if (name) strcpy(name, orig.name); } if (orig.data) { if (allocateData(orig._dataLen)) memcpy(data, orig.data, orig._dataLen); } } @@ -113,7 +117,7 @@ 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; } + if (name) { free(name); name = nullptr; } stopTransition(); deallocateData(); // copy source @@ -122,7 +126,7 @@ Segment& Segment::operator= (const Segment &orig) { data = nullptr; _dataLen = 0; // copy source data - if (orig.name) { name = new char[strlen(orig.name)+1]; if (name) strcpy(name, orig.name); } + if (orig.name) { name = static_cast(malloc(strlen(orig.name)+1)); if (name) strcpy(name, orig.name); } if (orig.data) { if (allocateData(orig._dataLen)) memcpy(data, orig.data, orig._dataLen); } } return *this; @@ -132,7 +136,7 @@ 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 + if (name) { free(name); name = nullptr; } // free old name stopTransition(); deallocateData(); // free old runtime data memcpy((void*)this, (void*)&orig, sizeof(Segment)); @@ -195,6 +199,9 @@ void Segment::resetIfRequired() { if (data && _dataLen > 0) memset(data, 0, _dataLen); // prevent heap fragmentation (just erase buffer instead of deallocateData()) next_time = 0; step = 0; call = 0; aux0 = 0; aux1 = 0; reset = false; + #ifdef WLED_ENABLE_GIF + endImagePlayback(this); + #endif } CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { @@ -253,29 +260,26 @@ void Segment::startTransition(uint16_t dur) { 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 + _t = new(std::nothrow) 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->_palTid = 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; - } + 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; } - } else { - for (size_t i=0; i_segT._colorT[i] = colors[i]; } #else for (size_t i=0; i_colorT[i] = colors[i]; @@ -296,6 +300,7 @@ void Segment::stopTransition() { delete _t; _t = nullptr; } + _transitionprogress = 0xFFFFU; // stop means stop - transition has ended } // transition progression between 0-65535 @@ -326,7 +331,7 @@ void Segment::swapSegenv(tmpsegd_t &tmpSeg) { tmpSeg._callT = call; tmpSeg._dataT = data; tmpSeg._dataLenT = _dataLen; - if (_t && &tmpSeg != &(_t->_segT)) { + if (isInTransition() && &tmpSeg != &(_t->_segT)) { // swap SEGENV with transitional data options = _t->_segT._optionsT; for (size_t i=0; i_segT._colorT[i]; @@ -347,9 +352,9 @@ void Segment::swapSegenv(tmpsegd_t &tmpSeg) { } } -void Segment::restoreSegenv(tmpsegd_t &tmpSeg) { +void Segment::restoreSegenv(const tmpsegd_t &tmpSeg) { //DEBUG_PRINTF_P(PSTR("-- Restoring temp seg: %p->(%p) [%d->%p]\n"), &tmpSeg, this, _dataLen, data); - if (_t && &(_t->_segT) != &tmpSeg) { + if (isInTransition() && &(_t->_segT) != &tmpSeg) { // update possibly changed variables to keep old effect running correctly _t->_segT._aux0T = aux0; _t->_segT._aux1T = aux1; @@ -379,29 +384,53 @@ void Segment::restoreSegenv(tmpsegd_t &tmpSeg) { #endif uint8_t Segment::currentBri(bool useCct) const { - unsigned prog = progress(); + unsigned prog = isInTransition() ? progress() : 0xFFFFU; + uint32_t curBri = useCct ? cct : (on ? opacity : 0); if (prog < 0xFFFFU) { - unsigned curBri = (useCct ? cct : (on ? opacity : 0)) * prog; - curBri += (useCct ? _t->_cctT : _t->_briT) * (0xFFFFU - prog); +#ifndef WLED_DISABLE_MODE_BLEND + uint8_t tmpBri = useCct ? _t->_cctT : (_t->_segT._optionsT & 0x0004 ? _t->_briT : 0); + // _modeBlend==true -> old effect + if (blendingStyle != BLEND_STYLE_FADE) return _modeBlend ? tmpBri : curBri; // not fade/blend transition, each effect uses its brightness +#else + uint8_t tmpBri = useCct ? _t->_cctT : _t->_briT; +#endif + curBri *= prog; + curBri += tmpBri * (0xFFFFU - prog); return curBri / 0xFFFFU; } - return (useCct ? cct : (on ? opacity : 0)); + return curBri; } uint8_t Segment::currentMode() const { #ifndef WLED_DISABLE_MODE_BLEND - unsigned prog = progress(); - if (modeBlending && prog < 0xFFFFU) return _t->_modeT; -#endif + unsigned prog = isInTransition() ? progress() : 0xFFFFU; + if (prog == 0xFFFFU) return mode; + if (blendingStyle != BLEND_STYLE_FADE) { + // workaround for on/off transition to respect blending style + uint8_t modeT = (bri != briT) && bri ? FX_MODE_STATIC : _t->_modeT; // On/Off transition active (bri!=briT) and final bri>0 : old mode is STATIC + uint8_t modeS = (bri != briT) && !bri ? FX_MODE_STATIC : mode; // On/Off transition active (bri!=briT) and final bri==0 : new mode is STATIC + return _modeBlend ? modeT : modeS; // _modeBlend==true -> old effect + } + return _modeBlend ? _t->_modeT : mode; // _modeBlend==true -> old effect +#else return mode; +#endif } uint32_t Segment::currentColor(uint8_t slot) const { if (slot >= NUM_COLORS) slot = 0; + unsigned prog = progress(); + if (prog == 0xFFFFU) return colors[slot]; #ifndef WLED_DISABLE_MODE_BLEND - return isInTransition() ? color_blend16(_t->_segT._colorT[slot], colors[slot], progress()) : colors[slot]; + if (blendingStyle != BLEND_STYLE_FADE) { + // workaround for on/off transition to respect blending style + uint32_t colT = (bri != briT) && bri ? BLACK : _t->_segT._colorT[slot]; // On/Off transition active (bri!=briT) and final bri>0 : old color is BLACK + uint32_t colS = (bri != briT) && !bri ? BLACK : colors[slot]; // On/Off transition active (bri!=briT) and final bri==0 : new color is BLACK + return _modeBlend ? colT : colS; // _modeBlend==true -> old effect + } + return color_blend16(_t->_segT._colorT[slot], colors[slot], prog); #else - return isInTransition() ? color_blend16(_t->_colorT[slot], colors[slot], progress()) : colors[slot]; + return color_blend16(_t->_colorT[slot], colors[slot], prog); #endif } @@ -411,44 +440,49 @@ void Segment::beginDraw() { _vHeight = virtualHeight(); _vLength = virtualLength(); _segBri = currentBri(); + unsigned prog = isInTransition() ? progress() : 0xFFFFU; // transition progress; 0xFFFFU = no transition active // adjust gamma for effects for (unsigned i = 0; i < NUM_COLORS; i++) { #ifndef WLED_DISABLE_MODE_BLEND - uint32_t col = isInTransition() ? color_blend16(_t->_segT._colorT[i], colors[i], progress()) : colors[i]; + uint32_t col = isInTransition() ? color_blend16(_t->_segT._colorT[i], colors[i], prog) : colors[i]; #else - uint32_t col = isInTransition() ? color_blend16(_t->_colorT[i], colors[i], progress()) : colors[i]; + uint32_t col = isInTransition() ? color_blend16(_t->_colorT[i], colors[i], prog) : colors[i]; #endif _currentColors[i] = gamma32(col); } // load palette into _currentPalette loadPalette(_currentPalette, palette); - unsigned prog = progress(); - if (strip.paletteFade && prog < 0xFFFFU) { - // blend palettes - // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in _dur time) - // minimum blend time is 100ms maximum is 65535ms - unsigned noOfBlends = ((255U * prog) / 0xFFFFU) - _t->_prevPaletteBlends; - for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, _currentPalette, 48); - _currentPalette = _t->_palT; // copy transitioning/temporary palette + if (prog < 0xFFFFU) { +#ifndef WLED_DISABLE_MODE_BLEND + if (blendingStyle > BLEND_STYLE_FADE) { + //if (_modeBlend) loadPalette(_currentPalette, _t->_palTid); // not fade/blend transition, each effect uses its palette + if (_modeBlend) _currentPalette = _t->_palT; // not fade/blend transition, each effect uses its palette + } else +#endif + { + // blend palettes + // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in _dur time) + // minimum blend time is 100ms maximum is 65535ms + unsigned noOfBlends = ((255U * prog) / 0xFFFFU) - _t->_prevPaletteBlends; + for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, _currentPalette, 48); + _currentPalette = _t->_palT; // copy transitioning/temporary palette + } } } // relies on WS2812FX::service() to call it for each frame void Segment::handleRandomPalette() { // 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 ((uint16_t)(millis()/1000U) - _lastPaletteChange > randomPaletteChangeTime) { + _newRandomPalette = useHarmonicRandomPalette ? generateHarmonicRandomPalette(_randomPalette) : generateRandomPalette(); + _lastPaletteChange = (uint16_t)(millis()/1000U); + _lastPaletteBlend = (uint16_t)(millis())-512; // starts blending immediately } - // if palette transitions is enabled, blend it according to Transition Time (if longer than minimum given by service calls) - if (strip.paletteFade) { - // assumes that 128 updates are sufficient to blend a palette, so shift by 7 (can be more, can be less) - // in reality there need to be 255 blends to fully blend two entirely different palettes - if ((uint16_t)((uint16_t)millis() - _lastPaletteBlend) < strip.getTransition() >> 7) return; // not yet time to fade, delay the update - _lastPaletteBlend = (uint16_t)millis(); - } + // 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)millis() - _lastPaletteBlend < strip.getTransition() >> 7) return; // not yet time to fade, delay the update + _lastPaletteBlend = (uint16_t)millis(); nblendPaletteTowardPalette(_randomPalette, _newRandomPalette, 48); } @@ -523,7 +557,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()); // start transition prior to change colors[slot] = c; stateChanged = true; // send UDP/WS broadcast return *this; @@ -536,7 +571,7 @@ Segment &Segment::setCCT(uint16_t k) { k = (k - 1900) >> 5; } if (cct != k) { - //DEBUGFX_PRINTF_P(PSTR("- Starting CCT transition: %d\n"), k); + //DEBUG_PRINTF_P(PSTR("- Starting CCT transition: %d\n"), k); startTransition(strip.getTransition()); // start transition prior to change cct = k; stateChanged = true; // send UDP/WS broadcast @@ -546,7 +581,7 @@ 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); + //DEBUG_PRINTF_P(PSTR("- Starting opacity transition: %d\n"), o); startTransition(strip.getTransition()); // start transition prior to change opacity = o; stateChanged = true; // send UDP/WS broadcast @@ -556,7 +591,7 @@ 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 + if (n == SEG_OPTION_ON && val != prevOn) startTransition(strip.getTransition()); // start transition prior to change if (val) options |= 0x01 << n; else options &= ~(0x01 << n); if (!(n == SEG_OPTION_SELECTED || n == SEG_OPTION_RESET)) stateChanged = true; // send UDP/WS broadcast @@ -570,7 +605,8 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) { // 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 + //DEBUG_PRINTF_P(PSTR("- Starting effect transition: %d\n"), fx); + startTransition(strip.getTransition()); // set effect transitions #endif mode = fx; int sOpt; @@ -605,7 +641,8 @@ 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 != palette) { - if (strip.paletteFade) startTransition(strip.getTransition()); + //DEBUG_PRINTF_P(PSTR("- Starting palette transition: %d\n"), pal); + startTransition(strip.getTransition()); palette = pal; stateChanged = true; // send UDP/WS broadcast } @@ -678,7 +715,7 @@ uint16_t 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); @@ -696,7 +733,34 @@ uint16_t Segment::virtualLength() const { return vLength; } -void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col) +// pixel is clipped if it falls outside clipping range (_modeBlend==true) or is inside clipping range (_modeBlend==false) +// if clipping start > stop the clipping range is inverted +// _modeBlend==true -> old effect during transition +// _modeBlend==false -> new effect during transition +bool IRAM_ATTR_YN Segment::isPixelClipped(int i) const { +#ifndef WLED_DISABLE_MODE_BLEND + if (_clipStart != _clipStop && blendingStyle > BLEND_STYLE_FADE) { + 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) ^ _modeBlend; + } + const bool iInside = (i >= start && i < stop); + //if (!invert && iInside) return _modeBlend; + //if ( invert && !iInside) return _modeBlend; + //return !_modeBlend; + return !iInside ^ invert ^ _modeBlend; // thanks @willmmiles (https://github.com/Aircoookie/WLED/pull/3877#discussion_r1554633876) + } +#endif + return false; +} + +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 @@ -828,6 +892,18 @@ void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col) } #endif +#ifndef WLED_DISABLE_MODE_BLEND + // if we blend using "push" style we need to "shift" new mode to left or right + if (isInTransition() && !_modeBlend && (blendingStyle == BLEND_STYLE_PUSH_RIGHT || blendingStyle == BLEND_STYLE_PUSH_LEFT)) { + unsigned prog = 0xFFFF - progress(); + unsigned dI = prog * vL / 0xFFFF; + if (blendingStyle == BLEND_STYLE_PUSH_RIGHT) i -= dI; + else i += dI; + } +#endif + + if (i >= vL || i < 0 || isPixelClipped(i)) return; // handle clipping on 1D + unsigned len = length(); // if color is unscaled if (!_colorScaled) col = color_fade(col, _segBri); @@ -853,14 +929,16 @@ void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col) indexMir += offset; // offset/phase if (indexMir >= stop) indexMir -= len; // wrap #ifndef WLED_DISABLE_MODE_BLEND - if (_modeBlend) tmpCol = color_blend16(strip.getPixelColor(indexMir), col, uint16_t(0xFFFFU - progress())); + // _modeBlend==true -> old effect + if (_modeBlend && blendingStyle == BLEND_STYLE_FADE) tmpCol = color_blend16(strip.getPixelColor(indexMir), col, 0xFFFFU - progress()); #endif strip.setPixelColor(indexMir, tmpCol); } indexSet += offset; // offset/phase if (indexSet >= stop) indexSet -= len; // wrap #ifndef WLED_DISABLE_MODE_BLEND - if (_modeBlend) tmpCol = color_blend16(strip.getPixelColor(indexSet), col, uint16_t(0xFFFFU - progress())); + // _modeBlend==true -> old effect + if (_modeBlend && blendingStyle == BLEND_STYLE_FADE) tmpCol = color_blend16(strip.getPixelColor(indexSet), col, 0xFFFFU - progress()); #endif strip.setPixelColor(indexSet, tmpCol); } @@ -869,7 +947,7 @@ void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t 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) @@ -906,6 +984,9 @@ uint32_t IRAM_ATTR_YN Segment::getPixelColor(int i) const { if (!isActive()) return 0; // not active + int vL = vLength(); + if (i >= vL || i < 0) return 0; + #ifndef WLED_DISABLE_2D if (is2D()) { const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) @@ -921,7 +1002,7 @@ uint32_t IRAM_ATTR_YN Segment::getPixelColor(int i) const break; } case M12_pArc: if (i >= vW && i >= vH) { - unsigned vI = sqrt16(i*i/2); + unsigned vI = sqrt32_bw(i*i/2); return getPixelColorXY(vI,vI); // use diagonal } case M12_pCorner: @@ -962,7 +1043,18 @@ uint32_t IRAM_ATTR_YN Segment::getPixelColor(int i) const } #endif - if (reverse) i = vLength() - i - 1; +#ifndef WLED_DISABLE_MODE_BLEND + if (isInTransition() && !_modeBlend && (blendingStyle == BLEND_STYLE_PUSH_RIGHT || blendingStyle == BLEND_STYLE_PUSH_LEFT)) { + unsigned prog = 0xFFFF - progress(); + unsigned dI = prog * vL / 0xFFFF; + if (blendingStyle == BLEND_STYLE_PUSH_RIGHT) i -= dI; + else i += dI; + } +#endif + + if (i >= vL || i < 0 || isPixelClipped(i)) return 0; // handle clipping on 1D + + if (reverse) i = vL - i - 1; i *= groupLength(); i += start; // offset/phase @@ -1134,7 +1226,7 @@ void Segment::blur(uint8_t blur_amount, bool smear) { 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++) { @@ -1195,7 +1287,7 @@ uint32_t Segment::color_from_palette(uint16_t i, bool mapping, bool wrap, uint8_ if (mapping && vL > 1) paletteIndex = (i*255)/(vL -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" - CRGBW palcol = ColorFromPalette(_currentPalette, paletteIndex, pbri, (strip.paletteBlend == 3)? NOBLEND:LINEARBLEND); // NOTE: paletteBlend should be global + CRGBW palcol = ColorFromPaletteWLED(_currentPalette, paletteIndex, pbri, (strip.paletteBlend == 3)? NOBLEND:LINEARBLEND); // NOTE: paletteBlend should be global palcol.w = W(color); return palcol.color32; @@ -1372,21 +1464,78 @@ void WS2812FX::service() { // 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 seg.beginDraw(); // set up parameters for get/setPixelColor() - frameDelay = (*_mode[seg.mode])(); // run new/current mode #ifndef WLED_DISABLE_MODE_BLEND - if (modeBlending && seg.mode != tmpMode) { + Segment::setClippingRect(0, 0); // disable clipping (just in case) + if (seg.isInTransition()) { + // set clipping rectangle + // new mode is run inside clipping area and old mode outside clipping area + unsigned p = seg.progress(); + unsigned w = seg.is2D() ? Segment::vWidth() : Segment::vLength(); + unsigned h = Segment::vHeight(); + unsigned dw = p * w / 0xFFFFU + 1; + unsigned dh = p * h / 0xFFFFU + 1; + unsigned orgBS = blendingStyle; + if (w*h == 1) blendingStyle = BLEND_STYLE_FADE; // disable belending for single pixel segments (use fade instead) + switch (blendingStyle) { + case BLEND_STYLE_FAIRY_DUST: // fairy dust (must set entire segment, see isPixelXYClipped()) + Segment::setClippingRect(0, w, 0, h); + break; + case BLEND_STYLE_SWIPE_RIGHT: // left-to-right + case BLEND_STYLE_PUSH_RIGHT: // left-to-right + Segment::setClippingRect(0, dw, 0, h); + break; + case BLEND_STYLE_SWIPE_LEFT: // right-to-left + case BLEND_STYLE_PUSH_LEFT: // right-to-left + Segment::setClippingRect(w - dw, w, 0, h); + break; + case BLEND_STYLE_PINCH_OUT: // corners + Segment::setClippingRect((w + dw)/2, (w - dw)/2, (h + dh)/2, (h - dh)/2); // inverted!! + break; + case BLEND_STYLE_INSIDE_OUT: // outward + Segment::setClippingRect((w - dw)/2, (w + dw)/2, (h - dh)/2, (h + dh)/2); + break; + case BLEND_STYLE_SWIPE_DOWN: // top-to-bottom (2D) + case BLEND_STYLE_PUSH_DOWN: // top-to-bottom (2D) + Segment::setClippingRect(0, w, 0, dh); + break; + case BLEND_STYLE_SWIPE_UP: // bottom-to-top (2D) + case BLEND_STYLE_PUSH_UP: // bottom-to-top (2D) + Segment::setClippingRect(0, w, h - dh, h); + break; + case BLEND_STYLE_OPEN_H: // horizontal-outward (2D) same look as INSIDE_OUT on 1D + Segment::setClippingRect((w - dw)/2, (w + dw)/2, 0, h); + break; + case BLEND_STYLE_OPEN_V: // vertical-outward (2D) + Segment::setClippingRect(0, w, (h - dh)/2, (h + dh)/2); + break; + case BLEND_STYLE_PUSH_TL: // TL-to-BR (2D) + Segment::setClippingRect(0, dw, 0, dh); + break; + case BLEND_STYLE_PUSH_TR: // TR-to-BL (2D) + Segment::setClippingRect(w - dw, w, 0, dh); + break; + case BLEND_STYLE_PUSH_BR: // BR-to-TL (2D) + Segment::setClippingRect(w - dw, w, h - dh, h); + break; + case BLEND_STYLE_PUSH_BL: // BL-to-TR (2D) + Segment::setClippingRect(0, dw, h - dh, h); + break; + } + frameDelay = (*_mode[seg.currentMode()])(); // run new/current mode + // now run old/previous mode Segment::tmpsegd_t _tmpSegData; Segment::modeBlend(true); // set semaphore seg.swapSegenv(_tmpSegData); // temporarily store new mode state (and swap it with transitional state) seg.beginDraw(); // set up parameters for get/setPixelColor() - unsigned d2 = (*_mode[tmpMode])(); // run old mode + frameDelay = min(frameDelay, (unsigned)(*_mode[seg.currentMode()])()); // run old mode + seg.call++; // increment old mode run counter seg.restoreSegenv(_tmpSegData); // restore mode state (will also update transitional state) - frameDelay = min(frameDelay,d2); // use shortest delay Segment::modeBlend(false); // unset semaphore - } + blendingStyle = orgBS; // restore blending style if it was modified for single pixel segment + } else #endif + frameDelay = (*_mode[seg.mode])(); // run effect mode (not in transition) seg.call++; if (seg.isInTransition() && frameDelay > FRAMETIME) frameDelay = FRAMETIME; // force faster updates during transition BusManager::setSegmentCCT(oldCCT); // restore old CCT for ABL adjustments @@ -1396,6 +1545,7 @@ void WS2812FX::service() { } _segment_index++; } + Segment::setClippingRect(0, 0); // disable clipping for overlays _isServicing = false; _triggered = false; @@ -1413,7 +1563,7 @@ void WS2812FX::service() { #endif } -void IRAM_ATTR WS2812FX::setPixelColor(unsigned i, uint32_t col) { +void IRAM_ATTR WS2812FX::setPixelColor(unsigned i, uint32_t col) const { i = getMappedPixelIndex(i); if (i >= _length) return; BusManager::setPixelColor(i, col); @@ -1694,9 +1844,9 @@ void WS2812FX::fixInvalidSegments() { //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; bgetStart() && seg.stop == bus->getStart() + bus->getLength()) aligned = true; @@ -1808,8 +1958,8 @@ bool WS2812FX::deserializeMap(unsigned n) { Segment::maxHeight = min(max(root[F("height")].as(), 1), 128); } - if (customMappingTable) delete[] customMappingTable; - customMappingTable = new uint16_t[getLengthTotal()]; + if (customMappingTable) free(customMappingTable); + customMappingTable = static_cast(malloc(sizeof(uint16_t)*getLengthTotal())); if (customMappingTable) { DEBUG_PRINT(F("Reading LED map from ")); DEBUG_PRINTLN(fileName); diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 2c0ba41a9..3abf61412 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -16,6 +16,9 @@ #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" @@ -27,7 +30,7 @@ extern bool cctICused; 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 uint8_t* buffer, uint8_t bri=255, bool isRGBW=false); // enable additional debug output #if defined(WLED_DEBUG_HOST) @@ -121,7 +124,7 @@ uint8_t *Bus::allocateData(size_t size) { } -BusDigital::BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com) +BusDigital::BusDigital(const BusConfig &bc, uint8_t nr, const ColorOrderMap &com) : Bus(bc.type, bc.start, bc.autoWhite, bc.count, bc.reversed, (bc.refreshReq || bc.type == TYPE_TM1814)) , _skip(bc.skipAmount) //sacrificial pixels , _colorOrder(bc.colorOrder) @@ -448,7 +451,7 @@ void BusDigital::cleanup() { #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; @@ -462,10 +465,7 @@ BusPwm::BusPwm(BusConfig &bc) 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 +#ifdef ARDUINO_ARCH_ESP32 // 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 @@ -556,13 +556,19 @@ uint32_t BusPwm::getPixelColor(unsigned pix) const { void BusPwm::show() { if (!_valid) return; + const unsigned 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) 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) - + 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 = _bri; @@ -582,20 +588,25 @@ void BusPwm::show() { // 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 @@ -604,9 +615,11 @@ 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 } } @@ -646,7 +659,7 @@ void BusPwm::deallocatePins() { } -BusOnOff::BusOnOff(BusConfig &bc) +BusOnOff::BusOnOff(const BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite, 1, bc.reversed) , _onoffdata(0) { @@ -699,7 +712,7 @@ std::vector BusOnOff::getLEDTypes() { }; } -BusNetwork::BusNetwork(BusConfig &bc) +BusNetwork::BusNetwork(const BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite, bc.count) , _broadcastLock(false) { @@ -778,7 +791,7 @@ void BusNetwork::cleanup() { //utility to get the approx. memory usage of a given BusConfig -uint32_t BusManager::memUsage(BusConfig &bc) { +uint32_t BusManager::memUsage(const BusConfig &bc) { if (Bus::isOnOff(bc.type) || Bus::isPWM(bc.type)) return OUTPUT_MAX_PINS; unsigned len = bc.count + bc.skipAmount; @@ -803,7 +816,7 @@ uint32_t BusManager::memUsage(unsigned maxChannels, unsigned maxCount, unsigned return (maxChannels * maxCount * minBuses * multiplier); } -int BusManager::add(BusConfig &bc) { +int BusManager::add(const BusConfig &bc) { if (getNumBusses() - getNumVirtualBusses() >= WLED_MAX_BUSSES) return -1; if (Bus::isVirtual(bc.type)) { busses[numBusses] = new BusNetwork(bc); diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index d90a66151..9aed01308 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -198,7 +198,7 @@ class Bus { class BusDigital : public Bus { public: - BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com); + BusDigital(const BusConfig &bc, uint8_t nr, const ColorOrderMap &com); ~BusDigital() { cleanup(); } void show() override; @@ -250,7 +250,7 @@ class BusDigital : public Bus { class BusPwm : public Bus { public: - BusPwm(BusConfig &bc); + BusPwm(const BusConfig &bc); ~BusPwm() { cleanup(); } void setPixelColor(unsigned pix, uint32_t c) override; @@ -277,7 +277,7 @@ class BusPwm : public Bus { class BusOnOff : public Bus { public: - BusOnOff(BusConfig &bc); + BusOnOff(const BusConfig &bc); ~BusOnOff() { cleanup(); } void setPixelColor(unsigned pix, uint32_t c) override; @@ -296,7 +296,7 @@ class BusOnOff : public Bus { 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 @@ -379,12 +379,12 @@ class BusManager { BusManager() {}; //utility to get the approx. memory usage of a given BusConfig - static uint32_t memUsage(BusConfig &bc); + static uint32_t memUsage(const BusConfig &bc); static uint32_t memUsage(unsigned channels, unsigned count, unsigned buses = 1); static uint16_t currentMilliamps() { return _milliAmpsUsed + MA_FOR_ESP; } static uint16_t ablMilliampsMax() { return _milliAmpsMax; } - static int add(BusConfig &bc); + static int add(const BusConfig &bc); static void useParallelOutput(); // workaround for inaccessible PolyBus //do not call this method from system context (network callback) diff --git a/wled00/button.cpp b/wled00/button.cpp index 4e063c120..5144f09f2 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -90,12 +90,12 @@ void doublePressAction(uint8_t b) #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; diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 38e804ed9..00cfc60d7 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -114,8 +114,8 @@ 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")]); @@ -444,12 +444,9 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); // fill look-up table 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")]); @@ -522,6 +519,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 @@ -819,7 +824,7 @@ void serializeConfig() { 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; @@ -937,10 +942,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; @@ -1001,6 +1003,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; diff --git a/wled00/colors.cpp b/wled00/colors.cpp index e64cf6758..f154a1aea 100644 --- a/wled00/colors.cpp +++ b/wled00/colors.cpp @@ -122,7 +122,7 @@ 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 = hw_random8(4); // color position of current random palette to keep @@ -391,7 +391,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; @@ -402,7 +402,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]; diff --git a/wled00/const.h b/wled00/const.h index bdd20beba..1ebcb9397 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -204,6 +204,7 @@ #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" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot @@ -249,6 +250,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 @@ -558,8 +560,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/index.htm b/wled00/data/index.htm index 8adec791f..aa06b5122 100644 --- a/wled00/data/index.htm +++ b/wled00/data/index.htm @@ -128,7 +128,7 @@
- +

Color palette

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

Transition:  s

+

Transition:  s

+

Blend: + +

diff --git a/wled00/data/index.js b/wled00/data/index.js index 1482c27b5..dc94cf426 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -677,8 +677,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"); @@ -1437,6 +1439,9 @@ 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; @@ -1698,6 +1703,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 diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 956cf7d90..02ebb6ed0 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -832,12 +832,7 @@ Swap:

Brightness factor: %

Transitions

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

Timed light

diff --git a/wled00/data/settings_sync.htm b/wled00/data/settings_sync.htm index 34b9fc6cd..ca6c0fb59 100644 --- a/wled00/data/settings_sync.htm +++ b/wled00/data/settings_sync.htm @@ -40,6 +40,8 @@ function getURL(path) { return (loc ? locproto + "//" + locip : "") + path; } + function hideDMXInput(){gId("dmxInput").style.display="none";} + function hideNoDMXInput(){gId("dmxInputOff").style.display="none";} @@ -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/dmx_input.cpp b/wled00/dmx_input.cpp new file mode 100644 index 000000000..3197375f1 --- /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))); + doSerializeConfig = 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)); + doSerializeConfig = 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 000000000..7845778d7 --- /dev/null +++ b/wled00/dmx_input.h @@ -0,0 +1,73 @@ +#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(); + +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 dbe70f2aa..eace2145e 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 bc26a0639..c16ed9332 100644 --- a/wled00/e131.cpp +++ b/wled00/e131.cpp @@ -116,6 +116,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; @@ -130,7 +135,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--; } @@ -211,7 +216,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) @@ -285,7 +290,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; diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index cfb18c25b..b508de150 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -161,12 +161,12 @@ class NeoGammaWLEDMethod { }; #define gamma32(c) NeoGammaWLEDMethod::Correct32(c) #define gamma8(c) NeoGammaWLEDMethod::rawGamma8(c) -[[gnu::hot]] uint32_t color_blend(uint32_t c1, uint32_t c2 , uint8_t blend); +[[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]] uint32_t color_add(uint32_t, uint32_t, bool preserveCR = false); -[[gnu::hot]] uint32_t color_fade(uint32_t c1, uint8_t amount, bool video=false); -[[gnu::hot]] uint32_t ColorFromPaletteWLED(const CRGBPalette16 &pal, unsigned index, uint8_t brightness = (uint8_t)255U, TBlendType blendType = LINEARBLEND); -CRGBPalette16 generateHarmonicRandomPalette(CRGBPalette16 &basepalette); +[[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(); inline uint32_t colorFromRgbw(byte* rgbw) { return uint32_t((byte(rgbw[3]) << 24) | (byte(rgbw[0]) << 16) | (byte(rgbw[1]) << 8) | (byte(rgbw[2]))); } void hsv2rgb(const CHSV32& hsv, uint32_t& rgb); @@ -176,33 +176,38 @@ inline CHSV rgb2hsv(const CRGB c) { CHSV32 hsv; rgb2hsv((uint32_t((byte(c.r) << 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 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); bool readObjectFromFile(const char* file, const char* key, JsonDocument* dest); 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 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) { 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); }; @@ -214,6 +219,19 @@ void onHueConnect(void* arg, AsyncClient* client); void sendHuePoll(); void onHueData(void* arg, AsyncClient* client, void *data, size_t len); +#include "FX.h" // must be below colors.cpp declarations (potentially due to duplicate declarations of e.g. color_blend) + +//image_loader.cpp +#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, @@ -243,11 +261,11 @@ void handleIR(); 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); @@ -318,7 +336,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); @@ -327,7 +346,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(); @@ -458,10 +477,10 @@ void userLoop(); #include "soc/wdev_reg.h" #define HW_RND_REGISTER REG_READ(WDEV_RND_REG) #endif -int getNumVal(const String* req, uint16_t pos); +[[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 minv=0, byte maxv=255); // getVal supports inc/decrementing and random ("X~Y(r|~[w][-][Z])" form) -bool getBoolVal(JsonVariant elem, bool dflt); +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); @@ -469,8 +488,8 @@ 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); @@ -482,8 +501,9 @@ 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); // 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) @@ -543,6 +563,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 bc3467202..46df8d380 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -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")); @@ -255,14 +255,14 @@ bool appendObjectToFile(const char* key, JsonDocument* content, uint32_t s, uint 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 diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp new file mode 100644 index 000000000..966505794 --- /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/json.cpp b/wled00/json.cpp index cc25d5f89..d4f0d7771 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -68,7 +68,7 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) if (elem["n"]) { // name field exists if (seg.name) { //clear old name - delete[] seg.name; + free(seg.name); seg.name = nullptr; } @@ -77,7 +77,7 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) 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]; + seg.name = static_cast(malloc(len+1)); if (seg.name) strlcpy(seg.name, name, WLED_MAX_SEGNAME_LEN+1); } else { // but is empty (already deleted above) @@ -86,7 +86,7 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) } else if (start != seg.start || stop != seg.stop) { // clearing or setting segment without name field if (seg.name) { - delete[] seg.name; + free(seg.name); seg.name = nullptr; } } @@ -332,15 +332,20 @@ 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); } } +#ifndef WLED_DISABLE_MODE_BLEND + blendingStyle = root[F("bs")] | blendingStyle; + blendingStyle = constrain(blendingStyle, 0, BLEND_STYLE_COUNT-1); +#endif + // 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; @@ -493,7 +498,7 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) return stateResponse; } -void serializeSegment(JsonObject& root, Segment& seg, byte id, bool forPreset, bool segmentBounds) +void serializeSegment(const JsonObject& root, const Segment& seg, byte id, bool forPreset, bool segmentBounds) { root["id"] = id; if (segmentBounds) { @@ -568,6 +573,9 @@ void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segme root["on"] = (bri > 0); root["bri"] = briLast; root[F("transition")] = transitionDelay/100; //in 100ms +#ifndef WLED_DISABLE_MODE_BLEND + root[F("bs")] = blendingStyle; +#endif } if (!forPreset) { @@ -761,7 +769,7 @@ void serializeInfo(JsonObject root) root[F("freeheap")] = ESP.getFreeHeap(); #if defined(ARDUINO_ARCH_ESP32) - if (psramSafe && psramFound()) root[F("psram")] = ESP.getFreePsram(); + if (psramFound()) root[F("psram")] = ESP.getFreePsram(); #endif root[F("uptime")] = millis()/1000 + rolloverMillis*4294967; @@ -1052,7 +1060,7 @@ void serveJson(AsyncWebServerRequest* request) } if (!requestJSONBufferLock(17)) { - serveJsonError(request, 503, ERR_NOBUF); + request->deferResponse(); return; } // releaseJSONBufferLock() will be called when "response" is destroyed (from AsyncWebServer) diff --git a/wled00/led.cpp b/wled00/led.cpp index 68169509d..9731da655 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -71,10 +71,9 @@ byte scaledBri(byte in) } -//applies global brightness +//applies global temporary brightness (briT) to strip void applyBri() { - if (!realtimeMode || !arlsForceMaxBri) - { + if (!(realtimeMode && arlsForceMaxBri)) { //DEBUG_PRINTF_P(PSTR("Applying strip brightness: %d (%d,%d)\n"), (int)briT, (int)bri, (int)briOld); strip.setBrightness(scaledBri(briT)); } @@ -86,6 +85,7 @@ void applyFinalBri() { briOld = bri; briT = bri; applyBri(); + strip.trigger(); // force one last update } @@ -129,30 +129,23 @@ 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; - } 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(); + return; } + + if (transitionActive) { + briOld = briT; + } else + strip.setTransitionMode(true); // force all segments to transition mode + transitionActive = true; + transitionStartTime = millis(); } -void updateInterfaces(uint8_t callMode) -{ +void updateInterfaces(uint8_t callMode) { if (!interfaceUpdateCallMode || millis() - lastInterfaceUpdate < INTERFACE_UPDATE_COOLDOWN) return; sendDataWs(); @@ -173,8 +166,7 @@ void updateInterfaces(uint8_t callMode) } -void handleTransitions() -{ +void handleTransitions() { //handle still pending interface update updateInterfaces(interfaceUpdateCallMode); @@ -205,8 +197,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 @@ -286,7 +277,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 7fd91ea02..1318686ce 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 38afeffe3..8cbe79093 100644 --- a/wled00/mqtt.cpp +++ b/wled00/mqtt.cpp @@ -68,8 +68,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 + if (payloadStr) free(payloadStr); // fail-safe: release buffer + payloadStr = static_cast(malloc(total+1)); // allocate new buffer } if (payloadStr == nullptr) return; // buffer not allocated @@ -94,7 +94,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; + free(payloadStr); payloadStr = nullptr; return; } @@ -124,7 +124,7 @@ static void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProp // topmost topic (just wled/MAC) parseMQTTBriPayload(payloadStr); } - delete[] payloadStr; + free(payloadStr); payloadStr = nullptr; } diff --git a/wled00/ntp.cpp b/wled00/ntp.cpp index 8d44e634e..12b698f44 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/pin_manager.cpp b/wled00/pin_manager.cpp index 37ebd41ec..6f1652301 100644 --- a/wled00/pin_manager.cpp +++ b/wled00/pin_manager.cpp @@ -141,7 +141,9 @@ bool PinManager::allocateMultiplePins(const managed_pin_type * mptArray, byte ar 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)) { diff --git a/wled00/pin_manager.h b/wled00/pin_manager.h index c8fb165ce..b285b6ee5 100644 --- a/wled00/pin_manager.h +++ b/wled00/pin_manager.h @@ -35,15 +35,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 // Use UserMod IDs from const.h here UM_Unspecified = USERMOD_ID_UNSPECIFIED, // 0x01 UM_Example = USERMOD_ID_EXAMPLE, // 0x02 // Usermod "usermod_v2_example.h" diff --git a/wled00/playlist.cpp b/wled00/playlist.cpp index 36235ab9e..1908334d7 100644 --- a/wled00/playlist.cpp +++ b/wled00/playlist.cpp @@ -61,7 +61,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; @@ -146,7 +146,7 @@ if (millis() - presetCycledTime > (100 * playlistEntryDur) || doAdvancePlaylist) } jsonTransitionOnce = true; - strip.setTransition(fadeTransition ? playlistEntries[playlistIndex].tr * 100 : 0); + strip.setTransition(playlistEntries[playlistIndex].tr * 100); playlistEntryDur = playlistEntries[playlistIndex].dur; applyPresetFromPlaylist(playlistEntries[playlistIndex].preset); doAdvancePlaylist = false; diff --git a/wled00/presets.cpp b/wled00/presets.cpp index 1abcb52b9..15eed3e46 100644 --- a/wled00/presets.cpp +++ b/wled00/presets.cpp @@ -76,8 +76,8 @@ static void doSaveState() { // clean up saveLedmap = -1; presetToSave = 0; - delete[] saveName; - delete[] quickLoad; + free(saveName); + free(quickLoad); saveName = nullptr; quickLoad = nullptr; playlistSave = false; @@ -216,8 +216,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(malloc(33)); + if (!quickLoad) quickLoad = static_cast(malloc(9)); if (!saveName || !quickLoad) return; if (index == 0 || (index > 250 && index < 255)) return; @@ -263,8 +263,8 @@ void savePreset(byte index, const char* pname, JsonObject sObj) presetsModifiedTime = toki.second(); //unix time updateFSInfo(); } - delete[] saveName; - delete[] quickLoad; + free(saveName); + free(quickLoad); saveName = nullptr; quickLoad = nullptr; } else { diff --git a/wled00/remote.cpp b/wled00/remote.cpp index eb19cc1f3..8c060a70c 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(); @@ -176,7 +182,7 @@ static bool remoteJson(int button) } // Callback function that will be executed when data is received -void handleRemote(uint8_t *incomingData, size_t len) { +void handleWiZdata(uint8_t *incomingData, size_t len) { message_structure_t *incoming = reinterpret_cast(incomingData); if (strcmp(last_signal_src, linked_remote) != 0) { @@ -202,8 +208,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 +232,10 @@ void handleRemote(uint8_t *incomingData, size_t len) { case WIZ_SMART_BUTTON_BRIGHT_DOWN : brightnessDown(); break; default: break; } - last_seq = cur_seq; + } + ESPNowButton = -1; } #else -void handleRemote(uint8_t *incomingData, size_t len) {} +void handleRemote() {} #endif diff --git a/wled00/set.cpp b/wled00/set.cpp index 160eb48f0..c0977f262 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -134,8 +134,8 @@ 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")); @@ -209,7 +209,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) // 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[s] = new(std::nothrow) BusConfig(type, pins, start, length, colorOrder | (channelSwap<<4), request->hasArg(cv), skip, awmode, freq, useGlobalLedBuffer, maPerLed, maMax); busesChanged = true; } //doInitBusses = busesChanged; // we will do that below to ensure all input data is processed @@ -326,11 +326,8 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) } NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); // fill look-up table - 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")); @@ -420,6 +417,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); @@ -623,7 +628,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(); @@ -982,18 +990,18 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) //set color from HEX or 32bit DEC pos = req.indexOf(F("CL=")); if (pos > 0) { - colorFromDecOrHexString(colIn, (char*)req.substring(pos + 3).c_str()); + colorFromDecOrHexString(colIn, req.substring(pos + 3).c_str()); col0Changed = true; } pos = req.indexOf(F("C2=")); if (pos > 0) { - colorFromDecOrHexString(colInSec, (char*)req.substring(pos + 3).c_str()); + colorFromDecOrHexString(colInSec, req.substring(pos + 3).c_str()); col1Changed = true; } pos = req.indexOf(F("C3=")); if (pos > 0) { byte tmpCol[4]; - colorFromDecOrHexString(tmpCol, (char*)req.substring(pos + 3).c_str()); + colorFromDecOrHexString(tmpCol, req.substring(pos + 3).c_str()); col2 = RGBW32(tmpCol[0], tmpCol[1], tmpCol[2], tmpCol[3]); selseg.setColor(2, col2); // defined above (SS= or main) col2Changed = true; @@ -1140,7 +1148,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) pos = req.indexOf(F("TT=")); if (pos > 0) transitionDelay = getNumVal(&req, pos); - if (fadeTransition) strip.setTransition(transitionDelay); + strip.setTransition(transitionDelay); //set time (unix timestamp) pos = req.indexOf(F("ST=")); diff --git a/wled00/udp.cpp b/wled00/udp.cpp index 47398bc8a..4395b285d 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -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,10 +225,8 @@ 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 @@ -810,7 +808,7 @@ static size_t sequenceNumber = 0; // this needs to be shared across all ou static const size_t ART_NET_HEADER_SIZE = 12; static const byte ART_NET_HEADER[] PROGMEM = {0x41,0x72,0x74,0x2d,0x4e,0x65,0x74,0x00,0x00,0x50,0x00,0x0e}; -uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, uint8_t *buffer, uint8_t bri, bool isRGBW) { +uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const uint8_t* buffer, uint8_t bri, bool isRGBW) { if (!(apActive || interfacesInited) || !client[0] || !length) return 1; // network not initialised or dummy/unset IP address 031522 ajn added check for ap WiFiUDP ddpUdp; @@ -963,7 +961,7 @@ void espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rs // handle WiZ Mote data if (data[0] == 0x91 || data[0] == 0x81 || data[0] == 0x80) { - handleRemote(data, len); + handleWiZdata(data, len); return; } diff --git a/wled00/util.cpp b/wled00/util.cpp index d9f1c00b9..4f2de0a2a 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -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 { @@ -151,7 +151,7 @@ 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) +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,7 +470,7 @@ 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) { @@ -507,7 +507,7 @@ um_data_t* simulateSound(uint8_t simulationId) 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]; + volumeSmth = fftResult[8]; break; case UMS_14_3: for (int i = 0; i<16; i++) @@ -538,7 +538,7 @@ void enumerateLedmaps() { #ifndef ESP8266 if (ledmapNames[i-1]) { //clear old name - delete[] ledmapNames[i-1]; + free(ledmapNames[i-1]); ledmapNames[i-1] = nullptr; } #endif @@ -556,7 +556,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 +564,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); } } @@ -595,6 +595,13 @@ 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(); @@ -608,4 +615,4 @@ int32_t hw_random(int32_t lowerlimit, int32_t upperlimit) { } uint32_t diff = upperlimit - lowerlimit; return hw_random(diff) + lowerlimit; -} \ No newline at end of file +} diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 3b8c60095..83ef1b105 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 @@ -297,6 +303,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; @@ -521,7 +528,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 @@ -772,7 +782,6 @@ int8_t WLED::findWiFi(bool doScan) { void WLED::initConnection() { DEBUG_PRINTF_P(PSTR("initConnection() called @ %lus.\n"), millis()/1000); - #ifdef WLED_ENABLE_WEBSOCKETS ws.onEvent(wsEvent); #endif @@ -801,6 +810,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.")); diff --git a/wled00/wled.h b/wled00/wled.h index 533b12063..a18199446 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -144,6 +144,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" @@ -459,7 +463,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 @@ -576,8 +588,7 @@ WLED_GLOBAL bool wasConnected _INIT(false); WLED_GLOBAL byte lastRandomIndex _INIT(0); // used to save last random color so the new one is not the same // 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) @@ -863,7 +874,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 diff --git a/wled00/wled_eeprom.cpp b/wled00/wled_eeprom.cpp index 8582b49df..e82493d11 100644 --- a/wled00/wled_eeprom.cpp +++ b/wled00/wled_eeprom.cpp @@ -224,7 +224,7 @@ void loadSettingsFromEEPROM() if (lastEEPROMversion > 7) { - strip.paletteFade = EEPROM.read(374); + //strip.paletteFade = EEPROM.read(374); strip.paletteBlend = EEPROM.read(382); for (int i = 0; i < 8; ++i) diff --git a/wled00/wled_math.cpp b/wled00/wled_math.cpp index a8ec55400..43c593080 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 ad9bb1413..a0e59c531 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 e8cbb41ae..da7fd2a3a 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,7 +170,7 @@ 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; @@ -288,7 +288,7 @@ void initServer() bool isConfig = false; if (!requestJSONBufferLock(14)) { - serveJsonError(request, 503, ERR_NOBUF); + request->deferResponse(); return; } @@ -359,7 +359,7 @@ 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); @@ -389,7 +389,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 +406,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 { diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 2a19cdfab..78d2d7d56 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -26,7 +26,7 @@ void XML_response(Print& dest) ); } -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 @@ -285,7 +285,7 @@ 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); @@ -369,10 +369,7 @@ 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); @@ -436,6 +433,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);