diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f77dc4de..2a8e4712 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -12,6 +12,21 @@ } }, + // To give the container access to a device serial port, you can uncomment one of the following lines. + // Note: If running on Windows, you will have to do some additional steps: + // https://stackoverflow.com/questions/68527888/how-can-i-use-a-usb-com-port-inside-of-a-vscode-development-container + // + // You can explicitly just forward the port you want to connect to. Replace `/dev/ttyACM0` with the serial port for + // your device. This will only work if the device is plugged in from the start without reconnecting. Adding the + // `dialout` group is needed if read/write permisions for the port are limitted to the dialout user. + // "runArgs": ["--device=/dev/ttyACM0", "--group-add", "dialout"], + // + // Alternatively, you can give more comprehensive access to the host system. This will expose all the host devices to + // the container. Adding the `dialout` group is needed if read/write permisions for the port are limitted to the + // dialout user. This could allow the container to modify unrelated serial devices, which would be a similar level of + // 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", diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index 811db619..00000000 --- a/.github/stale.yml +++ /dev/null @@ -1,20 +0,0 @@ -# Number of days of inactivity before an issue becomes stale -daysUntilStale: 120 -# Number of days of inactivity before a stale issue is closed -daysUntilClose: 7 -# Issues with these labels will never be considered stale -exemptLabels: - - pinned - - keep - - enhancement - - confirmed -# Label to use when marking an issue as stale -staleLabel: stale -# Comment to post when marking an issue as stale. Set to `false` to disable -markComment: > - Hey! This issue has been open for quite some time without any new comments now. - It will be closed automatically in a week if no further activity occurs. - - Thank you for using WLED! -# Comment to post when closing a stale issue. Set to `false` to disable -closeComment: false diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000..1f255716 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,30 @@ +name: 'Close stale issues and PRs' +on: + schedule: + - cron: '0 12 * * *' + workflow_dispatch: + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + days-before-stale: 120 + days-before-close: 7 + stale-issue-label: 'stale' + stale-pr-label: 'stale' + exempt-issue-labels: 'pinned,keep,enhancement,confirmed' + exempt-pr-labels: 'pinned,keep,enhancement,confirmed' + exempt-all-milestones: true + operations-per-run: 1000 + stale-issue-message: > + Hey! This issue has been open for quite some time without any new comments now. + It will be closed automatically in a week if no further activity occurs. + + Thank you for using WLED! ✨ + stale-pr-message: > + Hey! This pull request has been open for quite some time without any new comments now. + It will be closed automatically in a week if no further activity occurs. + + Thank you for contributing to WLED! ❤️ diff --git a/.github/workflows/wled-ci.yml b/.github/workflows/wled-ci.yml index 26d14d0f..1dcab26a 100644 --- a/.github/workflows/wled-ci.yml +++ b/.github/workflows/wled-ci.yml @@ -37,7 +37,7 @@ jobs: uses: actions/setup-node@v4 with: cache: 'npm' - - run: npm install + - run: npm ci - name: Cache PlatformIO uses: actions/cache@v4 with: @@ -45,7 +45,7 @@ jobs: ~/.platformio/.cache ~/.buildcache build_output - key: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}-${{ hashFiles('wled00/**') }} + key: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}-${{ hashFiles('wled00/**', 'usermods/**') }} restore-keys: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}- - name: Set up Python uses: actions/setup-python@v5 @@ -61,7 +61,7 @@ jobs: name: firmware-${{ matrix.environment }} path: | build_output/release/*.bin - build_output/release/*_ESP02.bin.gz + build_output/release/*_ESP02*.bin.gz release: name: Create Release runs-on: ubuntu-latest @@ -86,7 +86,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '20.x' cache: 'npm' diff --git a/.gitignore b/.gitignore index 8a2319a7..8f083e3f 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,4 @@ wled-update.sh /wled00/my_config.h /wled00/Release /wled00/wled00.ino.cpp -/wled00/html_*.h \ No newline at end of file +/wled00/html_*.h diff --git a/CHANGELOG.md b/CHANGELOG.md index e88af017..b2801b86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,163 @@ ## WLED changelog +#### Build 2406290 +- WLED 0.15.0-b4 release +- LED settings bus management update (WARNING only allow available outputs) +- Add ETH support for LILYGO-POE-Pro (#4030 by @rorosaurus) +- Update usermod_sn_photoresistor (#4017 by @xkvmoto) +- Several internal fixes and optimisations + - move LED_BUILTIN handling to BusManager class + - reduce max panels (web server limitation) + - edit WiFi TX power (ESP32) + - keep current ledmap ID in UI + - limit outputs in UI based on length + - wifi.ap addition to JSON Info (JSON API) + - relay pin init bugfix + - file editor button in UI + - ESP8266: update was restarting device on some occasions + - a bit of throttling in UI (for ESP8266) + +#### Build 2406120 +- Update NeoPixelBus to v2.8.0 +- Increased LED outputs one ESP32 using parallel I2S (up to 17) + - use single/mono I2S + 4x RMT for 5 outputs or less + - use parallel x8 I2S + 8x RMT for >5 outputs (limit of 300 LEDs per output) +- Fixed code of Smartnest and updated documentation (#4001 by @DevilPro1) +- ESP32-S3 WiFi fix (#4010 by @cstruck) +- TetrisAI usermod fix (#3897 by @muebau) +- ESP-NOW usermod hook +- Update wled.h regarding OTA Password (#3993 by @gsieben) +- Usermod BME68X Sensor Implementation (#3994 by @gsieben) +- Add a usermod for AHT10, AHT15 and AHT20 temperature/humidity sensors (#3977 by @LordMike) +- Update Battery usermod documentation (#3968 by @adamsthws) +- Add INA226 usermod for reading current and power over i2c (#3986 by @LordMike) +- Bugfixes: #3991 +- Several internal fixes and optimisations (WARNING: some effects may be broken that rely on overflow/narrow width) + - replace uint8_t and uint16_t with unsigned + - replace in8_t and int16_t with int + - reduces code by 1kB + +#### Build 2405180 +- WLED 0.14.4 release +- Fix for #3978 +- Official 0.15.0-b3 release +- Merge 0.14.3 fixes into 0_15 +- Added Pinwheel Expand 1D->2D effect mapping mode (#3961 by @Brandon502) +- Add changeable i2c address to BME280 usermod (#3966 by @LordMike) +- Effect: Firenoise - add palette selection +- Experimental parallel I2S support for ESP32 (compile time option) + - increased outputs to 17 + - increased max possible color order overrides + - use WLED_USE_PARALLEL_I2S during compile + WARNING: Do not set up more than 256 LEDs per output when using parallel I2S with NeoPixelBus less than 2.9.0 +- Update Usermod: Battery (#3964 by @adamsthws) +- Update Usermod: BME280 (#3965 by @LordMike) +- TM1914 chip support (#3913) +- Ignore brightness in Peek +- Antialiased line & circle drawing functions +- Enabled some audioreactive effects for single pixel strips/segments (#3942 by @gaaat98) +- Usermod Battery: Added Support for different battery types, Optimized file structure (#3003 by @itCarl) +- Skip playlist entry API (#3946 by @freakintoddles2) +- various optimisations and bugfixes (#3987, #3978) + +#### Build 2405030 +- Using brightness in analog clock overlay (#3944 by @paspiz85) +- Add Webpage shortcuts (#3945 by @w00000dy) +- ArtNet Poll reply (#3892 by @askask) +- Improved brightness change via long button presses (#3933 by @gaaat98) +- Relay open drain output (#3920 by @Suxsem) +- NEW JSON API: release info (update page, `info.release`) +- update esp32 platform to arduino-esp32 v2.0.9 (#3902) +- various optimisations and bugfixes (#3952, #3922, #3878, #3926, #3919, #3904 @DedeHai) + +#### Build 2404120 +- v0.15.0-b3 +- fix for #3896 & WS2815 current saving +- conditional compile for AA setPixelColor() + +#### Build 2404100 +- Internals: #3859, #3862, #3873, #3875 +- Prefer I2S1 over RMT on ESP32 +- usermod for Adafruit MAX17048 (#3667 by @ccruz09) +- Runtime detection of ESP32 PICO, general PSRAM support +- Extend JSON API "info" object + - add "clock" - CPU clock in MHz + - add "flash" - flash size in MB +- Fix for #3879 +- Analog PWM fix for ESP8266 (#3887 by @gaaat98) +- Fix for #3870 (#3880 by @DedeHai) +- ESP32 S3/S2 touch fix (#3798 by @DedeHai) +- PIO env. PSRAM fix for S3 & S3 with 4M flash + - audioreactive always included for S3 & S2 +- Fix for #3889 +- BREAKING: Effect: modified KITT (Scanner) (#3763) + +#### Build 2404040 +- WLED 0.14.3 release +- Fix for transition 0 (#3854, #3832, #3720) +- Fix for #3855 via #3873 (by @willmmiles) + +#### Build 2403280 +- Individual color channel control for JSON API (fixes #3860) + - "col":[int|string|object|array, int|string|object|array, int|string|object|array] + int = Kelvin temperature or 0 for black + string = hex representation of [WW]RRGGBB + object = individual channel control {"r":0,"g":127,"b":255,"w":255}, each being optional (valid to send {}) + array = direct channel values [r,g,b,w] (w element being optional) +- runtime selection for CCT IC (Athom 15W bulb) +- #3850 (by @w00000dy) +- Rotary encoder palette count bugfix +- bugfixes and optimisations + +#### Build 2403240 +- v0.15.0-b2 +- WS2805 support (RGB + WW + CW, 600kbps) +- Unified PSRAM use +- NeoPixelBus v2.7.9 +- 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"}]}`) +- FW1906 Support (#3810 by @deece and @Robert-github-com) +- ESPAsyncWebServer 2.2.0 (#3828 by @willmmiles) +- Bugfixes: #3843, #3844 + +#### Build 2403190 +- limit max PWM frequency (fix incorrect PWM resolution) +- Segment UI bugfix +- Updated AsyncWebServer (by @wlillmmiles) +- Simpler boot preset (fix for #3806) +- Effect: Fix for 2D Drift animation (#3816 by @BaptisteHudyma) +- Effect: Add twin option to 2D Drift +- MQTT cleanup +- DDP: Support sources that don't push (#3833 by @willmmiles) +- Usermod: Tetris AI usermod (#3711 by @muebau) + +#### Build 2403171 +- merge 0.14.2 changes into 0.15 + +#### Build 2403070 +- Add additional segment options when controlling over e1.31 (#3616 by @demophoon) +- LockedJsonResponse: Release early if possible (#3760 by @willmmiles) +- Update setup-node and cache usermods in wled-ci.yml (#3737 by @WoodyLetsCode) +- Fix preset sorting (#3790 by @WoodyLetsCode) +- compile time button configuration #3792 +- remove IR config if not compiled +- additional string optimisations +- Better low brightness level PWM handling (fixes #2767, #2868) + +#### Build 2402290 +- Multiple analog button fix for #3549 +- Preset caching on chips with PSRAM (credit @akaricchi) +- Fixing stairway usermod and adding buildflags (by @lost-hope) +- ESP-NOW packet modification +- JSON buffer lock error messages / Reduce wait time for lock to 100ms +- Reduce string RAM usage for ESP8266 +- Fixing a potential array bounds violation in ESPDMX +- Move timezone table to PROGMEM (#3766 by @willmmiles) +- Reposition upload warning message. (fixes #3778) +- ABL display fix & optimisation +- Add virtual Art-Net RGBW option (#3783 by @shammy642) + #### Build 2402090 - Added new Ethernet controller RGB2Go Tetra (duplicate of ESP3DEUXQuattro) - Usermod: httpPullLightControl (#3560 by @roelbroersma) @@ -15,7 +173,7 @@ #### Build 2309120 till build 2402010 - WLED version 0.15.0-a0 -- Multi-WiFi support. Add up to 3 (or more via cusom compile) WiFis to connect to +- Multi-WiFi support. Add up to 3 (or more via cusom compile) WiFis to connect to (with help from @JPZV) - Temporary AP. Use your WLED in public with temporary AP. - Github CI build system enhancements (#3718 by @WoodyLetsCode) - Accessibility: Node list ( #3715 by @WoodyLetsCode) @@ -97,6 +255,26 @@ - send UDP/WS on segment change - pop_back() when removing last segment +#### Build 2403170 +- WLED 0.14.2 release + +#### Build 2403110 +- Beta WLED 0.14.2-b2 +- New AsyncWebServer (improved performance and reduced memory use) +- New builds for ESP8266 with 160MHz CPU clock +- Fixing stairway usermod and adding buildflags (#3758 by @lost-hope) +- Fixing a potential array bounds violation in ESPDMX +- Reduced RAM usage (moved strings and TZ data (by @willmmiles) to PROGMEM) +- LockedJsonResponse: Release early if possible (by @willmmiles) + +#### Build 2402120 +- Beta WLED 0.14.2-b1 +- Possible fix for #3589 & partial fix for #3605 +- Prevent JSON buffer clear after failed lock attempt +- Multiple analog button fix for #3549 +- UM Audioreactive: add two compiler options (#3732 by @wled-install) +- Fix for #3693 + #### Build 2401141 - Official release of WLED 0.14.1 - Fix for #3566, #3665, #3672 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 560a7097..cd50b133 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,20 @@ Here are a few suggestions to make it easier for you to contribute! +### Describe your PR + +Please add a description of your proposed code changes. It does not need to be an exhaustive essay, however a PR with no description or just a few words might not get accepted, simply because very basic information is missing. + +A good description helps us to review and understand your proposed changes. For example, you could say a few words about +* what you try to achieve (new feature, fixing a bug, refactoring, security enhancements, etc.) +* how your code works (short technical summary - focus on important aspects that might not be obvious when reading the code) +* testing you performed, known limitations, open ends you possibly could not solve. +* any areas where you like to get help from an experienced maintainer (yes WLED has become big 😉) + +### Target branch for pull requests + +Please make all PRs against the `0_15` branch. + ### Code style When in doubt, it is easiest to replicate the code style you find in the files you want to edit :) @@ -14,7 +28,7 @@ You are all set if you have enabled `Editor: Detect Indentation` in VS Code. #### Blocks -Whether the opening bracket of e.g. an `if` block is in the same line as the condition or in a separate line is up to your discretion. If there is only one statement, leaving out block braches is acceptable. +Whether the opening bracket of e.g. an `if` block is in the same line as the condition or in a separate line is up to your discretion. If there is only one statement, leaving out block brackets is acceptable. Good: ```cpp @@ -35,7 +49,7 @@ if (a == b) doStuff(a); ``` There should always be a space between a keyword and its condition and between the condition and brace. -Within the condition, no space should be between the paranthesis and variables. +Within the condition, no space should be between the parenthesis and variables. Spaces between variables and operators are up to the authors discretion. There should be no space between function names and their argument parenthesis. @@ -73,6 +87,6 @@ Good: ``` -There is no set character limit for a comment within a line, -though as a rule of thumb you should wrap your comment if it exceeds the width of your editor window. +There is no hard character limit for a comment within a line, +though as a rule of thumb consider wrapping after 120 characters. Inline comments are OK if they describe that line only and are not exceedingly wide. \ No newline at end of file diff --git a/lib/ESP8266PWM/src/core_esp8266_waveform_pwm.cpp b/lib/ESP8266PWM/src/core_esp8266_waveform_pwm.cpp new file mode 100644 index 00000000..78c7160d --- /dev/null +++ b/lib/ESP8266PWM/src/core_esp8266_waveform_pwm.cpp @@ -0,0 +1,717 @@ +/* esp8266_waveform imported from platform source code + Modified for WLED to work around a fault in the NMI handling, + which can result in the system locking up and hard WDT crashes. + + Imported from https://github.com/esp8266/Arduino/blob/7e0d20e2b9034994f573a236364e0aef17fd66de/cores/esp8266/core_esp8266_waveform_pwm.cpp +*/ + +/* + esp8266_waveform - General purpose waveform generation and control, + supporting outputs on all pins in parallel. + + Copyright (c) 2018 Earle F. Philhower, III. All rights reserved. + + The core idea is to have a programmable waveform generator with a unique + high and low period (defined in microseconds or CPU clock cycles). TIMER1 + is set to 1-shot mode and is always loaded with the time until the next + edge of any live waveforms. + + Up to one waveform generator per pin supported. + + Each waveform generator is synchronized to the ESP clock cycle counter, not + the timer. This allows for removing interrupt jitter and delay as the + counter always increments once per 80MHz clock. Changes to a waveform are + contiguous and only take effect on the next waveform transition, + allowing for smooth transitions. + + This replaces older tone(), analogWrite(), and the Servo classes. + + Everywhere in the code where "cycles" is used, it means ESP.getCycleCount() + clock cycle count, or an interval measured in CPU clock cycles, but not + TIMER1 cycles (which may be 2 CPU clock cycles @ 160MHz). + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + + +#include +#include +#include "ets_sys.h" +#include "core_esp8266_waveform.h" +#include "user_interface.h" + +extern "C" { + +// Linker magic +void usePWMFixedNMI() {}; + +// Maximum delay between IRQs +#define MAXIRQUS (10000) + +// Waveform generator can create tones, PWM, and servos +typedef struct { + uint32_t nextServiceCycle; // ESP cycle timer when a transition required + uint32_t expiryCycle; // For time-limited waveform, the cycle when this waveform must stop + uint32_t timeHighCycles; // Actual running waveform period (adjusted using desiredCycles) + uint32_t timeLowCycles; // + uint32_t desiredHighCycles; // Ideal waveform period to drive the error signal + uint32_t desiredLowCycles; // + uint32_t lastEdge; // Cycle when this generator last changed +} Waveform; + +class WVFState { +public: + Waveform waveform[17]; // State of all possible pins + uint32_t waveformState = 0; // Is the pin high or low, updated in NMI so no access outside the NMI code + uint32_t waveformEnabled = 0; // Is it actively running, updated in NMI so no access outside the NMI code + + // Enable lock-free by only allowing updates to waveformState and waveformEnabled from IRQ service routine + uint32_t waveformToEnable = 0; // Message to the NMI handler to start a waveform on a inactive pin + uint32_t waveformToDisable = 0; // Message to the NMI handler to disable a pin from waveform generation + + uint32_t waveformToChange = 0; // Mask of pin to change. One bit set in main app, cleared when effected in the NMI + uint32_t waveformNewHigh = 0; + uint32_t waveformNewLow = 0; + + uint32_t (*timer1CB)() = NULL; + + // Optimize the NMI inner loop by keeping track of the min and max GPIO that we + // are generating. In the common case (1 PWM) these may be the same pin and + // we can avoid looking at the other pins. + uint16_t startPin = 0; + uint16_t endPin = 0; +}; +static WVFState wvfState; + + +// Ensure everything is read/written to RAM +#define MEMBARRIER() { __asm__ volatile("" ::: "memory"); } + +// Non-speed critical bits +#pragma GCC optimize ("Os") + +// Interrupt on/off control +static IRAM_ATTR void timer1Interrupt(); +static bool timerRunning = false; + +static __attribute__((noinline)) void initTimer() { + if (!timerRunning) { + timer1_disable(); + ETS_FRC_TIMER1_INTR_ATTACH(NULL, NULL); + ETS_FRC_TIMER1_NMI_INTR_ATTACH(timer1Interrupt); + timer1_enable(TIM_DIV1, TIM_EDGE, TIM_SINGLE); + timerRunning = true; + timer1_write(microsecondsToClockCycles(10)); + } +} + +static IRAM_ATTR void forceTimerInterrupt() { + if (T1L > microsecondsToClockCycles(10)) { + T1L = microsecondsToClockCycles(10); + } +} + +// PWM implementation using special purpose state machine +// +// Keep an ordered list of pins with the delta in cycles between each +// element, with a terminal entry making up the remainder of the PWM +// period. With this method sum(all deltas) == PWM period clock cycles. +// +// At t=0 set all pins high and set the timeout for the 1st edge. +// On interrupt, if we're at the last element reset to t=0 state +// Otherwise, clear that pin down and set delay for next element +// and so forth. + +constexpr int maxPWMs = 8; + +// PWM machine state +typedef struct PWMState { + uint32_t mask; // Bitmask of active pins + uint32_t cnt; // How many entries + uint32_t idx; // Where the state machine is along the list + uint8_t pin[maxPWMs + 1]; + uint32_t delta[maxPWMs + 1]; + uint32_t nextServiceCycle; // Clock cycle for next step + struct PWMState *pwmUpdate; // Set by main code, cleared by ISR +} PWMState; + +static PWMState pwmState; +static uint32_t _pwmFreq = 1000; +static uint32_t _pwmPeriod = microsecondsToClockCycles(1000000UL) / _pwmFreq; + + +// If there are no more scheduled activities, shut down Timer 1. +// Otherwise, do nothing. +static IRAM_ATTR void disableIdleTimer() { + if (timerRunning && !wvfState.waveformEnabled && !pwmState.cnt && !wvfState.timer1CB) { + ETS_FRC_TIMER1_NMI_INTR_ATTACH(NULL); + timer1_disable(); + timer1_isr_init(); + timerRunning = false; + } +} + +// Notify the NMI that a new PWM state is available through the mailbox. +// Wait for mailbox to be emptied (either busy or delay() as needed) +static IRAM_ATTR void _notifyPWM(PWMState *p, bool idle) { + p->pwmUpdate = nullptr; + pwmState.pwmUpdate = p; + MEMBARRIER(); + forceTimerInterrupt(); + while (pwmState.pwmUpdate) { + if (idle) { + esp_yield(); + } + MEMBARRIER(); + } +} + +static void _addPWMtoList(PWMState &p, int pin, uint32_t val, uint32_t range); + + +// Called when analogWriteFreq() changed to update the PWM total period +//extern void _setPWMFreq_weak(uint32_t freq) __attribute__((weak)); +void _setPWMFreq_weak(uint32_t freq) { + _pwmFreq = freq; + + // Convert frequency into clock cycles + uint32_t cc = microsecondsToClockCycles(1000000UL) / freq; + + // Simple static adjustment to bring period closer to requested due to overhead + // Empirically determined as a constant PWM delay and a function of the number of PWMs +#if F_CPU == 80000000 + cc -= ((microsecondsToClockCycles(pwmState.cnt) * 13) >> 4) + 110; +#else + cc -= ((microsecondsToClockCycles(pwmState.cnt) * 10) >> 4) + 75; +#endif + + if (cc == _pwmPeriod) { + return; // No change + } + + _pwmPeriod = cc; + + if (pwmState.cnt) { + PWMState p; // The working copy since we can't edit the one in use + p.mask = 0; + p.cnt = 0; + for (uint32_t i = 0; i < pwmState.cnt; i++) { + auto pin = pwmState.pin[i]; + _addPWMtoList(p, pin, wvfState.waveform[pin].desiredHighCycles, wvfState.waveform[pin].desiredLowCycles); + } + // Update and wait for mailbox to be emptied + initTimer(); + _notifyPWM(&p, true); + disableIdleTimer(); + } +} +/* +static void _setPWMFreq_bound(uint32_t freq) __attribute__((weakref("_setPWMFreq_weak"))); +void _setPWMFreq(uint32_t freq) { + _setPWMFreq_bound(freq); +} +*/ + +// Helper routine to remove an entry from the state machine +// and clean up any marked-off entries +static void _cleanAndRemovePWM(PWMState *p, int pin) { + uint32_t leftover = 0; + uint32_t in, out; + for (in = 0, out = 0; in < p->cnt; in++) { + if ((p->pin[in] != pin) && (p->mask & (1<pin[in]))) { + p->pin[out] = p->pin[in]; + p->delta[out] = p->delta[in] + leftover; + leftover = 0; + out++; + } else { + leftover += p->delta[in]; + p->mask &= ~(1<pin[in]); + } + } + p->cnt = out; + // Final pin is never used: p->pin[out] = 0xff; + p->delta[out] = p->delta[in] + leftover; +} + + +// Disable PWM on a specific pin (i.e. when a digitalWrite or analogWrite(0%/100%)) +//extern bool _stopPWM_weak(uint8_t pin) __attribute__((weak)); +IRAM_ATTR bool _stopPWM_weak(uint8_t pin) { + if (!((1<= _pwmPeriod) { + cc = _pwmPeriod - 1; + } + + if (p.cnt == 0) { + // Starting up from scratch, special case 1st element and PWM period + p.pin[0] = pin; + p.delta[0] = cc; + // Final pin is never used: p.pin[1] = 0xff; + p.delta[1] = _pwmPeriod - cc; + } else { + uint32_t ttl = 0; + uint32_t i; + // Skip along until we're at the spot to insert + for (i=0; (i <= p.cnt) && (ttl + p.delta[i] < cc); i++) { + ttl += p.delta[i]; + } + // Shift everything out by one to make space for new edge + for (int32_t j = p.cnt; j >= (int)i; j--) { + p.pin[j + 1] = p.pin[j]; + p.delta[j + 1] = p.delta[j]; + } + int off = cc - ttl; // The delta from the last edge to the one we're inserting + p.pin[i] = pin; + p.delta[i] = off; // Add the delta to this new pin + p.delta[i + 1] -= off; // And subtract it from the follower to keep sum(deltas) constant + } + p.cnt++; + p.mask |= 1<= maxPWMs) { + return false; // No space left + } + + // Sanity check for all-on/off + uint32_t cc = (_pwmPeriod * val) / range; + if ((cc == 0) || (cc >= _pwmPeriod)) { + digitalWrite(pin, cc ? HIGH : LOW); + return true; + } + + _addPWMtoList(p, pin, val, range); + + // Set mailbox and wait for ISR to copy it over + initTimer(); + _notifyPWM(&p, true); + disableIdleTimer(); + + // Potentially recalculate the PWM period if we've added another pin + _setPWMFreq(_pwmFreq); + + return true; +} +/* +static bool _setPWM_bound(int pin, uint32_t val, uint32_t range) __attribute__((weakref("_setPWM_weak"))); +bool _setPWM(int pin, uint32_t val, uint32_t range) { + return _setPWM_bound(pin, val, range); +} +*/ + +// Start up a waveform on a pin, or change the current one. Will change to the new +// waveform smoothly on next low->high transition. For immediate change, stopWaveform() +// first, then it will immediately begin. +//extern int startWaveformClockCycles_weak(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles, int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) __attribute__((weak)); +int startWaveformClockCycles_weak(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles, + int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) { + (void) alignPhase; + (void) phaseOffsetUS; + (void) autoPwm; + + if ((pin > 16) || isFlashInterfacePin(pin) || (timeHighCycles == 0)) { + return false; + } + Waveform *wave = &wvfState.waveform[pin]; + wave->expiryCycle = runTimeCycles ? ESP.getCycleCount() + runTimeCycles : 0; + if (runTimeCycles && !wave->expiryCycle) { + wave->expiryCycle = 1; // expiryCycle==0 means no timeout, so avoid setting it + } + + _stopPWM(pin); // Make sure there's no PWM live here + + uint32_t mask = 1<timeHighCycles = timeHighCycles; + wave->desiredHighCycles = timeHighCycles; + wave->timeLowCycles = timeLowCycles; + wave->desiredLowCycles = timeLowCycles; + wave->lastEdge = 0; + wave->nextServiceCycle = ESP.getCycleCount() + microsecondsToClockCycles(1); + wvfState.waveformToEnable |= mask; + MEMBARRIER(); + initTimer(); + forceTimerInterrupt(); + while (wvfState.waveformToEnable) { + esp_yield(); // Wait for waveform to update + MEMBARRIER(); + } + } + + return true; +} +/* +static int startWaveformClockCycles_bound(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles, int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) __attribute__((weakref("startWaveformClockCycles_weak"))); +int startWaveformClockCycles(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles, int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) { + return startWaveformClockCycles_bound(pin, timeHighCycles, timeLowCycles, runTimeCycles, alignPhase, phaseOffsetUS, autoPwm); +} + + +// This version falls-thru to the proper startWaveformClockCycles call and is invariant across waveform generators +int startWaveform(uint8_t pin, uint32_t timeHighUS, uint32_t timeLowUS, uint32_t runTimeUS, + int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) { + return startWaveformClockCycles_bound(pin, + microsecondsToClockCycles(timeHighUS), microsecondsToClockCycles(timeLowUS), + microsecondsToClockCycles(runTimeUS), alignPhase, microsecondsToClockCycles(phaseOffsetUS), autoPwm); +} +*/ + +// Set a callback. Pass in NULL to stop it +//extern void setTimer1Callback_weak(uint32_t (*fn)()) __attribute__((weak)); +void setTimer1Callback_weak(uint32_t (*fn)()) { + wvfState.timer1CB = fn; + if (fn) { + initTimer(); + forceTimerInterrupt(); + } + disableIdleTimer(); +} +/* +static void setTimer1Callback_bound(uint32_t (*fn)()) __attribute__((weakref("setTimer1Callback_weak"))); +void setTimer1Callback(uint32_t (*fn)()) { + setTimer1Callback_bound(fn); +} +*/ + +// Stops a waveform on a pin +//extern int stopWaveform_weak(uint8_t pin) __attribute__((weak)); +IRAM_ATTR int stopWaveform_weak(uint8_t pin) { + // Can't possibly need to stop anything if there is no timer active + if (!timerRunning) { + return false; + } + // If user sends in a pin >16 but <32, this will always point to a 0 bit + // If they send >=32, then the shift will result in 0 and it will also return false + uint32_t mask = 1<= (uintptr_t) &_UserExceptionVector_1)) { + // Address is good; save backup + epc3_backup = epc3; + eps3_backup = eps3; + } else { + // Address is inside the NMI handler -- restore from backup + __asm__ __volatile__("wsr %0,epc3; wsr %1,eps3"::"a"(epc3_backup),"a"(eps3_backup)); + } +} +// ----- @willmmiles end patch ----- + + +// The SDK and hardware take some time to actually get to our NMI code, so +// decrement the next IRQ's timer value by a bit so we can actually catch the +// real CPU cycle counter we want for the waveforms. + +// The SDK also sometimes is running at a different speed the the Arduino core +// so the ESP cycle counter is actually running at a variable speed. +// adjust(x) takes care of adjusting a delta clock cycle amount accordingly. +#if F_CPU == 80000000 + #define DELTAIRQ (microsecondsToClockCycles(9)/4) + #define adjust(x) ((x) << (turbo ? 1 : 0)) +#else + #define DELTAIRQ (microsecondsToClockCycles(9)/8) + #define adjust(x) ((x) >> 0) +#endif + +// When the time to the next edge is greater than this, RTI and set another IRQ to minimize CPU usage +#define MINIRQTIME microsecondsToClockCycles(6) + +static IRAM_ATTR void timer1Interrupt() { + // ----- @willmmiles begin patch ----- + nmiCrashWorkaround(); + // ----- @willmmiles end patch ----- + + // Flag if the core is at 160 MHz, for use by adjust() + bool turbo = (*(uint32_t*)0x3FF00014) & 1 ? true : false; + + uint32_t nextEventCycle = GetCycleCountIRQ() + microsecondsToClockCycles(MAXIRQUS); + uint32_t timeoutCycle = GetCycleCountIRQ() + microsecondsToClockCycles(14); + + if (wvfState.waveformToEnable || wvfState.waveformToDisable) { + // Handle enable/disable requests from main app + wvfState.waveformEnabled = (wvfState.waveformEnabled & ~wvfState.waveformToDisable) | wvfState.waveformToEnable; // Set the requested waveforms on/off + wvfState.waveformState &= ~wvfState.waveformToEnable; // And clear the state of any just started + wvfState.waveformToEnable = 0; + wvfState.waveformToDisable = 0; + // No mem barrier. Globals must be written to RAM on ISR exit. + // Find the first GPIO being generated by checking GCC's find-first-set (returns 1 + the bit of the first 1 in an int32_t) + wvfState.startPin = __builtin_ffs(wvfState.waveformEnabled) - 1; + // Find the last bit by subtracting off GCC's count-leading-zeros (no offset in this one) + wvfState.endPin = 32 - __builtin_clz(wvfState.waveformEnabled); + } else if (!pwmState.cnt && pwmState.pwmUpdate) { + // Start up the PWM generator by copying from the mailbox + pwmState.cnt = 1; + pwmState.idx = 1; // Ensure copy this cycle, cause it to start at t=0 + pwmState.nextServiceCycle = GetCycleCountIRQ(); // Do it this loop! + // No need for mem barrier here. Global must be written by IRQ exit + } + + bool done = false; + if (wvfState.waveformEnabled || pwmState.cnt) { + do { + nextEventCycle = GetCycleCountIRQ() + microsecondsToClockCycles(MAXIRQUS); + + // PWM state machine implementation + if (pwmState.cnt) { + int32_t cyclesToGo; + do { + cyclesToGo = pwmState.nextServiceCycle - GetCycleCountIRQ(); + if (cyclesToGo < 0) { + if (pwmState.idx == pwmState.cnt) { // Start of pulses, possibly copy new + if (pwmState.pwmUpdate) { + // Do the memory copy from temp to global and clear mailbox + pwmState = *(PWMState*)pwmState.pwmUpdate; + } + GPOS = pwmState.mask; // Set all active pins high + if (pwmState.mask & (1<<16)) { + GP16O = 1; + } + pwmState.idx = 0; + } else { + do { + // Drop the pin at this edge + if (pwmState.mask & (1<expiryCycle) { + int32_t expiryToGo = wave->expiryCycle - now; + if (expiryToGo < 0) { + // Done, remove! + if (i == 16) { + GP16O = 0; + } + GPOC = mask; + wvfState.waveformEnabled &= ~mask; + continue; + } + } + + // Check for toggles + int32_t cyclesToGo = wave->nextServiceCycle - now; + if (cyclesToGo < 0) { + uint32_t nextEdgeCycles; + uint32_t desired = 0; + uint32_t *timeToUpdate; + wvfState.waveformState ^= mask; + if (wvfState.waveformState & mask) { + if (i == 16) { + GP16O = 1; + } + GPOS = mask; + + if (wvfState.waveformToChange & mask) { + // Copy over next full-cycle timings + wave->timeHighCycles = wvfState.waveformNewHigh; + wave->desiredHighCycles = wvfState.waveformNewHigh; + wave->timeLowCycles = wvfState.waveformNewLow; + wave->desiredLowCycles = wvfState.waveformNewLow; + wave->lastEdge = 0; + wvfState.waveformToChange = 0; + } + if (wave->lastEdge) { + desired = wave->desiredLowCycles; + timeToUpdate = &wave->timeLowCycles; + } + nextEdgeCycles = wave->timeHighCycles; + } else { + if (i == 16) { + GP16O = 0; + } + GPOC = mask; + desired = wave->desiredHighCycles; + timeToUpdate = &wave->timeHighCycles; + nextEdgeCycles = wave->timeLowCycles; + } + if (desired) { + desired = adjust(desired); + int32_t err = desired - (now - wave->lastEdge); + if (abs(err) < desired) { // If we've lost > the entire phase, ignore this error signal + err /= 2; + *timeToUpdate += err; + } + } + nextEdgeCycles = adjust(nextEdgeCycles); + wave->nextServiceCycle = now + nextEdgeCycles; + wave->lastEdge = now; + } + nextEventCycle = earliest(nextEventCycle, wave->nextServiceCycle); + } + + // Exit the loop if we've hit the fixed runtime limit or the next event is known to be after that timeout would occur + uint32_t now = GetCycleCountIRQ(); + int32_t cycleDeltaNextEvent = nextEventCycle - now; + int32_t cyclesLeftTimeout = timeoutCycle - now; + done = (cycleDeltaNextEvent > MINIRQTIME) || (cyclesLeftTimeout < 0); + } while (!done); + } // if (wvfState.waveformEnabled) + + if (wvfState.timer1CB) { + nextEventCycle = earliest(nextEventCycle, GetCycleCountIRQ() + wvfState.timer1CB()); + } + + int32_t nextEventCycles = nextEventCycle - GetCycleCountIRQ(); + + if (nextEventCycles < MINIRQTIME) { + nextEventCycles = MINIRQTIME; + } + nextEventCycles -= DELTAIRQ; + + // Do it here instead of global function to save time and because we know it's edge-IRQ + T1L = nextEventCycles >> (turbo ? 1 : 0); +} + +}; diff --git a/package-lock.json b/package-lock.json index 99e3efc3..38f2099d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,57 +1,56 @@ { "name": "wled", - "version": "0.15.0-b1", + "version": "0.15.0-b4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wled", - "version": "0.15.0-b1", + "version": "0.15.0-b4", "license": "ISC", "dependencies": { "clean-css": "^5.3.3", "html-minifier-terser": "^7.2.0", "inliner": "^1.13.1", - "nodemon": "^3.0.2", - "zlib": "^1.0.5" + "nodemon": "^3.0.2" } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", - "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, "node_modules/@jridgewell/sourcemap-codec": { @@ -60,9 +59,9 @@ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.21", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.21.tgz", - "integrity": "sha512-SRfKmRe1KvYnxjEMtxEr+J4HIeMX5YBg/qhRHpxEIGjhX1rshcHlnFUE9K0GazhVKWM7B+nARSkV8LuvJdJ5/g==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -209,11 +208,14 @@ } }, "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/boolbase": { @@ -324,15 +326,9 @@ "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -345,6 +341,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -1376,9 +1375,9 @@ } }, "node_modules/nodemon": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.3.tgz", - "integrity": "sha512-7jH/NXbFPxVaMwmBCC2B9F/V6X1VkEdNgx3iu9jji8WxWcvhMWkmhNWhI5077zknOnZnBzba9hZP6bCPJLSReQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.0.tgz", + "integrity": "sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==", "dependencies": { "chokidar": "^3.5.2", "debug": "^4", @@ -1827,9 +1826,9 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -1925,9 +1924,9 @@ } }, "node_modules/stream-shift": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.2.tgz", - "integrity": "sha512-rV4Bovi9xx0BFzOb/X0B2GqoIjvqPCttZdu0Wgtx2Dxkj7ETyWl9gmqJ4EutWRLvtZWm8dxE+InQZX1IryZn/w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==" }, "node_modules/string_decoder": { "version": "0.10.31", @@ -1994,9 +1993,9 @@ } }, "node_modules/terser": { - "version": "5.27.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", - "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==", + "version": "5.29.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.2.tgz", + "integrity": "sha512-ZiGkhUBIM+7LwkNjXYJq8svgkd+QK3UUr0wJqY4MieaezBSAIPgbSPZyIx0idM6XWK5CMzSWa8MJIzmRcB8Caw==", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -2245,15 +2244,6 @@ "decamelize": "^1.0.0", "window-size": "0.1.0" } - }, - "node_modules/zlib": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/zlib/-/zlib-1.0.5.tgz", - "integrity": "sha512-40fpE2II+Cd3k8HWTWONfeKE2jL+P42iWJ1zzps5W51qcTsOUKM5Q5m2PFb0CLxlmFAaUuUdJGc3OfZy947v0w==", - "hasInstallScript": true, - "engines": { - "node": ">=0.2.0" - } } } } diff --git a/package.json b/package.json index f2c0e3d6..e3c12629 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wled", - "version": "0.15.0-b1", + "version": "0.15.0-b4", "description": "Tools for WLED project", "main": "tools/cdata.js", "directories": { @@ -26,7 +26,6 @@ "clean-css": "^5.3.3", "html-minifier-terser": "^7.2.0", "inliner": "^1.13.1", - "nodemon": "^3.0.2", - "zlib": "^1.0.5" + "nodemon": "^3.0.2" } } diff --git a/pio-scripts/obj-dump.py b/pio-scripts/obj-dump.py index 91bc3de5..174df509 100644 --- a/pio-scripts/obj-dump.py +++ b/pio-scripts/obj-dump.py @@ -1,9 +1,24 @@ # Little convenience script to get an object dump +# You may add "-S" to the objdump commandline (i.e. replace "-D -C " with "-d -S -C ") +# to get source code intermixed with disassembly (SLOW !) Import('env') def obj_dump_after_elf(source, target, env): + platform = env.PioPlatform() + board = env.BoardConfig() + mcu = board.get("build.mcu", "esp32") + print("Create firmware.asm") - env.Execute("xtensa-lx106-elf-objdump "+ "-D " + str(target[0]) + " > "+ "${PROGNAME}.asm") - + if mcu == "esp8266": + env.Execute("xtensa-lx106-elf-objdump "+ "-D -C " + str(target[0]) + " > "+ "$BUILD_DIR/${PROGNAME}.asm") + if mcu == "esp32": + env.Execute("xtensa-esp32-elf-objdump "+ "-D -C " + str(target[0]) + " > "+ "$BUILD_DIR/${PROGNAME}.asm") + if mcu == "esp32s2": + env.Execute("xtensa-esp32s2-elf-objdump "+ "-D -C " + str(target[0]) + " > "+ "$BUILD_DIR/${PROGNAME}.asm") + if mcu == "esp32s3": + env.Execute("xtensa-esp32s3-elf-objdump "+ "-D -C " + str(target[0]) + " > "+ "$BUILD_DIR/${PROGNAME}.asm") + if mcu == "esp32c3": + env.Execute("riscv32-esp-elf-objdump "+ "-D -C " + str(target[0]) + " > "+ "$BUILD_DIR/${PROGNAME}.asm") + env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", [obj_dump_after_elf]) diff --git a/pio-scripts/output_bins.py b/pio-scripts/output_bins.py index e12b11c2..63365400 100644 --- a/pio-scripts/output_bins.py +++ b/pio-scripts/output_bins.py @@ -4,6 +4,7 @@ import shutil import gzip OUTPUT_DIR = "build_output{}".format(os.path.sep) +#OUTPUT_DIR = os.path.join("build_output") def _get_cpp_define_value(env, define): define_list = [item[-1] for item in env["CPPDEFINES"] if item[0] == define] @@ -13,63 +14,52 @@ def _get_cpp_define_value(env, define): return None -def _create_dirs(dirs=["firmware", "map"]): - # check if output directories exist and create if necessary - if not os.path.isdir(OUTPUT_DIR): - os.mkdir(OUTPUT_DIR) - +def _create_dirs(dirs=["map", "release", "firmware"]): for d in dirs: - if not os.path.isdir("{}{}".format(OUTPUT_DIR, d)): - os.mkdir("{}{}".format(OUTPUT_DIR, d)) + os.makedirs(os.path.join(OUTPUT_DIR, d), exist_ok=True) def create_release(source): release_name = _get_cpp_define_value(env, "WLED_RELEASE_NAME") if release_name: - _create_dirs(["release"]) version = _get_cpp_define_value(env, "WLED_VERSION") - # get file extension of source file (.bin or .bin.gz) - ext = source.split(".", 1)[1] - release_file = "{}release{}WLED_{}_{}.{}".format(OUTPUT_DIR, os.path.sep, version, release_name, ext) + release_file = os.path.join(OUTPUT_DIR, "release", f"WLED_{version}_{release_name}.bin") + release_gz_file = release_file + ".gz" + print(f"Copying {source} to {release_file}") shutil.copy(source, release_file) + bin_gzip(release_file, release_gz_file) + else: + variant = env["PIOENV"] + bin_file = "{}firmware{}{}.bin".format(OUTPUT_DIR, os.path.sep, variant) + print(f"Copying {source} to {bin_file}") + shutil.copy(source, bin_file) def bin_rename_copy(source, target, env): _create_dirs() variant = env["PIOENV"] + builddir = os.path.join(env["PROJECT_BUILD_DIR"], variant) + source_map = os.path.join(builddir, env["PROGNAME"] + ".map") # create string with location and file names based on variant map_file = "{}map{}{}.map".format(OUTPUT_DIR, os.path.sep, variant) - bin_file = "{}firmware{}{}.bin".format(OUTPUT_DIR, os.path.sep, variant) - # check if new target files exist and remove if necessary - for f in [map_file, bin_file]: - if os.path.isfile(f): - os.remove(f) - - # copy firmware.bin to firmware/.bin - shutil.copy(str(target[0]), bin_file) - - create_release(bin_file) + create_release(str(target[0])) # copy firmware.map to map/.map if os.path.isfile("firmware.map"): - shutil.move("firmware.map", map_file) + print("Found linker mapfile firmware.map") + shutil.copy("firmware.map", map_file) + if os.path.isfile(source_map): + print(f"Found linker mapfile {source_map}") + shutil.copy(source_map, map_file) -def bin_gzip(source, target, env): - _create_dirs() - variant = env["PIOENV"] - - # create string with location and file names based on variant - bin_file = "{}firmware{}{}.bin".format(OUTPUT_DIR, os.path.sep, variant) - gzip_file = "{}firmware{}{}.bin.gz".format(OUTPUT_DIR, os.path.sep, variant) - - # check if new target files exist and remove if necessary - if os.path.isfile(gzip_file): os.remove(gzip_file) - - # write gzip firmware file - with open(bin_file,"rb") as fp: - with gzip.open(gzip_file, "wb", compresslevel = 9) as f: +def bin_gzip(source, target): + # only create gzip for esp8266 + if not env["PIOPLATFORM"] == "espressif8266": + return + + print(f"Creating gzip file {target} from {source}") + with open(source,"rb") as fp: + with gzip.open(target, "wb", compresslevel = 9) as f: shutil.copyfileobj(fp, f) - create_release(gzip_file) - -env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", [bin_rename_copy, bin_gzip]) +env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", bin_rename_copy) diff --git a/platformio.ini b/platformio.ini index d8ead510..ecb118c1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -10,7 +10,7 @@ # ------------------------------------------------------------------------------ # CI/release binaries -default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, esp32dev, esp32_eth, esp32dev_audioreactive, lolin_s2_mini, esp32c3dev, esp32s3dev_8MB, esp32s3dev_8MB_PSRAM_opi, esp32_wrover +default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, nodemcuv2_160, esp8266_2m_160, esp01_1m_full_160, esp32dev, esp32_eth, esp32dev_audioreactive, lolin_s2_mini, esp32c3dev, esp32s3dev_16MB_opi, esp32s3dev_8MB_opi, esp32s3_4M_qspi, esp32_wrover src_dir = ./wled00 data_dir = ./wled00/data @@ -41,14 +41,13 @@ arduino_core_git = https://github.com/platformio/platform-espressif8266#feature/ platform_wled_default = ${common.arduino_core_3_1_2} # We use 2.7.4.7 for all, includes PWM flicker fix and Wstring optimization #platform_packages = tasmota/framework-arduinoespressif8266 @ 3.20704.7 -platform_packages = platformio/framework-arduinoespressif8266 - platformio/toolchain-xtensa @ ~2.100300.220621 #2.40802.200502 +platform_packages = platformio/toolchain-xtensa @ ~2.100300.220621 #2.40802.200502 platformio/tool-esptool #@ ~1.413.0 platformio/tool-esptoolpy #@ ~1.30000.0 ## previous platform for 8266, in case of problems with the new one -## you'll need makuna/NeoPixelBus@ 2.6.9 for arduino_core_3_2_0, which does not support Ucs890x -;; platform_wled_default = ${common.arduino_core_3_2_0} +## you'll need makuna/NeoPixelBus@ 2.6.9 for arduino_core_3_0_2, which does not support Ucs890x +;; platform_wled_default = ${common.arduino_core_3_0_2} ;; platform_packages = tasmota/framework-arduinoespressif8266 @ 3.20704.7 ;; platformio/toolchain-xtensa @ ~2.40802.200502 ;; platformio/tool-esptool @ ~1.413.0 @@ -87,7 +86,6 @@ debug_flags = -D DEBUG=1 -D WLED_DEBUG # This reduces the OTA size with ~45KB, so it's especially useful on low memory boards (512k/1m). # ------------------------------------------------------------------------------ build_flags = - -Wno-attributes -DMQTT_MAX_PACKET_SIZE=1024 -DSECURE_CLIENT=SECURE_CLIENT_BEARSSL -DBEARSSL_SSL_BASIC @@ -105,10 +103,6 @@ build_flags = build_unflags = -build_flags_esp8266 = ${common.build_flags} ${esp8266.build_flags} -build_flags_esp32 = ${common.build_flags} ${esp32.build_flags} -build_flags_esp32_V4= ${common.build_flags} ${esp32_idf_V4.build_flags} - ldscript_1m128k = eagle.flash.1m128.ld ldscript_2m512k = eagle.flash.2m512.ld ldscript_2m1m = eagle.flash.2m1m.ld @@ -121,6 +115,7 @@ extra_scripts = post:pio-scripts/strip-floats.py pre:pio-scripts/user_config_copy.py pre:pio-scripts/build_ui.py + ; post:pio-scripts/obj-dump.py ;; convenience script to create a disassembly dump of the firmware (hardcore debugging) # ------------------------------------------------------------------------------ # COMMON SETTINGS: @@ -143,8 +138,9 @@ lib_compat_mode = strict lib_deps = fastled/FastLED @ 3.6.0 IRremoteESP8266 @ 2.8.2 - makuna/NeoPixelBus @ 2.7.5 - https://github.com/Aircoookie/ESPAsyncWebServer.git @ ~2.0.7 + makuna/NeoPixelBus @ 2.8.0 + #https://github.com/makuna/NeoPixelBus.git#CoreShaderBeta + https://github.com/Aircoookie/ESPAsyncWebServer.git @ 2.2.1 # for I2C interface ;Wire # ESP-NOW library @@ -164,6 +160,9 @@ lib_deps = #For ADS1115 sensor uncomment following ;adafruit/Adafruit BusIO @ 1.13.2 ;adafruit/Adafruit ADS1X15 @ 2.4.0 + #For MAX1704x Lipo Monitor / Fuel Gauge uncomment following + ; https://github.com/adafruit/Adafruit_BusIO @ 1.14.5 + ; https://github.com/adafruit/Adafruit_MAX1704X @ 1.0.2 #For MPU6050 IMU uncomment follwoing ;electroniccats/MPU6050 @1.0.1 # For -D USERMOD_ANIMARTRIX @@ -172,7 +171,7 @@ lib_deps = # SHT85 ;robtillaart/SHT85@~0.3.3 # Audioreactive usermod - ;https://github.com/kosme/arduinoFFT#develop @ ^1.9.2 + ;kosme/arduinoFFT @ 2.0.1 extra_scripts = ${scripts_defaults.extra_scripts} @@ -203,6 +202,7 @@ lib_deps = #https://github.com/lorol/LITTLEFS.git ESPAsyncTCP @ 1.2.2 ESPAsyncUDP + ESP8266PWM ${env.lib_deps} [esp32] @@ -216,14 +216,19 @@ build_flags = -g #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 +tiny_partitions = tools/WLED_ESP32_2MB_noOTA.csv default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv +extended_partitions = tools/WLED_ESP32_4MB_700k_FS.csv +big_partitions = tools/WLED_ESP32_4MB_256KB_FS.csv ;; 1.8MB firmware, 256KB filesystem, coredump support +large_partitions = tools/WLED_ESP32_8MB.csv +extreme_partitions = tools/WLED_ESP32_16MB_9MB_FS.csv lib_deps = https://github.com/lorol/LITTLEFS.git https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 ${env.lib_deps} # additional build flags for audioreactive -AR_build_flags = -D USERMOD_AUDIOREACTIVE -D UM_AUDIOREACTIVE_USE_NEW_FFT -AR_lib_deps = https://github.com/kosme/arduinoFFT#develop @ ^1.9.2 +AR_build_flags = -D USERMOD_AUDIOREACTIVE +AR_lib_deps = kosme/arduinoFFT @ 2.0.1 [esp32_idf_V4] ;; experimental build environment for ESP32 using ESP-IDF 4.4.x / arduino-esp32 v2.0.5 @@ -231,24 +236,21 @@ AR_lib_deps = https://github.com/kosme/arduinoFFT#develop @ ^1.9.2 ;; ;; please note that you can NOT update existing ESP32 installs with a "V4" build. Also updating by OTA will not work properly. ;; You need to completely erase your device (esptool erase_flash) first, then install the "V4" build from VSCode+platformio. -platform = espressif32@5.3.0 -platform_packages = +platform = espressif32@ ~6.3.2 +platform_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) build_flags = -g -Wshadow=compatible-local ;; emit warning in case a local variable "shadows" another local one -DARDUINO_ARCH_ESP32 -DESP32 - #-DCONFIG_LITTLEFS_FOR_IDF_3_2 -D CONFIG_ASYNC_TCP_USE_WDT=0 -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 -default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv lib_deps = https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 ${env.lib_deps} [esp32s2] ;; generic definitions for all ESP32-S2 boards -platform = espressif32@5.3.0 -platform_packages = -default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv +platform = espressif32@ ~6.3.2 +platform_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) build_flags = -g -DARDUINO_ARCH_ESP32 -DARDUINO_ARCH_ESP32S2 @@ -265,8 +267,8 @@ lib_deps = [esp32c3] ;; generic definitions for all ESP32-C3 boards -platform = espressif32@5.3.0 -platform_packages = +platform = espressif32@ ~6.3.2 +platform_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) build_flags = -g -DARDUINO_ARCH_ESP32 -DARDUINO_ARCH_ESP32C3 @@ -282,8 +284,8 @@ lib_deps = [esp32s3] ;; generic definitions for all ESP32-S3 boards -platform = espressif32@5.3.0 -platform_packages = +platform = espressif32@ ~6.3.2 +platform_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) build_flags = -g -DESP32 -DARDUINO_ARCH_ESP32 @@ -294,7 +296,6 @@ build_flags = -g -DCO ;; please make sure that the following flags are properly set (to 0 or 1) by your board.json, or included in your custom platformio_override.ini entry: ;; ARDUINO_USB_MODE, ARDUINO_USB_CDC_ON_BOOT - lib_deps = https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 ${env.lib_deps} @@ -310,45 +311,74 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 #-DWLED_DISABLE_2D +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=ESP8266 #-DWLED_DISABLE_2D lib_deps = ${esp8266.lib_deps} monitor_filters = esp8266_exception_decoder +[env:nodemcuv2_160] +extends = env:nodemcuv2 +board_build.f_cpu = 160000000L +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=ESP8266_160 #-DWLED_DISABLE_2D + [env:esp8266_2m] board = esp_wroom_02 platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP02 +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=ESP02 lib_deps = ${esp8266.lib_deps} +[env:esp8266_2m_160] +extends = env:esp8266_2m +board_build.f_cpu = 160000000L +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=ESP02_160 + [env:esp01_1m_full] board = esp01_1m platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_1m128k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP01 -D WLED_DISABLE_OTA - ; -D WLED_USE_UNREAL_MATH ;; may cause wrong sunset/sunrise times, but saves 7064 bytes FLASH and 975 bytes RAM +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=ESP01 -D WLED_DISABLE_OTA + ; -D WLED_USE_REAL_MATH ;; may fix wrong sunset/sunrise times, at the cost of 7064 bytes FLASH and 975 bytes RAM lib_deps = ${esp8266.lib_deps} +[env:esp01_1m_full_160] +extends = env:esp01_1m_full +board_build.f_cpu = 160000000L +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=ESP01_160 -D WLED_DISABLE_OTA + ; -D WLED_USE_REAL_MATH ;; may fix wrong sunset/sunrise times, at the cost of 7064 bytes FLASH and 975 bytes RAM + [env:esp32dev] board = esp32dev platform = ${esp32.platform} platform_packages = ${esp32.platform_packages} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32 #-D WLED_DISABLE_BROWNOUT_DET +build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=ESP32 #-D WLED_DISABLE_BROWNOUT_DET lib_deps = ${esp32.lib_deps} monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.default_partitions} +[env:esp32dev_8M] +board = esp32dev +platform = ${esp32_idf_V4.platform} +platform_packages = ${esp32_idf_V4.platform_packages} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=ESP32_8M #-D WLED_DISABLE_BROWNOUT_DET + ${esp32.AR_build_flags} +lib_deps = ${esp32_idf_V4.lib_deps} + ${esp32.AR_lib_deps} +monitor_filters = esp32_exception_decoder +board_build.partitions = ${esp32.large_partitions} +; board_build.f_flash = 80000000L + [env:esp32dev_audioreactive] board = esp32dev platform = ${esp32.platform} platform_packages = ${esp32.platform_packages} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_audioreactive #-D WLED_DISABLE_BROWNOUT_DET +build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=ESP32_audioreactive #-D WLED_DISABLE_BROWNOUT_DET ${esp32.AR_build_flags} lib_deps = ${esp32.lib_deps} ${esp32.AR_lib_deps} @@ -363,23 +393,26 @@ platform = ${esp32.platform} platform_packages = ${esp32.platform_packages} upload_speed = 921600 build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_Ethernet -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1 +build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=ESP32_Ethernet -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1 -D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only lib_deps = ${esp32.lib_deps} board_build.partitions = ${esp32.default_partitions} [env:esp32_wrover] -platform = ${esp32.platform} +extends = esp32_idf_V4 +platform = ${esp32_idf_V4.platform} +platform_packages = ${esp32_idf_V4.platform_packages} board = ttgo-t7-v14-mini32 board_build.f_flash = 80000000L board_build.flash_mode = qio board_build.partitions = ${esp32.default_partitions} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_WROVER - -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue - -D WLED_USE_PSRAM +build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=ESP32_WROVER + -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue ;; Older ESP32 (rev.<3) need a PSRAM fix (increases static RAM used) https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/external-ram.html -D LEDPIN=25 -lib_deps = ${esp32.lib_deps} + ; ${esp32.AR_build_flags} +lib_deps = ${esp32_idf_V4.lib_deps} + ; ${esp32.AR_lib_deps} [env:esp32c3dev] extends = esp32c3 @@ -387,7 +420,7 @@ platform = ${esp32c3.platform} platform_packages = ${esp32c3.platform_packages} framework = arduino board = esp32-c3-devkitm-1 -board_build.partitions = tools/WLED_ESP32_4MB_1MB_FS.csv +board_build.partitions = ${esp32.default_partitions} build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME=ESP32-C3 -D WLED_WATCHDOG_TIMEOUT=0 -DLOLIN_WIFI_FIX ; seems to work much better with this @@ -397,26 +430,28 @@ upload_speed = 460800 build_unflags = ${common.build_unflags} lib_deps = ${esp32c3.lib_deps} -[env:esp32s3dev_8MB] -;; ESP32-S3-DevKitC-1 development board, with 8MB FLASH, no PSRAM (flash_mode: qio) -board = esp32-s3-devkitc-1 +[env:esp32s3dev_16MB_opi] +;; ESP32-S3 development board, with 16MB FLASH and >= 8MB PSRAM (memory_type: qio_opi) +board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support +board_build.arduino.memory_type = qio_opi ;; use with PSRAM: 8MB or 16MB platform = ${esp32s3.platform} platform_packages = ${esp32s3.platform_packages} -upload_speed = 921600 ; or 460800 +upload_speed = 921600 build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_8MB +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_16MB_opi -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 - -D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip - ;-D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") - ;-D WLED_DEBUG + ;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip + -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") + -DBOARD_HAS_PSRAM + ${esp32.AR_build_flags} lib_deps = ${esp32s3.lib_deps} -board_build.partitions = tools/WLED_ESP32_8MB.csv + ${esp32.AR_lib_deps} +board_build.partitions = ${esp32.extreme_partitions} board_build.f_flash = 80000000L board_build.flash_mode = qio -; board_build.flash_mode = dio ;; try this if you have problems at startup monitor_filters = esp32_exception_decoder -[env:esp32s3dev_8MB_PSRAM_opi] +[env:esp32s3dev_8MB_opi] ;; ESP32-S3 development board, with 8MB FLASH and >= 8MB PSRAM (memory_type: qio_opi) board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support board_build.arduino.memory_type = qio_opi ;; use with PSRAM: 8MB or 16MB @@ -424,14 +459,35 @@ platform = ${esp32s3.platform} platform_packages = ${esp32s3.platform_packages} upload_speed = 921600 build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_8MB_PSRAM_opi +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_8MB_opi -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 ;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") - ; -D WLED_RELEASE_NAME=ESP32-S3_PSRAM - -D WLED_USE_PSRAM -DBOARD_HAS_PSRAM ; tells WLED that PSRAM shall be used + -DBOARD_HAS_PSRAM + ${esp32.AR_build_flags} lib_deps = ${esp32s3.lib_deps} -board_build.partitions = tools/WLED_ESP32_8MB.csv + ${esp32.AR_lib_deps} +board_build.partitions = ${esp32.large_partitions} +board_build.f_flash = 80000000L +board_build.flash_mode = qio +monitor_filters = esp32_exception_decoder + +[env:esp32s3_4M_qspi] +;; ESP32-S3, with 4MB FLASH and <= 4MB PSRAM (memory_type: qio_qspi) +board = lolin_s3_mini ;; -S3 mini, 4MB flash 2MB PSRAM +platform = ${esp32s3.platform} +platform_packages = ${esp32s3.platform_packages} +upload_speed = 921600 +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_4M_qspi + -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") + -DBOARD_HAS_PSRAM + -DLOLIN_WIFI_FIX ; seems to work much better with this + -D WLED_WATCHDOG_TIMEOUT=0 + ${esp32.AR_build_flags} +lib_deps = ${esp32s3.lib_deps} + ${esp32.AR_lib_deps} +board_build.partitions = ${esp32.default_partitions} board_build.f_flash = 80000000L board_build.flash_mode = qio monitor_filters = esp32_exception_decoder @@ -440,17 +496,16 @@ monitor_filters = esp32_exception_decoder platform = ${esp32s2.platform} platform_packages = ${esp32s2.platform_packages} board = lolin_s2_mini -board_build.partitions = tools/WLED_ESP32_4MB_1MB_FS.csv -;board_build.flash_mode = qio -;board_build.f_flash = 80000000L +board_build.partitions = ${esp32.default_partitions} +board_build.flash_mode = qio +board_build.f_flash = 80000000L build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s2.build_flags} -D WLED_RELEASE_NAME=ESP32-S2 - -DBOARD_HAS_PSRAM -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=0 + -DBOARD_HAS_PSRAM -DLOLIN_WIFI_FIX ; seems to work much better with this - -D WLED_USE_PSRAM -D WLED_WATCHDOG_TIMEOUT=0 -D CONFIG_ASYNC_TCP_USE_WDT=0 -D LEDPIN=16 @@ -460,8 +515,9 @@ build_flags = ${common.build_flags} ${esp32s2.build_flags} -D WLED_RELEASE_NAME= -D HW_PIN_DATASPI=11 -D HW_PIN_MISOSPI=9 ; -D STATUSLED=15 + ${esp32.AR_build_flags} lib_deps = ${esp32s2.lib_deps} - + ${esp32.AR_lib_deps} [env:esp32dev_hub75] board = esp32dev diff --git a/platformio_override.sample.ini b/platformio_override.sample.ini index 29f5c6b5..7f652492 100644 --- a/platformio_override.sample.ini +++ b/platformio_override.sample.ini @@ -10,7 +10,7 @@ default_envs = WLED_tasmota_1M # define as many as you need #---------- # SAMPLE #---------- -[env:WLED_tasmota_1M] +[env:WLED_generic8266_1M] extends = env:esp01_1m_full # when you want to extend the existing environment (define only updated options) ; board = esp01_1m # uncomment when ou need different board ; platform = ${common.platform_wled_default} # uncomment and change when you want particular platform @@ -26,14 +26,19 @@ lib_deps = ${esp8266.lib_deps} ; adafruit/Adafruit BME280 Library@^2.2.2 ; Wire ; robtillaart/SHT85@~0.3.3 -; gmag11/QuickESPNow ;@ 0.6.2 +; ;gmag11/QuickESPNow @ ~0.7.0 # will also load QuickDebug ; https://github.com/blazoncek/QuickESPNow.git#optional-debug ;; exludes debug library -; https://github.com/kosme/arduinoFFT#develop @ 1.9.2+sha.419d7b0 ;; used for USERMOD_AUDIOREACTIVE - using "known working" hash -; build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} +; ${esp32.AR_lib_deps} ;; used for USERMOD_AUDIOREACTIVE +; bitbank2/PNGdec@^1.0.1 ;; used for POV display uncomment following + +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags} ${esp8266.build_flags} ; ; *** To use the below defines/overrides, copy and paste each onto it's own line just below build_flags in the section above. ; +; Set a release name that may be used to distinguish required binary for flashing +; -D WLED_RELEASE_NAME=ESP32_MULTI_USREMODS +; ; disable specific features ; -D WLED_DISABLE_OTA ; -D WLED_DISABLE_ALEXA @@ -48,6 +53,11 @@ build_flags = ${common.build_flags_esp8266} ; -D WLED_DISABLE_ESPNOW ; -D WLED_DISABLE_BROWNOUT_DET ; +; enable optional built-in features +; -D WLED_ENABLE_PIXART +; -D WLED_ENABLE_USERMOD_PAGE # if created +; -D WLED_ENABLE_DMX +; ; PIN defines - uncomment and change, if needed: ; -D LEDPIN=2 ; or use this for multiple outputs @@ -56,10 +66,13 @@ build_flags = ${common.build_flags_esp8266} ; -D IRPIN=4 ; -D RLYPIN=12 ; -D RLYMDE=1 +; -D RLYODRAIN=0 ; -D LED_BUILTIN=2 # GPIO of built-in LED ; ; Limit max buses ; -D WLED_MAX_BUSSES=2 +; -D WLED_MAX_ANALOG_CHANNELS=3 # only 3 PWM HW pins available +; -D WLED_MAX_DIGITAL_CHANNELS=2 # only 2 HW accelerated pins available ; ; Configure default WiFi ; -D CLIENT_SSID='"MyNetwork"' @@ -90,6 +103,12 @@ build_flags = ${common.build_flags_esp8266} ; -D USERMOD_AUTO_SAVE ; -D AUTOSAVE_AFTER_SEC=90 ; +; Use AHT10/AHT15/AHT20 usermod +; -D USERMOD_AHT10 +; +; Use INA226 usermod +; -D USERMOD_INA226 +; ; Use 4 Line Display usermod with SPI display ; -D USERMOD_FOUR_LINE_DISPLAY ; -D USE_ALT_DISPlAY # mandatory @@ -118,12 +137,12 @@ build_flags = ${common.build_flags_esp8266} ; ; Use PIR sensor usermod and configure it to use GPIO4 and timer of 60s ; -D USERMOD_PIRSWITCH -; -D PIR_SENSOR_PIN=4 +; -D PIR_SENSOR_PIN=4 # use -1 to disable usermod ; -D PIR_SENSOR_OFF_SEC=60 +; -D PIR_SENSOR_MAX_SENSORS=2 # max allowable sensors (uses OR logic for triggering) ; ; Use Audioreactive usermod and configure I2S microphone ; -D USERMOD_AUDIOREACTIVE -; -D UM_AUDIOREACTIVE_USE_NEW_FFT ; -D AUDIOPIN=-1 ; -D DMTYPE=1 # 0-analog/disabled, 1-I2S generic, 2-ES7243, 3-SPH0645, 4-I2S+mclk, 5-I2S PDM ; -D I2S_SDPIN=36 @@ -135,6 +154,8 @@ build_flags = ${common.build_flags_esp8266} ; -D TACHO_PIN=33 ; -D PWM_PIN=32 ; +; Use POV Display usermod +; -D USERMOD_POV_DISPLAY ; Use built-in or custom LED as a status indicator (assumes LED is connected to GPIO16) ; -D STATUSLED=16 ; @@ -145,19 +166,22 @@ build_flags = ${common.build_flags_esp8266} ; -D DEFAULT_LED_COUNT=30 ; or this for multiple outputs ; -D PIXEL_COUNTS=30,30 -; -; set milliampere limit when using ESP pin to power leds +; +; set the default LED type +; -D DEFAULT_LED_TYPE=22 # see const.h (TYPE_xxxx) +; +; set milliampere limit when using ESP power pin (or inadequate PSU) to power LEDs ; -D ABL_MILLIAMPS_DEFAULT=850 +; -D LED_MILLIAMPS_DEFAULT=55 ; ; enable IR by setting remote type -; -D IRTYPE=0 ;0 Remote disabled | 1 24-key RGB | 2 24-key with CT | 3 40-key blue | 4 40-key RGB | 5 21-key RGB | 6 6-key black | 7 9-key red | 8 JSON remote +; -D IRTYPE=0 # 0 Remote disabled | 1 24-key RGB | 2 24-key with CT | 3 40-key blue | 4 40-key RGB | 5 21-key RGB | 6 6-key black | 7 9-key red | 8 JSON remote ; ; set default color order of your led strip ; -D DEFAULT_LED_COLOR_ORDER=COL_ORDER_GRB ; -; use PSRAM if a device (ESP) has one -; -DBOARD_HAS_PSRAM -; -D WLED_USE_PSRAM +; use PSRAM on classic ESP32 rev.1 (rev.3 or above has no issues) +; -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue # needed only for classic ESP32 rev.1 ; ; configure I2C and SPI interface (for various hardware) ; -D I2CSDAPIN=33 # initialise interface @@ -180,7 +204,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} +build_flags = ${common.build_flags} ${esp8266.build_flags} lib_deps = ${esp8266.lib_deps} [env:d1_mini] @@ -190,7 +214,7 @@ platform_packages = ${common.platform_packages} upload_speed = 921600 board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} +build_flags = ${common.build_flags} ${esp8266.build_flags} lib_deps = ${esp8266.lib_deps} monitor_filters = esp8266_exception_decoder @@ -200,7 +224,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} +build_flags = ${common.build_flags} ${esp8266.build_flags} lib_deps = ${esp8266.lib_deps} [env:h803wf] @@ -209,7 +233,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D LEDPIN=1 -D WLED_DISABLE_INFRARED +build_flags = ${common.build_flags} ${esp8266.build_flags} -D LEDPIN=1 -D WLED_DISABLE_INFRARED lib_deps = ${esp8266.lib_deps} [env:esp32dev_qio80] @@ -217,7 +241,7 @@ board = esp32dev platform = ${esp32.platform} platform_packages = ${esp32.platform_packages} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_qio80 #-D WLED_DISABLE_BROWNOUT_DET +build_flags = ${common.build_flags} ${esp32.build_flags} #-D WLED_DISABLE_BROWNOUT_DET lib_deps = ${esp32.lib_deps} monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.default_partitions} @@ -232,7 +256,7 @@ board = esp32dev platform = ${esp32_idf_V4.platform} platform_packages = ${esp32_idf_V4.platform_packages} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=ESP32_V4_qio80 #-D WLED_DISABLE_BROWNOUT_DET +build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} #-D WLED_DISABLE_BROWNOUT_DET lib_deps = ${esp32_idf_V4.lib_deps} monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32_idf_V4.default_partitions} @@ -241,14 +265,14 @@ board_build.flash_mode = dio [env:esp32s2_saola] board = esp32-s2-saola-1 -platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.2.2/platform-tasmota-espressif32-2.0.2.zip -platform_packages = +platform = ${esp32s2.platform} +platform_packages = ${esp32s2.platform_packages} framework = arduino board_build.partitions = tools/WLED_ESP32_4MB_1MB_FS.csv board_build.flash_mode = qio upload_speed = 460800 build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags} ${esp32s2.build_flags} #-D WLED_RELEASE_NAME=S2_saola +build_flags = ${common.build_flags} ${esp32s2.build_flags} ;-DLOLIN_WIFI_FIX ;; try this in case Wifi does not work -DARDUINO_USB_CDC_ON_BOOT=1 lib_deps = ${esp32s2.lib_deps} @@ -266,7 +290,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_1m128k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_DISABLE_OTA +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA lib_deps = ${esp8266.lib_deps} [env:esp8285_H801] @@ -275,7 +299,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_1m128k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_DISABLE_OTA +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA lib_deps = ${esp8266.lib_deps} [env:d1_mini_5CH_Shojo_PCB] @@ -284,7 +308,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_USE_SHOJO_PCB +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_USE_SHOJO_PCB lib_deps = ${esp8266.lib_deps} [env:d1_mini_debug] @@ -294,7 +318,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} ${common.debug_flags} +build_flags = ${common.build_flags} ${esp8266.build_flags} ${common.debug_flags} lib_deps = ${esp8266.lib_deps} [env:d1_mini_ota] @@ -306,7 +330,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} +build_flags = ${common.build_flags} ${esp8266.build_flags} lib_deps = ${esp8266.lib_deps} [env:anavi_miracle_controller] @@ -315,7 +339,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D LEDPIN=12 -D IRPIN=-1 -D RLYPIN=2 +build_flags = ${common.build_flags} ${esp8266.build_flags} -D LEDPIN=12 -D IRPIN=-1 -D RLYPIN=2 lib_deps = ${esp8266.lib_deps} [env:esp32c3dev_2MB] @@ -325,7 +349,7 @@ extends = esp32c3 platform = ${esp32c3.platform} platform_packages = ${esp32c3.platform_packages} board = esp32-c3-devkitm-1 -build_flags = ${common.build_flags} ${esp32c3.build_flags} #-D WLED_RELEASE_NAME=ESP32-C3 +build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_WATCHDOG_TIMEOUT=0 -D WLED_DISABLE_OTA ; -DARDUINO_USB_CDC_ON_BOOT=1 ;; for virtual CDC USB @@ -342,7 +366,7 @@ platform = ${esp32.platform} platform_packages = ${esp32.platform_packages} upload_speed = 460800 build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} +build_flags = ${common.build_flags} ${esp32.build_flags} -D LEDPIN=16 -D RLYPIN=19 -D BTNPIN=17 @@ -362,7 +386,7 @@ board_build.partitions = ${esp32.default_partitions} [env:m5atom] board = esp32dev build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} -D LEDPIN=27 -D BTNPIN=39 +build_flags = ${common.build_flags} ${esp32.build_flags} -D LEDPIN=27 -D BTNPIN=39 lib_deps = ${esp32.lib_deps} platform = ${esp32.platform} platform_packages = ${esp32.platform_packages} @@ -372,14 +396,14 @@ board_build.partitions = ${esp32.default_partitions} board = esp_wroom_02 platform = ${common.platform_wled_default} board_build.ldscript = ${common.ldscript_2m512k} -build_flags = ${common.build_flags_esp8266} -D LEDPIN=3 -D BTNPIN=1 +build_flags = ${common.build_flags} ${esp8266.build_flags} -D LEDPIN=3 -D BTNPIN=1 lib_deps = ${esp8266.lib_deps} [env:sp511e] board = esp_wroom_02 platform = ${common.platform_wled_default} board_build.ldscript = ${common.ldscript_2m512k} -build_flags = ${common.build_flags_esp8266} -D LEDPIN=3 -D BTNPIN=2 -D IRPIN=5 -D WLED_MAX_BUTTONS=3 +build_flags = ${common.build_flags} ${esp8266.build_flags} -D LEDPIN=3 -D BTNPIN=2 -D IRPIN=5 -D WLED_MAX_BUTTONS=3 lib_deps = ${esp8266.lib_deps} [env:Athom_RGBCW] ;7w and 5w(GU10) bulbs @@ -388,7 +412,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 -D BTNPIN=-1 -D RLYPIN=-1 -D DATA_PINS=4,12,14,13,5 +build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=-1 -D RLYPIN=-1 -D DATA_PINS=4,12,14,13,5 -D DEFAULT_LED_TYPE=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0 lib_deps = ${esp8266.lib_deps} @@ -398,7 +422,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 -D BTNPIN=-1 -D RLYPIN=-1 -D DATA_PINS=4,12,14,5,13 +build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=-1 -D RLYPIN=-1 -D DATA_PINS=4,12,14,5,13 -D DEFAULT_LED_TYPE=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0 -D WLED_USE_IC_CCT lib_deps = ${esp8266.lib_deps} @@ -408,7 +432,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 -D BTNPIN=0 -D RLYPIN=-1 -D LEDPIN=1 -D WLED_DISABLE_INFRARED +build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=0 -D RLYPIN=-1 -D LEDPIN=1 -D WLED_DISABLE_INFRARED lib_deps = ${esp8266.lib_deps} [env:Athom_4Pin_Controller] ; With clock and data interface @@ -417,7 +441,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 -D BTNPIN=0 -D RLYPIN=12 -D LEDPIN=1 -D WLED_DISABLE_INFRARED +build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=0 -D RLYPIN=12 -D LEDPIN=1 -D WLED_DISABLE_INFRARED lib_deps = ${esp8266.lib_deps} [env:Athom_5Pin_Controller] ;Analog light strip controller @@ -426,7 +450,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 -D BTNPIN=0 -D RLYPIN=-1 DATA_PINS=4,12,14,13 -D WLED_DISABLE_INFRARED +build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=0 -D RLYPIN=-1 DATA_PINS=4,12,14,13 -D WLED_DISABLE_INFRARED lib_deps = ${esp8266.lib_deps} [env:MY9291] @@ -435,7 +459,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_1m128k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP01 -D WLED_DISABLE_OTA -D USERMOD_MY9291 +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA -D USERMOD_MY9291 lib_deps = ${esp8266.lib_deps} # ------------------------------------------------------------------------------ @@ -449,7 +473,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} +build_flags = ${common.build_flags} ${esp8266.build_flags} lib_deps = ${esp8266.lib_deps} [env:codm-controller-0_6-rev2] @@ -458,7 +482,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} +build_flags = ${common.build_flags} ${esp8266.build_flags} lib_deps = ${esp8266.lib_deps} # ------------------------------------------------------------------------------ @@ -469,7 +493,7 @@ board = esp32dev platform = ${esp32.platform} platform_packages = ${esp32.platform_packages} upload_speed = 921600 -build_flags = ${common.build_flags_esp32} -D WLED_DISABLE_BROWNOUT_DET -D WLED_DISABLE_INFRARED +build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_DISABLE_BROWNOUT_DET -D WLED_DISABLE_INFRARED -D USERMOD_RTC -D USERMOD_ELEKSTUBE_IPS -D LEDPIN=12 diff --git a/requirements.txt b/requirements.txt index 1c0644f9..c4ce9445 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,7 +28,7 @@ h11==0.14.0 # via # uvicorn # wsproto -idna==3.4 +idna==3.7 # via # anyio # requests @@ -36,13 +36,13 @@ marshmallow==3.19.0 # via platformio packaging==23.1 # via marshmallow -platformio==6.1.6 +platformio==6.1.14 # via -r requirements.in pyelftools==0.29 # via platformio pyserial==3.5 # via platformio -requests==2.31.0 +requests==2.32.0 # via platformio semantic-version==2.10.0 # via platformio @@ -52,7 +52,9 @@ starlette==0.23.1 # via platformio tabulate==0.9.0 # via platformio -urllib3==1.26.18 +typing-extensions==4.11.0 + # via starlette +urllib3==1.26.19 # via requests uvicorn==0.20.0 # via platformio diff --git a/tools/WLED_ESP32-wrover_4MB.csv b/tools/WLED_ESP32-wrover_4MB.csv index a179a89d..39c88e54 100644 --- a/tools/WLED_ESP32-wrover_4MB.csv +++ b/tools/WLED_ESP32-wrover_4MB.csv @@ -1,6 +1,6 @@ # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, -app0, app, ota_0, 0x10000, 0x180000, -app1, app, ota_1, 0x190000,0x180000, -spiffs, data, spiffs, 0x310000,0xF0000, +app0, app, ota_0, 0x10000, 0x1A0000, +app1, app, ota_1, 0x1B0000,0x1A0000, +spiffs, data, spiffs, 0x350000,0xB0000, diff --git a/tools/WLED_ESP32_4MB_256KB_FS.csv b/tools/WLED_ESP32_4MB_256KB_FS.csv new file mode 100644 index 00000000..e54c22bb --- /dev/null +++ b/tools/WLED_ESP32_4MB_256KB_FS.csv @@ -0,0 +1,7 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x1D0000, +app1, app, ota_1, 0x1E0000,0x1D0000, +spiffs, data, spiffs, 0x3B0000,0x40000, +coredump, data, coredump,,64K \ No newline at end of file diff --git a/tools/WLED_ESP32_4MB_700k_FS.csv b/tools/WLED_ESP32_4MB_700k_FS.csv new file mode 100644 index 00000000..39c88e54 --- /dev/null +++ b/tools/WLED_ESP32_4MB_700k_FS.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x1A0000, +app1, app, ota_1, 0x1B0000,0x1A0000, +spiffs, data, spiffs, 0x350000,0xB0000, diff --git a/tools/cdata-test.js b/tools/cdata-test.js index 55f06807..6f27fb71 100644 --- a/tools/cdata-test.js +++ b/tools/cdata-test.js @@ -83,6 +83,7 @@ describe('Script', () => { // Backup files fs.cpSync("wled00/data", "wled00Backup", { recursive: true }); fs.cpSync("tools/cdata.js", "cdata.bak.js"); + fs.cpSync("package.json", "package.bak.json"); }); after(() => { // Restore backup @@ -90,6 +91,8 @@ describe('Script', () => { fs.renameSync("wled00Backup", "wled00/data"); fs.rmSync("tools/cdata.js"); fs.renameSync("cdata.bak.js", "tools/cdata.js"); + fs.rmSync("package.json"); + fs.renameSync("package.bak.json", "package.json"); }); // delete all html_*.h files @@ -131,7 +134,7 @@ describe('Script', () => { // run script cdata.js again and wait for it to finish await execPromise('node tools/cdata.js'); - checkIfFileWasNewlyCreated(path.join(folderPath, resultFile)); + await checkIfFileWasNewlyCreated(path.join(folderPath, resultFile)); } describe('should build if', () => { @@ -182,6 +185,10 @@ describe('Script', () => { it('cdata.js changes', async () => { await testFileModification('tools/cdata.js', 'html_ui.h'); }); + + it('package.json changes', async () => { + await testFileModification('package.json', 'html_ui.h'); + }); }); describe('should not build if', () => { diff --git a/tools/cdata.js b/tools/cdata.js index ef7e06f4..12dda1cb 100644 --- a/tools/cdata.js +++ b/tools/cdata.js @@ -2,7 +2,7 @@ * Writes compressed C arrays of data files (web interface) * How to use it? * - * 1) Install Node 11+ and npm + * 1) Install Node 20+ and npm * 2) npm install * 3) npm run build * @@ -15,10 +15,10 @@ * It uses NodeJS packages to inline, minify and GZIP files. See writeHtmlGzipped and writeChunks invocations at the bottom of the page. */ -const fs = require("fs"); +const fs = require("node:fs"); const path = require("path"); const inliner = require("inliner"); -const zlib = require("zlib"); +const zlib = require("node:zlib"); const CleanCSS = require("clean-css"); const minifyHtml = require("html-minifier-terser").minify; const packageJson = require("../package.json"); @@ -30,14 +30,12 @@ const output = ["wled00/html_ui.h", "wled00/html_pixart.h", "wled00/html_cpal.h" // \x1b[34m is blue, \x1b[36m is cyan, \x1b[0m is reset const wledBanner = ` -\t\x1b[34m## ## ## ######## ######## -\t\x1b[34m## ## ## ## ## ## ## -\t\x1b[34m## ## ## ## ## ## ## -\t\x1b[34m## ## ## ## ###### ## ## -\t\x1b[34m## ## ## ## ## ## ## -\t\x1b[34m## ## ## ## ## ## ## -\t\x1b[34m ### ### ######## ######## ######## -\t\t\x1b[36mbuild script for web UI +\t\x1b[34m ## ## ## ###### ###### +\t\x1b[34m## ## ## ## ## ## ## +\t\x1b[34m## ## ## ## ###### ## ## +\t\x1b[34m## ## ## ## ## ## ## +\t\x1b[34m ## ## ###### ###### ###### +\t\t\x1b[36m build script for web UI \x1b[0m`; const singleHeader = `/* @@ -209,7 +207,7 @@ function isAnyFileInFolderNewerThan(folderPath, time) { } // Check if the web UI is already built -function isAlreadyBuilt(folderPath) { +function isAlreadyBuilt(webUIPath, packageJsonPath = "package.json") { let lastBuildTime = Infinity; for (const file of output) { @@ -222,7 +220,7 @@ function isAlreadyBuilt(folderPath) { } } - return !isAnyFileInFolderNewerThan(folderPath, lastBuildTime) && !isFileNewerThan("tools/cdata.js", lastBuildTime); + return !isAnyFileInFolderNewerThan(webUIPath, lastBuildTime) && !isFileNewerThan(packageJsonPath, lastBuildTime) && !isFileNewerThan(__filename, lastBuildTime); } // Don't run this script if we're in a test environment diff --git a/tools/stress_test.sh b/tools/stress_test.sh new file mode 100644 index 00000000..d7c344c5 --- /dev/null +++ b/tools/stress_test.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# Some web server stress tests +# +# Perform a large number of parallel requests, stress testing the web server +# TODO: some kind of performance metrics + +# Accepts three command line arguments: +# - first argument - mandatory - IP or hostname of target server +# - second argument - target type (optional) +# - third argument - xfer count (for replicated targets) (optional) +HOST=$1 +declare -n TARGET_STR="${2:-JSON_LARGER}_TARGETS" +REPLICATE_COUNT=$(("${3:-10}")) + +PARALLEL_MAX=${PARALLEL_MAX:-50} + +CURL_ARGS="--compressed --parallel --parallel-immediate --parallel-max ${PARALLEL_MAX}" +CURL_PRINT_RESPONSE_ARGS="-w %{http_code}\n" + +JSON_TARGETS=('json/state' 'json/info' 'json/si', 'json/palettes' 'json/fxdata' 'settings/s.js?p=2') +FILE_TARGETS=('' 'iro.js' 'rangetouch.js' 'settings' 'settings/wifi') +# Replicate one target many times +function replicate() { + printf "${1}?%d " $(seq 1 ${REPLICATE_COUNT}) +} +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") + +# Expand target URLS to full arguments for curl +TARGETS=(${TARGET_STR[@]}) +#echo "${TARGETS[@]}" +FULL_TGT_OPTIONS=$(printf "http://${HOST}/%s -o /dev/null " "${TARGETS[@]}") +#echo ${FULL_TGT_OPTIONS} + +time curl ${CURL_ARGS} ${FULL_TGT_OPTIONS} diff --git a/tools/udp_test.py b/tools/udp_test.py new file mode 100644 index 00000000..c4c9129c --- /dev/null +++ b/tools/udp_test.py @@ -0,0 +1,46 @@ +import numpy as np +import socket + +class WledRealtimeClient: + def __init__(self, wled_controller_ip, num_pixels, udp_port=21324, max_pixels_per_packet=126): + self.wled_controller_ip = wled_controller_ip + self.num_pixels = num_pixels + self.udp_port = udp_port + self.max_pixels_per_packet = max_pixels_per_packet + self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self._prev_pixels = np.full((3, self.num_pixels), 253, dtype=np.uint8) + self.pixels = np.full((3, self.num_pixels), 1, dtype=np.uint8) + + def update(self): + # Truncate values and cast to integer + self.pixels = np.clip(self.pixels, 0, 255).astype(np.uint8) + p = np.copy(self.pixels) + + idx = np.where(~np.all(p == self._prev_pixels, axis=0))[0] + num_pixels = len(idx) + n_packets = (num_pixels + self.max_pixels_per_packet - 1) // self.max_pixels_per_packet + idx_split = np.array_split(idx, n_packets) + + header = bytes([1, 2]) # WARLS protocol header + for packet_indices in idx_split: + data = bytearray(header) + for i in packet_indices: + data.extend([i, *p[:, i]]) # Index and RGB values + self._sock.sendto(bytes(data), (self.wled_controller_ip, self.udp_port)) + + self._prev_pixels = np.copy(p) + + + +################################## LED blink test ################################## +if __name__ == "__main__": + WLED_CONTROLLER_IP = "192.168.1.153" + NUM_PIXELS = 255 # Amount of LEDs on your strip + import time + wled = WledRealtimeClient(WLED_CONTROLLER_IP, NUM_PIXELS) + print('Starting LED blink test') + while True: + for i in range(NUM_PIXELS): + wled.pixels[1, i] = 255 if wled.pixels[1, i] == 0 else 0 + wled.update() + time.sleep(.01) diff --git a/usermods/AHT10_v2/README.md b/usermods/AHT10_v2/README.md new file mode 100644 index 00000000..69fab467 --- /dev/null +++ b/usermods/AHT10_v2/README.md @@ -0,0 +1,36 @@ +# Usermod AHT10 +This Usermod is designed to read a `AHT10`, `AHT15` or `AHT20` sensor and output the following: +- Temperature +- Humidity + +Configuration is performed via the Usermod menu. The following settings can be configured in the Usermod Menu: +- I2CAddress: The i2c address in decimal. Set it to either 56 (0x38, the default) or 57 (0x39). +- SensorType, one of: + - 0 - AHT10 + - 1 - AHT15 + - 2 - AHT20 +- CheckInterval: Number of seconds between readings +- Decimals: Number of decimals to put in the output + +Dependencies, These must be added under `lib_deps` in your `platform.ini` (or `platform_override.ini`). +- Libraries + - `enjoyneering/AHT10@~1.1.0` (by [enjoyneering](https://registry.platformio.org/libraries/enjoyneering/AHT10)) + - `Wire` + +## Author +[@LordMike](https://github.com/LordMike) + +# Compiling + +To enable, compile with `USERMOD_AHT10` defined (e.g. in `platformio_override.ini`) +```ini +[env:aht10_example] +extends = env:esp32dev +build_flags = + ${common.build_flags} ${esp32.build_flags} + -D USERMOD_AHT10 + ; -D USERMOD_AHT10_DEBUG ; -- add a debug status to the info modal +lib_deps = + ${esp32.lib_deps} + enjoyneering/AHT10@~1.1.0 +``` diff --git a/usermods/AHT10_v2/platformio_override.ini b/usermods/AHT10_v2/platformio_override.ini new file mode 100644 index 00000000..30240f22 --- /dev/null +++ b/usermods/AHT10_v2/platformio_override.ini @@ -0,0 +1,9 @@ +[env:aht10_example] +extends = env:esp32dev +build_flags = + ${common.build_flags} ${esp32.build_flags} + -D USERMOD_AHT10 + ; -D USERMOD_AHT10_DEBUG ; -- add a debug status to the info modal +lib_deps = + ${esp32.lib_deps} + enjoyneering/AHT10@~1.1.0 \ No newline at end of file diff --git a/usermods/AHT10_v2/usermod_aht10.h b/usermods/AHT10_v2/usermod_aht10.h new file mode 100644 index 00000000..b5dc1841 --- /dev/null +++ b/usermods/AHT10_v2/usermod_aht10.h @@ -0,0 +1,327 @@ +#pragma once + +#include "wled.h" +#include + +#define AHT10_SUCCESS 1 + +class UsermodAHT10 : public Usermod +{ +private: + static const char _name[]; + + unsigned long _lastLoopCheck = 0; + + bool _settingEnabled : 1; // Enable the usermod + bool _mqttPublish : 1; // Publish mqtt values + bool _mqttPublishAlways : 1; // Publish always, regardless if there is a change + bool _mqttHomeAssistant : 1; // Enable Home Assistant docs + bool _initDone : 1; // Initialization is done + + // Settings. Some of these are stored in a different format than they're user settings - so we don't have to convert at runtime + uint8_t _i2cAddress = AHT10_ADDRESS_0X38; + ASAIR_I2C_SENSOR _ahtType = AHT10_SENSOR; + uint16_t _checkInterval = 60000; // milliseconds, user settings is in seconds + float _decimalFactor = 100; // a power of 10 factor. 1 would be no change, 10 is one decimal, 100 is two etc. User sees a power of 10 (0, 1, 2, ..) + + uint8_t _lastStatus = 0; + float _lastHumidity = 0; + float _lastTemperature = 0; + +#ifndef WLED_MQTT_DISABLE + float _lastHumiditySent = 0; + float _lastTemperatureSent = 0; +#endif + + AHT10 *_aht = nullptr; + + float truncateDecimals(float val) + { + return roundf(val * _decimalFactor) / _decimalFactor; + } + + void initializeAht() + { + if (_aht != nullptr) + { + delete _aht; + } + + _aht = new AHT10(_i2cAddress, _ahtType); + + _lastStatus = 0; + _lastHumidity = 0; + _lastTemperature = 0; + } + + ~UsermodAHT10() + { + delete _aht; + _aht = nullptr; + } + +#ifndef WLED_DISABLE_MQTT + void mqttInitialize() + { + // This is a generic "setup mqtt" function, So we must abort if we're not to do mqtt + if (!WLED_MQTT_CONNECTED || !_mqttPublish || !_mqttHomeAssistant) + return; + + char topic[128]; + snprintf_P(topic, 127, "%s/temperature", mqttDeviceTopic); + mqttCreateHassSensor(F("Temperature"), topic, F("temperature"), F("°C")); + + snprintf_P(topic, 127, "%s/humidity", mqttDeviceTopic); + mqttCreateHassSensor(F("Humidity"), topic, F("humidity"), F("%")); + } + + void mqttPublishIfChanged(const __FlashStringHelper *topic, float &lastState, float state, float minChange) + { + // Check if MQTT Connected, otherwise it will crash the 8266 + // Only report if the change is larger than the required diff + if (WLED_MQTT_CONNECTED && _mqttPublish && (_mqttPublishAlways || fabsf(lastState - state) > minChange)) + { + char subuf[128]; + snprintf_P(subuf, 127, PSTR("%s/%s"), mqttDeviceTopic, (const char *)topic); + mqtt->publish(subuf, 0, false, String(state).c_str()); + + lastState = state; + } + } + + // Create an MQTT Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop. + void mqttCreateHassSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement) + { + String t = String(F("homeassistant/sensor/")) + mqttClientID + "/" + name + F("/config"); + + StaticJsonDocument<600> doc; + + doc[F("name")] = name; + doc[F("state_topic")] = topic; + doc[F("unique_id")] = String(mqttClientID) + name; + if (unitOfMeasurement != "") + doc[F("unit_of_measurement")] = unitOfMeasurement; + if (deviceClass != "") + doc[F("device_class")] = deviceClass; + doc[F("expire_after")] = 1800; + + JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device + device[F("name")] = serverDescription; + device[F("identifiers")] = "wled-sensor-" + String(mqttClientID); + device[F("manufacturer")] = F(WLED_BRAND); + device[F("model")] = F(WLED_PRODUCT_NAME); + device[F("sw_version")] = versionString; + + String temp; + serializeJson(doc, temp); + DEBUG_PRINTLN(t); + DEBUG_PRINTLN(temp); + + mqtt->publish(t.c_str(), 0, true, temp.c_str()); + } +#endif + +public: + void setup() + { + initializeAht(); + } + + void loop() + { + // if usermod is disabled or called during strip updating just exit + // NOTE: on very long strips strip.isUpdating() may always return true so update accordingly + if (!_settingEnabled || strip.isUpdating()) + return; + + // do your magic here + unsigned long currentTime = millis(); + + if (currentTime - _lastLoopCheck < _checkInterval) + return; + _lastLoopCheck = currentTime; + + _lastStatus = _aht->readRawData(); + + if (_lastStatus == AHT10_ERROR) + { + // Perform softReset and retry + DEBUG_PRINTLN(F("AHTxx returned error, doing softReset")); + if (!_aht->softReset()) + { + DEBUG_PRINTLN(F("softReset failed")); + return; + } + + _lastStatus = _aht->readRawData(); + } + + if (_lastStatus == AHT10_SUCCESS) + { + float temperature = truncateDecimals(_aht->readTemperature(AHT10_USE_READ_DATA)); + float humidity = truncateDecimals(_aht->readHumidity(AHT10_USE_READ_DATA)); + +#ifndef WLED_DISABLE_MQTT + // Push to MQTT + + // We can avoid reporting if the change is insignificant. The threshold chosen is below the level of accuracy, but way above 0.01 which is the precision of the value provided. + // The AHT10/15/20 has an accuracy of 0.3C in the temperature readings + mqttPublishIfChanged(F("temperature"), _lastTemperatureSent, temperature, 0.1f); + + // The AHT10/15/20 has an accuracy in the humidity sensor of 2% + mqttPublishIfChanged(F("humidity"), _lastHumiditySent, humidity, 0.5f); +#endif + + // Store + _lastTemperature = temperature; + _lastHumidity = humidity; + } + } + +#ifndef WLED_DISABLE_MQTT + void onMqttConnect(bool sessionPresent) + { + mqttInitialize(); + } +#endif + + uint16_t getId() + { + return USERMOD_ID_AHT10; + } + + void addToJsonInfo(JsonObject &root) override + { + // if "u" object does not exist yet wee need to create it + JsonObject user = root["u"]; + if (user.isNull()) + user = root.createNestedObject("u"); + +#ifdef USERMOD_AHT10_DEBUG + JsonArray temp = user.createNestedArray(F("AHT last loop")); + temp.add(_lastLoopCheck); + + temp = user.createNestedArray(F("AHT last status")); + temp.add(_lastStatus); +#endif + + JsonArray jsonTemp = user.createNestedArray(F("Temperature")); + JsonArray jsonHumidity = user.createNestedArray(F("Humidity")); + + if (_lastLoopCheck == 0) + { + // Before first run + jsonTemp.add(F("Not read yet")); + jsonHumidity.add(F("Not read yet")); + return; + } + + if (_lastStatus != AHT10_SUCCESS) + { + jsonTemp.add(F("An error occurred")); + jsonHumidity.add(F("An error occurred")); + return; + } + + jsonTemp.add(_lastTemperature); + jsonTemp.add(F("°C")); + + jsonHumidity.add(_lastHumidity); + jsonHumidity.add(F("%")); + } + + void addToConfig(JsonObject &root) + { + JsonObject top = root.createNestedObject(FPSTR(_name)); + top[F("Enabled")] = _settingEnabled; + top[F("I2CAddress")] = static_cast(_i2cAddress); + top[F("SensorType")] = _ahtType; + top[F("CheckInterval")] = _checkInterval / 1000; + top[F("Decimals")] = log10f(_decimalFactor); +#ifndef WLED_DISABLE_MQTT + top[F("MqttPublish")] = _mqttPublish; + top[F("MqttPublishAlways")] = _mqttPublishAlways; + top[F("MqttHomeAssistantDiscovery")] = _mqttHomeAssistant; +#endif + + DEBUG_PRINTLN(F("AHT10 config saved.")); + } + + bool readFromConfig(JsonObject &root) override + { + // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor + // setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) + + JsonObject top = root[FPSTR(_name)]; + + bool configComplete = !top.isNull(); + if (!configComplete) + return false; + + bool tmpBool = false; + configComplete &= getJsonValue(top[F("Enabled")], tmpBool); + if (configComplete) + _settingEnabled = tmpBool; + + configComplete &= getJsonValue(top[F("I2CAddress")], _i2cAddress); + configComplete &= getJsonValue(top[F("CheckInterval")], _checkInterval); + if (configComplete) + { + if (1 <= _checkInterval && _checkInterval <= 600) + _checkInterval *= 1000; + else + // Invalid input + _checkInterval = 60000; + } + + configComplete &= getJsonValue(top[F("Decimals")], _decimalFactor); + if (configComplete) + { + if (0 <= _decimalFactor && _decimalFactor <= 5) + _decimalFactor = pow10f(_decimalFactor); + else + // Invalid input + _decimalFactor = 100; + } + + uint8_t tmpAhtType; + configComplete &= getJsonValue(top[F("SensorType")], tmpAhtType); + if (configComplete) + { + if (0 <= tmpAhtType && tmpAhtType <= 2) + _ahtType = static_cast(tmpAhtType); + else + // Invalid input + _ahtType = ASAIR_I2C_SENSOR::AHT10_SENSOR; + } + +#ifndef WLED_DISABLE_MQTT + configComplete &= getJsonValue(top[F("MqttPublish")], tmpBool); + if (configComplete) + _mqttPublish = tmpBool; + + configComplete &= getJsonValue(top[F("MqttPublishAlways")], tmpBool); + if (configComplete) + _mqttPublishAlways = tmpBool; + + configComplete &= getJsonValue(top[F("MqttHomeAssistantDiscovery")], tmpBool); + if (configComplete) + _mqttHomeAssistant = tmpBool; +#endif + + if (_initDone) + { + // Reloading config + initializeAht(); + +#ifndef WLED_DISABLE_MQTT + mqttInitialize(); +#endif + } + + _initDone = true; + return configComplete; + } +}; + +const char UsermodAHT10::_name[] PROGMEM = "AHTxx"; \ No newline at end of file diff --git a/usermods/BH1750_v2/usermod_bh1750.h b/usermods/BH1750_v2/usermod_bh1750.h index ede4aabc..2a2bd463 100644 --- a/usermods/BH1750_v2/usermod_bh1750.h +++ b/usermods/BH1750_v2/usermod_bh1750.h @@ -59,7 +59,7 @@ private: bool sensorFound = false; // Home Assistant and MQTT - String mqttLuminanceTopic = F(""); + String mqttLuminanceTopic; bool mqttInitialized = false; bool HomeAssistantDiscovery = true; // Publish Home Assistant Discovery messages diff --git a/usermods/BME280_v2/README.md b/usermods/BME280_v2/README.md index 0a4afbf1..a4fc229a 100644 --- a/usermods/BME280_v2/README.md +++ b/usermods/BME280_v2/README.md @@ -7,6 +7,7 @@ This Usermod is designed to read a `BME280` or `BMP280` sensor and output the fo - Dew Point (`BME280` only) Configuration is performed via the Usermod menu. There are no parameters to set in code! The following settings can be configured in the Usermod Menu: +- The i2c address in decimal. Set it to either 118 (0x76, the default) or 119 (0x77). - Temperature Decimals (number of decimal places to output) - Humidity Decimals - Pressure Decimals diff --git a/usermods/BME280_v2/usermod_bme280.h b/usermods/BME280_v2/usermod_bme280.h index 38930da5..9168f422 100644 --- a/usermods/BME280_v2/usermod_bme280.h +++ b/usermods/BME280_v2/usermod_bme280.h @@ -24,6 +24,7 @@ private: uint8_t PressureDecimals = 0; // Number of decimal places in published pressure values uint16_t TemperatureInterval = 5; // Interval to measure temperature (and humidity, dew point if available) in seconds uint16_t PressureInterval = 300; // Interval to measure pressure in seconds + BME280I2C::I2CAddr I2CAddress = BME280I2C::I2CAddr_0x76; // i2c address, defaults to 0x76 bool PublishAlways = false; // Publish values even when they have not changed bool UseCelsius = true; // Use Celsius for Reporting bool HomeAssistantDiscovery = false; // Publish Home Assistant Device Information @@ -35,20 +36,7 @@ private: #endif bool initDone = false; - // BME280 sensor settings - BME280I2C::Settings settings{ - BME280::OSR_X16, // Temperature oversampling x16 - BME280::OSR_X16, // Humidity oversampling x16 - BME280::OSR_X16, // Pressure oversampling x16 - // Defaults - BME280::Mode_Forced, - BME280::StandbyTime_1000ms, - BME280::Filter_Off, - BME280::SpiEnable_False, - BME280I2C::I2CAddr_0x76 // I2C address. I2C specific. Default 0x76 - }; - - BME280I2C bme{settings}; + BME280I2C bme; uint8_t sensorType; @@ -181,34 +169,52 @@ private: } } + void initializeBmeComms() + { + BME280I2C::Settings settings{ + BME280::OSR_X16, // Temperature oversampling x16 + BME280::OSR_X16, // Humidity oversampling x16 + BME280::OSR_X16, // Pressure oversampling x16 + BME280::Mode_Forced, + BME280::StandbyTime_1000ms, + BME280::Filter_Off, + BME280::SpiEnable_False, + I2CAddress + }; + + bme.setSettings(settings); + + if (!bme.begin()) + { + sensorType = 0; + DEBUG_PRINTLN(F("Could not find BME280 I2C sensor!")); + } + else + { + switch (bme.chipModel()) + { + case BME280::ChipModel_BME280: + sensorType = 1; + DEBUG_PRINTLN(F("Found BME280 sensor! Success.")); + break; + case BME280::ChipModel_BMP280: + sensorType = 2; + DEBUG_PRINTLN(F("Found BMP280 sensor! No Humidity available.")); + break; + default: + sensorType = 0; + DEBUG_PRINTLN(F("Found UNKNOWN sensor! Error!")); + } + } + } + public: void setup() { if (i2c_scl<0 || i2c_sda<0) { enabled = false; sensorType = 0; return; } - if (!bme.begin()) - { - sensorType = 0; - DEBUG_PRINTLN(F("Could not find BME280 I2C sensor!")); - } - else - { - switch (bme.chipModel()) - { - case BME280::ChipModel_BME280: - sensorType = 1; - DEBUG_PRINTLN(F("Found BME280 sensor! Success.")); - break; - case BME280::ChipModel_BMP280: - sensorType = 2; - DEBUG_PRINTLN(F("Found BMP280 sensor! No Humidity available.")); - break; - default: - sensorType = 0; - DEBUG_PRINTLN(F("Found UNKNOWN sensor! Error!")); - } - } - initDone=true; + initializeBmeComms(); + initDone = true; } void loop() @@ -365,12 +371,11 @@ public: } else if (sensorType==2) //BMP280 { - JsonArray temperature_json = user.createNestedArray(F("Temperature")); JsonArray pressure_json = user.createNestedArray(F("Pressure")); - temperature_json.add(roundf(sensorTemperature * powf(10, TemperatureDecimals))); + temperature_json.add(roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals)); temperature_json.add(tempScale); - pressure_json.add(roundf(sensorPressure * powf(10, PressureDecimals))); + pressure_json.add(roundf(sensorPressure * powf(10, PressureDecimals)) / powf(10, PressureDecimals)); pressure_json.add(F("hPa")); } else if (sensorType==1) //BME280 @@ -382,9 +387,9 @@ public: JsonArray dewpoint_json = user.createNestedArray(F("Dew Point")); temperature_json.add(roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals)); temperature_json.add(tempScale); - humidity_json.add(roundf(sensorHumidity * powf(10, HumidityDecimals))); + humidity_json.add(roundf(sensorHumidity * powf(10, HumidityDecimals)) / powf(10, HumidityDecimals)); humidity_json.add(F("%")); - pressure_json.add(roundf(sensorPressure * powf(10, PressureDecimals))); + pressure_json.add(roundf(sensorPressure * powf(10, PressureDecimals)) / powf(10, PressureDecimals)); pressure_json.add(F("hPa")); heatindex_json.add(roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals)); heatindex_json.add(tempScale); @@ -399,6 +404,7 @@ public: { JsonObject top = root.createNestedObject(FPSTR(_name)); top[FPSTR(_enabled)] = enabled; + top[F("I2CAddress")] = static_cast(I2CAddress); top[F("TemperatureDecimals")] = TemperatureDecimals; top[F("HumidityDecimals")] = HumidityDecimals; top[F("PressureDecimals")] = PressureDecimals; @@ -426,6 +432,10 @@ public: configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); // A 3-argument getJsonValue() assigns the 3rd argument as a default value if the Json value is missing + uint8_t tmpI2cAddress; + configComplete &= getJsonValue(top[F("I2CAddress")], tmpI2cAddress, 0x76); + I2CAddress = static_cast(tmpI2cAddress); + configComplete &= getJsonValue(top[F("TemperatureDecimals")], TemperatureDecimals, 1); configComplete &= getJsonValue(top[F("HumidityDecimals")], HumidityDecimals, 0); configComplete &= getJsonValue(top[F("PressureDecimals")], PressureDecimals, 0); @@ -440,8 +450,23 @@ public: // first run: reading from cfg.json DEBUG_PRINTLN(F(" config loaded.")); } else { - DEBUG_PRINTLN(F(" config (re)loaded.")); // changing parameters from settings page + DEBUG_PRINTLN(F(" config (re)loaded.")); + + // Reset all known values + sensorType = 0; + sensorTemperature = 0; + sensorHumidity = 0; + sensorHeatIndex = 0; + sensorDewPoint = 0; + sensorPressure = 0; + lastTemperature = 0; + lastHumidity = 0; + lastHeatIndex = 0; + lastDewPoint = 0; + lastPressure = 0; + + initializeBmeComms(); } return configComplete; diff --git a/usermods/BME68X_v2/BME680.pdf b/usermods/BME68X_v2/BME680.pdf new file mode 100644 index 00000000..f3cfb3f0 Binary files /dev/null and b/usermods/BME68X_v2/BME680.pdf differ diff --git a/usermods/BME68X_v2/README.md b/usermods/BME68X_v2/README.md new file mode 100644 index 00000000..72ae25a5 --- /dev/null +++ b/usermods/BME68X_v2/README.md @@ -0,0 +1,152 @@ +# Usermod BME68X +This usermod was developed for a BME680/BME68X sensor. The BME68X is not compatible with the BME280/BMP280 chip. It has its own library. The original 'BSEC Software Library' from Bosch was used to develop the code. The measured values are displayed on the WLED info page. + +

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

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

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

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

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

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

- +

+

+
+ ## ⚙️ Features -- 💯 Displays current battery voltage +- 💯 Displays current battery voltage - 🚥 Displays battery level -- 🚫 Auto-off with configurable Threshold +- 🚫 Auto-off with configurable threshold - 🚨 Low power indicator with many configuration possibilities +

+ ## 🎈 Installation -define `USERMOD_BATTERY` in `wled00/my_config.h` +| **Option 1** | **Option 2** | +|--------------|--------------| +| In `wled00/my_config.h`
Add the line: `#define USERMOD_BATTERY`

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

[Example: platformio_override.ini](assets/installation_platformio_override_ini.png) | -### Example wiring +

-

- -

+## 🔌 Example wiring -### Define Your Options +- (see [Useful Links](#useful-links)). + + + + + + + + +
+ +

ESP8266
+ With a 100k Ohm resistor, connect the positive
+ side of the battery to pin `A0`.

+
+ +

ESP32 (+S2, S3, C3 etc...)
+ Use a voltage divider (two resistors of equal value).
+ Connect to ADC1 (GPIO32 - GPIO39). GPIO35 is Default.

+
+ +

+ +## Define Your Options | Name | Unit | Description | | ----------------------------------------------- | ----------- |-------------------------------------------------------------------------------------- | -| `USERMOD_BATTERY` | | define this (in `my_config.h`) to have this usermod included wled00\usermods_list.cpp | -| `USERMOD_BATTERY_USE_LIPO` | | define this (in `my_config.h`) if you use LiPo rechargeables (1S) | -| `USERMOD_BATTERY_MEASUREMENT_PIN` | | defaults to A0 on ESP8266 and GPIO35 on ESP32 | -| `USERMOD_BATTERY_MEASUREMENT_INTERVAL` | ms | battery check interval. defaults to 30 seconds | -| `USERMOD_BATTERY_MIN_VOLTAGE` | v | minimum battery voltage. default is 2.6 (18650 battery standard) | -| `USERMOD_BATTERY_MAX_VOLTAGE` | v | maximum battery voltage. default is 4.2 (18650 battery standard) | -| `USERMOD_BATTERY_TOTAL_CAPACITY` | mAh | the capacity of all cells in parallel summed up | -| `USERMOD_BATTERY_CALIBRATION` | | offset / calibration number, fine tune the measured voltage by the microcontroller | +| `USERMOD_BATTERY` | | Define this (in `my_config.h`) to have this usermod included wled00\usermods_list.cpp | +| `USERMOD_BATTERY_MEASUREMENT_PIN` | | Defaults to A0 on ESP8266 and GPIO35 on ESP32 | +| `USERMOD_BATTERY_MEASUREMENT_INTERVAL` | ms | Battery check interval. defaults to 30 seconds | +| `USERMOD_BATTERY_INITIAL_DELAY` | ms | Delay before initial reading. defaults to 10 seconds to allow voltage stabilization | +| `USERMOD_BATTERY_{TYPE}_MIN_VOLTAGE` | v | Minimum battery voltage. default is 2.6 (18650 battery standard) | +| `USERMOD_BATTERY_{TYPE}_MAX_VOLTAGE` | v | Maximum battery voltage. default is 4.2 (18650 battery standard) | +| `USERMOD_BATTERY_{TYPE}_TOTAL_CAPACITY` | mAh | The capacity of all cells in parallel summed up | +| `USERMOD_BATTERY_{TYPE}_CALIBRATION` | | Offset / calibration number, fine tune the measured voltage by the microcontroller | | Auto-Off | --- | --- | -| `USERMOD_BATTERY_AUTO_OFF_ENABLED` | true/false | enables auto-off | -| `USERMOD_BATTERY_AUTO_OFF_THRESHOLD` | % (0-100) | when this threshold is reached master power turns off | +| `USERMOD_BATTERY_AUTO_OFF_ENABLED` | true/false | Enables auto-off | +| `USERMOD_BATTERY_AUTO_OFF_THRESHOLD` | % (0-100) | When this threshold is reached master power turns off | | Low-Power-Indicator | --- | --- | -| `USERMOD_BATTERY_LOW_POWER_INDICATOR_ENABLED` | true/false | enables low power indication | -| `USERMOD_BATTERY_LOW_POWER_INDICATOR_PRESET` | preset id | when low power is detected then use this preset to indicate low power | -| `USERMOD_BATTERY_LOW_POWER_INDICATOR_THRESHOLD` | % (0-100) | when this threshold is reached low power gets indicated | -| `USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION` | seconds | for this long the configured preset is played | +| `USERMOD_BATTERY_LOW_POWER_INDICATOR_ENABLED` | true/false | Enables low power indication | +| `USERMOD_BATTERY_LOW_POWER_INDICATOR_PRESET` | preset id | When low power is detected then use this preset to indicate low power | +| `USERMOD_BATTERY_LOW_POWER_INDICATOR_THRESHOLD` | % (0-100) | When this threshold is reached low power gets indicated | +| `USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION` | seconds | For this long the configured preset is played | All parameters can be configured at runtime via the Usermods settings page. +
+ +**NOTICE:** Each Battery type can be pre-configured individualy (in `my_config.h`) + +| Name | Alias | `my_config.h` example | +| --------------- | ------------- | ------------------------------------- | +| Lithium Polymer | lipo (Li-Po) | `USERMOD_BATTERY_lipo_MIN_VOLTAGE` | +| Lithium Ionen | lion (Li-Ion) | `USERMOD_BATTERY_lion_TOTAL_CAPACITY` | + +

+ +## 🔧 Calibration + +The calibration number is a value that is added to the final computed voltage after it has been scaled by the voltage multiplier. + +It fine-tunes the voltage reading so that it more closely matches the actual battery voltage, compensating for inaccuracies inherent in the voltage divider resistors or the ESP's ADC measurements. + +Set calibration either in the Usermods settings page or at compile time in `my_config.h` or `platformio_override.ini`. + +It can be either a positive or negative number. + +

+ ## ⚠️ Important -- Make sure you know your battery specifications! All batteries are **NOT** the same! -- Example: +Make sure you know your battery specifications! All batteries are **NOT** the same! -| Your battery specification table | | Options you can define | -| :-------------------------------- |:--------------- | :---------------------------- | -| Capacity | 3500mAh 12,5 Wh | | -| Minimum capacity | 3350mAh 11,9 Wh | | +Example: + +| Your battery specification table | | Options you can define | +| --------------------------------- | --------------- | ----------------------------- | +| Capacity | 3500mAh 12.5Wh | | +| Minimum capacity | 3350mAh 11.9Wh | | | Rated voltage | 3.6V - 3.7V | | -| **Charging end voltage** | **4,2V ± 0,05** | `USERMOD_BATTERY_MAX_VOLTAGE` | -| **Discharge voltage** | **2,5V** | `USERMOD_BATTERY_MIN_VOLTAGE` | +| **Charging end voltage** | **4.2V ± 0.05** | `USERMOD_BATTERY_MAX_VOLTAGE` | +| **Discharge voltage** | **2.5V** | `USERMOD_BATTERY_MIN_VOLTAGE` | | Max. discharge current (constant) | 10A (10000mA) | | | max. charging current | 1.7A (1700mA) | | | ... | ... | ... | | .. | .. | .. | -Specification from: [Molicel INR18650-M35A, 3500mAh 10A Lithium-ion battery, 3.6V - 3.7V](https://www.akkuteile.de/lithium-ionen-akkus/18650/molicel/molicel-inr18650-m35a-3500mah-10a-lithium-ionen-akku-3-6v-3-7v_100833) +Specification from: [Molicel INR18650-M35A, 3500mAh 10A Lithium-ion battery, 3.6V - 3.7V](https://www.akkuteile.de/lithium-ionen-akkus/18650/molicel/molicel-inr18650-m35a-3500mah-10a-lithium-ionen-akku-3-6v-3-7v_100833) + +

## 🌐 Useful Links - https://lazyzero.de/elektronik/esp8266/wemos_d1_mini_a0/start - https://arduinodiy.wordpress.com/2016/12/25/monitoring-lipo-battery-voltage-with-wemos-d1-minibattery-shield-and-thingspeak/ +

+ ## 📝 Change Log +2024-08-19 + +- Improved MQTT support +- Added battery percentage & battery voltage as MQTT topic + +2024-05-11 + +- Documentation updated + +2024-04-30 + +- Integrate factory pattern to make it easier to add other / custom battery types +- Update readme +- Improved initial reading accuracy by delaying initial measurement to allow voltage to stabilize at power-on + 2023-01-04 -- basic support for LiPo rechargeable batteries ( `-D USERMOD_BATTERY_USE_LIPO`) -- improved support for esp32 (read calibrated voltage) -- corrected config saving (measurement pin, and battery min/max were lost) -- various bugfixes +- Basic support for LiPo rechargeable batteries (`-D USERMOD_BATTERY_USE_LIPO`) +- Improved support for ESP32 (read calibrated voltage) +- Corrected config saving (measurement pin, and battery min/max were lost) +- Various bugfixes 2022-12-25 -- added "auto-off" feature -- added "low-power-indication" feature -- added "calibration/offset" field to configuration page -- added getter and setter, so that user usermods could interact with this one -- update readme (added new options, made it markdownlint compliant) +- Added "auto-off" feature +- Added "low-power-indication" feature +- Added "calibration/offset" field to configuration page +- Added getter and setter, so that user usermods could interact with this one +- Update readme (added new options, made it markdownlint compliant) 2021-09-02 -- added "Battery voltage" to info -- added circuit diagram to readme -- added MQTT support, sending battery voltage -- minor fixes +- Added "Battery voltage" to info +- Added circuit diagram to readme +- Added MQTT support, sending battery voltage +- Minor fixes 2021-08-15 -- changed `USERMOD_BATTERY_MIN_VOLTAGE` to 2.6 volt as default for 18650 batteries +- Changed `USERMOD_BATTERY_MIN_VOLTAGE` to 2.6 volt as default for 18650 batteries - Updated readme, added specification table 2021-08-10 diff --git a/usermods/Battery/types/LionUMBattery.h b/usermods/Battery/types/LionUMBattery.h new file mode 100644 index 00000000..801faee7 --- /dev/null +++ b/usermods/Battery/types/LionUMBattery.h @@ -0,0 +1,38 @@ +#ifndef UMBLion_h +#define UMBLion_h + +#include "../battery_defaults.h" +#include "../UMBattery.h" + +/** + * LiOn Battery + * + */ +class LionUMBattery : public UMBattery +{ + private: + + public: + LionUMBattery() : UMBattery() + { + this->setMinVoltage(USERMOD_BATTERY_LION_MIN_VOLTAGE); + this->setMaxVoltage(USERMOD_BATTERY_LION_MAX_VOLTAGE); + } + + float mapVoltage(float v, float min, float max) override + { + return this->linearMapping(v, min, max); // basic mapping + }; + + void calculateAndSetLevel(float voltage) override + { + this->setLevel(this->mapVoltage(voltage, this->getMinVoltage(), this->getMaxVoltage())); + }; + + virtual void setMaxVoltage(float voltage) override + { + this->maxVoltage = max(getMinVoltage()+1.0f, voltage); + } +}; + +#endif \ No newline at end of file diff --git a/usermods/Battery/types/LipoUMBattery.h b/usermods/Battery/types/LipoUMBattery.h new file mode 100644 index 00000000..bb6a6ef9 --- /dev/null +++ b/usermods/Battery/types/LipoUMBattery.h @@ -0,0 +1,54 @@ +#ifndef UMBLipo_h +#define UMBLipo_h + +#include "../battery_defaults.h" +#include "../UMBattery.h" + +/** + * LiPo Battery + * + */ +class LipoUMBattery : public UMBattery +{ + private: + + public: + LipoUMBattery() : UMBattery() + { + this->setMinVoltage(USERMOD_BATTERY_LIPO_MIN_VOLTAGE); + this->setMaxVoltage(USERMOD_BATTERY_LIPO_MAX_VOLTAGE); + } + + /** + * LiPo batteries have a differnt discharge curve, see + * https://blog.ampow.com/lipo-voltage-chart/ + */ + float mapVoltage(float v, float min, float max) override + { + float lvl = 0.0f; + lvl = this->linearMapping(v, min, max); // basic mapping + + if (lvl < 40.0f) + lvl = this->linearMapping(lvl, 0, 40, 0, 12); // last 45% -> drops very quickly + else { + if (lvl < 90.0f) + lvl = this->linearMapping(lvl, 40, 90, 12, 95); // 90% ... 40% -> almost linear drop + else // level > 90% + lvl = this->linearMapping(lvl, 90, 105, 95, 100); // highest 15% -> drop slowly + } + + return lvl; + }; + + void calculateAndSetLevel(float voltage) override + { + this->setLevel(this->mapVoltage(voltage, this->getMinVoltage(), this->getMaxVoltage())); + }; + + virtual void setMaxVoltage(float voltage) override + { + this->maxVoltage = max(getMinVoltage()+0.7f, voltage); + } +}; + +#endif \ No newline at end of file diff --git a/usermods/Battery/types/UnkownUMBattery.h b/usermods/Battery/types/UnkownUMBattery.h new file mode 100644 index 00000000..ede5ffd8 --- /dev/null +++ b/usermods/Battery/types/UnkownUMBattery.h @@ -0,0 +1,39 @@ +#ifndef UMBUnkown_h +#define UMBUnkown_h + +#include "../battery_defaults.h" +#include "../UMBattery.h" + +/** + * Unkown / Default Battery + * + */ +class UnkownUMBattery : public UMBattery +{ + private: + + public: + UnkownUMBattery() : UMBattery() + { + this->setMinVoltage(USERMOD_BATTERY_UNKOWN_MIN_VOLTAGE); + this->setMaxVoltage(USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE); + } + + void update(batteryConfig cfg) + { + if(cfg.minVoltage) this->setMinVoltage(cfg.minVoltage); else this->setMinVoltage(USERMOD_BATTERY_UNKOWN_MIN_VOLTAGE); + if(cfg.maxVoltage) this->setMaxVoltage(cfg.maxVoltage); else this->setMaxVoltage(USERMOD_BATTERY_UNKOWN_MAX_VOLTAGE); + } + + float mapVoltage(float v, float min, float max) override + { + return this->linearMapping(v, min, max); // basic mapping + }; + + void calculateAndSetLevel(float voltage) override + { + this->setLevel(this->mapVoltage(voltage, this->getMinVoltage(), this->getMaxVoltage())); + }; +}; + +#endif \ No newline at end of file diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index be3d8748..136d3a71 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -2,12 +2,15 @@ #include "wled.h" #include "battery_defaults.h" +#include "UMBattery.h" +#include "types/UnkownUMBattery.h" +#include "types/LionUMBattery.h" +#include "types/LipoUMBattery.h" /* * Usermod by Maximilian Mewes - * Mail: mewes.maximilian@gmx.de - * GitHub: itCarl - * Date: 25.12.2022 + * E-mail: mewes.maximilian@gmx.de + * Created at: 25.12.2022 * If you have any questions, please feel free to contact me. */ class UsermodBattery : public Usermod @@ -15,49 +18,39 @@ class UsermodBattery : public Usermod private: // battery pin can be defined in my_config.h int8_t batteryPin = USERMOD_BATTERY_MEASUREMENT_PIN; + + UMBattery* bat = new UnkownUMBattery(); + batteryConfig cfg; + + // Initial delay before first reading to allow voltage stabilization + unsigned long initialDelay = USERMOD_BATTERY_INITIAL_DELAY; + bool initialDelayComplete = false; + bool isFirstVoltageReading = true; // how often to read the battery voltage unsigned long readingInterval = USERMOD_BATTERY_MEASUREMENT_INTERVAL; unsigned long nextReadTime = 0; unsigned long lastReadTime = 0; - // battery min. voltage - float minBatteryVoltage = USERMOD_BATTERY_MIN_VOLTAGE; - // battery max. voltage - float maxBatteryVoltage = USERMOD_BATTERY_MAX_VOLTAGE; - // all battery cells summed up - unsigned int totalBatteryCapacity = USERMOD_BATTERY_TOTAL_CAPACITY; - // raw analog reading - float rawValue = 0.0f; - // calculated voltage - float voltage = maxBatteryVoltage; // between 0 and 1, to control strength of voltage smoothing filter - float alpha = 0.05f; - // multiplier for the voltage divider that is in place between ADC pin and battery, default will be 2 but might be adapted to readout voltages over ~5v ESP32 or ~6.6v ESP8266 - float voltageMultiplier = USERMOD_BATTERY_VOLTAGE_MULTIPLIER; - // mapped battery level based on voltage - int8_t batteryLevel = 100; - // offset or calibration value to fine tune the calculated voltage - float calibration = USERMOD_BATTERY_CALIBRATION; - - // time left estimation feature - // bool calculateTimeLeftEnabled = USERMOD_BATTERY_CALCULATE_TIME_LEFT_ENABLED; - // float estimatedTimeLeft = 0.0; + float alpha = USERMOD_BATTERY_AVERAGING_ALPHA; // auto shutdown/shutoff/master off feature bool autoOffEnabled = USERMOD_BATTERY_AUTO_OFF_ENABLED; - int8_t autoOffThreshold = USERMOD_BATTERY_AUTO_OFF_THRESHOLD; + uint8_t autoOffThreshold = USERMOD_BATTERY_AUTO_OFF_THRESHOLD; // low power indicator feature bool lowPowerIndicatorEnabled = USERMOD_BATTERY_LOW_POWER_INDICATOR_ENABLED; - int8_t lowPowerIndicatorPreset = USERMOD_BATTERY_LOW_POWER_INDICATOR_PRESET; - int8_t lowPowerIndicatorThreshold = USERMOD_BATTERY_LOW_POWER_INDICATOR_THRESHOLD; - int8_t lowPowerIndicatorReactivationThreshold = lowPowerIndicatorThreshold+10; - int8_t lowPowerIndicatorDuration = USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION; + uint8_t lowPowerIndicatorPreset = USERMOD_BATTERY_LOW_POWER_INDICATOR_PRESET; + uint8_t lowPowerIndicatorThreshold = USERMOD_BATTERY_LOW_POWER_INDICATOR_THRESHOLD; + uint8_t lowPowerIndicatorReactivationThreshold = lowPowerIndicatorThreshold+10; + uint8_t lowPowerIndicatorDuration = USERMOD_BATTERY_LOW_POWER_INDICATOR_DURATION; bool lowPowerIndicationDone = false; unsigned long lowPowerActivationTime = 0; // used temporary during active time - int8_t lastPreset = 0; + uint8_t lastPreset = 0; + // bool initDone = false; bool initializing = true; + bool HomeAssistantDiscovery = false; // strings to reduce flash memory usage (used more than twice) static const char _name[]; @@ -67,22 +60,29 @@ class UsermodBattery : public Usermod static const char _preset[]; static const char _duration[]; static const char _init[]; - - - // custom map function - // https://forum.arduino.cc/t/floating-point-using-map-function/348113/2 - double mapf(double x, double in_min, double in_max, double out_min, double out_max) - { - return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; - } + static const char _haDiscovery[]; + /** + * Helper for rounding floating point values + */ float dot2round(float x) { float nx = (int)(x * 100 + .5); return (float)(nx / 100); } - /* + /** + * Helper for converting a string to lowercase + */ + String stringToLower(String str) + { + for(int i = 0; i < str.length(); i++) + if(str[i] >= 'A' && str[i] <= 'Z') + str[i] += 32; + return str; + } + + /** * Turn off all leds */ void turnOff() @@ -91,15 +91,15 @@ class UsermodBattery : public Usermod stateUpdated(CALL_MODE_DIRECT_CHANGE); } - /* + /** * Indicate low power by activating a configured preset for a given time and then switching back to the preset that was selected previously */ void lowPowerIndicator() { if (!lowPowerIndicatorEnabled) return; if (batteryPin < 0) return; // no measurement - if (lowPowerIndicationDone && lowPowerIndicatorReactivationThreshold <= batteryLevel) lowPowerIndicationDone = false; - if (lowPowerIndicatorThreshold <= batteryLevel) return; + if (lowPowerIndicationDone && lowPowerIndicatorReactivationThreshold <= bat->getLevel()) lowPowerIndicationDone = false; + if (lowPowerIndicatorThreshold <= bat->getLevel()) return; if (lowPowerIndicationDone) return; if (lowPowerActivationTime <= 1) { lowPowerActivationTime = millis(); @@ -114,26 +114,88 @@ class UsermodBattery : public Usermod } } + /** + * read the battery voltage in different ways depending on the architecture + */ float readVoltage() { #ifdef ARDUINO_ARCH_ESP32 // use calibrated millivolts analogread on esp32 (150 mV ~ 2450 mV default attentuation) and divide by 1000 to get from milivolts to volts and multiply by voltage multiplier and apply calibration value - return (analogReadMilliVolts(batteryPin) / 1000.0f) * voltageMultiplier + calibration; + return (analogReadMilliVolts(batteryPin) / 1000.0f) * bat->getVoltageMultiplier() + bat->getCalibration(); #else // use analog read on esp8266 ( 0V ~ 1V no attenuation options) and divide by ADC precision 1023 and multiply by voltage multiplier and apply calibration value - return (analogRead(batteryPin) / 1023.0f) * voltageMultiplier + calibration; + return (analogRead(batteryPin) / 1023.0f) * bat->getVoltageMultiplier() + bat->getCalibration(); #endif } +#ifndef WLED_DISABLE_MQTT + void addMqttSensor(const String &name, const String &type, const String &topic, const String &deviceClass, const String &unitOfMeasurement = "", const bool &isDiagnostic = false) + { + StaticJsonDocument<600> doc; + char uid[128], json_str[1024], buf[128]; + + doc[F("name")] = name; + doc[F("stat_t")] = topic; + sprintf_P(uid, PSTR("%s_%s_%s"), escapedMac.c_str(), stringToLower(name).c_str(), type); + doc[F("uniq_id")] = uid; + doc[F("dev_cla")] = deviceClass; + doc[F("exp_aft")] = 1800; + + if(type == "binary_sensor") { + doc[F("pl_on")] = "on"; + doc[F("pl_off")] = "off"; + } + + if(unitOfMeasurement != "") + doc[F("unit_of_measurement")] = unitOfMeasurement; + + if(isDiagnostic) + doc[F("entity_category")] = "diagnostic"; + + JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device + device[F("name")] = serverDescription; + device[F("ids")] = String(F("wled-sensor-")) + mqttClientID; + device[F("mf")] = F(WLED_BRAND); + device[F("mdl")] = F(WLED_PRODUCT_NAME); + device[F("sw")] = versionString; + + sprintf_P(buf, PSTR("homeassistant/%s/%s/%s/config"), type, mqttClientID, uid); + DEBUG_PRINTLN(buf); + size_t payload_size = serializeJson(doc, json_str); + DEBUG_PRINTLN(json_str); + + mqtt->publish(buf, 0, true, json_str, payload_size); + } + + void publishMqtt(const char* topic, const char* state) + { + if (WLED_MQTT_CONNECTED) { + char buf[128]; + snprintf_P(buf, 127, PSTR("%s/%s"), mqttDeviceTopic, topic); + mqtt->publish(buf, 0, false, state); + } + } +#endif + public: //Functions called by WLED - /* + /** * setup() is called once at boot. WiFi is not yet connected at this point. * You can use it to initialize variables, sensors or similar. */ void setup() { + // plug in the right battery type + if(cfg.type == (batteryType)lipo) { + bat = new LipoUMBattery(); + } else if(cfg.type == (batteryType)lion) { + bat = new LionUMBattery(); + } + + // update the choosen battery type with configured values + bat->update(cfg); + #ifdef ARDUINO_ARCH_ESP32 bool success = false; DEBUG_PRINTLN(F("Allocating battery pin...")); @@ -141,7 +203,6 @@ class UsermodBattery : public Usermod if (pinManager.allocatePin(batteryPin, false, PinOwner::UM_Battery)) { DEBUG_PRINTLN(F("Battery pin allocation succeeded.")); success = true; - voltage = readVoltage(); } if (!success) { @@ -152,17 +213,17 @@ class UsermodBattery : public Usermod } #else //ESP8266 boards have only one analog input pin A0 pinMode(batteryPin, INPUT); - voltage = readVoltage(); #endif - nextReadTime = millis() + readingInterval; + // First voltage reading is delayed to allow voltage stabilization after powering up + nextReadTime = millis() + initialDelay; lastReadTime = millis(); initDone = true; } - /* + /** * connected() is called every time the WiFi is (re)connected * Use it to initialize network interfaces */ @@ -182,6 +243,25 @@ class UsermodBattery : public Usermod lowPowerIndicator(); + // Handling the initial delay + if (!initialDelayComplete && millis() < nextReadTime) + return; // Continue to return until the initial delay is over + + // Once the initial delay is over, set it as complete + if (!initialDelayComplete) + { + initialDelayComplete = true; + // Set the regular interval after initial delay + nextReadTime = millis() + readingInterval; + } + + // Make the first voltage reading after the initial delay has elapsed + if (isFirstVoltageReading) + { + bat->setVoltage(readVoltage()); + isFirstVoltageReading = false; + } + // check the battery level every USERMOD_BATTERY_MEASUREMENT_INTERVAL (ms) if (millis() < nextReadTime) return; @@ -191,58 +271,27 @@ class UsermodBattery : public Usermod if (batteryPin < 0) return; // nothing to read initializing = false; + float rawValue = readVoltage(); - rawValue = readVoltage(); // filter with exponential smoothing because ADC in esp32 is fluctuating too much for a good single readout - voltage = voltage + alpha * (rawValue - voltage); - - // check if voltage is within specified voltage range, allow 10% over/under voltage - removed cause this just makes it hard for people to troubleshoot as the voltage in the web gui will say invalid instead of displaying a voltage - //voltage = ((voltage < minBatteryVoltage * 0.85f) || (voltage > maxBatteryVoltage * 1.1f)) ? -1.0f : voltage; + float filteredVoltage = bat->getVoltage() + alpha * (rawValue - bat->getVoltage()); + bat->setVoltage(filteredVoltage); // translate battery voltage into percentage - /* - the standard "map" function doesn't work - https://www.arduino.cc/reference/en/language/functions/math/map/ notes and warnings at the bottom - */ - #ifdef USERMOD_BATTERY_USE_LIPO - batteryLevel = mapf(voltage, minBatteryVoltage, maxBatteryVoltage, 0, 100); // basic mapping - // LiPo batteries have a differnt dischargin curve, see - // https://blog.ampow.com/lipo-voltage-chart/ - if (batteryLevel < 40.0f) - batteryLevel = mapf(batteryLevel, 0, 40, 0, 12); // last 45% -> drops very quickly - else { - if (batteryLevel < 90.0f) - batteryLevel = mapf(batteryLevel, 40, 90, 12, 95); // 90% ... 40% -> almost linear drop - else // level > 90% - batteryLevel = mapf(batteryLevel, 90, 105, 95, 100); // highest 15% -> drop slowly - } - #else - batteryLevel = mapf(voltage, minBatteryVoltage, maxBatteryVoltage, 0, 100); - #endif - if (voltage > -1.0f) batteryLevel = constrain(batteryLevel, 0.0f, 110.0f); - - // if (calculateTimeLeftEnabled) { - // float currentBatteryCapacity = totalBatteryCapacity; - // estimatedTimeLeft = (currentBatteryCapacity/strip.currentMilliamps)*60; - // } + bat->calculateAndSetLevel(filteredVoltage); // Auto off -- Master power off - if (autoOffEnabled && (autoOffThreshold >= batteryLevel)) + if (autoOffEnabled && (autoOffThreshold >= bat->getLevel())) turnOff(); #ifndef WLED_DISABLE_MQTT - // SmartHome stuff - // still don't know much about MQTT and/or HA - if (WLED_MQTT_CONNECTED) { - char buf[64]; // buffer for snprintf() - snprintf_P(buf, 63, PSTR("%s/voltage"), mqttDeviceTopic); - mqtt->publish(buf, 0, false, String(voltage).c_str()); - } + publishMqtt("battery", String(bat->getLevel(), 0).c_str()); + publishMqtt("voltage", String(bat->getVoltage()).c_str()); #endif } - /* + /** * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. * Below it is shown how this could be used for e.g. a light sensor @@ -262,16 +311,6 @@ class UsermodBattery : public Usermod // info modal display names JsonArray infoPercentage = user.createNestedArray(F("Battery level")); JsonArray infoVoltage = user.createNestedArray(F("Battery voltage")); - // if (calculateTimeLeftEnabled) - // { - // JsonArray infoEstimatedTimeLeft = user.createNestedArray(F("Estimated time left")); - // if (initializing) { - // infoEstimatedTimeLeft.add(FPSTR(_init)); - // } else { - // infoEstimatedTimeLeft.add(estimatedTimeLeft); - // infoEstimatedTimeLeft.add(F(" min")); - // } - // } JsonArray infoNextUpdate = user.createNestedArray(F("Next update")); infoNextUpdate.add((nextReadTime - millis()) / 1000); @@ -283,46 +322,105 @@ class UsermodBattery : public Usermod return; } - if (batteryLevel < 0) { + if (bat->getLevel() < 0) { infoPercentage.add(F("invalid")); } else { - infoPercentage.add(batteryLevel); + infoPercentage.add(bat->getLevel()); } infoPercentage.add(F(" %")); - if (voltage < 0) { + if (bat->getVoltage() < 0) { infoVoltage.add(F("invalid")); } else { - infoVoltage.add(dot2round(voltage)); + infoVoltage.add(dot2round(bat->getVoltage())); } infoVoltage.add(F(" V")); } + void addBatteryToJsonObject(JsonObject& battery, bool forJsonState) + { + if(forJsonState) { battery[F("type")] = cfg.type; } else {battery[F("type")] = (String)cfg.type; } // has to be a String otherwise it won't get converted to a Dropdown + battery[F("min-voltage")] = bat->getMinVoltage(); + battery[F("max-voltage")] = bat->getMaxVoltage(); + battery[F("calibration")] = bat->getCalibration(); + battery[F("voltage-multiplier")] = bat->getVoltageMultiplier(); + battery[FPSTR(_readInterval)] = readingInterval; + battery[FPSTR(_haDiscovery)] = HomeAssistantDiscovery; - /* + JsonObject ao = battery.createNestedObject(F("auto-off")); // auto off section + ao[FPSTR(_enabled)] = autoOffEnabled; + ao[FPSTR(_threshold)] = autoOffThreshold; + + JsonObject lp = battery.createNestedObject(F("indicator")); // low power section + lp[FPSTR(_enabled)] = lowPowerIndicatorEnabled; + lp[FPSTR(_preset)] = lowPowerIndicatorPreset; // dropdown trickery (String)lowPowerIndicatorPreset; + lp[FPSTR(_threshold)] = lowPowerIndicatorThreshold; + lp[FPSTR(_duration)] = lowPowerIndicatorDuration; + } + + void getUsermodConfigFromJsonObject(JsonObject& battery) + { + getJsonValue(battery[F("type")], cfg.type); + getJsonValue(battery[F("min-voltage")], cfg.minVoltage); + getJsonValue(battery[F("max-voltage")], cfg.maxVoltage); + getJsonValue(battery[F("calibration")], cfg.calibration); + getJsonValue(battery[F("voltage-multiplier")], cfg.voltageMultiplier); + setReadingInterval(battery[FPSTR(_readInterval)] | readingInterval); + setHomeAssistantDiscovery(battery[FPSTR(_haDiscovery)] | HomeAssistantDiscovery); + + JsonObject ao = battery[F("auto-off")]; + setAutoOffEnabled(ao[FPSTR(_enabled)] | autoOffEnabled); + setAutoOffThreshold(ao[FPSTR(_threshold)] | autoOffThreshold); + + JsonObject lp = battery[F("indicator")]; + setLowPowerIndicatorEnabled(lp[FPSTR(_enabled)] | lowPowerIndicatorEnabled); + setLowPowerIndicatorPreset(lp[FPSTR(_preset)] | lowPowerIndicatorPreset); + setLowPowerIndicatorThreshold(lp[FPSTR(_threshold)] | lowPowerIndicatorThreshold); + lowPowerIndicatorReactivationThreshold = lowPowerIndicatorThreshold+10; + setLowPowerIndicatorDuration(lp[FPSTR(_duration)] | lowPowerIndicatorDuration); + + if(initDone) + bat->update(cfg); + } + + /** * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). * Values in the state object may be modified by connected clients */ - /* void addToJsonState(JsonObject& root) { + JsonObject battery = root.createNestedObject(FPSTR(_name)); + if (battery.isNull()) + battery = root.createNestedObject(FPSTR(_name)); + + addBatteryToJsonObject(battery, true); + + DEBUG_PRINTLN(F("Battery state exposed in JSON API.")); } - */ - /* + /** * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). * Values in the state object may be modified by connected clients */ /* void readFromJsonState(JsonObject& root) { + if (!initDone) return; // prevent crash on boot applyPreset() + + JsonObject battery = root[FPSTR(_name)]; + + if (!battery.isNull()) { + getUsermodConfigFromJsonObject(battery); + + DEBUG_PRINTLN(F("Battery state read from JSON API.")); + } } */ - /* + /** * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. * It will be called by WLED when settings are actually saved (for example, LED settings are saved) * If you want to force saving the current state, use serializeConfig() in your loop(). @@ -359,47 +457,42 @@ class UsermodBattery : public Usermod */ void addToConfig(JsonObject& root) { - JsonObject battery = root.createNestedObject(FPSTR(_name)); // usermodname + JsonObject battery = root.createNestedObject(FPSTR(_name)); + + if (battery.isNull()) { + battery = root.createNestedObject(FPSTR(_name)); + } + #ifdef ARDUINO_ARCH_ESP32 battery[F("pin")] = batteryPin; #endif - - // battery[F("time-left")] = calculateTimeLeftEnabled; - battery[F("min-voltage")] = minBatteryVoltage; - battery[F("max-voltage")] = maxBatteryVoltage; - battery[F("capacity")] = totalBatteryCapacity; - battery[F("calibration")] = calibration; - battery[F("voltage-multiplier")] = voltageMultiplier; - battery[FPSTR(_readInterval)] = readingInterval; - JsonObject ao = battery.createNestedObject(F("auto-off")); // auto off section - ao[FPSTR(_enabled)] = autoOffEnabled; - ao[FPSTR(_threshold)] = autoOffThreshold; - - JsonObject lp = battery.createNestedObject(F("indicator")); // low power section - lp[FPSTR(_enabled)] = lowPowerIndicatorEnabled; - lp[FPSTR(_preset)] = lowPowerIndicatorPreset; // dropdown trickery (String)lowPowerIndicatorPreset; - lp[FPSTR(_threshold)] = lowPowerIndicatorThreshold; - lp[FPSTR(_duration)] = lowPowerIndicatorDuration; + addBatteryToJsonObject(battery, false); // read voltage in case calibration or voltage multiplier changed to see immediate effect - voltage = readVoltage(); + bat->setVoltage(readVoltage()); DEBUG_PRINTLN(F("Battery config saved.")); } void appendConfigData() { - oappend(SET_F("addInfo('Battery:min-voltage', 1, 'v');")); - oappend(SET_F("addInfo('Battery:max-voltage', 1, 'v');")); - oappend(SET_F("addInfo('Battery:capacity', 1, 'mAh');")); - oappend(SET_F("addInfo('Battery:interval', 1, 'ms');")); - oappend(SET_F("addInfo('Battery:auto-off:threshold', 1, '%');")); - oappend(SET_F("addInfo('Battery:indicator:threshold', 1, '%');")); - oappend(SET_F("addInfo('Battery:indicator:duration', 1, 's');")); + // Total: 462 Bytes + oappend(SET_F("td=addDropdown('Battery','type');")); // 34 Bytes + oappend(SET_F("addOption(td,'Unkown','0');")); // 28 Bytes + oappend(SET_F("addOption(td,'LiPo','1');")); // 26 Bytes + oappend(SET_F("addOption(td,'LiOn','2');")); // 26 Bytes + oappend(SET_F("addInfo('Battery:type',1,'requires reboot');")); // 81 Bytes + oappend(SET_F("addInfo('Battery:min-voltage',1,'v');")); // 38 Bytes + oappend(SET_F("addInfo('Battery:max-voltage',1,'v');")); // 38 Bytes + oappend(SET_F("addInfo('Battery:interval',1,'ms');")); // 36 Bytes + oappend(SET_F("addInfo('Battery:HA-discovery',1,'');")); // 38 Bytes + oappend(SET_F("addInfo('Battery:auto-off:threshold',1,'%');")); // 45 Bytes + oappend(SET_F("addInfo('Battery:indicator:threshold',1,'%');")); // 46 Bytes + oappend(SET_F("addInfo('Battery:indicator:duration',1,'s');")); // 45 Bytes - // cannot quite get this mf to work. its exeeding some buffer limit i think - // what i wanted is a list of all presets to select one from + // this option list would exeed the oappend() buffer + // a list of all presets to select one from // oappend(SET_F("bd=addDropdown('Battery:low-power-indicator', 'preset');")); // the loop generates: oappend(SET_F("addOption(bd, 'preset name', preset id);")); // for(int8_t i=1; i < 42; i++) { @@ -412,7 +505,7 @@ class UsermodBattery : public Usermod } - /* + /** * readFromConfig() can be used to read back the custom settings you added with addToConfig(). * This is called by WLED when settings are loaded (currently this only happens immediately after boot, or after saving on the Usermod Settings page) * @@ -444,26 +537,14 @@ class UsermodBattery : public Usermod #ifdef ARDUINO_ARCH_ESP32 newBatteryPin = battery[F("pin")] | newBatteryPin; #endif - // calculateTimeLeftEnabled = battery[F("time-left")] | calculateTimeLeftEnabled; - setMinBatteryVoltage(battery[F("min-voltage")] | minBatteryVoltage); - setMaxBatteryVoltage(battery[F("max-voltage")] | maxBatteryVoltage); - setTotalBatteryCapacity(battery[F("capacity")] | totalBatteryCapacity); - setCalibration(battery[F("calibration")] | calibration); - setVoltageMultiplier(battery[F("voltage-multiplier")] | voltageMultiplier); + setMinBatteryVoltage(battery[F("min-voltage")] | bat->getMinVoltage()); + setMaxBatteryVoltage(battery[F("max-voltage")] | bat->getMaxVoltage()); + setCalibration(battery[F("calibration")] | bat->getCalibration()); + setVoltageMultiplier(battery[F("voltage-multiplier")] | bat->getVoltageMultiplier()); setReadingInterval(battery[FPSTR(_readInterval)] | readingInterval); + setHomeAssistantDiscovery(battery[FPSTR(_haDiscovery)] | HomeAssistantDiscovery); - JsonObject ao = battery[F("auto-off")]; - setAutoOffEnabled(ao[FPSTR(_enabled)] | autoOffEnabled); - setAutoOffThreshold(ao[FPSTR(_threshold)] | autoOffThreshold); - - JsonObject lp = battery[F("indicator")]; - setLowPowerIndicatorEnabled(lp[FPSTR(_enabled)] | lowPowerIndicatorEnabled); - setLowPowerIndicatorPreset(lp[FPSTR(_preset)] | lowPowerIndicatorPreset); // dropdown trickery (int)lp["preset"] - setLowPowerIndicatorThreshold(lp[FPSTR(_threshold)] | lowPowerIndicatorThreshold); - lowPowerIndicatorReactivationThreshold = lowPowerIndicatorThreshold+10; - setLowPowerIndicatorDuration(lp[FPSTR(_duration)] | lowPowerIndicatorDuration); - - DEBUG_PRINT(FPSTR(_name)); + getUsermodConfigFromJsonObject(battery); #ifdef ARDUINO_ARCH_ESP32 if (!initDone) @@ -491,37 +572,24 @@ class UsermodBattery : public Usermod return !battery[FPSTR(_readInterval)].isNull(); } - /* - * Generate a preset sample for low power indication - */ - void generateExamplePreset() +#ifndef WLED_DISABLE_MQTT + void onMqttConnect(bool sessionPresent) { - // StaticJsonDocument<300> j; - // JsonObject preset = j.createNestedObject(); - // preset["mainseg"] = 0; - // JsonArray seg = preset.createNestedArray("seg"); - // JsonObject seg0 = seg.createNestedObject(); - // seg0["id"] = 0; - // seg0["start"] = 0; - // seg0["stop"] = 60; - // seg0["grp"] = 0; - // seg0["spc"] = 0; - // seg0["on"] = true; - // seg0["bri"] = 255; + // Home Assistant Autodiscovery + if (!HomeAssistantDiscovery) + return; - // JsonArray col0 = seg0.createNestedArray("col"); - // JsonArray col00 = col0.createNestedArray(); - // col00.add(255); - // col00.add(0); - // col00.add(0); + // battery percentage + char mqttBatteryTopic[128]; + snprintf_P(mqttBatteryTopic, 127, PSTR("%s/battery"), mqttDeviceTopic); + this->addMqttSensor(F("Battery"), "sensor", mqttBatteryTopic, "battery", "%", true); - // seg0["fx"] = 1; - // seg0["sx"] = 128; - // seg0["ix"] = 128; - - // savePreset(199, "Low power Indicator", preset); + // voltage + char mqttVoltageTopic[128]; + snprintf_P(mqttVoltageTopic, 127, PSTR("%s/voltage"), mqttDeviceTopic); + this->addMqttSensor(F("Voltage"), "sensor", mqttVoltageTopic, "voltage", "V", true); } - +#endif /* * @@ -529,7 +597,7 @@ class UsermodBattery : public Usermod * */ - /* + /** * 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. */ @@ -538,13 +606,23 @@ class UsermodBattery : public Usermod return USERMOD_ID_BATTERY; } + /** + * get currently active battery type + */ + batteryType getBatteryType() + { + return cfg.type; + } + /** + * + */ unsigned long getReadingInterval() { return readingInterval; } - /* + /** * minimum repetition is 3000ms (3s) */ void setReadingInterval(unsigned long newReadingInterval) @@ -552,105 +630,84 @@ class UsermodBattery : public Usermod readingInterval = max((unsigned long)3000, newReadingInterval); } - - /* + /** * Get lowest configured battery voltage */ float getMinBatteryVoltage() { - return minBatteryVoltage; + return bat->getMinVoltage(); } - /* + /** * Set lowest battery voltage * can't be below 0 volt */ void setMinBatteryVoltage(float voltage) { - minBatteryVoltage = max(0.0f, voltage); + bat->setMinVoltage(voltage); } - /* + /** * Get highest configured battery voltage */ float getMaxBatteryVoltage() { - return maxBatteryVoltage; + return bat->getMaxVoltage(); } - /* + /** * Set highest battery voltage * can't be below minBatteryVoltage */ void setMaxBatteryVoltage(float voltage) { - #ifdef USERMOD_BATTERY_USE_LIPO - maxBatteryVoltage = max(getMinBatteryVoltage()+0.7f, voltage); - #else - maxBatteryVoltage = max(getMinBatteryVoltage()+1.0f, voltage); - #endif + bat->setMaxVoltage(voltage); } - /* - * Get the capacity of all cells in parralel sumed up - * unit: mAh - */ - unsigned int getTotalBatteryCapacity() - { - return totalBatteryCapacity; - } - - void setTotalBatteryCapacity(unsigned int capacity) - { - totalBatteryCapacity = capacity; - } - - - - /* + /** * Get the calculated voltage * formula: (adc pin value / adc precision * max voltage) + calibration */ float getVoltage() { - return voltage; + return bat->getVoltage(); } - /* + /** * Get the mapped battery level (0 - 100) based on voltage * important: voltage can drop when a load is applied, so its only an estimate */ int8_t getBatteryLevel() { - return batteryLevel; + return bat->getLevel(); } - /* + /** * Get the configured calibration value * a offset value to fine-tune the calculated voltage. */ float getCalibration() { - return calibration; + return bat->getCalibration(); } - /* + /** * Set the voltage calibration offset value * a offset value to fine-tune the calculated voltage. */ void setCalibration(float offset) { - calibration = offset; + bat->setCalibration(offset); } - /* + /** * Set the voltage multiplier value * A multiplier that may need adjusting for different voltage divider setups */ void setVoltageMultiplier(float multiplier) { - voltageMultiplier = multiplier; + bat->setVoltageMultiplier(multiplier); } /* @@ -659,10 +716,10 @@ class UsermodBattery : public Usermod */ float getVoltageMultiplier() { - return voltageMultiplier; + return bat->getVoltageMultiplier(); } - /* + /** * Get auto-off feature enabled status * is auto-off enabled, true/false */ @@ -671,7 +728,7 @@ class UsermodBattery : public Usermod return autoOffEnabled; } - /* + /** * Set auto-off feature status */ void setAutoOffEnabled(bool enabled) @@ -679,7 +736,7 @@ class UsermodBattery : public Usermod autoOffEnabled = enabled; } - /* + /** * Get auto-off threshold in percent (0-100) */ int8_t getAutoOffThreshold() @@ -687,7 +744,7 @@ class UsermodBattery : public Usermod return autoOffThreshold; } - /* + /** * Set auto-off threshold in percent (0-100) */ void setAutoOffThreshold(int8_t threshold) @@ -697,8 +754,7 @@ class UsermodBattery : public Usermod autoOffThreshold = lowPowerIndicatorEnabled /*&& autoOffEnabled*/ ? min(lowPowerIndicatorThreshold-1, (int)autoOffThreshold) : autoOffThreshold; } - - /* + /** * Get low-power-indicator feature enabled status * is the low-power-indicator enabled, true/false */ @@ -707,7 +763,7 @@ class UsermodBattery : public Usermod return lowPowerIndicatorEnabled; } - /* + /** * Set low-power-indicator feature status */ void setLowPowerIndicatorEnabled(bool enabled) @@ -715,7 +771,7 @@ class UsermodBattery : public Usermod lowPowerIndicatorEnabled = enabled; } - /* + /** * Get low-power-indicator preset to activate when low power is detected */ int8_t getLowPowerIndicatorPreset() @@ -723,7 +779,7 @@ class UsermodBattery : public Usermod return lowPowerIndicatorPreset; } - /* + /** * Set low-power-indicator preset to activate when low power is detected */ void setLowPowerIndicatorPreset(int8_t presetId) @@ -741,7 +797,7 @@ class UsermodBattery : public Usermod return lowPowerIndicatorThreshold; } - /* + /** * Set low-power-indicator threshold in percent (0-100) */ void setLowPowerIndicatorThreshold(int8_t threshold) @@ -751,7 +807,7 @@ class UsermodBattery : public Usermod lowPowerIndicatorThreshold = autoOffEnabled /*&& lowPowerIndicatorEnabled*/ ? max(autoOffThreshold+1, (int)lowPowerIndicatorThreshold) : max(5, (int)lowPowerIndicatorThreshold); } - /* + /** * Get low-power-indicator duration in seconds */ int8_t getLowPowerIndicatorDuration() @@ -759,7 +815,7 @@ class UsermodBattery : public Usermod return lowPowerIndicatorDuration; } - /* + /** * Set low-power-indicator duration in seconds */ void setLowPowerIndicatorDuration(int8_t duration) @@ -767,14 +823,29 @@ class UsermodBattery : public Usermod lowPowerIndicatorDuration = duration; } - - /* - * Get low-power-indicator status when the indication is done thsi returns true + /** + * Get low-power-indicator status when the indication is done this returns true */ bool getLowPowerIndicatorDone() { return lowPowerIndicationDone; } + + /** + * Set Home Assistant auto discovery + */ + void setHomeAssistantDiscovery(bool enable) + { + HomeAssistantDiscovery = enable; + } + + /** + * Get Home Assistant auto discovery + */ + bool getHomeAssistantDiscovery() + { + return HomeAssistantDiscovery; + } }; // strings to reduce flash memory usage (used more than twice) @@ -785,3 +856,4 @@ const char UsermodBattery::_threshold[] PROGMEM = "threshold"; const char UsermodBattery::_preset[] PROGMEM = "preset"; const char UsermodBattery::_duration[] PROGMEM = "duration"; const char UsermodBattery::_init[] PROGMEM = "init"; +const char UsermodBattery::_haDiscovery[] PROGMEM = "HA-discovery"; diff --git a/usermods/INA226_v2/README.md b/usermods/INA226_v2/README.md new file mode 100644 index 00000000..b1e69161 --- /dev/null +++ b/usermods/INA226_v2/README.md @@ -0,0 +1,77 @@ +# Usermod INA226 + +This Usermod is designed to read values from an INA226 sensor and output the following: +- Current +- Voltage +- Power +- Shunt Voltage +- Overflow status + +## Configuration + +The following settings can be configured in the Usermod Menu: +- **Enabled**: Enable or disable the usermod. +- **I2CAddress**: The I2C address in decimal. Default is 64 (0x40). +- **CheckInterval**: Number of seconds between readings. This should be higher than the time it takes to make a reading, determined by the two next options. +- **INASamples**: The number of samples to configure the INA226 to use for a measurement. Higher counts provide more accuracy. See the 'Understanding Samples and Conversion Times' section for more details. +- **INAConversionTime**: The time to use on converting and preparing readings on the INA226. Higher times provide more precision. See the 'Understanding Samples and Conversion Times' section for more details. +- **Decimals**: Number of decimals in the output. +- **ShuntResistor**: Shunt resistor value in milliohms. An R100 shunt resistor should be written as "100", while R010 should be "10". +- **CurrentRange**: Expected maximum current in milliamps (e.g., 5 A = 5000 mA). +- **MqttPublish**: Enable or disable MQTT publishing. +- **MqttPublishAlways**: Publish always, regardless if there is a change. +- **MqttHomeAssistantDiscovery**: Enable Home Assistant discovery. + +## Dependencies + +These must be added under `lib_deps` in your `platform.ini` (or `platform_override.ini`). + +- Libraries + - `wollewald/INA226_WE@~1.2.9` (by [wollewald](https://registry.platformio.org/libraries/wollewald/INA226_WE)) + - `Wire` + +## Understanding Samples and Conversion Times + +The INA226 uses a programmable ADC with configurable conversion times and averaging to optimize the measurement accuracy and speed. The conversion time and number of samples are determined based on the `INASamples` and `INAConversionTime` settings. The following table outlines the possible combinations: + +| Conversion Time (μs) | 1 Sample | 4 Samples | 16 Samples | 64 Samples | 128 Samples | 256 Samples | 512 Samples | 1024 Samples | +|----------------------|----------|-----------|------------|------------|-------------|-------------|-------------|--------------| +| 140 | 0.28 ms | 1.12 ms | 4.48 ms | 17.92 ms | 35.84 ms | 71.68 ms | 143.36 ms | 286.72 ms | +| 204 | 0.408 ms | 1.632 ms | 6.528 ms | 26.112 ms | 52.224 ms | 104.448 ms | 208.896 ms | 417.792 ms | +| 332 | 0.664 ms | 2.656 ms | 10.624 ms | 42.496 ms | 84.992 ms | 169.984 ms | 339.968 ms | 679.936 ms | +| 588 | 1.176 ms | 4.704 ms | 18.816 ms | 75.264 ms | 150.528 ms | 301.056 ms | 602.112 ms | 1204.224 ms | +| 1100 | 2.2 ms | 8.8 ms | 35.2 ms | 140.8 ms | 281.6 ms | 563.2 ms | 1126.4 ms | 2252.8 ms | +| 2116 | 4.232 ms | 16.928 ms | 67.712 ms | 270.848 ms | 541.696 ms | 1083.392 ms | 2166.784 ms | 4333.568 ms | +| 4156 | 8.312 ms | 33.248 ms | 132.992 ms | 531.968 ms | 1063.936 ms | 2127.872 ms | 4255.744 ms | 8511.488 ms | +| 8244 | 16.488 ms| 65.952 ms | 263.808 ms | 1055.232 ms| 2110.464 ms | 4220.928 ms | 8441.856 ms | 16883.712 ms | + +It is important to pick a combination that provides the needed balance between accuracy and precision while ensuring new readings within the `CheckInterval` setting. When `USERMOD_INA226_DEBUG` is defined, the info pane contains the expected time to make a reading, which can be seen in the table above. + +As an example, if you want a new reading every 5 seconds (`CheckInterval`), a valid combination is `256 samples` and `4156 μs` which would provide new values every 2.1 seconds. + +The picked values also slightly affect power usage. If the `CheckInterval` is set to more than 20 seconds, the INA226 is configured in `triggered` reading mode, where it only uses power as long as it's working. Then the conversion time and average samples counts determine how long the chip stays turned on every `CheckInterval` time. + +### Calculating Current and Power + +The INA226 calculates current by measuring the differential voltage across a shunt resistor and using the calibration register value to convert this measurement into current. Power is calculated by multiplying the current by the bus voltage. + +For detailed programming information and register configurations, refer to the [INA226 datasheet](https://www.ti.com/product/INA226). + +## Author +[@LordMike](https://github.com/LordMike) + +## Compiling + +To enable, compile with `USERMOD_INA226` defined (e.g. in `platformio_override.ini`). + +```ini +[env:ina226_example] +extends = env:esp32dev +build_flags = + ${common.build_flags} ${esp32.build_flags} + -D USERMOD_INA226 + ; -D USERMOD_INA226_DEBUG ; -- add a debug status to the info modal +lib_deps = + ${esp32.lib_deps} + wollewald/INA226_WE@~1.2.9 +``` \ No newline at end of file diff --git a/usermods/INA226_v2/platformio_override.ini b/usermods/INA226_v2/platformio_override.ini new file mode 100644 index 00000000..885b2dd1 --- /dev/null +++ b/usermods/INA226_v2/platformio_override.ini @@ -0,0 +1,9 @@ +[env:ina226_example] +extends = env:esp32dev +build_flags = + ${common.build_flags} ${esp32.build_flags} + -D USERMOD_INA226 + ; -D USERMOD_INA226_DEBUG ; -- add a debug status to the info modal +lib_deps = + ${esp32.lib_deps} + wollewald/INA226_WE@~1.2.9 \ No newline at end of file diff --git a/usermods/INA226_v2/usermod_ina226.h b/usermods/INA226_v2/usermod_ina226.h new file mode 100644 index 00000000..52bc3d83 --- /dev/null +++ b/usermods/INA226_v2/usermod_ina226.h @@ -0,0 +1,556 @@ +#pragma once + +#include "wled.h" +#include + +#define INA226_ADDRESS 0x40 // Default I2C address for INA226 + +#define DEFAULT_CHECKINTERVAL 60000 +#define DEFAULT_INASAMPLES 128 +#define DEFAULT_INASAMPLESENUM AVERAGE_128 +#define DEFAULT_INACONVERSIONTIME 1100 +#define DEFAULT_INACONVERSIONTIMEENUM CONV_TIME_1100 + +// A packed version of all INA settings enums and their human friendly counterparts packed into a 32 bit structure +// Some values are shifted and need to be preprocessed before usage +struct InaSettingLookup +{ + uint16_t avgSamples : 11; // Max 1024, which could be in 10 bits if we shifted by 1; if we somehow handle the edge case with "1" + uint8_t avgEnum : 4; // Shift by 8 to get the INA226_AVERAGES value, accepts 0x00 to 0x0F, we need 0x00 to 0x0E + uint16_t convTimeUs : 14; // We could save 2 bits by shifting this, but we won't save anything at present. + INA226_CONV_TIME convTimeEnum : 3; // Only the lowest 3 bits are defined in the conversion time enumerations +}; + +const InaSettingLookup _inaSettingsLookup[] = { + {1024, AVERAGE_1024 >> 8, 8244, CONV_TIME_8244}, + {512, AVERAGE_512 >> 8, 4156, CONV_TIME_4156}, + {256, AVERAGE_256 >> 8, 2116, CONV_TIME_2116}, + {128, AVERAGE_128 >> 8, 1100, CONV_TIME_1100}, + {64, AVERAGE_64 >> 8, 588, CONV_TIME_588}, + {16, AVERAGE_16 >> 8, 332, CONV_TIME_332}, + {4, AVERAGE_4 >> 8, 204, CONV_TIME_204}, + {1, AVERAGE_1 >> 8, 140, CONV_TIME_140}}; + +// Note: Will update the provided arg to be the correct value +INA226_AVERAGES getAverageEnum(uint16_t &samples) +{ + for (const auto &setting : _inaSettingsLookup) + { + // If a user supplies 2000 samples, we serve up the highest possible value + if (samples >= setting.avgSamples) + { + samples = setting.avgSamples; + return static_cast(setting.avgEnum << 8); + } + } + // Default value if not found + samples = DEFAULT_INASAMPLES; + return DEFAULT_INASAMPLESENUM; +} + +INA226_CONV_TIME getConversionTimeEnum(uint16_t &timeUs) +{ + for (const auto &setting : _inaSettingsLookup) + { + // If a user supplies 9000 μs, we serve up the highest possible value + if (timeUs >= setting.convTimeUs) + { + timeUs = setting.convTimeUs; + return setting.convTimeEnum; + } + } + // Default value if not found + timeUs = DEFAULT_INACONVERSIONTIME; + return DEFAULT_INACONVERSIONTIMEENUM; +} + +class UsermodINA226 : public Usermod +{ +private: + static const char _name[]; + + unsigned long _lastLoopCheck = 0; + unsigned long _lastTriggerTime = 0; + + bool _settingEnabled : 1; // Enable the usermod + bool _mqttPublish : 1; // Publish MQTT values + bool _mqttPublishAlways : 1; // Publish always, regardless if there is a change + bool _mqttHomeAssistant : 1; // Enable Home Assistant docs + bool _initDone : 1; // Initialization is done + bool _isTriggeredOperationMode : 1; // false = continuous, true = triggered + bool _measurementTriggered : 1; // if triggered mode, then true indicates we're waiting for measurements + uint16_t _settingInaConversionTimeUs : 12; // Conversion time, shift by 2 + uint16_t _settingInaSamples : 11; // Number of samples for averaging, max 1024 + + uint8_t _i2cAddress; + uint16_t _checkInterval; // milliseconds, user settings is in seconds + float _decimalFactor; // a power of 10 factor. 1 would be no change, 10 is one decimal, 100 is two etc. User sees a power of 10 (0, 1, 2, ..) + uint16_t _shuntResistor; // Shunt resistor value in milliohms + uint16_t _currentRange; // Expected maximum current in milliamps + + uint8_t _lastStatus = 0; + float _lastCurrent = 0; + float _lastVoltage = 0; + float _lastPower = 0; + float _lastShuntVoltage = 0; + bool _lastOverflow = false; + +#ifndef WLED_MQTT_DISABLE + float _lastCurrentSent = 0; + float _lastVoltageSent = 0; + float _lastPowerSent = 0; + float _lastShuntVoltageSent = 0; + bool _lastOverflowSent = false; +#endif + + INA226_WE *_ina226 = nullptr; + + float truncateDecimals(float val) + { + return roundf(val * _decimalFactor) / _decimalFactor; + } + + void initializeINA226() + { + if (_ina226 != nullptr) + { + delete _ina226; + } + + _ina226 = new INA226_WE(_i2cAddress); + if (!_ina226->init()) + { + DEBUG_PRINTLN(F("INA226 initialization failed!")); + return; + } + _ina226->setCorrectionFactor(1.0); + + uint16_t tmpShort = _settingInaSamples; + _ina226->setAverage(getAverageEnum(tmpShort)); + + tmpShort = _settingInaConversionTimeUs << 2; + _ina226->setConversionTime(getConversionTimeEnum(tmpShort)); + + if (_checkInterval >= 20000) + { + _isTriggeredOperationMode = true; + _ina226->setMeasureMode(TRIGGERED); + } + else + { + _isTriggeredOperationMode = false; + _ina226->setMeasureMode(CONTINUOUS); + } + + _ina226->setResistorRange(static_cast(_shuntResistor) / 1000.0, static_cast(_currentRange) / 1000.0); + } + + void fetchAndPushValues() + { + _lastStatus = _ina226->getI2cErrorCode(); + + if (_lastStatus != 0) + return; + + float current = truncateDecimals(_ina226->getCurrent_mA() / 1000.0); + float voltage = truncateDecimals(_ina226->getBusVoltage_V()); + float power = truncateDecimals(_ina226->getBusPower() / 1000.0); + float shuntVoltage = truncateDecimals(_ina226->getShuntVoltage_V()); + bool overflow = _ina226->overflow; + +#ifndef WLED_DISABLE_MQTT + mqttPublishIfChanged(F("current"), _lastCurrentSent, current, 0.01f); + mqttPublishIfChanged(F("voltage"), _lastVoltageSent, voltage, 0.01f); + mqttPublishIfChanged(F("power"), _lastPowerSent, power, 0.1f); + mqttPublishIfChanged(F("shunt_voltage"), _lastShuntVoltageSent, shuntVoltage, 0.01f); + mqttPublishIfChanged(F("overflow"), _lastOverflowSent, overflow); +#endif + + _lastCurrent = current; + _lastVoltage = voltage; + _lastPower = power; + _lastShuntVoltage = shuntVoltage; + _lastOverflow = overflow; + } + + void handleTriggeredMode(unsigned long currentTime) + { + if (_measurementTriggered) + { + // Test if we have a measurement every 400ms + if (currentTime - _lastTriggerTime >= 400) + { + _lastTriggerTime = currentTime; + if (_ina226->isBusy()) + return; + + fetchAndPushValues(); + _measurementTriggered = false; + } + } + else + { + if (currentTime - _lastLoopCheck >= _checkInterval) + { + // Start a measurement and use isBusy() later to determine when it is done + _ina226->startSingleMeasurementNoWait(); + _lastLoopCheck = currentTime; + _lastTriggerTime = currentTime; + _measurementTriggered = true; + } + } + } + + void handleContinuousMode(unsigned long currentTime) + { + if (currentTime - _lastLoopCheck >= _checkInterval) + { + _lastLoopCheck = currentTime; + fetchAndPushValues(); + } + } + + ~UsermodINA226() + { + delete _ina226; + _ina226 = nullptr; + } + +#ifndef WLED_DISABLE_MQTT + void mqttInitialize() + { + if (!WLED_MQTT_CONNECTED || !_mqttPublish || !_mqttHomeAssistant) + return; + + char topic[128]; + snprintf_P(topic, 127, "%s/current", mqttDeviceTopic); + mqttCreateHassSensor(F("Current"), topic, F("current"), F("A")); + + snprintf_P(topic, 127, "%s/voltage", mqttDeviceTopic); + mqttCreateHassSensor(F("Voltage"), topic, F("voltage"), F("V")); + + snprintf_P(topic, 127, "%s/power", mqttDeviceTopic); + mqttCreateHassSensor(F("Power"), topic, F("power"), F("W")); + + snprintf_P(topic, 127, "%s/shunt_voltage", mqttDeviceTopic); + mqttCreateHassSensor(F("Shunt Voltage"), topic, F("voltage"), F("V")); + + snprintf_P(topic, 127, "%s/overflow", mqttDeviceTopic); + mqttCreateHassBinarySensor(F("Overflow"), topic); + } + + void mqttPublishIfChanged(const __FlashStringHelper *topic, float &lastState, float state, float minChange) + { + if (WLED_MQTT_CONNECTED && _mqttPublish && (_mqttPublishAlways || fabsf(lastState - state) > minChange)) + { + char subuf[128]; + snprintf_P(subuf, 127, PSTR("%s/%s"), mqttDeviceTopic, (const char *)topic); + mqtt->publish(subuf, 0, false, String(state).c_str()); + + lastState = state; + } + } + + void mqttPublishIfChanged(const __FlashStringHelper *topic, bool &lastState, bool state) + { + if (WLED_MQTT_CONNECTED && _mqttPublish && (_mqttPublishAlways || lastState != state)) + { + char subuf[128]; + snprintf_P(subuf, 127, PSTR("%s/%s"), mqttDeviceTopic, (const char *)topic); + mqtt->publish(subuf, 0, false, state ? "true" : "false"); + + lastState = state; + } + } + + void mqttCreateHassSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement) + { + String t = String(F("homeassistant/sensor/")) + mqttClientID + "/" + name + F("/config"); + + StaticJsonDocument<600> doc; + + doc[F("name")] = name; + doc[F("state_topic")] = topic; + doc[F("unique_id")] = String(mqttClientID) + name; + if (unitOfMeasurement != "") + doc[F("unit_of_measurement")] = unitOfMeasurement; + if (deviceClass != "") + doc[F("device_class")] = deviceClass; + doc[F("expire_after")] = 1800; + + JsonObject device = doc.createNestedObject(F("device")); + device[F("name")] = serverDescription; + device[F("identifiers")] = "wled-sensor-" + String(mqttClientID); + device[F("manufacturer")] = F(WLED_BRAND); + device[F("model")] = F(WLED_PRODUCT_NAME); + device[F("sw_version")] = versionString; + + String temp; + serializeJson(doc, temp); + DEBUG_PRINTLN(t); + DEBUG_PRINTLN(temp); + + mqtt->publish(t.c_str(), 0, true, temp.c_str()); + } + + void mqttCreateHassBinarySensor(const String &name, const String &topic) + { + String t = String(F("homeassistant/binary_sensor/")) + mqttClientID + "/" + name + F("/config"); + + StaticJsonDocument<600> doc; + + doc[F("name")] = name; + doc[F("state_topic")] = topic; + doc[F("unique_id")] = String(mqttClientID) + name; + + JsonObject device = doc.createNestedObject(F("device")); + device[F("name")] = serverDescription; + device[F("identifiers")] = "wled-sensor-" + String(mqttClientID); + device[F("manufacturer")] = F(WLED_BRAND); + device[F("model")] = F(WLED_PRODUCT_NAME); + device[F("sw_version")] = versionString; + + String temp; + serializeJson(doc, temp); + DEBUG_PRINTLN(t); + DEBUG_PRINTLN(temp); + + mqtt->publish(t.c_str(), 0, true, temp.c_str()); + } +#endif + +public: + UsermodINA226() + { + // Default values + _settingInaSamples = DEFAULT_INASAMPLES; + _settingInaConversionTimeUs = DEFAULT_INACONVERSIONTIME; + + _i2cAddress = INA226_ADDRESS; + _checkInterval = DEFAULT_CHECKINTERVAL; + _decimalFactor = 100; + _shuntResistor = 1000; + _currentRange = 1000; + } + + void setup() + { + initializeINA226(); + } + + void loop() + { + if (!_settingEnabled || strip.isUpdating()) + return; + + unsigned long currentTime = millis(); + + if (_isTriggeredOperationMode) + { + handleTriggeredMode(currentTime); + } + else + { + handleContinuousMode(currentTime); + } + } + +#ifndef WLED_DISABLE_MQTT + void onMqttConnect(bool sessionPresent) + { + mqttInitialize(); + } +#endif + + uint16_t getId() + { + return USERMOD_ID_INA226; + } + + void addToJsonInfo(JsonObject &root) override + { + JsonObject user = root["u"]; + if (user.isNull()) + user = root.createNestedObject("u"); + +#ifdef USERMOD_INA226_DEBUG + JsonArray temp = user.createNestedArray(F("INA226 last loop")); + temp.add(_lastLoopCheck); + + temp = user.createNestedArray(F("INA226 last status")); + temp.add(_lastStatus); + + temp = user.createNestedArray(F("INA226 average samples")); + temp.add(_settingInaSamples); + temp.add(F("samples")); + + temp = user.createNestedArray(F("INA226 conversion time")); + temp.add(_settingInaConversionTimeUs << 2); + temp.add(F("μs")); + + // INA226 uses (2 * conversion time * samples) time to take a reading. + temp = user.createNestedArray(F("INA226 expected sample time")); + uint32_t sampleTimeNeededUs = (static_cast(_settingInaConversionTimeUs) << 2) * _settingInaSamples * 2; + temp.add(truncateDecimals(sampleTimeNeededUs / 1000.0)); + temp.add(F("ms")); + + temp = user.createNestedArray(F("INA226 mode")); + temp.add(_isTriggeredOperationMode ? F("triggered") : F("continuous")); + + if (_isTriggeredOperationMode) + { + temp = user.createNestedArray(F("INA226 triggered")); + temp.add(_measurementTriggered ? F("waiting for measurement") : F("")); + } +#endif + + JsonArray jsonCurrent = user.createNestedArray(F("Current")); + JsonArray jsonVoltage = user.createNestedArray(F("Voltage")); + JsonArray jsonPower = user.createNestedArray(F("Power")); + JsonArray jsonShuntVoltage = user.createNestedArray(F("Shunt Voltage")); + JsonArray jsonOverflow = user.createNestedArray(F("Overflow")); + + if (_lastLoopCheck == 0) + { + jsonCurrent.add(F("Not read yet")); + jsonVoltage.add(F("Not read yet")); + jsonPower.add(F("Not read yet")); + jsonShuntVoltage.add(F("Not read yet")); + jsonOverflow.add(F("Not read yet")); + return; + } + + if (_lastStatus != 0) + { + jsonCurrent.add(F("An error occurred")); + jsonVoltage.add(F("An error occurred")); + jsonPower.add(F("An error occurred")); + jsonShuntVoltage.add(F("An error occurred")); + jsonOverflow.add(F("An error occurred")); + return; + } + + jsonCurrent.add(_lastCurrent); + jsonCurrent.add(F("A")); + + jsonVoltage.add(_lastVoltage); + jsonVoltage.add(F("V")); + + jsonPower.add(_lastPower); + jsonPower.add(F("W")); + + jsonShuntVoltage.add(_lastShuntVoltage); + jsonShuntVoltage.add(F("V")); + + jsonOverflow.add(_lastOverflow ? F("true") : F("false")); + } + + void addToConfig(JsonObject &root) + { + JsonObject top = root.createNestedObject(FPSTR(_name)); + top[F("Enabled")] = _settingEnabled; + top[F("I2CAddress")] = static_cast(_i2cAddress); + top[F("CheckInterval")] = _checkInterval / 1000; + top[F("INASamples")] = _settingInaSamples; + top[F("INAConversionTime")] = _settingInaConversionTimeUs << 2; + top[F("Decimals")] = log10f(_decimalFactor); + top[F("ShuntResistor")] = _shuntResistor; + top[F("CurrentRange")] = _currentRange; +#ifndef WLED_DISABLE_MQTT + top[F("MqttPublish")] = _mqttPublish; + top[F("MqttPublishAlways")] = _mqttPublishAlways; + top[F("MqttHomeAssistantDiscovery")] = _mqttHomeAssistant; +#endif + + DEBUG_PRINTLN(F("INA226 config saved.")); + } + + bool readFromConfig(JsonObject &root) override + { + JsonObject top = root[FPSTR(_name)]; + + bool configComplete = !top.isNull(); + if (!configComplete) + return false; + + bool tmpBool; + if (getJsonValue(top[F("Enabled")], tmpBool)) + _settingEnabled = tmpBool; + else + configComplete = false; + + configComplete &= getJsonValue(top[F("I2CAddress")], _i2cAddress); + if (getJsonValue(top[F("CheckInterval")], _checkInterval)) + { + if (1 <= _checkInterval && _checkInterval <= 600) + _checkInterval *= 1000; + else + _checkInterval = DEFAULT_CHECKINTERVAL; + } + else + configComplete = false; + + uint16_t tmpShort; + if (getJsonValue(top[F("INASamples")], tmpShort)) + { + // The method below will fix the provided value to a valid one + getAverageEnum(tmpShort); + _settingInaSamples = tmpShort; + } + else + configComplete = false; + + if (getJsonValue(top[F("INAConversionTime")], tmpShort)) + { + // The method below will fix the provided value to a valid one + getConversionTimeEnum(tmpShort); + _settingInaConversionTimeUs = tmpShort >> 2; + } + else + configComplete = false; + + if (getJsonValue(top[F("Decimals")], _decimalFactor)) + { + if (0 <= _decimalFactor && _decimalFactor <= 5) + _decimalFactor = pow10f(_decimalFactor); + else + _decimalFactor = 100; + } + else + configComplete = false; + + configComplete &= getJsonValue(top[F("ShuntResistor")], _shuntResistor); + configComplete &= getJsonValue(top[F("CurrentRange")], _currentRange); + +#ifndef WLED_DISABLE_MQTT + if (getJsonValue(top[F("MqttPublish")], tmpBool)) + _mqttPublish = tmpBool; + else + configComplete = false; + + if (getJsonValue(top[F("MqttPublishAlways")], tmpBool)) + _mqttPublishAlways = tmpBool; + else + configComplete = false; + + if (getJsonValue(top[F("MqttHomeAssistantDiscovery")], tmpBool)) + _mqttHomeAssistant = tmpBool; + else + configComplete = false; +#endif + + if (_initDone) + { + initializeINA226(); + +#ifndef WLED_DISABLE_MQTT + mqttInitialize(); +#endif + } + + _initDone = true; + return configComplete; + } +}; + +const char UsermodINA226::_name[] PROGMEM = "INA226"; diff --git a/usermods/Internal_Temperature_v2/assets/screenshot_info.png b/usermods/Internal_Temperature_v2/assets/screenshot_info.png new file mode 100644 index 00000000..7990f2e8 Binary files /dev/null and b/usermods/Internal_Temperature_v2/assets/screenshot_info.png differ diff --git a/usermods/Internal_Temperature_v2/assets/screenshot_settings.png b/usermods/Internal_Temperature_v2/assets/screenshot_settings.png new file mode 100644 index 00000000..d97dc2be Binary files /dev/null and b/usermods/Internal_Temperature_v2/assets/screenshot_settings.png differ diff --git a/usermods/Internal_Temperature_v2/readme.md b/usermods/Internal_Temperature_v2/readme.md index 58a9e193..d574f3ab 100644 --- a/usermods/Internal_Temperature_v2/readme.md +++ b/usermods/Internal_Temperature_v2/readme.md @@ -1,17 +1,44 @@ # Internal Temperature Usermod -This usermod adds the temperature readout to the Info tab and also publishes that over the topic `mcutemp` topic. -## Important -A shown temp of 53,33°C might indicate that the internal temp is not supported. +![Screenshot of WLED info page](assets/screenshot_info.png) -ESP8266 does not have a internal temp sensor +![Screenshot of WLED usermod settings page](assets/screenshot_settings.png) + + +## Features + - 🌡️ Adds the internal temperature readout of the chip to the `Info` tab + - 🥵 High temperature indicator/action. (Configurable threshold and preset) + - 📣 Publishes the internal temperature over the MQTT topic: `mcutemp` + + +## Use Examples +- Warn of excessive/damaging temperatures by the triggering of a 'warning' preset +- Activate a cooling fan (when used with the multi-relay usermod) + + +## Compatibility +- A shown temp of 53,33°C might indicate that the internal temp is not supported +- ESP8266 does not have a internal temp sensor -> Disabled (Indicated with a readout of '-1') +- ESP32S2 seems to crash on reading the sensor -> Disabled (Indicated with a readout of '-1') -ESP32S2 seems to crash on reading the sensor -> disabled ## Installation -Add a build flag `-D USERMOD_INTERNAL_TEMPERATURE` to your `platformio.ini` (or `platformio_override.ini`). +- Add a build flag `-D USERMOD_INTERNAL_TEMPERATURE` to your `platformio.ini` (or `platformio_override.ini`). + + +## 📝 Change Log + +2024-06-26 + +- Added "high-temperature-indication" feature +- Documentation updated + +2023-09-01 + +* "Internal Temperature" usermod created + ## Authors -Soeren Willrodt [@lost-hope](https://github.com/lost-hope) - -Dimitry Zhemkov [@dima-zhemkov](https://github.com/dima-zhemkov) \ No newline at end of file +- Soeren Willrodt [@lost-hope](https://github.com/lost-hope) +- Dimitry Zhemkov [@dima-zhemkov](https://github.com/dima-zhemkov) +- Adam Matthews [@adamsthws](https://github.com/adamsthws) diff --git a/usermods/Internal_Temperature_v2/usermod_internal_temperature.h b/usermods/Internal_Temperature_v2/usermod_internal_temperature.h index 3989e766..2236bfea 100644 --- a/usermods/Internal_Temperature_v2/usermod_internal_temperature.h +++ b/usermods/Internal_Temperature_v2/usermod_internal_temperature.h @@ -6,14 +6,23 @@ class InternalTemperatureUsermod : public Usermod { private: + static constexpr unsigned long minLoopInterval = 1000; // minimum allowable interval (ms) unsigned long loopInterval = 10000; unsigned long lastTime = 0; bool isEnabled = false; - float temperature = 0; + float temperature = 0.0f; + uint8_t previousPlaylist = 0; // Stores the playlist that was active before high-temperature activation + uint8_t previousPreset = 0; // Stores the preset that was active before high-temperature activation + uint8_t presetToActivate = 0; // Preset to activate when temp goes above threshold (0 = disabled) + float activationThreshold = 95.0f; // Temperature threshold to trigger high-temperature actions + float resetMargin = 2.0f; // Margin below the activation threshold (Prevents frequent toggling when close to threshold) + bool isAboveThreshold = false; // Flag to track if the high temperature preset is currently active static const char _name[]; static const char _enabled[]; static const char _loopInterval[]; + static const char _activationThreshold[]; + static const char _presetToActivate[]; // any private methods should go here (non-inline method should be defined out of class) void publishMqtt(const char *state, bool retain = false); // example for publishing MQTT message @@ -32,6 +41,7 @@ public: lastTime = millis(); +// Measure the temperature #ifdef ESP8266 // ESP8266 // does not seem possible temperature = -1; @@ -41,6 +51,57 @@ public: temperature = roundf(temperatureRead() * 10) / 10; #endif + // Check if temperature has exceeded the activation threshold + if (temperature >= activationThreshold) { + // Update the state flag if not already set + if (!isAboveThreshold) { + isAboveThreshold = true; + } + // Check if a 'high temperature' preset is configured and it's not already active + if (presetToActivate != 0 && currentPreset != presetToActivate) { + // If a playlist is active, store it for reactivation later + if (currentPlaylist > 0) { + previousPlaylist = currentPlaylist; + } + // If a preset is active, store it for reactivation later + else if (currentPreset > 0) { + previousPreset = currentPreset; + // If no playlist or preset is active, save current state for reactivation later + } else { + saveTemporaryPreset(); + } + // Activate the 'high temperature' preset + applyPreset(presetToActivate); + } + } + // Check if temperature is back below the threshold + else if (temperature <= (activationThreshold - resetMargin)) { + // Update the state flag if not already set + if (isAboveThreshold){ + isAboveThreshold = false; + } + // Check if the 'high temperature' preset is active + if (currentPreset == presetToActivate) { + // Check if a previous playlist was stored + if (previousPlaylist > 0) { + // Reactivate the stored playlist + applyPreset(previousPlaylist); + // Clear the stored playlist + previousPlaylist = 0; + } + // Check if a previous preset was stored + else if (previousPreset > 0) { + // Reactivate the stored preset + applyPreset(previousPreset); + // Clear the stored preset + previousPreset = 0; + // If no previous playlist or preset was stored, revert to the stored state + } else { + applyTemporaryPreset(); + } + } + } + #ifndef WLED_DISABLE_MQTT if (WLED_MQTT_CONNECTED) { @@ -80,15 +141,30 @@ public: JsonObject top = root.createNestedObject(FPSTR(_name)); top[FPSTR(_enabled)] = isEnabled; top[FPSTR(_loopInterval)] = loopInterval; + top[FPSTR(_activationThreshold)] = activationThreshold; + top[FPSTR(_presetToActivate)] = presetToActivate; } + // Append useful info to the usermod settings gui + void appendConfigData() + { + // Display 'ms' next to the 'Loop Interval' setting + oappend(SET_F("addInfo('Internal Temperature:Loop Interval', 1, 'ms');")); + // Display '°C' next to the 'Activation Threshold' setting + oappend(SET_F("addInfo('Internal Temperature:Activation Threshold', 1, '°C');")); + // Display '0 = Disabled' next to the 'Preset To Activate' setting + oappend(SET_F("addInfo('Internal Temperature:Preset To Activate', 1, '0 = unused');")); + } + bool readFromConfig(JsonObject &root) { JsonObject top = root[FPSTR(_name)]; bool configComplete = !top.isNull(); configComplete &= getJsonValue(top[FPSTR(_enabled)], isEnabled); configComplete &= getJsonValue(top[FPSTR(_loopInterval)], loopInterval); - + loopInterval = max(loopInterval, minLoopInterval); // Makes sure the loop interval isn't too small. + configComplete &= getJsonValue(top[FPSTR(_presetToActivate)], presetToActivate); + configComplete &= getJsonValue(top[FPSTR(_activationThreshold)], activationThreshold); return configComplete; } @@ -101,6 +177,8 @@ public: const char InternalTemperatureUsermod::_name[] PROGMEM = "Internal Temperature"; const char InternalTemperatureUsermod::_enabled[] PROGMEM = "Enabled"; const char InternalTemperatureUsermod::_loopInterval[] PROGMEM = "Loop Interval"; +const char InternalTemperatureUsermod::_activationThreshold[] PROGMEM = "Activation Threshold"; +const char InternalTemperatureUsermod::_presetToActivate[] PROGMEM = "Preset To Activate"; void InternalTemperatureUsermod::publishMqtt(const char *state, bool retain) { diff --git a/usermods/LD2410_v2/readme.md b/usermods/LD2410_v2/readme.md new file mode 100644 index 00000000..40463d4b --- /dev/null +++ b/usermods/LD2410_v2/readme.md @@ -0,0 +1,36 @@ +# BH1750 usermod + +> This usermod requires a second UART and was only tested on the ESP32 + + +This usermod will read from a LD2410 movement/presence sensor. + +The movement and presence state are displayed in both the Info section of the web UI, as well as published to the `/movement` and `/stationary` MQTT topics respectively. + +## Dependencies +- Libraries + - `ncmreynolds/ld2410@^0.1.3` + - This must be added under `lib_deps` in your `platformio.ini` (or `platformio_override.ini`). +- Data is published over MQTT - make sure you've enabled the MQTT sync interface. + +## Compilation + +To enable, compile with `USERMOD_LD2410` defined (e.g. in `platformio_override.ini`) +```ini +[env:usermod_USERMOD_LD2410_esp32dev] +extends = env:esp32dev +build_flags = + ${common.build_flags_esp32} + -D USERMOD_LD2410 +lib_deps = + ${esp32.lib_deps} + ncmreynolds/ld2410@^0.1.3 +``` + +### Configuration Options +The Usermod screen allows you to: +- enable/disable the usermod +- Configure the RX/TX pins + +## Change log +- 2024-06 Created by @wesleygas (https://github.com/wesleygas/) diff --git a/usermods/LD2410_v2/usermod_ld2410.h b/usermods/LD2410_v2/usermod_ld2410.h new file mode 100644 index 00000000..4d96b32f --- /dev/null +++ b/usermods/LD2410_v2/usermod_ld2410.h @@ -0,0 +1,237 @@ +#warning **** Included USERMOD_LD2410 **** + +#ifndef WLED_ENABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + +#pragma once + +#include "wled.h" +#include + +class LD2410Usermod : public Usermod { + + private: + + bool enabled = true; + bool initDone = false; + bool sensorFound = false; + unsigned long lastTime = 0; + unsigned long last_mqtt_sent = 0; + + int8_t default_uart_rx = 19; + int8_t default_uart_tx = 18; + + + String mqttMovementTopic = F(""); + String mqttStationaryTopic = F(""); + bool mqttInitialized = false; + bool HomeAssistantDiscovery = true; // Publish Home Assistant Discovery messages + + + ld2410 radar; + bool stationary_detected = false; + bool last_stationary_state = false; + bool movement_detected = false; + bool last_movement_state = false; + + // These config variables have defaults set inside readFromConfig() + int8_t uart_rx_pin; + int8_t uart_tx_pin; + + // string that are used multiple time (this will save some flash memory) + static const char _name[]; + static const char _enabled[]; + + void publishMqtt(const char* topic, const char* state, bool retain); // example for publishing MQTT message + + void _mqttInitialize() + { + mqttMovementTopic = String(mqttDeviceTopic) + F("/ld2410/movement"); + mqttStationaryTopic = String(mqttDeviceTopic) + F("/ld2410/stationary"); + if (HomeAssistantDiscovery){ + _createMqttSensor(F("Movement"), mqttMovementTopic, F("motion"), F("")); + _createMqttSensor(F("Stationary"), mqttStationaryTopic, F("occupancy"), F("")); + } + } + + // Create an MQTT Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop. + void _createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement) + { + String t = String(F("homeassistant/binary_sensor/")) + mqttClientID + F("/") + name + F("/config"); + + StaticJsonDocument<600> doc; + + doc[F("name")] = String(serverDescription) + F(" Module"); + doc[F("state_topic")] = topic; + doc[F("unique_id")] = String(mqttClientID) + name; + if (unitOfMeasurement != "") + doc[F("unit_of_measurement")] = unitOfMeasurement; + if (deviceClass != "") + doc[F("device_class")] = deviceClass; + doc[F("expire_after")] = 1800; + doc[F("payload_off")] = "OFF"; + doc[F("payload_on")] = "ON"; + + JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device + device[F("name")] = serverDescription; + device[F("identifiers")] = "wled-sensor-" + String(mqttClientID); + device[F("manufacturer")] = F("WLED"); + device[F("model")] = F("FOSS"); + device[F("sw_version")] = versionString; + + String temp; + serializeJson(doc, temp); + DEBUG_PRINTLN(t); + DEBUG_PRINTLN(temp); + + mqtt->publish(t.c_str(), 0, true, temp.c_str()); + } + + public: + + inline bool isEnabled() { return enabled; } + + void setup() { + Serial1.begin(256000, SERIAL_8N1, uart_rx_pin, uart_tx_pin); + Serial.print(F("\nLD2410 radar sensor initialising: ")); + if(radar.begin(Serial1)){ + Serial.println(F("OK")); + } else { + Serial.println(F("not connected")); + } + initDone = true; + } + + + void loop() { + // NOTE: on very long strips strip.isUpdating() may always return true so update accordingly + if (!enabled || strip.isUpdating()) return; + radar.read(); + unsigned long curr_time = millis(); + if(curr_time - lastTime > 1000) //Try to Report every 1000ms + { + lastTime = curr_time; + sensorFound = radar.isConnected(); + if(!sensorFound) return; + stationary_detected = radar.presenceDetected(); + if(stationary_detected != last_stationary_state){ + if (WLED_MQTT_CONNECTED){ + publishMqtt("/ld2410/stationary", stationary_detected ? "ON":"OFF", false); + last_stationary_state = stationary_detected; + } + } + movement_detected = radar.movingTargetDetected(); + if(movement_detected != last_movement_state){ + if (WLED_MQTT_CONNECTED){ + publishMqtt("/ld2410/movement", movement_detected ? "ON":"OFF", false); + last_movement_state = movement_detected; + } + } + // If there hasn't been any activity, send current state to confirm sensor is alive + if(curr_time - last_mqtt_sent > 1000*60*5 && WLED_MQTT_CONNECTED){ + publishMqtt("/ld2410/stationary", stationary_detected ? "ON":"OFF", false); + publishMqtt("/ld2410/movement", movement_detected ? "ON":"OFF", false); + } + } + } + + + void addToJsonInfo(JsonObject& root) + { + // if "u" object does not exist yet wee need to create it + JsonObject user = root[F("u")]; + if (user.isNull()) user = root.createNestedObject(F("u")); + + JsonArray ld2410_sta_json = user.createNestedArray(F("LD2410 Stationary")); + JsonArray ld2410_mov_json = user.createNestedArray(F("LD2410 Movement")); + if (!enabled){ + ld2410_sta_json.add(F("disabled")); + ld2410_mov_json.add(F("disabled")); + } else if(!sensorFound){ + ld2410_sta_json.add(F("LD2410")); + ld2410_sta_json.add(" Not Found"); + } else { + ld2410_sta_json.add("Sta "); + ld2410_sta_json.add(stationary_detected ? "ON":"OFF"); + ld2410_mov_json.add("Mov "); + ld2410_mov_json.add(movement_detected ? "ON":"OFF"); + } + } + + void addToConfig(JsonObject& root) + { + JsonObject top = root.createNestedObject(FPSTR(_name)); + top[FPSTR(_enabled)] = enabled; + //save these vars persistently whenever settings are saved + top["uart_rx_pin"] = default_uart_rx; + top["uart_tx_pin"] = default_uart_tx; + } + + + bool readFromConfig(JsonObject& root) + { + // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor + // setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) + + JsonObject top = root[FPSTR(_name)]; + + bool configComplete = !top.isNull(); + if (!configComplete) + { + DEBUG_PRINT(FPSTR(_name)); + DEBUG_PRINT(F("LD2410")); + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + configComplete &= getJsonValue(top["uart_rx_pin"], uart_rx_pin, default_uart_rx); + configComplete &= getJsonValue(top["uart_tx_pin"], uart_tx_pin, default_uart_tx); + + return configComplete; + } + + +#ifndef WLED_DISABLE_MQTT + /** + * onMqttConnect() is called when MQTT connection is established + */ + void onMqttConnect(bool sessionPresent) { + // do any MQTT related initialisation here + if(!radar.isConnected()) return; + publishMqtt("/ld2410/status", "I am alive!", false); + if (!mqttInitialized) + { + _mqttInitialize(); + mqttInitialized = true; + } + } +#endif + + uint16_t getId() + { + return USERMOD_ID_LD2410; + } +}; + + +// add more strings here to reduce flash memory usage +const char LD2410Usermod::_name[] PROGMEM = "LD2410Usermod"; +const char LD2410Usermod::_enabled[] PROGMEM = "enabled"; + + +// implementation of non-inline member methods + +void LD2410Usermod::publishMqtt(const char* topic, const char* state, bool retain) +{ +#ifndef WLED_DISABLE_MQTT + //Check if MQTT Connected, otherwise it will crash + if (WLED_MQTT_CONNECTED) { + last_mqtt_sent = millis(); + char subuf[64]; + strcpy(subuf, mqttDeviceTopic); + strcat(subuf, topic); + mqtt->publish(subuf, 0, retain, state); + } +#endif +} diff --git a/usermods/MAX17048_v2/readme.md b/usermods/MAX17048_v2/readme.md new file mode 100644 index 00000000..958e6def --- /dev/null +++ b/usermods/MAX17048_v2/readme.md @@ -0,0 +1,64 @@ +# Adafruit MAX17048 Usermod (LiPo & LiIon Battery Monitor & Fuel Gauge) +This usermod reads information from an Adafruit MAX17048 and outputs the following: +- Battery Voltage +- Battery Level Percentage + + +## Dependencies +Libraries: +- `Adafruit_BusIO@~1.14.5` (by [adafruit](https://github.com/adafruit/Adafruit_BusIO)) +- `Adafruit_MAX1704X@~1.0.2` (by [adafruit](https://github.com/adafruit/Adafruit_MAX1704X)) + +These must be added under `lib_deps` in your `platform.ini` (or `platform_override.ini`). +Data is published over MQTT - make sure you've enabled the MQTT sync interface. + +## Compilation + +To enable, compile with `USERMOD_MAX17048` define in the build_flags (e.g. in `platformio.ini` or `platformio_override.ini`) such as in the example below: +```ini +[env:usermod_max17048_d1_mini] +extends = env:d1_mini +build_flags = + ${common.build_flags_esp8266} + -D USERMOD_MAX17048 +lib_deps = + ${esp8266.lib_deps} + https://github.com/adafruit/Adafruit_BusIO @ 1.14.5 + https://github.com/adafruit/Adafruit_MAX1704X @ 1.0.2 +``` + +### Configuration Options +The following settings can be set at compile-time but are configurable on the usermod menu (except First Monitor time): +- USERMOD_MAX17048_MIN_MONITOR_INTERVAL (the min number of milliseconds between checks, defaults to 10,000 ms) +- USERMOD_MAX17048_MAX_MONITOR_INTERVAL (the max number of milliseconds between checks, defaults to 10,000 ms) +- USERMOD_MAX17048_FIRST_MONITOR_AT + + +Additionally, the Usermod Menu allows you to: +- Enable or Disable the usermod +- Enable or Disable Home Assistant Discovery (turn on/off to sent MQTT Discovery entries for Home Assistant) +- Configure SCL/SDA GPIO Pins + +## API +The following method is available to interact with the usermod from other code modules: +- `getBatteryVoltageV` read the last battery voltage (in Volt) obtained from the sensor +- `getBatteryPercent` reads the last battery percentage obtained from the sensor + +## MQTT +MQTT topics are as follows (`` is set in MQTT section of Sync Setup menu): +Measurement type | MQTT topic +--- | --- +Battery Voltage | `/batteryVoltage` +Battery Percent | `/batteryPercent` + +## Authors +Carlos Cruz [@ccruz09](https://github.com/ccruz09) + + +## Revision History +Jan 2024 +- Added Home Assistant Discovery +- Implemented PinManager to register pins +- Added API call for other modules to read battery voltage and percentage +- Added info-screen outputs +- Updated `readme.md` \ No newline at end of file diff --git a/usermods/MAX17048_v2/usermod_max17048.h b/usermods/MAX17048_v2/usermod_max17048.h new file mode 100644 index 00000000..c3a2664a --- /dev/null +++ b/usermods/MAX17048_v2/usermod_max17048.h @@ -0,0 +1,281 @@ +// force the compiler to show a warning to confirm that this file is included +#warning **** Included USERMOD_MAX17048 V2.0 **** + +#pragma once + +#include "wled.h" +#include "Adafruit_MAX1704X.h" + + +// the max interval to check battery level, 10 seconds +#ifndef USERMOD_MAX17048_MAX_MONITOR_INTERVAL +#define USERMOD_MAX17048_MAX_MONITOR_INTERVAL 10000 +#endif + +// the min interval to check battery level, 500 ms +#ifndef USERMOD_MAX17048_MIN_MONITOR_INTERVAL +#define USERMOD_MAX17048_MIN_MONITOR_INTERVAL 500 +#endif + +// how many seconds after boot to perform the first check, 10 seconds +#ifndef USERMOD_MAX17048_FIRST_MONITOR_AT +#define USERMOD_MAX17048_FIRST_MONITOR_AT 10000 +#endif + +/* + * Usermod to display Battery Life using Adafruit's MAX17048 LiPoly/ LiIon Fuel Gauge and Battery Monitor. + */ +class Usermod_MAX17048 : public Usermod { + + private: + + bool enabled = true; + + unsigned long maxReadingInterval = USERMOD_MAX17048_MAX_MONITOR_INTERVAL; + unsigned long minReadingInterval = USERMOD_MAX17048_MIN_MONITOR_INTERVAL; + unsigned long lastCheck = UINT32_MAX - (USERMOD_MAX17048_MAX_MONITOR_INTERVAL - USERMOD_MAX17048_FIRST_MONITOR_AT); + unsigned long lastSend = UINT32_MAX - (USERMOD_MAX17048_MAX_MONITOR_INTERVAL - USERMOD_MAX17048_FIRST_MONITOR_AT); + + + uint8_t VoltageDecimals = 3; // Number of decimal places in published voltage values + uint8_t PercentDecimals = 1; // Number of decimal places in published percent values + + // string that are used multiple time (this will save some flash memory) + static const char _name[]; + static const char _enabled[]; + static const char _maxReadInterval[]; + static const char _minReadInterval[]; + static const char _HomeAssistantDiscovery[]; + + bool monitorFound = false; + bool firstReadComplete = false; + bool initDone = false; + + Adafruit_MAX17048 maxLipo; + float lastBattVoltage = -10; + float lastBattPercent = -1; + + // MQTT and Home Assistant Variables + bool HomeAssistantDiscovery = false; // Publish Home Assistant Device Information + bool mqttInitialized = false; + + void _mqttInitialize() + { + char mqttBatteryVoltageTopic[128]; + char mqttBatteryPercentTopic[128]; + + snprintf_P(mqttBatteryVoltageTopic, 127, PSTR("%s/batteryVoltage"), mqttDeviceTopic); + snprintf_P(mqttBatteryPercentTopic, 127, PSTR("%s/batteryPercent"), mqttDeviceTopic); + + if (HomeAssistantDiscovery) { + _createMqttSensor(F("BatteryVoltage"), mqttBatteryVoltageTopic, "voltage", F("V")); + _createMqttSensor(F("BatteryPercent"), mqttBatteryPercentTopic, "battery", F("%")); + } + } + + void _createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement) + { + String t = String(F("homeassistant/sensor/")) + mqttClientID + F("/") + name + F("/config"); + + StaticJsonDocument<600> doc; + + doc[F("name")] = String(serverDescription) + " " + name; + doc[F("state_topic")] = topic; + doc[F("unique_id")] = String(mqttClientID) + name; + if (unitOfMeasurement != "") + doc[F("unit_of_measurement")] = unitOfMeasurement; + if (deviceClass != "") + doc[F("device_class")] = deviceClass; + doc[F("expire_after")] = 1800; + + JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device + device[F("name")] = serverDescription; + device[F("identifiers")] = "wled-sensor-" + String(mqttClientID); + device[F("manufacturer")] = F("WLED"); + device[F("model")] = F("FOSS"); + device[F("sw_version")] = versionString; + + String temp; + serializeJson(doc, temp); + DEBUG_PRINTLN(t); + DEBUG_PRINTLN(temp); + + mqtt->publish(t.c_str(), 0, true, temp.c_str()); + } + + void publishMqtt(const char *topic, const char* state) { + #ifndef WLED_DISABLE_MQTT + //Check if MQTT Connected, otherwise it will crash the 8266 + if (WLED_MQTT_CONNECTED){ + char subuf[128]; + snprintf_P(subuf, 127, PSTR("%s/%s"), mqttDeviceTopic, topic); + mqtt->publish(subuf, 0, false, state); + } + #endif + } + + public: + + inline void enable(bool enable) { enabled = enable; } + + inline bool isEnabled() { return enabled; } + + void setup() { + // do your set-up here + if (i2c_scl<0 || i2c_sda<0) { enabled = false; return; } + monitorFound = maxLipo.begin(); + initDone = true; + } + + void loop() { + // if usermod is disabled or called during strip updating just exit + // NOTE: on very long strips strip.isUpdating() may always return true so update accordingly + if (!enabled || strip.isUpdating()) return; + + unsigned long now = millis(); + + if (now - lastCheck < minReadingInterval) { return; } + + bool shouldUpdate = now - lastSend > maxReadingInterval; + + float battVoltage = maxLipo.cellVoltage(); + float battPercent = maxLipo.cellPercent(); + lastCheck = millis(); + firstReadComplete = true; + + if (shouldUpdate) + { + lastBattVoltage = roundf(battVoltage * powf(10, VoltageDecimals)) / powf(10, VoltageDecimals); + lastBattPercent = roundf(battPercent * powf(10, PercentDecimals)) / powf(10, PercentDecimals); + lastSend = millis(); + + publishMqtt("batteryVoltage", String(lastBattVoltage, VoltageDecimals).c_str()); + publishMqtt("batteryPercent", String(lastBattPercent, PercentDecimals).c_str()); + DEBUG_PRINTLN(F("Battery Voltage: ") + String(lastBattVoltage, VoltageDecimals) + F("V")); + DEBUG_PRINTLN(F("Battery Percent: ") + String(lastBattPercent, PercentDecimals) + F("%")); + } + } + + void onMqttConnect(bool sessionPresent) + { + if (WLED_MQTT_CONNECTED && !mqttInitialized) + { + _mqttInitialize(); + mqttInitialized = true; + } + } + + inline float getBatteryVoltageV() { + return (float) lastBattVoltage; + } + + inline float getBatteryPercent() { + return (float) lastBattPercent; + } + + void addToJsonInfo(JsonObject& root) + { + // if "u" object does not exist yet wee need to create it + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + + JsonArray battery_json = user.createNestedArray(F("Battery Monitor")); + if (!enabled) { + battery_json.add(F("Disabled")); + } + else if(!monitorFound) { + battery_json.add(F("MAX17048 Not Found")); + } + else if (!firstReadComplete) { + // if we haven't read the sensor yet, let the user know + // that we are still waiting for the first measurement + battery_json.add((USERMOD_MAX17048_FIRST_MONITOR_AT - millis()) / 1000); + battery_json.add(F(" sec until read")); + } else { + battery_json.add(F("Enabled")); + JsonArray voltage_json = user.createNestedArray(F("Battery Voltage")); + voltage_json.add(lastBattVoltage); + voltage_json.add(F("V")); + JsonArray percent_json = user.createNestedArray(F("Battery Percent")); + percent_json.add(lastBattPercent); + percent_json.add(F("%")); + } + } + + void addToJsonState(JsonObject& root) + { + JsonObject usermod = root[FPSTR(_name)]; + if (usermod.isNull()) + { + usermod = root.createNestedObject(FPSTR(_name)); + } + usermod[FPSTR(_enabled)] = enabled; + } + + void readFromJsonState(JsonObject& root) + { + JsonObject usermod = root[FPSTR(_name)]; + if (!usermod.isNull()) + { + if (usermod[FPSTR(_enabled)].is()) + { + enabled = usermod[FPSTR(_enabled)].as(); + } + } + } + + void addToConfig(JsonObject& root) + { + JsonObject top = root.createNestedObject(FPSTR(_name)); + top[FPSTR(_enabled)] = enabled; + top[FPSTR(_maxReadInterval)] = maxReadingInterval; + top[FPSTR(_minReadInterval)] = minReadingInterval; + top[FPSTR(_HomeAssistantDiscovery)] = HomeAssistantDiscovery; + DEBUG_PRINT(F(_name)); + DEBUG_PRINTLN(F(" config saved.")); + } + + bool readFromConfig(JsonObject& root) + { + JsonObject top = root[FPSTR(_name)]; + + if (top.isNull()) { + DEBUG_PRINT(F(_name)); + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + bool configComplete = !top.isNull(); + + configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); + configComplete &= getJsonValue(top[FPSTR(_maxReadInterval)], maxReadingInterval, USERMOD_MAX17048_MAX_MONITOR_INTERVAL); + configComplete &= getJsonValue(top[FPSTR(_minReadInterval)], minReadingInterval, USERMOD_MAX17048_MIN_MONITOR_INTERVAL); + configComplete &= getJsonValue(top[FPSTR(_HomeAssistantDiscovery)], HomeAssistantDiscovery, false); + + DEBUG_PRINT(FPSTR(_name)); + if (!initDone) { + // first run: reading from cfg.json + DEBUG_PRINTLN(F(" config loaded.")); + } else { + DEBUG_PRINTLN(F(" config (re)loaded.")); + // changing parameters from settings page + } + + return configComplete; + } + + uint16_t getId() + { + return USERMOD_ID_MAX17048; + } + +}; + + +// add more strings here to reduce flash memory usage +const char Usermod_MAX17048::_name[] PROGMEM = "Adafruit MAX17048 Battery Monitor"; +const char Usermod_MAX17048::_enabled[] PROGMEM = "enabled"; +const char Usermod_MAX17048::_maxReadInterval[] PROGMEM = "max-read-interval-ms"; +const char Usermod_MAX17048::_minReadInterval[] PROGMEM = "min-read-interval-ms"; +const char Usermod_MAX17048::_HomeAssistantDiscovery[] PROGMEM = "HomeAssistantDiscovery"; diff --git a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h index d66b1b33..7a67dd74 100644 --- a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h +++ b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h @@ -58,7 +58,11 @@ private: bool sensorPinState[PIR_SENSOR_MAX_SENSORS] = {LOW}; // current PIR sensor pin state // configurable parameters +#if PIR_SENSOR_PIN < 0 + bool enabled = false; // PIR sensor disabled +#else bool enabled = true; // PIR sensor enabled +#endif int8_t PIRsensorPin[PIR_SENSOR_MAX_SENSORS] = {PIR_SENSOR_PIN}; // PIR sensor pin uint32_t m_switchOffDelay = PIR_SENSOR_OFF_SEC*1000; // delay before switch off after the sensor state goes LOW (10min) uint8_t m_onPreset = 0; // on preset diff --git a/usermods/SN_Photoresistor/usermod_sn_photoresistor.h b/usermods/SN_Photoresistor/usermod_sn_photoresistor.h index c09b5d90..45cdb66a 100644 --- a/usermods/SN_Photoresistor/usermod_sn_photoresistor.h +++ b/usermods/SN_Photoresistor/usermod_sn_photoresistor.h @@ -3,7 +3,9 @@ #include "wled.h" //Pin defaults for QuinLed Dig-Uno (A0) +#ifndef PHOTORESISTOR_PIN #define PHOTORESISTOR_PIN A0 +#endif // the frequency to check photoresistor, 10 seconds #ifndef USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL @@ -207,4 +209,4 @@ const char Usermod_SN_Photoresistor::_readInterval[] PROGMEM = "read-interval-s" const char Usermod_SN_Photoresistor::_referenceVoltage[] PROGMEM = "supplied-voltage"; const char Usermod_SN_Photoresistor::_resistorValue[] PROGMEM = "resistor-value"; const char Usermod_SN_Photoresistor::_adcPrecision[] PROGMEM = "adc-precision"; -const char Usermod_SN_Photoresistor::_offset[] PROGMEM = "offset"; \ No newline at end of file +const char Usermod_SN_Photoresistor::_offset[] PROGMEM = "offset"; diff --git a/usermods/Temperature/usermod_temperature.h b/usermods/Temperature/usermod_temperature.h index f7586e44..5ac992f9 100644 --- a/usermods/Temperature/usermod_temperature.h +++ b/usermods/Temperature/usermod_temperature.h @@ -57,7 +57,10 @@ class UsermodTemperature : public Usermod { static const char _parasite[]; static const char _parasitePin[]; static const char _domoticzIDX[]; - + static const char _sensor[]; + static const char _temperature[]; + static const char _Temperature[]; + //Dallas sensor quick (& dirty) reading. Credit to - Author: Peter Scargill, August 17th, 2013 float readDallas(); void requestTemperatures(); @@ -110,7 +113,7 @@ float UsermodTemperature::readDallas() { #ifdef WLED_DEBUG if (OneWire::crc8(data,8) != data[8]) { DEBUG_PRINTLN(F("CRC error reading temperature.")); - for (byte i=0; i < 9; i++) DEBUG_PRINTF_P(PSTR("0x%02X "), data[i]); + for (unsigned i=0; i < 9; i++) DEBUG_PRINTF_P(PSTR("0x%02X "), data[i]); DEBUG_PRINT(F(" => ")); DEBUG_PRINTF_P(PSTR("0x%02X\n"), OneWire::crc8(data,8)); } @@ -130,7 +133,7 @@ float UsermodTemperature::readDallas() { break; } } - for (byte i=1; i<9; i++) data[0] &= data[i]; + for (unsigned i=1; i<9; i++) data[0] &= data[i]; return data[0]==0xFF ? -127.0f : retVal; } @@ -191,9 +194,9 @@ void UsermodTemperature::publishHomeAssistantAutodiscovery() { sprintf_P(buf, PSTR("%s Temperature"), serverDescription); json[F("name")] = buf; strcpy(buf, mqttDeviceTopic); - strcat_P(buf, PSTR("/temperature")); + strcat_P(buf, _Temperature); json[F("state_topic")] = buf; - json[F("device_class")] = F("temperature"); + json[F("device_class")] = FPSTR(_temperature); json[F("unique_id")] = escapedMac.c_str(); json[F("unit_of_measurement")] = F("°C"); payload_size = serializeJson(json, json_str); @@ -272,7 +275,7 @@ void UsermodTemperature::loop() { // dont publish super low temperature as the graph will get messed up // the DallasTemperature library returns -127C or -196.6F when problem // reading the sensor - strcat_P(subuf, PSTR("/temperature")); + strcat_P(subuf, _Temperature); mqtt->publish(subuf, 0, false, String(getTemperatureC()).c_str()); strcat_P(subuf, PSTR("_f")); mqtt->publish(subuf, 0, false, String(getTemperatureF()).c_str()); @@ -335,9 +338,9 @@ void UsermodTemperature::addToJsonInfo(JsonObject& root) { temp.add(getTemperature()); temp.add(getTemperatureUnit()); - JsonObject sensor = root[F("sensor")]; - if (sensor.isNull()) sensor = root.createNestedObject(F("sensor")); - temp = sensor.createNestedArray(F("temperature")); + JsonObject sensor = root[FPSTR(_sensor)]; + if (sensor.isNull()) sensor = root.createNestedObject(FPSTR(_sensor)); + temp = sensor.createNestedArray(FPSTR(_temperature)); temp.add(getTemperature()); temp.add(getTemperatureUnit()); } @@ -367,7 +370,7 @@ void UsermodTemperature::addToConfig(JsonObject &root) { JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname top[FPSTR(_enabled)] = enabled; top["pin"] = temperaturePin; // usermodparam - top["degC"] = degC; // usermodparam + top[F("degC")] = degC; // usermodparam top[FPSTR(_readInterval)] = readingInterval / 1000; top[FPSTR(_parasite)] = parasite; top[FPSTR(_parasitePin)] = parasitePin; @@ -393,7 +396,7 @@ bool UsermodTemperature::readFromConfig(JsonObject &root) { enabled = top[FPSTR(_enabled)] | enabled; newTemperaturePin = top["pin"] | newTemperaturePin; - degC = top["degC"] | degC; + degC = top[F("degC")] | degC; readingInterval = top[FPSTR(_readInterval)] | readingInterval/1000; readingInterval = min(120,max(10,(int)readingInterval)) * 1000; // convert to ms parasite = top[FPSTR(_parasite)] | parasite; @@ -444,3 +447,6 @@ const char UsermodTemperature::_readInterval[] PROGMEM = "read-interval-s"; const char UsermodTemperature::_parasite[] PROGMEM = "parasite-pwr"; const char UsermodTemperature::_parasitePin[] PROGMEM = "parasite-pwr-pin"; const char UsermodTemperature::_domoticzIDX[] PROGMEM = "domoticz-idx"; +const char UsermodTemperature::_sensor[] PROGMEM = "sensor"; +const char UsermodTemperature::_temperature[] PROGMEM = "temperature"; +const char UsermodTemperature::_Temperature[] PROGMEM = "/temperature"; \ No newline at end of file diff --git a/usermods/TetrisAI_v2/gridbw.h b/usermods/TetrisAI_v2/gridbw.h new file mode 100644 index 00000000..deea027d --- /dev/null +++ b/usermods/TetrisAI_v2/gridbw.h @@ -0,0 +1,128 @@ +/****************************************************************************** + * @file : gridbw.h + * @brief : contains the tetris grid as binary so black and white version + ****************************************************************************** + * @attention + * + * Copyright (c) muebau 2023 + * All rights reserved. + * + ****************************************************************************** +*/ + +#ifndef __GRIDBW_H__ +#define __GRIDBW_H__ + +#include +#include +#include "pieces.h" + +using namespace std; + +class GridBW +{ +private: +public: + uint8_t width; + uint8_t height; + std::vector pixels; + + GridBW(uint8_t width, uint8_t height): + width(width), + height(height), + pixels(height) + { + if (width > 32) + { + this->width = 32; + } + } + + void placePiece(Piece* piece, uint8_t x, uint8_t y) + { + for (uint8_t row = 4 - piece->getRotation().height; row < 4; row++) + { + pixels[y + (row - (4 - piece->getRotation().height))] |= piece->getGridRow(x, row, width); + } + } + + void erasePiece(Piece* piece, uint8_t x, uint8_t y) + { + for (uint8_t row = 4 - piece->getRotation().height; row < 4; row++) + { + pixels[y + (row - (4 - piece->getRotation().height))] &= ~piece->getGridRow(x, row, width); + } + } + + bool noCollision(Piece* piece, uint8_t x, uint8_t y) + { + //if it touches a wall it is a collision + if (x > (this->width - piece->getRotation().width) || y > this->height - piece->getRotation().height) + { + return false; + } + + for (uint8_t row = 4 - piece->getRotation().height; row < 4; row++) + { + if (piece->getGridRow(x, row, width) & pixels[y + (row - (4 - piece->getRotation().height))]) + { + return false; + } + } + return true; + } + + void findLandingPosition(Piece* piece) + { + // move down until the piece bumps into some occupied pixels or the 'wall' + while (noCollision(piece, piece->x, piece->landingY)) + { + piece->landingY++; + } + + //at this point the positon is 'in the wall' or 'over some occupied pixel' + //so the previous position was the last correct one (clamped to 0 as minimum). + piece->landingY = piece->landingY > 0 ? piece->landingY - 1 : 0; + } + + void cleanupFullLines() + { + uint8_t offset = 0; + + //from "height - 1" to "0", so from bottom row to top + for (uint8_t row = height; row-- > 0; ) + { + //full line? + if (isLineFull(row)) + { + offset++; + pixels[row] = 0x0; + continue; + } + + if (offset > 0) + { + pixels[row + offset] = pixels[row]; + pixels[row] = 0x0; + } + } + } + + bool isLineFull(uint8_t y) + { + return pixels[y] == (uint32_t)((1 << width) - 1); + } + + void reset() + { + if (width > 32) + { + width = 32; + } + + pixels.clear(); + pixels.resize(height); + } +}; + +#endif /* __GRIDBW_H__ */ \ No newline at end of file diff --git a/usermods/TetrisAI_v2/gridcolor.h b/usermods/TetrisAI_v2/gridcolor.h new file mode 100644 index 00000000..815c2a56 --- /dev/null +++ b/usermods/TetrisAI_v2/gridcolor.h @@ -0,0 +1,140 @@ +/****************************************************************************** + * @file : gridcolor.h + * @brief : contains the tetris grid as 8bit indexed color version + ****************************************************************************** + * @attention + * + * Copyright (c) muebau 2023 + * All rights reserved. + * + ****************************************************************************** +*/ + +#ifndef __GRIDCOLOR_H__ +#define __GRIDCOLOR_H__ +#include +#include +#include +#include "gridbw.h" +#include "gridcolor.h" + +using namespace std; + +class GridColor +{ +private: +public: + uint8_t width; + uint8_t height; + GridBW gridBW; + std::vector pixels; + + GridColor(uint8_t width, uint8_t height): + width(width), + height(height), + gridBW(width, height), + pixels(width* height) + {} + + void clear() + { + for (uint8_t y = 0; y < height; y++) + { + gridBW.pixels[y] = 0x0; + for (int8_t x = 0; x < width; x++) + { + *getPixel(x, y) = 0; + } + } + } + + void placePiece(Piece* piece, uint8_t x, uint8_t y) + { + for (uint8_t pieceY = 0; pieceY < piece->getRotation().height; pieceY++) + { + for (uint8_t pieceX = 0; pieceX < piece->getRotation().width; pieceX++) + { + if (piece->getPixel(pieceX, pieceY)) + { + *getPixel(x + pieceX, y + pieceY) = piece->pieceData->colorIndex; + } + } + } + } + + void erasePiece(Piece* piece, uint8_t x, uint8_t y) + { + for (uint8_t pieceY = 0; pieceY < piece->getRotation().height; pieceY++) + { + for (uint8_t pieceX = 0; pieceX < piece->getRotation().width; pieceX++) + { + if (piece->getPixel(pieceX, pieceY)) + { + *getPixel(x + pieceX, y + pieceY) = 0; + } + } + } + } + + void cleanupFullLines() + { + uint8_t offset = 0; + //from "height - 1" to "0", so from bottom row to top + for (uint8_t y = height; y-- > 0; ) + { + if (gridBW.isLineFull(y)) + { + offset++; + for (uint8_t x = 0; x < width; x++) + { + pixels[y * width + x] = 0; + } + continue; + } + + if (offset > 0) + { + if (gridBW.pixels[y]) + { + for (uint8_t x = 0; x < width; x++) + { + pixels[(y + offset) * width + x] = pixels[y * width + x]; + pixels[y * width + x] = 0; + } + } + } + } + gridBW.cleanupFullLines(); + } + + uint8_t* getPixel(uint8_t x, uint8_t y) + { + return &pixels[y * width + x]; + } + + void sync() + { + for (uint8_t y = 0; y < height; y++) + { + gridBW.pixels[y] = 0x0; + for (int8_t x = 0; x < width; x++) + { + gridBW.pixels[y] <<= 1; + if (*getPixel(x, y) != 0) + { + gridBW.pixels[y] |= 0x1; + } + } + } + } + + void reset() + { + gridBW.reset(); + pixels.clear(); + pixels.resize(width* height); + clear(); + } +}; + +#endif /* __GRIDCOLOR_H__ */ \ No newline at end of file diff --git a/usermods/TetrisAI_v2/pieces.h b/usermods/TetrisAI_v2/pieces.h new file mode 100644 index 00000000..5d461615 --- /dev/null +++ b/usermods/TetrisAI_v2/pieces.h @@ -0,0 +1,184 @@ +/****************************************************************************** + * @file : pieces.h + * @brief : contains the tetris pieces with their colors indecies + ****************************************************************************** + * @attention + * + * Copyright (c) muebau 2022 + * All rights reserved. + * + ****************************************************************************** +*/ + +#ifndef __PIECES_H__ +#define __PIECES_H__ + +#include +#include + +#include +#include +#include +#include + +#define numPieces 7 + +struct PieceRotation +{ + uint8_t width; + uint8_t height; + uint16_t rows; +}; + +struct PieceData +{ + uint8_t rotCount; + uint8_t colorIndex; + PieceRotation rotations[4]; +}; + +PieceData piecesData[numPieces] = { + // I + { + 2, + 1, + { + { 1, 4, 0b0001000100010001}, + { 4, 1, 0b0000000000001111} + } + }, + // O + { + 1, + 2, + { + { 2, 2, 0b0000000000110011} + } + }, + // Z + { + 2, + 3, + { + { 3, 2, 0b0000000001100011}, + { 2, 3, 0b0000000100110010} + } + }, + // S + { + 2, + 4, + { + { 3, 2, 0b0000000000110110}, + { 2, 3, 0b0000001000110001} + } + }, + // L + { + 4, + 5, + { + { 2, 3, 0b0000001000100011}, + { 3, 2, 0b0000000001110100}, + { 2, 3, 0b0000001100010001}, + { 3, 2, 0b0000000000010111} + } + }, + // J + { + 4, + 6, + { + { 2, 3, 0b0000000100010011}, + { 3, 2, 0b0000000001000111}, + { 2, 3, 0b0000001100100010}, + { 3, 2, 0b0000000001110001} + } + }, + // T + { + 4, + 7, + { + { 3, 2, 0b0000000001110010}, + { 2, 3, 0b0000000100110001}, + { 3, 2, 0b0000000000100111}, + { 2, 3, 0b0000001000110010} + } + }, +}; + +class Piece +{ +private: +public: + uint8_t x; + uint8_t y; + PieceData* pieceData; + uint8_t rotation; + uint8_t landingY; + + Piece(uint8_t pieceIndex = 0): + x(0), + y(0), + rotation(0), + landingY(0) + { + this->pieceData = &piecesData[pieceIndex]; + } + + void reset() + { + this->rotation = 0; + this->x = 0; + this->y = 0; + this->landingY = 0; + } + + uint32_t getGridRow(uint8_t x, uint8_t y, uint8_t width) + { + if (x < width) + { + //shift the row with the "top-left" position to the "x" position + auto shiftx = (width - 1) - x; + auto topleftx = (getRotation().width - 1); + + auto finalShift = shiftx - topleftx; + auto row = getRow(y); + auto finalResult = row << finalShift; + + return finalResult; + } + return 0xffffffff; + } + + uint8_t getRow(uint8_t y) + { + if (y < 4) + { + return (getRotation().rows >> (12 - (4 * y))) & 0xf; + } + return 0xf; + } + + bool getPixel(uint8_t x, uint8_t y) + { + if(x > getRotation().width - 1 || y > getRotation().height - 1 ) + { + return false; + } + + if (x < 4 && y < 4) + { + return (getRow((4 - getRotation().height) + y) >> (3 - ((4 - getRotation().width) + x))) & 0x1; + } + return false; + } + + PieceRotation getRotation() + { + return this->pieceData->rotations[rotation]; + } +}; + +#endif /* __PIECES_H__ */ diff --git a/usermods/TetrisAI_v2/rating.h b/usermods/TetrisAI_v2/rating.h new file mode 100644 index 00000000..88320818 --- /dev/null +++ b/usermods/TetrisAI_v2/rating.h @@ -0,0 +1,64 @@ +/****************************************************************************** + * @file : rating.h + * @brief : contains the tetris rating of a grid + ****************************************************************************** + * @attention + * + * Copyright (c) muebau 2022 + * All rights reserved. + * + ****************************************************************************** +*/ + +#ifndef __RATING_H__ +#define __RATING_H__ + +#include +#include +#include +#include +#include +#include "rating.h" + +using namespace std; + +class Rating +{ +private: +public: + uint8_t minHeight; + uint8_t maxHeight; + uint16_t holes; + uint8_t fullLines; + uint16_t bumpiness; + uint16_t aggregatedHeight; + float score; + uint8_t width; + std::vector lineHights; + + Rating(uint8_t width): + width(width), + lineHights(width) + { + reset(); + } + + void reset() + { + this->minHeight = 0; + this->maxHeight = 0; + + for (uint8_t line = 0; line < this->width; line++) + { + this->lineHights[line] = 0; + } + + this->holes = 0; + this->fullLines = 0; + this->bumpiness = 0; + this->aggregatedHeight = 0; + this->score = -FLT_MAX; + } +}; + +#endif /* __RATING_H__ */ diff --git a/usermods/TetrisAI_v2/readme.md b/usermods/TetrisAI_v2/readme.md new file mode 100644 index 00000000..b56f801a --- /dev/null +++ b/usermods/TetrisAI_v2/readme.md @@ -0,0 +1,42 @@ +# Tetris AI effect usermod + +This usermod adds a self-playing Tetris game as an 'effect'. The mod requires version 0.14 or higher as it relies on matrix support. The effect was tested on an ESP32 4MB with a WS2812B 16x16 matrix. + +Version 1.0 + +## Installation + +Just activate the usermod with `-D USERMOD_TETRISAI` and the effect will become available under the name 'Tetris AI'. If you are running out of flash memory, use a different memory layout (e.g. [WLED_ESP32_4MB_256KB_FS.csv](https://github.com/Aircoookie/WLED/blob/main/tools/WLED_ESP32_4MB_256KB_FS.csv)). + +If needed simply add to `platformio_override.ini` (or `platformio_override.ini`): + +```ini +board_build.partitions = tools/WLED_ESP32_4MB_256KB_FS.csv +``` + +## Usage + +It is best to set the background color to black 🖤, the border color to light grey 🤍, the game over color (foreground) to dark grey 🩶, and color palette to 'Rainbow' 🌈. + +### Sliders and boxes + +#### Sliders + +* speed: speed the game plays +* look ahead: how many pieces is the AI allowed to know the next pieces (0 - 2) +* intelligence: how good the AI will play +* Rotate color: make the colors shift (rotate) every few moves +* Mistakes free: how many good moves between mistakes (if enabled) + +#### Checkboxes + +* show next: if true, a space of 5 pixels from the right will be used to show the next pieces. Otherwise the whole segment is used for the grid. +* show border: if true an additional column of 1 pixel is used to draw a border between the grid and the next pieces +* mistakes: if true, the worst decision will be made every few moves instead of the best (see above). + +## Best results + + If the speed is set to be a little bit faster than a good human could play with maximal intelligence and very few mistakes it makes people furious/happy at a party 😉. + +## Limits +The game grid is limited to a maximum width of 32 and a maximum height of 255 due to the internal structure of the code. The canvas of the effect will be centred in the segment if the segment exceeds the maximum width or height. \ No newline at end of file diff --git a/usermods/TetrisAI_v2/tetrisai.h b/usermods/TetrisAI_v2/tetrisai.h new file mode 100644 index 00000000..ba4fe60e --- /dev/null +++ b/usermods/TetrisAI_v2/tetrisai.h @@ -0,0 +1,207 @@ +/****************************************************************************** + * @file : ai.h + * @brief : contains the heuristic + ****************************************************************************** + * @attention + * + * Copyright (c) muebau 2023 + * All rights reserved. + * + ****************************************************************************** +*/ + +#ifndef __AI_H__ +#define __AI_H__ + +#include "gridbw.h" +#include "rating.h" + +using namespace std; + +class TetrisAI +{ +private: +public: + float aHeight; + float fullLines; + float holes; + float bumpiness; + bool findWorstMove = false; + + uint8_t countOnes(uint32_t vector) + { + uint8_t count = 0; + while (vector) + { + vector &= (vector - 1); + count++; + } + return count; + } + + void updateRating(GridBW grid, Rating* rating) + { + rating->minHeight = 0; + rating->maxHeight = 0; + rating->holes = 0; + rating->fullLines = 0; + rating->bumpiness = 0; + rating->aggregatedHeight = 0; + fill(rating->lineHights.begin(), rating->lineHights.end(), 0); + + uint32_t columnvector = 0x0; + uint32_t lastcolumnvector = 0x0; + for (uint8_t row = 0; row < grid.height; row++) + { + columnvector |= grid.pixels[row]; + + //first (highest) column makes it + if (rating->maxHeight == 0 && columnvector) + { + rating->maxHeight = grid.height - row; + } + + //if column vector is full we found the minimal height (or it stays zero) + if (rating->minHeight == 0 && (columnvector == (uint32_t)((1 << grid.width) - 1))) + { + rating->minHeight = grid.height - row; + } + + //line full if all ones in mask :-) + if (grid.isLineFull(row)) + { + rating->fullLines++; + } + + //holes are basically a XOR with the "full" columns + rating->holes += countOnes(columnvector ^ grid.pixels[row]); + + //calculate the difference (XOR) between the current column vector and the last one + uint32_t columnDelta = columnvector ^ lastcolumnvector; + + //process every new column + uint8_t index = 0; + while (columnDelta) + { + //if this is a new column + if (columnDelta & 0x1) + { + //update hight of this column + rating->lineHights[(grid.width - 1) - index] = grid.height - row; + + // update aggregatedHeight + rating->aggregatedHeight += grid.height - row; + } + index++; + columnDelta >>= 1; + } + lastcolumnvector = columnvector; + } + + //compare every two columns to get the difference and add them up + for (uint8_t column = 1; column < grid.width; column++) + { + rating->bumpiness += abs(rating->lineHights[column - 1] - rating->lineHights[column]); + } + + rating->score = (aHeight * (rating->aggregatedHeight)) + (fullLines * (rating->fullLines)) + (holes * (rating->holes)) + (bumpiness * (rating->bumpiness)); + } + + TetrisAI(): TetrisAI(-0.510066f, 0.760666f, -0.35663f, -0.184483f) + {} + + TetrisAI(float aHeight, float fullLines, float holes, float bumpiness): + aHeight(aHeight), + fullLines(fullLines), + holes(holes), + bumpiness(bumpiness) + {} + + void findBestMove(GridBW grid, Piece *piece) + { + vector pieces = {*piece}; + findBestMove(grid, &pieces); + *piece = pieces[0]; + } + + void findBestMove(GridBW grid, std::vector *pieces) + { + findBestMove(grid, pieces->begin(), pieces->end()); + } + + void findBestMove(GridBW grid, std::vector::iterator start, std::vector::iterator end) + { + Rating bestRating(grid.width); + findBestMove(grid, start, end, &bestRating); + } + + void findBestMove(GridBW grid, std::vector::iterator start, std::vector::iterator end, Rating* bestRating) + { + grid.cleanupFullLines(); + Rating curRating(grid.width); + Rating deeperRating(grid.width); + Piece piece = *start; + + // for every rotation of the piece + for (piece.rotation = 0; piece.rotation < piece.pieceData->rotCount; piece.rotation++) + { + // put piece to top left corner + piece.x = 0; + piece.y = 0; + + //test for every column + for (piece.x = 0; piece.x <= grid.width - piece.getRotation().width; piece.x++) + { + //todo optimise by the use of the previous grids height + piece.landingY = 0; + //will set landingY to final position + grid.findLandingPosition(&piece); + + // draw piece + grid.placePiece(&piece, piece.x, piece.landingY); + + if(start == end - 1) + { + //at the deepest level + updateRating(grid, &curRating); + } + else + { + //go deeper to take another piece into account + findBestMove(grid, start + 1, end, &deeperRating); + curRating = deeperRating; + } + + // eraese piece + grid.erasePiece(&piece, piece.x, piece.landingY); + + if(findWorstMove) + { + //init rating for worst + if(bestRating->score == -FLT_MAX) + { + bestRating->score = FLT_MAX; + } + + // update if we found a worse one + if (bestRating->score > curRating.score) + { + *bestRating = curRating; + (*start) = piece; + } + } + else + { + // update if we found a better one + if (bestRating->score < curRating.score) + { + *bestRating = curRating; + (*start) = piece; + } + } + } + } + } +}; + +#endif /* __AI_H__ */ \ No newline at end of file diff --git a/usermods/TetrisAI_v2/tetrisaigame.h b/usermods/TetrisAI_v2/tetrisaigame.h new file mode 100644 index 00000000..e4766d18 --- /dev/null +++ b/usermods/TetrisAI_v2/tetrisaigame.h @@ -0,0 +1,154 @@ +/****************************************************************************** + * @file : tetrisaigame.h + * @brief : main tetris functions + ****************************************************************************** + * @attention + * + * Copyright (c) muebau 2022 + * All rights reserved. + * + ****************************************************************************** +*/ + +#ifndef __TETRISAIGAME_H__ +#define __TETRISAIGAME_H__ + +#include +#include +#include +#include "pieces.h" +#include "gridcolor.h" +#include "tetrisbag.h" +#include "tetrisai.h" + +using namespace std; + +class TetrisAIGame +{ +private: + bool animateFallOfPiece(Piece* piece, bool skip) + { + if (skip || piece->y >= piece->landingY) + { + piece->y = piece->landingY; + grid.gridBW.placePiece(piece, piece->x, piece->landingY); + grid.placePiece(piece, piece->x, piece->y); + return false; + } + else + { + // eraese last drawing + grid.erasePiece(piece, piece->x, piece->y); + + //move piece down + piece->y++; + + // draw piece + grid.placePiece(piece, piece->x, piece->y); + + return true; + } + } + +public: + uint8_t width; + uint8_t height; + uint8_t nLookAhead; + uint8_t nPieces; + TetrisBag bag; + GridColor grid; + TetrisAI ai; + Piece curPiece; + PieceData* piecesData; + enum States { INIT, TEST_GAME_OVER, GET_NEXT_PIECE, FIND_BEST_MOVE, ANIMATE_MOVE, ANIMATE_GAME_OVER } state = INIT; + + TetrisAIGame(uint8_t width, uint8_t height, uint8_t nLookAhead, PieceData* piecesData, uint8_t nPieces): + width(width), + height(height), + nLookAhead(nLookAhead), + nPieces(nPieces), + bag(nPieces, 1, nLookAhead), + grid(width, height + 4), + ai(), + piecesData(piecesData) + { + } + + void nextPiece() + { + grid.cleanupFullLines(); + bag.queuePiece(); + } + + void findBestMove() + { + ai.findBestMove(grid.gridBW, &bag.piecesQueue); + } + + bool animateFall(bool skip) + { + return animateFallOfPiece(&(bag.piecesQueue[0]), skip); + } + + bool isGameOver() + { + //if there is something in the 4 lines of the hidden area the game is over + return grid.gridBW.pixels[0] || grid.gridBW.pixels[1] || grid.gridBW.pixels[2] || grid.gridBW.pixels[3]; + } + + void poll() + { + switch (state) + { + case INIT: + reset(); + state = TEST_GAME_OVER; + break; + case TEST_GAME_OVER: + if (isGameOver()) + { + state = ANIMATE_GAME_OVER; + } + else + { + state = GET_NEXT_PIECE; + } + break; + case GET_NEXT_PIECE: + nextPiece(); + state = FIND_BEST_MOVE; + break; + case FIND_BEST_MOVE: + findBestMove(); + state = ANIMATE_MOVE; + break; + case ANIMATE_MOVE: + if (!animateFall(false)) + { + state = TEST_GAME_OVER; + } + break; + case ANIMATE_GAME_OVER: + static auto curPixel = grid.pixels.size(); + grid.pixels[curPixel] = 254; + + if (curPixel == 0) + { + state = INIT; + curPixel = grid.pixels.size(); + } + curPixel--; + break; + } + } + + void reset() + { + grid.width = width; + grid.height = height + 4; + grid.reset(); + bag.reset(); + } +}; + +#endif /* __TETRISAIGAME_H__ */ diff --git a/usermods/TetrisAI_v2/tetrisbag.h b/usermods/TetrisAI_v2/tetrisbag.h new file mode 100644 index 00000000..592dac6c --- /dev/null +++ b/usermods/TetrisAI_v2/tetrisbag.h @@ -0,0 +1,111 @@ +/****************************************************************************** + * @file : tetrisbag.h + * @brief : the tetris implementation of a random piece generator + ****************************************************************************** + * @attention + * + * Copyright (c) muebau 2022 + * All rights reserved. + * + ****************************************************************************** +*/ + +#ifndef __TETRISBAG_H__ +#define __TETRISBAG_H__ + +#include +#include +#include + +#include "tetrisbag.h" + +class TetrisBag +{ +private: +public: + uint8_t nPieces; + uint8_t nBagLength; + uint8_t queueLength; + uint8_t bagIdx; + std::vector bag; + std::vector piecesQueue; + + TetrisBag(uint8_t nPieces, uint8_t nBagLength, uint8_t queueLength): + nPieces(nPieces), + nBagLength(nBagLength), + queueLength(queueLength), + bag(nPieces * nBagLength), + piecesQueue(queueLength) + { + init(); + } + + void init() + { + //will shuffle the bag at first use + bagIdx = nPieces - 1; + + for (uint8_t bagIndex = 0; bagIndex < nPieces * nBagLength; bagIndex++) + { + bag[bagIndex] = bagIndex % nPieces; + } + + //will init the queue + for (uint8_t index = 0; index < piecesQueue.size(); index++) + { + queuePiece(); + } + } + + void shuffleBag() + { + uint8_t temp; + uint8_t swapIdx; + for (int index = nPieces - 1; index > 0; index--) + { + //get candidate to swap + swapIdx = rand() % index; + + //swap it! + temp = bag[swapIdx]; + bag[swapIdx] = bag[index]; + bag[index] = temp; + } + } + + Piece getNextPiece() + { + bagIdx++; + if (bagIdx >= nPieces) + { + shuffleBag(); + bagIdx = 0; + } + return Piece(bag[bagIdx]); + } + + void queuePiece() + { + //move vector to left + std::rotate(piecesQueue.begin(), piecesQueue.begin() + 1, piecesQueue.end()); + piecesQueue[piecesQueue.size() - 1] = getNextPiece(); + } + + void queuePiece(uint8_t idx) + { + //move vector to left + std::rotate(piecesQueue.begin(), piecesQueue.begin() + 1, piecesQueue.end()); + piecesQueue[piecesQueue.size() - 1] = Piece(idx % nPieces); + } + + void reset() + { + bag.clear(); + bag.resize(nPieces * nBagLength); + piecesQueue.clear(); + piecesQueue.resize(queueLength); + init(); + } +}; + +#endif /* __TETRISBAG_H__ */ diff --git a/usermods/TetrisAI_v2/usermod_v2_tetrisai.h b/usermods/TetrisAI_v2/usermod_v2_tetrisai.h new file mode 100644 index 00000000..0f7039da --- /dev/null +++ b/usermods/TetrisAI_v2/usermod_v2_tetrisai.h @@ -0,0 +1,252 @@ +#pragma once + +#include "wled.h" +#include "FX.h" +#include "fcn_declare.h" + +#include "tetrisaigame.h" +// By: muebau + +typedef struct TetrisAI_data +{ + unsigned long lastTime = 0; + TetrisAIGame tetris; + uint8_t intelligence; + uint8_t rotate; + bool showNext; + bool showBorder; + uint8_t colorOffset; + uint8_t colorInc; + uint8_t mistaceCountdown; + uint16_t segcols; + uint16_t segrows; + uint16_t segOffsetX; + uint16_t segOffsetY; + uint16_t effectWidth; + uint16_t effectHeight; +} tetrisai_data; + +void drawGrid(TetrisAIGame* tetris, TetrisAI_data* tetrisai_data) +{ + SEGMENT.fill(SEGCOLOR(1)); + + //GRID + for (auto index_y = 4; index_y < tetris->grid.height; index_y++) + { + for (auto index_x = 0; index_x < tetris->grid.width; index_x++) + { + CRGB color; + if (*tetris->grid.getPixel(index_x, index_y) == 0) + { + //BG color + color = SEGCOLOR(1); + } + //game over animation + else if(*tetris->grid.getPixel(index_x, index_y) == 254) + { + //use fg + color = SEGCOLOR(0); + } + else + { + //spread the color over the whole palette + uint8_t colorIndex = *tetris->grid.getPixel(index_x, index_y) * 32; + colorIndex += tetrisai_data->colorOffset; + color = ColorFromPalette(SEGPALETTE, colorIndex, 255, NOBLEND); + } + + SEGMENT.setPixelColorXY(tetrisai_data->segOffsetX + index_x, tetrisai_data->segOffsetY + index_y - 4, color); + } + } + tetrisai_data->colorOffset += tetrisai_data->colorInc; + + //NEXT PIECE AREA + if (tetrisai_data->showNext) + { + //BORDER + if (tetrisai_data->showBorder) + { + //draw a line 6 pixels from right with the border color + for (auto index_y = 0; index_y < tetrisai_data->effectHeight; index_y++) + { + SEGMENT.setPixelColorXY(tetrisai_data->segOffsetX + tetrisai_data->effectWidth - 6, tetrisai_data->segOffsetY + index_y, SEGCOLOR(2)); + } + } + + //NEXT PIECE + int piecesOffsetX = tetrisai_data->effectWidth - 4; + int piecesOffsetY = 1; + for (uint8_t nextPieceIdx = 1; nextPieceIdx < tetris->nLookAhead; nextPieceIdx++) + { + uint8_t pieceNbrOffsetY = (nextPieceIdx - 1) * 5; + + Piece piece(tetris->bag.piecesQueue[nextPieceIdx]); + + for (uint8_t pieceY = 0; pieceY < piece.getRotation().height; pieceY++) + { + for (uint8_t pieceX = 0; pieceX < piece.getRotation().width; pieceX++) + { + if (piece.getPixel(pieceX, pieceY)) + { + uint8_t colIdx = ((piece.pieceData->colorIndex * 32) + tetrisai_data->colorOffset); + SEGMENT.setPixelColorXY(tetrisai_data->segOffsetX + piecesOffsetX + pieceX, tetrisai_data->segOffsetY + piecesOffsetY + pieceNbrOffsetY + pieceY, ColorFromPalette(SEGPALETTE, colIdx, 255, NOBLEND)); + } + } + } + } + } +} + +//////////////////////////// +// 2D Tetris AI // +//////////////////////////// +uint16_t mode_2DTetrisAI() +{ + if (!strip.isMatrix || !SEGENV.allocateData(sizeof(tetrisai_data))) + { + // not a 2D set-up + SEGMENT.fill(SEGCOLOR(0)); + return 350; + } + TetrisAI_data* tetrisai_data = reinterpret_cast(SEGENV.data); + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + //range 0 - 1024ms => 1024/255 ~ 4 + uint16_t msDelayMove = 1024 - (4 * SEGMENT.speed); + int16_t msDelayGameOver = msDelayMove / 4; + + //range 0 - 2 (not including current) + uint8_t nLookAhead = SEGMENT.intensity ? (SEGMENT.intensity >> 7) + 2 : 1; + //range 0 - 16 + tetrisai_data->colorInc = SEGMENT.custom2 >> 4; + + if (tetrisai_data->tetris.nLookAhead != nLookAhead + || tetrisai_data->segcols != cols + || tetrisai_data->segrows != rows + || tetrisai_data->showNext != SEGMENT.check1 + || tetrisai_data->showBorder != SEGMENT.check2 + ) + { + tetrisai_data->segcols = cols; + tetrisai_data->segrows = rows; + tetrisai_data->showNext = SEGMENT.check1; + tetrisai_data->showBorder = SEGMENT.check2; + + //not more than 32 columns and 255 rows as this is the limit of this implementation + uint8_t gridWidth = cols > 32 ? 32 : cols; + uint8_t gridHeight = rows > 255 ? 255 : rows; + + tetrisai_data->effectWidth = 0; + tetrisai_data->effectHeight = 0; + + // do we need space for the 'next' section? + if (tetrisai_data->showNext) + { + //does it get to tight? + if (gridWidth + 5 > cols) + { + // yes, so make the grid smaller + // make space for the piece and one pixel of space + gridWidth = (gridWidth - ((gridWidth + 5) - cols)); + } + tetrisai_data->effectWidth += 5; + + // do we need space for a border? + if (tetrisai_data->showBorder) + { + if (gridWidth + 5 + 1 > cols) + { + gridWidth -= 1; + } + tetrisai_data->effectWidth += 1; + } + } + + tetrisai_data->effectWidth += gridWidth; + tetrisai_data->effectHeight += gridHeight; + + tetrisai_data->segOffsetX = cols > tetrisai_data->effectWidth ? ((cols - tetrisai_data->effectWidth) / 2) : 0; + tetrisai_data->segOffsetY = rows > tetrisai_data->effectHeight ? ((rows - tetrisai_data->effectHeight) / 2) : 0; + + tetrisai_data->tetris = TetrisAIGame(gridWidth, gridHeight, nLookAhead, piecesData, numPieces); + tetrisai_data->tetris.state = TetrisAIGame::States::INIT; + SEGMENT.fill(SEGCOLOR(1)); + } + + if (tetrisai_data->intelligence != SEGMENT.custom1) + { + tetrisai_data->intelligence = SEGMENT.custom1; + float dui = 0.2f - (0.2f * (tetrisai_data->intelligence / 255.0f)); + + tetrisai_data->tetris.ai.aHeight = -0.510066f + dui; + tetrisai_data->tetris.ai.fullLines = 0.760666f - dui; + tetrisai_data->tetris.ai.holes = -0.35663f + dui; + tetrisai_data->tetris.ai.bumpiness = -0.184483f + dui; + } + + if (tetrisai_data->tetris.state == TetrisAIGame::ANIMATE_MOVE) + { + + if (strip.now - tetrisai_data->lastTime > msDelayMove) + { + drawGrid(&tetrisai_data->tetris, tetrisai_data); + tetrisai_data->lastTime = strip.now; + tetrisai_data->tetris.poll(); + } + } + else if (tetrisai_data->tetris.state == TetrisAIGame::ANIMATE_GAME_OVER) + { + if (strip.now - tetrisai_data->lastTime > msDelayGameOver) + { + drawGrid(&tetrisai_data->tetris, tetrisai_data); + tetrisai_data->lastTime = strip.now; + tetrisai_data->tetris.poll(); + } + } + else if (tetrisai_data->tetris.state == TetrisAIGame::FIND_BEST_MOVE) + { + if (SEGMENT.check3) + { + if(tetrisai_data->mistaceCountdown == 0) + { + tetrisai_data->tetris.ai.findWorstMove = true; + tetrisai_data->tetris.poll(); + tetrisai_data->tetris.ai.findWorstMove = false; + tetrisai_data->mistaceCountdown = SEGMENT.custom3; + } + tetrisai_data->mistaceCountdown--; + } + tetrisai_data->tetris.poll(); + } + else + { + tetrisai_data->tetris.poll(); + } + + return FRAMETIME; +} // mode_2DTetrisAI() +static const char _data_FX_MODE_2DTETRISAI[] PROGMEM = "Tetris AI@!,Look ahead,Intelligence,Rotate color,Mistake free,Show next,Border,Mistakes;Game Over,!,Border;!;2;sx=127,ix=64,c1=255,c2=0,c3=31,o1=1,o2=1,o3=0,pal=11"; + +class TetrisAIUsermod : public Usermod +{ + +private: + +public: + void setup() + { + strip.addEffect(255, &mode_2DTetrisAI, _data_FX_MODE_2DTETRISAI); + } + + void loop() + { + + } + + uint16_t getId() + { + return USERMOD_ID_TETRISAI; + } +}; diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h index 7c1c30ee..088ac880 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.h @@ -1,6 +1,9 @@ #pragma once #include "wled.h" + +#ifdef ARDUINO_ARCH_ESP32 + #include #include @@ -8,11 +11,9 @@ #error This audio reactive usermod is not compatible with DMX Out. #endif -#ifndef ARDUINO_ARCH_ESP32 - #error This audio reactive usermod does not support the ESP8266. #endif -#if defined(WLED_DEBUG) || defined(SR_DEBUG) +#if defined(ARDUINO_ARCH_ESP32) && (defined(WLED_DEBUG) || defined(SR_DEBUG)) #include #endif @@ -57,6 +58,50 @@ #define MAX_PALETTES 3 +static volatile bool disableSoundProcessing = false; // if true, sound processing (FFT, filters, AGC) will be suspended. "volatile" as its shared between tasks. +static uint8_t audioSyncEnabled = 0; // bit field: bit 0 - send, bit 1 - receive (config value) +static bool udpSyncConnected = false; // UDP connection status -> true if connected to multicast group + +#define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! + +// audioreactive variables +#ifdef ARDUINO_ARCH_ESP32 +static float micDataReal = 0.0f; // MicIn data with full 24bit resolution - lowest 8bit after decimal point +static float multAgc = 1.0f; // sample * multAgc = sampleAgc. Our AGC multiplier +static float sampleAvg = 0.0f; // Smoothed Average sample - sampleAvg < 1 means "quiet" (simple noise gate) +static float sampleAgc = 0.0f; // Smoothed AGC sample +static uint8_t soundAgc = 0; // Automagic gain control: 0 - none, 1 - normal, 2 - vivid, 3 - lazy (config value) +#endif +//static float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample +static float FFT_MajorPeak = 1.0f; // FFT: strongest (peak) frequency +static float FFT_Magnitude = 0.0f; // FFT: volume (magnitude) of peak frequency +static bool samplePeak = false; // Boolean flag for peak - used in effects. Responding routine may reset this flag. Auto-reset after strip.getMinShowDelay() +static bool udpSamplePeak = false; // Boolean flag for peak. Set at the same time as samplePeak, but reset by transmitAudioData +static unsigned long timeOfPeak = 0; // time of last sample peak detection. +static uint8_t fftResult[NUM_GEQ_CHANNELS]= {0};// Our calculated freq. channel result table to be used by effects + +// TODO: probably best not used by receive nodes +//static float agcSensitivity = 128; // AGC sensitivity estimation, based on agc gain (multAgc). calculated by getSensitivity(). range 0..255 + +// user settable parameters for limitSoundDynamics() +#ifdef UM_AUDIOREACTIVE_DYNAMICS_LIMITER_OFF +static bool limiterOn = false; // bool: enable / disable dynamics limiter +#else +static bool limiterOn = true; +#endif +static uint16_t attackTime = 80; // int: attack time in milliseconds. Default 0.08sec +static uint16_t decayTime = 1400; // int: decay time in milliseconds. Default 1.40sec + +// peak detection +#ifdef ARDUINO_ARCH_ESP32 +static void detectSamplePeak(void); // peak detection function (needs scaled FFT results in vReal[]) - no used for 8266 receive-only mode +#endif +static void autoResetPeak(void); // peak auto-reset function +static uint8_t maxVol = 31; // (was 10) Reasonable value for constant volume for 'peak detector', as it won't always trigger (deprecated) +static uint8_t binNum = 8; // Used to select the bin for FFT based beat detection (deprecated) + +#ifdef ARDUINO_ARCH_ESP32 + // use audio source class (ESP32 specific) #include "audio_source.h" constexpr i2s_port_t I2S_PORT = I2S_NUM_0; // I2S port to use (do not change !) @@ -74,18 +119,6 @@ static uint8_t inputLevel = 128; // UI slider value #else uint8_t sampleGain = SR_GAIN; // sample gain (config value) #endif -static uint8_t soundAgc = 1; // Automagic gain control: 0 - none, 1 - normal, 2 - vivid, 3 - lazy (config value) -static uint8_t audioSyncEnabled = 0; // bit field: bit 0 - send, bit 1 - receive (config value) -static bool udpSyncConnected = false; // UDP connection status -> true if connected to multicast group - -// user settable parameters for limitSoundDynamics() -#ifdef UM_AUDIOREACTIVE_DYNAMICS_LIMITER_OFF -static bool limiterOn = false; // bool: enable / disable dynamics limiter -#else -static bool limiterOn = true; -#endif -static uint16_t attackTime = 80; // int: attack time in milliseconds. Default 0.08sec -static uint16_t decayTime = 1400; // int: decay time in milliseconds. Default 1.40sec // user settable options for FFTResult scaling static uint8_t FFTScalingMode = 3; // 0 none; 1 optimized logarithmic; 2 optimized linear; 3 optimized square root @@ -109,25 +142,8 @@ const float agcSampleSmooth[AGC_NUM_PRESETS] = { 1/12.f, 1/6.f, 1/16.f}; // // AGC presets end static AudioSource *audioSource = nullptr; -static volatile bool disableSoundProcessing = false; // if true, sound processing (FFT, filters, AGC) will be suspended. "volatile" as its shared between tasks. static bool useBandPassFilter = false; // if true, enables a bandpass filter 80Hz-16Khz to remove noise. Applies before FFT. -// audioreactive variables shared with FFT task -static float micDataReal = 0.0f; // MicIn data with full 24bit resolution - lowest 8bit after decimal point -static float multAgc = 1.0f; // sample * multAgc = sampleAgc. Our AGC multiplier -static float sampleAvg = 0.0f; // Smoothed Average sample - sampleAvg < 1 means "quiet" (simple noise gate) -static float sampleAgc = 0.0f; // Smoothed AGC sample - -// peak detection -static bool samplePeak = false; // Boolean flag for peak - used in effects. Responding routine may reset this flag. Auto-reset after strip.getMinShowDelay() -static uint8_t maxVol = 31; // Reasonable value for constant volume for 'peak detector', as it won't always trigger (deprecated) -static uint8_t binNum = 8; // Used to select the bin for FFT based beat detection (deprecated) -static bool udpSamplePeak = false; // Boolean flag for peak. Set at the same time as samplePeak, but reset by transmitAudioData -static unsigned long timeOfPeak = 0; // time of last sample peak detection. -static void detectSamplePeak(void); // peak detection function (needs scaled FFT results in vReal[]) -static void autoResetPeak(void); // peak auto-reset function - - //////////////////// // Begin FFT Code // //////////////////// @@ -139,17 +155,12 @@ void FFTcode(void * parameter); // audio processing task: read samples, run static void runMicFilter(uint16_t numSamples, float *sampleBuffer); // pre-filtering of raw samples (band-pass) static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels); // post-processing and post-amp of GEQ channels -#define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! - static TaskHandle_t FFT_Task = nullptr; // Table of multiplication factors so that we can even out the frequency response. static float fftResultPink[NUM_GEQ_CHANNELS] = { 1.70f, 1.71f, 1.73f, 1.78f, 1.68f, 1.56f, 1.55f, 1.63f, 1.79f, 1.62f, 1.80f, 2.06f, 2.47f, 3.35f, 6.83f, 9.55f }; // globals and FFT Output variables shared with animations -static float FFT_MajorPeak = 1.0f; // FFT: strongest (peak) frequency -static float FFT_Magnitude = 0.0f; // FFT: volume (magnitude) of peak frequency -static uint8_t fftResult[NUM_GEQ_CHANNELS]= {0};// Our calculated freq. channel result table to be used by effects #if defined(WLED_DEBUG) || defined(SR_DEBUG) static uint64_t fftTime = 0; static uint64_t sampleTime = 0; @@ -183,31 +194,20 @@ constexpr uint16_t samplesFFT_2 = 256; // meaningfull part of FFT resul // These are the input and output vectors. Input vectors receive computed results from FFT. static float vReal[samplesFFT] = {0.0f}; // FFT sample inputs / freq output - these are our raw result bins static float vImag[samplesFFT] = {0.0f}; // imaginary parts -#ifdef UM_AUDIOREACTIVE_USE_NEW_FFT -static float windowWeighingFactors[samplesFFT] = {0.0f}; -#endif // Create FFT object -#ifdef UM_AUDIOREACTIVE_USE_NEW_FFT - // lib_deps += https://github.com/kosme/arduinoFFT#develop @ 1.9.2 - // these options actually cause slow-downs on all esp32 processors, don't use them. - // #define FFT_SPEED_OVER_PRECISION // enables use of reciprocals (1/x etc) - not faster on ESP32 - // #define FFT_SQRT_APPROXIMATION // enables "quake3" style inverse sqrt - slower on ESP32 - // Below options are forcing ArduinoFFT to use sqrtf() instead of sqrt() - #define sqrt(x) sqrtf(x) // little hack that reduces FFT time by 10-50% on ESP32 - #define sqrt_internal sqrtf // see https://github.com/kosme/arduinoFFT/pull/83 -#else - // around 40% slower on -S2 - // lib_deps += https://github.com/blazoncek/arduinoFFT.git -#endif +// lib_deps += https://github.com/kosme/arduinoFFT#develop @ 1.9.2 +// these options actually cause slow-downs on all esp32 processors, don't use them. +// #define FFT_SPEED_OVER_PRECISION // enables use of reciprocals (1/x etc) - not faster on ESP32 +// #define FFT_SQRT_APPROXIMATION // enables "quake3" style inverse sqrt - slower on ESP32 +// Below options are forcing ArduinoFFT to use sqrtf() instead of sqrt() +#define sqrt(x) sqrtf(x) // little hack that reduces FFT time by 10-50% on ESP32 +#define sqrt_internal sqrtf // see https://github.com/kosme/arduinoFFT/pull/83 #include -#ifdef UM_AUDIOREACTIVE_USE_NEW_FFT -static ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, windowWeighingFactors); -#else -static arduinoFFT FFT = arduinoFFT(vReal, vImag, samplesFFT, SAMPLE_RATE); -#endif +/* Create FFT object with weighing factor storage */ +static ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, true); // Helper functions @@ -288,28 +288,14 @@ void FFTcode(void * parameter) #endif // run FFT (takes 3-5ms on ESP32, ~12ms on ESP32-S2) -#ifdef UM_AUDIOREACTIVE_USE_NEW_FFT FFT.dcRemoval(); // remove DC offset FFT.windowing( FFTWindow::Flat_top, FFTDirection::Forward); // Weigh data using "Flat Top" function - better amplitude accuracy //FFT.windowing(FFTWindow::Blackman_Harris, FFTDirection::Forward); // Weigh data using "Blackman- Harris" window - sharp peaks due to excellent sideband rejection FFT.compute( FFTDirection::Forward ); // Compute FFT FFT.complexToMagnitude(); // Compute magnitudes -#else - FFT.DCRemoval(); // let FFT lib remove DC component, so we don't need to care about this in getSamples() + vReal[0] = 0; // The remaining DC offset on the signal produces a strong spike on position 0 that should be eliminated to avoid issues. - //FFT.Windowing( FFT_WIN_TYP_HAMMING, FFT_FORWARD ); // Weigh data - standard Hamming window - //FFT.Windowing( FFT_WIN_TYP_BLACKMAN, FFT_FORWARD ); // Blackman window - better side freq rejection - //FFT.Windowing( FFT_WIN_TYP_BLACKMAN_HARRIS, FFT_FORWARD );// Blackman-Harris - excellent sideband rejection - FFT.Windowing( FFT_WIN_TYP_FLT_TOP, FFT_FORWARD ); // Flat Top Window - better amplitude accuracy - FFT.Compute( FFT_FORWARD ); // Compute FFT - FFT.ComplexToMagnitude(); // Compute magnitudes -#endif - -#ifdef UM_AUDIOREACTIVE_USE_NEW_FFT - FFT.majorPeak(FFT_MajorPeak, FFT_Magnitude); // let the effects know which freq was most dominant -#else - FFT.MajorPeak(&FFT_MajorPeak, &FFT_Magnitude); // let the effects know which freq was most dominant -#endif + FFT.majorPeak(&FFT_MajorPeak, &FFT_Magnitude); // let the effects know which freq was most dominant FFT_MajorPeak = constrain(FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects #if defined(WLED_DEBUG) || defined(SR_DEBUG) @@ -547,6 +533,8 @@ static void detectSamplePeak(void) { } } +#endif + static void autoResetPeak(void) { uint16_t MinShowDelay = MAX(50, strip.getMinShowDelay()); // Fixes private class variable compiler error. Unsure if this is the correct way of fixing the root problem. -THATDONFC if (millis() - timeOfPeak > MinShowDelay) { // Auto-reset of samplePeak after a complete frame has passed. @@ -564,6 +552,8 @@ static void autoResetPeak(void) { class AudioReactive : public Usermod { private: +#ifdef ARDUINO_ARCH_ESP32 + #ifndef AUDIOPIN int8_t audioPin = -1; #else @@ -595,20 +585,23 @@ class AudioReactive : public Usermod { #else int8_t mclkPin = MCLK_PIN; #endif +#endif - // new "V2" audiosync struct - 40 Bytes - struct audioSyncPacket { - char header[6]; // 06 Bytes - float sampleRaw; // 04 Bytes - either "sampleRaw" or "rawSampleAgc" depending on soundAgc setting - float sampleSmth; // 04 Bytes - either "sampleAvg" or "sampleAgc" depending on soundAgc setting - uint8_t samplePeak; // 01 Bytes - 0 no peak; >=1 peak detected. In future, this will also provide peak Magnitude - uint8_t reserved1; // 01 Bytes - for future extensions - not used yet - uint8_t fftResult[16]; // 16 Bytes - float FFT_Magnitude; // 04 Bytes - float FFT_MajorPeak; // 04 Bytes + // new "V2" audiosync struct - 44 Bytes + struct __attribute__ ((packed)) audioSyncPacket { // "packed" ensures that there are no additional gaps + char header[6]; // 06 Bytes offset 0 + uint8_t reserved1[2]; // 02 Bytes, offset 6 - gap required by the compiler - not used yet + float sampleRaw; // 04 Bytes offset 8 - either "sampleRaw" or "rawSampleAgc" depending on soundAgc setting + float sampleSmth; // 04 Bytes offset 12 - either "sampleAvg" or "sampleAgc" depending on soundAgc setting + uint8_t samplePeak; // 01 Bytes offset 16 - 0 no peak; >=1 peak detected. In future, this will also provide peak Magnitude + uint8_t reserved2; // 01 Bytes offset 17 - for future extensions - not used yet + uint8_t fftResult[16]; // 16 Bytes offset 18 + uint16_t reserved3; // 02 Bytes, offset 34 - gap required by the compiler - not used yet + float FFT_Magnitude; // 04 Bytes offset 36 + float FFT_MajorPeak; // 04 Bytes offset 40 }; - // old "V1" audiosync struct - 83 Bytes - for backwards compatibility + // old "V1" audiosync struct - 83 Bytes payload, 88 bytes total (with padding added by compiler) - for backwards compatibility struct audioSyncPacket_v1 { char header[6]; // 06 Bytes uint8_t myVals[32]; // 32 Bytes @@ -621,6 +614,8 @@ class AudioReactive : public Usermod { double FFT_MajorPeak; // 08 Bytes }; + #define UDPSOUND_MAX_PACKET 88 // max packet size for audiosync + // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) #ifdef UM_AUDIOREACTIVE_ENABLE bool enabled = true; @@ -638,10 +633,14 @@ class AudioReactive : public Usermod { const uint16_t delayMs = 10; // I don't want to sample too often and overload WLED uint16_t audioSyncPort= 11988;// default port for UDP sound sync + bool updateIsRunning = false; // true during OTA. + +#ifdef ARDUINO_ARCH_ESP32 // used for AGC int last_soundAgc = -1; // used to detect AGC mode change (for resetting AGC internal error buffers) double control_integrated = 0.0; // persistent across calls to agcAvg(); "integrator control" = accumulated error + // variables used by getSample() and agcAvg() int16_t micIn = 0; // Current sample starts with negative values and large values, which is why it's 16 bit signed double sampleMax = 0.0; // Max sample over a few seconds. Needed for AGC controller. @@ -650,6 +649,7 @@ class AudioReactive : public Usermod { float sampleReal = 0.0f; // "sampleRaw" as float, to provide bits that are lost otherwise (before amplification by sampleGain or inputLevel). Needed for AGC. int16_t sampleRaw = 0; // Current sample. Must only be updated ONCE!!! (amplified mic value by sampleGain and inputLevel) int16_t rawSampleAgc = 0; // not smoothed AGC sample +#endif // variables used in effects float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample @@ -670,7 +670,9 @@ class AudioReactive : public Usermod { static const char _dynamics[]; static const char _frequency[]; static const char _inputLvl[]; +#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) static const char _analogmic[]; +#endif static const char _digitalmic[]; static const char _addPalettes[]; static const char UDP_SYNC_HEADER[]; @@ -697,11 +699,13 @@ class AudioReactive : public Usermod { //PLOT_PRINT("sampleAgc:"); PLOT_PRINT(sampleAgc); PLOT_PRINT("\t"); //PLOT_PRINT("sampleAvg:"); PLOT_PRINT(sampleAvg); PLOT_PRINT("\t"); //PLOT_PRINT("sampleReal:"); PLOT_PRINT(sampleReal); PLOT_PRINT("\t"); + #ifdef ARDUINO_ARCH_ESP32 //PLOT_PRINT("micIn:"); PLOT_PRINT(micIn); PLOT_PRINT("\t"); //PLOT_PRINT("sample:"); PLOT_PRINT(sample); PLOT_PRINT("\t"); //PLOT_PRINT("sampleMax:"); PLOT_PRINT(sampleMax); PLOT_PRINT("\t"); //PLOT_PRINT("samplePeak:"); PLOT_PRINT((samplePeak!=0) ? 128:0); PLOT_PRINT("\t"); //PLOT_PRINT("multAgc:"); PLOT_PRINT(multAgc, 4); PLOT_PRINT("\t"); + #endif PLOT_PRINTLN(); #endif @@ -757,6 +761,7 @@ class AudioReactive : public Usermod { } // logAudio() +#ifdef ARDUINO_ARCH_ESP32 ////////////////////// // Audio Processing // ////////////////////// @@ -927,6 +932,7 @@ class AudioReactive : public Usermod { sampleAvg = fabsf(sampleAvg); // make sure we have a positive value } // getSample() +#endif /* Limits the dynamics of volumeSmth (= sampleAvg or sampleAgc). * does not affect FFTResult[] or volumeRaw ( = sample or rawSampleAgc) @@ -973,12 +979,14 @@ class AudioReactive : public Usermod { if (udpSyncConnected) return; // already connected if (!(apActive || interfacesInited)) return; // neither AP nor other connections availeable if (millis() - last_connection_attempt < 15000) return; // only try once in 15 seconds + if (updateIsRunning) return; // if we arrive here, we need a UDP connection but don't have one last_connection_attempt = millis(); connected(); // try to start UDP } +#ifdef ARDUINO_ARCH_ESP32 void transmitAudioData() { if (!udpSyncConnected) return; @@ -993,7 +1001,6 @@ class AudioReactive : public Usermod { transmitData.sampleSmth = (soundAgc) ? sampleAgc : sampleAvg; transmitData.samplePeak = udpSamplePeak ? 1:0; udpSamplePeak = false; // Reset udpSamplePeak after we've transmitted it - transmitData.reserved1 = 0; for (int i = 0; i < NUM_GEQ_CHANNELS; i++) { transmitData.fftResult[i] = (uint8_t)constrain(fftResult[i], 0, 254); @@ -1009,37 +1016,44 @@ class AudioReactive : public Usermod { return; } // transmitAudioData() +#endif + static bool isValidUdpSyncVersion(const char *header) { - return strncmp_P(header, PSTR(UDP_SYNC_HEADER), 6) == 0; + return strncmp_P(header, UDP_SYNC_HEADER, 6) == 0; } static bool isValidUdpSyncVersion_v1(const char *header) { - return strncmp_P(header, PSTR(UDP_SYNC_HEADER_v1), 6) == 0; + return strncmp_P(header, UDP_SYNC_HEADER_v1, 6) == 0; } void decodeAudioData(int packetSize, uint8_t *fftBuff) { - audioSyncPacket *receivedPacket = reinterpret_cast(fftBuff); + audioSyncPacket receivedPacket; + memset(&receivedPacket, 0, sizeof(receivedPacket)); // start clean + memcpy(&receivedPacket, fftBuff, min((unsigned)packetSize, (unsigned)sizeof(receivedPacket))); // don't violate alignment - thanks @willmmiles# + // update samples for effects - volumeSmth = fmaxf(receivedPacket->sampleSmth, 0.0f); - volumeRaw = fmaxf(receivedPacket->sampleRaw, 0.0f); + volumeSmth = fmaxf(receivedPacket.sampleSmth, 0.0f); + volumeRaw = fmaxf(receivedPacket.sampleRaw, 0.0f); +#ifdef ARDUINO_ARCH_ESP32 // update internal samples sampleRaw = volumeRaw; sampleAvg = volumeSmth; rawSampleAgc = volumeRaw; sampleAgc = volumeSmth; multAgc = 1.0f; +#endif // Only change samplePeak IF it's currently false. // If it's true already, then the animation still needs to respond. autoResetPeak(); if (!samplePeak) { - samplePeak = receivedPacket->samplePeak >0 ? true:false; + samplePeak = receivedPacket.samplePeak >0 ? true:false; if (samplePeak) timeOfPeak = millis(); //userVar1 = samplePeak; } - //These values are only available on the ESP32 - for (int i = 0; i < NUM_GEQ_CHANNELS; i++) fftResult[i] = receivedPacket->fftResult[i]; - my_magnitude = fmaxf(receivedPacket->FFT_Magnitude, 0.0f); + //These values are only computed by ESP32 + for (int i = 0; i < NUM_GEQ_CHANNELS; i++) fftResult[i] = receivedPacket.fftResult[i]; + my_magnitude = fmaxf(receivedPacket.FFT_Magnitude, 0.0f); FFT_Magnitude = my_magnitude; - FFT_MajorPeak = constrain(receivedPacket->FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects + FFT_MajorPeak = constrain(receivedPacket.FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects } void decodeAudioData_v1(int packetSize, uint8_t *fftBuff) { @@ -1047,12 +1061,14 @@ class AudioReactive : public Usermod { // update samples for effects volumeSmth = fmaxf(receivedPacket->sampleAgc, 0.0f); volumeRaw = volumeSmth; // V1 format does not have "raw" AGC sample +#ifdef ARDUINO_ARCH_ESP32 // update internal samples sampleRaw = fmaxf(receivedPacket->sampleRaw, 0.0f); sampleAvg = fmaxf(receivedPacket->sampleAvg, 0.0f);; sampleAgc = volumeSmth; rawSampleAgc = volumeRaw; - multAgc = 1.0f; + multAgc = 1.0f; +#endif // Only change samplePeak IF it's currently false. // If it's true already, then the animation still needs to respond. autoResetPeak(); @@ -1074,9 +1090,12 @@ class AudioReactive : public Usermod { bool haveFreshData = false; size_t packetSize = fftUdp.parsePacket(); - if (packetSize > 5) { +#ifdef ARDUINO_ARCH_ESP32 + if ((packetSize > 0) && ((packetSize < 5) || (packetSize > UDPSOUND_MAX_PACKET))) fftUdp.flush(); // discard invalid packets (too small or too big) - only works on esp32 +#endif + if ((packetSize > 5) && (packetSize <= UDPSOUND_MAX_PACKET)) { //DEBUGSR_PRINTLN("Received UDP Sync Packet"); - uint8_t fftBuff[packetSize]; + uint8_t fftBuff[UDPSOUND_MAX_PACKET+1] = { 0 }; // fixed-size buffer for receiving (stack), to avoid heap fragmentation caused by variable sized arrays fftUdp.read(fftBuff, packetSize); // VERIFY THAT THIS IS A COMPATIBLE PACKET @@ -1138,6 +1157,9 @@ class AudioReactive : public Usermod { um_data->u_type[7] = UMT_BYTE; } + +#ifdef ARDUINO_ARCH_ESP32 + // Reset I2S peripheral for good measure i2s_driver_uninstall(I2S_NUM_0); // E (696) I2S: i2s_driver_uninstall(2006): I2S port 0 has not installed #if !defined(CONFIG_IDF_TARGET_ESP32C3) @@ -1147,6 +1169,11 @@ class AudioReactive : public Usermod { delay(100); // Give that poor microphone some time to setup. useBandPassFilter = false; + + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + if ((i2sckPin == I2S_PIN_NO_CHANGE) && (i2ssdPin >= 0) && (i2swsPin >= 0) && ((dmType == 1) || (dmType == 4)) ) dmType = 5; // dummy user support: SCK == -1 --means--> PDM microphone + #endif + switch (dmType) { #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) // stub cases for not-yet-supported I2S modes on other ESP32 chips @@ -1210,10 +1237,12 @@ class AudioReactive : public Usermod { delay(250); // give microphone enough time to initialise if (!audioSource) enabled = false; // audio failed to initialise - if (enabled) onUpdateBegin(false); // create FFT task - if (FFT_Task == nullptr) enabled = false; // FFT task creation failed - if (enabled) disableSoundProcessing = false; // all good - enable audio processing +#endif + if (enabled) onUpdateBegin(false); // create FFT task, and initialize network + +#ifdef ARDUINO_ARCH_ESP32 + if (FFT_Task == nullptr) enabled = false; // FFT task creation failed if((!audioSource) || (!audioSource->isInitialized())) { // audio source failed to initialize. Still stay "enabled", as there might be input arriving via UDP Sound Sync #ifdef WLED_DEBUG DEBUG_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings.")); @@ -1222,7 +1251,8 @@ class AudioReactive : public Usermod { #endif disableSoundProcessing = true; } - +#endif + if (enabled) disableSoundProcessing = false; // all good - enable audio processing if (enabled) connectUDPSoundSync(); if (enabled && addPalettes) createAudioPalettes(); initDone = true; @@ -1241,7 +1271,7 @@ class AudioReactive : public Usermod { } if (audioSyncPort > 0 && (audioSyncEnabled & 0x03)) { - #ifndef ESP8266 + #ifdef ARDUINO_ARCH_ESP32 udpSyncConnected = fftUdp.beginMulticast(IPAddress(239, 0, 0, 1), audioSyncPort); #else udpSyncConnected = fftUdp.beginMulticast(WiFi.localIP(), IPAddress(239, 0, 0, 1), audioSyncPort); @@ -1280,7 +1310,7 @@ class AudioReactive : public Usermod { ||(realtimeMode == REALTIME_MODE_ADALIGHT) ||(realtimeMode == REALTIME_MODE_ARTNET) ) ) // please add other modes here if needed { - #ifdef WLED_DEBUG + #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG) if ((disableSoundProcessing == false) && (audioSyncEnabled == 0)) { // we just switched to "disabled" DEBUG_PRINTLN(F("[AR userLoop] realtime mode active - audio processing suspended.")); DEBUG_PRINTF_P(PSTR(" RealtimeMode = %d; RealtimeOverride = %d\n"), int(realtimeMode), int(realtimeOverride)); @@ -1288,7 +1318,7 @@ class AudioReactive : public Usermod { #endif disableSoundProcessing = true; } else { - #ifdef WLED_DEBUG + #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG) if ((disableSoundProcessing == true) && (audioSyncEnabled == 0) && audioSource->isInitialized()) { // we just switched to "enabled" DEBUG_PRINTLN(F("[AR userLoop] realtime mode ended - audio processing resumed.")); DEBUG_PRINTF_P(PSTR(" RealtimeMode = %d; RealtimeOverride = %d\n"), int(realtimeMode), int(realtimeOverride)); @@ -1300,6 +1330,7 @@ class AudioReactive : public Usermod { if (audioSyncEnabled & 0x02) disableSoundProcessing = true; // make sure everything is disabled IF in audio Receive mode if (audioSyncEnabled & 0x01) disableSoundProcessing = false; // keep running audio IF we're in audio Transmit mode +#ifdef ARDUINO_ARCH_ESP32 if (!audioSource->isInitialized()) disableSoundProcessing = true; // no audio source @@ -1339,6 +1370,7 @@ class AudioReactive : public Usermod { limitSampleDynamics(); } // if (!disableSoundProcessing) +#endif autoResetPeak(); // auto-reset sample peak after strip minShowDelay if (!udpSyncConnected) udpSamplePeak = false; // reset UDP samplePeak while UDP is unconnected @@ -1372,6 +1404,7 @@ class AudioReactive : public Usermod { #endif // Info Page: keep max sample from last 5 seconds +#ifdef ARDUINO_ARCH_ESP32 if ((millis() - sampleMaxTimer) > CYCLE_SAMPLEMAX) { sampleMaxTimer = millis(); maxSample5sec = (0.15f * maxSample5sec) + 0.85f *((soundAgc) ? sampleAgc : sampleAvg); // reset, and start with some smoothing @@ -1379,13 +1412,25 @@ class AudioReactive : public Usermod { } else { if ((sampleAvg >= 1)) maxSample5sec = fmaxf(maxSample5sec, (soundAgc) ? rawSampleAgc : sampleRaw); // follow maximum volume } +#else // similar functionality for 8266 receive only - use VolumeSmth instead of raw sample data + if ((millis() - sampleMaxTimer) > CYCLE_SAMPLEMAX) { + sampleMaxTimer = millis(); + maxSample5sec = (0.15 * maxSample5sec) + 0.85 * volumeSmth; // reset, and start with some smoothing + if (volumeSmth < 1.0f) maxSample5sec = 0; // noise gate + if (maxSample5sec < 0.0f) maxSample5sec = 0; // avoid negative values + } else { + if (volumeSmth >= 1.0f) maxSample5sec = fmaxf(maxSample5sec, volumeRaw); // follow maximum volume + } +#endif +#ifdef ARDUINO_ARCH_ESP32 //UDP Microphone Sync - transmit mode if ((audioSyncEnabled & 0x01) && (millis() - lastTime > 20)) { // Only run the transmit code IF we're in Transmit mode transmitAudioData(); lastTime = millis(); } +#endif fillAudioPalettes(); } @@ -1398,7 +1443,7 @@ class AudioReactive : public Usermod { return true; } - +#ifdef ARDUINO_ARCH_ESP32 void onUpdateBegin(bool init) override { #ifdef WLED_DEBUG @@ -1447,9 +1492,32 @@ class AudioReactive : public Usermod { } micDataReal = 0.0f; // just to be sure if (enabled) disableSoundProcessing = false; + updateIsRunning = init; } +#else // reduced function for 8266 + void onUpdateBegin(bool init) + { + // gracefully suspend audio (if running) + disableSoundProcessing = true; + // reset sound data + volumeRaw = 0; volumeSmth = 0; + for(int i=(init?0:1); i don't process audio + updateIsRunning = init; + } +#endif +#ifdef ARDUINO_ARCH_ESP32 /** * handleButton() can be used to override default button behaviour. Returning true * will prevent button working in a default way. @@ -1467,7 +1535,7 @@ class AudioReactive : public Usermod { return false; } - +#endif //////////////////////////// // Settings and Info Page // //////////////////////////// @@ -1479,7 +1547,9 @@ class AudioReactive : public Usermod { */ void addToJsonInfo(JsonObject& root) override { - char myStringBuffer[16]; // buffer for snprintf() +#ifdef ARDUINO_ARCH_ESP32 + char myStringBuffer[16]; // buffer for snprintf() - not used yet on 8266 +#endif JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); @@ -1497,6 +1567,7 @@ class AudioReactive : public Usermod { infoArr.add(uiDomString); if (enabled) { +#ifdef ARDUINO_ARCH_ESP32 // Input Level Slider if (disableSoundProcessing == false) { // only show slider when audio processing is running if (soundAgc > 0) { @@ -1513,7 +1584,7 @@ class AudioReactive : public Usermod { uiDomString += F(" />
"); // infoArr.add(uiDomString); } - +#endif // The following can be used for troubleshooting user errors and is so not enclosed in #ifdef WLED_DEBUG // current Audio input @@ -1529,6 +1600,11 @@ class AudioReactive : public Usermod { } else { infoArr.add(F(" - no connection")); } +#ifndef ARDUINO_ARCH_ESP32 // substitute for 8266 + } else { + infoArr.add(F("sound sync Off")); + } +#else // ESP32 only } else { // Analog or I2S digital input if (audioSource && (audioSource->isInitialized())) { @@ -1573,7 +1649,7 @@ class AudioReactive : public Usermod { infoArr.add(roundf(multAgc*100.0f) / 100.0f); infoArr.add("x"); } - +#endif // UDP Sound Sync status infoArr = user.createNestedArray(F("UDP Sound Sync")); if (audioSyncEnabled) { @@ -1592,6 +1668,7 @@ class AudioReactive : public Usermod { } #if defined(WLED_DEBUG) || defined(SR_DEBUG) + #ifdef ARDUINO_ARCH_ESP32 infoArr = user.createNestedArray(F("Sampling time")); infoArr.add(float(sampleTime)/100.0f); infoArr.add(" ms"); @@ -1608,6 +1685,7 @@ class AudioReactive : public Usermod { DEBUGSR_PRINTF("AR Sampling time: %5.2f ms\n", float(sampleTime)/100.0f); DEBUGSR_PRINTF("AR FFT time : %5.2f ms\n", float(fftTime)/100.0f); #endif + #endif } } @@ -1646,9 +1724,11 @@ class AudioReactive : public Usermod { if (!prevEnabled && enabled) createAudioPalettes(); } } +#ifdef ARDUINO_ARCH_ESP32 if (usermod[FPSTR(_inputLvl)].is()) { inputLevel = min(255,max(0,usermod[FPSTR(_inputLvl)].as())); } +#endif } if (root.containsKey(F("rmcpal")) && root[F("rmcpal")].as()) { // handle removal of custom palettes from JSON call so we don't break things @@ -1704,6 +1784,7 @@ class AudioReactive : public Usermod { top[FPSTR(_enabled)] = enabled; top[FPSTR(_addPalettes)] = addPalettes; +#ifdef ARDUINO_ARCH_ESP32 #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) JsonObject amic = top.createNestedObject(FPSTR(_analogmic)); amic["pin"] = audioPin; @@ -1722,14 +1803,15 @@ class AudioReactive : public Usermod { cfg[F("gain")] = sampleGain; cfg[F("AGC")] = soundAgc; + JsonObject freqScale = top.createNestedObject(FPSTR(_frequency)); + freqScale[F("scale")] = FFTScalingMode; +#endif + JsonObject dynLim = top.createNestedObject(FPSTR(_dynamics)); dynLim[F("limiter")] = limiterOn; dynLim[F("rise")] = attackTime; dynLim[F("fall")] = decayTime; - JsonObject freqScale = top.createNestedObject(FPSTR(_frequency)); - freqScale[F("scale")] = FFTScalingMode; - JsonObject sync = top.createNestedObject("sync"); sync["port"] = audioSyncPort; sync["mode"] = audioSyncEnabled; @@ -1761,6 +1843,7 @@ class AudioReactive : public Usermod { configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); configComplete &= getJsonValue(top[FPSTR(_addPalettes)], addPalettes); +#ifdef ARDUINO_ARCH_ESP32 #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) configComplete &= getJsonValue(top[FPSTR(_analogmic)]["pin"], audioPin); #else @@ -1784,12 +1867,12 @@ class AudioReactive : public Usermod { configComplete &= getJsonValue(top[FPSTR(_config)][F("gain")], sampleGain); configComplete &= getJsonValue(top[FPSTR(_config)][F("AGC")], soundAgc); + configComplete &= getJsonValue(top[FPSTR(_frequency)][F("scale")], FFTScalingMode); + configComplete &= getJsonValue(top[FPSTR(_dynamics)][F("limiter")], limiterOn); configComplete &= getJsonValue(top[FPSTR(_dynamics)][F("rise")], attackTime); configComplete &= getJsonValue(top[FPSTR(_dynamics)][F("fall")], decayTime); - - configComplete &= getJsonValue(top[FPSTR(_frequency)][F("scale")], FFTScalingMode); - +#endif configComplete &= getJsonValue(top["sync"]["port"], audioSyncPort); configComplete &= getJsonValue(top["sync"]["mode"], audioSyncEnabled); @@ -1804,6 +1887,7 @@ class AudioReactive : public Usermod { void appendConfigData() override { +#ifdef ARDUINO_ARCH_ESP32 oappend(SET_F("dd=addDropdown('AudioReactive','digitalmic:type');")); #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) oappend(SET_F("addOption(dd,'Generic Analog',0);")); @@ -1835,11 +1919,15 @@ class AudioReactive : public Usermod { oappend(SET_F("addOption(dd,'Linear (Amplitude)',2);")); oappend(SET_F("addOption(dd,'Square Root (Energy)',3);")); oappend(SET_F("addOption(dd,'Logarithmic (Loudness)',1);")); +#endif oappend(SET_F("dd=addDropdown('AudioReactive','sync:mode');")); oappend(SET_F("addOption(dd,'Off',0);")); +#ifdef ARDUINO_ARCH_ESP32 oappend(SET_F("addOption(dd,'Send',1);")); +#endif oappend(SET_F("addOption(dd,'Receive',2);")); +#ifdef ARDUINO_ARCH_ESP32 oappend(SET_F("addInfo('AudioReactive:digitalmic:type',1,'requires reboot!');")); // 0 is field type, 1 is actual field oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',0,'sd/data/dout','I2S SD');")); oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',1,'ws/clk/lrck','I2S WS');")); @@ -1849,6 +1937,7 @@ class AudioReactive : public Usermod { #else oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',3,'master clock','I2S MCLK');")); #endif +#endif } @@ -1927,8 +2016,8 @@ CRGB AudioReactive::getCRGBForBand(int x, int pal) { void AudioReactive::fillAudioPalettes() { if (!palettes) return; size_t lastCustPalette = strip.customPalettes.size(); - if (lastCustPalette >= palettes) lastCustPalette -= palettes; - for (size_t pal=0; pal= palettes) lastCustPalette -= palettes; + for (int pal=0; pal= ESP_IDF_VERSION_VAL(4, 4, 0)) && (ESP_IDF_VERSION <= ESP_IDF_VERSION_VAL(4, 4, 4)) +#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)) && (ESP_IDF_VERSION <= ESP_IDF_VERSION_VAL(4, 4, 6)) // espressif bug: only_left has no sound, left and right are swapped // https://github.com/espressif/esp-idf/issues/9635 I2S mic not working since 4.4 (IDFGH-8138) // https://github.com/espressif/esp-idf/issues/8538 I2S channel selection issue? (IDFGH-6918) @@ -770,4 +770,4 @@ class SPH0654 : public I2SSource { #endif } }; -#endif \ No newline at end of file +#endif diff --git a/usermods/audioreactive/readme.md b/usermods/audioreactive/readme.md index 47804b61..4668ca88 100644 --- a/usermods/audioreactive/readme.md +++ b/usermods/audioreactive/readme.md @@ -27,18 +27,11 @@ Currently ESP8266 is not supported, due to low speed and small RAM of this chip. There are however plans to create a lightweight audioreactive for the 8266, with reduced features. ## Installation -### using customised _arduinoFFT_ library for use with this usermod -Add `-D USERMOD_AUDIOREACTIVE` to your PlatformIO environment `build_flags`, as well as `https://github.com/blazoncek/arduinoFFT.git` to your `lib_deps`. -If you are not using PlatformIO (which you should) try adding `#define USERMOD_AUDIOREACTIVE` to *my_config.h* and make sure you have _arduinoFFT_ library downloaded and installed. +### using latest _arduinoFFT_ library version 2.x +The latest arduinoFFT release version should be used for audioreactive. -Customised _arduinoFFT_ library for use with this usermod can be found at https://github.com/blazoncek/arduinoFFT.git - -### using latest (develop) _arduinoFFT_ library -Alternatively, you can use the latest arduinoFFT development version. -ArduinoFFT `develop` library is slightly more accurate, and slightly faster than our customised library, however also needs additional 2kB RAM. - -* `build_flags` = `-D USERMOD_AUDIOREACTIVE` `-D UM_AUDIOREACTIVE_USE_NEW_FFT` -* `lib_deps`= `https://github.com/kosme/arduinoFFT#develop @ 1.9.2` +* `build_flags` = `-D USERMOD_AUDIOREACTIVE` +* `lib_deps`= `kosme/arduinoFFT @ 2.0.1` ## Configuration diff --git a/usermods/mpu6050_imu/usermod_mpu6050_imu.h b/usermods/mpu6050_imu/usermod_mpu6050_imu.h index 51dd646c..436d0360 100644 --- a/usermods/mpu6050_imu/usermod_mpu6050_imu.h +++ b/usermods/mpu6050_imu/usermod_mpu6050_imu.h @@ -87,11 +87,11 @@ class MPU6050Driver : public Usermod { int16_t accel_offset[3]; }; config_t config; + bool configDirty = true; // does the configuration need an update? // MPU control/status vars bool irqBound = false; // set true if we have bound the IRQ pin bool dmpReady = false; // set true if DMP init was successful - uint8_t devStatus; // return status after each device operation (0 = success, !0 = error) uint16_t packetSize; // expected DMP packet size (default is 42 bytes) uint16_t fifoCount; // count of all bytes currently in FIFO uint8_t fifoBuffer[64]; // FIFO storage buffer @@ -157,7 +157,10 @@ class MPU6050Driver : public Usermod { um_data.u_type[8] = UMT_UINT32; } + configDirty = false; // we have now accepted the current configuration, success or not + if (!config.enabled) return; + // TODO: notice if these have changed ?? if (i2c_scl<0 || i2c_sda<0) { DEBUG_PRINTLN(F("MPU6050: I2C is no good.")); return; } // Check the interrupt pin if (config.interruptPin >= 0) { @@ -182,7 +185,7 @@ class MPU6050Driver : public Usermod { // load and configure the DMP DEBUG_PRINTLN(F("Initializing DMP...")); - devStatus = mpu.dmpInitialize(); + auto devStatus = mpu.dmpInitialize(); // set offsets (from config) mpu.setXGyroOffset(config.gyro_offset[0]); @@ -241,6 +244,8 @@ class MPU6050Driver : public Usermod { * loop() is called continuously. Here you can check for events, read sensors, etc. */ void loop() { + if (configDirty) setup(); + // if programming failed, don't try to do anything if (!config.enabled || !dmpReady || strip.isUpdating()) return; @@ -407,8 +412,8 @@ class MPU6050Driver : public Usermod { irqBound = false; } - // Just re-init - setup(); + // Re-call setup on the next loop() + configDirty = true; } return configComplete; diff --git a/usermods/multi_relay/usermod_multi_relay.h b/usermods/multi_relay/usermod_multi_relay.h index e13cf477..efb3c8ae 100644 --- a/usermods/multi_relay/usermod_multi_relay.h +++ b/usermods/multi_relay/usermod_multi_relay.h @@ -104,6 +104,9 @@ class MultiRelay : public Usermod { static const char _HAautodiscovery[]; static const char _pcf8574[]; static const char _pcfAddress[]; + static const char _switch[]; + static const char _toggle[]; + static const char _Command[]; void handleOffTimer(); void InitHtmlAPIHandle(); @@ -261,7 +264,7 @@ void MultiRelay::handleOffTimer() { void MultiRelay::InitHtmlAPIHandle() { // https://github.com/me-no-dev/ESPAsyncWebServer DEBUG_PRINTLN(F("Relays: Initialize HTML API")); - server.on("/relays", HTTP_GET, [this](AsyncWebServerRequest *request) { + server.on(SET_F("/relays"), HTTP_GET, [this](AsyncWebServerRequest *request) { DEBUG_PRINTLN(F("Relays: HTML API")); String janswer; String error = ""; @@ -271,9 +274,9 @@ void MultiRelay::InitHtmlAPIHandle() { // https://github.com/me-no-dev/ESPAsync if (getActiveRelayCount()) { // Commands - if(request->hasParam("switch")) { + if (request->hasParam(FPSTR(_switch))) { /**** Switch ****/ - AsyncWebParameter* p = request->getParam("switch"); + AsyncWebParameter* p = request->getParam(FPSTR(_switch)); // Get Values for (int i=0; ivalue(), ',', i); @@ -284,9 +287,9 @@ void MultiRelay::InitHtmlAPIHandle() { // https://github.com/me-no-dev/ESPAsync if (_relay[i].external) switchRelay(i, (bool)value); } } - } else if(request->hasParam("toggle")) { + } else if (request->hasParam(FPSTR(_toggle))) { /**** Toggle ****/ - AsyncWebParameter* p = request->getParam("toggle"); + AsyncWebParameter* p = request->getParam(FPSTR(_toggle)); // Get Values for (int i=0;ivalue(), ',', i); @@ -314,7 +317,7 @@ void MultiRelay::InitHtmlAPIHandle() { // https://github.com/me-no-dev/ESPAsync janswer += error; janswer += F("\","); janswer += F("\"SW Version\":\""); - janswer += String(GEOGABVERSION); + janswer += String(F(GEOGABVERSION)); janswer += F("\"}"); request->send(200, "application/json", janswer); }); @@ -420,7 +423,7 @@ uint8_t MultiRelay::getActiveRelayCount() { * topic should look like: /relay/X/command; where X is relay number, 0 based */ bool MultiRelay::onMqttMessage(char* topic, char* payload) { - if (strlen(topic) > 8 && strncmp_P(topic, PSTR("/relay/"), 7) == 0 && strncmp_P(topic+8, PSTR("/command"), 8) == 0) { + if (strlen(topic) > 8 && strncmp_P(topic, PSTR("/relay/"), 7) == 0 && strncmp_P(topic+8, _Command, 8) == 0) { uint8_t relay = strtoul(topic+7, NULL, 10); if (relaysubscribe(buf, 0); json[F("stat_t")] = "~"; @@ -664,7 +667,7 @@ void MultiRelay::addToJsonInfo(JsonObject &root) { for (int i=0; i"); - uiDomString += F(""); infoArr.add(uiDomString); } @@ -836,3 +839,6 @@ const char MultiRelay::_broadcast[] PROGMEM = "broadcast-sec"; const char MultiRelay::_HAautodiscovery[] PROGMEM = "HA-autodiscovery"; const char MultiRelay::_pcf8574[] PROGMEM = "use-PCF8574"; const char MultiRelay::_pcfAddress[] PROGMEM = "PCF8574-address"; +const char MultiRelay::_switch[] PROGMEM = "switch"; +const char MultiRelay::_toggle[] PROGMEM = "toggle"; +const char MultiRelay::_Command[] PROGMEM = "/command"; diff --git a/usermods/pixels_dice_tray/README.md b/usermods/pixels_dice_tray/README.md new file mode 100644 index 00000000..6daa4fa7 --- /dev/null +++ b/usermods/pixels_dice_tray/README.md @@ -0,0 +1,254 @@ +# A mod for using Pixel Dice with ESP32S3 boards + +A usermod to connect to and handle rolls from [Pixels Dice](https://gamewithpixels.com/). WLED acts as both an display controller, and a gateway to connect the die to the Wifi network. + +High level features: + +* Several LED effects that respond to die rolls + * Effect color and parameters can be modified like any other effect + * Different die can be set to control different segments +* An optional GUI on a TFT screen with custom button controls + * Gives die connection and roll status + * Can do basic LED effect controls + * Can display custom info for different roll types (ie. RPG stats/spell info) +* Publish MQTT events from die rolls + * Also report the selected roll type +* Control settings through the WLED web + +See for a write up of the design process of the hardware and software I used this with. + +I also set up a custom web installer for the usermod at for 8MB ESP32-S3 boards. + +## Table of Contents + + +* [Demos](#demos) + + [TFT GUI](#tft-gui) + + [Multiple Die Controlling Different Segments](#multiple-die-controlling-different-segments) +* [Hardware](#hardware) +* [Library used](#library-used) +* [Compiling](#compiling) + + [platformio_override.ini](#platformio_overrideini) + + [Manual platformio.ini changes](#manual-platformioini-changes) +* [Configuration](#configuration) + + [Controlling Dice Connections](#controlling-dice-connections) + + [Controlling Effects](#controlling-effects) + - [DieSimple](#diesimple) + - [DiePulse](#diepulse) + - [DieCheck](#diecheck) +* [TFT GUI](#tft-gui-1) + + [Status](#status) + + [Effect Menu](#effect-menu) + + [Roll Info](#roll-info) +* [MQTT](#mqtt) +* [Potential Modifications and Additional Features](#potential-modifications-and-additional-features) +* [ESP32 Issues](#esp32-issues) + + + +## Demos + + +### TFT GUI +[![Watch the video](https://img.youtube.com/vi/VNsHq1TbiW8/0.jpg)](https://youtu.be/VNsHq1TbiW8) + + +### Multiple Die Controlling Different Segments +[![Watch the video](https://img.youtube.com/vi/oCDr44C-qwM/0.jpg)](https://youtu.be/oCDr44C-qwM) + + +## Hardware + +The main purpose of this mod is to support [Pixels Dice](https://gamewithpixels.com/). The board acts as a BLE central for the dice acting as peripherals. While any ESP32 variant with BLE capabilities should be able to support this usermod, in practice I found that the original ESP32 did not work. See [ESP32 Issues](#esp32-issues) for a deeper dive. + +The only other ESP32 variant I tested was the ESP32-S3, which worked without issue. While there's still concern over the contention between BLE and WiFi for the radio, I haven't noticed any performance impact in practice. The only special behavior that was needed was setting `noWifiSleep = false;` to allow the OS to sleep the WiFi when the BLE is active. + +In addition, the BLE stack requires a lot of flash. This build takes 1.9MB with the TFT code, or 1.85MB without it. This makes it too big to fit in the `tools/WLED_ESP32_4MB_256KB_FS.csv` partition layout, and I needed to make a `WLED_ESP32_4MB_64KB_FS.csv` to even fit on 4MB devices. This only has 64KB of file system space, which is functional, but users with more than a handful of presets would run into problems with 64KB only. This means that while 4MB can be supported, larger flash sizes are needed for full functionality. + +The basic build of this usermod doesn't require any special hardware. However, the LCD status GUI was specifically designed for the [LILYGO T-QT Pro](https://www.lilygo.cc/products/t-qt-pro). + +It should be relatively easy to support other displays, though the positioning of the text may need to be adjusted. + + +## Library used + +[axlan/pixels-dice-interface](https://github.com/axlan/arduino-pixels-dice) + +Optional: [Bodmer/TFT_eSPI](https://github.com/Bodmer/TFT_eSPI) + + +## Compiling + + +### platformio_override.ini + +Copy and update the example `platformio_override.ini.sample` to the root directory of your particular build (renaming it `platformio_override.ini`). +This file should be placed in the same directory as `platformio.ini`. This file is set up for the [LILYGO T-QT Pro](https://www.lilygo.cc/products/t-qt-pro). Specifically, the 8MB flash version. See the next section for notes on setting the build flags. For other boards, you may want to use a different environment as the basis. + + +### Manual platformio.ini changes + +Using the `platformio_override.ini.sample` as a reference, you'll need to update the `build_flags` and `lib_deps` of the target you're building for. + +If you don't need the TFT GUI, you just need to add + + +```ini +... +build_flags = + ... + -D USERMOD_PIXELS_DICE_TRAY ;; Enables this UserMod +lib_deps = + ... + ESP32 BLE Arduino + axlan/pixels-dice-interface @ 1.2.0 +... +``` + +For the TFT support you'll need to add `Bodmer/TFT_eSPI` to `lib_deps`, and all of the required TFT parameters to `build_flags` (see `platformio_override.ini.sample`). + +Save the `platformio.ini` file, and perform the desired build. + + +## Configuration + +In addition to configuring which dice to connect to, this mod uses a lot of the built in WLED features: +* The LED segments, effects, and customization parameters +* The buttons for the UI +* The MQTT settings for reporting the dice rolls + + +### Controlling Dice Connections + +**NOTE:** To configure the die itself (set its name, the die LEDs, etc.), you still need to use the Pixels Dice phone App. + +The usermods settings page has the configuration for controlling the dice and the display: + * Ble Scan Duration - The time to look for BLE broadcasts before taking a break + * Rotation - If display used, set this parameter to rotate the display. + +The main setting here though are the Die 0 and 1 settings. A slot is disabled if it's left blank. Putting the name of a die will make that slot only connect to die with that name. Alteratively, if the name is set to `*` the slot will use the first unassociated die it sees. Saving the configuration while a wildcard slot is connected to a die will replace the `*` with that die's name. + +**NOTE:** The slot a die is in is important since that's how they're identified for controlling LED effects. Effects can be set to respond to die 0, 1, or any. + +The configuration also includes the pins configured in the TFT build flags. These are just so the UI recognizes that these pins are being used. The [Bodmer/TFT_eSPI](https://github.com/Bodmer/TFT_eSPI) requires that these are set at build time and changing these values is ignored. + + +### Controlling Effects + +The die effects for rolls take advantage of most of the normal WLED effect features: . + +If you have different segments, they can have different effects driven by the same die, or different dice. + + +#### DieSimple +Turn off LEDs while rolling, than light up solid LEDs in proportion to die roll. + +* Color 1 - Selects the "good" color that increases based on the die roll +* Color 2 - Selects the "background" color for the rest of the segment +* Custom 1 - Sets which die should control this effect. If the value is greater then 1, it will respond to both dice. + + +#### DiePulse +Play `breath` effect while rolling, than apply `blend` effect in proportion to die roll. + +* Color 1 - See `breath` and `blend` +* Color 2 - Selects the "background" color for the rest of the segment +* Palette - See `breath` and `blend` +* Custom 1 - Sets which die should control this effect. If the value is greater then 1, it will respond to both dice. + + +#### DieCheck +Play `running` effect while rolling, than apply `glitter` effect if roll passes threshold, or `gravcenter` if roll is below. + +* Color 1 - See `glitter` and `gravcenter`, used as first color for `running` +* Color 2 - See `glitter` and `gravcenter` +* Color 3 - Used as second color for `running` +* Palette - See `glitter` and `gravcenter` +* Custom 1 - Sets which die should control this effect. If the value is greater then 1, it will respond to both dice. +* Custom 2 - Sets the threshold for success animation. For example if 10, success plays on rolls of 10 or above. + + +## TFT GUI + +The optional TFT GUI currently supports 3 "screens": +1. Status +2. Effect Control +3. Roll Info + +Double pressing the right button goes forward through the screens, and double pressing left goes back (with rollover). + + +### Status +Status Menu + +Shows the status of each die slot (0 on top and 1 on the bottom). + +If a die is connected, its roll stats and battery status are shown. The rolls will continue to be tracked even when viewing other screens. + +Long press either button to clear the roll stats. + + +### Effect Menu +Effect Menu + +Allows limited customization of the die effect for the currently selected LED segment. + +The left button moves the cursor (blue box) up and down the options for the current field. + +The right button updates the value for the field. + +The first field is the effect. Updating it will switch between the die effects. + +The DieCheck effect has an additional field "PASS". Pressing the right button on this field will copy the current face up value from the most recently rolled die. + +Long pressing either value will set the effect parameters (color, palette, controlling dice, etc.) to a default set of values. + + +### Roll Info +Roll Info Menu + +Sets the "roll type" reported by MQTT events and can show additional info. + +Pressing the right button goes forward through the rolls, and double pressing left goes back (with rollover). + +The names and info for the rolls are generated from the `usermods/pixels_dice_tray/generate_roll_info.py` script. It updates `usermods/pixels_dice_tray/roll_info.h` with code generated from a simple markdown language. + + +## MQTT + +See for general MQTT configuration for WLED. + +The usermod produces two types of events + +* `$mqttDeviceTopic/dice/roll` - JSON that reports each die roll event with the following keys. + - name - The name of the die that triggered the event + - state - Integer indicating the die state `[UNKNOWN = 0, ON_FACE = 1, HANDLING = 2, ROLLING = 3, CROOKED = 4]` + - val - The value on the die's face. For d20 1-20 + - time - The uptime timestamp the roll was received in milliseconds. +* `$mqttDeviceTopic/dice/roll_label` - A string that indicates the roll type selected in the [Roll Info](#roll-info) TFT menu. + +Where `$mqttDeviceTopic` is the topic set in the WLED MQTT configuration. + +Events can be logged to a CSV file using the script `usermods/pixels_dice_tray/mqtt_client/mqtt_logger.py`. These can then be used to generate interactive HTML plots with `usermods/pixels_dice_tray/mqtt_client/mqtt_plotter.py`. + +Roll Plot + + +## Potential Modifications and Additional Features + +This usermod is in support of a particular dice box project, but it would be fairly straightforward to extend for other applications. +* Add more dice - There's no reason that several more dice slots couldn't be allowed. In addition LED effects that use multiple dice could be added (e.g. a contested roll). +* Better support for die other then d20's. There's a few places where I assume the die is a d20. It wouldn't be that hard to support arbitrary die sizes. +* TFT Menu - The menu system is pretty extensible. I put together some basic things I found useful, and was mainly limited by the screen size. +* Die controlled UI - I originally planned to make an alternative UI that used the die directly. You'd press a button, and the current face up on the die would trigger an action. This was an interesting idea, but didn't seem to practical since I could more flexibly reproduce this by responding to the dice MQTT events. + + +## ESP32 Issues + +I really wanted to have this work on the original ESP32 boards to lower the barrier to entry, but there were several issues. + +First there are the issues with the partition sizes for 4MB mentioned in the [Hardware](#hardware) section. + +The bigger issue is that the build consistently crashes if the BLE scan task starts up. It's a bit unclear to me exactly what is failing since the backtrace is showing an exception in `new[]` memory allocation in the UDP stack. There appears to be a ton of heap available, so my guess is that this is a synchronization issue of some sort from the tasks running in parallel. I tried messing with the task core affinity a bit but didn't make much progress. It's not really clear what difference between the ESP32S3 and ESP32 would cause this difference. + +At the end of the day, its generally not advised to run the BLE and Wifi at the same time anyway (though it appears to work without issue on the ESP32S3). Probably the best path forward would be to switch between them. This would actually not be too much of an issue, since discovering and getting data from the die should be possible to do in bursts (at least in theory). diff --git a/usermods/pixels_dice_tray/WLED_ESP32_4MB_64KB_FS.csv b/usermods/pixels_dice_tray/WLED_ESP32_4MB_64KB_FS.csv new file mode 100644 index 00000000..ffa509e6 --- /dev/null +++ b/usermods/pixels_dice_tray/WLED_ESP32_4MB_64KB_FS.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x1F0000, +app1, app, ota_1, 0x200000,0x1F0000, +spiffs, data, spiffs, 0x3F0000,0x10000, \ No newline at end of file diff --git a/usermods/pixels_dice_tray/dice_state.h b/usermods/pixels_dice_tray/dice_state.h new file mode 100644 index 00000000..eee4759f --- /dev/null +++ b/usermods/pixels_dice_tray/dice_state.h @@ -0,0 +1,76 @@ +/** + * Structs for passing around usermod state + */ +#pragma once + +#include // https://github.com/axlan/arduino-pixels-dice + +/** + * Here's how the rolls are tracked in this usermod. + * 1. The arduino-pixels-dice library reports rolls and state mapped to + * PixelsDieID. + * 2. The "configured_die_names" sets which die to connect to and their order. + * 3. The rest of the usermod references the die by this order (ie. the LED + * effect is triggered for rolls for die 0). + */ + +static constexpr size_t MAX_NUM_DICE = 2; +static constexpr uint8_t INVALID_ROLL_VALUE = 0xFF; + +/** + * The state of the connected die, and new events since the last update. + */ +struct DiceUpdate { + // The vectors to hold results queried from the library + // Since vectors allocate data, it's more efficient to keep reusing an instance + // instead of declaring them on the stack. + std::vector dice_list; + pixels::RollUpdates roll_updates; + pixels::BatteryUpdates battery_updates; + // The PixelsDieID for each dice index. 0 if the die isn't connected. + // The ordering here matches configured_die_names. + std::array connected_die_ids{0, 0}; +}; + +struct DiceSettings { + // The mapping of dice names, to the index of die used for effects (ie. The + // die named "Cat" is die 0). BLE discovery will stop when all the dice are + // found. The die slot is disabled if the name is empty. If the name is "*", + // the slot will use the first unassociated die it sees. + std::array configured_die_names{"*", "*"}; + // A label set to describe the next die roll. Index into GetRollName(). + uint8_t roll_label = INVALID_ROLL_VALUE; +}; + +// These are updated in the main loop, but accessed by the effect functions as +// well. My understand is that both of these accesses should be running on the +// same "thread/task" since WLED doesn't directly create additional threads. The +// exception would be network callbacks and interrupts, but I don't believe +// these accesses are triggered by those. If synchronization was needed, I could +// look at the example in `requestJSONBufferLock()`. +std::array last_die_events; + +static pixels::RollEvent GetLastRoll() { + pixels::RollEvent last_roll; + for (const auto& event : last_die_events) { + if (event.timestamp > last_roll.timestamp) { + last_roll = event; + } + } + return last_roll; +} + +/** + * Returns true if the container has an item that matches the value. + */ +template +static bool Contains(const C& container, T value) { + return std::find(container.begin(), container.end(), value) != + container.end(); +} + +// These aren't known until runtime since they're being added dynamically. +static uint8_t FX_MODE_SIMPLE_D20 = 0xFF; +static uint8_t FX_MODE_PULSE_D20 = 0xFF; +static uint8_t FX_MODE_CHECK_D20 = 0xFF; +std::array DIE_LED_MODES = {0xFF, 0xFF, 0xFF}; diff --git a/usermods/pixels_dice_tray/generate_roll_info.py b/usermods/pixels_dice_tray/generate_roll_info.py new file mode 100644 index 00000000..58959708 --- /dev/null +++ b/usermods/pixels_dice_tray/generate_roll_info.py @@ -0,0 +1,230 @@ +''' +File for generating roll labels and info text for the InfoMenu. + +Uses a very limited markdown language for styling text. +''' +import math +from pathlib import Path +import re +from textwrap import indent + +# Variables for calculating values in info text +CASTER_LEVEL = 9 +SPELL_ABILITY_MOD = 6 +BASE_ATK_BONUS = 6 +SIZE_BONUS = 1 +STR_BONUS = 2 +DEX_BONUS = -1 + +# TFT library color values +TFT_BLACK =0x0000 +TFT_NAVY =0x000F +TFT_DARKGREEN =0x03E0 +TFT_DARKCYAN =0x03EF +TFT_MAROON =0x7800 +TFT_PURPLE =0x780F +TFT_OLIVE =0x7BE0 +TFT_LIGHTGREY =0xD69A +TFT_DARKGREY =0x7BEF +TFT_BLUE =0x001F +TFT_GREEN =0x07E0 +TFT_CYAN =0x07FF +TFT_RED =0xF800 +TFT_MAGENTA =0xF81F +TFT_YELLOW =0xFFE0 +TFT_WHITE =0xFFFF +TFT_ORANGE =0xFDA0 +TFT_GREENYELLOW =0xB7E0 +TFT_PINK =0xFE19 +TFT_BROWN =0x9A60 +TFT_GOLD =0xFEA0 +TFT_SILVER =0xC618 +TFT_SKYBLUE =0x867D +TFT_VIOLET =0x915C + + +class Size: + def __init__(self, w, h): + self.w = w + self.h = h + + +# Font 1 6x8 +# Font 2 12x16 +CHAR_SIZE = { + 1: Size(6, 8), + 2: Size(12, 16), +} + +SCREEN_SIZE = Size(128, 128) + +# Calculates distance for short range spell. +def short_range() -> int: + return 25 + 5 * CASTER_LEVEL + +# Entries in markdown language. +# Parameter 0 of the tuple is the roll name +# Parameter 1 of the tuple is the roll info. +# The text will be shown when the roll type is selected. An error will be raised +# if the text would unexpectedly goes past the end of the screen. There are a +# few styling parameters that need to be on their own lines: +# $COLOR - The color for the text +# $SIZE - Sets the text size (see CHAR_SIZE) +# $WRAP - By default text won't wrap and generate an error. This enables text wrapping. Lines will wrap mid-word. +ENTRIES = [ + tuple(["Barb Chain", f'''\ +$COLOR({TFT_RED}) +Barb Chain +$COLOR({TFT_WHITE}) +Atk/CMD {BASE_ATK_BONUS + SPELL_ABILITY_MOD} +Range: {short_range()} +$WRAP(1) +$SIZE(1) +Summon {1 + math.floor((CASTER_LEVEL-1)/3)} chains. Make a melee atk 1d6 or a trip CMD=AT. On a hit make Will save or shaken 1d4 rnds. +''']), + tuple(["Saves", f'''\ +$COLOR({TFT_GREEN}) +Saves +$COLOR({TFT_WHITE}) +FORT 8 +REFLEX 8 +WILL 9 +''']), + tuple(["Skill", f'''\ +Skill +''']), + tuple(["Attack", f'''\ +Attack +Melee +{BASE_ATK_BONUS + SIZE_BONUS + STR_BONUS} +Range +{BASE_ATK_BONUS + SIZE_BONUS + DEX_BONUS} +''']), + tuple(["Cure", f'''\ +Cure +Lit 1d8+{min(5, CASTER_LEVEL)} +Mod 2d8+{min(10, CASTER_LEVEL)} +Ser 3d8+{min(15, CASTER_LEVEL)} +''']), + tuple(["Concentrate", f'''\ +Concentrat ++{CASTER_LEVEL + SPELL_ABILITY_MOD} +$SIZE(1) +Defensive 15+2*SP_LV +Dmg 10+DMG+SP_LV +Grapple 10+CMB+SP_LV +''']), +] + +RE_SIZE = re.compile(r'\$SIZE\(([0-9])\)') +RE_COLOR = re.compile(r'\$COLOR\(([0-9]+)\)') +RE_WRAP = re.compile(r'\$WRAP\(([0-9])\)') + +END_HEADER_TXT = '// GENERATED\n' + +def main(): + roll_info_file = Path(__file__).parent / 'roll_info.h' + old_contents = open(roll_info_file, 'r').read() + + end_header = old_contents.index(END_HEADER_TXT) + + with open(roll_info_file, 'w') as fd: + fd.write(old_contents[:end_header+len(END_HEADER_TXT)]) + + for key, entry in enumerate(ENTRIES): + size = 2 + wrap = False + y_loc = 0 + results = [] + for line in entry[1].splitlines(): + if line.startswith('$'): + m_size = RE_SIZE.match(line) + m_color = RE_COLOR.match(line) + m_wrap = RE_WRAP.match(line) + if m_size: + size = int(m_size.group(1)) + results.append(f'tft.setTextSize({size});') + elif m_color: + results.append( + f'tft.setTextColor({int(m_color.group(1))});') + elif m_wrap: + wrap = bool(int(m_wrap.group(1))) + else: + print(f'Entry {key} unknown modifier "{line}".') + exit(1) + else: + max_chars_per_line = math.floor( + SCREEN_SIZE.w / CHAR_SIZE[size].w) + if len(line) > max_chars_per_line: + if wrap: + while len(line) > max_chars_per_line: + results.append( + f'tft.println("{line[:max_chars_per_line]}");') + line = line[max_chars_per_line:].lstrip() + y_loc += CHAR_SIZE[size].h + else: + print(f'Entry {key} line "{line}" too long.') + exit(1) + + if len(line) > 0: + y_loc += CHAR_SIZE[size].h + results.append(f'tft.println("{line}");') + + if y_loc > SCREEN_SIZE.h: + print( + f'Entry {key} line "{line}" went past bottom of screen.') + exit(1) + + result = indent('\n'.join(results), ' ') + + fd.write(f'''\ +static void PrintRoll{key}() {{ +{result} +}} + +''') + + results = [] + for key, entry in enumerate(ENTRIES): + results.append(f'''\ +case {key}: + return "{entry[0]}";''') + + cases = indent('\n'.join(results), ' ') + + fd.write(f'''\ +static const char* GetRollName(uint8_t key) {{ + switch (key) {{ +{cases} + }} + return ""; +}} + +''') + + results = [] + for key, entry in enumerate(ENTRIES): + results.append(f'''\ +case {key}: + PrintRoll{key}(); + return;''') + + cases = indent('\n'.join(results), ' ') + + fd.write(f'''\ +static void PrintRollInfo(uint8_t key) {{ + tft.setTextColor(TFT_WHITE); + tft.setCursor(0, 0); + tft.setTextSize(2); + switch (key) {{ +{cases} + }} + tft.setTextColor(TFT_RED); + tft.setCursor(0, 60); + tft.println("Unknown"); +}} + +''') + + fd.write(f'static constexpr size_t NUM_ROLL_INFOS = {len(ENTRIES)};\n') + + +main() diff --git a/usermods/pixels_dice_tray/images/effect.webp b/usermods/pixels_dice_tray/images/effect.webp new file mode 100644 index 00000000..989ed1eb Binary files /dev/null and b/usermods/pixels_dice_tray/images/effect.webp differ diff --git a/usermods/pixels_dice_tray/images/info.webp b/usermods/pixels_dice_tray/images/info.webp new file mode 100644 index 00000000..5cb84efb Binary files /dev/null and b/usermods/pixels_dice_tray/images/info.webp differ diff --git a/usermods/pixels_dice_tray/images/roll_plot.png b/usermods/pixels_dice_tray/images/roll_plot.png new file mode 100644 index 00000000..47f56886 Binary files /dev/null and b/usermods/pixels_dice_tray/images/roll_plot.png differ diff --git a/usermods/pixels_dice_tray/images/status.webp b/usermods/pixels_dice_tray/images/status.webp new file mode 100644 index 00000000..878e9a55 Binary files /dev/null and b/usermods/pixels_dice_tray/images/status.webp differ diff --git a/usermods/pixels_dice_tray/led_effects.h b/usermods/pixels_dice_tray/led_effects.h new file mode 100644 index 00000000..7553d681 --- /dev/null +++ b/usermods/pixels_dice_tray/led_effects.h @@ -0,0 +1,124 @@ +/** + * The LED effects influenced by dice rolls. + */ +#pragma once + +#include "wled.h" + +#include "dice_state.h" + +// Reuse FX display functions. +extern uint16_t mode_breath(); +extern uint16_t mode_blends(); +extern uint16_t mode_glitter(); +extern uint16_t mode_gravcenter(); + +static constexpr uint8_t USER_ANY_DIE = 0xFF; +/** + * Two custom effect parameters are used. + * c1 - Source Die. Sets which die from [0 - MAX_NUM_DICE) controls this effect. + * If this is set to 0xFF, use the latest event regardless of which die it + * came from. + * c2 - Target Roll. Sets the "success" criteria for a roll to >= this value. + */ + +/** + * Return the last die roll based on the custom1 effect setting. + */ +static pixels::RollEvent GetLastRollForSegment() { + // If an invalid die is selected, fallback to using the most recent roll from + // any die. + if (SEGMENT.custom1 >= MAX_NUM_DICE) { + return GetLastRoll(); + } else { + return last_die_events[SEGMENT.custom1]; + } +} + + +/* + * Alternating pixels running function (copied static function). + */ +// paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) +#define PALETTE_SOLID_WRAP (strip.paletteBlend == 1 || strip.paletteBlend == 3) +static uint16_t running_copy(uint32_t color1, uint32_t color2, bool theatre = false) { + int width = (theatre ? 3 : 1) + (SEGMENT.intensity >> 4); // window + uint32_t cycleTime = 50 + (255 - SEGMENT.speed); + uint32_t it = strip.now / cycleTime; + bool usePalette = color1 == SEGCOLOR(0); + + for (int i = 0; i < SEGLEN; i++) { + uint32_t col = color2; + if (usePalette) color1 = SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0); + if (theatre) { + if ((i % width) == SEGENV.aux0) col = color1; + } else { + int pos = (i % (width<<1)); + if ((pos < SEGENV.aux0-width) || ((pos >= SEGENV.aux0) && (pos < SEGENV.aux0+width))) col = color1; + } + SEGMENT.setPixelColor(i,col); + } + + if (it != SEGENV.step) { + SEGENV.aux0 = (SEGENV.aux0 +1) % (theatre ? width : (width<<1)); + SEGENV.step = it; + } + return FRAMETIME; +} + +static uint16_t simple_roll() { + auto roll = GetLastRollForSegment(); + if (roll.state != pixels::RollState::ON_FACE) { + SEGMENT.fill(0); + } else { + uint16_t num_segments = float(roll.current_face + 1) / 20.0 * SEGLEN; + for (int i = 0; i <= num_segments; i++) { + SEGMENT.setPixelColor(i, SEGCOLOR(0)); + } + for (int i = num_segments; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, SEGCOLOR(1)); + } + } + return FRAMETIME; +} +// See https://kno.wled.ge/interfaces/json-api/#effect-metadata +// Name - DieSimple +// Parameters - +// * Selected Die (custom1) +// Colors - Uses color1 and color2 +// Palette - Not used +// Flags - Effect is optimized for use on 1D LED strips. +// Defaults - Selected Die set to 0xFF (USER_ANY_DIE) +static const char _data_FX_MODE_SIMPLE_DIE[] PROGMEM = + "DieSimple@,,Selected Die;!,!;;1;c1=255"; + +static uint16_t pulse_roll() { + auto roll = GetLastRollForSegment(); + if (roll.state != pixels::RollState::ON_FACE) { + return mode_breath(); + } else { + uint16_t ret = mode_blends(); + uint16_t num_segments = float(roll.current_face + 1) / 20.0 * SEGLEN; + for (int i = num_segments; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, SEGCOLOR(1)); + } + return ret; + } +} +static const char _data_FX_MODE_PULSE_DIE[] PROGMEM = + "DiePulse@!,!,Selected Die;!,!;!;1;sx=24,pal=50,c1=255"; + +static uint16_t check_roll() { + auto roll = GetLastRollForSegment(); + if (roll.state != pixels::RollState::ON_FACE) { + return running_copy(SEGCOLOR(0), SEGCOLOR(2)); + } else { + if (roll.current_face + 1 >= SEGMENT.custom2) { + return mode_glitter(); + } else { + return mode_gravcenter(); + } + } +} +static const char _data_FX_MODE_CHECK_DIE[] PROGMEM = + "DieCheck@!,!,Selected Die,Target Roll;1,2,3;!;1;pal=0,ix=128,m12=2,si=0,c1=255,c2=10"; diff --git a/usermods/pixels_dice_tray/mqtt_client/mqtt_logger.py b/usermods/pixels_dice_tray/mqtt_client/mqtt_logger.py new file mode 100644 index 00000000..a3e4aa01 --- /dev/null +++ b/usermods/pixels_dice_tray/mqtt_client/mqtt_logger.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python +import argparse +import json +import os +from pathlib import Path +import time + +# Dependency installed with `pip install paho-mqtt`. +# https://pypi.org/project/paho-mqtt/ +import paho.mqtt.client as mqtt + +state = {"label": "None"} + + +# Define MQTT callbacks +def on_connect(client, userdata, connect_flags, reason_code, properties): + print("Connected with result code " + str(reason_code)) + state["start_time"] = None + client.subscribe(f"{state['root_topic']}#") + + +def on_message(client, userdata, msg): + if msg.topic.endswith("roll_label"): + state["label"] = msg.payload.decode("ascii") + print(f"Label set to {state['label']}") + elif msg.topic.endswith("roll"): + json_str = msg.payload.decode("ascii") + msg_data = json.loads(json_str) + # Convert the relative timestamps reported to the dice to an approximate absolute time. + # The "last_time" check is to detect if the ESP32 was restarted or the counter rolled over. + if state["start_time"] is None or msg_data["time"] < state["last_time"]: + state["start_time"] = time.time() - (msg_data["time"] / 1000.0) + state["last_time"] = msg_data["time"] + timestamp = state["start_time"] + (msg_data["time"] / 1000.0) + state["csv_fd"].write( + f"{timestamp:.3f}, {msg_data['name']}, {state['label']}, {msg_data['state']}, {msg_data['val']}\n" + ) + state["csv_fd"].flush() + if msg_data["state"] == 1: + print( + f"{timestamp:.3f}: {msg_data['name']} rolled {msg_data['val']}") + + +def main(): + parser = argparse.ArgumentParser( + description="Log die rolls from WLED MQTT events to CSV.") + + # IP address (with a default value) + parser.add_argument( + "--host", + type=str, + default="127.0.0.1", + help="Host address of broker (default: 127.0.0.1)", + ) + parser.add_argument( + "--port", type=int, default=1883, help="Broker TCP port (default: 1883)" + ) + parser.add_argument("--user", type=str, help="Optional MQTT username") + parser.add_argument("--password", type=str, help="Optional MQTT password") + parser.add_argument( + "--topic", + type=str, + help="Optional MQTT topic to listen to. For example if topic is 'wled/e5a658/dice/', subscript to to 'wled/e5a658/dice/#'. By default, listen to all topics looking for ones that end in 'roll_label' and 'roll'.", + ) + parser.add_argument( + "-o", + "--output-dir", + type=Path, + default=Path(__file__).absolute().parent / "logs", + help="Directory to log to", + ) + args = parser.parse_args() + + timestr = time.strftime("%Y-%m-%d") + os.makedirs(args.output_dir, exist_ok=True) + state["csv_fd"] = open(args.output_dir / f"roll_log_{timestr}.csv", "a") + + # Create `an MQTT client + client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) + + # Set MQTT callbacks + client.on_connect = on_connect + client.on_message = on_message + + if args.user and args.password: + client.username_pw_set(args.user, args.password) + + state["root_topic"] = "" + + # Connect to the MQTT broker + client.connect(args.host, args.port, 60) + + try: + while client.loop(timeout=1.0) == mqtt.MQTT_ERR_SUCCESS: + time.sleep(0.1) + except KeyboardInterrupt: + exit(0) + + print("Connection Failure") + exit(1) + + +if __name__ == "__main__": + main() diff --git a/usermods/pixels_dice_tray/mqtt_client/mqtt_plotter.py b/usermods/pixels_dice_tray/mqtt_client/mqtt_plotter.py new file mode 100644 index 00000000..3ce0b7bf --- /dev/null +++ b/usermods/pixels_dice_tray/mqtt_client/mqtt_plotter.py @@ -0,0 +1,69 @@ +import argparse +from http import server +import os +from pathlib import Path +import socketserver + +import pandas as pd +import plotly.express as px + +# python -m http.server 8000 --directory /tmp/ + + +def main(): + parser = argparse.ArgumentParser( + description="Generate an html plot of rolls captured by mqtt_logger.py") + parser.add_argument("input_file", type=Path, help="Log file to plot") + parser.add_argument( + "-s", + "--start-server", + action="store_true", + help="After generating the plot, run a webserver pointing to it", + ) + parser.add_argument( + "-o", + "--output-dir", + type=Path, + default=Path(__file__).absolute().parent / "logs", + help="Directory to log to", + ) + args = parser.parse_args() + + df = pd.read_csv( + args.input_file, names=["timestamp", "die", "label", "state", "roll"] + ) + + df_filt = df[df["state"] == 1] + + time = (df_filt["timestamp"] - df_filt["timestamp"].min()) / 60 / 60 + + fig = px.bar( + df_filt, + x=time, + y="roll", + color="label", + labels={ + "x": "Game Time (min)", + }, + title=f"Roll Report: {args.input_file.name}", + ) + + output_path = args.output_dir / (args.input_file.stem + ".html") + + fig.write_html(output_path) + if args.start_server: + PORT = 8000 + os.chdir(args.output_dir) + try: + with socketserver.TCPServer( + ("", PORT), server.SimpleHTTPRequestHandler + ) as httpd: + print( + f"Serving HTTP on http://0.0.0.0:{PORT}/{output_path.name}") + httpd.serve_forever() + except KeyboardInterrupt: + pass + + +if __name__ == "__main__": + main() diff --git a/usermods/pixels_dice_tray/mqtt_client/requirements.txt b/usermods/pixels_dice_tray/mqtt_client/requirements.txt new file mode 100644 index 00000000..8fb305c7 --- /dev/null +++ b/usermods/pixels_dice_tray/mqtt_client/requirements.txt @@ -0,0 +1,2 @@ +plotly-express +paho-mqtt \ No newline at end of file diff --git a/usermods/pixels_dice_tray/pixels_dice_tray.h b/usermods/pixels_dice_tray/pixels_dice_tray.h new file mode 100644 index 00000000..238af314 --- /dev/null +++ b/usermods/pixels_dice_tray/pixels_dice_tray.h @@ -0,0 +1,535 @@ +#pragma once + +#include // https://github.com/axlan/arduino-pixels-dice +#include "wled.h" + +#include "dice_state.h" +#include "led_effects.h" +#include "tft_menu.h" + +// Set this parameter to rotate the display. 1-3 rotate by 90,180,270 degrees. +#ifndef USERMOD_PIXELS_DICE_TRAY_ROTATION + #define USERMOD_PIXELS_DICE_TRAY_ROTATION 0 +#endif + +// How often we are redrawing screen +#ifndef USERMOD_PIXELS_DICE_TRAY_REFRESH_RATE_MS + #define USERMOD_PIXELS_DICE_TRAY_REFRESH_RATE_MS 200 +#endif + +// Time with no updates before screen turns off (-1 to disable) +#ifndef USERMOD_PIXELS_DICE_TRAY_TIMEOUT_MS + #define USERMOD_PIXELS_DICE_TRAY_TIMEOUT_MS 5 * 60 * 1000 +#endif + +// Duration of each search for BLE devices. +#ifndef BLE_SCAN_DURATION_SEC + #define BLE_SCAN_DURATION_SEC 4 +#endif + +// Time between searches for BLE devices. +#ifndef BLE_TIME_BETWEEN_SCANS_SEC + #define BLE_TIME_BETWEEN_SCANS_SEC 5 +#endif + +#define WLED_DEBOUNCE_THRESHOLD \ + 50 // only consider button input of at least 50ms as valid (debouncing) +#define WLED_LONG_PRESS \ + 600 // long press if button is released after held for at least 600ms +#define WLED_DOUBLE_PRESS \ + 350 // double press if another press within 350ms after a short press + +class PixelsDiceTrayUsermod : public Usermod { + private: + bool enabled = true; + + DiceUpdate dice_update; + + // Settings + uint32_t ble_scan_duration_sec = BLE_SCAN_DURATION_SEC; + unsigned rotation = USERMOD_PIXELS_DICE_TRAY_ROTATION; + DiceSettings dice_settings; + +#if USING_TFT_DISPLAY + MenuController menu_ctrl; +#endif + + static void center(String& line, uint8_t width) { + int len = line.length(); + if (len < width) + for (byte i = (width - len) / 2; i > 0; i--) + line = ' ' + line; + for (byte i = line.length(); i < width; i++) + line += ' '; + } + + // NOTE: THIS MOD DOES NOT SUPPORT CHANGING THE SPI PINS FROM THE UI! The + // TFT_eSPI library requires that they are compiled in. + static void SetSPIPinsFromMacros() { +#if USING_TFT_DISPLAY + spi_mosi = TFT_MOSI; + // Done in TFT library. + if (TFT_MISO == TFT_MOSI) { + spi_miso = -1; + } + spi_sclk = TFT_SCLK; +#endif + } + + void UpdateDieNames( + const std::array& new_die_names) { + for (size_t i = 0; i < MAX_NUM_DICE; i++) { + // If the saved setting was a wildcard, and that connected to a die, use + // the new name instead of the wildcard. Saving this "locks" the name in. + bool overriden_wildcard = + new_die_names[i] == "*" && dice_update.connected_die_ids[i] != 0; + if (!overriden_wildcard && + new_die_names[i] != dice_settings.configured_die_names[i]) { + dice_settings.configured_die_names[i] = new_die_names[i]; + dice_update.connected_die_ids[i] = 0; + last_die_events[i] = pixels::RollEvent(); + } + } + } + + public: + PixelsDiceTrayUsermod() +#if USING_TFT_DISPLAY + : menu_ctrl(&dice_settings) +#endif + { + } + + // Functions called by WLED + + /* + * setup() is called once at boot. WiFi is not yet connected at this point. + * You can use it to initialize variables, sensors or similar. + */ + void setup() override { + DEBUG_PRINTLN(F("DiceTray: init")); +#if USING_TFT_DISPLAY + SetSPIPinsFromMacros(); + PinManagerPinType spiPins[] = { + {spi_mosi, true}, {spi_miso, false}, {spi_sclk, true}}; + if (!pinManager.allocateMultiplePins(spiPins, 3, PinOwner::HW_SPI)) { + enabled = false; + } else { + PinManagerPinType displayPins[] = { + {TFT_CS, true}, {TFT_DC, true}, {TFT_RST, true}, {TFT_BL, true}}; + if (!pinManager.allocateMultiplePins( + displayPins, sizeof(displayPins) / sizeof(PinManagerPinType), + PinOwner::UM_FourLineDisplay)) { + pinManager.deallocateMultiplePins(spiPins, 3, PinOwner::HW_SPI); + enabled = false; + } + } + + if (!enabled) { + DEBUG_PRINTLN(F("DiceTray: TFT Display pin allocations failed.")); + return; + } +#endif + + // Need to enable WiFi sleep: + // "E (1513) wifi:Error! Should enable WiFi modem sleep when both WiFi and Bluetooth are enabled!!!!!!" + noWifiSleep = false; + + // Get the mode indexes that the effects are registered to. + FX_MODE_SIMPLE_D20 = strip.addEffect(255, &simple_roll, _data_FX_MODE_SIMPLE_DIE); + FX_MODE_PULSE_D20 = strip.addEffect(255, &pulse_roll, _data_FX_MODE_PULSE_DIE); + FX_MODE_CHECK_D20 = strip.addEffect(255, &check_roll, _data_FX_MODE_CHECK_DIE); + DIE_LED_MODES = {FX_MODE_SIMPLE_D20, FX_MODE_PULSE_D20, FX_MODE_CHECK_D20}; + + // Start a background task scanning for dice. + // On completion the discovered dice are connected to. + pixels::ScanForDice(ble_scan_duration_sec, BLE_TIME_BETWEEN_SCANS_SEC); + +#if USING_TFT_DISPLAY + menu_ctrl.Init(rotation); +#endif + } + + /* + * connected() is called every time the WiFi is (re)connected + * Use it to initialize network interfaces + */ + void connected() override { + // Serial.println("Connected to WiFi!"); + } + + /* + * loop() is called continuously. Here you can check for events, read sensors, + * etc. + * + * Tips: + * 1. You can use "if (WLED_CONNECTED)" to check for a successful network + * connection. Additionally, "if (WLED_MQTT_CONNECTED)" is available to check + * for a connection to an MQTT broker. + * + * 2. Try to avoid using the delay() function. NEVER use delays longer than 10 + * milliseconds. Instead, use a timer check as shown here. + */ + void loop() override { + static long last_loop_time = 0; + static long last_die_connected_time = millis(); + + char mqtt_topic_buffer[MQTT_MAX_TOPIC_LEN + 16]; + char mqtt_data_buffer[128]; + + // Check if we time interval for redrawing passes. + if (millis() - last_loop_time < USERMOD_PIXELS_DICE_TRAY_REFRESH_RATE_MS) { + return; + } + last_loop_time = millis(); + + // Update dice_list with the connected dice + pixels::ListDice(dice_update.dice_list); + // Get all the roll/battery updates since the last loop + pixels::GetDieRollUpdates(dice_update.roll_updates); + pixels::GetDieBatteryUpdates(dice_update.battery_updates); + + // Go through list of connected die. + // TODO: Blacklist die that are connected to, but don't match the configured + // names. + std::array die_connected = {false, false}; + for (auto die_id : dice_update.dice_list) { + bool matched = false; + // First check if we've already matched this ID to a connected die. + for (size_t i = 0; i < MAX_NUM_DICE; i++) { + if (die_id == dice_update.connected_die_ids[i]) { + die_connected[i] = true; + matched = true; + break; + } + } + + // If this isn't already matched, check if its name matches an expected name. + if (!matched) { + auto die_name = pixels::GetDieDescription(die_id).name; + for (size_t i = 0; i < MAX_NUM_DICE; i++) { + if (0 == dice_update.connected_die_ids[i] && + die_name == dice_settings.configured_die_names[i]) { + dice_update.connected_die_ids[i] = die_id; + die_connected[i] = true; + matched = true; + DEBUG_PRINTF_P(PSTR("DiceTray: %u (%s) connected.\n"), i, + die_name.c_str()); + break; + } + } + + // If it doesn't match any expected names, check if there's any wildcards to match. + if (!matched) { + auto description = pixels::GetDieDescription(die_id); + for (size_t i = 0; i < MAX_NUM_DICE; i++) { + if (dice_settings.configured_die_names[i] == "*") { + dice_update.connected_die_ids[i] = die_id; + die_connected[i] = true; + dice_settings.configured_die_names[i] = die_name; + DEBUG_PRINTF_P(PSTR("DiceTray: %u (%s) connected as wildcard.\n"), + i, die_name.c_str()); + break; + } + } + } + } + } + + // Clear connected die that aren't still present. + bool all_found = true; + bool none_found = true; + for (size_t i = 0; i < MAX_NUM_DICE; i++) { + if (!die_connected[i]) { + if (dice_update.connected_die_ids[i] != 0) { + dice_update.connected_die_ids[i] = 0; + last_die_events[i] = pixels::RollEvent(); + DEBUG_PRINTF_P(PSTR("DiceTray: %u disconnected.\n"), i); + } + + if (!dice_settings.configured_die_names[i].empty()) { + all_found = false; + } + } else { + none_found = false; + } + } + + // Update last_die_events + for (const auto& roll : dice_update.roll_updates) { + for (size_t i = 0; i < MAX_NUM_DICE; i++) { + if (dice_update.connected_die_ids[i] == roll.first) { + last_die_events[i] = roll.second; + } + } + if (WLED_MQTT_CONNECTED) { + snprintf(mqtt_topic_buffer, sizeof(mqtt_topic_buffer), PSTR("%s/%s"), + mqttDeviceTopic, "dice/roll"); + const char* name = pixels::GetDieDescription(roll.first).name.c_str(); + snprintf(mqtt_data_buffer, sizeof(mqtt_data_buffer), + "{\"name\":\"%s\",\"state\":%d,\"val\":%d,\"time\":%d}", name, + int(roll.second.state), roll.second.current_face + 1, + roll.second.timestamp); + mqtt->publish(mqtt_topic_buffer, 0, false, mqtt_data_buffer); + } + } + +#if USERMOD_PIXELS_DICE_TRAY_TIMEOUT_MS > 0 && USING_TFT_DISPLAY + // If at least one die is configured, but none are found + if (none_found) { + if (millis() - last_die_connected_time > + USERMOD_PIXELS_DICE_TRAY_TIMEOUT_MS) { + // Turn off LEDs and backlight and go to sleep. + // Since none of the wake up pins are wired up, expect to sleep + // until power cycle or reset, so don't need to handle normal + // wakeup. + bri = 0; + applyFinalBri(); + menu_ctrl.EnableBacklight(false); + gpio_hold_en((gpio_num_t)TFT_BL); + gpio_deep_sleep_hold_en(); + esp_deep_sleep_start(); + } + } else { + last_die_connected_time = millis(); + } +#endif + + if (pixels::IsScanning() && all_found) { + DEBUG_PRINTF_P(PSTR("DiceTray: All dice found. Stopping search.\n")); + pixels::StopScanning(); + } else if (!pixels::IsScanning() && !all_found) { + DEBUG_PRINTF_P(PSTR("DiceTray: Resuming dice search.\n")); + pixels::ScanForDice(ble_scan_duration_sec, BLE_TIME_BETWEEN_SCANS_SEC); + } +#if USING_TFT_DISPLAY + menu_ctrl.Update(dice_update); +#endif + } + + /* + * addToJsonInfo() can be used to add custom entries to the /json/info part of + * the JSON API. Creating an "u" object allows you to add custom key/value + * pairs to the Info section of the WLED web UI. Below it is shown how this + * could be used for e.g. a light sensor + */ + void addToJsonInfo(JsonObject& root) override { + JsonObject user = root["u"]; + if (user.isNull()) + user = root.createNestedObject("u"); + + JsonArray lightArr = user.createNestedArray("DiceTray"); // name + lightArr.add(enabled ? F("installed") : F("disabled")); // unit + } + + /* + * addToJsonState() can be used to add custom entries to the /json/state part + * of the JSON API (state object). Values in the state object may be modified + * by connected clients + */ + void addToJsonState(JsonObject& root) override { + // root["user0"] = userVar0; + } + + /* + * readFromJsonState() can be used to receive data clients send to the + * /json/state part of the JSON API (state object). Values in the state object + * may be modified by connected clients + */ + void readFromJsonState(JsonObject& root) override { + // userVar0 = root["user0"] | userVar0; //if "user0" key exists in JSON, + // update, else keep old value if (root["bri"] == 255) + // Serial.println(F("Don't burn down your garage!")); + } + + /* + * addToConfig() can be used to add custom persistent settings to the cfg.json + * file in the "um" (usermod) object. It will be called by WLED when settings + * are actually saved (for example, LED settings are saved) If you want to + * force saving the current state, use serializeConfig() in your loop(). + * + * CAUTION: serializeConfig() will initiate a filesystem write operation. + * It might cause the LEDs to stutter and will cause flash wear if called too + * often. Use it sparingly and always in the loop, never in network callbacks! + * + * addToConfig() will also not yet add your setting to one of the settings + * pages automatically. To make that work you still have to add the setting to + * the HTML, xml.cpp and set.cpp manually. + * + * I highly recommend checking out the basics of ArduinoJson serialization and + * deserialization in order to use custom settings! + */ + void addToConfig(JsonObject& root) override { + JsonObject top = root.createNestedObject("DiceTray"); + top["ble_scan_duration"] = ble_scan_duration_sec; + top["die_0"] = dice_settings.configured_die_names[0]; + top["die_1"] = dice_settings.configured_die_names[1]; +#if USING_TFT_DISPLAY + top["rotation"] = rotation; + JsonArray pins = top.createNestedArray("pin"); + pins.add(TFT_CS); + pins.add(TFT_DC); + pins.add(TFT_RST); + pins.add(TFT_BL); +#endif + } + + void appendConfigData() override { + // Slightly annoying that you can't put text before an element. + // The an item on the usermod config page has the following HTML: + // ```html + // Die 0 + // + // + // ``` + // addInfo let's you add data before or after the two input fields. + // + // To work around this, add info text to the end of the preceding item. + // + // See addInfo in wled00/data/settings_um.htm for details on what this function does. + oappend(SET_F( + "addInfo('DiceTray:ble_scan_duration',1,'

Set to \"*\" to " + "connect to any die.
Leave Blank to disable.

Saving will replace \"*\" with die names.','');")); +#if USING_TFT_DISPLAY + oappend(SET_F("ddr=addDropdown('DiceTray','rotation');")); + oappend(SET_F("addOption(ddr,'0 deg',0);")); + oappend(SET_F("addOption(ddr,'90 deg',1);")); + oappend(SET_F("addOption(ddr,'180 deg',2);")); + oappend(SET_F("addOption(ddr,'270 deg',3);")); + oappend(SET_F( + "addInfo('DiceTray:rotation',1,'
DO NOT CHANGE " + "SPI PINS.
CHANGES ARE IGNORED.','');")); + oappend(SET_F("addInfo('TFT:pin[]',0,'','SPI CS');")); + oappend(SET_F("addInfo('TFT:pin[]',1,'','SPI DC');")); + oappend(SET_F("addInfo('TFT:pin[]',2,'','SPI RST');")); + oappend(SET_F("addInfo('TFT:pin[]',3,'','SPI BL');")); +#endif + } + + /* + * readFromConfig() can be used to read back the custom settings you added + * with addToConfig(). This is called by WLED when settings are loaded + * (currently this only happens once immediately after boot) + * + * readFromConfig() is called BEFORE setup(). This means you can use your + * persistent values in setup() (e.g. pin assignments, buffer sizes), but also + * that if you want to write persistent values to a dynamic buffer, you'd need + * to allocate it here instead of in setup. If you don't know what that is, + * don't fret. It most likely doesn't affect your use case :) + */ + bool readFromConfig(JsonObject& root) override { + // we look for JSON object: + // {"DiceTray":{"rotation":0,"font_size":1}} + JsonObject top = root["DiceTray"]; + if (top.isNull()) { + DEBUG_PRINTLN(F("DiceTray: No config found. (Using defaults.)")); + return false; + } + + if (top.containsKey("die_0") && top.containsKey("die_1")) { + const std::array new_die_names{ + top["die_0"], top["die_1"]}; + UpdateDieNames(new_die_names); + } else { + DEBUG_PRINTLN(F("DiceTray: No die names found.")); + } + +#if USING_TFT_DISPLAY + unsigned new_rotation = min(top["rotation"] | rotation, 3u); + + // Restore the SPI pins to their compiled in defaults. + SetSPIPinsFromMacros(); + + if (new_rotation != rotation) { + rotation = new_rotation; + menu_ctrl.Init(rotation); + } + + // Update with any modified settings. + menu_ctrl.Redraw(); +#endif + + // use "return !top["newestParameter"].isNull();" when updating Usermod with + // new features + return !top["DiceTray"].isNull(); + } + + /** + * handleButton() can be used to override default button behaviour. Returning true + * will prevent button working in a default way. + * Replicating button.cpp + */ +#if USING_TFT_DISPLAY + bool handleButton(uint8_t b) override { + if (!enabled || b > 1 // buttons 0,1 only + || buttonType[b] == BTN_TYPE_SWITCH || buttonType[b] == BTN_TYPE_NONE || + buttonType[b] == BTN_TYPE_RESERVED || + buttonType[b] == BTN_TYPE_PIR_SENSOR || + buttonType[b] == BTN_TYPE_ANALOG || + buttonType[b] == BTN_TYPE_ANALOG_INVERTED) { + return false; + } + + unsigned long now = millis(); + static bool buttonPressedBefore[2] = {false}; + static bool buttonLongPressed[2] = {false}; + static unsigned long buttonPressedTime[2] = {0}; + static unsigned long buttonWaitTime[2] = {0}; + + //momentary button logic + if (!buttonLongPressed[b] && isButtonPressed(b)) { //pressed + if (!buttonPressedBefore[b]) { + buttonPressedTime[b] = now; + } + buttonPressedBefore[b] = true; + + if (now - buttonPressedTime[b] > WLED_LONG_PRESS) { //long press + menu_ctrl.HandleButton(ButtonType::LONG, b); + buttonLongPressed[b] = true; + return true; + } + } else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released + + long dur = now - buttonPressedTime[b]; + if (dur < WLED_DEBOUNCE_THRESHOLD) { + buttonPressedBefore[b] = false; + return true; + } //too short "press", debounce + + bool doublePress = buttonWaitTime[b]; //did we have short press before? + buttonWaitTime[b] = 0; + + if (!buttonLongPressed[b]) { //short press + // if this is second release within 350ms it is a double press (buttonWaitTime!=0) + if (doublePress) { + menu_ctrl.HandleButton(ButtonType::DOUBLE, b); + } else { + buttonWaitTime[b] = now; + } + } + buttonPressedBefore[b] = false; + buttonLongPressed[b] = false; + } + // if 350ms elapsed since last press/release it is a short press + if (buttonWaitTime[b] && now - buttonWaitTime[b] > WLED_DOUBLE_PRESS && + !buttonPressedBefore[b]) { + buttonWaitTime[b] = 0; + menu_ctrl.HandleButton(ButtonType::SINGLE, b); + } + + return true; + } +#endif + + /* + * 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_PIXELS_DICE_TRAY; } + + // More methods can be added in the future, this example will then be + // extended. Your usermod will remain compatible as it does not need to + // implement all methods from the Usermod base class! +}; diff --git a/usermods/pixels_dice_tray/platformio_override.ini.sample b/usermods/pixels_dice_tray/platformio_override.ini.sample new file mode 100644 index 00000000..b712f8b2 --- /dev/null +++ b/usermods/pixels_dice_tray/platformio_override.ini.sample @@ -0,0 +1,115 @@ +[platformio] +default_envs = t_qt_pro_8MB_dice, esp32s3dev_8MB_qspi_dice + +# ------------------------------------------------------------------------------ +# T-QT Pro 8MB with integrated 128x128 TFT screen +# ------------------------------------------------------------------------------ +[env:t_qt_pro_8MB_dice] +board = esp32-s3-devkitc-1 ;; generic dev board; +platform = ${esp32s3.platform} +upload_speed = 921600 +build_unflags = ${common.build_unflags} +board_build.partitions = ${esp32.large_partitions} +board_build.f_flash = 80000000L +board_build.flash_mode = qio +monitor_filters = esp32_exception_decoder +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=T-QT-PRO-8MB + -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 + -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") + + -D USERMOD_PIXELS_DICE_TRAY ;; Enables this UserMod + -D USERMOD_PIXELS_DICE_TRAY_BL_ACTIVE_LOW=1 + -D USERMOD_PIXELS_DICE_TRAY_ROTATION=2 + + ;-D WLED_DEBUG + ;;;;;;;;;;;;;;;;;; TFT_eSPI Settings ;;;;;;;;;;;;;;;;;;;;;;;; + ;-DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG + -D USER_SETUP_LOADED=1 + + ; Define the TFT driver, pins etc. from: https://github.com/Bodmer/TFT_eSPI/blob/master/User_Setups/Setup211_LilyGo_T_QT_Pro_S3.h + ; GC9A01 128 x 128 display with no chip select line + -D USER_SETUP_ID=211 + -D GC9A01_DRIVER=1 + -D TFT_WIDTH=128 + -D TFT_HEIGHT=128 + + -D TFT_BACKLIGHT_ON=0 + -D TFT_ROTATION=3 + -D CGRAM_OFFSET=1 + + -D TFT_MISO=-1 + -D TFT_MOSI=2 + -D TFT_SCLK=3 + -D TFT_CS=5 + -D TFT_DC=6 + -D TFT_RST=1 + -D TFT_BL=10 + -D LOAD_GLCD=1 + -D LOAD_FONT2=1 + -D LOAD_FONT4=1 + -D LOAD_FONT6=1 + -D LOAD_FONT7=1 + -D LOAD_FONT8=1 + -D LOAD_GFXFF=1 + ; Avoid SPIFFS dependancy that was causing compile issues. + ;-D SMOOTH_FONT=1 + -D SPI_FREQUENCY=40000000 + -D SPI_READ_FREQUENCY=20000000 + -D SPI_TOUCH_FREQUENCY=2500000 + +lib_deps = ${esp32s3.lib_deps} + ${esp32.AR_lib_deps} + ESP32 BLE Arduino + bodmer/TFT_eSPI @ 2.5.43 + axlan/pixels-dice-interface @ 1.2.0 + +# ------------------------------------------------------------------------------ +# ESP32S3 dev board with 8MB flash and no extended RAM. +# ------------------------------------------------------------------------------ +[env:esp32s3dev_8MB_qspi_dice] +board = esp32-s3-devkitc-1 ;; generic dev board; +platform = ${esp32s3.platform} +upload_speed = 921600 +build_unflags = ${common.build_unflags} +board_build.partitions = ${esp32.large_partitions} +board_build.f_flash = 80000000L +board_build.flash_mode = qio +monitor_filters = esp32_exception_decoder +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_8MB_qspi + -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 + -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") + + -D USERMOD_PIXELS_DICE_TRAY ;; Enables this UserMod + + ;-D WLED_DEBUG +lib_deps = ${esp32s3.lib_deps} + ${esp32.AR_lib_deps} + ESP32 BLE Arduino + axlan/pixels-dice-interface @ 1.2.0 + +# ------------------------------------------------------------------------------ +# ESP32 dev board without screen +# ------------------------------------------------------------------------------ +# THIS DOES NOT WORK!!!!!! +# While it builds and programs onto the device, I ran into a series of issues +# trying to actually run. +# Right after the AP init there's an allocation exception which claims to be in +# the UDP server. There seems to be a ton of heap remaining, so the exact error +# might be a red herring. +# It appears that the BLE scanning task is conflicting with the networking tasks. +# I was successfully running simple applications with the pixels-dice-interface +# on ESP32 dev boards, so it may be an issue with too much being scheduled in +# parallel. Also not clear exactly what difference between the ESP32 and the +# ESP32S3 would be causing this, though they do run different BLE versions. +# May be related to some of the issues discussed in: +# https://github.com/Aircoookie/WLED/issues/1382 +; [env:esp32dev_dice] +; extends = env:esp32dev +; build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=ESP32 +; ; Enable Pixels dice mod +; -D USERMOD_PIXELS_DICE_TRAY +; lib_deps = ${esp32.lib_deps} +; ESP32 BLE Arduino +; axlan/pixels-dice-interface @ 1.2.0 +; ; Tiny file system partition, no core dump to fit BLE library. +; board_build.partitions = usermods/pixels_dice_tray/WLED_ESP32_4MB_64KB_FS.csv diff --git a/usermods/pixels_dice_tray/roll_info.h b/usermods/pixels_dice_tray/roll_info.h new file mode 100644 index 00000000..fd820313 --- /dev/null +++ b/usermods/pixels_dice_tray/roll_info.h @@ -0,0 +1,107 @@ +#pragma once + +#include + +extern TFT_eSPI tft; + +// The following functions are generated by: +// usermods/pixels_dice_tray/generate_roll_info.py + +// GENERATED +static void PrintRoll0() { + tft.setTextColor(63488); + tft.println("Barb Chain"); + tft.setTextColor(65535); + tft.println("Atk/CMD 12"); + tft.println("Range: 70"); + tft.setTextSize(1); + tft.println("Summon 3 chains. Make"); + tft.println("a melee atk 1d6 or a "); + tft.println("trip CMD=AT. On a hit"); + tft.println("make Will save or sha"); + tft.println("ken 1d4 rnds."); +} + +static void PrintRoll1() { + tft.setTextColor(2016); + tft.println("Saves"); + tft.setTextColor(65535); + tft.println("FORT 8"); + tft.println("REFLEX 8"); + tft.println("WILL 9"); +} + +static void PrintRoll2() { + tft.println("Skill"); +} + +static void PrintRoll3() { + tft.println("Attack"); + tft.println("Melee +9"); + tft.println("Range +6"); +} + +static void PrintRoll4() { + tft.println("Cure"); + tft.println("Lit 1d8+5"); + tft.println("Mod 2d8+9"); + tft.println("Ser 3d8+9"); +} + +static void PrintRoll5() { + tft.println("Concentrat"); + tft.println("+15"); + tft.setTextSize(1); + tft.println("Defensive 15+2*SP_LV"); + tft.println("Dmg 10+DMG+SP_LV"); + tft.println("Grapple 10+CMB+SP_LV"); +} + +static const char* GetRollName(uint8_t key) { + switch (key) { + case 0: + return "Barb Chain"; + case 1: + return "Saves"; + case 2: + return "Skill"; + case 3: + return "Attack"; + case 4: + return "Cure"; + case 5: + return "Concentrate"; + } + return ""; +} + +static void PrintRollInfo(uint8_t key) { + tft.setTextColor(TFT_WHITE); + tft.setCursor(0, 0); + tft.setTextSize(2); + switch (key) { + case 0: + PrintRoll0(); + return; + case 1: + PrintRoll1(); + return; + case 2: + PrintRoll2(); + return; + case 3: + PrintRoll3(); + return; + case 4: + PrintRoll4(); + return; + case 5: + PrintRoll5(); + return; + } + tft.setTextColor(TFT_RED); + tft.setCursor(0, 60); + tft.println("Unknown"); +} + +static constexpr size_t NUM_ROLL_INFOS = 6; diff --git a/usermods/pixels_dice_tray/tft_menu.h b/usermods/pixels_dice_tray/tft_menu.h new file mode 100644 index 00000000..0b8fd839 --- /dev/null +++ b/usermods/pixels_dice_tray/tft_menu.h @@ -0,0 +1,479 @@ +/** + * Code for using the 128x128 LCD and two buttons on the T-QT Pro as a GUI. + */ +#pragma once + +#ifndef TFT_WIDTH + #warning TFT parameters not specified, not using screen. +#else + #include + #include // https://github.com/axlan/arduino-pixels-dice + #include "wled.h" + + #include "dice_state.h" + #include "roll_info.h" + + #define USING_TFT_DISPLAY 1 + + #ifndef TFT_BL + #define TFT_BL -1 + #endif + +// Bitmask for icon +const uint8_t LIGHTNING_ICON_8X8[] PROGMEM = { + 0b00001111, 0b00010010, 0b00100100, 0b01001111, + 0b10000001, 0b11110010, 0b00010100, 0b00011000, +}; + +TFT_eSPI tft = TFT_eSPI(TFT_WIDTH, TFT_HEIGHT); + +/** + * Print text with box surrounding it. + * + * @param txt Text to draw + * @param color Color for box lines + */ +static void PrintLnInBox(const char* txt, uint32_t color) { + int16_t sx = tft.getCursorX(); + int16_t sy = tft.getCursorY(); + tft.setCursor(sx + 2, sy); + tft.print(txt); + int16_t w = tft.getCursorX() - sx + 1; + tft.println(); + int16_t h = tft.getCursorY() - sy - 1; + tft.drawRect(sx, sy, w, h, color); +} + +/** + * Override the current colors for the selected segment to the defaults for the + * selected die effect. + */ +void SetDefaultColors(uint8_t mode) { + Segment& seg = strip.getFirstSelectedSeg(); + if (mode == FX_MODE_SIMPLE_D20) { + seg.setColor(0, GREEN); + seg.setColor(1, 0); + } else if (mode == FX_MODE_PULSE_D20) { + seg.setColor(0, GREEN); + seg.setColor(1, RED); + } else if (mode == FX_MODE_CHECK_D20) { + seg.setColor(0, RED); + seg.setColor(1, 0); + seg.setColor(2, GREEN); + } +} + +/** + * Get the pointer to the custom2 value for the current LED segment. This is + * used to set the target roll for relevant effects. + */ +static uint8_t* GetCurrentRollTarget() { + return &strip.getFirstSelectedSeg().custom2; +} + +/** + * Class for drawing a histogram of roll results. + */ +class RollCountWidget { + private: + int16_t xs = 0; + int16_t ys = 0; + uint16_t border_color = TFT_RED; + uint16_t bar_color = TFT_GREEN; + uint16_t bar_width = 6; + uint16_t max_bar_height = 60; + unsigned roll_counts[20] = {0}; + unsigned total = 0; + unsigned max_count = 0; + + public: + RollCountWidget(int16_t xs = 0, int16_t ys = 0, + uint16_t border_color = TFT_RED, + uint16_t bar_color = TFT_GREEN, uint16_t bar_width = 6, + uint16_t max_bar_height = 60) + : xs(xs), + ys(ys), + border_color(border_color), + bar_color(bar_color), + bar_width(bar_width), + max_bar_height(max_bar_height) {} + + void Clear() { + memset(roll_counts, 0, sizeof(roll_counts)); + total = 0; + max_count = 0; + } + + unsigned GetNumRolls() const { return total; } + + void AddRoll(unsigned val) { + if (val > 19) { + return; + } + roll_counts[val]++; + total++; + max_count = max(roll_counts[val], max_count); + } + + void Draw() { + // Add 2 pixels to lengths for boarder width. + tft.drawRect(xs, ys, bar_width * 20 + 2, max_bar_height + 2, border_color); + for (size_t i = 0; i < 20; i++) { + if (roll_counts[i] > 0) { + // Scale bar by highest count. + uint16_t bar_height = round(float(roll_counts[i]) / float(max_count) * + float(max_bar_height)); + // Add space between bars + uint16_t padding = (bar_width > 1) ? 1 : 0; + // Need to start from top of bar and draw down + tft.fillRect(xs + 1 + bar_width * i, + ys + 1 + max_bar_height - bar_height, bar_width - padding, + bar_height, bar_color); + } + } + } +}; + +enum class ButtonType { SINGLE, DOUBLE, LONG }; + +// Base class for different menu pages. +class MenuBase { + public: + /** + * Handle new die events and connections. Called even when menu isn't visible. + */ + virtual void Update(const DiceUpdate& dice_update) = 0; + + /** + * Draw menu to the screen. + */ + virtual void Draw(const DiceUpdate& dice_update, bool force_redraw) = 0; + + /** + * Handle button presses if the menu is currently active. + */ + virtual void HandleButton(ButtonType type, uint8_t b) = 0; + + protected: + static DiceSettings* settings; + friend class MenuController; +}; +DiceSettings* MenuBase::settings = nullptr; + +/** + * Menu to show connection status and roll histograms. + */ +class DiceStatusMenu : public MenuBase { + public: + DiceStatusMenu() + : die_roll_counts{RollCountWidget{0, 20, TFT_BLUE, TFT_GREEN, 6, 40}, + RollCountWidget{0, SECTION_HEIGHT + 20, TFT_BLUE, + TFT_GREEN, 6, 40}} {} + + void Update(const DiceUpdate& dice_update) override { + for (size_t i = 0; i < MAX_NUM_DICE; i++) { + const auto die_id = dice_update.connected_die_ids[i]; + const auto connected = die_id != 0; + // Redraw if connection status changed. + die_updated[i] |= die_id != last_die_ids[i]; + last_die_ids[i] = die_id; + + if (connected) { + bool charging = false; + for (const auto& battery : dice_update.battery_updates) { + if (battery.first == die_id) { + if (die_battery[i].battery_level == INVALID_BATTERY || + battery.second.is_charging != die_battery[i].is_charging) { + die_updated[i] = true; + } + die_battery[i] = battery.second; + } + } + + for (const auto& roll : dice_update.roll_updates) { + if (roll.first == die_id && + roll.second.state == pixels::RollState::ON_FACE) { + die_roll_counts[i].AddRoll(roll.second.current_face); + die_updated[i] = true; + } + } + } + } + } + + void Draw(const DiceUpdate& dice_update, bool force_redraw) override { + // This could probably be optimized for partial redraws. + for (size_t i = 0; i < MAX_NUM_DICE; i++) { + const int16_t ys = SECTION_HEIGHT * i; + const auto die_id = dice_update.connected_die_ids[i]; + const auto connected = die_id != 0; + // Screen updates might be slow, yield in case network task needs to do + // work. + yield(); + bool battery_update = + connected && (millis() - last_update[i] > BATTERY_REFRESH_RATE_MS); + if (force_redraw || die_updated[i] || battery_update) { + last_update[i] = millis(); + tft.fillRect(0, ys, TFT_WIDTH, SECTION_HEIGHT, TFT_BLACK); + tft.drawRect(0, ys, TFT_WIDTH, SECTION_HEIGHT, TFT_BLUE); + if (settings->configured_die_names[i].empty()) { + tft.setTextColor(TFT_RED); + tft.setCursor(2, ys + 4); + tft.setTextSize(2); + tft.println("Connection"); + tft.setCursor(2, tft.getCursorY()); + tft.println("Disabled"); + } else if (!connected) { + tft.setTextColor(TFT_RED); + tft.setCursor(2, ys + 4); + tft.setTextSize(2); + tft.println(settings->configured_die_names[i].c_str()); + tft.setCursor(2, tft.getCursorY()); + tft.print("Waiting..."); + } else { + tft.setTextColor(TFT_WHITE); + tft.setCursor(0, ys + 2); + tft.setTextSize(1); + tft.println(settings->configured_die_names[i].c_str()); + tft.print("Cnt "); + tft.print(die_roll_counts[i].GetNumRolls()); + if (die_battery[i].battery_level != INVALID_BATTERY) { + tft.print(" Bat "); + tft.print(die_battery[i].battery_level); + tft.print("%"); + if (die_battery[i].is_charging) { + tft.drawBitmap(tft.getCursorX(), tft.getCursorY(), + LIGHTNING_ICON_8X8, 8, 8, TFT_YELLOW); + } + } + die_roll_counts[i].Draw(); + } + die_updated[i] = false; + } + } + } + + void HandleButton(ButtonType type, uint8_t b) override { + if (type == ButtonType::LONG) { + for (size_t i = 0; i < MAX_NUM_DICE; i++) { + die_roll_counts[i].Clear(); + die_updated[i] = true; + } + } + }; + + private: + static constexpr long BATTERY_REFRESH_RATE_MS = 60 * 1000; + static constexpr int16_t SECTION_HEIGHT = TFT_HEIGHT / MAX_NUM_DICE; + static constexpr uint8_t INVALID_BATTERY = 0xFF; + std::array last_update{0, 0}; + std::array last_die_ids{0, 0}; + std::array die_updated{false, false}; + std::array die_battery = { + pixels::BatteryEvent{INVALID_BATTERY, false}, + pixels::BatteryEvent{INVALID_BATTERY, false}}; + std::array die_roll_counts; +}; + +/** + * Some limited controls for setting the die effects on the current LED + * segment. + */ +class EffectMenu : public MenuBase { + public: + EffectMenu() = default; + + void Update(const DiceUpdate& dice_update) override {} + + void Draw(const DiceUpdate& dice_update, bool force_redraw) override { + // NOTE: This doesn't update automatically if the effect is updated on the + // web UI and vice-versa. + if (force_redraw) { + tft.fillScreen(TFT_BLACK); + uint8_t mode = strip.getFirstSelectedSeg().mode; + if (Contains(DIE_LED_MODES, mode)) { + char lineBuffer[CHAR_WIDTH_BIG + 1]; + extractModeName(mode, JSON_mode_names, lineBuffer, CHAR_WIDTH_BIG); + tft.setTextColor(TFT_WHITE); + tft.setCursor(0, 0); + tft.setTextSize(2); + PrintLnInBox(lineBuffer, (field_idx == 0) ? TFT_BLUE : TFT_BLACK); + if (mode == FX_MODE_CHECK_D20) { + snprintf(lineBuffer, sizeof(lineBuffer), "PASS: %u", + *GetCurrentRollTarget()); + PrintLnInBox(lineBuffer, (field_idx == 1) ? TFT_BLUE : TFT_BLACK); + } + } else { + char lineBuffer[CHAR_WIDTH_SMALL + 1]; + extractModeName(mode, JSON_mode_names, lineBuffer, CHAR_WIDTH_SMALL); + tft.setTextColor(TFT_WHITE); + tft.setCursor(0, 0); + tft.setTextSize(1); + tft.println(lineBuffer); + } + } + } + + /** + * Button 0 navigates up and down the settings for the effect. + * Button 1 changes the value for the selected settings. + * Long pressing a button resets the effect parameters to their defaults for + * the current die effect. + */ + void HandleButton(ButtonType type, uint8_t b) override { + Segment& seg = strip.getFirstSelectedSeg(); + auto mode_itr = + std::find(DIE_LED_MODES.begin(), DIE_LED_MODES.end(), seg.mode); + if (mode_itr != DIE_LED_MODES.end()) { + mode_idx = mode_itr - DIE_LED_MODES.begin(); + } + + if (mode_itr == DIE_LED_MODES.end()) { + seg.setMode(DIE_LED_MODES[mode_idx]); + } else { + if (type == ButtonType::LONG) { + // Need to set mode to different value so defaults are actually loaded. + seg.setMode(0); + seg.setMode(DIE_LED_MODES[mode_idx], true); + SetDefaultColors(DIE_LED_MODES[mode_idx]); + } else if (b == 0) { + field_idx = (field_idx + 1) % DIE_LED_MODE_NUM_FIELDS[mode_idx]; + } else { + if (field_idx == 0) { + mode_idx = (mode_idx + 1) % DIE_LED_MODES.size(); + seg.setMode(DIE_LED_MODES[mode_idx]); + } else if (DIE_LED_MODES[mode_idx] == FX_MODE_CHECK_D20 && + field_idx == 1) { + *GetCurrentRollTarget() = GetLastRoll().current_face + 1; + } + } + } + }; + + private: + static constexpr std::array DIE_LED_MODE_NUM_FIELDS = {1, 1, 2}; + static constexpr size_t CHAR_WIDTH_BIG = 10; + static constexpr size_t CHAR_WIDTH_SMALL = 21; + size_t mode_idx = 0; + size_t field_idx = 0; +}; + +constexpr std::array EffectMenu::DIE_LED_MODE_NUM_FIELDS; + +/** + * Menu for setting the roll label and some info for that roll type. + */ +class InfoMenu : public MenuBase { + public: + InfoMenu() = default; + + void Update(const DiceUpdate& dice_update) override {} + + void Draw(const DiceUpdate& dice_update, bool force_redraw) override { + if (force_redraw) { + tft.fillScreen(TFT_BLACK); + if (settings->roll_label != INVALID_ROLL_VALUE) { + PrintRollInfo(settings->roll_label); + } else { + tft.setTextColor(TFT_RED); + tft.setCursor(0, 60); + tft.setTextSize(2); + tft.println("Set Roll"); + } + } + } + + /** + * Single clicking navigates through the roll types. Button 0 goes down, and + * button 1 goes up with wrapping. + */ + void HandleButton(ButtonType type, uint8_t b) override { + if (settings->roll_label >= NUM_ROLL_INFOS) { + settings->roll_label = 0; + } else if (b == 0) { + settings->roll_label = (settings->roll_label == 0) + ? NUM_ROLL_INFOS - 1 + : settings->roll_label - 1; + } else if (b == 1) { + settings->roll_label = (settings->roll_label + 1) % NUM_ROLL_INFOS; + } + if (WLED_MQTT_CONNECTED) { + char mqtt_topic_buffer[MQTT_MAX_TOPIC_LEN + 16]; + snprintf(mqtt_topic_buffer, sizeof(mqtt_topic_buffer), PSTR("%s/%s"), + mqttDeviceTopic, "dice/settings->roll_label"); + mqtt->publish(mqtt_topic_buffer, 0, false, + GetRollName(settings->roll_label)); + } + }; +}; + +/** + * Interface for the rest of the app to update the menus. + */ +class MenuController { + public: + MenuController(DiceSettings* settings) { MenuBase::settings = settings; } + + void Init(unsigned rotation) { + tft.init(); + tft.setRotation(rotation); + tft.fillScreen(TFT_BLACK); + tft.setTextColor(TFT_RED); + tft.setCursor(0, 60); + tft.setTextDatum(MC_DATUM); + tft.setTextSize(2); + EnableBacklight(true); + + force_redraw = true; + } + + // Set the pin to turn the backlight on or off if available. + static void EnableBacklight(bool enable) { + #if TFT_BL > 0 + #if USERMOD_PIXELS_DICE_TRAY_BL_ACTIVE_LOW + enable = !enable; + #endif + digitalWrite(TFT_BL, enable); + #endif + } + + /** + * Double clicking navigates between menus. Button 0 goes down, and button 1 + * goes up with wrapping. + */ + void HandleButton(ButtonType type, uint8_t b) { + force_redraw = true; + // Switch menus with double click + if (ButtonType::DOUBLE == type) { + if (b == 0) { + current_index = + (current_index == 0) ? menu_ptrs.size() - 1 : current_index - 1; + } else { + current_index = (current_index + 1) % menu_ptrs.size(); + } + } else { + menu_ptrs[current_index]->HandleButton(type, b); + } + } + + void Update(const DiceUpdate& dice_update) { + for (auto menu_ptr : menu_ptrs) { + menu_ptr->Update(dice_update); + } + menu_ptrs[current_index]->Draw(dice_update, force_redraw); + force_redraw = false; + } + + void Redraw() { force_redraw = true; } + + private: + size_t current_index = 0; + bool force_redraw = true; + + DiceStatusMenu status_menu; + EffectMenu effect_menu; + InfoMenu info_menu; + const std::array menu_ptrs = {&status_menu, &effect_menu, + &info_menu}; +}; +#endif diff --git a/usermods/pov_display/usermod_pov_display.h b/usermods/pov_display/usermod_pov_display.h new file mode 100644 index 00000000..b1fc0dba --- /dev/null +++ b/usermods/pov_display/usermod_pov_display.h @@ -0,0 +1,85 @@ +#pragma once +#include "wled.h" +#include + +void * openFile(const char *filename, int32_t *size) { + f = WLED_FS.open(filename); + *size = f.size(); + return &f; +} + +void closeFile(void *handle) { + if (f) f.close(); +} + +int32_t readFile(PNGFILE *pFile, uint8_t *pBuf, int32_t iLen) +{ + int32_t iBytesRead; + iBytesRead = iLen; + File *f = static_cast(pFile->fHandle); + // Note: If you read a file all the way to the last byte, seek() stops working + if ((pFile->iSize - pFile->iPos) < iLen) + iBytesRead = pFile->iSize - pFile->iPos - 1; // <-- ugly work-around + if (iBytesRead <= 0) + return 0; + iBytesRead = (int32_t)f->read(pBuf, iBytesRead); + pFile->iPos = f->position(); + return iBytesRead; +} + +int32_t seekFile(PNGFILE *pFile, int32_t iPosition) +{ + int i = micros(); + File *f = static_cast(pFile->fHandle); + f->seek(iPosition); + pFile->iPos = (int32_t)f->position(); + i = micros() - i; + return pFile->iPos; +} + +void draw(PNGDRAW *pDraw) { + uint16_t usPixels[SEGLEN]; + png.getLineAsRGB565(pDraw, usPixels, PNG_RGB565_LITTLE_ENDIAN, 0xffffffff); + for(int x=0; x < SEGLEN; x++) { + uint16_t color = usPixels[x]; + byte r = ((color >> 11) & 0x1F); + byte g = ((color >> 5) & 0x3F); + byte b = (color & 0x1F); + SEGMENT.setPixelColor(x, RGBW32(r,g,b,0)); + } + strip.show(); +} + +uint16_t mode_pov_image(void) { + const char * filepath = SEGMENT.name; + int rc = png.open(filepath, openFile, closeFile, readFile, seekFile, draw); + if (rc == PNG_SUCCESS) { + rc = png.decode(NULL, 0); + png.close(); + return FRAMETIME; + } + return FRAMETIME; +} + +class PovDisplayUsermod : public Usermod +{ + public: + static const char _data_FX_MODE_POV_IMAGE[] PROGMEM = "POV Image@!;;;1"; + + PNG png; + File f; + + void setup() { + strip.addEffect(255, &mode_pov_image, _data_FX_MODE_POV_IMAGE); + } + + void loop() { + } + + uint16_t getId() + { + return USERMOD_ID_POV_DISPLAY; + } + + void connected() {} +}; diff --git a/usermods/smartnest/readme.md b/usermods/smartnest/readme.md index 5c3ef807..62bfcdad 100644 --- a/usermods/smartnest/readme.md +++ b/usermods/smartnest/readme.md @@ -1,61 +1,41 @@ # Smartnest -Enables integration with `smartnest.cz` service which provides MQTT integration with voice assistants. +Enables integration with `smartnest.cz` service which provides MQTT integration with voice assistants, for example Google Home, Alexa, Siri, Home Assistant and more! + In order to setup Smartnest follow the [documentation](https://www.docu.smartnest.cz/). + - You can create up to 5 different devices + - To add the project to Google Home you can find the information [here](https://www.docu.smartnest.cz/google-home-integration) + - To add the project to Alexa you can find the information [here](https://www.docu.smartnest.cz/alexa-integration) ## MQTT API The API is described in the Smartnest [Github repo](https://github.com/aososam/Smartnest/blob/master/Devices/lightRgb/lightRgb.ino). - ## Usermod installation -1. Register the usermod by adding `#include "../usermods/smartnest/usermod_smartnest.h"` at the top and `usermods.add(new Smartnest());` at the bottom of `usermods_list.cpp`. -or -2. Use `#define USERMOD_SMARTNEST` in wled.h or `-D USERMOD_SMARTNEST` in your platformio.ini - - -Example **usermods_list.cpp**: - -```cpp -#include "wled.h" -/* - * Register your v2 usermods here! - * (for v1 usermods using just usermod.cpp, you can ignore this file) - */ - -/* - * Add/uncomment your usermod filename here (and once more below) - * || || || - * \/ \/ \/ - */ -//#include "usermod_v2_example.h" -//#include "usermod_temperature.h" -#include "../usermods/usermod_smartnest.h" - -void registerUsermods() -{ - /* - * Add your usermod class name here - * || || || - * \/ \/ \/ - */ - //usermods.add(new MyExampleUsermod()); - //usermods.add(new UsermodTemperature()); - usermods.add(new Smartnest()); - -} -``` +1. Use `#define USERMOD_SMARTNEST` in wled.h or `-D USERMOD_SMARTNEST` in your platformio.ini (recommended). ## Configuration Usermod has no configuration, but it relies on the MQTT configuration.\ Under Config > Sync Interfaces > MQTT: -* Enable MQTT check box -* Set the `Broker` field to: `smartnest.cz` -* The `Username` and `Password` fields are the login information from the `smartnest.cz` website. + +* Enable `MQTT` check box. +* Set the `Broker` field to: `smartnest.cz` or `3.122.209.170`(both work). +* Set the `Port` field to: `1883` +* The `Username` and `Password` fields are the login information from the `smartnest.cz` website (It is located above in the 3 points). * `Client ID` field is obtained from the device configuration panel in `smartnest.cz`. +* `Device Topic` is obtained by entering the ClientID/report , remember to replace ClientId with your real information (Because they can ban your device). +* `Group Topic` keep the same Group Topic. + +Wait `1 minute` after turning it on, as it usually takes a while. ## Change log + 2022-09 -* First implementation. + * First implementation. + +2024-05 + * Solved code. + * Updated documentation. + * Second implementation. diff --git a/usermods/smartnest/usermod_smartnest.h b/usermods/smartnest/usermod_smartnest.h index 8d2b04ff..92d524c8 100644 --- a/usermods/smartnest/usermod_smartnest.h +++ b/usermods/smartnest/usermod_smartnest.h @@ -9,6 +9,10 @@ class Smartnest : public Usermod { private: + bool initialized = false; + unsigned long lastMqttReport = 0; + unsigned long mqttReportInterval = 60000; // Report every minute + void sendToBroker(const char *const topic, const char *const message) { if (!WLED_MQTT_CONNECTED) @@ -61,7 +65,7 @@ private: int position = 0; // We need to copy the string in order to keep it read only as strtok_r function requires mutable string - color_ = (char *)malloc(strlen(color)); + color_ = (char *)malloc(strlen(color) + 1); if (NULL == color_) { return -1; } @@ -150,7 +154,7 @@ public: delay(100); sendToBroker("report/firmware", versionString); // Reports the firmware version delay(100); - sendToBroker("report/ip", (char *)WiFi.localIP().toString().c_str()); // Reports the ip + sendToBroker("report/ip", (char *)WiFi.localIP().toString().c_str()); // Reports the IP delay(100); sendToBroker("report/network", (char *)WiFi.SSID().c_str()); // Reports the network name delay(100); @@ -168,4 +172,34 @@ public: { return USERMOD_ID_SMARTNEST; } + + /** + * setup() is called once at startup to initialize the usermod. + */ + void setup() { + DEBUG_PRINTF("Smartnest usermod setup initializing..."); + + // Publish initial status + sendToBroker("report/status", "Smartnest usermod initialized"); + } + + /** + * loop() is called continuously to keep the usermod running. + */ + void loop() { + // Periodically report status to MQTT broker + unsigned long currentMillis = millis(); + if (currentMillis - lastMqttReport >= mqttReportInterval) { + lastMqttReport = currentMillis; + + // Report current brightness + char brightnessMsg[11]; + sprintf(brightnessMsg, "%u", bri); + sendToBroker("report/brightness", brightnessMsg); + + // Report current signal strength + String signal(WiFi.RSSI(), 10); + sendToBroker("report/signal", signal.c_str()); + } + } }; diff --git a/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h index 2a63dd4d..52ff3cc1 100644 --- a/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h +++ b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h @@ -122,9 +122,9 @@ class AutoSaveUsermod : public Usermod { * Da loop. */ void loop() { - if (!autoSaveAfterSec || !enabled || strip.isUpdating() || currentPreset>0) return; // setting 0 as autosave seconds disables autosave - + static unsigned long lastRun = 0; unsigned long now = millis(); + if (!autoSaveAfterSec || !enabled || currentPreset>0 || (strip.isUpdating() && now - lastRun < 240)) return; // setting 0 as autosave seconds disables autosave uint8_t currentMode = strip.getMainSegment().mode; uint8_t currentPalette = strip.getMainSegment().palette; diff --git a/usermods/usermod_v2_four_line_display_ALT/readme.md b/usermods/usermod_v2_four_line_display_ALT/readme.md index ea9f4361..a8f386da 100644 --- a/usermods/usermod_v2_four_line_display_ALT/readme.md +++ b/usermods/usermod_v2_four_line_display_ALT/readme.md @@ -1,4 +1,4 @@ -# I2C 4 Line Display Usermod ALT +# I2C/SPI 4 Line Display Usermod ALT Thank you to the authors of the original version of these usermods. It would not have been possible without them! "usermod_v2_four_line_display" @@ -8,21 +8,20 @@ The core of these usermods are a copy of the originals. The main changes are to The display usermod UI has been completely changed. -The changes made to the RotaryEncoder usermod were made to support the new UI in the display usermod. -Without the display it, functions identical to the original. +The changes made to the RotaryEncoder usermod were made to support the new UI in the display usermod. +Without the display, it functions identical to the original. The original "usermod_v2_auto_save" will not work with the display just yet. Press the encoder to cycle through the options: - *Brightness - *Speed - *Intensity - *Palette - *Effect - *Main Color (only if display is used) - *Saturation (only if display is used) +* Brightness +* Speed +* Intensity +* Palette +* Effect +* Main Color (only if display is used) +* Saturation (only if display is used) -Press and hold the encoder to display Network Info - if AP is active, it will display AP, SSID and password +Press and hold the encoder to display Network Info. If AP is active, it will display AP, SSID and password Also shows if the timer is enabled @@ -30,11 +29,47 @@ Also shows if the timer is enabled ## Installation -Please refer to the original `usermod_v2_rotary_encoder_ui` readme for the main instructions -Then to activate this alternative usermod add `#define USE_ALT_DISPlAY` to the `usermods_list.cpp` file, +Please refer to the original `usermod_v2_rotary_encoder_ui` readme for the main instructions. + +Copy the example `platformio_override.sample.ini` from the usermod_v2_rotary_encoder_ui_ALT folder to the root directory of your particular build and rename it to `platformio_override.ini`. + +This file should be placed in the same directory as `platformio.ini`. + +Then, to activate this alternative usermod, add `#define USE_ALT_DISPlAY` (NOTE: CASE SENSITIVE) to the `usermods_list.cpp` file, or add `-D USE_ALT_DISPlAY` to the original `platformio_override.ini.sample` file +## Configuration + +These options are configurable in Config > Usermods + +### Usermod Setup + +* Global I2C GPIOs (HW) - Set the SDA and SCL pins + +### 4LineDisplay + +* `enabled` - enable/disable usermod +* `type` - display type in numeric format + * 1 = I2C SSD1306 128x32 + * 2 = I2C SH1106 128x32 + * 3 = I2C SSD1306 128x64 (4 double-height lines) + * 4 = I2C SSD1305 128x32 + * 5 = I2C SSD1305 128x64 (4 double-height lines) + * 6 = SPI SSD1306 128x32 + * 7 = SPI SSD1306 128x64 (4 double-height lines) + * 8 = SPI SSD1309 128x64 (4 double-height lines) + * 9 = I2C SSD1309 128x64 (4 double-height lines) +* `pin` - GPIO pins used for display; SPI displays can use SCK, MOSI, CS, DC & RST +* `flip` - flip/rotate display 180° +* `contrast` - set display contrast (higher contrast may reduce display lifetime) +* `screenTimeOutSec` - screen saver time-out in seconds +* `sleepMode` - enable/disable screen saver +* `clockMode` - enable/disable clock display in screen saver mode +* `showSeconds` - Show seconds on the clock display +* `i2c-freq-kHz` - I2C clock frequency in kHz (may help reduce dropped frames, range: 400-3400) + + ### PlatformIO requirements Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. diff --git a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h index 82a5e1a8..008647fa 100644 --- a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h +++ b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h @@ -17,7 +17,7 @@ // for WLED. // // Dependencies -// * This Usermod works best, by far, when coupled +// * This Usermod works best, by far, when coupled // with RotaryEncoderUI ALT Usermod. // // Make sure to enable NTP and set your time zone in WLED Config | Time. @@ -89,7 +89,8 @@ typedef enum { SSD1305_64, // U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C SSD1306_SPI, // U8X8_SSD1306_128X32_NONAME_HW_SPI SSD1306_SPI64, // U8X8_SSD1306_128X64_NONAME_HW_SPI - SSD1309_SPI64 // U8X8_SSD1309_128X64_NONAME0_4W_HW_SPI + SSD1309_SPI64, // U8X8_SSD1309_128X64_NONAME0_4W_HW_SPI + SSD1309_64 // U8X8_SSD1309_128X64_NONAME0_HW_I2C } DisplayType; @@ -235,7 +236,7 @@ class FourLineDisplayUsermod : public Usermod { void updateSpeed(); void updateIntensity(); void drawStatusIcons(); - + /** * marks the position of the arrow showing * the current setting being changed @@ -246,8 +247,8 @@ class FourLineDisplayUsermod : public Usermod { //Draw the arrow for the current setting being changed void drawArrow(); - //Display the current effect or palette (desiredEntry) - // on the appropriate line (row). + //Display the current effect or palette (desiredEntry) + // on the appropriate line (row). void showCurrentEffectOrPalette(int inputEffPal, const char *qstring, uint8_t row); /** @@ -314,14 +315,14 @@ class FourLineDisplayUsermod : public Usermod { * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. * It will be called by WLED when settings are actually saved (for example, LED settings are saved) * If you want to force saving the current state, use serializeConfig() in your loop(). - * + * * CAUTION: serializeConfig() will initiate a filesystem write operation. * It might cause the LEDs to stutter and will cause flash wear if called too often. * Use it sparingly and always in the loop, never in network callbacks! - * + * * addToConfig() will also not yet add your setting to one of the settings pages automatically. * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually. - * + * * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! */ void addToConfig(JsonObject& root) override; @@ -329,7 +330,7 @@ class FourLineDisplayUsermod : public Usermod { /* * readFromConfig() can be used to read back the custom settings you added with addToConfig(). * This is called by WLED when settings are loaded (currently this only happens once immediately after boot) - * + * * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) @@ -444,8 +445,8 @@ void FourLineDisplayUsermod::setPowerSave(uint8_t save) { void FourLineDisplayUsermod::center(String &line, uint8_t width) { int len = line.length(); - if (len0; i--) line = ' ' + line; - for (byte i=line.length(); i0; i--) line = ' ' + line; + for (unsigned i=line.length(); i127)) { // remove note symbol from effect names - for (byte i=5; i<=printedChars; i++) lineBuffer[i-5] = lineBuffer[i]; //include '\0' + for (unsigned i=5; i<=printedChars; i++) lineBuffer[i-5] = lineBuffer[i]; //include '\0' printedChars -= 5; } if (lineHeight == 2) { // use this code for 8 line display char smallBuffer1[MAX_MODE_LINE_SPACE]; char smallBuffer2[MAX_MODE_LINE_SPACE]; - uint8_t smallChars1 = 0; - uint8_t smallChars2 = 0; + unsigned smallChars1 = 0; + unsigned smallChars2 = 0; if (printedChars < MAX_MODE_LINE_SPACE) { // use big font if the text fits while (printedChars < (MAX_MODE_LINE_SPACE-1)) lineBuffer[printedChars++]=' '; lineBuffer[printedChars] = 0; drawString(1, row*lineHeight, lineBuffer); } else { // for long names divide the text into 2 lines and print them small bool spaceHit = false; - for (uint8_t i = 0; i < printedChars; i++) { + for (unsigned i = 0; i < printedChars; i++) { switch (lineBuffer[i]) { case ' ': if (i > 4 && !spaceHit) { @@ -857,14 +859,14 @@ void FourLineDisplayUsermod::showCurrentEffectOrPalette(int inputEffPal, const c while (smallChars1 < (MAX_MODE_LINE_SPACE-1)) smallBuffer1[smallChars1++]=' '; smallBuffer1[smallChars1] = 0; drawString(1, row*lineHeight, smallBuffer1, true); - while (smallChars2 < (MAX_MODE_LINE_SPACE-1)) smallBuffer2[smallChars2++]=' '; + while (smallChars2 < (MAX_MODE_LINE_SPACE-1)) smallBuffer2[smallChars2++]=' '; smallBuffer2[smallChars2] = 0; drawString(1, row*lineHeight+1, smallBuffer2, true); } } else { // use this code for 4 ling displays char smallBuffer3[MAX_MODE_LINE_SPACE+1]; // uses 1x1 icon for mode/palette - uint8_t smallChars3 = 0; - for (uint8_t i = 0; i < MAX_MODE_LINE_SPACE; i++) smallBuffer3[smallChars3++] = (i >= printedChars) ? ' ' : lineBuffer[i]; + unsigned smallChars3 = 0; + for (unsigned i = 0; i < MAX_MODE_LINE_SPACE; i++) smallBuffer3[smallChars3++] = (i >= printedChars) ? ' ' : lineBuffer[i]; smallBuffer3[smallChars3] = 0; drawString(1, row*lineHeight, smallBuffer3, true); } @@ -1133,10 +1135,12 @@ bool FourLineDisplayUsermod::handleButton(uint8_t b) { return handled; } -#if CONFIG_FREERTOS_UNICORE -#define ARDUINO_RUNNING_CORE 0 -#else -#define ARDUINO_RUNNING_CORE 1 +#ifndef ARDUINO_RUNNING_CORE + #if CONFIG_FREERTOS_UNICORE + #define ARDUINO_RUNNING_CORE 0 + #else + #define ARDUINO_RUNNING_CORE 1 + #endif #endif void FourLineDisplayUsermod::onUpdateBegin(bool init) { #if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) @@ -1150,7 +1154,7 @@ void FourLineDisplayUsermod::onUpdateBegin(bool init) { xTaskCreatePinnedToCore( [](void * par) { // Function to implement the task // see https://www.freertos.org/vtaskdelayuntil.html - const TickType_t xFrequency = REFRESH_RATE_MS * portTICK_PERIOD_MS / 2; + const TickType_t xFrequency = REFRESH_RATE_MS * portTICK_PERIOD_MS / 2; TickType_t xLastWakeTime = xTaskGetTickCount(); for(;;) { delay(1); // DO NOT DELETE THIS LINE! It is needed to give the IDLE(0) task enough time and to keep the watchdog happy. @@ -1205,6 +1209,7 @@ void FourLineDisplayUsermod::appendConfigData() { oappend(SET_F("addOption(dd,'SSD1306 128x64',3);")); oappend(SET_F("addOption(dd,'SSD1305',4);")); oappend(SET_F("addOption(dd,'SSD1305 128x64',5);")); + oappend(SET_F("addOption(dd,'SSD1309 128x64',9);")); oappend(SET_F("addOption(dd,'SSD1306 SPI',6);")); oappend(SET_F("addOption(dd,'SSD1306 SPI 128x64',7);")); oappend(SET_F("addOption(dd,'SSD1309 SPI 128x64',8);")); @@ -1218,14 +1223,14 @@ void FourLineDisplayUsermod::appendConfigData() { * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. * It will be called by WLED when settings are actually saved (for example, LED settings are saved) * If you want to force saving the current state, use serializeConfig() in your loop(). - * + * * CAUTION: serializeConfig() will initiate a filesystem write operation. * It might cause the LEDs to stutter and will cause flash wear if called too often. * Use it sparingly and always in the loop, never in network callbacks! - * + * * addToConfig() will also not yet add your setting to one of the settings pages automatically. * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually. - * + * * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! */ void FourLineDisplayUsermod::addToConfig(JsonObject& root) { @@ -1252,7 +1257,7 @@ void FourLineDisplayUsermod::addToConfig(JsonObject& root) { /* * readFromConfig() can be used to read back the custom settings you added with addToConfig(). * This is called by WLED when settings are loaded (currently this only happens once immediately after boot) - * + * * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) @@ -1260,7 +1265,7 @@ void FourLineDisplayUsermod::addToConfig(JsonObject& root) { bool FourLineDisplayUsermod::readFromConfig(JsonObject& root) { bool needsRedraw = false; DisplayType newType = type; - int8_t oldPin[3]; for (byte i=0; i<3; i++) oldPin[i] = ioPin[i]; + int8_t oldPin[3]; for (unsigned i=0; i<3; i++) oldPin[i] = ioPin[i]; JsonObject top = root[FPSTR(_name)]; if (top.isNull()) { @@ -1271,7 +1276,7 @@ bool FourLineDisplayUsermod::readFromConfig(JsonObject& root) { enabled = top[FPSTR(_enabled)] | enabled; newType = top["type"] | newType; - for (byte i=0; i<3; i++) ioPin[i] = top["pin"][i] | ioPin[i]; + for (unsigned i=0; i<3; i++) ioPin[i] = top["pin"][i] | ioPin[i]; flip = top[FPSTR(_flip)] | flip; contrast = top[FPSTR(_contrast)] | contrast; #ifndef ARDUINO_ARCH_ESP32 @@ -1297,7 +1302,7 @@ bool FourLineDisplayUsermod::readFromConfig(JsonObject& root) { DEBUG_PRINTLN(F(" config (re)loaded.")); // changing parameters from settings page bool pinsChanged = false; - for (byte i=0; i<3; i++) if (ioPin[i] != oldPin[i]) { pinsChanged = true; break; } + for (unsigned i=0; i<3; i++) if (ioPin[i] != oldPin[i]) { pinsChanged = true; break; } if (pinsChanged || type!=newType) { bool isSPI = (type == SSD1306_SPI || type == SSD1306_SPI64 || type == SSD1309_SPI64); bool newSPI = (newType == SSD1306_SPI || newType == SSD1306_SPI64 || newType == SSD1309_SPI64); @@ -1346,6 +1351,10 @@ bool FourLineDisplayUsermod::readFromConfig(JsonObject& root) { u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1305_128x64_adafruit, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino); u8x8_SetPin_HW_I2C(u8x8->getU8x8(), U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE); break; + case SSD1309_64: + u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1309_128x64_noname0, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino); + u8x8_SetPin_HW_I2C(u8x8->getU8x8(), U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE); + break; case SSD1306_SPI: u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1306_128x32_univision, u8x8_cad_001, u8x8_byte_arduino_hw_spi, u8x8_gpio_and_delay_arduino); u8x8_SetPin_4Wire_HW_SPI(u8x8->getU8x8(), ioPin[0], ioPin[1], ioPin[2]); // Pins are cs, dc, reset diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio–override.sample.ini b/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio–override.sample.ini new file mode 100644 index 00000000..6b32c71f --- /dev/null +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio–override.sample.ini @@ -0,0 +1,17 @@ +[platformio] +default_envs = esp32dev + +[env:esp32dev] +board = esp32dev +platform = ${esp32.platform} +build_unflags = ${common.build_unflags} +build_flags = + ${common.build_flags_esp32} + -D USERMOD_FOUR_LINE_DISPLAY -D USE_ALT_DISPlAY + -D USERMOD_ROTARY_ENCODER_UI -D ENCODER_DT_PIN=18 -D ENCODER_CLK_PIN=5 -D ENCODER_SW_PIN=19 +upload_speed = 460800 +lib_deps = + ${esp32.lib_deps} + U8g2@~2.34.4 + Wire + diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md b/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md index 51636238..10db879f 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md @@ -8,18 +8,18 @@ The core of these usermods are a copy of the originals. The main changes are to The display usermod UI has been completely changed. -The changes made to the RotaryEncoder usermod were made to support the new UI in the display usermod. +The changes made to the RotaryEncoder usermod were made to support the new UI in the display usermod. Without the display, it functions identical to the original. The original "usermod_v2_auto_save" will not work with the display just yet. Press the encoder to cycle through the options: - *Brightness - *Speed - *Intensity - *Palette - *Effect - *Main Color (only if display is used) - *Saturation (only if display is used) +* Brightness +* Speed +* Intensity +* Palette +* Effect +* Main Color (only if display is used) +* Saturation (only if display is used) Press and hold the encoder to display Network Info if AP is active, it will display the AP, SSID and Password @@ -30,10 +30,23 @@ Also shows if the timer is enabled. ## Installation -Please refer to the original `usermod_v2_rotary_encoder_ui` readme for the main instructions.
-To activate this alternative usermod, add `#define USE_ALT_DISPlAY` to the `usermods_list.cpp` file, -or add `-D USE_ALT_DISPlAY` to the original `platformio_override.ini.sample` file. +Copy the example `platformio_override.sample.ini` to the root directory of your particular build and rename it to `platformio_override.ini`. +To activate this alternative usermod, add `#define USE_ALT_DISPlAY` (NOTE: CASE SENSITIVE) to the `usermods_list.cpp` file, or add `-D USE_ALT_DISPlAY` to your `platformio_override.ini` file + +### Define Your Options + +* `USERMOD_ROTARY_ENCODER_UI` - define this to have this user mod included wled00\usermods_list.cpp +* `USERMOD_FOUR_LINE_DISPLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp + also tells this usermod that the display is available + (see the Four Line Display usermod `readme.md` for more details) +* `USE_ALT_DISPlAY` - Mandatory to use Four Line Display +* `ENCODER_DT_PIN` - defaults to 18 +* `ENCODER_CLK_PIN` - defaults to 5 +* `ENCODER_SW_PIN` - defaults to 19 +* `USERMOD_ROTARY_ENCODER_GPIO` - GPIO functionality: + `INPUT_PULLUP` to use internal pull-up + `INPUT` to use pull-up on the PCB ### PlatformIO requirements diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h index 6a15b520..5756fbb6 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h @@ -392,31 +392,31 @@ byte RotaryEncoderUIUsermod::readPin(uint8_t pin) { * modes_alpha_indexes and palettes_alpha_indexes. */ void RotaryEncoderUIUsermod::sortModesAndPalettes() { - DEBUG_PRINTLN(F("Sorting modes and palettes.")); + DEBUG_PRINT(F("Sorting modes: ")); DEBUG_PRINTLN(strip.getModeCount()); //modes_qstrings = re_findModeStrings(JSON_mode_names, strip.getModeCount()); modes_qstrings = strip.getModeDataSrc(); modes_alpha_indexes = re_initIndexArray(strip.getModeCount()); re_sortModes(modes_qstrings, modes_alpha_indexes, strip.getModeCount(), MODE_SORT_SKIP_COUNT); - palettes_qstrings = re_findModeStrings(JSON_palette_names, strip.getPaletteCount()+strip.customPalettes.size()); - palettes_alpha_indexes = re_initIndexArray(strip.getPaletteCount()+strip.customPalettes.size()); + DEBUG_PRINT(F("Sorting palettes: ")); DEBUG_PRINT(strip.getPaletteCount()); DEBUG_PRINT('/'); DEBUG_PRINTLN(strip.customPalettes.size()); + palettes_qstrings = re_findModeStrings(JSON_palette_names, strip.getPaletteCount()); + palettes_alpha_indexes = re_initIndexArray(strip.getPaletteCount()); if (strip.customPalettes.size()) { for (int i=0; i> 3) +10)); + unsigned var = 0; + unsigned counter = (strip.now * ((SEGMENT.speed >> 3) +10)) & 0xFFFFU; counter = (counter >> 2) + (counter >> 4); //0-16384 + 0-2048 if (counter < 16384) { if (counter > 8192) counter = 8192 - (counter - 8192); var = sin16(counter) / 103; //close to parabolic in range 0-8192, max val. 23170 } - uint8_t lum = 30 + var; + unsigned lum = 30 + var; for (int i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), lum)); } @@ -356,8 +366,8 @@ static const char _data_FX_MODE_BREATH[] PROGMEM = "Breathe@!;!,!;!;01"; * Fades the LEDs between two colors */ uint16_t mode_fade(void) { - uint16_t counter = (strip.now * ((SEGMENT.speed >> 3) +10)); - uint8_t lum = triwave16(counter) >> 8; + unsigned counter = (strip.now * ((SEGMENT.speed >> 3) +10)); + unsigned lum = triwave16(counter) >> 8; for (int i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), lum)); @@ -371,13 +381,13 @@ static const char _data_FX_MODE_FADE[] PROGMEM = "Fade@!;!,!;!;01"; /* * Scan mode parent function */ -uint16_t scan(bool dual) -{ +uint16_t scan(bool dual) { + if (SEGLEN == 1) return mode_static(); uint32_t cycleTime = 750 + (255 - SEGMENT.speed)*150; uint32_t perc = strip.now % cycleTime; - uint16_t prog = (perc * 65535) / cycleTime; - uint16_t size = 1 + ((SEGMENT.intensity * SEGLEN) >> 9); - uint16_t ledIndex = (prog * ((SEGLEN *2) - size *2)) >> 16; + int prog = (perc * 65535) / cycleTime; + int size = 1 + ((SEGMENT.intensity * SEGLEN) >> 9); + int ledIndex = (prog * ((SEGLEN *2) - size *2)) >> 16; if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); @@ -386,7 +396,7 @@ uint16_t scan(bool dual) if (dual) { for (int j = led_offset; j < led_offset + size; j++) { - uint16_t i2 = SEGLEN -1 -j; + unsigned i2 = SEGLEN -1 -j; SEGMENT.setPixelColor(i2, SEGMENT.color_from_palette(i2, true, PALETTE_SOLID_WRAP, (SEGCOLOR(2))? 2:0)); } } @@ -421,7 +431,7 @@ static const char _data_FX_MODE_DUAL_SCAN[] PROGMEM = "Scan Dual@!,# of dots,,,, * Cycles all LEDs at once through a rainbow. */ uint16_t mode_rainbow(void) { - uint16_t counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF; + unsigned counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF; counter = counter >> 8; if (SEGMENT.intensity < 128){ @@ -439,7 +449,7 @@ static const char _data_FX_MODE_RAINBOW[] PROGMEM = "Colorloop@!,Saturation;;!;0 * Cycles a rainbow over the entire string of LEDs. */ uint16_t mode_rainbow_cycle(void) { - uint16_t counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF; + unsigned counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF; counter = counter >> 8; for (int i = 0; i < SEGLEN; i++) { @@ -456,8 +466,8 @@ static const char _data_FX_MODE_RAINBOW_CYCLE[] PROGMEM = "Rainbow@!,Size;;!"; /* * Alternating pixels running function. */ -uint16_t running(uint32_t color1, uint32_t color2, bool theatre = false) { - uint8_t width = (theatre ? 3 : 1) + (SEGMENT.intensity >> 4); // window +static uint16_t running(uint32_t color1, uint32_t color2, bool theatre = false) { + int width = (theatre ? 3 : 1) + (SEGMENT.intensity >> 4); // window uint32_t cycleTime = 50 + (255 - SEGMENT.speed); uint32_t it = strip.now / cycleTime; bool usePalette = color1 == SEGCOLOR(0); @@ -468,7 +478,7 @@ uint16_t running(uint32_t color1, uint32_t color2, bool theatre = false) { if (theatre) { if ((i % width) == SEGENV.aux0) col = color1; } else { - int8_t pos = (i % (width<<1)); + int pos = (i % (width<<1)); if ((pos < SEGENV.aux0-width) || ((pos >= SEGENV.aux0) && (pos < SEGENV.aux0+width))) col = color1; } SEGMENT.setPixelColor(i,col); @@ -505,12 +515,12 @@ static const char _data_FX_MODE_THEATER_CHASE_RAINBOW[] PROGMEM = "Theater Rainb /* * Running lights effect with smooth sine transition base. */ -uint16_t running_base(bool saw, bool dual=false) { - uint8_t x_scale = SEGMENT.intensity >> 2; +static uint16_t running_base(bool saw, bool dual=false) { + unsigned x_scale = SEGMENT.intensity >> 2; uint32_t counter = (strip.now * SEGMENT.speed) >> 9; for (int i = 0; i < SEGLEN; i++) { - uint16_t a = i*x_scale - counter; + unsigned a = i*x_scale - counter; if (saw) { a &= 0xFF; if (a < 16) @@ -524,7 +534,7 @@ uint16_t running_base(bool saw, bool dual=false) { uint8_t s = dual ? sin_gap(a) : sin8(a); uint32_t ca = color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), s); if (dual) { - uint16_t b = (SEGLEN-1-i)*x_scale - counter; + unsigned b = (SEGLEN-1-i)*x_scale - counter; uint8_t t = sin_gap(b); uint32_t cb = color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 2), t); ca = color_blend(ca, cb, 127); @@ -575,7 +585,7 @@ uint16_t mode_twinkle(void) { uint32_t it = strip.now / cycleTime; if (it != SEGENV.step) { - uint16_t maxOn = map(SEGMENT.intensity, 0, 255, 1, SEGLEN); // make sure at least one LED is on + unsigned maxOn = map(SEGMENT.intensity, 0, 255, 1, SEGLEN); // make sure at least one LED is on if (SEGENV.aux0 >= maxOn) { SEGENV.aux0 = 0; @@ -585,13 +595,13 @@ uint16_t mode_twinkle(void) { SEGENV.step = it; } - uint16_t PRNG16 = SEGENV.aux1; + unsigned PRNG16 = SEGENV.aux1; for (unsigned i = 0; i < SEGENV.aux0; i++) { PRNG16 = (uint16_t)(PRNG16 * 2053) + 13849; // next 'random' number uint32_t p = (uint32_t)SEGLEN * (uint32_t)PRNG16; - uint16_t j = p >> 16; + unsigned j = p >> 16; SEGMENT.setPixelColor(j, SEGMENT.color_from_palette(j, true, PALETTE_SOLID_WRAP, 0)); } @@ -604,7 +614,7 @@ static const char _data_FX_MODE_TWINKLE[] PROGMEM = "Twinkle@!,!;!,!;!;;m12=0"; * Dissolve function */ uint16_t dissolve(uint32_t color) { - uint16_t dataSize = (SEGLEN+7) >> 3; //1 bit per LED + unsigned dataSize = (SEGLEN+7) >> 3; //1 bit per LED if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed if (SEGENV.call == 0) { @@ -744,7 +754,7 @@ uint16_t mode_multi_strobe(void) { } SEGENV.aux0 = 50 + 20*(uint16_t)(255-SEGMENT.speed); - uint16_t count = 2 * ((SEGMENT.intensity / 10) + 1); + unsigned count = 2 * ((SEGMENT.intensity / 10) + 1); if(SEGENV.aux1 < count) { if((SEGENV.aux1 & 1) == 0) { SEGMENT.fill(SEGCOLOR(0)); @@ -782,7 +792,7 @@ uint16_t mode_android(void) { if (SEGENV.aux1 < 2) SEGENV.aux0 = 0; } - uint16_t a = SEGENV.step; + unsigned a = SEGENV.step & 0xFFFFU; if (SEGENV.aux0 == 0) { @@ -798,15 +808,15 @@ uint16_t mode_android(void) { if (a + SEGENV.aux1 < SEGLEN) { - for (int i = a; i < a+SEGENV.aux1; i++) { + for (unsigned i = a; i < a+SEGENV.aux1; i++) { SEGMENT.setPixelColor(i, SEGCOLOR(0)); } } else { - for (int i = a; i < SEGLEN; i++) { + for (unsigned i = a; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGCOLOR(0)); } - for (int i = 0; i < SEGENV.aux1 - (SEGLEN -a); i++) { + for (unsigned i = 0; i < SEGENV.aux1 - (SEGLEN -a); i++) { SEGMENT.setPixelColor(i, SEGCOLOR(0)); } } @@ -822,7 +832,7 @@ static const char _data_FX_MODE_ANDROID[] PROGMEM = "Android@!,Width;!,!;!;;m12= * color1 = background color * color2 and color3 = colors of two adjacent leds */ -uint16_t chase(uint32_t color1, uint32_t color2, uint32_t color3, bool do_palette) { +static uint16_t chase(uint32_t color1, uint32_t color2, uint32_t color3, bool do_palette) { uint16_t counter = strip.now * ((SEGMENT.speed >> 2) + 1); uint16_t a = (counter * SEGLEN) >> 16; @@ -838,7 +848,7 @@ uint16_t chase(uint32_t color1, uint32_t color2, uint32_t color3, bool do_palett SEGENV.step = a; // Use intensity setting to vary chase up to 1/2 string length - uint8_t size = 1 + ((SEGMENT.intensity * SEGLEN) >> 10); + unsigned size = 1 + ((SEGMENT.intensity * SEGLEN) >> 10); uint16_t b = a + size; //"trail" of chase, filled with color1 if (b > SEGLEN) b -= SEGLEN; @@ -848,7 +858,7 @@ uint16_t chase(uint32_t color1, uint32_t color2, uint32_t color3, bool do_palett //background if (do_palette) { - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); } } else SEGMENT.fill(color1); @@ -857,31 +867,31 @@ uint16_t chase(uint32_t color1, uint32_t color2, uint32_t color3, bool do_palett if (chase_random) { color1 = SEGMENT.color_wheel(SEGENV.aux1); - for (int i = a; i < SEGLEN; i++) + for (unsigned i = a; i < SEGLEN; i++) SEGMENT.setPixelColor(i, color1); } //fill between points a and b with color2 if (a < b) { - for (int i = a; i < b; i++) + for (unsigned i = a; i < b; i++) SEGMENT.setPixelColor(i, color2); } else { - for (int i = a; i < SEGLEN; i++) //fill until end + for (unsigned i = a; i < SEGLEN; i++) //fill until end SEGMENT.setPixelColor(i, color2); - for (int i = 0; i < b; i++) //fill from start until b + for (unsigned i = 0; i < b; i++) //fill from start until b SEGMENT.setPixelColor(i, color2); } //fill between points b and c with color2 if (b < c) { - for (int i = b; i < c; i++) + for (unsigned i = b; i < c; i++) SEGMENT.setPixelColor(i, color3); } else { - for (int i = b; i < SEGLEN; i++) //fill until end + for (unsigned i = b; i < SEGLEN; i++) //fill until end SEGMENT.setPixelColor(i, color3); - for (int i = 0; i < c; i++) //fill from start until c + for (unsigned i = 0; i < c; i++) //fill from start until c SEGMENT.setPixelColor(i, color3); } @@ -911,9 +921,9 @@ static const char _data_FX_MODE_CHASE_RANDOM[] PROGMEM = "Chase Random@!,Width;! * Primary, secondary running on rainbow. */ uint16_t mode_chase_rainbow(void) { - uint8_t color_sep = 256 / SEGLEN; + unsigned color_sep = 256 / SEGLEN; if (color_sep == 0) color_sep = 1; // correction for segments longer than 256 LEDs - uint8_t color_index = SEGENV.call & 0xFF; + unsigned color_index = SEGENV.call & 0xFF; uint32_t color = SEGMENT.color_wheel(((SEGENV.step * color_sep) + color_index) & 0xFF); return chase(color, SEGCOLOR(0), SEGCOLOR(1), false); @@ -939,14 +949,14 @@ static const char _data_FX_MODE_CHASE_RAINBOW_WHITE[] PROGMEM = "Rainbow Runner@ * Red - Amber - Green - Blue lights running */ uint16_t mode_colorful(void) { - uint8_t numColors = 4; //3, 4, or 5 + unsigned numColors = 4; //3, 4, or 5 uint32_t cols[9]{0x00FF0000,0x00EEBB00,0x0000EE00,0x000077CC}; if (SEGMENT.intensity > 160 || SEGMENT.palette) { //palette or color if (!SEGMENT.palette) { numColors = 3; for (size_t i = 0; i < 3; i++) cols[i] = SEGCOLOR(i); } else { - uint16_t fac = 80; + unsigned fac = 80; if (SEGMENT.palette == 52) {numColors = 5; fac = 61;} //C9 2 has 5 colors for (size_t i = 0; i < numColors; i++) { cols[i] = SEGMENT.color_from_palette(i*fac, false, true, 255); @@ -970,9 +980,9 @@ uint16_t mode_colorful(void) { SEGENV.step = it; } - for (int i = 0; i < SEGLEN; i+= numColors) + for (unsigned i = 0; i < SEGLEN; i+= numColors) { - for (int j = 0; j < numColors; j++) SEGMENT.setPixelColor(i + j, cols[SEGENV.aux0 + j]); + for (unsigned j = 0; j < numColors; j++) SEGMENT.setPixelColor(i + j, cols[SEGENV.aux0 + j]); } return FRAMETIME; @@ -1018,17 +1028,17 @@ 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(); - uint8_t flash_step = SEGENV.call % ((FLASH_COUNT * 2) + 1); + unsigned flash_step = SEGENV.call % ((FLASH_COUNT * 2) + 1); for (int i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } - uint16_t delay = 10 + ((30 * (uint16_t)(255 - SEGMENT.speed)) / SEGLEN); + unsigned delay = 10 + ((30 * (uint16_t)(255 - SEGMENT.speed)) / SEGLEN); if(flash_step < (FLASH_COUNT * 2)) { if(flash_step % 2 == 0) { - uint16_t n = SEGENV.step; - uint16_t m = (SEGENV.step + 1) % SEGLEN; + unsigned n = SEGENV.step; + unsigned m = (SEGENV.step + 1) % SEGLEN; SEGMENT.setPixelColor( n, SEGCOLOR(1)); SEGMENT.setPixelColor( m, SEGCOLOR(1)); delay = 20; @@ -1048,16 +1058,16 @@ static const char _data_FX_MODE_CHASE_FLASH[] PROGMEM = "Chase Flash@!;Bg,Fx;!"; */ uint16_t mode_chase_flash_random(void) { if (SEGLEN == 1) return mode_static(); - uint8_t flash_step = SEGENV.call % ((FLASH_COUNT * 2) + 1); + unsigned flash_step = SEGENV.call % ((FLASH_COUNT * 2) + 1); for (int i = 0; i < SEGENV.aux1; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_wheel(SEGENV.aux0)); } - uint16_t delay = 1 + ((10 * (uint16_t)(255 - SEGMENT.speed)) / SEGLEN); + unsigned delay = 1 + ((10 * (uint16_t)(255 - SEGMENT.speed)) / SEGLEN); if(flash_step < (FLASH_COUNT * 2)) { - uint16_t n = SEGENV.aux1; - uint16_t m = (SEGENV.aux1 + 1) % SEGLEN; + unsigned n = SEGENV.aux1; + unsigned m = (SEGENV.aux1 + 1) % SEGLEN; if(flash_step % 2 == 0) { SEGMENT.setPixelColor( n, SEGCOLOR(0)); SEGMENT.setPixelColor( m, SEGCOLOR(0)); @@ -1096,14 +1106,14 @@ uint16_t mode_running_random(void) { uint32_t it = strip.now / cycleTime; if (SEGENV.call == 0) SEGENV.aux0 = random16(); // random seed for PRNG on start - uint8_t zoneSize = ((255-SEGMENT.intensity) >> 4) +1; + unsigned zoneSize = ((255-SEGMENT.intensity) >> 4) +1; uint16_t PRNG16 = SEGENV.aux0; - uint8_t z = it % zoneSize; + unsigned z = it % zoneSize; bool nzone = (!z && it != SEGENV.aux1); - for (int i=SEGLEN-1; i > 0; i--) { + for (unsigned i=SEGLEN-1; i > 0; i--) { if (nzone || z >= zoneSize) { - uint8_t lastrand = PRNG16 >> 8; + unsigned lastrand = PRNG16 >> 8; int16_t diff = 0; while (abs(diff) < 42) { // make sure the difference between adjacent colors is big enough PRNG16 = (uint16_t)(PRNG16 * 2053) + 13849; // next zone, next 'random' number @@ -1125,57 +1135,62 @@ uint16_t mode_running_random(void) { static const char _data_FX_MODE_RUNNING_RANDOM[] PROGMEM = "Stream@!,Zone size;;!"; -uint16_t larson_scanner(bool dual) { - if (SEGLEN == 1) return mode_static(); - uint16_t counter = strip.now * ((SEGMENT.speed >> 2) +8); - uint16_t index = (counter * SEGLEN) >> 16; - - SEGMENT.fade_out(SEGMENT.intensity); - - if (SEGENV.step > index && SEGENV.step - index > SEGLEN/2) { - SEGENV.aux0 = !SEGENV.aux0; - } - - for (int i = SEGENV.step; i < index; i++) { - uint16_t j = (SEGENV.aux0)?i:SEGLEN-1-i; - SEGMENT.setPixelColor( j, SEGMENT.color_from_palette(j, true, PALETTE_SOLID_WRAP, 0)); - } - if (dual) { - uint32_t c; - if (SEGCOLOR(2) != 0) { - c = SEGCOLOR(2); - } else { - c = SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0); - } - - for (int i = SEGENV.step; i < index; i++) { - uint16_t j = (SEGENV.aux0)?SEGLEN-1-i:i; - SEGMENT.setPixelColor(j, c); - } - } - - SEGENV.step = index; - return FRAMETIME; -} - - /* * K.I.T.T. */ -uint16_t mode_larson_scanner(void){ - return larson_scanner(false); -} -static const char _data_FX_MODE_LARSON_SCANNER[] PROGMEM = "Scanner@!,Fade rate;!,!;!;;m12=0"; +uint16_t mode_larson_scanner(void) { + 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 + + SEGMENT.fade_out(255-SEGMENT.intensity); + + if (SEGENV.step > strip.now) return FRAMETIME; // we have a pause + + unsigned index = SEGENV.aux1 + pixels; + // are we slow enough to use frames per pixel? + if (pixels == 0) { + const unsigned frames = speed / SEGLEN; // how many frames per 1 pixel + if (SEGENV.step++ < frames) return FRAMETIME; + SEGENV.step = 0; + index++; + } + + if (index > SEGLEN) { + + SEGENV.aux0 = !SEGENV.aux0; // change direction + SEGENV.aux1 = 0; // reset position + // set delay + if (SEGENV.aux0 || SEGMENT.check2) SEGENV.step = strip.now + SEGMENT.custom1 * 25; // multiply by 25ms + else SEGENV.step = 0; + + } else { + + // paint as many pixels as needed + for (unsigned i = SEGENV.aux1; i < index; i++) { + unsigned j = (SEGENV.aux0) ? i : SEGLEN - 1 - i; + uint32_t c = SEGMENT.color_from_palette(j, true, PALETTE_SOLID_WRAP, 0); + SEGMENT.setPixelColor(j, c); + if (SEGMENT.check1) { + SEGMENT.setPixelColor(SEGLEN - 1 - j, SEGCOLOR(2) ? SEGCOLOR(2) : c); + } + } + SEGENV.aux1 = index; + } + return FRAMETIME; +} +static const char _data_FX_MODE_LARSON_SCANNER[] PROGMEM = "Scanner@!,Trail,Delay,,,Dual,Bi-delay;!,!,!;!;;m12=0,c1=0"; /* * Creates two Larson scanners moving in opposite directions * Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/DualLarson.h */ uint16_t mode_dual_larson_scanner(void){ - return larson_scanner(true); + SEGMENT.check1 = true; + return mode_larson_scanner(); } -static const char _data_FX_MODE_DUAL_LARSON_SCANNER[] PROGMEM = "Scanner Dual@!,Fade rate;!,!,!;!;;m12=0"; +static const char _data_FX_MODE_DUAL_LARSON_SCANNER[] PROGMEM = "Scanner Dual@!,Trail,Delay,,,Dual,Bi-delay;!,!,!;!;;m12=0,c1=0"; /* @@ -1183,19 +1198,19 @@ static const char _data_FX_MODE_DUAL_LARSON_SCANNER[] PROGMEM = "Scanner Dual@!, */ uint16_t mode_comet(void) { if (SEGLEN == 1) return mode_static(); - uint16_t counter = strip.now * ((SEGMENT.speed >>2) +1); - uint16_t index = (counter * SEGLEN) >> 16; + unsigned counter = (strip.now * ((SEGMENT.speed >>2) +1)) & 0xFFFF; + unsigned index = (counter * SEGLEN) >> 16; if (SEGENV.call == 0) SEGENV.aux0 = index; SEGMENT.fade_out(SEGMENT.intensity); SEGMENT.setPixelColor( index, SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0)); if (index > SEGENV.aux0) { - for (int i = SEGENV.aux0; i < index ; i++) { + for (unsigned i = SEGENV.aux0; i < index ; i++) { SEGMENT.setPixelColor( i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } } else if (index < SEGENV.aux0 && index < 10) { - for (int i = 0; i < index ; i++) { + for (unsigned i = 0; i < index ; i++) { SEGMENT.setPixelColor( i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } } @@ -1220,15 +1235,18 @@ uint16_t mode_fireworks() { } SEGMENT.fade_out(128); - bool valid1 = (SEGENV.aux0 < width*height); - bool valid2 = (SEGENV.aux1 < width*height); uint8_t x = SEGENV.aux0%width, y = SEGENV.aux0/width; // 2D coordinates stored in upper and lower byte - uint32_t sv1 = 0, sv2 = 0; - if (valid1) sv1 = SEGMENT.is2D() ? SEGMENT.getPixelColorXY(x, y) : SEGMENT.getPixelColor(SEGENV.aux0); // get spark color - if (valid2) sv2 = SEGMENT.is2D() ? SEGMENT.getPixelColorXY(x, y) : SEGMENT.getPixelColor(SEGENV.aux1); - if (!SEGENV.step) SEGMENT.blur(16); - if (valid1) { if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(x, y, sv1); else SEGMENT.setPixelColor(SEGENV.aux0, sv1); } // restore spark color after blur - if (valid2) { if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(x, y, sv2); else SEGMENT.setPixelColor(SEGENV.aux1, sv2); } // restore old spark color after blur + if (!SEGENV.step) { + // fireworks mode (blur flares) + bool valid1 = (SEGENV.aux0 < width*height); + bool valid2 = (SEGENV.aux1 < width*height); + uint32_t sv1 = 0, sv2 = 0; + if (valid1) sv1 = SEGMENT.is2D() ? SEGMENT.getPixelColorXY(x, y) : SEGMENT.getPixelColor(SEGENV.aux0); // get spark color + if (valid2) sv2 = SEGMENT.is2D() ? SEGMENT.getPixelColorXY(x, y) : SEGMENT.getPixelColor(SEGENV.aux1); + SEGMENT.blur(16); // used in mode_rain() + if (valid1) { if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(x, y, sv1); else SEGMENT.setPixelColor(SEGENV.aux0, sv1); } // restore spark color after blur + if (valid2) { if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(x, y, sv2); else SEGMENT.setPixelColor(SEGENV.aux1, sv2); } // restore old spark color after blur + } for (int i=0; i> 1)) == 0) { @@ -1250,12 +1268,12 @@ 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(); - const uint16_t width = SEGMENT.virtualWidth(); - const uint16_t height = SEGMENT.virtualHeight(); + const unsigned width = SEGMENT.virtualWidth(); + const unsigned height = SEGMENT.virtualHeight(); SEGENV.step += FRAMETIME; if (SEGENV.call && SEGENV.step > SPEED_FORMULA_L) { SEGENV.step = 1; - if (strip.isMatrix) { + if (SEGMENT.is2D()) { //uint32_t ctemp[width]; //for (int i = 0; i> 8); //183-255, suitable for 1/5th of LEDs flashers + unsigned avgFlasherBri = flasherBriSum / flashersInZone; + unsigned globalPeakBri = 255 - ((avgFlasherBri * MAX_SHIMMER) >> 8); //183-255, suitable for 1/5th of LEDs flashers - for (int f = firstFlasher; f < firstFlasher + flashersInZone; f++) { - uint8_t bri = (flasherBri[f - firstFlasher] * globalPeakBri) / 255; + for (unsigned f = firstFlasher; f < firstFlasher + flashersInZone; f++) { + unsigned bri = (flasherBri[f - firstFlasher] * globalPeakBri) / 255; PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; //next 'random' number - uint16_t flasherPos = f*flasherDistance; + unsigned flasherPos = f*flasherDistance; SEGMENT.setPixelColor(flasherPos, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(PRNG16 >> 8, false, false, 0), bri)); - for (int i = flasherPos+1; i < flasherPos+flasherDistance && i < SEGLEN; i++) { + for (unsigned i = flasherPos+1; i < flasherPos+flasherDistance && i < SEGLEN; i++) { PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; //next 'random' number SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(PRNG16 >> 8, false, false, 0, globalPeakBri)); } @@ -1487,17 +1504,17 @@ static const char _data_FX_MODE_FAIRY[] PROGMEM = "Fairy@!,# of flashers;!,!;!"; * Warning: Uses 4 bytes of segment data per pixel */ uint16_t mode_fairytwinkle() { - uint16_t dataSize = sizeof(flasher) * SEGLEN; + unsigned dataSize = sizeof(flasher) * SEGLEN; if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Flasher* flashers = reinterpret_cast(SEGENV.data); - uint16_t now16 = strip.now & 0xFFFF; + unsigned now16 = strip.now & 0xFFFF; uint16_t PRNG16 = 5100 + strip.getCurrSegmentId(); - uint16_t riseFallTime = 400 + (255-SEGMENT.speed)*3; - uint16_t maxDur = riseFallTime/100 + ((255 - SEGMENT.intensity) >> 2) + 13 + ((255 - SEGMENT.intensity) >> 1); + unsigned riseFallTime = 400 + (255-SEGMENT.speed)*3; + unsigned maxDur = riseFallTime/100 + ((255 - SEGMENT.intensity) >> 2) + 13 + ((255 - SEGMENT.intensity) >> 1); for (int f = 0; f < SEGLEN; f++) { - uint16_t stateTime = now16 - flashers[f].stateStart; + unsigned stateTime = now16 - flashers[f].stateStart; //random on/off time reached, switch state if (stateTime > flashers[f].stateDur * 100) { flashers[f].stateOn = !flashers[f].stateOn; @@ -1517,10 +1534,10 @@ uint16_t mode_fairytwinkle() { } if (flashers[f].stateOn && flashers[f].stateDur > maxDur) flashers[f].stateDur = maxDur; //react more quickly on intensity change if (stateTime > riseFallTime) stateTime = riseFallTime; //for flasher brightness calculation, fades in first 255 ms of state - uint8_t fadeprog = 255 - ((stateTime * 255) / riseFallTime); - uint8_t flasherBri = (flashers[f].stateOn) ? 255-gamma8(fadeprog) : gamma8(fadeprog); - uint16_t lastR = PRNG16; - uint16_t diff = 0; + unsigned fadeprog = 255 - ((stateTime * 255) / riseFallTime); + unsigned flasherBri = (flashers[f].stateOn) ? 255-gamma8(fadeprog) : gamma8(fadeprog); + unsigned lastR = PRNG16; + unsigned diff = 0; while (diff < 0x4000) { //make sure colors of two adjacent LEDs differ enough PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; //next 'random' number diff = (PRNG16 > lastR) ? PRNG16 - lastR : lastR - PRNG16; @@ -1538,8 +1555,8 @@ static const char _data_FX_MODE_FAIRYTWINKLE[] PROGMEM = "Fairytwinkle@!,!;!,!;! uint16_t tricolor_chase(uint32_t color1, uint32_t color2) { uint32_t cycleTime = 50 + ((255 - SEGMENT.speed)<<1); uint32_t it = strip.now / cycleTime; // iterator - uint8_t width = (1 + (SEGMENT.intensity>>4)); // value of 1-16 for each colour - uint8_t index = it % (width*3); + unsigned width = (1 + (SEGMENT.intensity>>4)); // value of 1-16 for each colour + unsigned index = it % (width*3); for (int i = 0; i < SEGLEN; i++, index++) { if (index > (width*3)-1) index = 0; @@ -1567,8 +1584,8 @@ static const char _data_FX_MODE_TRICOLOR_CHASE[] PROGMEM = "Chase 3@!,Size;1,2,3 * ICU mode */ uint16_t mode_icu(void) { - uint16_t dest = SEGENV.step & 0xFFFF; - uint8_t space = (SEGMENT.intensity >> 3) +2; + unsigned dest = SEGENV.step & 0xFFFF; + unsigned space = (SEGMENT.intensity >> 3) +2; if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); @@ -1610,9 +1627,9 @@ static const char _data_FX_MODE_ICU[] PROGMEM = "ICU@!,!,,,,,Overlay;!,!;!"; uint16_t mode_tricolor_wipe(void) { uint32_t cycleTime = 1000 + (255 - SEGMENT.speed)*200; uint32_t perc = strip.now % cycleTime; - uint16_t prog = (perc * 65535) / cycleTime; - uint16_t ledIndex = (prog * SEGLEN * 3) >> 16; - uint16_t ledOffset = ledIndex; + unsigned prog = (perc * 65535) / cycleTime; + unsigned ledIndex = (prog * SEGLEN * 3) >> 16; + unsigned ledOffset = ledIndex; for (int i = 0; i < SEGLEN; i++) { @@ -1620,20 +1637,20 @@ uint16_t mode_tricolor_wipe(void) { } if(ledIndex < SEGLEN) { //wipe from 0 to 1 - for (int i = 0; i < SEGLEN; i++) + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, (i > ledOffset)? SEGCOLOR(0) : SEGCOLOR(1)); } } else if (ledIndex < SEGLEN*2) { //wipe from 1 to 2 ledOffset = ledIndex - SEGLEN; - for (int i = ledOffset +1; i < SEGLEN; i++) + for (unsigned i = ledOffset +1; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGCOLOR(1)); } } else //wipe from 2 to 0 { ledOffset = ledIndex - SEGLEN*2; - for (int i = 0; i <= ledOffset; i++) + for (unsigned i = 0; i <= ledOffset; i++) { SEGMENT.setPixelColor(i, SEGCOLOR(0)); } @@ -1650,11 +1667,11 @@ static const char _data_FX_MODE_TRICOLOR_WIPE[] PROGMEM = "Tri Wipe@!;1,2,3;!"; * Modified by Aircoookie */ uint16_t mode_tricolor_fade(void) { - uint16_t counter = strip.now * ((SEGMENT.speed >> 3) +1); - uint32_t prog = (counter * 768) >> 16; + unsigned counter = strip.now * ((SEGMENT.speed >> 3) +1); + uint16_t prog = (counter * 768) >> 16; uint32_t color1 = 0, color2 = 0; - byte stage = 0; + unsigned stage = 0; if(prog < 256) { color1 = SEGCOLOR(0); @@ -1671,7 +1688,7 @@ uint16_t mode_tricolor_fade(void) { } byte stp = prog; // % 256 - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { uint32_t color; if (stage == 2) { color = color_blend(SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 2), color2, stp); @@ -1692,19 +1709,20 @@ static const char _data_FX_MODE_TRICOLOR_FADE[] PROGMEM = "Tri Fade@!;1,2,3;!"; * Creates random comets * Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/MultiComet.h */ +#define MAX_COMETS 8 uint16_t mode_multi_comet(void) { uint32_t cycleTime = 10 + (uint32_t)(255 - SEGMENT.speed); uint32_t it = strip.now / cycleTime; if (SEGENV.step == it) return FRAMETIME; - if (!SEGENV.allocateData(sizeof(uint16_t) * 8)) return mode_static(); //allocation failed + if (!SEGENV.allocateData(sizeof(uint16_t) * MAX_COMETS)) return mode_static(); //allocation failed - SEGMENT.fade_out(SEGMENT.intensity); + SEGMENT.fade_out(SEGMENT.intensity/2 + 128); uint16_t* comets = reinterpret_cast(SEGENV.data); - for (int i=0; i < 8; i++) { + for (unsigned i=0; i < MAX_COMETS; i++) { if(comets[i] < SEGLEN) { - uint16_t index = comets[i]; + unsigned index = comets[i]; if (SEGCOLOR(2) != 0) { SEGMENT.setPixelColor(index, i % 2 ? SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0) : SEGCOLOR(2)); @@ -1723,8 +1741,8 @@ uint16_t mode_multi_comet(void) { SEGENV.step = it; return FRAMETIME; } -static const char _data_FX_MODE_MULTI_COMET[] PROGMEM = "Multi Comet"; - +static const char _data_FX_MODE_MULTI_COMET[] PROGMEM = "Multi Comet@!,Fade;!,!;!;1"; +#undef MAX_COMETS /* * Running random pixels ("Stream 2") @@ -1735,19 +1753,19 @@ uint16_t mode_random_chase(void) { SEGENV.step = RGBW32(random8(), random8(), random8(), 0); SEGENV.aux0 = random16(); } - uint16_t prevSeed = random16_get_seed(); // save seed so we can restore it at the end of the function + unsigned prevSeed = random16_get_seed(); // save seed so we can restore it at the end of the function uint32_t cycleTime = 25 + (3 * (uint32_t)(255 - SEGMENT.speed)); uint32_t it = strip.now / cycleTime; uint32_t color = SEGENV.step; random16_set_seed(SEGENV.aux0); - for (int i = SEGLEN -1; i > 0; i--) { + for (unsigned i = SEGLEN -1; i > 0; i--) { uint8_t r = random8(6) != 0 ? (color >> 16 & 0xFF) : random8(); uint8_t g = random8(6) != 0 ? (color >> 8 & 0xFF) : random8(); uint8_t b = random8(6) != 0 ? (color & 0xFF) : random8(); color = RGBW32(r, g, b, 0); - SEGMENT.setPixelColor(i, r, g, b); - if (i == SEGLEN -1 && SEGENV.aux1 != (it & 0xFFFF)) { //new first color in next frame + SEGMENT.setPixelColor(i, color); + if (i == SEGLEN -1U && SEGENV.aux1 != (it & 0xFFFFU)) { //new first color in next frame SEGENV.step = color; SEGENV.aux0 = random16_get_seed(); } @@ -1763,18 +1781,18 @@ static const char _data_FX_MODE_RANDOM_CHASE[] PROGMEM = "Stream 2@!;;"; //7 bytes typedef struct Oscillator { - int16_t pos; - int8_t size; - int8_t dir; - int8_t speed; + uint16_t pos; + uint8_t size; + int8_t dir; + uint8_t speed; } oscillator; /* / Oscillating bars of color, updated with standard framerate */ uint16_t mode_oscillate(void) { - uint8_t numOscillators = 3; - uint16_t dataSize = sizeof(oscillator) * numOscillators; + constexpr unsigned numOscillators = 3; + constexpr unsigned dataSize = sizeof(oscillator) * numOscillators; if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed @@ -1782,15 +1800,15 @@ uint16_t mode_oscillate(void) { if (SEGENV.call == 0) { - oscillators[0] = {(int16_t)(SEGLEN/4), (int8_t)(SEGLEN/8), 1, 1}; - oscillators[1] = {(int16_t)(SEGLEN/4*3), (int8_t)(SEGLEN/8), 1, 2}; - oscillators[2] = {(int16_t)(SEGLEN/4*2), (int8_t)(SEGLEN/8), -1, 1}; + oscillators[0] = {(uint16_t)(SEGLEN/4), (uint8_t)(SEGLEN/8), 1, 1}; + oscillators[1] = {(uint16_t)(SEGLEN/4*3), (uint8_t)(SEGLEN/8), 1, 2}; + oscillators[2] = {(uint16_t)(SEGLEN/4*2), (uint8_t)(SEGLEN/8), -1, 1}; } uint32_t cycleTime = 20 + (2 * (uint32_t)(255 - SEGMENT.speed)); uint32_t it = strip.now / cycleTime; - for (int i = 0; i < numOscillators; i++) { + for (unsigned i = 0; i < numOscillators; i++) { // if the counter has increased, move the oscillator by the random step if (it != SEGENV.step) oscillators[i].pos += oscillators[i].dir * oscillators[i].speed; oscillators[i].size = SEGLEN/(3+SEGMENT.intensity/8); @@ -1807,10 +1825,10 @@ uint16_t mode_oscillate(void) { } } - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { uint32_t color = BLACK; - for (int j = 0; j < numOscillators; j++) { - if(i >= oscillators[j].pos - oscillators[j].size && i <= oscillators[j].pos + oscillators[j].size) { + for (unsigned j = 0; j < numOscillators; j++) { + if(i >= (unsigned)oscillators[j].pos - oscillators[j].size && i <= oscillators[j].pos + oscillators[j].size) { color = (color == BLACK) ? SEGCOLOR(j) : color_blend(color, SEGCOLOR(j), 128); } } @@ -1826,8 +1844,8 @@ static const char _data_FX_MODE_OSCILLATE[] PROGMEM = "Oscillate"; //TODO uint16_t mode_lightning(void) { if (SEGLEN == 1) return mode_static(); - uint16_t ledstart = random16(SEGLEN); // Determine starting location of flash - uint16_t ledlen = 1 + random16(SEGLEN -ledstart); // Determine length of flash (not to go beyond NUM_LEDS-1) + unsigned ledstart = random16(SEGLEN); // Determine starting location of flash + unsigned ledlen = 1 + random16(SEGLEN -ledstart); // Determine length of flash (not to go beyond NUM_LEDS-1) uint8_t bri = 255/random8(1, 3); if (SEGENV.aux1 == 0) //init, leader flash @@ -1842,7 +1860,7 @@ uint16_t mode_lightning(void) { if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); if (SEGENV.aux1 > 3 && !(SEGENV.aux1 & 0x01)) { //flash on even number >2 - for (int i = ledstart; i < ledstart + ledlen; i++) + for (unsigned i = ledstart; i < ledstart + ledlen; i++) { SEGMENT.setPixelColor(i,SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0, bri)); } @@ -1871,30 +1889,30 @@ static const char _data_FX_MODE_LIGHTNING[] PROGMEM = "Lightning@!,!,,,,,Overlay // Animated, ever-changing rainbows. // by Mark Kriegsman: https://gist.github.com/kriegsman/964de772d64c502760e5 uint16_t mode_pride_2015(void) { - uint16_t duration = 10 + SEGMENT.speed; - uint16_t sPseudotime = SEGENV.step; - uint16_t sHue16 = SEGENV.aux0; + unsigned duration = 10 + SEGMENT.speed; + unsigned sPseudotime = SEGENV.step; + unsigned sHue16 = SEGENV.aux0; uint8_t sat8 = beatsin88( 87, 220, 250); uint8_t brightdepth = beatsin88( 341, 96, 224); - uint16_t brightnessthetainc16 = beatsin88( 203, (25 * 256), (40 * 256)); - uint8_t msmultiplier = beatsin88(147, 23, 60); + unsigned brightnessthetainc16 = beatsin88( 203, (25 * 256), (40 * 256)); + unsigned msmultiplier = beatsin88(147, 23, 60); - uint16_t hue16 = sHue16;//gHue * 256; - uint16_t hueinc16 = beatsin88(113, 1, 3000); + unsigned hue16 = sHue16;//gHue * 256; + unsigned hueinc16 = beatsin88(113, 1, 3000); sPseudotime += duration * msmultiplier; sHue16 += duration * beatsin88( 400, 5,9); - uint16_t brightnesstheta16 = sPseudotime; + unsigned brightnesstheta16 = sPseudotime; - for (int i = 0 ; i < SEGLEN; i++) { + for (unsigned i = 0 ; i < SEGLEN; i++) { hue16 += hueinc16; uint8_t hue8 = hue16 >> 8; brightnesstheta16 += brightnessthetainc16; - uint16_t b16 = sin16( brightnesstheta16 ) + 32768; + unsigned b16 = sin16( brightnesstheta16 ) + 32768; - uint16_t bri16 = (uint32_t)((uint32_t)b16 * (uint32_t)b16) / 65536; + unsigned bri16 = (uint32_t)((uint32_t)b16 * (uint32_t)b16) / 65536; uint8_t bri8 = (uint32_t)(((uint32_t)bri16) * brightdepth) / 65536; bri8 += (255 - brightdepth); @@ -1917,7 +1935,7 @@ uint16_t mode_juggle(void) { CRGB fastled_col; byte dothue = 0; for (int i = 0; i < 8; i++) { - uint16_t index = 0 + beatsin88((16 + SEGMENT.speed)*(i + 7), 0, SEGLEN -1); + int index = 0 + beatsin88((16 + SEGMENT.speed)*(i + 7), 0, SEGLEN -1); fastled_col = CRGB(SEGMENT.getPixelColor(index)); fastled_col |= (SEGMENT.palette==0)?CHSV(dothue, 220, 255):ColorFromPalette(SEGPALETTE, dothue, 255); SEGMENT.setPixelColor(index, fastled_col); @@ -1933,7 +1951,7 @@ uint16_t mode_palette() { #ifdef ESP8266 using mathType = int32_t; using wideMathType = int64_t; - using angleType = uint16_t; + using angleType = unsigned; constexpr mathType sInt16Scale = 0x7FFF; constexpr mathType maxAngle = 0x8000; constexpr mathType staticRotationScale = 256; @@ -2057,7 +2075,7 @@ static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Shift,Size,Rotation // in step 3 above) (Effect Intensity = Sparking). uint16_t mode_fire_2012() { if (SEGLEN == 1) return mode_static(); - const uint16_t strips = SEGMENT.nrOfVStrips(); + const unsigned strips = SEGMENT.nrOfVStrips(); if (!SEGENV.allocateData(strips * SEGLEN)) return mode_static(); //allocation failed byte* heat = SEGENV.data; @@ -2092,47 +2110,52 @@ uint16_t mode_fire_2012() { // Step 4. Map from heat cells to LED colors for (int j = 0; j < SEGLEN; j++) { - SEGMENT.setPixelColor(indexToVStrip(j, stripNr), ColorFromPalette(SEGPALETTE, MIN(heat[j],240), 255, NOBLEND)); + SEGMENT.setPixelColor(indexToVStrip(j, stripNr), ColorFromPalette(SEGPALETTE, min(heat[j], byte(240)), 255, NOBLEND)); } } }; - for (int stripNr=0; stripNr> 2; + if (blurAmount > 48) blurAmount += blurAmount-48; // extra blur when slider > 192 (bush burn) + if (blurAmount < 16) SEGMENT.blurCols(SEGMENT.custom2 >> 1); // no side-burn when slider < 64 (faster) + else SEGMENT.blur(blurAmount); + } if (it != SEGENV.step) SEGENV.step = it; return FRAMETIME; } -static const char _data_FX_MODE_FIRE_2012[] PROGMEM = "Fire 2012@Cooling,Spark rate,,,Boost;;!;1;sx=64,ix=160,m12=1"; // bars +static const char _data_FX_MODE_FIRE_2012[] PROGMEM = "Fire 2012@Cooling,Spark rate,,2D Blur,Boost;;!;1;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() { - uint16_t duration = 10 + SEGMENT.speed; - uint16_t sPseudotime = SEGENV.step; - uint16_t sHue16 = SEGENV.aux0; + unsigned duration = 10 + SEGMENT.speed; + unsigned sPseudotime = SEGENV.step; + unsigned sHue16 = SEGENV.aux0; - uint8_t brightdepth = beatsin88(341, 96, 224); - uint16_t brightnessthetainc16 = beatsin88( 203, (25 * 256), (40 * 256)); - uint8_t msmultiplier = beatsin88(147, 23, 60); + unsigned brightdepth = beatsin88(341, 96, 224); + unsigned brightnessthetainc16 = beatsin88( 203, (25 * 256), (40 * 256)); + unsigned msmultiplier = beatsin88(147, 23, 60); - uint16_t hue16 = sHue16;//gHue * 256; - uint16_t hueinc16 = beatsin88(113, 60, 300)*SEGMENT.intensity*10/255; // Use the Intensity Slider for the hues + unsigned hue16 = sHue16;//gHue * 256; + unsigned hueinc16 = beatsin88(113, 60, 300)*SEGMENT.intensity*10/255; // Use the Intensity Slider for the hues sPseudotime += duration * msmultiplier; sHue16 += duration * beatsin88(400, 5, 9); - uint16_t brightnesstheta16 = sPseudotime; + unsigned brightnesstheta16 = sPseudotime; for (int i = 0 ; i < SEGLEN; i++) { hue16 += hueinc16; uint8_t hue8 = hue16 >> 8; - uint16_t h16_128 = hue16 >> 7; + unsigned h16_128 = hue16 >> 7; if ( h16_128 & 0x100) { hue8 = 255 - (h16_128 >> 1); } else { @@ -2140,9 +2163,9 @@ uint16_t mode_colorwaves() { } brightnesstheta16 += brightnessthetainc16; - uint16_t b16 = sin16(brightnesstheta16) + 32768; + unsigned b16 = sin16(brightnesstheta16) + 32768; - uint16_t bri16 = (uint32_t)((uint32_t)b16 * (uint32_t)b16) / 65536; + unsigned bri16 = (uint32_t)((uint32_t)b16 * (uint32_t)b16) / 65536; uint8_t bri8 = (uint32_t)(((uint32_t)bri16) * brightdepth) / 65536; bri8 += (255 - brightdepth); @@ -2171,11 +2194,8 @@ static const char _data_FX_MODE_BPM[] PROGMEM = "Bpm@!;!;!;;sx=64"; uint16_t mode_fillnoise8() { if (SEGENV.call == 0) SEGENV.step = random16(12345); - //CRGB fastled_col; for (int i = 0; i < SEGLEN; i++) { - uint8_t index = inoise8(i * SEGLEN, SEGENV.step + i * SEGLEN); - //fastled_col = ColorFromPalette(SEGPALETTE, index, 255, LINEARBLEND); - //SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); + unsigned index = inoise8(i * SEGLEN, SEGENV.step + i * SEGLEN); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } SEGENV.step += beatsin8(SEGMENT.speed, 1, 6); //10,1,4 @@ -2186,21 +2206,18 @@ static const char _data_FX_MODE_FILLNOISE8[] PROGMEM = "Fill Noise@!;!;!"; uint16_t mode_noise16_1() { - uint16_t scale = 320; // the "zoom factor" for the noise - //CRGB fastled_col; + unsigned scale = 320; // the "zoom factor" for the noise SEGENV.step += (1 + SEGMENT.speed/16); for (int i = 0; i < SEGLEN; i++) { - uint16_t shift_x = beatsin8(11); // the x position of the noise field swings @ 17 bpm - uint16_t shift_y = SEGENV.step/42; // the y position becomes slowly incremented - uint16_t real_x = (i + shift_x) * scale; // the x position of the noise field swings @ 17 bpm - uint16_t real_y = (i + shift_y) * scale; // the y position becomes slowly incremented + unsigned shift_x = beatsin8(11); // the x position of the noise field swings @ 17 bpm + unsigned shift_y = SEGENV.step/42; // the y position becomes slowly incremented + unsigned real_x = (i + shift_x) * scale; // the x position of the noise field swings @ 17 bpm + unsigned real_y = (i + shift_y) * scale; // the y position becomes slowly incremented uint32_t real_z = SEGENV.step; // the z position becomes quickly incremented - uint8_t noise = inoise16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down - uint8_t index = sin8(noise * 3); // map LED color based on noise data + unsigned noise = inoise16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down + unsigned index = sin8(noise * 3); // map LED color based on noise data - //fastled_col = ColorFromPalette(SEGPALETTE, index, 255, LINEARBLEND); // With that value, look up the 8 bit colour palette value and assign it to the current LED. - //SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } @@ -2210,18 +2227,15 @@ static const char _data_FX_MODE_NOISE16_1[] PROGMEM = "Noise 1@!;!;!"; uint16_t mode_noise16_2() { - uint16_t scale = 1000; // the "zoom factor" for the noise - //CRGB fastled_col; + unsigned scale = 1000; // the "zoom factor" for the noise SEGENV.step += (1 + (SEGMENT.speed >> 1)); for (int i = 0; i < SEGLEN; i++) { - uint16_t shift_x = SEGENV.step >> 6; // x as a function of time + unsigned shift_x = SEGENV.step >> 6; // x as a function of time uint32_t real_x = (i + shift_x) * scale; // calculate the coordinates within the noise field - uint8_t noise = inoise16(real_x, 0, 4223) >> 8; // get the noise data and scale it down - uint8_t index = sin8(noise * 3); // map led color based on noise data + unsigned noise = inoise16(real_x, 0, 4223) >> 8; // get the noise data and scale it down + unsigned index = sin8(noise * 3); // map led color based on noise data - //fastled_col = ColorFromPalette(SEGPALETTE, index, noise, LINEARBLEND); // With that value, look up the 8 bit colour palette value and assign it to the current LED. - //SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0, noise)); } @@ -2231,21 +2245,18 @@ static const char _data_FX_MODE_NOISE16_2[] PROGMEM = "Noise 2@!;!;!"; uint16_t mode_noise16_3() { - uint16_t scale = 800; // the "zoom factor" for the noise - //CRGB fastled_col; + unsigned scale = 800; // the "zoom factor" for the noise SEGENV.step += (1 + SEGMENT.speed); for (int i = 0; i < SEGLEN; i++) { - uint16_t shift_x = 4223; // no movement along x and y - uint16_t shift_y = 1234; + unsigned shift_x = 4223; // no movement along x and y + unsigned shift_y = 1234; uint32_t real_x = (i + shift_x) * scale; // calculate the coordinates within the noise field uint32_t real_y = (i + shift_y) * scale; // based on the precalculated positions uint32_t real_z = SEGENV.step*8; - uint8_t noise = inoise16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down - uint8_t index = sin8(noise * 3); // map led color based on noise data + unsigned noise = inoise16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down + unsigned index = sin8(noise * 3); // map led color based on noise data - //fastled_col = ColorFromPalette(SEGPALETTE, index, noise, LINEARBLEND); // With that value, look up the 8 bit colour palette value and assign it to the current LED. - //SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0, noise)); } @@ -2256,12 +2267,9 @@ static const char _data_FX_MODE_NOISE16_3[] PROGMEM = "Noise 3@!;!;!"; //https://github.com/aykevl/ledstrip-spark/blob/master/ledstrip.ino uint16_t mode_noise16_4() { - //CRGB fastled_col; uint32_t stp = (strip.now * SEGMENT.speed) >> 7; for (int i = 0; i < SEGLEN; i++) { - int16_t index = inoise16(uint32_t(i) << 12, stp); - //fastled_col = ColorFromPalette(SEGPALETTE, index); - //SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); + int index = inoise16(uint32_t(i) << 12, stp); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } return FRAMETIME; @@ -2271,7 +2279,7 @@ static const char _data_FX_MODE_NOISE16_4[] PROGMEM = "Noise 4@!;!;!"; //based on https://gist.github.com/kriegsman/5408ecd397744ba0393e uint16_t mode_colortwinkle() { - uint16_t dataSize = (SEGLEN+7) >> 3; //1 bit per LED + unsigned dataSize = (SEGLEN+7) >> 3; //1 bit per LED if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed CRGB fastled_col, prev; @@ -2280,8 +2288,8 @@ uint16_t mode_colortwinkle() { for (int i = 0; i < SEGLEN; i++) { fastled_col = SEGMENT.getPixelColor(i); prev = fastled_col; - uint16_t index = i >> 3; - uint8_t bitNum = i & 0x07; + unsigned index = i >> 3; + unsigned bitNum = i & 0x07; bool fadeUp = bitRead(SEGENV.data[index], bitNum); if (fadeUp) { @@ -2310,8 +2318,8 @@ uint16_t mode_colortwinkle() { int i = random16(SEGLEN); if (SEGMENT.getPixelColor(i) == 0) { fastled_col = ColorFromPalette(SEGPALETTE, random8(), 64, NOBLEND); - uint16_t index = i >> 3; - uint8_t bitNum = i & 0x07; + unsigned index = i >> 3; + unsigned bitNum = i & 0x07; bitWrite(SEGENV.data[index], bitNum, true); SEGMENT.setPixelColor(i, fastled_col); break; //only spawn 1 new pixel per frame per 50 LEDs @@ -2326,18 +2334,15 @@ static const char _data_FX_MODE_COLORTWINKLE[] PROGMEM = "Colortwinkles@Fade spe //Calm effect, like a lake at night uint16_t mode_lake() { - uint8_t sp = SEGMENT.speed/10; + unsigned sp = SEGMENT.speed/10; int wave1 = beatsin8(sp +2, -64,64); int wave2 = beatsin8(sp +1, -64,64); - uint8_t wave3 = beatsin8(sp +2, 0,80); - //CRGB fastled_col; + int wave3 = beatsin8(sp +2, 0,80); for (int i = 0; i < SEGLEN; i++) { int index = cos8((i*15)+ wave1)/2 + cubicwave8((i*23)+ wave2)/2; uint8_t lum = (index > wave3) ? index - wave3 : 0; - //fastled_col = ColorFromPalette(SEGPALETTE, map(index,0,255,0,240), lum, LINEARBLEND); - //SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, false, 0, lum)); } @@ -2355,35 +2360,45 @@ uint16_t mode_meteor() { byte* trail = SEGENV.data; - const unsigned meteorSize= 1 + SEGLEN / 20; // 5% - uint16_t counter = strip.now * ((SEGMENT.speed >> 2) +8); + const unsigned meteorSize = 1 + SEGLEN / 20; // 5% + unsigned counter = strip.now * ((SEGMENT.speed >> 2) +8); uint16_t in = counter * SEGLEN >> 16; - const int max = SEGMENT.palette==5 || !SEGMENT.check1 ? 240 : 255; + const int max = SEGMENT.palette==5 ? 239 : 255; // "* Colors only" palette blends end with start // fade all leds to colors[1] in LEDs one step for (int i = 0; i < SEGLEN; i++) { if (random8() <= 255 - SEGMENT.intensity) { - byte meteorTrailDecay = 162 + random8(92); + int meteorTrailDecay = 128 + random8(127); trail[i] = scale8(trail[i], meteorTrailDecay); - uint32_t col = SEGMENT.check1 ? SEGMENT.color_from_palette(i, true, false, 0, trail[i]) : SEGMENT.color_from_palette(trail[i], false, true, 255); + int index = trail[i]; + int idx = 255; + int bri = SEGMENT.palette==35 || SEGMENT.palette==36 ? 255 : trail[i]; + if (!SEGMENT.check1) { + idx = 0; + index = map(i,0,SEGLEN,0,max); + bri = trail[i]; + } + uint32_t col = SEGMENT.color_from_palette(index, false, false, idx, bri); // full brightness for Fire SEGMENT.setPixelColor(i, col); } } // draw meteor for (unsigned j = 0; j < meteorSize; j++) { - uint16_t index = in + j; - if (index >= SEGLEN) { - index -= SEGLEN; + int index = (in + j) % SEGLEN; + int idx = 255; + int i = trail[index] = max; + if (!SEGMENT.check1) { + i = map(index,0,SEGLEN,0,max); + idx = 0; } - trail[index] = max; - uint32_t col = SEGMENT.check1 ? SEGMENT.color_from_palette(index, true, false, 0, trail[index]) : SEGMENT.color_from_palette(trail[index], false, true, 255); + uint32_t col = SEGMENT.color_from_palette(i, false, false, idx, 255); // full brightness SEGMENT.setPixelColor(index, col); } return FRAMETIME; } -static const char _data_FX_MODE_METEOR[] PROGMEM = "Meteor@!,Trail,,,,Gradient;;!;1"; +static const char _data_FX_MODE_METEOR[] PROGMEM = "Meteor@!,Trail,,,,Gradient;!;!;1"; // smooth meteor effect @@ -2395,12 +2410,12 @@ uint16_t mode_meteor_smooth() { byte* trail = SEGENV.data; - const unsigned meteorSize= 1+ SEGLEN / 20; // 5% + const unsigned meteorSize = 1+ SEGLEN / 20; // 5% uint16_t in = map((SEGENV.step >> 6 & 0xFF), 0, 255, 0, SEGLEN -1); const int max = SEGMENT.palette==5 || !SEGMENT.check1 ? 240 : 255; // fade all leds to colors[1] in LEDs one step - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { if (/*trail[i] != 0 &&*/ random8() <= 255 - SEGMENT.intensity) { int change = trail[i] + 4 - random8(24); //change each time between -20 and +4 trail[i] = constrain(change, 0, max); @@ -2411,7 +2426,7 @@ uint16_t mode_meteor_smooth() { // draw meteor for (unsigned j = 0; j < meteorSize; j++) { - uint16_t index = in + j; + unsigned index = in + j; if (index >= SEGLEN) { index -= SEGLEN; } @@ -2429,7 +2444,7 @@ static const char _data_FX_MODE_METEOR_SMOOTH[] PROGMEM = "Meteor Smooth@!,Trail //Railway Crossing / Christmas Fairy lights uint16_t mode_railway() { if (SEGLEN == 1) return mode_static(); - uint16_t dur = (256 - SEGMENT.speed) * 40; + unsigned dur = (256 - SEGMENT.speed) * 40; uint16_t rampdur = (dur * SEGMENT.intensity) >> 8; if (SEGENV.step > dur) { @@ -2437,10 +2452,10 @@ uint16_t mode_railway() { SEGENV.step = 0; SEGENV.aux0 = !SEGENV.aux0; } - uint8_t pos = 255; + unsigned pos = 255; if (rampdur != 0) { - uint16_t p0 = (SEGENV.step * 255) / rampdur; + unsigned p0 = (SEGENV.step * 255) / rampdur; if (p0 < 255) pos = p0; } if (SEGENV.aux0) pos = 255 - pos; @@ -2474,43 +2489,42 @@ typedef struct Ripple { #else #define MAX_RIPPLES 100 #endif -uint16_t ripple_base() -{ - uint16_t maxRipples = min(1 + (SEGLEN >> 2), MAX_RIPPLES); // 56 max for 16 segment ESP8266 - uint16_t dataSize = sizeof(ripple) * maxRipples; +static uint16_t ripple_base() { + unsigned maxRipples = min(1 + (SEGLEN >> 2), MAX_RIPPLES); // 56 max for 16 segment ESP8266 + unsigned dataSize = sizeof(ripple) * maxRipples; if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Ripple* ripples = reinterpret_cast(SEGENV.data); //draw wave - for (int i = 0; i < maxRipples; i++) { - uint16_t ripplestate = ripples[i].state; + for (unsigned i = 0; i < maxRipples; i++) { + unsigned ripplestate = ripples[i].state; if (ripplestate) { - uint8_t rippledecay = (SEGMENT.speed >> 4) +1; //faster decay if faster propagation - uint16_t rippleorigin = ripples[i].pos; + unsigned rippledecay = (SEGMENT.speed >> 4) +1; //faster decay if faster propagation + unsigned rippleorigin = ripples[i].pos; uint32_t col = SEGMENT.color_from_palette(ripples[i].color, false, false, 255); - uint16_t propagation = ((ripplestate/rippledecay - 1) * (SEGMENT.speed + 1)); - int16_t propI = propagation >> 8; - uint8_t propF = propagation & 0xFF; - uint8_t amp = (ripplestate < 17) ? triwave8((ripplestate-1)*8) : map(ripplestate,17,255,255,2); + unsigned propagation = ((ripplestate/rippledecay - 1) * (SEGMENT.speed + 1)); + int propI = propagation >> 8; + unsigned propF = propagation & 0xFF; + unsigned amp = (ripplestate < 17) ? triwave8((ripplestate-1)*8) : map(ripplestate,17,255,255,2); #ifndef WLED_DISABLE_2D if (SEGMENT.is2D()) { propI /= 2; - uint16_t cx = rippleorigin >> 8; - uint16_t cy = rippleorigin & 0xFF; - uint8_t mag = scale8(sin8((propF>>2)), amp); - if (propI > 0) SEGMENT.draw_circle(cx, cy, propI, color_blend(SEGMENT.getPixelColorXY(cx + propI, cy), col, mag)); + unsigned cx = rippleorigin >> 8; + unsigned cy = rippleorigin & 0xFF; + unsigned mag = scale8(sin8((propF>>2)), amp); + if (propI > 0) SEGMENT.drawCircle(cx, cy, propI, color_blend(SEGMENT.getPixelColorXY(cx + propI, cy), col, mag), true); } else #endif { - int16_t left = rippleorigin - propI -1; - for (int16_t v = left; v < left +4; v++) { - uint8_t mag = scale8(cubicwave8((propF>>2)+(v-left)*64), amp); - SEGMENT.setPixelColor(v, color_blend(SEGMENT.getPixelColor(v), col, mag)); // TODO - int16_t w = left + propI*2 + 3 -(v-left); - SEGMENT.setPixelColor(w, color_blend(SEGMENT.getPixelColor(w), col, mag)); // TODO + int left = rippleorigin - propI -1; + int right = rippleorigin + propI +3; + for (int v = 0; v < 4; v++) { + unsigned mag = scale8(cubicwave8((propF>>2)+(v-left)*64), amp); + SEGMENT.setPixelColor(left + v, color_blend(SEGMENT.getPixelColor(left + v), col, mag)); // TODO + SEGMENT.setPixelColor(right - v, color_blend(SEGMENT.getPixelColor(right - v), col, mag)); // TODO } } ripplestate += rippledecay; @@ -2561,24 +2575,24 @@ static const char _data_FX_MODE_RIPPLE_RAINBOW[] PROGMEM = "Ripple Rainbow@!,Wav // // TwinkleFOX: Twinkling 'holiday' lights that fade in and out. // Colors are chosen from a palette. Read more about this effect using the link above! -CRGB twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool cat) +static CRGB twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool cat) { // Overall twinkle speed (changed) - uint16_t ticks = ms / SEGENV.aux0; - uint8_t fastcycle8 = ticks; - uint16_t slowcycle16 = (ticks >> 8) + salt; + unsigned ticks = ms / SEGENV.aux0; + unsigned fastcycle8 = ticks; + unsigned slowcycle16 = (ticks >> 8) + salt; slowcycle16 += sin8(slowcycle16); slowcycle16 = (slowcycle16 * 2053) + 1384; - uint8_t slowcycle8 = (slowcycle16 & 0xFF) + (slowcycle16 >> 8); + unsigned slowcycle8 = (slowcycle16 & 0xFF) + (slowcycle16 >> 8); // Overall twinkle density. // 0 (NONE lit) to 8 (ALL lit at once). // Default is 5. - uint8_t twinkleDensity = (SEGMENT.intensity >> 5) +1; + unsigned twinkleDensity = (SEGMENT.intensity >> 5) +1; - uint8_t bright = 0; + unsigned bright = 0; if (((slowcycle8 & 0x0E)/2) < twinkleDensity) { - uint8_t ph = fastcycle8; + unsigned ph = fastcycle8; // This is like 'triwave8', which produces a // symmetrical up-and-down triangle sawtooth waveform, except that this // function produces a triangle wave with a faster attack and a slower decay @@ -2595,7 +2609,7 @@ CRGB twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool cat) } } - uint8_t hue = slowcycle8 - salt; + unsigned hue = slowcycle8 - salt; CRGB c; if (bright > 0) { c = ColorFromPalette(SEGPALETTE, hue, bright, NOBLEND); @@ -2605,7 +2619,7 @@ CRGB twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool cat) // way that incandescent bulbs fade toward 'red' as they dim. if (fastcycle8 >= 128) { - uint8_t cooling = (fastcycle8 - 128) >> 4; + unsigned cooling = (fastcycle8 - 128) >> 4; c.g = qsub8(c.g, cooling); c.b = qsub8(c.b, cooling * 2); } @@ -2621,7 +2635,7 @@ CRGB twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool cat) // "CalculateOneTwinkle" on each pixel. It then displays // either the twinkle color of the background color, // whichever is brighter. -uint16_t twinklefox_base(bool cat) +static uint16_t twinklefox_base(bool cat) { // "PRNG16" is the pseudorandom number generator // It MUST be reset to the same starting value each time @@ -2635,7 +2649,7 @@ uint16_t twinklefox_base(bool cat) // Set up the background color, "bg". CRGB bg = CRGB(SEGCOLOR(1)); - uint8_t bglight = bg.getAverageLight(); + unsigned bglight = bg.getAverageLight(); if (bglight > 64) { bg.nscale8_video(16); // very bright, so scale to 1/16th } else if (bglight > 16) { @@ -2644,25 +2658,25 @@ uint16_t twinklefox_base(bool cat) bg.nscale8_video(86); // dim, scale to 1/3rd. } - uint8_t backgroundBrightness = bg.getAverageLight(); + unsigned backgroundBrightness = bg.getAverageLight(); for (int i = 0; i < SEGLEN; i++) { PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; // next 'random' number - uint16_t myclockoffset16= PRNG16; // use that number as clock offset + unsigned myclockoffset16= PRNG16; // use that number as clock offset PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; // next 'random' number // use that number as clock speed adjustment factor (in 8ths, from 8/8ths to 23/8ths) - uint8_t myspeedmultiplierQ5_3 = ((((PRNG16 & 0xFF)>>4) + (PRNG16 & 0x0F)) & 0x0F) + 0x08; + unsigned myspeedmultiplierQ5_3 = ((((PRNG16 & 0xFF)>>4) + (PRNG16 & 0x0F)) & 0x0F) + 0x08; uint32_t myclock30 = (uint32_t)((strip.now * myspeedmultiplierQ5_3) >> 3) + myclockoffset16; - uint8_t myunique8 = PRNG16 >> 8; // get 'salt' value for this pixel + unsigned myunique8 = PRNG16 >> 8; // get 'salt' value for this pixel // We now have the adjusted 'clock' for this pixel, now we call // the function that computes what color the pixel should be based // on the "brightness = f( time )" idea. CRGB c = twinklefox_one_twinkle(myclock30, myunique8, cat); - uint8_t cbright = c.getAverageLight(); - int16_t deltabright = cbright - backgroundBrightness; + unsigned cbright = c.getAverageLight(); + int deltabright = cbright - backgroundBrightness; if (deltabright >= 32 || (!bg)) { // If the new pixel is significantly brighter than the background color, // use the new color. @@ -2718,10 +2732,10 @@ uint16_t mode_halloween_eyes() }; if (SEGLEN == 1) return mode_static(); - const uint16_t maxWidth = strip.isMatrix ? SEGMENT.virtualWidth() : SEGLEN; - const uint16_t HALLOWEEN_EYE_SPACE = MAX(2, strip.isMatrix ? SEGMENT.virtualWidth()>>4: SEGLEN>>5); - const uint16_t HALLOWEEN_EYE_WIDTH = HALLOWEEN_EYE_SPACE/2; - uint16_t eyeLength = (2*HALLOWEEN_EYE_WIDTH) + HALLOWEEN_EYE_SPACE; + const unsigned maxWidth = strip.isMatrix ? SEGMENT.virtualWidth() : SEGLEN; + const unsigned HALLOWEEN_EYE_SPACE = MAX(2, strip.isMatrix ? SEGMENT.virtualWidth()>>4: SEGLEN>>5); + const unsigned HALLOWEEN_EYE_WIDTH = HALLOWEEN_EYE_SPACE/2; + unsigned eyeLength = (2*HALLOWEEN_EYE_WIDTH) + HALLOWEEN_EYE_SPACE; if (eyeLength >= maxWidth) return mode_static(); //bail if segment too short if (!SEGENV.allocateData(sizeof(EyeData))) return mode_static(); //allocation failed @@ -2730,7 +2744,7 @@ uint16_t mode_halloween_eyes() if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); //fill background data.state = static_cast(data.state % eyeState::count); - uint16_t duration = max(uint16_t{1u}, data.duration); + unsigned duration = max(uint16_t{1u}, data.duration); const uint32_t elapsedTime = strip.now - data.startTime; switch (data.state) { @@ -2755,9 +2769,9 @@ uint16_t mode_halloween_eyes() // - randomly switch to the blink (sub-)state, and initialize it with a blink duration (more precisely, a blink end time stamp) // - never switch to the blink state if the animation just started or is about to end - uint16_t start2ndEye = data.startPos + HALLOWEEN_EYE_WIDTH + HALLOWEEN_EYE_SPACE; + unsigned start2ndEye = data.startPos + HALLOWEEN_EYE_WIDTH + HALLOWEEN_EYE_SPACE; // If the user reduces the input while in this state, limit the duration. - duration = min(duration, static_cast(128u + (SEGMENT.intensity * 64u))); + duration = min(duration, (128u + (SEGMENT.intensity * 64u))); constexpr uint32_t minimumOnTimeBegin = 1024u; constexpr uint32_t minimumOnTimeEnd = 1024u; @@ -2781,10 +2795,10 @@ uint16_t mode_halloween_eyes() if (c != backgroundColor) { // render eyes - for (int i = 0; i < HALLOWEEN_EYE_WIDTH; i++) { + for (unsigned i = 0; i < HALLOWEEN_EYE_WIDTH; i++) { if (strip.isMatrix) { - SEGMENT.setPixelColorXY(data.startPos + i, SEGMENT.offset, c); - SEGMENT.setPixelColorXY(start2ndEye + i, SEGMENT.offset, c); + SEGMENT.setPixelColorXY(data.startPos + i, (unsigned)SEGMENT.offset, c); + SEGMENT.setPixelColorXY(start2ndEye + i, (unsigned)SEGMENT.offset, c); } else { SEGMENT.setPixelColor(data.startPos + i, c); SEGMENT.setPixelColor(start2ndEye + i, c); @@ -2807,7 +2821,7 @@ uint16_t mode_halloween_eyes() // - select a duration // - immediately switch to eyes-off state - const uint16_t eyeOffTimeBase = SEGMENT.speed*128u; + const unsigned eyeOffTimeBase = SEGMENT.speed*128u; duration = eyeOffTimeBase + random16(eyeOffTimeBase); data.duration = duration; data.state = eyeState::off; @@ -2818,8 +2832,8 @@ uint16_t mode_halloween_eyes() // - not much to do here // If the user reduces the input while in this state, limit the duration. - const uint16_t eyeOffTimeBase = SEGMENT.speed*128u; - duration = min(duration, static_cast(2u * eyeOffTimeBase)); + const unsigned eyeOffTimeBase = SEGMENT.speed*128u; + duration = min(duration, (2u * eyeOffTimeBase)); break; } case eyeState::count: { @@ -2855,10 +2869,10 @@ static const char _data_FX_MODE_HALLOWEEN_EYES[] PROGMEM = "Halloween Eyes@Eye o //Speed slider sets amount of LEDs lit, intensity sets unlit uint16_t mode_static_pattern() { - uint16_t lit = 1 + SEGMENT.speed; - uint16_t unlit = 1 + SEGMENT.intensity; + unsigned lit = 1 + SEGMENT.speed; + unsigned unlit = 1 + SEGMENT.intensity; bool drawingLit = true; - uint16_t cnt = 0; + unsigned cnt = 0; for (int i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, (drawingLit) ? SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0) : SEGCOLOR(1)); @@ -2876,9 +2890,9 @@ static const char _data_FX_MODE_STATIC_PATTERN[] PROGMEM = "Solid Pattern@Fg siz uint16_t mode_tri_static_pattern() { - uint8_t segSize = (SEGMENT.intensity >> 5) +1; - uint8_t currSeg = 0; - uint16_t currSegCount = 0; + unsigned segSize = (SEGMENT.intensity >> 5) +1; + unsigned currSeg = 0; + unsigned currSegCount = 0; for (int i = 0; i < SEGLEN; i++) { if ( currSeg % 3 == 0 ) { @@ -2886,7 +2900,7 @@ uint16_t mode_tri_static_pattern() } else if( currSeg % 3 == 1) { SEGMENT.setPixelColor(i, SEGCOLOR(1)); } else { - SEGMENT.setPixelColor(i, (SEGCOLOR(2) > 0 ? SEGCOLOR(2) : WHITE)); + SEGMENT.setPixelColor(i, SEGCOLOR(2)); } currSegCount += 1; if (currSegCount >= segSize) { @@ -2900,25 +2914,25 @@ uint16_t mode_tri_static_pattern() static const char _data_FX_MODE_TRI_STATIC_PATTERN[] PROGMEM = "Solid Pattern Tri@,Size;1,2,3;;;pal=0"; -uint16_t spots_base(uint16_t threshold) +static uint16_t spots_base(uint16_t threshold) { if (SEGLEN == 1) return mode_static(); if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); - uint16_t maxZones = SEGLEN >> 2; - uint16_t zones = 1 + ((SEGMENT.intensity * maxZones) >> 8); - uint16_t zoneLen = SEGLEN / zones; - uint16_t offset = (SEGLEN - zones * zoneLen) >> 1; + unsigned maxZones = SEGLEN >> 2; + unsigned zones = 1 + ((SEGMENT.intensity * maxZones) >> 8); + unsigned zoneLen = SEGLEN / zones; + unsigned offset = (SEGLEN - zones * zoneLen) >> 1; - for (int z = 0; z < zones; z++) + for (unsigned z = 0; z < zones; z++) { - uint16_t pos = offset + z * zoneLen; - for (int i = 0; i < zoneLen; i++) + unsigned pos = offset + z * zoneLen; + for (unsigned i = 0; i < zoneLen; i++) { - uint16_t wave = triwave16((i * 0xFFFF) / zoneLen); + unsigned wave = triwave16((i * 0xFFFF) / zoneLen); if (wave > threshold) { - uint16_t index = 0 + pos + i; - uint8_t s = (wave - threshold)*255 / (0xFFFF - threshold); + unsigned index = 0 + pos + i; + unsigned s = (wave - threshold)*255 / (0xFFFF - threshold); SEGMENT.setPixelColor(index, color_blend(SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0), SEGCOLOR(1), 255-s)); } } @@ -2939,9 +2953,9 @@ static const char _data_FX_MODE_SPOTS[] PROGMEM = "Spots@Spread,Width,,,,,Overla //Intensity slider sets number of "lights", LEDs per light fade in and out uint16_t mode_spots_fade() { - uint16_t counter = strip.now * ((SEGMENT.speed >> 2) +8); - uint16_t t = triwave16(counter); - uint16_t tr = (t >> 1) + (t >> 2); + unsigned counter = strip.now * ((SEGMENT.speed >> 2) +8); + unsigned t = triwave16(counter); + unsigned tr = (t >> 1) + (t >> 2); return spots_base(tr); } static const char _data_FX_MODE_SPOTS_FADE[] PROGMEM = "Spots Fade@Spread,Width,,,,,Overlay;!,!;!"; @@ -2960,9 +2974,9 @@ typedef struct Ball { uint16_t mode_bouncing_balls(void) { if (SEGLEN == 1) return mode_static(); //allocate segment data - const uint16_t strips = SEGMENT.nrOfVStrips(); // adapt for 2D + const unsigned strips = SEGMENT.nrOfVStrips(); // adapt for 2D const size_t maxNumBalls = 16; - uint16_t dataSize = sizeof(ball) * maxNumBalls; + unsigned dataSize = sizeof(ball) * maxNumBalls; if (!SEGENV.allocateData(dataSize * strips)) return mode_static(); //allocation failed Ball* balls = reinterpret_cast(SEGENV.data); @@ -2976,7 +2990,7 @@ uint16_t mode_bouncing_balls(void) { static void runStrip(size_t stripNr, Ball* balls) { // number of balls based on intensity setting to max of 7 (cycles colors) // non-chosen color is a random color - uint16_t numBalls = (SEGMENT.intensity * (maxNumBalls - 1)) / 255 + 1; // minimum 1 ball + unsigned numBalls = (SEGMENT.intensity * (maxNumBalls - 1)) / 255 + 1; // minimum 1 ball const float gravity = -9.81f; // standard value of gravity const bool hasCol2 = SEGCOLOR(2); const unsigned long time = strip.now; @@ -3013,13 +3027,17 @@ uint16_t mode_bouncing_balls(void) { } int pos = roundf(balls[i].height * (SEGLEN - 1)); + #ifdef WLED_USE_AA_PIXELS if (SEGLEN<32) SEGMENT.setPixelColor(indexToVStrip(pos, stripNr), color); // encode virtual strip into index else SEGMENT.setPixelColor(balls[i].height + (stripNr+1)*10.0f, color); + #else + SEGMENT.setPixelColor(indexToVStrip(pos, stripNr), color); // encode virtual strip into index + #endif } } }; - for (int stripNr=0; stripNr(SEGENV.data); // number of balls based on intensity setting to max of 16 (cycles colors) // non-chosen color is a random color - uint8_t numBalls = SEGMENT.intensity/16 + 1; + unsigned numBalls = SEGMENT.intensity/16 + 1; bool hasCol2 = SEGCOLOR(2); if (SEGENV.call == 0) { SEGMENT.fill(hasCol2 ? BLACK : SEGCOLOR(1)); // start clean - for (int i = 0; i < maxNumBalls; i++) { + for (unsigned i = 0; i < maxNumBalls; i++) { balls[i].lastBounceUpdate = strip.now; balls[i].velocity = 20.0f * float(random16(1000, 10000))/10000.0f; // number from 1 to 10 if (random8()<128) balls[i].velocity = -balls[i].velocity; // 50% chance of reverse direction @@ -3071,7 +3089,7 @@ static uint16_t rolling_balls(void) { if (!SEGMENT.check2) SEGMENT.fill(hasCol2 ? BLACK : SEGCOLOR(1)); // don't fill with background color if user wants to see trails } - for (int i = 0; i < numBalls; i++) { + for (unsigned i = 0; i < numBalls; i++) { float timeSinceLastUpdate = float((strip.now - balls[i].lastBounceUpdate))/cfac; float thisHeight = balls[i].height + balls[i].velocity * timeSinceLastUpdate; // this method keeps higher resolution // test if intensity level was increased and some balls are way off the track then put them back @@ -3087,7 +3105,7 @@ static uint16_t rolling_balls(void) { } // check for collisions if (SEGMENT.check1) { - for (int j = i+1; j < numBalls; j++) { + for (unsigned j = i+1; j < numBalls; j++) { if (balls[j].velocity != balls[i].velocity) { // tcollided + balls[j].lastBounceUpdate is acutal time of collision (this keeps precision with long to float conversions) float tcollided = (cfac*(balls[i].height - balls[j].height) + @@ -3117,7 +3135,7 @@ static uint16_t rolling_balls(void) { if (thisHeight < 0.0f) thisHeight = 0.0f; if (thisHeight > 1.0f) thisHeight = 1.0f; - uint16_t pos = round(thisHeight * (SEGLEN - 1)); + unsigned pos = round(thisHeight * (SEGLEN - 1)); SEGMENT.setPixelColor(pos, color); balls[i].lastBounceUpdate = strip.now; balls[i].height = thisHeight; @@ -3131,10 +3149,10 @@ static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of b /* * Sinelon stolen from FASTLED examples */ -uint16_t sinelon_base(bool dual, bool rainbow=false) { +static uint16_t sinelon_base(bool dual, bool rainbow=false) { if (SEGLEN == 1) return mode_static(); SEGMENT.fade_out(SEGMENT.intensity); - uint16_t pos = beatsin16(SEGMENT.speed/10,0,SEGLEN-1); + unsigned pos = beatsin16(SEGMENT.speed/10,0,SEGLEN-1); if (SEGENV.call == 0) SEGENV.aux0 = pos; uint32_t color1 = SEGMENT.color_from_palette(pos, true, false, 0); uint32_t color2 = SEGCOLOR(2); @@ -3149,12 +3167,12 @@ uint16_t sinelon_base(bool dual, bool rainbow=false) { } if (SEGENV.aux0 != pos) { if (SEGENV.aux0 < pos) { - for (int i = SEGENV.aux0; i < pos ; i++) { + for (unsigned i = SEGENV.aux0; i < pos ; i++) { SEGMENT.setPixelColor(i, color1); if (dual) SEGMENT.setPixelColor(SEGLEN-1-i, color2); } } else { - for (int i = SEGENV.aux0; i > pos ; i--) { + for (unsigned i = SEGENV.aux0; i > pos ; i--) { SEGMENT.setPixelColor(i, color1); if (dual) SEGMENT.setPixelColor(SEGLEN-1-i, color2); } @@ -3186,23 +3204,30 @@ static const char _data_FX_MODE_SINELON_RAINBOW[] PROGMEM = "Sinelon Rainbow@!,T // utility function that will add random glitter to SEGMENT void glitter_base(uint8_t intensity, uint32_t col = ULTRAWHITE) { - if (intensity > random8()) { - if (SEGMENT.is2D()) { - SEGMENT.setPixelColorXY(random16(SEGMENT.virtualWidth()),random16(SEGMENT.virtualHeight()), col); - } else { - SEGMENT.setPixelColor(random16(SEGLEN), col); - } - } + if (intensity > random8()) SEGMENT.setPixelColor(random16(SEGLEN), col); } //Glitter with palette background, inspired by https://gist.github.com/kriegsman/062e10f7f07ba8518af6 uint16_t mode_glitter() { - if (!SEGMENT.check2) mode_palette(); // use "* Color 1" palette for solid background (replacing "Solid glitter") + if (!SEGMENT.check2) { // use "* Color 1" palette for solid background (replacing "Solid glitter") + unsigned counter = 0; + if (SEGMENT.speed != 0) { + counter = (strip.now * ((SEGMENT.speed >> 3) +1)) & 0xFFFF; + counter = counter >> 8; + } + + bool noWrap = (strip.paletteBlend == 2 || (strip.paletteBlend == 0 && SEGMENT.speed == 0)); + for (unsigned i = 0; i < SEGLEN; i++) { + unsigned colorIndex = (i * 255 / SEGLEN) - counter; + if (noWrap) colorIndex = map(colorIndex, 0, 255, 0, 240); //cut off blend at palette "end" + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(colorIndex, false, true, 255)); + } + } glitter_base(SEGMENT.intensity, SEGCOLOR(2) ? SEGCOLOR(2) : ULTRAWHITE); return FRAMETIME; } -static const char _data_FX_MODE_GLITTER[] PROGMEM = "Glitter@!,!,,,,,Overlay;1,2,Glitter color;!;;pal=0,m12=0"; //pixels +static const char _data_FX_MODE_GLITTER[] PROGMEM = "Glitter@!,!,,,,,Overlay;,,Glitter color;!;;pal=0,m12=0"; //pixels //Solid colour background with glitter (can be replaced by Glitter) @@ -3215,7 +3240,7 @@ uint16_t mode_solid_glitter() static const char _data_FX_MODE_SOLID_GLITTER[] PROGMEM = "Solid Glitter@,!;Bg,,Glitter color;;;m12=0"; -//each needs 19 bytes +//each needs 20 bytes //Spark type is used for popcorn, 1D fireworks, and drip typedef struct Spark { float pos, posX; @@ -3232,8 +3257,10 @@ typedef struct Spark { uint16_t mode_popcorn(void) { if (SEGLEN == 1) return mode_static(); //allocate segment data - uint16_t strips = SEGMENT.nrOfVStrips(); - uint16_t dataSize = sizeof(spark) * maxNumPopcorn; + unsigned strips = SEGMENT.nrOfVStrips(); + unsigned usablePopcorns = maxNumPopcorn; + if (usablePopcorns * strips * sizeof(spark) > FAIR_DATA_PER_SEG) usablePopcorns = FAIR_DATA_PER_SEG / (strips * sizeof(spark)) + 1; // at least 1 popcorn per vstrip + unsigned dataSize = sizeof(spark) * usablePopcorns; // on a matrix 64x64 this could consume a little less than 27kB when Bar expansion is used if (!SEGENV.allocateData(dataSize * strips)) return mode_static(); //allocation failed Spark* popcorn = reinterpret_cast(SEGENV.data); @@ -3242,14 +3269,14 @@ uint16_t mode_popcorn(void) { if (!SEGMENT.check2) SEGMENT.fill(hasCol2 ? BLACK : SEGCOLOR(1)); struct virtualStrip { - static void runStrip(uint16_t stripNr, Spark* popcorn) { - float gravity = -0.0001 - (SEGMENT.speed/200000.0); // m/s/s + static void runStrip(uint16_t stripNr, Spark* popcorn, unsigned usablePopcorns) { + float gravity = -0.0001f - (SEGMENT.speed/200000.0f); // m/s/s gravity *= SEGLEN; - uint8_t numPopcorn = SEGMENT.intensity*maxNumPopcorn/255; + unsigned numPopcorn = SEGMENT.intensity * usablePopcorns / 255; if (numPopcorn == 0) numPopcorn = 1; - for(int i = 0; i < numPopcorn; i++) { + for(unsigned i = 0; i < numPopcorn; i++) { if (popcorn[i].pos >= 0.0f) { // if kernel is active, update its position popcorn[i].pos += popcorn[i].vel; popcorn[i].vel += gravity; @@ -3257,7 +3284,7 @@ uint16_t mode_popcorn(void) { if (random8() < 2) { // POP!!! popcorn[i].pos = 0.01f; - uint16_t peakHeight = 128 + random8(128); //0-255 + unsigned peakHeight = 128 + random8(128); //0-255 peakHeight = (peakHeight * (SEGLEN -1)) >> 8; popcorn[i].vel = sqrtf(-2.0f * gravity * peakHeight); @@ -3274,15 +3301,15 @@ uint16_t mode_popcorn(void) { if (popcorn[i].pos >= 0.0f) { // draw now active popcorn (either active before or just popped) uint32_t col = SEGMENT.color_wheel(popcorn[i].colIndex); if (!SEGMENT.palette && popcorn[i].colIndex < NUM_COLORS) col = SEGCOLOR(popcorn[i].colIndex); - uint16_t ledIndex = popcorn[i].pos; + unsigned ledIndex = popcorn[i].pos; if (ledIndex < SEGLEN) SEGMENT.setPixelColor(indexToVStrip(ledIndex, stripNr), col); } } } }; - for (int stripNr=0; stripNr 1) { //allocate segment data - uint16_t dataSize = max(1, SEGLEN -1) *3; //max. 1365 pixels (ESP8266) + unsigned dataSize = max(1, SEGLEN -1) *3; //max. 1365 pixels (ESP8266) if (!SEGENV.allocateData(dataSize)) return candle(false); //allocation failed } //max. flicker range controlled by intensity - uint8_t valrange = SEGMENT.intensity; - uint8_t rndval = valrange >> 1; //max 127 + unsigned valrange = SEGMENT.intensity; + unsigned rndval = valrange >> 1; //max 127 //step (how much to move closer to target per frame) coarsely set by speed - uint8_t speedFactor = 4; + unsigned speedFactor = 4; if (SEGMENT.speed > 252) { //epilepsy speedFactor = 1; } else if (SEGMENT.speed > 99) { //regular candle (mode called every ~25 ms, so 4 frames to have a new target every 100ms) @@ -3315,13 +3342,13 @@ uint16_t candle(bool multi) speedFactor = 3; } //else 4 (slowest) - uint16_t numCandles = (multi) ? SEGLEN : 1; + unsigned numCandles = (multi) ? SEGLEN : 1; - for (int i = 0; i < numCandles; i++) + for (unsigned i = 0; i < numCandles; i++) { - uint16_t d = 0; //data location + unsigned d = 0; //data location - uint8_t s = SEGENV.aux0, s_target = SEGENV.aux1, fadeStep = SEGENV.step; + unsigned s = SEGENV.aux0, s_target = SEGENV.aux1, fadeStep = SEGENV.step; if (i > 0) { d = (i-1) *3; s = SEGENV.data[d]; s_target = SEGENV.data[d+1]; fadeStep = SEGENV.data[d+2]; @@ -3342,16 +3369,16 @@ uint16_t candle(bool multi) if (newTarget) { s_target = random8(rndval) + random8(rndval); //between 0 and rndval*2 -2 = 252 if (s_target < (rndval >> 1)) s_target = (rndval >> 1) + random8(rndval); - uint8_t offset = (255 - valrange); + unsigned offset = (255 - valrange); s_target += offset; - uint8_t dif = (s_target > s) ? s_target - s : s - s_target; + unsigned dif = (s_target > s) ? s_target - s : s - s_target; fadeStep = dif >> speedFactor; if (fadeStep == 0) fadeStep = 1; } - if (i > 0) { + if (i > 0) { SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), s)); SEGENV.data[d] = s; SEGENV.data[d+1] = s_target; SEGENV.data[d+2] = fadeStep; @@ -3404,15 +3431,15 @@ typedef struct particle { uint16_t mode_starburst(void) { if (SEGLEN == 1) return mode_static(); - uint16_t maxData = FAIR_DATA_PER_SEG; //ESP8266: 256 ESP32: 640 - uint8_t segs = strip.getActiveSegmentsNum(); + 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 if (segs <= (strip.getMaxSegments() /4)) maxData *= 2; //ESP8266: 1024 if <= 4 segs ESP32: 2560 if <= 8 segs - uint16_t maxStars = maxData / sizeof(star); //ESP8266: max. 4/9/19 stars/seg, ESP32: max. 10/21/42 stars/seg + unsigned maxStars = maxData / sizeof(star); //ESP8266: max. 4/9/19 stars/seg, ESP32: max. 10/21/42 stars/seg - uint8_t numStars = 1 + (SEGLEN >> 3); + unsigned numStars = 1 + (SEGLEN >> 3); if (numStars > maxStars) numStars = maxStars; - uint16_t dataSize = sizeof(star) * numStars; + unsigned dataSize = sizeof(star) * numStars; if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed @@ -3424,18 +3451,18 @@ uint16_t mode_starburst(void) { float particleIgnition = 250.0f; // How long to "flash" float particleFadeTime = 1500.0f; // Fade out time - for (int j = 0; j < numStars; j++) + for (unsigned j = 0; j < numStars; j++) { // speed to adjust chance of a burst, max is nearly always. if (random8((144-(SEGMENT.speed >> 1))) == 0 && stars[j].birth == 0) { // Pick a random color and location. - uint16_t startPos = (SEGLEN > 1) ? random16(SEGLEN-1) : 0; - float multiplier = (float)(random8())/255.0 * 1.0; + unsigned startPos = random16(SEGLEN-1); + float multiplier = (float)(random8())/255.0f * 1.0f; stars[j].color = CRGB(SEGMENT.color_wheel(random8())); stars[j].pos = startPos; - stars[j].vel = maxSpeed * (float)(random8())/255.0 * multiplier; + stars[j].vel = maxSpeed * (float)(random8())/255.0f * multiplier; stars[j].birth = it; stars[j].last = it; // more fragments means larger burst effect @@ -3450,7 +3477,7 @@ uint16_t mode_starburst(void) { if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); - for (int j=0; j> 1; + unsigned i = index >> 1; if (stars[j].fragment[i] > 0) { float loc = stars[j].fragment[i]; if (mirrored) loc -= (loc-stars[j].pos)*2; @@ -3524,18 +3551,18 @@ static const char _data_FX_MODE_STARBURST[] PROGMEM = "Fireworks Starburst@Chanc uint16_t mode_exploding_fireworks(void) { if (SEGLEN == 1) return mode_static(); - const uint16_t cols = SEGMENT.is2D() ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = SEGMENT.is2D() ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const int cols = SEGMENT.is2D() ? SEGMENT.virtualWidth() : 1; + const int rows = SEGMENT.is2D() ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); //allocate segment data - uint16_t maxData = FAIR_DATA_PER_SEG; //ESP8266: 256 ESP32: 640 - uint8_t segs = strip.getActiveSegmentsNum(); + 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 if (segs <= (strip.getMaxSegments() /4)) maxData *= 2; //ESP8266: 1024 if <= 4 segs ESP32: 2560 if <= 8 segs int maxSparks = maxData / sizeof(spark); //ESP8266: max. 21/42/85 sparks/seg, ESP32: max. 53/106/213 sparks/seg - uint16_t numSparks = min(2 + ((rows*cols) >> 1), maxSparks); - uint16_t dataSize = sizeof(spark) * numSparks; + unsigned numSparks = min(2 + ((rows*cols) >> 1), maxSparks); + unsigned dataSize = sizeof(spark) * numSparks; if (!SEGENV.allocateData(dataSize + sizeof(float))) return mode_static(); //allocation failed float *dying_gravity = reinterpret_cast(SEGENV.data + dataSize); @@ -3557,7 +3584,7 @@ uint16_t mode_exploding_fireworks(void) if (SEGENV.aux0 == 0) { //init flare flare->pos = 0; flare->posX = SEGMENT.is2D() ? random16(2,cols-3) : (SEGMENT.intensity > random8()); // will enable random firing side on 1D - uint16_t peakHeight = 75 + random8(180); //0-255 + unsigned peakHeight = 75 + random8(180); //0-255 peakHeight = (peakHeight * (rows -1)) >> 8; flare->vel = sqrtf(-2.0f * gravity * peakHeight); flare->velX = SEGMENT.is2D() ? (random8(9)-4)/64.0f : 0; // no X velocity on 1D @@ -3568,7 +3595,7 @@ uint16_t mode_exploding_fireworks(void) // launch if (flare->vel > 12 * gravity) { // flare - if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(int(flare->posX), rows - uint16_t(flare->pos) - 1, flare->col, flare->col, flare->col); + if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(unsigned(flare->posX), rows - uint16_t(flare->pos) - 1, flare->col, flare->col, flare->col); else SEGMENT.setPixelColor((flare->posX > 0.0f) ? rows - int(flare->pos) - 1 : int(flare->pos), flare->col, flare->col, flare->col); flare->pos += flare->vel; flare->pos = constrain(flare->pos, 0, rows-1); @@ -3588,12 +3615,12 @@ uint16_t mode_exploding_fireworks(void) * Explosion happens where the flare ended. * Size is proportional to the height. */ - int nSparks = flare->pos + random8(4); + unsigned nSparks = flare->pos + random8(4); nSparks = constrain(nSparks, 4, numSparks); // initialize sparks if (SEGENV.aux0 == 2) { - for (int i = 1; i < nSparks; i++) { + for (unsigned i = 1; i < nSparks; i++) { sparks[i].pos = flare->pos; sparks[i].posX = flare->posX; sparks[i].vel = (float(random16(20001)) / 10000.0f) - 0.9f; // from -0.9 to 1.1 @@ -3612,7 +3639,7 @@ uint16_t mode_exploding_fireworks(void) } if (sparks[1].col > 4) {//&& sparks[1].pos > 0) { // as long as our known spark is lit, work with all the sparks - for (int i = 1; i < nSparks; i++) { + for (unsigned i = 1; i < nSparks; i++) { sparks[i].pos += sparks[i].vel; sparks[i].posX += sparks[i].velX; sparks[i].vel += *dying_gravity; @@ -3621,14 +3648,14 @@ uint16_t mode_exploding_fireworks(void) if (sparks[i].pos > 0 && sparks[i].pos < rows) { if (SEGMENT.is2D() && !(sparks[i].posX >= 0 && sparks[i].posX < cols)) continue; - uint16_t prog = sparks[i].col; + unsigned prog = sparks[i].col; uint32_t spColor = (SEGMENT.palette) ? SEGMENT.color_wheel(sparks[i].colIndex) : SEGCOLOR(0); CRGB c = CRGB::Black; //HeatColor(sparks[i].col); if (prog > 300) { //fade from white to spark color c = CRGB(color_blend(spColor, WHITE, (prog - 300)*5)); } else if (prog > 45) { //fade from spark color to black c = CRGB(color_blend(BLACK, spColor, prog - 45)); - uint8_t cooling = (300 - prog) >> 5; + unsigned cooling = (300 - prog) >> 5; c.g = qsub8(c.g, cooling); c.b = qsub8(c.b, cooling * 2); } @@ -3636,7 +3663,7 @@ uint16_t mode_exploding_fireworks(void) else SEGMENT.setPixelColor(int(sparks[i].posX) ? rows - int(sparks[i].pos) - 1 : int(sparks[i].pos), c.red, c.green, c.blue); } } - SEGMENT.blur(16); + if (SEGMENT.check3) SEGMENT.blur(16); *dying_gravity *= .8f; // as sparks burn out they fall slower } else { SEGENV.aux0 = 6 + random8(10); //wait for this many frames @@ -3651,7 +3678,7 @@ uint16_t mode_exploding_fireworks(void) return FRAMETIME; } #undef MAX_SPARKS -static const char _data_FX_MODE_EXPLODING_FIREWORKS[] PROGMEM = "Fireworks 1D@Gravity,Firing side;!,!;!;12;pal=11,ix=128"; +static const char _data_FX_MODE_EXPLODING_FIREWORKS[] PROGMEM = "Fireworks 1D@Gravity,Firing side,,,,,,Blur;!,!;!;12;pal=11,ix=128"; /* @@ -3662,9 +3689,9 @@ uint16_t mode_drip(void) { if (SEGLEN == 1) return mode_static(); //allocate segment data - uint16_t strips = SEGMENT.nrOfVStrips(); + unsigned strips = SEGMENT.nrOfVStrips(); const int maxNumDrops = 4; - uint16_t dataSize = sizeof(spark) * maxNumDrops; + unsigned dataSize = sizeof(spark) * maxNumDrops; if (!SEGENV.allocateData(dataSize * strips)) return mode_static(); //allocation failed Spark* drops = reinterpret_cast(SEGENV.data); @@ -3673,13 +3700,13 @@ uint16_t mode_drip(void) struct virtualStrip { static void runStrip(uint16_t stripNr, Spark* drops) { - uint8_t numDrops = 1 + (SEGMENT.intensity >> 6); // 255>>6 = 3 + unsigned numDrops = 1 + (SEGMENT.intensity >> 6); // 255>>6 = 3 - float gravity = -0.0005 - (SEGMENT.speed/50000.0); + float gravity = -0.0005f - (SEGMENT.speed/50000.0f); gravity *= max(1, SEGLEN-1); int sourcedrop = 12; - for (int j=0;j= SEGLEN occasionally + unsigned pos = constrain(uint16_t(drops[j].pos) +i, 0, SEGLEN-1); //this is BAD, returns a pos >= SEGLEN occasionally SEGMENT.setPixelColor(indexToVStrip(pos, stripNr), color_blend(BLACK,SEGCOLOR(0),drops[j].col/i)); //spread pixel with fade while falling } @@ -3733,7 +3760,7 @@ uint16_t mode_drip(void) } }; - for (int stripNr=0; stripNr(SEGENV.data); @@ -3824,7 +3851,7 @@ uint16_t mode_tetrix(void) { } }; - for (int stripNr=0; stripNr> 5))+thisPhase) & 0xFF)/2 // factor=23 // Create a wave and add a phase change and add another wave with its own phase change. - + cos8((i*(1+ 2*(SEGMENT.speed >> 5))+thatPhase) & 0xFF)/2; // factor=15 // Hey, you can even change the frequencies if you wish. - uint8_t thisBright = qsub8(colorIndex, beatsin8(7,0, (128 - (SEGMENT.intensity>>1)))); - //CRGB color = ColorFromPalette(SEGPALETTE, colorIndex, thisBright, LINEARBLEND); - //SEGMENT.setPixelColor(i, color.red, color.green, color.blue); + for (unsigned i = 0; i < SEGLEN; i++) { // For each of the LED's in the strand, set color & brightness based on a wave as follows: + unsigned colorIndex = cubicwave8((i*(2+ 3*(SEGMENT.speed >> 5))+thisPhase) & 0xFF)/2 // factor=23 // Create a wave and add a phase change and add another wave with its own phase change. + + cos8((i*(1+ 2*(SEGMENT.speed >> 5))+thatPhase) & 0xFF)/2; // factor=15 // Hey, you can even change the frequencies if you wish. + unsigned thisBright = qsub8(colorIndex, beatsin8(7,0, (128 - (SEGMENT.intensity>>1)))); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(colorIndex, false, PALETTE_SOLID_WRAP, 0, thisBright)); } @@ -3864,12 +3889,12 @@ static const char _data_FX_MODE_PLASMA[] PROGMEM = "Plasma@Phase,!;!;!"; */ uint16_t mode_percent(void) { - uint8_t percent = SEGMENT.intensity; + unsigned percent = SEGMENT.intensity; percent = constrain(percent, 0, 200); - uint16_t active_leds = (percent < 100) ? roundf(SEGLEN * percent / 100.0f) + unsigned active_leds = (percent < 100) ? roundf(SEGLEN * percent / 100.0f) : roundf(SEGLEN * (200 - percent) / 100.0f); - uint8_t size = (1 + ((SEGMENT.speed * SEGLEN) >> 11)); + unsigned size = (1 + ((SEGMENT.speed * SEGLEN) >> 11)); if (SEGMENT.speed == 255) size = 255; if (percent <= 100) { @@ -3916,7 +3941,7 @@ static const char _data_FX_MODE_PERCENT[] PROGMEM = "Percent@,% of fill,,,,One c * (unimplemented?) tries to draw an ECG approximation on a 2D matrix */ uint16_t mode_heartbeat(void) { - uint8_t bpm = 40 + (SEGMENT.speed >> 3); + unsigned bpm = 40 + (SEGMENT.speed >> 3); uint32_t msPerBeat = (60000L / bpm); uint32_t secondBeat = (msPerBeat / 3); uint32_t bri_lower = SEGENV.aux1; @@ -3969,18 +3994,18 @@ 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 -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, CRGBPalette16& p, uint16_t cistart, uint16_t wavescale, uint8_t bri, uint16_t ioff) { - uint16_t ci = cistart; - uint16_t waveangle = ioff; - uint16_t wavescale_half = (wavescale >> 1) + 20; + unsigned ci = cistart; + unsigned waveangle = ioff; + unsigned wavescale_half = (wavescale >> 1) + 20; waveangle += ((120 + SEGMENT.intensity) * i); //original 250 * i - uint16_t s16 = sin16(waveangle) + 32768; - uint16_t cs = scale16(s16, wavescale_half) + wavescale_half; + unsigned s16 = sin16(waveangle) + 32768; + unsigned cs = scale16(s16, wavescale_half) + wavescale_half; ci += (cs * i); - uint16_t sindex16 = sin16(ci) + 32768; - uint8_t sindex8 = scale16(sindex16, 240); + unsigned sindex16 = sin16(ci) + 32768; + unsigned sindex8 = scale16(sindex16, 240); return ColorFromPalette(p, sindex8, bri, LINEARBLEND); } @@ -4006,13 +4031,13 @@ uint16_t mode_pacifica() // Increment the four "color index start" counters, one for each wave layer. // Each is incremented at a different speed, and the speeds vary over time. - uint16_t sCIStart1 = SEGENV.aux0, sCIStart2 = SEGENV.aux1, sCIStart3 = SEGENV.step, sCIStart4 = SEGENV.step >> 16; + unsigned sCIStart1 = SEGENV.aux0, sCIStart2 = SEGENV.aux1, sCIStart3 = SEGENV.step, sCIStart4 = SEGENV.step >> 16; uint32_t deltams = (FRAMETIME >> 2) + ((FRAMETIME * SEGMENT.speed) >> 7); uint64_t deltat = (strip.now >> 2) + ((strip.now * SEGMENT.speed) >> 7); strip.now = deltat; - uint16_t speedfactor1 = beatsin16(3, 179, 269); - uint16_t speedfactor2 = beatsin16(4, 179, 269); + unsigned speedfactor1 = beatsin16(3, 179, 269); + unsigned speedfactor2 = beatsin16(4, 179, 269); uint32_t deltams1 = (deltams * speedfactor1) / 256; uint32_t deltams2 = (deltams * speedfactor2) / 256; uint32_t deltams21 = (deltams1 + deltams2) / 2; @@ -4026,8 +4051,8 @@ uint16_t mode_pacifica() // Clear out the LED array to a dim background blue-green //SEGMENT.fill(132618); - uint8_t basethreshold = beatsin8( 9, 55, 65); - uint8_t wave = beat8( 7 ); + unsigned basethreshold = beatsin8( 9, 55, 65); + unsigned wave = beat8( 7 ); for (int i = 0; i < SEGLEN; i++) { CRGB c = CRGB(2, 6, 10); @@ -4038,12 +4063,12 @@ uint16_t mode_pacifica() c += pacifica_one_layer(i, pacifica_palette_3, sCIStart4, 5 * 256 , beatsin8(8, 10,28) , beat16(601)); // Add extra 'white' to areas where the four layers of light have lined up brightly - uint8_t threshold = scale8( sin8( wave), 20) + basethreshold; + unsigned threshold = scale8( sin8( wave), 20) + basethreshold; wave += 7; - uint8_t l = c.getAverageLight(); + unsigned l = c.getAverageLight(); if (l > threshold) { - uint8_t overage = l - threshold; - uint8_t overage2 = qadd8(overage, overage); + unsigned overage = l - threshold; + unsigned overage2 = qadd8(overage, overage); c += CRGB(overage, overage2, qadd8(overage2, overage2)); } @@ -4076,15 +4101,15 @@ uint16_t mode_sunrise() { } SEGMENT.fill(BLACK); - uint16_t stage = 0xFFFF; + unsigned stage = 0xFFFF; uint32_t s10SinceStart = (millis() - SEGENV.step) /100; //tenths of seconds if (SEGMENT.speed > 120) { //quick sunrise and sunset - uint16_t counter = (strip.now >> 1) * (((SEGMENT.speed -120) >> 1) +1); + unsigned counter = (strip.now >> 1) * (((SEGMENT.speed -120) >> 1) +1); stage = triwave16(counter); } else if (SEGMENT.speed) { //sunrise - uint8_t durMins = SEGMENT.speed; + unsigned durMins = SEGMENT.speed; if (durMins > 60) durMins -= 60; uint32_t s10Target = durMins * 600; if (s10SinceStart > s10Target) s10SinceStart = s10Target; @@ -4097,7 +4122,7 @@ uint16_t mode_sunrise() { //default palette is Fire uint32_t c = SEGMENT.color_from_palette(0, false, true, 255); //background - uint16_t wave = triwave16((i * stage) / SEGLEN); + unsigned wave = triwave16((i * stage) / SEGLEN); wave = (wave >> 8) + ((wave * SEGMENT.intensity) >> 15); @@ -4118,22 +4143,22 @@ static const char _data_FX_MODE_SUNRISE[] PROGMEM = "Sunrise@Time [min],Width;;! /* * Effects by Andrew Tuline */ -uint16_t phased_base(uint8_t moder) { // We're making sine waves here. By Andrew Tuline. +static uint16_t phased_base(uint8_t moder) { // We're making sine waves here. By Andrew Tuline. - uint8_t allfreq = 16; // Base frequency. + unsigned allfreq = 16; // Base frequency. float *phase = reinterpret_cast(&SEGENV.step); // Phase change value gets calculated (float fits into unsigned long). - uint8_t cutOff = (255-SEGMENT.intensity); // You can change the number of pixels. AKA INTENSITY (was 192). - uint8_t modVal = 5;//SEGMENT.fft1/8+1; // You can change the modulus. AKA FFT1 (was 5). + unsigned cutOff = (255-SEGMENT.intensity); // You can change the number of pixels. AKA INTENSITY (was 192). + unsigned modVal = 5;//SEGMENT.fft1/8+1; // You can change the modulus. AKA FFT1 (was 5). - uint8_t index = strip.now/64; // Set color rotation speed + unsigned index = strip.now/64; // Set color rotation speed *phase += SEGMENT.speed/32.0; // You can change the speed of the wave. AKA SPEED (was .4) for (int i = 0; i < SEGLEN; i++) { if (moder == 1) modVal = (inoise8(i*10 + i*10) /16); // Let's randomize our mod length with some Perlin noise. - uint16_t val = (i+1) * allfreq; // This sets the frequency of the waves. The +1 makes sure that led 0 is used. + unsigned val = (i+1) * allfreq; // This sets the frequency of the waves. The +1 makes sure that led 0 is used. if (modVal == 0) modVal = 1; val += *phase * (i % modVal +1) /2; // This sets the varying phase change of the waves. By Andrew Tuline. - uint8_t b = cubicwave8(val); // Now we make an 8 bit sinewave. + unsigned b = cubicwave8(val); // Now we make an 8 bit sinewave. b = (b > cutOff) ? (b - cutOff) : 0; // A ternary operator to cutoff the light. SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(index, false, false, 0), b)); index += 256 / SEGLEN; @@ -4157,12 +4182,12 @@ static const char _data_FX_MODE_PHASEDNOISE[] PROGMEM = "Phased Noise@!,!;!,!;!" uint16_t mode_twinkleup(void) { // A very short twinkle routine with fade-in and dual controls. By Andrew Tuline. - uint16_t prevSeed = random16_get_seed(); // save seed so we can restore it at the end of the function + unsigned prevSeed = random16_get_seed(); // save seed so we can restore it at the end of the function random16_set_seed(535); // The randomizer needs to be re-set each time through the loop in order for the same 'random' numbers to be the same each time through. for (int i = 0; i < SEGLEN; i++) { - uint8_t ranstart = random8(); // The starting value (aka brightness) for each pixel. Must be consistent each time through the loop for this to work. - uint8_t pixBri = sin8(ranstart + 16 * strip.now/(256-SEGMENT.speed)); + unsigned ranstart = random8(); // The starting value (aka brightness) for each pixel. Must be consistent each time through the loop for this to work. + unsigned pixBri = sin8(ranstart + 16 * strip.now/(256-SEGMENT.speed)); if (random8() > SEGMENT.intensity) pixBri = 0; SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(random8()+strip.now/100, false, PALETTE_SOLID_WRAP, 0), pixBri)); } @@ -4175,20 +4200,20 @@ static const char _data_FX_MODE_TWINKLEUP[] PROGMEM = "Twinkleup@!,Intensity;!,! // Peaceful noise that's slow and with gradually changing palettes. Does not support WLED palettes or default colours or controls. uint16_t mode_noisepal(void) { // Slow noise palette by Andrew Tuline. - uint16_t scale = 15 + (SEGMENT.intensity >> 2); //default was 30 + unsigned scale = 15 + (SEGMENT.intensity >> 2); //default was 30 //#define scale 30 - uint16_t dataSize = sizeof(CRGBPalette16) * 2; //allocate space for 2 Palettes (2 * 16 * 3 = 96 bytes) + unsigned dataSize = sizeof(CRGBPalette16) * 2; //allocate space for 2 Palettes (2 * 16 * 3 = 96 bytes) if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed CRGBPalette16* palettes = reinterpret_cast(SEGENV.data); - uint16_t changePaletteMs = 4000 + SEGMENT.speed *10; //between 4 - 6.5sec + unsigned changePaletteMs = 4000 + SEGMENT.speed *10; //between 4 - 6.5sec if (strip.now - SEGENV.step > changePaletteMs) { SEGENV.step = strip.now; - uint8_t baseI = random8(); + unsigned baseI = random8(); palettes[1] = CRGBPalette16(CHSV(baseI+random8(64), 255, random8(128,255)), CHSV(baseI+128, 255, random8(128,255)), CHSV(baseI+random8(92), 192, random8(128,255)), CHSV(baseI+random8(92), 255, random8(128,255))); } @@ -4200,7 +4225,7 @@ uint16_t mode_noisepal(void) { // Slow noise if (SEGMENT.palette > 0) palettes[0] = SEGPALETTE; for (int i = 0; i < SEGLEN; i++) { - uint8_t index = inoise8(i*scale, SEGENV.aux0+i*scale); // Get a value from the noise function. I'm using both x and y axis. + unsigned index = inoise8(i*scale, SEGENV.aux0+i*scale); // Get a value from the noise function. I'm using both x and y axis. color = ColorFromPalette(palettes[0], index, 255, LINEARBLEND); // Use the my own palette. SEGMENT.setPixelColor(i, color.red, color.green, color.blue); } @@ -4218,10 +4243,10 @@ static const char _data_FX_MODE_NOISEPAL[] PROGMEM = "Noise Pal@!,Scale;;!"; uint16_t mode_sinewave(void) { // Adjustable sinewave. By Andrew Tuline //#define qsuba(x, b) ((x>b)?x-b:0) // Analog Unsigned subtraction macro. if result <0, then => 0 - uint16_t colorIndex = strip.now /32;//(256 - SEGMENT.fft1); // Amount of colour change. + unsigned colorIndex = strip.now /32;//(256 - SEGMENT.fft1); // Amount of colour change. SEGENV.step += SEGMENT.speed/16; // Speed of animation. - uint16_t freq = SEGMENT.intensity/4;//SEGMENT.fft2/8; // Frequency of the signal. + unsigned freq = SEGMENT.intensity/4;//SEGMENT.fft2/8; // Frequency of the signal. for (int i = 0; i < SEGLEN; i++) { // For each of the LED's in the strand, set a brightness based on a wave as follows: int pixBri = cubicwave8((i*freq)+SEGENV.step);//qsuba(cubicwave8((i*freq)+SEGENV.step), (255-SEGMENT.intensity)); // qsub sets a minimum value called thiscutoff. If < thiscutoff, then bright = 0. Otherwise, bright = 128 (as defined in qsub).. @@ -4231,7 +4256,7 @@ uint16_t mode_sinewave(void) { // Adjustable sinewave. By Andrew Tul return FRAMETIME; } -static const char _data_FX_MODE_SINEWAVE[] PROGMEM = "Sine"; +static const char _data_FX_MODE_SINEWAVE[] PROGMEM = "Sine@!,Scale;;!"; /* @@ -4239,29 +4264,29 @@ static const char _data_FX_MODE_SINEWAVE[] PROGMEM = "Sine"; */ uint16_t mode_flow(void) { - uint16_t counter = 0; + unsigned counter = 0; if (SEGMENT.speed != 0) { counter = strip.now * ((SEGMENT.speed >> 2) +1); counter = counter >> 8; } - uint16_t maxZones = SEGLEN / 6; //only looks good if each zone has at least 6 LEDs - uint16_t zones = (SEGMENT.intensity * maxZones) >> 8; + unsigned maxZones = SEGLEN / 6; //only looks good if each zone has at least 6 LEDs + unsigned zones = (SEGMENT.intensity * maxZones) >> 8; if (zones & 0x01) zones++; //zones must be even if (zones < 2) zones = 2; - uint16_t zoneLen = SEGLEN / zones; - uint16_t offset = (SEGLEN - zones * zoneLen) >> 1; + unsigned zoneLen = SEGLEN / zones; + unsigned offset = (SEGLEN - zones * zoneLen) >> 1; SEGMENT.fill(SEGMENT.color_from_palette(-counter, false, true, 255)); - for (int z = 0; z < zones; z++) + for (unsigned z = 0; z < zones; z++) { - uint16_t pos = offset + z * zoneLen; - for (int i = 0; i < zoneLen; i++) + unsigned pos = offset + z * zoneLen; + for (unsigned i = 0; i < zoneLen; i++) { - uint8_t colorIndex = (i * 255 / zoneLen) - counter; - uint16_t led = (z & 0x01) ? i : (zoneLen -1) -i; + unsigned colorIndex = (i * 255 / zoneLen) - counter; + unsigned led = (z & 0x01) ? i : (zoneLen -1) -i; if (SEGMENT.reverse) led = (zoneLen -1) -led; SEGMENT.setPixelColor(pos + led, SEGMENT.color_from_palette(colorIndex, false, true, 255)); } @@ -4280,17 +4305,17 @@ uint16_t mode_chunchun(void) { if (SEGLEN == 1) return mode_static(); SEGMENT.fade_out(254); // add a bit of trail - uint16_t counter = strip.now * (6 + (SEGMENT.speed >> 4)); - uint16_t numBirds = 2 + (SEGLEN >> 3); // 2 + 1/8 of a segment - uint16_t span = (SEGMENT.intensity << 8) / numBirds; + unsigned counter = strip.now * (6 + (SEGMENT.speed >> 4)); + unsigned numBirds = 2 + (SEGLEN >> 3); // 2 + 1/8 of a segment + unsigned span = (SEGMENT.intensity << 8) / numBirds; - for (int i = 0; i < numBirds; i++) + for (unsigned i = 0; i < numBirds; i++) { counter -= span; - uint16_t megumin = sin16(counter) + 0x8000; - uint16_t bird = uint32_t(megumin * SEGLEN) >> 16; + unsigned megumin = sin16(counter) + 0x8000; + unsigned bird = uint32_t(megumin * SEGLEN) >> 16; uint32_t c = SEGMENT.color_from_palette((i * 255)/ numBirds, false, false, 0); // no palette wrapping - bird = constrain(bird, 0, SEGLEN-1); + bird = constrain(bird, 0U, SEGLEN-1U); SEGMENT.setPixelColor(bird, c); } return FRAMETIME; @@ -4331,11 +4356,11 @@ typedef struct Spotlight { uint16_t mode_dancing_shadows(void) { if (SEGLEN == 1) return mode_static(); - uint8_t numSpotlights = map(SEGMENT.intensity, 0, 255, 2, SPOT_MAX_COUNT); // 49 on 32 segment ESP32, 17 on 16 segment ESP8266 + 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; - uint16_t dataSize = sizeof(spotlight) * numSpotlights; + unsigned dataSize = sizeof(spotlight) * numSpotlights; if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Spotlight* spotlights = reinterpret_cast(SEGENV.data); @@ -4347,7 +4372,7 @@ uint16_t mode_dancing_shadows(void) for (size_t i = 0; i < numSpotlights; i++) { if (!initialize) { // advance the position of the spotlight - int16_t delta = (float)(time - spotlights[i].lastUpdateTime) * + int delta = (float)(time - spotlights[i].lastUpdateTime) * (spotlights[i].speed * ((1.0 + SEGMENT.speed)/100.0)); if (abs(delta) >= 1) { @@ -4470,20 +4495,20 @@ static const char _data_FX_MODE_WASHING_MACHINE[] PROGMEM = "Washing Machine@!,! Modified, originally by Mark Kriegsman https://gist.github.com/kriegsman/1f7ccbbfa492a73c015e */ uint16_t mode_blends(void) { - uint16_t pixelLen = SEGLEN > UINT8_MAX ? UINT8_MAX : SEGLEN; - uint16_t dataSize = sizeof(uint32_t) * (pixelLen + 1); // max segment length of 56 pixels on 16 segment ESP8266 + unsigned pixelLen = SEGLEN > UINT8_MAX ? UINT8_MAX : SEGLEN; + unsigned dataSize = sizeof(uint32_t) * (pixelLen + 1); // max segment length of 56 pixels on 16 segment ESP8266 if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed uint32_t* pixels = reinterpret_cast(SEGENV.data); - uint8_t blendSpeed = map(SEGMENT.intensity, 0, UINT8_MAX, 10, 128); - uint8_t shift = (strip.now * ((SEGMENT.speed >> 3) +1)) >> 8; + unsigned blendSpeed = map(SEGMENT.intensity, 0, UINT8_MAX, 10, 128); + unsigned shift = (strip.now * ((SEGMENT.speed >> 3) +1)) >> 8; - for (int i = 0; i < pixelLen; i++) { + for (unsigned i = 0; i < pixelLen; i++) { pixels[i] = color_blend(pixels[i], SEGMENT.color_from_palette(shift + quadwave8((i + 1) * 16), false, PALETTE_SOLID_WRAP, 255), blendSpeed); shift += 3; } - uint16_t offset = 0; - for (int i = 0; i < SEGLEN; i++) { + unsigned offset = 0; + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, pixels[offset++]); if (offset > pixelLen) offset = 0; } @@ -4519,7 +4544,7 @@ typedef struct TvSim { } tvSim; uint16_t mode_tv_simulator(void) { - uint16_t nr, ng, nb, r, g, b, i, hue; + int nr, ng, nb, r, g, b, i, hue; uint8_t sat, bri, j; if (!SEGENV.allocateData(sizeof(tvSim))) return mode_static(); //allocation failed @@ -4546,7 +4571,7 @@ uint16_t mode_tv_simulator(void) { } // slightly change the color-tone in this sceene - if ( SEGENV.aux0 == 0) { + if (SEGENV.aux0 == 0) { // hue change in both directions j = random8(4 * colorIntensity); hue = (random8() < 128) ? ((j < tvSimulator->sceeneColorHue) ? tvSimulator->sceeneColorHue - j : 767 - tvSimulator->sceeneColorHue - j) : // negative @@ -4795,8 +4820,8 @@ uint16_t mode_perlinmove(void) { if (SEGLEN == 1) return mode_static(); SEGMENT.fade_out(255-SEGMENT.custom1); for (int i = 0; i < SEGMENT.intensity/16 + 1; i++) { - uint16_t locn = inoise16(strip.now*128/(260-SEGMENT.speed)+i*15000, strip.now*128/(260-SEGMENT.speed)); // Get a new pixel location from moving noise. - uint16_t pixloc = map(locn, 50*256, 192*256, 0, SEGLEN-1); // Map that to the length of the strand, and ensure we don't go over. + unsigned locn = inoise16(strip.now*128/(260-SEGMENT.speed)+i*15000, strip.now*128/(260-SEGMENT.speed)); // Get a new pixel location from moving noise. + unsigned pixloc = map(locn, 50*256, 192*256, 0, SEGLEN-1); // Map that to the length of the strand, and ensure we don't go over. SEGMENT.setPixelColor(pixloc, SEGMENT.color_from_palette(pixloc%255, false, PALETTE_SOLID_WRAP, 0)); } @@ -4828,8 +4853,8 @@ 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) { - - const uint16_t hl = SEGLEN * 10 / 13; + 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); @@ -4856,9 +4881,9 @@ static const char _data_FX_MODE_FLOWSTRIPE[] PROGMEM = "Flow Stripe@Hue speed,Ef uint16_t mode_2DBlackHole(void) { // By: Stepko https://editor.soulmatelights.com/gallery/1012 , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); - uint16_t x, y; + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); + int x, y; SEGMENT.fadeToBlackBy(16 + (SEGMENT.speed>>3)); // create fading trails unsigned long t = strip.now/128; // timebase @@ -4877,11 +4902,11 @@ uint16_t mode_2DBlackHole(void) { // By: Stepko https://editor.soulma // central white dot SEGMENT.setPixelColorXY(cols/2, rows/2, WHITE); // blur everything a bit - SEGMENT.blur(16); + if (SEGMENT.check3) SEGMENT.blur(16, cols*rows < 100); return FRAMETIME; } // mode_2DBlackHole() -static const char _data_FX_MODE_2DBLACKHOLE[] PROGMEM = "Black Hole@Fade rate,Outer Y freq.,Outer X freq.,Inner X freq.,Inner Y freq.,Solid;!;!;2;pal=11"; +static const char _data_FX_MODE_2DBLACKHOLE[] PROGMEM = "Black Hole@Fade rate,Outer Y freq.,Outer X freq.,Inner X freq.,Inner Y freq.,Solid,,Blur;!;!;2;pal=11"; //////////////////////////// @@ -4890,8 +4915,8 @@ static const char _data_FX_MODE_2DBLACKHOLE[] PROGMEM = "Black Hole@Fade rate,Ou uint16_t mode_2DColoredBursts() { // By: ldirko https://editor.soulmatelights.com/gallery/819-colored-bursts , modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); if (SEGENV.call == 0) { SEGENV.aux0 = 0; // start with red hue @@ -4942,8 +4967,8 @@ static const char _data_FX_MODE_2DCOLOREDBURSTS[] PROGMEM = "Colored Bursts@Spee uint16_t mode_2Ddna(void) { // dna originally by by ldirko at https://pastebin.com/pCkkkzcs. Updated by Preyy. WLED conversion by Andrew Tuline. if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); SEGMENT.fadeToBlackBy(64); for (int i = 0; i < cols; i++) { @@ -4963,31 +4988,31 @@ static const char _data_FX_MODE_2DDNA[] PROGMEM = "DNA@Scroll speed,Blur;;!;2"; uint16_t mode_2DDNASpiral() { // By: ldirko https://editor.soulmatelights.com/gallery/512-dna-spiral-variation , modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); if (SEGENV.call == 0) { SEGMENT.fill(BLACK); } - uint8_t speeds = SEGMENT.speed/2 + 7; - uint8_t freq = SEGMENT.intensity/8; + unsigned speeds = SEGMENT.speed/2 + 7; + unsigned freq = SEGMENT.intensity/8; uint32_t ms = strip.now / 20; SEGMENT.fadeToBlackBy(135); for (int i = 0; i < rows; i++) { - uint16_t x = beatsin8(speeds, 0, cols - 1, 0, i * freq) + beatsin8(speeds - 7, 0, cols - 1, 0, i * freq + 128); - uint16_t x1 = beatsin8(speeds, 0, cols - 1, 0, 128 + i * freq) + beatsin8(speeds - 7, 0, cols - 1, 0, 128 + 64 + i * freq); - uint8_t hue = (i * 128 / rows) + ms; + int x = beatsin8(speeds, 0, cols - 1, 0, i * freq) + beatsin8(speeds - 7, 0, cols - 1, 0, i * freq + 128); + int x1 = beatsin8(speeds, 0, cols - 1, 0, 128 + i * freq) + beatsin8(speeds - 7, 0, cols - 1, 0, 128 + 64 + i * freq); + unsigned hue = (i * 128 / rows) + ms; // skip every 4th row every now and then (fade it more) if ((i + ms / 8) & 3) { // draw a gradient line between x and x1 x = x / 2; x1 = x1 / 2; - uint8_t steps = abs8(x - x1) + 1; + unsigned steps = abs8(x - x1) + 1; for (size_t k = 1; k <= steps; k++) { - uint8_t rate = k * 255 / steps; - uint8_t dx = lerp8by8(x, x1, rate); + unsigned rate = k * 255 / steps; + unsigned dx = lerp8by8(x, x1, rate); //SEGMENT.setPixelColorXY(dx, i, ColorFromPalette(SEGPALETTE, hue, 255, LINEARBLEND).nscale8_video(rate)); SEGMENT.addPixelColorXY(dx, i, ColorFromPalette(SEGPALETTE, hue, 255, LINEARBLEND)); // use setPixelColorXY for different look SEGMENT.fadePixelColorXY(dx, i, rate); @@ -5008,24 +5033,28 @@ static const char _data_FX_MODE_2DDNASPIRAL[] PROGMEM = "DNA Spiral@Scroll speed uint16_t mode_2DDrift() { // By: Stepko https://editor.soulmatelights.com/gallery/884-drift , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); + + const int colsCenter = (cols>>1) + (cols%2); + const int rowsCenter = (rows>>1) + (rows%2); SEGMENT.fadeToBlackBy(128); - const uint16_t maxDim = MAX(cols, rows)/2; + const float maxDim = MAX(cols, rows)/2; unsigned long t = strip.now / (32 - (SEGMENT.speed>>3)); unsigned long t_20 = t/20; // softhack007: pre-calculating this gives about 10% speedup - for (float i = 1; i < maxDim; i += 0.25) { + for (float i = 1.0f; i < maxDim; i += 0.25f) { float angle = radians(t * (maxDim - i)); - uint16_t myX = (cols>>1) + (uint16_t)(sin_t(angle) * i) + (cols%2); - uint16_t myY = (rows>>1) + (uint16_t)(cos_t(angle) * i) + (rows%2); - SEGMENT.setPixelColorXY(myX, myY, ColorFromPalette(SEGPALETTE, (i * 20) + t_20, 255, LINEARBLEND)); + int mySin = sin_t(angle) * i; + int myCos = cos_t(angle) * i; + SEGMENT.setPixelColorXY(colsCenter + mySin, rowsCenter + myCos, ColorFromPalette(SEGPALETTE, (i * 20) + t_20, 255, LINEARBLEND)); + if (SEGMENT.check1) SEGMENT.setPixelColorXY(colsCenter + myCos, rowsCenter + mySin, ColorFromPalette(SEGPALETTE, (i * 20) + t_20, 255, LINEARBLEND)); } SEGMENT.blur(SEGMENT.intensity>>3); return FRAMETIME; } // mode_2DDrift() -static const char _data_FX_MODE_2DDRIFT[] PROGMEM = "Drift@Rotation speed,Blur amount;;!;2"; +static const char _data_FX_MODE_2DDRIFT[] PROGMEM = "Drift@Rotation speed,Blur amount,,,,Twin;;!;2"; ////////////////////////// @@ -5034,32 +5063,32 @@ static const char _data_FX_MODE_2DDRIFT[] PROGMEM = "Drift@Rotation speed,Blur a uint16_t mode_2Dfirenoise(void) { // firenoise2d. By Andrew Tuline. Yet another short routine. if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); if (SEGENV.call == 0) { SEGMENT.fill(BLACK); } - uint16_t xscale = SEGMENT.intensity*4; - uint32_t yscale = SEGMENT.speed*8; - uint8_t indexx = 0; + unsigned xscale = SEGMENT.intensity*4; + unsigned yscale = SEGMENT.speed*8; + unsigned indexx = 0; - SEGPALETTE = CRGBPalette16( CRGB(0,0,0), CRGB(0,0,0), CRGB(0,0,0), CRGB(0,0,0), - CRGB::Red, CRGB::Red, CRGB::Red, CRGB::DarkOrange, - CRGB::DarkOrange,CRGB::DarkOrange, CRGB::Orange, CRGB::Orange, - CRGB::Yellow, CRGB::Orange, CRGB::Yellow, CRGB::Yellow); + CRGBPalette16 pal = SEGMENT.check1 ? SEGPALETTE : CRGBPalette16(CRGB::Black, CRGB::Black, CRGB::Black, CRGB::Black, + CRGB::Red, CRGB::Red, CRGB::Red, CRGB::DarkOrange, + CRGB::DarkOrange,CRGB::DarkOrange, CRGB::Orange, CRGB::Orange, + CRGB::Yellow, CRGB::Orange, CRGB::Yellow, CRGB::Yellow); for (int j=0; j < cols; j++) { for (int i=0; i < rows; i++) { - indexx = inoise8(j*yscale*rows/255, i*xscale+strip.now/4); // We're moving along our Perlin map. - SEGMENT.setPixelColorXY(j, i, ColorFromPalette(SEGPALETTE, min(i*(indexx)>>4, 255), i*255/cols, LINEARBLEND)); // With that value, look up the 8 bit colour palette value and assign it to the current LED. + indexx = inoise8(j*yscale*rows/255, i*xscale+strip.now/4); // We're moving along our Perlin map. + SEGMENT.setPixelColorXY(j, i, ColorFromPalette(pal, min(i*(indexx)>>4, 255U), i*255/cols, LINEARBLEND)); // With that value, look up the 8 bit colour palette value and assign it to the current LED. } // for i } // for j return FRAMETIME; } // mode_2Dfirenoise() -static const char _data_FX_MODE_2DFIRENOISE[] PROGMEM = "Firenoise@X scale,Y scale;;!;2"; +static const char _data_FX_MODE_2DFIRENOISE[] PROGMEM = "Firenoise@X scale,Y scale,,,,Palette;;!;2;pal=66"; ////////////////////////////// @@ -5068,8 +5097,8 @@ static const char _data_FX_MODE_2DFIRENOISE[] PROGMEM = "Firenoise@X scale,Y sca uint16_t mode_2DFrizzles(void) { // By: Stepko https://editor.soulmatelights.com/gallery/640-color-frizzles , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); SEGMENT.fadeToBlackBy(16); for (size_t i = 8; i > 0; i--) { @@ -5095,10 +5124,10 @@ typedef struct ColorCount { uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://natureofcode.com/book/chapter-7-cellular-automata/ and https://github.com/DougHaber/nlife-color if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); - const uint16_t dataSize = sizeof(CRGB) * SEGMENT.length(); // using width*height prevents reallocation if mirroring is enabled - const uint16_t crcBufferLen = 2; //(SEGMENT.width() + SEGMENT.height())*71/100; // roughly sqrt(2)/2 for better repetition detection (Ewowi) + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); + const unsigned dataSize = sizeof(CRGB) * SEGMENT.length(); // using width*height prevents reallocation if mirroring is enabled + const int crcBufferLen = 2; //(SEGMENT.width() + SEGMENT.height())*71/100; // roughly sqrt(2)/2 for better repetition detection (Ewowi) if (!SEGENV.allocateData(dataSize + sizeof(uint16_t)*crcBufferLen)) return mode_static(); //allocation failed CRGB *prevLeds = reinterpret_cast(SEGENV.data); @@ -5113,7 +5142,7 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: //give the leds random state and colors (based on intensity, colors from palette or all posible colors are chosen) for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) { - uint8_t state = random8()%2; + unsigned state = random8()%2; if (state == 0) SEGMENT.setPixelColorXY(x,y, backgroundColor); else @@ -5142,11 +5171,11 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: for (int i = -1; i <= 1; i++) for (int j = -1; j <= 1; j++) { // iterate through 3*3 matrix if (i==0 && j==0) continue; // ignore itself // wrap around segment - int16_t xx = x+i, yy = y+j; + int xx = x+i, yy = y+j; if (x+i < 0) xx = cols-1; else if (x+i >= cols) xx = 0; if (y+j < 0) yy = rows-1; else if (y+j >= rows) yy = 0; - uint16_t xy = XY(xx, yy); // previous cell xy to check + unsigned xy = XY(xx, yy); // previous cell xy to check // count different neighbours and colors if (prevLeds[xy] != backgroundColor) { neighbors++; @@ -5201,8 +5230,8 @@ static const char _data_FX_MODE_2DGAMEOFLIFE[] PROGMEM = "Game Of Life@!;!,!;!;2 uint16_t mode_2DHiphotic() { // By: ldirko https://editor.soulmatelights.com/gallery/810 , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); const uint32_t a = strip.now / ((SEGMENT.custom3>>1)+1); for (int x = 0; x < cols; x++) { @@ -5233,8 +5262,8 @@ typedef struct Julia { uint16_t mode_2DJulia(void) { // An animated Julia set by Andrew Tuline. if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); if (!SEGENV.allocateData(sizeof(julia))) return mode_static(); Julia* julias = reinterpret_cast(SEGENV.data); @@ -5339,8 +5368,8 @@ static const char _data_FX_MODE_2DJULIA[] PROGMEM = "Julia@,Max iterations per p uint16_t mode_2DLissajous(void) { // By: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); SEGMENT.fadeToBlackBy(SEGMENT.intensity); uint_fast16_t phase = (strip.now * (1 + SEGENV.custom3)) /32; // allow user to control rotation speed @@ -5367,10 +5396,10 @@ static const char _data_FX_MODE_2DLISSAJOUS[] PROGMEM = "Lissajous@X frequency,F uint16_t mode_2Dmatrix(void) { // Matrix2D. By Jeremy Williams. Adapted by Andrew Tuline & improved by merkisoft and ewowi, and softhack007. if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); - uint16_t dataSize = (SEGMENT.length()+7) >> 3; //1 bit per LED for trails + unsigned dataSize = (SEGMENT.length()+7) >> 3; //1 bit per LED for trails if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed if (SEGENV.call == 0) { @@ -5379,7 +5408,7 @@ uint16_t mode_2Dmatrix(void) { // Matrix2D. By Jeremy Williams. } uint8_t fade = map(SEGMENT.custom1, 0, 255, 50, 250); // equals trail size - uint8_t speed = (256-SEGMENT.speed) >> map(MIN(rows, 150), 0, 150, 0, 3); // slower speeds for small displays + uint8_t speed = (256-SEGMENT.speed) >> map(min(rows, 150), 0, 150, 0, 3); // slower speeds for small displays uint32_t spawnColor; uint32_t trailColor; @@ -5437,29 +5466,29 @@ static const char _data_FX_MODE_2DMATRIX[] PROGMEM = "Matrix@!,Spawning rate,Tra uint16_t mode_2Dmetaballs(void) { // Metaballs by Stefan Petrick. Cannot have one of the dimensions be 2 or less. Adapted by Andrew Tuline. if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); float speed = 0.25f * (1+(SEGMENT.speed>>6)); // get some 2 random moving points - uint8_t x2 = map(inoise8(strip.now * speed, 25355, 685), 0, 255, 0, cols-1); - uint8_t y2 = map(inoise8(strip.now * speed, 355, 11685), 0, 255, 0, rows-1); + int x2 = map(inoise8(strip.now * speed, 25355, 685), 0, 255, 0, cols-1); + int y2 = map(inoise8(strip.now * speed, 355, 11685), 0, 255, 0, rows-1); - uint8_t x3 = map(inoise8(strip.now * speed, 55355, 6685), 0, 255, 0, cols-1); - uint8_t y3 = map(inoise8(strip.now * speed, 25355, 22685), 0, 255, 0, rows-1); + int x3 = map(inoise8(strip.now * speed, 55355, 6685), 0, 255, 0, cols-1); + int y3 = map(inoise8(strip.now * speed, 25355, 22685), 0, 255, 0, rows-1); // and one Lissajou function - uint8_t x1 = beatsin8(23 * speed, 0, cols-1); - uint8_t y1 = beatsin8(28 * speed, 0, rows-1); + int x1 = beatsin8(23 * speed, 0, cols-1); + int y1 = beatsin8(28 * speed, 0, rows-1); for (int y = 0; y < rows; y++) { for (int x = 0; x < cols; x++) { // calculate distances of the 3 points from actual pixel // and add them together with weightening - uint16_t dx = abs(x - x1); - uint16_t dy = abs(y - y1); - uint16_t dist = 2 * sqrt16((dx * dx) + (dy * dy)); + unsigned dx = abs(x - x1); + unsigned dy = abs(y - y1); + unsigned dist = 2 * sqrt16((dx * dx) + (dy * dy)); dx = abs(x - x2); dy = abs(y - y2); @@ -5470,7 +5499,7 @@ uint16_t mode_2Dmetaballs(void) { // Metaballs by Stefan Petrick. Cannot have dist += sqrt16((dx * dx) + (dy * dy)); // inverse result - byte color = dist ? 1000 / dist : 255; + int color = dist ? 1000 / dist : 255; // map color between thresholds if (color > 0 and color < 60) { @@ -5496,10 +5525,10 @@ static const char _data_FX_MODE_2DMETABALLS[] PROGMEM = "Metaballs@!;;!;2"; uint16_t mode_2Dnoise(void) { // By Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); - const uint16_t scale = SEGMENT.intensity+2; + const unsigned scale = SEGMENT.intensity+2; for (int y = 0; y < rows; y++) { for (int x = 0; x < cols; x++) { @@ -5519,21 +5548,21 @@ static const char _data_FX_MODE_2DNOISE[] PROGMEM = "Noise2D@!,Scale;;!;2"; uint16_t mode_2DPlasmaball(void) { // By: Stepko https://editor.soulmatelights.com/gallery/659-plasm-ball , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); SEGMENT.fadeToBlackBy(SEGMENT.custom1>>2); uint_fast32_t t = (strip.now * 8) / (256 - SEGMENT.speed); // optimized to avoid float for (int i = 0; i < cols; i++) { - uint16_t thisVal = inoise8(i * 30, t, t); - uint16_t thisMax = map(thisVal, 0, 255, 0, cols-1); + unsigned thisVal = inoise8(i * 30, t, t); + unsigned thisMax = map(thisVal, 0, 255, 0, cols-1); for (int j = 0; j < rows; j++) { - uint16_t thisVal_ = inoise8(t, j * 30, t); - uint16_t thisMax_ = map(thisVal_, 0, 255, 0, rows-1); - uint16_t x = (i + thisMax_ - cols / 2); - uint16_t y = (j + thisMax - cols / 2); - uint16_t cx = (i + thisMax_); - uint16_t cy = (j + thisMax); + unsigned thisVal_ = inoise8(t, j * 30, t); + unsigned thisMax_ = map(thisVal_, 0, 255, 0, rows-1); + int x = (i + thisMax_ - cols / 2); + int y = (j + thisMax - cols / 2); + int cx = (i + thisMax_); + int cy = (j + thisMax); SEGMENT.addPixelColorXY(i, j, ((x - y > -2) && (x - y < 2)) || ((cols - 1 - x - y) > -2 && (cols - 1 - x - y < 2)) || @@ -5559,8 +5588,8 @@ static const char _data_FX_MODE_2DPLASMABALL[] PROGMEM = "Plasma Ball@Speed,,Fad uint16_t mode_2DPolarLights(void) { // By: Kostyantyn Matviyevskyy https://editor.soulmatelights.com/gallery/762-polar-lights , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); CRGBPalette16 auroraPalette = {0x000000, 0x003300, 0x006600, 0x009900, 0x00cc00, 0x00ff00, 0x33ff00, 0x66ff00, 0x99ff00, 0xccff00, 0xffff00, 0xffcc00, 0xff9900, 0xff6600, 0xff3300, 0xff0000}; @@ -5570,7 +5599,7 @@ uint16_t mode_2DPolarLights(void) { // By: Kostyantyn Matviyevskyy https } float adjustHeight = (float)map(rows, 8, 32, 28, 12); // maybe use mapf() ??? - uint16_t adjScale = map(cols, 8, 64, 310, 63); + unsigned adjScale = map(cols, 8, 64, 310, 63); /* if (SEGENV.aux1 != SEGMENT.custom1/12) { // Hacky palette rotation. We need that black. SEGENV.aux1 = SEGMENT.custom1/12; @@ -5586,8 +5615,8 @@ uint16_t mode_2DPolarLights(void) { // By: Kostyantyn Matviyevskyy https } } */ - uint16_t _scale = map(SEGMENT.intensity, 0, 255, 30, adjScale); - byte _speed = map(SEGMENT.speed, 0, 255, 128, 16); + unsigned _scale = map(SEGMENT.intensity, 0, 255, 30, adjScale); + int _speed = map(SEGMENT.speed, 0, 255, 128, 16); for (int x = 0; x < cols; x++) { for (int y = 0; y < rows; y++) { @@ -5610,16 +5639,16 @@ static const char _data_FX_MODE_2DPOLARLIGHTS[] PROGMEM = "Polar Lights@!,Scale; uint16_t mode_2DPulser(void) { // By: ldirko https://editor.soulmatelights.com/gallery/878-pulse-test , modifed by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); SEGMENT.fadeToBlackBy(8 - (SEGMENT.intensity>>5)); uint32_t a = strip.now / (18 - SEGMENT.speed / 16); - uint16_t x = (a / 14) % cols; - uint16_t y = map((sin8(a * 5) + sin8(a * 4) + sin8(a * 2)), 0, 765, rows-1, 0); + int x = (a / 14) % cols; + int y = map((sin8(a * 5) + sin8(a * 4) + sin8(a * 2)), 0, 765, rows-1, 0); SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, map(y, 0, rows-1, 0, 255), 255, LINEARBLEND)); - SEGMENT.blur(1 + (SEGMENT.intensity>>4)); + SEGMENT.blur(SEGMENT.intensity>>4); return FRAMETIME; } // mode_2DPulser() @@ -5632,8 +5661,8 @@ static const char _data_FX_MODE_2DPULSER[] PROGMEM = "Pulser@!,Blur;;!;2"; uint16_t mode_2DSindots(void) { // By: ldirko https://editor.soulmatelights.com/gallery/597-sin-dots , modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); if (SEGENV.call == 0) { SEGMENT.fill(BLACK); @@ -5644,8 +5673,8 @@ uint16_t mode_2DSindots(void) { // By: ldirko http byte t1 = strip.now / (257 - SEGMENT.speed); // 20; byte t2 = sin8(t1) / 4 * 2; for (int i = 0; i < 13; i++) { - byte x = sin8(t1 + i * SEGMENT.intensity/8)*(cols-1)/255; // max index now 255x15/255=15! - byte y = sin8(t2 + i * SEGMENT.intensity/8)*(rows-1)/255; // max index now 255x15/255=15! + int x = sin8(t1 + i * SEGMENT.intensity/8)*(cols-1)/255; // max index now 255x15/255=15! + int y = sin8(t2 + i * SEGMENT.intensity/8)*(rows-1)/255; // max index now 255x15/255=15! SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, i * 255 / 13, 255, LINEARBLEND)); } SEGMENT.blur(SEGMENT.custom2>>3); @@ -5663,22 +5692,21 @@ uint16_t mode_2Dsquaredswirl(void) { // By: Mark Kriegsman. https://g // Modifed by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); const uint8_t kBorderWidth = 2; SEGMENT.fadeToBlackBy(24); - uint8_t blurAmount = SEGMENT.custom3>>1; // reduced resolution slider - SEGMENT.blur(blurAmount); + SEGMENT.blur(SEGMENT.custom3>>1); // Use two out-of-sync sine waves - uint8_t i = beatsin8(19, kBorderWidth, cols-kBorderWidth); - uint8_t j = beatsin8(22, kBorderWidth, cols-kBorderWidth); - uint8_t k = beatsin8(17, kBorderWidth, cols-kBorderWidth); - uint8_t m = beatsin8(18, kBorderWidth, rows-kBorderWidth); - uint8_t n = beatsin8(15, kBorderWidth, rows-kBorderWidth); - uint8_t p = beatsin8(20, kBorderWidth, rows-kBorderWidth); + int i = beatsin8(19, kBorderWidth, cols-kBorderWidth); + int j = beatsin8(22, kBorderWidth, cols-kBorderWidth); + int k = beatsin8(17, kBorderWidth, cols-kBorderWidth); + int m = beatsin8(18, kBorderWidth, rows-kBorderWidth); + int n = beatsin8(15, kBorderWidth, rows-kBorderWidth); + int p = beatsin8(20, kBorderWidth, rows-kBorderWidth); SEGMENT.addPixelColorXY(i, m, ColorFromPalette(SEGPALETTE, strip.now/29, 255, LINEARBLEND)); SEGMENT.addPixelColorXY(j, n, ColorFromPalette(SEGPALETTE, strip.now/41, 255, LINEARBLEND)); @@ -5695,8 +5723,8 @@ static const char _data_FX_MODE_2DSQUAREDSWIRL[] PROGMEM = "Squared Swirl@,,,,Bl uint16_t mode_2DSunradiation(void) { // By: ldirko https://editor.soulmatelights.com/gallery/599-sun-radiation , modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); if (!SEGENV.allocateData(sizeof(byte)*(cols+2)*(rows+2))) return mode_static(); //allocation failed byte *bump = reinterpret_cast(SEGENV.data); @@ -5706,7 +5734,7 @@ uint16_t mode_2DSunradiation(void) { // By: ldirko https://edi } unsigned long t = strip.now / 4; - int index = 0; + unsigned index = 0; uint8_t someVal = SEGMENT.speed/4; // Was 25. for (int j = 0; j < (rows + 2); j++) { for (int i = 0; i < (cols + 2); i++) { @@ -5716,16 +5744,16 @@ uint16_t mode_2DSunradiation(void) { // By: ldirko https://edi } int yindex = cols + 3; - int16_t vly = -(rows / 2 + 1); + int vly = -(rows / 2 + 1); for (int y = 0; y < rows; y++) { ++vly; - int16_t vlx = -(cols / 2 + 1); + int vlx = -(cols / 2 + 1); for (int x = 0; x < cols; x++) { ++vlx; - int8_t nx = bump[x + yindex + 1] - bump[x + yindex - 1]; - int8_t ny = bump[x + yindex + (cols + 2)] - bump[x + yindex - (cols + 2)]; - byte difx = abs8(vlx * 7 - nx); - byte dify = abs8(vly * 7 - ny); + int nx = bump[x + yindex + 1] - bump[x + yindex - 1]; + int ny = bump[x + yindex + (cols + 2)] - bump[x + yindex - (cols + 2)]; + unsigned difx = abs8(vlx * 7 - nx); + unsigned dify = abs8(vly * 7 - ny); int temp = difx * difx + dify * dify; int col = 255 - temp / 8; //8 its a size of effect if (col < 0) col = 0; @@ -5745,8 +5773,8 @@ static const char _data_FX_MODE_2DSUNRADIATION[] PROGMEM = "Sun Radiation@Varian uint16_t mode_2Dtartan(void) { // By: Elliott Kember https://editor.soulmatelights.com/gallery/3-tartan , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); if (SEGENV.call == 0) { SEGMENT.fill(BLACK); @@ -5784,12 +5812,12 @@ static const char _data_FX_MODE_2DTARTAN[] PROGMEM = "Tartan@X scale,Y scale,,,S uint16_t mode_2Dspaceships(void) { //// Space ships by stepko (c)05.02.21 [https://editor.soulmatelights.com/gallery/639-space-ships], adapted by Blaz Kristan (AKA blazoncek) if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); uint32_t tb = strip.now >> 12; // every ~4s if (tb > SEGENV.step) { - int8_t dir = ++SEGENV.aux0; + int dir = ++SEGENV.aux0; dir += (int)random8(3)-1; if (dir > 7) SEGENV.aux0 = 0; else if (dir < 0) SEGENV.aux0 = 7; @@ -5801,8 +5829,8 @@ uint16_t mode_2Dspaceships(void) { //// Space ships by stepko (c)05.02.21 [ht SEGMENT.move(SEGENV.aux0, 1); for (size_t i = 0; i < 8; i++) { - byte x = beatsin8(12 + i, 2, cols - 3); - byte y = beatsin8(15 + i, 2, rows - 3); + int x = beatsin8(12 + i, 2, cols - 3); + int y = beatsin8(15 + i, 2, rows - 3); CRGB color = ColorFromPalette(SEGPALETTE, beatsin8(12 + i, 0, 255), 255); SEGMENT.addPixelColorXY(x, y, color); if (cols > 24 || rows > 24) { @@ -5827,8 +5855,8 @@ static const char _data_FX_MODE_2DSPACESHIPS[] PROGMEM = "Spaceships@!,Blur;;!;2 uint16_t mode_2Dcrazybees(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); byte n = MIN(MAX_BEES, (rows * cols) / 256 + 1); @@ -5872,7 +5900,7 @@ uint16_t mode_2Dcrazybees(void) { SEGMENT.addPixelColorXY(bee[i].aimX, bee[i].aimY - 1, CHSV(bee[i].hue, 255, 255)); if (bee[i].posX != bee[i].aimX || bee[i].posY != bee[i].aimY) { SEGMENT.setPixelColorXY(bee[i].posX, bee[i].posY, CRGB(CHSV(bee[i].hue, 60, 255))); - int8_t error2 = bee[i].error * 2; + int error2 = bee[i].error * 2; if (error2 > -bee[i].deltaY) { bee[i].error -= bee[i].deltaY; bee[i].posX += bee[i].signX; @@ -5890,7 +5918,7 @@ uint16_t mode_2Dcrazybees(void) { return FRAMETIME; } static const char _data_FX_MODE_2DCRAZYBEES[] PROGMEM = "Crazy Bees@!,Blur;;;2"; - +#undef MAX_BEES ///////////////////////// // 2D Ghost Rider // @@ -5900,8 +5928,8 @@ static const char _data_FX_MODE_2DCRAZYBEES[] PROGMEM = "Crazy Bees@!,Blur;;;2"; uint16_t mode_2Dghostrider(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); typedef struct Lighter { int16_t gPosX; @@ -5980,7 +6008,7 @@ uint16_t mode_2Dghostrider(void) { return FRAMETIME; } static const char _data_FX_MODE_2DGHOSTRIDER[] PROGMEM = "Ghost Rider@Fade rate,Blur;;!;2"; - +#undef LIGHTERS_AM //////////////////////////// // 2D Floating Blobs // @@ -5990,8 +6018,8 @@ static const char _data_FX_MODE_2DGHOSTRIDER[] PROGMEM = "Ghost Rider@Fade rate, uint16_t mode_2Dfloatingblobs(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); typedef struct Blob { float x[MAX_BLOBS], y[MAX_BLOBS]; @@ -6001,7 +6029,7 @@ uint16_t mode_2Dfloatingblobs(void) { byte color[MAX_BLOBS]; } blob_t; - uint8_t Amount = (SEGMENT.intensity>>5) + 1; // NOTE: be sure to update MAX_BLOBS if you change this + size_t Amount = (SEGMENT.intensity>>5) + 1; // NOTE: be sure to update MAX_BLOBS if you change this if (!SEGENV.allocateData(sizeof(blob_t))) return mode_static(); //allocation failed blob_t *blob = reinterpret_cast(SEGENV.data); @@ -6043,8 +6071,8 @@ uint16_t mode_2Dfloatingblobs(void) { } } uint32_t c = SEGMENT.color_from_palette(blob->color[i], false, false, 0); - if (blob->r[i] > 1.f) SEGMENT.fill_circle(blob->x[i], blob->y[i], roundf(blob->r[i]), c); - else SEGMENT.setPixelColorXY(blob->x[i], blob->y[i], c); + if (blob->r[i] > 1.f) SEGMENT.fillCircle(roundf(blob->x[i]), roundf(blob->y[i]), roundf(blob->r[i]), c); + else SEGMENT.setPixelColorXY((int)roundf(blob->x[i]), (int)roundf(blob->y[i]), c); // move x if (blob->x[i] + blob->r[i] >= cols - 1) blob->x[i] += (blob->sX[i] * ((cols - 1 - blob->x[i]) / blob->r[i] + 0.005f)); else if (blob->x[i] - blob->r[i] <= 0) blob->x[i] += (blob->sX[i] * (blob->x[i] / blob->r[i] + 0.005f)); @@ -6078,8 +6106,8 @@ uint16_t mode_2Dfloatingblobs(void) { return FRAMETIME; } -#undef MAX_BLOBS static const char _data_FX_MODE_2DBLOBS[] PROGMEM = "Blobs@!,# blobs,Blur,Trail;!;!;2;c1=8"; +#undef MAX_BLOBS //////////////////////////// @@ -6088,11 +6116,11 @@ static const char _data_FX_MODE_2DBLOBS[] PROGMEM = "Blobs@!,# blobs,Blur,Trail; uint16_t mode_2Dscrollingtext(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); - int letterWidth, rotLW; - int letterHeight, rotLH; + unsigned letterWidth, rotLW; + unsigned letterHeight, rotLH; switch (map(SEGMENT.custom2, 0, 255, 1, 5)) { default: case 1: letterWidth = 4; letterHeight = 6; break; @@ -6189,8 +6217,8 @@ static const char _data_FX_MODE_2DSCROLLTEXT[] PROGMEM = "Scrolling Text@!,Y Off uint16_t mode_2Ddriftrose(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); const float CX = (cols-cols%2)/2.f - .5f; const float CY = (rows-rows%2)/2.f - .5f; @@ -6198,11 +6226,12 @@ uint16_t mode_2Ddriftrose(void) { SEGMENT.fadeToBlackBy(32+(SEGMENT.speed>>3)); for (size_t i = 1; i < 37; i++) { - uint32_t x = (CX + (sin_t(radians(i * 10)) * (beatsin8(i, 0, L*2)-L))) * 255.f; - uint32_t y = (CY + (cos_t(radians(i * 10)) * (beatsin8(i, 0, L*2)-L))) * 255.f; + float angle = radians(i * 10); + uint32_t x = (CX + (sin_t(angle) * (beatsin8(i, 0, L*2)-L))) * 255.f; + uint32_t y = (CY + (cos_t(angle) * (beatsin8(i, 0, L*2)-L))) * 255.f; SEGMENT.wu_pixel(x, y, CHSV(i * 10, 255, 255)); } - SEGMENT.blur((SEGMENT.intensity>>4)+1); + SEGMENT.blur(SEGMENT.intensity>>4); return FRAMETIME; } @@ -6215,15 +6244,15 @@ static const char _data_FX_MODE_2DDRIFTROSE[] PROGMEM = "Drift Rose@Fade,Blur;;; uint16_t mode_2Dplasmarotozoom() { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); - uint16_t dataSize = SEGMENT.length() + sizeof(float); + unsigned dataSize = SEGMENT.length() + sizeof(float); if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed float *a = reinterpret_cast(SEGENV.data); byte *plasma = reinterpret_cast(SEGENV.data+sizeof(float)); - uint16_t ms = strip.now/15; + unsigned ms = strip.now/15; // plasma for (int j = 0; j < rows; j++) { @@ -6244,7 +6273,7 @@ uint16_t mode_2Dplasmarotozoom() { for (int j = 0; j < rows; j++) { byte u = abs8(u1 - j * sinus) % cols; byte v = abs8(v1 + j * kosinus) % rows; - SEGMENT.setPixelColorXY(i, j, SEGMENT.color_from_palette(plasma[v*cols+u], false, PALETTE_SOLID_WRAP, 0)); + SEGMENT.setPixelColorXY(i, j, SEGMENT.color_from_palette(plasma[v*cols+u], false, PALETTE_SOLID_WRAP, 255)); } } *a -= 0.03f + float(SEGENV.speed-128)*0.0002f; // rotation speed @@ -6308,16 +6337,12 @@ uint16_t mode_ripplepeak(void) { // * Ripple peak. By Andrew Tuli // This currently has no controls. #define maxsteps 16 // Case statement wouldn't allow a variable. - uint16_t maxRipples = 16; - uint16_t dataSize = sizeof(Ripple) * maxRipples; + unsigned maxRipples = 16; + unsigned dataSize = sizeof(Ripple) * maxRipples; if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Ripple* ripples = reinterpret_cast(SEGENV.data); - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); uint8_t samplePeak = *(uint8_t*)um_data->u_data[3]; #ifdef ESP32 float FFT_MajorPeak = *(float*) um_data->u_data[4]; @@ -6388,8 +6413,8 @@ static const char _data_FX_MODE_RIPPLEPEAK[] PROGMEM = "Ripple Peak@Fade rate,Ma uint16_t mode_2DSwirl(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); if (SEGENV.call == 0) { SEGMENT.fill(BLACK); @@ -6399,18 +6424,14 @@ uint16_t mode_2DSwirl(void) { SEGMENT.blur(SEGMENT.custom1); - uint8_t i = beatsin8( 27*SEGMENT.speed/255, borderWidth, cols - borderWidth); - uint8_t j = beatsin8( 41*SEGMENT.speed/255, borderWidth, rows - borderWidth); - uint8_t ni = (cols - 1) - i; - uint8_t nj = (cols - 1) - j; + int i = beatsin8( 27*SEGMENT.speed/255, borderWidth, cols - borderWidth); + int j = beatsin8( 41*SEGMENT.speed/255, borderWidth, rows - borderWidth); + int ni = (cols - 1) - i; + int nj = (cols - 1) - j; - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } - float volumeSmth = *(float*) um_data->u_data[0]; //ewowi: use instead of sampleAvg??? - int16_t volumeRaw = *(int16_t*) um_data->u_data[1]; + um_data_t *um_data = getAudioData(); + float volumeSmth = *(float*) um_data->u_data[0]; //ewowi: use instead of sampleAvg??? + int volumeRaw = *(int16_t*) um_data->u_data[1]; SEGMENT.addPixelColorXY( i, j, ColorFromPalette(SEGPALETTE, (strip.now / 11 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 11, 200, 255); SEGMENT.addPixelColorXY( j, i, ColorFromPalette(SEGPALETTE, (strip.now / 13 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 13, 200, 255); @@ -6431,38 +6452,34 @@ static const char _data_FX_MODE_2DSWIRL[] PROGMEM = "Swirl@!,Sensitivity,Blur;,B uint16_t mode_2DWaverly(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; SEGMENT.fadeToBlackBy(SEGMENT.speed); long t = strip.now / 2; for (int i = 0; i < cols; i++) { - uint16_t thisVal = (1 + SEGMENT.intensity/64) * inoise8(i * 45 , t , t)/2; + unsigned thisVal = (1 + SEGMENT.intensity/64) * inoise8(i * 45 , t , t)/2; // use audio if available if (um_data) { thisVal /= 32; // reduce intensity of inoise8() thisVal *= volumeSmth; } - uint16_t thisMax = map(thisVal, 0, 512, 0, rows); + int thisMax = map(thisVal, 0, 512, 0, rows); for (int j = 0; j < thisMax; j++) { SEGMENT.addPixelColorXY(i, j, ColorFromPalette(SEGPALETTE, map(j, 0, thisMax, 250, 0), 255, LINEARBLEND)); SEGMENT.addPixelColorXY((cols - 1) - i, (rows - 1) - j, ColorFromPalette(SEGPALETTE, map(j, 0, thisMax, 250, 0), 255, LINEARBLEND)); } } - SEGMENT.blur(16); + if (SEGMENT.check3) SEGMENT.blur(16, cols*rows < 100); return FRAMETIME; } // mode_2DWaverly() -static const char _data_FX_MODE_2DWAVERLY[] PROGMEM = "Waverly@Amplification,Sensitivity;;!;2v;ix=64,si=0"; // Beatsin +static const char _data_FX_MODE_2DWAVERLY[] PROGMEM = "Waverly@Amplification,Sensitivity,,,,,Blur;;!;2v;ix=64,si=0"; // Beatsin #endif // WLED_DISABLE_2D @@ -6483,15 +6500,11 @@ typedef struct Gravity { uint16_t mode_gravcenter(void) { // Gravcenter. By Andrew Tuline. if (SEGLEN == 1) return mode_static(); - const uint16_t dataSize = sizeof(gravity); + const unsigned dataSize = sizeof(gravity); if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Gravity* gravcen = reinterpret_cast(SEGENV.data); - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; //SEGMENT.fade_out(240); @@ -6501,7 +6514,7 @@ uint16_t mode_gravcenter(void) { // Gravcenter. By Andrew Tuline. segmentSampleAvg *= 0.125; // divide by 8, to compensate for later "sensitivity" upscaling float mySampleAvg = mapf(segmentSampleAvg*2.0, 0, 32, 0, (float)SEGLEN/2.0f); // map to pixels available in current segment - uint16_t tempsamp = constrain(mySampleAvg, 0, SEGLEN/2); // Keep the sample from overflowing. + int tempsamp = constrain(mySampleAvg, 0, SEGLEN/2); // Keep the sample from overflowing. uint8_t gravity = 8 - SEGMENT.speed/32; for (int i=0; i(SEGENV.data); - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; //SEGMENT.fade_out(240); @@ -6629,24 +6634,20 @@ static const char _data_FX_MODE_GRAVIMETER[] PROGMEM = "Gravimeter@Rate of fall, // * JUGGLES // ////////////////////// uint16_t mode_juggles(void) { // Juggles. By Andrew Tuline. - if (SEGLEN == 1) return mode_static(); - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; SEGMENT.fade_out(224); // 6.25% - uint16_t my_sampleAgc = fmax(fmin(volumeSmth, 255.0), 0); + unsigned my_sampleAgc = fmax(fmin(volumeSmth, 255.0), 0); for (size_t i=0; iu_data[1]; + um_data_t *um_data = getAudioData(); + int volumeRaw = *(int16_t*)um_data->u_data[1]; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); @@ -6688,11 +6685,7 @@ uint16_t mode_midnoise(void) { // Midnoise. By Andrew Tuline. if (SEGLEN == 1) return mode_static(); // Changing xdist to SEGENV.aux0 and ydist to SEGENV.aux1. - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; SEGMENT.fade_out(SEGMENT.speed); @@ -6727,17 +6720,13 @@ uint16_t mode_noisefire(void) { // Noisefire. By Andrew Tuline. CRGB::DarkOrange, CRGB::DarkOrange, CRGB::Orange, CRGB::Orange, CRGB::Yellow, CRGB::Orange, CRGB::Yellow, CRGB::Yellow); - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; if (SEGENV.call == 0) SEGMENT.fill(BLACK); for (int i = 0; i < SEGLEN; i++) { - uint16_t index = inoise8(i*SEGMENT.speed/64,strip.now*SEGMENT.speed/64*SEGLEN/255); // X location is constant, but we move along the Y at the rate of millis(). By Andrew Tuline. + unsigned index = inoise8(i*SEGMENT.speed/64,strip.now*SEGMENT.speed/64*SEGLEN/255); // X location is constant, but we move along the Y at the rate of millis(). By Andrew Tuline. index = (255 - i*256/SEGLEN) * index/(256-SEGMENT.intensity); // Now we need to scale index so that it gets blacker as we get close to one of the ends. // This is a simple y=mx+b equation that's been scaled. index/128 is another scaling. @@ -6747,7 +6736,7 @@ uint16_t mode_noisefire(void) { // Noisefire. By Andrew Tuline. return FRAMETIME; } // mode_noisefire() -static const char _data_FX_MODE_NOISEFIRE[] PROGMEM = "Noisefire@!,!;;;1v;m12=2,si=0"; // Circle, Beatsin +static const char _data_FX_MODE_NOISEFIRE[] PROGMEM = "Noisefire@!,!;;;01v;m12=2,si=0"; // Circle, Beatsin /////////////////////// @@ -6755,13 +6744,9 @@ static const char _data_FX_MODE_NOISEFIRE[] PROGMEM = "Noisefire@!,!;;;1v;m12=2, /////////////////////// uint16_t mode_noisemeter(void) { // Noisemeter. By Andrew Tuline. - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; - int16_t volumeRaw = *(int16_t*)um_data->u_data[1]; + int volumeRaw = *(int16_t*)um_data->u_data[1]; //uint8_t fadeRate = map(SEGMENT.speed,0,255,224,255); uint8_t fadeRate = map(SEGMENT.speed,0,255,200,254); @@ -6796,12 +6781,8 @@ uint16_t mode_pixelwave(void) { // Pixelwave. By Andrew Tuline. SEGMENT.fill(BLACK); } - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } - int16_t volumeRaw = *(int16_t*)um_data->u_data[1]; + um_data_t *um_data = getAudioData(); + int volumeRaw = *(int16_t*)um_data->u_data[1]; uint8_t secondHand = micros()/(256-SEGMENT.speed)/500+1 % 16; if (SEGENV.aux0 != secondHand) { @@ -6832,11 +6813,7 @@ uint16_t mode_plasmoid(void) { // Plasmoid. By Andrew Tuline. if (!SEGENV.allocateData(sizeof(plasphase))) return mode_static(); //allocation failed Plasphase* plasmoip = reinterpret_cast(SEGENV.data); - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; SEGMENT.fadeToBlackBy(32); @@ -6857,7 +6834,7 @@ uint16_t mode_plasmoid(void) { // Plasmoid. By Andrew Tuline. return FRAMETIME; } // mode_plasmoid() -static const char _data_FX_MODE_PLASMOID[] PROGMEM = "Plasmoid@Phase,# of pixels;!,!;!;1v;sx=128,ix=128,m12=0,si=0"; // Pixels, Beatsin +static const char _data_FX_MODE_PLASMOID[] PROGMEM = "Plasmoid@Phase,# of pixels;!,!;!;01v;sx=128,ix=128,m12=0,si=0"; // Pixels, Beatsin /////////////////////// @@ -6867,15 +6844,11 @@ static const char _data_FX_MODE_PLASMOID[] PROGMEM = "Plasmoid@Phase,# of pixels uint16_t mode_puddlepeak(void) { // Puddlepeak. By Andrew Tuline. if (SEGLEN == 1) return mode_static(); - uint16_t size = 0; + unsigned size = 0; uint8_t fadeVal = map(SEGMENT.speed,0,255, 224, 254); - uint16_t pos = random16(SEGLEN); // Set a random starting position. + unsigned pos = random16(SEGLEN); // Set a random starting position. - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); uint8_t samplePeak = *(uint8_t*)um_data->u_data[3]; uint8_t *maxVol = (uint8_t*)um_data->u_data[6]; uint8_t *binNum = (uint8_t*)um_data->u_data[7]; @@ -6896,7 +6869,7 @@ uint16_t mode_puddlepeak(void) { // Puddlepeak. By Andrew Tuline. if (pos+size>= SEGLEN) size = SEGLEN - pos; } - for (int i=0; iu_data[2]; if (SEGENV.call == 0) { @@ -7033,13 +6994,14 @@ uint16_t mode_DJLight(void) { // Written by ??? Adapted by Wil CRGB color = CRGB(fftResult[15]/2, fftResult[5]/2, fftResult[0]/2); // 16-> 15 as 16 is out of bounds SEGMENT.setPixelColor(mid, color.fadeToBlackBy(map(fftResult[4], 0, 255, 255, 4))); // TODO - Update + // if SEGLEN equals 1 these loops won't execute for (int i = SEGLEN - 1; i > mid; i--) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i-1)); // move to the left for (int i = 0; i < mid; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // move to the right } return FRAMETIME; } // mode_DJLight() -static const char _data_FX_MODE_DJLIGHT[] PROGMEM = "DJ Light@Speed;;;1f;m12=2,si=0"; // Circle, Beatsin +static const char _data_FX_MODE_DJLIGHT[] PROGMEM = "DJ Light@Speed;;;01f;m12=2,si=0"; // Circle, Beatsin //////////////////// @@ -7050,11 +7012,7 @@ uint16_t mode_freqmap(void) { // Map FFT_MajorPeak to SEGLEN. // Start frequency = 60 Hz and log10(60) = 1.78 // End frequency = MAX_FREQUENCY in Hz and lo10(MAX_FREQUENCY) = MAX_FREQ_LOG10 - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); float FFT_MajorPeak = *(float*)um_data->u_data[4]; float my_magnitude = *(float*)um_data->u_data[5] / 4.0f; if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; // log10(0) is "forbidden" (throws exception) @@ -7067,10 +7025,10 @@ uint16_t mode_freqmap(void) { // Map FFT_MajorPeak to SEGLEN. if (locn < 1) locn = 0; // avoid underflow if (locn >=SEGLEN) locn = SEGLEN-1; - uint16_t pixCol = (log10f(FFT_MajorPeak) - 1.78f) * 255.0f/(MAX_FREQ_LOG10 - 1.78f); // Scale log10 of frequency values to the 255 colour index. + unsigned pixCol = (log10f(FFT_MajorPeak) - 1.78f) * 255.0f/(MAX_FREQ_LOG10 - 1.78f); // Scale log10 of frequency values to the 255 colour index. if (FFT_MajorPeak < 61.0f) pixCol = 0; // handle underflow - uint16_t bright = (int)my_magnitude; + unsigned bright = (int)my_magnitude; SEGMENT.setPixelColor(locn, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(SEGMENT.intensity+pixCol, false, PALETTE_SOLID_WRAP, 0), bright)); @@ -7083,12 +7041,8 @@ static const char _data_FX_MODE_FREQMAP[] PROGMEM = "Freqmap@Fade rate,Starting // ** Freqmatrix // /////////////////////// uint16_t mode_freqmatrix(void) { // Freqmatrix. By Andreas Pleschung. - if (SEGLEN == 1) return mode_static(); - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + // No need to prevent from executing on single led strips, we simply change pixel 0 each time and avoid the shift + um_data_t *um_data = getAudioData(); float FFT_MajorPeak = *(float*)um_data->u_data[4]; float volumeSmth = *(float*)um_data->u_data[0]; @@ -7119,19 +7073,20 @@ uint16_t mode_freqmatrix(void) { // Freqmatrix. By Andreas Plesch int upperLimit = 80 + 42 * SEGMENT.custom2; int lowerLimit = 80 + 3 * SEGMENT.custom1; uint8_t i = lowerLimit!=upperLimit ? map(FFT_MajorPeak, lowerLimit, upperLimit, 0, 255) : FFT_MajorPeak; // may under/overflow - so we enforce uint8_t - uint16_t b = 255 * intensity; + unsigned b = 255 * intensity; if (b > 255) b = 255; color = CHSV(i, 240, (uint8_t)b); // implicit conversion to RGB supplied by FastLED } // shift the pixels one pixel up SEGMENT.setPixelColor(0, color); + // if SEGLEN equals 1 this loop won't execute for (int i = SEGLEN - 1; i > 0; i--) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i-1)); //move to the left } return FRAMETIME; } // mode_freqmatrix() -static const char _data_FX_MODE_FREQMATRIX[] PROGMEM = "Freqmatrix@Speed,Sound effect,Low bin,High bin,Sensitivity;;;1f;m12=3,si=0"; // Corner, Beatsin +static const char _data_FX_MODE_FREQMATRIX[] PROGMEM = "Freqmatrix@Speed,Sound effect,Low bin,High bin,Sensitivity;;;01f;m12=3,si=0"; // Corner, Beatsin ////////////////////// @@ -7142,11 +7097,7 @@ static const char _data_FX_MODE_FREQMATRIX[] PROGMEM = "Freqmatrix@Speed,Sound e // SEGMENT.speed select faderate // SEGMENT.intensity select colour index uint16_t mode_freqpixels(void) { // Freqpixel. By Andrew Tuline. - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); float FFT_MajorPeak = *(float*)um_data->u_data[4]; float my_magnitude = *(float*)um_data->u_data[5] / 16.0f; if (FFT_MajorPeak < 1) FFT_MajorPeak = 1.0f; // log10(0) is "forbidden" (throws exception) @@ -7154,7 +7105,7 @@ uint16_t mode_freqpixels(void) { // Freqpixel. By Andrew Tuline. // this code translates to speed * (2 - speed/255) which is a) speed*2 or b) speed (when speed is 255) // and since fade_out() can only take 0-255 it will behave incorrectly when speed > 127 //uint16_t fadeRate = 2*SEGMENT.speed - SEGMENT.speed*SEGMENT.speed/255; // Get to 255 as quick as you can. - uint16_t fadeRate = SEGMENT.speed*SEGMENT.speed; // Get to 255 as quick as you can. + unsigned fadeRate = SEGMENT.speed*SEGMENT.speed; // Get to 255 as quick as you can. fadeRate = map(fadeRate, 0, 65535, 1, 255); int fadeoutDelay = (256 - SEGMENT.speed) / 64; @@ -7163,7 +7114,7 @@ uint16_t mode_freqpixels(void) { // Freqpixel. By Andrew Tuline. uint8_t pixCol = (log10f(FFT_MajorPeak) - 1.78f) * 255.0f/(MAX_FREQ_LOG10 - 1.78f); // Scale log10 of frequency values to the 255 colour index. if (FFT_MajorPeak < 61.0f) pixCol = 0; // handle underflow for (int i=0; i < SEGMENT.intensity/32+1; i++) { - uint16_t locn = random16(0,SEGLEN); + unsigned locn = random16(0,SEGLEN); SEGMENT.setPixelColor(locn, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(SEGMENT.intensity+pixCol, false, PALETTE_SOLID_WRAP, 0), (int)my_magnitude)); } @@ -7188,12 +7139,8 @@ static const char _data_FX_MODE_FREQPIXELS[] PROGMEM = "Freqpixels@Fade rate,Sta // As a compromise between speed and accuracy we are currently sampling with 10240Hz, from which we can then determine with a 512bin FFT our max frequency is 5120Hz. // Depending on the music stream you have you might find it useful to change the frequency mapping. uint16_t mode_freqwave(void) { // Freqwave. By Andreas Pleschung. - if (SEGLEN == 1) return mode_static(); - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + // As before, this effect can also work on single pixels, we just lose the shifting effect + um_data_t *um_data = getAudioData(); float FFT_MajorPeak = *(float*)um_data->u_data[4]; float volumeSmth = *(float*)um_data->u_data[0]; @@ -7206,10 +7153,8 @@ uint16_t mode_freqwave(void) { // Freqwave. By Andreas Pleschun SEGENV.aux0 = secondHand; float sensitivity = mapf(SEGMENT.custom3, 1, 31, 1, 10); // reduced resolution slider - float pixVal = volumeSmth * (float)SEGMENT.intensity / 256.0f * sensitivity; - if (pixVal > 255) pixVal = 255; - - float intensity = mapf(pixVal, 0, 255, 0, 100) / 100.0f; // make a brightness from the last avg + float pixVal = min(255.0f, volumeSmth * (float)SEGMENT.intensity / 256.0f * sensitivity); + float intensity = mapf(pixVal, 0.0f, 255.0f, 0.0f, 100.0f) / 100.0f; // make a brightness from the last avg CRGB color = 0; @@ -7224,21 +7169,21 @@ uint16_t mode_freqwave(void) { // Freqwave. By Andreas Pleschun int upperLimit = 80 + 42 * SEGMENT.custom2; int lowerLimit = 80 + 3 * SEGMENT.custom1; uint8_t i = lowerLimit!=upperLimit ? map(FFT_MajorPeak, lowerLimit, upperLimit, 0, 255) : FFT_MajorPeak; // may under/overflow - so we enforce uint8_t - uint16_t b = 255.0 * intensity; - if (b > 255) b=255; + unsigned b = min(255.0f, 255.0f * intensity); color = CHSV(i, 240, (uint8_t)b); // implicit conversion to RGB supplied by FastLED } SEGMENT.setPixelColor(SEGLEN/2, color); // shift the pixels one pixel outwards + // if SEGLEN equals 1 these loops won't execute for (int i = SEGLEN - 1; i > SEGLEN/2; i--) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i-1)); //move to the left for (int i = 0; i < SEGLEN/2; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // move to the right } return FRAMETIME; } // mode_freqwave() -static const char _data_FX_MODE_FREQWAVE[] PROGMEM = "Freqwave@Speed,Sound effect,Low bin,High bin,Pre-amp;;;1f;m12=2,si=0"; // Circle, Beatsin +static const char _data_FX_MODE_FREQWAVE[] PROGMEM = "Freqwave@Speed,Sound effect,Low bin,High bin,Pre-amp;;;01f;m12=2,si=0"; // Circle, Beatsin /////////////////////// @@ -7246,15 +7191,11 @@ static const char _data_FX_MODE_FREQWAVE[] PROGMEM = "Freqwave@Speed,Sound effec /////////////////////// uint16_t mode_gravfreq(void) { // Gravfreq. By Andrew Tuline. if (SEGLEN == 1) return mode_static(); - uint16_t dataSize = sizeof(gravity); + unsigned dataSize = sizeof(gravity); if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Gravity* gravcen = reinterpret_cast(SEGENV.data); - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); float FFT_MajorPeak = *(float*)um_data->u_data[4]; float volumeSmth = *(float*)um_data->u_data[0]; if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; // log10(0) is "forbidden" (throws exception) @@ -7297,12 +7238,7 @@ static const char _data_FX_MODE_GRAVFREQ[] PROGMEM = "Gravfreq@Rate of fall,Sens // ** Noisemove // ////////////////////// uint16_t mode_noisemove(void) { // Noisemove. By: Andrew Tuline - if (SEGLEN == 1) return mode_static(); - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; int fadeoutDelay = (256 - SEGMENT.speed) / 96; @@ -7310,26 +7246,22 @@ uint16_t mode_noisemove(void) { // Noisemove. By: Andrew Tuli uint8_t numBins = map(SEGMENT.intensity,0,255,0,16); // Map slider to fftResult bins. for (int i=0; iu_data[4]; float my_magnitude = *(float*) um_data->u_data[5] / 16.0f; @@ -7351,13 +7283,13 @@ uint16_t mode_rocktaves(void) { // Rocktaves. Same note from eac frTemp -= 132.0f; // This should give us a base musical note of C3 frTemp = fabsf(frTemp * 2.1f); // Fudge factors to compress octave range starting at 0 and going to 255; - uint16_t i = map(beatsin8(8+octCount*4, 0, 255, 0, octCount*8), 0, 255, 0, SEGLEN-1); - i = constrain(i, 0, SEGLEN-1); + unsigned i = map(beatsin8(8+octCount*4, 0, 255, 0, octCount*8), 0, 255, 0, SEGLEN-1); + i = constrain(i, 0U, SEGLEN-1U); SEGMENT.addPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette((uint8_t)frTemp, false, PALETTE_SOLID_WRAP, 0), volTemp)); return FRAMETIME; } // mode_rocktaves() -static const char _data_FX_MODE_ROCKTAVES[] PROGMEM = "Rocktaves@;!,!;!;1f;m12=1,si=0"; // Bar, Beatsin +static const char _data_FX_MODE_ROCKTAVES[] PROGMEM = "Rocktaves@;!,!;!;01f;m12=1,si=0"; // Bar, Beatsin /////////////////////// @@ -7365,13 +7297,9 @@ static const char _data_FX_MODE_ROCKTAVES[] PROGMEM = "Rocktaves@;!,!;!;1f;m12=1 /////////////////////// // Combines peak detection with FFT_MajorPeak and FFT_Magnitude. uint16_t mode_waterfall(void) { // Waterfall. By: Andrew Tuline - if (SEGLEN == 1) return mode_static(); - - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + // effect can work on single pixels, we just lose the shifting effect + + 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]; @@ -7403,12 +7331,13 @@ uint16_t mode_waterfall(void) { // Waterfall. By: Andrew Tulin } else { SEGMENT.setPixelColor(SEGLEN-1, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(pixCol+SEGMENT.intensity, false, PALETTE_SOLID_WRAP, 0), (int)my_magnitude)); } + // loop will not execute if SEGLEN equals 1 for (int i = 0; i < SEGLEN-1; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // shift left } return FRAMETIME; } // mode_waterfall() -static const char _data_FX_MODE_WATERFALL[] PROGMEM = "Waterfall@!,Adjust color,Select bin,Volume (min);!,!;!;1f;c2=0,m12=2,si=0"; // Circles, Beatsin +static const char _data_FX_MODE_WATERFALL[] PROGMEM = "Waterfall@!,Adjust color,Select bin,Volume (min);!,!;!;01f;c2=0,m12=2,si=0"; // Circles, Beatsin #ifndef WLED_DISABLE_2D @@ -7419,17 +7348,13 @@ uint16_t mode_2DGEQ(void) { // By Will Tatam. Code reduction by Ewoud Wijma. if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up const int NUM_BANDS = map(SEGMENT.custom1, 0, 255, 1, 16); - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); if (!SEGENV.allocateData(cols*sizeof(uint16_t))) return mode_static(); //allocation failed uint16_t *previousBarHeight = reinterpret_cast(SEGENV.data); //array of previous bar heights per frequency band - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; if (SEGENV.call == 0) for (int i=0; i previousBarHeight[x]) previousBarHeight[x] = barHeight; //drive the peak up uint32_t ledColor = BLACK; @@ -7476,8 +7401,8 @@ static const char _data_FX_MODE_2DGEQ[] PROGMEM = "GEQ@Fade speed,Ripple decay,# uint16_t mode_2DFunkyPlank(void) { // Written by ??? Adapted by Will Tatam. if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); int NUMB_BANDS = map(SEGMENT.custom1, 0, 255, 1, 16); int barWidth = (cols / NUMB_BANDS); @@ -7488,11 +7413,7 @@ uint16_t mode_2DFunkyPlank(void) { // Written by ??? Adapted by Wil bandInc = (NUMB_BANDS / cols); } - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; if (SEGENV.call == 0) { @@ -7568,10 +7489,10 @@ static uint8_t akemi[] PROGMEM = { uint16_t mode_2DAkemi(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); - uint16_t counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF; + unsigned counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF; counter = counter >> 8; const float lightFactor = 0.15f; @@ -7587,9 +7508,9 @@ uint16_t mode_2DAkemi(void) { //draw and color Akemi for (int y=0; y < rows; y++) for (int x=0; x < cols; x++) { CRGB color; - CRGB soundColor = ORANGE; - CRGB faceColor = SEGMENT.color_wheel(counter); - CRGB armsAndLegsColor = SEGCOLOR(1) > 0 ? SEGCOLOR(1) : 0xFFE0A0; //default warmish white 0xABA8FF; //0xFF52e5;// + CRGB soundColor = CRGB::Orange; + CRGB faceColor = CRGB(SEGMENT.color_wheel(counter)); + CRGB armsAndLegsColor = CRGB(SEGCOLOR(1) > 0 ? SEGCOLOR(1) : 0xFFE0A0); //default warmish white 0xABA8FF; //0xFF52e5;// uint8_t ak = pgm_read_byte_near(akemi + ((y * 32)/rows) * 32 + (x * 32)/cols); // akemi[(y * 32)/rows][(x * 32)/cols] switch (ak) { case 3: armsAndLegsColor.r *= lightFactor; armsAndLegsColor.g *= lightFactor; armsAndLegsColor.b *= lightFactor; color = armsAndLegsColor; break; //light arms and legs 0x9B9B9B @@ -7613,10 +7534,10 @@ uint16_t mode_2DAkemi(void) { //add geq left and right if (um_data && fftResult) { for (int x=0; x < cols/8; x++) { - uint16_t band = x * cols/8; + unsigned band = x * cols/8; band = constrain(band, 0, 15); - uint16_t barHeight = map(fftResult[band], 0, 255, 0, 17*rows/32); - CRGB color = SEGMENT.color_from_palette((band * 35), false, PALETTE_SOLID_WRAP, 0); + int barHeight = map(fftResult[band], 0, 255, 0, 17*rows/32); + CRGB color = CRGB(SEGMENT.color_from_palette((band * 35), false, PALETTE_SOLID_WRAP, 0)); for (int y=0; y < barHeight; y++) { SEGMENT.setPixelColorXY(x, rows/2-y, color); @@ -7636,29 +7557,29 @@ static const char _data_FX_MODE_2DAKEMI[] PROGMEM = "Akemi@Color speed,Dance;Hea uint16_t mode_2Ddistortionwaves() { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); uint8_t speed = SEGMENT.speed/32; uint8_t scale = SEGMENT.intensity/32; uint8_t w = 2; - uint16_t a = strip.now/32; - uint16_t a2 = a/2; - uint16_t a3 = a/3; + unsigned a = strip.now/32; + unsigned a2 = a/2; + unsigned a3 = a/3; - uint16_t cx = beatsin8(10-speed,0,cols-1)*scale; - uint16_t cy = beatsin8(12-speed,0,rows-1)*scale; - uint16_t cx1 = beatsin8(13-speed,0,cols-1)*scale; - uint16_t cy1 = beatsin8(15-speed,0,rows-1)*scale; - uint16_t cx2 = beatsin8(17-speed,0,cols-1)*scale; - uint16_t cy2 = beatsin8(14-speed,0,rows-1)*scale; + unsigned cx = beatsin8(10-speed,0,cols-1)*scale; + unsigned cy = beatsin8(12-speed,0,rows-1)*scale; + unsigned cx1 = beatsin8(13-speed,0,cols-1)*scale; + unsigned cy1 = beatsin8(15-speed,0,rows-1)*scale; + unsigned cx2 = beatsin8(17-speed,0,cols-1)*scale; + unsigned cy2 = beatsin8(14-speed,0,rows-1)*scale; - uint16_t xoffs = 0; + unsigned xoffs = 0; for (int x = 0; x < cols; x++) { xoffs += scale; - uint16_t yoffs = 0; + unsigned yoffs = 0; for (int y = 0; y < rows; y++) { yoffs += scale; @@ -7691,8 +7612,8 @@ static const char _data_FX_MODE_2DDISTORTIONWAVES[] PROGMEM = "Distortion Waves@ uint16_t mode_2Dsoap() { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); const size_t dataSize = SEGMENT.width() * SEGMENT.height() * sizeof(uint8_t); // prevent reallocation if mirrored or grouped if (!SEGENV.allocateData(dataSize + sizeof(uint32_t)*3)) return mode_static(); //allocation failed @@ -7739,8 +7660,8 @@ uint16_t mode_2Dsoap() { int zD; int zF; int amplitude; - int8_t shiftX = 0; //(SEGMENT.custom1 - 128) / 4; - int8_t shiftY = 0; //(SEGMENT.custom2 - 128) / 4; + int shiftX = 0; //(SEGMENT.custom1 - 128) / 4; + int shiftY = 0; //(SEGMENT.custom2 - 128) / 4; CRGB ledsbuff[MAX(cols,rows)]; amplitude = (cols >= 16) ? (cols-8)/8 : 1; @@ -7803,8 +7724,8 @@ static const char _data_FX_MODE_2DSOAP[] PROGMEM = "Soap@!,Smoothness;;!;2"; uint16_t mode_2Doctopus() { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); const uint8_t mapp = 180 / MAX(cols,rows); typedef struct { @@ -7842,8 +7763,8 @@ uint16_t mode_2Doctopus() { byte angle = rMap[XY(x,y)].angle; byte radius = rMap[XY(x,y)].radius; //CRGB c = CHSV(SEGENV.step / 2 - radius, 255, sin8(sin8((angle * 4 - radius) / 4 + SEGENV.step) + radius - SEGENV.step * 2 + angle * (SEGMENT.custom3/3+1))); - uint16_t intensity = sin8(sin8((angle * 4 - radius) / 4 + SEGENV.step/2) + radius - SEGENV.step + angle * (SEGMENT.custom3/4+1)); - intensity = map(intensity*intensity, 0, 65535, 0, 255); // add a bit of non-linearity for cleaner display + unsigned intensity = sin8(sin8((angle * 4 - radius) / 4 + SEGENV.step/2) + radius - SEGENV.step + angle * (SEGMENT.custom3/4+1)); + intensity = map((intensity*intensity) & 0xFFFF, 0, 65535, 0, 255); // add a bit of non-linearity for cleaner display CRGB c = ColorFromPalette(SEGPALETTE, SEGENV.step / 2 - radius, intensity); SEGMENT.setPixelColorXY(x, y, c); } @@ -7859,8 +7780,8 @@ static const char _data_FX_MODE_2DOCTOPUS[] PROGMEM = "Octopus@!,,Offset X,Offse uint16_t mode_2Dwavingcell() { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); uint32_t t = strip.now/(257-SEGMENT.speed); uint8_t aX = SEGMENT.custom1/16 + 9; @@ -7884,18 +7805,23 @@ static const char _data_RESERVED[] PROGMEM = "RSVD"; // add (or replace reserved) effect mode and data into vector // use id==255 to find unallocated gaps (with "Reserved" data string) // if vector size() is smaller than id (single) data is appended at the end (regardless of id) -void WS2812FX::addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name) { +// return the actual id used for the effect or 255 if the add failed. +uint8_t WS2812FX::addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name) { if (id == 255) { // find empty slot for (size_t i=1; i<_mode.size(); i++) if (_modeData[i] == _data_RESERVED) { id = i; break; } } if (id < _mode.size()) { - if (_modeData[id] != _data_RESERVED) return; // do not overwrite alerady added effect + if (_modeData[id] != _data_RESERVED) return 255; // do not overwrite an already added effect _mode[id] = mode_fn; _modeData[id] = mode_name; - } else { + return id; + } else if(_mode.size() < 255) { // 255 is reserved for indicating the effect wasn't added _mode.push_back(mode_fn); _modeData.push_back(mode_name); if (_modeCount < _mode.size()) _modeCount++; + return _mode.size() - 1; + } else { + return 255; // The vector is full so return 255 } } diff --git a/wled00/FX.h b/wled00/FX.h index 3aa19bc3..b1b1fbcb 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -59,13 +59,12 @@ /* Not used in all effects yet */ #define WLED_FPS 42 #define FRAMETIME_FIXED (1000/WLED_FPS) -//#define FRAMETIME _frametime #define FRAMETIME strip.getFrameTime() /* each segment uses 82 bytes of SRAM memory, so if you're application fails because of insufficient memory, decreasing MAX_NUM_SEGMENTS may help */ #ifdef ESP8266 - #define MAX_NUM_SEGMENTS 12 + #define MAX_NUM_SEGMENTS 16 /* How much data bytes all segments combined may allocate */ #define MAX_SEGMENT_DATA 5120 #else @@ -73,11 +72,7 @@ #define MAX_NUM_SEGMENTS 32 #endif #if defined(ARDUINO_ARCH_ESP32S2) - #if defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) - #define MAX_SEGMENT_DATA MAX_NUM_SEGMENTS*1024 // 32k by default - #else - #define MAX_SEGMENT_DATA MAX_NUM_SEGMENTS*768 // 24k by default - #endif + #define MAX_SEGMENT_DATA MAX_NUM_SEGMENTS*768 // 24k by default (S2 is short on free RAM) #else #define MAX_SEGMENT_DATA MAX_NUM_SEGMENTS*1280 // 40k by default #endif @@ -95,7 +90,7 @@ //#define SEGCOLOR(x) strip._segments[strip.getCurrSegmentId()].currentColor(x, strip._segments[strip.getCurrSegmentId()].colors[x]) //#define SEGLEN strip._segments[strip.getCurrSegmentId()].virtualLength() #define SEGCOLOR(x) strip.segColor(x) /* saves us a few kbytes of code */ -#define SEGPALETTE strip._currentPalette +#define SEGPALETTE Segment::getCurrentPalette() #define SEGLEN strip._virtualSegmentLength /* saves us a few kbytes of code */ #define SPEED_FORMULA_L (5U + (50U*(255U - SEGMENT.speed))/SEGLEN) @@ -111,6 +106,10 @@ #define PURPLE (uint32_t)0x400080 #define ORANGE (uint32_t)0xFF3000 #define PINK (uint32_t)0xFF1493 +#define GREY (uint32_t)0x808080 +#define GRAY GREY +#define DARKGREY (uint32_t)0x333333 +#define DARKGRAY DARKGREY #define ULTRAWHITE (uint32_t)0xFFFFFFFF #define DARKSLATEGRAY (uint32_t)0x2F4F4F #define DARKSLATEGREY DARKSLATEGRAY @@ -187,7 +186,7 @@ #define FX_MODE_LIGHTNING 57 #define FX_MODE_ICU 58 #define FX_MODE_MULTI_COMET 59 -#define FX_MODE_DUAL_LARSON_SCANNER 60 +#define FX_MODE_DUAL_LARSON_SCANNER 60 // candidate for removal (use Scanner with with check 1) #define FX_MODE_RANDOM_CHASE 61 #define FX_MODE_OSCILLATE 62 #define FX_MODE_PRIDE_2015 63 @@ -325,7 +324,8 @@ typedef enum mapping1D2D { M12_Pixels = 0, M12_pBar = 1, M12_pArc = 2, - M12_pCorner = 3 + M12_pCorner = 3, + M12_sPinwheel = 4 } mapping1D2D_t; // segment, 80 bytes @@ -418,6 +418,7 @@ typedef struct Segment { static uint16_t _usedSegmentData; // perhaps this should be per segment, not static + static CRGBPalette16 _currentPalette; // palette used for current effect (includes transition, used in color_from_palette()) static CRGBPalette16 _randomPalette; // actual random palette static CRGBPalette16 _newRandomPalette; // target random palette static uint16_t _lastPaletteChange; // last random palette change time in millis()/1000 @@ -535,6 +536,7 @@ typedef struct Segment { static void modeBlend(bool blend) { _modeBlend = blend; } #endif static void handleRandomPalette(); + inline static const CRGBPalette16 &getCurrentPalette(void) { return Segment::_currentPalette; } void setUp(uint16_t i1, uint16_t i2, uint8_t grp=1, uint8_t spc=0, uint16_t ofs=UINT16_MAX, uint16_t i1Y=0, uint16_t i2Y=1); bool setColor(uint8_t slot, uint32_t c); //returns true if changed @@ -561,18 +563,18 @@ typedef struct Segment { // transition functions void startTransition(uint16_t dur); // transition has to start before actual segment values change - void stopTransition(void); // ends transition mode by destroying transition structure - void handleTransition(void); + void stopTransition(void); // ends transition mode by destroying transition structure (does nothing if not in transition) + inline void handleTransition(void) { 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 #endif - uint16_t progress(void); // transition progression between 0-65535 - uint8_t currentBri(bool useCct = false); // current segment brightness/CCT (blended while in transition) - uint8_t currentMode(void); // currently active effect/mode (while in transition) - uint32_t currentColor(uint8_t slot); // currently active segment color (blended while in transition) + uint16_t progress(void) const; // transition progression between 0-65535 + uint8_t currentBri(bool useCct = false) const; // current segment brightness/CCT (blended while in transition) + uint8_t currentMode(void) const; // currently active effect/mode (while in transition) + uint32_t currentColor(uint8_t slot) const; // currently active segment color (blended while in transition) CRGBPalette16 &loadPalette(CRGBPalette16 &tgt, uint8_t pal); - CRGBPalette16 ¤tPalette(CRGBPalette16 &tgt, uint8_t paletteID); + void setCurrentPalette(void); // 1D strip uint16_t virtualLength(void) const; @@ -580,12 +582,14 @@ typedef struct Segment { 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)); } + #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); } - uint32_t getPixelColor(int i); + #endif + uint32_t getPixelColor(int i) const; // 1D support functions (some implement 2D as well) - void blur(uint8_t); + void blur(uint8_t, bool smear = false); void fill(uint32_t c); void fade_out(uint8_t r); void fadeToBlackBy(uint8_t fadeBy); @@ -595,8 +599,18 @@ typedef struct Segment { inline void addPixelColor(int n, byte r, byte g, byte b, byte w = 0, bool fast = false) { addPixelColor(n, RGBW32(r,g,b,w), fast); } inline void addPixelColor(int n, CRGB c, bool fast = false) { addPixelColor(n, RGBW32(c.r,c.g,c.b,0), fast); } inline void fadePixelColor(uint16_t n, uint8_t fade) { setPixelColor(n, color_fade(getPixelColor(n), fade, true)); } - uint32_t color_from_palette(uint16_t, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri = 255); - uint32_t color_wheel(uint8_t pos); + uint32_t color_from_palette(uint16_t, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri = 255) const; + uint32_t color_wheel(uint8_t pos) const; + + // 2D Blur: shortcuts for bluring columns or rows only (50% faster than full 2D blur) + inline void blurCols(fract8 blur_amount, bool smear = false) { // blur all columns + const unsigned cols = virtualWidth(); + for (unsigned k = 0; k < cols; k++) blurCol(k, blur_amount, smear); + } + inline void blurRows(fract8 blur_amount, bool smear = false) { // blur all rows + const unsigned rows = virtualHeight(); + for ( unsigned i = 0; i < rows; i++) blurRow(i, blur_amount, smear); + } // 2D matrix uint16_t virtualWidth(void) const; // segment width in virtual pixels (accounts for groupping and spacing) @@ -608,10 +622,13 @@ typedef struct Segment { 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)); } + #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); } - uint32_t getPixelColorXY(uint16_t x, uint16_t y); + #endif + uint32_t getPixelColorXY(int x, int y) const; // 2D support functions inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t color, uint8_t blend) { setPixelColorXY(x, y, color_blend(getPixelColorXY(x,y), color, blend)); } inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) { blendPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), blend); } @@ -619,48 +636,57 @@ typedef struct Segment { inline void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool fast = false) { addPixelColorXY(x, y, RGBW32(r,g,b,w), fast); } inline void addPixelColorXY(int x, int y, CRGB c, bool fast = false) { addPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), fast); } inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) { setPixelColorXY(x, y, color_fade(getPixelColorXY(x,y), fade, true)); } - void box_blur(uint16_t i, bool vertical, fract8 blur_amount); // 1D box blur (with weight) - void blurRow(uint16_t row, fract8 blur_amount); - void blurCol(uint16_t col, fract8 blur_amount); + void box_blur(unsigned r = 1U, bool smear = false); // 2D box blur + void blur2D(uint8_t blur_amount, bool smear = false); + void blurRow(uint32_t row, fract8 blur_amount, bool smear = false); + void blurCol(uint32_t col, fract8 blur_amount, bool smear = false); void moveX(int8_t delta, bool wrap = false); void moveY(int8_t delta, bool wrap = false); void move(uint8_t dir, uint8_t delta, bool wrap = false); - void draw_circle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c); - void fill_circle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c); - void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c); - inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c) { drawLine(x0, y0, x1, y1, RGBW32(c.r,c.g,c.b,0)); } // automatic inline + void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false); + inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) { drawCircle(cx, cy, radius, RGBW32(c.r,c.g,c.b,0), soft); } + void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false); + inline void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) { fillCircle(cx, cy, radius, RGBW32(c.r,c.g,c.b,0), soft); } + void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft = false); + inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c, bool soft = false) { drawLine(x0, y0, x1, y1, RGBW32(c.r,c.g,c.b,0), soft); } // automatic inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t col2 = 0, int8_t rotate = 0); inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c) { drawCharacter(chr, x, y, w, h, RGBW32(c.r,c.g,c.b,0)); } // automatic inline inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2, int8_t rotate = 0) { drawCharacter(chr, x, y, w, h, RGBW32(c.r,c.g,c.b,0), RGBW32(c2.r,c2.g,c2.b,0), rotate); } // automatic inline void wu_pixel(uint32_t x, uint32_t y, CRGB c); - void blur1d(fract8 blur_amount); // blur all rows in 1 dimension inline void blur2d(fract8 blur_amount) { blur(blur_amount); } inline void fill_solid(CRGB c) { fill(RGBW32(c.r,c.g,c.b,0)); } - void nscale8(uint8_t scale); #else inline uint16_t XY(uint16_t x, uint16_t y) { return x; } inline void setPixelColorXY(int x, int y, uint32_t c) { setPixelColor(x, c); } + inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) { setPixelColor(int(x), c); } inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) { setPixelColor(x, RGBW32(r,g,b,w)); } inline void setPixelColorXY(int x, int y, CRGB c) { setPixelColor(x, RGBW32(c.r,c.g,c.b,0)); } + inline void setPixelColorXY(unsigned x, unsigned y, CRGB c) { setPixelColor(int(x), RGBW32(c.r,c.g,c.b,0)); } + #ifdef WLED_USE_AA_PIXELS inline void setPixelColorXY(float x, float y, uint32_t c, bool aa = true) { setPixelColor(x, c, aa); } inline void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) { setPixelColor(x, RGBW32(r,g,b,w), aa); } inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) { setPixelColor(x, RGBW32(c.r,c.g,c.b,0), aa); } - inline uint32_t getPixelColorXY(uint16_t x, uint16_t y) { return getPixelColor(x); } + #endif + inline uint32_t getPixelColorXY(int x, int y) { return getPixelColor(x); } inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t c, uint8_t blend) { blendPixelColor(x, c, blend); } inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) { blendPixelColor(x, RGBW32(c.r,c.g,c.b,0), blend); } inline void addPixelColorXY(int x, int y, uint32_t color, bool fast = false) { addPixelColor(x, color, fast); } inline void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool fast = false) { addPixelColor(x, RGBW32(r,g,b,w), fast); } inline void addPixelColorXY(int x, int y, CRGB c, bool fast = false) { addPixelColor(x, RGBW32(c.r,c.g,c.b,0), fast); } inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) { fadePixelColor(x, fade); } - inline void box_blur(uint16_t i, bool vertical, fract8 blur_amount) {} - inline void blurRow(uint16_t row, fract8 blur_amount) {} - inline void blurCol(uint16_t col, fract8 blur_amount) {} + inline void box_blur(unsigned i, bool vertical, fract8 blur_amount) {} + inline void blur2D(uint8_t blur_amount, bool smear = false) {} + inline void blurRow(uint32_t row, fract8 blur_amount, bool smear = false) {} + inline void blurCol(uint32_t col, fract8 blur_amount, bool smear = false) {} inline void moveX(int8_t delta, bool wrap = false) {} inline void moveY(int8_t delta, bool wrap = false) {} inline void move(uint8_t dir, uint8_t delta, bool wrap = false) {} - inline void fill_circle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c) {} - inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c) {} - inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c) {} + inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) {} + inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) {} + inline void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) {} + inline void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) {} + inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft = false) {} + inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c, bool soft = false) {} inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t = 0, int8_t = 0) {} inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB color) {} inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2, int8_t rotate = 0) {} @@ -694,8 +720,10 @@ class WS2812FX { // 96 bytes #ifndef WLED_DISABLE_2D panels(1), #endif + autoSegments(false), + correctWB(false), + cctFromRgb(false), // semi-private (just obscured) used in effect functions through macros - _currentPalette(CRGBPalette16(CRGB::Black)), _colors_t{0,0,0}, _virtualSegmentLength(0), // true private variables @@ -716,15 +744,7 @@ class WS2812FX { // 96 bytes customMappingSize(0), _lastShow(0), _segment_index(0), - _mainSegment(0), - _queuedChangesSegId(255), - _qStart(0), - _qStop(0), - _qStartY(0), - _qStopY(0), - _qGrouping(0), - _qSpacing(0), - _qOffset(0) + _mainSegment(0) { WS2812FX::instance = this; _mode.reserve(_modeCount); // allocate memory to prevent initial fragmentation (does not increase size()) @@ -766,7 +786,6 @@ class WS2812FX { // 96 bytes setPixelColor(unsigned n, uint32_t c), // paints absolute strip pixel with index n and color c show(void), // initiates LED output setTargetFps(uint8_t fps), - addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name), // add effect to the list; defined in FX.cpp setupEffectData(void); // add default effects to the list; defined in FX.cpp inline void restartRuntime() { for (Segment &seg : _segments) seg.markForReset(); } @@ -785,56 +804,56 @@ class WS2812FX { // 96 bytes bool paletteFade, checkSegmentAlignment(void), - hasRGBWBus(void), - hasCCTBus(void), - // return true if the strip is being sent pixel updates - isUpdating(void), + hasRGBWBus(void) const, + hasCCTBus(void) const, + isUpdating(void) const, // return true if the strip is being sent pixel updates deserializeMap(uint8_t n=0); - inline bool isServicing(void) { return _isServicing; } // returns true if strip.service() is executing - inline bool hasWhiteChannel(void) { return _hasWhiteChannel; } // returns true if strip contains separate white chanel - inline bool isOffRefreshRequired(void) { return _isOffRefreshRequired; } // returns true if strip requires regular updates (i.e. TM1814 chipset) - inline bool isSuspended(void) { return _suspend; } // returns true if strip.service() execution is suspended - inline bool needsUpdate(void) { return _triggered; } // returns true if strip received a trigger() request + inline bool isServicing(void) const { return _isServicing; } // returns true if strip.service() is executing + inline bool hasWhiteChannel(void) const { return _hasWhiteChannel; } // returns true if strip contains separate white chanel + inline bool isOffRefreshRequired(void) const { return _isOffRefreshRequired; } // returns true if strip requires regular updates (i.e. TM1814 chipset) + inline bool isSuspended(void) const { return _suspend; } // returns true if strip.service() execution is suspended + inline bool needsUpdate(void) const { return _triggered; } // returns true if strip received a trigger() request uint8_t paletteBlend, cctBlending, - getActiveSegmentsNum(void), - getFirstSelectedSegId(void), - getLastActiveSegmentId(void), - getActiveSegsLightCapabilities(bool selectedOnly = false); + getActiveSegmentsNum(void) const, + getFirstSelectedSegId(void) const, + getLastActiveSegmentId(void) const, + getActiveSegsLightCapabilities(bool selectedOnly = false) const, + addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name); // add effect to the list; defined in FX.cpp; - inline uint8_t getBrightness(void) { return _brightness; } // returns current strip brightness - inline uint8_t getMaxSegments(void) { return MAX_NUM_SEGMENTS; } // returns maximum number of supported segments (fixed value) - inline uint8_t getSegmentsNum(void) { return _segments.size(); } // returns currently present segments - inline uint8_t getCurrSegmentId(void) { return _segment_index; } // returns current segment index (only valid while strip.isServicing()) - inline uint8_t getMainSegmentId(void) { return _mainSegment; } // returns main segment index - inline uint8_t getPaletteCount() { return 13 + GRADIENT_PALETTE_COUNT; } // will only return built-in palette count - inline uint8_t getTargetFps() { return _targetFps; } // returns rough FPS value for las 2s interval - inline uint8_t getModeCount() { return _modeCount; } // returns number of registered modes/effects + inline uint8_t getBrightness(void) const { return _brightness; } // returns current strip brightness + inline uint8_t getMaxSegments(void) const { return MAX_NUM_SEGMENTS; } // returns maximum number of supported segments (fixed value) + inline uint8_t getSegmentsNum(void) const { return _segments.size(); } // returns currently present segments + inline uint8_t getCurrSegmentId(void) const { return _segment_index; } // returns current segment index (only valid while strip.isServicing()) + inline uint8_t getMainSegmentId(void) const { return _mainSegment; } // returns main segment index + inline uint8_t getPaletteCount() const { return 13 + GRADIENT_PALETTE_COUNT + customPalettes.size(); } + inline uint8_t getTargetFps() const { return _targetFps; } // returns rough FPS value for las 2s interval + inline uint8_t getModeCount() const { return _modeCount; } // returns number of registered modes/effects uint16_t - getLengthPhysical(void), - getLengthTotal(void), // will include virtual/nonexistent pixels in matrix - getFps(), - getMappedPixelIndex(uint16_t index); + getLengthPhysical(void) const, + getLengthTotal(void) const, // will include virtual/nonexistent pixels in matrix + getFps() const, + getMappedPixelIndex(uint16_t index) const; - inline uint16_t getFrameTime(void) { return _frametime; } // returns amount of time a frame should take (in ms) - inline uint16_t getMinShowDelay(void) { return MIN_SHOW_DELAY; } // returns minimum amount of time strip.service() can be delayed (constant) - inline uint16_t getLength(void) { return _length; } // returns actual amount of LEDs on a strip (2D matrix may have less LEDs than W*H) - inline uint16_t getTransition(void) { return _transitionDur; } // returns currently set transition time (in ms) + inline uint16_t getFrameTime(void) const { return _frametime; } // returns amount of time a frame should take (in ms) + inline uint16_t getMinShowDelay(void) const { return MIN_SHOW_DELAY; } // returns minimum amount of time strip.service() can be delayed (constant) + inline uint16_t getLength(void) const { return _length; } // returns actual amount of LEDs on a strip (2D matrix may have less LEDs than W*H) + inline uint16_t getTransition(void) const { return _transitionDur; } // returns currently set transition time (in ms) uint32_t now, timebase, - getPixelColor(uint16_t); + getPixelColor(uint16_t) const; - inline uint32_t getLastShow(void) { return _lastShow; } // returns millis() timestamp of last strip.show() call - inline uint32_t segColor(uint8_t i) { return _colors_t[i]; } // returns currently valid color (for slot i) AKA SEGCOLOR(); may be blended between two colors while in transition + inline uint32_t getLastShow(void) const { return _lastShow; } // returns millis() timestamp of last strip.show() call + inline uint32_t segColor(uint8_t i) const { return _colors_t[i]; } // returns currently valid color (for slot i) AKA SEGCOLOR(); may be blended between two colors while in transition const char * - getModeData(uint8_t id = 0) { return (id && id<_modeCount) ? _modeData[id] : PSTR("Solid"); } + getModeData(uint8_t id = 0) const { return (id && id<_modeCount) ? _modeData[id] : PSTR("Solid"); } const char ** getModeDataSrc(void) { return &(_modeData[0]); } // vectors use arrays for underlying data @@ -849,7 +868,7 @@ class WS2812FX { // 96 bytes isMatrix; #ifndef WLED_DISABLE_2D - #define WLED_MAX_PANELS 64 + #define WLED_MAX_PANELS 18 uint8_t panels; @@ -885,14 +904,19 @@ class WS2812FX { // 96 bytes inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) { setPixelColorXY(x, y, RGBW32(r,g,b,w)); } inline void setPixelColorXY(int x, int y, CRGB c) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); } - inline uint32_t getPixelColorXY(uint16_t x, uint16_t y) { 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 void loadCustomPalettes(void); // loads custom palettes from JSON - CRGBPalette16 _currentPalette; // palette used for current effect (includes transition) std::vector customPalettes; // TODO: move custom palettes out of WS2812FX class + struct { + bool autoSegments : 1; + bool correctWB : 1; + bool cctFromRgb : 1; + }; + // using public variables to reduce code size increase due to inline function getSegment() (with bounds checking) // and color transitions uint32_t _colors_t[3]; // color used for effect (includes transition) @@ -933,14 +957,6 @@ class WS2812FX { // 96 bytes uint8_t _segment_index; uint8_t _mainSegment; - uint8_t _queuedChangesSegId; - uint16_t _qStart, _qStop, _qStartY, _qStopY; - uint8_t _qGrouping, _qSpacing; - uint16_t _qOffset; -/* - void - setUpSegmentFromQueuedChanges(void); -*/ }; extern const char JSON_mode_names[]; diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index 7aecd227..44919f92 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -65,9 +65,10 @@ void WS2812FX::setUpMatrix() { customMappingSize = 0; // prevent use of mapping if anything goes wrong - if (customMappingTable == nullptr) customMappingTable = new uint16_t[getLengthTotal()]; + if (customMappingTable) delete[] customMappingTable; + customMappingTable = new uint16_t[getLengthTotal()]; - if (customMappingTable != nullptr) { + if (customMappingTable) { customMappingSize = getLengthTotal(); // fill with empty in case we don't fill the entire matrix @@ -109,11 +110,11 @@ void WS2812FX::setUpMatrix() { releaseJSONBufferLock(); } - uint16_t x, y, pix=0; //pixel + unsigned x, y, pix=0; //pixel for (size_t pan = 0; pan < panel.size(); pan++) { Panel &p = panel[pan]; - uint16_t h = p.vertical ? p.height : p.width; - uint16_t v = p.vertical ? p.width : p.height; + unsigned h = p.vertical ? p.height : p.width; + unsigned v = p.vertical ? p.width : p.height; for (size_t j = 0; j < v; j++){ for(size_t i = 0; i < h; i++) { y = (p.vertical?p.rightStart:p.bottomStart) ? v-j-1 : j; @@ -138,7 +139,7 @@ void WS2812FX::setUpMatrix() { DEBUG_PRINTLN(); #endif } else { // memory allocation error - DEBUG_PRINTLN(F("Ledmap alloc error.")); + DEBUG_PRINTLN(F("ERROR 2D LED map allocation error.")); isMatrix = false; panels = 0; panel.clear(); @@ -162,8 +163,8 @@ void WS2812FX::setUpMatrix() { // XY(x,y) - gets pixel index within current segment (often used to reference leds[] array element) uint16_t IRAM_ATTR Segment::XY(uint16_t x, uint16_t y) { - uint16_t width = virtualWidth(); // segment width in logical pixels (can be 0 if segment is inactive) - uint16_t height = virtualHeight(); // segment height in logical pixels (is always >= 1) + unsigned width = virtualWidth(); // segment width in logical pixels (can be 0 if segment is inactive) + unsigned height = virtualHeight(); // segment height in logical pixels (is always >= 1) return isActive() ? (x%width) + (y%height) * width : 0; } @@ -174,26 +175,25 @@ void IRAM_ATTR Segment::setPixelColorXY(int x, int y, uint32_t col) uint8_t _bri_t = currentBri(); if (_bri_t < 255) { - byte r = scale8(R(col), _bri_t); - byte g = scale8(G(col), _bri_t); - byte b = scale8(B(col), _bri_t); - byte w = scale8(W(col), _bri_t); - col = RGBW32(r, g, b, w); + col = color_fade(col, _bri_t); } if (reverse ) x = virtualWidth() - x - 1; if (reverse_y) y = virtualHeight() - y - 1; - if (transpose) { uint16_t t = x; x = y; y = t; } // swap X & Y if segment transposed + if (transpose) { std::swap(x,y); } // swap X & Y if segment transposed x *= groupLength(); // expand to physical pixels y *= groupLength(); // expand to physical pixels - if (x >= width() || y >= height()) return; // if pixel would fall out of segment just exit + + int W = width(); + int H = height(); + if (x >= W || y >= H) return; // if pixel would fall out of segment just exit uint32_t tmpCol = col; for (int j = 0; j < grouping; j++) { // groupping vertically for (int g = 0; g < grouping; g++) { // groupping horizontally - uint16_t xX = (x+g), yY = (y+j); - if (xX >= width() || yY >= height()) continue; // we have reached one dimension's end + int xX = (x+g), yY = (y+j); + if (xX >= W || yY >= H) continue; // we have reached one dimension's end #ifndef WLED_DISABLE_MODE_BLEND // if blending modes, blend with underlying pixel @@ -217,22 +217,23 @@ void IRAM_ATTR 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) { if (!isActive()) return; // not active if (x<0.0f || x>1.0f || y<0.0f || y>1.0f) return; // not normalized - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); + const unsigned cols = virtualWidth(); + const unsigned rows = virtualHeight(); float fX = x * (cols-1); float fY = y * (rows-1); if (aa) { - uint16_t xL = roundf(fX-0.49f); - uint16_t xR = roundf(fX+0.49f); - uint16_t yT = roundf(fY-0.49f); - uint16_t yB = roundf(fY+0.49f); + unsigned xL = roundf(fX-0.49f); + unsigned xR = roundf(fX+0.49f); + unsigned yT = roundf(fY-0.49f); + unsigned yB = roundf(fY+0.49f); float dL = (fX - xL)*(fX - xL); float dR = (xR - fX)*(xR - fX); float dT = (fY - yT)*(fY - yT); @@ -260,14 +261,15 @@ void Segment::setPixelColorXY(float x, float y, uint32_t col, bool aa) setPixelColorXY(uint16_t(roundf(fX)), uint16_t(roundf(fY)), col); } } +#endif // returns RGBW values of pixel -uint32_t IRAM_ATTR Segment::getPixelColorXY(uint16_t x, uint16_t y) { +uint32_t IRAM_ATTR Segment::getPixelColorXY(int x, int y) const { if (!isActive()) return 0; // not active if (x >= virtualWidth() || y >= virtualHeight() || x<0 || y<0) return 0; // if pixel would fall out of virtual segment just exit if (reverse ) x = virtualWidth() - x - 1; if (reverse_y) y = virtualHeight() - y - 1; - if (transpose) { uint16_t t = x; x = y; y = t; } // swap X & Y if segment transposed + if (transpose) { std::swap(x,y); } // swap X & Y if segment transposed x *= groupLength(); // expand to physical pixels y *= groupLength(); // expand to physical pixels if (x >= width() || y >= height()) return 0; @@ -275,119 +277,197 @@ uint32_t IRAM_ATTR Segment::getPixelColorXY(uint16_t x, uint16_t y) { } // blurRow: perform a blur on a row of a rectangular matrix -void Segment::blurRow(uint16_t row, fract8 blur_amount) { +void Segment::blurRow(uint32_t row, fract8 blur_amount, bool smear){ if (!isActive() || blur_amount == 0) return; // not active - const uint_fast16_t cols = virtualWidth(); - const uint_fast16_t rows = virtualHeight(); + const unsigned cols = virtualWidth(); + const unsigned rows = virtualHeight(); if (row >= rows) return; // blur one row - uint8_t keep = 255 - blur_amount; + uint8_t keep = smear ? 255 : 255 - blur_amount; uint8_t seep = blur_amount >> 1; - CRGB carryover = CRGB::Black; + uint32_t carryover = BLACK; + uint32_t lastnew; + uint32_t last; + uint32_t curnew = BLACK; for (unsigned x = 0; x < cols; x++) { - CRGB cur = getPixelColorXY(x, row); - CRGB before = cur; // remember color before blur - CRGB part = cur; - part.nscale8(seep); - cur.nscale8(keep); - cur += carryover; - if (x>0) { - CRGB prev = CRGB(getPixelColorXY(x-1, row)) + part; - setPixelColorXY(x-1, row, prev); - } - if (before != cur) // optimization: only set pixel if color has changed - setPixelColorXY(x, row, cur); + uint32_t cur = getPixelColorXY(x, row); + uint32_t part = color_fade(cur, seep); + curnew = color_fade(cur, keep); + if (x > 0) { + if (carryover) + curnew = color_add(curnew, carryover, true); + uint32_t prev = color_add(lastnew, part, true); + if (last != prev) // optimization: only set pixel if color has changed + setPixelColorXY(x - 1, row, prev); + } else // first pixel + setPixelColorXY(x, row, curnew); + lastnew = curnew; + last = cur; // save original value for comparison on next iteration carryover = part; } + setPixelColorXY(cols-1, row, curnew); // set last pixel } // blurCol: perform a blur on a column of a rectangular matrix -void Segment::blurCol(uint16_t col, fract8 blur_amount) { +void Segment::blurCol(uint32_t col, fract8 blur_amount, bool smear) { if (!isActive() || blur_amount == 0) return; // not active - const uint_fast16_t cols = virtualWidth(); - const uint_fast16_t rows = virtualHeight(); + const unsigned cols = virtualWidth(); + const unsigned rows = virtualHeight(); if (col >= cols) return; // blur one column - uint8_t keep = 255 - blur_amount; + uint8_t keep = smear ? 255 : 255 - blur_amount; uint8_t seep = blur_amount >> 1; - CRGB carryover = CRGB::Black; + uint32_t carryover = BLACK; + uint32_t lastnew; + uint32_t last; + uint32_t curnew = BLACK; for (unsigned y = 0; y < rows; y++) { - CRGB cur = getPixelColorXY(col, y); - CRGB part = cur; - CRGB before = cur; // remember color before blur - part.nscale8(seep); - cur.nscale8(keep); - cur += carryover; - if (y>0) { - CRGB prev = CRGB(getPixelColorXY(col, y-1)) + part; - setPixelColorXY(col, y-1, prev); - } - if (before != cur) // optimization: only set pixel if color has changed - setPixelColorXY(col, y, cur); - carryover = part; + uint32_t cur = getPixelColorXY(col, y); + uint32_t part = color_fade(cur, seep); + curnew = color_fade(cur, keep); + if (y > 0) { + if (carryover) + curnew = color_add(curnew, carryover, true); + uint32_t prev = color_add(lastnew, part, true); + if (last != prev) // optimization: only set pixel if color has changed + setPixelColorXY(col, y - 1, prev); + } else // first pixel + setPixelColorXY(col, y, curnew); + lastnew = curnew; + last = cur; //save original value for comparison on next iteration + carryover = part; } + setPixelColorXY(col, rows - 1, curnew); } -// 1D Box blur (with added weight - blur_amount: [0=no blur, 255=max blur]) -void Segment::box_blur(uint16_t i, bool vertical, fract8 blur_amount) { +void Segment::blur2D(uint8_t blur_amount, bool smear) { if (!isActive() || blur_amount == 0) return; // not active - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); - const uint16_t dim1 = vertical ? rows : cols; - const uint16_t dim2 = vertical ? cols : rows; - if (i >= dim2) return; - const float seep = blur_amount/255.f; - const float keep = 3.f - 2.f*seep; - // 1D box blur - CRGB tmp[dim1]; - for (int j = 0; j < dim1; j++) { - uint16_t x = vertical ? i : j; - uint16_t y = vertical ? j : i; - int16_t xp = vertical ? x : x-1; // "signed" to prevent underflow - int16_t yp = vertical ? y-1 : y; // "signed" to prevent underflow - uint16_t xn = vertical ? x : x+1; - uint16_t yn = vertical ? y+1 : y; - CRGB curr = getPixelColorXY(x,y); - CRGB prev = (xp<0 || yp<0) ? CRGB::Black : getPixelColorXY(xp,yp); - CRGB next = ((vertical && yn>=dim1) || (!vertical && xn>=dim1)) ? CRGB::Black : getPixelColorXY(xn,yn); - uint16_t r, g, b; - r = (curr.r*keep + (prev.r + next.r)*seep) / 3; - g = (curr.g*keep + (prev.g + next.g)*seep) / 3; - b = (curr.b*keep + (prev.b + next.b)*seep) / 3; - tmp[j] = CRGB(r,g,b); + const unsigned cols = virtualWidth(); + const unsigned rows = virtualHeight(); + + const uint8_t keep = smear ? 255 : 255 - blur_amount; + const uint8_t seep = blur_amount >> (1 + smear); + uint32_t lastnew; + uint32_t last; + for (unsigned row = 0; row < rows; row++) { + uint32_t carryover = BLACK; + uint32_t curnew = BLACK; + for (unsigned x = 0; x < cols; x++) { + uint32_t cur = getPixelColorXY(x, row); + uint32_t part = color_fade(cur, seep); + curnew = color_fade(cur, keep); + if (x > 0) { + if (carryover) curnew = color_add(curnew, carryover, true); + uint32_t prev = color_add(lastnew, part, true); + // optimization: only set pixel if color has changed + if (last != prev) setPixelColorXY(x - 1, row, prev); + } else setPixelColorXY(x, row, curnew); // first pixel + lastnew = curnew; + last = cur; // save original value for comparison on next iteration + carryover = part; + } + setPixelColorXY(cols-1, row, curnew); // set last pixel } - for (int j = 0; j < dim1; j++) { - uint16_t x = vertical ? i : j; - uint16_t y = vertical ? j : i; - setPixelColorXY(x, y, tmp[j]); + for (unsigned col = 0; col < cols; col++) { + uint32_t carryover = BLACK; + uint32_t curnew = BLACK; + for (unsigned y = 0; y < rows; y++) { + uint32_t cur = getPixelColorXY(col, y); + uint32_t part = color_fade(cur, seep); + curnew = color_fade(cur, keep); + if (y > 0) { + if (carryover) curnew = color_add(curnew, carryover, true); + uint32_t prev = color_add(lastnew, part, true); + // optimization: only set pixel if color has changed + if (last != prev) setPixelColorXY(col, y - 1, prev); + } else setPixelColorXY(col, y, curnew); // first pixel + lastnew = curnew; + last = cur; //save original value for comparison on next iteration + carryover = part; + } + setPixelColorXY(col, rows - 1, curnew); } } -// blur1d: one-dimensional blur filter. Spreads light to 2 line neighbors. -// blur2d: two-dimensional blur filter. Spreads light to 8 XY neighbors. -// -// 0 = no spread at all -// 64 = moderate spreading -// 172 = maximum smooth, even spreading -// -// 173..255 = wider spreading, but increasing flicker -// -// Total light is NOT entirely conserved, so many repeated -// calls to 'blur' will also result in the light fading, -// eventually all the way to black; this is by design so that -// it can be used to (slowly) clear the LEDs to black. - -void Segment::blur1d(fract8 blur_amount) { - const uint16_t rows = virtualHeight(); - for (unsigned y = 0; y < rows; y++) blurRow(y, blur_amount); +// 2D Box blur +void Segment::box_blur(unsigned radius, bool smear) { + if (!isActive() || radius == 0) return; // not active + if (radius > 3) radius = 3; + const unsigned d = (1 + 2*radius) * (1 + 2*radius); // averaging divisor + const unsigned cols = virtualWidth(); + const unsigned rows = virtualHeight(); + uint16_t *tmpRSum = new uint16_t[cols*rows]; + uint16_t *tmpGSum = new uint16_t[cols*rows]; + uint16_t *tmpBSum = new uint16_t[cols*rows]; + uint16_t *tmpWSum = new uint16_t[cols*rows]; + // fill summed-area table (https://en.wikipedia.org/wiki/Summed-area_table) + for (unsigned x = 0; x < cols; x++) { + unsigned rS, gS, bS, wS; + unsigned index; + rS = gS = bS = wS = 0; + for (unsigned y = 0; y < rows; y++) { + index = x * cols + y; + if (x > 0) { + unsigned index2 = (x - 1) * cols + y; + tmpRSum[index] = tmpRSum[index2]; + tmpGSum[index] = tmpGSum[index2]; + tmpBSum[index] = tmpBSum[index2]; + tmpWSum[index] = tmpWSum[index2]; + } else { + tmpRSum[index] = 0; + tmpGSum[index] = 0; + tmpBSum[index] = 0; + tmpWSum[index] = 0; + } + uint32_t c = getPixelColorXY(x, y); + rS += R(c); + gS += G(c); + bS += B(c); + wS += W(c); + tmpRSum[index] += rS; + tmpGSum[index] += gS; + tmpBSum[index] += bS; + tmpWSum[index] += wS; + } + } + // do a box blur using pre-calculated sums + for (unsigned x = 0; x < cols; x++) { + for (unsigned y = 0; y < rows; y++) { + // sum = D + A - B - C where k = (x,y) + // +----+-+---- (x) + // | | | + // +----A-B + // | |k| + // +----C-D + // | + //(y) + unsigned x0 = x < radius ? 0 : x - radius; + unsigned y0 = y < radius ? 0 : y - radius; + unsigned x1 = x >= cols - radius ? cols - 1 : x + radius; + unsigned y1 = y >= rows - radius ? rows - 1 : y + radius; + unsigned A = x0 * cols + y0; + unsigned B = x1 * cols + y0; + unsigned C = x0 * cols + y1; + unsigned D = x1 * cols + y1; + unsigned r = tmpRSum[D] + tmpRSum[A] - tmpRSum[C] - tmpRSum[B]; + unsigned g = tmpGSum[D] + tmpGSum[A] - tmpGSum[C] - tmpGSum[B]; + unsigned b = tmpBSum[D] + tmpBSum[A] - tmpBSum[C] - tmpBSum[B]; + unsigned w = tmpWSum[D] + tmpWSum[A] - tmpWSum[C] - tmpWSum[B]; + setPixelColorXY(x, y, RGBW32(r/d, g/d, b/d, w/d)); + } + } + delete[] tmpRSum; + delete[] tmpGSum; + delete[] tmpBSum; + delete[] tmpWSum; } void Segment::moveX(int8_t delta, bool wrap) { if (!isActive()) return; // not active - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); + const int cols = virtualWidth(); + const int rows = virtualHeight(); if (!delta || abs(delta) >= cols) return; uint32_t newPxCol[cols]; for (int y = 0; y < rows; y++) { @@ -404,8 +484,8 @@ void Segment::moveX(int8_t delta, bool wrap) { void Segment::moveY(int8_t delta, bool wrap) { if (!isActive()) return; // not active - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); + const int cols = virtualWidth(); + const int rows = virtualHeight(); if (!delta || abs(delta) >= rows) return; uint32_t newPxCol[rows]; for (int x = 0; x < cols; x++) { @@ -438,69 +518,131 @@ void Segment::move(uint8_t dir, uint8_t delta, bool wrap) { } } -void Segment::draw_circle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB col) { +void Segment::drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, bool soft) { if (!isActive() || radius == 0) return; // not active - // Bresenham’s Algorithm - int d = 3 - (2*radius); - int y = radius, x = 0; - while (y >= x) { - setPixelColorXY(cx+x, cy+y, col); - setPixelColorXY(cx-x, cy+y, col); - setPixelColorXY(cx+x, cy-y, col); - setPixelColorXY(cx-x, cy-y, col); - setPixelColorXY(cx+y, cy+x, col); - setPixelColorXY(cx-y, cy+x, col); - setPixelColorXY(cx+y, cy-x, col); - setPixelColorXY(cx-y, cy-x, col); - x++; - if (d > 0) { - y--; - d += 4 * (x - y) + 10; - } else { - d += 4 * x + 6; + if (soft) { + // Xiaolin Wu’s algorithm + int rsq = radius*radius; + int x = 0; + int y = radius; + unsigned oldFade = 0; + while (x < y) { + float yf = sqrtf(float(rsq - x*x)); // needs to be floating point + unsigned fade = float(0xFFFF) * (ceilf(yf) - yf); // how much color to keep + if (oldFade > fade) y--; + oldFade = fade; + setPixelColorXY(cx+x, cy+y, color_blend(col, getPixelColorXY(cx+x, cy+y), fade, true)); + setPixelColorXY(cx-x, cy+y, color_blend(col, getPixelColorXY(cx-x, cy+y), fade, true)); + setPixelColorXY(cx+x, cy-y, color_blend(col, getPixelColorXY(cx+x, cy-y), fade, true)); + setPixelColorXY(cx-x, cy-y, color_blend(col, getPixelColorXY(cx-x, cy-y), fade, true)); + setPixelColorXY(cx+y, cy+x, color_blend(col, getPixelColorXY(cx+y, cy+x), fade, true)); + setPixelColorXY(cx-y, cy+x, color_blend(col, getPixelColorXY(cx-y, cy+x), fade, true)); + setPixelColorXY(cx+y, cy-x, color_blend(col, getPixelColorXY(cx+y, cy-x), fade, true)); + setPixelColorXY(cx-y, cy-x, color_blend(col, getPixelColorXY(cx-y, cy-x), fade, true)); + setPixelColorXY(cx+x, cy+y-1, color_blend(getPixelColorXY(cx+x, cy+y-1), col, fade, true)); + setPixelColorXY(cx-x, cy+y-1, color_blend(getPixelColorXY(cx-x, cy+y-1), col, fade, true)); + setPixelColorXY(cx+x, cy-y+1, color_blend(getPixelColorXY(cx+x, cy-y+1), col, fade, true)); + setPixelColorXY(cx-x, cy-y+1, color_blend(getPixelColorXY(cx-x, cy-y+1), col, fade, true)); + setPixelColorXY(cx+y-1, cy+x, color_blend(getPixelColorXY(cx+y-1, cy+x), col, fade, true)); + setPixelColorXY(cx-y+1, cy+x, color_blend(getPixelColorXY(cx-y+1, cy+x), col, fade, true)); + setPixelColorXY(cx+y-1, cy-x, color_blend(getPixelColorXY(cx+y-1, cy-x), col, fade, true)); + setPixelColorXY(cx-y+1, cy-x, color_blend(getPixelColorXY(cx-y+1, cy-x), col, fade, true)); + x++; + } + } else { + // Bresenham’s Algorithm + int d = 3 - (2*radius); + int y = radius, x = 0; + while (y >= x) { + setPixelColorXY(cx+x, cy+y, col); + setPixelColorXY(cx-x, cy+y, col); + setPixelColorXY(cx+x, cy-y, col); + setPixelColorXY(cx-x, cy-y, col); + setPixelColorXY(cx+y, cy+x, col); + setPixelColorXY(cx-y, cy+x, col); + setPixelColorXY(cx+y, cy-x, col); + setPixelColorXY(cx-y, cy-x, col); + x++; + if (d > 0) { + y--; + d += 4 * (x - y) + 10; + } else { + d += 4 * x + 6; + } } } } // by stepko, taken from https://editor.soulmatelights.com/gallery/573-blobs -void Segment::fill_circle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB col) { +void Segment::fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, bool soft) { if (!isActive() || radius == 0) return; // not active - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); - for (int16_t y = -radius; y <= radius; y++) { - for (int16_t x = -radius; x <= radius; x++) { + // draw soft bounding circle + if (soft) drawCircle(cx, cy, radius, col, soft); + // fill it + const int cols = virtualWidth(); + const int rows = virtualHeight(); + for (int y = -radius; y <= radius; y++) { + for (int x = -radius; x <= radius; x++) { if (x * x + y * y <= radius * radius && - int16_t(cx)+x>=0 && int16_t(cy)+y>=0 && - int16_t(cx)+x=0 && int(cy)+y>=0 && + int(cx)+x= cols || x1 >= cols || y0 >= rows || y1 >= rows) return; - const int16_t dx = abs(x1-x0), sx = x0dy ? dx : -dy)/2, e2; - for (;;) { - setPixelColorXY(x0,y0,c); - if (x0==x1 && y0==y1) break; - e2 = err; - if (e2 >-dx) { err -= dy; x0 += sx; } - if (e2 < dy) { err += dx; y0 += sy; } + + const int dx = abs(x1-x0), sx = x0 dx; + if (steep) { + // we need to go along longest dimension + std::swap(x0,y0); + std::swap(x1,y1); + } + if (x0 > x1) { + // we need to go in increasing fashion + std::swap(x0,x1); + std::swap(y0,y1); + } + float gradient = x1-x0 == 0 ? 1.0f : float(y1-y0) / float(x1-x0); + float intersectY = y0; + for (int x = x0; x <= x1; x++) { + unsigned keep = float(0xFFFF) * (intersectY-int(intersectY)); // how much color to keep + unsigned seep = 0xFFFF - keep; // how much background to keep + int y = int(intersectY); + if (steep) std::swap(x,y); // temporaryly swap if steep + // pixel coverage is determined by fractional part of y co-ordinate + setPixelColorXY(x, y, color_blend(c, getPixelColorXY(x, y), keep, true)); + setPixelColorXY(x+int(steep), y+int(!steep), color_blend(c, getPixelColorXY(x+int(steep), y+int(!steep)), seep, true)); + intersectY += gradient; + if (steep) std::swap(x,y); // restore if steep + } + } else { + // Bresenham's algorithm + int err = (dx>dy ? dx : -dy)/2; // error direction + for (;;) { + setPixelColorXY(x0, y0, c); + if (x0==x1 && y0==y1) break; + int e2 = err; + if (e2 >-dx) { err -= dy; x0 += sx; } + if (e2 < dy) { err += dx; y0 += sy; } + } } } @@ -516,8 +658,8 @@ void Segment::drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, if (!isActive()) return; // not active if (chr < 32 || chr > 126) return; // only ASCII 32-126 supported chr -= 32; // align with font table entries - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); + const int cols = virtualWidth(); + const int rows = virtualHeight(); const int font = w*h; CRGB col = CRGB(color); @@ -556,7 +698,7 @@ void Segment::drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, void Segment::wu_pixel(uint32_t x, uint32_t y, CRGB c) { //awesome wu_pixel procedure by reddit u/sutaburosu if (!isActive()) return; // not active // extract the fractional parts and derive their inverses - uint8_t xx = x & 0xff, yy = y & 0xff, ix = 255 - xx, iy = 255 - yy; + unsigned xx = x & 0xff, yy = y & 0xff, ix = 255 - xx, iy = 255 - yy; // calculate the intensities for each affected pixel uint8_t wu[4] = {WU_WEIGHT(ix, iy), WU_WEIGHT(xx, iy), WU_WEIGHT(ix, yy), WU_WEIGHT(xx, yy)}; diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 42e98452..a06d4f84 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -77,6 +77,7 @@ uint16_t Segment::_usedSegmentData = 0U; // amount of RAM all segments use for t uint16_t Segment::maxWidth = DEFAULT_LED_COUNT; uint16_t Segment::maxHeight = 1; +CRGBPalette16 Segment::_currentPalette = CRGBPalette16(CRGB::Black); CRGBPalette16 Segment::_randomPalette = generateRandomPalette(); // was CRGBPalette16(DEFAULT_COLOR); CRGBPalette16 Segment::_newRandomPalette = generateRandomPalette(); // was CRGBPalette16(DEFAULT_COLOR); uint16_t Segment::_lastPaletteChange = 0; // perhaps it should be per segment @@ -203,7 +204,7 @@ void Segment::resetIfRequired() { CRGBPalette16 IRAM_ATTR &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { if (pal < 245 && pal > GRADIENT_PALETTE_COUNT+13) pal = 0; - if (pal > 245 && (strip.customPalettes.size() == 0 || 255U-pal > strip.customPalettes.size()-1)) pal = 0; + if (pal > 245 && (strip.customPalettes.size() == 0 || 255U-pal > strip.customPalettes.size()-1)) pal = 0; // TODO remove strip dependency by moving customPalettes out of strip //default palette. Differs depending on effect if (pal == 0) switch (mode) { case FX_MODE_FIRE_2012 : pal = 35; break; // heat palette @@ -326,16 +327,11 @@ void Segment::stopTransition() { } } -void Segment::handleTransition() { - uint16_t _progress = progress(); - if (_progress == 0xFFFFU) stopTransition(); -} - // transition progression between 0-65535 -uint16_t IRAM_ATTR Segment::progress() { +uint16_t IRAM_ATTR Segment::progress() const { if (isInTransition()) { - unsigned long timeNow = millis(); - if (_t->_dur > 0 && timeNow - _t->_start < _t->_dur) return (timeNow - _t->_start) * 0xFFFFU / _t->_dur; + unsigned diff = millis() - _t->_start; + if (_t->_dur > 0 && diff < _t->_dur) return diff * 0xFFFFU / _t->_dur; } return 0xFFFFU; } @@ -411,25 +407,25 @@ void Segment::restoreSegenv(tmpsegd_t &tmpSeg) { } #endif -uint8_t IRAM_ATTR Segment::currentBri(bool useCct) { - uint32_t prog = progress(); +uint8_t IRAM_ATTR Segment::currentBri(bool useCct) const { + unsigned prog = progress(); if (prog < 0xFFFFU) { - uint32_t curBri = (useCct ? cct : (on ? opacity : 0)) * prog; + unsigned curBri = (useCct ? cct : (on ? opacity : 0)) * prog; curBri += (useCct ? _t->_cctT : _t->_briT) * (0xFFFFU - prog); return curBri / 0xFFFFU; } return (useCct ? cct : (on ? opacity : 0)); } -uint8_t IRAM_ATTR Segment::currentMode() { +uint8_t IRAM_ATTR Segment::currentMode() const { #ifndef WLED_DISABLE_MODE_BLEND - uint16_t prog = progress(); + unsigned prog = progress(); if (modeBlending && prog < 0xFFFFU) return _t->_modeT; #endif return mode; } -uint32_t IRAM_ATTR Segment::currentColor(uint8_t slot) { +uint32_t IRAM_ATTR Segment::currentColor(uint8_t slot) const { if (slot >= NUM_COLORS) slot = 0; #ifndef WLED_DISABLE_MODE_BLEND return isInTransition() ? color_blend(_t->_segT._colorT[slot], colors[slot], progress(), true) : colors[slot]; @@ -438,35 +434,34 @@ uint32_t IRAM_ATTR Segment::currentColor(uint8_t slot) { #endif } -CRGBPalette16 IRAM_ATTR &Segment::currentPalette(CRGBPalette16 &targetPalette, uint8_t pal) { - loadPalette(targetPalette, pal); - uint16_t prog = progress(); +void Segment::setCurrentPalette() { + 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 - uint16_t noOfBlends = ((255U * prog) / 0xFFFFU) - _t->_prevPaletteBlends; - for (int i=0; i_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, targetPalette, 48); - targetPalette = _t->_palT; // copy transitioning/temporary palette + 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 } - return targetPalette; } // relies on WS2812FX::service() to call it for each frame void Segment::handleRandomPalette() { // is it time to generate a new palette? - if ((millis()/1000U) - _lastPaletteChange > randomPaletteChangeTime) { + if ((uint16_t)((uint16_t)(millis() / 1000U) - _lastPaletteChange) > randomPaletteChangeTime){ _newRandomPalette = useHarmonicRandomPalette ? generateHarmonicRandomPalette(_randomPalette) : generateRandomPalette(); - _lastPaletteChange = millis()/1000U; - _lastPaletteBlend = (uint16_t)(millis() & 0xFFFF)-512; // starts blending immediately + _lastPaletteChange = (uint16_t)(millis() / 1000U); + _lastPaletteBlend = (uint16_t)((uint16_t)millis() - 512); // starts blending immediately } // if palette transitions is enabled, blend it according to Transition Time (if longer than minimum given by service calls) if (strip.paletteFade) { // assumes that 128 updates are sufficient to blend a palette, so shift by 7 (can be more, can be less) // in reality there need to be 255 blends to fully blend two entirely different palettes - if ((millis() & 0xFFFF) - _lastPaletteBlend < strip.getTransition() >> 7) return; // not yet time to fade, delay the update - _lastPaletteBlend = millis(); + if ((uint16_t)((uint16_t)millis() - _lastPaletteBlend) < strip.getTransition() >> 7) return; // not yet time to fade, delay the update + _lastPaletteBlend = (uint16_t)millis(); } nblendPaletteTowardPalette(_randomPalette, _newRandomPalette, 48); } @@ -576,7 +571,7 @@ void Segment::setMode(uint8_t fx, bool loadDefaults) { mode = fx; // load default values from effect string if (loadDefaults) { - int16_t sOpt; + int sOpt; sOpt = extractModeDefaults(fx, "sx"); speed = (sOpt >= 0) ? sOpt : DEFAULT_SPEED; sOpt = extractModeDefaults(fx, "ix"); intensity = (sOpt >= 0) ? sOpt : DEFAULT_INTENSITY; sOpt = extractModeDefaults(fx, "c1"); custom1 = (sOpt >= 0) ? sOpt : DEFAULT_C1; @@ -610,21 +605,21 @@ void Segment::setPalette(uint8_t pal) { // 2D matrix uint16_t IRAM_ATTR Segment::virtualWidth() const { - uint16_t groupLen = groupLength(); - uint16_t vWidth = ((transpose ? height() : width()) + groupLen - 1) / groupLen; + unsigned groupLen = groupLength(); + unsigned vWidth = ((transpose ? height() : width()) + groupLen - 1) / groupLen; if (mirror) vWidth = (vWidth + 1) /2; // divide by 2 if mirror, leave at least a single LED return vWidth; } uint16_t IRAM_ATTR Segment::virtualHeight() const { - uint16_t groupLen = groupLength(); - uint16_t vHeight = ((transpose ? width() : height()) + groupLen - 1) / groupLen; + unsigned groupLen = groupLength(); + unsigned vHeight = ((transpose ? width() : height()) + groupLen - 1) / groupLen; if (mirror_y) vHeight = (vHeight + 1) /2; // divide by 2 if mirror, leave at least a single LED return vHeight; } uint16_t IRAM_ATTR Segment::nrOfVStrips() const { - uint16_t vLen = 1; + unsigned vLen = 1; #ifndef WLED_DISABLE_2D if (is2D()) { switch (map1D2D) { @@ -637,27 +632,71 @@ uint16_t IRAM_ATTR Segment::nrOfVStrips() const { return vLen; } +// Constants for mapping mode "Pinwheel" +#ifndef WLED_DISABLE_2D +constexpr int Pinwheel_Steps_Small = 72; // no holes up to 16x16 +constexpr int Pinwheel_Size_Small = 16; // larger than this -> use "Medium" +constexpr int Pinwheel_Steps_Medium = 192; // no holes up to 32x32 +constexpr int Pinwheel_Size_Medium = 32; // larger than this -> use "Big" +constexpr int Pinwheel_Steps_Big = 304; // no holes up to 50x50 +constexpr int Pinwheel_Size_Big = 50; // larger than this -> use "XL" +constexpr int Pinwheel_Steps_XL = 368; +constexpr float Int_to_Rad_Small = (DEG_TO_RAD * 360) / Pinwheel_Steps_Small; // conversion: from 0...72 to Radians +constexpr float Int_to_Rad_Med = (DEG_TO_RAD * 360) / Pinwheel_Steps_Medium; // conversion: from 0...192 to Radians +constexpr float Int_to_Rad_Big = (DEG_TO_RAD * 360) / Pinwheel_Steps_Big; // conversion: from 0...304 to Radians +constexpr float Int_to_Rad_XL = (DEG_TO_RAD * 360) / Pinwheel_Steps_XL; // conversion: from 0...368 to Radians + +constexpr int Fixed_Scale = 512; // fixpoint scaling factor (9bit for fraction) + +// Pinwheel helper function: pixel index to radians +static float getPinwheelAngle(int i, int vW, int vH) { + int maxXY = max(vW, vH); + if (maxXY <= Pinwheel_Size_Small) return float(i) * Int_to_Rad_Small; + if (maxXY <= Pinwheel_Size_Medium) return float(i) * Int_to_Rad_Med; + if (maxXY <= Pinwheel_Size_Big) return float(i) * Int_to_Rad_Big; + // else + return float(i) * Int_to_Rad_XL; +} +// Pinwheel helper function: matrix dimensions to number of rays +static int getPinwheelLength(int vW, int vH) { + int maxXY = max(vW, vH); + if (maxXY <= Pinwheel_Size_Small) return Pinwheel_Steps_Small; + if (maxXY <= Pinwheel_Size_Medium) return Pinwheel_Steps_Medium; + if (maxXY <= Pinwheel_Size_Big) return Pinwheel_Steps_Big; + // else + return Pinwheel_Steps_XL; +} +#endif + // 1D strip uint16_t IRAM_ATTR Segment::virtualLength() const { #ifndef WLED_DISABLE_2D if (is2D()) { - uint16_t vW = virtualWidth(); - uint16_t vH = virtualHeight(); - uint16_t vLen = vW * vH; // use all pixels from segment + unsigned vW = virtualWidth(); + unsigned vH = virtualHeight(); + unsigned vLen; switch (map1D2D) { case M12_pBar: vLen = vH; break; case M12_pCorner: - case M12_pArc: vLen = max(vW,vH); // get the longest dimension break; + case M12_pArc: + vLen = sqrt16(vH*vH + vW*vW); // use diagonal + break; + case M12_sPinwheel: + vLen = getPinwheelLength(vW, vH); + break; + default: + vLen = vW * vH; // use all pixels from segment + break; } return vLen; } #endif - uint16_t groupLen = groupLength(); // is always >= 1 - uint16_t vLength = (length() + groupLen - 1) / groupLen; + unsigned groupLen = groupLength(); // is always >= 1 + unsigned vLength = (length() + groupLen - 1) / groupLen; if (mirror) vLength = (vLength + 1) /2; // divide by 2 if mirror, leave at least a single LED return vLength; } @@ -674,8 +713,8 @@ void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) #ifndef WLED_DISABLE_2D if (is2D()) { - uint16_t vH = virtualHeight(); // segment height in logical pixels - uint16_t vW = virtualWidth(); + int vH = virtualHeight(); // segment height in logical pixels + int vW = virtualWidth(); switch (map1D2D) { case M12_Pixels: // use all available pixels as a long strip @@ -691,12 +730,14 @@ void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) if (i==0) setPixelColorXY(0, 0, col); else { - float step = HALF_PI / (2.85f*i); - for (float rad = 0.0f; rad <= HALF_PI+step/2; rad += step) { - // may want to try float version as well (with or without antialiasing) - int x = roundf(sin_t(rad) * i); - int y = roundf(cos_t(rad) * i); + float r = i; + float step = HALF_PI / (2.8284f * r + 4); // we only need (PI/4)/(r/sqrt(2)+1) steps + for (float rad = 0.0f; rad <= (HALF_PI/2)+step/2; rad += step) { + int x = roundf(sin_t(rad) * r); + int y = roundf(cos_t(rad) * r); + // exploit symmetry setPixelColorXY(x, y, col); + setPixelColorXY(y, x, col); } // Bresenham’s Algorithm (may not fill every pixel) //int d = 3 - (2*i); @@ -718,6 +759,52 @@ void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) for (int x = 0; x <= i; x++) setPixelColorXY(x, i, col); for (int y = 0; y < i; y++) setPixelColorXY(i, y, col); break; + case M12_sPinwheel: { + // i = angle --> 0 - 296 (Big), 0 - 192 (Medium), 0 - 72 (Small) + float centerX = roundf((vW-1) / 2.0f); + float centerY = roundf((vH-1) / 2.0f); + float angleRad = getPinwheelAngle(i, vW, vH); // angle in radians + float cosVal = cos_t(angleRad); + float sinVal = sin_t(angleRad); + + // avoid re-painting the same pixel + int lastX = INT_MIN; // impossible position + int lastY = INT_MIN; // impossible position + // draw line at angle, starting at center and ending at the segment edge + // we use fixed point math for better speed. Starting distance is 0.5 for better rounding + // int_fast16_t and int_fast32_t types changed to int, minimum bits commented + int posx = (centerX + 0.5f * cosVal) * Fixed_Scale; // X starting position in fixed point 18 bit + int posy = (centerY + 0.5f * sinVal) * Fixed_Scale; // Y starting position in fixed point 18 bit + int inc_x = cosVal * Fixed_Scale; // X increment per step (fixed point) 10 bit + int inc_y = sinVal * Fixed_Scale; // Y increment per step (fixed point) 10 bit + + int32_t maxX = vW * Fixed_Scale; // X edge in fixedpoint + int32_t maxY = vH * Fixed_Scale; // Y edge in fixedpoint + + // Odd rays start further from center if prevRay started at center. + static int prevRay = INT_MIN; // previous ray number + if ((i % 2 == 1) && (i - 1 == prevRay || i + 1 == prevRay)) { + int jump = min(vW/3, vH/3); // can add 2 if using medium pinwheel + posx += inc_x * jump; + posy += inc_y * jump; + } + prevRay = i; + + // draw ray until we hit any edge + while ((posx >= 0) && (posy >= 0) && (posx < maxX) && (posy < maxY)) { + // scale down to integer (compiler will replace division with appropriate bitshift) + int x = posx / Fixed_Scale; + int y = posy / Fixed_Scale; + // set pixel + if (x != lastX || y != lastY) setPixelColorXY(x, y, col); // only paint if pixel position is different + lastX = x; + lastY = y; + // advance to next position + posx += inc_x; + posy += inc_y; + } + break; + } } return; } else if (Segment::maxHeight!=1 && (width()==1 || height()==1)) { @@ -732,14 +819,10 @@ void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) } #endif - uint16_t len = length(); + unsigned len = length(); uint8_t _bri_t = currentBri(); if (_bri_t < 255) { - byte r = scale8(R(col), _bri_t); - byte g = scale8(G(col), _bri_t); - byte b = scale8(B(col), _bri_t); - byte w = scale8(W(col), _bri_t); - col = RGBW32(r, g, b, w); + col = color_fade(col, _bri_t); } // expand pixel (taking into account start, grouping, spacing [and offset]) @@ -777,6 +860,7 @@ void IRAM_ATTR 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) { @@ -788,8 +872,8 @@ void Segment::setPixelColor(float i, uint32_t col, bool aa) float fC = i * (virtualLength()-1); if (aa) { - uint16_t iL = roundf(fC-0.49f); - uint16_t iR = roundf(fC+0.49f); + unsigned iL = roundf(fC-0.49f); + unsigned iR = roundf(fC+0.49f); float dL = (fC - iL)*(fC - iL); float dR = (iR - fC)*(iR - fC); uint32_t cIL = getPixelColor(iL | (vStrip<<16)); @@ -806,11 +890,12 @@ void Segment::setPixelColor(float i, uint32_t col, bool aa) setPixelColor(iL | (vStrip<<16), col); } } else { - setPixelColor(uint16_t(roundf(fC)) | (vStrip<<16), col); + setPixelColor(int(roundf(fC)) | (vStrip<<16), col); } } +#endif -uint32_t IRAM_ATTR Segment::getPixelColor(int i) +uint32_t IRAM_ATTR Segment::getPixelColor(int i) const { if (!isActive()) return 0; // not active #ifndef WLED_DISABLE_2D @@ -820,8 +905,8 @@ uint32_t IRAM_ATTR Segment::getPixelColor(int i) #ifndef WLED_DISABLE_2D if (is2D()) { - uint16_t vH = virtualHeight(); // segment height in logical pixels - uint16_t vW = virtualWidth(); + int vH = virtualHeight(); // segment height in logical pixels + int vW = virtualWidth(); switch (map1D2D) { case M12_Pixels: return getPixelColorXY(i % vW, i / vW); @@ -831,11 +916,44 @@ uint32_t IRAM_ATTR Segment::getPixelColor(int i) else return getPixelColorXY(0, vH - i -1); break; case M12_pArc: + if (i >= vW && i >= vH) { + unsigned vI = sqrt16(i*i/2); + return getPixelColorXY(vI,vI); // use diagonal + } case M12_pCorner: // use longest dimension return vW>vH ? getPixelColorXY(i, 0) : getPixelColorXY(0, i); break; - } + case M12_sPinwheel: + // not 100% accurate, returns pixel at outer edge + // i = angle --> 0 - 296 (Big), 0 - 192 (Medium), 0 - 72 (Small) + float centerX = roundf((vW-1) / 2.0f); + float centerY = roundf((vH-1) / 2.0f); + float angleRad = getPinwheelAngle(i, vW, vH); // angle in radians + float cosVal = cos_t(angleRad); + float sinVal = sin_t(angleRad); + + int posx = (centerX + 0.5f * cosVal) * Fixed_Scale; // X starting position in fixed point 18 bit + int posy = (centerY + 0.5f * sinVal) * Fixed_Scale; // Y starting position in fixed point 18 bit + int inc_x = cosVal * Fixed_Scale; // X increment per step (fixed point) 10 bit + int inc_y = sinVal * Fixed_Scale; // Y increment per step (fixed point) 10 bit + int32_t maxX = vW * Fixed_Scale; // X edge in fixedpoint + int32_t maxY = vH * Fixed_Scale; // Y edge in fixedpoint + + // trace ray from center until we hit any edge - to avoid rounding problems, we use the same method as in setPixelColor + int x = INT_MIN; + int y = INT_MIN; + while ((posx >= 0) && (posy >= 0) && (posx < maxX) && (posy < maxY)) { + // scale down to integer (compiler will replace division with appropriate bitshift) + x = posx / Fixed_Scale; + y = posy / Fixed_Scale; + // advance to next position + posx += inc_x; + posy += inc_y; + } + return getPixelColorXY(x, y); + break; + } return 0; } #endif @@ -843,9 +961,9 @@ uint32_t IRAM_ATTR Segment::getPixelColor(int i) if (reverse) i = virtualLength() - i - 1; i *= groupLength(); i += start; - /* offset/phase */ + // offset/phase i += offset; - if ((i >= stop) && (stop>0)) i -= length(); // avoids negative pixel index (stop = 0 is a possible value) + if (i >= stop) i -= length(); return strip.getPixelColor(i); } @@ -877,9 +995,9 @@ uint8_t Segment::differs(Segment& b) const { } void Segment::refreshLightCapabilities() { - uint8_t capabilities = 0; - uint16_t segStartIdx = 0xFFFFU; - uint16_t segStopIdx = 0; + unsigned capabilities = 0; + unsigned segStartIdx = 0xFFFFU; + unsigned segStopIdx = 0; if (!isActive()) { _capabilities = 0; @@ -889,7 +1007,7 @@ void Segment::refreshLightCapabilities() { if (start < Segment::maxWidth * Segment::maxHeight) { // we are withing 2D matrix (includes 1D segments) for (int y = startY; y < stopY; y++) for (int x = start; x < stop; x++) { - uint16_t index = strip.getMappedPixelIndex(x + Segment::maxWidth * y); // convert logical address to physical + unsigned index = strip.getMappedPixelIndex(x + Segment::maxWidth * y); // convert logical address to physical if (index < 0xFFFFU) { if (segStartIdx > index) segStartIdx = index; if (segStopIdx < index) segStopIdx = index; @@ -910,11 +1028,11 @@ void Segment::refreshLightCapabilities() { if (bus->getStart() + bus->getLength() <= segStartIdx) continue; //uint8_t type = bus->getType(); - if (bus->hasRGB() || (cctFromRgb && bus->hasCCT())) capabilities |= SEG_CAPABILITY_RGB; - if (!cctFromRgb && bus->hasCCT()) capabilities |= SEG_CAPABILITY_CCT; - if (correctWB && (bus->hasRGB() || bus->hasCCT())) capabilities |= SEG_CAPABILITY_CCT; //white balance correction (CCT slider) + if (bus->hasRGB() || (strip.cctFromRgb && bus->hasCCT())) capabilities |= SEG_CAPABILITY_RGB; + if (!strip.cctFromRgb && bus->hasCCT()) capabilities |= SEG_CAPABILITY_CCT; + if (strip.correctWB && (bus->hasRGB() || bus->hasCCT())) capabilities |= SEG_CAPABILITY_CCT; //white balance correction (CCT slider) if (bus->hasWhite()) { - uint8_t aWM = Bus::getGlobalAWMode() == AW_GLOBAL_DISABLED ? bus->getAutoWhiteMode() : Bus::getGlobalAWMode(); + unsigned aWM = Bus::getGlobalAWMode() == AW_GLOBAL_DISABLED ? bus->getAutoWhiteMode() : Bus::getGlobalAWMode(); bool whiteSlider = (aWM == RGBW_MODE_DUAL || aWM == RGBW_MODE_MANUAL_ONLY); // white slider allowed // if auto white calculation from RGB is active (Accurate/Brighter), force RGB controls even if there are no RGB busses if (!whiteSlider) capabilities |= SEG_CAPABILITY_RGB; @@ -930,8 +1048,8 @@ void Segment::refreshLightCapabilities() { */ void Segment::fill(uint32_t c) { if (!isActive()) return; // not active - const uint16_t cols = is2D() ? virtualWidth() : virtualLength(); - const uint16_t rows = virtualHeight(); // will be 1 for 1D + const int cols = is2D() ? virtualWidth() : virtualLength(); + const int rows = virtualHeight(); // will be 1 for 1D for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) { if (is2D()) setPixelColorXY(x, y, c); else setPixelColor(x, c); @@ -943,11 +1061,11 @@ void Segment::fill(uint32_t c) { */ void Segment::fade_out(uint8_t rate) { if (!isActive()) return; // not active - const uint16_t cols = is2D() ? virtualWidth() : virtualLength(); - const uint16_t rows = virtualHeight(); // will be 1 for 1D + const int cols = is2D() ? virtualWidth() : virtualLength(); + const int rows = virtualHeight(); // will be 1 for 1D rate = (255-rate) >> 1; - float mappedRate = float(rate) +1.1f; + float mappedRate = 1.0f / (float(rate) + 1.1f); uint32_t color = colors[1]; // SEGCOLOR(1); // target color int w2 = W(color); @@ -957,15 +1075,16 @@ void Segment::fade_out(uint8_t rate) { for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) { color = is2D() ? getPixelColorXY(x, y) : getPixelColor(x); + if (color == colors[1]) continue; // already at target color int w1 = W(color); int r1 = R(color); int g1 = G(color); int b1 = B(color); - int wdelta = (w2 - w1) / mappedRate; - int rdelta = (r2 - r1) / mappedRate; - int gdelta = (g2 - g1) / mappedRate; - int bdelta = (b2 - b1) / mappedRate; + int wdelta = (w2 - w1) * mappedRate; + int rdelta = (r2 - r1) * mappedRate; + int gdelta = (g2 - g1) * mappedRate; + int bdelta = (b2 - b1) * mappedRate; // if fade isn't complete, make sure delta is at least 1 (fixes rounding issues) wdelta += (w2 == w1) ? 0 : (w2 > w1) ? 1 : -1; @@ -981,8 +1100,8 @@ void Segment::fade_out(uint8_t rate) { // fades all pixels to black using nscale8() void Segment::fadeToBlackBy(uint8_t fadeBy) { if (!isActive() || fadeBy == 0) return; // optimization - no scaling to apply - const uint16_t cols = is2D() ? virtualWidth() : virtualLength(); - const uint16_t rows = virtualHeight(); // will be 1 for 1D + const int cols = is2D() ? virtualWidth() : virtualLength(); + const int rows = virtualHeight(); // will be 1 for 1D for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) { if (is2D()) setPixelColorXY(x, y, color_fade(getPixelColorXY(x,y), 255-fadeBy)); @@ -993,33 +1112,39 @@ void Segment::fadeToBlackBy(uint8_t fadeBy) { /* * blurs segment content, source: FastLED colorutils.cpp */ -void Segment::blur(uint8_t blur_amount) { +void Segment::blur(uint8_t blur_amount, bool smear) { if (!isActive() || blur_amount == 0) return; // optimization: 0 means "don't blur" #ifndef WLED_DISABLE_2D if (is2D()) { // compatibility with 2D - const unsigned cols = virtualWidth(); - const unsigned rows = virtualHeight(); - for (unsigned i = 0; i < rows; i++) blurRow(i, blur_amount); // blur all rows - for (unsigned k = 0; k < cols; k++) blurCol(k, blur_amount); // blur all columns + blur2D(blur_amount, smear); + //box_blur(map(blur_amount,1,255,1,3), smear); return; } #endif - uint8_t keep = 255 - blur_amount; - uint8_t seep = blur_amount >> 1; - uint32_t carryover = BLACK; + uint8_t keep = smear ? 255 : 255 - blur_amount; + uint8_t seep = blur_amount >> (1 + smear); unsigned vlength = virtualLength(); + uint32_t carryover = BLACK; + uint32_t lastnew; + uint32_t last; + uint32_t curnew = BLACK; for (unsigned i = 0; i < vlength; i++) { uint32_t cur = getPixelColor(i); uint32_t part = color_fade(cur, seep); - cur = color_add(color_fade(cur, keep), carryover, true); + curnew = color_fade(cur, keep); if (i > 0) { - uint32_t c = getPixelColor(i-1); - setPixelColor(i-1, color_add(c, part, true)); - } - setPixelColor(i, cur); + if (carryover) curnew = color_add(curnew, carryover, true); + uint32_t prev = color_add(lastnew, part, true); + // optimization: only set pixel if color has changed + if (last != prev) setPixelColor(i - 1, prev); + } else // first pixel + setPixelColor(i, curnew); + lastnew = curnew; + last = cur; // save original value for comparison on next iteration carryover = part; } + setPixelColor(vlength - 1, curnew); } /* @@ -1027,7 +1152,7 @@ void Segment::blur(uint8_t blur_amount) { * The colours are a transition r -> g -> b -> back to r * Inspired by the Adafruit examples. */ -uint32_t Segment::color_wheel(uint8_t pos) { +uint32_t Segment::color_wheel(uint8_t pos) const { if (palette) return color_from_palette(pos, false, true, 0); // perhaps "strip.paletteBlend < 2" should be better instead of "true" uint8_t w = W(currentColor(0)); pos = 255 - pos; @@ -1051,19 +1176,17 @@ uint32_t Segment::color_wheel(uint8_t pos) { * @param pbri Value to scale the brightness of the returned color by. Default is 255. (no scaling) * @returns Single color from palette */ -uint32_t Segment::color_from_palette(uint16_t i, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri) { +uint32_t Segment::color_from_palette(uint16_t i, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri) const { uint32_t color = gamma32(currentColor(mcol)); // default palette or no RGB support on segment if ((palette == 0 && mcol < NUM_COLORS) || !_isRGB) return (pbri == 255) ? color : color_fade(color, pbri, true); - uint8_t paletteIndex = i; + unsigned paletteIndex = i; if (mapping && virtualLength() > 1) paletteIndex = (i*255)/(virtualLength() -1); // paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) if (!wrap && strip.paletteBlend != 3) paletteIndex = scale8(paletteIndex, 240); //cut off blend at palette "end" - CRGBPalette16 curPal; - curPal = currentPalette(curPal, palette); - CRGB fastled_col = ColorFromPalette(curPal, paletteIndex, pbri, (strip.paletteBlend == 3)? NOBLEND:LINEARBLEND); // NOTE: paletteBlend should be global + CRGB fastled_col = ColorFromPalette(_currentPalette, paletteIndex, pbri, (strip.paletteBlend == 3)? NOBLEND:LINEARBLEND); // NOTE: paletteBlend should be global return RGBW32(fastled_col.r, fastled_col.g, fastled_col.b, W(color)); } @@ -1084,6 +1207,7 @@ void WS2812FX::finalizeInit(void) { // for the lack of better place enumerate ledmaps here // if we do it in json.cpp (serializeInfo()) we are getting flashes on LEDs // unfortunately this means we do not get updates after uploads + // the other option is saving UI settings which will cause enumeration enumerateLedmaps(); _hasWhiteChannel = _isOffRefreshRequired = false; @@ -1091,17 +1215,28 @@ void WS2812FX::finalizeInit(void) { //if busses failed to load, add default (fresh install, FS issue, ...) if (BusManager::getNumBusses() == 0) { DEBUG_PRINTLN(F("No busses, init default")); - const uint8_t defDataPins[] = {DATA_PINS}; - const uint16_t defCounts[] = {PIXEL_COUNTS}; - const uint8_t defNumBusses = ((sizeof defDataPins) / (sizeof defDataPins[0])); - const uint8_t defNumCounts = ((sizeof defCounts) / (sizeof defCounts[0])); - uint16_t prevLen = 0; - for (int i = 0; i < defNumBusses && i < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; i++) { - uint8_t defPin[] = {defDataPins[i]}; - uint16_t start = prevLen; - uint16_t count = defCounts[(i < defNumCounts) ? i : defNumCounts -1]; + const unsigned defDataPins[] = {DATA_PINS}; + const unsigned defCounts[] = {PIXEL_COUNTS}; + const unsigned defNumPins = ((sizeof defDataPins) / (sizeof defDataPins[0])); + const unsigned defNumCounts = ((sizeof defCounts) / (sizeof defCounts[0])); + // if number of pins is divisible by counts, use number of counts to determine number of buses, otherwise use pins + const unsigned defNumBusses = defNumPins > defNumCounts && defNumPins%defNumCounts == 0 ? defNumCounts : defNumPins; + const unsigned pinsPerBus = defNumPins / defNumBusses; + unsigned prevLen = 0; + for (unsigned i = 0; i < defNumBusses && i < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; i++) { + uint8_t defPin[5]; // max 5 pins + for (unsigned j = 0; j < pinsPerBus; j++) defPin[j] = defDataPins[i*pinsPerBus + j]; + // when booting without config (1st boot) we need to make sure GPIOs defined for LED output don't clash with hardware + // i.e. DEBUG (GPIO1), DMX (2), SPI RAM/FLASH (16&17 on ESP32-WROVER/PICO), etc + if (pinManager.isPinAllocated(defPin[0])) { + defPin[0] = 1; // start with GPIO1 and work upwards + while (pinManager.isPinAllocated(defPin[0]) && defPin[0] < WLED_NUM_PINS) defPin[0]++; + } + unsigned start = prevLen; + // if we have less counts than pins and they do not align, use last known count to set current count + unsigned count = defCounts[(i < defNumCounts) ? i : defNumCounts -1]; prevLen += count; - BusConfig defCfg = BusConfig(DEFAULT_LED_TYPE, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY); + BusConfig defCfg = BusConfig(DEFAULT_LED_TYPE, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY, 0, useGlobalLedBuffer); if (BusManager::add(defCfg) == -1) break; } } @@ -1114,15 +1249,16 @@ void WS2812FX::finalizeInit(void) { //RGBW mode is enabled if at least one of the strips is RGBW _hasWhiteChannel |= bus->hasWhite(); //refresh is required to remain off if at least one of the strips requires the refresh. - _isOffRefreshRequired |= bus->isOffRefreshRequired(); - uint16_t busEnd = bus->getStart() + bus->getLength(); + _isOffRefreshRequired |= bus->isOffRefreshRequired() && !bus->isPWM(); // use refresh bit for phase shift with analog + unsigned busEnd = bus->getStart() + bus->getLength(); if (busEnd > _length) _length = busEnd; #ifdef ESP8266 - if ((!IS_DIGITAL(bus->getType()) || IS_2PIN(bus->getType()))) continue; - uint8_t pins[5]; - if (!bus->getPins(pins)) continue; - BusDigital* bd = static_cast(bus); - if (pins[0] == 3) bd->reinit(); + // why do we need to reinitialise GPIO3??? + //if (!bus->isDigital() || bus->is2Pin()) continue; + //uint8_t pins[5]; + //if (!bus->getPins(pins)) continue; + //BusDigital* bd = static_cast(bus); + //if (pins[0] == 3) bd->reinit(); #endif } @@ -1159,15 +1295,19 @@ void WS2812FX::service() { if (nowUp > seg.next_time || _triggered || (doShow && seg.mode == FX_MODE_STATIC)) { doShow = true; - uint16_t delay = FRAMETIME; + unsigned delay = FRAMETIME; if (!seg.freeze) { //only run effect function if not frozen + int oldCCT = BusManager::getSegmentCCT(); // store original CCT value (actually it is not Segment based) _virtualSegmentLength = seg.virtualLength(); //SEGLEN _colors_t[0] = gamma32(seg.currentColor(0)); _colors_t[1] = gamma32(seg.currentColor(1)); _colors_t[2] = gamma32(seg.currentColor(2)); - seg.currentPalette(_currentPalette, seg.palette); // we need to pass reference - if (!cctFromRgb || correctWB) BusManager::setSegmentCCT(seg.currentBri(true), correctWB); + seg.setCurrentPalette(); // load actual palette + // when correctWB is true we need to correct/adjust RGB value according to desired CCT value, but it will also affect actual WW/CW ratio + // when cctFromRgb is true we implicitly calculate WW and CW from RGB values + if (cctFromRgb) BusManager::setSegmentCCT(-1); + else BusManager::setSegmentCCT(seg.currentBri(true), correctWB); // Effect blending // When two effects are being blended, each may have different segment data, this // data needs to be saved first and then restored before running previous mode. @@ -1182,7 +1322,7 @@ void WS2812FX::service() { Segment::modeBlend(true); // set semaphore seg.swapSegenv(_tmpSegData); // temporarily store new mode state (and swap it with transitional state) _virtualSegmentLength = seg.virtualLength(); // update SEGLEN (mapping may have changed) - uint16_t d2 = (*_mode[tmpMode])(); // run old mode + unsigned d2 = (*_mode[tmpMode])(); // run old mode seg.restoreSegenv(_tmpSegData); // restore mode state (will also update transitional state) delay = MIN(delay,d2); // use shortest delay Segment::modeBlend(false); // unset semaphore @@ -1190,28 +1330,27 @@ void WS2812FX::service() { #endif seg.call++; if (seg.isInTransition() && delay > FRAMETIME) delay = FRAMETIME; // force faster updates during transition + BusManager::setSegmentCCT(oldCCT); // restore old CCT for ABL adjustments } seg.next_time = nowUp + delay; } -// if (_segment_index == _queuedChangesSegId) setUpSegmentFromQueuedChanges(); _segment_index++; } _virtualSegmentLength = 0; - BusManager::setSegmentCCT(-1); _isServicing = false; _triggered = false; #ifdef WLED_DEBUG - if (millis() - nowUp > _frametime) DEBUG_PRINTLN(F("Slow effects.")); + if (millis() - nowUp > _frametime) DEBUG_PRINTF_P(PSTR("Slow effects %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime); #endif if (doShow) { yield(); - Segment::handleRandomPalette(); // slowly transtion random palette; move it into for loop when each segment has individual random palette + Segment::handleRandomPalette(); // slowly transition random palette; move it into for loop when each segment has individual random palette show(); } #ifdef WLED_DEBUG - if (millis() - nowUp > _frametime) DEBUG_PRINTLN(F("Slow strip.")); + if (millis() - nowUp > _frametime) DEBUG_PRINTF_P(PSTR("Slow strip %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime); #endif } @@ -1221,7 +1360,7 @@ void IRAM_ATTR WS2812FX::setPixelColor(unsigned i, uint32_t col) { BusManager::setPixelColor(i, col); } -uint32_t IRAM_ATTR WS2812FX::getPixelColor(uint16_t i) { +uint32_t IRAM_ATTR WS2812FX::getPixelColor(uint16_t i) const { i = getMappedPixelIndex(i); if (i >= _length) return 0; return BusManager::getPixelColor(i); @@ -1249,7 +1388,7 @@ void WS2812FX::show(void) { * Returns a true value if any of the strips are still being updated. * On some hardware (ESP32), strip updates are done asynchronously. */ -bool WS2812FX::isUpdating() { +bool WS2812FX::isUpdating() const { return !BusManager::canAllShow(); } @@ -1257,7 +1396,7 @@ bool WS2812FX::isUpdating() { * Returns the refresh rate of the LED strip. Useful for finding out whether a given setup is fast enough. * Only updates on show() or is set to 0 fps if last show is more than 2 secs ago, so accuracy varies */ -uint16_t WS2812FX::getFps() { +uint16_t WS2812FX::getFps() const { if (millis() - _lastShow > 2000) return 0; return _cumulativeFps +1; } @@ -1316,17 +1455,17 @@ void WS2812FX::setBrightness(uint8_t b, bool direct) { } } -uint8_t WS2812FX::getActiveSegsLightCapabilities(bool selectedOnly) { +uint8_t WS2812FX::getActiveSegsLightCapabilities(bool selectedOnly) const { uint8_t totalLC = 0; - for (segment &seg : _segments) { + for (const segment &seg : _segments) { if (seg.isActive() && (!selectedOnly || seg.isSelected())) totalLC |= seg.getLightCapabilities(); } return totalLC; } -uint8_t WS2812FX::getFirstSelectedSegId(void) { +uint8_t WS2812FX::getFirstSelectedSegId(void) const { size_t i = 0; - for (segment &seg : _segments) { + for (const segment &seg : _segments) { if (seg.isActive() && seg.isSelected()) return i; i++; } @@ -1342,14 +1481,14 @@ void WS2812FX::setMainSegmentId(uint8_t n) { return; } -uint8_t WS2812FX::getLastActiveSegmentId(void) { +uint8_t WS2812FX::getLastActiveSegmentId(void) const { for (size_t i = _segments.size() -1; i > 0; i--) { if (_segments[i].isActive()) return i; } return 0; } -uint8_t WS2812FX::getActiveSegmentsNum(void) { +uint8_t WS2812FX::getActiveSegmentsNum(void) const { uint8_t c = 0; for (size_t i = 0; i < _segments.size(); i++) { if (_segments[i].isActive()) c++; @@ -1357,14 +1496,14 @@ uint8_t WS2812FX::getActiveSegmentsNum(void) { return c; } -uint16_t WS2812FX::getLengthTotal(void) { - uint16_t len = Segment::maxWidth * Segment::maxHeight; // will be _length for 1D (see finalizeInit()) but should cover whole matrix for 2D +uint16_t WS2812FX::getLengthTotal(void) const { + unsigned len = Segment::maxWidth * Segment::maxHeight; // will be _length for 1D (see finalizeInit()) but should cover whole matrix for 2D if (isMatrix && _length > len) len = _length; // for 2D with trailing strip return len; } -uint16_t WS2812FX::getLengthPhysical(void) { - uint16_t len = 0; +uint16_t WS2812FX::getLengthPhysical(void) const { + unsigned len = 0; for (size_t b = 0; b < BusManager::getNumBusses(); b++) { Bus *bus = BusManager::getBus(b); if (bus->getType() >= TYPE_NET_DDP_RGB) continue; //exclude non-physical network busses @@ -1376,7 +1515,7 @@ uint16_t WS2812FX::getLengthPhysical(void) { //used for JSON API info.leds.rgbw. Little practical use, deprecate with info.leds.rgbw. //returns if there is an RGBW bus (supports RGB and White, not only white) //not influenced by auto-white mode, also true if white slider does not affect output white channel -bool WS2812FX::hasRGBWBus(void) { +bool WS2812FX::hasRGBWBus(void) const { for (size_t b = 0; b < BusManager::getNumBusses(); b++) { Bus *bus = BusManager::getBus(b); if (bus == nullptr || bus->getLength()==0) break; @@ -1385,16 +1524,12 @@ bool WS2812FX::hasRGBWBus(void) { return false; } -bool WS2812FX::hasCCTBus(void) { +bool WS2812FX::hasCCTBus(void) const { if (cctFromRgb && !correctWB) return false; for (size_t b = 0; b < BusManager::getNumBusses(); b++) { Bus *bus = BusManager::getBus(b); if (bus == nullptr || bus->getLength()==0) break; - switch (bus->getType()) { - case TYPE_ANALOG_5CH: - case TYPE_ANALOG_2CH: - return true; - } + if (bus->hasCCT()) return true; } return false; } @@ -1425,31 +1560,12 @@ void WS2812FX::setSegment(uint8_t segId, uint16_t i1, uint16_t i2, uint8_t group appendSegment(Segment(0, strip.getLengthTotal())); segId = getSegmentsNum()-1; // segments are added at the end of list } -/* - if (_queuedChangesSegId == segId) _queuedChangesSegId = 255; // cancel queued change if already queued for this segment - - if (segId < getMaxSegments() && segId == getCurrSegmentId() && isServicing()) { // queue change to prevent concurrent access - // queuing a change for a second segment will lead to the loss of the first change if not yet applied - // however this is not a problem as the queued change is applied immediately after the effect function in that segment returns - _qStart = i1; _qStop = i2; _qStartY = startY; _qStopY = stopY; - _qGrouping = grouping; _qSpacing = spacing; _qOffset = offset; - _queuedChangesSegId = segId; - DEBUG_PRINT(F("Segment queued: ")); DEBUG_PRINTLN(segId); - return; // queued changes are applied immediately after effect function returns - } -*/ suspend(); _segments[segId].setUp(i1, i2, grouping, spacing, offset, startY, stopY); resume(); if (segId > 0 && segId == getSegmentsNum()-1 && i2 <= i1) _segments.pop_back(); // if last segment was deleted remove it from vector } -/* -void WS2812FX::setUpSegmentFromQueuedChanges() { - if (_queuedChangesSegId >= getSegmentsNum()) return; - _segments[_queuedChangesSegId].setUp(_qStart, _qStop, _qGrouping, _qSpacing, _qOffset, _qStartY, _qStopY); - _queuedChangesSegId = 255; -} -*/ + void WS2812FX::resetSegments() { _segments.clear(); // destructs all Segment as part of clearing #ifndef WLED_DISABLE_2D @@ -1464,8 +1580,8 @@ void WS2812FX::resetSegments() { void WS2812FX::makeAutoSegments(bool forceReset) { if (autoSegments) { //make one segment per bus - uint16_t segStarts[MAX_NUM_SEGMENTS] = {0}; - uint16_t segStops [MAX_NUM_SEGMENTS] = {0}; + unsigned segStarts[MAX_NUM_SEGMENTS] = {0}; + unsigned segStops [MAX_NUM_SEGMENTS] = {0}; size_t s = 0; #ifndef WLED_DISABLE_2D @@ -1484,7 +1600,7 @@ void WS2812FX::makeAutoSegments(bool forceReset) { segStops[s] = segStarts[s] + b->getLength(); #ifndef WLED_DISABLE_2D - if (isMatrix && segStops[s] < Segment::maxWidth*Segment::maxHeight) continue; // ignore buses comprising matrix + if (isMatrix && segStops[s] <= Segment::maxWidth*Segment::maxHeight) continue; // ignore buses comprising matrix if (isMatrix && segStarts[s] < Segment::maxWidth*Segment::maxHeight) segStarts[s] = Segment::maxWidth*Segment::maxHeight; #endif @@ -1512,6 +1628,7 @@ void WS2812FX::makeAutoSegments(bool forceReset) { for (size_t i = 1; i < s; i++) { _segments.push_back(Segment(segStarts[i], segStops[i])); } + DEBUG_PRINTF_P(PSTR("%d auto segments created.\n"), _segments.size()); } else { @@ -1557,6 +1674,8 @@ void WS2812FX::fixInvalidSegments() { if (_segments[i].stop > _length) _segments[i].stop = _length; } } + // if any segments were deleted free memory + purgeSegments(); // this is always called as the last step after finalizeInit(), update covered bus types for (segment &seg : _segments) seg.refreshLightCapabilities(); @@ -1587,12 +1706,11 @@ void WS2812FX::setRange(uint16_t i, uint16_t i2, uint32_t col) { void WS2812FX::printSize() { size_t size = 0; for (const Segment &seg : _segments) size += seg.getSize(); - DEBUG_PRINTF_P(PSTR("Segments: %d -> %uB\n"), _segments.size(), size); + DEBUG_PRINTF_P(PSTR("Segments: %d -> %u/%dB\n"), _segments.size(), size, Segment::getUsedSegmentData()); + for (const Segment &seg : _segments) DEBUG_PRINTF_P(PSTR(" Seg: %d,%d [A=%d, 2D=%d, RGB=%d, W=%d, CCT=%d]\n"), seg.width(), seg.height(), seg.isActive(), seg.is2D(), seg.hasRGB(), seg.hasWhite(), seg.isCCT()); DEBUG_PRINTF_P(PSTR("Modes: %d*%d=%uB\n"), sizeof(mode_ptr), _mode.size(), (_mode.capacity()*sizeof(mode_ptr))); DEBUG_PRINTF_P(PSTR("Data: %d*%d=%uB\n"), sizeof(const char *), _modeData.size(), (_modeData.capacity()*sizeof(const char *))); DEBUG_PRINTF_P(PSTR("Map: %d*%d=%uB\n"), sizeof(uint16_t), (int)customMappingSize, customMappingSize*sizeof(uint16_t)); - size = getLengthTotal(); - if (useGlobalLedBuffer) DEBUG_PRINTF_P(PSTR("Buffer: %d*%u=%uB\n"), sizeof(CRGB), size, size*sizeof(CRGB)); } #endif @@ -1656,13 +1774,15 @@ bool WS2812FX::deserializeMap(uint8_t n) { bool isFile = WLED_FS.exists(fileName); customMappingSize = 0; // prevent use of mapping if anything goes wrong + currentLedmap = 0; + if (n == 0 || isFile) interfaceUpdateCallMode = CALL_MODE_WS_SEND; // schedule WS update (to inform UI) if (!isFile && n==0 && isMatrix) { setUpMatrix(); return false; } - if (!isFile || !requestJSONBufferLock(7)) return false; // this will trigger setUpMatrix() when called from wled.cpp + if (!isFile || !requestJSONBufferLock(7)) return false; if (!readObjectFromFile(fileName, nullptr, pDoc)) { DEBUG_PRINT(F("ERROR Invalid ledmap in ")); DEBUG_PRINTLN(fileName); @@ -1670,22 +1790,33 @@ bool WS2812FX::deserializeMap(uint8_t n) { return false; // if file does not load properly then exit } - DEBUG_PRINT(F("Reading LED map from ")); DEBUG_PRINTLN(fileName); - - if (customMappingTable == nullptr) customMappingTable = new uint16_t[getLengthTotal()]; - JsonObject root = pDoc->as(); - JsonArray map = root[F("map")]; - if (!map.isNull() && map.size()) { // not an empty map - customMappingSize = min((unsigned)map.size(), (unsigned)getLengthTotal()); - for (unsigned i=0; i(), 1), 128); + Segment::maxHeight = min(max(root[F("height")].as(), 1), 128); + } + + if (customMappingTable) delete[] customMappingTable; + customMappingTable = new uint16_t[getLengthTotal()]; + + if (customMappingTable) { + DEBUG_PRINT(F("Reading LED map from ")); DEBUG_PRINTLN(fileName); + JsonArray map = root[F("map")]; + if (!map.isNull() && map.size()) { // not an empty map + customMappingSize = min((unsigned)map.size(), (unsigned)getLengthTotal()); + for (unsigned i=0; i 0); } -uint16_t IRAM_ATTR WS2812FX::getMappedPixelIndex(uint16_t index) { +uint16_t IRAM_ATTR WS2812FX::getMappedPixelIndex(uint16_t index) const { // convert logical address to physical if (index < customMappingSize && (realtimeMode == REALTIME_MODE_INACTIVE || realtimeRespectLedMaps)) index = customMappingTable[index]; diff --git a/wled00/NodeStruct.h b/wled00/NodeStruct.h index 2d4d3609..34f73ab4 100644 --- a/wled00/NodeStruct.h +++ b/wled00/NodeStruct.h @@ -34,7 +34,7 @@ struct NodeStruct NodeStruct() : age(0), nodeType(0), build(0) { - for (uint8_t i = 0; i < 4; ++i) { ip[i] = 0; } + for (unsigned i = 0; i < 4; ++i) { ip[i] = 0; } } }; typedef std::map NodesMap; diff --git a/wled00/alexa.cpp b/wled00/alexa.cpp index 179a522c..b108f294 100644 --- a/wled00/alexa.cpp +++ b/wled00/alexa.cpp @@ -25,7 +25,7 @@ void alexaInit() // names are identical as the preset names, switching off can be done by switching off any of them if (alexaNumPresets) { String name = ""; - for (byte presetIndex = 1; presetIndex <= alexaNumPresets; presetIndex++) + for (unsigned presetIndex = 1; presetIndex <= alexaNumPresets; presetIndex++) { if (!getPresetName(presetIndex, name)) break; // no more presets EspalexaDevice* dev = new EspalexaDevice(name.c_str(), onAlexaChange, EspalexaDeviceType::extendedcolor); @@ -64,7 +64,7 @@ void onAlexaChange(EspalexaDevice* dev) } else // switch-on behavior for preset devices { // turn off other preset devices - for (byte i = 1; i < espalexa.getDeviceCount(); i++) + for (unsigned i = 1; i < espalexa.getDeviceCount(); i++) { if (i == dev->getId()) continue; espalexa.getDevice(i)->setValue(0); // turn off other presets @@ -87,7 +87,7 @@ void onAlexaChange(EspalexaDevice* dev) applyPreset(macroAlexaOff, CALL_MODE_ALEXA); // below for loop stops Alexa from complaining if macroAlexaOff does not actually turn off } - for (byte i = 0; i < espalexa.getDeviceCount(); i++) + for (unsigned i = 0; i < espalexa.getDeviceCount(); i++) { espalexa.getDevice(i)->setValue(0); } diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp old mode 100755 new mode 100644 index ca0c6af9..d2a65b6e --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -4,15 +4,27 @@ #include #include +#ifdef ARDUINO_ARCH_ESP32 +#include "driver/ledc.h" +#include "soc/ledc_struct.h" + #if !(defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3)) + #define LEDC_MUTEX_LOCK() do {} while (xSemaphoreTake(_ledc_sys_lock, portMAX_DELAY) != pdPASS) + #define LEDC_MUTEX_UNLOCK() xSemaphoreGive(_ledc_sys_lock) + extern xSemaphoreHandle _ledc_sys_lock; + #else + #define LEDC_MUTEX_LOCK() + #define LEDC_MUTEX_UNLOCK() + #endif +#endif #include "const.h" #include "pin_manager.h" #include "bus_wrapper.h" #include "bus_manager.h" +extern bool cctICused; + //colors.cpp uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb); -uint16_t approximateKelvinFromRGB(uint32_t rgb); -void colorRGBtoRGBW(byte* rgb); //udp.cpp uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, byte *buffer, uint8_t bri=255, bool isRGBW=false); @@ -48,54 +60,62 @@ uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, byte #define W(c) (byte((c) >> 24)) -void ColorOrderMap::add(uint16_t start, uint16_t len, uint8_t colorOrder) { - if (_count >= WLED_MAX_COLOR_ORDER_MAPPINGS) { - return; - } - if (len == 0) { - return; - } - // upper nibble contains W swap information - if ((colorOrder & 0x0F) > COL_ORDER_MAX) { - return; - } - _mappings[_count].start = start; - _mappings[_count].len = len; - _mappings[_count].colorOrder = colorOrder; - _count++; +bool ColorOrderMap::add(uint16_t start, uint16_t len, uint8_t colorOrder) { + if (count() >= WLED_MAX_COLOR_ORDER_MAPPINGS || len == 0 || (colorOrder & 0x0F) > COL_ORDER_MAX) return false; // upper nibble contains W swap information + _mappings.push_back({start,len,colorOrder}); + return true; } uint8_t IRAM_ATTR ColorOrderMap::getPixelColorOrder(uint16_t pix, uint8_t defaultColorOrder) const { - if (_count > 0) { - // upper nibble contains W swap information - // when ColorOrderMap's upper nibble contains value >0 then swap information is used from it, otherwise global swap is used - for (unsigned i = 0; i < _count; i++) { - if (pix >= _mappings[i].start && pix < (_mappings[i].start + _mappings[i].len)) { - return _mappings[i].colorOrder | ((_mappings[i].colorOrder >> 4) ? 0 : (defaultColorOrder & 0xF0)); - } + // upper nibble contains W swap information + // when ColorOrderMap's upper nibble contains value >0 then swap information is used from it, otherwise global swap is used + for (unsigned i = 0; i < count(); i++) { + if (pix >= _mappings[i].start && pix < (_mappings[i].start + _mappings[i].len)) { + return _mappings[i].colorOrder | ((_mappings[i].colorOrder >> 4) ? 0 : (defaultColorOrder & 0xF0)); } } return defaultColorOrder; } -uint32_t Bus::autoWhiteCalc(uint32_t c) { - uint8_t aWM = _autoWhiteMode; +void Bus::calculateCCT(uint32_t c, uint8_t &ww, uint8_t &cw) { + unsigned cct = 0; //0 - full warm white, 255 - full cold white + unsigned w = W(c); + + if (_cct > -1) { // using RGB? + if (_cct >= 1900) cct = (_cct - 1900) >> 5; // convert K in relative format + else if (_cct < 256) cct = _cct; // already relative + } else { + cct = (approximateKelvinFromRGB(c) - 1900) >> 5; // convert K (from RGB value) to relative format + } + + //0 - linear (CCT 127 = 50% warm, 50% cold), 127 - additive CCT blending (CCT 127 = 100% warm, 100% cold) + if (cct < _cctBlend) ww = 255; + else ww = ((255-cct) * 255) / (255 - _cctBlend); + if ((255-cct) < _cctBlend) cw = 255; + else cw = (cct * 255) / (255 - _cctBlend); + + ww = (w * ww) / 255; //brightness scaling + cw = (w * cw) / 255; +} + +uint32_t Bus::autoWhiteCalc(uint32_t c) const { + unsigned aWM = _autoWhiteMode; if (_gAWM < AW_GLOBAL_DISABLED) aWM = _gAWM; if (aWM == RGBW_MODE_MANUAL_ONLY) return c; - uint8_t w = W(c); + unsigned w = W(c); //ignore auto-white calculation if w>0 and mode DUAL (DUAL behaves as BRIGHTER if w==0) if (w > 0 && aWM == RGBW_MODE_DUAL) return c; - uint8_t r = R(c); - uint8_t g = G(c); - uint8_t b = B(c); + unsigned r = R(c); + unsigned g = G(c); + unsigned b = B(c); if (aWM == RGBW_MODE_MAX) return RGBW32(r, g, b, r > g ? (r > b ? r : b) : (g > b ? g : b)); // brightest RGB channel w = r < g ? (r < b ? r : b) : (g < b ? g : b); if (aWM == RGBW_MODE_AUTO_ACCURATE) { r -= w; g -= w; b -= w; } //subtract w in ACCURATE mode return RGBW32(r, g, b, w); } -uint8_t *Bus::allocData(size_t size) { +uint8_t *Bus::allocateData(size_t size) { if (_data) free(_data); // should not happen, but for safety return _data = (uint8_t *)(size>0 ? calloc(size, sizeof(uint8_t)) : nullptr); } @@ -109,11 +129,11 @@ BusDigital::BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com) , _milliAmpsMax(bc.milliAmpsMax) , _colorOrderMap(com) { - if (!IS_DIGITAL(bc.type) || !bc.count) return; + if (!isDigital(bc.type) || !bc.count) return; if (!pinManager.allocatePin(bc.pins[0], true, PinOwner::BusDigital)) return; _frequencykHz = 0U; _pins[0] = bc.pins[0]; - if (IS_2PIN(bc.type)) { + if (is2Pin(bc.type)) { if (!pinManager.allocatePin(bc.pins[1], true, PinOwner::BusDigital)) { cleanup(); return; @@ -123,13 +143,16 @@ BusDigital::BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com) } _iType = PolyBus::getI(bc.type, _pins, nr); if (_iType == I_NONE) return; - if (bc.doubleBuffer && !allocData(bc.count * (Bus::hasWhite(_type) + 3*Bus::hasRGB(_type)))) return; //warning: hardcoded channel count + _hasRgb = hasRGB(bc.type); + _hasWhite = hasWhite(bc.type); + _hasCCT = hasCCT(bc.type); + if (bc.doubleBuffer && !allocateData(bc.count * Bus::getNumberOfChannels(bc.type))) return; //_buffering = bc.doubleBuffer; uint16_t lenToCreate = bc.count; if (bc.type == TYPE_WS2812_1CH_X3) lenToCreate = NUM_ICS_WS2812_1CH_3X(bc.count); // only needs a third of "RGB" LEDs for NeoPixelBus _busPtr = PolyBus::create(_iType, _pins, lenToCreate + _skip, nr, _frequencykHz); _valid = (_busPtr != nullptr); - DEBUG_PRINTF("%successfully inited strip %u (len %u) with type %u and pins %u,%u (itype %u). mA=%d/%d\n", _valid?"S":"Uns", nr, bc.count, bc.type, _pins[0], _pins[1], _iType, _milliAmpsPerLed, _milliAmpsMax); + DEBUG_PRINTF_P(PSTR("%successfully inited strip %u (len %u) with type %u and pins %u,%u (itype %u). mA=%d/%d\n"), _valid?"S":"Uns", nr, bc.count, bc.type, _pins[0], is2Pin(bc.type)?_pins[1]:255, _iType, _milliAmpsPerLed, _milliAmpsMax); } //fine tune power estimation constants for your setup @@ -150,7 +173,7 @@ BusDigital::BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com) //I am NOT to be held liable for burned down garages or houses! // To disable brightness limiter we either set output max current to 0 or single LED current to 0 -uint8_t BusDigital::estimateCurrentAndLimitBri() { +uint8_t BusDigital::estimateCurrentAndLimitBri(void) { bool useWackyWS2815PowerModel = false; byte actualMilliampsPerLed = _milliAmpsPerLed; @@ -202,18 +225,20 @@ uint8_t BusDigital::estimateCurrentAndLimitBri() { return newBri; } -void BusDigital::show() { +void BusDigital::show(void) { _milliAmpsTotal = 0; if (!_valid) return; - uint8_t newBri = estimateCurrentAndLimitBri(); // will fill _milliAmpsTotal + uint8_t cctWW = 0, cctCW = 0; + unsigned newBri = estimateCurrentAndLimitBri(); // will fill _milliAmpsTotal if (newBri < _bri) PolyBus::setBrightness(_busPtr, _iType, newBri); // limit brightness to stay within current limits - if (_data) { // use _buffering this causes ~20% FPS drop - size_t channels = Bus::hasWhite(_type) + 3*Bus::hasRGB(_type); + if (_data) { + size_t channels = getNumberOfChannels(); + int16_t oldCCT = Bus::_cct; // temporarily save bus CCT for (size_t i=0; i<_len; i++) { - size_t offset = i*channels; - uint8_t co = _colorOrderMap.getPixelColorOrder(i+_start, _colorOrder); + size_t offset = i * channels; + unsigned co = _colorOrderMap.getPixelColorOrder(i+_start, _colorOrder); uint32_t c; if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs (_len is always a multiple of 3) switch (i%3) { @@ -222,25 +247,35 @@ void BusDigital::show() { case 2: c = RGBW32(_data[offset-2], _data[offset-1], _data[offset] , 0); break; } } else { - c = RGBW32(_data[offset],_data[offset+1],_data[offset+2],(Bus::hasWhite(_type)?_data[offset+3]:0)); + if (hasRGB()) c = RGBW32(_data[offset], _data[offset+1], _data[offset+2], hasWhite() ? _data[offset+3] : 0); + else c = RGBW32(0, 0, 0, _data[offset]); } - uint16_t pix = i; + if (hasCCT()) { + // unfortunately as a segment may span multiple buses or a bus may contain multiple segments and each segment may have different CCT + // we need to extract and appy CCT value for each pixel individually even though all buses share the same _cct variable + // TODO: there is an issue if CCT is calculated from RGB value (_cct==-1), we cannot do that with double buffer + Bus::_cct = _data[offset+channels-1]; + Bus::calculateCCT(c, cctWW, cctCW); + } + unsigned pix = i; if (_reversed) pix = _len - pix -1; pix += _skip; - PolyBus::setPixelColor(_busPtr, _iType, pix, c, co); + PolyBus::setPixelColor(_busPtr, _iType, pix, c, co, (cctCW<<8) | cctWW); } #if !defined(STATUSLED) || STATUSLED>=0 if (_skip) PolyBus::setPixelColor(_busPtr, _iType, 0, 0, _colorOrderMap.getPixelColorOrder(_start, _colorOrder)); // paint skipped pixels black #endif for (int i=1; i<_skip; i++) PolyBus::setPixelColor(_busPtr, _iType, i, 0, _colorOrderMap.getPixelColorOrder(_start, _colorOrder)); // paint skipped pixels black + Bus::_cct = oldCCT; } else { if (newBri < _bri) { - uint16_t hwLen = _len; + unsigned hwLen = _len; if (_type == TYPE_WS2812_1CH_X3) hwLen = NUM_ICS_WS2812_1CH_3X(_len); // only needs a third of "RGB" LEDs for NeoPixelBus for (unsigned i = 0; i < hwLen; i++) { // use 0 as color order, actual order does not matter here as we just update the channel values as-is uint32_t c = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, i, 0), _bri); - PolyBus::setPixelColor(_busPtr, _iType, i, c, 0); // repaint all pixels with new brightness + if (hasCCT()) Bus::calculateCCT(c, cctWW, cctCW); // this will unfortunately corrupt (segment) CCT data on every bus + PolyBus::setPixelColor(_busPtr, _iType, i, c, 0, (cctCW<<8) | cctWW); // repaint all pixels with new brightness } } } @@ -251,19 +286,13 @@ void BusDigital::show() { if (newBri < _bri) PolyBus::setBrightness(_busPtr, _iType, _bri); } -bool BusDigital::canShow() { +bool BusDigital::canShow(void) const { if (!_valid) return true; return PolyBus::canShow(_busPtr, _iType); } void BusDigital::setBrightness(uint8_t b) { if (_bri == b) return; - //Fix for turning off onboard LED breaking bus - #ifdef LED_BUILTIN - if (_bri == 0) { // && b > 0, covered by guard if above - if (_pins[0] == LED_BUILTIN || _pins[1] == LED_BUILTIN) reinit(); - } - #endif Bus::setBrightness(b); PolyBus::setBrightness(_busPtr, _iType, b); } @@ -279,23 +308,26 @@ void BusDigital::setStatusPixel(uint32_t c) { void IRAM_ATTR BusDigital::setPixelColor(uint16_t pix, uint32_t c) { if (!_valid) return; - if (Bus::hasWhite(_type)) c = autoWhiteCalc(c); - if (_cct >= 1900) c = colorBalanceFromKelvin(_cct, c); //color correction from CCT - if (_data) { // use _buffering this causes ~20% FPS drop - size_t channels = Bus::hasWhite(_type) + 3*Bus::hasRGB(_type); - size_t offset = pix*channels; - if (Bus::hasRGB(_type)) { + uint8_t cctWW = 0, cctCW = 0; + if (hasWhite()) c = autoWhiteCalc(c); + if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT + if (_data) { + size_t offset = pix * getNumberOfChannels(); + if (hasRGB()) { _data[offset++] = R(c); _data[offset++] = G(c); _data[offset++] = B(c); } - if (Bus::hasWhite(_type)) _data[offset] = W(c); + if (hasWhite()) _data[offset++] = W(c); + // unfortunately as a segment may span multiple buses or a bus may contain multiple segments and each segment may have different CCT + // we need to store CCT value for each pixel (if there is a color correction in play, convert K in CCT ratio) + if (hasCCT()) _data[offset] = Bus::_cct >= 1900 ? (Bus::_cct - 1900) >> 5 : (Bus::_cct < 0 ? 127 : Bus::_cct); // TODO: if _cct == -1 we simply ignore it } else { if (_reversed) pix = _len - pix -1; pix += _skip; - uint8_t co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); + unsigned co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs - uint16_t pOld = pix; + unsigned pOld = pix; pix = IC_INDEX_WS2812_1CH_3X(pix); uint32_t cOld = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, pix, co),_bri); switch (pOld % 3) { // change only the single channel (TODO: this can cause loss because of get/set) @@ -304,32 +336,32 @@ void IRAM_ATTR BusDigital::setPixelColor(uint16_t pix, uint32_t c) { case 2: c = RGBW32(R(cOld), G(cOld), W(c) , 0); break; } } - PolyBus::setPixelColor(_busPtr, _iType, pix, c, co); + if (hasCCT()) Bus::calculateCCT(c, cctWW, cctCW); + PolyBus::setPixelColor(_busPtr, _iType, pix, c, co, (cctCW<<8) | cctWW); } } // returns original color if global buffering is enabled, else returns lossly restored color from bus -uint32_t IRAM_ATTR BusDigital::getPixelColor(uint16_t pix) { +uint32_t IRAM_ATTR BusDigital::getPixelColor(uint16_t pix) const { if (!_valid) return 0; - if (_data) { // use _buffering this causes ~20% FPS drop - size_t channels = Bus::hasWhite(_type) + 3*Bus::hasRGB(_type); - size_t offset = pix*channels; + if (_data) { + size_t offset = pix * getNumberOfChannels(); uint32_t c; - if (!Bus::hasRGB(_type)) { + if (!hasRGB()) { c = RGBW32(_data[offset], _data[offset], _data[offset], _data[offset]); } else { - c = RGBW32(_data[offset], _data[offset+1], _data[offset+2], Bus::hasWhite(_type) ? _data[offset+3] : 0); + c = RGBW32(_data[offset], _data[offset+1], _data[offset+2], hasWhite() ? _data[offset+3] : 0); } return c; } else { if (_reversed) pix = _len - pix -1; pix += _skip; - uint8_t co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); + unsigned co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); uint32_t c = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, (_type==TYPE_WS2812_1CH_X3) ? IC_INDEX_WS2812_1CH_3X(pix) : pix, co),_bri); if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs - uint8_t r = R(c); - uint8_t g = _reversed ? B(c) : G(c); // should G and B be switched if _reversed? - uint8_t b = _reversed ? G(c) : B(c); + unsigned r = R(c); + unsigned g = _reversed ? B(c) : G(c); // should G and B be switched if _reversed? + unsigned b = _reversed ? G(c) : B(c); switch (pix % 3) { // get only the single channel case 0: c = RGBW32(g, g, g, g); break; case 1: c = RGBW32(r, r, r, r); break; @@ -340,9 +372,9 @@ uint32_t IRAM_ATTR BusDigital::getPixelColor(uint16_t pix) { } } -uint8_t BusDigital::getPins(uint8_t* pinArray) { - uint8_t numPins = IS_2PIN(_type) ? 2 : 1; - for (unsigned i = 0; i < numPins; i++) pinArray[i] = _pins[i]; +uint8_t BusDigital::getPins(uint8_t* pinArray) const { + unsigned numPins = is2Pin(_type) + 1; + if (pinArray) for (unsigned i = 0; i < numPins; i++) pinArray[i] = _pins[i]; return numPins; } @@ -352,12 +384,38 @@ void BusDigital::setColorOrder(uint8_t colorOrder) { _colorOrder = colorOrder; } -void BusDigital::reinit() { +// credit @willmmiles & @netmindz https://github.com/Aircoookie/WLED/pull/4056 +std::vector BusDigital::getLEDTypes() { + return { + {TYPE_WS2812_RGB, "D", PSTR("WS281x")}, + {TYPE_SK6812_RGBW, "D", PSTR("SK6812/WS2814 RGBW")}, + {TYPE_TM1814, "D", PSTR("TM1814")}, + {TYPE_WS2811_400KHZ, "D", PSTR("400kHz")}, + {TYPE_TM1829, "D", PSTR("TM1829")}, + {TYPE_UCS8903, "D", PSTR("UCS8903")}, + {TYPE_APA106, "D", PSTR("APA106/PL9823")}, + {TYPE_TM1914, "D", PSTR("TM1914")}, + {TYPE_FW1906, "D", PSTR("FW1906 GRBCW")}, + {TYPE_UCS8904, "D", PSTR("UCS8904 RGBW")}, + {TYPE_WS2805, "D", PSTR("WS2805 RGBCW")}, + {TYPE_SM16825, "D", PSTR("SM16825 RGBCW")}, + {TYPE_WS2812_1CH_X3, "D", PSTR("WS2811 White")}, + //{TYPE_WS2812_2CH_X3, "D", PSTR("WS2811 CCT")}, // not implemented + //{TYPE_WS2812_WWA, "D", PSTR("WS2811 WWA")}, // not implemented + {TYPE_WS2801, "2P", PSTR("WS2801")}, + {TYPE_APA102, "2P", PSTR("APA102")}, + {TYPE_LPD8806, "2P", PSTR("LPD8806")}, + {TYPE_LPD6803, "2P", PSTR("LPD6803")}, + {TYPE_P9813, "2P", PSTR("PP9813")}, + }; +} + +void BusDigital::reinit(void) { if (!_valid) return; PolyBus::begin(_busPtr, _iType, _pins); } -void BusDigital::cleanup() { +void BusDigital::cleanup(void) { DEBUG_PRINTLN(F("Digital Cleanup.")); PolyBus::cleanup(_busPtr, _iType); _iType = I_NONE; @@ -369,85 +427,108 @@ void BusDigital::cleanup() { } -BusPwm::BusPwm(BusConfig &bc) -: Bus(bc.type, bc.start, bc.autoWhite, 1, bc.reversed) -{ - if (!IS_PWM(bc.type)) return; - uint8_t numPins = NUM_PWM_PINS(bc.type); - _frequency = bc.frequency ? bc.frequency : WLED_PWM_FREQ; - - #ifdef ESP8266 - analogWriteRange(255); //same range as one RGB channel - analogWriteFreq(_frequency); +#ifdef ESP8266 + // 1 MHz clock + #define CLOCK_FREQUENCY 1000000UL +#else + // Use XTAL clock if possible to avoid timer frequency error when setting APB clock < 80 Mhz + // https://github.com/espressif/arduino-esp32/blob/2.0.2/cores/esp32/esp32-hal-ledc.c + #ifdef SOC_LEDC_SUPPORT_XTAL_CLOCK + #define CLOCK_FREQUENCY 40000000UL #else + #define CLOCK_FREQUENCY 80000000UL + #endif +#endif + +#ifdef ESP8266 + #define MAX_BIT_WIDTH 10 +#else + #ifdef SOC_LEDC_TIMER_BIT_WIDE_NUM + // C6/H2/P4: 20 bit, S2/S3/C2/C3: 14 bit + #define MAX_BIT_WIDTH SOC_LEDC_TIMER_BIT_WIDE_NUM + #else + // ESP32: 20 bit (but in reality we would never go beyond 16 bit as the frequency would be to low) + #define MAX_BIT_WIDTH 14 + #endif +#endif + +BusPwm::BusPwm(BusConfig &bc) +: Bus(bc.type, bc.start, bc.autoWhite, 1, bc.reversed, bc.refreshReq) // hijack Off refresh flag to indicate usage of dithering +{ + if (!isPWM(bc.type)) return; + unsigned numPins = numPWMPins(bc.type); + [[maybe_unused]] const bool dithering = _needsRefresh; + _frequency = bc.frequency ? bc.frequency : WLED_PWM_FREQ; + // duty cycle resolution (_depth) can be extracted from this formula: CLOCK_FREQUENCY > _frequency * 2^_depth + for (_depth = MAX_BIT_WIDTH; _depth > 8; _depth--) if (((CLOCK_FREQUENCY/_frequency) >> _depth) > 0) break; + + managed_pin_type pins[numPins]; + for (unsigned i = 0; i < numPins; i++) pins[i] = {(int8_t)bc.pins[i], true}; + if (!pinManager.allocateMultiplePins(pins, numPins, PinOwner::BusPwm)) return; + +#ifdef ESP8266 + analogWriteRange((1<<_depth)-1); + analogWriteFreq(_frequency); +#else + // for 2 pin PWM CCT strip pinManager will make sure both LEDC channels are in the same speed group and sharing the same timer _ledcStart = pinManager.allocateLedc(numPins); if (_ledcStart == 255) { //no more free LEDC channels - deallocatePins(); return; + pinManager.deallocateMultiplePins(pins, numPins, PinOwner::BusPwm); + return; } - #endif + // if _needsRefresh is true (UI hack) we are using dithering (credit @dedehai & @zalatnaicsongor) + if (dithering) _depth = 12; // fixed 8 bit depth PWM with 4 bit dithering (ESP8266 has no hardware to support dithering) +#endif for (unsigned i = 0; i < numPins; i++) { - uint8_t currentPin = bc.pins[i]; - if (!pinManager.allocatePin(currentPin, true, PinOwner::BusPwm)) { - deallocatePins(); return; - } - _pins[i] = currentPin; //store only after allocatePin() succeeds + _pins[i] = bc.pins[i]; // store only after allocateMultiplePins() succeeded #ifdef ESP8266 pinMode(_pins[i], OUTPUT); #else - ledcSetup(_ledcStart + i, _frequency, 8); - ledcAttachPin(_pins[i], _ledcStart + i); + unsigned channel = _ledcStart + i; + ledcSetup(channel, _frequency, _depth - (dithering*4)); // with dithering _frequency doesn't really matter as resolution is 8 bit + ledcAttachPin(_pins[i], channel); + // LEDC timer reset credit @dedehai + uint8_t group = (channel / 8), timer = ((channel / 2) % 4); // same fromula as in ledcSetup() + ledc_timer_rst((ledc_mode_t)group, (ledc_timer_t)timer); // reset timer so all timers are almost in sync (for phase shift) #endif } + _hasRgb = hasRGB(bc.type); + _hasWhite = hasWhite(bc.type); + _hasCCT = hasCCT(bc.type); _data = _pwmdata; // avoid malloc() and use stack _valid = true; + DEBUG_PRINTF_P(PSTR("%successfully inited PWM strip with type %u, frequency %u, bit depth %u and pins %u,%u,%u,%u,%u\n"), _valid?"S":"Uns", bc.type, _frequency, _depth, _pins[0], _pins[1], _pins[2], _pins[3], _pins[4]); } void BusPwm::setPixelColor(uint16_t pix, uint32_t c) { if (pix != 0 || !_valid) return; //only react to first pixel if (_type != TYPE_ANALOG_3CH) c = autoWhiteCalc(c); - if (_cct >= 1900 && (_type == TYPE_ANALOG_3CH || _type == TYPE_ANALOG_4CH)) { - c = colorBalanceFromKelvin(_cct, c); //color correction from CCT + if (Bus::_cct >= 1900 && (_type == TYPE_ANALOG_3CH || _type == TYPE_ANALOG_4CH)) { + c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT } uint8_t r = R(c); uint8_t g = G(c); uint8_t b = B(c); uint8_t w = W(c); - uint8_t cct = 0; //0 - full warm white, 255 - full cold white - if (_cct > -1) { - if (_cct >= 1900) cct = (_cct - 1900) >> 5; - else if (_cct < 256) cct = _cct; - } else { - cct = (approximateKelvinFromRGB(c) - 1900) >> 5; - } - - uint8_t ww, cw; - #ifdef WLED_USE_IC_CCT - ww = w; - cw = cct; - #else - //0 - linear (CCT 127 = 50% warm, 50% cold), 127 - additive CCT blending (CCT 127 = 100% warm, 100% cold) - if (cct < _cctBlend) ww = 255; - else ww = ((255-cct) * 255) / (255 - _cctBlend); - - if ((255-cct) < _cctBlend) cw = 255; - else cw = (cct * 255) / (255 - _cctBlend); - - ww = (w * ww) / 255; //brightness scaling - cw = (w * cw) / 255; - #endif switch (_type) { case TYPE_ANALOG_1CH: //one channel (white), relies on auto white calculation _data[0] = w; break; case TYPE_ANALOG_2CH: //warm white + cold white - _data[1] = cw; - _data[0] = ww; + if (cctICused) { + _data[0] = w; + _data[1] = Bus::_cct < 0 || Bus::_cct > 255 ? 127 : Bus::_cct; + } else { + Bus::calculateCCT(c, _data[0], _data[1]); + } break; case TYPE_ANALOG_5CH: //RGB + warm white + cold white - _data[4] = cw; - w = ww; + if (cctICused) + _data[4] = Bus::_cct < 0 || Bus::_cct > 255 ? 127 : Bus::_cct; + else + Bus::calculateCCT(c, w, _data[4]); case TYPE_ANALOG_4CH: //RGBW _data[3] = w; case TYPE_ANALOG_3CH: //standard dumb RGB @@ -457,43 +538,114 @@ void BusPwm::setPixelColor(uint16_t pix, uint32_t c) { } //does no index check -uint32_t BusPwm::getPixelColor(uint16_t pix) { +uint32_t BusPwm::getPixelColor(uint16_t pix) const { if (!_valid) return 0; - return RGBW32(_data[0], _data[1], _data[2], _data[3]); + // TODO getting the reverse from CCT is involved (a quick approximation when CCT blending is ste to 0 implemented) + switch (_type) { + case TYPE_ANALOG_1CH: //one channel (white), relies on auto white calculation + return RGBW32(0, 0, 0, _data[0]); + case TYPE_ANALOG_2CH: //warm white + cold white + if (cctICused) return RGBW32(0, 0, 0, _data[0]); + else return RGBW32(0, 0, 0, _data[0] + _data[1]); + case TYPE_ANALOG_5CH: //RGB + warm white + cold white + if (cctICused) return RGBW32(_data[0], _data[1], _data[2], _data[3]); + else return RGBW32(_data[0], _data[1], _data[2], _data[3] + _data[4]); + case TYPE_ANALOG_4CH: //RGBW + return RGBW32(_data[0], _data[1], _data[2], _data[3]); + case TYPE_ANALOG_3CH: //standard dumb RGB + return RGBW32(_data[0], _data[1], _data[2], 0); + } + return RGBW32(_data[0], _data[0], _data[0], _data[0]); } void BusPwm::show() { if (!_valid) return; - uint8_t numPins = NUM_PWM_PINS(_type); + // 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) + + // use CIE brightness formula (cubic) to fit (or approximate linearity of) human eye perceived brightness + // the formula is based on 12 bit resolution as there is no need for greater precision + // see: https://en.wikipedia.org/wiki/Lightness + unsigned pwmBri = (unsigned)_bri * 100; // enlarge to use integer math for linear response + if (pwmBri < 2040) { + // linear response for values [0-20] + pwmBri = ((pwmBri << 12) + 115043) / 230087; //adding '0.5' before division for correct rounding + } else { + // cubic response for values [21-255] + pwmBri += 4080; + float temp = (float)pwmBri / 29580.0f; + temp = temp * temp * temp * (float)maxBri; + pwmBri = (unsigned)temp; // pwmBri is in range [0-maxBri] + } + + [[maybe_unused]] unsigned hPoint = 0; // phase shift (0 - maxBri) + // we will be phase shifting every channel by previous pulse length (plus dead time if required) + // phase shifting is only mandatory when using H-bridge to drive reverse-polarity PWM CCT (2 wire) LED type + // CCT additive blending must be 0 (WW & CW will not overlap) otherwise signals *will* overlap + // for all other cases it will just try to "spread" the load on PSU + // Phase shifting requires that LEDC timers are synchronised (see setup()). For PWM CCT (and H-bridge) it is + // also mandatory that both channels use the same timer (pinManager takes care of that). for (unsigned i = 0; i < numPins; i++) { - uint8_t scaled = (_data[i] * _bri) / 255; - if (_reversed) scaled = 255 - scaled; + unsigned duty = (_data[i] * pwmBri) / 255; #ifdef ESP8266 - analogWrite(_pins[i], scaled); + if (_reversed) duty = maxBri - duty; + analogWrite(_pins[i], duty); #else - ledcWrite(_ledcStart + i, scaled); + int 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 (_reversed) duty = maxBri - duty; + unsigned channel = _ledcStart + i; + unsigned gr = channel/8; // high/low speed group + unsigned ch = channel%8; // group channel + // directly write to LEDC struct as there is no HAL exposed function for dithering + // duty has 20 bit resolution with 4 fractional bits (24 bits in total) + 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 } } -uint8_t BusPwm::getPins(uint8_t* pinArray) { +uint8_t BusPwm::getPins(uint8_t* pinArray) const { if (!_valid) return 0; - uint8_t numPins = NUM_PWM_PINS(_type); - for (unsigned i = 0; i < numPins; i++) { - pinArray[i] = _pins[i]; - } + unsigned numPins = numPWMPins(_type); + if (pinArray) for (unsigned i = 0; i < numPins; i++) pinArray[i] = _pins[i]; return numPins; } -void BusPwm::deallocatePins() { - uint8_t numPins = NUM_PWM_PINS(_type); +// credit @willmmiles & @netmindz https://github.com/Aircoookie/WLED/pull/4056 +std::vector BusPwm::getLEDTypes() { + return { + {TYPE_ANALOG_1CH, "A", PSTR("PWM White")}, + {TYPE_ANALOG_2CH, "AA", PSTR("PWM CCT")}, + {TYPE_ANALOG_3CH, "AAA", PSTR("PWM RGB")}, + {TYPE_ANALOG_4CH, "AAAA", PSTR("PWM RGBW")}, + {TYPE_ANALOG_5CH, "AAAAA", PSTR("PWM RGB+CCT")}, + //{TYPE_ANALOG_6CH, "AAAAAA", PSTR("PWM RGB+DCCT")}, // unimplementable ATM + }; +} + +void BusPwm::deallocatePins(void) { + unsigned numPins = getPins(); for (unsigned i = 0; i < numPins; i++) { pinManager.deallocatePin(_pins[i], PinOwner::BusPwm); if (!pinManager.isPinOk(_pins[i])) continue; #ifdef ESP8266 digitalWrite(_pins[i], LOW); //turn off PWM interrupt #else - if (_ledcStart < 16) ledcDetachPin(_pins[i]); + if (_ledcStart < WLED_MAX_ANALOG_CHANNELS) ledcDetachPin(_pins[i]); #endif } #ifdef ARDUINO_ARCH_ESP32 @@ -506,7 +658,7 @@ BusOnOff::BusOnOff(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite, 1, bc.reversed) , _onoffdata(0) { - if (bc.type != TYPE_ONOFF) return; + if (!Bus::isOnOff(bc.type)) return; uint8_t currentPin = bc.pins[0]; if (!pinManager.allocatePin(currentPin, true, PinOwner::BusOnOff)) { @@ -514,8 +666,12 @@ BusOnOff::BusOnOff(BusConfig &bc) } _pin = currentPin; //store only after allocatePin() succeeds pinMode(_pin, OUTPUT); + _hasRgb = false; + _hasWhite = false; + _hasCCT = false; _data = &_onoffdata; // avoid malloc() and use stack _valid = true; + DEBUG_PRINTF_P(PSTR("%successfully inited On/Off strip with pin %u\n"), _valid?"S":"Uns", _pin); } void BusOnOff::setPixelColor(uint16_t pix, uint32_t c) { @@ -528,22 +684,28 @@ void BusOnOff::setPixelColor(uint16_t pix, uint32_t c) { _data[0] = bool(r|g|b|w) && bool(_bri) ? 0xFF : 0; } -uint32_t BusOnOff::getPixelColor(uint16_t pix) { +uint32_t BusOnOff::getPixelColor(uint16_t pix) const { if (!_valid) return 0; return RGBW32(_data[0], _data[0], _data[0], _data[0]); } -void BusOnOff::show() { +void BusOnOff::show(void) { if (!_valid) return; digitalWrite(_pin, _reversed ? !(bool)_data[0] : (bool)_data[0]); } -uint8_t BusOnOff::getPins(uint8_t* pinArray) { +uint8_t BusOnOff::getPins(uint8_t* pinArray) const { if (!_valid) return 0; - pinArray[0] = _pin; + if (pinArray) pinArray[0] = _pin; return 1; } +// credit @willmmiles & @netmindz https://github.com/Aircoookie/WLED/pull/4056 +std::vector BusOnOff::getLEDTypes() { + return { + {TYPE_ONOFF, "", PSTR("On/Off")}, + }; +} BusNetwork::BusNetwork(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite, bc.count) @@ -551,55 +713,72 @@ BusNetwork::BusNetwork(BusConfig &bc) { switch (bc.type) { case TYPE_NET_ARTNET_RGB: - _rgbw = false; + _UDPtype = 2; + break; + case TYPE_NET_ARTNET_RGBW: _UDPtype = 2; break; case TYPE_NET_E131_RGB: - _rgbw = false; _UDPtype = 1; break; default: // TYPE_NET_DDP_RGB / TYPE_NET_DDP_RGBW - _rgbw = bc.type == TYPE_NET_DDP_RGBW; _UDPtype = 0; break; } - _UDPchannels = _rgbw ? 4 : 3; + _hasRgb = hasRGB(bc.type); + _hasWhite = hasWhite(bc.type); + _hasCCT = false; + _UDPchannels = _hasWhite + 3; _client = IPAddress(bc.pins[0],bc.pins[1],bc.pins[2],bc.pins[3]); - _valid = (allocData(_len * _UDPchannels) != nullptr); + _valid = (allocateData(_len * _UDPchannels) != nullptr); + DEBUG_PRINTF_P(PSTR("%successfully inited virtual strip with type %u and IP %u.%u.%u.%u\n"), _valid?"S":"Uns", bc.type, bc.pins[0], bc.pins[1], bc.pins[2], bc.pins[3]); } void BusNetwork::setPixelColor(uint16_t pix, uint32_t c) { if (!_valid || pix >= _len) return; - if (_rgbw) c = autoWhiteCalc(c); - if (_cct >= 1900) c = colorBalanceFromKelvin(_cct, c); //color correction from CCT - uint16_t offset = pix * _UDPchannels; + if (_hasWhite) c = autoWhiteCalc(c); + if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT + unsigned offset = pix * _UDPchannels; _data[offset] = R(c); _data[offset+1] = G(c); _data[offset+2] = B(c); - if (_rgbw) _data[offset+3] = W(c); + if (_hasWhite) _data[offset+3] = W(c); } -uint32_t BusNetwork::getPixelColor(uint16_t pix) { +uint32_t BusNetwork::getPixelColor(uint16_t pix) const { if (!_valid || pix >= _len) return 0; - uint16_t offset = pix * _UDPchannels; - return RGBW32(_data[offset], _data[offset+1], _data[offset+2], (_rgbw ? _data[offset+3] : 0)); + unsigned offset = pix * _UDPchannels; + return RGBW32(_data[offset], _data[offset+1], _data[offset+2], (hasWhite() ? _data[offset+3] : 0)); } -void BusNetwork::show() { +void BusNetwork::show(void) { if (!_valid || !canShow()) return; _broadcastLock = true; - realtimeBroadcast(_UDPtype, _client, _len, _data, _bri, _rgbw); + realtimeBroadcast(_UDPtype, _client, _len, _data, _bri, hasWhite()); _broadcastLock = false; } -uint8_t BusNetwork::getPins(uint8_t* pinArray) { - for (unsigned i = 0; i < 4; i++) { - pinArray[i] = _client[i]; - } +uint8_t BusNetwork::getPins(uint8_t* pinArray) const { + if (pinArray) for (unsigned i = 0; i < 4; i++) pinArray[i] = _client[i]; return 4; } -void BusNetwork::cleanup() { +// credit @willmmiles & @netmindz https://github.com/Aircoookie/WLED/pull/4056 +std::vector BusNetwork::getLEDTypes() { + return { + {TYPE_NET_DDP_RGB, "N", PSTR("DDP RGB (network)")}, // should be "NNNN" to determine 4 "pin" fields + {TYPE_NET_ARTNET_RGB, "N", PSTR("Art-Net RGB (network)")}, + {TYPE_NET_DDP_RGBW, "N", PSTR("DDP RGBW (network)")}, + {TYPE_NET_ARTNET_RGBW, "N", PSTR("Art-Net RGBW (network)")}, + // hypothetical extensions + //{TYPE_VIRTUAL_I2C_W, "V", PSTR("I2C White (virtual)")}, // allows setting I2C address in _pin[0] + //{TYPE_VIRTUAL_I2C_CCT, "V", PSTR("I2C CCT (virtual)")}, // allows setting I2C address in _pin[0] + //{TYPE_VIRTUAL_I2C_RGB, "VVV", PSTR("I2C RGB (virtual)")}, // allows setting I2C address in _pin[0] and 2 additional values in _pin[1] & _pin[2] + //{TYPE_USERMOD, "VVVVV", PSTR("Usermod (virtual)")}, // 5 data fields (see https://github.com/Aircoookie/WLED/pull/4123) + }; +} + +void BusNetwork::cleanup(void) { _type = I_NONE; _valid = false; freeData(); @@ -800,42 +979,41 @@ void BusHub75Matrix::deallocatePins() { //utility to get the approx. memory usage of a given BusConfig uint32_t BusManager::memUsage(BusConfig &bc) { - if (bc.type == TYPE_ONOFF || IS_PWM(bc.type)) return 5; + if (Bus::isOnOff(bc.type) || Bus::isPWM(bc.type)) return 5; - uint16_t len = bc.count + bc.skipAmount; - uint16_t channels = 3; - uint16_t multiplier = 1; - if (IS_DIGITAL(bc.type)) { // digital types - if (IS_16BIT(bc.type)) len *= 2; // 16-bit LEDs + unsigned len = bc.count + bc.skipAmount; + unsigned channels = Bus::getNumberOfChannels(bc.type); + unsigned multiplier = 1; + if (Bus::isDigital(bc.type)) { // digital types + if (Bus::is16bit(bc.type)) len *= 2; // 16-bit LEDs #ifdef ESP8266 - if (bc.type > 28) channels = 4; //RGBW if (bc.pins[0] == 3) { //8266 DMA uses 5x the mem multiplier = 5; } - #else //ESP32 RMT uses double buffer, I2S uses 5x buffer - if (bc.type > 28) channels = 4; //RGBW - multiplier = 2; + #else //ESP32 RMT uses double buffer, parallel I2S uses 8x buffer (3 times) + multiplier = PolyBus::isParallelI2S1Output() ? 24 : 2; #endif } - if (IS_VIRTUAL(bc.type)) { - switch (bc.type) { - case TYPE_NET_DDP_RGBW: channels = 4; break; - } - } - return len * channels * multiplier; //RGB + return (len * multiplier + bc.doubleBuffer * (bc.count + bc.skipAmount)) * channels; +} + +uint32_t BusManager::memUsage(unsigned maxChannels, unsigned maxCount, unsigned minBuses) { + //ESP32 RMT uses double buffer, parallel I2S uses 8x buffer (3 times) + unsigned multiplier = PolyBus::isParallelI2S1Output() ? 3 : 2; + return (maxChannels * maxCount * minBuses * multiplier); } int BusManager::add(BusConfig &bc) { if (getNumBusses() - getNumVirtualBusses() >= WLED_MAX_BUSSES) return -1; - if (IS_VIRTUAL(bc.type)) { + if (Bus::isVirtual(bc.type)) { busses[numBusses] = new BusNetwork(bc); #ifdef WLED_ENABLE_HUB75MATRIX - } else if (bc.type >= TYPE_HUB75MATRIX && bc.type <= (TYPE_HUB75MATRIX + 10)) { + } else if (Bus::isHub75(bc.type)) { busses[numBusses] = new BusHub75Matrix(bc); #endif - } else if (IS_DIGITAL(bc.type)) { + } else if (Bus::isDigital(bc.type)) { busses[numBusses] = new BusDigital(bc, numBusses, colorOrderMap); - } else if (bc.type == TYPE_ONOFF) { + } else if (Bus::isOnOff(bc.type)) { busses[numBusses] = new BusOnOff(bc); } else { busses[numBusses] = new BusPwm(bc); @@ -843,16 +1021,119 @@ int BusManager::add(BusConfig &bc) { return numBusses++; } +// credit @willmmiles +static String LEDTypesToJson(const std::vector& types) { + String json; + for (const auto &type : types) { + // capabilities follows similar pattern as JSON API + int capabilities = Bus::hasRGB(type.id) | Bus::hasWhite(type.id)<<1 | Bus::hasCCT(type.id)<<2 | Bus::is16bit(type.id)<<4; + char str[256]; + sprintf_P(str, PSTR("{i:%d,c:%d,t:\"%s\",n:\"%s\"},"), type.id, capabilities, type.type, type.name); + json += str; + } + return json; +} + +// credit @willmmiles & @netmindz https://github.com/Aircoookie/WLED/pull/4056 +String BusManager::getLEDTypesJSONString(void) { + String json = "["; + json += LEDTypesToJson(BusDigital::getLEDTypes()); + json += LEDTypesToJson(BusOnOff::getLEDTypes()); + json += LEDTypesToJson(BusPwm::getLEDTypes()); + json += LEDTypesToJson(BusNetwork::getLEDTypes()); + //json += LEDTypesToJson(BusVirtual::getLEDTypes()); + json.setCharAt(json.length()-1, ']'); // replace last comma with bracket + return json; +} + +void BusManager::useParallelOutput(void) { + _parallelOutputs = 8; // hardcoded since we use NPB I2S x8 methods + PolyBus::setParallelI2S1Output(); +} + //do not call this method from system context (network callback) -void BusManager::removeAll() { +void BusManager::removeAll(void) { DEBUG_PRINTLN(F("Removing all.")); //prevents crashes due to deleting busses while in use. while (!canAllShow()) yield(); for (unsigned i = 0; i < numBusses; i++) delete busses[i]; numBusses = 0; + _parallelOutputs = 1; + PolyBus::setParallelI2S1Output(false); } -void BusManager::show() { +#ifdef ESP32_DATA_IDLE_HIGH +// #2478 +// If enabled, RMT idle level is set to HIGH when off +// to prevent leakage current when using an N-channel MOSFET to toggle LED power +void BusManager::esp32RMTInvertIdle(void) { + bool idle_out; + unsigned rmt = 0; + for (unsigned u = 0; u < numBusses(); u++) { + #if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, only has 1 I2S but NPB does not support it ATM + if (u > 1) return; + rmt = u; + #elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, only has 1 I2S bus, supported in NPB + if (u > 3) return; + rmt = u; + #elif defined(CONFIG_IDF_TARGET_ESP32S3) // 4 RMT, has 2 I2S but NPB does not support them ATM + if (u > 3) return; + rmt = u; + #else + if (u < _parallelOutputs) continue; + if (u >= _parallelOutputs + 8) return; // only 8 RMT channels + rmt = u - _parallelOutputs; + #endif + if (busses[u]->getLength()==0 || !busses[u]->isDigital() || busses[u]->is2Pin()) continue; + //assumes that bus number to rmt channel mapping stays 1:1 + rmt_channel_t ch = static_cast(rmt); + rmt_idle_level_t lvl; + rmt_get_idle_level(ch, &idle_out, &lvl); + if (lvl == RMT_IDLE_LEVEL_HIGH) lvl = RMT_IDLE_LEVEL_LOW; + else if (lvl == RMT_IDLE_LEVEL_LOW) lvl = RMT_IDLE_LEVEL_HIGH; + else continue; + rmt_set_idle_level(ch, idle_out, lvl); + } +} +#endif + +void BusManager::on(void) { + #ifdef ESP8266 + //Fix for turning off onboard LED breaking bus + if (pinManager.getPinOwner(LED_BUILTIN) == PinOwner::BusDigital) { + for (unsigned i = 0; i < numBusses; i++) { + uint8_t pins[2] = {255,255}; + if (busses[i]->isDigital() && busses[i]->getPins(pins)) { + if (pins[0] == LED_BUILTIN || pins[1] == LED_BUILTIN) { + BusDigital *bus = static_cast(busses[i]); + bus->reinit(); + break; + } + } + } + } + #endif + #ifdef ESP32_DATA_IDLE_HIGH + esp32RMTInvertIdle(); + #endif +} + +void BusManager::off(void) { + #ifdef ESP8266 + // turn off built-in LED if strip is turned off + // this will break digital bus so will need to be re-initialised on On + if (pinManager.getPinOwner(LED_BUILTIN) == PinOwner::BusDigital) { + for (unsigned i = 0; i < numBusses; i++) if (busses[i]->isOffRefreshRequired()) return; + pinMode(LED_BUILTIN, OUTPUT); + digitalWrite(LED_BUILTIN, HIGH); + } + #endif + #ifdef ESP32_DATA_IDLE_HIGH + esp32RMTInvertIdle(); + #endif +} + +void BusManager::show(void) { _milliAmpsUsed = 0; for (unsigned i = 0; i < numBusses; i++) { busses[i]->show(); @@ -869,9 +1150,8 @@ void BusManager::setStatusPixel(uint32_t c) { void IRAM_ATTR BusManager::setPixelColor(uint16_t pix, uint32_t c) { for (unsigned i = 0; i < numBusses; i++) { - Bus* b = busses[i]; - uint16_t bstart = b->getStart(); - if (pix < bstart || pix >= bstart + b->getLength()) continue; + unsigned bstart = busses[i]->getStart(); + if (pix < bstart || pix >= bstart + busses[i]->getLength()) continue; busses[i]->setPixelColor(pix - bstart, c); } } @@ -887,21 +1167,20 @@ void BusManager::setSegmentCCT(int16_t cct, bool allowWBCorrection) { if (cct >= 0) { //if white balance correction allowed, save as kelvin value instead of 0-255 if (allowWBCorrection) cct = 1900 + (cct << 5); - } else cct = -1; + } else cct = -1; // will use kelvin approximation from RGB Bus::setCCT(cct); } uint32_t BusManager::getPixelColor(uint16_t pix) { for (unsigned i = 0; i < numBusses; i++) { - Bus* b = busses[i]; - uint16_t bstart = b->getStart(); - if (pix < bstart || pix >= bstart + b->getLength()) continue; - return b->getPixelColor(pix - bstart); + unsigned bstart = busses[i]->getStart(); + if (!busses[i]->containsPixel(pix)) continue; + return busses[i]->getPixelColor(pix - bstart); } return 0; } -bool BusManager::canAllShow() { +bool BusManager::canAllShow(void) { for (unsigned i = 0; i < numBusses; i++) { if (!busses[i]->canShow()) return false; } @@ -914,12 +1193,14 @@ Bus* BusManager::getBus(uint8_t busNr) { } //semi-duplicate of strip.getLengthTotal() (though that just returns strip._length, calculated in finalizeInit()) -uint16_t BusManager::getTotalLength() { - uint16_t len = 0; +uint16_t BusManager::getTotalLength(void) { + unsigned len = 0; for (unsigned i=0; igetLength(); return len; } +bool PolyBus::useParallelI2S = false; + // Bus static member definition int16_t Bus::_cct = -1; uint8_t Bus::_cctBlend = 0; @@ -931,4 +1212,5 @@ uint8_t BusManager::numBusses = 0; Bus* BusManager::busses[WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES]; ColorOrderMap BusManager::colorOrderMap = {}; uint16_t BusManager::_milliAmpsUsed = 0; -uint16_t BusManager::_milliAmpsMax = ABL_MILLIAMPS_DEFAULT; \ No newline at end of file +uint16_t BusManager::_milliAmpsMax = ABL_MILLIAMPS_DEFAULT; +uint8_t BusManager::_parallelOutputs = 1; diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 8ffc13c3..df11cf4b 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -9,6 +9,10 @@ */ #include "const.h" +#include + +//colors.cpp +uint16_t approximateKelvinFromRGB(uint32_t rgb); #define GET_BIT(var,bit) (((var)>>(bit))&0x01) #define SET_BIT(var,bit) ((var)|=(uint16_t)(0x0001<<(bit))) @@ -21,94 +25,44 @@ #define IC_INDEX_WS2812_2CH_3X(i) ((i)*2/3) #define WS2812_2CH_3X_SPANS_2_ICS(i) ((i)&0x01) // every other LED zone is on two different ICs -// flag for using double buffering in BusDigital -extern bool useGlobalLedBuffer; - - -//temporary struct for passing bus configuration to bus -struct BusConfig { - uint8_t type; - uint16_t count; - uint16_t start; - uint8_t colorOrder; - bool reversed; - uint8_t skipAmount; - bool refreshReq; - uint8_t autoWhite; - uint8_t pins[5] = {LEDPIN, 255, 255, 255, 255}; - uint16_t frequency; - bool doubleBuffer; - uint8_t milliAmpsPerLed; - uint16_t milliAmpsMax; - - BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U, bool dblBfr=false, uint8_t maPerLed=55, uint16_t maMax=ABL_MILLIAMPS_DEFAULT) - : count(len) - , start(pstart) - , colorOrder(pcolorOrder) - , reversed(rev) - , skipAmount(skip) - , autoWhite(aw) - , frequency(clock_kHz) - , doubleBuffer(dblBfr) - , milliAmpsPerLed(maPerLed) - , milliAmpsMax(maMax) - { - refreshReq = (bool) GET_BIT(busType,7); - type = busType & 0x7F; // bit 7 may be/is hacked to include refresh info (1=refresh in off state, 0=no refresh) - size_t nPins = 1; - if (type >= TYPE_NET_DDP_RGB && type < 96) nPins = 4; //virtual network bus. 4 "pins" store IP address - else if (type > 47) nPins = 2; - else if (type > 40 && type < 46) nPins = NUM_PWM_PINS(type); - else if (type >= TYPE_HUB75MATRIX && type <= (TYPE_HUB75MATRIX + 10)) nPins = 0; - for (size_t i = 0; i < nPins; i++) pins[i] = ppins[i]; - } - - //validates start and length and extends total if needed - bool adjustBounds(uint16_t& total) { - if (!count) count = 1; - if (count > MAX_LEDS_PER_BUS) count = MAX_LEDS_PER_BUS; - if (start >= MAX_LEDS) return false; - //limit length of strip if it would exceed total permissible LEDs - if (start + count > MAX_LEDS) count = MAX_LEDS - start; - //extend total count accordingly - if (start + count > total) total = start + count; - return true; - } -}; - +struct BusConfig; // forward declaration // Defines an LED Strip and its color ordering. -struct ColorOrderMapEntry { +typedef struct { uint16_t start; uint16_t len; uint8_t colorOrder; -}; +} ColorOrderMapEntry; struct ColorOrderMap { - void add(uint16_t start, uint16_t len, uint8_t colorOrder); + bool add(uint16_t start, uint16_t len, uint8_t colorOrder); - uint8_t count() const { return _count; } + inline uint8_t count() const { return _mappings.size(); } void reset() { - _count = 0; - memset(_mappings, 0, sizeof(_mappings)); + _mappings.clear(); + _mappings.shrink_to_fit(); } const ColorOrderMapEntry* get(uint8_t n) const { - if (n > _count) { - return nullptr; - } + if (n >= count()) return nullptr; return &(_mappings[n]); } uint8_t getPixelColorOrder(uint16_t pix, uint8_t defaultColorOrder) const; private: - uint8_t _count; - ColorOrderMapEntry _mappings[WLED_MAX_COLOR_ORDER_MAPPINGS]; + std::vector _mappings; }; +typedef struct { + uint8_t id; + const char *type; + const char *name; +} LEDType; + + //parent class of BusDigital, BusPwm, and BusNetwork class Bus { public: @@ -127,80 +81,115 @@ class Bus { virtual ~Bus() {} //throw the bus under the bus - virtual void show() = 0; - virtual bool canShow() { return true; } - virtual void setStatusPixel(uint32_t c) {} + virtual void show(void) = 0; + virtual bool canShow(void) const { return true; } + virtual void setStatusPixel(uint32_t c) {} virtual void setPixelColor(uint16_t pix, uint32_t c) = 0; - virtual uint32_t getPixelColor(uint16_t pix) { return 0; } - virtual void setBrightness(uint8_t b) { _bri = b; }; - virtual uint8_t getPins(uint8_t* pinArray) { return 0; } - virtual uint16_t getLength() { return _len; } - virtual void setColorOrder(uint8_t co) {} - virtual uint8_t getColorOrder() { return COL_ORDER_RGB; } - virtual uint8_t skippedLeds() { return 0; } - virtual uint16_t getFrequency() { return 0U; } - virtual uint16_t getLEDCurrent() { return 0; } - virtual uint16_t getUsedCurrent() { return 0; } - virtual uint16_t getMaxCurrent() { return 0; } - inline void setReversed(bool reversed) { _reversed = reversed; } - inline uint16_t getStart() { return _start; } - inline void setStart(uint16_t start) { _start = start; } - inline uint8_t getType() { return _type; } - inline bool isOk() { return _valid; } - inline bool isReversed() { return _reversed; } - inline bool isOffRefreshRequired() { return _needsRefresh; } - bool containsPixel(uint16_t pix) { return pix >= _start && pix < _start+_len; } + virtual void setBrightness(uint8_t b) { _bri = b; }; + virtual void setColorOrder(uint8_t co) {} + virtual uint32_t getPixelColor(uint16_t pix) const { return 0; } + virtual uint8_t getPins(uint8_t* pinArray = nullptr) const { return 0; } + virtual uint16_t getLength(void) const { return isOk() ? _len : 0; } + virtual uint8_t getColorOrder(void) const { return COL_ORDER_RGB; } + virtual uint8_t skippedLeds(void) const { return 0; } + virtual uint16_t getFrequency(void) const { return 0U; } + virtual uint16_t getLEDCurrent(void) const { return 0; } + virtual uint16_t getUsedCurrent(void) const { return 0; } + virtual uint16_t getMaxCurrent(void) const { return 0; } - virtual bool hasRGB(void) { return Bus::hasRGB(_type); } - static bool hasRGB(uint8_t type) { - if ((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || type == TYPE_ANALOG_1CH || type == TYPE_ANALOG_2CH || type == TYPE_ONOFF) return false; - return true; + inline bool hasRGB(void) const { return _hasRgb; } + inline bool hasWhite(void) const { return _hasWhite; } + inline bool hasCCT(void) const { return _hasCCT; } + inline bool isDigital(void) const { return isDigital(_type); } + inline bool is2Pin(void) const { return is2Pin(_type); } + inline bool isOnOff(void) const { return isOnOff(_type); } + inline bool isPWM(void) const { return isPWM(_type); } + inline bool isVirtual(void) const { return isVirtual(_type); } + inline bool is16bit(void) const { return is16bit(_type); } + inline void setReversed(bool reversed) { _reversed = reversed; } + inline void setStart(uint16_t start) { _start = start; } + inline void setAutoWhiteMode(uint8_t m) { if (m < 5) _autoWhiteMode = m; } + inline uint8_t getAutoWhiteMode(void) const { return _autoWhiteMode; } + inline uint8_t getNumberOfChannels(void) const { return hasWhite() + 3*hasRGB() + hasCCT(); } + inline uint16_t getStart(void) const { return _start; } + inline uint8_t getType(void) const { return _type; } + inline bool isOk(void) const { return _valid; } + inline bool isReversed(void) const { return _reversed; } + inline bool isOffRefreshRequired(void) const { return _needsRefresh; } + inline bool containsPixel(uint16_t pix) const { return pix >= _start && pix < _start + _len; } + + static inline std::vector getLEDTypes(void) { return {{TYPE_NONE, "", PSTR("None")}}; } // not used. just for reference for derived classes + static constexpr uint8_t getNumberOfPins(uint8_t type) { return isVirtual(type) ? 4 : isPWM(type) ? numPWMPins(type) : is2Pin(type) + 1; } // credit @PaoloTK + static constexpr uint8_t getNumberOfChannels(uint8_t type) { return hasWhite(type) + 3*hasRGB(type) + hasCCT(type); } + static constexpr bool hasRGB(uint8_t type) { + return !((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || type == TYPE_ANALOG_1CH || type == TYPE_ANALOG_2CH || type == TYPE_ONOFF); } - virtual bool hasWhite(void) { return Bus::hasWhite(_type); } - static bool hasWhite(uint8_t type) { - if ((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || type == TYPE_SK6812_RGBW || type == TYPE_TM1814 || type == TYPE_UCS8904) return true; // digital types with white channel - if (type > TYPE_ONOFF && type <= TYPE_ANALOG_5CH && type != TYPE_ANALOG_3CH) return true; // analog types with white channel - if (type == TYPE_NET_DDP_RGBW) return true; // network types with white channel - return false; + static constexpr bool hasWhite(uint8_t type) { + return (type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || + type == TYPE_SK6812_RGBW || type == TYPE_TM1814 || type == TYPE_UCS8904 || + type == TYPE_FW1906 || type == TYPE_WS2805 || type == TYPE_SM16825 || // digital types with white channel + (type > TYPE_ONOFF && type <= TYPE_ANALOG_5CH && type != TYPE_ANALOG_3CH) || // analog types with white channel + type == TYPE_NET_DDP_RGBW || type == TYPE_NET_ARTNET_RGBW; // network types with white channel } - virtual bool hasCCT(void) { return Bus::hasCCT(_type); } - static bool hasCCT(uint8_t type) { - if (type == TYPE_WS2812_2CH_X3 || type == TYPE_WS2812_WWA || - type == TYPE_ANALOG_2CH || type == TYPE_ANALOG_5CH) return true; - return false; + static constexpr bool hasCCT(uint8_t type) { + return type == TYPE_WS2812_2CH_X3 || type == TYPE_WS2812_WWA || + type == TYPE_ANALOG_2CH || type == TYPE_ANALOG_5CH || + type == TYPE_FW1906 || type == TYPE_WS2805 || + type == TYPE_SM16825; } - static void setCCT(uint16_t cct) { - _cct = cct; - } - static void setCCTBlend(uint8_t b) { - if (b > 100) b = 100; - _cctBlend = (b * 127) / 100; + static constexpr bool isTypeValid(uint8_t type) { return (type > 15 && type < 128); } + static constexpr bool isDigital(uint8_t type) { return (type >= TYPE_DIGITAL_MIN && type <= TYPE_DIGITAL_MAX) || is2Pin(type); } + static constexpr bool is2Pin(uint8_t type) { return (type >= TYPE_2PIN_MIN && type <= TYPE_2PIN_MAX); } + static constexpr bool isOnOff(uint8_t type) { return (type == TYPE_ONOFF); } + static constexpr bool isPWM(uint8_t type) { return (type >= TYPE_ANALOG_MIN && type <= TYPE_ANALOG_MAX); } + static constexpr bool isVirtual(uint8_t type) { return (type >= TYPE_VIRTUAL_MIN && type <= TYPE_VIRTUAL_MAX); } + static constexpr bool is16bit(uint8_t type) { return type == TYPE_UCS8903 || type == TYPE_UCS8904 || type == TYPE_SM16825; } + static constexpr int numPWMPins(uint8_t type) { return (type - 40); } + + static inline int16_t getCCT(void) { return _cct; } + static inline void setGlobalAWMode(uint8_t m) { if (m < 5) _gAWM = m; else _gAWM = AW_GLOBAL_DISABLED; } + static inline uint8_t getGlobalAWMode(void) { return _gAWM; } + static inline void setCCT(int16_t cct) { _cct = cct; } + static inline uint8_t getCCTBlend(void) { return _cctBlend; } + static inline void setCCTBlend(uint8_t b) { + _cctBlend = (std::min((int)b,100) * 127) / 100; //compile-time limiter for hardware that can't power both white channels at max #ifdef WLED_MAX_CCT_BLEND if (_cctBlend > WLED_MAX_CCT_BLEND) _cctBlend = WLED_MAX_CCT_BLEND; #endif } - inline void setAutoWhiteMode(uint8_t m) { if (m < 5) _autoWhiteMode = m; } - inline uint8_t getAutoWhiteMode() { return _autoWhiteMode; } - inline static void setGlobalAWMode(uint8_t m) { if (m < 5) _gAWM = m; else _gAWM = AW_GLOBAL_DISABLED; } - inline static uint8_t getGlobalAWMode() { return _gAWM; } + static void calculateCCT(uint32_t c, uint8_t &ww, uint8_t &cw); protected: uint8_t _type; uint8_t _bri; uint16_t _start; uint16_t _len; - bool _reversed; - bool _valid; - bool _needsRefresh; + //struct { //using bitfield struct adds abour 250 bytes to binary size + bool _reversed;// : 1; + bool _valid;// : 1; + bool _needsRefresh;// : 1; + bool _hasRgb;// : 1; + bool _hasWhite;// : 1; + bool _hasCCT;// : 1; + //} __attribute__ ((packed)); uint8_t _autoWhiteMode; uint8_t *_data; + // global Auto White Calculation override static uint8_t _gAWM; + // _cct has the following menaings (see calculateCCT() & BusManager::setSegmentCCT()): + // -1 means to extract approximate CCT value in K from RGB (in calcualteCCT()) + // [0,255] is the exact CCT value where 0 means warm and 255 cold + // [1900,10060] only for color correction expressed in K (colorBalanceFromKelvin()) static int16_t _cct; + // _cctBlend determines WW/CW blending: + // 0 - linear (CCT 127 => 50% warm, 50% cold) + // 63 - semi additive/nonlinear (CCT 127 => 66% warm, 66% cold) + // 127 - additive CCT blending (CCT 127 => 100% warm, 100% cold) static uint8_t _cctBlend; - uint32_t autoWhiteCalc(uint32_t c); - uint8_t *allocData(size_t size = 1); + uint32_t autoWhiteCalc(uint32_t c) const; + uint8_t *allocateData(size_t size = 1); void freeData() { if (_data != nullptr) free(_data); _data = nullptr; } }; @@ -210,23 +199,24 @@ class BusDigital : public Bus { BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com); ~BusDigital() { cleanup(); } - void show() override; - bool canShow() override; + void show(void) override; + bool canShow(void) const override; void setBrightness(uint8_t b) override; void setStatusPixel(uint32_t c) override; void setPixelColor(uint16_t pix, uint32_t c) override; void setColorOrder(uint8_t colorOrder) override; - uint32_t getPixelColor(uint16_t pix) override; - uint8_t getColorOrder() override { return _colorOrder; } - uint8_t getPins(uint8_t* pinArray) override; - uint8_t skippedLeds() override { return _skip; } - uint16_t getFrequency() override { return _frequencykHz; } - uint8_t estimateCurrentAndLimitBri(); - uint16_t getLEDCurrent() override { return _milliAmpsPerLed; } - uint16_t getUsedCurrent() override { return _milliAmpsTotal; } - uint16_t getMaxCurrent() override { return _milliAmpsMax; } - void reinit(); - void cleanup(); + uint32_t getPixelColor(uint16_t pix) const override; + uint8_t getColorOrder(void) const override { return _colorOrder; } + uint8_t getPins(uint8_t* pinArray = nullptr) const override; + uint8_t skippedLeds(void) const override { return _skip; } + uint16_t getFrequency(void) const override { return _frequencykHz; } + uint16_t getLEDCurrent(void) const override { return _milliAmpsPerLed; } + uint16_t getUsedCurrent(void) const override { return _milliAmpsTotal; } + uint16_t getMaxCurrent(void) const override { return _milliAmpsMax; } + void reinit(void); + void cleanup(void); + + static std::vector getLEDTypes(void); private: uint8_t _skip; @@ -241,7 +231,7 @@ class BusDigital : public Bus { static uint16_t _milliAmpsTotal; // is overwitten/recalculated on each show() - inline uint32_t restoreColorLossy(uint32_t c, uint8_t restoreBri) { + inline uint32_t restoreColorLossy(uint32_t c, uint8_t restoreBri) const { if (restoreBri < 255) { uint8_t* chan = (uint8_t*) &c; for (uint_fast8_t i=0; i<4; i++) { @@ -251,6 +241,8 @@ class BusDigital : public Bus { } return c; } + + uint8_t estimateCurrentAndLimitBri(void); }; @@ -260,11 +252,13 @@ class BusPwm : public Bus { ~BusPwm() { cleanup(); } void setPixelColor(uint16_t pix, uint32_t c) override; - uint32_t getPixelColor(uint16_t pix) override; //does no index check - uint8_t getPins(uint8_t* pinArray) override; - uint16_t getFrequency() override { return _frequency; } - void show() override; - void cleanup() { deallocatePins(); } + uint32_t getPixelColor(uint16_t pix) const override; //does no index check + uint8_t getPins(uint8_t* pinArray = nullptr) const override; + uint16_t getFrequency(void) const override { return _frequency; } + void show(void) override; + void cleanup(void) { deallocatePins(); } + + static std::vector getLEDTypes(void); private: uint8_t _pins[5]; @@ -272,9 +266,10 @@ class BusPwm : public Bus { #ifdef ARDUINO_ARCH_ESP32 uint8_t _ledcStart; #endif + uint8_t _depth; uint16_t _frequency; - void deallocatePins(); + void deallocatePins(void); }; @@ -284,10 +279,12 @@ class BusOnOff : public Bus { ~BusOnOff() { cleanup(); } void setPixelColor(uint16_t pix, uint32_t c) override; - uint32_t getPixelColor(uint16_t pix) override; - uint8_t getPins(uint8_t* pinArray) override; - void show() override; - void cleanup() { pinManager.deallocatePin(_pin, PinOwner::BusOnOff); } + uint32_t getPixelColor(uint16_t pix) const override; + uint8_t getPins(uint8_t* pinArray) const override; + void show(void) override; + void cleanup(void) { pinManager.deallocatePin(_pin, PinOwner::BusOnOff); } + + static std::vector getLEDTypes(void); private: uint8_t _pin; @@ -300,20 +297,19 @@ class BusNetwork : public Bus { BusNetwork(BusConfig &bc); ~BusNetwork() { cleanup(); } - bool hasRGB() override { return true; } - bool hasWhite() override { return _rgbw; } - bool canShow() override { return !_broadcastLock; } // this should be a return value from UDP routine if it is still sending data out + bool canShow(void) const override { return !_broadcastLock; } // this should be a return value from UDP routine if it is still sending data out void setPixelColor(uint16_t pix, uint32_t c) override; - uint32_t getPixelColor(uint16_t pix) override; - uint8_t getPins(uint8_t* pinArray) override; - void show() override; - void cleanup(); + uint32_t getPixelColor(uint16_t pix) const override; + uint8_t getPins(uint8_t* pinArray = nullptr) const override; + void show(void) override; + void cleanup(void); + + static std::vector getLEDTypes(void); private: IPAddress _client; uint8_t _UDPtype; uint8_t _UDPchannels; - bool _rgbw; bool _broadcastLock; }; @@ -361,37 +357,93 @@ class BusHub75Matrix : public Bus { }; #endif +//temporary struct for passing bus configuration to bus +struct BusConfig { + uint8_t type; + uint16_t count; + uint16_t start; + uint8_t colorOrder; + bool reversed; + uint8_t skipAmount; + bool refreshReq; + uint8_t autoWhite; + uint8_t pins[5] = {255, 255, 255, 255, 255}; + uint16_t frequency; + bool doubleBuffer; + uint8_t milliAmpsPerLed; + uint16_t milliAmpsMax; + + BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U, bool dblBfr=false, uint8_t maPerLed=LED_MILLIAMPS_DEFAULT, uint16_t maMax=ABL_MILLIAMPS_DEFAULT) + : count(len) + , start(pstart) + , colorOrder(pcolorOrder) + , reversed(rev) + , skipAmount(skip) + , autoWhite(aw) + , frequency(clock_kHz) + , doubleBuffer(dblBfr) + , milliAmpsPerLed(maPerLed) + , milliAmpsMax(maMax) + { + refreshReq = (bool) GET_BIT(busType,7); + type = busType & 0x7F; // bit 7 may be/is hacked to include refresh info (1=refresh in off state, 0=no refresh) + size_t nPins = Bus::getNumberOfPins(type); + for (size_t i = 0; i < nPins; i++) pins[i] = ppins[i]; + } + + //validates start and length and extends total if needed + bool adjustBounds(uint16_t& total) { + if (!count) count = 1; + if (count > MAX_LEDS_PER_BUS) count = MAX_LEDS_PER_BUS; + if (start >= MAX_LEDS) return false; + //limit length of strip if it would exceed total permissible LEDs + if (start + count > MAX_LEDS) count = MAX_LEDS - start; + //extend total count accordingly + if (start + count > total) total = start + count; + return true; + } +}; + + class BusManager { public: BusManager() {}; //utility to get the approx. memory usage of a given BusConfig static uint32_t memUsage(BusConfig &bc); + static uint32_t memUsage(unsigned channels, unsigned count, unsigned buses = 1); static uint16_t currentMilliamps(void) { return _milliAmpsUsed; } static uint16_t ablMilliampsMax(void) { return _milliAmpsMax; } static int add(BusConfig &bc); + static void useParallelOutput(void); // workaround for inaccessible PolyBus //do not call this method from system context (network callback) - static void removeAll(); + static void removeAll(void); - static void show(); - static bool canAllShow(); + static void on(void); + static void off(void); + + static void show(void); + static bool canAllShow(void); static void setStatusPixel(uint32_t c); static void setPixelColor(uint16_t pix, uint32_t c); static void setBrightness(uint8_t b); + // for setSegmentCCT(), cct can only be in [-1,255] range; allowWBCorrection will convert it to K + // WARNING: setSegmentCCT() is a misleading name!!! much better would be setGlobalCCT() or just setCCT() static void setSegmentCCT(int16_t cct, bool allowWBCorrection = false); - static void setMilliampsMax(uint16_t max) { _milliAmpsMax = max;} + static inline void setMilliampsMax(uint16_t max) { _milliAmpsMax = max;} static uint32_t getPixelColor(uint16_t pix); + static inline int16_t getSegmentCCT(void) { return Bus::getCCT(); } static Bus* getBus(uint8_t busNr); //semi-duplicate of strip.getLengthTotal() (though that just returns strip._length, calculated in finalizeInit()) - static uint16_t getTotalLength(); - static uint8_t getNumBusses() { return numBusses; } + static uint16_t getTotalLength(void); + static inline uint8_t getNumBusses(void) { return numBusses; } + static String getLEDTypesJSONString(void); - static void updateColorOrderMap(const ColorOrderMap &com) { memcpy(&colorOrderMap, &com, sizeof(ColorOrderMap)); } - static const ColorOrderMap& getColorOrderMap() { return colorOrderMap; } + static inline ColorOrderMap& getColorOrderMap(void) { return colorOrderMap; } private: static uint8_t numBusses; @@ -399,10 +451,14 @@ class BusManager { static ColorOrderMap colorOrderMap; static uint16_t _milliAmpsUsed; static uint16_t _milliAmpsMax; + static uint8_t _parallelOutputs; - static uint8_t getNumVirtualBusses() { + #ifdef ESP32_DATA_IDLE_HIGH + static void esp32RMTInvertIdle(void) ; + #endif + static uint8_t getNumVirtualBusses(void) { int j = 0; - for (int i=0; igetType() >= TYPE_NET_DDP_RGB && busses[i]->getType() < 96) j++; + for (int i=0; iisVirtual()) j++; return j; } }; diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index c63e055a..bf2d30c0 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -2,6 +2,7 @@ #define BusWrapper_h #include "NeoPixelBusLg.h" +#include "bus_manager.h" // temporary - these defines should actually be set in platformio.ini // C3: I2S0 and I2S1 methods not supported (has one I2S bus) @@ -63,52 +64,81 @@ #define I_8266_U1_UCS_4 54 #define I_8266_DM_UCS_4 55 #define I_8266_BB_UCS_4 56 +//FW1906 GRBCW +#define I_8266_U0_FW6_5 66 +#define I_8266_U1_FW6_5 67 +#define I_8266_DM_FW6_5 68 +#define I_8266_BB_FW6_5 69 //ESP8266 APA106 #define I_8266_U0_APA106_3 81 #define I_8266_U1_APA106_3 82 #define I_8266_DM_APA106_3 83 #define I_8266_BB_APA106_3 84 +//WS2805 (RGBCW) +#define I_8266_U0_2805_5 89 +#define I_8266_U1_2805_5 90 +#define I_8266_DM_2805_5 91 +#define I_8266_BB_2805_5 92 +//TM1914 (RGB) +#define I_8266_U0_TM1914_3 99 +#define I_8266_U1_TM1914_3 100 +#define I_8266_DM_TM1914_3 101 +#define I_8266_BB_TM1914_3 102 +//SM16825 (RGBCW) +#define I_8266_U0_SM16825_5 103 +#define I_8266_U1_SM16825_5 104 +#define I_8266_DM_SM16825_5 105 +#define I_8266_BB_SM16825_5 106 /*** ESP32 Neopixel methods ***/ //RGB #define I_32_RN_NEO_3 21 #define I_32_I0_NEO_3 22 #define I_32_I1_NEO_3 23 -#define I_32_BB_NEO_3 24 // bitbanging on ESP32 not recommended //RGBW #define I_32_RN_NEO_4 25 #define I_32_I0_NEO_4 26 #define I_32_I1_NEO_4 27 -#define I_32_BB_NEO_4 28 // bitbanging on ESP32 not recommended //400Kbps #define I_32_RN_400_3 29 #define I_32_I0_400_3 30 #define I_32_I1_400_3 31 -#define I_32_BB_400_3 32 // bitbanging on ESP32 not recommended //TM1814 (RGBW) #define I_32_RN_TM1_4 33 #define I_32_I0_TM1_4 34 #define I_32_I1_TM1_4 35 -//Bit Bang theoratically possible, but very undesirable and not needed (no pin restrictions on RMT and I2S) //TM1829 (RGB) #define I_32_RN_TM2_3 36 #define I_32_I0_TM2_3 37 #define I_32_I1_TM2_3 38 -//Bit Bang theoratically possible, but very undesirable and not needed (no pin restrictions on RMT and I2S) //UCS8903 (RGB) #define I_32_RN_UCS_3 57 #define I_32_I0_UCS_3 58 #define I_32_I1_UCS_3 59 -//Bit Bang theoratically possible, but very undesirable and not needed (no pin restrictions on RMT and I2S) //UCS8904 (RGBW) #define I_32_RN_UCS_4 60 #define I_32_I0_UCS_4 61 #define I_32_I1_UCS_4 62 -//Bit Bang theoratically possible, but very undesirable and not needed (no pin restrictions on RMT and I2S) +//FW1906 GRBCW +#define I_32_RN_FW6_5 63 +#define I_32_I0_FW6_5 64 +#define I_32_I1_FW6_5 65 +//APA106 #define I_32_RN_APA106_3 85 #define I_32_I0_APA106_3 86 #define I_32_I1_APA106_3 87 -#define I_32_BB_APA106_3 88 // bitbangging on ESP32 not recommended +//WS2805 (RGBCW) +#define I_32_RN_2805_5 93 +#define I_32_I0_2805_5 94 +#define I_32_I1_2805_5 95 +//TM1914 (RGB) +#define I_32_RN_TM1914_3 96 +#define I_32_I0_TM1914_3 97 +#define I_32_I1_TM1914_3 98 +//SM16825 (RGBCW) +#define I_32_RN_SM16825_5 107 +#define I_32_I0_SM16825_5 108 +#define I_32_I1_SM16825_5 109 //APA102 #define I_HS_DOT_3 39 //hardware SPI @@ -157,10 +187,10 @@ #define B_8266_DM_TM1_4 NeoPixelBusLg #define B_8266_BB_TM1_4 NeoPixelBusLg //TM1829 (RGB) -#define B_8266_U0_TM2_4 NeoPixelBusLg -#define B_8266_U1_TM2_4 NeoPixelBusLg -#define B_8266_DM_TM2_4 NeoPixelBusLg -#define B_8266_BB_TM2_4 NeoPixelBusLg +#define B_8266_U0_TM2_3 NeoPixelBusLg +#define B_8266_U1_TM2_3 NeoPixelBusLg +#define B_8266_DM_TM2_3 NeoPixelBusLg +#define B_8266_BB_TM2_3 NeoPixelBusLg //UCS8903 #define B_8266_U0_UCS_3 NeoPixelBusLg //3 chan, esp8266, gpio1 #define B_8266_U1_UCS_3 NeoPixelBusLg //3 chan, esp8266, gpio2 @@ -176,82 +206,90 @@ #define B_8266_U1_APA106_3 NeoPixelBusLg //3 chan, esp8266, gpio2 #define B_8266_DM_APA106_3 NeoPixelBusLg //3 chan, esp8266, gpio3 #define B_8266_BB_APA106_3 NeoPixelBusLg //3 chan, esp8266, bb (any pin but 16) +//FW1906 GRBCW +#define B_8266_U0_FW6_5 NeoPixelBusLg //esp8266, gpio1 +#define B_8266_U1_FW6_5 NeoPixelBusLg //esp8266, gpio2 +#define B_8266_DM_FW6_5 NeoPixelBusLg //esp8266, gpio3 +#define B_8266_BB_FW6_5 NeoPixelBusLg //esp8266, bb +//WS2805 GRBCW +#define B_8266_U0_2805_5 NeoPixelBusLg //esp8266, gpio1 +#define B_8266_U1_2805_5 NeoPixelBusLg //esp8266, gpio2 +#define B_8266_DM_2805_5 NeoPixelBusLg //esp8266, gpio3 +#define B_8266_BB_2805_5 NeoPixelBusLg //esp8266, bb +//TM1914 (RGB) +#define B_8266_U0_TM1914_3 NeoPixelBusLg +#define B_8266_U1_TM1914_3 NeoPixelBusLg +#define B_8266_DM_TM1914_3 NeoPixelBusLg +#define B_8266_BB_TM1914_3 NeoPixelBusLg +//Sm16825 (RGBWC) +#define B_8266_U0_SM16825_5 NeoPixelBusLg +#define B_8266_U1_SM16825_5 NeoPixelBusLg +#define B_8266_DM_SM16825_5 NeoPixelBusLg +#define B_8266_BB_SM16825_5 NeoPixelBusLg #endif /*** ESP32 Neopixel methods ***/ #ifdef ARDUINO_ARCH_ESP32 //RGB #define B_32_RN_NEO_3 NeoPixelBusLg -#ifndef WLED_NO_I2S0_PIXELBUS -#define B_32_I0_NEO_3 NeoPixelBusLg -#endif -#ifndef WLED_NO_I2S1_PIXELBUS -#define B_32_I1_NEO_3 NeoPixelBusLg -#endif -//#define B_32_BB_NEO_3 NeoPixelBusLg // NeoEsp8266BitBang800KbpsMethod +#define B_32_I0_NEO_3 NeoPixelBusLg +#define B_32_I1_NEO_3 NeoPixelBusLg +#define B_32_I1_NEO_3P NeoPixelBusLg // parallel I2S //RGBW -#define B_32_RN_NEO_4 NeoPixelBusLg -#ifndef WLED_NO_I2S0_PIXELBUS -#define B_32_I0_NEO_4 NeoPixelBusLg -#endif -#ifndef WLED_NO_I2S1_PIXELBUS -#define B_32_I1_NEO_4 NeoPixelBusLg -#endif -//#define B_32_BB_NEO_4 NeoPixelBusLg // NeoEsp8266BitBang800KbpsMethod +#define B_32_RN_NEO_4 NeoPixelBusLg +#define B_32_I0_NEO_4 NeoPixelBusLg +#define B_32_I1_NEO_4 NeoPixelBusLg +#define B_32_I1_NEO_4P NeoPixelBusLg // parallel I2S //400Kbps #define B_32_RN_400_3 NeoPixelBusLg -#ifndef WLED_NO_I2S0_PIXELBUS #define B_32_I0_400_3 NeoPixelBusLg -#endif -#ifndef WLED_NO_I2S1_PIXELBUS #define B_32_I1_400_3 NeoPixelBusLg -#endif -//#define B_32_BB_400_3 NeoPixelBusLg // NeoEsp8266BitBang400KbpsMethod +#define B_32_I1_400_3P NeoPixelBusLg // parallel I2S //TM1814 (RGBW) #define B_32_RN_TM1_4 NeoPixelBusLg -#ifndef WLED_NO_I2S0_PIXELBUS #define B_32_I0_TM1_4 NeoPixelBusLg -#endif -#ifndef WLED_NO_I2S1_PIXELBUS #define B_32_I1_TM1_4 NeoPixelBusLg -#endif -//Bit Bang theoratically possible, but very undesirable and not needed (no pin restrictions on RMT and I2S) +#define B_32_I1_TM1_4P NeoPixelBusLg // parallel I2S //TM1829 (RGB) #define B_32_RN_TM2_3 NeoPixelBusLg -#ifndef WLED_NO_I2S0_PIXELBUS #define B_32_I0_TM2_3 NeoPixelBusLg -#endif -#ifndef WLED_NO_I2S1_PIXELBUS #define B_32_I1_TM2_3 NeoPixelBusLg -#endif -//Bit Bang theoratically possible, but very undesirable and not needed (no pin restrictions on RMT and I2S) +#define B_32_I1_TM2_3P NeoPixelBusLg // parallel I2S //UCS8903 #define B_32_RN_UCS_3 NeoPixelBusLg -#ifndef WLED_NO_I2S0_PIXELBUS #define B_32_I0_UCS_3 NeoPixelBusLg -#endif -#ifndef WLED_NO_I2S1_PIXELBUS #define B_32_I1_UCS_3 NeoPixelBusLg -#endif -//Bit Bang theoratically possible, but very undesirable and not needed (no pin restrictions on RMT and I2S) +#define B_32_I1_UCS_3P NeoPixelBusLg // parallel I2S //UCS8904 #define B_32_RN_UCS_4 NeoPixelBusLg -#ifndef WLED_NO_I2S0_PIXELBUS #define B_32_I0_UCS_4 NeoPixelBusLg -#endif -#ifndef WLED_NO_I2S1_PIXELBUS #define B_32_I1_UCS_4 NeoPixelBusLg -#endif -//Bit Bang theoratically possible, but very undesirable and not needed (no pin restrictions on RMT and I2S) +#define B_32_I1_UCS_4P NeoPixelBusLg// parallel I2S +//APA106 #define B_32_RN_APA106_3 NeoPixelBusLg -#ifndef WLED_NO_I2S0_PIXELBUS #define B_32_I0_APA106_3 NeoPixelBusLg -#endif -#ifndef WLED_NO_I2S1_PIXELBUS #define B_32_I1_APA106_3 NeoPixelBusLg -#endif -//#define B_32_BB_APA106_3 NeoPixelBusLg // NeoEsp8266BitBang800KbpsMethod - +#define B_32_I1_APA106_3P NeoPixelBusLg // parallel I2S +//FW1906 GRBCW +#define B_32_RN_FW6_5 NeoPixelBusLg +#define B_32_I0_FW6_5 NeoPixelBusLg +#define B_32_I1_FW6_5 NeoPixelBusLg +#define B_32_I1_FW6_5P NeoPixelBusLg // parallel I2S +//WS2805 RGBWC +#define B_32_RN_2805_5 NeoPixelBusLg +#define B_32_I0_2805_5 NeoPixelBusLg +#define B_32_I1_2805_5 NeoPixelBusLg +#define B_32_I1_2805_5P NeoPixelBusLg // parallel I2S +//TM1914 (RGB) +#define B_32_RN_TM1914_3 NeoPixelBusLg +#define B_32_I0_TM1914_3 NeoPixelBusLg +#define B_32_I1_TM1914_3 NeoPixelBusLg +#define B_32_I1_TM1914_3P NeoPixelBusLg // parallel I2S +//Sm16825 (RGBWC) +#define B_32_RN_SM16825_5 NeoPixelBusLg +#define B_32_I0_SM16825_5 NeoPixelBusLg +#define B_32_I1_SM16825_5 NeoPixelBusLg +#define B_32_I1_SM16825_5P NeoPixelBusLg // parallel I2S #endif //APA102 @@ -289,7 +327,13 @@ //handles pointer type conversion for all possible bus types class PolyBus { + private: + static bool useParallelI2S; + public: + static inline void setParallelI2S1Output(bool b = true) { useParallelI2S = b; } + static inline bool isParallelI2S1Output(void) { return useParallelI2S; } + // initialize SPI bus speed for DotStar methods template static void beginDotStar(void* busPtr, int8_t sck, int8_t miso, int8_t mosi, int8_t ss, uint16_t clock_kHz = 0U) { @@ -312,6 +356,13 @@ class PolyBus { tm1814_strip->SetPixelSettings(NeoTm1814Settings(/*R*/225, /*G*/225, /*B*/225, /*W*/225)); } + template + static void beginTM1914(void* busPtr) { + T tm1914_strip = static_cast(busPtr); + tm1914_strip->Begin(); + tm1914_strip->SetPixelSettings(NeoTm1914Settings()); //NeoTm1914_Mode_DinFdinAutoSwitch, NeoTm1914_Mode_DinOnly, NeoTm1914_Mode_FdinOnly + } + static void begin(void* busPtr, uint8_t busType, uint8_t* pins, uint16_t clock_kHz = 0U) { switch (busType) { case I_NONE: break; @@ -332,10 +383,10 @@ class PolyBus { case I_8266_U1_TM1_4: beginTM1814(busPtr); break; case I_8266_DM_TM1_4: beginTM1814(busPtr); break; case I_8266_BB_TM1_4: beginTM1814(busPtr); break; - case I_8266_U0_TM2_3: (static_cast(busPtr))->Begin(); break; - case I_8266_U1_TM2_3: (static_cast(busPtr))->Begin(); break; - case I_8266_DM_TM2_3: (static_cast(busPtr))->Begin(); break; - case I_8266_BB_TM2_3: (static_cast(busPtr))->Begin(); break; + case I_8266_U0_TM2_3: (static_cast(busPtr))->Begin(); break; + case I_8266_U1_TM2_3: (static_cast(busPtr))->Begin(); break; + case I_8266_DM_TM2_3: (static_cast(busPtr))->Begin(); break; + case I_8266_BB_TM2_3: (static_cast(busPtr))->Begin(); break; case I_HS_DOT_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; case I_HS_LPD_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; case I_HS_LPO_3: beginDotStar(busPtr, -1, -1, -1, -1, clock_kHz); break; @@ -353,66 +404,67 @@ class PolyBus { case I_8266_U1_APA106_3: (static_cast(busPtr))->Begin(); break; case I_8266_DM_APA106_3: (static_cast(busPtr))->Begin(); break; case I_8266_BB_APA106_3: (static_cast(busPtr))->Begin(); break; + case I_8266_U0_FW6_5: (static_cast(busPtr))->Begin(); break; + case I_8266_U1_FW6_5: (static_cast(busPtr))->Begin(); break; + case I_8266_DM_FW6_5: (static_cast(busPtr))->Begin(); break; + case I_8266_BB_FW6_5: (static_cast(busPtr))->Begin(); break; + case I_8266_U0_2805_5: (static_cast(busPtr))->Begin(); break; + case I_8266_U1_2805_5: (static_cast(busPtr))->Begin(); break; + case I_8266_DM_2805_5: (static_cast(busPtr))->Begin(); break; + case I_8266_BB_2805_5: (static_cast(busPtr))->Begin(); break; + case I_8266_U0_TM1914_3: beginTM1914(busPtr); break; + case I_8266_U1_TM1914_3: beginTM1914(busPtr); break; + case I_8266_DM_TM1914_3: beginTM1914(busPtr); break; + case I_8266_BB_TM1914_3: beginTM1914(busPtr); break; + case I_8266_U0_SM16825_5: (static_cast(busPtr))->Begin(); break; + case I_8266_U1_SM16825_5: (static_cast(busPtr))->Begin(); break; + case I_8266_DM_SM16825_5: (static_cast(busPtr))->Begin(); break; + case I_8266_BB_SM16825_5: (static_cast(busPtr))->Begin(); break; #endif #ifdef ARDUINO_ARCH_ESP32 + // RMT buses case I_32_RN_NEO_3: (static_cast(busPtr))->Begin(); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: (static_cast(busPtr))->Begin(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: (static_cast(busPtr))->Begin(); break; - #endif -// case I_32_BB_NEO_3: (static_cast(busPtr))->Begin(); break; case I_32_RN_NEO_4: (static_cast(busPtr))->Begin(); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_4: (static_cast(busPtr))->Begin(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_4: (static_cast(busPtr))->Begin(); break; - #endif -// case I_32_BB_NEO_4: (static_cast(busPtr))->Begin(); break; case I_32_RN_400_3: (static_cast(busPtr))->Begin(); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_400_3: (static_cast(busPtr))->Begin(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_400_3: (static_cast(busPtr))->Begin(); break; - #endif -// case I_32_BB_400_3: (static_cast(busPtr))->Begin(); break; case I_32_RN_TM1_4: beginTM1814(busPtr); break; case I_32_RN_TM2_3: (static_cast(busPtr))->Begin(); break; + case I_32_RN_UCS_3: (static_cast(busPtr))->Begin(); break; + case I_32_RN_UCS_4: (static_cast(busPtr))->Begin(); break; + case I_32_RN_FW6_5: (static_cast(busPtr))->Begin(); break; + case I_32_RN_APA106_3: (static_cast(busPtr))->Begin(); break; + case I_32_RN_2805_5: (static_cast(busPtr))->Begin(); break; + case I_32_RN_TM1914_3: beginTM1914(busPtr); break; + case I_32_RN_SM16825_5: (static_cast(busPtr))->Begin(); break; + // I2S1 bus or parellel buses + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I1_NEO_4: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I1_400_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I1_TM1_4: if (useParallelI2S) beginTM1814(busPtr); else beginTM1814(busPtr); break; + case I_32_I1_TM2_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I1_UCS_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I1_UCS_4: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I1_APA106_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I1_TM1914_3: if (useParallelI2S) beginTM1914(busPtr); else beginTM1914(busPtr); break; + case I_32_I1_SM16825_5: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + #endif + // I2S0 bus #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_3: (static_cast(busPtr))->Begin(); break; + case I_32_I0_NEO_4: (static_cast(busPtr))->Begin(); break; + case I_32_I0_400_3: (static_cast(busPtr))->Begin(); break; case I_32_I0_TM1_4: beginTM1814(busPtr); break; case I_32_I0_TM2_3: (static_cast(busPtr))->Begin(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_TM1_4: beginTM1814(busPtr); break; - case I_32_I1_TM2_3: (static_cast(busPtr))->Begin(); break; - #endif - case I_32_RN_UCS_3: (static_cast(busPtr))->Begin(); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_3: (static_cast(busPtr))->Begin(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_3: (static_cast(busPtr))->Begin(); break; - #endif -// case I_32_BB_UCS_3: (static_cast(busPtr))->Begin(); break; - case I_32_RN_UCS_4: (static_cast(busPtr))->Begin(); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_4: (static_cast(busPtr))->Begin(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_4: (static_cast(busPtr))->Begin(); break; - #endif -// case I_32_BB_UCS_4: (static_cast(busPtr))->Begin(); break; - case I_32_RN_APA106_3: (static_cast(busPtr))->Begin(); break; - #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_FW6_5: (static_cast(busPtr))->Begin(); break; case I_32_I0_APA106_3: (static_cast(busPtr))->Begin(); break; + case I_32_I0_2805_5: (static_cast(busPtr))->Begin(); break; + case I_32_I0_TM1914_3: beginTM1914(busPtr); break; + case I_32_I0_SM16825_5: (static_cast(busPtr))->Begin(); break; #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_APA106_3: (static_cast(busPtr))->Begin(); break; - #endif -// case I_32_BB_APA106_3: (static_cast(busPtr))->Begin(); break; // ESP32 can (and should, to avoid inadvertantly driving the chip select signal) specify the pins used for SPI, but only in begin() case I_HS_DOT_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; case I_HS_LPD_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; @@ -429,6 +481,12 @@ class PolyBus { } static void* create(uint8_t busType, uint8_t* pins, uint16_t len, uint8_t channel, uint16_t clock_kHz = 0U) { + #if defined(ARDUINO_ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3)) + // NOTE: "channel" is only used on ESP32 (and its variants) for RMT channel allocation + // since 0.15.0-b3 I2S1 is favoured for classic ESP32 and moved to position 0 (channel 0) so we need to subtract 1 for correct RMT allocation + if (useParallelI2S && channel > 7) channel -= 8; // accommodate parallel I2S1 which is used 1st on classic ESP32 + else if (channel > 0) channel--; // accommodate I2S1 which is used as 1st bus on classic ESP32 + #endif void* busPtr = nullptr; switch (busType) { case I_NONE: break; @@ -449,10 +507,10 @@ class PolyBus { case I_8266_U1_TM1_4: busPtr = new B_8266_U1_TM1_4(len, pins[0]); break; case I_8266_DM_TM1_4: busPtr = new B_8266_DM_TM1_4(len, pins[0]); break; case I_8266_BB_TM1_4: busPtr = new B_8266_BB_TM1_4(len, pins[0]); break; - case I_8266_U0_TM2_3: busPtr = new B_8266_U0_TM2_4(len, pins[0]); break; - case I_8266_U1_TM2_3: busPtr = new B_8266_U1_TM2_4(len, pins[0]); break; - case I_8266_DM_TM2_3: busPtr = new B_8266_DM_TM2_4(len, pins[0]); break; - case I_8266_BB_TM2_3: busPtr = new B_8266_BB_TM2_4(len, pins[0]); break; + case I_8266_U0_TM2_3: busPtr = new B_8266_U0_TM2_3(len, pins[0]); break; + case I_8266_U1_TM2_3: busPtr = new B_8266_U1_TM2_3(len, pins[0]); break; + case I_8266_DM_TM2_3: busPtr = new B_8266_DM_TM2_3(len, pins[0]); break; + case I_8266_BB_TM2_3: busPtr = new B_8266_BB_TM2_3(len, pins[0]); break; case I_8266_U0_UCS_3: busPtr = new B_8266_U0_UCS_3(len, pins[0]); break; case I_8266_U1_UCS_3: busPtr = new B_8266_U1_UCS_3(len, pins[0]); break; case I_8266_DM_UCS_3: busPtr = new B_8266_DM_UCS_3(len, pins[0]); break; @@ -465,66 +523,67 @@ class PolyBus { case I_8266_U1_APA106_3: busPtr = new B_8266_U1_APA106_3(len, pins[0]); break; case I_8266_DM_APA106_3: busPtr = new B_8266_DM_APA106_3(len, pins[0]); break; case I_8266_BB_APA106_3: busPtr = new B_8266_BB_APA106_3(len, pins[0]); break; + case I_8266_U0_FW6_5: busPtr = new B_8266_U0_FW6_5(len, pins[0]); break; + case I_8266_U1_FW6_5: busPtr = new B_8266_U1_FW6_5(len, pins[0]); break; + case I_8266_DM_FW6_5: busPtr = new B_8266_DM_FW6_5(len, pins[0]); break; + case I_8266_BB_FW6_5: busPtr = new B_8266_BB_FW6_5(len, pins[0]); break; + case I_8266_U0_2805_5: busPtr = new B_8266_U0_2805_5(len, pins[0]); break; + case I_8266_U1_2805_5: busPtr = new B_8266_U1_2805_5(len, pins[0]); break; + case I_8266_DM_2805_5: busPtr = new B_8266_DM_2805_5(len, pins[0]); break; + case I_8266_BB_2805_5: busPtr = new B_8266_BB_2805_5(len, pins[0]); break; + case I_8266_U0_TM1914_3: busPtr = new B_8266_U0_TM1914_3(len, pins[0]); break; + case I_8266_U1_TM1914_3: busPtr = new B_8266_U1_TM1914_3(len, pins[0]); break; + case I_8266_DM_TM1914_3: busPtr = new B_8266_DM_TM1914_3(len, pins[0]); break; + case I_8266_BB_TM1914_3: busPtr = new B_8266_BB_TM1914_3(len, pins[0]); break; + case I_8266_U0_SM16825_5: busPtr = new B_8266_U0_SM16825_5(len, pins[0]); break; + case I_8266_U1_SM16825_5: busPtr = new B_8266_U1_SM16825_5(len, pins[0]); break; + case I_8266_DM_SM16825_5: busPtr = new B_8266_DM_SM16825_5(len, pins[0]); break; + case I_8266_BB_SM16825_5: busPtr = new B_8266_BB_SM16825_5(len, pins[0]); break; #endif #ifdef ARDUINO_ARCH_ESP32 + // RMT buses case I_32_RN_NEO_3: busPtr = new B_32_RN_NEO_3(len, pins[0], (NeoBusChannel)channel); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: busPtr = new B_32_I0_NEO_3(len, pins[0]); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: busPtr = new B_32_I1_NEO_3(len, pins[0]); break; - #endif -// case I_32_BB_NEO_3: busPtr = new B_32_BB_NEO_3(len, pins[0], (NeoBusChannel)channel); break; case I_32_RN_NEO_4: busPtr = new B_32_RN_NEO_4(len, pins[0], (NeoBusChannel)channel); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_4: busPtr = new B_32_I0_NEO_4(len, pins[0]); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_4: busPtr = new B_32_I1_NEO_4(len, pins[0]); break; - #endif -// case I_32_BB_NEO_4: busPtr = new B_32_BB_NEO_4(len, pins[0], (NeoBusChannel)channel); break; case I_32_RN_400_3: busPtr = new B_32_RN_400_3(len, pins[0], (NeoBusChannel)channel); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_400_3: busPtr = new B_32_I0_400_3(len, pins[0]); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_400_3: busPtr = new B_32_I1_400_3(len, pins[0]); break; - #endif -// case I_32_BB_400_3: busPtr = new B_32_BB_400_3(len, pins[0], (NeoBusChannel)channel); break; case I_32_RN_TM1_4: busPtr = new B_32_RN_TM1_4(len, pins[0], (NeoBusChannel)channel); break; case I_32_RN_TM2_3: busPtr = new B_32_RN_TM2_3(len, pins[0], (NeoBusChannel)channel); break; + case I_32_RN_UCS_3: busPtr = new B_32_RN_UCS_3(len, pins[0], (NeoBusChannel)channel); break; + case I_32_RN_UCS_4: busPtr = new B_32_RN_UCS_4(len, pins[0], (NeoBusChannel)channel); break; + case I_32_RN_APA106_3: busPtr = new B_32_RN_APA106_3(len, pins[0], (NeoBusChannel)channel); break; + case I_32_RN_FW6_5: busPtr = new B_32_RN_FW6_5(len, pins[0], (NeoBusChannel)channel); break; + case I_32_RN_2805_5: busPtr = new B_32_RN_2805_5(len, pins[0], (NeoBusChannel)channel); break; + case I_32_RN_TM1914_3: busPtr = new B_32_RN_TM1914_3(len, pins[0], (NeoBusChannel)channel); break; + case I_32_RN_SM16825_5: busPtr = new B_32_RN_SM16825_5(len, pins[0], (NeoBusChannel)channel); break; + // I2S1 bus or paralell buses + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_NEO_3: if (useParallelI2S) busPtr = new B_32_I1_NEO_3P(len, pins[0]); else busPtr = new B_32_I1_NEO_3(len, pins[0]); break; + case I_32_I1_NEO_4: if (useParallelI2S) busPtr = new B_32_I1_NEO_4P(len, pins[0]); else busPtr = new B_32_I1_NEO_4(len, pins[0]); break; + case I_32_I1_400_3: if (useParallelI2S) busPtr = new B_32_I1_400_3P(len, pins[0]); else busPtr = new B_32_I1_400_3(len, pins[0]); break; + case I_32_I1_TM1_4: if (useParallelI2S) busPtr = new B_32_I1_TM1_4P(len, pins[0]); else busPtr = new B_32_I1_TM1_4(len, pins[0]); break; + case I_32_I1_TM2_3: if (useParallelI2S) busPtr = new B_32_I1_TM2_3P(len, pins[0]); else busPtr = new B_32_I1_TM2_3(len, pins[0]); break; + case I_32_I1_UCS_3: if (useParallelI2S) busPtr = new B_32_I1_UCS_3P(len, pins[0]); else busPtr = new B_32_I1_UCS_3(len, pins[0]); break; + case I_32_I1_UCS_4: if (useParallelI2S) busPtr = new B_32_I1_UCS_4P(len, pins[0]); else busPtr = new B_32_I1_UCS_4(len, pins[0]); break; + case I_32_I1_APA106_3: if (useParallelI2S) busPtr = new B_32_I1_APA106_3P(len, pins[0]); else busPtr = new B_32_I1_APA106_3(len, pins[0]); break; + case I_32_I1_FW6_5: if (useParallelI2S) busPtr = new B_32_I1_FW6_5P(len, pins[0]); else busPtr = new B_32_I1_FW6_5(len, pins[0]); break; + case I_32_I1_2805_5: if (useParallelI2S) busPtr = new B_32_I1_2805_5P(len, pins[0]); else busPtr = new B_32_I1_2805_5(len, pins[0]); break; + case I_32_I1_TM1914_3: if (useParallelI2S) busPtr = new B_32_I1_TM1914_3P(len, pins[0]); else busPtr = new B_32_I1_TM1914_3(len, pins[0]); break; + case I_32_I1_SM16825_5: if (useParallelI2S) busPtr = new B_32_I1_SM16825_5P(len, pins[0]); else busPtr = new B_32_I1_SM16825_5(len, pins[0]); break; + #endif + // I2S0 bus #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_3: busPtr = new B_32_I0_NEO_3(len, pins[0]); break; + case I_32_I0_NEO_4: busPtr = new B_32_I0_NEO_4(len, pins[0]); break; + case I_32_I0_400_3: busPtr = new B_32_I0_400_3(len, pins[0]); break; case I_32_I0_TM1_4: busPtr = new B_32_I0_TM1_4(len, pins[0]); break; case I_32_I0_TM2_3: busPtr = new B_32_I0_TM2_3(len, pins[0]); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_TM1_4: busPtr = new B_32_I1_TM1_4(len, pins[0]); break; - case I_32_I1_TM2_3: busPtr = new B_32_I1_TM2_3(len, pins[0]); break; - #endif - case I_32_RN_UCS_3: busPtr = new B_32_RN_UCS_3(len, pins[0], (NeoBusChannel)channel); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_3: busPtr = new B_32_I0_UCS_3(len, pins[0]); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_3: busPtr = new B_32_I1_UCS_3(len, pins[0]); break; - #endif -// case I_32_BB_UCS_3: busPtr = new B_32_BB_UCS_3(len, pins[0], (NeoBusChannel)channel); break; - case I_32_RN_UCS_4: busPtr = new B_32_RN_UCS_4(len, pins[0], (NeoBusChannel)channel); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_4: busPtr = new B_32_I0_UCS_4(len, pins[0]); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_4: busPtr = new B_32_I1_UCS_4(len, pins[0]); break; - #endif -// case I_32_BB_UCS_4: busPtr = new B_32_BB_UCS_4(len, pins[0], (NeoBusChannel)channel); break; - case I_32_RN_APA106_3: busPtr = new B_32_RN_APA106_3(len, pins[0], (NeoBusChannel)channel); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_APA106_3: busPtr = new B_32_I0_APA106_3(len, pins[0]); break; + case I_32_I0_FW6_5: busPtr = new B_32_I0_FW6_5(len, pins[0]); break; + case I_32_I0_2805_5: busPtr = new B_32_I0_2805_5(len, pins[0]); break; + case I_32_I0_TM1914_3: busPtr = new B_32_I0_TM1914_3(len, pins[0]); break; + case I_32_I0_SM16825_5: busPtr = new B_32_I0_SM16825_5(len, pins[0]); break; #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_APA106_3: busPtr = new B_32_I1_APA106_3(len, pins[0]); break; - #endif -// case I_32_BB_APA106_3: busPtr = new B_32_BB_APA106_3(len, pins[0], (NeoBusChannel)channel); break; #endif // for 2-wire: pins[1] is clk, pins[0] is dat. begin expects (len, clk, dat) case I_HS_DOT_3: busPtr = new B_HS_DOT_3(len, pins[1], pins[0]); break; @@ -562,10 +621,10 @@ class PolyBus { case I_8266_U1_TM1_4: (static_cast(busPtr))->Show(consistent); break; case I_8266_DM_TM1_4: (static_cast(busPtr))->Show(consistent); break; case I_8266_BB_TM1_4: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U0_TM2_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_U1_TM2_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_DM_TM2_3: (static_cast(busPtr))->Show(consistent); break; - case I_8266_BB_TM2_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U0_TM2_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U1_TM2_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_DM_TM2_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_BB_TM2_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_U0_UCS_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_U1_UCS_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_DM_UCS_3: (static_cast(busPtr))->Show(consistent); break; @@ -578,66 +637,67 @@ class PolyBus { case I_8266_U1_APA106_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_DM_APA106_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_BB_APA106_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U0_FW6_5: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U1_FW6_5: (static_cast(busPtr))->Show(consistent); break; + case I_8266_DM_FW6_5: (static_cast(busPtr))->Show(consistent); break; + case I_8266_BB_FW6_5: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U0_2805_5: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U1_2805_5: (static_cast(busPtr))->Show(consistent); break; + case I_8266_DM_2805_5: (static_cast(busPtr))->Show(consistent); break; + case I_8266_BB_2805_5: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U0_TM1914_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U1_TM1914_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_DM_TM1914_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_BB_TM1914_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U0_SM16825_5: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U1_SM16825_5: (static_cast(busPtr))->Show(consistent); break; + case I_8266_DM_SM16825_5: (static_cast(busPtr))->Show(consistent); break; + case I_8266_BB_SM16825_5: (static_cast(busPtr))->Show(consistent); break; #endif #ifdef ARDUINO_ARCH_ESP32 + // RMT buses case I_32_RN_NEO_3: (static_cast(busPtr))->Show(consistent); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: (static_cast(busPtr))->Show(consistent); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: (static_cast(busPtr))->Show(consistent); break; - #endif -// case I_32_BB_NEO_3: (static_cast(busPtr))->Show(consistent); break; case I_32_RN_NEO_4: (static_cast(busPtr))->Show(consistent); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_4: (static_cast(busPtr))->Show(consistent); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_4: (static_cast(busPtr))->Show(consistent); break; - #endif -// case I_32_BB_NEO_4: (static_cast(busPtr))->Show(consistent); break; case I_32_RN_400_3: (static_cast(busPtr))->Show(consistent); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_400_3: (static_cast(busPtr))->Show(consistent); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_400_3: (static_cast(busPtr))->Show(consistent); break; - #endif -// case I_32_BB_400_3: (static_cast(busPtr))->Show(consistent); break; case I_32_RN_TM1_4: (static_cast(busPtr))->Show(consistent); break; case I_32_RN_TM2_3: (static_cast(busPtr))->Show(consistent); break; + case I_32_RN_UCS_3: (static_cast(busPtr))->Show(consistent); break; + case I_32_RN_UCS_4: (static_cast(busPtr))->Show(consistent); break; + case I_32_RN_APA106_3: (static_cast(busPtr))->Show(consistent); break; + case I_32_RN_FW6_5: (static_cast(busPtr))->Show(consistent); break; + case I_32_RN_2805_5: (static_cast(busPtr))->Show(consistent); break; + case I_32_RN_TM1914_3: (static_cast(busPtr))->Show(consistent); break; + case I_32_RN_SM16825_5: (static_cast(busPtr))->Show(consistent); break; + // I2S1 bus or paralell buses + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I1_NEO_4: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I1_400_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I1_TM1_4: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I1_TM2_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I1_UCS_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I1_UCS_4: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I1_APA106_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I1_TM1914_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I1_SM16825_5: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + #endif + // I2S0 bus #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_3: (static_cast(busPtr))->Show(consistent); break; + case I_32_I0_NEO_4: (static_cast(busPtr))->Show(consistent); break; + case I_32_I0_400_3: (static_cast(busPtr))->Show(consistent); break; case I_32_I0_TM1_4: (static_cast(busPtr))->Show(consistent); break; case I_32_I0_TM2_3: (static_cast(busPtr))->Show(consistent); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_TM1_4: (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_TM2_3: (static_cast(busPtr))->Show(consistent); break; - #endif - case I_32_RN_UCS_3: (static_cast(busPtr))->Show(consistent); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_3: (static_cast(busPtr))->Show(consistent); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_3: (static_cast(busPtr))->Show(consistent); break; - #endif -// case I_32_BB_UCS_3: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_UCS_4: (static_cast(busPtr))->Show(consistent); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_4: (static_cast(busPtr))->Show(consistent); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_4: (static_cast(busPtr))->Show(consistent); break; - #endif -// case I_32_BB_UCS_4: (static_cast(busPtr))->Show(consistent); break; - case I_32_RN_APA106_3: (static_cast(busPtr))->Show(consistent); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_APA106_3: (static_cast(busPtr))->Show(consistent); break; + case I_32_I0_FW6_5: (static_cast(busPtr))->Show(consistent); break; + case I_32_I0_2805_5: (static_cast(busPtr))->Show(consistent); break; + case I_32_I0_TM1914_3: (static_cast(busPtr))->Show(consistent); break; + case I_32_I0_SM16825_5: (static_cast(busPtr))->Show(consistent); break; #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_APA106_3: (static_cast(busPtr))->Show(consistent); break; - #endif -// case I_32_BB_APA106_3: (static_cast(busPtr))->Show(consistent); break; #endif case I_HS_DOT_3: (static_cast(busPtr))->Show(consistent); break; case I_SS_DOT_3: (static_cast(busPtr))->Show(consistent); break; @@ -672,10 +732,10 @@ class PolyBus { case I_8266_U1_TM1_4: return (static_cast(busPtr))->CanShow(); break; case I_8266_DM_TM1_4: return (static_cast(busPtr))->CanShow(); break; case I_8266_BB_TM1_4: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U0_TM2_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_U1_TM2_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_DM_TM2_3: return (static_cast(busPtr))->CanShow(); break; - case I_8266_BB_TM2_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U0_TM2_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U1_TM2_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_DM_TM2_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_BB_TM2_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_U0_UCS_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_U1_UCS_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_DM_UCS_3: return (static_cast(busPtr))->CanShow(); break; @@ -687,66 +747,67 @@ class PolyBus { case I_8266_U1_APA106_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_DM_APA106_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_BB_APA106_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U0_FW6_5: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U1_FW6_5: return (static_cast(busPtr))->CanShow(); break; + case I_8266_DM_FW6_5: return (static_cast(busPtr))->CanShow(); break; + case I_8266_BB_FW6_5: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U0_2805_5: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U1_2805_5: return (static_cast(busPtr))->CanShow(); break; + case I_8266_DM_2805_5: return (static_cast(busPtr))->CanShow(); break; + case I_8266_BB_2805_5: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U0_TM1914_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U1_TM1914_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_DM_TM1914_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_BB_TM1914_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U0_SM16825_5: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U1_SM16825_5: return (static_cast(busPtr))->CanShow(); break; + case I_8266_DM_SM16825_5: return (static_cast(busPtr))->CanShow(); break; + case I_8266_BB_SM16825_5: return (static_cast(busPtr))->CanShow(); break; #endif #ifdef ARDUINO_ARCH_ESP32 - case I_32_RN_NEO_3: return (static_cast(busPtr))->CanShow(); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: return (static_cast(busPtr))->CanShow(); break; - #endif + // RMT buses + case I_32_RN_NEO_3: (static_cast(busPtr))->CanShow(); break; + case I_32_RN_NEO_4: (static_cast(busPtr))->CanShow(); break; + case I_32_RN_400_3: (static_cast(busPtr))->CanShow(); break; + case I_32_RN_TM1_4: (static_cast(busPtr))->CanShow(); break; + case I_32_RN_TM2_3: (static_cast(busPtr))->CanShow(); break; + case I_32_RN_UCS_3: (static_cast(busPtr))->CanShow(); break; + case I_32_RN_UCS_4: (static_cast(busPtr))->CanShow(); break; + case I_32_RN_APA106_3: (static_cast(busPtr))->CanShow(); break; + case I_32_RN_FW6_5: (static_cast(busPtr))->CanShow(); break; + case I_32_RN_2805_5: (static_cast(busPtr))->CanShow(); break; + case I_32_RN_TM1914_3: (static_cast(busPtr))->CanShow(); break; + case I_32_RN_SM16825_5: (static_cast(busPtr))->CanShow(); break; + // I2S1 bus or paralell buses #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: return (static_cast(busPtr))->CanShow(); break; + case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; + case I_32_I1_NEO_4: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; + case I_32_I1_400_3: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; + case I_32_I1_TM1_4: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; + case I_32_I1_TM2_3: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; + case I_32_I1_UCS_3: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; + case I_32_I1_UCS_4: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; + case I_32_I1_APA106_3: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; + case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; + case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; + case I_32_I1_TM1914_3: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; + case I_32_I1_SM16825_5: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; #endif -// case I_32_BB_NEO_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_NEO_4: return (static_cast(busPtr))->CanShow(); break; + // I2S0 bus #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_4: return (static_cast(busPtr))->CanShow(); break; + case I_32_I0_NEO_3: (static_cast(busPtr))->CanShow(); break; + case I_32_I0_NEO_4: (static_cast(busPtr))->CanShow(); break; + case I_32_I0_400_3: (static_cast(busPtr))->CanShow(); break; + case I_32_I0_TM1_4: (static_cast(busPtr))->CanShow(); break; + case I_32_I0_TM2_3: (static_cast(busPtr))->CanShow(); break; + case I_32_I0_UCS_3: (static_cast(busPtr))->CanShow(); break; + case I_32_I0_UCS_4: (static_cast(busPtr))->CanShow(); break; + case I_32_I0_APA106_3: (static_cast(busPtr))->CanShow(); break; + case I_32_I0_FW6_5: (static_cast(busPtr))->CanShow(); break; + case I_32_I0_2805_5: (static_cast(busPtr))->CanShow(); break; + case I_32_I0_TM1914_3: (static_cast(busPtr))->CanShow(); break; + case I_32_I0_SM16825_5: (static_cast(busPtr))->CanShow(); break; #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_4: return (static_cast(busPtr))->CanShow(); break; - #endif -// case I_32_BB_NEO_4: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_400_3: return (static_cast(busPtr))->CanShow(); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_400_3: return (static_cast(busPtr))->CanShow(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_400_3: return (static_cast(busPtr))->CanShow(); break; - #endif -// case I_32_BB_400_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_TM1_4: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_TM2_3: return (static_cast(busPtr))->CanShow(); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_TM1_4: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_TM2_3: return (static_cast(busPtr))->CanShow(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_TM1_4: return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_TM2_3: return (static_cast(busPtr))->CanShow(); break; - #endif - case I_32_RN_UCS_3: return (static_cast(busPtr))->CanShow(); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_UCS_3: return (static_cast(busPtr))->CanShow(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_3: return (static_cast(busPtr))->CanShow(); break; - #endif -// case I_32_BB_UCS_3: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_UCS_4: return (static_cast(busPtr))->CanShow(); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_UCS_4: return (static_cast(busPtr))->CanShow(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_4: return (static_cast(busPtr))->CanShow(); break; - #endif -// case I_32_BB_UCS_4: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_APA106_3: return (static_cast(busPtr))->CanShow(); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_APA106_3: return (static_cast(busPtr))->CanShow(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_APA106_3: return (static_cast(busPtr))->CanShow(); break; - #endif -// case I_32_BB_APA106_3: return (static_cast(busPtr))->CanShow(); break; #endif case I_HS_DOT_3: return (static_cast(busPtr))->CanShow(); break; case I_SS_DOT_3: return (static_cast(busPtr))->CanShow(); break; @@ -762,12 +823,13 @@ class PolyBus { return true; } - static void setPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint32_t c, uint8_t co) { + static void setPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint32_t c, uint8_t co, uint16_t wwcw = 0) { uint8_t r = c >> 16; uint8_t g = c >> 8; uint8_t b = c >> 0; uint8_t w = c >> 24; RgbwColor col; + uint8_t cctWW = wwcw & 0xFF, cctCW = (wwcw>>8) & 0xFF; // reorder channels to selected order switch (co & 0x0F) { @@ -784,6 +846,7 @@ class PolyBus { case 1: col.W = col.B; col.B = w; break; // swap W & B case 2: col.W = col.G; col.G = w; break; // swap W & G case 3: col.W = col.R; col.R = w; break; // swap W & R + case 4: std::swap(cctWW, cctCW); break; // swap WW & CW } switch (busType) { @@ -805,10 +868,10 @@ class PolyBus { case I_8266_U1_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_8266_DM_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_8266_BB_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_8266_U0_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_U1_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_DM_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_8266_BB_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_U0_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_U1_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_DM_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_BB_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_U0_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; case I_8266_U1_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; case I_8266_DM_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; @@ -821,66 +884,67 @@ class PolyBus { case I_8266_U1_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_DM_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_BB_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_U0_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_8266_U1_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_8266_DM_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_8266_BB_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_8266_U0_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_8266_U1_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_8266_DM_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_8266_BB_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_8266_U0_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_U1_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_DM_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_BB_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_U0_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; + case I_8266_U1_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; + case I_8266_DM_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; + case I_8266_BB_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; #endif #ifdef ARDUINO_ARCH_ESP32 + // RMT buses case I_32_RN_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - #endif -// case I_32_BB_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_32_RN_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - #endif -// case I_32_BB_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_32_RN_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - #endif -// case I_32_BB_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(colB)); break; case I_32_RN_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_32_RN_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_RN_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; + case I_32_RN_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; + case I_32_RN_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_RN_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_32_RN_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_32_RN_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_RN_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; + // I2S1 bus or paralell buses + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_I1_NEO_4: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, col); break; + case I_32_I1_400_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_I1_TM1_4: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, col); break; + case I_32_I1_TM2_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_I1_UCS_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; + case I_32_I1_UCS_4: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; + case I_32_I1_APA106_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); else (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); else (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_32_I1_TM1914_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_I1_SM16825_5: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); else (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; + #endif + // I2S0 bus #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_I0_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; + case I_32_I0_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_32_I0_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_32_I0_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_32_I1_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - #endif - case I_32_RN_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - #endif -// case I_32_BB_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - case I_32_RN_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - #endif -// case I_32_BB_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - case I_32_RN_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_I0_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_32_I0_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_32_I0_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_I0_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - #endif -// case I_32_BB_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; #endif case I_HS_DOT_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_SS_DOT_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; @@ -915,10 +979,10 @@ class PolyBus { case I_8266_U1_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; case I_8266_DM_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; case I_8266_BB_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U0_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_U1_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_DM_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_8266_BB_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U0_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U1_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_DM_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_BB_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; case I_8266_U0_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; case I_8266_U1_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; case I_8266_DM_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; @@ -931,66 +995,67 @@ class PolyBus { case I_8266_U1_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; case I_8266_DM_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; case I_8266_BB_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U0_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U1_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_DM_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_BB_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U0_2805_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U1_2805_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_DM_2805_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_BB_2805_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U0_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U1_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_DM_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_BB_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U0_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U1_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_DM_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_BB_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; #endif #ifdef ARDUINO_ARCH_ESP32 + // RMT buses case I_32_RN_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; - #endif -// case I_32_BB_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; case I_32_RN_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; - #endif -// case I_32_BB_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; case I_32_RN_400_3: (static_cast(busPtr))->SetLuminance(b); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_400_3: (static_cast(busPtr))->SetLuminance(b); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_400_3: (static_cast(busPtr))->SetLuminance(b); break; - #endif -// case I_32_BB_400_3: (static_cast(busPtr))->SetLuminance(b); break; case I_32_RN_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; case I_32_RN_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_RN_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_RN_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_RN_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_RN_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_RN_2805_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_RN_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_RN_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; + // I2S1 bus or paralell buses + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_NEO_4: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_400_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_TM1_4: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_TM2_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_UCS_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_UCS_4: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_APA106_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_TM1914_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_SM16825_5: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + #endif + // I2S0 bus #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I0_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I0_400_3: (static_cast(busPtr))->SetLuminance(b); break; case I_32_I0_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; case I_32_I0_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; - #endif - case I_32_RN_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; - #endif -// case I_32_BB_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; - #endif -// case I_32_BB_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_RN_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I0_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I0_2805_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I0_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I0_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; - #endif -// case I_32_BB_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; #endif case I_HS_DOT_3: (static_cast(busPtr))->SetLuminance(b); break; case I_SS_DOT_3: (static_cast(busPtr))->SetLuminance(b); break; @@ -1026,10 +1091,10 @@ class PolyBus { case I_8266_U1_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_DM_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_BB_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U0_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_U1_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_DM_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_8266_BB_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_U0_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_U1_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_DM_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_BB_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_U0_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; case I_8266_U1_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; case I_8266_DM_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; @@ -1042,66 +1107,67 @@ class PolyBus { case I_8266_U1_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_DM_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_BB_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_U0_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_8266_U1_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_8266_DM_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_8266_BB_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_8266_U0_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_8266_U1_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_8266_DM_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_8266_BB_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_8266_U0_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_U1_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_DM_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_BB_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_U0_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_8266_U1_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_8266_DM_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_8266_BB_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W #endif #ifdef ARDUINO_ARCH_ESP32 + // RMT buses case I_32_RN_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #endif -// case I_32_BB_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_RN_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #endif -// case I_32_BB_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_RN_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #endif -// case I_32_BB_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_RN_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_RN_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_RN_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; + case I_32_RN_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; + case I_32_RN_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_RN_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_32_RN_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_32_RN_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_RN_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,max(c.WW,c.CW)/257); } break; // will not return original W + // I2S1 bus or paralell buses + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_NEO_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I1_NEO_4: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I1_400_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I1_TM1_4: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I1_TM2_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I1_UCS_3: { Rgb48Color c = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,0); } break; + case I_32_I1_UCS_4: { Rgbw64Color c = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,c.W/257); } break; + case I_32_I1_APA106_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I1_FW6_5: { RgbwwColor c = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_32_I1_2805_5: { RgbwwColor c = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_32_I1_TM1914_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I1_SM16825_5: { Rgbww80Color c = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,max(c.WW,c.CW)/257); } break; // will not return original W + #endif + // I2S0 bus #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I0_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I0_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_I0_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_I0_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I1_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #endif - case I_32_RN_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; - #endif -// case I_32_BB_UCS_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_RN_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; - #endif -// case I_32_BB_UCS_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_RN_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,0); } break; + case I_32_I0_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,c.W/257); } break; case I_32_I0_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I0_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_32_I0_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_32_I0_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I0_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,max(c.WW,c.CW)/257); } break; // will not return original W #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #endif -// case I_32_BB_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; #endif case I_HS_DOT_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_SS_DOT_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; @@ -1155,10 +1221,10 @@ class PolyBus { case I_8266_U1_TM1_4: delete (static_cast(busPtr)); break; case I_8266_DM_TM1_4: delete (static_cast(busPtr)); break; case I_8266_BB_TM1_4: delete (static_cast(busPtr)); break; - case I_8266_U0_TM2_3: delete (static_cast(busPtr)); break; - case I_8266_U1_TM2_3: delete (static_cast(busPtr)); break; - case I_8266_DM_TM2_3: delete (static_cast(busPtr)); break; - case I_8266_BB_TM2_3: delete (static_cast(busPtr)); break; + case I_8266_U0_TM2_3: delete (static_cast(busPtr)); break; + case I_8266_U1_TM2_3: delete (static_cast(busPtr)); break; + case I_8266_DM_TM2_3: delete (static_cast(busPtr)); break; + case I_8266_BB_TM2_3: delete (static_cast(busPtr)); break; case I_8266_U0_UCS_3: delete (static_cast(busPtr)); break; case I_8266_U1_UCS_3: delete (static_cast(busPtr)); break; case I_8266_DM_UCS_3: delete (static_cast(busPtr)); break; @@ -1171,66 +1237,67 @@ class PolyBus { case I_8266_U1_APA106_3: delete (static_cast(busPtr)); break; case I_8266_DM_APA106_3: delete (static_cast(busPtr)); break; case I_8266_BB_APA106_3: delete (static_cast(busPtr)); break; + case I_8266_U0_FW6_5: delete (static_cast(busPtr)); break; + case I_8266_U1_FW6_5: delete (static_cast(busPtr)); break; + case I_8266_DM_FW6_5: delete (static_cast(busPtr)); break; + case I_8266_BB_FW6_5: delete (static_cast(busPtr)); break; + case I_8266_U0_2805_5: delete (static_cast(busPtr)); break; + case I_8266_U1_2805_5: delete (static_cast(busPtr)); break; + case I_8266_DM_2805_5: delete (static_cast(busPtr)); break; + case I_8266_BB_2805_5: delete (static_cast(busPtr)); break; + case I_8266_U0_TM1914_3: delete (static_cast(busPtr)); break; + case I_8266_U1_TM1914_3: delete (static_cast(busPtr)); break; + case I_8266_DM_TM1914_3: delete (static_cast(busPtr)); break; + case I_8266_BB_TM1914_3: delete (static_cast(busPtr)); break; + case I_8266_U0_SM16825_5: delete (static_cast(busPtr)); break; + case I_8266_U1_SM16825_5: delete (static_cast(busPtr)); break; + case I_8266_DM_SM16825_5: delete (static_cast(busPtr)); break; + case I_8266_BB_SM16825_5: delete (static_cast(busPtr)); break; #endif #ifdef ARDUINO_ARCH_ESP32 + // RMT buses case I_32_RN_NEO_3: delete (static_cast(busPtr)); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: delete (static_cast(busPtr)); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: delete (static_cast(busPtr)); break; - #endif -// case I_32_BB_NEO_3: delete (static_cast(busPtr)); break; case I_32_RN_NEO_4: delete (static_cast(busPtr)); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_4: delete (static_cast(busPtr)); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_4: delete (static_cast(busPtr)); break; - #endif -// case I_32_BB_NEO_4: delete (static_cast(busPtr)); break; case I_32_RN_400_3: delete (static_cast(busPtr)); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_400_3: delete (static_cast(busPtr)); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_400_3: delete (static_cast(busPtr)); break; - #endif -// case I_32_BB_400_3: delete (static_cast(busPtr)); break; case I_32_RN_TM1_4: delete (static_cast(busPtr)); break; case I_32_RN_TM2_3: delete (static_cast(busPtr)); break; + case I_32_RN_UCS_3: delete (static_cast(busPtr)); break; + case I_32_RN_UCS_4: delete (static_cast(busPtr)); break; + case I_32_RN_APA106_3: delete (static_cast(busPtr)); break; + case I_32_RN_FW6_5: delete (static_cast(busPtr)); break; + case I_32_RN_2805_5: delete (static_cast(busPtr)); break; + case I_32_RN_TM1914_3: delete (static_cast(busPtr)); break; + case I_32_RN_SM16825_5: delete (static_cast(busPtr)); break; + // I2S1 bus or paralell buses + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_NEO_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I1_NEO_4: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I1_400_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I1_TM1_4: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I1_TM2_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I1_UCS_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I1_UCS_4: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I1_APA106_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I1_FW6_5: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I1_2805_5: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I1_TM1914_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I1_SM16825_5: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + #endif + // I2S0 bus #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_3: delete (static_cast(busPtr)); break; + case I_32_I0_NEO_4: delete (static_cast(busPtr)); break; + case I_32_I0_400_3: delete (static_cast(busPtr)); break; case I_32_I0_TM1_4: delete (static_cast(busPtr)); break; case I_32_I0_TM2_3: delete (static_cast(busPtr)); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_TM1_4: delete (static_cast(busPtr)); break; - case I_32_I1_TM2_3: delete (static_cast(busPtr)); break; - #endif - case I_32_RN_UCS_3: delete (static_cast(busPtr)); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_3: delete (static_cast(busPtr)); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_3: delete (static_cast(busPtr)); break; - #endif -// case I_32_BB_UCS_3: delete (static_cast(busPtr)); break; - case I_32_RN_UCS_4: delete (static_cast(busPtr)); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_4: delete (static_cast(busPtr)); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_4: delete (static_cast(busPtr)); break; - #endif -// case I_32_BB_UCS_4: delete (static_cast(busPtr)); break; - case I_32_RN_APA106_3: delete (static_cast(busPtr)); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_APA106_3: delete (static_cast(busPtr)); break; + case I_32_I0_FW6_5: delete (static_cast(busPtr)); break; + case I_32_I0_2805_5: delete (static_cast(busPtr)); break; + case I_32_I0_TM1914_3: delete (static_cast(busPtr)); break; + case I_32_I0_SM16825_5: delete (static_cast(busPtr)); break; #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_APA106_3: delete (static_cast(busPtr)); break; - #endif -// case I_32_BB_APA106_3: delete (static_cast(busPtr)); break; #endif case I_HS_DOT_3: delete (static_cast(busPtr)); break; case I_SS_DOT_3: delete (static_cast(busPtr)); break; @@ -1247,8 +1314,8 @@ class PolyBus { //gives back the internal type index (I_XX_XXX_X above) for the input static uint8_t getI(uint8_t busType, uint8_t* pins, uint8_t num = 0) { - if (!IS_DIGITAL(busType)) return I_NONE; - if (IS_2PIN(busType)) { //SPI LED chips + if (!Bus::isDigital(busType)) return I_NONE; + if (Bus::is2Pin(busType)) { //SPI LED chips bool isHSPI = false; #ifdef ESP8266 if (pins[0] == P_8266_HS_MOSI && pins[1] == P_8266_HS_CLK) isHSPI = true; @@ -1292,13 +1359,21 @@ class PolyBus { return I_8266_U0_UCS_4 + offset; case TYPE_APA106: return I_8266_U0_APA106_3 + offset; + case TYPE_FW1906: + return I_8266_U0_FW6_5 + offset; + case TYPE_WS2805: + return I_8266_U0_2805_5 + offset; + case TYPE_TM1914: + return I_8266_U0_TM1914_3 + offset; + case TYPE_SM16825: + return I_8266_U0_SM16825_5 + offset; } #else //ESP32 - uint8_t offset = 0; //0 = RMT (num 0-7) 8 = I2S0 9 = I2S1 + uint8_t offset = 0; // 0 = RMT (num 1-8), 1 = I2S0 (used by Audioreactive), 2 = I2S1 #if defined(CONFIG_IDF_TARGET_ESP32S2) // ESP32-S2 only has 4 RMT channels if (num > 4) return I_NONE; - if (num > 3) offset = 1; // only one I2S + if (num > 3) offset = 1; // only one I2S (use last to allow Audioreactive) #elif defined(CONFIG_IDF_TARGET_ESP32C3) // On ESP32-C3 only the first 2 RMT channels are usable for transmitting if (num > 1) return I_NONE; @@ -1309,8 +1384,15 @@ class PolyBus { //if (num > 3) offset = num -4; // I2S not supported yet #else // standard ESP32 has 8 RMT and 2 I2S channels - if (num > 9) return I_NONE; - if (num > 7) offset = num -7; + if (useParallelI2S) { + if (num > 16) return I_NONE; + if (num < 8) offset = 2; // prefer 8 parallel I2S1 channels + if (num == 16) offset = 1; + } else { + if (num > 9) return I_NONE; + if (num > 8) offset = 1; + if (num == 0) offset = 2; // prefer I2S1 for 1st bus (less flickering but more RAM needed) + } #endif switch (busType) { case TYPE_WS2812_1CH_X3: @@ -1332,11 +1414,18 @@ class PolyBus { return I_32_RN_UCS_4 + offset; case TYPE_APA106: return I_32_RN_APA106_3 + offset; + case TYPE_FW1906: + return I_32_RN_FW6_5 + offset; + case TYPE_WS2805: + return I_32_RN_2805_5 + offset; + case TYPE_TM1914: + return I_32_RN_TM1914_3 + offset; + case TYPE_SM16825: + return I_32_RN_SM16825_5 + offset; } #endif } return I_NONE; } }; - -#endif \ No newline at end of file +#endif diff --git a/wled00/button.cpp b/wled00/button.cpp index 29cb0abe..8b366e05 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -7,11 +7,13 @@ #define WLED_DEBOUNCE_THRESHOLD 50 // only consider button input of at least 50ms as valid (debouncing) #define WLED_LONG_PRESS 600 // long press if button is released after held for at least 600ms #define WLED_DOUBLE_PRESS 350 // double press if another press within 350ms after a short press -#define WLED_LONG_REPEATED_ACTION 300 // how often a repeated action (e.g. dimming) is fired on long press on button IDs >0 +#define WLED_LONG_REPEATED_ACTION 400 // how often a repeated action (e.g. dimming) is fired on long press on button IDs >0 #define WLED_LONG_AP 5000 // how long button 0 needs to be held to activate WLED-AP #define WLED_LONG_FACTORY_RESET 10000 // how long button 0 needs to be held to trigger a factory reset +#define WLED_LONG_BRI_STEPS 16 // how much to increase/decrease the brightness with each long press repetition static const char _mqtt_topic_button[] PROGMEM = "%s/button/%d"; // optimize flash usage +static bool buttonBriDirection = false; // true: increase brightness, false: decrease brightness void shortPressAction(uint8_t b) { @@ -21,7 +23,6 @@ void shortPressAction(uint8_t b) case 1: ++effectCurrent %= strip.getModeCount(); stateChanged = true; colorUpdated(CALL_MODE_BUTTON); break; } } else { - unloadPlaylist(); // applying a preset unloads the playlist applyPreset(macroButton[b], CALL_MODE_BUTTON_PRESET); } @@ -40,10 +41,21 @@ void longPressAction(uint8_t b) if (!macroLongPress[b]) { switch (b) { case 0: setRandomColor(col); colorUpdated(CALL_MODE_BUTTON); break; - case 1: bri += 8; stateUpdated(CALL_MODE_BUTTON); buttonPressedTime[b] = millis(); break; // repeatable action + case 1: + if(buttonBriDirection) { + if (bri == 255) break; // avoid unnecessary updates to brightness + if (bri >= 255 - WLED_LONG_BRI_STEPS) bri = 255; + else bri += WLED_LONG_BRI_STEPS; + } else { + if (bri == 1) break; // avoid unnecessary updates to brightness + if (bri <= WLED_LONG_BRI_STEPS) bri = 1; + else bri -= WLED_LONG_BRI_STEPS; + } + stateUpdated(CALL_MODE_BUTTON); + buttonPressedTime[b] = millis(); + break; // repeatable action } } else { - unloadPlaylist(); // applying a preset unloads the playlist applyPreset(macroLongPress[b], CALL_MODE_BUTTON_PRESET); } @@ -65,7 +77,6 @@ void doublePressAction(uint8_t b) case 1: ++effectPalette %= strip.getPaletteCount(); colorUpdated(CALL_MODE_BUTTON); break; } } else { - unloadPlaylist(); // applying a preset unloads the playlist applyPreset(macroDoublePress[b], CALL_MODE_BUTTON_PRESET); } @@ -82,7 +93,7 @@ void doublePressAction(uint8_t b) bool isButtonPressed(uint8_t i) { if (btnPin[i]<0) return false; - uint8_t pin = btnPin[i]; + unsigned pin = btnPin[i]; switch (buttonType[i]) { case BTN_TYPE_NONE: @@ -99,9 +110,13 @@ bool isButtonPressed(uint8_t i) case BTN_TYPE_TOUCH: case BTN_TYPE_TOUCH_SWITCH: #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) - if (digitalPinToTouchChannel(btnPin[i]) >= 0 && touchRead(pin) <= touchThreshold) return true; + #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; + #endif #endif - break; + break; } return false; } @@ -156,20 +171,20 @@ void handleAnalog(uint8_t b) { static uint8_t oldRead[WLED_MAX_BUTTONS] = {0}; static float filteredReading[WLED_MAX_BUTTONS] = {0.0f}; - uint16_t rawReading; // raw value from analogRead, scaled to 12bit + unsigned rawReading; // raw value from analogRead, scaled to 12bit DEBUG_PRINT(F("Analog: Reading button ")); DEBUG_PRINTLN(b); #ifdef ESP8266 rawReading = analogRead(A0) << 2; // convert 10bit read to 12bit #else - if ((btnPin[b] < 0) || (digitalPinToAnalogChannel(btnPin[b]) < 0)) return; // pin must support analog ADC - newer esp32 frameworks throw lots of warnings otherwise + if ((btnPin[b] < 0) /*|| (digitalPinToAnalogChannel(btnPin[b]) < 0)*/) return; // pin must support analog ADC - newer esp32 frameworks throw lots of warnings otherwise rawReading = analogRead(btnPin[b]); // collect at full 12bit resolution #endif yield(); // keep WiFi task running - analog read may take several millis on ESP8266 filteredReading[b] += POT_SMOOTHING * ((float(rawReading) / 16.0f) - filteredReading[b]); // filter raw input, and scale to [0..255] - uint16_t aRead = max(min(int(filteredReading[b]), 255), 0); // squash into 8bit + unsigned aRead = max(min(int(filteredReading[b]), 255), 0); // squash into 8bit if(aRead <= POT_SENSITIVITY) aRead = 0; // make sure that 0 and 255 are used if(aRead >= 255-POT_SENSITIVITY) aRead = 255; @@ -245,7 +260,7 @@ void handleButton() if (strip.isUpdating() && (now - lastRun < ANALOG_BTN_READ_CYCLE+1)) return; // don't interfere with strip update (unless strip is updating continuously, e.g. very long strips) lastRun = now; - for (uint8_t b=0; b WLED_LONG_PRESS) { //long press - if (!buttonLongPressed[b]) longPressAction(b); - else if (b) { //repeatable action (~3 times per s) on button > 0 + if (!buttonLongPressed[b]) { + buttonBriDirection = !buttonBriDirection; //toggle brightness direction on long press longPressAction(b); - buttonPressedTime[b] = now - WLED_LONG_REPEATED_ACTION; //333ms + } else if (b) { //repeatable action (~5 times per s) on button > 0 + longPressAction(b); + buttonPressedTime[b] = now - WLED_LONG_REPEATED_ACTION; //200ms } buttonLongPressed[b] = true; } - } else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released + } else if (buttonPressedBefore[b]) { //released long dur = now - buttonPressedTime[b]; // released after rising-edge short press action @@ -341,68 +358,39 @@ void handleButton() } } -// If enabled, RMT idle level is set to HIGH when off -// to prevent leakage current when using an N-channel MOSFET to toggle LED power -#ifdef ESP32_DATA_IDLE_HIGH -void esp32RMTInvertIdle() -{ - bool idle_out; - for (uint8_t u = 0; u < BusManager::getNumBusses(); u++) - { - if (u > 7) return; // only 8 RMT channels, TODO: ESP32 variants have less RMT channels - Bus *bus = BusManager::getBus(u); - if (!bus || bus->getLength()==0 || !IS_DIGITAL(bus->getType()) || IS_2PIN(bus->getType())) continue; - //assumes that bus number to rmt channel mapping stays 1:1 - rmt_channel_t ch = static_cast(u); - rmt_idle_level_t lvl; - rmt_get_idle_level(ch, &idle_out, &lvl); - if (lvl == RMT_IDLE_LEVEL_HIGH) lvl = RMT_IDLE_LEVEL_LOW; - else if (lvl == RMT_IDLE_LEVEL_LOW) lvl = RMT_IDLE_LEVEL_HIGH; - else continue; - rmt_set_idle_level(ch, idle_out, lvl); - } -} -#endif - +// handleIO() happens *after* handleTransitions() (see wled.cpp) which may change bri/briT but *before* strip.service() +// where actual LED painting occurrs +// this is important for relay control and in the event of turning off on-board LED void handleIO() { handleButton(); - //set relay when LEDs turn on - if (strip.getBrightness()) - { + // if we want to control on-board LED (ESP8266) or relay we have to do it here as the final show() may not happen until + // next loop() cycle + if (strip.getBrightness()) { lastOnTime = millis(); - if (offMode) - { - #ifdef ESP32_DATA_IDLE_HIGH - esp32RMTInvertIdle(); - #endif + if (offMode) { + BusManager::on(); if (rlyPin>=0) { - pinMode(rlyPin, OUTPUT); + pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT); digitalWrite(rlyPin, rlyMde); } offMode = false; } - } else if (millis() - lastOnTime > 600) - { + } else if (millis() - lastOnTime > 600 && !strip.needsUpdate()) { + // for turning LED or relay off we need to wait until strip no longer needs updates (strip.trigger()) if (!offMode) { - #ifdef ESP8266 - // turn off built-in LED if strip is turned off - // this will break digital bus so will need to be re-initialised on On - PinOwner ledPinOwner = pinManager.getPinOwner(LED_BUILTIN); - if (!strip.isOffRefreshRequired() && (ledPinOwner == PinOwner::None || ledPinOwner == PinOwner::BusDigital)) { - pinMode(LED_BUILTIN, OUTPUT); - digitalWrite(LED_BUILTIN, HIGH); - } - #endif - #ifdef ESP32_DATA_IDLE_HIGH - esp32RMTInvertIdle(); - #endif + BusManager::off(); if (rlyPin>=0) { - pinMode(rlyPin, OUTPUT); + pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT); digitalWrite(rlyPin, !rlyMde); } + offMode = true; } - offMode = true; } } + +void IRAM_ATTR touchButtonISR() +{ + // used for ESP32 S2 and S3: nothing to do, ISR is just used to update registers of HAL driver +} diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp old mode 100755 new mode 100644 index 771ada25..5f0c8593 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -20,17 +20,19 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { //long vid = doc[F("vid")]; // 2010020 - #ifdef WLED_USE_ETHERNET +#ifdef WLED_USE_ETHERNET JsonObject ethernet = doc[F("eth")]; CJSON(ethernetType, ethernet["type"]); // NOTE: Ethernet configuration takes priority over other use of pins WLED::instance().initEthernet(); - #endif +#endif JsonObject id = doc["id"]; getStringFromJson(cmDNS, id[F("mdns")], 33); getStringFromJson(serverDescription, id[F("name")], 33); +#ifndef WLED_DISABLE_ALEXA getStringFromJson(alexaInvocationName, id[F("inv")], 33); +#endif CJSON(simplifiedUI, id[F("sui")]); JsonObject nw = doc["nw"]; @@ -79,25 +81,26 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { getStringFromJson(apSSID, ap[F("ssid")], 33); getStringFromJson(apPass, ap["psk"] , 65); //normally not present due to security //int ap_pskl = ap[F("pskl")]; - CJSON(apChannel, ap[F("chan")]); if (apChannel > 13 || apChannel < 1) apChannel = 1; - CJSON(apHide, ap[F("hide")]); if (apHide > 1) apHide = 1; - CJSON(apBehavior, ap[F("behav")]); - /* JsonArray ap_ip = ap["ip"]; - for (byte i = 0; i < 4; i++) { + for (unsigned i = 0; i < 4; i++) { apIP[i] = ap_ip; } */ - noWifiSleep = doc[F("wifi")][F("sleep")] | !noWifiSleep; // inverted - noWifiSleep = !noWifiSleep; - force802_3g = doc[F("wifi")][F("phy")] | force802_3g; //force phy mode g? + JsonObject wifi = doc[F("wifi")]; + noWifiSleep = !(wifi[F("sleep")] | !noWifiSleep); // inverted + //noWifiSleep = !noWifiSleep; + CJSON(force802_3g, wifi[F("phy")]); //force phy mode g? +#ifdef ARDUINO_ARCH_ESP32 + CJSON(txPower, wifi[F("txpwr")]); + txPower = min(max((int)txPower, (int)WIFI_POWER_2dBm), (int)WIFI_POWER_19_5dBm); +#endif JsonObject hw = doc[F("hw")]; @@ -108,8 +111,9 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { uint16_t ablMilliampsMax = hw_led[F("maxpwr")] | BusManager::ablMilliampsMax(); BusManager::setMilliampsMax(ablMilliampsMax); Bus::setGlobalAWMode(hw_led[F("rgbwm")] | AW_GLOBAL_DISABLED); - CJSON(correctWB, hw_led["cct"]); - CJSON(cctFromRgb, hw_led[F("cr")]); + 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); strip.setTargetFps(hw_led["fps"]); //NOP if 0, default 42 FPS @@ -123,7 +127,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(strip.panels, matrix[F("mpc")]); strip.panel.clear(); JsonArray panels = matrix[F("panels")]; - uint8_t s = 0; + int s = 0; if (!panels.isNull()) { strip.panel.reserve(max(1U,min((size_t)strip.panels,(size_t)WLED_MAX_PANELS))); // pre-allocate memory for panels for (JsonObject pnl : panels) { @@ -155,23 +159,49 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonArray ins = hw_led["ins"]; if (fromFS || !ins.isNull()) { - uint8_t s = 0; // bus iterator + DEBUG_PRINTF_P(PSTR("Heap before buses: %d\n"), ESP.getFreeHeap()); + int s = 0; // bus iterator if (fromFS) BusManager::removeAll(); // can't safely manipulate busses directly in network callback - uint32_t mem = 0, globalBufMem = 0; - uint16_t maxlen = 0; - bool busesChanged = false; + unsigned mem = 0; + + // determine if it is sensible to use parallel I2S outputs on ESP32 (i.e. more than 5 outputs = 1 I2S + 4 RMT) + bool useParallel = false; + #if defined(ARDUINO_ARCH_ESP32) && !defined(ARDUINO_ARCH_ESP32S2) && !defined(ARDUINO_ARCH_ESP32S3) && !defined(ARDUINO_ARCH_ESP32C3) + unsigned digitalCount = 0; + unsigned maxLedsOnBus = 0; + unsigned maxChannels = 0; + for (JsonObject elm : ins) { + unsigned type = elm["type"] | TYPE_WS2812_RGB; + unsigned len = elm["len"] | DEFAULT_LED_COUNT; + if (!Bus::isDigital(type)) continue; + if (!Bus::is2Pin(type)) { + digitalCount++; + unsigned channels = Bus::getNumberOfChannels(type); + if (len > maxLedsOnBus) maxLedsOnBus = len; + if (channels > maxChannels) maxChannels = channels; + } + } + DEBUG_PRINTF_P(PSTR("Maximum LEDs on a bus: %u\nDigital buses: %u\n"), maxLedsOnBus, digitalCount); + // we may remove 300 LEDs per bus limit when NeoPixelBus is updated beyond 2.9.0 + if (maxLedsOnBus <= 300 && digitalCount > 5) { + DEBUG_PRINTLN(F("Switching to parallel I2S.")); + useParallel = true; + BusManager::useParallelOutput(); + mem = BusManager::memUsage(maxChannels, maxLedsOnBus, 8); // use alternate memory calculation + } + #endif + for (JsonObject elm : ins) { if (s >= WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES) break; uint8_t pins[5] = {255, 255, 255, 255, 255}; JsonArray pinArr = elm["pin"]; if (pinArr.size() == 0) continue; - pins[0] = pinArr[0]; - uint8_t i = 0; + //pins[0] = pinArr[0]; + unsigned i = 0; for (int p : pinArr) { pins[i++] = p; if (i>4) break; } - uint16_t length = elm["len"] | 1; uint8_t colorOrder = (int)elm[F("order")]; // contains white channel swap option in upper nibble uint8_t skipFirst = elm[F("skip")]; @@ -180,62 +210,58 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { uint8_t ledType = elm["type"] | TYPE_WS2812_RGB; bool reversed = elm["rev"]; bool refresh = elm["ref"] | false; - uint16_t freqkHz = elm[F("freq")] | 0; // will be in kHz for DotStar and Hz for PWM (not yet implemented fully) + uint16_t freqkHz = elm[F("freq")] | 0; // will be in kHz for DotStar and Hz for PWM uint8_t AWmode = elm[F("rgbwm")] | RGBW_MODE_MANUAL_ONLY; - uint8_t maPerLed = elm[F("ledma")] | 55; + uint8_t maPerLed = elm[F("ledma")] | LED_MILLIAMPS_DEFAULT; uint16_t maMax = elm[F("maxpwr")] | (ablMilliampsMax * length) / total; // rough (incorrect?) per strip ABL calculation when no config exists // To disable brightness limiter we either set output max current to 0 or single LED current to 0 (we choose output max current) - if ((ledType > TYPE_TM1814 && ledType < TYPE_WS2801) || ledType >= TYPE_NET_DDP_RGB) { // analog and virtual + if (Bus::isPWM(ledType) || Bus::isOnOff(ledType) || Bus::isVirtual(ledType)) { // analog and virtual maPerLed = 0; maMax = 0; } ledType |= refresh << 7; // hack bit 7 to indicate strip requires off refresh if (fromFS) { BusConfig bc = BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, useGlobalLedBuffer, maPerLed, maMax); - mem += BusManager::memUsage(bc); - if (useGlobalLedBuffer && start + length > maxlen) { - maxlen = start + length; - globalBufMem = maxlen * 4; - } - if (mem + globalBufMem <= MAX_LED_MEMORY) if (BusManager::add(bc) == -1) break; // finalization will be done in WLED::beginStrip() + if (useParallel && s < 8) { + // if for some unexplained reason the above pre-calculation was wrong, update + unsigned memT = BusManager::memUsage(bc); // includes x8 memory allocation for parallel I2S + if (memT > mem) mem = memT; // if we have unequal LED count use the largest + } else + mem += BusManager::memUsage(bc); // includes global buffer + if (mem <= MAX_LED_MEMORY) if (BusManager::add(bc) == -1) break; // finalization will be done in WLED::beginStrip() } else { if (busConfigs[s] != nullptr) delete busConfigs[s]; busConfigs[s] = new BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, useGlobalLedBuffer, maPerLed, maMax); - busesChanged = true; + doInitBusses = true; // finalization done in beginStrip() } s++; } - doInitBusses = busesChanged; - // finalization done in beginStrip() + DEBUG_PRINTF_P(PSTR("LED buffer size: %uB\n"), mem); + DEBUG_PRINTF_P(PSTR("Heap after buses: %d\n"), ESP.getFreeHeap()); } if (hw_led["rev"]) BusManager::getBus(0)->setReversed(true); //set 0.11 global reversed setting for first bus // read color order map configuration JsonArray hw_com = hw[F("com")]; if (!hw_com.isNull()) { - ColorOrderMap com = {}; - uint8_t s = 0; for (JsonObject entry : hw_com) { - if (s > WLED_MAX_COLOR_ORDER_MAPPINGS) break; uint16_t start = entry["start"] | 0; uint16_t len = entry["len"] | 0; uint8_t colorOrder = (int)entry[F("order")]; - com.add(start, len, colorOrder); - s++; + if (!BusManager::getColorOrderMap().add(start, len, colorOrder)) break; } - BusManager::updateColorOrderMap(com); } // read multiple button configuration JsonObject btn_obj = hw["btn"]; + CJSON(touchThreshold, btn_obj[F("tt")]); bool pull = btn_obj[F("pull")] | (!disablePullUp); // if true, pullup is enabled disablePullUp = !pull; JsonArray hw_btn_ins = btn_obj["ins"]; if (!hw_btn_ins.isNull()) { - for (uint8_t b = 0; b < WLED_MAX_BUTTONS; b++) { // deallocate existing button pins - pinManager.deallocatePin(btnPin[b], PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button - } - uint8_t s = 0; + // deallocate existing button pins + for (unsigned b = 0; b < WLED_MAX_BUTTONS; b++) pinManager.deallocatePin(btnPin[b], PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button + unsigned s = 0; for (JsonObject btn : hw_btn_ins) { CJSON(buttonType[s], btn["type"]); int8_t pin = btn["pin"][0] | -1; @@ -243,14 +269,33 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { btnPin[s] = pin; #ifdef ARDUINO_ARCH_ESP32 // ESP32 only: check that analog button pin is a valid ADC gpio - if (((buttonType[s] == BTN_TYPE_ANALOG) || (buttonType[s] == BTN_TYPE_ANALOG_INVERTED)) && (digitalPinToAnalogChannel(btnPin[s]) < 0)) + if ((buttonType[s] == BTN_TYPE_ANALOG) || (buttonType[s] == BTN_TYPE_ANALOG_INVERTED)) { + if (digitalPinToAnalogChannel(btnPin[s]) < 0) { + // not an ADC analog pin + DEBUG_PRINT(F("PIN ALLOC error: GPIO")); DEBUG_PRINT(btnPin[s]); + DEBUG_PRINT(F("for analog button #")); DEBUG_PRINT(s); + DEBUG_PRINTLN(F(" is not an analog pin!")); + btnPin[s] = -1; + pinManager.deallocatePin(pin,PinOwner::Button); + } else { + analogReadResolution(12); // see #4040 + } + } + else if ((buttonType[s] == BTN_TYPE_TOUCH || buttonType[s] == BTN_TYPE_TOUCH_SWITCH)) { - // not an ADC analog pin - DEBUG_PRINT(F("PIN ALLOC error: GPIO")); DEBUG_PRINT(btnPin[s]); - DEBUG_PRINT(F("for analog button #")); DEBUG_PRINT(s); - DEBUG_PRINTLN(F(" is not an analog pin!")); - btnPin[s] = -1; - pinManager.deallocatePin(pin,PinOwner::Button); + if (digitalPinToTouchChannel(btnPin[s]) < 0) { + // not a touch pin + DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not an touch pin!\n"), btnPin[s], s); + btnPin[s] = -1; + pinManager.deallocatePin(pin,PinOwner::Button); + } + //if touch pin, enable the touch interrupt on ESP32 S2 & S3 + #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a function to check touch state but need to attach an interrupt to do so + else + { + touchAttachInterrupt(btnPin[s], touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000) + } + #endif } else #endif @@ -286,22 +331,32 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { // new install/missing configuration (button 0 has defaults) if (fromFS) { // relies upon only being called once with fromFS == true, which is currently true. - uint8_t s = 0; - if (pinManager.allocatePin(btnPin[0], false, PinOwner::Button)) { // initialized to #define value BTNPIN, or zero if not defined(!) - ++s; // do not clear default button if allocated successfully - } - for (; s= 0) { + if (disablePullUp) { + pinMode(btnPin[s], INPUT); + } else { + #ifdef ESP32 + pinMode(btnPin[s], buttonType[s]==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP); + #else + pinMode(btnPin[s], INPUT_PULLUP); + #endif + } + } macroButton[s] = 0; macroLongPress[s] = 0; macroDoublePress[s] = 0; } } } - CJSON(touchThreshold,btn_obj[F("tt")]); + CJSON(buttonPublishMqtt,btn_obj["mqtt"]); + #ifndef WLED_DISABLE_INFRARED int hw_ir_pin = hw["ir"]["pin"] | -2; // 4 if (hw_ir_pin > -2) { pinManager.deallocatePin(irPin, PinOwner::IR); @@ -312,15 +367,18 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { } } CJSON(irEnabled, hw["ir"]["type"]); + #endif CJSON(irApplyToAllSelected, hw["ir"]["sel"]); JsonObject relay = hw[F("relay")]; + + rlyOpenDrain = relay[F("odrain")] | rlyOpenDrain; int hw_relay_pin = relay["pin"] | -2; if (hw_relay_pin > -2) { pinManager.deallocatePin(rlyPin, PinOwner::Relay); if (pinManager.allocatePin(hw_relay_pin,true, PinOwner::Relay)) { rlyPin = hw_relay_pin; - pinMode(rlyPin, OUTPUT); + pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT); } else { rlyPin = -1; } @@ -370,7 +428,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonObject light = doc[F("light")]; CJSON(briMultiplier, light[F("scale-bri")]); CJSON(strip.paletteBlend, light[F("pal-mode")]); - CJSON(autoSegments, light[F("aseg")]); + CJSON(strip.autoSegments, light[F("aseg")]); CJSON(gammaCorrectVal, light["gc"]["val"]); // default 2.8 float light_gc_bri = light["gc"]["bri"]; @@ -440,7 +498,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { if (if_sync_send[F("twice")]) udpNumRetries = 1; // import setting from 0.13 and earlier CJSON(udpNumRetries, if_sync_send["ret"]); - JsonObject if_nodes = interfaces[F("nodes")]; + JsonObject if_nodes = interfaces["nodes"]; CJSON(nodeListEnabled, if_nodes[F("list")]); CJSON(nodeBroadcastEnabled, if_nodes[F("bcast")]); @@ -469,14 +527,14 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(arlsDisableGammaCorrection, if_live[F("no-gc")]); // false CJSON(arlsOffset, if_live[F("offset")]); // 0 +#ifndef WLED_DISABLE_ALEXA CJSON(alexaEnabled, interfaces["va"][F("alexa")]); // false - CJSON(macroAlexaOn, interfaces["va"]["macros"][0]); CJSON(macroAlexaOff, interfaces["va"]["macros"][1]); - CJSON(alexaNumPresets, interfaces["va"]["p"]); +#endif -#ifdef WLED_ENABLE_MQTT +#ifndef WLED_DISABLE_MQTT JsonObject if_mqtt = interfaces["mqtt"]; CJSON(mqttEnabled, if_mqtt["en"]); getStringFromJson(mqttServer, if_mqtt[F("broker")], MQTT_MAX_SERVER_LEN+1); @@ -504,7 +562,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonArray if_hue_ip = if_hue["ip"]; - for (byte i = 0; i < 4; i++) + for (unsigned i = 0; i < 4; i++) CJSON(hueIP[i], if_hue_ip[i]); #endif @@ -616,20 +674,23 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { return (doc["sv"] | true); } + +static const char s_cfg_json[] PROGMEM = "/cfg.json"; + void deserializeConfigFromFS() { bool success = deserializeConfigSec(); + #ifdef WLED_ADD_EEPROM_SUPPORT if (!success) { //if file does not exist, try reading from EEPROM - #ifdef WLED_ADD_EEPROM_SUPPORT deEEPSettings(); return; - #endif } + #endif if (!requestJSONBufferLock(1)) return; DEBUG_PRINTLN(F("Reading settings from /cfg.json...")); - success = readObjectFromFile("/cfg.json", nullptr, pDoc); + success = readObjectFromFile(s_cfg_json, nullptr, pDoc); if (!success) { // if file does not exist, optionally try reading from EEPROM and then save defaults to FS releaseJSONBufferLock(); #ifdef WLED_ADD_EEPROM_SUPPORT @@ -675,7 +736,9 @@ void serializeConfig() { JsonObject id = root.createNestedObject("id"); id[F("mdns")] = cmDNS; id[F("name")] = serverDescription; +#ifndef WLED_DISABLE_ALEXA id[F("inv")] = alexaInvocationName; +#endif id[F("sui")] = simplifiedUI; JsonObject nw = root.createNestedObject("nw"); @@ -720,13 +783,16 @@ void serializeConfig() { JsonObject wifi = root.createNestedObject(F("wifi")); wifi[F("sleep")] = !noWifiSleep; wifi[F("phy")] = force802_3g; +#ifdef ARDUINO_ARCH_ESP32 + wifi[F("txpwr")] = txPower; +#endif - #ifdef WLED_USE_ETHERNET +#ifdef WLED_USE_ETHERNET JsonObject ethernet = root.createNestedObject("eth"); ethernet["type"] = ethernetType; if (ethernetType != WLED_ETH_NONE && ethernetType < WLED_NUM_ETH_TYPES) { JsonArray pins = ethernet.createNestedArray("pin"); - for (uint8_t p=0; p=0) pins.add(ethernetBoards[ethernetType].eth_power); if (ethernetBoards[ethernetType].eth_mdc>=0) pins.add(ethernetBoards[ethernetType].eth_mdc); if (ethernetBoards[ethernetType].eth_mdio>=0) pins.add(ethernetBoards[ethernetType].eth_mdio); @@ -743,7 +809,7 @@ void serializeConfig() { break; } } - #endif +#endif JsonObject hw = root.createNestedObject(F("hw")); @@ -751,8 +817,9 @@ void serializeConfig() { hw_led[F("total")] = strip.getLengthTotal(); //provided for compatibility on downgrade and per-output ABL hw_led[F("maxpwr")] = BusManager::ablMilliampsMax(); hw_led[F("ledma")] = 0; // no longer used - hw_led["cct"] = correctWB; - hw_led[F("cr")] = cctFromRgb; + hw_led["cct"] = strip.correctWB; + hw_led[F("cr")] = strip.cctFromRgb; + hw_led[F("ic")] = cctICused; hw_led[F("cb")] = strip.cctBlending; hw_led["fps"] = strip.getTargetFps(); hw_led[F("rgbwm")] = Bus::getGlobalAWMode(); // global auto white mode override @@ -764,7 +831,7 @@ void serializeConfig() { JsonObject matrix = hw_led.createNestedObject(F("matrix")); matrix[F("mpc")] = strip.panels; JsonArray panels = matrix.createNestedArray(F("panels")); - for (uint8_t i=0; igetLength()==0) break; JsonObject ins = hw_led_ins.createNestedObject(); @@ -789,7 +856,7 @@ void serializeConfig() { JsonArray ins_pin = ins.createNestedArray("pin"); uint8_t pins[5]; uint8_t nPins = bus->getPins(pins); - for (uint8_t i = 0; i < nPins; i++) ins_pin.add(pins[i]); + for (int i = 0; i < nPins; i++) ins_pin.add(pins[i]); ins[F("order")] = bus->getColorOrder(); ins["rev"] = bus->isReversed(); ins[F("skip")] = bus->skippedLeds(); @@ -803,7 +870,7 @@ void serializeConfig() { JsonArray hw_com = hw.createNestedArray(F("com")); const ColorOrderMap& com = BusManager::getColorOrderMap(); - for (uint8_t s = 0; s < com.count(); s++) { + for (size_t s = 0; s < com.count(); s++) { const ColorOrderMapEntry *entry = com.get(s); if (!entry) break; @@ -820,7 +887,7 @@ void serializeConfig() { JsonArray hw_btn_ins = hw_btn.createNestedArray("ins"); // configuration for all buttons - for (uint8_t i=0; i max) max = g; if (b > max) max = b; if (w > max) max = w; @@ -65,24 +65,27 @@ uint32_t color_add(uint32_t c1, uint32_t c2, bool fast) * fades color toward black * if using "video" method the resulting color will never become black unless it is already black */ + uint32_t color_fade(uint32_t c1, uint8_t amount, bool video) { - uint8_t r = R(c1); - uint8_t g = G(c1); - uint8_t b = B(c1); - uint8_t w = W(c1); + uint32_t scaledcolor; // color order is: W R G B from MSB to LSB + uint32_t r = R(c1); + uint32_t g = G(c1); + uint32_t b = B(c1); + uint32_t w = W(c1); + uint32_t scale = amount + !video; // 32bit for faster calculation if (video) { - r = scale8_video(r, amount); - g = scale8_video(g, amount); - b = scale8_video(b, amount); - w = scale8_video(w, amount); + scaledcolor = (((r * scale) >> 8) << 16) + ((r && scale) ? 1 : 0); + scaledcolor |= (((g * scale) >> 8) << 8) + ((g && scale) ? 1 : 0); + scaledcolor |= ((b * scale) >> 8) + ((b && scale) ? 1 : 0); + scaledcolor |= (((w * scale) >> 8) << 24) + ((w && scale) ? 1 : 0); } else { - r = scale8(r, amount); - g = scale8(g, amount); - b = scale8(b, amount); - w = scale8(w, amount); + scaledcolor = ((r * scale) >> 8) << 16; + scaledcolor |= ((g * scale) >> 8) << 8; + scaledcolor |= (b * scale) >> 8; + scaledcolor |= ((w * scale) >> 8) << 24; } - return RGBW32(r, g, b, w); + return scaledcolor; } void setRandomColor(byte* rgb) @@ -134,25 +137,25 @@ CRGBPalette16 generateHarmonicRandomPalette(CRGBPalette16 &basepalette) case 1: // triadic harmonics[0] = basehue + 113 + random8(15); harmonics[1] = basehue + 233 + random8(15); - harmonics[2] = basehue -7 + random8(15); + harmonics[2] = basehue - 7 + random8(15); break; case 2: // split-complementary harmonics[0] = basehue + 145 + random8(10); harmonics[1] = basehue + 205 + random8(10); - harmonics[2] = basehue - 5 + random8(10); + harmonics[2] = basehue - 5 + random8(10); break; case 3: // square - harmonics[0] = basehue + 85 + random8(10); + harmonics[0] = basehue + 85 + random8(10); harmonics[1] = basehue + 175 + random8(10); harmonics[2] = basehue + 265 + random8(10); break; case 4: // tetradic - harmonics[0] = basehue + 80 + random8(20); + harmonics[0] = basehue + 80 + random8(20); harmonics[1] = basehue + 170 + random8(20); - harmonics[2] = basehue + random8(30)-15; + harmonics[2] = basehue - 15 + random8(30); break; } @@ -202,13 +205,13 @@ CRGBPalette16 generateRandomPalette(void) //generate fully random palette void colorHStoRGB(uint16_t hue, byte sat, byte* rgb) //hue, sat to rgb { - float h = ((float)hue)/65535.0f; + float h = ((float)hue)/10922.5f; // hue*6/65535 float s = ((float)sat)/255.0f; - int i = floorf(h*6); - float f = h * 6.0f - i; + int i = int(h); + float f = h - i; int p = int(255.0f * (1.0f-s)); - int q = int(255.0f * (1.0f-f*s)); - int t = int(255.0f * (1.0f-(1.0f-f)*s)); + int q = int(255.0f * (1.0f-s*f)); + int t = int(255.0f * (1.0f-s*(1.0f-f))); p = constrain(p, 0, 255); q = constrain(q, 0, 255); t = constrain(t, 0, 255); @@ -378,13 +381,13 @@ bool colorFromHexString(byte* rgb, const char* in) { return true; } -float minf (float v, float w) +static inline float minf(float v, float w) { if (w > v) return v; return w; } -float maxf (float v, float w) +static inline float maxf(float v, float w) { if (w > v) return w; return v; diff --git a/wled00/const.h b/wled00/const.h index af3e27a4..327facbc 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -46,44 +46,59 @@ #ifndef WLED_MAX_BUSSES #ifdef ESP8266 - #define WLED_MAX_BUSSES 3 + #define WLED_MAX_DIGITAL_CHANNELS 3 + #define WLED_MAX_ANALOG_CHANNELS 5 + #define WLED_MAX_BUSSES 4 // will allow 3 digital & 1 analog RGB #define WLED_MIN_VIRTUAL_BUSSES 2 #else + #define WLED_MAX_ANALOG_CHANNELS (LEDC_CHANNEL_MAX*LEDC_SPEED_MODE_MAX) #if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, 6 LEDC, only has 1 I2S but NPB does not support it ATM - #define WLED_MAX_BUSSES 3 // will allow 2 digital & 1 analog (or the other way around) + #define WLED_MAX_BUSSES 4 // will allow 2 digital & 2 analog RGB + #define WLED_MAX_DIGITAL_CHANNELS 2 + //#define WLED_MAX_ANALOG_CHANNELS 6 #define WLED_MIN_VIRTUAL_BUSSES 3 #elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, 8 LEDC, only has 1 I2S bus, supported in NPB - #if defined(USERMOD_AUDIOREACTIVE) // requested by @softhack007 https://github.com/blazoncek/WLED/issues/33 - #define WLED_MAX_BUSSES 6 // will allow 4 digital & 2 analog - #define WLED_MIN_VIRTUAL_BUSSES 4 - #else - #define WLED_MAX_BUSSES 7 // will allow 5 digital & 2 analog - #define WLED_MIN_VIRTUAL_BUSSES 3 - #endif + // the 5th bus (I2S) will prevent Audioreactive usermod from functioning (it is last used though) + #define WLED_MAX_BUSSES 7 // will allow 5 digital & 2 analog RGB + #define WLED_MAX_DIGITAL_CHANNELS 5 + //#define WLED_MAX_ANALOG_CHANNELS 8 + #define WLED_MIN_VIRTUAL_BUSSES 3 #elif defined(CONFIG_IDF_TARGET_ESP32S3) // 4 RMT, 8 LEDC, has 2 I2S but NPB does not support them ATM - #define WLED_MAX_BUSSES 6 // will allow 4 digital & 2 analog + #define WLED_MAX_BUSSES 6 // will allow 4 digital & 2 analog RGB + #define WLED_MAX_DIGITAL_CHANNELS 4 + //#define WLED_MAX_ANALOG_CHANNELS 8 #define WLED_MIN_VIRTUAL_BUSSES 4 #else - #if defined(USERMOD_AUDIOREACTIVE) // requested by @softhack007 https://github.com/blazoncek/WLED/issues/33 - #define WLED_MAX_BUSSES 8 - #define WLED_MIN_VIRTUAL_BUSSES 2 - #else - #define WLED_MAX_BUSSES 10 - #define WLED_MIN_VIRTUAL_BUSSES 0 - #endif + // the last digital bus (I2S0) will prevent Audioreactive usermod from functioning + #define WLED_MAX_BUSSES 20 // will allow 17 digital & 3 analog RGB + #define WLED_MAX_DIGITAL_CHANNELS 17 + //#define WLED_MAX_ANALOG_CHANNELS 16 + #define WLED_MIN_VIRTUAL_BUSSES 4 #endif #endif #else #ifdef ESP8266 - #if WLED_MAX_BUSES > 5 + #if WLED_MAX_BUSSES > 5 #error Maximum number of buses is 5. #endif + #ifndef WLED_MAX_ANALOG_CHANNELS + #error You must also define WLED_MAX_ANALOG_CHANNELS. + #endif + #ifndef WLED_MAX_DIGITAL_CHANNELS + #error You must also define WLED_MAX_DIGITAL_CHANNELS. + #endif #define WLED_MIN_VIRTUAL_BUSSES (5-WLED_MAX_BUSSES) #else - #if WLED_MAX_BUSES > 10 - #error Maximum number of buses is 10. + #if WLED_MAX_BUSSES > 20 + #error Maximum number of buses is 20. #endif - #define WLED_MIN_VIRTUAL_BUSSES (10-WLED_MAX_BUSSES) + #ifndef WLED_MAX_ANALOG_CHANNELS + #error You must also define WLED_MAX_ANALOG_CHANNELS. + #endif + #ifndef WLED_MAX_DIGITAL_CHANNELS + #error You must also define WLED_MAX_DIGITAL_CHANNELS. + #endif + #define WLED_MIN_VIRTUAL_BUSSES (20-WLED_MAX_BUSSES) #endif #endif @@ -93,6 +108,11 @@ #else #define WLED_MAX_BUTTONS 4 #endif +#else + #if WLED_MAX_BUTTONS < 2 + #undef WLED_MAX_BUTTONS + #define WLED_MAX_BUTTONS 2 + #endif #endif #ifdef ESP8266 @@ -175,6 +195,14 @@ #define USERMOD_ID_STAIRWAY_WIPE 44 //Usermod "stairway-wipe-usermod-v2.h" #define USERMOD_ID_ANIMARTRIX 45 //Usermod "usermod_v2_animartrix.h" #define USERMOD_ID_HTTP_PULL_LIGHT_CONTROL 46 //usermod "usermod_v2_HttpPullLightControl.h" +#define USERMOD_ID_TETRISAI 47 //Usermod "usermod_v2_tetris.h" +#define USERMOD_ID_MAX17048 48 //Usermod "usermod_max17048.h" +#define USERMOD_ID_BME68X 49 //Usermod "usermod_bme68x.h +#define USERMOD_ID_INA226 50 //Usermod "usermod_ina226.h" +#define USERMOD_ID_AHT10 51 //Usermod "usermod_aht10.h" +#define USERMOD_ID_LD2410 52 //Usermod "usermod_ld2410.h" +#define USERMOD_ID_POV_DISPLAY 53 //Usermod "usermod_pov_display.h" +#define USERMOD_ID_PIXELS_DICE_TRAY 54 //Usermod "pixels_dice_tray.h" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot @@ -190,9 +218,9 @@ #define CALL_MODE_INIT 0 //no updates on init, can be used to disable updates #define CALL_MODE_DIRECT_CHANGE 1 #define CALL_MODE_BUTTON 2 //default button actions applied to selected segments -#define CALL_MODE_NOTIFICATION 3 -#define CALL_MODE_NIGHTLIGHT 4 -#define CALL_MODE_NO_NOTIFY 5 +#define CALL_MODE_NOTIFICATION 3 //caused by incoming notification (UDP or DMX preset) +#define CALL_MODE_NIGHTLIGHT 4 //nightlight progress +#define CALL_MODE_NO_NOTIFY 5 //change state but do not send notifications (UDP) #define CALL_MODE_FX_CHANGED 6 //no longer used #define CALL_MODE_HUE 7 #define CALL_MODE_PRESET_CYCLE 8 //no longer used @@ -254,6 +282,7 @@ #define TYPE_NONE 0 //light is not configured #define TYPE_RESERVED 1 //unused. Might indicate a "virtual" light //Digital types (data pin only) (16-39) +#define TYPE_DIGITAL_MIN 16 // first usable digital type #define TYPE_WS2812_1CH 18 //white-only chips (1 channel per IC) (unused) #define TYPE_WS2812_1CH_X3 19 //white-only chips (3 channels per IC) #define TYPE_WS2812_2CH_X3 20 //CCT chips (1st IC controls WW + CW of 1st zone and CW of 2nd zone, 2nd IC controls WW of 2nd zone and WW + CW of 3rd zone) @@ -264,38 +293,55 @@ #define TYPE_TM1829 25 #define TYPE_UCS8903 26 #define TYPE_APA106 27 +#define TYPE_FW1906 28 //RGB + CW + WW + unused channel (6 channels per IC) #define TYPE_UCS8904 29 //first RGBW digital type (hardcoded in busmanager.cpp, memUsage()) #define TYPE_SK6812_RGBW 30 #define TYPE_TM1814 31 +#define TYPE_WS2805 32 //RGB + WW + CW +#define TYPE_TM1914 33 //RGB +#define TYPE_SM16825 34 //RGB + WW + CW +#define TYPE_DIGITAL_MAX 39 // last usable digital type //"Analog" types (40-47) #define TYPE_ONOFF 40 //binary output (relays etc.; NOT PWM) +#define TYPE_ANALOG_MIN 41 // first usable analog type #define TYPE_ANALOG_1CH 41 //single channel PWM. Uses value of brightest RGBW channel #define TYPE_ANALOG_2CH 42 //analog WW + CW #define TYPE_ANALOG_3CH 43 //analog RGB #define TYPE_ANALOG_4CH 44 //analog RGBW #define TYPE_ANALOG_5CH 45 //analog RGB + WW + CW +#define TYPE_ANALOG_6CH 46 //analog RGB + A + WW + CW +#define TYPE_ANALOG_MAX 47 // last usable analog type //Digital types (data + clock / SPI) (48-63) +#define TYPE_2PIN_MIN 48 #define TYPE_WS2801 50 #define TYPE_APA102 51 #define TYPE_LPD8806 52 #define TYPE_P9813 53 #define TYPE_LPD6803 54 +#define TYPE_2PIN_MAX 63 #define TYPE_HUB75MATRIX 100 // 100 - 110 //Network types (master broadcast) (80-95) +#define TYPE_VIRTUAL_MIN 80 #define TYPE_NET_DDP_RGB 80 //network DDP RGB bus (master broadcast bus) #define TYPE_NET_E131_RGB 81 //network E131 RGB bus (master broadcast bus, unused) #define TYPE_NET_ARTNET_RGB 82 //network ArtNet RGB bus (master broadcast bus, unused) #define TYPE_NET_DDP_RGBW 88 //network DDP RGBW bus (master broadcast bus) +#define TYPE_NET_ARTNET_RGBW 89 //network ArtNet RGB bus (master broadcast bus, unused) +#define TYPE_VIRTUAL_MAX 95 +/* +// old macros that have been moved to Bus class #define IS_TYPE_VALID(t) ((t) > 15 && (t) < 128) #define IS_DIGITAL(t) (((t) > 15 && (t) < 40) || ((t) > 47 && (t) < 64)) //digital are 16-39 and 48-63 #define IS_2PIN(t) ((t) > 47 && (t) < 64) #define IS_16BIT(t) ((t) == TYPE_UCS8903 || (t) == TYPE_UCS8904) +#define IS_ONOFF(t) ((t) == 40) #define IS_PWM(t) ((t) > 40 && (t) < 46) //does not include on/Off type #define NUM_PWM_PINS(t) ((t) - 40) //for analog PWM 41-45 only #define IS_VIRTUAL(t) ((t) >= 80 && (t) < 96) //this was a poor choice a better would be 96-111 +*/ //Color orders #define COL_ORDER_GRB 0 //GRB(w),defaut @@ -324,7 +370,7 @@ #define BTN_TYPE_TOUCH_SWITCH 9 //Ethernet board types -#define WLED_NUM_ETH_TYPES 12 +#define WLED_NUM_ETH_TYPES 13 #define WLED_ETH_NONE 0 #define WLED_ETH_WT32_ETH01 1 @@ -338,6 +384,7 @@ #define WLED_ETH_ABCWLEDV43ETH 9 #define WLED_ETH_SERG74 10 #define WLED_ETH_ESP32_POE_WROVER 11 +#define WLED_ETH_LILYGO_T_POE_PRO 12 //Hue error codes #define HUE_ERROR_INACTIVE 0 @@ -370,6 +417,7 @@ //Playlist option byte #define PL_OPTION_SHUFFLE 0x01 +#define PL_OPTION_RESTORE 0x02 // Segment capability byte #define SEG_CAPABILITY_RGB 0x01 @@ -448,7 +496,7 @@ // string temp buffer (now stored in stack locally) #ifdef ESP8266 -#define SETTINGS_STACK_BUF_SIZE 2048 +#define SETTINGS_STACK_BUF_SIZE 2560 #else #define SETTINGS_STACK_BUF_SIZE 3840 // warning: quite a large value for stack (640 * WLED_MAX_USERMODS) #endif @@ -473,12 +521,26 @@ #endif #endif +#ifndef LED_MILLIAMPS_DEFAULT + #define LED_MILLIAMPS_DEFAULT 55 // common WS2812B +#else + #if LED_MILLIAMPS_DEFAULT < 1 || LED_MILLIAMPS_DEFAULT > 100 + #warning "Unusual LED mA current, overriding with default value." + #undef LED_MILLIAMPS_DEFAULT + #define LED_MILLIAMPS_DEFAULT 55 + #endif +#endif + // PWM settings #ifndef WLED_PWM_FREQ #ifdef ESP8266 #define WLED_PWM_FREQ 880 //PWM frequency proven as good for LEDs #else - #define WLED_PWM_FREQ 19531 + #ifdef SOC_LEDC_SUPPORT_XTAL_CLOCK + #define WLED_PWM_FREQ 9765 // XTAL clock is 40MHz (this will allow 12 bit resolution) + #else + #define WLED_PWM_FREQ 19531 // APB clock is 80MHz + #endif #endif #endif @@ -507,10 +569,10 @@ //this is merely a default now and can be changed at runtime #ifndef LEDPIN -#if defined(ESP8266) || (defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_PSRAM)) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ARDUINO_ESP32_PICO) - #define LEDPIN 2 // GPIO2 (D4) on Wemos D1 mini compatible boards, and on boards where GPIO16 is not available +#if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32C3) //|| (defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM)) || defined(ARDUINO_ESP32_PICO) + #define LEDPIN 2 // GPIO2 (D4) on Wemos D1 mini compatible boards, safe to use on any board #else - #define LEDPIN 16 // aligns with GPIO2 (D4) on Wemos D1 mini32 compatible boards + #define LEDPIN 16 // aligns with GPIO2 (D4) on Wemos D1 mini32 compatible boards (if it is unusable it will be reassigned in WS2812FX::finalizeInit()) #endif #endif diff --git a/wled00/data/index.css b/wled00/data/index.css index 37eb6a59..0952cca2 100644 --- a/wled00/data/index.css +++ b/wled00/data/index.css @@ -358,7 +358,7 @@ button { #putil, #segutil, #segutil2 { min-height: 42px; - margin: 13px auto 0; + margin: 0 auto; } #segutil .segin { @@ -923,6 +923,7 @@ select.sel-p, select.sel-pl, select.sel-ple { margin: 5px 0; width: 100%; height: 40px; + padding: 0 20px 0 8px; } div.sel-p { position: relative; @@ -1290,6 +1291,7 @@ TD .checkmark, TD .radiomark { margin: 0 auto 12px; min-height: 40px; border: 1px solid var(--c-2); + width: 100%; } /* Simplify segments */ diff --git a/wled00/data/index.htm b/wled00/data/index.htm index a58c76da..86b3b187 100644 --- a/wled00/data/index.htm +++ b/wled00/data/index.htm @@ -126,9 +126,10 @@
+ - +

Color palette

@@ -309,7 +310,7 @@

- Made with ❤︎ by Aircoookie and the WLED community + Made with ❤︎ by Aircoookie and the WLED community