diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index d09c8a605..1340da91c 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -2,12 +2,7 @@ # [Choice] Python version: 3, 3.9, 3.8, 3.7, 3.6 ARG VARIANT="3" -FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} - -# [Option] Install Node.js -ARG INSTALL_NODE="true" -ARG NODE_VERSION="lts/*" -RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi +FROM mcr.microsoft.com/devcontainers/python:0-${VARIANT} # [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. # COPY requirements.txt /tmp/pip-tmp/ diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f77dc4de4..241acd79d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,13 +5,25 @@ "context": "..", "args": { // Update 'VARIANT' to pick a Python version: 3, 3.6, 3.7, 3.8, 3.9 - "VARIANT": "3", - // Options - "INSTALL_NODE": "true", - "NODE_VERSION": "lts/*" + "VARIANT": "3" } }, + // 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", @@ -39,7 +51,7 @@ // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "npm install", + "postCreateCommand": "bash -i -c 'nvm install && npm ci'", // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "vscode" diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index 811db619a..000000000 --- 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/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..2bac314f3 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,81 @@ +name: WLED Build + +# Only included into other workflows +on: + workflow_call: + +jobs: + + get_default_envs: + name: Gather Environments + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: 'pip' + - name: Install PlatformIO + run: pip install -r requirements.txt + - name: Get default environments + id: envs + run: | + echo "environments=$(pio project config --json-output | jq -cr '.[0][1][0][1]')" >> $GITHUB_OUTPUT + outputs: + environments: ${{ steps.envs.outputs.environments }} + + + build: + name: Build Enviornments + runs-on: ubuntu-latest + needs: get_default_envs + strategy: + fail-fast: false + matrix: + environment: ${{ fromJSON(needs.get_default_envs.outputs.environments) }} + steps: + - uses: actions/checkout@v4 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'npm' + - run: npm ci + - name: Cache PlatformIO + uses: actions/cache@v4 + with: + path: | + ~/.platformio/.cache + ~/.buildcache + build_output + key: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}-${{ hashFiles('wled00/**', 'usermods/**') }} + restore-keys: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}- + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: 'pip' + - name: Install PlatformIO + run: pip install -r requirements.txt + - name: Build firmware + run: pio run -e ${{ matrix.environment }} + - uses: actions/upload-artifact@v4 + with: + name: firmware-${{ matrix.environment }} + path: | + build_output/release/*.bin + build_output/release/*_ESP02*.bin.gz + + + testCdata: + name: Test cdata.js + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'npm' + - run: npm ci + - run: npm test diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 000000000..138730058 --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,40 @@ + +name: Deploy Nightly +on: + # This can be used to automatically publish nightlies at UTC nighttime + schedule: + - cron: '0 2 * * *' # run at 2 AM UTC + # This can be used to allow manually triggering nightlies from the web interface + workflow_dispatch: + +jobs: + wled_build: + uses: ./.github/workflows/build.yml + nightly: + name: Deploy nightly + runs-on: ubuntu-latest + needs: wled_build + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + merge-multiple: true + - name: Show Files + run: ls -la + - name: "✏️ Generate release changelog" + id: changelog + uses: janheinrichmerker/action-github-changelog-generator@v2.3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + sinceTag: v0.15.0 + - name: Update Nightly Release + uses: andelf/nightly-release@main + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: nightly + name: 'Nightly Release $$' + prerelease: true + body: ${{ steps.changelog.outputs.changelog }} + files: | + ./*.bin \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..27beec99c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,28 @@ +name: WLED Release CI + +on: + push: + tags: + - '*' + +jobs: + + wled_build: + uses: ./.github/workflows/build.yml + + release: + name: Create Release + runs-on: ubuntu-latest + needs: wled_build + steps: + - uses: actions/download-artifact@v4 + with: + merge-multiple: true + - name: Create draft release + uses: softprops/action-gh-release@v1 + with: + draft: True + files: | + *.bin + *.bin.gz + diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..1f2557160 --- /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 f9ffb6481..3c862c185 100644 --- a/.github/workflows/wled-ci.yml +++ b/.github/workflows/wled-ci.yml @@ -1,94 +1,11 @@ name: WLED CI -on: [push, pull_request] +on: + push: + branches: + - '*' + pull_request: jobs: - - get_default_envs: - name: Gather Environments - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.12' - cache: 'pip' - - name: Install PlatformIO - run: pip install -r requirements.txt - - name: Get default environments - id: envs - run: | - echo "environments=$(pio project config --json-output | jq -cr '.[0][1][0][1]')" >> $GITHUB_OUTPUT - outputs: - environments: ${{ steps.envs.outputs.environments }} - - - build: - name: Build Enviornments - runs-on: ubuntu-latest - needs: get_default_envs - strategy: - fail-fast: false - matrix: - environment: ${{ fromJSON(needs.get_default_envs.outputs.environments) }} - steps: - - uses: actions/checkout@v4 - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - cache: 'npm' - - run: npm install - - name: Cache PlatformIO - uses: actions/cache@v4 - with: - path: | - ~/.platformio/.cache - ~/.buildcache - build_output - key: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}-${{ hashFiles('wled00/**', 'usermods/**') }} - restore-keys: pio-${{ runner.os }}-${{ matrix.environment }}-${{ hashFiles('platformio.ini', 'pio-scripts/output_bins.py') }}- - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.12' - cache: 'pip' - - name: Install PlatformIO - run: pip install -r requirements.txt - - name: Build firmware - run: pio run -e ${{ matrix.environment }} - - uses: actions/upload-artifact@v4 - with: - name: firmware-${{ matrix.environment }} - path: | - build_output/release/*.bin - build_output/release/*_ESP02.bin.gz - release: - name: Create Release - runs-on: ubuntu-latest - needs: build - if: startsWith(github.ref, 'refs/tags/') - steps: - - uses: actions/download-artifact@v4 - with: - merge-multiple: true - - name: Create draft release - uses: softprops/action-gh-release@v1 - with: - draft: True - files: | - *.bin - *.bin.gz - - - testCdata: - name: Test cdata.js - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: '20.x' - cache: 'npm' - - run: npm ci - - run: npm test + wled_build: + uses: ./.github/workflows/build.yml diff --git a/.gitignore b/.gitignore index 8a2319a72..8f083e3f6 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/.nvmrc b/.nvmrc new file mode 100644 index 000000000..10fef252a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20.18 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d432e357..c570ac1f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,200 @@ ## WLED changelog +#### Build 2410270 +- WLED 0.15.0-b7 release +- Re-license the WLED project from MIT to EUPL (#4194 by @Aircoookie) +- Fix alexa devices invisible/uncontrollable (#4214 by @Svennte) +- Add visual expand button on hover (#4172) +- Usermod: Audioreactive tuning and performance enhancements (by @softhack007) +- `/json/live` (JSON live data/peek) only enabled when WebSockets are disabled +- Various bugfixes and optimisations: #4179, #4215, #4219, #4222, #4223, #4224, #4228, #4230 + +#### Build 2410140 +- WLED 0.15.0-b6 release +- Added BRT timezone (#4188 by @LuisFadini) +- Fixed the positioning of the "Download the latest binary" button (#4184 by @maxi4329) +- Add WLED_AUTOSEGMENTS compile flag (#4183 by @PaoloTK) +- New 512kB FS parition map for 4MB devices +- Internal API change: Static PinManager & UsermodManager +- Change in Improv chip ID and version generation +- Various optimisations, bugfixes and enhancements (#4005, #4174 & #4175 by @Xevel, #4180, #4168, #4154, #4189 by @dosipod) + +#### Build 2409170 +- UI: Introduce common.js in settings pages (size optimisation) +- Add the ability to toggle the reception of palette synchronizations (#4137 by @felddy) +- Usermod/FX: Temperature usermod added Temperature effect (example usermod effect by @blazoncek) +- Fix AsyncWebServer version pin + +#### Build 2409140 +- Configure different kinds of busses at compile (#4107 by @PaoloTK) + - BREAKING: removes LEDPIN and DEFAULT_LED_TYPE compile overrides +- Fetch LED types from Bus classes (dynamic UI) (#4129 by @netmindz, @blazoncek, @dedehai) +- Temperature usermod: update OneWire to 2.3.8 (#4131 by @iammattcoleman) + +#### Build 2409100 +- WLED 0.15.0-b5 release +- Audioreactive usermod included by default in all compatible builds (including ESP8266) +- Demystified some byte definitions of WiZmote ESP-NOW message (#4114 by @ChuckMash) +- Update usermod "Battery" improved MQTT support (#4110 by @itCarl) +- Added a usermod for interacting with BLE Pixels Dice (#4093 by @axlan) +- Allow lower values for touch threshold (#4081 by @RobinMeis) +- Added POV image effect usermod (#3539 by @Liliputech) +- Remove repeating code to fetch audio data (#4103 by @netmindz) +- Loxone JSON parser doesn't handle lx=0 correctly (#4104 by @FreakyJ, fixes #3809) +- Rename wled00.ino to wled_main.cpp (#4090 by @willmmiles) +- SM16825 chip support including WW & CW channel swap (#4092) +- Add stress testing scripts (#4088 by @willmmiles) +- Improve jsonBufferLock management (#4089 by @willmmiles) +- Fix incorrect PWM bit depth on Esp32 with XTAL clock (#4082 by @PaoloTK) +- Devcontainer args (#4073 by @axlan) +- Effect: Fire2012 optional blur amount (#4078 by @apanteleev) +- Effect: GEQ fix bands (#4077 by @adrianschroeter) +- Boot delay option (#4060 by @DedeHai) +- ESP8266 Audioreactive sync (#3962 by @gaaat98, @netmindz, @softhack007) +- ESP8266 PWM crash fix (#4035 by @willmmiles) +- Usermod: Battery fix (#4051 by @Nickbert7) +- Usermod: Mpu6050 usermod crash fix (#4048 by @willmmiles) +- Usermod: Internal Temperature V2 (#4033 by @adamsthws) +- Various fixes and improvements (including build environments to emulate 0.14.0 for ESP8266) + +#### Build 2407070 +- Various fixes and improvements (mainly LED settings fix) + +#### Build 2406290 +- WLED 0.15.0-b4 release +- LED settings bus management update (WARNING: only allows 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) @@ -38,7 +233,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) @@ -120,6 +315,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 560a70973..670b5561d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,34 @@ 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 `main` branch. + +### Updating your code +While the PR is open - and under review by maintainers - you may be asked to modify your PR source code. +You can simply update your own branch, and push changes in response to reviewer recommendations. +Github will pick up the changes so your PR stays up-to-date. + +> [!CAUTION] +> Do not use "force-push" while your PR is open! +> It has many subtle and unexpected consequences on our github reposistory. +> For example, we regularly lost review comments when the PR author force-pushes code changes. So, pretty please, do not force-push. + + +You can find a collection of very useful tips and tricks here: https://github.com/Aircoookie/WLED/wiki/How-to-properly-submit-a-PR + + ### Code style When in doubt, it is easiest to replicate the code style you find in the files you want to edit :) @@ -14,7 +42,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 @@ -23,6 +51,11 @@ if (a == b) { } ``` +```cpp +if (a == b) doStuff(a); +``` + +Acceptable - however the first variant is usually easier to read: ```cpp if (a == b) { @@ -30,12 +63,9 @@ if (a == b) } ``` -```cpp -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 +103,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. -Inline comments are OK if they describe that line only and are not exceedingly wide. \ No newline at end of file +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. diff --git a/LICENSE b/LICENSE index 69325d21c..cca21c008 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,294 @@ -MIT License +Copyright (c) 2016-present Christian Schwinne and individual WLED contributors +Licensed under the EUPL v. 1.2 or later -Copyright (c) 2016 Christian Schwinne + EUROPEAN UNION PUBLIC LICENCE v. 1.2 + EUPL © the European Union 2007, 2016 -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +This European Union Public Licence (the ‘EUPL’) applies to the Work (as +defined below) which is provided under the terms of this Licence. Any use of +the Work, other than as authorised under this Licence is prohibited (to the +extent such use is covered by a right of the copyright holder of the Work). -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The Work is provided under the terms of this Licence when the Licensor (as +defined below) has placed the following notice immediately following the +copyright notice for the Work: -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file + Licensed under the EUPL + +or has expressed by any other means his willingness to license under the EUPL. + +1. Definitions + +In this Licence, the following terms have the following meaning: + +- ‘The Licence’: this Licence. + +- ‘The Original Work’: the work or software distributed or communicated by the + Licensor under this Licence, available as Source Code and also as Executable + Code as the case may be. + +- ‘Derivative Works’: the works or software that could be created by the + Licensee, based upon the Original Work or modifications thereof. This + Licence does not define the extent of modification or dependence on the + Original Work required in order to classify a work as a Derivative Work; + this extent is determined by copyright law applicable in the country + mentioned in Article 15. + +- ‘The Work’: the Original Work or its Derivative Works. + +- ‘The Source Code’: the human-readable form of the Work which is the most + convenient for people to study and modify. + +- ‘The Executable Code’: any code which has generally been compiled and which + is meant to be interpreted by a computer as a program. + +- ‘The Licensor’: the natural or legal person that distributes or communicates + the Work under the Licence. + +- ‘Contributor(s)’: any natural or legal person who modifies the Work under + the Licence, or otherwise contributes to the creation of a Derivative Work. + +- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of + the Work under the terms of the Licence. + +- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, + renting, distributing, communicating, transmitting, or otherwise making + available, online or offline, copies of the Work or providing access to its + essential functionalities at the disposal of any other natural or legal + person. + +2. Scope of the rights granted by the Licence + +The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, +sublicensable licence to do the following, for the duration of copyright +vested in the Original Work: + +- use the Work in any circumstance and for all usage, +- reproduce the Work, +- modify the Work, and make Derivative Works based upon the Work, +- communicate to the public, including the right to make available or display + the Work or copies thereof to the public and perform publicly, as the case + may be, the Work, +- distribute the Work or copies thereof, +- lend and rent the Work or copies thereof, +- sublicense rights in the Work or copies thereof. + +Those rights can be exercised on any media, supports and formats, whether now +known or later invented, as far as the applicable law permits so. + +In the countries where moral rights apply, the Licensor waives his right to +exercise his moral right to the extent allowed by law in order to make +effective the licence of the economic rights here above listed. + +The Licensor grants to the Licensee royalty-free, non-exclusive usage rights +to any patents held by the Licensor, to the extent necessary to make use of +the rights granted on the Work under this Licence. + +3. Communication of the Source Code + +The Licensor may provide the Work either in its Source Code form, or as +Executable Code. If the Work is provided as Executable Code, the Licensor +provides in addition a machine-readable copy of the Source Code of the Work +along with each copy of the Work that the Licensor distributes or indicates, +in a notice following the copyright notice attached to the Work, a repository +where the Source Code is easily and freely accessible for as long as the +Licensor continues to distribute or communicate the Work. + +4. Limitations on copyright + +Nothing in this Licence is intended to deprive the Licensee of the benefits +from any exception or limitation to the exclusive rights of the rights owners +in the Work, of the exhaustion of those rights or of other applicable +limitations thereto. + +5. Obligations of the Licensee + +The grant of the rights mentioned above is subject to some restrictions and +obligations imposed on the Licensee. Those obligations are the following: + +Attribution right: The Licensee shall keep intact all copyright, patent or +trademarks notices and all notices that refer to the Licence and to the +disclaimer of warranties. The Licensee must include a copy of such notices and +a copy of the Licence with every copy of the Work he/she distributes or +communicates. The Licensee must cause any Derivative Work to carry prominent +notices stating that the Work has been modified and the date of modification. + +Copyleft clause: If the Licensee distributes or communicates copies of the +Original Works or Derivative Works, this Distribution or Communication will be +done under the terms of this Licence or of a later version of this Licence +unless the Original Work is expressly distributed only under this version of +the Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee +(becoming Licensor) cannot offer or impose any additional terms or conditions +on the Work or Derivative Work that alter or restrict the terms of the +Licence. + +Compatibility clause: If the Licensee Distributes or Communicates Derivative +Works or copies thereof based upon both the Work and another work licensed +under a Compatible Licence, this Distribution or Communication can be done +under the terms of this Compatible Licence. For the sake of this clause, +‘Compatible Licence’ refers to the licences listed in the appendix attached to +this Licence. Should the Licensee's obligations under the Compatible Licence +conflict with his/her obligations under this Licence, the obligations of the +Compatible Licence shall prevail. + +Provision of Source Code: When distributing or communicating copies of the +Work, the Licensee will provide a machine-readable copy of the Source Code or +indicate a repository where this Source will be easily and freely available +for as long as the Licensee continues to distribute or communicate the Work. + +Legal Protection: This Licence does not grant permission to use the trade +names, trademarks, service marks, or names of the Licensor, except as required +for reasonable and customary use in describing the origin of the Work and +reproducing the content of the copyright notice. + +6. Chain of Authorship + +The original Licensor warrants that the copyright in the Original Work granted +hereunder is owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each Contributor warrants that the copyright in the modifications he/she +brings to the Work are owned by him/her or licensed to him/her and that he/she +has the power and authority to grant the Licence. + +Each time You accept the Licence, the original Licensor and subsequent +Contributors grant You a licence to their contributions to the Work, under the +terms of this Licence. + +7. Disclaimer of Warranty + +The Work is a work in progress, which is continuously improved by numerous +Contributors. It is not a finished work and may therefore contain defects or +‘bugs’ inherent to this type of development. + +For the above reason, the Work is provided under the Licence on an ‘as is’ +basis and without warranties of any kind concerning the Work, including +without limitation merchantability, fitness for a particular purpose, absence +of defects or errors, accuracy, non-infringement of intellectual property +rights other than copyright as stated in Article 6 of this Licence. + +This disclaimer of warranty is an essential part of the Licence and a +condition for the grant of any rights to the Work. + +8. Disclaimer of Liability + +Except in the cases of wilful misconduct or damages directly caused to natural +persons, the Licensor will in no event be liable for any direct or indirect, +material or moral, damages of any kind, arising out of the Licence or of the +use of the Work, including without limitation, damages for loss of goodwill, +work stoppage, computer failure or malfunction, loss of data or any commercial +damage, even if the Licensor has been advised of the possibility of such +damage. However, the Licensor will be liable under statutory product liability +laws as far such laws apply to the Work. + +9. Additional agreements + +While distributing the Work, You may choose to conclude an additional +agreement, defining obligations or services consistent with this Licence. +However, if accepting obligations, You may act only on your own behalf and on +your sole responsibility, not on behalf of the original Licensor or any other +Contributor, and only if You agree to indemnify, defend, and hold each +Contributor harmless for any liability incurred by, or claims asserted against +such Contributor by the fact You have accepted any warranty or additional +liability. + +10. Acceptance of the Licence + +The provisions of this Licence can be accepted by clicking on an icon ‘I +agree’ placed under the bottom of a window displaying the text of this Licence +or by affirming consent in any other similar way, in accordance with the rules +of applicable law. Clicking on that icon indicates your clear and irrevocable +acceptance of this Licence and all of its terms and conditions. + +Similarly, you irrevocably accept this Licence and all of its terms and +conditions by exercising any rights granted to You by Article 2 of this +Licence, such as the use of the Work, the creation by You of a Derivative Work +or the Distribution or Communication by You of the Work or copies thereof. + +11. Information to the public + +In case of any Distribution or Communication of the Work by means of +electronic communication by You (for example, by offering to download the Work +from a remote location) the distribution channel or media (for example, a +website) must at least provide to the public the information requested by the +applicable law regarding the Licensor, the Licence and the way it may be +accessible, concluded, stored and reproduced by the Licensee. + +12. Termination of the Licence + +The Licence and the rights granted hereunder will terminate automatically upon +any breach by the Licensee of the terms of the Licence. + +Such a termination will not terminate the licences of any person who has +received the Work from the Licensee under the Licence, provided such persons +remain in full compliance with the Licence. + +13. Miscellaneous + +Without prejudice of Article 9 above, the Licence represents the complete +agreement between the Parties as to the Work. + +If any provision of the Licence is invalid or unenforceable under applicable +law, this will not affect the validity or enforceability of the Licence as a +whole. Such provision will be construed or reformed so as necessary to make it +valid and enforceable. + +The European Commission may publish other linguistic versions or new versions +of this Licence or updated versions of the Appendix, so far this is required +and reasonable, without reducing the scope of the rights granted by the +Licence. New versions of the Licence will be published with a unique version +number. + +All linguistic versions of this Licence, approved by the European Commission, +have identical value. Parties can take advantage of the linguistic version of +their choice. + +14. Jurisdiction + +Without prejudice to specific agreement between parties, + +- any litigation resulting from the interpretation of this License, arising + between the European Union institutions, bodies, offices or agencies, as a + Licensor, and any Licensee, will be subject to the jurisdiction of the Court + of Justice of the European Union, as laid down in article 272 of the Treaty + on the Functioning of the European Union, + +- any litigation arising between other parties and resulting from the + interpretation of this License, will be subject to the exclusive + jurisdiction of the competent court where the Licensor resides or conducts + its primary business. + +15. Applicable Law + +Without prejudice to specific agreement between parties, + +- this Licence shall be governed by the law of the European Union Member State + where the Licensor has his seat, resides or has his registered office, + +- this licence shall be governed by Belgian law if the Licensor has no seat, + residence or registered office inside a European Union Member State. + +Appendix + +‘Compatible Licences’ according to Article 5 EUPL are: + +- GNU General Public License (GPL) v. 2, v. 3 +- GNU Affero General Public License (AGPL) v. 3 +- Open Software License (OSL) v. 2.1, v. 3.0 +- Eclipse Public License (EPL) v. 1.0 +- CeCILL v. 2.0, v. 2.1 +- Mozilla Public Licence (MPL) v. 2 +- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 +- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for + works other than software +- European Union Public Licence (EUPL) v. 1.1, v. 1.2 +- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong + Reciprocity (LiLiQ-R+). + +The European Commission may update this Appendix to later versions of the +above licences without producing a new version of the EUPL, as long as they +provide the rights granted in Article 2 of this Licence and protect the +covered Source Code from exclusive appropriation. + +All other changes or additions to this Appendix require the production of a +new EUPL version. \ 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 000000000..78c7160d9 --- /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 99e3efc3d..0afeeaafd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,82 +1,79 @@ { "name": "wled", - "version": "0.15.0-b1", + "version": "0.16.0-dev", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wled", - "version": "0.15.0-b1", + "version": "0.16.0-dev", "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.1.7" + }, + "engines": { + "node": ">=20.0.0" } }, "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": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "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" } }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "bin": { "acorn": "bin/acorn" }, @@ -191,9 +188,9 @@ } }, "node_modules/aws4": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", - "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==" }, "node_modules/balanced-match": { "version": "1.0.2", @@ -209,11 +206,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": { @@ -231,11 +231,11 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -324,15 +324,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 +339,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -702,9 +699,9 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1282,17 +1279,6 @@ "node": ">=0.10.0" } }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -1376,9 +1362,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.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", + "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", "dependencies": { "chokidar": "^3.5.2", "debug": "^4", @@ -1403,11 +1389,11 @@ } }, "node_modules/nodemon/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1419,9 +1405,9 @@ } }, "node_modules/nodemon/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/nodemon/node_modules/supports-color": { "version": "5.5.0", @@ -1434,20 +1420,6 @@ "node": ">=4" } }, - "node_modules/nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "*" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -1508,6 +1480,7 @@ "version": "0.1.5", "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "deprecated": "This package is no longer supported.", "dependencies": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" @@ -1621,6 +1594,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", "engines": { "node": ">=0.6.0", "teleport": ">=0.2.0" @@ -1827,12 +1801,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==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -1925,9 +1896,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 +1965,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.34.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.34.0.tgz", + "integrity": "sha512-y5NUX+U9HhVsK/zihZwoq4r9dICLyV2jXGOriDAVOeKhq3LKVjgJbGO90FisozXLlJfvjHqgckGmJFBb9KYoWQ==", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -2043,12 +2014,9 @@ } }, "node_modules/touch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", - "dependencies": { - "nopt": "~1.0.10" - }, + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", "bin": { "nodetouch": "bin/nodetouch.js" } @@ -2066,9 +2034,9 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" }, "node_modules/tunnel-agent": { "version": "0.6.0", @@ -2230,11 +2198,6 @@ "node": ">=0.10.0" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/yargs": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", @@ -2245,15 +2208,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 f2c0e3d65..68260982e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wled", - "version": "0.15.0-b1", + "version": "0.16.0-alpha", "description": "Tools for WLED project", "main": "tools/cdata.js", "directories": { @@ -26,7 +26,9 @@ "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.1.7" + }, + "engines": { + "node": ">=20.0.0" } } diff --git a/pio-scripts/obj-dump.py b/pio-scripts/obj-dump.py index 91bc3de58..174df509c 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 e12b11c2c..4d1594d84 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,53 @@ 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"]) + release_name_def = _get_cpp_define_value(env, "WLED_RELEASE_NAME") + if release_name_def: + release_name = release_name_def.replace("\\\"", "") version = _get_cpp_define_value(env, "WLED_VERSION") - # 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 c08c632a4..2c7d1442a 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, nodemcuv2_compat, esp8266_2m_compat, esp01_1m_full_compat, esp32dev, esp32_eth, lolin_s2_mini, esp32c3dev, esp32s3dev_16MB_opi, esp32s3dev_8MB_opi, esp32s3_4M_qspi, esp32_wrover 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#v2.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,11 +171,12 @@ lib_deps = # SHT85 ;robtillaart/SHT85@~0.3.3 # Audioreactive usermod - ;kosme/arduinoFFT @ 2.0.0 + ;kosme/arduinoFFT @ 2.0.1 extra_scripts = ${scripts_defaults.extra_scripts} [esp8266] +build_unflags = ${common.build_unflags} build_flags = -DESP8266 -DFP_IN_IROM @@ -198,17 +198,52 @@ build_flags = ; decrease code cache size and increase IRAM to fit all pixel functions -D PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48 ;; in case of linker errors like "section `.text1' will not fit in region `iram1_0_seg'" ; -D PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48_SECHEAP_SHARED ;; (experimental) adds some extra heap, but may cause slowdown + -D NON32XFER_HANDLER ;; ask forgiveness for PROGMEM misuse lib_deps = #https://github.com/lorol/LITTLEFS.git ESPAsyncTCP @ 1.2.2 ESPAsyncUDP + ESP8266PWM ${env.lib_deps} +;; compatibilty flags - same as 0.14.0 which seems to work better on some 8266 boards. Not using PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48 +build_flags_compat = + -DESP8266 + -DFP_IN_IROM + ;;-Wno-deprecated-declarations + -Wno-misleading-indentation + ;;-Wno-attributes ;; silence warnings about unknown attribute 'maybe_unused' in NeoPixelBus + -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x_190703 + -DPIO_FRAMEWORK_ARDUINO_LWIP_HIGHER_BANDWIDTH + -DVTABLES_IN_FLASH + -DMIMETYPE_MINIMAL + -DWLED_SAVE_IRAM ;; needed to prevent linker error + +;; this platform version was used for WLED 0.14.0 +platform_compat = espressif8266@4.2.0 +platform_packages_compat = + platformio/toolchain-xtensa @ ~2.100300.220621 #2.40802.200502 + platformio/tool-esptool #@ ~1.413.0 + platformio/tool-esptoolpy #@ ~1.30000.0 + +;; experimental - for using older NeoPixelBus 2.7.9 +lib_deps_compat = + ESPAsyncTCP @ 1.2.2 + ESPAsyncUDP + ESP8266PWM + fastled/FastLED @ 3.6.0 + IRremoteESP8266 @ 2.8.2 + makuna/NeoPixelBus @ 2.7.9 + https://github.com/blazoncek/QuickESPNow.git#optional-debug + https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.2.1 + + [esp32] #platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.2.3/platform-espressif32-2.0.2.3.zip platform = espressif32@3.5.0 platform_packages = framework-arduinoespressif32 @ https://github.com/Aircoookie/arduino-esp32.git#1.0.6.4 +build_unflags = ${common.build_unflags} build_flags = -g -DARDUINO_ARCH_ESP32 #-DCONFIG_LITTLEFS_FOR_IDF_3_2 @@ -216,7 +251,12 @@ 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 @@ -224,8 +264,10 @@ lib_deps = https://github.com/Aircoookie/GifDecoder#bc3af18 ${env.lib_deps} # additional build flags for audioreactive -AR_build_flags = -D USERMOD_AUDIOREACTIVE -AR_lib_deps = kosme/arduinoFFT @ 2.0.0 +AR_build_flags = -D USERMOD_AUDIOREACTIVE + -D sqrt_internal=sqrtf ;; -fsingle-precision-constant ;; forces ArduinoFFT to use float math (2x faster) +AR_lib_deps = kosme/arduinoFFT @ 2.0.1 +board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs [esp32_idf_V4] ;; experimental build environment for ESP32 using ESP-IDF 4.4.x / arduino-esp32 v2.0.5 @@ -233,24 +275,24 @@ AR_lib_deps = kosme/arduinoFFT @ 2.0.0 ;; ;; 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_unflags = ${common.build_unflags} build_flags = -g -Wshadow=compatible-local ;; emit warning in case a local variable "shadows" another local one -DARDUINO_ARCH_ESP32 -DESP32 - #-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} +board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs [esp32s2] ;; generic definitions for all ESP32-S2 boards -platform = espressif32@5.3.0 -platform_packages = -default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv +platform = ${esp32_idf_V4.platform} +platform_packages = ${esp32_idf_V4.platform_packages} +build_unflags = ${common.build_unflags} build_flags = -g -DARDUINO_ARCH_ESP32 -DARDUINO_ARCH_ESP32S2 @@ -264,11 +306,13 @@ build_flags = -g lib_deps = https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 ${env.lib_deps} +board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs [esp32c3] ;; generic definitions for all ESP32-C3 boards -platform = espressif32@5.3.0 -platform_packages = +platform = ${esp32_idf_V4.platform} +platform_packages = ${esp32_idf_V4.platform_packages} +build_unflags = ${common.build_unflags} build_flags = -g -DARDUINO_ARCH_ESP32 -DARDUINO_ARCH_ESP32C3 @@ -281,11 +325,13 @@ build_flags = -g lib_deps = https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 ${env.lib_deps} +board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs [esp32s3] ;; generic definitions for all ESP32-S3 boards -platform = espressif32@5.3.0 -platform_packages = +platform = ${esp32_idf_V4.platform} +platform_packages = ${esp32_idf_V4.platform_packages} +build_unflags = ${common.build_unflags} build_flags = -g -DESP32 -DARDUINO_ARCH_ESP32 @@ -296,10 +342,10 @@ 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} +board_build.partitions = ${esp32.large_partitions} ;; default partioning for 8MB flash - can be overridden in build envs # ------------------------------------------------------------------------------ @@ -312,52 +358,127 @@ 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_compat] +extends = env:nodemcuv2 +;; using platform version and build options from WLED 0.14.0 +platform = ${esp8266.platform_compat} +platform_packages = ${esp8266.platform_packages_compat} +build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP8266_compat\" #-DWLED_DISABLE_2D +;; lib_deps = ${esp8266.lib_deps_compat} ;; experimental - use older NeoPixelBus 2.7.9 + +[env:nodemcuv2_160] +extends = env:nodemcuv2 +board_build.f_cpu = 160000000L +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_160\" #-DWLED_DISABLE_2D + -D USERMOD_AUDIOREACTIVE + [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_compat] +extends = env:esp8266_2m +;; using platform version and build options from WLED 0.14.0 +platform = ${esp8266.platform_compat} +platform_packages = ${esp8266.platform_packages_compat} +build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP02_compat\" #-DWLED_DISABLE_2D + +[env:esp8266_2m_160] +extends = env:esp8266_2m +board_build.f_cpu = 160000000L +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP02_160\" + -D USERMOD_AUDIOREACTIVE + [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_compat] +extends = env:esp01_1m_full +;; using platform version and build options from WLED 0.14.0 +platform = ${esp8266.platform_compat} +platform_packages = ${esp8266.platform_packages_compat} +build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP01_compat\" -D WLED_DISABLE_OTA #-DWLED_DISABLE_2D + +[env:esp01_1m_full_160] +extends = env:esp01_1m_full +board_build.f_cpu = 160000000L +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP01_160\" -D WLED_DISABLE_OTA + -D USERMOD_AUDIOREACTIVE + ; -D WLED_USE_REAL_MATH ;; may fix wrong sunset/sunrise times, at the cost of 7064 bytes FLASH and 975 bytes RAM + [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 -lib_deps = ${esp32.lib_deps} -monitor_filters = esp32_exception_decoder -board_build.partitions = ${esp32.default_partitions} - -[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\" #-D WLED_DISABLE_BROWNOUT_DET ${esp32.AR_build_flags} lib_deps = ${esp32.lib_deps} ${esp32.AR_lib_deps} monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.default_partitions} + +[env:esp32dev_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_upload.flash_size = 8MB +board_upload.maximum_size = 8388608 ; board_build.f_flash = 80000000L -; board_build.flash_mode = dio +; board_build.flash_mode = qio + +[env:esp32dev_16M] +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_16M\" #-D WLED_DISABLE_BROWNOUT_DET + ${esp32.AR_build_flags} +lib_deps = ${esp32_idf_V4.lib_deps} + ${esp32.AR_lib_deps} +monitor_filters = esp32_exception_decoder +board_build.partitions = ${esp32.extreme_partitions} +board_upload.flash_size = 16MB +board_upload.maximum_size = 16777216 +board_build.f_flash = 80000000L +board_build.flash_mode = dio + +;[env:esp32dev_audioreactive] +;board = esp32dev +;platform = ${esp32.platform} +;platform_packages = ${esp32.platform_packages} +;build_unflags = ${common.build_unflags} +;build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"ESP32_audioreactive\" #-D WLED_DISABLE_BROWNOUT_DET +; ${esp32.AR_build_flags} +;lib_deps = ${esp32.lib_deps} +; ${esp32.AR_lib_deps} +;monitor_filters = esp32_exception_decoder +;board_build.partitions = ${esp32.default_partitions} +;; board_build.f_flash = 80000000L +;; board_build.flash_mode = dio [env:esp32_eth] board = esp32-poe @@ -365,23 +486,28 @@ 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 - -D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only +build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"ESP32_Ethernet\" -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1 +; -D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only + ${esp32.AR_build_flags} lib_deps = ${esp32.lib_deps} + ${esp32.AR_lib_deps} board_build.partitions = ${esp32.default_partitions} [env:esp32_wrover] -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} +board_build.partitions = ${esp32.extended_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 - -D LEDPIN=25 -lib_deps = ${esp32.lib_deps} +build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_WROVER\" + -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue ;; Older ESP32 (rev.<3) need a PSRAM fix (increases static RAM used) https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/external-ram.html + -D DATA_PINS=25 + ${esp32.AR_build_flags} +lib_deps = ${esp32_idf_V4.lib_deps} + ${esp32.AR_lib_deps} [env:esp32c3dev] extends = esp32c3 @@ -389,8 +515,8 @@ 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 -build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME=ESP32-C3 +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 -DARDUINO_USB_CDC_ON_BOOT=1 ;; for virtual CDC USB @@ -399,26 +525,30 @@ 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_upload.flash_size = 16MB +board_upload.maximum_size = 16777216 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 @@ -426,14 +556,62 @@ 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_wroom2] +;; For ESP32-S3 WROOM-2, a.k.a. ESP32-S3 DevKitC-1 v1.1 +;; with >= 16MB FLASH and >= 8MB PSRAM (memory_type: opi_opi) +platform = ${esp32s3.platform} +platform_packages = ${esp32s3.platform_packages} +board = esp32s3camlcd ;; this is the only standard board with "opi_opi" +board_build.arduino.memory_type = opi_opi +upload_speed = 921600 +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_WROOM-2\" + -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 + -D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip + ;; -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") + -DBOARD_HAS_PSRAM + -D LEDPIN=38 -D DATA_PINS=38 ;; buildin WS2812b LED + -D BTNPIN=0 -D RLYPIN=16 -D IRPIN=17 -D AUDIOPIN=-1 + -D WLED_DEBUG + ${esp32.AR_build_flags} + -D SR_DMTYPE=1 -D I2S_SDPIN=13 -D I2S_CKPIN=14 -D I2S_WSPIN=15 -D MCLK_PIN=4 ;; I2S mic +lib_deps = ${esp32s3.lib_deps} + ${esp32.AR_lib_deps} + +board_build.partitions = ${esp32.extreme_partitions} +board_upload.flash_size = 16MB +board_upload.maximum_size = 16777216 +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 @@ -442,24 +620,25 @@ 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 +build_flags = ${common.build_flags} ${esp32s2.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S2\" -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 + -D DATA_PINS=16 -D HW_PIN_SCL=35 -D HW_PIN_SDA=33 -D HW_PIN_CLOCKSPI=7 -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} diff --git a/platformio_override.sample.ini b/platformio_override.sample.ini index 29f5c6b57..cb5b43e7b 100644 --- a/platformio_override.sample.ini +++ b/platformio_override.sample.ini @@ -5,12 +5,12 @@ # Please visit documentation: https://docs.platformio.org/page/projectconf.html [platformio] -default_envs = WLED_tasmota_1M # define as many as you need +default_envs = WLED_generic8266_1M, esp32dev_V4_dio80 # put the name(s) of your own build environment here. You can define as many as you need #---------- # SAMPLE #---------- -[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} +; bitbank2/PNGdec@^1.0.1 ;; used for POV display uncomment following +; ${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE + +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags} ${esp8266.build_flags} ; ; *** To use the below defines/overrides, copy and paste each onto it's own line just below build_flags in the section above. ; +; 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,18 +53,26 @@ 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 +; -D DATA_PINS=2 ; or use this for multiple outputs ; -D DATA_PINS=1,3 ; -D BTNPIN=0 ; -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,9 +103,14 @@ 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 ; -DFLD_SPI_DEFAULT ; -D FLD_TYPE=SSD1306_SPI64 ; -D FLD_PIN_CLOCKSPI=14 @@ -118,12 +136,13 @@ 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 +; ${esp32.AR_build_flags} ;; default flags required to properly configure ArduinoFFT +; ;; don't forget to add ArduinoFFT to your libs_deps: ${esp32.AR_lib_deps} ; -D AUDIOPIN=-1 ; -D DMTYPE=1 # 0-analog/disabled, 1-I2S generic, 2-ES7243, 3-SPH0645, 4-I2S+mclk, 5-I2S PDM ; -D I2S_SDPIN=36 @@ -135,29 +154,36 @@ 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 -; +; ; set the name of the module - make sure there is a quote-backslash-quote before the name and a backslash-quote-quote after the name ; -D SERVERNAME="\"WLED\"" -; +; ; set the number of LEDs -; -D DEFAULT_LED_COUNT=30 +; -D PIXEL_COUNTS=30 ; or this for multiple outputs ; -D PIXEL_COUNTS=30,30 -; -; set milliampere limit when using ESP pin to power leds -; -D ABL_MILLIAMPS_DEFAULT=850 ; -; enable IR by setting remote type -; -D IRTYPE=0 ;0 Remote disabled | 1 24-key RGB | 2 24-key with CT | 3 40-key blue | 4 40-key RGB | 5 21-key RGB | 6 6-key black | 7 9-key red | 8 JSON remote -; +; set the default LED type +; -D LED_TYPES=22 # see const.h (TYPE_xxxx) +; or this for multiple outputs +; -D LED_TYPES=TYPE_SK6812_RGBW,TYPE_WS2812_RGB +; ; set default color order of your led strip ; -D DEFAULT_LED_COLOR_ORDER=COL_ORDER_GRB ; -; use PSRAM if a device (ESP) has one -; -DBOARD_HAS_PSRAM -; -D WLED_USE_PSRAM +; 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 +; +; 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 +206,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 +216,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 +226,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,18 +235,17 @@ 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 DATA_PINS=1 -D WLED_DISABLE_INFRARED lib_deps = ${esp8266.lib_deps} [env:esp32dev_qio80] +extends = env:esp32dev # we want to extend the existing esp32dev environment (and define only updated options) board = esp32dev -platform = ${esp32.platform} -platform_packages = ${esp32.platform_packages} -build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_qio80 #-D WLED_DISABLE_BROWNOUT_DET +build_flags = ${common.build_flags} ${esp32.build_flags} #-D WLED_DISABLE_BROWNOUT_DET + ${esp32.AR_build_flags} ;; optional - includes USERMOD_AUDIOREACTIVE lib_deps = ${esp32.lib_deps} + ${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE monitor_filters = esp32_exception_decoder -board_build.partitions = ${esp32.default_partitions} board_build.f_flash = 80000000L board_build.flash_mode = qio @@ -228,27 +253,26 @@ board_build.flash_mode = qio ;; experimental ESP32 env using ESP-IDF V4.4.x ;; Warning: this build environment is not stable!! ;; please erase your device before installing. +extends = esp32_idf_V4 # based on newer "esp-idf V4" platform environment board = esp32dev -platform = ${esp32_idf_V4.platform} -platform_packages = ${esp32_idf_V4.platform_packages} -build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_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 + ${esp32.AR_build_flags} ;; includes USERMOD_AUDIOREACTIVE lib_deps = ${esp32_idf_V4.lib_deps} + ${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE monitor_filters = esp32_exception_decoder -board_build.partitions = ${esp32_idf_V4.default_partitions} +board_build.partitions = ${esp32.default_partitions} ;; if you get errors about "out of program space", change this to ${esp32.extended_partitions} or even ${esp32.big_partitions} board_build.f_flash = 80000000L board_build.flash_mode = dio [env:esp32s2_saola] +extends = esp32s2 board = esp32-s2-saola-1 -platform = 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 ;; NB: WLED_USE_SHOJO_PCB is not used anywhere in the source code. Not sure why its needed. lib_deps = ${esp8266.lib_deps} [env:d1_mini_debug] @@ -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 DATA_PINS=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 @@ -335,51 +359,65 @@ upload_speed = 115200 lib_deps = ${esp32c3.lib_deps} board_build.partitions = tools/WLED_ESP32_2MB_noOTA.csv board_build.flash_mode = dio +board_upload.flash_size = 2MB +board_upload.maximum_size = 2097152 [env:wemos_shield_esp32] +extends = esp32 ;; use default esp32 platform board = esp32dev -platform = ${esp32.platform} -platform_packages = ${esp32.platform_packages} upload_speed = 460800 -build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} - -D LEDPIN=16 +build_flags = ${common.build_flags} ${esp32.build_flags} + -D WLED_RELEASE_NAME=\"ESP32_wemos_shield\" + -D DATA_PINS=16 -D RLYPIN=19 -D BTNPIN=17 -D IRPIN=18 - -D UWLED_USE_MY_CONFIG + -UWLED_USE_MY_CONFIG -D USERMOD_DALLASTEMPERATURE -D USERMOD_FOUR_LINE_DISPLAY -D TEMPERATURE_PIN=23 - -D USE_ALT_DISPlAY ; new versions of USERMOD_FOUR_LINE_DISPLAY and USERMOD_ROTARY_ENCODER_UI - -D USERMOD_AUDIOREACTIVE + ${esp32.AR_build_flags} ;; includes USERMOD_AUDIOREACTIVE lib_deps = ${esp32.lib_deps} - OneWire@~2.3.5 - olikraus/U8g2 @ ^2.28.8 - https://github.com/blazoncek/arduinoFFT.git + OneWire@~2.3.5 ;; needed for USERMOD_DALLASTEMPERATURE + olikraus/U8g2 @ ^2.28.8 ;; needed for USERMOD_FOUR_LINE_DISPLAY + ${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE board_build.partitions = ${esp32.default_partitions} -[env:m5atom] -board = esp32dev -build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} -D LEDPIN=27 -D BTNPIN=39 +[env:esp32_pico-D4] +extends = esp32 ;; use default esp32 platform +board = pico32 ;; pico32-D4 is different from the standard esp32dev + ;; hardware details from https://github.com/srg74/WLED-ESP32-pico +build_flags = ${common.build_flags} ${esp32.build_flags} + -D WLED_RELEASE_NAME=\"pico32-D4\" -D SERVERNAME='"WLED-pico32"' + -D WLED_DISABLE_ADALIGHT ;; no serial-to-USB chip on this board - better to disable serial protocols + -D DATA_PINS=2,18 ;; LED pins + -D RLYPIN=19 -D BTNPIN=0 -D IRPIN=-1 ;; no default pin for IR + ${esp32.AR_build_flags} ;; include USERMOD_AUDIOREACTIVE + -D UM_AUDIOREACTIVE_ENABLE ;; enable AR by default + ;; Audioreactive settings for on-board microphone (ICS-43432) + -D SR_DMTYPE=1 -D I2S_SDPIN=25 -D I2S_WSPIN=15 -D I2S_CKPIN=14 + -D SR_SQUELCH=5 -D SR_GAIN=30 lib_deps = ${esp32.lib_deps} -platform = ${esp32.platform} -platform_packages = ${esp32.platform_packages} + ${esp32.AR_lib_deps} ;; needed for USERMOD_AUDIOREACTIVE board_build.partitions = ${esp32.default_partitions} +board_build.f_flash = 80000000L + +[env:m5atom] +extends = env:esp32dev # we want to extend the existing esp32dev environment (and define only updated options) +build_flags = ${common.build_flags} ${esp32.build_flags} -D DATA_PINS=27 -D BTNPIN=39 [env:sp501e] board = esp_wroom_02 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 DATA_PINS=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 DATA_PINS=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,8 +426,8 @@ 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 - -D DEFAULT_LED_TYPE=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0 +build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=-1 -D RLYPIN=-1 -D DATA_PINS=4,12,14,13,5 + -D LED_TYPES=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0 lib_deps = ${esp8266.lib_deps} [env:Athom_15w_RGBCW] ;15w bulb @@ -398,8 +436,8 @@ 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 - -D DEFAULT_LED_TYPE=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0 -D WLED_USE_IC_CCT +build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=-1 -D RLYPIN=-1 -D DATA_PINS=4,12,14,5,13 + -D LED_TYPES=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0 -D WLED_USE_IC_CCT lib_deps = ${esp8266.lib_deps} [env:Athom_3Pin_Controller] ;small controller with only data @@ -408,7 +446,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 DATA_PINS=1 -D WLED_DISABLE_INFRARED lib_deps = ${esp8266.lib_deps} [env:Athom_4Pin_Controller] ; With clock and data interface @@ -417,7 +455,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 DATA_PINS=1 -D WLED_DISABLE_INFRARED lib_deps = ${esp8266.lib_deps} [env:Athom_5Pin_Controller] ;Analog light strip controller @@ -426,7 +464,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 +473,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 +487,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,24 +496,23 @@ 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} # ------------------------------------------------------------------------------ # EleksTube-IPS # ------------------------------------------------------------------------------ [env:elekstube_ips] +extends = esp32 ;; use default esp32 platform 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 + -D DATA_PINS=12 -D RLYPIN=27 -D BTNPIN=34 - -D DEFAULT_LED_COUNT=6 + -D PIXEL_COUNTS=6 # Display config -D ST7789_DRIVER -D TFT_WIDTH=135 @@ -491,5 +528,4 @@ build_flags = ${common.build_flags_esp32} -D WLED_DISABLE_BROWNOUT_DET -D WLED_D monitor_filters = esp32_exception_decoder lib_deps = ${esp32.lib_deps} - TFT_eSPI @ ^2.3.70 -board_build.partitions = ${esp32.default_partitions} + TFT_eSPI @ 2.5.33 ;; this is the last version that compiles with the WLED default framework - newer versions require platform = espressif32 @ ^6.3.2 diff --git a/readme.md b/readme.md index 11c1733f8..8c9a08801 100644 --- a/readme.md +++ b/readme.md @@ -12,7 +12,7 @@ # Welcome to my project WLED! ✨ -A fast and feature-rich implementation of an ESP8266/ESP32 webserver to control NeoPixel (WS2812B, WS2811, SK6812) LEDs or also SPI based chipsets like the WS2801 and APA102! +A fast and feature-rich implementation of an ESP32 and ESP8266 webserver to control NeoPixel (WS2812B, WS2811, SK6812) LEDs or also SPI based chipsets like the WS2801 and APA102! ## ⚙️ Features - WS2812FX library with more than 100 special effects @@ -21,7 +21,7 @@ A fast and feature-rich implementation of an ESP8266/ESP32 webserver to control - Segments to set different effects and colors to user defined parts of the LED string - Settings page - configuration via the network - Access Point and station mode - automatic failsafe AP -- Up to 10 LED outputs per instance +- [Up to 10 LED outputs](https://kno.wled.ge/features/multi-strip/#esp32) per instance - Support for RGBW strips - Up to 250 user presets to save and load colors/effects easily, supports cycling through them. - Presets can be used to automatically execute API calls @@ -61,7 +61,7 @@ See [here](https://kno.wled.ge/basics/compatible-hardware)! ## ✌️ Other -Licensed under the MIT license +Licensed under the EUPL v1.2 license Credits [here](https://kno.wled.ge/about/contributors/)! Join the Discord server to discuss everything about WLED! @@ -80,5 +80,5 @@ If WLED really brightens up your day, you can [![](https://img.shields.io/badge/ If you are prone to photosensitive epilepsy, we recommended you do **not** use this software. If you still want to try, don't use strobe, lighting or noise modes or high effect speed settings. -As per the MIT license, I assume no liability for any damage to you or any other person or equipment. +As per the EUPL license, I assume no liability for any damage to you or any other person or equipment. diff --git a/requirements.txt b/requirements.txt index 1c0644f98..666122aa2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,19 +4,17 @@ # # pip-compile # -aiofiles==22.1.0 - # via platformio ajsonrpc==1.2.0 # via platformio -anyio==3.6.2 +anyio==4.6.0 # via starlette -bottle==0.12.25 +bottle==0.13.1 # via platformio -certifi==2023.7.22 +certifi==2024.8.30 # via requests -charset-normalizer==3.1.0 +charset-normalizer==3.3.2 # via requests -click==8.1.3 +click==8.1.7 # via # platformio # uvicorn @@ -28,33 +26,33 @@ h11==0.14.0 # via # uvicorn # wsproto -idna==3.4 +idna==3.10 # via # anyio # requests -marshmallow==3.19.0 +marshmallow==3.22.0 # via platformio -packaging==23.1 +packaging==24.1 # via marshmallow -platformio==6.1.6 +platformio==6.1.16 # via -r requirements.in -pyelftools==0.29 +pyelftools==0.31 # via platformio pyserial==3.5 # via platformio -requests==2.31.0 +requests==2.32.3 # via platformio semantic-version==2.10.0 # via platformio -sniffio==1.3.0 +sniffio==1.3.1 # via anyio -starlette==0.23.1 +starlette==0.39.1 # via platformio tabulate==0.9.0 # via platformio -urllib3==1.26.18 +urllib3==2.2.3 # via requests -uvicorn==0.20.0 +uvicorn==0.30.6 # via platformio wsproto==1.2.0 # via platformio diff --git a/tools/WLED_ESP32-wrover_4MB.csv b/tools/WLED_ESP32-wrover_4MB.csv index a179a89d0..39c88e543 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 000000000..e54c22bbd --- /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_512KB_FS.csv b/tools/WLED_ESP32_4MB_512KB_FS.csv new file mode 100644 index 000000000..5281a6124 --- /dev/null +++ b/tools/WLED_ESP32_4MB_512KB_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, 0x1B0000, +app1, app, ota_1, 0x1C0000,0x1B0000, +spiffs, data, spiffs, 0x370000,0x80000, +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 000000000..39c88e543 --- /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/all_xml.sh b/tools/all_xml.sh new file mode 100644 index 000000000..68ed07bbd --- /dev/null +++ b/tools/all_xml.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Pull all settings pages for comparison +HOST=$1 +TGT_PATH=$2 +CURL_ARGS="--compressed" + +# Replicate one target many times +function replicate() { + for i in {0..10} + do + echo -n " http://${HOST}/settings.js?p=$i -o ${TGT_PATH}/$i.xml" + done +} +read -a TARGETS <<< $(replicate) + +mkdir -p ${TGT_PATH} +curl ${CURL_ARGS} ${TARGETS[@]} diff --git a/tools/cdata-test.js b/tools/cdata-test.js index 55f068073..6f27fb717 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 ef7e06f41..c5d3c6aa5 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 = `/* @@ -103,6 +101,7 @@ function adoptVersionAndRepo(html) { async function minify(str, type = "plain") { const options = { collapseWhitespace: true, + conservativeCollapse: true, // preserve spaces in text collapseBooleanAttributes: true, collapseInlineTagWhitespace: true, minifyCSS: true, @@ -118,7 +117,8 @@ async function minify(str, type = "plain") { } else if (type == "css-minify") { return new CleanCSS({}).minify(str).styles; } else if (type == "js-minify") { - return await minifyHtml('', options).replace(/<[\/]*script>/g, ''); + let js = await minifyHtml('', options); + return js.replace(/<[\/]*script>/g, ''); } else if (type == "html-minify") { return await minifyHtml(str, options); } @@ -209,7 +209,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 +222,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 @@ -254,6 +254,12 @@ writeChunks( str .replace("%%", "%") }, + { + file: "common.js", + name: "JS_common", + method: "gzip", + filter: "js-minify", + }, { file: "settings.htm", name: "PAGE_settings", diff --git a/tools/stress_test.sh b/tools/stress_test.sh new file mode 100644 index 000000000..d7c344c58 --- /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 000000000..c4c9129cf --- /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 000000000..69fab4671 --- /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 000000000..30240f222 --- /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 000000000..b5dc1841d --- /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/Analog_Clock/Analog_Clock.h b/usermods/Analog_Clock/Analog_Clock.h index 596f0acb3..9d82f7670 100644 --- a/usermods/Analog_Clock/Analog_Clock.h +++ b/usermods/Analog_Clock/Analog_Clock.h @@ -102,9 +102,9 @@ private: void secondsEffectSineFade(int16_t secondLed, Toki::Time const& time) { uint32_t ms = time.ms % 1000; - uint8_t b0 = (cos8(ms * 64 / 1000) - 128) * 2; + uint8_t b0 = (cos8_t(ms * 64 / 1000) - 128) * 2; setPixelColor(secondLed, gamma32(scale32(secondColor, b0))); - uint8_t b1 = (sin8(ms * 64 / 1000) - 128) * 2; + uint8_t b1 = (sin8_t(ms * 64 / 1000) - 128) * 2; setPixelColor(inc(secondLed, 1, secondsSegment), gamma32(scale32(secondColor, b1))); } diff --git a/usermods/Animated_Staircase/Animated_Staircase.h b/usermods/Animated_Staircase/Animated_Staircase.h index 8953756d3..54a9b3331 100644 --- a/usermods/Animated_Staircase/Animated_Staircase.h +++ b/usermods/Animated_Staircase/Animated_Staircase.h @@ -332,7 +332,7 @@ class Animated_Staircase : public Usermod { }; // NOTE: this *WILL* return TRUE if all the pins are set to -1. // this is *BY DESIGN*. - if (!pinManager.allocateMultiplePins(pins, 4, PinOwner::UM_AnimatedStaircase)) { + if (!PinManager::allocateMultiplePins(pins, 4, PinOwner::UM_AnimatedStaircase)) { topPIRorTriggerPin = -1; topEchoPin = -1; bottomPIRorTriggerPin = -1; @@ -425,10 +425,10 @@ class Animated_Staircase : public Usermod { } void appendConfigData() { - //oappend(SET_F("dd=addDropdown('staircase','selectfield');")); - //oappend(SET_F("addOption(dd,'1st value',0);")); - //oappend(SET_F("addOption(dd,'2nd value',1);")); - //oappend(SET_F("addInfo('staircase:selectfield',1,'additional info');")); // 0 is field type, 1 is actual field + //oappend(F("dd=addDropdown('staircase','selectfield');")); + //oappend(F("addOption(dd,'1st value',0);")); + //oappend(F("addOption(dd,'2nd value',1);")); + //oappend(F("addInfo('staircase:selectfield',1,'additional info');")); // 0 is field type, 1 is actual field } @@ -513,10 +513,10 @@ class Animated_Staircase : public Usermod { (oldBottomAPin != bottomPIRorTriggerPin) || (oldBottomBPin != bottomEchoPin)) { changed = true; - pinManager.deallocatePin(oldTopAPin, PinOwner::UM_AnimatedStaircase); - pinManager.deallocatePin(oldTopBPin, PinOwner::UM_AnimatedStaircase); - pinManager.deallocatePin(oldBottomAPin, PinOwner::UM_AnimatedStaircase); - pinManager.deallocatePin(oldBottomBPin, PinOwner::UM_AnimatedStaircase); + PinManager::deallocatePin(oldTopAPin, PinOwner::UM_AnimatedStaircase); + PinManager::deallocatePin(oldTopBPin, PinOwner::UM_AnimatedStaircase); + PinManager::deallocatePin(oldBottomAPin, PinOwner::UM_AnimatedStaircase); + PinManager::deallocatePin(oldBottomBPin, PinOwner::UM_AnimatedStaircase); } if (changed) setup(); } diff --git a/usermods/Animated_Staircase/README.md b/usermods/Animated_Staircase/README.md index 320b744a5..2ad66b5ae 100644 --- a/usermods/Animated_Staircase/README.md +++ b/usermods/Animated_Staircase/README.md @@ -18,7 +18,7 @@ Before compiling, you have to make the following modifications: Edit `usermods_list.cpp`: 1. Open `wled00/usermods_list.cpp` 2. add `#include "../usermods/Animated_Staircase/Animated_Staircase.h"` to the top of the file -3. add `usermods.add(new Animated_Staircase());` to the end of the `void registerUsermods()` function. +3. add `UsermodManager::add(new Animated_Staircase());` to the end of the `void registerUsermods()` function. You can configure usermod using the Usermods settings page. Please enter GPIO pins for PIR or ultrasonic sensors (trigger and echo). diff --git a/usermods/BH1750_v2/usermod_bh1750.h b/usermods/BH1750_v2/usermod_bh1750.h index ede4aabc4..2a2bd4637 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 0a4afbf1f..a4fc229a3 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 38930da5a..9168f4229 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 000000000..f3cfb3f05 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 000000000..72ae25a57 --- /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 000000000..572875595 --- /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 000000000..ee85410d3 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 000000000..7ebef21bb 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 000000000..2296cd69a 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 000000000..4b906c012 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 000000000..d01bfc147 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 000000000..6c36310a9 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 000000000..aca24d0a2 --- /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(F("dd=addDropdown('")); + oappend(UMOD_NAME); + oappend(F("','")); + oappend(_nameTempScale); + oappend(F("');")); + oappend(F("addOption(dd,'Celsius',0);")); + oappend(F("addOption(dd,'Fahrenheit',1);")); + + /* i²C Address*/ + oappend(F("dd=addDropdown('")); + oappend(UMOD_NAME); + oappend(F("','")); + oappend(_nameI2CAdr); + oappend(F("');")); + oappend(F("addOption(dd,'0x76',0x76);")); + oappend(F("addOption(dd,'0x77',0x77);")); +} + +/** + * @brief Called by WLED: Read Usermod Config Settings default settings values could be set here (or below using the 3-argument getJsonValue()) + * instead of in the class definition or constructor setting them inside readFromConfig() is slightly more robust, handling the rare but + * plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) + * This is called whenever WLED boots and loads cfg.json, or when the UM config + * page is saved. Will properly re-instantiate the SHT class upon type change and + * publish HA discovery after enabling. + * NOTE: Here are the default settings of the user module + * @param JsonObject Pointer + * @return bool + * @see Usermod::readFromConfig() + * @see UsermodManager::readFromConfig() + */ +bool UsermodBME68X::readFromConfig(JsonObject& root) { + DEBUG_PRINT(F(UMOD_DEBUG_NAME "Reading configuration: ")); + + JsonObject top = root[FPSTR(UMOD_NAME)]; + bool configComplete = !top.isNull(); + + /* general settings */ /* DEFAULTS */ + configComplete &= getJsonValue(top[FPSTR(_enabled)], settings.enabled, 1 ); // Usermod enabled per default + configComplete &= getJsonValue(top[FPSTR(_nameI2CAdr)], settings.I2cadress, 0x77 ); // Defalut IC2 adress set to 0x77 (some modules are set to 0x76) + configComplete &= getJsonValue(top[FPSTR(_nameInterval)], settings.Interval, 1 ); // Executed every second + configComplete &= getJsonValue(top[FPSTR(_namePublishChange)], settings.PublischChange, false ); // Publish changed values only + configComplete &= getJsonValue(top[FPSTR(_nameTempScale)], settings.tempScale, 0 ); // Temp sale set to Celsius (1=Fahrenheit) + configComplete &= getJsonValue(top[FPSTR(_nameTempOffset)], settings.tempOffset, 0 ); // Temp offset is set to 0 (Celsius) + configComplete &= getJsonValue(top[FPSTR(_namePubSenState)], settings.publishSensorState, 1 ); // Publish the sensor states + configComplete &= getJsonValue(top[FPSTR(_namePubAc)], settings.pubAcc, 1 ); // Publish accuracy values + configComplete &= getJsonValue(top[FPSTR(_nameHADisc)], settings.HomeAssistantDiscovery, true ); // Activate HomeAssistant Discovery (this Module will be shown as MQTT device in HA) + configComplete &= getJsonValue(top[FPSTR(_namePauseOnActWL)], settings.pauseOnActiveWled, false ); // Pause on active WLED not activated per default + configComplete &= getJsonValue(top[FPSTR(_nameDelCalib)], flags.DeleteCaibration, false ); // IF checked the calibration file will be delete when the save button is pressed + + /* Decimal places */ /* no of digs / -1 means deactivated */ + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameTemp)], settings.decimals.temperature, 1 ); // One decimal places + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameHum)], settings.decimals.humidity, 1 ); + configComplete &= getJsonValue(top["Sensors"][FPSTR(_namePress)], settings.decimals.pressure, 0 ); // Zero decimal places + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameGasRes)], settings.decimals.gasResistance, -1 ); // deavtivated + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameDrewP)], settings.decimals.drewPoint, 1 ); + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameAHum)], settings.decimals.absHumidity, 1 ); + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameIaq)], settings.decimals.iaq, 0 ); // Index for Air Quality Number is active + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameIaqVerb)], settings.PublishIAQVerbal, -1 ); // deactivated - Index for Air Quality (IAQ) verbal classification + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameStaticIaq)], settings.decimals.staticIaq, 0 ); // activated - Static IAQ is better than IAQ for devices that are not moved + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameStaticIaqVerb)], settings.PublishStaticIAQVerbal, 0 ); // activated + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameCo2)], settings.decimals.co2, 0 ); + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameVoc)], settings.decimals.Voc, 0 ); + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameGasPer)], settings.decimals.gasPerc, 0 ); + + DEBUG_PRINTLN(F(OK)); + + /* Set the selected temperature unit */ + if (settings.tempScale) { + tempScale = F(_unitFahrenheit); + } + else { + tempScale = F(_unitCelsius); + } + + if (flags.DeleteCaibration) { + DEBUG_PRINT(F(UMOD_DEBUG_NAME "Deleting Calibration File")); + flags.DeleteCaibration = false; + if (WLED_FS.remove(CALIB_FILE_NAME)) { + DEBUG_PRINTLN(F(OK)); + } + else { + DEBUG_PRINTLN(F(FAIL)); + } + } + + if (settings.Interval < 1) settings.Interval = 1; // Correct interval on need (A number less than 1 is not permitted) + iaqSensor.setTemperatureOffset(settings.tempOffset); // Set Temp Offset + + return configComplete; +} + +/** + * @brief Called by WLED: Retunrs the user modul id number + * + * @return uint16_t User module number + */ +uint16_t UsermodBME68X::getId() { + return USERMOD_ID_BME68X; +} + + +/** + * @brief Returns the current temperature in the scale which is choosen in settings + * @return Temperature value (°C or °F as choosen in settings) +*/ +inline float UsermodBME68X::getTemperature() { + return ValuesPtr->temperature; +} + +/** + * @brief Returns the current humidity + * @return Humididty value (%) +*/ +inline float UsermodBME68X::getHumidity() { + return ValuesPtr->humidity; +} + +/** + * @brief Returns the current pressure + * @return Pressure value (hPa) +*/ +inline float UsermodBME68X::getPressure() { + return ValuesPtr->pressure; +} + +/** + * @brief Returns the current gas resistance + * @return Gas resistance value (kΩ) +*/ +inline float UsermodBME68X::getGasResistance() { + return ValuesPtr->gasResistance; +} + +/** + * @brief Returns the current absolute humidity + * @return Absolute humidity value (g/m³) +*/ +inline float UsermodBME68X::getAbsoluteHumidity() { + return ValuesPtr->absHumidity; +} + +/** + * @brief Returns the current dew point + * @return Dew point (°C or °F as choosen in settings) +*/ +inline float UsermodBME68X::getDewPoint() { + return ValuesPtr->drewPoint; +} + +/** + * @brief Returns the current iaq (Indoor Air Quallity) + * @return Iaq value (0-500) +*/ +inline float UsermodBME68X::getIaq() { + return ValuesPtr->iaq; +} + +/** + * @brief Returns the current static iaq (Indoor Air Quallity) (NOTE: Static iaq is the better choice than iaq for fixed devices such as the wled module) + * @return Static iaq value (float) +*/ +inline float UsermodBME68X::getStaticIaq() { + return ValuesPtr->staticIaq; +} + +/** + * @brief Returns the current co2 + * @return Co2 value (ppm) +*/ +inline float UsermodBME68X::getCo2() { + return ValuesPtr->co2; +} + +/** + * @brief Returns the current voc (Breath VOC concentration estimate [ppm]) + * @return Voc value (ppm) +*/ +inline float UsermodBME68X::getVoc() { + return ValuesPtr->Voc; +} + +/** + * @brief Returns the current gas percentage + * @return Gas percentage value (%) +*/ +inline float UsermodBME68X::getGasPerc() { + return ValuesPtr->gasPerc; +} + +/** + * @brief Returns the current iaq accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated) + * @return Iaq accuracy value (0-3) +*/ +inline uint8_t UsermodBME68X::getIaqAccuracy() { + return ValuesPtr->iaqAccuracy ; +} + +/** + * @brief Returns the current static iaq accuracy accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated) + * @return Static iaq accuracy value (0-3) +*/ +inline uint8_t UsermodBME68X::getStaticIaqAccuracy() { + return ValuesPtr->staticIaqAccuracy; +} + +/** + * @brief Returns the current co2 accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated) + * @return Co2 accuracy value (0-3) +*/ +inline uint8_t UsermodBME68X::getCo2Accuracy() { + return ValuesPtr->co2Accuracy; +} + +/** + * @brief Returns the current voc accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated) + * @return Voc accuracy value (0-3) +*/ +inline uint8_t UsermodBME68X::getVocAccuracy() { + return ValuesPtr->VocAccuracy; +} + +/** + * @brief Returns the current gas percentage accuracy (0 = not calibrated, 2 = being calibrated, 3 = calibrated) + * @return Gas percentage accuracy value (0-3) +*/ +inline uint8_t UsermodBME68X::getGasPercAccuracy() { + return ValuesPtr->gasPercAccuracy; +} + +/** + * @brief Returns the current stab status. + * Indicates when the sensor is ready after after switch-on + * @return stab status value (0 = switched on / 1 = stabilized) +*/ +inline bool UsermodBME68X::getStabStatus() { + return ValuesPtr->stabStatus; +} + +/** + * @brief Returns the current run in status. + * Indicates if the sensor is undergoing initial stabilization during its first use after production + * @return Tun status accuracy value (0 = switched on first time / 1 = stabilized) +*/ +inline bool UsermodBME68X::getRunInStatus() { + return ValuesPtr->runInStatus; +} + + +/** + * @brief Checks whether the library and the sensor are running. + */ +void UsermodBME68X::checkIaqSensorStatus() { + + if (iaqSensor.bsecStatus != BSEC_OK) { + InfoPageStatusLine = "BSEC Library "; + DEBUG_PRINT(UMOD_DEBUG_NAME + InfoPageStatusLine); + flags.InitSuccessful = false; + if (iaqSensor.bsecStatus < BSEC_OK) { + InfoPageStatusLine += " Error Code : " + String(iaqSensor.bsecStatus); + DEBUG_PRINTLN(FAIL); + } + else { + InfoPageStatusLine += " Warning Code : " + String(iaqSensor.bsecStatus); + DEBUG_PRINTLN(WARN); + } + } + else { + InfoPageStatusLine = "Sensor BME68X "; + DEBUG_PRINT(UMOD_DEBUG_NAME + InfoPageStatusLine); + + if (iaqSensor.bme68xStatus != BME68X_OK) { + flags.InitSuccessful = false; + if (iaqSensor.bme68xStatus < BME68X_OK) { + InfoPageStatusLine += "error code: " + String(iaqSensor.bme68xStatus); + DEBUG_PRINTLN(FAIL); + } + else { + InfoPageStatusLine += "warning code: " + String(iaqSensor.bme68xStatus); + DEBUG_PRINTLN(WARN); + } + } + else { + InfoPageStatusLine += F("OK"); + DEBUG_PRINTLN(OK); + } + } +} + +/** + * @brief Loads the calibration data from the file system of the device + */ +void UsermodBME68X::loadState() { + if (WLED_FS.exists(CALIB_FILE_NAME)) { + DEBUG_PRINT(F(UMOD_DEBUG_NAME "Read the calibration file: ")); + File file = WLED_FS.open(CALIB_FILE_NAME, FILE_READ); + if (!file) { + DEBUG_PRINTLN(FAIL); + } + else { + file.read(bsecState, BSEC_MAX_STATE_BLOB_SIZE); + file.close(); + DEBUG_PRINTLN(OK); + iaqSensor.setState(bsecState); + } + } + else { + DEBUG_PRINTLN(F(UMOD_DEBUG_NAME "Calibration file not found.")); + } +} + +/** + * @brief Saves the calibration data from the file system of the device + */ +void UsermodBME68X::saveState() { + DEBUG_PRINT(F(UMOD_DEBUG_NAME "Write the calibration file ")); + File file = WLED_FS.open(CALIB_FILE_NAME, FILE_WRITE); + if (!file) { + DEBUG_PRINTLN(FAIL); + } + else { + iaqSensor.getState(bsecState); + file.write(bsecState, BSEC_MAX_STATE_BLOB_SIZE); + file.close(); + stateUpdateCounter++; + DEBUG_PRINTF("(saved %d times)" OK "\n", stateUpdateCounter); + flags.SaveState = false; // Clear save state flag + + char contbuffer[30]; + + /* Timestamp */ + time_t curr_time; + tm* curr_tm; + time(&curr_time); + curr_tm = localtime(&curr_time); + + snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, UMOD_NAME "/Calib Last Run"); + strftime(contbuffer, 30, "%d %B %Y - %T", curr_tm); + if (WLED_MQTT_CONNECTED) mqtt->publish(charbuffer, 0, false, contbuffer); + + snprintf(contbuffer, 30, "%d", stateUpdateCounter); + snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, UMOD_NAME "/Calib Count"); + if (WLED_MQTT_CONNECTED) mqtt->publish(charbuffer, 0, false, contbuffer); + } +} diff --git a/usermods/Battery/UMBattery.h b/usermods/Battery/UMBattery.h new file mode 100644 index 000000000..8a8ad891e --- /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 000000000..3890f477e 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 000000000..2dff54f88 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 000000000..06235a224 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 000000000..72cca7dc1 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 958bfe526..ddbd114e4 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 999c0a541..c3d3d8bf4 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 000000000..801faee7c --- /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 000000000..bb6a6ef94 --- /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 000000000..ede5ffd88 --- /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 be3d8748b..b36c5f4d6 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,34 +114,95 @@ 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...")); if (batteryPin >= 0 && digitalPinToAnalogChannel(batteryPin) >= 0) - if (pinManager.allocatePin(batteryPin, false, PinOwner::UM_Battery)) { + 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,60 +457,55 @@ 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(F("td=addDropdown('Battery','type');")); // 34 Bytes + oappend(F("addOption(td,'Unkown','0');")); // 28 Bytes + oappend(F("addOption(td,'LiPo','1');")); // 26 Bytes + oappend(F("addOption(td,'LiOn','2');")); // 26 Bytes + oappend(F("addInfo('Battery:type',1,'requires reboot');")); // 81 Bytes + oappend(F("addInfo('Battery:min-voltage',1,'v');")); // 38 Bytes + oappend(F("addInfo('Battery:max-voltage',1,'v');")); // 38 Bytes + oappend(F("addInfo('Battery:interval',1,'ms');")); // 36 Bytes + oappend(F("addInfo('Battery:HA-discovery',1,'');")); // 38 Bytes + oappend(F("addInfo('Battery:auto-off:threshold',1,'%');")); // 45 Bytes + oappend(F("addInfo('Battery:indicator:threshold',1,'%');")); // 46 Bytes + oappend(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 - // oappend(SET_F("bd=addDropdown('Battery:low-power-indicator', 'preset');")); - // the loop generates: oappend(SET_F("addOption(bd, 'preset name', preset id);")); + // this option list would exeed the oappend() buffer + // a list of all presets to select one from + // oappend(F("bd=addDropdown('Battery:low-power-indicator', 'preset');")); + // the loop generates: oappend(F("addOption(bd, 'preset name', preset id);")); // for(int8_t i=1; i < 42; i++) { - // oappend(SET_F("addOption(bd, 'Preset#")); + // oappend(F("addOption(bd, 'Preset#")); // oappendi(i); - // oappend(SET_F("',")); + // oappend(F("',")); // oappendi(i); - // oappend(SET_F(");")); + // oappend(F(");")); // } } - /* + /** * 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) @@ -480,7 +561,7 @@ class UsermodBattery : public Usermod if (newBatteryPin != batteryPin) { // deallocate pin - pinManager.deallocatePin(batteryPin, PinOwner::UM_Battery); + PinManager::deallocatePin(batteryPin, PinOwner::UM_Battery); batteryPin = newBatteryPin; // initialise setup(); @@ -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/EXAMPLE_v2/usermod_v2_example.h b/usermods/EXAMPLE_v2/usermod_v2_example.h index 32374fde2..df05f3e3d 100644 --- a/usermods/EXAMPLE_v2/usermod_v2_example.h +++ b/usermods/EXAMPLE_v2/usermod_v2_example.h @@ -71,7 +71,7 @@ class MyExampleUsermod : public Usermod { // #endif // in setup() // #ifdef USERMOD_EXAMPLE - // UM = (MyExampleUsermod*) usermods.lookup(USERMOD_ID_EXAMPLE); + // UM = (MyExampleUsermod*) UsermodManager::lookup(USERMOD_ID_EXAMPLE); // #endif // somewhere in loop() or other member method // #ifdef USERMOD_EXAMPLE @@ -287,11 +287,11 @@ class MyExampleUsermod : public Usermod { */ void appendConfigData() override { - oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":great")); oappend(SET_F("',1,'(this is a great config value)');")); - oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":testString")); oappend(SET_F("',1,'enter any string you want');")); - oappend(SET_F("dd=addDropdown('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F("','testInt');")); - oappend(SET_F("addOption(dd,'Nothing',0);")); - oappend(SET_F("addOption(dd,'Everything',42);")); + oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":great")); oappend(F("',1,'(this is a great config value)');")); + oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":testString")); oappend(F("',1,'enter any string you want');")); + oappend(F("dd=addDropdown('")); oappend(String(FPSTR(_name)).c_str()); oappend(F("','testInt');")); + oappend(F("addOption(dd,'Nothing',0);")); + oappend(F("addOption(dd,'Everything',42);")); } diff --git a/usermods/Fix_unreachable_netservices_v2/readme.md b/usermods/Fix_unreachable_netservices_v2/readme.md index 006eaf9f9..07d64bc67 100644 --- a/usermods/Fix_unreachable_netservices_v2/readme.md +++ b/usermods/Fix_unreachable_netservices_v2/readme.md @@ -59,10 +59,10 @@ void registerUsermods() * || || || * \/ \/ \/ */ - //usermods.add(new MyExampleUsermod()); - //usermods.add(new UsermodTemperature()); - //usermods.add(new UsermodRenameMe()); - usermods.add(new FixUnreachableNetServices()); + //UsermodManager::add(new MyExampleUsermod()); + //UsermodManager::add(new UsermodTemperature()); + //UsermodManager::add(new UsermodRenameMe()); + UsermodManager::add(new FixUnreachableNetServices()); } ``` diff --git a/usermods/INA226_v2/README.md b/usermods/INA226_v2/README.md new file mode 100644 index 000000000..b1e691618 --- /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 000000000..885b2dd1e --- /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 000000000..52bc3d83f --- /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 000000000..7990f2e8f 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 000000000..d97dc2bea 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 58a9e1939..d574f3abf 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 3989e7668..6d4d4577c 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; @@ -40,6 +50,58 @@ public: #else // ESP32 ESP32S3 and ESP32C3 temperature = roundf(temperatureRead() * 10) / 10; #endif + if(presetToActivate != 0){ + // Check if temperature has exceeded the activation threshold + if (temperature >= activationThreshold) { + // Update the state flag if not already set + if (!isAboveThreshold) { + isAboveThreshold = true; + } + // Check if a 'high temperature' preset is configured and it's not already active + if (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 +142,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(F("addInfo('Internal Temperature:Loop Interval', 1, 'ms');")); + // Display '°C' next to the 'Activation Threshold' setting + oappend(F("addInfo('Internal Temperature:Activation Threshold', 1, '°C');")); + // Display '0 = Disabled' next to the 'Preset To Activate' setting + oappend(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 +178,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 000000000..40463d4b0 --- /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 000000000..4d96b32ff --- /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/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h b/usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h index 393fc2232..03f4c078a 100644 --- a/usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h +++ b/usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h @@ -30,7 +30,7 @@ class LDR_Dusk_Dawn_v2 : public Usermod { void setup() { // register ldrPin if ((ldrPin >= 0) && (digitalPinToAnalogChannel(ldrPin) >= 0)) { - if(!pinManager.allocatePin(ldrPin, false, PinOwner::UM_LDR_DUSK_DAWN)) ldrEnabled = false; // pin already in use -> disable usermod + if(!PinManager::allocatePin(ldrPin, false, PinOwner::UM_LDR_DUSK_DAWN)) ldrEnabled = false; // pin already in use -> disable usermod else pinMode(ldrPin, INPUT); // alloc success -> configure pin for input } else ldrEnabled = false; // invalid pin -> disable usermod initDone = true; @@ -110,7 +110,7 @@ class LDR_Dusk_Dawn_v2 : public Usermod { if (initDone && (ldrPin != oldLdrPin)) { // pin changed - un-register previous pin, register new pin - if (oldLdrPin >= 0) pinManager.deallocatePin(oldLdrPin, PinOwner::UM_LDR_DUSK_DAWN); + if (oldLdrPin >= 0) PinManager::deallocatePin(oldLdrPin, PinOwner::UM_LDR_DUSK_DAWN); setup(); // setup new pin } return configComplete; @@ -139,7 +139,7 @@ class LDR_Dusk_Dawn_v2 : public Usermod { //LDR_Off_Count.add(ldrOffCount); //bool pinValid = ((ldrPin >= 0) && (digitalPinToAnalogChannel(ldrPin) >= 0)); - //if (pinManager.getPinOwner(ldrPin) != PinOwner::UM_LDR_DUSK_DAWN) pinValid = false; + //if (PinManager::getPinOwner(ldrPin) != PinOwner::UM_LDR_DUSK_DAWN) pinValid = false; //JsonArray LDR_valid = user.createNestedArray(F("LDR pin")); //LDR_valid.add(ldrPin); //LDR_valid.add(pinValid ? F(" OK"): F(" invalid")); diff --git a/usermods/MAX17048_v2/readme.md b/usermods/MAX17048_v2/readme.md new file mode 100644 index 000000000..958e6def2 --- /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 000000000..c3a2664ab --- /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/readme.md b/usermods/PIR_sensor_switch/readme.md index 4dfdb07bd..fac5419f0 100644 --- a/usermods/PIR_sensor_switch/readme.md +++ b/usermods/PIR_sensor_switch/readme.md @@ -52,7 +52,7 @@ class MyUsermod : public Usermod { void togglePIRSensor() { #ifdef USERMOD_PIR_SENSOR_SWITCH - PIRsensorSwitch *PIRsensor = (PIRsensorSwitch::*) usermods.lookup(USERMOD_ID_PIRSWITCH); + PIRsensorSwitch *PIRsensor = (PIRsensorSwitch::*) UsermodManager::lookup(USERMOD_ID_PIRSWITCH); if (PIRsensor != nullptr) { PIRsensor->EnablePIRsensor(!PIRsensor->PIRsensorEnabled()); } diff --git a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h index d66b1b333..0deda181c 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 @@ -371,7 +375,7 @@ void PIRsensorSwitch::setup() sensorPinState[i] = LOW; if (PIRsensorPin[i] < 0) continue; // pin retrieved from cfg.json (readFromConfig()) prior to running setup() - if (pinManager.allocatePin(PIRsensorPin[i], false, PinOwner::UM_PIR)) { + if (PinManager::allocatePin(PIRsensorPin[i], false, PinOwner::UM_PIR)) { // PIR Sensor mode INPUT_PULLDOWN #ifdef ESP8266 pinMode(PIRsensorPin[i], PIRsensorPin[i]==16 ? INPUT_PULLDOWN_16 : INPUT_PULLUP); // ESP8266 has INPUT_PULLDOWN on GPIO16 only @@ -507,8 +511,8 @@ void PIRsensorSwitch::addToConfig(JsonObject &root) void PIRsensorSwitch::appendConfigData() { - oappend(SET_F("addInfo('PIRsensorSwitch:HA-discovery',1,'HA=Home Assistant');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('PIRsensorSwitch:override',1,'Cancel timer on change');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('PIRsensorSwitch:HA-discovery',1,'HA=Home Assistant');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('PIRsensorSwitch:override',1,'Cancel timer on change');")); // 0 is field type, 1 is actual field for (int i = 0; i < PIR_SENSOR_MAX_SENSORS; i++) { char str[128]; sprintf_P(str, PSTR("addInfo('PIRsensorSwitch:pin[]',%d,'','#%d');"), i, i); @@ -560,7 +564,7 @@ bool PIRsensorSwitch::readFromConfig(JsonObject &root) DEBUG_PRINTLN(F(" config loaded.")); } else { for (int i = 0; i < PIR_SENSOR_MAX_SENSORS; i++) - if (oldPin[i] >= 0) pinManager.deallocatePin(oldPin[i], PinOwner::UM_PIR); + if (oldPin[i] >= 0) PinManager::deallocatePin(oldPin[i], PinOwner::UM_PIR); setup(); DEBUG_PRINTLN(F(" config (re)loaded.")); } diff --git a/usermods/PWM_fan/usermod_PWM_fan.h b/usermods/PWM_fan/usermod_PWM_fan.h index 1b78cfd4c..c3ef24fe4 100644 --- a/usermods/PWM_fan/usermod_PWM_fan.h +++ b/usermods/PWM_fan/usermod_PWM_fan.h @@ -75,7 +75,7 @@ class PWMFanUsermod : public Usermod { static const char _lock[]; void initTacho(void) { - if (tachoPin < 0 || !pinManager.allocatePin(tachoPin, false, PinOwner::UM_Unspecified)){ + if (tachoPin < 0 || !PinManager::allocatePin(tachoPin, false, PinOwner::UM_Unspecified)){ tachoPin = -1; return; } @@ -88,7 +88,7 @@ class PWMFanUsermod : public Usermod { void deinitTacho(void) { if (tachoPin < 0) return; detachInterrupt(digitalPinToInterrupt(tachoPin)); - pinManager.deallocatePin(tachoPin, PinOwner::UM_Unspecified); + PinManager::deallocatePin(tachoPin, PinOwner::UM_Unspecified); tachoPin = -1; } @@ -111,7 +111,7 @@ class PWMFanUsermod : public Usermod { // https://randomnerdtutorials.com/esp32-pwm-arduino-ide/ void initPWMfan(void) { - if (pwmPin < 0 || !pinManager.allocatePin(pwmPin, true, PinOwner::UM_Unspecified)) { + if (pwmPin < 0 || !PinManager::allocatePin(pwmPin, true, PinOwner::UM_Unspecified)) { enabled = false; pwmPin = -1; return; @@ -121,7 +121,7 @@ class PWMFanUsermod : public Usermod { analogWriteRange(255); analogWriteFreq(WLED_PWM_FREQ); #else - pwmChannel = pinManager.allocateLedc(1); + pwmChannel = PinManager::allocateLedc(1); if (pwmChannel == 255) { //no more free LEDC channels deinitPWMfan(); return; } @@ -136,9 +136,9 @@ class PWMFanUsermod : public Usermod { void deinitPWMfan(void) { if (pwmPin < 0) return; - pinManager.deallocatePin(pwmPin, PinOwner::UM_Unspecified); + PinManager::deallocatePin(pwmPin, PinOwner::UM_Unspecified); #ifdef ARDUINO_ARCH_ESP32 - pinManager.deallocateLedc(pwmChannel, 1); + PinManager::deallocateLedc(pwmChannel, 1); #endif pwmPin = -1; } @@ -191,9 +191,9 @@ class PWMFanUsermod : public Usermod { void setup() override { #ifdef USERMOD_DALLASTEMPERATURE // This Usermod requires Temperature usermod - tempUM = (UsermodTemperature*) usermods.lookup(USERMOD_ID_TEMPERATURE); + tempUM = (UsermodTemperature*) UsermodManager::lookup(USERMOD_ID_TEMPERATURE); #elif defined(USERMOD_SHT) - tempUM = (ShtUsermod*) usermods.lookup(USERMOD_ID_SHT); + tempUM = (ShtUsermod*) UsermodManager::lookup(USERMOD_ID_SHT); #endif initTacho(); initPWMfan(); diff --git a/usermods/SN_Photoresistor/usermod_sn_photoresistor.h b/usermods/SN_Photoresistor/usermod_sn_photoresistor.h index c09b5d907..45cdb66ad 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/SN_Photoresistor/usermods_list.cpp b/usermods/SN_Photoresistor/usermods_list.cpp index 649e19739..a2c6ca165 100644 --- a/usermods/SN_Photoresistor/usermods_list.cpp +++ b/usermods/SN_Photoresistor/usermods_list.cpp @@ -9,6 +9,6 @@ void registerUsermods() { #ifdef USERMOD_SN_PHOTORESISTOR - usermods.add(new Usermod_SN_Photoresistor()); + UsermodManager::add(new Usermod_SN_Photoresistor()); #endif } \ No newline at end of file diff --git a/usermods/ST7789_display/ST7789_display.h b/usermods/ST7789_display/ST7789_display.h index 281fba25d..65f4cae5d 100644 --- a/usermods/ST7789_display/ST7789_display.h +++ b/usermods/ST7789_display/ST7789_display.h @@ -23,6 +23,9 @@ #ifndef TFT_RST #error Please define TFT_RST #endif + #ifndef TFT_CS + #error Please define TFT_CS + #endif #ifndef LOAD_GLCD #error Please define LOAD_GLCD #endif @@ -135,10 +138,10 @@ class St7789DisplayUsermod : public Usermod { void setup() override { PinManagerPinType spiPins[] = { { spi_mosi, true }, { spi_miso, false}, { spi_sclk, true } }; - if (!pinManager.allocateMultiplePins(spiPins, 3, PinOwner::HW_SPI)) { enabled = false; return; } + if (!PinManager::allocateMultiplePins(spiPins, 3, PinOwner::HW_SPI)) { enabled = false; return; } 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); + if (!PinManager::allocateMultiplePins(displayPins, sizeof(displayPins)/sizeof(PinManagerPinType), PinOwner::UM_FourLineDisplay)) { + PinManager::deallocateMultiplePins(spiPins, 3, PinOwner::HW_SPI); enabled = false; return; } @@ -374,10 +377,10 @@ class St7789DisplayUsermod : public Usermod { void appendConfigData() override { - oappend(SET_F("addInfo('ST7789:pin[]',0,'','SPI CS');")); - oappend(SET_F("addInfo('ST7789:pin[]',1,'','SPI DC');")); - oappend(SET_F("addInfo('ST7789:pin[]',2,'','SPI RST');")); - oappend(SET_F("addInfo('ST7789:pin[]',2,'','SPI BL');")); + oappend(F("addInfo('ST7789:pin[]',0,'','SPI CS');")); + oappend(F("addInfo('ST7789:pin[]',1,'','SPI DC');")); + oappend(F("addInfo('ST7789:pin[]',2,'','SPI RST');")); + oappend(F("addInfo('ST7789:pin[]',3,'','SPI BL');")); } /* diff --git a/usermods/Temperature/platformio_override.ini b/usermods/Temperature/platformio_override.ini index 0e354da9e..cc86367fd 100644 --- a/usermods/Temperature/platformio_override.ini +++ b/usermods/Temperature/platformio_override.ini @@ -7,6 +7,4 @@ extends = env:d1_mini build_flags = ${common.build_flags_esp8266} -D USERMOD_DALLASTEMPERATURE lib_deps = ${env.lib_deps} - paulstoffregen/OneWire@~2.3.7 -# you may want to use following with ESP32 -; https://github.com/blazoncek/OneWire.git # fixes Sensor error on ESP32 \ No newline at end of file + paulstoffregen/OneWire@~2.3.8 \ No newline at end of file diff --git a/usermods/Temperature/readme.md b/usermods/Temperature/readme.md index b41e3e119..3d65be7eb 100644 --- a/usermods/Temperature/readme.md +++ b/usermods/Temperature/readme.md @@ -43,10 +43,8 @@ default_envs = d1_mini ... lib_deps = ... - #For Dallas sensor uncomment following line - OneWire@~2.3.7 - # ... or you may want to use following with ESP32 -; https://github.com/blazoncek/OneWire.git # fixes Sensor error on ESP32... + #For Dallas sensor uncomment following + paulstoffregen/OneWire @ ~2.3.8 ``` ## Change Log @@ -56,8 +54,14 @@ lib_deps = * Do not report erroneous low temperatures to MQTT * Disable plugin if temperature sensor not detected * Report the number of seconds until the first read in the info screen instead of sensor error + 2021-04 * Adaptation for runtime configuration. + 2023-05 * Rewrite to conform to newer recommendations. -* Recommended @blazoncek fork of OneWire for ESP32 to avoid Sensor error \ No newline at end of file +* Recommended @blazoncek fork of OneWire for ESP32 to avoid Sensor error + +2024-09 +* Update OneWire to version 2.3.8, which includes stickbreaker's and garyd9's ESP32 fixes: + blazoncek's fork is no longer needed \ No newline at end of file diff --git a/usermods/Temperature/usermod_temperature.h b/usermods/Temperature/usermod_temperature.h index 5b6b21d8c..178bc05a0 100644 --- a/usermods/Temperature/usermod_temperature.h +++ b/usermods/Temperature/usermod_temperature.h @@ -17,6 +17,8 @@ #define USERMOD_DALLASTEMPERATURE_MEASUREMENT_INTERVAL 60000 #endif +static uint16_t mode_temperature(); + class UsermodTemperature : public Usermod { private: @@ -60,6 +62,7 @@ class UsermodTemperature : public Usermod { static const char _sensor[]; static const char _temperature[]; static const char _Temperature[]; + static const char _data_fx[]; //Dallas sensor quick (& dirty) reading. Credit to - Author: Peter Scargill, August 17th, 2013 float readDallas(); @@ -70,8 +73,13 @@ class UsermodTemperature : public Usermod { void publishHomeAssistantAutodiscovery(); #endif + static UsermodTemperature* _instance; // to overcome nonstatic getTemperatureC() method and avoid UsermodManager::lookup(USERMOD_ID_TEMPERATURE); + public: + UsermodTemperature() { _instance = this; } + static UsermodTemperature *getInstance() { return UsermodTemperature::_instance; } + /* * API calls te enable data exchange between WLED modules */ @@ -113,7 +121,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)); } @@ -133,7 +141,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; } @@ -215,14 +223,14 @@ void UsermodTemperature::setup() { // config says we are enabled DEBUG_PRINTLN(F("Allocating temperature pin...")); // pin retrieved from cfg.json (readFromConfig()) prior to running setup() - if (temperaturePin >= 0 && pinManager.allocatePin(temperaturePin, true, PinOwner::UM_Temperature)) { + if (temperaturePin >= 0 && PinManager::allocatePin(temperaturePin, true, PinOwner::UM_Temperature)) { oneWire = new OneWire(temperaturePin); if (oneWire->reset()) { while (!findSensor() && retries--) { delay(25); // try to find sensor } } - if (parasite && pinManager.allocatePin(parasitePin, true, PinOwner::UM_Temperature)) { + if (parasite && PinManager::allocatePin(parasitePin, true, PinOwner::UM_Temperature)) { pinMode(parasitePin, OUTPUT); digitalWrite(parasitePin, LOW); // deactivate power (close MOSFET) } else { @@ -234,6 +242,7 @@ void UsermodTemperature::setup() { } temperaturePin = -1; // allocation failed } + if (sensorFound && !initDone) strip.addEffect(255, &mode_temperature, _data_fx); } lastMeasurement = millis() - readingInterval + 10000; initDone = true; @@ -414,9 +423,9 @@ bool UsermodTemperature::readFromConfig(JsonObject &root) { DEBUG_PRINTLN(F("Re-init temperature.")); // deallocate pin and release memory delete oneWire; - pinManager.deallocatePin(temperaturePin, PinOwner::UM_Temperature); + PinManager::deallocatePin(temperaturePin, PinOwner::UM_Temperature); temperaturePin = newTemperaturePin; - pinManager.deallocatePin(parasitePin, PinOwner::UM_Temperature); + PinManager::deallocatePin(parasitePin, PinOwner::UM_Temperature); // initialise setup(); } @@ -426,10 +435,10 @@ bool UsermodTemperature::readFromConfig(JsonObject &root) { } void UsermodTemperature::appendConfigData() { - oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":")); oappend(String(FPSTR(_parasite)).c_str()); - oappend(SET_F("',1,'(if no Vcc connected)');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(SET_F(":")); oappend(String(FPSTR(_parasitePin)).c_str()); - oappend(SET_F("',1,'(for external MOSFET)');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":")); oappend(String(FPSTR(_parasite)).c_str()); + oappend(F("',1,'(if no Vcc connected)');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('")); oappend(String(FPSTR(_name)).c_str()); oappend(F(":")); oappend(String(FPSTR(_parasitePin)).c_str()); + oappend(F("',1,'(for external MOSFET)');")); // 0 is field type, 1 is actual field } float UsermodTemperature::getTemperature() { @@ -440,6 +449,8 @@ const char *UsermodTemperature::getTemperatureUnit() { return degC ? "°C" : "°F"; } +UsermodTemperature* UsermodTemperature::_instance = nullptr; + // strings to reduce flash memory usage (used more than twice) const char UsermodTemperature::_name[] PROGMEM = "Temperature"; const char UsermodTemperature::_enabled[] PROGMEM = "enabled"; @@ -449,4 +460,14 @@ 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 +const char UsermodTemperature::_Temperature[] PROGMEM = "/temperature"; +const char UsermodTemperature::_data_fx[] PROGMEM = "Temperature@Min,Max;;!;01;pal=54,sx=255,ix=0"; + +static uint16_t mode_temperature() { + float low = roundf(mapf((float)SEGMENT.speed, 0.f, 255.f, -150.f, 150.f)); // default: 15°C, range: -15°C to 15°C + float high = roundf(mapf((float)SEGMENT.intensity, 0.f, 255.f, 300.f, 600.f)); // default: 30°C, range 30°C to 60°C + float temp = constrain(UsermodTemperature::getInstance()->getTemperatureC()*10.f, low, high); // get a little better resolution (*10) + unsigned i = map(roundf(temp), (unsigned)low, (unsigned)high, 0, 248); + SEGMENT.fill(SEGMENT.color_from_palette(i, false, false, 255)); + return FRAMETIME; +} diff --git a/usermods/TetrisAI_v2/gridbw.h b/usermods/TetrisAI_v2/gridbw.h new file mode 100644 index 000000000..deea027d7 --- /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 000000000..815c2a560 --- /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 000000000..5d461615a --- /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 000000000..88320818e --- /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 000000000..b56f801a8 --- /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 000000000..ba4fe60e4 --- /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 000000000..e4766d18b --- /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 000000000..592dac6c7 --- /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 000000000..0f7039dac --- /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 61915170c..9c463e0a1 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.getFrameTime() +static bool udpSamplePeak = false; // Boolean flag for peak. Set at the same time as samplePeak, but reset by transmitAudioData +static unsigned long timeOfPeak = 0; // time of last sample peak detection. +static uint8_t fftResult[NUM_GEQ_CHANNELS]= {0};// Our calculated freq. channel result table to be used by effects + +// 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,47 +142,24 @@ 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 // //////////////////// // some prototypes, to ensure consistent interfaces -static float mapf(float x, float in_min, float in_max, float out_min, float out_max); // map function for float static float fftAddAvg(int from, int to); // average of several FFT result bins void FFTcode(void * parameter); // audio processing task: read samples, run FFT, fill GEQ channels from FFT results 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; @@ -181,9 +191,8 @@ constexpr uint16_t samplesFFT_2 = 256; // meaningfull part of FFT resul #define LOG_256 5.54517744f // log(256) // 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 -static float windowWeighingFactors[samplesFFT] = {0.0f}; +static float* vReal = nullptr; // FFT sample inputs / freq output - these are our raw result bins +static float* vImag = nullptr; // imaginary parts // Create FFT object // lib_deps += https://github.com/kosme/arduinoFFT#develop @ 1.9.2 @@ -191,20 +200,11 @@ static float windowWeighingFactors[samplesFFT] = {0.0f}; // #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 - -static ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, windowWeighingFactors); +// #define sqrt_internal sqrtf // see https://github.com/kosme/arduinoFFT/pull/83 - since v2.0.0 this must be done in build_flags +#include // FFT object is created in FFTcode // Helper functions -// float version of map() -static float mapf(float x, float in_min, float in_max, float out_min, float out_max){ - return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; -} - // compute average of several FFT result bins static float fftAddAvg(int from, int to) { float result = 0.0f; @@ -221,6 +221,18 @@ void FFTcode(void * parameter) { DEBUGSR_PRINT("FFT started on core: "); DEBUGSR_PRINTLN(xPortGetCoreID()); + // allocate FFT buffers on first call + if (vReal == nullptr) vReal = (float*) calloc(sizeof(float), samplesFFT); + if (vImag == nullptr) vImag = (float*) calloc(sizeof(float), samplesFFT); + if ((vReal == nullptr) || (vImag == nullptr)) { + // something went wrong + if (vReal) free(vReal); vReal = nullptr; + if (vImag) free(vImag); vImag = nullptr; + return; + } + // Create FFT object with weighing factor storage + ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, true); + // see https://www.freertos.org/vtaskdelayuntil.html const TickType_t xFrequency = FFT_MIN_CYCLE * portTICK_PERIOD_MS; @@ -242,6 +254,7 @@ void FFTcode(void * parameter) // get a fresh batch of samples from I2S if (audioSource) audioSource->getSamples(vReal, samplesFFT); + memset(vImag, 0, samplesFFT * sizeof(float)); // set imaginary parts to 0 #if defined(WLED_DEBUG) || defined(SR_DEBUG) if (start < esp_timer_get_time()) { // filter out overflows @@ -260,8 +273,6 @@ void FFTcode(void * parameter) // find highest sample in the batch float maxSample = 0.0f; // max sample from FFT batch for (int i=0; i < samplesFFT; i++) { - // set imaginary parts to 0 - vImag[i] = 0; // pick our our current mic sample - we take the max value from all samples that go into FFT if ((vReal[i] <= (INT16_MAX - 1024)) && (vReal[i] >= (INT16_MIN + 1024))) //skip extreme values - normally these are artefacts if (fabsf((float)vReal[i]) > maxSample) maxSample = fabsf((float)vReal[i]); @@ -282,6 +293,7 @@ void FFTcode(void * parameter) //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 + 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.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 @@ -291,7 +303,7 @@ void FFTcode(void * parameter) #endif } else { // noise gate closed - only clear results as FFT was skipped. MIC samples are still valid when we do this. - memset(vReal, 0, sizeof(vReal)); + memset(vReal, 0, samplesFFT * sizeof(float)); FFT_MajorPeak = 1; FFT_Magnitude = 0.001; } @@ -521,9 +533,11 @@ static void detectSamplePeak(void) { } } +#endif + static void autoResetPeak(void) { - uint16_t MinShowDelay = MAX(50, strip.getMinShowDelay()); // Fixes private class variable compiler error. Unsure if this is the correct way of fixing the root problem. -THATDONFC - if (millis() - timeOfPeak > MinShowDelay) { // Auto-reset of samplePeak after a complete frame has passed. + uint16_t peakDelay = max(uint16_t(50), strip.getFrameTime()); + if (millis() - timeOfPeak > peakDelay) { // Auto-reset of samplePeak after at least one complete frame has passed. samplePeak = false; if (audioSyncEnabled == 0) udpSamplePeak = false; // this is normally reset by transmitAudioData } @@ -538,6 +552,8 @@ static void autoResetPeak(void) { class AudioReactive : public Usermod { private: +#ifdef ARDUINO_ARCH_ESP32 + #ifndef AUDIOPIN int8_t audioPin = -1; #else @@ -569,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 @@ -595,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; @@ -612,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. @@ -624,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 @@ -644,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[]; @@ -671,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 @@ -731,6 +761,7 @@ class AudioReactive : public Usermod { } // logAudio() +#ifdef ARDUINO_ARCH_ESP32 ////////////////////// // Audio Processing // ////////////////////// @@ -901,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) @@ -947,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; @@ -967,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); @@ -983,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) { @@ -1021,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(); @@ -1048,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 @@ -1112,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) @@ -1121,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 @@ -1184,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.")); @@ -1196,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; @@ -1215,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); @@ -1254,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)); @@ -1262,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)); @@ -1274,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 @@ -1313,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 @@ -1346,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 @@ -1353,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(); } @@ -1372,7 +1443,7 @@ class AudioReactive : public Usermod { return true; } - +#ifdef ARDUINO_ARCH_ESP32 void onUpdateBegin(bool init) override { #ifdef WLED_DEBUG @@ -1421,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. @@ -1441,7 +1535,7 @@ class AudioReactive : public Usermod { return false; } - +#endif //////////////////////////// // Settings and Info Page // //////////////////////////// @@ -1453,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"); @@ -1471,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) { @@ -1487,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 @@ -1503,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())) { @@ -1547,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) { @@ -1566,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"); @@ -1582,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 } } @@ -1620,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 @@ -1678,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; @@ -1696,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; @@ -1735,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 @@ -1758,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); @@ -1776,53 +1885,61 @@ class AudioReactive : public Usermod { } - void appendConfigData() override + void appendConfigData(Print& uiScript) override { - oappend(SET_F("dd=addDropdown('AudioReactive','digitalmic:type');")); + uiScript.print(F("ux='AudioReactive';")); // ux = shortcut for Audioreactive - fingers crossed that "ux" isn't already used as JS var, html post parameter or css style +#ifdef ARDUINO_ARCH_ESP32 + uiScript.print(F("uxp=ux+':digitalmic:pin[]';")); // uxp = shortcut for AudioReactive:digitalmic:pin[] + uiScript.print(F("dd=addDropdown(ux,'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);")); + uiScript.print(F("addOption(dd,'Generic Analog',0);")); #endif - oappend(SET_F("addOption(dd,'Generic I2S',1);")); - oappend(SET_F("addOption(dd,'ES7243',2);")); - oappend(SET_F("addOption(dd,'SPH0654',3);")); - oappend(SET_F("addOption(dd,'Generic I2S with Mclk',4);")); + uiScript.print(F("addOption(dd,'Generic I2S',1);")); + uiScript.print(F("addOption(dd,'ES7243',2);")); + uiScript.print(F("addOption(dd,'SPH0654',3);")); + uiScript.print(F("addOption(dd,'Generic I2S with Mclk',4);")); #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - oappend(SET_F("addOption(dd,'Generic I2S PDM',5);")); + uiScript.print(F("addOption(dd,'Generic I2S PDM',5);")); #endif - oappend(SET_F("addOption(dd,'ES8388',6);")); + uiScript.print(F("addOption(dd,'ES8388',6);")); - oappend(SET_F("dd=addDropdown('AudioReactive','config:AGC');")); - oappend(SET_F("addOption(dd,'Off',0);")); - oappend(SET_F("addOption(dd,'Normal',1);")); - oappend(SET_F("addOption(dd,'Vivid',2);")); - oappend(SET_F("addOption(dd,'Lazy',3);")); + uiScript.print(F("dd=addDropdown(ux,'config:AGC');")); + uiScript.print(F("addOption(dd,'Off',0);")); + uiScript.print(F("addOption(dd,'Normal',1);")); + uiScript.print(F("addOption(dd,'Vivid',2);")); + uiScript.print(F("addOption(dd,'Lazy',3);")); - oappend(SET_F("dd=addDropdown('AudioReactive','dynamics:limiter');")); - oappend(SET_F("addOption(dd,'Off',0);")); - oappend(SET_F("addOption(dd,'On',1);")); - oappend(SET_F("addInfo('AudioReactive:dynamics:limiter',0,' On ');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('AudioReactive:dynamics:rise',1,'ms (♪ effects only)');")); - oappend(SET_F("addInfo('AudioReactive:dynamics:fall',1,'ms (♪ effects only)');")); + uiScript.print(F("dd=addDropdown(ux,'dynamics:limiter');")); + uiScript.print(F("addOption(dd,'Off',0);")); + uiScript.print(F("addOption(dd,'On',1);")); + uiScript.print(F("addInfo(ux+':dynamics:limiter',0,' On ');")); // 0 is field type, 1 is actual field + uiScript.print(F("addInfo(ux+':dynamics:rise',1,'ms (♪ effects only)');")); + uiScript.print(F("addInfo(ux+':dynamics:fall',1,'ms (♪ effects only)');")); - oappend(SET_F("dd=addDropdown('AudioReactive','frequency:scale');")); - oappend(SET_F("addOption(dd,'None',0);")); - 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);")); + uiScript.print(F("dd=addDropdown(ux,'frequency:scale');")); + uiScript.print(F("addOption(dd,'None',0);")); + uiScript.print(F("addOption(dd,'Linear (Amplitude)',2);")); + uiScript.print(F("addOption(dd,'Square Root (Energy)',3);")); + uiScript.print(F("addOption(dd,'Logarithmic (Loudness)',1);")); +#endif - oappend(SET_F("dd=addDropdown('AudioReactive','sync:mode');")); - oappend(SET_F("addOption(dd,'Off',0);")); - oappend(SET_F("addOption(dd,'Send',1);")); - oappend(SET_F("addOption(dd,'Receive',2);")); - 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');")); - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',2,'sck/bclk','I2S SCK');")); + uiScript.print(F("dd=addDropdown(ux,'sync:mode');")); + uiScript.print(F("addOption(dd,'Off',0);")); +#ifdef ARDUINO_ARCH_ESP32 + uiScript.print(F("addOption(dd,'Send',1);")); +#endif + uiScript.print(F("addOption(dd,'Receive',2);")); +#ifdef ARDUINO_ARCH_ESP32 + uiScript.print(F("addInfo(ux+':digitalmic:type',1,'requires reboot!');")); // 0 is field type, 1 is actual field + uiScript.print(F("addInfo(uxp,0,'sd/data/dout','I2S SD');")); + uiScript.print(F("addInfo(uxp,1,'ws/clk/lrck','I2S WS');")); + uiScript.print(F("addInfo(uxp,2,'sck/bclk','I2S SCK');")); #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',3,'only use -1, 0, 1 or 3','I2S MCLK');")); + uiScript.print(F("addInfo(uxp,3,'only use -1, 0, 1 or 3','I2S MCLK');")); #else - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',3,'master clock','I2S MCLK');")); + uiScript.print(F("addInfo(uxp,3,'master clock','I2S MCLK');")); #endif +#endif } @@ -1901,8 +2018,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) @@ -194,8 +194,8 @@ class I2SSource : public AudioSource { virtual void initialize(int8_t i2swsPin = I2S_PIN_NO_CHANGE, int8_t i2ssdPin = I2S_PIN_NO_CHANGE, int8_t i2sckPin = I2S_PIN_NO_CHANGE, int8_t mclkPin = I2S_PIN_NO_CHANGE) { DEBUGSR_PRINTLN(F("I2SSource:: initialize().")); if (i2swsPin != I2S_PIN_NO_CHANGE && i2ssdPin != I2S_PIN_NO_CHANGE) { - if (!pinManager.allocatePin(i2swsPin, true, PinOwner::UM_Audioreactive) || - !pinManager.allocatePin(i2ssdPin, false, PinOwner::UM_Audioreactive)) { // #206 + if (!PinManager::allocatePin(i2swsPin, true, PinOwner::UM_Audioreactive) || + !PinManager::allocatePin(i2ssdPin, false, PinOwner::UM_Audioreactive)) { // #206 DEBUGSR_PRINTF("\nAR: Failed to allocate I2S pins: ws=%d, sd=%d\n", i2swsPin, i2ssdPin); return; } @@ -203,7 +203,7 @@ class I2SSource : public AudioSource { // i2ssckPin needs special treatment, since it might be unused on PDM mics if (i2sckPin != I2S_PIN_NO_CHANGE) { - if (!pinManager.allocatePin(i2sckPin, true, PinOwner::UM_Audioreactive)) { + if (!PinManager::allocatePin(i2sckPin, true, PinOwner::UM_Audioreactive)) { DEBUGSR_PRINTF("\nAR: Failed to allocate I2S pins: sck=%d\n", i2sckPin); return; } @@ -249,7 +249,7 @@ class I2SSource : public AudioSource { // Reserve the master clock pin if provided _mclkPin = mclkPin; if (mclkPin != I2S_PIN_NO_CHANGE) { - if(!pinManager.allocatePin(mclkPin, true, PinOwner::UM_Audioreactive)) { + if(!PinManager::allocatePin(mclkPin, true, PinOwner::UM_Audioreactive)) { DEBUGSR_PRINTF("\nAR: Failed to allocate I2S pin: MCLK=%d\n", mclkPin); return; } else @@ -307,11 +307,11 @@ class I2SSource : public AudioSource { DEBUGSR_PRINTF("Failed to uninstall i2s driver: %d\n", err); return; } - if (_pinConfig.ws_io_num != I2S_PIN_NO_CHANGE) pinManager.deallocatePin(_pinConfig.ws_io_num, PinOwner::UM_Audioreactive); - if (_pinConfig.data_in_num != I2S_PIN_NO_CHANGE) pinManager.deallocatePin(_pinConfig.data_in_num, PinOwner::UM_Audioreactive); - if (_pinConfig.bck_io_num != I2S_PIN_NO_CHANGE) pinManager.deallocatePin(_pinConfig.bck_io_num, PinOwner::UM_Audioreactive); + if (_pinConfig.ws_io_num != I2S_PIN_NO_CHANGE) PinManager::deallocatePin(_pinConfig.ws_io_num, PinOwner::UM_Audioreactive); + if (_pinConfig.data_in_num != I2S_PIN_NO_CHANGE) PinManager::deallocatePin(_pinConfig.data_in_num, PinOwner::UM_Audioreactive); + if (_pinConfig.bck_io_num != I2S_PIN_NO_CHANGE) PinManager::deallocatePin(_pinConfig.bck_io_num, PinOwner::UM_Audioreactive); // Release the master clock pin - if (_mclkPin != I2S_PIN_NO_CHANGE) pinManager.deallocatePin(_mclkPin, PinOwner::UM_Audioreactive); + if (_mclkPin != I2S_PIN_NO_CHANGE) PinManager::deallocatePin(_mclkPin, PinOwner::UM_Audioreactive); } virtual void getSamples(float *buffer, uint16_t num_samples) { @@ -589,7 +589,7 @@ class I2SAdcSource : public I2SSource { void initialize(int8_t audioPin, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE, int8_t = I2S_PIN_NO_CHANGE) { DEBUGSR_PRINTLN(F("I2SAdcSource:: initialize().")); _myADCchannel = 0x0F; - if(!pinManager.allocatePin(audioPin, false, PinOwner::UM_Audioreactive)) { + if(!PinManager::allocatePin(audioPin, false, PinOwner::UM_Audioreactive)) { DEBUGSR_PRINTF("failed to allocate GPIO for audio analog input: %d\n", audioPin); return; } @@ -717,7 +717,7 @@ class I2SAdcSource : public I2SSource { } void deinitialize() { - pinManager.deallocatePin(_audioPin, PinOwner::UM_Audioreactive); + PinManager::deallocatePin(_audioPin, PinOwner::UM_Audioreactive); _initialized = false; _myADCchannel = 0x0F; @@ -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 47804b611..aad269c67 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 -D sqrt_internal=sqrtf` +* `lib_deps`= `kosme/arduinoFFT @ 2.0.1` ## Configuration diff --git a/usermods/boblight/boblight.h b/usermods/boblight/boblight.h index 32208a4fa..b04b78fac 100644 --- a/usermods/boblight/boblight.h +++ b/usermods/boblight/boblight.h @@ -1,459 +1,459 @@ -#pragma once - -#include "wled.h" - -/* - * Usermod that implements BobLight "ambilight" protocol - * - * See the accompanying README.md file for more info. - */ - -#ifndef BOB_PORT - #define BOB_PORT 19333 // Default boblightd port -#endif - -class BobLightUsermod : public Usermod { - typedef struct _LIGHT { - char lightname[5]; - float hscan[2]; - float vscan[2]; - } light_t; - - private: - unsigned long lastTime = 0; - bool enabled = false; - bool initDone = false; - - light_t *lights = nullptr; - uint16_t numLights = 0; // 16 + 9 + 16 + 9 - uint16_t top, bottom, left, right; // will be filled in readFromConfig() - uint16_t pct; - - WiFiClient bobClient; - WiFiServer *bob; - uint16_t bobPort = BOB_PORT; - - static const char _name[]; - static const char _enabled[]; - - /* - # boblight - # Copyright (C) Bob 2009 - # - # makeboblight.sh created by Adam Boeglin - # - # boblight is free software: you can redistribute it and/or modify it - # under the terms of the GNU General Public License as published by the - # Free Software Foundation, either version 3 of the License, or - # (at your option) any later version. - # - # boblight 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 General Public License for more details. - # - # You should have received a copy of the GNU General Public License along - # with this program. If not, see . - */ - - // fills the lights[] array with position & depth of scan for each LED - void fillBobLights(int bottom, int left, int top, int right, float pct_scan) { - - int lightcount = 0; - int total = top+left+right+bottom; - int bcount; - - if (total > strip.getLengthTotal()) { - DEBUG_PRINTLN(F("BobLight: Too many lights.")); - return; - } - - // start left part of bottom strip (clockwise direction, 1st half) - if (bottom > 0) { - bcount = 1; - float brange = 100.0/bottom; - float bcurrent = 50.0; - if (bottom < top) { - int diff = top - bottom; - brange = 100.0/top; - bcurrent -= (diff/2)*brange; - } - while (bcount <= bottom/2) { - float btop = bcurrent - brange; - String name = "b"+String(bcount); - strncpy(lights[lightcount].lightname, name.c_str(), 4); - lights[lightcount].hscan[0] = btop; - lights[lightcount].hscan[1] = bcurrent; - lights[lightcount].vscan[0] = 100 - pct_scan; - lights[lightcount].vscan[1] = 100; - lightcount+=1; - bcurrent = btop; - bcount+=1; - } - } - - // left side - if (left > 0) { - int lcount = 1; - float lrange = 100.0/left; - float lcurrent = 100.0; - while (lcount <= left) { - float ltop = lcurrent - lrange; - String name = "l"+String(lcount); - strncpy(lights[lightcount].lightname, name.c_str(), 4); - lights[lightcount].hscan[0] = 0; - lights[lightcount].hscan[1] = pct_scan; - lights[lightcount].vscan[0] = ltop; - lights[lightcount].vscan[1] = lcurrent; - lightcount+=1; - lcurrent = ltop; - lcount+=1; - } - } - - // top side - if (top > 0) { - int tcount = 1; - float trange = 100.0/top; - float tcurrent = 0; - while (tcount <= top) { - float ttop = tcurrent + trange; - String name = "t"+String(tcount); - strncpy(lights[lightcount].lightname, name.c_str(), 4); - lights[lightcount].hscan[0] = tcurrent; - lights[lightcount].hscan[1] = ttop; - lights[lightcount].vscan[0] = 0; - lights[lightcount].vscan[1] = pct_scan; - lightcount+=1; - tcurrent = ttop; - tcount+=1; - } - } - - // right side - if (right > 0) { - int rcount = 1; - float rrange = 100.0/right; - float rcurrent = 0; - while (rcount <= right) { - float rtop = rcurrent + rrange; - String name = "r"+String(rcount); - strncpy(lights[lightcount].lightname, name.c_str(), 4); - lights[lightcount].hscan[0] = 100-pct_scan; - lights[lightcount].hscan[1] = 100; - lights[lightcount].vscan[0] = rcurrent; - lights[lightcount].vscan[1] = rtop; - lightcount+=1; - rcurrent = rtop; - rcount+=1; - } - } - - // right side of bottom strip (2nd half) - if (bottom > 0) { - float brange = 100.0/bottom; - float bcurrent = 100; - if (bottom < top) { - brange = 100.0/top; - } - while (bcount <= bottom) { - float btop = bcurrent - brange; - String name = "b"+String(bcount); - strncpy(lights[lightcount].lightname, name.c_str(), 4); - lights[lightcount].hscan[0] = btop; - lights[lightcount].hscan[1] = bcurrent; - lights[lightcount].vscan[0] = 100 - pct_scan; - lights[lightcount].vscan[1] = 100; - lightcount+=1; - bcurrent = btop; - bcount+=1; - } - } - - numLights = lightcount; - - #if WLED_DEBUG - DEBUG_PRINTLN(F("Fill light data: ")); - DEBUG_PRINTF_P(PSTR(" lights %d\n"), numLights); - for (int i=0; i strip.getLengthTotal() ) { - DEBUG_PRINTLN(F("BobLight: Too many lights.")); - DEBUG_PRINTF_P(PSTR("%d+%d+%d+%d>%d\n"), bottom, left, top, right, strip.getLengthTotal()); - totalLights = strip.getLengthTotal(); - top = bottom = (uint16_t) roundf((float)totalLights * 16.0f / 50.0f); - left = right = (uint16_t) roundf((float)totalLights * 9.0f / 50.0f); - } - lights = new light_t[totalLights]; - if (lights) fillBobLights(bottom, left, top, right, float(pct)); // will fill numLights - else enable(false); - initDone = true; - } - - void connected() override { - // we can only start server when WiFi is connected - if (!bob) bob = new WiFiServer(bobPort, 1); - bob->begin(); - bob->setNoDelay(true); - } - - void loop() override { - if (!enabled || strip.isUpdating()) return; - if (millis() - lastTime > 10) { - lastTime = millis(); - pollBob(); - } - } - - void enable(bool en) { enabled = en; } - -#ifndef WLED_DISABLE_MQTT - /** - * handling of MQTT message - * topic only contains stripped topic (part after /wled/MAC) - * topic should look like: /swipe with amessage of [up|down] - */ - bool onMqttMessage(char* topic, char* payload) override { - //if (strlen(topic) == 6 && strncmp_P(topic, PSTR("/subtopic"), 6) == 0) { - // String action = payload; - // if (action == "on") { - // enable(true); - // return true; - // } else if (action == "off") { - // enable(false); - // return true; - // } - //} - return false; - } - - /** - * subscribe to MQTT topic for controlling usermod - */ - void onMqttConnect(bool sessionPresent) override { - //char subuf[64]; - //if (mqttDeviceTopic[0] != 0) { - // strcpy(subuf, mqttDeviceTopic); - // strcat_P(subuf, PSTR("/subtopic")); - // mqtt->subscribe(subuf, 0); - //} - } -#endif - - void addToJsonInfo(JsonObject& root) override - { - JsonObject user = root["u"]; - if (user.isNull()) user = root.createNestedObject("u"); - - JsonArray infoArr = user.createNestedArray(FPSTR(_name)); - String uiDomString = F(""); - infoArr.add(uiDomString); - } - - /* - * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - void addToJsonState(JsonObject& root) override - { - } - - /* - * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - void readFromJsonState(JsonObject& root) override { - if (!initDone) return; // prevent crash on boot applyPreset() - bool en = enabled; - JsonObject um = root[FPSTR(_name)]; - if (!um.isNull()) { - if (um[FPSTR(_enabled)].is()) { - en = um[FPSTR(_enabled)].as(); - } else { - String str = um[FPSTR(_enabled)]; // checkbox -> off or on - en = (bool)(str!="off"); // off is guaranteed to be present - } - if (en != enabled && lights) { - enable(en); - if (!enabled && bob && bob->hasClient()) { - if (bobClient) bobClient.stop(); - bobClient = bob->available(); - BobClear(); - exitRealtime(); - } - } - } - } - - void appendConfigData() override { - //oappend(SET_F("dd=addDropdown('usermod','selectfield');")); - //oappend(SET_F("addOption(dd,'1st value',0);")); - //oappend(SET_F("addOption(dd,'2nd value',1);")); - oappend(SET_F("addInfo('BobLight:top',1,'LEDs');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('BobLight:bottom',1,'LEDs');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('BobLight:left',1,'LEDs');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('BobLight:right',1,'LEDs');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('BobLight:pct',1,'Depth of scan [%]');")); // 0 is field type, 1 is actual field - } - - void addToConfig(JsonObject& root) override { - JsonObject umData = root.createNestedObject(FPSTR(_name)); - umData[FPSTR(_enabled)] = enabled; - umData[ "port" ] = bobPort; - umData[F("top")] = top; - umData[F("bottom")] = bottom; - umData[F("left")] = left; - umData[F("right")] = right; - umData[F("pct")] = pct; - } - - bool readFromConfig(JsonObject& root) override { - JsonObject umData = root[FPSTR(_name)]; - bool configComplete = !umData.isNull(); - - bool en = enabled; - configComplete &= getJsonValue(umData[FPSTR(_enabled)], en); - enable(en); - - configComplete &= getJsonValue(umData[ "port" ], bobPort); - configComplete &= getJsonValue(umData[F("bottom")], bottom, 16); - configComplete &= getJsonValue(umData[F("top")], top, 16); - configComplete &= getJsonValue(umData[F("left")], left, 9); - configComplete &= getJsonValue(umData[F("right")], right, 9); - configComplete &= getJsonValue(umData[F("pct")], pct, 5); // Depth of scan [%] - pct = MIN(50,MAX(1,pct)); - - uint16_t totalLights = bottom + left + top + right; - if (initDone && numLights != totalLights) { - if (lights) delete[] lights; - setup(); - } - return configComplete; - } - - /* - * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors. - * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode. - * Commonly used for custom clocks (Cronixie, 7 segment) - */ - void handleOverlayDraw() override { - //strip.setPixelColor(0, RGBW32(0,0,0,0)) // set the first pixel to black - } - - uint16_t getId() override { return USERMOD_ID_BOBLIGHT; } - -}; - -// strings to reduce flash memory usage (used more than twice) -const char BobLightUsermod::_name[] PROGMEM = "BobLight"; -const char BobLightUsermod::_enabled[] PROGMEM = "enabled"; - -// main boblight handling (definition here prevents inlining) -void BobLightUsermod::pollBob() { - - //check if there are any new clients - if (bob && bob->hasClient()) { - //find free/disconnected spot - if (!bobClient || !bobClient.connected()) { - if (bobClient) bobClient.stop(); - bobClient = bob->available(); - DEBUG_PRINTLN(F("Boblight: Client connected.")); - } - //no free/disconnected spot so reject - WiFiClient bobClientTmp = bob->available(); - bobClientTmp.stop(); - BobClear(); - exitRealtime(); - } - - //check clients for data - if (bobClient && bobClient.connected()) { - realtimeLock(realtimeTimeoutMs); // lock strip as we have a client connected - - //get data from the client - while (bobClient.available()) { - String input = bobClient.readStringUntil('\n'); - // DEBUG_PRINT(F("Client: ")); DEBUG_PRINTLN(input); // may be to stressful on Serial - if (input.startsWith(F("hello"))) { - DEBUG_PRINTLN(F("hello")); - bobClient.print(F("hello\n")); - } else if (input.startsWith(F("ping"))) { - DEBUG_PRINTLN(F("ping 1")); - bobClient.print(F("ping 1\n")); - } else if (input.startsWith(F("get version"))) { - DEBUG_PRINTLN(F("version 5")); - bobClient.print(F("version 5\n")); - } else if (input.startsWith(F("get lights"))) { - char tmp[64]; - String answer = ""; - sprintf_P(tmp, PSTR("lights %d\n"), numLights); - DEBUG_PRINT(tmp); - answer.concat(tmp); - for (int i=0; i ... - input.remove(0,10); - String tmp = input.substring(0,input.indexOf(' ')); - - int light_id = -1; - for (uint16_t i=0; iavailable(); - BobClear(); - } - } - } -} +#pragma once + +#include "wled.h" + +/* + * Usermod that implements BobLight "ambilight" protocol + * + * See the accompanying README.md file for more info. + */ + +#ifndef BOB_PORT + #define BOB_PORT 19333 // Default boblightd port +#endif + +class BobLightUsermod : public Usermod { + typedef struct _LIGHT { + char lightname[5]; + float hscan[2]; + float vscan[2]; + } light_t; + + private: + unsigned long lastTime = 0; + bool enabled = false; + bool initDone = false; + + light_t *lights = nullptr; + uint16_t numLights = 0; // 16 + 9 + 16 + 9 + uint16_t top, bottom, left, right; // will be filled in readFromConfig() + uint16_t pct; + + WiFiClient bobClient; + WiFiServer *bob; + uint16_t bobPort = BOB_PORT; + + static const char _name[]; + static const char _enabled[]; + + /* + # boblight + # Copyright (C) Bob 2009 + # + # makeboblight.sh created by Adam Boeglin + # + # boblight is free software: you can redistribute it and/or modify it + # under the terms of the GNU General Public License as published by the + # Free Software Foundation, either version 3 of the License, or + # (at your option) any later version. + # + # boblight 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 General Public License for more details. + # + # You should have received a copy of the GNU General Public License along + # with this program. If not, see . + */ + + // fills the lights[] array with position & depth of scan for each LED + void fillBobLights(int bottom, int left, int top, int right, float pct_scan) { + + int lightcount = 0; + int total = top+left+right+bottom; + int bcount; + + if (total > strip.getLengthTotal()) { + DEBUG_PRINTLN(F("BobLight: Too many lights.")); + return; + } + + // start left part of bottom strip (clockwise direction, 1st half) + if (bottom > 0) { + bcount = 1; + float brange = 100.0/bottom; + float bcurrent = 50.0; + if (bottom < top) { + int diff = top - bottom; + brange = 100.0/top; + bcurrent -= (diff/2)*brange; + } + while (bcount <= bottom/2) { + float btop = bcurrent - brange; + String name = "b"+String(bcount); + strncpy(lights[lightcount].lightname, name.c_str(), 4); + lights[lightcount].hscan[0] = btop; + lights[lightcount].hscan[1] = bcurrent; + lights[lightcount].vscan[0] = 100 - pct_scan; + lights[lightcount].vscan[1] = 100; + lightcount+=1; + bcurrent = btop; + bcount+=1; + } + } + + // left side + if (left > 0) { + int lcount = 1; + float lrange = 100.0/left; + float lcurrent = 100.0; + while (lcount <= left) { + float ltop = lcurrent - lrange; + String name = "l"+String(lcount); + strncpy(lights[lightcount].lightname, name.c_str(), 4); + lights[lightcount].hscan[0] = 0; + lights[lightcount].hscan[1] = pct_scan; + lights[lightcount].vscan[0] = ltop; + lights[lightcount].vscan[1] = lcurrent; + lightcount+=1; + lcurrent = ltop; + lcount+=1; + } + } + + // top side + if (top > 0) { + int tcount = 1; + float trange = 100.0/top; + float tcurrent = 0; + while (tcount <= top) { + float ttop = tcurrent + trange; + String name = "t"+String(tcount); + strncpy(lights[lightcount].lightname, name.c_str(), 4); + lights[lightcount].hscan[0] = tcurrent; + lights[lightcount].hscan[1] = ttop; + lights[lightcount].vscan[0] = 0; + lights[lightcount].vscan[1] = pct_scan; + lightcount+=1; + tcurrent = ttop; + tcount+=1; + } + } + + // right side + if (right > 0) { + int rcount = 1; + float rrange = 100.0/right; + float rcurrent = 0; + while (rcount <= right) { + float rtop = rcurrent + rrange; + String name = "r"+String(rcount); + strncpy(lights[lightcount].lightname, name.c_str(), 4); + lights[lightcount].hscan[0] = 100-pct_scan; + lights[lightcount].hscan[1] = 100; + lights[lightcount].vscan[0] = rcurrent; + lights[lightcount].vscan[1] = rtop; + lightcount+=1; + rcurrent = rtop; + rcount+=1; + } + } + + // right side of bottom strip (2nd half) + if (bottom > 0) { + float brange = 100.0/bottom; + float bcurrent = 100; + if (bottom < top) { + brange = 100.0/top; + } + while (bcount <= bottom) { + float btop = bcurrent - brange; + String name = "b"+String(bcount); + strncpy(lights[lightcount].lightname, name.c_str(), 4); + lights[lightcount].hscan[0] = btop; + lights[lightcount].hscan[1] = bcurrent; + lights[lightcount].vscan[0] = 100 - pct_scan; + lights[lightcount].vscan[1] = 100; + lightcount+=1; + bcurrent = btop; + bcount+=1; + } + } + + numLights = lightcount; + + #if WLED_DEBUG + DEBUG_PRINTLN(F("Fill light data: ")); + DEBUG_PRINTF_P(PSTR(" lights %d\n"), numLights); + for (int i=0; i strip.getLengthTotal() ) { + DEBUG_PRINTLN(F("BobLight: Too many lights.")); + DEBUG_PRINTF_P(PSTR("%d+%d+%d+%d>%d\n"), bottom, left, top, right, strip.getLengthTotal()); + totalLights = strip.getLengthTotal(); + top = bottom = (uint16_t) roundf((float)totalLights * 16.0f / 50.0f); + left = right = (uint16_t) roundf((float)totalLights * 9.0f / 50.0f); + } + lights = new light_t[totalLights]; + if (lights) fillBobLights(bottom, left, top, right, float(pct)); // will fill numLights + else enable(false); + initDone = true; + } + + void connected() override { + // we can only start server when WiFi is connected + if (!bob) bob = new WiFiServer(bobPort, 1); + bob->begin(); + bob->setNoDelay(true); + } + + void loop() override { + if (!enabled || strip.isUpdating()) return; + if (millis() - lastTime > 10) { + lastTime = millis(); + pollBob(); + } + } + + void enable(bool en) { enabled = en; } + +#ifndef WLED_DISABLE_MQTT + /** + * handling of MQTT message + * topic only contains stripped topic (part after /wled/MAC) + * topic should look like: /swipe with amessage of [up|down] + */ + bool onMqttMessage(char* topic, char* payload) override { + //if (strlen(topic) == 6 && strncmp_P(topic, PSTR("/subtopic"), 6) == 0) { + // String action = payload; + // if (action == "on") { + // enable(true); + // return true; + // } else if (action == "off") { + // enable(false); + // return true; + // } + //} + return false; + } + + /** + * subscribe to MQTT topic for controlling usermod + */ + void onMqttConnect(bool sessionPresent) override { + //char subuf[64]; + //if (mqttDeviceTopic[0] != 0) { + // strcpy(subuf, mqttDeviceTopic); + // strcat_P(subuf, PSTR("/subtopic")); + // mqtt->subscribe(subuf, 0); + //} + } +#endif + + void addToJsonInfo(JsonObject& root) override + { + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray infoArr = user.createNestedArray(FPSTR(_name)); + String uiDomString = F(""); + infoArr.add(uiDomString); + } + + /* + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void addToJsonState(JsonObject& root) override + { + } + + /* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void readFromJsonState(JsonObject& root) override { + if (!initDone) return; // prevent crash on boot applyPreset() + bool en = enabled; + JsonObject um = root[FPSTR(_name)]; + if (!um.isNull()) { + if (um[FPSTR(_enabled)].is()) { + en = um[FPSTR(_enabled)].as(); + } else { + String str = um[FPSTR(_enabled)]; // checkbox -> off or on + en = (bool)(str!="off"); // off is guaranteed to be present + } + if (en != enabled && lights) { + enable(en); + if (!enabled && bob && bob->hasClient()) { + if (bobClient) bobClient.stop(); + bobClient = bob->available(); + BobClear(); + exitRealtime(); + } + } + } + } + + void appendConfigData() override { + //oappend(F("dd=addDropdown('usermod','selectfield');")); + //oappend(F("addOption(dd,'1st value',0);")); + //oappend(F("addOption(dd,'2nd value',1);")); + oappend(F("addInfo('BobLight:top',1,'LEDs');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('BobLight:bottom',1,'LEDs');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('BobLight:left',1,'LEDs');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('BobLight:right',1,'LEDs');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('BobLight:pct',1,'Depth of scan [%]');")); // 0 is field type, 1 is actual field + } + + void addToConfig(JsonObject& root) override { + JsonObject umData = root.createNestedObject(FPSTR(_name)); + umData[FPSTR(_enabled)] = enabled; + umData[ "port" ] = bobPort; + umData[F("top")] = top; + umData[F("bottom")] = bottom; + umData[F("left")] = left; + umData[F("right")] = right; + umData[F("pct")] = pct; + } + + bool readFromConfig(JsonObject& root) override { + JsonObject umData = root[FPSTR(_name)]; + bool configComplete = !umData.isNull(); + + bool en = enabled; + configComplete &= getJsonValue(umData[FPSTR(_enabled)], en); + enable(en); + + configComplete &= getJsonValue(umData[ "port" ], bobPort); + configComplete &= getJsonValue(umData[F("bottom")], bottom, 16); + configComplete &= getJsonValue(umData[F("top")], top, 16); + configComplete &= getJsonValue(umData[F("left")], left, 9); + configComplete &= getJsonValue(umData[F("right")], right, 9); + configComplete &= getJsonValue(umData[F("pct")], pct, 5); // Depth of scan [%] + pct = MIN(50,MAX(1,pct)); + + uint16_t totalLights = bottom + left + top + right; + if (initDone && numLights != totalLights) { + if (lights) delete[] lights; + setup(); + } + return configComplete; + } + + /* + * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors. + * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode. + * Commonly used for custom clocks (Cronixie, 7 segment) + */ + void handleOverlayDraw() override { + //strip.setPixelColor(0, RGBW32(0,0,0,0)) // set the first pixel to black + } + + uint16_t getId() override { return USERMOD_ID_BOBLIGHT; } + +}; + +// strings to reduce flash memory usage (used more than twice) +const char BobLightUsermod::_name[] PROGMEM = "BobLight"; +const char BobLightUsermod::_enabled[] PROGMEM = "enabled"; + +// main boblight handling (definition here prevents inlining) +void BobLightUsermod::pollBob() { + + //check if there are any new clients + if (bob && bob->hasClient()) { + //find free/disconnected spot + if (!bobClient || !bobClient.connected()) { + if (bobClient) bobClient.stop(); + bobClient = bob->available(); + DEBUG_PRINTLN(F("Boblight: Client connected.")); + } + //no free/disconnected spot so reject + WiFiClient bobClientTmp = bob->available(); + bobClientTmp.stop(); + BobClear(); + exitRealtime(); + } + + //check clients for data + if (bobClient && bobClient.connected()) { + realtimeLock(realtimeTimeoutMs); // lock strip as we have a client connected + + //get data from the client + while (bobClient.available()) { + String input = bobClient.readStringUntil('\n'); + // DEBUG_PRINT(F("Client: ")); DEBUG_PRINTLN(input); // may be to stressful on Serial + if (input.startsWith(F("hello"))) { + DEBUG_PRINTLN(F("hello")); + bobClient.print(F("hello\n")); + } else if (input.startsWith(F("ping"))) { + DEBUG_PRINTLN(F("ping 1")); + bobClient.print(F("ping 1\n")); + } else if (input.startsWith(F("get version"))) { + DEBUG_PRINTLN(F("version 5")); + bobClient.print(F("version 5\n")); + } else if (input.startsWith(F("get lights"))) { + char tmp[64]; + String answer = ""; + sprintf_P(tmp, PSTR("lights %d\n"), numLights); + DEBUG_PRINT(tmp); + answer.concat(tmp); + for (int i=0; i ... + input.remove(0,10); + String tmp = input.substring(0,input.indexOf(' ')); + + int light_id = -1; + for (uint16_t i=0; iavailable(); + BobClear(); + } + } + } +} diff --git a/usermods/deep_sleep/readme.md b/usermods/deep_sleep/readme.md new file mode 100644 index 000000000..006aa31fd --- /dev/null +++ b/usermods/deep_sleep/readme.md @@ -0,0 +1,84 @@ +# Deep Sleep usermod + +This usermod unleashes the low power capabilities of th ESP: when you power off your LEDs (using the UI power button or a macro) the ESP will be put into deep sleep mode, reducing power consumption to a minimum. +During deep sleep the ESP is shut down completely: no WiFi, no CPU, no outputs. The only way to wake it up is to use an external signal or a button. Once it wakes from deep sleep it reboots so ***make sure to use a boot-up preset.*** + +# A word of warning + +When you disable the WLED option 'Turn LEDs on after power up/reset' and 'DelaySleep' is set to zero the ESP will go into deep sleep directly after power-up and only start WLED after it has been woken up. +If the ESP can not be awoken from deep sleep due to a wrong configuration it has to be factory reset, disabling sleep at power-up. There is no other way to wake it up. + +# Power Consumption in deep sleep + +The current drawn by the ESP in deep sleep mode depends on the type and is in the range of 5uA-20uA (as in micro Amperes): +- ESP32: 10uA +- ESP32 S3: 8uA +- ESP32 S2: 20uA +- ESP32 C3: 5uA +- ESP8266: 20uA (not supported in this usermod) + +However, there is usually additional components on a controller that increase the value: +- Power LED: the power LED on a ESP board draws 500uA - 1mA +- LDO: the voltage regulator also draws idle current. Depending on the type used this can be around 50uA up to 10mA (LM1117). Special low power LDOs with very low idle currents do exist +- Digital LEDs: WS2812 for example draw a current of about 1mA per LED. To make good use of this usermod it is required to power them off using MOSFETs or a Relay + +For lowest power consumption, remove the Power LED and make sure your board does not use an LM1117. On a ESP32 C3 Supermini with the power LED removed (no other modifications) powered through the 5V pin I measured a current draw of 50uA in deep sleep. + +# Useable GPIOs + +The GPIOs that can be used to wake the ESP from deep sleep are limited. Only pins connected to the internal RTC unit can be used: + +- ESP32: GPIO 0, 2, 4, 12-15, 25-39 +- ESP32 S3: GPIO 0-21 +- ESP32 S2: GPIO 0-21 +- ESP32 C3: GPIO 0-5 +- ESP8266 is not supported in this usermod + +You can however use the selected wake-up pin normally in WLED, it only gets activated as a wake-up pin when your LEDs are powered down. + +# Limitations + +To keep this usermod simple and easy to use, it is a very basic implementation of the low-power capabilities provided by the ESP. If you need more advanced control you are welcome to implement your own version based on this usermod. + +## Usermod installation + +Use `#define USERMOD_DEEP_SLEEP` in wled.h or `-D USERMOD_DEEP_SLEEP` in your platformio.ini. Settings can be changed in the usermod config UI. + +### Define Settings + +There are five parameters you can set: + +- GPIO: the pin to use for wake-up +- WakeWhen High/Low: the pin state that triggers the wake-up +- Pull-up/down disable: enable or disable the internal pullup resistors during sleep (does not affect normal use while running) +- Wake after: if set larger than 0, ESP will automatically wake-up after this many seconds (Turn LEDs on after power up/reset is overriden, it will always turn on) +- Delay sleep: if set larger than 0, ESP will not go to sleep for this many seconds after you power it off. Timer is reset when switched back on during this time. + +To override the default settings, place the `#define` in wled.h or add `-D DEEPSLEEP_xxx` to your platformio_override.ini build flags + +* `DEEPSLEEP_WAKEUPPIN x` - define the pin to be used for wake-up, see list of useable pins above. The pin can be used normally as a button pin in WLED. +* `DEEPSLEEP_WAKEWHENHIGH` - if defined, wakes up when pin goes high (default is low) +* `DEEPSLEEP_DISABLEPULL` - if defined, internal pullup/pulldown is disabled in deep sleep (default is ebnabled) +* `DEEPSLEEP_WAKEUPINTERVAL` - number of seconds after which a wake-up happens automatically, sooner if button is pressed. 0 = never. accuracy is about 2% +* `DEEPSLEEP_DELAY` - delay between power-off and sleep + +example for env build flags: + `-D USERMOD_DEEP_SLEEP` + `-D DEEPSLEEP_WAKEUPPIN=4` + `-D DEEPSLEEP_DISABLEPULL=0` ;enable pull-up/down resistors by default + `-D DEEPSLEEP_WAKEUPINTERVAL=43200` ;wake up after 12 hours (or when button is pressed) + +### Hardware Setup + +To wake from deep-sleep an external trigger signal on the configured GPIO is required. When using timed-only wake-up, use a GPIO that has an on-board pull-up resistor (GPIO0 on most boards). When using push-buttons it is highly recommended to use an external pull-up resistor: not all IO's on all devices have properly working internal resistors. + +Using sensors like PIR, IR, touch sensors or any other sensor with a digital output can be used instead of a button. + +now go on and save some power +@dedehai + +## Change log +2024-09 +* Initial version +2024-10 +* Changed from #define configuration to UI configuration \ No newline at end of file diff --git a/usermods/deep_sleep/usermod_deep_sleep.h b/usermods/deep_sleep/usermod_deep_sleep.h new file mode 100644 index 000000000..7f4efd5ca --- /dev/null +++ b/usermods/deep_sleep/usermod_deep_sleep.h @@ -0,0 +1,227 @@ +#pragma once + +#include "wled.h" +#include "driver/rtc_io.h" + +#ifdef ESP8266 +#error The "Deep Sleep" usermod does not support ESP8266 +#endif + +#ifndef DEEPSLEEP_WAKEUPPIN +#define DEEPSLEEP_WAKEUPPIN 0 +#endif +#ifndef DEEPSLEEP_WAKEWHENHIGH +#define DEEPSLEEP_WAKEWHENHIGH 0 +#endif +#ifndef DEEPSLEEP_DISABLEPULL +#define DEEPSLEEP_DISABLEPULL 1 +#endif +#ifndef DEEPSLEEP_WAKEUPINTERVAL +#define DEEPSLEEP_WAKEUPINTERVAL 0 +#endif +#ifndef DEEPSLEEP_DELAY +#define DEEPSLEEP_DELAY 1 +#endif + +RTC_DATA_ATTR bool powerup = true; // variable in RTC data persists on a reboot + +class DeepSleepUsermod : public Usermod { + + private: + + bool enabled = true; + bool initDone = false; + uint8_t wakeupPin = DEEPSLEEP_WAKEUPPIN; + uint8_t wakeWhenHigh = DEEPSLEEP_WAKEWHENHIGH; // wake up when pin goes high if 1, triggers on low if 0 + bool noPull = true; // use pullup/pulldown resistor + int wakeupAfter = DEEPSLEEP_WAKEUPINTERVAL; // in seconds, <=0: button only + int sleepDelay = DEEPSLEEP_DELAY; // in seconds, 0 = immediate + int delaycounter = 5; // delay deep sleep at bootup until preset settings are applied + uint32_t lastLoopTime = 0; + // string that are used multiple time (this will save some flash memory) + static const char _name[]; + static const char _enabled[]; + + bool pin_is_valid(uint8_t wakePin) { + #ifdef CONFIG_IDF_TARGET_ESP32 //ESP32: GPIOs 0,2,4, 12-15, 25-39 can be used for wake-up + if (wakePin == 0 || wakePin == 2 || wakePin == 4 || (wakePin >= 12 && wakePin <= 15) || (wakePin >= 25 && wakePin <= 27) || (wakePin >= 32 && wakePin <= 39)) { + return true; + } + #endif + #if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32S2) //ESP32 S3 & S3: GPIOs 0-21 can be used for wake-up + if (wakePin <= 21) { + return true; + } + #endif + #ifdef CONFIG_IDF_TARGET_ESP32C3 // ESP32 C3: GPIOs 0-5 can be used for wake-up + if (wakePin <= 5) { + return true; + } + #endif + DEBUG_PRINTLN(F("Error: unsupported deep sleep wake-up pin")); + return false; + } + + public: + + inline void enable(bool enable) { enabled = enable; } // Enable/Disable the usermod + inline bool isEnabled() { return enabled; } //Get usermod enabled/disabled state + + // setup is called at boot (or in this case after every exit of sleep mode) + void setup() { + //TODO: if the de-init of RTC pins is required to do it could be done here + //rtc_gpio_deinit(wakeupPin); + initDone = true; + } + + void loop() { + if (!enabled || !offMode) { // disabled or LEDs are on + lastLoopTime = 0; // reset timer + return; + } + + if (sleepDelay > 0) { + if(lastLoopTime == 0) lastLoopTime = millis(); // initialize + if (millis() - lastLoopTime < sleepDelay * 1000) { + return; // wait until delay is over + } + } + + if(powerup == false && delaycounter) { // delay sleep in case a preset is being loaded and turnOnAtBoot is disabled (handleIO() does enable offMode temporarily in this case) + delaycounter--; + if(delaycounter == 2 && offMode) { // force turn on, no matter the settings (device is bricked if user set sleepDelay=0, no bootup preset and turnOnAtBoot=false) + if (briS == 0) bri = 10; // turn on at low brightness + else bri = briS; + strip.setBrightness(bri); // needed to make handleIO() not turn off LEDs (really? does not help in bootup preset) + offMode = false; + applyPresetWithFallback(0, CALL_MODE_INIT, FX_MODE_STATIC, 0); // try to apply preset 0, fallback to static + if (rlyPin >= 0) { + digitalWrite(rlyPin, (rlyMde ? HIGH : LOW)); // turn relay on TODO: this should be done by wled, what function to call? + } + } + return; + } + + DEBUG_PRINTLN(F("DeepSleep UM: entering deep sleep...")); + powerup = false; // turn leds on in all subsequent bootups (overrides Turn LEDs on after power up/reset' at reboot) + if(!pin_is_valid(wakeupPin)) return; + esp_err_t halerror = ESP_OK; + pinMode(wakeupPin, INPUT); // make sure GPIO is input with pullup/pulldown disabled + esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); //disable all wake-up sources (just in case) + + if(wakeupAfter) + esp_sleep_enable_timer_wakeup((uint64_t)wakeupAfter * (uint64_t)1e6); //sleep for x seconds + + #if defined(CONFIG_IDF_TARGET_ESP32C3) // ESP32 C3 + if(noPull) + gpio_sleep_set_pull_mode((gpio_num_t)wakeupPin, GPIO_FLOATING); + else { // enable pullup/pulldown resistor + if(wakeWhenHigh) + gpio_sleep_set_pull_mode((gpio_num_t)wakeupPin, GPIO_PULLDOWN_ONLY); + else + gpio_sleep_set_pull_mode((gpio_num_t)wakeupPin, GPIO_PULLUP_ONLY); + } + if(wakeWhenHigh) + halerror = esp_deep_sleep_enable_gpio_wakeup(1<(0 = never)');")); + oappend(SET_F("addInfo('DeepSleep:delaySleep',1,'seconds (0 = sleep at powerup)');")); // first string is suffix, second string is prefix + } + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() { + return USERMOD_ID_DEEP_SLEEP; + } + +}; + +// add more strings here to reduce flash memory usage +const char DeepSleepUsermod::_name[] PROGMEM = "DeepSleep"; +const char DeepSleepUsermod::_enabled[] PROGMEM = "enabled"; \ No newline at end of file diff --git a/usermods/mpu6050_imu/readme.md b/usermods/mpu6050_imu/readme.md index b804ba602..c324738d6 100644 --- a/usermods/mpu6050_imu/readme.md +++ b/usermods/mpu6050_imu/readme.md @@ -86,6 +86,6 @@ Example **usermods_list.cpp**: void registerUsermods() { - usermods.add(new MPU6050Driver()); + UsermodManager::add(new MPU6050Driver()); } ``` diff --git a/usermods/mpu6050_imu/usermod_gyro_surge.h b/usermods/mpu6050_imu/usermod_gyro_surge.h index c49d930ff..c19358de7 100644 --- a/usermods/mpu6050_imu/usermod_gyro_surge.h +++ b/usermods/mpu6050_imu/usermod_gyro_surge.h @@ -163,7 +163,7 @@ class GyroSurge : public Usermod { void loop() { // get IMU data um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_IMU)) { + if (!UsermodManager::getUMData(&um_data, USERMOD_ID_IMU)) { // Apply max strip.getSegment(0).fadeToBlackBy(max); return; diff --git a/usermods/mpu6050_imu/usermod_mpu6050_imu.h b/usermods/mpu6050_imu/usermod_mpu6050_imu.h index 51dd646c7..f04578fe3 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,11 +157,14 @@ 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) { - irqBound = pinManager.allocatePin(config.interruptPin, false, PinOwner::UM_IMU); + irqBound = PinManager::allocatePin(config.interruptPin, false, PinOwner::UM_IMU); if (!irqBound) { DEBUG_PRINTLN(F("MPU6050: IRQ pin already in use.")); return; } pinMode(config.interruptPin, INPUT); }; @@ -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; @@ -403,12 +408,12 @@ class MPU6050Driver : public Usermod { // Previously loaded and config changed if (irqBound && ((old_cfg.interruptPin != config.interruptPin) || !config.enabled)) { detachInterrupt(old_cfg.interruptPin); - pinManager.deallocatePin(old_cfg.interruptPin, PinOwner::UM_IMU); + PinManager::deallocatePin(old_cfg.interruptPin, PinOwner::UM_IMU); irqBound = false; } - // Just re-init - setup(); + // Re-call setup on the next loop() + configDirty = true; } return configComplete; diff --git a/usermods/mqtt_switch_v2/README.md b/usermods/mqtt_switch_v2/README.md index 744d7fe3c..382f72d0e 100644 --- a/usermods/mqtt_switch_v2/README.md +++ b/usermods/mqtt_switch_v2/README.md @@ -19,7 +19,7 @@ Example `usermods_list.cpp`: void registerUsermods() { - usermods.add(new UsermodMqttSwitch()); + UsermodManager::add(new UsermodMqttSwitch()); } ``` @@ -33,7 +33,7 @@ board = esp12e platform = ${common.platform_wled_default} board_build.ldscript = ${common.ldscript_4m1m} build_flags = ${common.build_flags_esp8266} - -D LEDPIN=3 + -D DATA_PINS=3 -D BTNPIN=4 -D RLYPIN=12 -D RLYMDE=1 diff --git a/usermods/multi_relay/readme.md b/usermods/multi_relay/readme.md index 24dd394b8..eaa069ae7 100644 --- a/usermods/multi_relay/readme.md +++ b/usermods/multi_relay/readme.md @@ -41,7 +41,7 @@ When a relay is switched, a message is published: ## Usermod installation -1. Register the usermod by adding `#include "../usermods/multi_relay/usermod_multi_relay.h"` at the top and `usermods.add(new MultiRelay());` at the bottom of `usermods_list.cpp`. +1. Register the usermod by adding `#include "../usermods/multi_relay/usermod_multi_relay.h"` at the top and `UsermodManager::add(new MultiRelay());` at the bottom of `usermods_list.cpp`. or 2. Use `#define USERMOD_MULTI_RELAY` in wled.h or `-D USERMOD_MULTI_RELAY` in your platformio.ini @@ -90,9 +90,9 @@ void registerUsermods() * || || || * \/ \/ \/ */ - //usermods.add(new MyExampleUsermod()); - //usermods.add(new UsermodTemperature()); - usermods.add(new MultiRelay()); + //UsermodManager::add(new MyExampleUsermod()); + //UsermodManager::add(new UsermodTemperature()); + UsermodManager::add(new MultiRelay()); } ``` diff --git a/usermods/multi_relay/usermod_multi_relay.h b/usermods/multi_relay/usermod_multi_relay.h index cb1eec8e1..c4446c7a2 100644 --- a/usermods/multi_relay/usermod_multi_relay.h +++ b/usermods/multi_relay/usermod_multi_relay.h @@ -264,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(SET_F("/relays"), HTTP_GET, [this](AsyncWebServerRequest *request) { + server.on(F("/relays"), HTTP_GET, [this](AsyncWebServerRequest *request) { DEBUG_PRINTLN(F("Relays: HTML API")); String janswer; String error = ""; @@ -516,7 +516,7 @@ void MultiRelay::setup() { if (!_relay[i].external) _relay[i].state = !offMode; state |= (uint8_t)(_relay[i].invert ? !_relay[i].state : _relay[i].state) << pin; } else if (_relay[i].pin<100 && _relay[i].pin>=0) { - if (pinManager.allocatePin(_relay[i].pin,true, PinOwner::UM_MultiRelay)) { + if (PinManager::allocatePin(_relay[i].pin,true, PinOwner::UM_MultiRelay)) { if (!_relay[i].external) _relay[i].state = !offMode; switchRelay(i, _relay[i].state); _relay[i].active = false; @@ -667,7 +667,7 @@ void MultiRelay::addToJsonInfo(JsonObject &root) { for (int i=0; i(not hex!)');")); - oappend(SET_F("addInfo('MultiRelay:broadcast-sec',1,'(MQTT message)');")); - //oappend(SET_F("addInfo('MultiRelay:relay-0:pin',1,'(use -1 for PCF8574)');")); - oappend(SET_F("d.extra.push({'MultiRelay':{pin:[['P0',100],['P1',101],['P2',102],['P3',103],['P4',104],['P5',105],['P6',106],['P7',107]]}});")); + oappend(F("addInfo('MultiRelay:PCF8574-address',1,'(not hex!)');")); + oappend(F("addInfo('MultiRelay:broadcast-sec',1,'(MQTT message)');")); + //oappend(F("addInfo('MultiRelay:relay-0:pin',1,'(use -1 for PCF8574)');")); + oappend(F("d.extra.push({'MultiRelay':{pin:[['P0',100],['P1',101],['P2',102],['P3',103],['P4',104],['P5',105],['P6',106],['P7',107]]}});")); } /** @@ -817,7 +817,7 @@ bool MultiRelay::readFromConfig(JsonObject &root) { // deallocate all pins 1st for (int i=0; i=0 && oldPin[i]<100) { - pinManager.deallocatePin(oldPin[i], PinOwner::UM_MultiRelay); + PinManager::deallocatePin(oldPin[i], PinOwner::UM_MultiRelay); } // allocate new pins setup(); diff --git a/usermods/pixels_dice_tray/README.md b/usermods/pixels_dice_tray/README.md new file mode 100644 index 000000000..6daa4fa72 --- /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 000000000..ffa509e6d --- /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 000000000..eee4759fd --- /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 000000000..589597086 --- /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 000000000..989ed1eb9 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 000000000..5cb84efb3 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 000000000..47f568863 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 000000000..878e9a558 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 000000000..7553d6817 --- /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 000000000..a3e4aa014 --- /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 000000000..3ce0b7bf1 --- /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 000000000..8fb305c7e --- /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 000000000..61348ebb8 --- /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(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(F("ddr=addDropdown('DiceTray','rotation');")); + oappend(F("addOption(ddr,'0 deg',0);")); + oappend(F("addOption(ddr,'90 deg',1);")); + oappend(F("addOption(ddr,'180 deg',2);")); + oappend(F("addOption(ddr,'270 deg',3);")); + oappend(F( + "addInfo('DiceTray:rotation',1,'
DO NOT CHANGE " + "SPI PINS.
CHANGES ARE IGNORED.','');")); + oappend(F("addInfo('TFT:pin[]',0,'','SPI CS');")); + oappend(F("addInfo('TFT:pin[]',1,'','SPI DC');")); + oappend(F("addInfo('TFT:pin[]',2,'','SPI RST');")); + oappend(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 000000000..b712f8b2e --- /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 000000000..fd8203134 --- /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 000000000..0b8fd8394 --- /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 000000000..b1fc0dba6 --- /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/pwm_outputs/usermod_pwm_outputs.h b/usermods/pwm_outputs/usermod_pwm_outputs.h index 1880308c4..09232f043 100644 --- a/usermods/pwm_outputs/usermod_pwm_outputs.h +++ b/usermods/pwm_outputs/usermod_pwm_outputs.h @@ -29,13 +29,13 @@ class PwmOutput { return; DEBUG_PRINTF("pwm_output[%d]: setup to freq %d\n", pin_, freq_); - if (!pinManager.allocatePin(pin_, true, PinOwner::UM_PWM_OUTPUTS)) + if (!PinManager::allocatePin(pin_, true, PinOwner::UM_PWM_OUTPUTS)) return; - channel_ = pinManager.allocateLedc(1); + channel_ = PinManager::allocateLedc(1); if (channel_ == 255) { DEBUG_PRINTF("pwm_output[%d]: failed to quire ledc\n", pin_); - pinManager.deallocatePin(pin_, PinOwner::UM_PWM_OUTPUTS); + PinManager::deallocatePin(pin_, PinOwner::UM_PWM_OUTPUTS); return; } @@ -49,9 +49,9 @@ class PwmOutput { DEBUG_PRINTF("pwm_output[%d]: close\n", pin_); if (!enabled_) return; - pinManager.deallocatePin(pin_, PinOwner::UM_PWM_OUTPUTS); + PinManager::deallocatePin(pin_, PinOwner::UM_PWM_OUTPUTS); if (channel_ != 255) - pinManager.deallocateLedc(channel_, 1); + PinManager::deallocateLedc(channel_, 1); channel_ = 255; duty_ = 0.0f; enabled_ = false; diff --git a/usermods/quinled-an-penta/quinled-an-penta.h b/usermods/quinled-an-penta/quinled-an-penta.h index 10b784334..e44672039 100644 --- a/usermods/quinled-an-penta/quinled-an-penta.h +++ b/usermods/quinled-an-penta/quinled-an-penta.h @@ -129,7 +129,7 @@ class QuinLEDAnPentaUsermod : public Usermod void initOledDisplay() { PinManagerPinType pins[5] = { { oledSpiClk, true }, { oledSpiData, true }, { oledSpiCs, true }, { oledSpiDc, true }, { oledSpiRst, true } }; - if (!pinManager.allocateMultiplePins(pins, 5, PinOwner::UM_QuinLEDAnPenta)) { + if (!PinManager::allocateMultiplePins(pins, 5, PinOwner::UM_QuinLEDAnPenta)) { DEBUG_PRINTF("[%s] OLED pin allocation failed!\n", _name); oledEnabled = oledInitDone = false; return; @@ -164,11 +164,11 @@ class QuinLEDAnPentaUsermod : public Usermod oledDisplay->clear(); } - pinManager.deallocatePin(oledSpiClk, PinOwner::UM_QuinLEDAnPenta); - pinManager.deallocatePin(oledSpiData, PinOwner::UM_QuinLEDAnPenta); - pinManager.deallocatePin(oledSpiCs, PinOwner::UM_QuinLEDAnPenta); - pinManager.deallocatePin(oledSpiDc, PinOwner::UM_QuinLEDAnPenta); - pinManager.deallocatePin(oledSpiRst, PinOwner::UM_QuinLEDAnPenta); + PinManager::deallocatePin(oledSpiClk, PinOwner::UM_QuinLEDAnPenta); + PinManager::deallocatePin(oledSpiData, PinOwner::UM_QuinLEDAnPenta); + PinManager::deallocatePin(oledSpiCs, PinOwner::UM_QuinLEDAnPenta); + PinManager::deallocatePin(oledSpiDc, PinOwner::UM_QuinLEDAnPenta); + PinManager::deallocatePin(oledSpiRst, PinOwner::UM_QuinLEDAnPenta); delete oledDisplay; @@ -184,7 +184,7 @@ class QuinLEDAnPentaUsermod : public Usermod void initSht30TempHumiditySensor() { PinManagerPinType pins[2] = { { shtSda, true }, { shtScl, true } }; - if (!pinManager.allocateMultiplePins(pins, 2, PinOwner::UM_QuinLEDAnPenta)) { + if (!PinManager::allocateMultiplePins(pins, 2, PinOwner::UM_QuinLEDAnPenta)) { DEBUG_PRINTF("[%s] SHT30 pin allocation failed!\n", _name); shtEnabled = shtInitDone = false; return; @@ -212,8 +212,8 @@ class QuinLEDAnPentaUsermod : public Usermod sht30TempHumidSensor->reset(); } - pinManager.deallocatePin(shtSda, PinOwner::UM_QuinLEDAnPenta); - pinManager.deallocatePin(shtScl, PinOwner::UM_QuinLEDAnPenta); + PinManager::deallocatePin(shtSda, PinOwner::UM_QuinLEDAnPenta); + PinManager::deallocatePin(shtScl, PinOwner::UM_QuinLEDAnPenta); delete sht30TempHumidSensor; diff --git a/usermods/rgb-rotary-encoder/readme.md b/usermods/rgb-rotary-encoder/readme.md index ba5aad4df..653179179 100644 --- a/usermods/rgb-rotary-encoder/readme.md +++ b/usermods/rgb-rotary-encoder/readme.md @@ -9,7 +9,7 @@ The actual / original code that controls the LED modes is from Adam Zeloof. I ta It was quite a bit more work than I hoped, but I got there eventually :) ## Requirements -* "ESP Rotary" by Lennart Hennigs, v1.5.0 or higher: https://github.com/LennartHennigs/ESPRotary +* "ESP Rotary" by Lennart Hennigs, v2.1.1 or higher: https://github.com/LennartHennigs/ESPRotary ## Usermod installation Simply copy the below block (build task) to your `platformio_override.ini` and compile WLED using this new build task. Or use an existing one and add the buildflag `-D RGB_ROTARY_ENCODER`. @@ -20,7 +20,7 @@ ESP32: extends = env:esp32dev build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32 -D RGB_ROTARY_ENCODER lib_deps = ${esp32.lib_deps} - lennarthennigs/ESP Rotary@^1.5.0 + lennarthennigs/ESP Rotary@^2.1.1 ``` ESP8266 / D1 Mini: @@ -29,7 +29,7 @@ ESP8266 / D1 Mini: extends = env:d1_mini build_flags = ${common.build_flags_esp8266} -D RGB_ROTARY_ENCODER lib_deps = ${esp8266.lib_deps} - lennarthennigs/ESP Rotary@^1.5.0 + lennarthennigs/ESP Rotary@^2.1.1 ``` ## How to connect the board to your ESP diff --git a/usermods/rgb-rotary-encoder/rgb-rotary-encoder.h b/usermods/rgb-rotary-encoder/rgb-rotary-encoder.h index e57641bf9..00fc22725 100644 --- a/usermods/rgb-rotary-encoder/rgb-rotary-encoder.h +++ b/usermods/rgb-rotary-encoder/rgb-rotary-encoder.h @@ -40,7 +40,7 @@ class RgbRotaryEncoderUsermod : public Usermod void initRotaryEncoder() { PinManagerPinType pins[2] = { { eaIo, false }, { ebIo, false } }; - if (!pinManager.allocateMultiplePins(pins, 2, PinOwner::UM_RGBRotaryEncoder)) { + if (!PinManager::allocateMultiplePins(pins, 2, PinOwner::UM_RGBRotaryEncoder)) { eaIo = -1; ebIo = -1; cleanup(); @@ -108,11 +108,11 @@ class RgbRotaryEncoderUsermod : public Usermod { // Only deallocate pins if we allocated them ;) if (eaIo != -1) { - pinManager.deallocatePin(eaIo, PinOwner::UM_RGBRotaryEncoder); + PinManager::deallocatePin(eaIo, PinOwner::UM_RGBRotaryEncoder); eaIo = -1; } if (ebIo != -1) { - pinManager.deallocatePin(ebIo, PinOwner::UM_RGBRotaryEncoder); + PinManager::deallocatePin(ebIo, PinOwner::UM_RGBRotaryEncoder); ebIo = -1; } @@ -303,8 +303,8 @@ class RgbRotaryEncoderUsermod : public Usermod } if (eaIo != oldEaIo || ebIo != oldEbIo || stepsPerClick != oldStepsPerClick || incrementPerClick != oldIncrementPerClick) { - pinManager.deallocatePin(oldEaIo, PinOwner::UM_RGBRotaryEncoder); - pinManager.deallocatePin(oldEbIo, PinOwner::UM_RGBRotaryEncoder); + PinManager::deallocatePin(oldEaIo, PinOwner::UM_RGBRotaryEncoder); + PinManager::deallocatePin(oldEbIo, PinOwner::UM_RGBRotaryEncoder); delete rotaryEncoder; initRotaryEncoder(); diff --git a/usermods/sd_card/usermod_sd_card.h b/usermods/sd_card/usermod_sd_card.h index 5dac79159..da1999d9b 100644 --- a/usermods/sd_card/usermod_sd_card.h +++ b/usermods/sd_card/usermod_sd_card.h @@ -45,7 +45,7 @@ class UsermodSdCard : public Usermod { { configPinPico, true } }; - if (!pinManager.allocateMultiplePins(pins, 4, PinOwner::UM_SdCard)) { + if (!PinManager::allocateMultiplePins(pins, 4, PinOwner::UM_SdCard)) { DEBUG_PRINTF("[%s] SD (SPI) pin allocation failed!\n", _name); sdInitDone = false; return; @@ -75,10 +75,10 @@ class UsermodSdCard : public Usermod { SD_ADAPTER.end(); DEBUG_PRINTF("[%s] deallocate pins!\n", _name); - pinManager.deallocatePin(configPinSourceSelect, PinOwner::UM_SdCard); - pinManager.deallocatePin(configPinSourceClock, PinOwner::UM_SdCard); - pinManager.deallocatePin(configPinPoci, PinOwner::UM_SdCard); - pinManager.deallocatePin(configPinPico, PinOwner::UM_SdCard); + PinManager::deallocatePin(configPinSourceSelect, PinOwner::UM_SdCard); + PinManager::deallocatePin(configPinSourceClock, PinOwner::UM_SdCard); + PinManager::deallocatePin(configPinPoci, PinOwner::UM_SdCard); + PinManager::deallocatePin(configPinPico, PinOwner::UM_SdCard); sdInitDone = false; } diff --git a/usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h b/usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h index 5c2fac0d4..1436f8fc4 100644 --- a/usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h +++ b/usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h @@ -165,7 +165,7 @@ private: void _showElements(String *map, int timevar, bool isColon, bool removeZero ) { - if (!(*map).equals("") && !(*map) == NULL) { + if ((map != nullptr) && (*map != nullptr) && !(*map).equals("")) { int length = String(timevar).length(); bool addZero = false; if (length == 1) { @@ -236,11 +236,13 @@ private: } void _setLeds(int lednr, int lastSeenLedNr, bool range, int countSegments, int number, bool colon) { + if ((lednr < 0) || (lednr >= umSSDRLength)) return; // prevent array bounds violation + if (!(colon && umSSDRColonblink) && ((number < 0) || (countSegments < 0))) return; if ((colon && umSSDRColonblink) || umSSDRNumbers[number][countSegments]) { if (range) { - for(int i = lastSeenLedNr; i <= lednr; i++) { + for(int i = max(0, lastSeenLedNr); i <= lednr; i++) { umSSDRMask[i] = true; } } else { @@ -383,7 +385,7 @@ public: _setAllFalse(); #ifdef USERMOD_SN_PHOTORESISTOR - ptr = (Usermod_SN_Photoresistor*) usermods.lookup(USERMOD_ID_SN_PHOTORESISTOR); + ptr = (Usermod_SN_Photoresistor*) UsermodManager::lookup(USERMOD_ID_SN_PHOTORESISTOR); #endif DEBUG_PRINTLN(F("Setup done")); } diff --git a/usermods/sht/usermod_sht.h b/usermods/sht/usermod_sht.h index c6e17221b..f10c78a25 100644 --- a/usermods/sht/usermod_sht.h +++ b/usermods/sht/usermod_sht.h @@ -310,22 +310,22 @@ void ShtUsermod::onMqttConnect(bool sessionPresent) { * @return void */ void ShtUsermod::appendConfigData() { - oappend(SET_F("dd=addDropdown('")); + oappend(F("dd=addDropdown('")); oappend(_name); - oappend(SET_F("','")); + oappend(F("','")); oappend(_shtType); - oappend(SET_F("');")); - oappend(SET_F("addOption(dd,'SHT30',0);")); - oappend(SET_F("addOption(dd,'SHT31',1);")); - oappend(SET_F("addOption(dd,'SHT35',2);")); - oappend(SET_F("addOption(dd,'SHT85',3);")); - oappend(SET_F("dd=addDropdown('")); + oappend(F("');")); + oappend(F("addOption(dd,'SHT30',0);")); + oappend(F("addOption(dd,'SHT31',1);")); + oappend(F("addOption(dd,'SHT35',2);")); + oappend(F("addOption(dd,'SHT85',3);")); + oappend(F("dd=addDropdown('")); oappend(_name); - oappend(SET_F("','")); + oappend(F("','")); oappend(_unitOfTemp); - oappend(SET_F("');")); - oappend(SET_F("addOption(dd,'Celsius',0);")); - oappend(SET_F("addOption(dd,'Fahrenheit',1);")); + oappend(F("');")); + oappend(F("addOption(dd,'Celsius',0);")); + oappend(F("addOption(dd,'Fahrenheit',1);")); } /** diff --git a/usermods/smartnest/readme.md b/usermods/smartnest/readme.md index 5c3ef8072..62bfcdada 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 8d2b04ff9..9d21ef2e7 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) @@ -45,7 +49,7 @@ private: void setColor(int r, int g, int b) { - strip.setColor(0, r, g, b); + strip.getMainSegment().setColor(0, RGBW32(r, g, b, 0)); stateUpdated(CALL_MODE_DIRECT_CHANGE); char msg[18] {}; sprintf(msg, "rgb(%d,%d,%d)", r, g, b); @@ -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/stairway_wipe_basic/stairway-wipe-usermod-v2.h b/usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h index f712316b8..707479df1 100644 --- a/usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h +++ b/usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h @@ -96,7 +96,7 @@ void setup() { jsonTransitionOnce = true; strip.setTransition(0); //no transition effectCurrent = FX_MODE_COLOR_WIPE; - resetTimebase(); //make sure wipe starts from beginning + strip.resetTimebase(); //make sure wipe starts from beginning //set wipe direction Segment& seg = strip.getSegment(0); diff --git a/usermods/stairway_wipe_basic/wled06_usermod.ino b/usermods/stairway_wipe_basic/wled06_usermod.ino index c1264ebfb..dc2159ee9 100644 --- a/usermods/stairway_wipe_basic/wled06_usermod.ino +++ b/usermods/stairway_wipe_basic/wled06_usermod.ino @@ -86,7 +86,7 @@ void startWipe() bri = briLast; //turn on transitionDelayTemp = 0; //no transition effectCurrent = FX_MODE_COLOR_WIPE; - resetTimebase(); //make sure wipe starts from beginning + strip.resetTimebase(); //make sure wipe starts from beginning //set wipe direction Segment& seg = strip.getSegment(0); diff --git a/usermods/usermod_v2_HttpPullLightControl/readme.md b/usermods/usermod_v2_HttpPullLightControl/readme.md index cf7f971f7..eb56d505d 100644 --- a/usermods/usermod_v2_HttpPullLightControl/readme.md +++ b/usermods/usermod_v2_HttpPullLightControl/readme.md @@ -93,7 +93,7 @@ After getting the URL (it can be a static file like static.json or a mylogic.php - -D ABL_MILLIAMPS_DEFAULT=450 - -D DEFAULT_LED_COUNT=60 ; For a LED Ring of 60 LEDs - -D BTNPIN=41 ; The M5Stack Atom S3 Lite has a button on GPIO41 -- -D LEDPIN=2 ; The M5Stack Atom S3 Lite has a Grove connector on the front, we use this GPIO2 +- -D DATA_PINS=2 ; The M5Stack Atom S3 Lite has a Grove connector on the front, we use this GPIO2 - -D STATUSLED=35 ; The M5Stack Atom S3 Lite has a Multi-Color LED on GPIO35, although I didnt managed to control it - -D IRPIN=4 ; The M5Stack Atom S3 Lite has a IR LED on GPIO4 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 2a63dd4d8..a257413b4 100644 --- a/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h +++ b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h @@ -103,7 +103,7 @@ class AutoSaveUsermod : public Usermod { #ifdef USERMOD_FOUR_LINE_DISPLAY // This Usermod has enhanced functionality if // FourLineDisplayUsermod is available. - display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP); + display = (FourLineDisplayUsermod*) UsermodManager::lookup(USERMOD_ID_FOUR_LINE_DISP); #endif initDone = true; if (enabled && applyAutoSaveOnBoot) applyPreset(autoSavePreset); @@ -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/platformio_override.sample.ini b/usermods/usermod_v2_four_line_display_ALT/platformio_override.sample.ini new file mode 100644 index 000000000..e59637453 --- /dev/null +++ b/usermods/usermod_v2_four_line_display_ALT/platformio_override.sample.ini @@ -0,0 +1,18 @@ +[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 FLD_TYPE=SH1106 + -D I2CSCLPIN=27 + -D I2CSDAPIN=26 + +lib_deps = + ${esp32.lib_deps} + U8g2@~2.34.4 + Wire diff --git a/usermods/usermod_v2_four_line_display_ALT/readme.md b/usermods/usermod_v2_four_line_display_ALT/readme.md index ea9f43610..39bb5d28e 100644 --- a/usermods/usermod_v2_four_line_display_ALT/readme.md +++ b/usermods/usermod_v2_four_line_display_ALT/readme.md @@ -1,38 +1,57 @@ -# 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" -"usermod_v2_rotary_encoder_ui" +This usermod could be used in compination with `usermod_v2_rotary_encoder_ui_ALT`. -The core of these usermods are a copy of the originals. The main changes are to the FourLineDisplay usermod. -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 original "usermod_v2_auto_save" will not work with the display just yet. +## Functionalities 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 +* Saturation -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 the AP, SSID and Password -Also shows if the timer is enabled +Also shows if the timer is enabled. [See the pair of usermods in action](https://www.youtube.com/watch?v=ulZnBt9z3TI) ## 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, - 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. + +## 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 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 82a5e1a81..684dd86e4 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. @@ -1198,34 +1202,35 @@ void FourLineDisplayUsermod::onUpdateBegin(bool init) { //} void FourLineDisplayUsermod::appendConfigData() { - oappend(SET_F("dd=addDropdown('4LineDisplay','type');")); - oappend(SET_F("addOption(dd,'None',0);")); - oappend(SET_F("addOption(dd,'SSD1306',1);")); - oappend(SET_F("addOption(dd,'SH1106',2);")); - 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,'SSD1306 SPI',6);")); - oappend(SET_F("addOption(dd,'SSD1306 SPI 128x64',7);")); - oappend(SET_F("addOption(dd,'SSD1309 SPI 128x64',8);")); - oappend(SET_F("addInfo('4LineDisplay:type',1,'
Change may require reboot','');")); - oappend(SET_F("addInfo('4LineDisplay:pin[]',0,'','SPI CS');")); - oappend(SET_F("addInfo('4LineDisplay:pin[]',1,'','SPI DC');")); - oappend(SET_F("addInfo('4LineDisplay:pin[]',2,'','SPI RST');")); + oappend(F("dd=addDropdown('4LineDisplay','type');")); + oappend(F("addOption(dd,'None',0);")); + oappend(F("addOption(dd,'SSD1306',1);")); + oappend(F("addOption(dd,'SH1106',2);")); + oappend(F("addOption(dd,'SSD1306 128x64',3);")); + oappend(F("addOption(dd,'SSD1305',4);")); + oappend(F("addOption(dd,'SSD1305 128x64',5);")); + oappend(F("addOption(dd,'SSD1309 128x64',9);")); + oappend(F("addOption(dd,'SSD1306 SPI',6);")); + oappend(F("addOption(dd,'SSD1306 SPI 128x64',7);")); + oappend(F("addOption(dd,'SSD1309 SPI 128x64',8);")); + oappend(F("addInfo('4LineDisplay:type',1,'
Change may require reboot','');")); + oappend(F("addInfo('4LineDisplay:pin[]',0,'','SPI CS');")); + oappend(F("addInfo('4LineDisplay:pin[]',1,'','SPI DC');")); + oappend(F("addInfo('4LineDisplay:pin[]',2,'','SPI RST');")); } /* * 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,12 +1302,12 @@ 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); if (isSPI) { - if (pinsChanged || !newSPI) pinManager.deallocateMultiplePins((const uint8_t*)oldPin, 3, PinOwner::UM_FourLineDisplay); + if (pinsChanged || !newSPI) PinManager::deallocateMultiplePins((const uint8_t*)oldPin, 3, PinOwner::UM_FourLineDisplay); if (!newSPI) { // was SPI but is no longer SPI if (i2c_scl<0 || i2c_sda<0) { newType=NONE; } @@ -1310,7 +1315,7 @@ bool FourLineDisplayUsermod::readFromConfig(JsonObject& root) { // still SPI but pins changed PinManagerPinType cspins[3] = { { ioPin[0], true }, { ioPin[1], true }, { ioPin[2], true } }; if (ioPin[0]<0 || ioPin[1]<0 || ioPin[1]<0) { newType=NONE; } - else if (!pinManager.allocateMultiplePins(cspins, 3, PinOwner::UM_FourLineDisplay)) { newType=NONE; } + else if (!PinManager::allocateMultiplePins(cspins, 3, PinOwner::UM_FourLineDisplay)) { newType=NONE; } } } else if (newSPI) { // was I2C but is now SPI @@ -1319,7 +1324,7 @@ bool FourLineDisplayUsermod::readFromConfig(JsonObject& root) { } else { PinManagerPinType pins[3] = { { ioPin[0], true }, { ioPin[1], true }, { ioPin[2], true } }; if (ioPin[0]<0 || ioPin[1]<0 || ioPin[1]<0) { newType=NONE; } - else if (!pinManager.allocateMultiplePins(pins, 3, PinOwner::UM_FourLineDisplay)) { newType=NONE; } + else if (!PinManager::allocateMultiplePins(pins, 3, PinOwner::UM_FourLineDisplay)) { newType=NONE; } } } else { // just I2C type changed @@ -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 000000000..8a88fd6b5 --- /dev/null +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.sample.ini @@ -0,0 +1,14 @@ +[platformio] +default_envs = esp32dev + +[env:esp32dev] +board = esp32dev +platform = ${esp32.platform} +build_unflags = ${common.build_unflags} +build_flags = + ${common.build_flags_esp32} + -D USERMOD_ROTARY_ENCODER_UI + -D USERMOD_ROTARY_ENCODER_GPIO=INPUT + -D ENCODER_DT_PIN=21 + -D ENCODER_CLK_PIN=23 + -D ENCODER_SW_PIN=0 diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md b/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md index 516362380..c46e87663 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md @@ -1,28 +1,19 @@ # Rotary Encoder UI 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" -"usermod_v2_rotary_encoder_ui" +This usermod supports the UI of the `usermod_v2_rotary_encoder_ui_ALT`. -The core of these usermods are a copy of the originals. The main changes are to the FourLineDisplay usermod. -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 original "usermod_v2_auto_save" will not work with the display just yet. +## Functionalities 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 +Press and hold the encoder to display Network Info. If AP is active, it will display the AP, SSID and Password Also shows if the timer is enabled. @@ -30,14 +21,24 @@ 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. +### Define Your Options + +* `USERMOD_ROTARY_ENCODER_UI` - define this to have this user mod included wled00\usermods_list.cpp +* `USERMOD_FOUR_LINE_DISPLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp + also tells this usermod that the display is available + (see the Four Line Display usermod `readme.md` for more details) +* `ENCODER_DT_PIN` - defaults to 18 +* `ENCODER_CLK_PIN` - defaults to 5 +* `ENCODER_SW_PIN` - defaults to 19 +* `USERMOD_ROTARY_ENCODER_GPIO` - GPIO functionality: + `INPUT_PULLUP` to use internal pull-up + `INPUT` to use pull-up on the PCB ### PlatformIO requirements -Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. +No special requirements. ## Change Log 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 6a15b520b..383c1193e 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= 0 && pinManager.allocatePin(pinIRQ, false, PinOwner::UM_RotaryEncoderUI)) { + if (pinIRQ >= 0 && PinManager::allocatePin(pinIRQ, false, PinOwner::UM_RotaryEncoderUI)) { pinMode(pinIRQ, INPUT_PULLUP); attachInterrupt(pinIRQ, i2cReadingISR, FALLING); // RISING, FALLING, CHANGE, ONLOW, ONHIGH DEBUG_PRINTLN(F("Interrupt attached.")); @@ -502,7 +502,7 @@ void RotaryEncoderUIUsermod::setup() } } else { PinManagerPinType pins[3] = { { pinA, false }, { pinB, false }, { pinC, false } }; - if (pinA<0 || pinB<0 || !pinManager.allocateMultiplePins(pins, 3, PinOwner::UM_RotaryEncoderUI)) { + if (pinA<0 || pinB<0 || !PinManager::allocateMultiplePins(pins, 3, PinOwner::UM_RotaryEncoderUI)) { pinA = pinB = pinC = -1; enabled = false; return; @@ -525,7 +525,7 @@ void RotaryEncoderUIUsermod::setup() #ifdef USERMOD_FOUR_LINE_DISPLAY // This Usermod uses FourLineDisplayUsermod for the best experience. // But it's optional. But you want it. - display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP); + display = (FourLineDisplayUsermod*) UsermodManager::lookup(USERMOD_ID_FOUR_LINE_DISP); if (display != nullptr) { display->setMarkLine(1, 0); } @@ -700,7 +700,7 @@ void RotaryEncoderUIUsermod::findCurrentEffectAndPalette() { effectPaletteIndex = 0; DEBUG_PRINTLN(effectPalette); - for (uint8_t i = 0; i < strip.getPaletteCount()+strip.customPalettes.size(); i++) { + for (unsigned i = 0; i < strip.getPaletteCount()+strip.customPalettes.size(); i++) { if (palettes_alpha_indexes[i] == effectPalette) { effectPaletteIndex = i; DEBUG_PRINTLN(F("Found palette.")); @@ -764,7 +764,7 @@ void RotaryEncoderUIUsermod::changeEffect(bool increase) { effectCurrent = modes_alpha_indexes[effectCurrentIndex]; stateChanged = true; if (applyToAll) { - for (byte i=0; i(not hex!)
');")); - oappend(SET_F("d.extra.push({'Rotary-Encoder':{pin:[['P0',100],['P1',101],['P2',102],['P3',103],['P4',104],['P5',105],['P6',106],['P7',107]]}});")); + oappend(F("addInfo('Rotary-Encoder:PCF8574-address',1,'(not hex!)');")); + oappend(F("d.extra.push({'Rotary-Encoder':{pin:[['P0',100],['P1',101],['P2',102],['P3',103],['P4',104],['P5',105],['P6',106],['P7',107]]}});")); } /** @@ -1138,14 +1138,14 @@ bool RotaryEncoderUIUsermod::readFromConfig(JsonObject &root) { if (oldPcf8574) { if (pinIRQ >= 0) { detachInterrupt(pinIRQ); - pinManager.deallocatePin(pinIRQ, PinOwner::UM_RotaryEncoderUI); + PinManager::deallocatePin(pinIRQ, PinOwner::UM_RotaryEncoderUI); DEBUG_PRINTLN(F("Deallocated old IRQ pin.")); } pinIRQ = newIRQpin<100 ? newIRQpin : -1; // ignore PCF8574 pins } else { - pinManager.deallocatePin(pinA, PinOwner::UM_RotaryEncoderUI); - pinManager.deallocatePin(pinB, PinOwner::UM_RotaryEncoderUI); - pinManager.deallocatePin(pinC, PinOwner::UM_RotaryEncoderUI); + PinManager::deallocatePin(pinA, PinOwner::UM_RotaryEncoderUI); + PinManager::deallocatePin(pinB, PinOwner::UM_RotaryEncoderUI); + PinManager::deallocatePin(pinC, PinOwner::UM_RotaryEncoderUI); DEBUG_PRINTLN(F("Deallocated old pins.")); } pinA = newDTpin; diff --git a/usermods/usermod_v2_word_clock/usermod_v2_word_clock.h b/usermods/usermod_v2_word_clock/usermod_v2_word_clock.h index b66be290a..7ecec08e5 100644 --- a/usermods/usermod_v2_word_clock/usermod_v2_word_clock.h +++ b/usermods/usermod_v2_word_clock/usermod_v2_word_clock.h @@ -433,8 +433,8 @@ class WordClockUsermod : public Usermod void appendConfigData() { - oappend(SET_F("addInfo('WordClockUsermod:ledOffset', 1, 'Number of LEDs before the letters');")); - oappend(SET_F("addInfo('WordClockUsermod:Norddeutsch', 1, 'Viertel vor instead of Dreiviertel');")); + oappend(F("addInfo('WordClockUsermod:ledOffset', 1, 'Number of LEDs before the letters');")); + oappend(F("addInfo('WordClockUsermod:Norddeutsch', 1, 'Viertel vor instead of Dreiviertel');")); } /* diff --git a/usermods/wireguard/wireguard.h b/usermods/wireguard/wireguard.h index 8c88d0001..8656a704a 100644 --- a/usermods/wireguard/wireguard.h +++ b/usermods/wireguard/wireguard.h @@ -54,13 +54,13 @@ class WireguardUsermod : public Usermod { } void appendConfigData() { - oappend(SET_F("addInfo('WireGuard:host',1,'Server Hostname');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('WireGuard:port',1,'Server Port');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('WireGuard:ip',1,'Device IP');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('WireGuard:psk',1,'Pre Shared Key (optional)');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('WireGuard:pem',1,'Private Key');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('WireGuard:pub',1,'Public Key');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('WireGuard:tz',1,'POSIX timezone string');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('WireGuard:host',1,'Server Hostname');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('WireGuard:port',1,'Server Port');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('WireGuard:ip',1,'Device IP');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('WireGuard:psk',1,'Pre Shared Key (optional)');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('WireGuard:pem',1,'Private Key');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('WireGuard:pub',1,'Public Key');")); // 0 is field type, 1 is actual field + oappend(F("addInfo('WireGuard:tz',1,'POSIX timezone string');")); // 0 is field type, 1 is actual field } void addToConfig(JsonObject& root) { diff --git a/usermods/word-clock-matrix/usermod_word_clock_matrix.h b/usermods/word-clock-matrix/usermod_word_clock_matrix.h index 506c1275e..82499c0ce 100644 --- a/usermods/word-clock-matrix/usermod_word_clock_matrix.h +++ b/usermods/word-clock-matrix/usermod_word_clock_matrix.h @@ -31,14 +31,14 @@ public: //strip.getSegment(1).setOption(SEG_OPTION_SELECTED, true); //select first two segments (background color + FX settable) - WS2812FX::Segment &seg = strip.getSegment(0); + Segment &seg = strip.getSegment(0); seg.colors[0] = ((0 << 24) | ((0 & 0xFF) << 16) | ((0 & 0xFF) << 8) | ((0 & 0xFF))); strip.getSegment(0).setOption(0, false); strip.getSegment(0).setOption(2, false); //other segments are text for (int i = 1; i < 10; i++) { - WS2812FX::Segment &seg = strip.getSegment(i); + Segment &seg = strip.getSegment(i); seg.colors[0] = ((0 << 24) | ((0 & 0xFF) << 16) | ((190 & 0xFF) << 8) | ((180 & 0xFF))); strip.getSegment(i).setOption(0, true); strip.setBrightness(64); @@ -80,61 +80,61 @@ public: void displayTime(byte hour, byte minute) { bool isToHour = false; //true if minute > 30 - strip.setSegment(0, 0, 64); // background - strip.setSegment(1, 0, 2); //It is + strip.getSegment(0).setGeometry(0, 64); // background + strip.getSegment(1).setGeometry(0, 2); //It is - strip.setSegment(2, 0, 0); - strip.setSegment(3, 0, 0); //disable minutes - strip.setSegment(4, 0, 0); //past - strip.setSegment(6, 0, 0); //to - strip.setSegment(8, 0, 0); //disable o'clock + strip.getSegment(2).setGeometry(0, 0); + strip.getSegment(3).setGeometry(0, 0); //disable minutes + strip.getSegment(4).setGeometry(0, 0); //past + strip.getSegment(6).setGeometry(0, 0); //to + strip.getSegment(8).setGeometry(0, 0); //disable o'clock if (hour < 24) //valid time, display { if (minute == 30) { - strip.setSegment(2, 3, 6); //half - strip.setSegment(3, 0, 0); //minutes + strip.getSegment(2).setGeometry(3, 6); //half + strip.getSegment(3).setGeometry(0, 0); //minutes } else if (minute == 15 || minute == 45) { - strip.setSegment(3, 0, 0); //minutes + strip.getSegment(3).setGeometry(0, 0); //minutes } else if (minute == 10) { - //strip.setSegment(5, 6, 8); //ten + //strip.getSegment(5).setGeometry(6, 8); //ten } else if (minute == 5) { - //strip.setSegment(5, 16, 18); //five + //strip.getSegment(5).setGeometry(16, 18); //five } else if (minute == 0) { - strip.setSegment(3, 0, 0); //minutes + strip.getSegment(3).setGeometry(0, 0); //minutes //hourChime(); } else { - strip.setSegment(3, 18, 22); //minutes + strip.getSegment(3).setGeometry(18, 22); //minutes } //past or to? if (minute == 0) { //full hour - strip.setSegment(3, 0, 0); //disable minutes - strip.setSegment(4, 0, 0); //disable past - strip.setSegment(6, 0, 0); //disable to - strip.setSegment(8, 60, 64); //o'clock + strip.getSegment(3).setGeometry(0, 0); //disable minutes + strip.getSegment(4).setGeometry(0, 0); //disable past + strip.getSegment(6).setGeometry(0, 0); //disable to + strip.getSegment(8).setGeometry(60, 64); //o'clock } else if (minute > 34) { - //strip.setSegment(6, 22, 24); //to + //strip.getSegment(6).setGeometry(22, 24); //to //minute = 60 - minute; isToHour = true; } else { - //strip.setSegment(4, 24, 27); //past + //strip.getSegment(4).setGeometry(24, 27); //past //isToHour = false; } } @@ -143,68 +143,68 @@ public: if (minute <= 4) { - strip.setSegment(3, 0, 0); //nothing - strip.setSegment(5, 0, 0); //nothing - strip.setSegment(6, 0, 0); //nothing - strip.setSegment(8, 60, 64); //o'clock + strip.getSegment(3).setGeometry(0, 0); //nothing + strip.getSegment(5).setGeometry(0, 0); //nothing + strip.getSegment(6).setGeometry(0, 0); //nothing + strip.getSegment(8).setGeometry(60, 64); //o'clock } else if (minute <= 9) { - strip.setSegment(5, 16, 18); // five past - strip.setSegment(4, 24, 27); //past + strip.getSegment(5).setGeometry(16, 18); // five past + strip.getSegment(4).setGeometry(24, 27); //past } else if (minute <= 14) { - strip.setSegment(5, 6, 8); // ten past - strip.setSegment(4, 24, 27); //past + strip.getSegment(5).setGeometry(6, 8); // ten past + strip.getSegment(4).setGeometry(24, 27); //past } else if (minute <= 19) { - strip.setSegment(5, 8, 12); // quarter past - strip.setSegment(3, 0, 0); //minutes - strip.setSegment(4, 24, 27); //past + strip.getSegment(5).setGeometry(8, 12); // quarter past + strip.getSegment(3).setGeometry(0, 0); //minutes + strip.getSegment(4).setGeometry(24, 27); //past } else if (minute <= 24) { - strip.setSegment(5, 12, 16); // twenty past - strip.setSegment(4, 24, 27); //past + strip.getSegment(5).setGeometry(12, 16); // twenty past + strip.getSegment(4).setGeometry(24, 27); //past } else if (minute <= 29) { - strip.setSegment(5, 12, 18); // twenty-five past - strip.setSegment(4, 24, 27); //past + strip.getSegment(5).setGeometry(12, 18); // twenty-five past + strip.getSegment(4).setGeometry(24, 27); //past } else if (minute <= 34) { - strip.setSegment(5, 3, 6); // half past - strip.setSegment(3, 0, 0); //minutes - strip.setSegment(4, 24, 27); //past + strip.getSegment(5).setGeometry(3, 6); // half past + strip.getSegment(3).setGeometry(0, 0); //minutes + strip.getSegment(4).setGeometry(24, 27); //past } else if (minute <= 39) { - strip.setSegment(5, 12, 18); // twenty-five to - strip.setSegment(6, 22, 24); //to + strip.getSegment(5).setGeometry(12, 18); // twenty-five to + strip.getSegment(6).setGeometry(22, 24); //to } else if (minute <= 44) { - strip.setSegment(5, 12, 16); // twenty to - strip.setSegment(6, 22, 24); //to + strip.getSegment(5).setGeometry(12, 16); // twenty to + strip.getSegment(6).setGeometry(22, 24); //to } else if (minute <= 49) { - strip.setSegment(5, 8, 12); // quarter to - strip.setSegment(3, 0, 0); //minutes - strip.setSegment(6, 22, 24); //to + strip.getSegment(5).setGeometry(8, 12); // quarter to + strip.getSegment(3).setGeometry(0, 0); //minutes + strip.getSegment(6).setGeometry(22, 24); //to } else if (minute <= 54) { - strip.setSegment(5, 6, 8); // ten to - strip.setSegment(6, 22, 24); //to + strip.getSegment(5).setGeometry(6, 8); // ten to + strip.getSegment(6).setGeometry(22, 24); //to } else if (minute <= 59) { - strip.setSegment(5, 16, 18); // five to - strip.setSegment(6, 22, 24); //to + strip.getSegment(5).setGeometry(16, 18); // five to + strip.getSegment(6).setGeometry(22, 24); //to } //hours @@ -220,45 +220,45 @@ public: switch (hour) { case 1: - strip.setSegment(7, 27, 29); + strip.getSegment(7).setGeometry(27, 29); break; //one case 2: - strip.setSegment(7, 35, 37); + strip.getSegment(7).setGeometry(35, 37); break; //two case 3: - strip.setSegment(7, 29, 32); + strip.getSegment(7).setGeometry(29, 32); break; //three case 4: - strip.setSegment(7, 32, 35); + strip.getSegment(7).setGeometry(32, 35); break; //four case 5: - strip.setSegment(7, 37, 40); + strip.getSegment(7).setGeometry(37, 40); break; //five case 6: - strip.setSegment(7, 43, 45); + strip.getSegment(7).setGeometry(43, 45); break; //six case 7: - strip.setSegment(7, 40, 43); + strip.getSegment(7).setGeometry(40, 43); break; //seven case 8: - strip.setSegment(7, 45, 48); + strip.getSegment(7).setGeometry(45, 48); break; //eight case 9: - strip.setSegment(7, 48, 50); + strip.getSegment(7).setGeometry(48, 50); break; //nine case 10: - strip.setSegment(7, 54, 56); + strip.getSegment(7).setGeometry(54, 56); break; //ten case 11: - strip.setSegment(7, 50, 54); + strip.getSegment(7).setGeometry(50, 54); break; //eleven case 12: - strip.setSegment(7, 56, 60); + strip.getSegment(7).setGeometry(56, 60); break; //twelve } selectWordSegments(true); - applyMacro(1); + applyPreset(1); } void timeOfDay() diff --git a/usermods/word-clock-matrix/word-clock-matrix.cpp b/usermods/word-clock-matrix/word-clock-matrix.cpp deleted file mode 100644 index 67c5b1e47..000000000 --- a/usermods/word-clock-matrix/word-clock-matrix.cpp +++ /dev/null @@ -1,305 +0,0 @@ -#include "wled.h" -/* - * This v1 usermod file allows you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality - * EEPROM bytes 2750+ are reserved for your custom use case. (if you extend #define EEPSIZE in const.h) - * If you just need 8 bytes, use 2551-2559 (you do not need to increase EEPSIZE) - * - * Consider the v2 usermod API if you need a more advanced feature set! - */ - - -uint8_t minuteLast = 99; -int dayBrightness = 128; -int nightBrightness = 16; - -//Use userVar0 and userVar1 (API calls &U0=,&U1=, uint16_t) - -//gets called once at boot. Do all initialization that doesn't depend on network here -void userSetup() -{ -saveMacro(14, "A=128", false); -saveMacro(15, "A=64", false); -saveMacro(16, "A=16", false); - -saveMacro(1, "&FX=0&R=255&G=255&B=255", false); - -//strip.getSegment(1).setOption(SEG_OPTION_SELECTED, true); - - //select first two segments (background color + FX settable) - Segment &seg = strip.getSegment(0); - seg.colors[0] = ((0 << 24) | ((0 & 0xFF) << 16) | ((0 & 0xFF) << 8) | ((0 & 0xFF))); - strip.getSegment(0).setOption(0, false); - strip.getSegment(0).setOption(2, false); - //other segments are text - for (int i = 1; i < 10; i++) - { - Segment &seg = strip.getSegment(i); - seg.colors[0] = ((0 << 24) | ((0 & 0xFF) << 16) | ((190 & 0xFF) << 8) | ((180 & 0xFF))); - strip.getSegment(i).setOption(0, true); - strip.setBrightness(128); - } -} - -//gets called every time WiFi is (re-)connected. Initialize own network interfaces here -void userConnected() -{ -} - -void selectWordSegments(bool state) -{ - for (int i = 1; i < 10; i++) - { - //Segment &seg = strip.getSegment(i); - strip.getSegment(i).setOption(0, state); - // strip.getSegment(1).setOption(SEG_OPTION_SELECTED, true); - //seg.mode = 12; - //seg.palette = 1; - //strip.setBrightness(255); - } - strip.getSegment(0).setOption(0, !state); -} - -void hourChime() -{ - //strip.resetSegments(); - selectWordSegments(true); - colorUpdated(CALL_MODE_FX_CHANGED); - //savePreset(255); - selectWordSegments(false); - //strip.getSegment(0).setOption(0, true); - strip.getSegment(0).setOption(2, true); - applyPreset(12); - colorUpdated(CALL_MODE_FX_CHANGED); -} - -void displayTime(byte hour, byte minute) -{ - bool isToHour = false; //true if minute > 30 - strip.setSegment(0, 0, 64); // background - strip.setSegment(1, 0, 2); //It is - - strip.setSegment(2, 0, 0); - strip.setSegment(3, 0, 0); //disable minutes - strip.setSegment(4, 0, 0); //past - strip.setSegment(6, 0, 0); //to - strip.setSegment(8, 0, 0); //disable o'clock - - if (hour < 24) //valid time, display - { - if (minute == 30) - { - strip.setSegment(2, 3, 6); //half - strip.setSegment(3, 0, 0); //minutes - } - else if (minute == 15 || minute == 45) - { - strip.setSegment(3, 0, 0); //minutes - } - else if (minute == 10) - { - //strip.setSegment(5, 6, 8); //ten - } - else if (minute == 5) - { - //strip.setSegment(5, 16, 18); //five - } - else if (minute == 0) - { - strip.setSegment(3, 0, 0); //minutes - //hourChime(); - } - else - { - strip.setSegment(3, 18, 22); //minutes - } - - //past or to? - if (minute == 0) - { //full hour - strip.setSegment(3, 0, 0); //disable minutes - strip.setSegment(4, 0, 0); //disable past - strip.setSegment(6, 0, 0); //disable to - strip.setSegment(8, 60, 64); //o'clock - } - else if (minute > 34) - { - //strip.setSegment(6, 22, 24); //to - //minute = 60 - minute; - isToHour = true; - } - else - { - //strip.setSegment(4, 24, 27); //past - //isToHour = false; - } - } - else - { //temperature display - } - - //byte minuteRem = minute %10; - - if (minute <= 4) - { - strip.setSegment(3, 0, 0); //nothing - strip.setSegment(5, 0, 0); //nothing - strip.setSegment(6, 0, 0); //nothing - strip.setSegment(8, 60, 64); //o'clock - } - else if (minute <= 9) - { - strip.setSegment(5, 16, 18); // five past - strip.setSegment(4, 24, 27); //past - } - else if (minute <= 14) - { - strip.setSegment(5, 6, 8); // ten past - strip.setSegment(4, 24, 27); //past - } - else if (minute <= 19) - { - strip.setSegment(5, 8, 12); // quarter past - strip.setSegment(3, 0, 0); //minutes - strip.setSegment(4, 24, 27); //past - } - else if (minute <= 24) - { - strip.setSegment(5, 12, 16); // twenty past - strip.setSegment(4, 24, 27); //past - } - else if (minute <= 29) - { - strip.setSegment(5, 12, 18); // twenty-five past - strip.setSegment(4, 24, 27); //past - } - else if (minute <= 34) - { - strip.setSegment(5, 3, 6); // half past - strip.setSegment(3, 0, 0); //minutes - strip.setSegment(4, 24, 27); //past - } - else if (minute <= 39) - { - strip.setSegment(5, 12, 18); // twenty-five to - strip.setSegment(6, 22, 24); //to - } - else if (minute <= 44) - { - strip.setSegment(5, 12, 16); // twenty to - strip.setSegment(6, 22, 24); //to - } - else if (minute <= 49) - { - strip.setSegment(5, 8, 12); // quarter to - strip.setSegment(3, 0, 0); //minutes - strip.setSegment(6, 22, 24); //to - } - else if (minute <= 54) - { - strip.setSegment(5, 6, 8); // ten to - strip.setSegment(6, 22, 24); //to - } - else if (minute <= 59) - { - strip.setSegment(5, 16, 18); // five to - strip.setSegment(6, 22, 24); //to - } - - //hours - if (hour > 23) - return; - if (isToHour) - hour++; - if (hour > 12) - hour -= 12; - if (hour == 0) - hour = 12; - - switch (hour) - { - case 1: - strip.setSegment(7, 27, 29); - break; //one - case 2: - strip.setSegment(7, 35, 37); - break; //two - case 3: - strip.setSegment(7, 29, 32); - break; //three - case 4: - strip.setSegment(7, 32, 35); - break; //four - case 5: - strip.setSegment(7, 37, 40); - break; //five - case 6: - strip.setSegment(7, 43, 45); - break; //six - case 7: - strip.setSegment(7, 40, 43); - break; //seven - case 8: - strip.setSegment(7, 45, 48); - break; //eight - case 9: - strip.setSegment(7, 48, 50); - break; //nine - case 10: - strip.setSegment(7, 54, 56); - break; //ten - case 11: - strip.setSegment(7, 50, 54); - break; //eleven - case 12: - strip.setSegment(7, 56, 60); - break; //twelve - } - -selectWordSegments(true); -applyMacro(1); -} - -void timeOfDay() { -// NOT USED: use timed macros instead - //Used to set brightness dependant of time of day - lights dimmed at night - - //monday to thursday and sunday - - if ((weekday(localTime) == 6) | (weekday(localTime) == 7)) { - if (hour(localTime) > 0 | hour(localTime) < 8) { - strip.setBrightness(nightBrightness); - } - else { - strip.setBrightness(dayBrightness); - } - } - else { - if (hour(localTime) < 6 | hour(localTime) >= 22) { - strip.setBrightness(nightBrightness); - } - else { - strip.setBrightness(dayBrightness); - } - } -} - -//loop. You can use "if (WLED_CONNECTED)" to check for successful connection -void userLoop() -{ - if (minute(localTime) != minuteLast) - { - updateLocalTime(); - //timeOfDay(); - minuteLast = minute(localTime); - displayTime(hour(localTime), minute(localTime)); - if (minute(localTime) == 0){ - hourChime(); - } - if (minute(localTime) == 1){ - //turn off background segment; - strip.getSegment(0).setOption(2, false); - //applyPreset(255); - } - } -} diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 5543b54ba..081009f4e 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -2,24 +2,10 @@ WS2812FX.cpp contains all effect methods Harm Aldick - 2016 www.aldick.org - LICENSE - The MIT License (MIT) + Copyright (c) 2016 Harm Aldick - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. + Licensed under the EUPL v. 1.2 or later + Adapted from code originally licensed under the MIT license Modified heavily for WLED */ @@ -28,18 +14,60 @@ #include "FX.h" #include "fcn_declare.h" -#define IBN 5100 + ////////////// + // DEV INFO // + ////////////// +/* + information for FX metadata strings: https://kno.wled.ge/interfaces/json-api/#effect-metadata + + Audio Reactive: use the following code to pass usermod variables to effect + + uint8_t *binNum = (uint8_t*)&SEGENV.aux1, *maxVol = (uint8_t*)(&SEGENV.aux1+1); // just in case assignment + bool samplePeak = false; + float FFT_MajorPeak = 1.0; + uint8_t *fftResult = nullptr; + float *fftBin = nullptr; + um_data_t *um_data; + if (usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + volumeSmth = *(float*) um_data->u_data[0]; + volumeRaw = *(float*) um_data->u_data[1]; + fftResult = (uint8_t*) um_data->u_data[2]; + samplePeak = *(uint8_t*) um_data->u_data[3]; + FFT_MajorPeak = *(float*) um_data->u_data[4]; + my_magnitude = *(float*) um_data->u_data[5]; + maxVol = (uint8_t*) um_data->u_data[6]; // requires UI element (SEGMENT.customX?), changes source element + binNum = (uint8_t*) um_data->u_data[7]; // requires UI element (SEGMENT.customX?), changes source element + fftBin = (float*) um_data->u_data[8]; + } else { + // add support for no audio data + um_data = simulateSound(SEGMENT.soundSim); + } +*/ + + +#define IBN 5100 // paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) #define PALETTE_SOLID_WRAP (strip.paletteBlend == 1 || strip.paletteBlend == 3) #define PALETTE_MOVING_WRAP !(strip.paletteBlend == 2 || (strip.paletteBlend == 0 && SEGMENT.speed == 0)) #define indexToVStrip(index, stripNr) ((index) | (int((stripNr)+1)<<16)) +// a few constants needed for AudioReactive effects +// for 22Khz sampling +#define MAX_FREQUENCY 11025 // sample frequency / 2 (as per Nyquist criterion) +#define MAX_FREQ_LOG10 4.04238f // log10(MAX_FREQUENCY) +// for 20Khz sampling +//#define MAX_FREQUENCY 10240 +//#define MAX_FREQ_LOG10 4.0103f +// for 10Khz sampling +//#define MAX_FREQUENCY 5120 +//#define MAX_FREQ_LOG10 3.71f + // effect utility functions uint8_t sin_gap(uint16_t in) { if (in & 0x100) return 0; - return sin8(in + 192); // correct phase shift of sine so that it starts and stops at 0 + return sin8_t(in + 192); // correct phase shift of sine so that it starts and stops at 0 } uint16_t triwave16(uint16_t in) { @@ -73,6 +101,15 @@ int8_t tristate_square8(uint8_t x, uint8_t pulsewidth, uint8_t attdec) { return 0; } +static um_data_t* getAudioData() { + um_data_t *um_data; + if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + return um_data; +} + // effect functions /* @@ -109,7 +146,7 @@ uint16_t blink(uint32_t color1, uint32_t color2, bool strobe, bool do_palette) { uint32_t color = on ? color1 : color2; if (color == color1 && do_palette) { - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } } else SEGMENT.fill(color); @@ -160,9 +197,10 @@ static const char _data_FX_MODE_STROBE_RAINBOW[] PROGMEM = "Strobe Rainbow@!;,!; * if (bool rev == true) then LEDs are turned off in reverse order */ uint16_t color_wipe(bool rev, bool useRandomColors) { + if (SEGLEN == 1) return mode_static(); uint32_t cycleTime = 750 + (255 - SEGMENT.speed)*150; uint32_t perc = strip.now % cycleTime; - uint16_t prog = (perc * 65535) / cycleTime; + unsigned prog = (perc * 65535) / cycleTime; bool back = (prog > 32767); if (back) { prog -= 32767; @@ -173,7 +211,7 @@ uint16_t color_wipe(bool rev, bool useRandomColors) { if (useRandomColors) { if (SEGENV.call == 0) { - SEGENV.aux0 = random8(); + SEGENV.aux0 = hw_random8(); SEGENV.step = 3; } if (SEGENV.step == 1) { //if flag set, change to new random color @@ -186,16 +224,16 @@ uint16_t color_wipe(bool rev, bool useRandomColors) { } } - uint16_t ledIndex = (prog * SEGLEN) >> 15; - uint16_t rem = 0; + unsigned ledIndex = (prog * SEGLEN) >> 15; + unsigned rem = 0; rem = (prog * SEGLEN) * 2; //mod 0xFFFF rem /= (SEGMENT.intensity +1); if (rem > 255) rem = 255; uint32_t col1 = useRandomColors? SEGMENT.color_wheel(SEGENV.aux1) : SEGCOLOR(1); - for (int i = 0; i < SEGLEN; i++) + for (unsigned i = 0; i < SEGLEN; i++) { - uint16_t index = (rev && back)? SEGLEN -1 -i : i; + unsigned index = (rev && back)? SEGLEN -1 -i : i; uint32_t col0 = useRandomColors? SEGMENT.color_wheel(SEGENV.aux0) : SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0); if (i < ledIndex) @@ -204,7 +242,7 @@ uint16_t color_wipe(bool rev, bool useRandomColors) { } else { SEGMENT.setPixelColor(index, back? col0 : col1); - if (i == ledIndex) SEGMENT.setPixelColor(index, color_blend(back? col0 : col1, back? col1 : col0, rem)); + if (i == ledIndex) SEGMENT.setPixelColor(index, color_blend(back? col0 : col1, back? col1 : col0, uint8_t(rem))); } } return FRAMETIME; @@ -256,7 +294,7 @@ uint16_t mode_random_color(void) { uint32_t cycleTime = 200 + (255 - SEGMENT.speed)*50; uint32_t it = strip.now / cycleTime; uint32_t rem = strip.now % cycleTime; - uint16_t fadedur = (cycleTime * SEGMENT.intensity) >> 8; + unsigned fadedur = (cycleTime * SEGMENT.intensity) >> 8; uint32_t fade = 255; if (fadedur) { @@ -265,7 +303,7 @@ uint16_t mode_random_color(void) { } if (SEGENV.call == 0) { - SEGENV.aux0 = random8(); + SEGENV.aux0 = hw_random8(); SEGENV.step = 2; } if (it != SEGENV.step) //new color @@ -275,7 +313,7 @@ uint16_t mode_random_color(void) { SEGENV.step = it; } - SEGMENT.fill(color_blend(SEGMENT.color_wheel(SEGENV.aux1), SEGMENT.color_wheel(SEGENV.aux0), fade)); + SEGMENT.fill(color_blend(SEGMENT.color_wheel(SEGENV.aux1), SEGMENT.color_wheel(SEGENV.aux0), uint8_t(fade))); return FRAMETIME; } static const char _data_FX_MODE_RANDOM_COLOR[] PROGMEM = "Random Colors@!,Fade time;;!;01"; @@ -290,25 +328,25 @@ uint16_t mode_dynamic(void) { if(SEGENV.call == 0) { //SEGMENT.fill(BLACK); - for (int i = 0; i < SEGLEN; i++) SEGENV.data[i] = random8(); + for (unsigned i = 0; i < SEGLEN; i++) SEGENV.data[i] = hw_random8(); } uint32_t cycleTime = 50 + (255 - SEGMENT.speed)*15; uint32_t it = strip.now / cycleTime; if (it != SEGENV.step && SEGMENT.speed != 0) //new color { - for (int i = 0; i < SEGLEN; i++) { - if (random8() <= SEGMENT.intensity) SEGENV.data[i] = random8(); // random color index + for (unsigned i = 0; i < SEGLEN; i++) { + if (hw_random8() <= SEGMENT.intensity) SEGENV.data[i] = hw_random8(); // random color index } SEGENV.step = it; } if (SEGMENT.check1) { - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.blendPixelColor(i, SEGMENT.color_wheel(SEGENV.data[i]), 16); } } else { - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_wheel(SEGENV.data[i])); } } @@ -334,16 +372,16 @@ static const char _data_FX_MODE_DYNAMIC_SMOOTH[] PROGMEM = "Dynamic Smooth@!,!;; * Does the "standby-breathing" of well known i-Devices. */ uint16_t mode_breath(void) { - uint16_t var = 0; - uint16_t counter = (strip.now * ((SEGMENT.speed >> 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 + var = sin16_t(counter) / 103; //close to parabolic in range 0-8192, max val. 23170 } uint8_t lum = 30 + var; - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), lum)); } @@ -356,10 +394,10 @@ 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)); + unsigned counter = (strip.now * ((SEGMENT.speed >> 3) +10)); uint8_t lum = triwave16(counter) >> 8; - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), lum)); } @@ -371,13 +409,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 +424,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,11 +459,11 @@ 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){ - SEGMENT.fill(color_blend(SEGMENT.color_wheel(counter),WHITE,128-SEGMENT.intensity)); + SEGMENT.fill(color_blend(SEGMENT.color_wheel(counter),WHITE,uint8_t(128-SEGMENT.intensity))); } else { SEGMENT.fill(SEGMENT.color_wheel(counter)); } @@ -439,10 +477,10 @@ 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++) { + for (unsigned i = 0; i < SEGLEN; i++) { //intensity/29 = 0 (1/16) 1 (1/8) 2 (1/4) 3 (1/2) 4 (1) 5 (2) 6 (4) 7 (8) 8 (16) uint8_t index = (i * (16 << (SEGMENT.intensity /29)) / SEGLEN) + counter; SEGMENT.setPixelColor(i, SEGMENT.color_wheel(index)); @@ -456,19 +494,19 @@ 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); - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { uint32_t col = color2; if (usePalette) color1 = SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0); if (theatre) { 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 +543,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; + for (unsigned i = 0; i < SEGLEN; i++) { + unsigned a = i*x_scale - counter; if (saw) { a &= 0xFF; if (a < 16) @@ -521,13 +559,13 @@ uint16_t running_base(bool saw, bool dual=false) { } a = 255 - a; } - uint8_t s = dual ? sin_gap(a) : sin8(a); + uint8_t s = dual ? sin_gap(a) : sin8_t(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); + ca = color_blend(ca, cb, uint8_t(127)); } SEGMENT.setPixelColor(i, ca); } @@ -575,23 +613,23 @@ 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; - SEGENV.aux1 = random16(); //new seed for our PRNG + SEGENV.aux1 = hw_random(); //new seed for our PRNG } SEGENV.aux0++; 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 +642,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) { @@ -612,10 +650,10 @@ uint16_t dissolve(uint32_t color) { SEGENV.aux0 = 1; } - for (int j = 0; j <= SEGLEN / 15; j++) { - if (random8() <= SEGMENT.intensity) { + for (unsigned j = 0; j <= SEGLEN / 15; j++) { + if (hw_random8() <= SEGMENT.intensity) { for (size_t times = 0; times < 10; times++) { //attempt to spawn a new pixel 10 times - unsigned i = random16(SEGLEN); + unsigned i = hw_random16(SEGLEN); unsigned index = i >> 3; unsigned bitNum = i & 0x07; bool fadeUp = bitRead(SEGENV.data[index], bitNum); @@ -655,7 +693,7 @@ uint16_t dissolve(uint32_t color) { * Blink several LEDs on and then off */ uint16_t mode_dissolve(void) { - return dissolve(SEGMENT.check1 ? SEGMENT.color_wheel(random8()) : SEGCOLOR(0)); + return dissolve(SEGMENT.check1 ? SEGMENT.color_wheel(hw_random8()) : SEGCOLOR(0)); } static const char _data_FX_MODE_DISSOLVE[] PROGMEM = "Dissolve@Repeat speed,Dissolve speed,,,,Random;!,!;!"; @@ -664,7 +702,7 @@ static const char _data_FX_MODE_DISSOLVE[] PROGMEM = "Dissolve@Repeat speed,Diss * Blink several LEDs on and then off in random colors */ uint16_t mode_dissolve_random(void) { - return dissolve(SEGMENT.color_wheel(random8())); + return dissolve(SEGMENT.color_wheel(hw_random8())); } static const char _data_FX_MODE_DISSOLVE_RANDOM[] PROGMEM = "Dissolve Rnd@Repeat speed,Dissolve speed;,!;!"; @@ -674,14 +712,14 @@ static const char _data_FX_MODE_DISSOLVE_RANDOM[] PROGMEM = "Dissolve Rnd@Repeat * Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/ */ uint16_t mode_sparkle(void) { - if (!SEGMENT.check2) for(int i = 0; i < SEGLEN; i++) { + if (!SEGMENT.check2) for(unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); } uint32_t cycleTime = 10 + (255 - SEGMENT.speed)*2; uint32_t it = strip.now / cycleTime; if (it != SEGENV.step) { - SEGENV.aux0 = random16(SEGLEN); // aux0 stores the random led index + SEGENV.aux0 = hw_random16(SEGLEN); // aux0 stores the random led index SEGENV.step = it; } @@ -696,13 +734,13 @@ static const char _data_FX_MODE_SPARKLE[] PROGMEM = "Sparkle@!,,,,,,Overlay;!,!; * Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/ */ uint16_t mode_flash_sparkle(void) { - if (!SEGMENT.check2) for (int i = 0; i < SEGLEN; i++) { + if (!SEGMENT.check2) for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } if (strip.now - SEGENV.aux0 > SEGENV.step) { - if(random8((255-SEGMENT.intensity) >> 4) == 0) { - SEGMENT.setPixelColor(random16(SEGLEN), SEGCOLOR(1)); //flash + if(hw_random8((255-SEGMENT.intensity) >> 4) == 0) { + SEGMENT.setPixelColor(hw_random16(SEGLEN), SEGCOLOR(1)); //flash } SEGENV.step = strip.now; SEGENV.aux0 = 255-SEGMENT.speed; @@ -717,14 +755,15 @@ static const char _data_FX_MODE_FLASH_SPARKLE[] PROGMEM = "Sparkle Dark@!,!,,,,, * Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/ */ uint16_t mode_hyper_sparkle(void) { - if (!SEGMENT.check2) for (int i = 0; i < SEGLEN; i++) { + if (!SEGMENT.check2) for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } if (strip.now - SEGENV.aux0 > SEGENV.step) { - if (random8((255-SEGMENT.intensity) >> 4) == 0) { - for (int i = 0; i < max(1, SEGLEN/3); i++) { - SEGMENT.setPixelColor(random16(SEGLEN), SEGCOLOR(1)); + if (hw_random8((255-SEGMENT.intensity) >> 4) == 0) { + int len = max(1, (int)SEGLEN/3); + for (int i = 0; i < len; i++) { + SEGMENT.setPixelColor(hw_random16(SEGLEN), SEGCOLOR(1)); } } SEGENV.step = strip.now; @@ -739,12 +778,12 @@ static const char _data_FX_MODE_HYPER_SPARKLE[] PROGMEM = "Sparkle+@!,!,,,,,Over * Strobe effect with different strobe count and pause, controlled by speed. */ uint16_t mode_multi_strobe(void) { - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); } 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)); @@ -770,7 +809,7 @@ static const char _data_FX_MODE_MULTI_STROBE[] PROGMEM = "Strobe Mega@!,!;!,!;!; */ uint16_t mode_android(void) { - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); } @@ -782,7 +821,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 +837,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 +861,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 +877,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 +887,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 +896,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 +950,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 +978,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 +1009,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; @@ -985,10 +1024,10 @@ static const char _data_FX_MODE_COLORFUL[] PROGMEM = "Colorful@!,Saturation;1,2, */ uint16_t mode_traffic_light(void) { if (SEGLEN == 1) return mode_static(); - 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)); uint32_t mdelay = 500; - for (int i = 0; i < SEGLEN-2 ; i+=3) + for (unsigned i = 0; i < SEGLEN-2 ; i+=3) { switch (SEGENV.aux0) { @@ -1018,17 +1057,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++) { + for (unsigned 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 +1087,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)); @@ -1094,16 +1133,16 @@ static const char _data_FX_MODE_RUNNING_COLOR[] PROGMEM = "Chase 2@!,Width;!,!;! uint16_t mode_running_random(void) { uint32_t cycleTime = 25 + (3 * (uint32_t)(255 - SEGMENT.speed)); uint32_t it = strip.now / cycleTime; - if (SEGENV.call == 0) SEGENV.aux0 = random16(); // random seed for PRNG on start + if (SEGENV.call == 0) SEGENV.aux0 = hw_random(); // random seed for PRNG on start - 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 +1164,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 +1227,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)); } } @@ -1211,8 +1255,8 @@ static const char _data_FX_MODE_COMET[] PROGMEM = "Lighthouse@!,Fade rate;!,!;!" */ uint16_t mode_fireworks() { if (SEGLEN == 1) return mode_static(); - const uint16_t width = SEGMENT.is2D() ? SEGMENT.virtualWidth() : SEGMENT.virtualLength(); - const uint16_t height = SEGMENT.virtualHeight(); + const uint16_t width = SEGMENT.is2D() ? SEG_W : SEGLEN; + const uint16_t height = SEG_H; if (SEGENV.call == 0) { SEGENV.aux0 = UINT16_MAX; @@ -1220,22 +1264,25 @@ 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) { - uint16_t index = random16(width*height); + if (hw_random8(129 - (SEGMENT.intensity >> 1)) == 0) { + uint16_t index = hw_random16(width*height); x = index % width; y = index / width; - uint32_t col = SEGMENT.color_from_palette(random8(), false, false, 0); + uint32_t col = SEGMENT.color_from_palette(hw_random8(), false, false, 0); if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(x, y, col); else SEGMENT.setPixelColor(index, col); SEGENV.aux1 = SEGENV.aux0; // old spark @@ -1250,12 +1297,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 = SEG_W; + const unsigned height = SEG_H; 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; ipp) ? p2:pp) - i); } else { val = min(abs(pp-i),min(abs(p1-i),abs(p2-i))); } val = (brd > val) ? (val * 255) / brd : 255; - SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(0), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1), val)); + SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(0), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1), uint8_t(val))); } return FRAMETIME; @@ -1356,42 +1403,27 @@ uint16_t mode_loading(void) { } static const char _data_FX_MODE_LOADING[] PROGMEM = "Loading@!,Fade;!,!;!;;ix=16"; - -//American Police Light with all LEDs Red and Blue -uint16_t police_base(uint32_t color1, uint32_t color2) { - if (SEGLEN == 1) return mode_static(); - uint16_t delay = 1 + (FRAMETIME<<3) / SEGLEN; // longer segments should change faster +/* + * Two dots running + */ +uint16_t mode_two_dots() { + if (SEGLEN == 1) return mode_static(); + unsigned delay = 1 + (FRAMETIME<<3) / SEGLEN; // longer segments should change faster uint32_t it = strip.now / map(SEGMENT.speed, 0, 255, delay<<4, delay); - uint16_t offset = it % SEGLEN; - - uint16_t width = ((SEGLEN*(SEGMENT.intensity+1))>>9); //max width is half the strip + unsigned offset = it % SEGLEN; + unsigned width = ((SEGLEN*(SEGMENT.intensity+1))>>9); //max width is half the strip if (!width) width = 1; - for (int i = 0; i < width; i++) { - uint16_t indexR = (offset + i) % SEGLEN; - uint16_t indexB = (offset + i + (SEGLEN>>1)) % SEGLEN; + if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(2)); + const uint32_t color1 = SEGCOLOR(0); + const uint32_t color2 = (SEGCOLOR(1) == SEGCOLOR(2)) ? color1 : SEGCOLOR(1); + for (unsigned i = 0; i < width; i++) { + unsigned indexR = (offset + i) % SEGLEN; + unsigned indexB = (offset + i + (SEGLEN>>1)) % SEGLEN; SEGMENT.setPixelColor(indexR, color1); SEGMENT.setPixelColor(indexB, color2); } return FRAMETIME; } - - -//Police Lights Red and Blue -//uint16_t mode_police() -//{ -// SEGMENT.fill(SEGCOLOR(1)); -// return police_base(RED, BLUE); -//} -//static const char _data_FX_MODE_POLICE[] PROGMEM = "Police@!,Width;,Bg;0"; - - -//Police Lights with custom colors -uint16_t mode_two_dots() -{ - if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(2)); - uint32_t color2 = (SEGCOLOR(1) == SEGCOLOR(2)) ? SEGCOLOR(0) : SEGCOLOR(1); - return police_base(SEGCOLOR(0), color2); -} static const char _data_FX_MODE_TWO_DOTS[] PROGMEM = "Two Dots@!,Dot size,,,,,Overlay;1,2,Bg;!"; @@ -1411,43 +1443,43 @@ typedef struct Flasher { uint16_t mode_fairy() { //set every pixel to a 'random' color from palette (using seed so it doesn't change between frames) uint16_t PRNG16 = 5100 + strip.getCurrSegmentId(); - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; //next 'random' number SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(PRNG16 >> 8, false, false, 0)); } //amount of flasher pixels depending on intensity (0: none, 255: every LED) if (SEGMENT.intensity == 0) return FRAMETIME; - uint8_t flasherDistance = ((255 - SEGMENT.intensity) / 28) +1; //1-10 - uint16_t numFlashers = (SEGLEN / flasherDistance) +1; + unsigned flasherDistance = ((255 - SEGMENT.intensity) / 28) +1; //1-10 + unsigned numFlashers = (SEGLEN / flasherDistance) +1; - uint16_t dataSize = sizeof(flasher) * numFlashers; + unsigned dataSize = sizeof(flasher) * numFlashers; if (!SEGENV.allocateData(dataSize)) return FRAMETIME; //allocation failed Flasher* flashers = reinterpret_cast(SEGENV.data); - uint16_t now16 = strip.now & 0xFFFF; + unsigned now16 = strip.now & 0xFFFF; //Up to 11 flashers in one brightness zone, afterwards a new zone for every 6 flashers - uint16_t zones = numFlashers/FLASHERS_PER_ZONE; + unsigned zones = numFlashers/FLASHERS_PER_ZONE; if (!zones) zones = 1; - uint8_t flashersInZone = numFlashers/zones; + unsigned flashersInZone = numFlashers/zones; uint8_t flasherBri[FLASHERS_PER_ZONE*2 -1]; - for (int z = 0; z < zones; z++) { - uint16_t flasherBriSum = 0; - uint16_t firstFlasher = z*flashersInZone; + for (unsigned z = 0; z < zones; z++) { + unsigned flasherBriSum = 0; + unsigned firstFlasher = z*flashersInZone; if (z == zones-1) flashersInZone = numFlashers-(flashersInZone*(zones-1)); - for (int f = firstFlasher; f < firstFlasher + flashersInZone; f++) { - uint16_t stateTime = now16 - flashers[f].stateStart; + for (unsigned f = firstFlasher; f < firstFlasher + flashersInZone; f++) { + unsigned stateTime = uint16_t(now16 - flashers[f].stateStart); //random on/off time reached, switch state if (stateTime > flashers[f].stateDur * 10) { flashers[f].stateOn = !flashers[f].stateOn; if (flashers[f].stateOn) { - flashers[f].stateDur = 12 + random8(12 + ((255 - SEGMENT.speed) >> 2)); //*10, 250ms to 1250ms + flashers[f].stateDur = 12 + hw_random8(12 + ((255 - SEGMENT.speed) >> 2)); //*10, 250ms to 1250ms } else { - flashers[f].stateDur = 20 + random8(6 + ((255 - SEGMENT.speed) >> 2)); //*10, 250ms to 1250ms + flashers[f].stateDur = 20 + hw_random8(6 + ((255 - SEGMENT.speed) >> 2)); //*10, 250ms to 1250ms } - //flashers[f].stateDur = 51 + random8(2 + ((255 - SEGMENT.speed) >> 1)); + //flashers[f].stateDur = 51 + hw_random8(2 + ((255 - SEGMENT.speed) >> 1)); flashers[f].stateStart = now16; if (stateTime < 255) { flashers[f].stateStart -= 255 -stateTime; //start early to get correct bri @@ -1463,15 +1495,15 @@ uint16_t mode_fairy() { flasherBriSum += flasherBri[f - firstFlasher]; } //dim factor, to create "shimmer" as other pixels get less voltage if a lot of flashers are on - uint8_t avgFlasherBri = flasherBriSum / flashersInZone; - uint8_t globalPeakBri = 255 - ((avgFlasherBri * MAX_SHIMMER) >> 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++) { + for (unsigned f = firstFlasher; f < firstFlasher + flashersInZone; f++) { uint8_t 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,40 +1519,40 @@ 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++) { + for (unsigned f = 0; f < SEGLEN; f++) { uint16_t stateTime = now16 - flashers[f].stateStart; //random on/off time reached, switch state if (stateTime > flashers[f].stateDur * 100) { flashers[f].stateOn = !flashers[f].stateOn; bool init = !flashers[f].stateDur; if (flashers[f].stateOn) { - flashers[f].stateDur = riseFallTime/100 + ((255 - SEGMENT.intensity) >> 2) + random8(12 + ((255 - SEGMENT.intensity) >> 1)) +1; + flashers[f].stateDur = riseFallTime/100 + ((255 - SEGMENT.intensity) >> 2) + hw_random8(12 + ((255 - SEGMENT.intensity) >> 1)) +1; } else { - flashers[f].stateDur = riseFallTime/100 + random8(3 + ((255 - SEGMENT.speed) >> 6)) +1; + flashers[f].stateDur = riseFallTime/100 + hw_random8(3 + ((255 - SEGMENT.speed) >> 6)) +1; } flashers[f].stateStart = now16; stateTime = 0; if (init) { flashers[f].stateStart -= riseFallTime; //start lit - flashers[f].stateDur = riseFallTime/100 + random8(12 + ((255 - SEGMENT.intensity) >> 1)) +5; //fire up a little quicker + flashers[f].stateDur = riseFallTime/100 + hw_random8(12 + ((255 - SEGMENT.intensity) >> 1)) +5; //fire up a little quicker stateTime = riseFallTime; } } if (flashers[f].stateOn && flashers[f].stateDur > maxDur) flashers[f].stateDur = maxDur; //react more quickly on intensity change if (stateTime > riseFallTime) stateTime = riseFallTime; //for flasher brightness calculation, fades in first 255 ms of state - uint8_t fadeprog = 255 - ((stateTime * 255) / riseFallTime); + unsigned 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 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,10 +1570,10 @@ 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++) { + for (unsigned i = 0; i < SEGLEN; i++, index++) { if (index > (width*3)-1) index = 0; uint32_t color = color1; @@ -1567,8 +1599,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)); @@ -1579,13 +1611,13 @@ uint16_t mode_icu(void) { SEGMENT.setPixelColor(dest + SEGLEN/space, col); if(SEGENV.aux0 == dest) { // pause between eye movements - if(random8(6) == 0) { // blink once in a while + if(hw_random8(6) == 0) { // blink once in a while SEGMENT.setPixelColor(dest, SEGCOLOR(1)); SEGMENT.setPixelColor(dest + SEGLEN/space, SEGCOLOR(1)); return 200; } - SEGENV.aux0 = random16(SEGLEN-SEGLEN/space); - return 1000 + random16(2000); + SEGENV.aux0 = hw_random16(SEGLEN-SEGLEN/space); + return 1000 + hw_random16(2000); } if(SEGENV.aux0 > SEGENV.step) { @@ -1610,30 +1642,30 @@ 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++) + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 2)); } 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 +1682,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 +1703,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 +1724,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)); @@ -1714,7 +1747,7 @@ uint16_t mode_multi_comet(void) { } comets[i]++; } else { - if(!random16(SEGLEN)) { + if(!hw_random16(SEGLEN)) { comets[i] = 0; } } @@ -1723,8 +1756,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 +1768,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 +1796,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 +1815,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); @@ -1798,20 +1831,20 @@ uint16_t mode_oscillate(void) { oscillators[i].pos = 0; oscillators[i].dir = 1; // make bigger steps for faster speeds - oscillators[i].speed = SEGMENT.speed > 100 ? random8(2, 4):random8(1, 3); + oscillators[i].speed = SEGMENT.speed > 100 ? hw_random8(2, 4):hw_random8(1, 3); } if((oscillators[i].dir == 1) && (oscillators[i].pos >= (SEGLEN - 1))) { oscillators[i].pos = SEGLEN - 1; oscillators[i].dir = -1; - oscillators[i].speed = SEGMENT.speed > 100 ? random8(2, 4):random8(1, 3); + oscillators[i].speed = SEGMENT.speed > 100 ? hw_random8(2, 4):hw_random8(1, 3); } } - for (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) { - color = (color == BLACK) ? SEGCOLOR(j) : color_blend(color, SEGCOLOR(j), 128); + 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), uint8_t(128)); } } SEGMENT.setPixelColor(i, color); @@ -1826,13 +1859,13 @@ 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) - uint8_t bri = 255/random8(1, 3); + unsigned ledstart = hw_random16(SEGLEN); // Determine starting location of flash + unsigned ledlen = 1 + hw_random16(SEGLEN -ledstart); // Determine length of flash (not to go beyond NUM_LEDS-1) + uint8_t bri = 255/hw_random8(1, 3); if (SEGENV.aux1 == 0) //init, leader flash { - SEGENV.aux1 = random8(4, 4 + SEGMENT.intensity/20); //number of flashes + SEGENV.aux1 = hw_random8(4, 4 + SEGMENT.intensity/20); //number of flashes SEGENV.aux1 *= 2; bri = 52; //leader has lower brightness @@ -1842,22 +1875,22 @@ 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)); } SEGENV.aux1--; SEGENV.step = strip.now; - //return random8(4, 10); // each flash only lasts one frame/every 24ms... originally 4-10 milliseconds + //return hw_random8(4, 10); // each flash only lasts one frame/every 24ms... originally 4-10 milliseconds } else { if (strip.now - SEGENV.step > SEGENV.aux0) { SEGENV.aux1--; if (SEGENV.aux1 < 2) SEGENV.aux1 = 0; - SEGENV.aux0 = (50 + random8(100)); //delay between flashes + SEGENV.aux0 = (50 + hw_random8(100)); //delay between flashes if (SEGENV.aux1 == 2) { - SEGENV.aux0 = (random8(255 - SEGMENT.speed) * 100); // delay between strikes + SEGENV.aux0 = (hw_random8(255 - SEGMENT.speed) * 100); // delay between strikes } SEGENV.step = strip.now; } @@ -1871,30 +1904,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); + uint8_t sat8 = beatsin88_t( 87, 220, 250); + uint8_t brightdepth = beatsin88_t( 341, 96, 224); + unsigned brightnessthetainc16 = beatsin88_t( 203, (25 * 256), (40 * 256)); + unsigned msmultiplier = beatsin88_t(147, 23, 60); - uint16_t hue16 = sHue16;//gHue * 256; - uint16_t hueinc16 = beatsin88(113, 1, 3000); + unsigned hue16 = sHue16;//gHue * 256; + unsigned hueinc16 = beatsin88_t(113, 1, 3000); sPseudotime += duration * msmultiplier; - sHue16 += duration * beatsin88( 400, 5,9); - uint16_t brightnesstheta16 = sPseudotime; + sHue16 += duration * beatsin88_t( 400, 5,9); + 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_t( 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,9 +1950,9 @@ 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_t((16 + SEGMENT.speed)*(i + 7), 0, SEGLEN -1); fastled_col = CRGB(SEGMENT.getPixelColor(index)); - fastled_col |= (SEGMENT.palette==0)?CHSV(dothue, 220, 255):ColorFromPalette(SEGPALETTE, dothue, 255); + fastled_col |= (SEGMENT.palette==0)?CHSV(dothue, 220, 255):CRGB(ColorFromPalette(SEGPALETTE, dothue, 255)); SEGMENT.setPixelColor(index, fastled_col); dothue += 32; } @@ -1933,13 +1966,13 @@ 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; constexpr mathType animatedRotationScale = 1; - constexpr int16_t (*sinFunction)(uint16_t) = &sin16; - constexpr int16_t (*cosFunction)(uint16_t) = &cos16; + constexpr int16_t (*sinFunction)(uint16_t) = &sin16_t; + constexpr int16_t (*cosFunction)(uint16_t) = &cos16_t; #else using mathType = float; using wideMathType = float; @@ -1952,8 +1985,8 @@ uint16_t mode_palette() { constexpr float (*cosFunction)(float) = &cos_t; #endif const bool isMatrix = strip.isMatrix; - const int cols = SEGMENT.virtualWidth(); - const int rows = isMatrix ? SEGMENT.virtualHeight() : strip.getActiveSegmentsNum(); + const int cols = SEG_W; + const int rows = isMatrix ? SEG_H : strip.getActiveSegmentsNum(); const int inputShift = SEGMENT.speed; const int inputSize = SEGMENT.intensity; @@ -1962,7 +1995,7 @@ uint16_t mode_palette() { const bool inputAnimateRotation = SEGMENT.check2; const bool inputAssumeSquare = SEGMENT.check3; - const angleType theta = (!inputAnimateRotation) ? (inputRotation * maxAngle / staticRotationScale) : (((strip.now * ((inputRotation >> 4) +1)) & 0xFFFF) * animatedRotationScale); + const angleType theta = (!inputAnimateRotation) ? ((inputRotation + 128) * maxAngle / staticRotationScale) : (((strip.now * ((inputRotation >> 4) +1)) & 0xFFFF) * animatedRotationScale); const mathType sinTheta = sinFunction(theta); const mathType cosTheta = cosFunction(theta); @@ -1981,7 +2014,7 @@ uint16_t mode_palette() { // So the rectangle needs to have exactly the right size. That size depends on the rotation. // This scale computation here only considers one dimension. You can think of it like the rectangle is always scaled so that // the left and right most points always match the left and right side of the display. - const mathType scale = std::abs(sinTheta) + (std::abs(cosTheta) * maxYOut / maxXOut); + const mathType scale = std::abs(sinTheta) + (std::abs(cosTheta) * maxYOut / maxXOut); // 2D simulation: // If we are dealing with a 1D setup, we assume that each segment represents one line on a 2-dimensional display. // The function is called once per segments, so we need to handle one line at a time. @@ -1999,7 +2032,7 @@ uint16_t mode_palette() { const mathType sourceX = xtSinTheta + ytCosTheta + centerX; // The computation was scaled just right so that the result should always be in range [0, maxXOut], but enforce this anyway // to account for imprecision. Then scale it so that the range is [0, 255], which we can use with the palette. - int colorIndex = (std::min(std::max(sourceX, mathType(0)), maxXOut * sInt16Scale) * 255) / (sInt16Scale * maxXOut); + int colorIndex = (std::min(std::max(sourceX, mathType(0)), maxXOut * sInt16Scale) * wideMathType(255)) / (sInt16Scale * maxXOut); // inputSize determines by how much we want to scale the palette: // values < 128 display a fraction of a palette, // values > 128 display multiple palettes. @@ -2012,8 +2045,8 @@ uint16_t mode_palette() { colorIndex = ((inputSize - 112) * colorIndex) / 16; } // Finally, shift the palette a bit. - const int paletteOffset = (!inputAnimateShift) ? (inputShift-128) : (((strip.now * ((inputShift >> 3) +1)) & 0xFFFF) >> 8); - colorIndex += paletteOffset; + const int paletteOffset = (!inputAnimateShift) ? (inputShift) : (((strip.now * ((inputShift >> 3) +1)) & 0xFFFF) >> 8); + colorIndex -= paletteOffset; const uint32_t color = SEGMENT.color_wheel((uint8_t)colorIndex); if (isMatrix) { SEGMENT.setPixelColorXY(x, y, color); @@ -2024,7 +2057,7 @@ uint16_t mode_palette() { } return FRAMETIME; } -static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Shift,Size,Rotation,,,Animate Shift,Animate Rotation,Anamorphic;;!;12;c1=128,c2=128,c3=128,o1=1,o2=1,o3=0"; +static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Shift,Size,Rotation,,,Animate Shift,Animate Rotation,Anamorphic;;!;12;ix=112,c1=0,o1=1,o2=0,o3=1"; // WLED limitation: Analog Clock overlay will NOT work when Fire2012 is active @@ -2057,7 +2090,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; @@ -2066,11 +2099,11 @@ uint16_t mode_fire_2012() { struct virtualStrip { static void runStrip(uint16_t stripNr, byte* heat, uint32_t it) { - const uint8_t ignition = max(3,SEGLEN/10); // ignition area: 10% of segment length or minimum 3 pixels + const uint8_t ignition = MAX(3,SEGLEN/10); // ignition area: 10% of segment length or minimum 3 pixels // Step 1. Cool down every cell a little - for (int i = 0; i < SEGLEN; i++) { - uint8_t cool = (it != SEGENV.step) ? random8((((20 + SEGMENT.speed/3) * 16) / SEGLEN)+2) : random8(4); + for (unsigned i = 0; i < SEGLEN; i++) { + uint8_t cool = (it != SEGENV.step) ? hw_random8((((20 + SEGMENT.speed/3) * 16) / SEGLEN)+2) : hw_random8(4); uint8_t minTemp = (i> 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;pal=35,sx=64,ix=160,m12=1,c2=128"; // bars // ColorWavesWithPalettes by Mark Kriegsman: https://gist.github.com/kriegsman/8281905786e8b2632aeb // This function draws color waves with an ever-changing, // widely-varying set of parameters, using a color palette. uint16_t mode_colorwaves() { - 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_t(341, 96, 224); + unsigned brightnessthetainc16 = beatsin88_t( 203, (25 * 256), (40 * 256)); + unsigned msmultiplier = beatsin88_t(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_t(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; + sHue16 += duration * beatsin88_t(400, 5, 9); + unsigned brightnesstheta16 = sPseudotime; - for (int i = 0 ; i < SEGLEN; i++) { + for (unsigned 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 +2178,9 @@ uint16_t mode_colorwaves() { } brightnesstheta16 += brightnessthetainc16; - uint16_t b16 = sin16(brightnesstheta16) + 32768; + unsigned b16 = sin16_t(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); @@ -2153,14 +2191,14 @@ uint16_t mode_colorwaves() { return FRAMETIME; } -static const char _data_FX_MODE_COLORWAVES[] PROGMEM = "Colorwaves@!,Hue;!;!"; +static const char _data_FX_MODE_COLORWAVES[] PROGMEM = "Colorwaves@!,Hue;!;!;;pal=26"; // colored stripes pulsing at a defined Beats-Per-Minute (BPM) uint16_t mode_bpm() { uint32_t stp = (strip.now / 20) & 0xFF; - uint8_t beat = beatsin8(SEGMENT.speed, 64, 255); - for (int i = 0; i < SEGLEN; i++) { + uint8_t beat = beatsin8_t(SEGMENT.speed, 64, 255); + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(stp + (i * 2), false, PALETTE_SOLID_WRAP, 0, beat - stp + (i * 10))); } @@ -2170,15 +2208,12 @@ 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); + if (SEGENV.call == 0) SEGENV.step = hw_random(); + for (unsigned i = 0; i < SEGLEN; i++) { + 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 + SEGENV.step += beatsin8_t(SEGMENT.speed, 1, 6); //10,1,4 return FRAMETIME; } @@ -2186,134 +2221,121 @@ 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 + for (unsigned i = 0; i < SEGLEN; i++) { + unsigned shift_x = beatsin8_t(11); // the x position of the noise field swings @ 17 bpm + unsigned shift_y = SEGENV.step/42; // the y position becomes slowly incremented + unsigned real_x = (i + shift_x) * scale; // the x position of the noise field swings @ 17 bpm + unsigned real_y = (i + shift_y) * scale; // the y position becomes slowly incremented uint32_t real_z = SEGENV.step; // the z position becomes quickly incremented - 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_t(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)); } return FRAMETIME; } -static const char _data_FX_MODE_NOISE16_1[] PROGMEM = "Noise 1@!;!;!"; +static const char _data_FX_MODE_NOISE16_1[] PROGMEM = "Noise 1@!;!;!;;pal=20"; uint16_t mode_noise16_2() { - 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 + for (unsigned i = 0; i < SEGLEN; i++) { + unsigned shift_x = SEGENV.step >> 6; // x as a function of time uint32_t real_x = (i + shift_x) * scale; // calculate the coordinates within the noise field - 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_t(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)); } return FRAMETIME; } -static const char _data_FX_MODE_NOISE16_2[] PROGMEM = "Noise 2@!;!;!"; +static const char _data_FX_MODE_NOISE16_2[] PROGMEM = "Noise 2@!;!;!;;pal=43"; uint16_t mode_noise16_3() { - 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; + for (unsigned i = 0; i < SEGLEN; i++) { + unsigned shift_x = 4223; // no movement along x and y + unsigned shift_y = 1234; uint32_t real_x = (i + shift_x) * scale; // calculate the coordinates within the noise field uint32_t real_y = (i + shift_y) * scale; // based on the precalculated positions uint32_t real_z = SEGENV.step*8; - 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_t(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)); } return FRAMETIME; } -static const char _data_FX_MODE_NOISE16_3[] PROGMEM = "Noise 3@!;!;!"; +static const char _data_FX_MODE_NOISE16_3[] PROGMEM = "Noise 3@!;!;!;;pal=35"; //https://github.com/aykevl/ledstrip-spark/blob/master/ledstrip.ino uint16_t mode_noise16_4() { - //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); + for (unsigned i = 0; i < SEGLEN; i++) { + int index = inoise16(uint32_t(i) << 12, stp); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } return FRAMETIME; } -static const char _data_FX_MODE_NOISE16_4[] PROGMEM = "Noise 4@!;!;!"; +static const char _data_FX_MODE_NOISE16_4[] PROGMEM = "Noise 4@!;!;!;;pal=26"; //based on https://gist.github.com/kriegsman/5408ecd397744ba0393e uint16_t mode_colortwinkle() { - 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; + CRGBW col, prev; fract8 fadeUpAmount = strip.getBrightness()>28 ? 8 + (SEGMENT.speed>>2) : 68-strip.getBrightness(); fract8 fadeDownAmount = strip.getBrightness()>28 ? 8 + (SEGMENT.speed>>3) : 68-strip.getBrightness(); - for (int i = 0; i < SEGLEN; i++) { - fastled_col = SEGMENT.getPixelColor(i); - prev = fastled_col; - uint16_t index = i >> 3; - uint8_t bitNum = i & 0x07; + for (unsigned i = 0; i < SEGLEN; i++) { + CRGBW cur = SEGMENT.getPixelColor(i); + prev = cur; + unsigned index = i >> 3; + unsigned bitNum = i & 0x07; bool fadeUp = bitRead(SEGENV.data[index], bitNum); if (fadeUp) { - CRGB incrementalColor = fastled_col; - incrementalColor.nscale8_video(fadeUpAmount); - fastled_col += incrementalColor; + CRGBW incrementalColor = color_fade(cur, fadeUpAmount, true); + col = color_add(cur, incrementalColor); - if (fastled_col.red == 255 || fastled_col.green == 255 || fastled_col.blue == 255) { + if (col.r == 255 || col.g == 255 || col.b == 255) { bitWrite(SEGENV.data[index], bitNum, false); } - SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); - if (SEGMENT.getPixelColor(i) == RGBW32(prev.r, prev.g, prev.b, 0)) { //fix "stuck" pixels - fastled_col += fastled_col; - SEGMENT.setPixelColor(i, fastled_col); + if (cur == prev) { //fix "stuck" pixels + color_add(col, col); + SEGMENT.setPixelColor(i, col); } - } else { - fastled_col.nscale8(255 - fadeDownAmount); - SEGMENT.setPixelColor(i, fastled_col); + else SEGMENT.setPixelColor(i, col); + } + else { + col = color_fade(cur, 255 - fadeDownAmount); + SEGMENT.setPixelColor(i, col); } } for (unsigned j = 0; j <= SEGLEN / 50; j++) { - if (random8() <= SEGMENT.intensity) { + if (hw_random8() <= SEGMENT.intensity) { for (unsigned times = 0; times < 5; times++) { //attempt to spawn a new pixel 5 times - int i = random16(SEGLEN); + int i = hw_random16(SEGLEN); if (SEGMENT.getPixelColor(i) == 0) { - fastled_col = ColorFromPalette(SEGPALETTE, random8(), 64, NOBLEND); - 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); + SEGMENT.setPixelColor(i, ColorFromPalette(SEGPALETTE, hw_random8(), 64, NOBLEND)); break; //only spawn 1 new pixel per frame per 50 LEDs } } @@ -2326,18 +2348,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; - 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; + unsigned sp = SEGMENT.speed/10; + int wave1 = beatsin8_t(sp +2, -64,64); + int wave2 = beatsin8_t(sp +1, -64,64); + int wave3 = beatsin8_t(sp +2, 0,80); - for (int i = 0; i < SEGLEN; i++) + for (unsigned i = 0; i < SEGLEN; i++) { - int index = cos8((i*15)+ wave1)/2 + cubicwave8((i*23)+ wave2)/2; + int index = cos8_t((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)); } @@ -2346,90 +2365,79 @@ uint16_t mode_lake() { static const char _data_FX_MODE_LAKE[] PROGMEM = "Lake@!;Fx;!"; -// meteor effect +// meteor effect & meteor smooth (merged by @dedehai) // send a meteor from begining to to the end of the strip with a trail that randomly decays. // adapted from https://www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/#LEDStripEffectMeteorRain uint16_t mode_meteor() { if (SEGLEN == 1) return mode_static(); if (!SEGENV.allocateData(SEGLEN)) return mode_static(); //allocation failed - + const bool meteorSmooth = SEGMENT.check3; byte* trail = SEGENV.data; - const unsigned meteorSize= 1 + SEGLEN / 20; // 5% - uint16_t counter = strip.now * ((SEGMENT.speed >> 2) +8); - uint16_t in = counter * SEGLEN >> 16; + const unsigned meteorSize = 1 + SEGLEN / 20; // 5% + uint16_t meteorstart; + if(meteorSmooth) meteorstart = map((SEGENV.step >> 6 & 0xFF), 0, 255, 0, SEGLEN -1); + else { + unsigned counter = strip.now * ((SEGMENT.speed >> 2) + 8); + meteorstart = (counter * SEGLEN) >> 16; + } const int max = SEGMENT.palette==5 || !SEGMENT.check1 ? 240 : 255; // 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); - 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); + for (unsigned i = 0; i < SEGLEN; i++) { + uint32_t col; + if (hw_random8() <= 255 - SEGMENT.intensity) { + if(meteorSmooth) { + int change = trail[i] + 4 - hw_random8(24); //change each time between -20 and +4 + trail[i] = constrain(change, 0, max); + col = SEGMENT.check1 ? SEGMENT.color_from_palette(i, true, false, 0, trail[i]) : SEGMENT.color_from_palette(trail[i], false, true, 255); + } + else { + trail[i] = scale8(trail[i], 128 + hw_random8(127)); + int index = trail[i]; + int idx = 255; + int bri = SEGMENT.palette==35 || SEGMENT.palette==36 ? 255 : trail[i]; + if (!SEGMENT.check1) { + idx = 0; + index = map(i,0,SEGLEN,0,max); + bri = trail[i]; + } + col = SEGMENT.color_from_palette(index, false, false, idx, bri); // full brightness for Fire + } SEGMENT.setPixelColor(i, col); } } // draw meteor for (unsigned j = 0; j < meteorSize; j++) { - uint16_t index = in + j; - if (index >= SEGLEN) { - index -= SEGLEN; + unsigned index = (meteorstart + j) % SEGLEN; + if(meteorSmooth) { + trail[index] = max; + uint32_t col = SEGMENT.check1 ? SEGMENT.color_from_palette(index, true, false, 0, trail[index]) : SEGMENT.color_from_palette(trail[index], false, true, 255); + SEGMENT.setPixelColor(index, col); } - trail[index] = max; - uint32_t col = SEGMENT.check1 ? SEGMENT.color_from_palette(index, true, false, 0, trail[index]) : SEGMENT.color_from_palette(trail[index], false, true, 255); - SEGMENT.setPixelColor(index, col); - } - - return FRAMETIME; -} -static const char _data_FX_MODE_METEOR[] PROGMEM = "Meteor@!,Trail,,,,Gradient;;!;1"; - - -// smooth meteor effect -// send a meteor from begining to to the end of the strip with a trail that randomly decays. -// adapted from https://www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/#LEDStripEffectMeteorRain -uint16_t mode_meteor_smooth() { - if (SEGLEN == 1) return mode_static(); - if (!SEGENV.allocateData(SEGLEN)) return mode_static(); //allocation failed - - byte* trail = SEGENV.data; - - const unsigned meteorSize= 1+ SEGLEN / 20; // 5% - uint16_t in = map((SEGENV.step >> 6 & 0xFF), 0, 255, 0, SEGLEN -1); - - const int max = SEGMENT.palette==5 || !SEGMENT.check1 ? 240 : 255; - // fade all leds to colors[1] in LEDs one step - for (int i = 0; i < SEGLEN; i++) { - if (/*trail[i] != 0 &&*/ random8() <= 255 - SEGMENT.intensity) { - int change = trail[i] + 4 - random8(24); //change each time between -20 and +4 - trail[i] = constrain(change, 0, max); - uint32_t col = SEGMENT.check1 ? SEGMENT.color_from_palette(i, true, false, 0, trail[i]) : SEGMENT.color_from_palette(trail[i], false, true, 255); - SEGMENT.setPixelColor(i, col); + else{ + int idx = 255; + int i = trail[index] = max; + if (!SEGMENT.check1) { + i = map(index,0,SEGLEN,0,max); + idx = 0; + } + uint32_t col = SEGMENT.color_from_palette(i, false, false, idx, 255); // full brightness + SEGMENT.setPixelColor(index, col); } } - // draw meteor - for (unsigned j = 0; j < meteorSize; j++) { - uint16_t index = in + j; - if (index >= SEGLEN) { - index -= SEGLEN; - } - trail[index] = max; - uint32_t col = SEGMENT.check1 ? SEGMENT.color_from_palette(index, true, false, 0, trail[index]) : SEGMENT.color_from_palette(trail[index], false, true, 255); - SEGMENT.setPixelColor(index, col); - } - SEGENV.step += SEGMENT.speed +1; return FRAMETIME; } -static const char _data_FX_MODE_METEOR_SMOOTH[] PROGMEM = "Meteor Smooth@!,Trail,,,,Gradient;;!;1"; +static const char _data_FX_MODE_METEOR[] PROGMEM = "Meteor@!,Trail,,,,Gradient,,Smooth;;!;1"; //Railway Crossing / Christmas Fairy lights uint16_t mode_railway() { if (SEGLEN == 1) return mode_static(); - 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,14 +2445,14 @@ 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; - for (int i = 0; i < SEGLEN; i += 2) + for (unsigned i = 0; i < SEGLEN; i += 2) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(255 - pos, false, false, 255)); // do not use color 1 or 2, always use palette if (i < SEGLEN -1) @@ -2455,7 +2463,7 @@ uint16_t mode_railway() { SEGENV.step += FRAMETIME; return FRAMETIME; } -static const char _data_FX_MODE_RAILWAY[] PROGMEM = "Railway@!,Smoothness;1,2;!"; +static const char _data_FX_MODE_RAILWAY[] PROGMEM = "Railway@!,Smoothness;1,2;!;;pal=3"; //Water ripple @@ -2474,56 +2482,55 @@ 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(uint8_t blurAmount = 0) { + unsigned maxRipples = min(1 + (int)(SEGLEN >> 2), MAX_RIPPLES); // 56 max for 16 segment ESP8266 + unsigned dataSize = sizeof(ripple) * maxRipples; if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed 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_t((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 +2; + for (int v = 0; v < 4; v++) { + uint8_t mag = scale8(cubicwave8((propF>>2) + v * 64), amp); + SEGMENT.setPixelColor(left + v, color_blend(SEGMENT.getPixelColor(left + v), col, mag)); // TODO + SEGMENT.setPixelColor(right - v, color_blend(SEGMENT.getPixelColor(right - v), col, mag)); // TODO } } ripplestate += rippledecay; ripples[i].state = (ripplestate > 254) ? 0 : ripplestate; } else {//randomly create new wave - if (random16(IBN + 10000) <= (SEGMENT.intensity >> (SEGMENT.is2D()*3))) { + if (hw_random16(IBN + 10000) <= (SEGMENT.intensity >> (SEGMENT.is2D()*3))) { ripples[i].state = 1; - ripples[i].pos = SEGMENT.is2D() ? ((random8(SEGENV.virtualWidth())<<8) | (random8(SEGENV.virtualHeight()))) : random16(SEGLEN); - ripples[i].color = random8(); //color + ripples[i].pos = SEGMENT.is2D() ? ((hw_random8(SEG_W)<<8) | (hw_random8(SEG_H))) : hw_random16(SEGLEN); + ripples[i].color = hw_random8(); //color } } } - + SEGMENT.blur(blurAmount); return FRAMETIME; } #undef MAX_RIPPLES @@ -2531,27 +2538,30 @@ uint16_t ripple_base() uint16_t mode_ripple(void) { if (SEGLEN == 1) return mode_static(); - if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); - else SEGMENT.fade_out(250); - return ripple_base(); + if(SEGMENT.custom1 || SEGMENT.check2) // blur or overlay + SEGMENT.fade_out(250); + else + SEGMENT.fill(SEGCOLOR(1)); + + return ripple_base(SEGMENT.custom1>>1); } -static const char _data_FX_MODE_RIPPLE[] PROGMEM = "Ripple@!,Wave #,,,,,Overlay;,!;!;12"; +static const char _data_FX_MODE_RIPPLE[] PROGMEM = "Ripple@!,Wave #,Blur,,,,Overlay;,!;!;12;c1=0"; uint16_t mode_ripple_rainbow(void) { if (SEGLEN == 1) return mode_static(); if (SEGENV.call ==0) { - SEGENV.aux0 = random8(); - SEGENV.aux1 = random8(); + SEGENV.aux0 = hw_random8(); + SEGENV.aux1 = hw_random8(); } if (SEGENV.aux0 == SEGENV.aux1) { - SEGENV.aux1 = random8(); + SEGENV.aux1 = hw_random8(); } else if (SEGENV.aux1 > SEGENV.aux0) { SEGENV.aux0++; } else { SEGENV.aux0--; } - SEGMENT.fill(color_blend(SEGMENT.color_wheel(SEGENV.aux0),BLACK,235)); + SEGMENT.fill(color_blend(SEGMENT.color_wheel(SEGENV.aux0),BLACK,uint8_t(235))); return ripple_base(); } static const char _data_FX_MODE_RIPPLE_RAINBOW[] PROGMEM = "Ripple Rainbow@!,Wave #;;!;12"; @@ -2561,24 +2571,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; + unsigned ticks = ms / SEGENV.aux0; + unsigned fastcycle8 = uint8_t(ticks); uint16_t slowcycle16 = (ticks >> 8) + salt; - slowcycle16 += sin8(slowcycle16); + slowcycle16 += sin8_t(slowcycle16); slowcycle16 = (slowcycle16 * 2053) + 1384; uint8_t 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 +2605,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 +2615,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 +2631,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 +2645,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,37 +2654,37 @@ 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++) { + for (unsigned 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. - SEGMENT.setPixelColor(i, c.red, c.green, c.blue); + SEGMENT.setPixelColor(i, c); } else if (deltabright > 0) { // If the new pixel is just slightly brighter than the background color, // mix a blend of the new color and the background color - SEGMENT.setPixelColor(i, color_blend(RGBW32(bg.r,bg.g,bg.b,0), RGBW32(c.r,c.g,c.b,0), deltabright * 8)); + SEGMENT.setPixelColor(i, color_blend(RGBW32(bg.r,bg.g,bg.b,0), RGBW32(c.r,c.g,c.b,0), uint8_t(deltabright * 8))); } else { // if the new pixel is not at all brighter than the background color, // just use the background color. - SEGMENT.setPixelColor(i, bg.r, bg.g, bg.b); + SEGMENT.setPixelColor(i, bg); } } return FRAMETIME; @@ -2718,10 +2728,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 ? SEG_W : SEGLEN; + const unsigned HALLOWEEN_EYE_SPACE = MAX(2, strip.isMatrix ? SEG_W>>4: SEGLEN>>5); + const unsigned HALLOWEEN_EYE_WIDTH = HALLOWEEN_EYE_SPACE/2; + unsigned eyeLength = (2*HALLOWEEN_EYE_WIDTH) + HALLOWEEN_EYE_SPACE; if (eyeLength >= maxWidth) return mode_static(); //bail if segment too short if (!SEGENV.allocateData(sizeof(EyeData))) return mode_static(); //allocation failed @@ -2730,7 +2740,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) { @@ -2740,10 +2750,10 @@ uint16_t mode_halloween_eyes() // - select a duration // - immediately switch to eyes on state. - data.startPos = random16(0, maxWidth - eyeLength - 1); - data.color = random8(); - if (strip.isMatrix) SEGMENT.offset = random16(SEGMENT.virtualHeight()-1); // a hack: reuse offset since it is not used in matrices - duration = 128u + random16(SEGMENT.intensity*64u); + data.startPos = hw_random16(0, maxWidth - eyeLength - 1); + data.color = hw_random8(); + if (strip.isMatrix) SEGMENT.offset = hw_random16(SEG_H-1); // a hack: reuse offset since it is not used in matrices + duration = 128u + hw_random16(SEGMENT.intensity*64u); data.duration = duration; data.state = eyeState::on; [[fallthrough]]; @@ -2755,9 +2765,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; @@ -2766,25 +2776,25 @@ uint16_t mode_halloween_eyes() const uint32_t eyeColor = SEGMENT.color_from_palette(data.color, false, false, 0); uint32_t c = eyeColor; if (fadeInAnimationState < 256u) { - c = color_blend(backgroundColor, eyeColor, fadeInAnimationState); + c = color_blend(backgroundColor, eyeColor, uint8_t(fadeInAnimationState)); } else if (elapsedTime > minimumOnTimeBegin) { const uint32_t remainingTime = (elapsedTime >= duration) ? 0u : (duration - elapsedTime); if (remainingTime > minimumOnTimeEnd) { - if (random8() < 4u) + if (hw_random8() < 4u) { c = backgroundColor; data.state = eyeState::blink; - data.blinkEndTime = strip.now + random8(8, 128); + data.blinkEndTime = strip.now + hw_random8(8, 128); } } } 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,8 +2817,8 @@ uint16_t mode_halloween_eyes() // - select a duration // - immediately switch to eyes-off state - const uint16_t eyeOffTimeBase = SEGMENT.speed*128u; - duration = eyeOffTimeBase + random16(eyeOffTimeBase); + const unsigned eyeOffTimeBase = SEGMENT.speed*128u; + duration = eyeOffTimeBase + hw_random16(eyeOffTimeBase); data.duration = duration; data.state = eyeState::off; [[fallthrough]]; @@ -2818,8 +2828,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,12 +2865,12 @@ 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++) { + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, (drawingLit) ? SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0) : SEGCOLOR(1)); cnt++; if (cnt >= ((drawingLit) ? lit : unlit)) { @@ -2876,17 +2886,17 @@ 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++) { + for (unsigned i = 0; i < SEGLEN; i++) { if ( currSeg % 3 == 0 ) { SEGMENT.setPixelColor(i, SEGCOLOR(0)); } 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,26 +2910,26 @@ 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); - SEGMENT.setPixelColor(index, color_blend(SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0), SEGCOLOR(1), 255-s)); + 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), uint8_t(255-s))); } } } @@ -2939,9 +2949,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 +2970,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 +2986,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; @@ -2998,7 +3008,7 @@ uint16_t mode_bouncing_balls(void) { balls[i].lastBounceTime = time; if (balls[i].impactVelocity < 0.015f) { - float impactVelocityStart = sqrtf(-2.0f * gravity) * random8(5,11)/10.0f; // randomize impact velocity + float impactVelocityStart = sqrtf(-2.0f * gravity) * hw_random8(5,11)/10.0f; // randomize impact velocity balls[i].impactVelocity = impactVelocityStart; } } else if (balls[i].height > 1.0f) { @@ -3013,13 +3023,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 - balls[i].height = (float(random16(0, 10000)) / 10000.0f); // from 0. to 1. - balls[i].mass = (float(random16(1000, 10000)) / 10000.0f); // from .1 to 1. + balls[i].velocity = 20.0f * float(hw_random16(1000, 10000))/10000.0f; // number from 1 to 10 + if (hw_random8()<128) balls[i].velocity = -balls[i].velocity; // 50% chance of reverse direction + balls[i].height = (float(hw_random16(0, 10000)) / 10000.0f); // from 0. to 1. + balls[i].mass = (float(hw_random16(1000, 10000)) / 10000.0f); // from .1 to 1. } } @@ -3071,12 +3085,12 @@ 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 if (thisHeight < -0.5f || thisHeight > 1.5f) { - thisHeight = balls[i].height = (float(random16(0, 10000)) / 10000.0f); // from 0. to 1. + thisHeight = balls[i].height = (float(hw_random16(0, 10000)) / 10000.0f); // from 0. to 1. balls[i].lastBounceUpdate = strip.now; } // check if reached ends of the strip @@ -3087,7 +3101,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 +3131,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 +3145,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_t(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 +3163,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 +3200,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 > hw_random8()) SEGMENT.setPixelColor(hw_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=11,m12=0"; //pixels //Solid colour background with glitter (can be replaced by Glitter) @@ -3215,7 +3236,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 +3253,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,30 +3265,30 @@ 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; } else { // if kernel is inactive, randomly pop it - if (random8() < 2) { // POP!!! + if (hw_random8() < 2) { // POP!!! popcorn[i].pos = 0.01f; - uint16_t peakHeight = 128 + random8(128); //0-255 + unsigned peakHeight = 128 + hw_random8(128); //0-255 peakHeight = (peakHeight * (SEGLEN -1)) >> 8; popcorn[i].vel = sqrtf(-2.0f * gravity * peakHeight); if (SEGMENT.palette) { - popcorn[i].colIndex = random8(); + popcorn[i].colIndex = hw_random8(); } else { - byte col = random8(0, NUM_COLORS); + byte col = hw_random8(0, NUM_COLORS); if (!SEGCOLOR(2) || !SEGCOLOR(col)) col = 0; popcorn[i].colIndex = col; } @@ -3274,15 +3297,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, (int)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,19 +3338,19 @@ 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]; } if (fadeStep == 0) { //init vals - s = 128; s_target = 130 + random8(4); fadeStep = 1; + s = 128; s_target = 130 + hw_random8(4); fadeStep = 1; } bool newTarget = false; @@ -3340,24 +3363,24 @@ 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); + s_target = hw_random8(rndval) + hw_random8(rndval); //between 0 and rndval*2 -2 = 252 + if (s_target < (rndval >> 1)) s_target = (rndval >> 1) + hw_random8(rndval); + unsigned offset = (255 - valrange); s_target += offset; - 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) { - SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), s)); + if (i > 0) { + SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), uint8_t(s))); SEGENV.data[d] = s; SEGENV.data[d+1] = s_target; SEGENV.data[d+2] = fadeStep; } else { - for (int j = 0; j < SEGLEN; j++) { - SEGMENT.setPixelColor(j, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(j, true, PALETTE_SOLID_WRAP, 0), s)); + for (unsigned j = 0; j < SEGLEN; j++) { + SEGMENT.setPixelColor(j, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(j, true, PALETTE_SOLID_WRAP, 0), uint8_t(s))); } SEGENV.aux0 = s; SEGENV.aux1 = s_target; SEGENV.step = fadeStep; @@ -3404,15 +3427,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,22 +3447,22 @@ 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) + if (hw_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 = hw_random16(SEGLEN-1); + float multiplier = (float)(hw_random8())/255.0f * 1.0f; - stars[j].color = CRGB(SEGMENT.color_wheel(random8())); + stars[j].color = CRGB(SEGMENT.color_wheel(hw_random8())); stars[j].pos = startPos; - stars[j].vel = maxSpeed * (float)(random8())/255.0 * multiplier; + stars[j].vel = maxSpeed * (float)(hw_random8())/255.0f * multiplier; stars[j].birth = it; stars[j].last = it; // more fragments means larger burst effect - int num = random8(3,6 + (SEGMENT.intensity >> 5)); + int num = hw_random8(3,6 + (SEGMENT.intensity >> 5)); for (int i=0; i < STARBURST_MAX_FRAG; i++) { if (i < num) stars[j].fragment[i] = startPos; @@ -3450,7 +3473,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; - int start = loc - particleSize; - int end = loc + particleSize; + unsigned start = loc - particleSize; + unsigned end = loc + particleSize; if (start < 0) start = 0; if (start == end) end++; if (end > SEGLEN) end = SEGLEN; - for (int p = start; p < end; p++) { - SEGMENT.setPixelColor(p, c.r, c.g, c.b); + for (unsigned p = start; p < end; p++) { + SEGMENT.setPixelColor(p, c); } } } @@ -3524,18 +3546,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() ? SEG_W : 1; + const int rows = SEGMENT.is2D() ? SEG_H : SEGLEN; //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(5 + ((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); @@ -3556,11 +3578,11 @@ uint16_t mode_exploding_fireworks(void) if (SEGENV.aux0 < 2) { //FLARE if (SEGENV.aux0 == 0) { //init flare flare->pos = 0; - flare->posX = SEGMENT.is2D() ? random16(2,cols-3) : (SEGMENT.intensity > random8()); // will enable random firing side on 1D - uint16_t peakHeight = 75 + random8(180); //0-255 + flare->posX = SEGMENT.is2D() ? hw_random16(2,cols-3) : (SEGMENT.intensity > hw_random8()); // will enable random firing side on 1D + unsigned peakHeight = 75 + hw_random8(180); //0-255 peakHeight = (peakHeight * (rows -1)) >> 8; flare->vel = sqrtf(-2.0f * gravity * peakHeight); - flare->velX = SEGMENT.is2D() ? (random8(9)-4)/64.0f : 0; // no X velocity on 1D + flare->velX = SEGMENT.is2D() ? (hw_random8(9)-4)/64.0f : 0; // no X velocity on 1D flare->col = 255; //brightness SEGENV.aux0 = 1; } @@ -3568,7 +3590,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,20 +3610,21 @@ uint16_t mode_exploding_fireworks(void) * Explosion happens where the flare ended. * Size is proportional to the height. */ - int nSparks = flare->pos + random8(4); - nSparks = constrain(nSparks, 4, numSparks); + unsigned nSparks = flare->pos + hw_random8(4); + nSparks = std::max(nSparks, 4U); // This is not a standard constrain; numSparks is not guaranteed to be at least 4 + nSparks = std::min(nSparks, numSparks); // 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 + sparks[i].vel = (float(hw_random16(20001)) / 10000.0f) - 0.9f; // from -0.9 to 1.1 sparks[i].vel *= rows<32 ? 0.5f : 1; // reduce velocity for smaller strips - sparks[i].velX = SEGMENT.is2D() ? (float(random16(20001)) / 10000.0f) - 1.0f : 0; // from -1 to 1 + sparks[i].velX = SEGMENT.is2D() ? (float(hw_random16(20001)) / 10000.0f) - 1.0f : 0; // from -1 to 1 sparks[i].col = 345;//abs(sparks[i].vel * 750.0); // set colors before scaling velocity to keep them bright //sparks[i].col = constrain(sparks[i].col, 0, 345); - sparks[i].colIndex = random8(); + sparks[i].colIndex = hw_random8(); sparks[i].vel *= flare->pos/rows; // proportional to height sparks[i].velX *= SEGMENT.is2D() ? flare->posX/cols : 0; // proportional to width sparks[i].vel *= -gravity *50; @@ -3612,7 +3635,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,25 +3644,25 @@ 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); + CRGBW c = BLACK; //HeatColor(sparks[i].col); if (prog > 300) { //fade from white to spark color - c = CRGB(color_blend(spColor, WHITE, (prog - 300)*5)); + c = color_blend(spColor, WHITE, uint8_t((prog - 300)*5)); } else if (prog > 45) { //fade from spark color to black - c = CRGB(color_blend(BLACK, spColor, prog - 45)); - uint8_t cooling = (300 - prog) >> 5; + c = color_blend(BLACK, spColor, uint8_t(prog - 45)); + unsigned cooling = (300 - prog) >> 5; c.g = qsub8(c.g, cooling); c.b = qsub8(c.b, cooling * 2); } - if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(int(sparks[i].posX), rows - int(sparks[i].pos) - 1, c.red, c.green, c.blue); - else SEGMENT.setPixelColor(int(sparks[i].posX) ? rows - int(sparks[i].pos) - 1 : int(sparks[i].pos), c.red, c.green, c.blue); + if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(int(sparks[i].posX), rows - int(sparks[i].pos) - 1, c); + else SEGMENT.setPixelColor(int(sparks[i].posX) ? rows - int(sparks[i].pos) - 1 : int(sparks[i].pos), c); } } - 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 + SEGENV.aux0 = 6 + hw_random8(10); //wait for this many frames } } else { SEGENV.aux0--; @@ -3651,7 +3674,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 +3685,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 +3696,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); - gravity *= max(1, SEGLEN-1); + float gravity = -0.0005f - (SEGMENT.speed/50000.0f); + gravity *= max(1, (int)SEGLEN-1); int sourcedrop = 12; - for (int j=0;j255) drops[j].col=255; - SEGMENT.setPixelColor(indexToVStrip(uint16_t(drops[j].pos), stripNr), color_blend(BLACK,SEGCOLOR(0),drops[j].col)); + SEGMENT.setPixelColor(indexToVStrip(uint16_t(drops[j].pos), stripNr), color_blend(BLACK,SEGCOLOR(0),uint8_t(drops[j].col))); drops[j].col += map(SEGMENT.speed, 0, 255, 1, 6); // swelling - if (random8() < drops[j].col/10) { // random drop + if (hw_random8() < drops[j].col/10) { // random drop drops[j].colIndex=2; //fall drops[j].col=255; } @@ -3706,12 +3729,12 @@ uint16_t mode_drip(void) drops[j].vel += gravity; // gravity is negative for (int i=1;i<7-drops[j].colIndex;i++) { // some minor math so we don't expand bouncing droplets - uint16_t pos = constrain(uint16_t(drops[j].pos) +i, 0, SEGLEN-1); //this is BAD, returns a pos >= SEGLEN occasionally - SEGMENT.setPixelColor(indexToVStrip(pos, stripNr), color_blend(BLACK,SEGCOLOR(0),drops[j].col/i)); //spread pixel with fade while falling + unsigned pos = constrain(unsigned(drops[j].pos) +i, 0, SEGLEN-1); //this is BAD, returns a pos >= SEGLEN occasionally + SEGMENT.setPixelColor(indexToVStrip(pos, stripNr), color_blend(BLACK,SEGCOLOR(0),uint8_t(drops[j].col/i))); //spread pixel with fade while falling } if (drops[j].colIndex > 2) { // during bounce, some water is on the floor - SEGMENT.setPixelColor(indexToVStrip(0, stripNr), color_blend(SEGCOLOR(0),BLACK,drops[j].col)); + SEGMENT.setPixelColor(indexToVStrip(0, stripNr), color_blend(SEGCOLOR(0),BLACK,uint8_t(drops[j].col))); } } else { // we hit bottom if (drops[j].colIndex > 2) { // already hit once, so back to forming @@ -3733,7 +3756,7 @@ uint16_t mode_drip(void) } }; - for (int stripNr=0; stripNr(SEGENV.data); @@ -3780,17 +3803,17 @@ uint16_t mode_tetrix(void) { // speed calculation: a single brick should reach bottom of strip in X seconds // if the speed is set to 1 this should take 5s and at 255 it should take 0.25s // as this is dependant on SEGLEN it should be taken into account and the fact that effect runs every FRAMETIME s - int speed = SEGMENT.speed ? SEGMENT.speed : random8(1,255); + int speed = SEGMENT.speed ? SEGMENT.speed : hw_random8(1,255); speed = map(speed, 1, 255, 5000, 250); // time taken for full (SEGLEN) drop drop->speed = float(SEGLEN * FRAMETIME) / float(speed); // set speed drop->pos = SEGLEN; // start at end of segment (no need to subtract 1) - if (!SEGMENT.check1) drop->col = random8(0,15)<<4; // limit color choices so there is enough HUE gap + if (!SEGMENT.check1) drop->col = hw_random8(0,15)<<4; // limit color choices so there is enough HUE gap drop->step = 1; // drop state (0 init, 1 forming, 2 falling) - drop->brick = (SEGMENT.intensity ? (SEGMENT.intensity>>5)+1 : random8(1,5)) * (1+(SEGLEN>>6)); // size of brick + drop->brick = (SEGMENT.intensity ? (SEGMENT.intensity>>5)+1 : hw_random8(1,5)) * (1+(SEGLEN>>6)); // size of brick } if (drop->step == 1) { // forming - if (random8()>>6) { // random drop + if (hw_random8()>>6) { // random drop drop->step = 2; // fall } } @@ -3799,8 +3822,8 @@ uint16_t mode_tetrix(void) { if (drop->pos > drop->stack) { // fall until top of stack drop->pos -= drop->speed; // may add gravity as: speed += gravity if (int(drop->pos) < int(drop->stack)) drop->pos = drop->stack; - for (int i = int(drop->pos); i < SEGLEN; i++) { - uint32_t col = ipos)+drop->brick ? SEGMENT.color_from_palette(drop->col, false, false, 0) : SEGCOLOR(1); + for (unsigned i = unsigned(drop->pos); i < SEGLEN; i++) { + uint32_t col = i < unsigned(drop->pos)+drop->brick ? SEGMENT.color_from_palette(drop->col, false, false, 0) : SEGCOLOR(1); SEGMENT.setPixelColor(indexToVStrip(i, stripNr), col); } } else { // we hit bottom @@ -3814,7 +3837,7 @@ uint16_t mode_tetrix(void) { drop->brick = 0; // reset brick size (no more growing) if (drop->step > strip.now) { // allow fading of virtual strip - for (int i = 0; i < SEGLEN; i++) SEGMENT.blendPixelColor(indexToVStrip(i, stripNr), SEGCOLOR(1), 25); // 10% blend + for (unsigned i = 0; i < SEGLEN; i++) SEGMENT.blendPixelColor(indexToVStrip(i, stripNr), SEGCOLOR(1), 25); // 10% blend } else { drop->stack = 0; // reset brick stack size drop->step = 0; // proceed with next brick @@ -3824,7 +3847,7 @@ uint16_t mode_tetrix(void) { } }; - for (int stripNr=0; stripNr> 11)); + unsigned size = (1 + ((SEGMENT.speed * SEGLEN) >> 11)); if (SEGMENT.speed == 255) size = 255; if (percent <= 100) { - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { if (i < SEGENV.aux1) { if (SEGMENT.check1) SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(map(percent,0,100,0,255), false, false, 0)); @@ -3885,7 +3906,7 @@ uint16_t mode_percent(void) { } } } else { - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { if (i < (SEGLEN - SEGENV.aux1)) { SEGMENT.setPixelColor(i, SEGCOLOR(1)); } @@ -3916,7 +3937,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; @@ -3935,8 +3956,8 @@ uint16_t mode_heartbeat(void) { SEGENV.step = strip.now; } - for (int i = 0; i < SEGLEN; i++) { - SEGMENT.setPixelColor(i, color_blend(SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), SEGCOLOR(1), 255 - (SEGENV.aux1 >> 8))); + for (unsigned i = 0; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, color_blend(SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), SEGCOLOR(1), uint8_t(255 - (SEGENV.aux1 >> 8)))); } return FRAMETIME; @@ -3969,19 +3990,19 @@ 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_t(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); - return ColorFromPalette(p, sindex8, bri, LINEARBLEND); + unsigned sindex16 = sin16_t(ci) + 32768; + unsigned sindex8 = scale16(sindex16, 240); + return CRGB(ColorFromPalette(p, sindex8, bri, LINEARBLEND)); } uint16_t mode_pacifica() @@ -4006,44 +4027,44 @@ 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 & 0xFFFF, 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_t(3, 179, 269); + unsigned speedfactor2 = beatsin16_t(4, 179, 269); uint32_t deltams1 = (deltams * speedfactor1) / 256; uint32_t deltams2 = (deltams * speedfactor2) / 256; uint32_t deltams21 = (deltams1 + deltams2) / 2; - sCIStart1 += (deltams1 * beatsin88(1011,10,13)); - sCIStart2 -= (deltams21 * beatsin88(777,8,11)); - sCIStart3 -= (deltams1 * beatsin88(501,5,7)); - sCIStart4 -= (deltams2 * beatsin88(257,4,6)); + sCIStart1 += (deltams1 * beatsin88_t(1011,10,13)); + sCIStart2 -= (deltams21 * beatsin88_t(777,8,11)); + sCIStart3 -= (deltams1 * beatsin88_t(501,5,7)); + sCIStart4 -= (deltams2 * beatsin88_t(257,4,6)); SEGENV.aux0 = sCIStart1; SEGENV.aux1 = sCIStart2; - SEGENV.step = sCIStart4; SEGENV.step = (SEGENV.step << 16) + sCIStart3; + SEGENV.step = (sCIStart4 << 16) | (sCIStart3 & 0xFFFF); // 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_t( 9, 55, 65); + unsigned wave = beat8( 7 ); - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { CRGB c = CRGB(2, 6, 10); // Render each of four layers, with different scales and speeds, that vary over time - c += pacifica_one_layer(i, pacifica_palette_1, sCIStart1, beatsin16(3, 11 * 256, 14 * 256), beatsin8(10, 70, 130), 0-beat16(301)); - c += pacifica_one_layer(i, pacifica_palette_2, sCIStart2, beatsin16(4, 6 * 256, 9 * 256), beatsin8(17, 40, 80), beat16(401)); - c += pacifica_one_layer(i, pacifica_palette_3, sCIStart3, 6 * 256 , beatsin8(9, 10,38) , 0-beat16(503)); - c += pacifica_one_layer(i, pacifica_palette_3, sCIStart4, 5 * 256 , beatsin8(8, 10,28) , beat16(601)); + c += pacifica_one_layer(i, pacifica_palette_1, sCIStart1, beatsin16_t(3, 11 * 256, 14 * 256), beatsin8_t(10, 70, 130), 0-beat16(301)); + c += pacifica_one_layer(i, pacifica_palette_2, sCIStart2, beatsin16_t(4, 6 * 256, 9 * 256), beatsin8_t(17, 40, 80), beat16(401)); + c += pacifica_one_layer(i, pacifica_palette_3, sCIStart3, 6 * 256 , beatsin8_t(9, 10,38) , 0-beat16(503)); + c += pacifica_one_layer(i, pacifica_palette_3, sCIStart4, 5 * 256 , beatsin8_t(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_t( 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)); } @@ -4052,7 +4073,7 @@ uint16_t mode_pacifica() c.green = scale8(c.green, 200); c |= CRGB( 2, 5, 7); - SEGMENT.setPixelColor(i, c.red, c.green, c.blue); + SEGMENT.setPixelColor(i, c); } strip.now = nowOld; @@ -4076,15 +4097,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; @@ -4092,15 +4113,12 @@ uint16_t mode_sunrise() { if (SEGMENT.speed > 60) stage = 0xFFFF - stage; //sunset } - for (int i = 0; i <= SEGLEN/2; i++) + for (unsigned i = 0; i <= SEGLEN/2; i++) { - //default palette is Fire - uint32_t c = SEGMENT.color_from_palette(0, false, true, 255); //background - - uint16_t wave = triwave16((i * stage) / SEGLEN); - + //default palette is Fire + unsigned wave = triwave16((i * stage) / SEGLEN); wave = (wave >> 8) + ((wave * SEGMENT.intensity) >> 15); - + uint32_t c; if (wave > 240) { //clipped, full white sun c = SEGMENT.color_from_palette( 240, false, true, 255); } else { //transition @@ -4112,30 +4130,30 @@ uint16_t mode_sunrise() { return FRAMETIME; } -static const char _data_FX_MODE_SUNRISE[] PROGMEM = "Sunrise@Time [min],Width;;!;;sx=60"; +static const char _data_FX_MODE_SUNRISE[] PROGMEM = "Sunrise@Time [min],Width;;!;;pal=35,sx=60"; /* * 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++) { + for (unsigned 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)); + SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(index, false, false, 0), uint8_t(b))); index += 256 / SEGLEN; if (SEGLEN > 256) index ++; // Correction for segments longer than 256 LEDs } @@ -4157,12 +4175,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)); + for (unsigned i = 0; i < SEGLEN; i++) { + unsigned ranstart = random8(); // The starting value (aka brightness) for each pixel. Must be consistent each time through the loop for this to work. + unsigned pixBri = sin8_t(ranstart + 16 * strip.now/(256-SEGMENT.speed)); if (random8() > SEGMENT.intensity) pixBri = 0; SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(random8()+strip.now/100, false, PALETTE_SOLID_WRAP, 0), pixBri)); } @@ -4175,37 +4193,34 @@ 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(); - palettes[1] = CRGBPalette16(CHSV(baseI+random8(64), 255, random8(128,255)), CHSV(baseI+128, 255, random8(128,255)), CHSV(baseI+random8(92), 192, random8(128,255)), CHSV(baseI+random8(92), 255, random8(128,255))); + unsigned baseI = hw_random8(); + palettes[1] = CRGBPalette16(CHSV(baseI+hw_random8(64), 255, hw_random8(128,255)), CHSV(baseI+128, 255, hw_random8(128,255)), CHSV(baseI+hw_random8(92), 192, hw_random8(128,255)), CHSV(baseI+hw_random8(92), 255, hw_random8(128,255))); } - CRGB color; - //EVERY_N_MILLIS(10) { //(don't have to time this, effect function is only called every 24ms) nblendPaletteTowardPalette(palettes[0], palettes[1], 48); // Blend towards the target palette over 48 iterations. if (SEGMENT.palette > 0) palettes[0] = SEGPALETTE; - for (int i = 0; i < SEGLEN; i++) { - 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. - color = ColorFromPalette(palettes[0], index, 255, LINEARBLEND); // Use the my own palette. - SEGMENT.setPixelColor(i, color.red, color.green, color.blue); + for (unsigned i = 0; i < SEGLEN; i++) { + unsigned index = inoise8(i*scale, SEGENV.aux0+i*scale); // Get a value from the noise function. I'm using both x and y axis. + SEGMENT.setPixelColor(i, ColorFromPalette(palettes[0], index, 255, LINEARBLEND)); // Use my own palette. } - SEGENV.aux0 += beatsin8(10,1,4); // Moving along the distance. Vary it a bit with a sine wave. + SEGENV.aux0 += beatsin8_t(10,1,4); // Moving along the distance. Vary it a bit with a sine wave. return FRAMETIME; } @@ -4218,20 +4233,20 @@ 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).. + for (unsigned i = 0; i < SEGLEN; i++) { // For each of the LED's in the strand, set a brightness based on a wave as follows: + uint8_t pixBri = cubicwave8((i*freq)+SEGENV.step);//qsuba(cubicwave8((i*freq)+SEGENV.step), (255-SEGMENT.intensity)); // qsub sets a minimum value called thiscutoff. If < thiscutoff, then bright = 0. Otherwise, bright = 128 (as defined in qsub).. //setPixCol(i, i*colorIndex/255, pixBri); SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i*colorIndex/255, false, PALETTE_SOLID_WRAP, 0), pixBri)); } return FRAMETIME; } -static const char _data_FX_MODE_SINEWAVE[] PROGMEM = "Sine"; +static const char _data_FX_MODE_SINEWAVE[] PROGMEM = "Sine@!,Scale;;!"; /* @@ -4239,29 +4254,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,18 +4295,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; - uint32_t c = SEGMENT.color_from_palette((i * 255)/ numBirds, false, false, 0); // no palette wrapping - bird = constrain(bird, 0, SEGLEN-1); - SEGMENT.setPixelColor(bird, c); + unsigned megumin = sin16_t(counter) + 0x8000; + unsigned bird = uint32_t(megumin * SEGLEN) >> 16; + bird = constrain(bird, 0U, SEGLEN-1U); + SEGMENT.setPixelColor(bird, SEGMENT.color_from_palette((i * 255)/ numBirds, false, false, 0)); // no palette wrapping } return FRAMETIME; } @@ -4331,11 +4345,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 +4361,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) { @@ -4355,21 +4369,21 @@ uint16_t mode_dancing_shadows(void) spotlights[i].lastUpdateTime = time; } - respawn = (spotlights[i].speed > 0.0 && spotlights[i].position > (SEGLEN + 2)) + respawn = (spotlights[i].speed > 0.0 && spotlights[i].position > (int)(SEGLEN + 2)) || (spotlights[i].speed < 0.0 && spotlights[i].position < -(spotlights[i].width + 2)); } if (initialize || respawn) { - spotlights[i].colorIdx = random8(); - spotlights[i].width = random8(1, 10); + spotlights[i].colorIdx = hw_random8(); + spotlights[i].width = hw_random8(1, 10); - spotlights[i].speed = 1.0/random8(4, 50); + spotlights[i].speed = 1.0/hw_random8(4, 50); if (initialize) { - spotlights[i].position = random16(SEGLEN); - spotlights[i].speed *= random8(2) ? 1.0 : -1.0; + spotlights[i].position = hw_random16(SEGLEN); + spotlights[i].speed *= hw_random8(2) ? 1.0 : -1.0; } else { - if (random8(2)) { + if (hw_random8(2)) { spotlights[i].position = SEGLEN + spotlights[i].width; spotlights[i].speed *= -1.0; }else { @@ -4378,14 +4392,14 @@ uint16_t mode_dancing_shadows(void) } spotlights[i].lastUpdateTime = time; - spotlights[i].type = random8(SPOT_TYPES_COUNT); + spotlights[i].type = hw_random8(SPOT_TYPES_COUNT); } uint32_t color = SEGMENT.color_from_palette(spotlights[i].colorIdx, false, false, 255); int start = spotlights[i].position; if (spotlights[i].width <= 1) { - if (start >= 0 && start < SEGLEN) { + if (start >= 0 && start < (int)SEGLEN) { SEGMENT.blendPixelColor(start, color, 128); } } else { @@ -4455,8 +4469,8 @@ uint16_t mode_washing_machine(void) { SEGENV.step += (speed * 2048) / (512 - SEGMENT.speed); - for (int i = 0; i < SEGLEN; i++) { - uint8_t col = sin8(((SEGMENT.intensity / 25 + 1) * 255 * i / SEGLEN) + (SEGENV.step >> 7)); + for (unsigned i = 0; i < SEGLEN; i++) { + uint8_t col = sin8_t(((SEGMENT.intensity / 25 + 1) * 255 * i / SEGLEN) + (SEGENV.step >> 7)); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(col, false, PALETTE_SOLID_WRAP, 3)); } @@ -4488,22 +4502,22 @@ static const char _data_FX_MODE_IMAGE[] PROGMEM = "Image@!,;;;12;sx=128"; 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 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; + if (offset >= pixelLen) offset = 0; } return FRAMETIME; @@ -4537,7 +4551,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 @@ -4555,27 +4569,27 @@ uint16_t mode_tv_simulator(void) { // create a new sceene if (((strip.now - tvSimulator->sceeneStart) >= tvSimulator->sceeneDuration) || SEGENV.aux1 == 0) { tvSimulator->sceeneStart = strip.now; // remember the start of the new sceene - tvSimulator->sceeneDuration = random16(60* 250* colorSpeed, 60* 750 * colorSpeed); // duration of a "movie sceene" which has similar colors (5 to 15 minutes with max speed slider) - tvSimulator->sceeneColorHue = random16( 0, 768); // random start color-tone for the sceene - tvSimulator->sceeneColorSat = random8 ( 100, 130 + colorIntensity); // random start color-saturation for the sceene - tvSimulator->sceeneColorBri = random8 ( 200, 240); // random start color-brightness for the sceene + tvSimulator->sceeneDuration = hw_random16(60* 250* colorSpeed, 60* 750 * colorSpeed); // duration of a "movie sceene" which has similar colors (5 to 15 minutes with max speed slider) + tvSimulator->sceeneColorHue = hw_random16( 0, 768); // random start color-tone for the sceene + tvSimulator->sceeneColorSat = hw_random8 ( 100, 130 + colorIntensity); // random start color-saturation for the sceene + tvSimulator->sceeneColorBri = hw_random8 ( 200, 240); // random start color-brightness for the sceene SEGENV.aux1 = 1; SEGENV.aux0 = 0; } // 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 + j = hw_random8(4 * colorIntensity); + hue = (hw_random8() < 128) ? ((j < tvSimulator->sceeneColorHue) ? tvSimulator->sceeneColorHue - j : 767 - tvSimulator->sceeneColorHue - j) : // negative ((j + tvSimulator->sceeneColorHue) < 767 ? tvSimulator->sceeneColorHue + j : tvSimulator->sceeneColorHue + j - 767) ; // positive // saturation - j = random8(2 * colorIntensity); + j = hw_random8(2 * colorIntensity); sat = (tvSimulator->sceeneColorSat - j) < 0 ? 0 : tvSimulator->sceeneColorSat - j; // brightness - j = random8(100); + j = hw_random8(100); bri = (tvSimulator->sceeneColorBri - j) < 0 ? 0 : tvSimulator->sceeneColorBri - j; // calculate R,G,B from HSV @@ -4601,9 +4615,9 @@ uint16_t mode_tv_simulator(void) { SEGENV.aux0 = 1; // randomize total duration and fade duration for the actual color - tvSimulator->totalTime = random16(250, 2500); // Semi-random pixel-to-pixel time - tvSimulator->fadeTime = random16(0, tvSimulator->totalTime); // Pixel-to-pixel transition time - if (random8(10) < 3) tvSimulator->fadeTime = 0; // Force scene cut 30% of time + tvSimulator->totalTime = hw_random16(250, 2500); // Semi-random pixel-to-pixel time + tvSimulator->fadeTime = hw_random16(0, tvSimulator->totalTime); // Pixel-to-pixel transition time + if (hw_random8(10) < 3) tvSimulator->fadeTime = 0; // Force scene cut 30% of time tvSimulator->startTime = strip.now; } // end of initialization @@ -4623,7 +4637,7 @@ uint16_t mode_tv_simulator(void) { } // set strip color - for (i = 0; i < SEGLEN; i++) { + for (i = 0; i < (int)SEGLEN; i++) { SEGMENT.setPixelColor(i, r >> 8, g >> 8, b >> 8); // Quantize to 8-bit } @@ -4637,7 +4651,7 @@ uint16_t mode_tv_simulator(void) { return FRAMETIME; } -static const char _data_FX_MODE_TV_SIMULATOR[] PROGMEM = "TV Simulator@!,!;;"; +static const char _data_FX_MODE_TV_SIMULATOR[] PROGMEM = "TV Simulator@!,!;;!;01"; /* @@ -4668,15 +4682,15 @@ class AuroraWave { public: void init(uint32_t segment_length, CRGB color) { - ttl = random16(500, 1501); + ttl = hw_random16(500, 1501); basecolor = color; - basealpha = random8(60, 101) / (float)100; + basealpha = hw_random8(60, 101) / (float)100; age = 0; - width = random16(segment_length / 20, segment_length / W_WIDTH_FACTOR); //half of width to make math easier + width = hw_random16(segment_length / 20, segment_length / W_WIDTH_FACTOR); //half of width to make math easier if (!width) width = 1; - center = random8(101) / (float)100 * segment_length; - goingleft = random8(0, 2) == 0; - speed_factor = (random8(10, 31) / (float)100 * W_MAX_SPEED / 255); + center = hw_random8(101) / (float)100 * segment_length; + goingleft = hw_random8(0, 2) == 0; + speed_factor = (hw_random8(10, 31) / (float)100 * W_MAX_SPEED / 255); alive = true; } @@ -4761,7 +4775,7 @@ uint16_t mode_aurora(void) { waves = reinterpret_cast(SEGENV.data); for (int i = 0; i < SEGENV.aux1; i++) { - waves[i].init(SEGLEN, CRGB(SEGMENT.color_from_palette(random8(), false, false, random8(0, 3)))); + waves[i].init(SEGLEN, CRGB(SEGMENT.color_from_palette(hw_random8(), false, false, hw_random8(0, 3)))); } } else { waves = reinterpret_cast(SEGENV.data); @@ -4773,7 +4787,7 @@ uint16_t mode_aurora(void) { if(!(waves[i].stillAlive())) { //If a wave dies, reinitialize it starts over. - waves[i].init(SEGLEN, CRGB(SEGMENT.color_from_palette(random8(), false, false, random8(0, 3)))); + waves[i].init(SEGLEN, CRGB(SEGMENT.color_from_palette(hw_random8(), false, false, hw_random8(0, 3)))); } } @@ -4782,7 +4796,7 @@ uint16_t mode_aurora(void) { if (SEGCOLOR(1)) backlight++; if (SEGCOLOR(2)) backlight++; //Loop through LEDs to determine color - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { CRGB mixedRgb = CRGB(backlight, backlight, backlight); //For each LED we must check each wave if it is "active" at this position. @@ -4813,8 +4827,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)); } @@ -4829,9 +4843,9 @@ static const char _data_FX_MODE_PERLINMOVE[] PROGMEM = "Perlin Move@!,# of pixel // Uses beatsin8() + phase shifting. By: Andrew Tuline uint16_t mode_wavesins(void) { - for (int i = 0; i < SEGLEN; i++) { - uint8_t bri = sin8(strip.now/4 + i * SEGMENT.intensity); - uint8_t index = beatsin8(SEGMENT.speed, SEGMENT.custom1, SEGMENT.custom1+SEGMENT.custom2, 0, i * (SEGMENT.custom3<<3)); // custom3 is reduced resolution slider + for (unsigned i = 0; i < SEGLEN; i++) { + uint8_t bri = sin8_t(strip.now/4 + i * SEGMENT.intensity); + uint8_t index = beatsin8_t(SEGMENT.speed, SEGMENT.custom1, SEGMENT.custom1+SEGMENT.custom2, 0, i * (SEGMENT.custom3<<3)); // custom3 is reduced resolution slider //SEGMENT.setPixelColor(i, ColorFromPalette(SEGPALETTE, index, bri, LINEARBLEND)); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0, bri)); } @@ -4846,16 +4860,16 @@ 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); - for (int i = 0; i < SEGLEN; i++) { - int c = (abs(i - hl) / hl) * 127; - c = sin8(c); - c = sin8(c / 2 + t); - byte b = sin8(c + t/8); + for (unsigned i = 0; i < SEGLEN; i++) { + int c = (abs((int)i - hl) / hl) * 127; + c = sin8_t(c); + c = sin8_t(c / 2 + t); + byte b = sin8_t(c + t/8); SEGMENT.setPixelColor(i, CHSV(b + hue, 255, 255)); } @@ -4874,32 +4888,32 @@ 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 = SEG_W; + const int rows = SEG_H; + int x, y; SEGMENT.fadeToBlackBy(16 + (SEGMENT.speed>>3)); // create fading trails unsigned long t = strip.now/128; // timebase // outer stars for (size_t i = 0; i < 8; i++) { - x = beatsin8(SEGMENT.custom1>>3, 0, cols - 1, 0, ((i % 2) ? 128 : 0) + t * i); - y = beatsin8(SEGMENT.intensity>>3, 0, rows - 1, 0, ((i % 2) ? 192 : 64) + t * i); + x = beatsin8_t(SEGMENT.custom1>>3, 0, cols - 1, 0, ((i % 2) ? 128 : 0) + t * i); + y = beatsin8_t(SEGMENT.intensity>>3, 0, rows - 1, 0, ((i % 2) ? 192 : 64) + t * i); SEGMENT.addPixelColorXY(x, y, SEGMENT.color_from_palette(i*32, false, PALETTE_SOLID_WRAP, SEGMENT.check1?0:255)); } // inner stars for (size_t i = 0; i < 4; i++) { - x = beatsin8(SEGMENT.custom2>>3, cols/4, cols - 1 - cols/4, 0, ((i % 2) ? 128 : 0) + t * i); - y = beatsin8(SEGMENT.custom3 , rows/4, rows - 1 - rows/4, 0, ((i % 2) ? 192 : 64) + t * i); + x = beatsin8_t(SEGMENT.custom2>>3, cols/4, cols - 1 - cols/4, 0, ((i % 2) ? 128 : 0) + t * i); + y = beatsin8_t(SEGMENT.custom3 , rows/4, rows - 1 - rows/4, 0, ((i % 2) ? 192 : 64) + t * i); SEGMENT.addPixelColorXY(x, y, SEGMENT.color_from_palette(255-i*64, false, PALETTE_SOLID_WRAP, SEGMENT.check1?0:255)); } // 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"; //////////////////////////// @@ -4908,8 +4922,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 = SEG_W; + const int rows = SEG_H; if (SEGENV.call == 0) { SEGENV.aux0 = 0; // start with red hue @@ -4921,13 +4935,13 @@ uint16_t mode_2DColoredBursts() { // By: ldirko https://editor.so byte numLines = SEGMENT.intensity/16 + 1; SEGENV.aux0++; // hue - SEGMENT.fadeToBlackBy(40); + SEGMENT.fadeToBlackBy(40 - SEGMENT.check2 * 8); for (size_t i = 0; i < numLines; i++) { - byte x1 = beatsin8(2 + SEGMENT.speed/16, 0, (cols - 1)); - byte x2 = beatsin8(1 + SEGMENT.speed/16, 0, (cols - 1)); - byte y1 = beatsin8(5 + SEGMENT.speed/16, 0, (rows - 1), 0, i * 24); - byte y2 = beatsin8(3 + SEGMENT.speed/16, 0, (rows - 1), 0, i * 48 + 64); - CRGB color = ColorFromPalette(SEGPALETTE, i * 255 / numLines + (SEGENV.aux0&0xFF), 255, LINEARBLEND); + byte x1 = beatsin8_t(2 + SEGMENT.speed/16, 0, (cols - 1)); + byte x2 = beatsin8_t(1 + SEGMENT.speed/16, 0, (rows - 1)); + byte y1 = beatsin8_t(5 + SEGMENT.speed/16, 0, (cols - 1), 0, i * 24); + byte y2 = beatsin8_t(3 + SEGMENT.speed/16, 0, (rows - 1), 0, i * 48 + 64); + uint32_t color = ColorFromPalette(SEGPALETTE, i * 255 / numLines + (SEGENV.aux0&0xFF), 255, LINEARBLEND); byte xsteps = abs8(x1 - y1) + 1; byte ysteps = abs8(x2 - y2) + 1; @@ -4947,11 +4961,11 @@ uint16_t mode_2DColoredBursts() { // By: ldirko https://editor.so SEGMENT.setPixelColorXY(y1, y2, DARKSLATEGRAY); } } - if (SEGMENT.custom3) SEGMENT.blur(SEGMENT.custom3/2); + SEGMENT.blur(SEGMENT.custom3>>1, SEGMENT.check2); return FRAMETIME; } // mode_2DColoredBursts() -static const char _data_FX_MODE_2DCOLOREDBURSTS[] PROGMEM = "Colored Bursts@Speed,# of lines,,,Blur,Gradient,,Dots;;!;2;c3=16"; +static const char _data_FX_MODE_2DCOLOREDBURSTS[] PROGMEM = "Colored Bursts@Speed,# of lines,,,Blur,Gradient,Smear,Dots;;!;2;c3=16"; ///////////////////// @@ -4960,20 +4974,19 @@ static const char _data_FX_MODE_2DCOLOREDBURSTS[] PROGMEM = "Colored Bursts@Spee uint16_t mode_2Ddna(void) { // dna originally by by ldirko at https://pastebin.com/pCkkkzcs. Updated by Preyy. WLED conversion by Andrew Tuline. if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; SEGMENT.fadeToBlackBy(64); for (int i = 0; i < cols; i++) { - SEGMENT.setPixelColorXY(i, beatsin8(SEGMENT.speed/8, 0, rows-1, 0, i*4 ), ColorFromPalette(SEGPALETTE, i*5+strip.now/17, beatsin8(5, 55, 255, 0, i*10), LINEARBLEND)); - SEGMENT.setPixelColorXY(i, beatsin8(SEGMENT.speed/8, 0, rows-1, 0, i*4+128), ColorFromPalette(SEGPALETTE, i*5+128+strip.now/17, beatsin8(5, 55, 255, 0, i*10+128), LINEARBLEND)); + SEGMENT.setPixelColorXY(i, beatsin8_t(SEGMENT.speed/8, 0, rows-1, 0, i*4 ), ColorFromPalette(SEGPALETTE, i*5+strip.now/17, beatsin8_t(5, 55, 255, 0, i*10), LINEARBLEND)); + SEGMENT.setPixelColorXY(i, beatsin8_t(SEGMENT.speed/8, 0, rows-1, 0, i*4+128), ColorFromPalette(SEGPALETTE, i*5+128+strip.now/17, beatsin8_t(5, 55, 255, 0, i*10+128), LINEARBLEND)); } - SEGMENT.blur(SEGMENT.intensity>>3); + SEGMENT.blur(SEGMENT.intensity / (8 - (SEGMENT.check1 * 2)), SEGMENT.check1); return FRAMETIME; } // mode_2Ddna() -static const char _data_FX_MODE_2DDNA[] PROGMEM = "DNA@Scroll speed,Blur;;!;2"; - +static const char _data_FX_MODE_2DDNA[] PROGMEM = "DNA@Scroll speed,Blur,,,,Smear;;!;2;ix=0"; ///////////////////////// // 2D DNA Spiral // @@ -4981,31 +4994,33 @@ 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 = SEG_W; + const int rows = SEG_H; 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_t(speeds, 0, cols - 1, 0, i * freq) + beatsin8_t(speeds - 7, 0, cols - 1, 0, i * freq + 128); + int x1 = beatsin8_t(speeds, 0, cols - 1, 0, 128 + i * freq) + beatsin8_t(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; + bool positive = (x1 >= x); // direction of drawing 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); + unsigned dx = positive? (x + k-1) : (x - k+1); // behaves the same as "lerp8by8" but does not create holes //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); @@ -5014,10 +5029,11 @@ uint16_t mode_2DDNASpiral() { // By: ldirko https://editor.soulma SEGMENT.setPixelColorXY(x1, i, WHITE); } } + SEGMENT.blur(((uint16_t)SEGMENT.custom1 * 3) / (6 + SEGMENT.check1), SEGMENT.check1); return FRAMETIME; } // mode_2DDNASpiral() -static const char _data_FX_MODE_2DDNASPIRAL[] PROGMEM = "DNA Spiral@Scroll speed,Y frequency;;!;2"; +static const char _data_FX_MODE_2DDNASPIRAL[] PROGMEM = "DNA Spiral@Scroll speed,Y frequency,Blur,,,Smear;;!;2;c1=0"; ///////////////////////// @@ -5026,24 +5042,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 = SEG_W; + const int rows = SEG_H; + + 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); + SEGMENT.blur(SEGMENT.intensity>>(3 - SEGMENT.check2), SEGMENT.check2); return FRAMETIME; } // mode_2DDrift() -static const char _data_FX_MODE_2DDRIFT[] PROGMEM = "Drift@Rotation speed,Blur amount;;!;2"; +static const char _data_FX_MODE_2DDRIFT[] PROGMEM = "Drift@Rotation speed,Blur,,,,Twin,Smear;;!;2;ix=0"; ////////////////////////// @@ -5052,32 +5072,28 @@ 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 = SEG_W; + const int rows = SEG_H; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); } - uint16_t xscale = SEGMENT.intensity*4; - uint32_t yscale = SEGMENT.speed*8; - uint8_t 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); + unsigned xscale = SEGMENT.intensity*4; + unsigned yscale = SEGMENT.speed*8; + unsigned indexx = 0; + CRGBPalette16 pal = SEGMENT.check1 ? SEGPALETTE : SEGMENT.loadPalette(pal, 35); 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/11, 225U), i*255/rows, LINEARBLEND)); // With that value, look up the 8 bit colour palette value and assign it to the current LED. } // for i } // for j return FRAMETIME; } // mode_2Dfirenoise() -static const char _data_FX_MODE_2DFIRENOISE[] PROGMEM = "Firenoise@X scale,Y scale;;!;2"; +static const char _data_FX_MODE_2DFIRENOISE[] PROGMEM = "Firenoise@X scale,Y scale,,,,Palette;;!;2;pal=66"; ////////////////////////////// @@ -5086,20 +5102,19 @@ static const char _data_FX_MODE_2DFIRENOISE[] PROGMEM = "Firenoise@X scale,Y sca uint16_t mode_2DFrizzles(void) { // By: Stepko https://editor.soulmatelights.com/gallery/640-color-frizzles , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; - SEGMENT.fadeToBlackBy(16); + SEGMENT.fadeToBlackBy(16 + SEGMENT.check1 * 10); for (size_t i = 8; i > 0; i--) { - SEGMENT.addPixelColorXY(beatsin8(SEGMENT.speed/8 + i, 0, cols - 1), - beatsin8(SEGMENT.intensity/8 - i, 0, rows - 1), - ColorFromPalette(SEGPALETTE, beatsin8(12, 0, 255), 255, LINEARBLEND)); + SEGMENT.addPixelColorXY(beatsin8_t(SEGMENT.speed/8 + i, 0, cols - 1), + beatsin8_t(SEGMENT.intensity/8 - i, 0, rows - 1), + ColorFromPalette(SEGPALETTE, beatsin8_t(12, 0, 255), 255, LINEARBLEND)); } - SEGMENT.blur(SEGMENT.custom1>>3); - + SEGMENT.blur(SEGMENT.custom1 >> (3 + SEGMENT.check1), SEGMENT.check1); return FRAMETIME; } // mode_2DFrizzles() -static const char _data_FX_MODE_2DFRIZZLES[] PROGMEM = "Frizzles@X frequency,Y frequency,Blur;;!;2"; +static const char _data_FX_MODE_2DFRIZZLES[] PROGMEM = "Frizzles@X frequency,Y frequency,Blur,,,Smear;;!;2"; /////////////////////////////////////////// @@ -5113,10 +5128,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 = SEG_W; + const int rows = SEG_H; + 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); @@ -5127,15 +5142,14 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: if (SEGENV.call == 0 || strip.now - SEGMENT.step > 3000) { SEGENV.step = strip.now; SEGENV.aux0 = 0; - //random16_set_seed(millis()>>2); //seed the random generator //give the leds random state and colors (based on intensity, colors from palette or all posible colors are chosen) for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) { - uint8_t state = random8()%2; + unsigned state = hw_random8()%2; if (state == 0) SEGMENT.setPixelColorXY(x,y, backgroundColor); else - SEGMENT.setPixelColorXY(x,y, SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 255)); + SEGMENT.setPixelColorXY(x,y, SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 255)); } for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) prevLeds[XY(x,y)] = CRGB::Black; @@ -5160,11 +5174,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++; @@ -5190,9 +5204,9 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: for (int i=0; i<9 && colorsCount[i].count != 0; i++) if (colorsCount[i].count > dominantColorCount.count) dominantColorCount = colorsCount[i]; // assign the dominant color w/ a bit of randomness to avoid "gliders" - if (dominantColorCount.count > 0 && random8(128)) SEGMENT.setPixelColorXY(x,y, dominantColorCount.color); - } else if ((col == bgc) && (neighbors == 2) && !random8(128)) { // Mutation - SEGMENT.setPixelColorXY(x,y, SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 255)); + if (dominantColorCount.count > 0 && hw_random8(128)) SEGMENT.setPixelColorXY(x,y, dominantColorCount.color); + } else if ((col == bgc) && (neighbors == 2) && !hw_random8(128)) { // Mutation + SEGMENT.setPixelColorXY(x,y, SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 255)); } // else do nothing! } //x,y @@ -5219,13 +5233,13 @@ 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 = SEG_W; + const int rows = SEG_H; const uint32_t a = strip.now / ((SEGMENT.custom3>>1)+1); for (int x = 0; x < cols; x++) { for (int y = 0; y < rows; y++) { - SEGMENT.setPixelColorXY(x, y, SEGMENT.color_from_palette(sin8(cos8(x * SEGMENT.speed/16 + a / 3) + sin8(y * SEGMENT.intensity/16 + a / 4) + a), false, PALETTE_SOLID_WRAP, 0)); + SEGMENT.setPixelColorXY(x, y, SEGMENT.color_from_palette(sin8_t(cos8_t(x * SEGMENT.speed/16 + a / 3) + sin8_t(y * SEGMENT.intensity/16 + a / 4) + a), false, PALETTE_SOLID_WRAP, 0)); } } @@ -5251,8 +5265,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 = SEG_W; + const int rows = SEG_H; if (!SEGENV.allocateData(sizeof(julia))) return mode_static(); Julia* julias = reinterpret_cast(SEGENV.data); @@ -5301,8 +5315,8 @@ uint16_t mode_2DJulia(void) { // An animated Julia set reAl = -0.94299f; // PixelBlaze example imAg = 0.3162f; - reAl += sin_t((float)strip.now/305.f)/20.f; - imAg += sin_t((float)strip.now/405.f)/20.f; + reAl += (float)sin16_t(strip.now * 34) / 655340.f; + imAg += (float)sin16_t(strip.now * 26) / 655340.f; dx = (xmax - xmin) / (cols); // Scale the delta x and y values to our matrix size. dy = (ymax - ymin) / (rows); @@ -5344,11 +5358,12 @@ uint16_t mode_2DJulia(void) { // An animated Julia set } y += dy; } -// SEGMENT.blur(64); + if(SEGMENT.check1) + SEGMENT.blur(100, true); return FRAMETIME; } // mode_2DJulia() -static const char _data_FX_MODE_2DJULIA[] PROGMEM = "Julia@,Max iterations per pixel,X center,Y center,Area size;!;!;2;ix=24,c1=128,c2=128,c3=16"; +static const char _data_FX_MODE_2DJULIA[] PROGMEM = "Julia@,Max iterations per pixel,X center,Y center,Area size, Blur;!;!;2;ix=24,c1=128,c2=128,c3=16"; ////////////////////////////// @@ -5357,26 +5372,27 @@ 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 = SEG_W; + const int rows = SEG_H; SEGMENT.fadeToBlackBy(SEGMENT.intensity); uint_fast16_t phase = (strip.now * (1 + SEGENV.custom3)) /32; // allow user to control rotation speed //for (int i=0; i < 4*(cols+rows); i ++) { for (int i=0; i < 256; i ++) { - //float xlocn = float(sin8(now/4+i*(SEGMENT.speed>>5))) / 255.0f; - //float ylocn = float(cos8(now/4+i*2)) / 255.0f; - uint_fast8_t xlocn = sin8(phase/2 + (i*SEGMENT.speed)/32); - uint_fast8_t ylocn = cos8(phase/2 + i*2); + //float xlocn = float(sin8_t(now/4+i*(SEGMENT.speed>>5))) / 255.0f; + //float ylocn = float(cos8_t(now/4+i*2)) / 255.0f; + uint_fast8_t xlocn = sin8_t(phase/2 + (i*SEGMENT.speed)/32); + uint_fast8_t ylocn = cos8_t(phase/2 + i*2); xlocn = (cols < 2) ? 1 : (map(2*xlocn, 0,511, 0,2*(cols-1)) +1) /2; // softhack007: "(2* ..... +1) /2" for proper rounding ylocn = (rows < 2) ? 1 : (map(2*ylocn, 0,511, 0,2*(rows-1)) +1) /2; // "rows > 1" is needed to avoid div/0 in map() SEGMENT.setPixelColorXY((uint8_t)xlocn, (uint8_t)ylocn, SEGMENT.color_from_palette(strip.now/100+i, false, PALETTE_SOLID_WRAP, 0)); } + SEGMENT.blur(SEGMENT.custom1 >> (1 + SEGMENT.check1 * 3), SEGMENT.check1); return FRAMETIME; } // mode_2DLissajous() -static const char _data_FX_MODE_2DLISSAJOUS[] PROGMEM = "Lissajous@X frequency,Fade rate,,,Speed;!;!;2;c3=15"; +static const char _data_FX_MODE_2DLISSAJOUS[] PROGMEM = "Lissajous@X frequency,Fade rate,Blur,,Speed,Smear;!;!;2;c1=0"; /////////////////////// @@ -5385,10 +5401,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 = SEG_W; + const int rows = SEG_H; - 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) { @@ -5397,7 +5413,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; @@ -5434,8 +5450,8 @@ uint16_t mode_2Dmatrix(void) { // Matrix2D. By Jeremy Williams. } // spawn new falling code - if (random8() <= SEGMENT.intensity || emptyScreen) { - uint8_t spawnX = random8(cols); + if (hw_random8() <= SEGMENT.intensity || emptyScreen) { + uint8_t spawnX = hw_random8(cols); SEGMENT.setPixelColorXY(spawnX, 0, spawnColor); // update hint for next run unsigned index = XY(spawnX, 0) >> 3; @@ -5455,29 +5471,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 = SEG_W; + const int rows = SEG_H; 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_t(23 * speed, 0, cols-1); + int y1 = beatsin8_t(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); @@ -5488,7 +5504,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) { @@ -5514,10 +5530,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 = SEG_W; + const int rows = SEG_H; - 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++) { @@ -5537,21 +5553,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 = SEG_W; + const int rows = SEG_H; SEGMENT.fadeToBlackBy(SEGMENT.custom1>>2); uint_fast32_t t = (strip.now * 8) / (256 - SEGMENT.speed); // optimized to avoid float for (int i = 0; i < cols; i++) { - 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)) || @@ -5571,16 +5587,12 @@ static const char _data_FX_MODE_2DPLASMABALL[] PROGMEM = "Plasma Ball@Speed,,Fad //////////////////////////////// // 2D Polar Lights // //////////////////////////////// -//static float fmap(const float x, const float in_min, const float in_max, const float out_min, const float out_max) { -// return (out_max - out_min) * (x - in_min) / (in_max - in_min) + out_min; -//} -uint16_t mode_2DPolarLights(void) { // By: Kostyantyn Matviyevskyy https://editor.soulmatelights.com/gallery/762-polar-lights , Modified by: Andrew Tuline + +uint16_t mode_2DPolarLights(void) { // By: Kostyantyn Matviyevskyy https://editor.soulmatelights.com/gallery/762-polar-lights , Modified by: Andrew Tuline & @dedehai (palette support) if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); - - CRGBPalette16 auroraPalette = {0x000000, 0x003300, 0x006600, 0x009900, 0x00cc00, 0x00ff00, 0x33ff00, 0x66ff00, 0x99ff00, 0xccff00, 0xffff00, 0xffcc00, 0xff9900, 0xff6600, 0xff3300, 0xff0000}; + const int cols = SEG_W; + const int rows = SEG_H; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); @@ -5588,38 +5600,23 @@ 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); -/* - if (SEGENV.aux1 != SEGMENT.custom1/12) { // Hacky palette rotation. We need that black. - SEGENV.aux1 = SEGMENT.custom1/12; - for (int i = 0; i < 16; i++) { - long ilk; - ilk = (long)currentPalette[i].r << 16; - ilk += (long)currentPalette[i].g << 8; - ilk += (long)currentPalette[i].b; - ilk = (ilk << SEGENV.aux1) | (ilk >> (24 - SEGENV.aux1)); - currentPalette[i].r = ilk >> 16; - currentPalette[i].g = ilk >> 8; - currentPalette[i].b = ilk; - } - } -*/ - uint16_t _scale = map(SEGMENT.intensity, 0, 255, 30, adjScale); - byte _speed = map(SEGMENT.speed, 0, 255, 128, 16); + unsigned adjScale = map(cols, 8, 64, 310, 63); + unsigned _scale = map(SEGMENT.intensity, 0, 255, 30, adjScale); + int _speed = map(SEGMENT.speed, 0, 255, 128, 16); for (int x = 0; x < cols; x++) { for (int y = 0; y < rows; y++) { SEGENV.step++; - SEGMENT.setPixelColorXY(x, y, ColorFromPalette(auroraPalette, - qsub8( - inoise8((SEGENV.step%2) + x * _scale, y * 16 + SEGENV.step % 16, SEGENV.step / _speed), - fabsf((float)rows / 2.0f - (float)y) * adjustHeight))); + uint8_t palindex = qsub8(inoise8((SEGENV.step%2) + x * _scale, y * 16 + SEGENV.step % 16, SEGENV.step / _speed), fabsf((float)rows / 2.0f - (float)y) * adjustHeight); + uint8_t palbrightness = palindex; + if(SEGMENT.check1) palindex = 255 - palindex; //flip palette + SEGMENT.setPixelColorXY(x, y, SEGMENT.color_from_palette(palindex, false, false, 255, palbrightness)); } } return FRAMETIME; } // mode_2DPolarLights() -static const char _data_FX_MODE_2DPOLARLIGHTS[] PROGMEM = "Polar Lights@!,Scale;;;2"; +static const char _data_FX_MODE_2DPOLARLIGHTS[] PROGMEM = "Polar Lights@!,Scale,,,,Flip Palette;;!;2;pal=71"; ///////////////////////// @@ -5628,16 +5625,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 = SEG_W; + const int rows = SEG_H; 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_t(a * 5) + sin8_t(a * 4) + sin8_t(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() @@ -5650,27 +5647,27 @@ 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 = SEG_W; + const int rows = SEG_H; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); } - SEGMENT.fadeToBlackBy(SEGMENT.custom1>>3); + SEGMENT.fadeToBlackBy((SEGMENT.custom1>>3) + (SEGMENT.check1 * 24)); byte t1 = strip.now / (257 - SEGMENT.speed); // 20; - byte t2 = sin8(t1) / 4 * 2; + byte t2 = sin8_t(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_t(t1 + i * SEGMENT.intensity/8)*(cols-1)/255; // max index now 255x15/255=15! + int y = sin8_t(t2 + i * SEGMENT.intensity/8)*(rows-1)/255; // max index now 255x15/255=15! SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, i * 255 / 13, 255, LINEARBLEND)); } - SEGMENT.blur(SEGMENT.custom2>>3); + SEGMENT.blur(SEGMENT.custom2 >> (3 + SEGMENT.check1), SEGMENT.check1); return FRAMETIME; } // mode_2DSindots() -static const char _data_FX_MODE_2DSINDOTS[] PROGMEM = "Sindots@!,Dot distance,Fade rate,Blur;;!;2"; +static const char _data_FX_MODE_2DSINDOTS[] PROGMEM = "Sindots@!,Dot distance,Fade rate,Blur,,Smear;;!;2;"; ////////////////////////////// @@ -5681,22 +5678,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 = SEG_W; + const int rows = SEG_H; const uint8_t kBorderWidth = 2; - SEGMENT.fadeToBlackBy(24); - uint8_t blurAmount = SEGMENT.custom3>>1; // reduced resolution slider - SEGMENT.blur(blurAmount); + SEGMENT.fadeToBlackBy(1 + SEGMENT.intensity / 5); + 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_t(19, kBorderWidth, cols-kBorderWidth); + int j = beatsin8_t(22, kBorderWidth, cols-kBorderWidth); + int k = beatsin8_t(17, kBorderWidth, cols-kBorderWidth); + int m = beatsin8_t(18, kBorderWidth, rows-kBorderWidth); + int n = beatsin8_t(15, kBorderWidth, rows-kBorderWidth); + int p = beatsin8_t(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)); @@ -5704,7 +5700,7 @@ uint16_t mode_2Dsquaredswirl(void) { // By: Mark Kriegsman. https://g return FRAMETIME; } // mode_2Dsquaredswirl() -static const char _data_FX_MODE_2DSQUAREDSWIRL[] PROGMEM = "Squared Swirl@,,,,Blur;;!;2"; +static const char _data_FX_MODE_2DSQUAREDSWIRL[] PROGMEM = "Squared Swirl@,Fade,,,Blur;;!;2"; ////////////////////////////// @@ -5713,8 +5709,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 = SEG_W; + const int rows = SEG_H; if (!SEGENV.allocateData(sizeof(byte)*(cols+2)*(rows+2))) return mode_static(); //allocation failed byte *bump = reinterpret_cast(SEGENV.data); @@ -5724,7 +5720,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++) { @@ -5734,16 +5730,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; @@ -5763,8 +5759,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 = SEG_W; + const int rows = SEG_H; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); @@ -5772,19 +5768,19 @@ uint16_t mode_2Dtartan(void) { // By: Elliott Kember https://editor.so uint8_t hue, bri; size_t intensity; - int offsetX = beatsin16(3, -360, 360); - int offsetY = beatsin16(2, -360, 360); + int offsetX = beatsin16_t(3, -360, 360); + int offsetY = beatsin16_t(2, -360, 360); int sharpness = SEGMENT.custom3 / 8; // 0-3 for (int x = 0; x < cols; x++) { for (int y = 0; y < rows; y++) { - hue = x * beatsin16(10, 1, 10) + offsetY; - intensity = bri = sin8(x * SEGMENT.speed/2 + offsetX); + hue = x * beatsin16_t(10, 1, 10) + offsetY; + intensity = bri = sin8_t(x * SEGMENT.speed/2 + offsetX); for (int i=0; i>= 8*sharpness; SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, hue, intensity, LINEARBLEND)); hue = y * 3 + offsetX; - intensity = bri = sin8(y * SEGMENT.intensity/2 + offsetY); + intensity = bri = sin8_t(y * SEGMENT.intensity/2 + offsetY); for (int i=0; i>= 8*sharpness; SEGMENT.addPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, hue, intensity, LINEARBLEND)); @@ -5802,26 +5798,26 @@ 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 = SEG_W; + const int rows = SEG_H; uint32_t tb = strip.now >> 12; // every ~4s if (tb > SEGENV.step) { - int8_t dir = ++SEGENV.aux0; - dir += (int)random8(3)-1; + int dir = ++SEGENV.aux0; + dir += (int)hw_random8(3)-1; if (dir > 7) SEGENV.aux0 = 0; else if (dir < 0) SEGENV.aux0 = 7; else SEGENV.aux0 = dir; - SEGENV.step = tb + random8(4); + SEGENV.step = tb + hw_random8(4); } SEGMENT.fadeToBlackBy(map(SEGMENT.speed, 0, 255, 248, 16)); 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); - CRGB color = ColorFromPalette(SEGPALETTE, beatsin8(12 + i, 0, 255), 255); + int x = beatsin8_t(12 + i, 2, cols - 3); + int y = beatsin8_t(15 + i, 2, rows - 3); + uint32_t color = ColorFromPalette(SEGPALETTE, beatsin8_t(12 + i, 0, 255), 255); SEGMENT.addPixelColorXY(x, y, color); if (cols > 24 || rows > 24) { SEGMENT.addPixelColorXY(x+1, y, color); @@ -5830,23 +5826,23 @@ uint16_t mode_2Dspaceships(void) { //// Space ships by stepko (c)05.02.21 [ht SEGMENT.addPixelColorXY(x, y-1, color); } } - SEGMENT.blur(SEGMENT.intensity>>3); + SEGMENT.blur(SEGMENT.intensity >> 3, SEGMENT.check1); return FRAMETIME; } -static const char _data_FX_MODE_2DSPACESHIPS[] PROGMEM = "Spaceships@!,Blur;;!;2"; +static const char _data_FX_MODE_2DSPACESHIPS[] PROGMEM = "Spaceships@!,Blur,,,,Smear;;!;2"; ///////////////////////// // 2D Crazy Bees // ///////////////////////// -//// Crazy bees by stepko (c)12.02.21 [https://editor.soulmatelights.com/gallery/651-crazy-bees], adapted by Blaz Kristan (AKA blazoncek) +//// Crazy bees by stepko (c)12.02.21 [https://editor.soulmatelights.com/gallery/651-crazy-bees], adapted by Blaz Kristan (AKA blazoncek), improved by @dedehai #define MAX_BEES 5 uint16_t mode_2Dcrazybees(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; byte n = MIN(MAX_BEES, (rows * cols) / 256 + 1); @@ -5880,17 +5876,17 @@ uint16_t mode_2Dcrazybees(void) { if (strip.now > SEGENV.step) { SEGENV.step = strip.now + (FRAMETIME * 16 / ((SEGMENT.speed>>4)+1)); - - SEGMENT.fadeToBlackBy(32); - + SEGMENT.fadeToBlackBy(32 + ((SEGMENT.check1*SEGMENT.intensity) / 25)); + SEGMENT.blur(SEGMENT.intensity / (2 + SEGMENT.check1 * 9), SEGMENT.check1); for (size_t i = 0; i < n; i++) { - SEGMENT.addPixelColorXY(bee[i].aimX + 1, bee[i].aimY, CHSV(bee[i].hue, 255, 255)); - SEGMENT.addPixelColorXY(bee[i].aimX, bee[i].aimY + 1, CHSV(bee[i].hue, 255, 255)); - SEGMENT.addPixelColorXY(bee[i].aimX - 1, bee[i].aimY, CHSV(bee[i].hue, 255, 255)); - SEGMENT.addPixelColorXY(bee[i].aimX, bee[i].aimY - 1, CHSV(bee[i].hue, 255, 255)); + uint32_t flowerCcolor = SEGMENT.color_from_palette(bee[i].hue, false, true, 255); + SEGMENT.addPixelColorXY(bee[i].aimX + 1, bee[i].aimY, flowerCcolor); + SEGMENT.addPixelColorXY(bee[i].aimX, bee[i].aimY + 1, flowerCcolor); + SEGMENT.addPixelColorXY(bee[i].aimX - 1, bee[i].aimY, flowerCcolor); + SEGMENT.addPixelColorXY(bee[i].aimX, bee[i].aimY - 1, flowerCcolor); if (bee[i].posX != bee[i].aimX || bee[i].posY != bee[i].aimY) { SEGMENT.setPixelColorXY(bee[i].posX, bee[i].posY, CRGB(CHSV(bee[i].hue, 60, 255))); - 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; @@ -5903,11 +5899,11 @@ uint16_t mode_2Dcrazybees(void) { bee[i].aimed(cols, rows); } } - SEGMENT.blur(SEGMENT.intensity>>4); } return FRAMETIME; } -static const char _data_FX_MODE_2DCRAZYBEES[] PROGMEM = "Crazy Bees@!,Blur;;;2"; +static const char _data_FX_MODE_2DCRAZYBEES[] PROGMEM = "Crazy Bees@!,Blur,,,,Smear;;!;2;pal=11,ix=0"; +#undef MAX_BEES ///////////////////////// @@ -5918,8 +5914,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 = SEG_W; + const int rows = SEG_H; typedef struct Lighter { int16_t gPosX; @@ -5942,9 +5938,8 @@ uint16_t mode_2Dghostrider(void) { if (SEGENV.aux0 != cols || SEGENV.aux1 != rows) { SEGENV.aux0 = cols; SEGENV.aux1 = rows; - //random16_set_seed(strip.now); - lighter->angleSpeed = random8(0,20) - 10; - lighter->gAngle = random16(); + lighter->angleSpeed = hw_random8(0,20) - 10; + lighter->gAngle = hw_random16(); lighter->Vspeed = 5; lighter->gPosX = (cols/2) * 10; lighter->gPosY = (rows/2) * 10; @@ -5972,7 +5967,7 @@ uint16_t mode_2Dghostrider(void) { if (lighter->gPosY < 0) lighter->gPosY = (rows - 1) * 10; if (lighter->gPosY > (rows - 1) * 10) lighter->gPosY = 0; for (size_t i = 0; i < maxLighters; i++) { - lighter->time[i] += random8(5, 20); + lighter->time[i] += hw_random8(5, 20); if (lighter->time[i] >= 255 || (lighter->lightersPosX[i] <= 0) || (lighter->lightersPosX[i] >= (cols - 1) * 10) || @@ -5983,7 +5978,7 @@ uint16_t mode_2Dghostrider(void) { if (lighter->reg[i]) { lighter->lightersPosY[i] = lighter->gPosY; lighter->lightersPosX[i] = lighter->gPosX; - lighter->Angle[i] = lighter->gAngle + ((int)random8(20) - 10); + lighter->Angle[i] = lighter->gAngle + ((int)hw_random8(20) - 10); lighter->time[i] = 0; lighter->reg[i] = false; } else { @@ -5998,7 +5993,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 // @@ -6008,8 +6003,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 = SEG_W; + const int rows = SEG_H; typedef struct Blob { float x[MAX_BLOBS], y[MAX_BLOBS]; @@ -6019,7 +6014,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); @@ -6029,12 +6024,12 @@ uint16_t mode_2Dfloatingblobs(void) { SEGENV.aux1 = rows; //SEGMENT.fill(BLACK); for (size_t i = 0; i < MAX_BLOBS; i++) { - blob->r[i] = random8(1, cols>8 ? (cols/4) : 2); - blob->sX[i] = (float) random8(3, cols) / (float)(256 - SEGMENT.speed); // speed x - blob->sY[i] = (float) random8(3, rows) / (float)(256 - SEGMENT.speed); // speed y - blob->x[i] = random8(0, cols-1); - blob->y[i] = random8(0, rows-1); - blob->color[i] = random8(); + blob->r[i] = hw_random8(1, cols>8 ? (cols/4) : 2); + blob->sX[i] = (float) hw_random8(3, cols) / (float)(256 - SEGMENT.speed); // speed x + blob->sY[i] = (float) hw_random8(3, rows) / (float)(256 - SEGMENT.speed); // speed y + blob->x[i] = hw_random8(0, cols-1); + blob->y[i] = hw_random8(0, rows-1); + blob->color[i] = hw_random8(); blob->grow[i] = (blob->r[i] < 1.f); if (blob->sX[i] == 0) blob->sX[i] = 1; if (blob->sY[i] == 0) blob->sY[i] = 1; @@ -6061,8 +6056,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)); @@ -6073,19 +6068,19 @@ uint16_t mode_2Dfloatingblobs(void) { else blob->y[i] += blob->sY[i]; // bounce x if (blob->x[i] < 0.01f) { - blob->sX[i] = (float)random8(3, cols) / (256 - SEGMENT.speed); + blob->sX[i] = (float)hw_random8(3, cols) / (256 - SEGMENT.speed); blob->x[i] = 0.01f; } else if (blob->x[i] > (float)cols - 1.01f) { - blob->sX[i] = (float)random8(3, cols) / (256 - SEGMENT.speed); + blob->sX[i] = (float)hw_random8(3, cols) / (256 - SEGMENT.speed); blob->sX[i] = -blob->sX[i]; blob->x[i] = (float)cols - 1.01f; } // bounce y if (blob->y[i] < 0.01f) { - blob->sY[i] = (float)random8(3, rows) / (256 - SEGMENT.speed); + blob->sY[i] = (float)hw_random8(3, rows) / (256 - SEGMENT.speed); blob->y[i] = 0.01f; } else if (blob->y[i] > (float)rows - 1.01f) { - blob->sY[i] = (float)random8(3, rows) / (256 - SEGMENT.speed); + blob->sY[i] = (float)hw_random8(3, rows) / (256 - SEGMENT.speed); blob->sY[i] = -blob->sY[i]; blob->y[i] = (float)rows - 1.01f; } @@ -6096,8 +6091,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 //////////////////////////// @@ -6106,11 +6101,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 = SEG_W; + const int rows = SEG_H; - 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; @@ -6182,17 +6177,21 @@ uint16_t mode_2Dscrollingtext(void) { } if (!SEGMENT.check2) SEGMENT.fade_out(255 - (SEGMENT.custom1>>4)); // trail + bool usePaletteGradient = false; + uint32_t col1 = SEGMENT.color_from_palette(SEGENV.aux1, false, PALETTE_SOLID_WRAP, 0); + uint32_t col2 = BLACK; + if (SEGMENT.check1) { // use gradient + if(SEGMENT.palette == 0) { // use colors for gradient + col1 = SEGCOLOR(0); + col2 = SEGCOLOR(2); + } + else usePaletteGradient = true; + } for (int i = 0; i < numberOfLetters; i++) { int xoffset = int(cols) - int(SEGENV.aux0) + rotLW*i; if (xoffset + rotLW < 0) continue; // don't draw characters off-screen - uint32_t col1 = SEGMENT.color_from_palette(SEGENV.aux1, false, PALETTE_SOLID_WRAP, 0); - uint32_t col2 = BLACK; - if (SEGMENT.check1 && SEGMENT.palette == 0) { - col1 = SEGCOLOR(0); - col2 = SEGCOLOR(2); - } - SEGMENT.drawCharacter(text[i], xoffset, yoffset, letterWidth, letterHeight, col1, col2, map(SEGMENT.custom3, 0, 31, -2, 2)); + SEGMENT.drawCharacter(text[i], xoffset, yoffset, letterWidth, letterHeight, col1, col2, map(SEGMENT.custom3, 0, 31, -2, 2), usePaletteGradient); } return FRAMETIME; @@ -6203,12 +6202,12 @@ static const char _data_FX_MODE_2DSCROLLTEXT[] PROGMEM = "Scrolling Text@!,Y Off //////////////////////////// // 2D Drift Rose // //////////////////////////// -//// Drift Rose by stepko (c)2021 [https://editor.soulmatelights.com/gallery/1369-drift-rose-pattern], adapted by Blaz Kristan (AKA blazoncek) +//// Drift Rose by stepko (c)2021 [https://editor.soulmatelights.com/gallery/1369-drift-rose-pattern], adapted by Blaz Kristan (AKA blazoncek) improved by @dedehai uint16_t mode_2Ddriftrose(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEG_W; + const int rows = SEG_H; const float CX = (cols-cols%2)/2.f - .5f; const float CY = (rows-rows%2)/2.f - .5f; @@ -6216,15 +6215,17 @@ 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; - SEGMENT.wu_pixel(x, y, CHSV(i * 10, 255, 255)); + float angle = radians(i * 10); + uint32_t x = (CX + (sin_t(angle) * (beatsin8_t(i, 0, L*2)-L))) * 255.f; + uint32_t y = (CY + (cos_t(angle) * (beatsin8_t(i, 0, L*2)-L))) * 255.f; + if(SEGMENT.palette == 0) SEGMENT.wu_pixel(x, y, CHSV(i * 10, 255, 255)); + else SEGMENT.wu_pixel(x, y, ColorFromPalette(SEGPALETTE, i * 10)); } - SEGMENT.blur((SEGMENT.intensity>>4)+1); + SEGMENT.blur(SEGMENT.intensity >> 4, SEGMENT.check1); return FRAMETIME; } -static const char _data_FX_MODE_2DDRIFTROSE[] PROGMEM = "Drift Rose@Fade,Blur;;;2"; +static const char _data_FX_MODE_2DDRIFTROSE[] PROGMEM = "Drift Rose@Fade,Blur,,,,Smear;;!;2;pal=11"; ///////////////////////////// // 2D PLASMA ROTOZOOMER // @@ -6233,15 +6234,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 = SEG_W; + const int rows = SEG_H; - 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++) { @@ -6262,10 +6263,11 @@ 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 + if(*a < -6283.18530718f) *a += 6283.18530718f; // 1000*2*PI, protect sin/cos from very large input float values (will give wrong results) return FRAMETIME; } @@ -6279,63 +6281,19 @@ static const char _data_FX_MODE_2DPLASMAROTOZOOM[] PROGMEM = "Rotozoomer@!,Scale /////////////////////////////////////////////////////////////////////////////// -/* use the following code to pass AudioReactive usermod variables to effect - - uint8_t *binNum = (uint8_t*)&SEGENV.aux1, *maxVol = (uint8_t*)(&SEGENV.aux1+1); // just in case assignment - bool samplePeak = false; - float FFT_MajorPeak = 1.0; - uint8_t *fftResult = nullptr; - float *fftBin = nullptr; - um_data_t *um_data; - if (usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - volumeSmth = *(float*) um_data->u_data[0]; - volumeRaw = *(float*) um_data->u_data[1]; - fftResult = (uint8_t*) um_data->u_data[2]; - samplePeak = *(uint8_t*) um_data->u_data[3]; - FFT_MajorPeak = *(float*) um_data->u_data[4]; - my_magnitude = *(float*) um_data->u_data[5]; - maxVol = (uint8_t*) um_data->u_data[6]; // requires UI element (SEGMENT.customX?), changes source element - binNum = (uint8_t*) um_data->u_data[7]; // requires UI element (SEGMENT.customX?), changes source element - fftBin = (float*) um_data->u_data[8]; - } else { - // add support for no audio data - um_data = simulateSound(SEGMENT.soundSim); - } -*/ - - -// a few constants needed for AudioReactive effects - -// for 22Khz sampling -#define MAX_FREQUENCY 11025 // sample frequency / 2 (as per Nyquist criterion) -#define MAX_FREQ_LOG10 4.04238f // log10(MAX_FREQUENCY) - -// for 20Khz sampling -//#define MAX_FREQUENCY 10240 -//#define MAX_FREQ_LOG10 4.0103f - -// for 10Khz sampling -//#define MAX_FREQUENCY 5120 -//#define MAX_FREQ_LOG10 3.71f - - ///////////////////////////////// // * Ripple Peak // ///////////////////////////////// uint16_t mode_ripplepeak(void) { // * Ripple peak. By Andrew Tuline. // This currently has no controls. - #define maxsteps 16 // Case statement wouldn't allow a variable. + #define MAXSTEPS 16 // Case statement wouldn't allow a variable. - 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]; @@ -6346,7 +6304,6 @@ uint16_t mode_ripplepeak(void) { // * Ripple peak. By Andrew Tuli // printUmData(); if (SEGENV.call == 0) { - SEGENV.aux0 = 255; SEGMENT.custom1 = *binNum; SEGMENT.custom2 = *maxVol * 2; } @@ -6365,29 +6322,29 @@ uint16_t mode_ripplepeak(void) { // * Ripple peak. By Andrew Tuli break; case 255: // Initialize ripple variables. - ripples[i].pos = random16(SEGLEN); + ripples[i].pos = hw_random16(SEGLEN); #ifdef ESP32 if (FFT_MajorPeak > 1) // log10(0) is "forbidden" (throws exception) ripples[i].color = (int)(log10f(FFT_MajorPeak)*128); else ripples[i].color = 0; #else - ripples[i].color = random8(); + ripples[i].color = hw_random8(); #endif ripples[i].state = 0; break; case 0: - SEGMENT.setPixelColor(ripples[i].pos, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(ripples[i].color, false, PALETTE_SOLID_WRAP, 0), SEGENV.aux0)); + SEGMENT.setPixelColor(ripples[i].pos, SEGMENT.color_from_palette(ripples[i].color, false, PALETTE_SOLID_WRAP, 0)); ripples[i].state++; break; - case maxsteps: // At the end of the ripples. 254 is an inactive mode. + case MAXSTEPS: // At the end of the ripples. 254 is an inactive mode. ripples[i].state = 254; break; default: // Middle of the ripples. - SEGMENT.setPixelColor((ripples[i].pos + ripples[i].state + SEGLEN) % SEGLEN, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(ripples[i].color, false, PALETTE_SOLID_WRAP, 0), SEGENV.aux0/ripples[i].state*2)); - SEGMENT.setPixelColor((ripples[i].pos - ripples[i].state + SEGLEN) % SEGLEN, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(ripples[i].color, false, PALETTE_SOLID_WRAP, 0), SEGENV.aux0/ripples[i].state*2)); + SEGMENT.setPixelColor((ripples[i].pos + ripples[i].state + SEGLEN) % SEGLEN, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(ripples[i].color, false, PALETTE_SOLID_WRAP, 0), uint8_t(2*255/ripples[i].state))); + SEGMENT.setPixelColor((ripples[i].pos - ripples[i].state + SEGLEN) % SEGLEN, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(ripples[i].color, false, PALETTE_SOLID_WRAP, 0), uint8_t(2*255/ripples[i].state))); ripples[i].state++; // Next step. break; } // switch step @@ -6406,8 +6363,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 = SEG_W; + const int rows = SEG_H; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); @@ -6417,18 +6374,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_t( 27*SEGMENT.speed/255, borderWidth, cols - borderWidth); + int j = beatsin8_t( 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); @@ -6449,46 +6402,37 @@ 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 = SEG_W; + const int rows = SEG_H; - 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 -// float version of map() -static float mapf(float x, float in_min, float in_max, float out_min, float out_max){ - return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; -} - // Gravity struct requited for GRAV* effects typedef struct Gravity { int topLED; @@ -6498,101 +6442,105 @@ typedef struct Gravity { /////////////////////// // * GRAVCENTER // /////////////////////// -uint16_t mode_gravcenter(void) { // Gravcenter. By Andrew Tuline. +// Gravcenter effects By Andrew Tuline. +// Gravcenter base function for Gravcenter (0), Gravcentric (1), Gravimeter (2), Gravfreq (3) (merged by @dedehai) + +uint16_t mode_gravcenter_base(unsigned mode) { if (SEGLEN == 1) return mode_static(); - const 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); - SEGMENT.fade_out(251); // 30% + if(mode == 1) SEGMENT.fade_out(253); // //Gravcentric + else if(mode == 2) SEGMENT.fade_out(249); // Gravimeter + else if(mode == 3) SEGMENT.fade_out(250); // Gravfreq + else SEGMENT.fade_out(251); // Gravcenter + float mySampleAvg; + int tempsamp; float segmentSampleAvg = volumeSmth * (float)SEGMENT.intensity / 255.0f; - segmentSampleAvg *= 0.125; // divide by 8, to compensate for later "sensitivity" upscaling - float mySampleAvg = mapf(segmentSampleAvg*2.0, 0, 32, 0, (float)SEGLEN/2.0f); // map to pixels available in current segment - uint16_t tempsamp = constrain(mySampleAvg, 0, SEGLEN/2); // Keep the sample from overflowing. + if(mode == 2) { //Gravimeter + segmentSampleAvg *= 0.25; // divide by 4, to compensate for later "sensitivity" upscaling + mySampleAvg = mapf(segmentSampleAvg*2.0, 0, 64, 0, (SEGLEN-1)); // map to pixels availeable in current segment + tempsamp = constrain(mySampleAvg,0,SEGLEN-1); // Keep the sample from overflowing. + } + else { // Gravcenter or Gravcentric or Gravfreq + segmentSampleAvg *= 0.125f; // divide by 8, to compensate for later "sensitivity" upscaling + mySampleAvg = mapf(segmentSampleAvg*2.0, 0.0f, 32.0f, 0.0f, (float)SEGLEN/2.0f); // map to pixels availeable in current segment + tempsamp = constrain(mySampleAvg, 0, SEGLEN/2); // Keep the sample from overflowing. + } + uint8_t gravity = 8 - SEGMENT.speed/32; - - for (int i=0; i= gravcen->topLED) gravcen->topLED = tempsamp-offset; + else if (gravcen->gravityCounter % gravity == 0) gravcen->topLED--; + + if(mode == 1) { //Gravcentric + for (int i=0; itopLED >= 0) { + SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, CRGB::Gray); + SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, CRGB::Gray); + } } - - if (tempsamp >= gravcen->topLED) - gravcen->topLED = tempsamp-1; - else if (gravcen->gravityCounter % gravity == 0) - gravcen->topLED--; - - if (gravcen->topLED >= 0) { - SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0)); - SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0)); + else if(mode == 2) { //Gravimeter + for (int i=0; itopLED > 0) { + SEGMENT.setPixelColor(gravcen->topLED, SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0)); + } } + else if(mode == 3) { //Gravfreq + for (int i=0; iu_data[4]; // used in mode 3: Gravfreq + if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; + uint8_t index = (log10f(FFT_MajorPeak) - (MAX_FREQ_LOG10 - 1.78f)) * 255; + SEGMENT.setPixelColor(i+SEGLEN/2, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); + SEGMENT.setPixelColor(SEGLEN/2-i-1, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); + } + if (gravcen->topLED >= 0) { + SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, CRGB::Gray); + SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, CRGB::Gray); + } + } + else { //Gravcenter + for (int i=0; itopLED >= 0) { + SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0)); + SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0)); + } + } gravcen->gravityCounter = (gravcen->gravityCounter + 1) % gravity; return FRAMETIME; -} // mode_gravcenter() -static const char _data_FX_MODE_GRAVCENTER[] PROGMEM = "Gravcenter@Rate of fall,Sensitivity;!,!;!;1v;ix=128,m12=2,si=0"; // Circle, Beatsin +} +uint16_t mode_gravcenter(void) { // Gravcenter. By Andrew Tuline. + return mode_gravcenter_base(0); +} +static const char _data_FX_MODE_GRAVCENTER[] PROGMEM = "Gravcenter@Rate of fall,Sensitivity;!,!;!;1v;ix=128,m12=2,si=0"; // Circle, Beatsin /////////////////////// // * GRAVCENTRIC // /////////////////////// -uint16_t mode_gravcentric(void) { // Gravcentric. By Andrew Tuline. - if (SEGLEN == 1) return mode_static(); - - uint16_t 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); - } - float volumeSmth = *(float*) um_data->u_data[0]; - - // printUmData(); - - //SEGMENT.fade_out(240); - //SEGMENT.fade_out(240); // twice? really? - SEGMENT.fade_out(253); // 50% - - float segmentSampleAvg = volumeSmth * (float)SEGMENT.intensity / 255.0f; - segmentSampleAvg *= 0.125f; // divide by 8, to compensate for later "sensitivity" upscaling - - float mySampleAvg = mapf(segmentSampleAvg*2.0, 0.0f, 32.0f, 0.0f, (float)SEGLEN/2.0f); // map to pixels availeable in current segment - int tempsamp = constrain(mySampleAvg, 0, SEGLEN/2); // Keep the sample from overflowing. - uint8_t gravity = 8 - SEGMENT.speed/32; - - for (int i=0; i= gravcen->topLED) - gravcen->topLED = tempsamp-1; - else if (gravcen->gravityCounter % gravity == 0) - gravcen->topLED--; - - if (gravcen->topLED >= 0) { - SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, CRGB::Gray); - SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, CRGB::Gray); - } - gravcen->gravityCounter = (gravcen->gravityCounter + 1) % gravity; - - return FRAMETIME; -} // mode_gravcentric() +uint16_t mode_gravcentric(void) { // Gravcentric. By Andrew Tuline. + return mode_gravcenter_base(1); +} static const char _data_FX_MODE_GRAVCENTRIC[] PROGMEM = "Gravcentric@Rate of fall,Sensitivity;!,!;!;1v;ix=128,m12=3,si=0"; // Corner, Beatsin @@ -6600,71 +6548,38 @@ static const char _data_FX_MODE_GRAVCENTRIC[] PROGMEM = "Gravcentric@Rate of fal // * GRAVIMETER // /////////////////////// uint16_t mode_gravimeter(void) { // Gravmeter. By Andrew Tuline. - if (SEGLEN == 1) return mode_static(); - - uint16_t 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); - } - float volumeSmth = *(float*) um_data->u_data[0]; - - //SEGMENT.fade_out(240); - SEGMENT.fade_out(249); // 25% - - float segmentSampleAvg = volumeSmth * (float)SEGMENT.intensity / 255.0; - segmentSampleAvg *= 0.25; // divide by 4, to compensate for later "sensitivity" upscaling - - float mySampleAvg = mapf(segmentSampleAvg*2.0, 0, 64, 0, (SEGLEN-1)); // map to pixels availeable in current segment - int tempsamp = constrain(mySampleAvg,0,SEGLEN-1); // Keep the sample from overflowing. - uint8_t gravity = 8 - SEGMENT.speed/32; - - for (int i=0; i= gravcen->topLED) - gravcen->topLED = tempsamp; - else if (gravcen->gravityCounter % gravity == 0) - gravcen->topLED--; - - if (gravcen->topLED > 0) { - SEGMENT.setPixelColor(gravcen->topLED, SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0)); - } - gravcen->gravityCounter = (gravcen->gravityCounter + 1) % gravity; - - return FRAMETIME; -} // mode_gravimeter() + return mode_gravcenter_base(2); +} static const char _data_FX_MODE_GRAVIMETER[] PROGMEM = "Gravimeter@Rate of fall,Sensitivity;!,!;!;1v;ix=128,m12=2,si=0"; // Circle, Beatsin +/////////////////////// +// ** Gravfreq // +/////////////////////// +uint16_t mode_gravfreq(void) { // Gravfreq. By Andrew Tuline. + return mode_gravcenter_base(3); +} +static const char _data_FX_MODE_GRAVFREQ[] PROGMEM = "Gravfreq@Rate of fall,Sensitivity;!,!;!;1f;ix=128,m12=0,si=0"; // Pixels, Beatsin + + ////////////////////// // * JUGGLES // ////////////////////// 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); + uint8_t 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); @@ -6689,8 +6600,8 @@ uint16_t mode_matripix(void) { // Matripix. By Andrew Tuline. if(SEGENV.aux0 != secondHand) { SEGENV.aux0 = secondHand; - int pixBri = volumeRaw * SEGMENT.intensity / 64; - for (int i = 0; i < SEGLEN-1; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // shift left + uint8_t pixBri = volumeRaw * SEGMENT.intensity / 64; + for (unsigned i = 0; i < SEGLEN-1; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // shift left SEGMENT.setPixelColor(SEGLEN-1, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0), pixBri)); } @@ -6706,11 +6617,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); @@ -6719,16 +6626,16 @@ uint16_t mode_midnoise(void) { // Midnoise. By Andrew Tuline. float tmpSound2 = volumeSmth * (float)SEGMENT.intensity / 256.0; // Too sensitive. tmpSound2 *= (float)SEGMENT.intensity / 128.0; // Reduce sensitivity/length. - int maxLen = mapf(tmpSound2, 0, 127, 0, SEGLEN/2); + unsigned maxLen = mapf(tmpSound2, 0, 127, 0, SEGLEN/2); if (maxLen >SEGLEN/2) maxLen = SEGLEN/2; - for (int i=(SEGLEN/2-maxLen); i<(SEGLEN/2+maxLen); i++) { + for (unsigned i=(SEGLEN/2-maxLen); i<(SEGLEN/2+maxLen); i++) { uint8_t index = inoise8(i*volumeSmth+SEGENV.aux0, SEGENV.aux1+i*volumeSmth); // Get a value from the noise function. I'm using both x and y axis. SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } - SEGENV.aux0=SEGENV.aux0+beatsin8(5,0,10); - SEGENV.aux1=SEGENV.aux1+beatsin8(4,0,10); + SEGENV.aux0=SEGENV.aux0+beatsin8_t(5,0,10); + SEGENV.aux1=SEGENV.aux1+beatsin8_t(4,0,10); return FRAMETIME; } // mode_midnoise() @@ -6745,27 +6652,22 @@ 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. + for (unsigned i = 0; i < SEGLEN; i++) { + unsigned index = inoise8(i*SEGMENT.speed/64,strip.now*SEGMENT.speed/64*SEGLEN/255); // X location is constant, but we move along the Y at the rate of millis(). By Andrew Tuline. index = (255 - i*256/SEGLEN) * index/(256-SEGMENT.intensity); // Now we need to scale index so that it gets blacker as we get close to one of the ends. // This is a simple y=mx+b equation that's been scaled. index/128 is another scaling. - CRGB color = ColorFromPalette(myPal, index, volumeSmth*2, LINEARBLEND); // Use the my own palette. - SEGMENT.setPixelColor(i, color); + SEGMENT.setPixelColor(i, ColorFromPalette(myPal, index, volumeSmth*2, LINEARBLEND)); // Use my own palette. } return FRAMETIME; } // 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 /////////////////////// @@ -6773,30 +6675,26 @@ 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); SEGMENT.fade_out(fadeRate); float tmpSound2 = volumeRaw * 2.0 * (float)SEGMENT.intensity / 255.0; - int maxLen = mapf(tmpSound2, 0, 255, 0, SEGLEN); // map to pixels availeable in current segment // Still a bit too sensitive. - if (maxLen <0) maxLen = 0; - if (maxLen >SEGLEN) maxLen = SEGLEN; + unsigned maxLen = mapf(tmpSound2, 0, 255, 0, SEGLEN); // map to pixels availeable in current segment // Still a bit too sensitive. + if (maxLen < 0) maxLen = 0; + if (maxLen > SEGLEN) maxLen = SEGLEN; - for (int i=0; iu_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) { SEGENV.aux0 = secondHand; - int pixBri = volumeRaw * SEGMENT.intensity / 64; + uint8_t pixBri = volumeRaw * SEGMENT.intensity / 64; SEGMENT.setPixelColor(SEGLEN/2, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(strip.now, false, PALETTE_SOLID_WRAP, 0), pixBri)); - for (int i = SEGLEN - 1; i > SEGLEN/2; i--) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i-1)); //move to the left - for (int i = 0; i < SEGLEN/2; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // move to the right + for (unsigned i = SEGLEN - 1; i > SEGLEN/2; i--) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i-1)); //move to the left + for (unsigned i = 0; i < SEGLEN/2; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // move to the right } return FRAMETIME; @@ -6850,22 +6744,18 @@ 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); - plasmoip->thisphase += beatsin8(6,-4,4); // You can change direction and speed individually. - plasmoip->thatphase += beatsin8(7,-4,4); // Two phase values to make a complex pattern. By Andrew Tuline. + plasmoip->thisphase += beatsin8_t(6,-4,4); // You can change direction and speed individually. + plasmoip->thatphase += beatsin8_t(7,-4,4); // Two phase values to make a complex pattern. By Andrew Tuline. - for (int i = 0; i < SEGLEN; i++) { // For each of the LED's in the strand, set a brightness based on a wave as follows. + for (unsigned i = 0; i < SEGLEN; i++) { // For each of the LED's in the strand, set a brightness based on a wave as follows. // updated, similar to "plasma" effect - softhack007 uint8_t thisbright = cubicwave8(((i*(1 + (3*SEGMENT.speed/32)))+plasmoip->thisphase) & 0xFF)/2; - thisbright += cos8(((i*(97 +(5*SEGMENT.speed/32)))+plasmoip->thatphase) & 0xFF)/2; // Let's munge the brightness a bit and animate it all with the phases. + thisbright += cos8_t(((i*(97 +(5*SEGMENT.speed/32)))+plasmoip->thatphase) & 0xFF)/2; // Let's munge the brightness a bit and animate it all with the phases. uint8_t colorIndex=thisbright; if (volumeSmth * SEGMENT.intensity / 64 < thisbright) {thisbright = 0;} @@ -6875,83 +6765,57 @@ 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 - - -/////////////////////// -// * PUDDLEPEAK // -/////////////////////// -// Andrew's crappy peak detector. If I were 40+ years younger, I'd learn signal processing. -uint16_t mode_puddlepeak(void) { // Puddlepeak. By Andrew Tuline. - if (SEGLEN == 1) return mode_static(); - - uint16_t size = 0; - uint8_t fadeVal = map(SEGMENT.speed,0,255, 224, 254); - uint16_t 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); - } - uint8_t samplePeak = *(uint8_t*)um_data->u_data[3]; - uint8_t *maxVol = (uint8_t*)um_data->u_data[6]; - uint8_t *binNum = (uint8_t*)um_data->u_data[7]; - float volumeSmth = *(float*) um_data->u_data[0]; - - if (SEGENV.call == 0) { - SEGMENT.custom1 = *binNum; - SEGMENT.custom2 = *maxVol * 2; - } - - *binNum = SEGMENT.custom1; // Select a bin. - *maxVol = SEGMENT.custom2 / 2; // Our volume comparator. - - SEGMENT.fade_out(fadeVal); - - if (samplePeak == 1) { - size = volumeSmth * SEGMENT.intensity /256 /4 + 1; // Determine size of the flash based on the volume. - if (pos+size>= SEGLEN) size = SEGLEN - pos; - } - - for (int i=0; iu_data[1]; + um_data_t *um_data = getAudioData(); + int volumeRaw = *(int16_t*)um_data->u_data[1]; + uint8_t samplePeak = *(uint8_t*)um_data->u_data[3]; + uint8_t *maxVol = (uint8_t*)um_data->u_data[6]; + uint8_t *binNum = (uint8_t*)um_data->u_data[7]; + float volumeSmth = *(float*) um_data->u_data[0]; - if (volumeRaw > 1) { - size = volumeRaw * SEGMENT.intensity /256 /8 + 1; // Determine size of the flash based on the volume. - if (pos+size >= SEGLEN) size = SEGLEN - pos; + if(peakdetect) { // puddles peak + *binNum = SEGMENT.custom1; // Select a bin. + *maxVol = SEGMENT.custom2 / 2; // Our volume comparator. + if (samplePeak == 1) { + size = volumeSmth * SEGMENT.intensity /256 /4 + 1; // Determine size of the flash based on the volume. + if (pos+size>= SEGLEN) size = SEGLEN - pos; + } } - - for (int i=0; i 1) { + size = volumeRaw * SEGMENT.intensity /256 /8 + 1; // Determine size of the flash based on the volume. + if (pos+size >= SEGLEN) size = SEGLEN - pos; + } + } + + for (unsigned i=0; i(SEGENV.data); // Used to store a pile of samples because WLED frame rate and WLED sample rate are not synchronized. Frame rate is too low. um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { um_data = simulateSound(SEGMENT.soundSim); } float volumeSmth = *(float*) um_data->u_data[0]; @@ -6975,20 +6839,14 @@ uint16_t mode_pixels(void) { // Pixels. By Andrew Tuline. SEGMENT.fade_out(64+(SEGMENT.speed>>1)); for (int i=0; i u_data[2]; if (SEGENV.call == 0) { @@ -7013,12 +6867,12 @@ uint16_t mode_blurz(void) { // Blurz. By Andrew Tuline. SEGENV.step += FRAMETIME; if (SEGENV.step > SPEED_FORMULA_L) { - uint16_t segLoc = random16(SEGLEN); - SEGMENT.setPixelColor(segLoc, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(2*fftResult[SEGENV.aux0%16]*240/max(1, SEGLEN-1), false, PALETTE_SOLID_WRAP, 0), 2*fftResult[SEGENV.aux0%16])); + unsigned segLoc = hw_random16(SEGLEN); + SEGMENT.setPixelColor(segLoc, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(2*fftResult[SEGENV.aux0%16]*240/max(1, (int)SEGLEN-1), false, PALETTE_SOLID_WRAP, 0), uint8_t(2*fftResult[SEGENV.aux0%16]))); ++(SEGENV.aux0) %= 16; // make sure it doesn't cross 16 SEGENV.step = 1; - SEGMENT.blur(SEGMENT.intensity); + SEGMENT.blur(SEGMENT.intensity); // note: blur > 210 results in a alternating pattern, this could be fixed by mapping but some may like it (very old bug) } return FRAMETIME; @@ -7030,14 +6884,10 @@ static const char _data_FX_MODE_BLURZ[] PROGMEM = "Blurz@Fade rate,Blur;!,Color // ** DJLight // ///////////////////////// uint16_t mode_DJLight(void) { // Written by ??? Adapted by Will Tatam. - if (SEGLEN == 1) return mode_static(); + // No need to prevent from executing on single led strips, only mid will be set (mid = 0) const int mid = SEGLEN / 2; - 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) { @@ -7051,13 +6901,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 //////////////////// @@ -7068,11 +6919,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) @@ -7084,11 +6931,11 @@ uint16_t mode_freqmap(void) { // Map FFT_MajorPeak to SEGLEN. int locn = (log10f((float)FFT_MajorPeak) - 1.78f) * (float)SEGLEN/(MAX_FREQ_LOG10 - 1.78f); // log10 frequency range is from 1.78 to 3.71. Let's scale to SEGLEN. if (locn < 1) locn = 0; // avoid underflow - if (locn >=SEGLEN) locn = SEGLEN-1; - 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. + if (locn >= (int)SEGLEN) locn = SEGLEN-1; + unsigned pixCol = (log10f(FFT_MajorPeak) - 1.78f) * 255.0f/(MAX_FREQ_LOG10 - 1.78f); // Scale log10 of frequency values to the 255 colour index. if (FFT_MajorPeak < 61.0f) pixCol = 0; // handle underflow - uint16_t bright = (int)my_magnitude; + uint8_t bright = (uint8_t)my_magnitude; SEGMENT.setPixelColor(locn, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(SEGMENT.intensity+pixCol, false, PALETTE_SOLID_WRAP, 0), bright)); @@ -7101,12 +6948,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]; @@ -7137,19 +6980,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 ////////////////////// @@ -7160,11 +7004,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) @@ -7172,7 +7012,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; @@ -7181,8 +7021,8 @@ uint16_t mode_freqpixels(void) { // Freqpixel. By Andrew Tuline. uint8_t pixCol = (log10f(FFT_MajorPeak) - 1.78f) * 255.0f/(MAX_FREQ_LOG10 - 1.78f); // Scale log10 of frequency values to the 255 colour index. if (FFT_MajorPeak < 61.0f) pixCol = 0; // handle underflow for (int i=0; i < SEGMENT.intensity/32+1; i++) { - uint16_t locn = random16(0,SEGLEN); - SEGMENT.setPixelColor(locn, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(SEGMENT.intensity+pixCol, false, PALETTE_SOLID_WRAP, 0), (int)my_magnitude)); + unsigned locn = hw_random16(0,SEGLEN); + SEGMENT.setPixelColor(locn, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(SEGMENT.intensity+pixCol, false, PALETTE_SOLID_WRAP, 0), (uint8_t)my_magnitude)); } return FRAMETIME; @@ -7206,12 +7046,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]; @@ -7224,10 +7060,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; @@ -7242,85 +7076,28 @@ 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 - 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 + // if SEGLEN equals 1 these loops won't execute + for (unsigned i = SEGLEN - 1; i > SEGLEN/2; i--) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i-1)); //move to the left + for (unsigned i = 0; i < SEGLEN/2; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // move to the right } return FRAMETIME; } // mode_freqwave() -static const char _data_FX_MODE_FREQWAVE[] PROGMEM = "Freqwave@Speed,Sound effect,Low bin,High bin,Pre-amp;;;1f;m12=2,si=0"; // Circle, Beatsin - - -/////////////////////// -// ** Gravfreq // -/////////////////////// -uint16_t mode_gravfreq(void) { // Gravfreq. By Andrew Tuline. - if (SEGLEN == 1) return mode_static(); - uint16_t 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); - } - float FFT_MajorPeak = *(float*)um_data->u_data[4]; - float volumeSmth = *(float*)um_data->u_data[0]; - if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; // log10(0) is "forbidden" (throws exception) - - SEGMENT.fade_out(250); - - float segmentSampleAvg = volumeSmth * (float)SEGMENT.intensity / 255.0f; - segmentSampleAvg *= 0.125f; // divide by 8, to compensate for later "sensitivity" upscaling - - float mySampleAvg = mapf(segmentSampleAvg*2.0f, 0,32, 0, (float)SEGLEN/2.0f); // map to pixels availeable in current segment - int tempsamp = constrain(mySampleAvg,0,SEGLEN/2); // Keep the sample from overflowing. - uint8_t gravity = 8 - SEGMENT.speed/32; - - for (int i=0; i= gravcen->topLED) - gravcen->topLED = tempsamp-1; - else if (gravcen->gravityCounter % gravity == 0) - gravcen->topLED--; - - if (gravcen->topLED >= 0) { - SEGMENT.setPixelColor(gravcen->topLED+SEGLEN/2, CRGB::Gray); - SEGMENT.setPixelColor(SEGLEN/2-1-gravcen->topLED, CRGB::Gray); - } - gravcen->gravityCounter = (gravcen->gravityCounter + 1) % gravity; - - return FRAMETIME; -} // mode_gravfreq() -static const char _data_FX_MODE_GRAVFREQ[] PROGMEM = "Gravfreq@Rate of fall,Sensitivity;!,!;!;1f;ix=128,m12=0,si=0"; // Pixels, Beatsin +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 ////////////////////// // ** 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; @@ -7328,26 +7105,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; @@ -7369,13 +7142,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_t(8+octCount*4, 0, 255, 0, octCount*8), 0, 255, 0, SEGLEN-1); + i = constrain(i, 0U, SEGLEN-1U); SEGMENT.addPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette((uint8_t)frTemp, false, PALETTE_SOLID_WRAP, 0), volTemp)); return FRAMETIME; } // mode_rocktaves() -static const char _data_FX_MODE_ROCKTAVES[] PROGMEM = "Rocktaves@;!,!;!;1f;m12=1,si=0"; // Bar, Beatsin +static const char _data_FX_MODE_ROCKTAVES[] PROGMEM = "Rocktaves@;!,!;!;01f;m12=1,si=0"; // Bar, Beatsin /////////////////////// @@ -7383,13 +7156,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]; @@ -7419,14 +7188,15 @@ uint16_t mode_waterfall(void) { // Waterfall. By: Andrew Tulin if (samplePeak) { SEGMENT.setPixelColor(SEGLEN-1, CHSV(92,92,92)); } else { - SEGMENT.setPixelColor(SEGLEN-1, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(pixCol+SEGMENT.intensity, false, PALETTE_SOLID_WRAP, 0), (int)my_magnitude)); + SEGMENT.setPixelColor(SEGLEN-1, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(pixCol+SEGMENT.intensity, false, PALETTE_SOLID_WRAP, 0), (uint8_t)my_magnitude)); } - for (int i = 0; i < SEGLEN-1; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // shift left + // loop will not execute if SEGLEN equals 1 + for (unsigned i = 0; i < SEGLEN-1; i++) SEGMENT.setPixelColor(i, SEGMENT.getPixelColor(i+1)); // shift left } 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 @@ -7437,17 +7207,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 = SEG_W; + const int rows = SEG_H; 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; @@ -7494,8 +7260,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 = SEG_W; + const int rows = SEG_H; int NUMB_BANDS = map(SEGMENT.custom1, 0, 255, 1, 16); int barWidth = (cols / NUMB_BANDS); @@ -7506,11 +7272,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) { @@ -7586,17 +7348,17 @@ 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 = SEG_W; + const int rows = SEG_H; - 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; const float normalFactor = 0.4f; um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { um_data = simulateSound(SEGMENT.soundSim); } uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; @@ -7605,9 +7367,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 @@ -7630,11 +7392,12 @@ 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; + int xMax = cols/8; + for (int x=0; x < xMax; x++) { + unsigned band = map(x, 0, max(xMax,4), 0, 15); // map 0..cols/8 to 16 GEQ bands 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); + uint32_t color = SEGMENT.color_from_palette((band * 35), false, PALETTE_SOLID_WRAP, 0); for (int y=0; y < barHeight; y++) { SEGMENT.setPixelColorXY(x, rows/2-y, color); @@ -7654,44 +7417,44 @@ 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 = SEG_W; + const int rows = SEG_H; 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_t(10-speed,0,cols-1)*scale; + unsigned cy = beatsin8_t(12-speed,0,rows-1)*scale; + unsigned cx1 = beatsin8_t(13-speed,0,cols-1)*scale; + unsigned cy1 = beatsin8_t(15-speed,0,rows-1)*scale; + unsigned cx2 = beatsin8_t(17-speed,0,cols-1)*scale; + unsigned cy2 = beatsin8_t(14-speed,0,rows-1)*scale; - 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; - byte rdistort = cos8((cos8(((x<<3)+a )&255)+cos8(((y<<3)-a2)&255)+a3 )&255)>>1; - byte gdistort = cos8((cos8(((x<<3)-a2)&255)+cos8(((y<<3)+a3)&255)+a+32 )&255)>>1; - byte bdistort = cos8((cos8(((x<<3)+a3)&255)+cos8(((y<<3)-a) &255)+a2+64)&255)>>1; + byte rdistort = cos8_t((cos8_t(((x<<3)+a )&255)+cos8_t(((y<<3)-a2)&255)+a3 )&255)>>1; + byte gdistort = cos8_t((cos8_t(((x<<3)-a2)&255)+cos8_t(((y<<3)+a3)&255)+a+32 )&255)>>1; + byte bdistort = cos8_t((cos8_t(((x<<3)+a3)&255)+cos8_t(((y<<3)-a) &255)+a2+64)&255)>>1; byte valueR = rdistort+ w* (a- ( ((xoffs - cx) * (xoffs - cx) + (yoffs - cy) * (yoffs - cy))>>7 )); byte valueG = gdistort+ w* (a2-( ((xoffs - cx1) * (xoffs - cx1) + (yoffs - cy1) * (yoffs - cy1))>>7 )); byte valueB = bdistort+ w* (a3-( ((xoffs - cx2) * (xoffs - cx2) + (yoffs - cy2) * (yoffs - cy2))>>7 )); - valueR = gamma8(cos8(valueR)); - valueG = gamma8(cos8(valueG)); - valueB = gamma8(cos8(valueB)); + valueR = gamma8(cos8_t(valueR)); + valueG = gamma8(cos8_t(valueG)); + valueB = gamma8(cos8_t(valueB)); SEGMENT.setPixelColorXY(x, y, RGBW32(valueR, valueG, valueB, 0)); } @@ -7709,8 +7472,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 = SEG_W; + const int rows = SEG_H; 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 @@ -7726,9 +7489,9 @@ uint16_t mode_2Dsoap() { // init if (SEGENV.call == 0) { - *noise32_x = random16(); - *noise32_y = random16(); - *noise32_z = random16(); + *noise32_x = hw_random(); + *noise32_y = hw_random(); + *noise32_z = hw_random(); } else { *noise32_x += mov; *noise32_y += mov; @@ -7757,8 +7520,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; @@ -7811,7 +7574,7 @@ uint16_t mode_2Dsoap() { return FRAMETIME; } -static const char _data_FX_MODE_2DSOAP[] PROGMEM = "Soap@!,Smoothness;;!;2"; +static const char _data_FX_MODE_2DSOAP[] PROGMEM = "Soap@!,Smoothness;;!;2;pal=11"; //Idea from https://www.youtube.com/watch?v=HsA-6KIbgto&ab_channel=GreatScott%21 @@ -7821,8 +7584,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 = SEG_W; + const int rows = SEG_H; const uint8_t mapp = 180 / MAX(cols,rows); typedef struct { @@ -7848,8 +7611,10 @@ uint16_t mode_2Doctopus() { const int C_Y = (rows / 2) + ((SEGMENT.custom2 - 128)*rows)/255; for (int x = 0; x < cols; x++) { for (int y = 0; y < rows; y++) { - rMap[XY(x, y)].angle = 40.7436f * atan2f((y - C_Y), (x - C_X)); // avoid 128*atan2()/PI - rMap[XY(x, y)].radius = hypotf((x - C_X), (y - C_Y)) * mapp; //thanks Sutaburosu + int dx = (x - C_X); + int dy = (y - C_Y); + rMap[XY(x, y)].angle = int(40.7436f * atan2_t(dy, dx)); // avoid 128*atan2()/PI + rMap[XY(x, y)].radius = sqrtf(dx * dx + dy * dy) * mapp; //thanks Sutaburosu } } } @@ -7859,37 +7624,41 @@ uint16_t mode_2Doctopus() { for (int y = 0; y < rows; y++) { 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 - CRGB c = ColorFromPalette(SEGPALETTE, SEGENV.step / 2 - radius, intensity); - SEGMENT.setPixelColorXY(x, y, c); + //CRGB c = CHSV(SEGENV.step / 2 - radius, 255, sin8_t(sin8_t((angle * 4 - radius) / 4 + SEGENV.step) + radius - SEGENV.step * 2 + angle * (SEGMENT.custom3/3+1))); + unsigned intensity = sin8_t(sin8_t((angle * 4 - radius) / 4 + SEGENV.step/2) + radius - SEGENV.step + angle * (SEGMENT.custom3/4+1)); + intensity = map((intensity*intensity) & 0xFFFF, 0, 65535, 0, 255); // add a bit of non-linearity for cleaner display + SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, SEGENV.step / 2 - radius, intensity)); } } return FRAMETIME; } -static const char _data_FX_MODE_2DOCTOPUS[] PROGMEM = "Octopus@!,,Offset X,Offset Y,Legs;;!;2;"; +static const char _data_FX_MODE_2DOCTOPUS[] PROGMEM = "Octopus@!,,Offset X,Offset Y,Legs,fasttan;;!;2;"; //Waving Cell //@Stepko (https://editor.soulmatelights.com/gallery/1704-wavingcells) -// adapted for WLED by @blazoncek +// adapted for WLED by @blazoncek, improvements by @dedehai uint16_t mode_2Dwavingcell() { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); - - uint32_t t = strip.now/(257-SEGMENT.speed); - uint8_t aX = SEGMENT.custom1/16 + 9; - uint8_t aY = SEGMENT.custom2/16 + 1; - uint8_t aZ = SEGMENT.custom3 + 1; - for (int x = 0; x < cols; x++) for (int y = 0; y >3; + uint32_t aX = SEGMENT.custom1/16 + 9; + uint32_t aY = SEGMENT.custom2/16 + 1; + uint32_t aZ = SEGMENT.custom3 + 1; + for (int x = 0; x < cols; x++) { + for (int y = 0; y < rows; y++) { + uint32_t wave = sin8_t((x * aX) + sin8_t((((y<<8) + t) * aY)>>8)) + cos8_t(y * aZ); // bit shifts to increase temporal resolution + uint8_t colorIndex = wave + (t>>(8-(SEGMENT.check2*3))); + SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, colorIndex)); + } + } + SEGMENT.blur(SEGMENT.intensity); return FRAMETIME; } -static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,,Amplitude 1,Amplitude 2,Amplitude 3;;!;2"; +static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,Blur,Amplitude 1,Amplitude 2,Amplitude 3,,Flow;;!;2;ix=0"; #endif // WLED_DISABLE_2D @@ -7902,18 +7671,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 } } @@ -8005,7 +7779,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_COLORTWINKLE, &mode_colortwinkle, _data_FX_MODE_COLORTWINKLE); addEffect(FX_MODE_LAKE, &mode_lake, _data_FX_MODE_LAKE); addEffect(FX_MODE_METEOR, &mode_meteor, _data_FX_MODE_METEOR); - addEffect(FX_MODE_METEOR_SMOOTH, &mode_meteor_smooth, _data_FX_MODE_METEOR_SMOOTH); + //addEffect(FX_MODE_METEOR_SMOOTH, &mode_meteor_smooth, _data_FX_MODE_METEOR_SMOOTH); // merged with mode_meteor addEffect(FX_MODE_RAILWAY, &mode_railway, _data_FX_MODE_RAILWAY); addEffect(FX_MODE_RIPPLE, &mode_ripple, _data_FX_MODE_RIPPLE); addEffect(FX_MODE_TWINKLEFOX, &mode_twinklefox, _data_FX_MODE_TWINKLEFOX); diff --git a/wled00/FX.h b/wled00/FX.h index d72580935..09f9992d5 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -2,24 +2,10 @@ WS2812FX.h - Library for WS2812 LED effects. Harm Aldick - 2016 www.aldick.org - LICENSE - The MIT License (MIT) + Copyright (c) 2016 Harm Aldick - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. + Licensed under the EUPL v. 1.2 or later + Adapted from code originally licensed under the MIT license Modified for WLED */ @@ -30,6 +16,7 @@ #include #include "const.h" +#include "bus_manager.h" #define FASTLED_INTERNAL //remove annoying pragma messages #define USE_GET_MILLISECOND_TIMER @@ -56,16 +43,35 @@ #define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b)))) #endif +extern bool realtimeRespectLedMaps; // used in getMappedPixelIndex() +extern byte realtimeMode; // used in getMappedPixelIndex() + /* Not used in all effects yet */ #define WLED_FPS 42 #define FRAMETIME_FIXED (1000/WLED_FPS) -//#define FRAMETIME _frametime #define FRAMETIME strip.getFrameTime() +#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S2) + #define MIN_FRAME_DELAY 2 // minimum wait between repaints, to keep other functions like WiFi alive +#elif defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) + #define MIN_FRAME_DELAY 3 // S2/C3 are slower than normal esp32, and only have one core +#else + #define MIN_FRAME_DELAY 8 // 8266 legacy MIN_SHOW_DELAY +#endif +#define FPS_UNLIMITED 0 + +// FPS calculation (can be defined as compile flag for debugging) +#ifndef FPS_CALC_AVG +#define FPS_CALC_AVG 7 // average FPS calculation over this many frames (moving average) +#endif +#ifndef FPS_MULTIPLIER +#define FPS_MULTIPLIER 1 // dev option: multiplier to get sub-frame FPS without floats +#endif +#define FPS_CALC_SHIFT 7 // bit shift for fixed point math /* each segment uses 82 bytes of SRAM memory, so if you're application fails because of insufficient memory, decreasing MAX_NUM_SEGMENTS may help */ #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 +79,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 @@ -87,16 +89,14 @@ assuming each segment uses the same amount of data. 256 for ESP8266, 640 for ESP32. */ #define FAIR_DATA_PER_SEG (MAX_SEGMENT_DATA / strip.getMaxSegments()) -#define MIN_SHOW_DELAY (_frametime < 16 ? 8 : 15) - #define NUM_COLORS 3 /* number of colors per segment */ #define SEGMENT strip._segments[strip.getCurrSegmentId()] #define SEGENV strip._segments[strip.getCurrSegmentId()] -//#define SEGCOLOR(x) strip._segments[strip.getCurrSegmentId()].currentColor(x, strip._segments[strip.getCurrSegmentId()].colors[x]) -//#define SEGLEN strip._segments[strip.getCurrSegmentId()].virtualLength() -#define SEGCOLOR(x) strip.segColor(x) /* saves us a few kbytes of code */ -#define SEGPALETTE strip._currentPalette -#define SEGLEN strip._virtualSegmentLength /* saves us a few kbytes of code */ +#define SEGCOLOR(x) Segment::getCurrentColor(x) +#define SEGPALETTE Segment::getCurrentPalette() +#define SEGLEN Segment::vLength() +#define SEG_W Segment::vWidth() +#define SEG_H Segment::vHeight() #define SPEED_FORMULA_L (5U + (50U*(255U - SEGMENT.speed))/SEGLEN) // some common colors @@ -111,6 +111,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 @@ -180,14 +184,14 @@ #define FX_MODE_TWO_DOTS 50 #define FX_MODE_FAIRYTWINKLE 51 //was Two Areas prior to 0.13.0-b6 (use "Two Dots" with full intensity) #define FX_MODE_RUNNING_DUAL 52 -#define FX_MODE_IMAGE 53 // was Halloween before 0.14 +#define FX_MODE_IMAGE 53 #define FX_MODE_TRICOLOR_CHASE 54 #define FX_MODE_TRICOLOR_WIPE 55 #define FX_MODE_TRICOLOR_FADE 56 #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 @@ -204,7 +208,7 @@ #define FX_MODE_COLORTWINKLE 74 #define FX_MODE_LAKE 75 #define FX_MODE_METEOR 76 -#define FX_MODE_METEOR_SMOOTH 77 +//#define FX_MODE_METEOR_SMOOTH 77 // merged with meteor #define FX_MODE_RAILWAY 78 #define FX_MODE_RIPPLE 79 #define FX_MODE_TWINKLEFOX 80 @@ -325,7 +329,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 @@ -368,6 +373,7 @@ typedef struct Segment { }; uint8_t startY; // start Y coodrinate 2D (top); there should be no more than 255 rows uint8_t stopY; // stop Y coordinate 2D (bottom); there should be no more than 255 rows + // note: two bytes of padding are added here char *name; // runtime data @@ -396,7 +402,7 @@ typedef struct Segment { uint32_t _stepT; uint32_t _callT; uint8_t *_dataT; - uint16_t _dataLenT; + unsigned _dataLenT; TemporarySegmentData() : _dataT(nullptr) // just in case... , _dataLenT(0) @@ -414,14 +420,20 @@ typedef struct Segment { uint8_t _reserved : 4; }; }; - uint16_t _dataLen; - static uint16_t _usedSegmentData; - - // perhaps this should be per segment, not static + uint8_t _default_palette; // palette number that gets assigned to pal0 + unsigned _dataLen; + static unsigned _usedSegmentData; + static uint8_t _segBri; // brightness of segment for current effect + static unsigned _vLength; // 1D dimension used for current effect + static unsigned _vWidth, _vHeight; // 2D dimensions used for current effect + static uint32_t _currentColors[NUM_COLORS]; // colors used for current effect + static bool _colorScaled; // color has been scaled prior to setPixelColor() call + static CRGBPalette16 _currentPalette; // palette used for current effect (includes transition, used in color_from_palette()) static CRGBPalette16 _randomPalette; // actual random palette static CRGBPalette16 _newRandomPalette; // target random palette static uint16_t _lastPaletteChange; // last random palette change time in millis()/1000 static uint16_t _lastPaletteBlend; // blend palette according to set Transition Delay in millis()%0xFFFF + static uint16_t _transitionprogress; // current transition progress 0 - 0xFFFF #ifndef WLED_DISABLE_MODE_BLEND static bool _modeBlend; // mode/effect blending semaphore #endif @@ -448,6 +460,8 @@ typedef struct Segment { {} } *_t; + [[gnu::hot]] void _setPixelColorXY_raw(int& x, int& y, uint32_t& col); // set pixel without mapping (internal use only) + public: Segment(uint16_t sStart=0, uint16_t sStop=30) : @@ -480,6 +494,7 @@ typedef struct Segment { aux1(0), data(nullptr), _capabilities(0), + _default_palette(0), _dataLen(0), _t(nullptr) { @@ -516,154 +531,187 @@ typedef struct Segment { #endif inline bool getOption(uint8_t n) const { return ((options >> n) & 0x01); } - inline bool isSelected(void) const { return selected; } - inline bool isInTransition(void) const { return _t != nullptr; } - inline bool isActive(void) const { return stop > start; } - inline bool is2D(void) const { return (width()>1 && height()>1); } - inline bool hasRGB(void) const { return _isRGB; } - inline bool hasWhite(void) const { return _hasW; } - inline bool isCCT(void) const { return _isCCT; } - inline uint16_t width(void) const { return isActive() ? (stop - start) : 0; } // segment width in physical pixels (length if 1D) - inline uint16_t height(void) const { return stopY - startY; } // segment height (if 2D) in physical pixels (it *is* always >=1) - inline uint16_t length(void) const { return width() * height(); } // segment length (count) in physical pixels - inline uint16_t groupLength(void) const { return grouping + spacing; } - inline uint8_t getLightCapabilities(void) const { return _capabilities; } + inline bool isSelected() const { return selected; } + inline bool isInTransition() const { return _t != nullptr; } + inline bool isActive() const { return stop > start; } + inline bool is2D() const { return (width()>1 && height()>1); } + inline bool hasRGB() const { return _isRGB; } + inline bool hasWhite() const { return _hasW; } + inline bool isCCT() const { return _isCCT; } + inline uint16_t width() const { return isActive() ? (stop - start) : 0; } // segment width in physical pixels (length if 1D) + inline uint16_t height() const { return stopY - startY; } // segment height (if 2D) in physical pixels (it *is* always >=1) + inline uint16_t length() const { return width() * height(); } // segment length (count) in physical pixels + inline uint16_t groupLength() const { return grouping + spacing; } + inline uint8_t getLightCapabilities() const { return _capabilities; } + inline void deactivate() { setGeometry(0,0); } - static uint16_t getUsedSegmentData(void) { return _usedSegmentData; } - static void addUsedSegmentData(int len) { _usedSegmentData += len; } + inline static unsigned getUsedSegmentData() { return Segment::_usedSegmentData; } + inline static void addUsedSegmentData(int len) { Segment::_usedSegmentData += len; } #ifndef WLED_DISABLE_MODE_BLEND - static void modeBlend(bool blend) { _modeBlend = blend; } + inline static void modeBlend(bool blend) { _modeBlend = blend; } #endif - static void handleRandomPalette(); + inline static unsigned vLength() { return Segment::_vLength; } + inline static unsigned vWidth() { return Segment::_vWidth; } + inline static unsigned vHeight() { return Segment::_vHeight; } + inline static uint32_t getCurrentColor(unsigned i) { return Segment::_currentColors[i]; } // { return i < 3 ? Segment::_currentColors[i] : 0; } + inline static const CRGBPalette16 &getCurrentPalette() { return Segment::_currentPalette; } + inline static uint8_t getCurrentBrightness() { return Segment::_segBri; } + static void handleRandomPalette(); - 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 - void setCCT(uint16_t k); - void setOpacity(uint8_t o); - void setOption(uint8_t n, bool val); - void setMode(uint8_t fx, bool loadDefaults = false); - void setPalette(uint8_t pal); - uint8_t differs(Segment& b) const; - void refreshLightCapabilities(void); + void beginDraw(); // set up parameters for current effect + void setGeometry(uint16_t i1, uint16_t i2, uint8_t grp=1, uint8_t spc=0, uint16_t ofs=UINT16_MAX, uint16_t i1Y=0, uint16_t i2Y=1, uint8_t m12=0); + Segment &setColor(uint8_t slot, uint32_t c); + Segment &setCCT(uint16_t k); + Segment &setOpacity(uint8_t o); + Segment &setOption(uint8_t n, bool val); + Segment &setMode(uint8_t fx, bool loadDefaults = false); + Segment &setPalette(uint8_t pal); + uint8_t differs(const Segment& b) const; + void refreshLightCapabilities(); // runtime data functions - inline uint16_t dataSize(void) const { return _dataLen; } + inline uint16_t dataSize() const { return _dataLen; } bool allocateData(size_t len); // allocates effect data buffer in heap and clears it - void deallocateData(void); // deallocates (frees) effect data buffer from heap - void resetIfRequired(void); // sets all SEGENV variables to 0 and clears data buffer + void deallocateData(); // deallocates (frees) effect data buffer from heap + void resetIfRequired(); // sets all SEGENV variables to 0 and clears data buffer /** * Flags that before the next effect is calculated, * the internal segment state should be reset. * Call resetIfRequired before calling the next effect function. * Safe to call from interrupts and network requests. */ - inline void markForReset(void) { reset = true; } // setOption(SEG_OPTION_RESET, true) + inline Segment &markForReset() { reset = true; return *this; } // setOption(SEG_OPTION_RESET, true) // 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(); // ends transition mode by destroying transition structure (does nothing if not in transition) + inline void handleTransition() { updateTransitionProgress(); if (progress() == 0xFFFFU) stopTransition(); } #ifndef WLED_DISABLE_MODE_BLEND void swapSegenv(tmpsegd_t &tmpSegD); // copies segment data into specifed buffer, if buffer is not a transition buffer, segment data is overwritten from transition buffer void restoreSegenv(tmpsegd_t &tmpSegD); // restores segment data from buffer, if buffer is not transition buffer, changed values are copied to transition buffer #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) + [[gnu::hot]] void updateTransitionProgress(); // set current progression of transition + inline uint16_t progress() const { return _transitionprogress; }; // transition progression between 0-65535 + [[gnu::hot]] uint8_t currentBri(bool useCct = false) const; // current segment brightness/CCT (blended while in transition) + uint8_t currentMode() const; // currently active effect/mode (while in transition) + [[gnu::hot]] uint32_t currentColor(uint8_t slot) const; // currently active segment color (blended while in transition) CRGBPalette16 &loadPalette(CRGBPalette16 &tgt, uint8_t pal); - CRGBPalette16 ¤tPalette(CRGBPalette16 &tgt, uint8_t paletteID); // 1D strip - uint16_t virtualLength(void) const; - void setPixelColor(int n, uint32_t c); // set relative pixel within segment with color + [[gnu::hot]] uint16_t virtualLength() const; + [[gnu::hot]] void setPixelColor(int n, uint32_t c); // set relative pixel within segment with color inline void setPixelColor(unsigned n, uint32_t c) { setPixelColor(int(n), c); } inline void setPixelColor(int n, byte r, byte g, byte b, byte w = 0) { setPixelColor(n, RGBW32(r,g,b,w)); } inline void setPixelColor(int n, CRGB c) { setPixelColor(n, RGBW32(c.r,c.g,c.b,0)); } + #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 + [[gnu::hot]] 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); inline void blendPixelColor(int n, uint32_t color, uint8_t blend) { setPixelColor(n, color_blend(getPixelColor(n), color, blend)); } inline void blendPixelColor(int n, CRGB c, uint8_t blend) { blendPixelColor(n, RGBW32(c.r,c.g,c.b,0), blend); } - inline void addPixelColor(int n, uint32_t color, bool fast = false) { setPixelColor(n, color_add(getPixelColor(n), color, fast)); } - inline void addPixelColor(int n, byte r, byte g, byte b, byte w = 0, bool fast = false) { addPixelColor(n, RGBW32(r,g,b,w), fast); } - inline void addPixelColor(int n, CRGB c, bool fast = false) { addPixelColor(n, RGBW32(c.r,c.g,c.b,0), fast); } + inline void addPixelColor(int n, uint32_t color, bool preserveCR = true) { setPixelColor(n, color_add(getPixelColor(n), color, preserveCR)); } + inline void addPixelColor(int n, byte r, byte g, byte b, byte w = 0, bool preserveCR = true) { addPixelColor(n, RGBW32(r,g,b,w), preserveCR); } + inline void addPixelColor(int n, CRGB c, bool preserveCR = true) { addPixelColor(n, RGBW32(c.r,c.g,c.b,0), preserveCR); } 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); + [[gnu::hot]] uint32_t color_from_palette(uint16_t, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri = 255) const; + [[gnu::hot]] uint32_t color_wheel(uint8_t pos) const; + + // 2D Blur: shortcuts for bluring columns or rows only (50% faster than full 2D blur) + inline void blurCols(fract8 blur_amount, bool smear = false) { // blur all columns + blur2D(0, blur_amount, smear); + } + inline void blurRows(fract8 blur_amount, bool smear = false) { // blur all rows + blur2D(blur_amount, 0, smear); + } // 2D matrix - uint16_t virtualWidth(void) const; // segment width in virtual pixels (accounts for groupping and spacing) - uint16_t virtualHeight(void) const; // segment height in virtual pixels (accounts for groupping and spacing) - uint16_t nrOfVStrips(void) const; // returns number of virtual vertical strips in 2D matrix (used to expand 1D effects into 2D) + [[gnu::hot]] unsigned virtualWidth() const; // segment width in virtual pixels (accounts for groupping and spacing) + [[gnu::hot]] unsigned virtualHeight() const; // segment height in virtual pixels (accounts for groupping and spacing) + inline unsigned nrOfVStrips() const { // returns number of virtual vertical strips in 2D matrix (used to expand 1D effects into 2D) + #ifndef WLED_DISABLE_2D + return (is2D() && map1D2D == M12_pBar) ? virtualWidth() : 1; + #else + return 1; + #endif + } #ifndef WLED_DISABLE_2D - uint16_t XY(uint16_t x, uint16_t y); // support function to get relative index within segment - void setPixelColorXY(int x, int y, uint32_t c); // set relative pixel within segment with color + [[gnu::hot]] uint16_t XY(int x, int y); // support function to get relative index within segment + [[gnu::hot]] void setPixelColorXY(int x, int y, uint32_t c); // set relative pixel within segment with color inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) { setPixelColorXY(int(x), int(y), c); } inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) { setPixelColorXY(x, y, RGBW32(r,g,b,w)); } inline void setPixelColorXY(int x, int y, CRGB c) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); } + inline void setPixelColorXY(unsigned x, unsigned y, CRGB c) { setPixelColorXY(int(x), int(y), RGBW32(c.r,c.g,c.b,0)); } + #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 + [[gnu::hot]] uint32_t getPixelColorXY(int x, int y) const; // 2D support functions inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t color, uint8_t blend) { setPixelColorXY(x, y, color_blend(getPixelColorXY(x,y), color, blend)); } inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) { blendPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), blend); } - inline void addPixelColorXY(int x, int y, uint32_t color, bool fast = false) { setPixelColorXY(x, y, color_add(getPixelColorXY(x,y), color, fast)); } - inline void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool fast = false) { addPixelColorXY(x, y, RGBW32(r,g,b,w), fast); } - inline void addPixelColorXY(int x, int y, CRGB c, bool fast = false) { addPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), fast); } - inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) { setPixelColorXY(x, y, color_fade(getPixelColorXY(x,y), fade, true)); } - void box_blur(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 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 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 addPixelColorXY(int x, int y, uint32_t color, bool preserveCR = true) { setPixelColorXY(x, y, color_add(getPixelColorXY(x,y), color, preserveCR)); } + inline void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool preserveCR = true) { addPixelColorXY(x, y, RGBW32(r,g,b,w), preserveCR); } + inline void addPixelColorXY(int x, int y, CRGB c, bool preserveCR = true) { addPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), preserveCR); } + inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) { setPixelColorXY(x, y, color_fade(getPixelColorXY(x,y), fade, true)); } + //void box_blur(unsigned r = 1U, bool smear = false); // 2D box blur + void blur2D(uint8_t blur_x, uint8_t blur_y, bool smear = false); + void moveX(int delta, bool wrap = false); + void moveY(int delta, bool wrap = false); + void move(unsigned dir, unsigned delta, bool wrap = false); + void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false); + inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) { drawCircle(cx, cy, radius, RGBW32(c.r,c.g,c.b,0), soft); } + void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false); + inline void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) { fillCircle(cx, cy, radius, RGBW32(c.r,c.g,c.b,0), soft); } + void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft = false); + inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c, bool soft = false) { drawLine(x0, y0, x1, y1, RGBW32(c.r,c.g,c.b,0), soft); } // automatic inline + void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t col2 = 0, int8_t rotate = 0, bool usePalGrad = false); 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 + 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, bool usePalGrad = false) { drawCharacter(chr, x, y, w, h, RGBW32(c.r,c.g,c.b,0), RGBW32(c2.r,c2.g,c2.b,0), rotate, usePalGrad); } // 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 uint16_t XY(int x, int 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 addPixelColorXY(int x, int y, uint32_t color, bool saturate = false) { addPixelColor(x, color, saturate); } + inline void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool saturate = false) { addPixelColor(x, RGBW32(r,g,b,w), saturate); } + inline void addPixelColorXY(int x, int y, CRGB c, bool saturate = false) { addPixelColor(x, RGBW32(c.r,c.g,c.b,0), saturate); } 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 moveX(int8_t delta, bool wrap = false) {} - inline void moveY(int8_t delta, bool wrap = false) {} + //inline void box_blur(unsigned i, bool vertical, fract8 blur_amount) {} + inline void blur2D(uint8_t blur_x, uint8_t blur_y, bool smear = false) {} + inline void blurRow(int row, fract8 blur_amount, bool smear = false) {} + inline void blurCol(int col, fract8 blur_amount, bool smear = false) {} + inline void moveX(int delta, bool wrap = false) {} + inline void moveY(int delta, bool wrap = false) {} inline void move(uint8_t dir, uint8_t delta, bool wrap = false) {} - inline void 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 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 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, bool = false) {} inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB color) {} - inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2, int8_t rotate = 0) {} + inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2, int8_t rotate = 0, bool usePalGrad = false) {} inline void wu_pixel(uint32_t x, uint32_t y, CRGB c) {} #endif } segment; @@ -671,8 +719,8 @@ typedef struct Segment { // main "strip" class class WS2812FX { // 96 bytes - typedef uint16_t (*mode_ptr)(void); // pointer to mode function - typedef void (*show_callback)(void); // pre show callback + typedef uint16_t (*mode_ptr)(); // pointer to mode function + typedef void (*show_callback)(); // pre show callback typedef struct ModeData { uint8_t _id; // mode (effect) id mode_ptr _fcn; // mode (effect) function @@ -694,10 +742,13 @@ class WS2812FX { // 96 bytes #ifndef WLED_DISABLE_2D panels(1), #endif - // semi-private (just obscured) used in effect functions through macros - _currentPalette(CRGBPalette16(CRGB::Black)), - _colors_t{0,0,0}, - _virtualSegmentLength(0), +#ifdef WLED_AUTOSEGMENTS + autoSegments(true), +#else + autoSegments(false), +#endif + correctWB(false), + cctFromRgb(false), // true private variables _suspend(false), _length(DEFAULT_LED_COUNT), @@ -705,7 +756,7 @@ class WS2812FX { // 96 bytes _transitionDur(750), _targetFps(WLED_FPS), _frametime(FRAMETIME_FIXED), - _cumulativeFps(2), + _cumulativeFps(50 << FPS_CALC_SHIFT), _isServicing(false), _isOffRefreshRequired(false), _hasWhiteChannel(false), @@ -715,16 +766,9 @@ class WS2812FX { // 96 bytes customMappingTable(nullptr), customMappingSize(0), _lastShow(0), + _lastServiceShow(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()) @@ -744,112 +788,105 @@ class WS2812FX { // 96 bytes customPalettes.clear(); } - static WS2812FX* getInstance(void) { return instance; } + static WS2812FX* getInstance() { return instance; } void #ifdef WLED_DEBUG printSize(), // prints memory usage for strip components #endif finalizeInit(), // initialises strip components - service(void), // executes effect functions when due and calls strip.show() - setMode(uint8_t segid, uint8_t m), // sets effect/mode for given segment (high level API) - setColor(uint8_t slot, uint32_t c), // sets color (in slot) for given segment (high level API) + service(), // executes effect functions when due and calls strip.show() setCCT(uint16_t k), // sets global CCT (either in relative 0-255 value or in K) setBrightness(uint8_t b, bool direct = false), // sets strip brightness setRange(uint16_t i, uint16_t i2, uint32_t col), // used for clock overlay - purgeSegments(void), // removes inactive segments from RAM (may incure penalty and memory fragmentation but reduces vector footprint) - setSegment(uint8_t n, uint16_t start, uint16_t stop, uint8_t grouping = 1, uint8_t spacing = 0, uint16_t offset = UINT16_MAX, uint16_t startY=0, uint16_t stopY=1), - setMainSegmentId(uint8_t n), + purgeSegments(), // removes inactive segments from RAM (may incure penalty and memory fragmentation but reduces vector footprint) + setMainSegmentId(unsigned n = 0), resetSegments(), // marks all segments for reset makeAutoSegments(bool forceReset = false), // will create segments based on configured outputs fixInvalidSegments(), // fixes incorrect segment configuration setPixelColor(unsigned n, uint32_t c), // paints absolute strip pixel with index n and color c - 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 + show(), // initiates LED output + setTargetFps(unsigned fps), + setupEffectData(); // add default effects to the list; defined in FX.cpp - inline void restartRuntime() { for (Segment &seg : _segments) seg.markForReset(); } + inline void resetTimebase() { timebase = 0UL - millis(); } + inline void restartRuntime() { for (Segment &seg : _segments) { seg.markForReset().resetIfRequired(); } } inline void setTransitionMode(bool t) { for (Segment &seg : _segments) seg.startTransition(t ? _transitionDur : 0); } - inline void setColor(uint8_t slot, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) { setColor(slot, RGBW32(r,g,b,w)); } inline void setPixelColor(unsigned n, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) { setPixelColor(n, RGBW32(r,g,b,w)); } inline void setPixelColor(unsigned n, CRGB c) { setPixelColor(n, c.red, c.green, c.blue); } inline void fill(uint32_t c) { for (unsigned i = 0; i < getLengthTotal(); i++) setPixelColor(i, c); } // fill whole strip with color (inline) - inline void trigger(void) { _triggered = true; } // Forces the next frame to be computed on all active segments. + inline void trigger() { _triggered = true; } // Forces the next frame to be computed on all active segments. inline void setShowCallback(show_callback cb) { _callback = cb; } inline void setTransition(uint16_t t) { _transitionDur = t; } // sets transition time (in ms) inline void appendSegment(const Segment &seg = Segment()) { if (_segments.size() < getMaxSegments()) _segments.push_back(seg); } - inline void suspend(void) { _suspend = true; } // will suspend (and canacel) strip.service() execution - inline void resume(void) { _suspend = false; } // will resume strip.service() execution + inline void suspend() { _suspend = true; } // will suspend (and canacel) strip.service() execution + inline void resume() { _suspend = false; } // will resume strip.service() execution bool paletteFade, - checkSegmentAlignment(void), - hasRGBWBus(void), - hasCCTBus(void), - // return true if the strip is being sent pixel updates - isUpdating(void), - deserializeMap(uint8_t n=0); + checkSegmentAlignment(), + hasRGBWBus() const, + hasCCTBus() const, + deserializeMap(unsigned 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 isUpdating() const { return !BusManager::canAllShow(); } // return true if the strip is being sent pixel updates + inline bool isServicing() const { return _isServicing; } // returns true if strip.service() is executing + inline bool hasWhiteChannel() const { return _hasWhiteChannel; } // returns true if strip contains separate white chanel + inline bool isOffRefreshRequired() const { return _isOffRefreshRequired; } // returns true if strip requires regular updates (i.e. TM1814 chipset) + inline bool isSuspended() const { return _suspend; } // returns true if strip.service() execution is suspended + inline bool needsUpdate() const { return _triggered; } // returns true if strip received a trigger() request uint8_t paletteBlend, cctBlending, - getActiveSegmentsNum(void), - getFirstSelectedSegId(void), - getLastActiveSegmentId(void), - getActiveSegsLightCapabilities(bool selectedOnly = false); + getActiveSegmentsNum() const, + getFirstSelectedSegId() const, + getLastActiveSegmentId() const, + getActiveSegsLightCapabilities(bool selectedOnly = false) const, + addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name); // add effect to the list; defined in FX.cpp; - 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() const { return _brightness; } // returns current strip brightness + inline static constexpr unsigned getMaxSegments() { return MAX_NUM_SEGMENTS; } // returns maximum number of supported segments (fixed value) + inline uint8_t getSegmentsNum() const { return _segments.size(); } // returns currently present segments + inline uint8_t getCurrSegmentId() const { return _segment_index; } // returns current segment index (only valid while strip.isServicing()) + inline uint8_t getMainSegmentId() const { return _mainSegment; } // returns main segment index + inline uint8_t getPaletteCount() const { return 13 + GRADIENT_PALETTE_COUNT + customPalettes.size(); } + inline uint8_t getTargetFps() const { return _targetFps; } // returns rough FPS value for las 2s interval + inline uint8_t getModeCount() const { return _modeCount; } // returns number of registered modes/effects uint16_t - getLengthPhysical(void), - getLengthTotal(void), // will include virtual/nonexistent pixels in matrix - getFps(), - getMappedPixelIndex(uint16_t index); + getLengthPhysical() const, + getLengthTotal() const; // will include virtual/nonexistent pixels in matrix - 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 getFps() const { return (millis() - _lastShow > 2000) ? 0 : (FPS_MULTIPLIER * _cumulativeFps) >> FPS_CALC_SHIFT; } // Returns the refresh rate of the LED strip (_cumulativeFps is stored in fixed point) + inline uint16_t getFrameTime() const { return _frametime; } // returns amount of time a frame should take (in ms) + inline uint16_t getMinShowDelay() const { return MIN_FRAME_DELAY; } // returns minimum amount of time strip.service() can be delayed (constant) + inline uint16_t getLength() const { return _length; } // returns actual amount of LEDs on a strip (2D matrix may have less LEDs than W*H) + inline uint16_t getTransition() const { return _transitionDur; } // returns currently set transition time (in ms) + inline uint16_t getMappedPixelIndex(uint16_t index) const { // convert logical address to physical + if (index < customMappingSize && (realtimeMode == REALTIME_MODE_INACTIVE || realtimeRespectLedMaps)) index = customMappingTable[index]; + return index; + }; - uint32_t - now, - timebase, - getPixelColor(uint16_t); + unsigned long now, timebase; + uint32_t getPixelColor(unsigned) 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() const { return _lastShow; } // returns millis() timestamp of last strip.show() call - const char * - getModeData(uint8_t id = 0) { return (id && id<_modeCount) ? _modeData[id] : PSTR("Solid"); } + const char *getModeData(unsigned id = 0) const { return (id && id < _modeCount) ? _modeData[id] : PSTR("Solid"); } + inline const char **getModeDataSrc() { return &(_modeData[0]); } // vectors use arrays for underlying data - const char ** - getModeDataSrc(void) { return &(_modeData[0]); } // vectors use arrays for underlying data - - Segment& getSegment(uint8_t id); - inline Segment& getFirstSelectedSeg(void) { return _segments[getFirstSelectedSegId()]; } // returns reference to first segment that is "selected" - inline Segment& getMainSegment(void) { return _segments[getMainSegmentId()]; } // returns reference to main segment - inline Segment* getSegments(void) { return &(_segments[0]); } // returns pointer to segment vector structure (warning: use carefully) + Segment& getSegment(unsigned id); + inline Segment& getFirstSelectedSeg() { return _segments[getFirstSelectedSegId()]; } // returns reference to first segment that is "selected" + inline Segment& getMainSegment() { return _segments[getMainSegmentId()]; } // returns reference to main segment + inline Segment* getSegments() { return &(_segments[0]); } // returns pointer to segment vector structure (warning: use carefully) // 2D support (panels) bool isMatrix; #ifndef WLED_DISABLE_2D - #define WLED_MAX_PANELS 64 + #define WLED_MAX_PANELS 18 uint8_t panels; @@ -885,18 +922,18 @@ 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) + void loadCustomPalettes(); // loads custom palettes from JSON std::vector customPalettes; // TODO: move custom palettes out of WS2812FX class - // using public variables to reduce code size increase due to inline function getSegment() (with bounds checking) - // and color transitions - uint32_t _colors_t[3]; // color used for effect (includes transition) - uint16_t _virtualSegmentLength; + struct { + bool autoSegments : 1; + bool correctWB : 1; + bool cctFromRgb : 1; + }; std::vector _segments; friend class Segment; @@ -930,17 +967,10 @@ class WS2812FX { // 96 bytes uint16_t customMappingSize; unsigned long _lastShow; + unsigned long _lastServiceShow; 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 7aecd2271..f00e7147d 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -1,24 +1,9 @@ /* FX_2Dfcn.cpp contains all 2D utility functions - LICENSE - The MIT License (MIT) Copyright (c) 2022 Blaz Kristan (https://blaz.at/home) - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. + Licensed under the EUPL v. 1.2 or later + Adapted from code originally licensed under the MIT license Parts of the code adapted from WLED Sound Reactive */ @@ -65,9 +50,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 +95,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 +124,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(); @@ -160,79 +146,87 @@ void WS2812FX::setUpMatrix() { #ifndef WLED_DISABLE_2D // XY(x,y) - gets pixel index within current segment (often used to reference leds[] array element) -uint16_t IRAM_ATTR Segment::XY(uint16_t x, uint16_t y) +uint16_t IRAM_ATTR_YN Segment::XY(int x, int 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) - return isActive() ? (x%width) + (y%height) * width : 0; + const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) + const int vH = vHeight(); // segment height in logical pixels (is always >= 1) + return isActive() ? (x%vW) + (y%vH) * vW : 0; } -void IRAM_ATTR Segment::setPixelColorXY(int x, int y, uint32_t col) +// raw setColor function without checks (checks are done in setPixelColorXY()) +void IRAM_ATTR_YN Segment::_setPixelColorXY_raw(int& x, int& y, uint32_t& col) +{ + const int baseX = start + x; + const int baseY = startY + y; +#ifndef WLED_DISABLE_MODE_BLEND + // if blending modes, blend with underlying pixel + if (_modeBlend) col = color_blend16(strip.getPixelColorXY(baseX, baseY), col, 0xFFFFU - progress()); +#endif + strip.setPixelColorXY(baseX, baseY, col); + + // Apply mirroring + if (mirror || mirror_y) { + auto setMirroredPixel = [&](int mx, int my) { + strip.setPixelColorXY(mx, my, col); + }; + + const int mirrorX = start + width() - x - 1; + const int mirrorY = startY + height() - y - 1; + + if (mirror) setMirroredPixel(transpose ? baseX : mirrorX, transpose ? mirrorY : baseY); + if (mirror_y) setMirroredPixel(transpose ? mirrorX : baseX, transpose ? baseY : mirrorY); + if (mirror && mirror_y) setMirroredPixel(mirrorX, mirrorY); + } +} + +void IRAM_ATTR_YN Segment::setPixelColorXY(int x, int y, uint32_t col) { if (!isActive()) return; // not active - if (x >= virtualWidth() || y >= virtualHeight() || x<0 || y<0) return; // if pixel would fall out of virtual segment just exit - uint8_t _bri_t = currentBri(); - if (_bri_t < 255) { - 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); - } + const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) + const int vH = vHeight(); // segment height in logical pixels (is always >= 1) + // negative values of x & y cast into unsigend will become very large values and will therefore be greater than vW/vH + if (unsigned(x) >= unsigned(vW) || unsigned(y) >= unsigned(vH)) return; // if pixel would fall out of virtual segment just exit - 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 color is unscaled + if (!_colorScaled) col = color_fade(col, _segBri); - 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 + if (reverse ) x = vW - x - 1; + if (reverse_y) y = vH - y - 1; + if (transpose) { std::swap(x,y); } // swap X & Y if segment transposed + unsigned groupLen = groupLength(); - 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 - -#ifndef WLED_DISABLE_MODE_BLEND - // if blending modes, blend with underlying pixel - if (_modeBlend) tmpCol = color_blend(strip.getPixelColorXY(start + xX, startY + yY), col, 0xFFFFU - progress(), true); -#endif - - strip.setPixelColorXY(start + xX, startY + yY, tmpCol); - - if (mirror) { //set the corresponding horizontally mirrored pixel - if (transpose) strip.setPixelColorXY(start + xX, startY + height() - yY - 1, tmpCol); - else strip.setPixelColorXY(start + width() - xX - 1, startY + yY, tmpCol); - } - if (mirror_y) { //set the corresponding vertically mirrored pixel - if (transpose) strip.setPixelColorXY(start + width() - xX - 1, startY + yY, tmpCol); - else strip.setPixelColorXY(start + xX, startY + height() - yY - 1, tmpCol); - } - if (mirror_y && mirror) { //set the corresponding vertically AND horizontally mirrored pixel - strip.setPixelColorXY(width() - xX - 1, height() - yY - 1, tmpCol); + if (groupLen > 1) { + int W = width(); + int H = height(); + x *= groupLen; // expand to physical pixels + y *= groupLen; // expand to physical pixels + const int maxY = std::min(y + grouping, H); + const int maxX = std::min(x + grouping, W); + for (int yY = y; yY < maxY; yY++) { + for (int xX = x; xX < maxX; xX++) { + _setPixelColorXY_raw(xX, yY, col); } } + } else { + _setPixelColorXY_raw(x, y, col); } } +#ifdef WLED_USE_AA_PIXELS // anti-aliased version of setPixelColorXY() void Segment::setPixelColorXY(float x, float y, uint32_t col, bool aa) { 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(); - - float fX = x * (cols-1); - float fY = y * (rows-1); + float fX = x * (vWidth()-1); + float fY = y * (vHeight()-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,163 +254,201 @@ 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_YN 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 + const int vW = vWidth(); + const int vH = vHeight(); + if (unsigned(x) >= unsigned(vW) || unsigned(y) >= unsigned(vH)) return 0; // if pixel would fall out of virtual segment just exit + if (reverse ) x = vW - x - 1; + if (reverse_y) y = vH - y - 1; + if (transpose) { std::swap(x,y); } // swap X & Y if segment transposed x *= groupLength(); // expand to physical pixels y *= groupLength(); // expand to physical pixels if (x >= width() || y >= height()) return 0; return strip.getPixelColorXY(start + x, startY + y); } -// blurRow: perform a blur on a row of a rectangular matrix -void Segment::blurRow(uint16_t row, fract8 blur_amount) { - if (!isActive() || blur_amount == 0) return; // not active - const uint_fast16_t cols = virtualWidth(); - const uint_fast16_t rows = virtualHeight(); +// 2D blurring, can be asymmetrical +void Segment::blur2D(uint8_t blur_x, uint8_t blur_y, bool smear) { + if (!isActive()) return; // not active + const unsigned cols = vWidth(); + const unsigned rows = vHeight(); + uint32_t lastnew; + uint32_t last; + if (blur_x) { + const uint8_t keepx = smear ? 255 : 255 - blur_x; + const uint8_t seepx = blur_x >> 1; + for (unsigned row = 0; row < rows; row++) { // blur rows (x direction) + uint32_t carryover = BLACK; + uint32_t curnew = BLACK; + for (unsigned x = 0; x < cols; x++) { + uint32_t cur = getPixelColorXY(x, row); + uint32_t part = color_fade(cur, seepx); + curnew = color_fade(cur, keepx); + if (x > 0) { + if (carryover) curnew = color_add(curnew, carryover); + uint32_t prev = color_add(lastnew, part); + // optimization: only set pixel if color has changed + if (last != prev) 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 + } + } + if (blur_y) { + const uint8_t keepy = smear ? 255 : 255 - blur_y; + const uint8_t seepy = blur_y >> 1; + for (unsigned col = 0; col < cols; col++) { + uint32_t carryover = BLACK; + uint32_t curnew = BLACK; + for (unsigned y = 0; y < rows; y++) { + uint32_t cur = getPixelColorXY(col, y); + uint32_t part = color_fade(cur, seepy); + curnew = color_fade(cur, keepy); + if (y > 0) { + if (carryover) curnew = color_add(curnew, carryover); + uint32_t prev = color_add(lastnew, part); + // optimization: only set pixel if color has changed + if (last != prev) 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); + } + } +} - if (row >= rows) return; - // blur one row - uint8_t keep = 255 - blur_amount; - uint8_t seep = blur_amount >> 1; - CRGB carryover = CRGB::Black; +/* +// 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 = vWidth(); + const unsigned rows = vHeight(); + uint16_t *tmpRSum = new uint16_t[cols*rows]; + uint16_t *tmpGSum = new uint16_t[cols*rows]; + uint16_t *tmpBSum = new uint16_t[cols*rows]; + 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++) { - 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); + 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; } - if (before != cur) // optimization: only set pixel if color has changed - setPixelColorXY(x, row, cur); - carryover = part; } -} - -// blurCol: perform a blur on a column of a rectangular matrix -void Segment::blurCol(uint16_t col, fract8 blur_amount) { - if (!isActive() || blur_amount == 0) return; // not active - const uint_fast16_t cols = virtualWidth(); - const uint_fast16_t rows = virtualHeight(); - - if (col >= cols) return; - // blur one column - uint8_t keep = 255 - blur_amount; - uint8_t seep = blur_amount >> 1; - CRGB carryover = CRGB::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); + // 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)); } - if (before != cur) // optimization: only set pixel if color has changed - setPixelColorXY(col, y, cur); - carryover = part; } + delete[] tmpRSum; + delete[] tmpGSum; + delete[] tmpBSum; + delete[] tmpWSum; } - -// 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) { - 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); +*/ +void Segment::moveX(int delta, bool wrap) { + if (!isActive() || !delta) return; // not active + const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) + const int vH = vHeight(); // segment height in logical pixels (is always >= 1) + int absDelta = abs(delta); + if (absDelta >= vW) return; + uint32_t newPxCol[vW]; + int newDelta; + int stop = vW; + int start = 0; + if (wrap) newDelta = (delta + vW) % vW; // +cols in case delta < 0 + else { + if (delta < 0) start = absDelta; + stop = vW - absDelta; + newDelta = delta > 0 ? delta : 0; } - for (int j = 0; j < dim1; j++) { - uint16_t x = vertical ? i : j; - uint16_t y = vertical ? j : i; - setPixelColorXY(x, y, tmp[j]); - } -} - -// 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); -} - -void Segment::moveX(int8_t delta, bool wrap) { - if (!isActive()) return; // not active - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); - if (!delta || abs(delta) >= cols) return; - uint32_t newPxCol[cols]; - for (int y = 0; y < rows; y++) { - if (delta > 0) { - for (int x = 0; x < cols-delta; x++) newPxCol[x] = getPixelColorXY((x + delta), y); - for (int x = cols-delta; x < cols; x++) newPxCol[x] = getPixelColorXY(wrap ? (x + delta) - cols : x, y); - } else { - for (int x = cols-1; x >= -delta; x--) newPxCol[x] = getPixelColorXY((x + delta), y); - for (int x = -delta-1; x >= 0; x--) newPxCol[x] = getPixelColorXY(wrap ? (x + delta) + cols : x, y); + for (int y = 0; y < vH; y++) { + for (int x = 0; x < stop; x++) { + int srcX = x + newDelta; + if (wrap) srcX %= vW; // Wrap using modulo when `wrap` is true + newPxCol[x] = getPixelColorXY(srcX, y); } - for (int x = 0; x < cols; x++) setPixelColorXY(x, y, newPxCol[x]); + for (int x = 0; x < stop; x++) setPixelColorXY(x + start, y, newPxCol[x]); } } -void Segment::moveY(int8_t delta, bool wrap) { - if (!isActive()) return; // not active - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); - if (!delta || abs(delta) >= rows) return; - uint32_t newPxCol[rows]; - for (int x = 0; x < cols; x++) { - if (delta > 0) { - for (int y = 0; y < rows-delta; y++) newPxCol[y] = getPixelColorXY(x, (y + delta)); - for (int y = rows-delta; y < rows; y++) newPxCol[y] = getPixelColorXY(x, wrap ? (y + delta) - rows : y); - } else { - for (int y = rows-1; y >= -delta; y--) newPxCol[y] = getPixelColorXY(x, (y + delta)); - for (int y = -delta-1; y >= 0; y--) newPxCol[y] = getPixelColorXY(x, wrap ? (y + delta) + rows : y); +void Segment::moveY(int delta, bool wrap) { + if (!isActive() || !delta) return; // not active + const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) + const int vH = vHeight(); // segment height in logical pixels (is always >= 1) + int absDelta = abs(delta); + if (absDelta >= vH) return; + uint32_t newPxCol[vH]; + int newDelta; + int stop = vH; + int start = 0; + if (wrap) newDelta = (delta + vH) % vH; // +rows in case delta < 0 + else { + if (delta < 0) start = absDelta; + stop = vH - absDelta; + newDelta = delta > 0 ? delta : 0; + } + for (int x = 0; x < vW; x++) { + for (int y = 0; y < stop; y++) { + int srcY = y + newDelta; + if (wrap) srcY %= vH; // Wrap using modulo when `wrap` is true + newPxCol[y] = getPixelColorXY(x, srcY); } - for (int y = 0; y < rows; y++) setPixelColorXY(x, y, newPxCol[y]); + for (int y = 0; y < stop; y++) setPixelColorXY(x, y + start, newPxCol[y]); } } @@ -424,7 +456,7 @@ void Segment::moveY(int8_t delta, bool wrap) { // @param dir direction: 0=left, 1=left-up, 2=up, 3=right-up, 4=right, 5=right-down, 6=down, 7=left-down // @param delta number of pixels to move // @param wrap around -void Segment::move(uint8_t dir, uint8_t delta, bool wrap) { +void Segment::move(unsigned dir, unsigned delta, bool wrap) { if (delta==0) return; switch (dir) { case 0: moveX( delta, wrap); break; @@ -438,69 +470,143 @@ 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 + const int rsq = radius*radius; + int x = 0; + int y = radius; + unsigned oldFade = 0; + while (x < y) { + float yf = sqrtf(float(rsq - x*x)); // needs to be floating point + uint8_t fade = float(0xFF) * (ceilf(yf) - yf); // how much color to keep + if (oldFade > fade) y--; + oldFade = fade; + int px, py; + for (uint8_t i = 0; i < 16; i++) { + int swaps = (i & 0x4 ? 1 : 0); // 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1 + int adj = (i < 8) ? 0 : 1; // 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1 + int dx = (i & 1) ? -1 : 1; // 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1 + int dy = (i & 2) ? -1 : 1; // 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1 + if (swaps) { + px = cx + (y - adj) * dx; + py = cy + x * dy; + } else { + px = cx + x * dx; + py = cy + (y - adj) * dy; + } + uint32_t pixCol = getPixelColorXY(px, py); + setPixelColorXY(px, py, adj ? + color_blend(pixCol, col, fade) : + color_blend(col, pixCol, fade)); + } + x++; } + } else { + // pre-scale color for all pixels + col = color_fade(col, _segBri); + _colorScaled = true; + // Bresenham’s Algorithm + int d = 3 - (2*radius); + int y = radius, x = 0; + while (y >= x) { + for (int i = 0; i < 4; i++) { + int dx = (i & 1) ? -x : x; + int dy = (i & 2) ? -y : y; + setPixelColorXY(cx + dx, cy + dy, col); + setPixelColorXY(cx + dy, cy + dx, col); + } + x++; + if (d > 0) { + y--; + d += 4 * (x - y) + 10; + } else { + d += 4 * x + 6; + } + } + _colorScaled = false; } } // 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++) { + const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) + const int vH = vHeight(); // segment height in logical pixels (is always >= 1) + // draw soft bounding circle + if (soft) drawCircle(cx, cy, radius, col, soft); + // pre-scale color for all pixels + col = color_fade(col, _segBri); + _colorScaled = true; + // fill it + 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 < vW && int(cy)+y < vH) setPixelColorXY(cx + x, cy + y, col); } } -} - -void Segment::nscale8(uint8_t scale) { - if (!isActive()) return; // not active - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); - for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) { - setPixelColorXY(x, y, CRGB(getPixelColorXY(x, y)).nscale8(scale)); - } + _colorScaled = false; } //line function -void Segment::drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c) { +void Segment::drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft) { if (!isActive()) return; // not active - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); - if (x0 >= 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 vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) + const int vH = vHeight(); // segment height in logical pixels (is always >= 1) + if (x0 >= vW || x1 >= vW || y0 >= vH || y1 >= vH) return; + + const int dx = abs(x1-x0), sx = x0 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++) { + uint8_t keep = float(0xFF) * (intersectY-int(intersectY)); // how much color to keep + uint8_t seep = 0xFF - 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)); + setPixelColorXY(x+int(steep), y+int(!steep), color_blend(c, getPixelColorXY(x+int(steep), y+int(!steep)), seep)); + intersectY += gradient; + if (steep) std::swap(x,y); // restore if steep + } + } else { + // pre-scale color for all pixels + c = color_fade(c, _segBri); + _colorScaled = true; + // 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; } + } + _colorScaled = false; } } @@ -512,16 +618,15 @@ void Segment::drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint3 // draws a raster font character on canvas // only supports: 4x6=24, 5x8=40, 5x12=60, 6x8=48 and 7x9=63 fonts ATM -void Segment::drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t col2, int8_t rotate) { +void Segment::drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t col2, int8_t rotate, bool usePalGrad) { 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 font = w*h; CRGB col = CRGB(color); CRGBPalette16 grad = CRGBPalette16(col, col2 ? CRGB(col2) : col); + if(usePalGrad) grad = SEGPALETTE; // selected palette as gradient //if (w<5 || w>6 || h!=8) return; for (int i = 0; i= cols || y0 < 0 || y0 >= rows) continue; // drawing off-screen + if (x0 < 0 || x0 >= (int)vWidth() || y0 < 0 || y0 >= (int)vHeight()) continue; // drawing off-screen if (((bits>>(j+(8-w))) & 0x01)) { // bit set - setPixelColorXY(x0, y0, col); + setPixelColorXY(x0, y0, c); } } + _colorScaled = false; } } @@ -556,17 +665,20 @@ 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)}; // multiply the intensities by the colour, and saturating-add them to the pixels for (int i = 0; i < 4; i++) { - CRGB led = getPixelColorXY((x >> 8) + (i & 1), (y >> 8) + ((i >> 1) & 1)); + int wu_x = (x >> 8) + (i & 1); // precalculate x + int wu_y = (y >> 8) + ((i >> 1) & 1); // precalculate y + CRGB led = getPixelColorXY(wu_x, wu_y); + CRGB oldLed = led; led.r = qadd8(led.r, c.r * wu[i] >> 8); led.g = qadd8(led.g, c.g * wu[i] >> 8); led.b = qadd8(led.b, c.b * wu[i] >> 8); - setPixelColorXY(int((x >> 8) + (i & 1)), int((y >> 8) + ((i >> 1) & 1)), led); + if (led != oldLed) setPixelColorXY(wu_x, wu_y, led); // don't repaint if same color } } #undef WU_WEIGHT diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 7a2c00574..af277718d 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -2,24 +2,10 @@ WS2812FX_fcn.cpp contains all utility functions Harm Aldick - 2016 www.aldick.org - LICENSE - The MIT License (MIT) + Copyright (c) 2016 Harm Aldick - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. + Licensed under the EUPL v. 1.2 or later + Adapted from code originally licensed under the MIT license Modified heavily for WLED */ @@ -43,21 +29,16 @@ 19, 18, 17, 16, 15, 20, 21, 22, 23, 24, 29, 28, 27, 26, 25]} */ -//factory defaults LED setup -//#define PIXEL_COUNTS 30, 30, 30, 30 -//#define DATA_PINS 16, 1, 3, 4 -//#define DEFAULT_LED_TYPE TYPE_WS2812_RGB - #ifndef PIXEL_COUNTS #define PIXEL_COUNTS DEFAULT_LED_COUNT #endif #ifndef DATA_PINS - #define DATA_PINS LEDPIN + #define DATA_PINS DEFAULT_LED_PIN #endif -#ifndef DEFAULT_LED_TYPE - #define DEFAULT_LED_TYPE TYPE_WS2812_RGB +#ifndef LED_TYPES + #define LED_TYPES DEFAULT_LED_TYPE #endif #ifndef DEFAULT_LED_COLOR_ORDER @@ -69,18 +50,37 @@ #error "Max segments must be at least max number of busses!" #endif +static constexpr unsigned sumPinsRequired(const unsigned* current, size_t count) { + return (count > 0) ? (Bus::getNumberOfPins(*current) + sumPinsRequired(current+1,count-1)) : 0; +} + +static constexpr bool validatePinsAndTypes(const unsigned* types, unsigned numTypes, unsigned numPins ) { + // Pins provided < pins required -> always invalid + // Pins provided = pins required -> always valid + // Pins provided > pins required -> valid if excess pins are a product of last type pins since it will be repeated + return (sumPinsRequired(types, numTypes) > numPins) ? false : + (numPins - sumPinsRequired(types, numTypes)) % Bus::getNumberOfPins(types[numTypes-1]) == 0; +} + /////////////////////////////////////////////////////////////////////////////// // Segment class implementation /////////////////////////////////////////////////////////////////////////////// -uint16_t Segment::_usedSegmentData = 0U; // amount of RAM all segments use for their data[] -uint16_t Segment::maxWidth = DEFAULT_LED_COUNT; -uint16_t Segment::maxHeight = 1; - +unsigned Segment::_usedSegmentData = 0U; // amount of RAM all segments use for their data[] +uint16_t Segment::maxWidth = DEFAULT_LED_COUNT; +uint16_t Segment::maxHeight = 1; +unsigned Segment::_vLength = 0; +unsigned Segment::_vWidth = 0; +unsigned Segment::_vHeight = 0; +uint8_t Segment::_segBri = 0; +uint32_t Segment::_currentColors[NUM_COLORS] = {0,0,0}; +bool Segment::_colorScaled = false; +CRGBPalette16 Segment::_currentPalette = CRGBPalette16(CRGB::Black); CRGBPalette16 Segment::_randomPalette = generateRandomPalette(); // was CRGBPalette16(DEFAULT_COLOR); CRGBPalette16 Segment::_newRandomPalette = generateRandomPalette(); // was CRGBPalette16(DEFAULT_COLOR); uint16_t Segment::_lastPaletteChange = 0; // perhaps it should be per segment uint16_t Segment::_lastPaletteBlend = 0; //in millis (lowest 16 bits only) +uint16_t Segment::_transitionprogress = 0xFFFF; #ifndef WLED_DISABLE_MODE_BLEND bool Segment::_modeBlend = false; @@ -145,7 +145,7 @@ Segment& Segment::operator= (Segment &&orig) noexcept { } // allocates effect data buffer on heap and initialises (erases) it -bool IRAM_ATTR Segment::allocateData(size_t len) { +bool IRAM_ATTR_YN Segment::allocateData(size_t len) { if (len == 0) return false; // nothing to do if (data && _dataLen >= len) { // already allocated enough (reduce fragmentation) if (call == 0) memset(data, 0, len); // erase buffer if called during effect initialisation @@ -169,17 +169,13 @@ bool IRAM_ATTR Segment::allocateData(size_t len) { return true; } -void IRAM_ATTR Segment::deallocateData() { +void IRAM_ATTR_YN Segment::deallocateData() { if (!data) { _dataLen = 0; return; } //DEBUG_PRINTF_P(PSTR("--- Released data (%p): %d/%d -> %p\n"), this, _dataLen, Segment::getUsedSegmentData(), data); if ((Segment::getUsedSegmentData() > 0) && (_dataLen > 0)) { // check that we don't have a dangling / inconsistent data pointer free(data); } else { - DEBUG_PRINT(F("---- Released data ")); - DEBUG_PRINTF_P(PSTR("(%p): "), this); - DEBUG_PRINT(F("inconsistent UsedSegmentData ")); - DEBUG_PRINTF_P(PSTR("(%d/%d)"), _dataLen, Segment::getUsedSegmentData()); - DEBUG_PRINTLN(F(", cowardly refusing to free nothing.")); + DEBUG_PRINTF_P(PSTR("---- Released data (%p): inconsistent UsedSegmentData (%d/%d), cowardly refusing to free nothing.\n"), this, _dataLen, Segment::getUsedSegmentData()); } data = nullptr; Segment::addUsedSegmentData(_dataLen <= Segment::getUsedSegmentData() ? -_dataLen : -Segment::getUsedSegmentData()); @@ -204,28 +200,16 @@ void Segment::resetIfRequired() { #endif } -CRGBPalette16 IRAM_ATTR &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { +CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { if (pal < 245 && pal > GRADIENT_PALETTE_COUNT+13) pal = 0; - if (pal > 245 && (strip.customPalettes.size() == 0 || 255U-pal > strip.customPalettes.size()-1)) pal = 0; + 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 - case FX_MODE_COLORWAVES : pal = 26; break; // landscape 33 - case FX_MODE_FILLNOISE8 : pal = 9; break; // ocean colors - case FX_MODE_NOISE16_1 : pal = 20; break; // Drywet - case FX_MODE_NOISE16_2 : pal = 43; break; // Blue cyan yellow - case FX_MODE_NOISE16_3 : pal = 35; break; // heat palette - case FX_MODE_NOISE16_4 : pal = 26; break; // landscape 33 - case FX_MODE_GLITTER : pal = 11; break; // rainbow colors - case FX_MODE_SUNRISE : pal = 35; break; // heat palette - case FX_MODE_RAILWAY : pal = 3; break; // prim + sec - case FX_MODE_2DSOAP : pal = 11; break; // rainbow colors - } + if (pal == 0) pal = _default_palette; //load default palette set in FX _data, party colors as default switch (pal) { case 0: //default palette. Exceptions for specific effects above targetPalette = PartyColors_p; break; case 1: //randomly generated palette - targetPalette = _randomPalette; //random palette is generated at intervals in handleRandomPalette() + targetPalette = _randomPalette; //random palette is generated at intervals in handleRandomPalette() break; case 2: {//primary color only CRGB prim = gamma32(colors[0]); @@ -249,23 +233,11 @@ CRGBPalette16 IRAM_ATTR &Segment::loadPalette(CRGBPalette16 &targetPalette, uint targetPalette = CRGBPalette16(prim,prim,prim,prim,prim,prim,prim,prim,sec,sec,sec,sec,sec,sec,sec,sec); } break;} - case 6: //Party colors - targetPalette = PartyColors_p; break; - case 7: //Cloud colors - targetPalette = CloudColors_p; break; - case 8: //Lava colors - targetPalette = LavaColors_p; break; - case 9: //Ocean colors - targetPalette = OceanColors_p; break; - case 10: //Forest colors - targetPalette = ForestColors_p; break; - case 11: //Rainbow colors - targetPalette = RainbowColors_p; break; - case 12: //Rainbow stripe colors - targetPalette = RainbowStripeColors_p; break; default: //progmem palettes if (pal>245) { targetPalette = strip.customPalettes[255-pal]; // we checked bounds above + } else if (pal < 13) { // palette 6 - 12, fastled palettes + targetPalette = *fastledPalettes[pal-6]; } else { byte tcp[72]; memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[pal-13])), 72); @@ -329,18 +301,13 @@ 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() { +inline void Segment::updateTransitionProgress() { + _transitionprogress = 0xFFFFU; 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) _transitionprogress = diff * 0xFFFFU / _t->_dur; } - return 0xFFFFU; } #ifndef WLED_DISABLE_MODE_BLEND @@ -414,68 +381,84 @@ void Segment::restoreSegenv(tmpsegd_t &tmpSeg) { } #endif -uint8_t IRAM_ATTR Segment::currentBri(bool useCct) { - uint32_t prog = progress(); +uint8_t 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 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 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]; + return isInTransition() ? color_blend16(_t->_segT._colorT[slot], colors[slot], progress()) : colors[slot]; #else - return isInTransition() ? color_blend(_t->_colorT[slot], colors[slot], progress(), true) : colors[slot]; + return isInTransition() ? color_blend16(_t->_colorT[slot], colors[slot], progress()) : colors[slot]; #endif } -CRGBPalette16 IRAM_ATTR &Segment::currentPalette(CRGBPalette16 &targetPalette, uint8_t pal) { - loadPalette(targetPalette, pal); - uint16_t prog = progress(); +// pre-calculate drawing parameters for faster access (based on the idea from @softhack007 from MM fork) +void Segment::beginDraw() { + _vWidth = virtualWidth(); + _vHeight = virtualHeight(); + _vLength = virtualLength(); + _segBri = currentBri(); + // adjust gamma for effects + for (unsigned i = 0; i < NUM_COLORS; i++) { + #ifndef WLED_DISABLE_MODE_BLEND + uint32_t col = isInTransition() ? color_blend16(_t->_segT._colorT[i], colors[i], progress()) : colors[i]; + #else + uint32_t col = isInTransition() ? color_blend16(_t->_colorT[i], colors[i], progress()) : colors[i]; + #endif + _currentColors[i] = gamma32(col); + } + // load palette into _currentPalette + loadPalette(_currentPalette, palette); + unsigned prog = progress(); if (strip.paletteFade && prog < 0xFFFFU) { // blend palettes // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in _dur time) // minimum blend time is 100ms maximum is 65535ms - 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); } -// segId is given when called from network callback, changes are queued if that segment is currently in its effect function -void Segment::setUp(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t ofs, uint16_t i1Y, uint16_t i2Y) { +// sets Segment geometry (length or width/height and grouping, spacing and offset as well as 2D mapping) +// strip must be suspended (strip.suspend()) before calling this function +// this function may call fill() to clear pixels if spacing or mapping changed (which requires setting _vWidth, _vHeight, _vLength or beginDraw()) +void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t ofs, uint16_t i1Y, uint16_t i2Y, uint8_t m12) { // return if neither bounds nor grouping have changed bool boundsUnchanged = (start == i1 && stop == i2); #ifndef WLED_DISABLE_2D @@ -483,11 +466,19 @@ void Segment::setUp(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t #endif if (boundsUnchanged && (!grp || (grouping == grp && spacing == spc)) - && (ofs == UINT16_MAX || ofs == offset)) return; + && (ofs == UINT16_MAX || ofs == offset) + && (m12 == map1D2D) + ) return; stateChanged = true; // send UDP/WS broadcast - if (stop) fill(BLACK); // turn old segment range off (clears pixels if changing spacing) + if (stop || spc != spacing || m12 != map1D2D) { + _vWidth = virtualWidth(); + _vHeight = virtualHeight(); + _vLength = virtualLength(); + _segBri = currentBri(); + fill(BLACK); // turn old segment range off or clears pixels if changing spacing (requires _vWidth/_vHeight/_vLength/_segBri) + } if (grp) { // prevent assignment of 0 grouping = grp; spacing = spc; @@ -496,6 +487,7 @@ void Segment::setUp(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t spacing = 0; } if (ofs < UINT16_MAX) offset = ofs; + map1D2D = constrain(m12, 0, 7); DEBUG_PRINT(F("setUp segment: ")); DEBUG_PRINT(i1); DEBUG_PRINT(','); DEBUG_PRINT(i2); @@ -528,46 +520,53 @@ void Segment::setUp(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t } -bool Segment::setColor(uint8_t slot, uint32_t c) { //returns true if changed - if (slot >= NUM_COLORS || c == colors[slot]) return false; +Segment &Segment::setColor(uint8_t slot, uint32_t c) { + if (slot >= NUM_COLORS || c == colors[slot]) return *this; if (!_isRGB && !_hasW) { - if (slot == 0 && c == BLACK) return false; // on/off segment cannot have primary color black - if (slot == 1 && c != BLACK) return false; // on/off segment cannot have secondary color non black + if (slot == 0 && c == BLACK) return *this; // on/off segment cannot have primary color black + if (slot == 1 && c != BLACK) return *this; // on/off segment cannot have secondary color non black } if (fadeTransition) startTransition(strip.getTransition()); // start transition prior to change colors[slot] = c; stateChanged = true; // send UDP/WS broadcast - return true; + return *this; } -void Segment::setCCT(uint16_t k) { +Segment &Segment::setCCT(uint16_t k) { if (k > 255) { //kelvin value, convert to 0-255 if (k < 1900) k = 1900; if (k > 10091) k = 10091; k = (k - 1900) >> 5; } - if (cct == k) return; - if (fadeTransition) startTransition(strip.getTransition()); // start transition prior to change - cct = k; - stateChanged = true; // send UDP/WS broadcast + if (cct != k) { + //DEBUGFX_PRINTF_P(PSTR("- Starting CCT transition: %d\n"), k); + startTransition(strip.getTransition()); // start transition prior to change + cct = k; + stateChanged = true; // send UDP/WS broadcast + } + return *this; } -void Segment::setOpacity(uint8_t o) { - if (opacity == o) return; - if (fadeTransition) startTransition(strip.getTransition()); // start transition prior to change - opacity = o; - stateChanged = true; // send UDP/WS broadcast +Segment &Segment::setOpacity(uint8_t o) { + if (opacity != o) { + //DEBUGFX_PRINTF_P(PSTR("- Starting opacity transition: %d\n"), o); + startTransition(strip.getTransition()); // start transition prior to change + opacity = o; + stateChanged = true; // send UDP/WS broadcast + } + return *this; } -void Segment::setOption(uint8_t n, bool val) { +Segment &Segment::setOption(uint8_t n, bool val) { bool prevOn = on; if (fadeTransition && n == SEG_OPTION_ON && val != prevOn) startTransition(strip.getTransition()); // start transition prior to change if (val) options |= 0x01 << n; else options &= ~(0x01 << n); if (!(n == SEG_OPTION_SELECTED || n == SEG_OPTION_RESET)) stateChanged = true; // send UDP/WS broadcast + return *this; } -void Segment::setMode(uint8_t fx, bool loadDefaults) { +Segment &Segment::setMode(uint8_t fx, bool loadDefaults) { // skip reserved while (fx < strip.getModeCount() && strncmp_P("RSVD", strip.getModeData(fx), 4) == 0) fx++; if (fx >= strip.getModeCount()) fx = 0; // set solid mode @@ -577,9 +576,9 @@ void Segment::setMode(uint8_t fx, bool loadDefaults) { if (modeBlending) startTransition(strip.getTransition()); // set effect transitions #endif mode = fx; + int sOpt; // load default values from effect string if (loadDefaults) { - int16_t 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; @@ -596,12 +595,16 @@ void Segment::setMode(uint8_t fx, bool loadDefaults) { sOpt = extractModeDefaults(fx, "mY"); if (sOpt >= 0) mirror_y = (bool)sOpt; // NOTE: setting this option is a risky business sOpt = extractModeDefaults(fx, "pal"); if (sOpt >= 0) setPalette(sOpt); //else setPalette(0); } + sOpt = extractModeDefaults(fx, "pal"); // always extract 'pal' to set _default_palette + if(sOpt <= 0) sOpt = 6; // partycolors if zero or not set + _default_palette = sOpt; // _deault_palette is loaded into pal0 in loadPalette() (if selected) markForReset(); stateChanged = true; // send UDP/WS broadcast } + return *this; } -void Segment::setPalette(uint8_t pal) { +Segment &Segment::setPalette(uint8_t pal) { if (pal < 245 && pal > GRADIENT_PALETTE_COUNT+13) pal = 0; // built in palettes if (pal > 245 && (strip.customPalettes.size() == 0 || 255U-pal > strip.customPalettes.size()-1)) pal = 0; // custom palettes if (pal != palette) { @@ -609,76 +612,120 @@ void Segment::setPalette(uint8_t pal) { palette = pal; stateChanged = true; // send UDP/WS broadcast } + return *this; } // 2D matrix -uint16_t IRAM_ATTR Segment::virtualWidth() const { - uint16_t groupLen = groupLength(); - uint16_t vWidth = ((transpose ? height() : width()) + groupLen - 1) / groupLen; +unsigned Segment::virtualWidth() const { + unsigned groupLen = groupLength(); + unsigned vWidth = ((transpose ? height() : width()) + groupLen - 1) / groupLen; if (mirror) vWidth = (vWidth + 1) /2; // divide by 2 if mirror, leave at least a single LED return vWidth; } -uint16_t IRAM_ATTR Segment::virtualHeight() const { - uint16_t groupLen = groupLength(); - uint16_t vHeight = ((transpose ? width() : height()) + groupLen - 1) / groupLen; +unsigned Segment::virtualHeight() const { + unsigned groupLen = groupLength(); + unsigned vHeight = ((transpose ? width() : height()) + groupLen - 1) / groupLen; if (mirror_y) vHeight = (vHeight + 1) /2; // divide by 2 if mirror, leave at least a single LED return vHeight; } -uint16_t IRAM_ATTR Segment::nrOfVStrips() const { - uint16_t vLen = 1; +// Constants for mapping mode "Pinwheel" #ifndef WLED_DISABLE_2D - if (is2D()) { - switch (map1D2D) { - case M12_pBar: - vLen = virtualWidth(); - break; - } - } -#endif - return vLen; +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 { +uint16_t 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; } -void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) +void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col) { - if (!isActive()) return; // not active + if (!isActive() || i < 0) return; // not active or invalid index #ifndef WLED_DISABLE_2D - int vStrip = i>>16; // hack to allow running on virtual strips (2D segment columns/rows) + int vStrip = 0; #endif - i &= 0xFFFF; - - if (i >= virtualLength() || i<0) return; // if pixel would fall out of segment just exit + int vL = vLength(); + // if the 1D effect is using virtual strips "i" will have virtual strip id stored in upper 16 bits + // in such case "i" will be > virtualLength() + if (i >= vL) { + // check if this is a virtual strip + #ifndef WLED_DISABLE_2D + vStrip = i>>16; // hack to allow running on virtual strips (2D segment columns/rows) + i &= 0xFFFF; //truncate vstrip index + if (i >= vL) return; // if pixel would still fall out of segment just exit + #else + return; + #endif + } #ifndef WLED_DISABLE_2D if (is2D()) { - uint16_t vH = virtualHeight(); // segment height in logical pixels - uint16_t vW = virtualWidth(); + const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) + const int vH = vHeight(); // segment height in logical pixels (is always >= 1) + // pre-scale color for all pixels + col = color_fade(col, _segBri); + _colorScaled = true; switch (map1D2D) { case M12_Pixels: // use all available pixels as a long strip @@ -686,20 +733,22 @@ void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) break; case M12_pBar: // expand 1D effect vertically or have it play on virtual strips - if (vStrip>0) setPixelColorXY(vStrip - 1, vH - i - 1, col); - else for (int x = 0; x < vW; x++) setPixelColorXY(x, vH - i - 1, col); + if (vStrip > 0) setPixelColorXY(vStrip - 1, vH - i - 1, col); + else for (int x = 0; x < vW; x++) setPixelColorXY(x, vH - i - 1, col); break; case M12_pArc: // expand in circular fashion from center - if (i==0) + 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); @@ -721,29 +770,70 @@ 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; + } } + _colorScaled = false; return; - } else if (Segment::maxHeight!=1 && (width()==1 || height()==1)) { + } else if (Segment::maxHeight != 1 && (width() == 1 || height() == 1)) { if (start < Segment::maxWidth*Segment::maxHeight) { // we have a vertical or horizontal 1D segment (WARNING: virtual...() may be transposed) int x = 0, y = 0; - if (virtualHeight()>1) y = i; - if (virtualWidth() >1) x = i; + if (vHeight() > 1) y = i; + if (vWidth() > 1) x = i; setPixelColorXY(x, y, col); return; } } #endif - uint16_t 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); - } + unsigned len = length(); + // if color is unscaled + if (!_colorScaled) col = color_fade(col, _segBri); // expand pixel (taking into account start, grouping, spacing [and offset]) i = i * groupLength(); @@ -766,20 +856,21 @@ void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) indexMir += offset; // offset/phase if (indexMir >= stop) indexMir -= len; // wrap #ifndef WLED_DISABLE_MODE_BLEND - if (_modeBlend) tmpCol = color_blend(strip.getPixelColor(indexMir), col, 0xFFFFU - progress(), true); + if (_modeBlend) tmpCol = color_blend16(strip.getPixelColor(indexMir), col, uint16_t(0xFFFFU - progress())); #endif strip.setPixelColor(indexMir, tmpCol); } indexSet += offset; // offset/phase if (indexSet >= stop) indexSet -= len; // wrap #ifndef WLED_DISABLE_MODE_BLEND - if (_modeBlend) tmpCol = color_blend(strip.getPixelColor(indexSet), col, 0xFFFFU - progress(), true); + if (_modeBlend) tmpCol = color_blend16(strip.getPixelColor(indexSet), col, uint16_t(0xFFFFU - progress())); #endif strip.setPixelColor(indexSet, tmpCol); } } } +#ifdef WLED_USE_AA_PIXELS // anti-aliased normalized version of setPixelColor() void Segment::setPixelColor(float i, uint32_t col, bool aa) { @@ -791,8 +882,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)); @@ -809,50 +900,81 @@ 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_YN Segment::getPixelColor(int i) const { if (!isActive()) return 0; // not active -#ifndef WLED_DISABLE_2D - int vStrip = i>>16; -#endif - i &= 0xFFFF; #ifndef WLED_DISABLE_2D if (is2D()) { - uint16_t vH = virtualHeight(); // segment height in logical pixels - uint16_t vW = virtualWidth(); + const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) + const int vH = vHeight(); // segment height in logical pixels (is always >= 1) switch (map1D2D) { case M12_Pixels: return getPixelColorXY(i % vW, i / vW); break; - case M12_pBar: - if (vStrip>0) return getPixelColorXY(vStrip - 1, vH - i -1); - else return getPixelColorXY(0, vH - i -1); - break; + case M12_pBar: { + int vStrip = i>>16; // virtual strips are only relevant in Bar expansion mode + if (vStrip > 0) return getPixelColorXY(vStrip - 1, vH - (i & 0xFFFF) -1); + 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 - if (reverse) i = virtualLength() - i - 1; + if (reverse) i = vLength() - 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); } -uint8_t Segment::differs(Segment& b) const { +uint8_t Segment::differs(const Segment& b) const { uint8_t d = 0; if (start != b.start) d |= SEG_DIFFERS_BOUNDS; if (stop != b.stop) d |= SEG_DIFFERS_BOUNDS; @@ -880,9 +1002,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; @@ -892,7 +1014,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; @@ -912,12 +1034,11 @@ void Segment::refreshLightCapabilities() { if (bus->getStart() >= segStopIdx) continue; 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; @@ -933,12 +1054,16 @@ 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() ? vWidth() : vLength(); + const int rows = vHeight(); // will be 1 for 1D + // pre-scale color for all pixels + c = color_fade(c, _segBri); + _colorScaled = true; for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) { if (is2D()) setPixelColorXY(x, y, c); else setPixelColor(x, c); } + _colorScaled = false; } /* @@ -946,11 +1071,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() ? vWidth() : vLength(); + const int rows = vHeight(); // 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); @@ -960,15 +1085,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; @@ -984,8 +1110,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() ? vWidth() : vLength(); + const int rows = vHeight(); // 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)); @@ -995,34 +1121,40 @@ void Segment::fadeToBlackBy(uint8_t fadeBy) { /* * blurs segment content, source: FastLED colorutils.cpp + * Note: for blur_amount > 215 this function does not work properly (creates alternating pattern) */ -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, blur_amount, smear); // symmetrical 2D blur + //box_blur(map(blur_amount,1,255,1,3), smear); return; } #endif - uint8_t keep = 255 - blur_amount; + uint8_t keep = smear ? 255 : 255 - blur_amount; uint8_t seep = blur_amount >> 1; + unsigned vlength = vLength(); uint32_t carryover = BLACK; - unsigned vlength = virtualLength(); + 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); + uint32_t prev = color_add(lastnew, part); + // optimization: only set pixel if color has changed + if (last != prev) setPixelColor(i - 1, prev); + } else setPixelColor(i, curnew); // first pixel + lastnew = curnew; + last = cur; // save original value for comparison on next iteration carryover = part; } + setPixelColor(vlength - 1, curnew); } /* @@ -1030,13 +1162,13 @@ 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)); + uint8_t w = W(getCurrentColor(0)); pos = 255 - pos; if (pos < 85) { return RGBW32((255 - pos * 3), 0, (pos * 3), w); - } else if(pos < 170) { + } else if (pos < 170) { pos -= 85; return RGBW32(0, (pos * 3), (255 - pos * 3), w); } else { @@ -1054,21 +1186,22 @@ 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 color = gamma32(currentColor(mcol)); - +uint32_t Segment::color_from_palette(uint16_t i, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri) const { + uint32_t color = getCurrentColor(mcol < NUM_COLORS ? mcol : 0); // default palette or no RGB support on segment - if ((palette == 0 && mcol < NUM_COLORS) || !_isRGB) return (pbri == 255) ? color : color_fade(color, pbri, true); + if ((palette == 0 && mcol < NUM_COLORS) || !_isRGB) { + return color_fade(color, pbri, true); + } - uint8_t paletteIndex = i; - if (mapping && virtualLength() > 1) paletteIndex = (i*255)/(virtualLength() -1); + const int vL = vLength(); + unsigned paletteIndex = i; + if (mapping && vL > 1) paletteIndex = (i*255)/(vL -1); // paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) if (!wrap && strip.paletteBlend != 3) paletteIndex = scale8(paletteIndex, 240); //cut off blend at palette "end" - CRGBPalette16 curPal; - curPal = currentPalette(curPal, palette); - CRGB fastled_col = ColorFromPalette(curPal, paletteIndex, pbri, (strip.paletteBlend == 3)? NOBLEND:LINEARBLEND); // NOTE: paletteBlend should be global + CRGBW palcol = ColorFromPalette(_currentPalette, paletteIndex, pbri, (strip.paletteBlend == 3)? NOBLEND:LINEARBLEND); // NOTE: paletteBlend should be global + palcol.w = W(color); - return RGBW32(fastled_col.r, fastled_col.g, fastled_col.b, W(color)); + return palcol.color32; } @@ -1077,16 +1210,14 @@ uint32_t Segment::color_from_palette(uint16_t i, bool mapping, bool wrap, uint8_ /////////////////////////////////////////////////////////////////////////////// //do not call this method from system context (network callback) -void WS2812FX::finalizeInit(void) { +void WS2812FX::finalizeInit() { //reset segment runtimes - for (segment &seg : _segments) { - seg.markForReset(); - seg.resetIfRequired(); - } + restartRuntime(); // 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; @@ -1094,17 +1225,82 @@ 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]; + constexpr unsigned defDataTypes[] = {LED_TYPES}; + constexpr unsigned defDataPins[] = {DATA_PINS}; + constexpr unsigned defCounts[] = {PIXEL_COUNTS}; + constexpr unsigned defNumTypes = ((sizeof defDataTypes) / (sizeof defDataTypes[0])); + constexpr unsigned defNumPins = ((sizeof defDataPins) / (sizeof defDataPins[0])); + constexpr unsigned defNumCounts = ((sizeof defCounts) / (sizeof defCounts[0])); + + static_assert(validatePinsAndTypes(defDataTypes, defNumTypes, defNumPins), + "The default pin list defined in DATA_PINS does not match the pin requirements for the default buses defined in LED_TYPES"); + + unsigned prevLen = 0; + unsigned pinsIndex = 0; + for (unsigned i = 0; i < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; i++) { + uint8_t defPin[OUTPUT_MAX_PINS]; + // if we have less types than requested outputs and they do not align, use last known type to set current type + unsigned dataType = defDataTypes[(i < defNumTypes) ? i : defNumTypes -1]; + unsigned busPins = Bus::getNumberOfPins(dataType); + + // if we need more pins than available all outputs have been configured + if (pinsIndex + busPins > defNumPins) break; + + // Assign all pins first so we can check for conflicts on this bus + for (unsigned j = 0; j < busPins && j < OUTPUT_MAX_PINS; j++) defPin[j] = defDataPins[pinsIndex + j]; + + for (unsigned j = 0; j < busPins && j < OUTPUT_MAX_PINS; j++) { + bool validPin = true; + // When booting without config (1st boot) we need to make sure GPIOs defined for LED output don't clash with hardware + // i.e. DEBUG (GPIO1), DMX (2), SPI RAM/FLASH (16&17 on ESP32-WROVER/PICO), read/only pins, etc. + // Pin should not be already allocated, read/only or defined for current bus + while (PinManager::isPinAllocated(defPin[j]) || !PinManager::isPinOk(defPin[j],true)) { + if (validPin) { + DEBUG_PRINTLN(F("Some of the provided pins cannot be used to configure this LED output.")); + defPin[j] = 1; // start with GPIO1 and work upwards + validPin = false; + } else if (defPin[j] < WLED_NUM_PINS) { + defPin[j]++; + } else { + DEBUG_PRINTLN(F("No available pins left! Can't configure output.")); + return; + } + // is the newly assigned pin already defined or used previously? + // try next in line until there are no clashes or we run out of pins + bool clash; + do { + clash = false; + // check for conflicts on current bus + for (const auto &pin : defPin) { + if (&pin != &defPin[j] && pin == defPin[j]) { + clash = true; + break; + } + } + // We already have a clash on current bus, no point checking next buses + if (!clash) { + // check for conflicts in defined pins + for (const auto &pin : defDataPins) { + if (pin == defPin[j]) { + clash = true; + break; + } + } + } + if (clash) defPin[j]++; + if (defPin[j] >= WLED_NUM_PINS) break; + } while (clash); + } + } + pinsIndex += busPins; + + unsigned start = prevLen; + // if we have less counts than pins and they do not align, use last known count to set current count + unsigned count = defCounts[(i < defNumCounts) ? i : defNumCounts -1]; + // analog always has length 1 + if (Bus::isPWM(dataType) || Bus::isOnOff(dataType)) count = 1; prevLen += count; - BusConfig defCfg = BusConfig(DEFAULT_LED_TYPE, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY); + BusConfig defCfg = BusConfig(dataType, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY, 0, useGlobalLedBuffer); if (BusManager::add(defCfg) == -1) break; } } @@ -1117,16 +1313,12 @@ 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(); - #endif + + // This must be done after all buses have been created, as some kinds (parallel I2S) interact + bus->begin(); } Segment::maxWidth = _length; @@ -1142,7 +1334,14 @@ void WS2812FX::finalizeInit(void) { void WS2812FX::service() { unsigned long nowUp = millis(); // Be aware, millis() rolls over every 49 days now = nowUp + timebase; - if (nowUp - _lastShow < MIN_SHOW_DELAY || _suspend) return; + if (_suspend) return; + unsigned long elapsed = nowUp - _lastServiceShow; + + if (elapsed <= MIN_FRAME_DELAY) return; // keep wifi alive - no matter if triggered or unlimited + if ( !_triggered && (_targetFps != FPS_UNLIMITED)) { // unlimited mode = no frametime + if (elapsed < _frametime) return; // too early for service + } + bool doShow = false; _isServicing = true; @@ -1159,18 +1358,17 @@ void WS2812FX::service() { if (!seg.isActive()) continue; // last condition ensures all solid segments are updated at the same time - if (nowUp > seg.next_time || _triggered || (doShow && seg.mode == FX_MODE_STATIC)) + if (nowUp >= seg.next_time || _triggered || (doShow && seg.mode == FX_MODE_STATIC)) { doShow = true; - uint16_t delay = FRAMETIME; + unsigned frameDelay = FRAMETIME; if (!seg.freeze) { //only run effect function if not frozen - _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); + int oldCCT = BusManager::getSegmentCCT(); // store original CCT value (actually it is not Segment based) + // 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. @@ -1178,43 +1376,43 @@ void WS2812FX::service() { // overwritten by later effect. To enable seamless blending for every effect, additional LED buffer // would need to be allocated for each effect and then blended together for each pixel. [[maybe_unused]] uint8_t tmpMode = seg.currentMode(); // this will return old mode while in transition - delay = (*_mode[seg.mode])(); // run new/current mode + seg.beginDraw(); // set up parameters for get/setPixelColor() + frameDelay = (*_mode[seg.mode])(); // run new/current mode #ifndef WLED_DISABLE_MODE_BLEND if (modeBlending && seg.mode != tmpMode) { Segment::tmpsegd_t _tmpSegData; Segment::modeBlend(true); // set semaphore seg.swapSegenv(_tmpSegData); // temporarily store new mode state (and swap it with transitional state) - _virtualSegmentLength = seg.virtualLength(); // update SEGLEN (mapping may have changed) - uint16_t d2 = (*_mode[tmpMode])(); // run old mode + seg.beginDraw(); // set up parameters for get/setPixelColor() + 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 + frameDelay = min(frameDelay,d2); // use shortest delay Segment::modeBlend(false); // unset semaphore } #endif seg.call++; - if (seg.isInTransition() && delay > FRAMETIME) delay = FRAMETIME; // force faster updates during transition + if (seg.isInTransition() && frameDelay > FRAMETIME) frameDelay = FRAMETIME; // force faster updates during transition + BusManager::setSegmentCCT(oldCCT); // restore old CCT for ABL adjustments } - seg.next_time = nowUp + delay; + seg.next_time = nowUp + frameDelay; } -// 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 ((_targetFps != FPS_UNLIMITED) && (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(); + _lastServiceShow = nowUp; // update timestamp, for precise FPS control } #ifdef WLED_DEBUG - if (millis() - nowUp > _frametime) DEBUG_PRINTLN(F("Slow strip.")); + if ((_targetFps != FPS_UNLIMITED) && (millis() - nowUp > _frametime)) DEBUG_PRINTF_P(PSTR("Slow strip %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime); #endif } @@ -1224,71 +1422,36 @@ 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(unsigned i) const { i = getMappedPixelIndex(i); if (i >= _length) return 0; return BusManager::getPixelColor(i); } -void WS2812FX::show(void) { +void WS2812FX::show() { // avoid race condition, capture _callback value show_callback callback = _callback; if (callback) callback(); + unsigned long showNow = millis(); // some buses send asynchronously and this method will return before // all of the data has been sent. // See https://github.com/Makuna/NeoPixelBus/wiki/ESP32-NeoMethods#neoesp32rmt-methods BusManager::show(); - unsigned long showNow = millis(); size_t diff = showNow - _lastShow; - size_t fpsCurr = 200; - if (diff > 0) fpsCurr = 1000 / diff; - _cumulativeFps = (3 * _cumulativeFps + fpsCurr +2) >> 2; // "+2" for proper rounding (2/4 = 0.5) - _lastShow = showNow; -} -/** - * 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() { - return !BusManager::canAllShow(); -} - -/** - * Returns the refresh rate of the LED strip. Useful for finding out whether a given setup is fast enough. - * Only updates on show() or is set to 0 fps if last show is more than 2 secs ago, so accuracy varies - */ -uint16_t WS2812FX::getFps() { - if (millis() - _lastShow > 2000) return 0; - return _cumulativeFps +1; -} - -void WS2812FX::setTargetFps(uint8_t fps) { - if (fps > 0 && fps <= 120) _targetFps = fps; - _frametime = 1000 / _targetFps; -} - -void WS2812FX::setMode(uint8_t segid, uint8_t m) { - if (segid >= _segments.size()) return; - - if (m >= getModeCount()) m = getModeCount() - 1; - - if (_segments[segid].mode != m) { - _segments[segid].setMode(m); // do not load defaults + if (diff > 0) { // skip calculation if no time has passed + size_t fpsCurr = (1000 << FPS_CALC_SHIFT) / diff; // fixed point math + _cumulativeFps = (FPS_CALC_AVG * _cumulativeFps + fpsCurr + FPS_CALC_AVG / 2) / (FPS_CALC_AVG + 1); // "+FPS_CALC_AVG/2" for proper rounding + _lastShow = showNow; } } -//applies to all active and selected segments -void WS2812FX::setColor(uint8_t slot, uint32_t c) { - if (slot >= NUM_COLORS) return; - - for (segment &seg : _segments) { - if (seg.isActive() && seg.isSelected()) { - seg.setColor(slot, c); - } - } +void WS2812FX::setTargetFps(unsigned fps) { + if (fps <= 250) _targetFps = fps; + if (_targetFps > 0) _frametime = 1000 / _targetFps; + else _frametime = MIN_FRAME_DELAY; // unlimited mode } void WS2812FX::setCCT(uint16_t k) { @@ -1315,21 +1478,21 @@ void WS2812FX::setBrightness(uint8_t b, bool direct) { BusManager::setBrightness(b); if (!direct) { unsigned long t = millis(); - if (_segments[0].next_time > t + 22 && t - _lastShow > MIN_SHOW_DELAY) trigger(); //apply brightness change immediately if no refresh soon + if (_segments[0].next_time > t + 22 && t - _lastShow > MIN_FRAME_DELAY) trigger(); //apply brightness change immediately if no refresh soon } } -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() const { size_t i = 0; - for (segment &seg : _segments) { + for (const segment &seg : _segments) { if (seg.isActive() && seg.isSelected()) return i; i++; } @@ -1337,7 +1500,7 @@ uint8_t WS2812FX::getFirstSelectedSegId(void) { return getMainSegmentId(); } -void WS2812FX::setMainSegmentId(uint8_t n) { +void WS2812FX::setMainSegmentId(unsigned n) { _mainSegment = 0; if (n < _segments.size()) { _mainSegment = n; @@ -1345,14 +1508,14 @@ void WS2812FX::setMainSegmentId(uint8_t n) { return; } -uint8_t WS2812FX::getLastActiveSegmentId(void) { +uint8_t WS2812FX::getLastActiveSegmentId() 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() const { uint8_t c = 0; for (size_t i = 0; i < _segments.size(); i++) { if (_segments[i].isActive()) c++; @@ -1360,17 +1523,17 @@ 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() 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() 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 + if (bus->isVirtual()) continue; //exclude non-physical network busses len += bus->getLength(); } return len; @@ -1379,7 +1542,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() const { for (size_t b = 0; b < BusManager::getNumBusses(); b++) { Bus *bus = BusManager::getBus(b); if (bus == nullptr || bus->getLength()==0) break; @@ -1388,16 +1551,12 @@ bool WS2812FX::hasRGBWBus(void) { return false; } -bool WS2812FX::hasCCTBus(void) { +bool WS2812FX::hasCCTBus() const { if (cctFromRgb && !correctWB) return false; for (size_t b = 0; b < BusManager::getNumBusses(); b++) { Bus *bus = BusManager::getBus(b); if (bus == nullptr || bus->getLength()==0) break; - switch (bus->getType()) { - case TYPE_ANALOG_5CH: - case TYPE_ANALOG_2CH: - return true; - } + if (bus->hasCCT()) return true; } return false; } @@ -1417,42 +1576,10 @@ void WS2812FX::purgeSegments() { } } -Segment& WS2812FX::getSegment(uint8_t id) { +Segment& WS2812FX::getSegment(unsigned id) { return _segments[id >= _segments.size() ? getMainSegmentId() : id]; // vectors } -// sets new segment bounds, queues if that segment is currently running -void WS2812FX::setSegment(uint8_t segId, uint16_t i1, uint16_t i2, uint8_t grouping, uint8_t spacing, uint16_t offset, uint16_t startY, uint16_t stopY) { - if (segId >= getSegmentsNum()) { - if (i2 <= i1) return; // do not append empty/inactive segments - appendSegment(Segment(0, strip.getLengthTotal())); - segId = getSegmentsNum()-1; // segments are added at the end of list - } -/* - 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 @@ -1467,8 +1594,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 @@ -1487,7 +1614,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 @@ -1515,6 +1642,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 { @@ -1560,6 +1688,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(); @@ -1590,12 +1720,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 @@ -1649,7 +1778,7 @@ void WS2812FX::loadCustomPalettes() { } //load custom mapping table from JSON file (called from finalizeInit() or deserializeState()) -bool WS2812FX::deserializeMap(uint8_t n) { +bool WS2812FX::deserializeMap(unsigned n) { // 2D support creates its own ledmap (on the fly) if a ledmap.json exists it will overwrite built one. char fileName[32]; @@ -1659,13 +1788,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); @@ -1673,27 +1804,30 @@ 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); } @@ -1708,5 +1842,5 @@ const char JSON_palette_names[] PROGMEM = R"=====([ "Magenta","Magred","Yelmag","Yelblu","Orange & Teal","Tiamat","April Night","Orangery","C9","Sakura", "Aurora","Atlantica","C9 2","C9 New","Temperature","Aurora 2","Retro Clown","Candy","Toxy Reaf","Fairy Reaf", "Semi Blue","Pink Candy","Red Reaf","Aqua Flash","Yelblu Hot","Lite Light","Red Flash","Blink Red","Red Shift","Red Tide", -"Candy2" +"Candy2","Traffic Light" ])====="; diff --git a/wled00/NodeStruct.h b/wled00/NodeStruct.h index 2d4d3609b..34f73ab41 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 179a522c0..81b9ec346 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); } @@ -126,10 +126,10 @@ void onAlexaChange(EspalexaDevice* dev) } else { colorKtoRGB(k, rgbw); } - strip.setColor(0, RGBW32(rgbw[0], rgbw[1], rgbw[2], rgbw[3])); + strip.getMainSegment().setColor(0, RGBW32(rgbw[0], rgbw[1], rgbw[2], rgbw[3])); } else { uint32_t color = dev->getRGB(); - strip.setColor(0, color); + strip.getMainSegment().setColor(0, color); } stateUpdated(CALL_MODE_ALEXA); } diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 3ac12c04e..2c0ba41a9 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -4,14 +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); //udp.cpp uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, byte *buffer, uint8_t bri=255, bool isRGBW=false); @@ -47,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); } @@ -108,12 +129,12 @@ BusDigital::BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com) , _milliAmpsMax(bc.milliAmpsMax) , _colorOrderMap(com) { - if (!IS_DIGITAL(bc.type) || !bc.count) return; - if (!pinManager.allocatePin(bc.pins[0], true, PinOwner::BusDigital)) 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 (!pinManager.allocatePin(bc.pins[1], true, PinOwner::BusDigital)) { + if (is2Pin(bc.type)) { + if (!PinManager::allocatePin(bc.pins[1], true, PinOwner::BusDigital)) { cleanup(); return; } @@ -122,25 +143,18 @@ 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); + _busPtr = PolyBus::create(_iType, _pins, lenToCreate + _skip, nr); _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 -//you can set it to 0 if the ESP is powered by USB and the LEDs by external -#ifndef MA_FOR_ESP - #ifdef ESP8266 - #define MA_FOR_ESP 80 //how much mA does the ESP use (Wemos D1 about 80mA) - #else - #define MA_FOR_ESP 120 //how much mA does the ESP use (ESP32 about 120mA) - #endif -#endif - //DISCLAIMER //The following function attemps to calculate the current LED power usage, //and will limit the brightness to stay below a set amperage threshold. @@ -205,14 +219,16 @@ void BusDigital::show() { _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) { @@ -221,25 +237,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 } } } @@ -250,19 +276,13 @@ void BusDigital::show() { if (newBri < _bri) PolyBus::setBrightness(_busPtr, _iType, _bri); } -bool BusDigital::canShow() { +bool BusDigital::canShow() 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); } @@ -276,25 +296,28 @@ void BusDigital::setStatusPixel(uint32_t c) { } } -void IRAM_ATTR BusDigital::setPixelColor(uint16_t pix, uint32_t c) { +void IRAM_ATTR BusDigital::setPixelColor(unsigned pix, uint32_t c) { if (!_valid) return; - 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)) { - _data[offset++] = R(c); - _data[offset++] = G(c); - _data[offset++] = B(c); + 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(); + uint8_t* dataptr = _data + offset; + if (hasRGB()) { + *dataptr++ = R(c); + *dataptr++ = G(c); + *dataptr++ = B(c); } - if (Bus::hasWhite(_type)) _data[offset] = W(c); + if (hasWhite()) *dataptr++ = 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()) *dataptr = 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) @@ -303,32 +326,38 @@ 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); + uint16_t wwcw = 0; + if (hasCCT()) { + uint8_t cctWW = 0, cctCW = 0; + Bus::calculateCCT(c, cctWW, cctCW); + wwcw = (cctCW<<8) | cctWW; + } + + PolyBus::setPixelColor(_busPtr, _iType, pix, c, co, wwcw); } } // returns original color if global buffering is enabled, else returns lossly restored color from bus -uint32_t IRAM_ATTR BusDigital::getPixelColor(uint16_t pix) { +uint32_t IRAM_ATTR BusDigital::getPixelColor(unsigned 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) { + const 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); + const unsigned co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); uint32_t c = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, (_type==TYPE_WS2812_1CH_X3) ? IC_INDEX_WS2812_1CH_3X(pix) : pix, co),_bri); if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs - 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; @@ -339,9 +368,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; } @@ -351,9 +380,35 @@ 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::begin() { if (!_valid) return; - PolyBus::begin(_busPtr, _iType, _pins); + PolyBus::begin(_busPtr, _iType, _pins, _frequencykHz); } void BusDigital::cleanup() { @@ -363,99 +418,113 @@ void BusDigital::cleanup() { _valid = false; _busPtr = nullptr; if (_data != nullptr) freeData(); - pinManager.deallocatePin(_pins[1], PinOwner::BusDigital); - pinManager.deallocatePin(_pins[0], PinOwner::BusDigital); + PinManager::deallocatePin(_pins[1], PinOwner::BusDigital); + PinManager::deallocatePin(_pins[0], PinOwner::BusDigital); } -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 + // 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 - // duty cycle resolution (_depth) can be extracted from this formula: 1MHz > _frequency * 2^_depth - if (_frequency > 1760) _depth = 8; - else if (_frequency > 880) _depth = 9; - else _depth = 10; // WLED_PWM_FREQ <= 880Hz +#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 - _ledcStart = pinManager.allocateLedc(numPins); +#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; } - // duty cycle resolution (_depth) can be extracted from this formula: 80MHz > _frequency * 2^_depth - if (_frequency > 78124) _depth = 9; - else if (_frequency > 39062) _depth = 10; - else if (_frequency > 19531) _depth = 11; - else _depth = 12; // WLED_PWM_FREQ <= 19531Hz - #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, _depth); - 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) { +void BusPwm::setPixelColor(unsigned pix, uint32_t c) { if (pix != 0 || !_valid) return; //only react to first pixel if (_type != TYPE_ANALOG_3CH) c = autoWhiteCalc(c); - if (_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 @@ -465,84 +534,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(unsigned 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]); } -#ifndef ESP8266 -static const uint16_t cieLUT[256] = { - 0, 2, 4, 5, 7, 9, 11, 13, 15, 16, - 18, 20, 22, 24, 26, 27, 29, 31, 33, 35, - 34, 36, 37, 39, 41, 43, 45, 47, 49, 52, - 54, 56, 59, 61, 64, 67, 69, 72, 75, 78, - 81, 84, 87, 90, 94, 97, 100, 104, 108, 111, - 115, 119, 123, 127, 131, 136, 140, 144, 149, 154, - 158, 163, 168, 173, 178, 183, 189, 194, 200, 205, - 211, 217, 223, 229, 235, 241, 247, 254, 261, 267, - 274, 281, 288, 295, 302, 310, 317, 325, 333, 341, - 349, 357, 365, 373, 382, 391, 399, 408, 417, 426, - 436, 445, 455, 464, 474, 484, 494, 505, 515, 526, - 536, 547, 558, 569, 580, 592, 603, 615, 627, 639, - 651, 663, 676, 689, 701, 714, 727, 741, 754, 768, - 781, 795, 809, 824, 838, 853, 867, 882, 897, 913, - 928, 943, 959, 975, 991, 1008, 1024, 1041, 1058, 1075, - 1092, 1109, 1127, 1144, 1162, 1180, 1199, 1217, 1236, 1255, - 1274, 1293, 1312, 1332, 1352, 1372, 1392, 1412, 1433, 1454, - 1475, 1496, 1517, 1539, 1561, 1583, 1605, 1628, 1650, 1673, - 1696, 1719, 1743, 1767, 1791, 1815, 1839, 1864, 1888, 1913, - 1939, 1964, 1990, 2016, 2042, 2068, 2095, 2121, 2148, 2176, - 2203, 2231, 2259, 2287, 2315, 2344, 2373, 2402, 2431, 2461, - 2491, 2521, 2551, 2581, 2612, 2643, 2675, 2706, 2738, 2770, - 2802, 2835, 2867, 2900, 2934, 2967, 3001, 3035, 3069, 3104, - 3138, 3174, 3209, 3244, 3280, 3316, 3353, 3389, 3426, 3463, - 3501, 3539, 3576, 3615, 3653, 3692, 3731, 3770, 3810, 3850, - 3890, 3930, 3971, 4012, 4053, 4095 -}; -#endif - void BusPwm::show() { if (!_valid) return; - uint8_t numPins = NUM_PWM_PINS(_type); - unsigned maxBri = (1<<_depth) - 1; - #ifdef ESP8266 - unsigned pwmBri = (unsigned)(roundf(powf((float)_bri / 255.0f, 1.7f) * (float)maxBri + 0.5f)); // using gamma 1.7 to extrapolate PWM duty cycle - #else - unsigned pwmBri = cieLUT[_bri] >> (12 - _depth); // use CIE LUT - #endif + // 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 (linear + cubic) to approximate human eye perceived brightness + // see: https://en.wikipedia.org/wiki/Lightness + unsigned pwmBri = _bri; + if (pwmBri < 21) { // linear response for values [0-20] + pwmBri = (pwmBri * maxBri + 2300 / 2) / 2300 ; // adding '0.5' before division for correct rounding, 2300 gives a good match to CIE curve + } else { // cubic response for values [21-255] + float temp = float(pwmBri + 41) / float(255 + 41); // 41 is to match offset & slope to linear part + temp = temp * temp * temp * (float)maxBri; + pwmBri = (unsigned)temp; // pwmBri is in range [0-maxBri] C + } + + [[maybe_unused]] unsigned hPoint = 0; // phase shift (0 - maxBri) + // we will be phase shifting every channel by previous pulse length (plus dead time if required) + // phase shifting is only mandatory when using H-bridge to drive reverse-polarity PWM CCT (2 wire) LED type + // CCT additive blending must be 0 (WW & CW will not overlap) otherwise signals *will* overlap + // for all other cases it will just try to "spread" the load on PSU + // Phase shifting requires that LEDC timers are synchronised (see setup()). For PWM CCT (and H-bridge) it is + // also mandatory that both channels use the same timer (pinManager takes care of that). for (unsigned i = 0; i < numPins; i++) { - unsigned scaled = (_data[i] * pwmBri) / 255; - if (_reversed) scaled = maxBri - 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; } +// 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() { - uint8_t numPins = NUM_PWM_PINS(_type); + unsigned numPins = getPins(); for (unsigned i = 0; i < numPins; i++) { - pinManager.deallocatePin(_pins[i], PinOwner::BusPwm); - if (!pinManager.isPinOk(_pins[i])) continue; + 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 - pinManager.deallocateLedc(_ledcStart, numPins); + PinManager::deallocateLedc(_ledcStart, numPins); #endif } @@ -551,19 +650,23 @@ 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)) { + if (!PinManager::allocatePin(currentPin, true, PinOwner::BusOnOff)) { return; } _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) { +void BusOnOff::setPixelColor(unsigned pix, uint32_t c) { if (pix != 0 || !_valid) return; //only react to first pixel c = autoWhiteCalc(c); uint8_t r = R(c); @@ -573,7 +676,7 @@ 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(unsigned pix) const { if (!_valid) return 0; return RGBW32(_data[0], _data[0], _data[0], _data[0]); } @@ -583,12 +686,18 @@ void BusOnOff::show() { 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) @@ -596,58 +705,71 @@ BusNetwork::BusNetwork(BusConfig &bc) { switch (bc.type) { case TYPE_NET_ARTNET_RGB: - _rgbw = false; _UDPtype = 2; break; case TYPE_NET_ARTNET_RGBW: - _rgbw = true; _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) { +void BusNetwork::setPixelColor(unsigned 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(unsigned 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() { 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; } +// 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() { _type = I_NONE; _valid = false; @@ -657,38 +779,37 @@ void BusNetwork::cleanup() { //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 OUTPUT_MAX_PINS; - 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); - } 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); @@ -696,6 +817,36 @@ 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 | Bus::mustRefresh(type.id)<<5; + 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() { + 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() { + _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() { DEBUG_PRINTLN(F("Removing all.")); @@ -703,6 +854,79 @@ void BusManager::removeAll() { while (!canAllShow()) yield(); for (unsigned i = 0; i < numBusses; i++) delete busses[i]; numBusses = 0; + _parallelOutputs = 1; + PolyBus::setParallelI2S1Output(false); +} + +#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() { + 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() { + #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->begin(); + break; + } + } + } + } + #endif + #ifdef ESP32_DATA_IDLE_HIGH + esp32RMTInvertIdle(); + #endif +} + +void BusManager::off() { + #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() { @@ -711,7 +935,6 @@ void BusManager::show() { busses[i]->show(); _milliAmpsUsed += busses[i]->getUsedCurrent(); } - if (_milliAmpsUsed) _milliAmpsUsed += MA_FOR_ESP; } void BusManager::setStatusPixel(uint32_t c) { @@ -720,11 +943,10 @@ void BusManager::setStatusPixel(uint32_t c) { } } -void IRAM_ATTR BusManager::setPixelColor(uint16_t pix, uint32_t c) { +void IRAM_ATTR BusManager::setPixelColor(unsigned 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); } } @@ -740,16 +962,15 @@ 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) { +uint32_t BusManager::getPixelColor(unsigned 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; } @@ -768,11 +989,13 @@ 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; + 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; @@ -784,4 +1007,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 0b791adf3..d90a66151 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -6,6 +6,11 @@ */ #include "const.h" +#include "pin_manager.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))) @@ -18,93 +23,45 @@ #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); - 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(); } + inline void reserve(size_t num) { _mappings.reserve(num); } 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; + [[gnu::hot]] 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: @@ -123,80 +80,118 @@ class Bus { virtual ~Bus() {} //throw the bus under the bus + virtual void begin() {}; virtual void show() = 0; - virtual bool canShow() { 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 bool canShow() const { return true; } + virtual void setStatusPixel(uint32_t c) {} + virtual void setPixelColor(unsigned pix, uint32_t c) = 0; + virtual void setBrightness(uint8_t b) { _bri = b; }; + virtual void setColorOrder(uint8_t co) {} + virtual uint32_t getPixelColor(unsigned pix) const { return 0; } + virtual uint8_t getPins(uint8_t* pinArray = nullptr) const { return 0; } + virtual uint16_t getLength() const { return isOk() ? _len : 0; } + virtual uint8_t getColorOrder() const { return COL_ORDER_RGB; } + virtual uint8_t skippedLeds() const { return 0; } + virtual uint16_t getFrequency() const { return 0U; } + virtual uint16_t getLEDCurrent() const { return 0; } + virtual uint16_t getUsedCurrent() const { return 0; } + virtual uint16_t getMaxCurrent() const { return 0; } - virtual 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() const { return _hasRgb; } + inline bool hasWhite() const { return _hasWhite; } + inline bool hasCCT() const { return _hasCCT; } + inline bool isDigital() const { return isDigital(_type); } + inline bool is2Pin() const { return is2Pin(_type); } + inline bool isOnOff() const { return isOnOff(_type); } + inline bool isPWM() const { return isPWM(_type); } + inline bool isVirtual() const { return isVirtual(_type); } + inline bool is16bit() const { return is16bit(_type); } + inline bool mustRefresh() const { return mustRefresh(_type); } + inline void setReversed(bool reversed) { _reversed = reversed; } + inline void setStart(uint16_t start) { _start = start; } + inline void setAutoWhiteMode(uint8_t m) { if (m < 5) _autoWhiteMode = m; } + inline uint8_t getAutoWhiteMode() const { return _autoWhiteMode; } + inline uint32_t getNumberOfChannels() const { return hasWhite() + 3*hasRGB() + hasCCT(); } + inline uint16_t getStart() const { return _start; } + inline uint8_t getType() const { return _type; } + inline bool isOk() const { return _valid; } + inline bool isReversed() const { return _reversed; } + inline bool isOffRefreshRequired() const { return _needsRefresh; } + inline bool containsPixel(uint16_t pix) const { return pix >= _start && pix < _start + _len; } + + static inline std::vector getLEDTypes() { return {{TYPE_NONE, "", PSTR("None")}}; } // not used. just for reference for derived classes + static constexpr uint32_t getNumberOfPins(uint8_t type) { return isVirtual(type) ? 4 : isPWM(type) ? numPWMPins(type) : is2Pin(type) + 1; } // credit @PaoloTK + static constexpr uint32_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 bool mustRefresh(uint8_t type) { return type == TYPE_TM1814; } + static constexpr int numPWMPins(uint8_t type) { return (type - 40); } + + static inline int16_t getCCT() { return _cct; } + static inline void setGlobalAWMode(uint8_t m) { if (m < 5) _gAWM = m; else _gAWM = AW_GLOBAL_DISABLED; } + static inline uint8_t getGlobalAWMode() { return _gAWM; } + static inline void setCCT(int16_t cct) { _cct = cct; } + static inline uint8_t getCCTBlend() { return _cctBlend; } + static inline void setCCTBlend(uint8_t b) { + _cctBlend = (std::min((int)b,100) * 127) / 100; //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; } }; @@ -207,23 +202,24 @@ class BusDigital : public Bus { ~BusDigital() { cleanup(); } void show() override; - bool canShow() override; + bool canShow() const override; void setBrightness(uint8_t b) override; void setStatusPixel(uint32_t c) override; - void setPixelColor(uint16_t pix, uint32_t c) override; + [[gnu::hot]] void setPixelColor(unsigned pix, uint32_t c) override; void setColorOrder(uint8_t colorOrder) override; - 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(); + [[gnu::hot]] uint32_t getPixelColor(unsigned pix) const override; + uint8_t getColorOrder() const override { return _colorOrder; } + uint8_t getPins(uint8_t* pinArray = nullptr) const override; + uint8_t skippedLeds() const override { return _skip; } + uint16_t getFrequency() const override { return _frequencykHz; } + uint16_t getLEDCurrent() const override { return _milliAmpsPerLed; } + uint16_t getUsedCurrent() const override { return _milliAmpsTotal; } + uint16_t getMaxCurrent() const override { return _milliAmpsMax; } + void begin() override; void cleanup(); + static std::vector getLEDTypes(); + private: uint8_t _skip; uint8_t _colorOrder; @@ -237,7 +233,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++) { @@ -247,6 +243,8 @@ class BusDigital : public Bus { } return c; } + + uint8_t estimateCurrentAndLimitBri(); }; @@ -255,16 +253,18 @@ class BusPwm : public Bus { BusPwm(BusConfig &bc); ~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 setPixelColor(unsigned pix, uint32_t c) override; + uint32_t getPixelColor(unsigned pix) const override; //does no index check + uint8_t getPins(uint8_t* pinArray = nullptr) const override; + uint16_t getFrequency() const override { return _frequency; } void show() override; void cleanup() { deallocatePins(); } + static std::vector getLEDTypes(); + private: - uint8_t _pins[5]; - uint8_t _pwmdata[5]; + uint8_t _pins[OUTPUT_MAX_PINS]; + uint8_t _pwmdata[OUTPUT_MAX_PINS]; #ifdef ARDUINO_ARCH_ESP32 uint8_t _ledcStart; #endif @@ -280,11 +280,13 @@ class BusOnOff : public Bus { BusOnOff(BusConfig &bc); ~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 setPixelColor(unsigned pix, uint32_t c) override; + uint32_t getPixelColor(unsigned pix) const override; + uint8_t getPins(uint8_t* pinArray) const override; void show() override; - void cleanup() { pinManager.deallocatePin(_pin, PinOwner::BusOnOff); } + void cleanup() { PinManager::deallocatePin(_pin, PinOwner::BusOnOff); } + + static std::vector getLEDTypes(); private: uint8_t _pin; @@ -297,55 +299,120 @@ 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 - void setPixelColor(uint16_t pix, uint32_t c) override; - uint32_t getPixelColor(uint16_t pix) override; - uint8_t getPins(uint8_t* pinArray) override; + bool canShow() const override { return !_broadcastLock; } // this should be a return value from UDP routine if it is still sending data out + void setPixelColor(unsigned pix, uint32_t c) override; + uint32_t getPixelColor(unsigned pix) const override; + uint8_t getPins(uint8_t* pinArray = nullptr) const override; void show() override; void cleanup(); + static std::vector getLEDTypes(); + private: IPAddress _client; uint8_t _UDPtype; uint8_t _UDPchannels; - bool _rgbw; bool _broadcastLock; }; +//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; + } +}; + + +//fine tune power estimation constants for your setup +//you can set it to 0 if the ESP is powered by USB and the LEDs by external +#ifndef MA_FOR_ESP + #ifdef ESP8266 + #define MA_FOR_ESP 80 //how much mA does the ESP use (Wemos D1 about 80mA) + #else + #define MA_FOR_ESP 120 //how much mA does the ESP use (ESP32 about 120mA) + #endif +#endif + class BusManager { public: BusManager() {}; //utility to get the approx. memory usage of a given BusConfig static uint32_t memUsage(BusConfig &bc); - static uint16_t currentMilliamps(void) { return _milliAmpsUsed; } - static uint16_t ablMilliampsMax(void) { return _milliAmpsMax; } + static uint32_t memUsage(unsigned channels, unsigned count, unsigned buses = 1); + static uint16_t currentMilliamps() { return _milliAmpsUsed + MA_FOR_ESP; } + static uint16_t ablMilliampsMax() { return _milliAmpsMax; } static int add(BusConfig &bc); + static void useParallelOutput(); // workaround for inaccessible PolyBus //do not call this method from system context (network callback) static void removeAll(); + static void on(); + static void off(); + static void show(); static bool canAllShow(); static void setStatusPixel(uint32_t c); - static void setPixelColor(uint16_t pix, uint32_t c); + [[gnu::hot]] static void setPixelColor(unsigned 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 uint32_t getPixelColor(uint16_t pix); + static inline void setMilliampsMax(uint16_t max) { _milliAmpsMax = max;} + [[gnu::hot]] static uint32_t getPixelColor(unsigned pix); + static inline int16_t getSegmentCCT() { 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 inline uint8_t getNumBusses() { return numBusses; } + static String getLEDTypesJSONString(); - static void updateColorOrderMap(const ColorOrderMap &com) { memcpy(&colorOrderMap, &com, sizeof(ColorOrderMap)); } - static const ColorOrderMap& getColorOrderMap() { return colorOrderMap; } + static inline ColorOrderMap& getColorOrderMap() { return colorOrderMap; } private: static uint8_t numBusses; @@ -353,10 +420,14 @@ class BusManager { static ColorOrderMap colorOrderMap; static uint16_t _milliAmpsUsed; static uint16_t _milliAmpsMax; + static uint8_t _parallelOutputs; + #ifdef ESP32_DATA_IDLE_HIGH + static void esp32RMTInvertIdle() ; + #endif static uint8_t getNumVirtualBusses() { int j = 0; - for (int i=0; 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 c63e055a8..d2a18c9d8 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,10 +327,16 @@ //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) { + static void beginDotStar(void* busPtr, int8_t sck, int8_t miso, int8_t mosi, int8_t ss, uint16_t clock_kHz /* 0 == use default */) { T dotStar_strip = static_cast(busPtr); #ifdef ESP8266 dotStar_strip->Begin(); @@ -312,7 +356,14 @@ class PolyBus { tm1814_strip->SetPixelSettings(NeoTm1814Settings(/*R*/225, /*G*/225, /*B*/225, /*W*/225)); } - static void begin(void* busPtr, uint8_t busType, uint8_t* pins, uint16_t clock_kHz = 0U) { + 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 /* only used by DotStar */) { switch (busType) { case I_NONE: break; #ifdef ESP8266 @@ -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; @@ -428,7 +480,13 @@ class PolyBus { } } - static void* create(uint8_t busType, uint8_t* pins, uint16_t len, uint8_t channel, uint16_t clock_kHz = 0U) { + static void* create(uint8_t busType, uint8_t* pins, uint16_t len, uint8_t channel) { + #if defined(ARDUINO_ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3)) + // NOTE: "channel" is only used on ESP32 (and its variants) for RMT channel allocation + // since 0.15.0-b3 I2S1 is favoured for classic ESP32 and moved to position 0 (channel 0) so we need to subtract 1 for correct RMT allocation + if (useParallelI2S && channel > 7) channel -= 8; // accommodate parallel I2S1 which is used 1st on classic ESP32 + else if (channel > 0) channel--; // accommodate I2S1 which is used as 1st bus on classic ESP32 + #endif 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; @@ -538,7 +597,7 @@ class PolyBus { case I_HS_P98_3: busPtr = new B_HS_P98_3(len, pins[1], pins[0]); break; case I_SS_P98_3: busPtr = new B_SS_P98_3(len, pins[1], pins[0]); break; } - begin(busPtr, busType, pins, clock_kHz); + return busPtr; } @@ -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 + // RMT buses 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 - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: return (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; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_4: return (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; + case I_32_RN_UCS_3: return (static_cast(busPtr))->CanShow(); break; + case I_32_RN_UCS_4: return (static_cast(busPtr))->CanShow(); break; + case I_32_RN_APA106_3: return (static_cast(busPtr))->CanShow(); break; + case I_32_RN_FW6_5: return (static_cast(busPtr))->CanShow(); break; + case I_32_RN_2805_5: return (static_cast(busPtr))->CanShow(); break; + case I_32_RN_TM1914_3: return (static_cast(busPtr))->CanShow(); break; + case I_32_RN_SM16825_5: return (static_cast(busPtr))->CanShow(); break; + // I2S1 bus or paralell buses + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_NEO_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I1_NEO_4: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I1_400_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I1_TM1_4: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I1_TM2_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I1_UCS_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I1_UCS_4: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I1_APA106_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I1_FW6_5: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I1_2805_5: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I1_TM1914_3: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + case I_32_I1_SM16825_5: if (useParallelI2S) return (static_cast(busPtr))->CanShow(); else return (static_cast(busPtr))->CanShow(); break; + #endif + // I2S0 bus #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_3: return (static_cast(busPtr))->CanShow(); break; + case I_32_I0_NEO_4: return (static_cast(busPtr))->CanShow(); break; + case I_32_I0_400_3: return (static_cast(busPtr))->CanShow(); break; case I_32_I0_TM1_4: return (static_cast(busPtr))->CanShow(); break; case I_32_I0_TM2_3: return (static_cast(busPtr))->CanShow(); break; - #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; + case I_32_I0_FW6_5: return (static_cast(busPtr))->CanShow(); break; + case I_32_I0_2805_5: return (static_cast(busPtr))->CanShow(); break; + case I_32_I0_TM1914_3: return (static_cast(busPtr))->CanShow(); break; + case I_32_I0_SM16825_5: return (static_cast(busPtr))->CanShow(); break; #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 29cb0abeb..4e063c120 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,14 +23,13 @@ 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); } #ifndef WLED_DISABLE_MQTT // publish MQTT message if (buttonPublishMqtt && WLED_MQTT_CONNECTED) { - char subuf[64]; + char subuf[MQTT_MAX_TOPIC_LEN + 32]; sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b); mqtt->publish(subuf, 0, false, "short"); } @@ -40,17 +41,28 @@ 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); } #ifndef WLED_DISABLE_MQTT // publish MQTT message if (buttonPublishMqtt && WLED_MQTT_CONNECTED) { - char subuf[64]; + char subuf[MQTT_MAX_TOPIC_LEN + 32]; sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b); mqtt->publish(subuf, 0, false, "long"); } @@ -65,14 +77,13 @@ 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); } #ifndef WLED_DISABLE_MQTT // publish MQTT message if (buttonPublishMqtt && WLED_MQTT_CONNECTED) { - char subuf[64]; + char subuf[MQTT_MAX_TOPIC_LEN + 32]; sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b); mqtt->publish(subuf, 0, false, "double"); } @@ -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; } @@ -110,7 +125,7 @@ void handleSwitch(uint8_t b) { // isButtonPressed() handles inverted/noninverted logic if (buttonPressedBefore[b] != isButtonPressed(b)) { - DEBUG_PRINT(F("Switch: State changed ")); DEBUG_PRINTLN(b); + DEBUG_PRINTF_P(PSTR("Switch: State changed %u\n"), b); buttonPressedTime[b] = millis(); buttonPressedBefore[b] = !buttonPressedBefore[b]; } @@ -118,15 +133,15 @@ void handleSwitch(uint8_t b) if (buttonLongPressed[b] == buttonPressedBefore[b]) return; if (millis() - buttonPressedTime[b] > WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce) - DEBUG_PRINT(F("Switch: Activating ")); DEBUG_PRINTLN(b); + DEBUG_PRINTF_P(PSTR("Switch: Activating %u\n"), b); if (!buttonPressedBefore[b]) { // on -> off - DEBUG_PRINT(F("Switch: On -> Off ")); DEBUG_PRINTLN(b); + DEBUG_PRINTF_P(PSTR("Switch: On -> Off (%u)\n"), b); if (macroButton[b]) applyPreset(macroButton[b], CALL_MODE_BUTTON_PRESET); else { //turn on if (!bri) {toggleOnOff(); stateUpdated(CALL_MODE_BUTTON);} } } else { // off -> on - DEBUG_PRINT(F("Switch: Off -> On ")); DEBUG_PRINTLN(b); + DEBUG_PRINTF_P(PSTR("Switch: Off -> On (%u)\n"), b); if (macroLongPress[b]) applyPreset(macroLongPress[b], CALL_MODE_BUTTON_PRESET); else { //turn off if (bri) {toggleOnOff(); stateUpdated(CALL_MODE_BUTTON);} @@ -136,7 +151,7 @@ void handleSwitch(uint8_t b) #ifndef WLED_DISABLE_MQTT // publish MQTT message if (buttonPublishMqtt && WLED_MQTT_CONNECTED) { - char subuf[64]; + char subuf[MQTT_MAX_TOPIC_LEN + 32]; if (buttonType[b] == BTN_TYPE_PIR_SENSOR) sprintf_P(subuf, PSTR("%s/motion/%d"), mqttDeviceTopic, (int)b); else sprintf_P(subuf, _mqtt_topic_button, mqttDeviceTopic, (int)b); mqtt->publish(subuf, 0, false, !buttonPressedBefore[b] ? "off" : "on"); @@ -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); + DEBUG_PRINTF_P(PSTR("Analog: Reading button %u\n"), 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; @@ -178,8 +193,8 @@ void handleAnalog(uint8_t b) // remove noise & reduce frequency of UI updates if (abs(int(aRead) - int(oldRead[b])) <= POT_SENSITIVITY) return; // no significant change in reading - DEBUG_PRINT(F("Analog: Raw = ")); DEBUG_PRINT(rawReading); - DEBUG_PRINT(F(" Filtered = ")); DEBUG_PRINTLN(aRead); + DEBUG_PRINTF_P(PSTR("Analog: Raw = %u\n"), rawReading); + DEBUG_PRINTF_P(PSTR(" Filtered = %u\n"), aRead); // Unomment the next lines if you still see flickering related to potentiometer // This waits until strip finishes updating (why: strip was not updating at the start of handleButton() but may have started during analogRead()?) @@ -192,7 +207,7 @@ void handleAnalog(uint8_t b) // if no macro for "short press" and "long press" is defined use brightness control if (!macroButton[b] && !macroLongPress[b]) { - DEBUG_PRINT(F("Analog: Action = ")); DEBUG_PRINTLN(macroDoublePress[b]); + DEBUG_PRINTF_P(PSTR("Analog: Action = %u\n"), macroDoublePress[b]); // if "double press" macro defines which option to change if (macroDoublePress[b] >= 250) { // global brightness @@ -200,6 +215,7 @@ void handleAnalog(uint8_t b) briLast = bri; bri = 0; } else { + if (bri == 0) strip.restartRuntime(); bri = aRead; } } else if (macroDoublePress[b] == 249) { @@ -245,14 +261,14 @@ 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 ANALOG_BTN_READ_CYCLE) { @@ -283,15 +299,17 @@ void handleButton() buttonPressedBefore[b] = true; if (now - buttonPressedTime[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 +359,40 @@ 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); + delay(50); // wait for relay to switch and power to stabilize } 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 index e51b666e4..38e804ed9 100644 --- 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")]; @@ -182,75 +212,89 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { bool refresh = elm["ref"] | false; 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; + BusManager::getColorOrderMap().reserve(std::min(hw_com.size(), (size_t)WLED_MAX_COLOR_ORDER_MAPPINGS)); 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; - if (pin > -1 && pinManager.allocatePin(pin, false, PinOwner::Button)) { + if (pin > -1 && PinManager::allocatePin(pin, false, PinOwner::Button)) { 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_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), btnPin[s], s); + btnPin[s] = -1; + PinManager::deallocatePin(pin,PinOwner::Button); + } 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 a 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 @@ -287,7 +331,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { if (fromFS) { // relies upon only being called once with fromFS == true, which is currently true. for (size_t s = 0; s < WLED_MAX_BUTTONS; s++) { - if (buttonType[s] == BTN_TYPE_NONE || btnPin[s] < 0 || !pinManager.allocatePin(btnPin[s], false, PinOwner::Button)) { + if (buttonType[s] == BTN_TYPE_NONE || btnPin[s] < 0 || !PinManager::allocatePin(btnPin[s], false, PinOwner::Button)) { btnPin[s] = -1; buttonType[s] = BTN_TYPE_NONE; } @@ -308,14 +352,14 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { } } } - 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); - if (pinManager.allocatePin(hw_ir_pin, false, PinOwner::IR)) { + PinManager::deallocatePin(irPin, PinOwner::IR); + if (PinManager::allocatePin(hw_ir_pin, false, PinOwner::IR)) { irPin = hw_ir_pin; } else { irPin = -1; @@ -326,12 +370,14 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { 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)) { + 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; } @@ -348,7 +394,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(i2c_sda, hw_if_i2c[0]); CJSON(i2c_scl, hw_if_i2c[1]); PinManagerPinType i2c[2] = { { i2c_sda, true }, { i2c_scl, true } }; - if (i2c_scl >= 0 && i2c_sda >= 0 && pinManager.allocateMultiplePins(i2c, 2, PinOwner::HW_I2C)) { + if (i2c_scl >= 0 && i2c_sda >= 0 && PinManager::allocateMultiplePins(i2c, 2, PinOwner::HW_I2C)) { #ifdef ESP32 if (!Wire.setPins(i2c_sda, i2c_scl)) { i2c_scl = i2c_sda = -1; } // this will fail if Wire is initialised (Wire.begin() called prior) else Wire.begin(); @@ -364,7 +410,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(spi_sclk, hw_if_spi[1]); CJSON(spi_miso, hw_if_spi[2]); PinManagerPinType spi[3] = { { spi_mosi, true }, { spi_miso, true }, { spi_sclk, true } }; - if (spi_mosi >= 0 && spi_sclk >= 0 && pinManager.allocateMultiplePins(spi, 3, PinOwner::HW_SPI)) { + if (spi_mosi >= 0 && spi_sclk >= 0 && PinManager::allocateMultiplePins(spi, 3, PinOwner::HW_SPI)) { #ifdef ESP32 SPI.begin(spi_sclk, spi_miso, spi_mosi); // SPI global uses VSPI on ESP32 and FSPI on C3, S3 #else @@ -381,7 +427,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"]; @@ -390,13 +436,12 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { else gammaCorrectBri = false; if (light_gc_col > 1.0f) gammaCorrectCol = true; else gammaCorrectCol = false; - if (gammaCorrectVal > 1.0f && gammaCorrectVal <= 3) { - if (gammaCorrectVal != 2.8f) NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); - } else { + if (gammaCorrectVal <= 1.0f || gammaCorrectVal > 3) { gammaCorrectVal = 1.0f; // no gamma correction gammaCorrectBri = false; gammaCorrectCol = false; } + NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); // fill look-up table JsonObject light_tr = light["tr"]; CJSON(fadeTransition, light_tr["mode"]); @@ -436,6 +481,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(receiveNotificationBrightness, if_sync_recv["bri"]); CJSON(receiveNotificationColor, if_sync_recv["col"]); CJSON(receiveNotificationEffects, if_sync_recv["fx"]); + CJSON(receiveNotificationPalette, if_sync_recv["pal"]); CJSON(receiveGroups, if_sync_recv["grp"]); CJSON(receiveSegmentOptions, if_sync_recv["seg"]); CJSON(receiveSegmentBounds, if_sync_recv["sb"]); @@ -480,14 +526,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); @@ -515,7 +561,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 @@ -617,7 +663,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { DEBUG_PRINTLN(F("Starting usermod config.")); JsonObject usermods_settings = doc["um"]; if (!usermods_settings.isNull()) { - needsSave = !usermods.readFromConfig(usermods_settings); + needsSave = !UsermodManager::readFromConfig(usermods_settings); } if (fromFS) return needsSave; @@ -632,12 +678,12 @@ 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; @@ -653,7 +699,7 @@ void deserializeConfigFromFS() { // save default values to /cfg.json // call readFromConfig() with an empty object so that usermods can initialize to defaults prior to saving JsonObject empty = JsonObject(); - usermods.readFromConfig(empty); + UsermodManager::readFromConfig(empty); serializeConfig(); // init Ethernet (in case default type is set at compile time) #ifdef WLED_USE_ETHERNET @@ -689,7 +735,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"); @@ -734,13 +782,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); @@ -757,7 +808,7 @@ void serializeConfig() { break; } } - #endif +#endif JsonObject hw = root.createNestedObject(F("hw")); @@ -765,8 +816,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 @@ -778,7 +830,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(); @@ -803,7 +855,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(); @@ -817,7 +869,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; @@ -834,7 +886,7 @@ void serializeConfig() { JsonArray hw_btn_ins = hw_btn.createNestedArray("ins"); // configuration for all buttons - for (uint8_t i=0; i> shift; - uint32_t r3 = ((r2 * blend) + (r1 * (blendmax - blend))) >> shift; - uint32_t g3 = ((g2 * blend) + (g1 * (blendmax - blend))) >> shift; - uint32_t b3 = ((b2 * blend) + (b1 * (blendmax - blend))) >> shift; - - return RGBW32(r3, g3, b3, w3); +uint32_t color_blend(uint32_t color1, uint32_t color2, uint8_t blend) { + // min / max blend checking is omitted: calls with 0 or 255 are rare, checking lowers overall performance + uint32_t rb1 = color1 & 0x00FF00FF; + uint32_t wg1 = (color1>>8) & 0x00FF00FF; + uint32_t rb2 = color2 & 0x00FF00FF; + uint32_t wg2 = (color2>>8) & 0x00FF00FF; + uint32_t rb3 = ((((rb1 << 8) | rb2) + (rb2 * blend) - (rb1 * blend)) >> 8) & 0x00FF00FF; + uint32_t wg3 = ((((wg1 << 8) | wg2) + (wg2 * blend) - (wg1 * blend))) & 0xFF00FF00; + return rb3 | wg3; } /* * color add function that preserves ratio - * idea: https://github.com/Aircoookie/WLED/pull/2465 by https://github.com/Proto-molecule + * original idea: https://github.com/Aircoookie/WLED/pull/2465 by https://github.com/Proto-molecule + * speed optimisations by @dedehai */ -uint32_t color_add(uint32_t c1, uint32_t c2, bool fast) +uint32_t color_add(uint32_t c1, uint32_t c2, bool preserveCR) { - if (fast) { - uint8_t r = R(c1); - uint8_t g = G(c1); - uint8_t b = B(c1); - uint8_t w = W(c1); - r = qadd8(r, R(c2)); - g = qadd8(g, G(c2)); - b = qadd8(b, B(c2)); - w = qadd8(w, W(c2)); - return RGBW32(r,g,b,w); + if (c1 == BLACK) return c2; + if (c2 == BLACK) return c1; + uint32_t rb = (c1 & 0x00FF00FF) + (c2 & 0x00FF00FF); // mask and add two colors at once + uint32_t wg = ((c1>>8) & 0x00FF00FF) + ((c2>>8) & 0x00FF00FF); + uint32_t r = rb >> 16; // extract single color values + uint32_t b = rb & 0xFFFF; + uint32_t w = wg >> 16; + uint32_t g = wg & 0xFFFF; + + if (preserveCR) { // preserve color ratios + uint32_t max = std::max(r,g); // check for overflow note + max = std::max(max,b); + max = std::max(max,w); + //unsigned max = r; // check for overflow note + //max = g > max ? g : max; + //max = b > max ? b : max; + //max = w > max ? w : max; + if (max > 255) { + uint32_t scale = (uint32_t(255)<<8) / max; // division of two 8bit (shifted) values does not work -> use bit shifts and multiplaction instead + rb = ((rb * scale) >> 8) & 0x00FF00FF; // + wg = (wg * scale) & 0xFF00FF00; + } else wg = wg << 8; //shift white and green back to correct position + return rb | wg; } else { - uint32_t r = R(c1) + R(c2); - uint32_t g = G(c1) + G(c2); - uint32_t b = B(c1) + B(c2); - uint32_t w = W(c1) + W(c2); - uint16_t max = r; - if (g > max) max = g; - if (b > max) max = b; - if (w > max) max = w; - if (max < 256) return RGBW32(r, g, b, w); - else return RGBW32(r * 255 / max, g * 255 / max, b * 255 / max, w * 255 / max); + r = r > 255 ? 255 : r; + g = g > 255 ? 255 : g; + b = b > 255 ? 255 : b; + w = w > 255 ? 255 : w; + return RGBW32(r,g,b,w); } } @@ -65,24 +62,54 @@ 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); - if (video) { - r = scale8_video(r, amount); - g = scale8_video(g, amount); - b = scale8_video(b, amount); - w = scale8_video(w, amount); - } else { - r = scale8(r, amount); - g = scale8(g, amount); - b = scale8(b, amount); - w = scale8(w, amount); + if (amount == 255) return c1; + if (c1 == BLACK || amount == 0) return BLACK; + uint32_t scaledcolor; // color order is: W R G B from MSB to LSB + uint32_t scale = amount; // 32bit for faster calculation + uint32_t addRemains = 0; + if (!video) scale++; // add one for correct scaling using bitshifts + else { // video scaling: make sure colors do not dim to zero if they started non-zero + addRemains = R(c1) ? 0x00010000 : 0; + addRemains |= G(c1) ? 0x00000100 : 0; + addRemains |= B(c1) ? 0x00000001 : 0; + addRemains |= W(c1) ? 0x01000000 : 0; } - return RGBW32(r, g, b, w); + uint32_t rb = (((c1 & 0x00FF00FF) * scale) >> 8) & 0x00FF00FF; // scale red and blue + uint32_t wg = (((c1 & 0xFF00FF00) >> 8) * scale) & 0xFF00FF00; // scale white and green + scaledcolor = (rb | wg) + addRemains; + return scaledcolor; +} + +// 1:1 replacement of fastled function optimized for ESP, slightly faster, more accurate and uses less flash (~ -200bytes) +uint32_t ColorFromPaletteWLED(const CRGBPalette16& pal, unsigned index, uint8_t brightness, TBlendType blendType) +{ + if (blendType == LINEARBLEND_NOWRAP) { + index = (index*240) >> 8; // Blend range is affected by lo4 blend of values, remap to avoid wrapping + } + unsigned hi4 = byte(index) >> 4; + const CRGB* entry = (CRGB*)((uint8_t*)(&(pal[0])) + (hi4 * sizeof(CRGB))); + unsigned red1 = entry->r; + unsigned green1 = entry->g; + unsigned blue1 = entry->b; + if (blendType != NOBLEND) { + if (hi4 == 15) entry = &(pal[0]); + else ++entry; + unsigned f2 = ((index & 0x0F) << 4) + 1; // +1 so we scale by 256 as a max value, then result can just be shifted by 8 + unsigned f1 = (257 - f2); // f2 is 1 minimum, so this is 256 max + red1 = (red1 * f1 + (unsigned)entry->r * f2) >> 8; + green1 = (green1 * f1 + (unsigned)entry->g * f2) >> 8; + blue1 = (blue1 * f1 + (unsigned)entry->b * f2) >> 8; + } + if (brightness < 255) { // note: zero checking could be done to return black but that is hardly ever used so it is omitted + uint32_t scale = brightness + 1; // adjust for rounding (bitshift) + red1 = (red1 * scale) >> 8; + green1 = (green1 * scale) >> 8; + blue1 = (blue1 * scale) >> 8; + } + return RGBW32(red1,green1,blue1,0); } void setRandomColor(byte* rgb) @@ -97,91 +124,91 @@ void setRandomColor(byte* rgb) */ CRGBPalette16 generateHarmonicRandomPalette(CRGBPalette16 &basepalette) { - CHSV palettecolors[4]; //array of colors for the new palette - uint8_t keepcolorposition = random8(4); //color position of current random palette to keep - palettecolors[keepcolorposition] = rgb2hsv_approximate(basepalette.entries[keepcolorposition*5]); //read one of the base colors of the current palette - palettecolors[keepcolorposition].hue += random8(10)-5; // +/- 5 randomness of base color - //generate 4 saturation and brightness value numbers - //only one saturation is allowed to be below 200 creating mostly vibrant colors - //only one brightness value number is allowed below 200, creating mostly bright palettes + CHSV palettecolors[4]; // array of colors for the new palette + uint8_t keepcolorposition = hw_random8(4); // color position of current random palette to keep + palettecolors[keepcolorposition] = rgb2hsv(basepalette.entries[keepcolorposition*5]); // read one of the base colors of the current palette + palettecolors[keepcolorposition].hue += hw_random8(10)-5; // +/- 5 randomness of base color + // generate 4 saturation and brightness value numbers + // only one saturation is allowed to be below 200 creating mostly vibrant colors + // only one brightness value number is allowed below 200, creating mostly bright palettes - for (int i = 0; i < 3; i++) { //generate three high values - palettecolors[i].saturation = random8(200,255); - palettecolors[i].value = random8(220,255); + for (int i = 0; i < 3; i++) { // generate three high values + palettecolors[i].saturation = hw_random8(200,255); + palettecolors[i].value = hw_random8(220,255); } - //allow one to be lower - palettecolors[3].saturation = random8(20,255); - palettecolors[3].value = random8(80,255); + // allow one to be lower + palettecolors[3].saturation = hw_random8(20,255); + palettecolors[3].value = hw_random8(80,255); - //shuffle the arrays + // shuffle the arrays for (int i = 3; i > 0; i--) { - std::swap(palettecolors[i].saturation, palettecolors[random8(i + 1)].saturation); - std::swap(palettecolors[i].value, palettecolors[random8(i + 1)].value); + std::swap(palettecolors[i].saturation, palettecolors[hw_random8(i + 1)].saturation); + std::swap(palettecolors[i].value, palettecolors[hw_random8(i + 1)].value); } - //now generate three new hues based off of the hue of the chosen current color + // now generate three new hues based off of the hue of the chosen current color uint8_t basehue = palettecolors[keepcolorposition].hue; - uint8_t harmonics[3]; //hues that are harmonic but still a little random - uint8_t type = random8(5); //choose a harmony type + uint8_t harmonics[3]; // hues that are harmonic but still a little random + uint8_t type = hw_random8(5); // choose a harmony type switch (type) { case 0: // analogous - harmonics[0] = basehue + random8(30, 50); - harmonics[1] = basehue + random8(10, 30); - harmonics[2] = basehue - random8(10, 30); + harmonics[0] = basehue + hw_random8(30, 50); + harmonics[1] = basehue + hw_random8(10, 30); + harmonics[2] = basehue - hw_random8(10, 30); break; case 1: // triadic - harmonics[0] = basehue + 113 + random8(15); - harmonics[1] = basehue + 233 + random8(15); - harmonics[2] = basehue -7 + random8(15); + harmonics[0] = basehue + 113 + hw_random8(15); + harmonics[1] = basehue + 233 + hw_random8(15); + harmonics[2] = basehue - 7 + hw_random8(15); break; case 2: // split-complementary - harmonics[0] = basehue + 145 + random8(10); - harmonics[1] = basehue + 205 + random8(10); - harmonics[2] = basehue - 5 + random8(10); + harmonics[0] = basehue + 145 + hw_random8(10); + harmonics[1] = basehue + 205 + hw_random8(10); + harmonics[2] = basehue - 5 + hw_random8(10); break; - + case 3: // square - harmonics[0] = basehue + 85 + random8(10); - harmonics[1] = basehue + 175 + random8(10); - harmonics[2] = basehue + 265 + random8(10); + harmonics[0] = basehue + 85 + hw_random8(10); + harmonics[1] = basehue + 175 + hw_random8(10); + harmonics[2] = basehue + 265 + hw_random8(10); break; case 4: // tetradic - harmonics[0] = basehue + 80 + random8(20); - harmonics[1] = basehue + 170 + random8(20); - harmonics[2] = basehue + random8(30)-15; + harmonics[0] = basehue + 80 + hw_random8(20); + harmonics[1] = basehue + 170 + hw_random8(20); + harmonics[2] = basehue - 15 + hw_random8(30); break; } - if (random8() < 128) { - //50:50 chance of shuffling hues or keep the color order + if (hw_random8() < 128) { + // 50:50 chance of shuffling hues or keep the color order for (int i = 2; i > 0; i--) { - std::swap(harmonics[i], harmonics[random8(i + 1)]); + std::swap(harmonics[i], harmonics[hw_random8(i + 1)]); } } - //now set the hues + // now set the hues int j = 0; for (int i = 0; i < 4; i++) { - if (i==keepcolorposition) continue; //skip the base color + if (i==keepcolorposition) continue; // skip the base color palettecolors[i].hue = harmonics[j]; j++; } bool makepastelpalette = false; - if (random8() < 25) { //~10% chance of desaturated 'pastel' colors + if (hw_random8() < 25) { // ~10% chance of desaturated 'pastel' colors makepastelpalette = true; } - //apply saturation & gamma correction + // apply saturation & gamma correction CRGB RGBpalettecolors[4]; for (int i = 0; i < 4; i++) { - if (makepastelpalette && palettecolors[i].saturation > 180) { + if (makepastelpalette && palettecolors[i].saturation > 180) { palettecolors[i].saturation -= 160; //desaturate all four colors - } + } RGBpalettecolors[i] = (CRGB)palettecolors[i]; //convert to RGB RGBpalettecolors[i] = gamma32(((uint32_t)RGBpalettecolors[i]) & 0x00FFFFFFU); //strip alpha from CRGB } @@ -192,34 +219,72 @@ CRGBPalette16 generateHarmonicRandomPalette(CRGBPalette16 &basepalette) RGBpalettecolors[3]); } -CRGBPalette16 generateRandomPalette(void) //generate fully random palette +CRGBPalette16 generateRandomPalette() // generate fully random palette { - return CRGBPalette16(CHSV(random8(), random8(160, 255), random8(128, 255)), - CHSV(random8(), random8(160, 255), random8(128, 255)), - CHSV(random8(), random8(160, 255), random8(128, 255)), - CHSV(random8(), random8(160, 255), random8(128, 255))); + return CRGBPalette16(CHSV(hw_random8(), hw_random8(160, 255), hw_random8(128, 255)), + CHSV(hw_random8(), hw_random8(160, 255), hw_random8(128, 255)), + CHSV(hw_random8(), hw_random8(160, 255), hw_random8(128, 255)), + CHSV(hw_random8(), hw_random8(160, 255), hw_random8(128, 255))); } -void colorHStoRGB(uint16_t hue, byte sat, byte* rgb) //hue, sat to rgb +void hsv2rgb(const CHSV32& hsv, uint32_t& rgb) // convert HSV (16bit hue) to RGB (32bit with white = 0) { - float h = ((float)hue)/65535.0f; - float s = ((float)sat)/255.0f; - int i = floorf(h*6); - float f = h * 6.0f - 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)); - p = constrain(p, 0, 255); - q = constrain(q, 0, 255); - t = constrain(t, 0, 255); - switch (i%6) { - case 0: rgb[0]=255,rgb[1]=t, rgb[2]=p; break; - case 1: rgb[0]=q, rgb[1]=255,rgb[2]=p; break; - case 2: rgb[0]=p, rgb[1]=255,rgb[2]=t; break; - case 3: rgb[0]=p, rgb[1]=q, rgb[2]=255;break; - case 4: rgb[0]=t, rgb[1]=p, rgb[2]=255;break; - case 5: rgb[0]=255,rgb[1]=p, rgb[2]=q; break; + unsigned int remainder, region, p, q, t; + unsigned int h = hsv.h; + unsigned int s = hsv.s; + unsigned int v = hsv.v; + if (s == 0) { + rgb = v << 16 | v << 8 | v; + return; } + region = h / 10923; // 65536 / 6 = 10923 + remainder = (h - (region * 10923)) * 6; + p = (v * (255 - s)) >> 8; + q = (v * (255 - ((s * remainder) >> 16))) >> 8; + t = (v * (255 - ((s * (65535 - remainder)) >> 16))) >> 8; + switch (region) { + case 0: + rgb = v << 16 | t << 8 | p; break; + case 1: + rgb = q << 16 | v << 8 | p; break; + case 2: + rgb = p << 16 | v << 8 | t; break; + case 3: + rgb = p << 16 | q << 8 | v; break; + case 4: + rgb = t << 16 | p << 8 | v; break; + default: + rgb = v << 16 | p << 8 | q; break; + } +} + +void rgb2hsv(const uint32_t rgb, CHSV32& hsv) // convert RGB to HSV (16bit hue), much more accurate and faster than fastled version +{ + hsv.raw = 0; + int32_t r = (rgb>>16)&0xFF; + int32_t g = (rgb>>8)&0xFF; + int32_t b = rgb&0xFF; + int32_t minval, maxval, delta; + minval = min(r, g); + minval = min(minval, b); + maxval = max(r, g); + maxval = max(maxval, b); + if (maxval == 0) return; // black + hsv.v = maxval; + delta = maxval - minval; + hsv.s = (255 * delta) / maxval; + if (hsv.s == 0) return; // gray value + if (maxval == r) hsv.h = (10923 * (g - b)) / delta; + else if (maxval == g) hsv.h = 21845 + (10923 * (b - r)) / delta; + else hsv.h = 43690 + (10923 * (r - g)) / delta; +} + +void colorHStoRGB(uint16_t hue, byte sat, byte* rgb) { //hue, sat to rgb + uint32_t crgb; + hsv2rgb(CHSV32(hue, sat, 255), crgb); + rgb[0] = byte((crgb) >> 16); + rgb[1] = byte((crgb) >> 8); + rgb[2] = byte(crgb); } //get RGB values from color temperature in K (https://tannerhelland.com/2012/09/18/convert-temperature-rgb-algorithm-code.html) @@ -378,13 +443,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; @@ -446,24 +511,8 @@ uint16_t approximateKelvinFromRGB(uint32_t rgb) { } } -//gamma 2.8 lookup table used for color correction -uint8_t NeoGammaWLEDMethod::gammaT[256] = { - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, - 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, - 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, - 10, 10, 11, 11, 11, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16, - 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 24, 24, 25, - 25, 26, 27, 27, 28, 29, 29, 30, 31, 32, 32, 33, 34, 35, 35, 36, - 37, 38, 39, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 50, - 51, 52, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 66, 67, 68, - 69, 70, 72, 73, 74, 75, 77, 78, 79, 81, 82, 83, 85, 86, 87, 89, - 90, 92, 93, 95, 96, 98, 99,101,102,104,105,107,109,110,112,114, - 115,117,119,120,122,124,126,127,129,131,133,135,137,138,140,142, - 144,146,148,150,152,154,156,158,160,162,164,167,169,171,173,175, - 177,180,182,184,186,189,191,193,196,198,200,203,205,208,210,213, - 215,218,220,223,225,228,231,233,236,239,241,244,247,249,252,255 }; +// gamma lookup table used for color correction (filled on 1st use (cfg.cpp & set.cpp)) +uint8_t NeoGammaWLEDMethod::gammaT[256]; // re-calculates & fills gamma table void NeoGammaWLEDMethod::calcGammaTable(float gamma) @@ -473,14 +522,14 @@ void NeoGammaWLEDMethod::calcGammaTable(float gamma) } } -uint8_t NeoGammaWLEDMethod::Correct(uint8_t value) +uint8_t IRAM_ATTR_YN NeoGammaWLEDMethod::Correct(uint8_t value) { if (!gammaCorrectCol) return value; return gammaT[value]; } // used for color gamma correction -uint32_t NeoGammaWLEDMethod::Correct32(uint32_t color) +uint32_t IRAM_ATTR_YN NeoGammaWLEDMethod::Correct32(uint32_t color) { if (!gammaCorrectCol) return color; uint8_t w = W(color); diff --git a/wled00/const.h b/wled00/const.h index dd965bc40..bdd20beba 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -5,7 +5,7 @@ * Readability defines and their associated numerical values + compile-time constants */ -#define GRADIENT_PALETTE_COUNT 58 +#define GRADIENT_PALETTE_COUNT 59 // You can define custom product info from build flags. // This is useful to allow API consumer to identify what type of WLED version @@ -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 6 // will allow 2 digital & 2 analog RGB or 6 PWM white + #define WLED_MAX_DIGITAL_CHANNELS 2 + //#define WLED_MAX_ANALOG_CHANNELS 6 #define WLED_MIN_VIRTUAL_BUSSES 3 #elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, 8 LEDC, only has 1 I2S bus, supported in NPB - #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 @@ -180,6 +195,15 @@ #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" +#define USERMOD_ID_DEEP_SLEEP 55 //Usermod "usermod_deep_sleep.h" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot @@ -195,9 +219,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 @@ -259,6 +283,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) @@ -269,36 +294,52 @@ #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 //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 @@ -327,7 +368,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 @@ -341,6 +382,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 @@ -373,6 +415,7 @@ //Playlist option byte #define PL_OPTION_SHUFFLE 0x01 +#define PL_OPTION_RESTORE 0x02 // Segment capability byte #define SEG_CAPABILITY_RGB 0x01 @@ -424,6 +467,9 @@ #define NTP_PACKET_SIZE 48 // size of NTP receive buffer #define NTP_MIN_PACKET_SIZE 48 // min expected size - NTP v4 allows for "extended information" appended to the standard fields +// Maximum number of pins per output. 5 for RGBCCT analog LEDs. +#define OUTPUT_MAX_PINS 5 + //maximum number of rendered LEDs - this does not have to match max. physical LEDs, e.g. if there are virtual busses #ifndef MAX_LEDS #ifdef ESP8266 @@ -451,7 +497,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 @@ -476,12 +522,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 @@ -508,26 +568,19 @@ #define WLED_MAX_NODES 150 #endif -//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 +// Defaults pins, type and counts to configure LED output +#if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32C3) + #ifdef WLED_ENABLE_DMX + #define DEFAULT_LED_PIN 1 + #warning "Compiling with DMX. The default LED pin has been changed to pin 1." + #else + #define DEFAULT_LED_PIN 2 // GPIO2 (D4) on Wemos D1 mini compatible boards, safe to use on any board + #endif #else - #define LEDPIN 16 // aligns with GPIO2 (D4) on Wemos D1 mini32 compatible boards -#endif -#endif - -#ifdef WLED_ENABLE_DMX -#if (LEDPIN == 2) - #undef LEDPIN - #define LEDPIN 1 - #warning "Pin conflict compiling with DMX and LEDs on pin 2. The default LED pin has been changed to pin 1." -#endif -#endif - -#ifndef DEFAULT_LED_COUNT - #define DEFAULT_LED_COUNT 30 + #define DEFAULT_LED_PIN 16 // aligns with GPIO2 (D4) on Wemos D1 mini32 compatible boards (if it is unusable it will be reassigned in WS2812FX::finalizeInit()) #endif +#define DEFAULT_LED_TYPE TYPE_WS2812_RGB +#define DEFAULT_LED_COUNT 30 #define INTERFACE_UPDATE_COOLDOWN 1000 // time in ms to wait between websockets, alexa, and MQTT updates @@ -589,4 +642,12 @@ #define HW_PIN_MISOSPI MISO #endif +// IRAM_ATTR for 8266 with 32Kb IRAM causes error: section `.text1' will not fit in region `iram1_0_seg' +// this hack removes the IRAM flag for some 1D/2D functions - somewhat slower, but it solves problems with some older 8266 chips +#ifdef WLED_SAVE_IRAM + #define IRAM_ATTR_YN +#else + #define IRAM_ATTR_YN IRAM_ATTR +#endif + #endif diff --git a/wled00/data/common.js b/wled00/data/common.js new file mode 100644 index 000000000..9378ef07a --- /dev/null +++ b/wled00/data/common.js @@ -0,0 +1,118 @@ +var d=document; +var loc = false, locip, locproto = "http:"; + +function H(pg="") { window.open("https://kno.wled.ge/"+pg); } +function GH() { window.open("https://github.com/Aircoookie/WLED"); } +function gId(c) { return d.getElementById(c); } // getElementById +function cE(e) { return d.createElement(e); } // createElement +function gEBCN(c) { return d.getElementsByClassName(c); } // getElementsByClassName +function gN(s) { return d.getElementsByName(s)[0]; } // getElementsByName +function isE(o) { return Object.keys(o).length === 0; } // isEmpty +function isO(i) { return (i && typeof i === 'object' && !Array.isArray(i)); } // isObject +function isN(n) { return !isNaN(parseFloat(n)) && isFinite(n); } // isNumber +// https://stackoverflow.com/questions/3885817/how-do-i-check-that-a-number-is-float-or-integer +function isF(n) { return n === +n && n !== (n|0); } // isFloat +function isI(n) { return n === +n && n === (n|0); } // isInteger +function toggle(el) { gId(el).classList.toggle("hide"); gId('No'+el).classList.toggle("hide"); } +function tooltip(cont=null) { + d.querySelectorAll((cont?cont+" ":"")+"[title]").forEach((element)=>{ + element.addEventListener("mouseover", ()=>{ + // save title + element.setAttribute("data-title", element.getAttribute("title")); + const tooltip = d.createElement("span"); + tooltip.className = "tooltip"; + tooltip.textContent = element.getAttribute("title"); + + // prevent default title popup + element.removeAttribute("title"); + + let { top, left, width } = element.getBoundingClientRect(); + + d.body.appendChild(tooltip); + + const { offsetHeight, offsetWidth } = tooltip; + + const offset = element.classList.contains("sliderwrap") ? 4 : 10; + top -= offsetHeight + offset; + left += (width - offsetWidth) / 2; + + tooltip.style.top = top + "px"; + tooltip.style.left = left + "px"; + tooltip.classList.add("visible"); + }); + + element.addEventListener("mouseout", ()=>{ + d.querySelectorAll('.tooltip').forEach((tooltip)=>{ + tooltip.classList.remove("visible"); + d.body.removeChild(tooltip); + }); + // restore title + element.setAttribute("title", element.getAttribute("data-title")); + }); + }); +}; +// https://www.educative.io/edpresso/how-to-dynamically-load-a-js-file-in-javascript +function loadJS(FILE_URL, async = true, preGetV = undefined, postGetV = undefined) { + let scE = d.createElement("script"); + scE.setAttribute("src", FILE_URL); + scE.setAttribute("type", "text/javascript"); + scE.setAttribute("async", async); + d.body.appendChild(scE); + // success event + scE.addEventListener("load", () => { + //console.log("File loaded"); + if (preGetV) preGetV(); + GetV(); + if (postGetV) postGetV(); + }); + // error event + scE.addEventListener("error", (ev) => { + console.log("Error on loading file", ev); + alert("Loading of configuration script failed.\nIncomplete page data!"); + }); +} +function getLoc() { + let l = window.location; + if (l.protocol == "file:") { + loc = true; + locip = localStorage.getItem('locIp'); + if (!locip) { + locip = prompt("File Mode. Please enter WLED IP!"); + localStorage.setItem('locIp', locip); + } + } else { + // detect reverse proxy + let path = l.pathname; + let paths = path.slice(1,path.endsWith('/')?-1:undefined).split("/"); + if (paths.length > 1) paths.pop(); // remove subpage (or "settings") + if (paths.length > 0 && paths[paths.length-1]=="settings") paths.pop(); // remove "settings" + if (paths.length > 1) { + locproto = l.protocol; + loc = true; + locip = l.hostname + (l.port ? ":" + l.port : "") + "/" + paths.join('/'); + } + } +} +function getURL(path) { return (loc ? locproto + "//" + locip : "") + path; } +function B() { window.open(getURL("/settings"),"_self"); } +var timeout; +function showToast(text, error = false) { + var x = gId("toast"); + if (!x) return; + x.innerHTML = text; + x.className = error ? "error":"show"; + clearTimeout(timeout); + x.style.animation = 'none'; + timeout = setTimeout(function(){ x.className = x.className.replace("show", ""); }, 2900); +} +function uploadFile(fileObj, name) { + var req = new XMLHttpRequest(); + req.addEventListener('load', function(){showToast(this.responseText,this.status >= 400)}); + req.addEventListener('error', function(e){showToast(e.stack,true);}); + req.open("POST", "/upload"); + var formData = new FormData(); + formData.append("data", fileObj.files[0], name); + req.send(formData); + fileObj.value = ''; + return false; +} diff --git a/wled00/data/cpal/cpal.htm b/wled00/data/cpal/cpal.htm index a4b913592..8fa715bc8 100644 --- a/wled00/data/cpal/cpal.htm +++ b/wled00/data/cpal/cpal.htm @@ -167,9 +167,10 @@
-
- Currently in use custom palettes -
+
+
+ Currently in use custom palettes +
@@ -187,7 +188,7 @@ Available static palettes - + @@ -204,6 +205,13 @@ var paletteName = []; // Holds the names of the palettes after load. var svgSave = '' var svgEdit = '' + var svgDist = '' + var svgTrash = '' + + const distDiv = gId("distDiv"); + distDiv.addEventListener('click', distribute); + distDiv.setAttribute('title', 'Distribute colors equally'); + distDiv.innerHTML = svgDist; function recOf() { rect = gradientBox.getBoundingClientRect(); @@ -433,7 +441,7 @@ renderY = e.srcElement.getBoundingClientRect().y + 13; trash.id = "trash"; - trash.innerHTML = ''; + trash.innerHTML = svgTrash; trash.style.position = "absolute"; trash.style.left = (renderX) + "px"; trash.style.top = (renderY) + "px"; @@ -608,8 +616,8 @@ } function generatePaletteDivs() { - const palettesDiv = d.getElementById("palettes"); - const staticPalettesDiv = d.getElementById("staticPalettes"); + const palettesDiv = gId("palettes"); + const staticPalettesDiv = gId("staticPalettes"); const paletteDivs = Array.from(palettesDiv.children).filter((child) => { return child.id.match(/^palette\d$/); // match only elements with id starting with "palette" followed by a single digit }); @@ -620,25 +628,25 @@ for (let i = 0; i < paletteArray.length; i++) { const palette = paletteArray[i]; - const paletteDiv = d.createElement("div"); + const paletteDiv = cE("div"); paletteDiv.id = `palette${i}`; paletteDiv.classList.add("palette"); const thisKey = Object.keys(palette)[0]; paletteDiv.dataset.colarray = JSON.stringify(palette[thisKey]); - const gradientDiv = d.createElement("div"); + const gradientDiv = cE("div"); gradientDiv.id = `paletteGradient${i}` - const buttonsDiv = d.createElement("div"); + const buttonsDiv = cE("div"); buttonsDiv.id = `buttonsDiv${i}`; buttonsDiv.classList.add("buttonsDiv") - const sendSpan = d.createElement("span"); + const sendSpan = cE("span"); sendSpan.id = `sendSpan${i}`; sendSpan.onclick = function() {initiateUpload(i)}; sendSpan.setAttribute('title', `Send current editor to slot ${i}`); // perhaps Save instead of Send? sendSpan.innerHTML = svgSave; sendSpan.classList.add("sendSpan") - const editSpan = d.createElement("span"); + const editSpan = cE("span"); editSpan.id = `editSpan${i}`; editSpan.onclick = function() {loadForEdit(i)}; editSpan.setAttribute('title', `Copy slot ${i} palette to editor`); @@ -712,9 +720,27 @@ } } + function distribute() { + let colorMarkers = [...gradientBox.querySelectorAll('.color-marker')]; + colorMarkers.sort((a, b) => a.getAttribute('data-truepos') - b.getAttribute('data-truepos')); + colorMarkers = colorMarkers.slice(1, -1); + const spacing = Math.round(256 / (colorMarkers.length + 1)); + + colorMarkers.forEach((e, i) => { + const markerId = e.id.match(/\d+/)[0]; + const trueCol = e.getAttribute("data-truecol"); + gradientBox.removeChild(e); + gradientBox.removeChild(gId(`colorPicker${markerId}`)); + gradientBox.removeChild(gId(`colorPickerMarker${markerId}`)); + gradientBox.removeChild(gId(`deleteMarker${markerId}`)); + addC(spacing * (i + 1), trueCol); + }); + } + function rgbToHex(r, g, b) { const hex = ((r << 16) | (g << 8) | b).toString(16); return "#" + "0".repeat(6 - hex.length) + hex; } + diff --git a/wled00/data/index.css b/wled00/data/index.css index 37eb6a596..31e2daa92 100644 --- a/wled00/data/index.css +++ b/wled00/data/index.css @@ -35,6 +35,7 @@ --sgp: "block"; --bmt: 0; --sti: 42px; + --stp: 42px; } html { @@ -96,6 +97,7 @@ button { .labels { margin: 0; padding: 8px 0 2px 0; + font-size: 19px; } #namelabel { @@ -143,7 +145,7 @@ button { } .huge { - font-size: 42px; + font-size: 60px !important; } .segt, .plentry TABLE { @@ -358,7 +360,7 @@ button { #putil, #segutil, #segutil2 { min-height: 42px; - margin: 13px auto 0; + margin: 0 auto; } #segutil .segin { @@ -468,7 +470,7 @@ button { padding: 4px 2px; position: relative; opacity: 1; - transition: opacity .5s linear, height .25s, transform .25s; + transition: opacity .25s linear, height .2s, transform .2s; } .filter { @@ -583,6 +585,10 @@ button { z-index: 3; } +#rover .ibtn { + margin: 5px; +} + #ndlt { margin: 12px 0; } @@ -623,7 +629,7 @@ button { padding-bottom: 8px; } -.infobtn { +#info .ibtn { margin: 5px; } @@ -847,7 +853,7 @@ input[type=range]::-moz-range-thumb { width: 135px; } -#nodes .infobtn { +#nodes .ibtn { margin: 0; } @@ -885,12 +891,12 @@ a.btn { line-height: 28px; } -/* Quick color select Black button (has white border) */ -.qcsb { +/* Quick color select Black and White button (has white/black border, depending on the theme) */ +.qcsb, .qcsw { width: 26px; height: 26px; line-height: 26px; - border: 1px solid #fff; + border: 1px solid var(--c-f); } /* Hex color input wrapper div */ @@ -923,6 +929,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; @@ -1034,7 +1041,7 @@ textarea { .segname .flr, .pname .flr { transform: rotate(0deg); - right: -6px; + /*right: -6px;*/ } /* segment power wrapper */ @@ -1290,6 +1297,15 @@ TD .checkmark, TD .radiomark { margin: 0 auto 12px; min-height: 40px; border: 1px solid var(--c-2); + width: 100%; +} + +#segutil { + margin-bottom: 12px; +} + +#segcont > div:first-child, #fxFind { + margin-top: 4px; } /* Simplify segments */ @@ -1328,15 +1344,22 @@ TD .checkmark, TD .radiomark { box-shadow: 0 0 10px 4px var(--c-1); } +.lstI .flr:hover { + background: var(--c-6); + border-radius: 100%; +} + #pcont .selected:not([class*="expanded"]) { bottom: 52px; top: 42px; } -#fxlist .lstI.selected, -#pallist .lstI.selected { +#fxlist .lstI.selected { top: calc(var(--sti) + 42px); } +#pallist .lstI.selected { + top: calc(var(--stp) + 42px); +} dialog::backdrop { backdrop-filter: blur(10px); @@ -1351,10 +1374,12 @@ dialog { color: var(--c-f); } -#fxlist .lstI.sticky, -#pallist .lstI.sticky { +#fxlist .lstI.sticky { top: var(--sti); } +#pallist .lstI.sticky { + top: var(--stp); +} /* list item content */ .lstIcontent { @@ -1422,6 +1447,11 @@ dialog { position: relative; } +.presin { + width: 100%; + box-sizing: border-box; +} + .btn-s, .btn-n { border: 1px solid var(--c-2); @@ -1517,7 +1547,7 @@ dialog { #info table .btn, #nodes table .btn { width: 200px; } - #info .infobtn, #nodes .infobtn { + #info .ibtn, #nodes .ibtn { width: 145px; } #info div, #nodes div, #nodes a.btn { diff --git a/wled00/data/index.htm b/wled00/data/index.htm index a58c76da6..8adec791f 100644 --- a/wled00/data/index.htm +++ b/wled00/data/index.htm @@ -106,7 +106,7 @@
-
+

@@ -126,9 +126,10 @@
+ - +

Color palette

@@ -303,13 +304,13 @@
Loading...

- - - - + + + +

- Made with ❤︎ by Aircoookie and the WLED community + Made with ❤︎ by Aircoookie and the WLED community @@ -330,8 +331,8 @@
?


To use built-in effects, use an override button below.
You can return to realtime mode by pressing the star in the top left corner.
- -
+ +
For best performance, it is recommended to turn off the streaming source when not in use. diff --git a/wled00/data/index.js b/wled00/data/index.js index 7889e3b34..1482c27b5 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -1,7 +1,7 @@ //page js var loc = false, locip, locproto = "http:"; var isOn = false, nlA = false, isLv = false, isInfo = false, isNodes = false, syncSend = false/*, syncTglRecv = true*/; -var hasWhite = false, hasRGB = false, hasCCT = false; +var hasWhite = false, hasRGB = false, hasCCT = false, has2D = false; var nlDur = 60, nlTar = 0; var nlMode = false; var segLmax = 0; // size (in pixels) of largest selected segment @@ -272,6 +272,7 @@ function onLoad() selectSlot(0); updateTablinks(0); + handleLocationHash(); cpick.on("input:end", () => {setColor(1);}); cpick.on("color:change", () => {updatePSliders()}); pmtLS = localStorage.getItem('wledPmt'); @@ -281,12 +282,12 @@ function onLoad() // fill effect extra data array loadFXData(()=>{ // load and populate effects - loadFX(()=>{ + setTimeout(()=>{loadFX(()=>{ loadPalettesData(()=>{ requestJson();// will load presets and create WS if (cfg.comp.css) setTimeout(()=>{loadSkinCSS('skinCss')},50); }); - }); + })},50); }); }); resetUtil(); @@ -304,7 +305,6 @@ function updateTablinks(tabI) { var tablinks = gEBCN("tablinks"); for (var i of tablinks) i.classList.remove('active'); - if (pcMode) return; tablinks[tabI].classList.add('active'); } @@ -315,6 +315,21 @@ function openTab(tabI, force = false) _C.classList.toggle('smooth', false); _C.style.setProperty('--i', iSlide); updateTablinks(tabI); + switch (tabI) { + case 0: window.location.hash = "Colors"; break; + case 1: window.location.hash = "Effects"; break; + case 2: window.location.hash = "Segments"; break; + case 3: window.location.hash = "Presets"; break; + } +} + +function handleLocationHash() { + switch (window.location.hash) { + case "#Colors": openTab(0); break; + case "#Effects": openTab(1); break; + case "#Segments": openTab(2); break; + case "#Presets": openTab(3); break; + } } var timeout; @@ -430,7 +445,7 @@ function presetError(empty) if (bckstr.length > 10) hasBackup = true; } catch (e) {} - var cn = `
`; + var cn = `
`; if (empty) cn += `You have no presets yet!`; else @@ -442,8 +457,8 @@ function presetError(empty) cn += `However, there is backup preset data of a previous installation available.
(Saving a preset will hide this and overwrite the backup)`; else cn += `Here is a backup of the last known good state:`; - cn += `
`; - cn += `
`; + cn += `
`; + cn += `
`; } cn += `
`; gId('pcont').innerHTML = cn; @@ -573,7 +588,7 @@ function loadFXData(callback = null) fxdata = []; if (!retry) { retry = true; - setTimeout(loadFXData, 500); // retry + setTimeout(()=>{loadFXData(loadFX);}, 500); // retry } showToast(e, true); }) @@ -618,7 +633,7 @@ function populatePresets(fromls) cn += `
`; if (cfg.comp.pid) cn += `
${i}
`; - cn += `
${isPlaylist(i)?"":""}${pName(i)} + cn += `
${i==lastinfo.leds.bootps?"":""}${isPlaylist(i)?"":""}${pName(i)}
@@ -654,18 +669,15 @@ function parseInfo(i) { //syncTglRecv = i.str; maxSeg = i.leds.maxseg; pmt = i.fs.pmt; + if (pcMode && !i.wifi.ap) gId('edit').classList.remove("hide"); else gId('edit').classList.add("hide"); gId('buttonNodes').style.display = lastinfo.ndc > 0 ? null:"none"; // do we have a matrix set-up mw = i.leds.matrix ? i.leds.matrix.w : 0; mh = i.leds.matrix ? i.leds.matrix.h : 0; isM = mw>0 && mh>0; if (!isM) { - //gId("filter0D").classList.remove('hide'); - //gId("filter1D").classList.add('hide'); gId("filter2D").classList.add('hide'); } else { - //gId("filter0D").classList.add('hide'); - //gId("filter1D").classList.remove('hide'); gId("filter2D").classList.remove('hide'); } // if (i.noaudio) { @@ -694,8 +706,6 @@ function parseInfo(i) { function populateInfo(i) { var cn=""; - var heap = i.freeheap/1024; - heap = heap.toFixed(1); var pwr = i.leds.pwr; var pwru = "Not calculated"; if (pwr > 1000) {pwr /= 1000; pwr = pwr.toFixed((pwr > 10) ? 0 : 1); pwru = pwr + " A";} @@ -720,20 +730,22 @@ ${inforow("Build",i.vid)} ${inforow("Signal strength",i.wifi.signal +"% ("+ i.wifi.rssi, " dBm)")} ${inforow("Uptime",getRuntimeStr(i.uptime))} ${inforow("Time",i.time)} -${inforow("Free heap",heap," kB")} +${inforow("Free heap",(i.freeheap/1024).toFixed(1)," kB")} ${i.psram?inforow("Free PSRAM",(i.psram/1024).toFixed(1)," kB"):""} ${inforow("Estimated current",pwru)} ${inforow("Average FPS",i.leds.fps)} ${inforow("MAC address",i.mac)} +${inforow("CPU clock",i.clock," MHz")} +${inforow("Flash size",i.flash," MB")} ${inforow("Filesystem",i.fs.u + "/" + i.fs.t + " kB (" +Math.round(i.fs.u*100/i.fs.t) + "%)")} ${inforow("Environment",i.arch + " " + i.core + " (" + i.lwip + ")")} `; gId('kv').innerHTML = cn; // update all sliders in Info - for (let sd of (d.querySelectorAll('#kv .sliderdisplay')||[])) { + d.querySelectorAll('#kv .sliderdisplay').forEach((sd,i) => { let s = sd.previousElementSibling; if (s) updateTrail(s); - } + }); } function populateSegments(s) @@ -786,6 +798,7 @@ function populateSegments(s) ``+ ``+ ``+ + ``+ `
`+ `
`; let sndSim = `
Sound sim
`+ @@ -863,14 +876,11 @@ function populateSegments(s) gId("segcont").classList.remove("hide"); let noNewSegs = (lowestUnused >= maxSeg); resetUtil(noNewSegs); - if (gId('selall')) gId('selall').checked = true; for (var i = 0; i <= lSeg; i++) { if (!gId(`seg${i}`)) continue; updateLen(i); updateTrail(gId(`seg${i}bri`)); gId(`segr${i}`).classList.add("hide"); - //if (i 1) ? "block":"none"; // rsbtn parent if (Array.isArray(li.maps) && li.maps.length>1) { - let cont = `Ledmap: `; + for (const k of li.maps) cont += ``; cont += "
"; gId("ledmap").innerHTML = cont; gId("ledmap").classList.remove('hide'); @@ -978,13 +988,12 @@ function populatePalettes() function redrawPalPrev() { - let palettes = d.querySelectorAll('#pallist .lstI'); - for (var pal of (palettes||[])) { + d.querySelectorAll('#pallist .lstI').forEach((pal,i) =>{ let lP = pal.querySelector('.lstIprev'); if (lP) { lP.style = genPalPrevCss(pal.dataset.id); } - } + }); } function genPalPrevCss(id) @@ -1330,7 +1339,7 @@ function updateSelectedFx() if (ds.id>0) { if (segLmax==0) fx.classList.add('hide'); // none of the segments selected (hide all effects) else { - if ((segLmax==1 && (!opts[3] || opts[3].indexOf("0")<0)) || (!isM && opts[3] && ((opts[3].indexOf("2")>=0 && opts[3].indexOf("1")<0)))) fx.classList.add('hide'); + if ((segLmax==1 && (!opts[3] || opts[3].indexOf("0")<0)) || (!has2D && opts[3] && ((opts[3].indexOf("2")>=0 && opts[3].indexOf("1")<0)))) fx.classList.add('hide'); else fx.classList.remove('hide'); } } @@ -1345,10 +1354,12 @@ function updateSelectedFx() } // hide 2D mapping and/or sound simulation options - var segs = gId("segcont").querySelectorAll(`div[data-map="map2D"]`); - for (const seg of segs) if (selectedName.indexOf("\u25A6")<0) seg.classList.remove('hide'); else seg.classList.add('hide'); - var segs = gId("segcont").querySelectorAll(`div[data-snd="si"]`); - for (const seg of segs) if (selectedName.indexOf("\u266A")<0 && selectedName.indexOf("\u266B")<0) seg.classList.add('hide'); else seg.classList.remove('hide'); // also "♫ "? + gId("segcont").querySelectorAll(`div[data-map="map2D"]`).forEach((seg)=>{ + if (selectedName.indexOf("\u25A6")<0) seg.classList.remove('hide'); else seg.classList.add('hide'); + }); + gId("segcont").querySelectorAll(`div[data-snd="si"]`).forEach((seg)=>{ + if (selectedName.indexOf("\u266A")<0 && selectedName.indexOf("\u266B")<0) seg.classList.add('hide'); else seg.classList.remove('hide'); // also "♫ "? + }); } } @@ -1430,7 +1441,7 @@ function readState(s,command=false) populateSegments(s); var selc=0; var sellvl=0; // 0: selc is invalid, 1: selc is mainseg, 2: selc is first selected - hasRGB = hasWhite = hasCCT = false; + hasRGB = hasWhite = hasCCT = has2D = false; segLmax = 0; for (let i = 0; i < (s.seg||[]).length; i++) { @@ -1441,20 +1452,23 @@ function readState(s,command=false) if (s.seg[i].sel) { if (sellvl < 2) selc = i; // get first selected segment sellvl = 2; - var lc = lastinfo.leds.seglc[i]; + let w = (s.seg[i].stop - s.seg[i].start); + let h = s.seg[i].stopY ? (s.seg[i].stopY - s.seg[i].startY) : 1; + let lc = lastinfo.leds.seglc[i]; hasRGB |= !!(lc & 0x01); hasWhite |= !!(lc & 0x02); hasCCT |= !!(lc & 0x04); - let sLen = (s.seg[i].stop - s.seg[i].start)*(s.seg[i].stopY?(s.seg[i].stopY - s.seg[i].startY):1); - segLmax = segLmax < sLen ? sLen : segLmax; + has2D |= w > 1 && h > 1; + if (w*h > segLmax) segLmax = w*h; } } var i=s.seg[selc]; if (sellvl == 1) { - var lc = lastinfo.leds.seglc[selc]; + let lc = lastinfo.leds.seglc[selc]; hasRGB = !!(lc & 0x01); hasWhite = !!(lc & 0x02); hasCCT = !!(lc & 0x04); + has2D = (i.stop - i.start) > 1 && (i.stopY ? (i.stopY - i.startY) : 1) > 1; } if (!i) { showToast('No Segments!', true); @@ -1462,8 +1476,6 @@ function readState(s,command=false) return true; } - if (s.seg.length>2) d.querySelectorAll(".pop").forEach((e)=>{e.classList.remove("hide");}); - var cd = gId('csl').querySelectorAll("button"); for (let e = cd.length-1; e >= 0; e--) { cd[e].dataset.r = i.col[e][0]; @@ -1487,6 +1499,12 @@ function readState(s,command=false) if (s.error && s.error != 0) { var errstr = ""; switch (s.error) { + case 1: + errstr = "Denied!"; + break; + case 3: + errstr = "Buffer locked!"; + break; case 8: errstr = "Effect RAM depleted!"; break; @@ -1551,8 +1569,7 @@ function setEffectParameters(idx) var paOnOff = (effectPars.length<3 || effectPars[2]=='')?[]:effectPars[2].split(","); // set html slider items on/off - let sliders = d.querySelectorAll("#sliders .sliderwrap"); - sliders.forEach((slider, i)=>{ + d.querySelectorAll("#sliders .sliderwrap").forEach((slider, i)=>{ let text = slider.getAttribute("title"); if ((!controlDefined && i<((idx<128)?2:nSliders)) || (slOnOff.length>i && slOnOff[i]!="")) { if (slOnOff.length>i && slOnOff[i]!="!") text = slOnOff[i]; @@ -1566,8 +1583,7 @@ function setEffectParameters(idx) if (slOnOff.length > 5) { // up to 3 checkboxes gId('fxopt').classList.remove('fade'); - let checks = d.querySelectorAll("#sliders .ochkl"); - checks.forEach((check, i)=>{ + d.querySelectorAll("#sliders .ochkl").forEach((check, i)=>{ let text = check.getAttribute("title"); if (5+i5+i && slOnOff[5+i]!="!") text = slOnOff[5+i]; @@ -1694,9 +1710,7 @@ function requestJson(command=null) fetch(getURL('/json/si'), { method: type, - headers: { - "Content-type": "application/json; charset=UTF-8" - }, + headers: {"Content-Type": "application/json; charset=UTF-8"}, body: req }) .then(res => { @@ -1838,7 +1852,7 @@ function makeSeg() }); var cn = `
`+ `
`+ - ``+ + ``+ ``+ ``+ ``+ @@ -1864,13 +1878,19 @@ function makeSeg() function resetUtil(off=false) { - gId('segutil').innerHTML = `
` + gId('segutil').innerHTML = `
` + '' + `
Add segment
` + '
' + `` + '
' + '
'; + gId('selall').checked = true; + for (var i = 0; i <= lSeg; i++) { + if (!gId(`seg${i}`)) continue; + if (!gId(`seg${i}sel`).checked) gId('selall').checked = false; // uncheck if at least one is unselected. + } + if (lSeg>2) d.querySelectorAll("#Segments .pop").forEach((e)=>{e.classList.remove("hide");}); } function makePlSel(el, incPl=false) @@ -1959,6 +1979,7 @@ function plR(p) function makeP(i,pl) { var content = ""; + const bps = lastinfo.leds.bootps; if (pl) { if (i===0) plJson[0] = { ps: [1], @@ -1983,7 +2004,7 @@ function makeP(i,pl)
End preset:
@@ -2007,7 +2028,7 @@ ${makePlSel(plJson[i].end?plJson[i].end:0, true)} `; if (Array.isArray(lastinfo.maps) && lastinfo.maps.length>1) { content += `
Ledmap: 
"; } } @@ -2024,6 +2045,11 @@ ${makePlSel(plJson[i].end?plJson[i].end:0, true)}
API command
${content}
+
Save to ID 0)?i:getLowestUnusedP()}>
@@ -2150,13 +2176,12 @@ function selGrp(g) { event.preventDefault(); event.stopPropagation(); - var sel = gId(`segcont`).querySelectorAll(`div[data-set="${g}"]`); var obj = {"seg":[]}; for (let i=0; i<=lSeg; i++) if (gId(`seg${i}`)) obj.seg.push({"id":i,"sel":false}); - for (let s of (sel||[])) { + gId(`segcont`).querySelectorAll(`div[data-set="${g}"]`).forEach((s)=>{ let i = parseInt(s.id.substring(3)); obj.seg[i] = {"id":i,"sel":true}; - } + }); if (obj.seg.length) requestJson(obj); } @@ -2445,8 +2470,9 @@ function saveP(i,pl) if (gId(`p${i}lmp`) && gId(`p${i}lmp`).value!=="") obj.ledmap = parseInt(gId(`p${i}lmp`).value); } } - - obj.psave = pI; obj.n = pN; + if (gId(`p${i}bps`).checked) obj.bootps = pI; + obj.psave = pI; + obj.n = pN; var pQN = gId(`p${i}ql`).value; if (pQN.length > 0) obj.ql = pQN; @@ -2667,7 +2693,9 @@ function setBalance(b) function rmtTgl(ip,i) { event.preventDefault(); event.stopPropagation(); - fetch(`http://${ip}/win&T=2`, {method: 'get'}) + fetch(`http://${ip}/win&T=2`, { + method: 'get' + }) .then((r)=>{ return r.text(); }) @@ -2759,21 +2787,23 @@ function loadPalettesData(callback = null) function getPalettesData(page, callback) { fetch(getURL(`/json/palx?page=${page}`), { - method: 'get', - headers: { - "Content-type": "application/json; charset=UTF-8" - } + method: 'get' }) .then(res => { if (!res.ok) showErrorToast(); return res.json(); }) .then(json => { + retry = false; palettesData = Object.assign({}, palettesData, json.p); - if (page < json.m) setTimeout(()=>{ getPalettesData(page + 1, callback); }, 50); + if (page < json.m) setTimeout(()=>{ getPalettesData(page + 1, callback); }, 75); else callback(); }) .catch((error)=>{ + if (!retry) { + retry = true; + setTimeout(()=>{getPalettesData(page,callback);}, 500); // retry + } showToast(error, true); }); } @@ -2796,9 +2826,14 @@ function search(field, listId = null) { const search = field.value !== ''; // restore default preset sorting if no search term is entered - if (listId === 'pcont' && !search) { - populatePresets(); - return; + if (!search) { + if (listId === 'pcont') { populatePresets(); return; } + if (listId === 'pallist') { + let id = parseInt(d.querySelector('#pallist input[name="palette"]:checked').value); // preserve selected palette + populatePalettes(); + updateSelectedPalette(id); + return; + } } // clear filter if searching in fxlist @@ -2809,15 +2844,19 @@ function search(field, listId = null) { // do not search if filter is active if (gId("filters").querySelectorAll("input[type=checkbox]:checked").length) return; - const listItems = gId(listId).querySelectorAll('.lstI'); // filter list items but leave (Default & Solid) always visible - for (i = (listId === 'pcont' ? 0 : 1); i < listItems.length; i++) { - const listItem = listItems[i]; + const listItems = gId(listId).querySelectorAll('.lstI'); + listItems.forEach((listItem, i) => { + if (listId !== 'pcont' && i === 0) return; const listItemName = listItem.querySelector('.lstIname').innerText.toUpperCase(); const searchIndex = listItemName.indexOf(field.value.toUpperCase()); - listItem.style.display = (searchIndex < 0) ? 'none' : ''; - listItem.dataset.searchIndex = searchIndex; - } + if (searchIndex < 0) { + listItem.dataset.searchIndex = Number.MAX_SAFE_INTEGER; + } else { + listItem.dataset.searchIndex = searchIndex; + } + listItem.style.display = (searchIndex < 0) && !listItem.classList.contains("selected") ? 'none' : ''; + }); // sort list items by search index and name const sortedListItems = Array.from(listItems).sort((a, b) => { @@ -2857,18 +2896,25 @@ function initFilters() { function filterFocus(e) { const f = gId("filters"); - if (e.type === "focus") f.classList.remove('fade'); // immediately show (still has transition) - // compute sticky top (with delay for transition) - setTimeout(() => { - const sti = parseInt(getComputedStyle(d.documentElement).getPropertyValue('--sti')) + (e.type === "focus" ? 1 : -1) * f.offsetHeight; - sCol('--sti', sti + "px"); - }, 252); + const c = !!f.querySelectorAll("input[type=checkbox]:checked").length; + const h = f.offsetHeight; + const sti = parseInt(getComputedStyle(d.documentElement).getPropertyValue('--sti')); + if (e.type === "focus") { + // compute sticky top (with delay for transition) + if (!h) setTimeout(() => { + sCol('--sti', (sti+f.offsetHeight) + "px"); // has an unpleasant consequence on palette offset + }, 255); + f.classList.remove('fade'); // immediately show (still has transition) + } if (e.type === "blur") { setTimeout(() => { if (e.target === document.activeElement && document.hasFocus()) return; // do not hide if filter is active - if (gId("filters").querySelectorAll("input[type=checkbox]:checked").length) return; - f.classList.add('fade'); + if (!c) { + // compute sticky top + sCol('--sti', (sti-h) + "px"); // has an unpleasant consequence on palette offset + f.classList.add('fade'); + } }, 255); // wait with hiding } } @@ -2878,14 +2924,12 @@ function filterFx() { inputField.value = ''; inputField.focus(); clean(inputField.nextElementSibling); - const listItems = gId("fxlist").querySelectorAll('.lstI'); - for (let i = 1; i < listItems.length; i++) { - const listItem = listItems[i]; + gId("fxlist").querySelectorAll('.lstI').forEach((listItem, i) => { const listItemName = listItem.querySelector('.lstIname').innerText; let hide = false; - gId("filters").querySelectorAll("input[type=checkbox]").forEach((e) => { if (e.checked && !listItemName.includes(e.dataset.flt)) hide = true; }); - listItem.style.display = hide ? 'none' : ''; - } + gId("filters").querySelectorAll("input[type=checkbox]").forEach((e) => { if (e.checked && !listItemName.includes(e.dataset.flt)) hide = i > 0 /*true*/; }); + listItem.style.display = hide && !listItem.classList.contains("selected") ? 'none' : ''; + }); } function preventBlur(e) { @@ -3036,6 +3080,7 @@ function size() function togglePcMode(fromB = false) { + let ap = (fromB && !lastinfo) || (lastinfo && lastinfo.wifi && lastinfo.wifi.ap); if (fromB) { pcModeA = !pcModeA; localStorage.setItem('pcm', pcModeA); @@ -3043,9 +3088,9 @@ function togglePcMode(fromB = false) pcMode = (wW >= 1024) && pcModeA; if (cpick) cpick.resize(pcMode && wW>1023 && wW<1250 ? 230 : 260); // for tablet in landscape if (!fromB && ((wW < 1024 && lastw < 1024) || (wW >= 1024 && lastw >= 1024))) return; // no change in size and called from size() - openTab(0, true); - updateTablinks(0); + if (pcMode) openTab(0, true); gId('buttonPcm').className = (pcMode) ? "active":""; + if (pcMode && !ap) gId('edit').classList.remove("hide"); else gId('edit').classList.add("hide"); gId('bot').style.height = (pcMode && !cfg.comp.pcmbot) ? "0":"auto"; sCol('--bh', gId('bot').clientHeight + "px"); _C.style.width = (pcMode || simplifiedUI)?'100%':'400%'; @@ -3071,8 +3116,7 @@ function mergeDeep(target, ...sources) function tooltip(cont=null) { - const elements = d.querySelectorAll((cont?cont+" ":"")+"[title]"); - elements.forEach((element)=>{ + d.querySelectorAll((cont?cont+" ":"")+"[title]").forEach((element)=>{ element.addEventListener("mouseover", ()=>{ // save title element.setAttribute("data-title", element.getAttribute("title")); @@ -3099,8 +3143,7 @@ function tooltip(cont=null) }); element.addEventListener("mouseout", ()=>{ - const tooltips = d.querySelectorAll('.tooltip'); - tooltips.forEach((tooltip)=>{ + d.querySelectorAll('.tooltip').forEach((tooltip)=>{ tooltip.classList.remove("visible"); d.body.removeChild(tooltip); }); @@ -3174,7 +3217,7 @@ function simplifyUI() { createDropdown("palw", "Change palette"); createDropdown("fx", "Change effect", [gId("fxFind"), gId("fxlist")]); - // Hide pallete label + // Hide palette label gId("pall").style.display = "none"; gId("Colors").insertBefore(document.createElement("br"), gId("pall")); // Hide effect label @@ -3206,6 +3249,7 @@ size(); _C.style.setProperty('--n', N); window.addEventListener('resize', size, true); +window.addEventListener('hashchange', handleLocationHash); _C.addEventListener('mousedown', lock, false); _C.addEventListener('touchstart', lock, false); diff --git a/wled00/data/pxmagic/pxmagic.htm b/wled00/data/pxmagic/pxmagic.htm index d59f924cf..8ec11f454 100644 --- a/wled00/data/pxmagic/pxmagic.htm +++ b/wled00/data/pxmagic/pxmagic.htm @@ -882,10 +882,8 @@ hostnameLabel(); })(); - function gId(id) { - return d.getElementById(id); - } - + function gId(e) {return d.getElementById(e);} + function cE(e) {return d.createElement(e);} function hostnameLabel() { const link = gId("wledEdit"); link.href = WLED_URL + "/edit"; @@ -1675,7 +1673,7 @@ } function createCanvas(width, height) { - const canvas = d.createElement("canvas"); + const canvas = cE("canvas"); canvas.width = width; canvas.height = height; @@ -1719,7 +1717,7 @@ const blob = new Blob([text], { type: mimeType }); const url = URL.createObjectURL(blob); - const anchorElement = d.createElement("a"); + const anchorElement = cE("a"); anchorElement.href = url; anchorElement.download = `${filename}.${fileExtension}`; @@ -1790,7 +1788,7 @@ hideElement = "preview" ) { const hide = gId(hideElement); - const toast = d.createElement("div"); + const toast = cE("div"); const wait = 100; toast.style.animation = "fadeIn"; @@ -1799,14 +1797,14 @@ toast.classList.add("toast", type); - const body = d.createElement("span"); + const body = cE("span"); body.classList.add("toast-body"); body.textContent = message; toast.appendChild(body); - const progress = d.createElement("div"); + const progress = cE("div"); progress.classList.add("toast-progress"); progress.style.animation = "progress"; @@ -1831,7 +1829,7 @@ function carousel(id, images, delay = 3000) { let index = 0; - const carousel = d.createElement("div"); + const carousel = cE("div"); carousel.classList.add("carousel"); images.forEach((canvas, i) => { @@ -1959,7 +1957,7 @@ let errorElement = parent.querySelector(".error-message"); if (!errorElement) { - errorElement = d.createElement("div"); + errorElement = cE("div"); errorElement.classList.add("error-message"); parent.appendChild(errorElement); } diff --git a/wled00/data/settings.htm b/wled00/data/settings.htm index 52b64006b..82c778214 100644 --- a/wled00/data/settings.htm +++ b/wled00/data/settings.htm @@ -4,53 +4,12 @@ WLED Settings +
-
+

Imma firin ma lazer (if it has DMX support)

diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 611653a64..956cf7d90 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -4,59 +4,52 @@ LED Settings +
-
+

LED & Hardware setup

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

Advanced

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

diff --git a/wled00/data/settings_sec.htm b/wled00/data/settings_sec.htm index 96e88b923..34f8603d0 100644 --- a/wled00/data/settings_sec.htm +++ b/wled00/data/settings_sec.htm @@ -4,55 +4,9 @@ Misc Settings +
-
+


diff --git a/wled00/data/settings_um.htm b/wled00/data/settings_um.htm index a50fc8269..c2f0ffbf2 100644 --- a/wled00/data/settings_um.htm +++ b/wled00/data/settings_um.htm @@ -4,75 +4,55 @@ Usermod Settings +
-
+

WiFi setup

@@ -222,8 +178,23 @@ Static subnet mask:

Experimental

Force 802.11g mode (ESP8266 only):
Disable WiFi sleep:
- Can help with connectivity issues.
- Do not enable if WiFi is working correctly, increases power consumption.
+ Can help with connectivity issues and Audioreactive sync.
+ Disabling WiFi sleep increases power consumption.

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

ESP-NOW Wireless

@@ -246,6 +217,7 @@ Static subnet mask:
+ diff --git a/wled00/data/update.htm b/wled00/data/update.htm index f157f98d8..23a6a866e 100644 --- a/wled00/data/update.htm +++ b/wled00/data/update.htm @@ -17,7 +17,8 @@

WLED Software Update

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


diff --git a/wled00/dmx.cpp b/wled00/dmx.cpp index 6bdf80a79..dbe70f2aa 100644 --- a/wled00/dmx.cpp +++ b/wled00/dmx.cpp @@ -22,7 +22,7 @@ void handleDMX() bool calc_brightness = true; // check if no shutter channel is set - for (byte i = 0; i < DMXChannels; i++) + for (unsigned i = 0; i < DMXChannels; i++) { if (DMXFixtureMap[i] == 5) calc_brightness = false; } diff --git a/wled00/e131.cpp b/wled00/e131.cpp index e54f60bf3..bc26a0639 100644 --- a/wled00/e131.cpp +++ b/wled00/e131.cpp @@ -11,6 +11,7 @@ //DDP protocol support, called by handleE131Packet //handles RGB data only void handleDDPPacket(e131_packet_t* p) { + static bool ddpSeenPush = false; // have we seen a push yet? int lastPushSeq = e131LastSequenceNumber[0]; //reject late packets belonging to previous frame (assuming 4 packets max. before push) @@ -25,28 +26,30 @@ void handleDDPPacket(e131_packet_t* p) { } } - uint8_t ddpChannelsPerLed = ((p->dataType & 0b00111000)>>3 == 0b011) ? 4 : 3; // data type 0x1B (formerly 0x1A) is RGBW (type 3, 8 bit/channel) + unsigned ddpChannelsPerLed = ((p->dataType & 0b00111000)>>3 == 0b011) ? 4 : 3; // data type 0x1B (formerly 0x1A) is RGBW (type 3, 8 bit/channel) uint32_t start = htonl(p->channelOffset) / ddpChannelsPerLed; start += DMXAddress / ddpChannelsPerLed; - uint16_t stop = start + htons(p->dataLen) / ddpChannelsPerLed; + unsigned stop = start + htons(p->dataLen) / ddpChannelsPerLed; uint8_t* data = p->data; - uint16_t c = 0; + unsigned c = 0; if (p->flags & DDP_TIMECODE_FLAG) c = 4; //packet has timecode flag, we do not support it, but data starts 4 bytes later + if (realtimeMode != REALTIME_MODE_DDP) ddpSeenPush = false; // just starting, no push yet realtimeLock(realtimeTimeoutMs, REALTIME_MODE_DDP); if (!realtimeOverride || (realtimeMode && useMainSegmentOnly)) { - for (uint16_t i = start; i < stop; i++) { + if (useMainSegmentOnly) strip.getMainSegment().beginDraw(); + for (unsigned i = start; i < stop; i++, c += ddpChannelsPerLed) { setRealtimePixel(i, data[c], data[c+1], data[c+2], ddpChannelsPerLed >3 ? data[c+3] : 0); - c += ddpChannelsPerLed; } } bool push = p->flags & DDP_PUSH_FLAG; - if (push) { + ddpSeenPush |= push; + if (!ddpSeenPush || push) { // if we've never seen a push, or this is one, render display e131NewData = true; - byte sn = p->sequenceNum & 0xF; + int sn = p->sequenceNum & 0xF; if (sn) e131LastSequenceNumber[0] = sn; } } @@ -54,9 +57,9 @@ void handleDDPPacket(e131_packet_t* p) { //E1.31 and Art-Net protocol support void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ - uint16_t uni = 0, dmxChannels = 0; + int uni = 0, dmxChannels = 0; uint8_t* e131_data = nullptr; - uint8_t seq = 0, mde = REALTIME_MODE_E131; + int seq = 0, mde = REALTIME_MODE_E131; if (protocol == P_ARTNET) { @@ -102,17 +105,11 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ // only listen for universes we're handling & allocated memory if (uni < e131Universe || uni >= (e131Universe + E131_MAX_UNIVERSE_COUNT)) return; - uint8_t previousUniverses = uni - e131Universe; + unsigned previousUniverses = uni - e131Universe; if (e131SkipOutOfSequence) if (seq < e131LastSequenceNumber[previousUniverses] && seq > 20 && e131LastSequenceNumber[previousUniverses] < 250){ - DEBUG_PRINT(F("skipping E1.31 frame (last seq=")); - DEBUG_PRINT(e131LastSequenceNumber[previousUniverses]); - DEBUG_PRINT(F(", current seq=")); - DEBUG_PRINT(seq); - DEBUG_PRINT(F(", universe=")); - DEBUG_PRINT(uni); - DEBUG_PRINTLN(")"); + DEBUG_PRINTF_P(PSTR("skipping E1.31 frame (last seq=%d, current seq=%d, universe=%d)\n"), e131LastSequenceNumber[previousUniverses], seq, uni); return; } e131LastSequenceNumber[previousUniverses] = seq; @@ -120,12 +117,12 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ // update status info realtimeIP = clientIP; byte wChannel = 0; - uint16_t totalLen = strip.getLengthTotal(); - uint16_t availDMXLen = 0; - uint16_t dataOffset = DMXAddress; + unsigned totalLen = strip.getLengthTotal(); + unsigned availDMXLen = 0; + unsigned dataOffset = DMXAddress; // For legacy DMX start address 0 the available DMX length offset is 0 - const uint16_t dmxLenOffset = (DMXAddress == 0) ? 0 : 1; + const unsigned dmxLenOffset = (DMXAddress == 0) ? 0 : 1; // Check if DMX start address fits in available channels if (dmxChannels >= DMXAddress) { @@ -151,7 +148,8 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; wChannel = (availDMXLen > 3) ? e131_data[dataOffset+3] : 0; - for (uint16_t i = 0; i < totalLen; i++) + if (useMainSegmentOnly) strip.getMainSegment().beginDraw(); + for (unsigned i = 0; i < totalLen; i++) setRealtimePixel(i, e131_data[dataOffset+0], e131_data[dataOffset+1], e131_data[dataOffset+2], wChannel); break; @@ -168,7 +166,8 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ strip.setBrightness(bri, true); } - for (uint16_t i = 0; i < totalLen; i++) + if (useMainSegmentOnly) strip.getMainSegment().beginDraw(); + for (unsigned i = 0; i < totalLen; i++) setRealtimePixel(i, e131_data[dataOffset+1], e131_data[dataOffset+2], e131_data[dataOffset+3], wChannel); break; @@ -177,14 +176,13 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ if (uni != e131Universe || availDMXLen < 2) return; // limit max. selectable preset to 250, even though DMX max. val is 255 - uint8_t dmxValPreset = (e131_data[dataOffset+1] > 250 ? 250 : e131_data[dataOffset+1]); + int dmxValPreset = (e131_data[dataOffset+1] > 250 ? 250 : e131_data[dataOffset+1]); // only apply preset if value changed if (dmxValPreset != 0 && dmxValPreset != currentPreset && // only apply preset if not in playlist, or playlist changed (currentPlaylist < 0 || dmxValPreset != currentPlaylist)) { presetCycCurr = dmxValPreset; - unloadPlaylist(); // applying a preset unloads the playlist applyPreset(dmxValPreset, CALL_MODE_NOTIFICATION); } @@ -205,8 +203,8 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ { if (uni != e131Universe) return; bool isSegmentMode = DMXMode == DMX_MODE_EFFECT_SEGMENT || DMXMode == DMX_MODE_EFFECT_SEGMENT_W; - uint8_t dmxEffectChannels = (DMXMode == DMX_MODE_EFFECT || DMXMode == DMX_MODE_EFFECT_SEGMENT) ? 15 : 18; - for (uint8_t id = 0; id < strip.getSegmentsNum(); id++) { + unsigned dmxEffectChannels = (DMXMode == DMX_MODE_EFFECT || DMXMode == DMX_MODE_EFFECT_SEGMENT) ? 15 : 18; + for (unsigned id = 0; id < strip.getSegmentsNum(); id++) { Segment& seg = strip.getSegment(id); if (isSegmentMode) dataOffset = DMXAddress + id * (dmxEffectChannels + DMXSegmentSpacing); @@ -253,7 +251,7 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ // Set segment opacity or global brightness if (isSegmentMode) { if (e131_data[dataOffset] != seg.opacity) seg.setOpacity(e131_data[dataOffset]); - } else if ( id == strip.getSegmentsNum()-1 ) { + } else if ( id == strip.getSegmentsNum()-1U ) { if (bri != e131_data[dataOffset]) { bri = e131_data[dataOffset]; strip.setBrightness(bri, true); @@ -269,10 +267,10 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ case DMX_MODE_MULTIPLE_RGBW: { bool is4Chan = (DMXMode == DMX_MODE_MULTIPLE_RGBW); - const uint16_t dmxChannelsPerLed = is4Chan ? 4 : 3; - const uint16_t ledsPerUniverse = is4Chan ? MAX_4_CH_LEDS_PER_UNIVERSE : MAX_3_CH_LEDS_PER_UNIVERSE; + const unsigned dmxChannelsPerLed = is4Chan ? 4 : 3; + const unsigned ledsPerUniverse = is4Chan ? MAX_4_CH_LEDS_PER_UNIVERSE : MAX_3_CH_LEDS_PER_UNIVERSE; uint8_t stripBrightness = bri; - uint16_t previousLeds, dmxOffset, ledsTotal; + unsigned previousLeds, dmxOffset, ledsTotal; if (previousUniverses == 0) { if (availDMXLen < 1) return; @@ -288,8 +286,8 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ } else { // All subsequent universes start at the first channel. dmxOffset = (protocol == P_ARTNET) ? 0 : 1; - const uint16_t dimmerOffset = (DMXMode == DMX_MODE_MULTIPLE_DRGB) ? 1 : 0; - uint16_t ledsInFirstUniverse = (((MAX_CHANNELS_PER_UNIVERSE - DMXAddress) + dmxLenOffset) - dimmerOffset) / dmxChannelsPerLed; + const unsigned dimmerOffset = (DMXMode == DMX_MODE_MULTIPLE_DRGB) ? 1 : 0; + unsigned ledsInFirstUniverse = (((MAX_CHANNELS_PER_UNIVERSE - DMXAddress) + dmxLenOffset) - dimmerOffset) / dmxChannelsPerLed; previousLeds = ledsInFirstUniverse + (previousUniverses - 1) * ledsPerUniverse; ledsTotal = previousLeds + (dmxChannels / dmxChannelsPerLed); } @@ -313,13 +311,14 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ } } + if (useMainSegmentOnly) strip.getMainSegment().beginDraw(); if (!is4Chan) { - for (uint16_t i = previousLeds; i < ledsTotal; i++) { + for (unsigned i = previousLeds; i < ledsTotal; i++) { setRealtimePixel(i, e131_data[dmxOffset], e131_data[dmxOffset+1], e131_data[dmxOffset+2], 0); dmxOffset+=3; } } else { - for (uint16_t i = previousLeds; i < ledsTotal; i++) { + for (unsigned i = previousLeds; i < ledsTotal; i++) { setRealtimePixel(i, e131_data[dmxOffset], e131_data[dmxOffset+1], e131_data[dmxOffset+2], e131_data[dmxOffset+3]); dmxOffset+=4; } @@ -339,12 +338,11 @@ void handleArtnetPollReply(IPAddress ipAddress) { ArtPollReply artnetPollReply; prepareArtnetPollReply(&artnetPollReply); - uint16_t startUniverse = e131Universe; - uint16_t endUniverse = e131Universe; + unsigned startUniverse = e131Universe; + unsigned endUniverse = e131Universe; switch (DMXMode) { case DMX_MODE_DISABLED: - return; // nothing to do break; case DMX_MODE_SINGLE_RGB: @@ -361,15 +359,15 @@ void handleArtnetPollReply(IPAddress ipAddress) { case DMX_MODE_MULTIPLE_RGBW: { bool is4Chan = (DMXMode == DMX_MODE_MULTIPLE_RGBW); - const uint16_t dmxChannelsPerLed = is4Chan ? 4 : 3; - const uint16_t dimmerOffset = (DMXMode == DMX_MODE_MULTIPLE_DRGB) ? 1 : 0; - const uint16_t dmxLenOffset = (DMXAddress == 0) ? 0 : 1; // For legacy DMX start address 0 - const uint16_t ledsInFirstUniverse = (((MAX_CHANNELS_PER_UNIVERSE - DMXAddress) + dmxLenOffset) - dimmerOffset) / dmxChannelsPerLed; - const uint16_t totalLen = strip.getLengthTotal(); + const unsigned dmxChannelsPerLed = is4Chan ? 4 : 3; + const unsigned dimmerOffset = (DMXMode == DMX_MODE_MULTIPLE_DRGB) ? 1 : 0; + const unsigned dmxLenOffset = (DMXAddress == 0) ? 0 : 1; // For legacy DMX start address 0 + const unsigned ledsInFirstUniverse = (((MAX_CHANNELS_PER_UNIVERSE - DMXAddress) + dmxLenOffset) - dimmerOffset) / dmxChannelsPerLed; + const unsigned totalLen = strip.getLengthTotal(); if (totalLen > ledsInFirstUniverse) { - const uint16_t ledsPerUniverse = is4Chan ? MAX_4_CH_LEDS_PER_UNIVERSE : MAX_3_CH_LEDS_PER_UNIVERSE; - const uint16_t remainLED = totalLen - ledsInFirstUniverse; + const unsigned ledsPerUniverse = is4Chan ? MAX_4_CH_LEDS_PER_UNIVERSE : MAX_3_CH_LEDS_PER_UNIVERSE; + const unsigned remainLED = totalLen - ledsInFirstUniverse; endUniverse += (remainLED / ledsPerUniverse); @@ -389,9 +387,17 @@ void handleArtnetPollReply(IPAddress ipAddress) { break; } - for (uint16_t i = startUniverse; i <= endUniverse; ++i) { - sendArtnetPollReply(&artnetPollReply, ipAddress, i); + if (DMXMode != DMX_MODE_DISABLED) { + for (unsigned i = startUniverse; i <= endUniverse; ++i) { + sendArtnetPollReply(&artnetPollReply, ipAddress, i); + } } + + #ifdef WLED_ENABLE_DMX + if (e131ProxyUniverse > 0 && (DMXMode == DMX_MODE_DISABLED || (e131ProxyUniverse < startUniverse || e131ProxyUniverse > endUniverse))) { + sendArtnetPollReply(&artnetPollReply, ipAddress, e131ProxyUniverse); + } + #endif } void prepareArtnetPollReply(ArtPollReply *reply) { @@ -408,7 +414,7 @@ void prepareArtnetPollReply(ArtPollReply *reply) { reply->reply_opcode = ARTNET_OPCODE_OPPOLLREPLY; IPAddress localIP = Network.localIP(); - for (uint8_t i = 0; i < 4; i++) { + for (unsigned i = 0; i < 4; i++) { reply->reply_ip[i] = localIP[i]; } @@ -484,7 +490,7 @@ void prepareArtnetPollReply(ArtPollReply *reply) { Network.localMAC(reply->reply_mac); - for (uint8_t i = 0; i < 4; i++) { + for (unsigned i = 0; i < 4; i++) { reply->reply_bind_ip[i] = localIP[i]; } @@ -508,7 +514,7 @@ void prepareArtnetPollReply(ArtPollReply *reply) { // Node does not support fail-over reply->reply_status_3 = 0x00; - for (uint8_t i = 0; i < 21; i++) { + for (unsigned i = 0; i < 21; i++) { reply->reply_filler[i] = 0x00; } } diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 4619e640a..bd45a2f30 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -5,7 +5,6 @@ * All globally accessible functions are declared here */ - //alexa.cpp #ifndef WLED_DISABLE_ALEXA void onAlexaChange(EspalexaDevice* dev); @@ -21,6 +20,7 @@ void doublePressAction(uint8_t b=0); bool isButtonPressed(uint8_t b=0); void handleButton(); void handleIO(); +void IRAM_ATTR touchButtonISR(); //cfg.cpp bool deserializeConfig(JsonObject doc, bool fromFS = false); @@ -66,25 +66,113 @@ typedef struct WiFiConfig { } wifi_config; //colors.cpp +#define ColorFromPalette ColorFromPaletteWLED // override fastled version + +// CRGBW can be used to manipulate 32bit colors faster. However: if it is passed to functions, it adds overhead compared to a uint32_t color +// use with caution and pay attention to flash size. Usually converting a uint32_t to CRGBW to extract r, g, b, w values is slower than using bitshifts +// it can be useful to avoid back and forth conversions between uint32_t and fastled CRGB +struct CRGBW { + union { + uint32_t color32; // Access as a 32-bit value (0xWWRRGGBB) + struct { + uint8_t b; + uint8_t g; + uint8_t r; + uint8_t w; + }; + uint8_t raw[4]; // Access as an array in the order B, G, R, W + }; + + // Default constructor + inline CRGBW() __attribute__((always_inline)) = default; + + // Constructor from a 32-bit color (0xWWRRGGBB) + constexpr CRGBW(uint32_t color) __attribute__((always_inline)) : color32(color) {} + + // Constructor with r, g, b, w values + constexpr CRGBW(uint8_t red, uint8_t green, uint8_t blue, uint8_t white = 0) __attribute__((always_inline)) : b(blue), g(green), r(red), w(white) {} + + // Constructor from CRGB + constexpr CRGBW(CRGB rgb) __attribute__((always_inline)) : b(rgb.b), g(rgb.g), r(rgb.r), w(0) {} + + // Access as an array + inline const uint8_t& operator[] (uint8_t x) const __attribute__((always_inline)) { return raw[x]; } + + // Assignment from 32-bit color + inline CRGBW& operator=(uint32_t color) __attribute__((always_inline)) { color32 = color; return *this; } + + // Assignment from r, g, b, w + inline CRGBW& operator=(const CRGB& rgb) __attribute__((always_inline)) { b = rgb.b; g = rgb.g; r = rgb.r; w = 0; return *this; } + + // Conversion operator to uint32_t + inline operator uint32_t() const __attribute__((always_inline)) { + return color32; + } + /* + // Conversion operator to CRGB + inline operator CRGB() const __attribute__((always_inline)) { + return CRGB(r, g, b); + } + + CRGBW& scale32 (uint8_t scaledown) // 32bit math + { + if (color32 == 0) return *this; // 2 extra instructions, worth it if called a lot on black (which probably is true) adding check if scaledown is zero adds much more overhead as its 8bit + uint32_t scale = scaledown + 1; + uint32_t rb = (((color32 & 0x00FF00FF) * scale) >> 8) & 0x00FF00FF; // scale red and blue + uint32_t wg = (((color32 & 0xFF00FF00) >> 8) * scale) & 0xFF00FF00; // scale white and green + color32 = rb | wg; + return *this; + }*/ + +}; + +struct CHSV32 { // 32bit HSV color with 16bit hue for more accurate conversions + union { + struct { + uint16_t h; // hue + uint8_t s; // saturation + uint8_t v; // value + }; + uint32_t raw; // 32bit access + }; + inline CHSV32() __attribute__((always_inline)) = default; // default constructor + + /// Allow construction from hue, saturation, and value + /// @param ih input hue + /// @param is input saturation + /// @param iv input value + inline CHSV32(uint16_t ih, uint8_t is, uint8_t iv) __attribute__((always_inline)) // constructor from 16bit h, s, v + : h(ih), s(is), v(iv) {} + inline CHSV32(uint8_t ih, uint8_t is, uint8_t iv) __attribute__((always_inline)) // constructor from 8bit h, s, v + : h((uint16_t)ih << 8), s(is), v(iv) {} + inline CHSV32(const CHSV& chsv) __attribute__((always_inline)) // constructor from CHSV + : h((uint16_t)chsv.h << 8), s(chsv.s), v(chsv.v) {} + inline operator CHSV() const { return CHSV((uint8_t)(h >> 8), s, v); } // typecast to CHSV +}; // similar to NeoPixelBus NeoGammaTableMethod but allows dynamic changes (superseded by NPB::NeoGammaDynamicTableMethod) class NeoGammaWLEDMethod { public: - static uint8_t Correct(uint8_t value); // apply Gamma to single channel - static uint32_t Correct32(uint32_t color); // apply Gamma to RGBW32 color (WLED specific, not used by NPB) - static void calcGammaTable(float gamma); // re-calculates & fills gamma table + [[gnu::hot]] static uint8_t Correct(uint8_t value); // apply Gamma to single channel + [[gnu::hot]] static uint32_t Correct32(uint32_t color); // apply Gamma to RGBW32 color (WLED specific, not used by NPB) + static void calcGammaTable(float gamma); // re-calculates & fills gamma table static inline uint8_t rawGamma8(uint8_t val) { return gammaT[val]; } // get value from Gamma table (WLED specific, not used by NPB) private: static uint8_t gammaT[]; }; #define gamma32(c) NeoGammaWLEDMethod::Correct32(c) #define gamma8(c) NeoGammaWLEDMethod::rawGamma8(c) -uint32_t color_blend(uint32_t,uint32_t,uint16_t,bool b16=false); -uint32_t color_add(uint32_t,uint32_t, bool fast=false); -uint32_t color_fade(uint32_t c1, uint8_t amount, bool video=false); +[[gnu::hot]] uint32_t color_blend(uint32_t c1, uint32_t c2 , uint8_t blend); +inline uint32_t color_blend16(uint32_t c1, uint32_t c2, uint16_t b) { return color_blend(c1, c2, b >> 8); }; +[[gnu::hot]] uint32_t color_add(uint32_t, uint32_t, bool preserveCR = false); +[[gnu::hot]] uint32_t color_fade(uint32_t c1, uint8_t amount, bool video=false); +[[gnu::hot]] uint32_t ColorFromPaletteWLED(const CRGBPalette16 &pal, unsigned index, uint8_t brightness = (uint8_t)255U, TBlendType blendType = LINEARBLEND); CRGBPalette16 generateHarmonicRandomPalette(CRGBPalette16 &basepalette); -CRGBPalette16 generateRandomPalette(void); +CRGBPalette16 generateRandomPalette(); inline uint32_t colorFromRgbw(byte* rgbw) { return uint32_t((byte(rgbw[3]) << 24) | (byte(rgbw[0]) << 16) | (byte(rgbw[1]) << 8) | (byte(rgbw[2]))); } -void colorHStoRGB(uint16_t hue, byte sat, byte* rgb); //hue, sat to rgb +void hsv2rgb(const CHSV32& hsv, uint32_t& rgb); +void colorHStoRGB(uint16_t hue, byte sat, byte* rgb); +void rgb2hsv(const uint32_t rgb, CHSV32& hsv); +inline CHSV rgb2hsv(const CRGB c) { CHSV32 hsv; rgb2hsv((uint32_t((byte(c.r) << 16) | (byte(c.g) << 8) | (byte(c.b)))), hsv); return CHSV(hsv); } // CRGB to hsv void colorKtoRGB(uint16_t kelvin, byte* rgb); void colorCTtoRGB(uint16_t mired, byte* rgb); //white spectrum to rgb void colorXYtoRGB(float x, float y, byte* rgb); // only defined if huesync disabled TODO @@ -156,26 +244,15 @@ void handleImprovWifiScan(); void sendImprovIPRPCResult(ImprovRPCType type); //ir.cpp -void applyRepeatActions(); -byte relativeChange(byte property, int8_t amount, byte lowerBoundary = 0, byte higherBoundary = 0xFF); -void decodeIR(uint32_t code); -void decodeIR24(uint32_t code); -void decodeIR24OLD(uint32_t code); -void decodeIR24CT(uint32_t code); -void decodeIR40(uint32_t code); -void decodeIR44(uint32_t code); -void decodeIR21(uint32_t code); -void decodeIR6(uint32_t code); -void decodeIR9(uint32_t code); -void decodeIRJson(uint32_t code); - void initIR(); +void deInitIR(); void handleIR(); //json.cpp #include "ESPAsyncWebServer.h" #include "src/dependencies/json/ArduinoJson-v6.h" #include "src/dependencies/json/AsyncJson-v6.h" +#include "FX.h" bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0); bool deserializeState(JsonObject root, byte callMode = CALL_MODE_DIRECT_CHANGE, byte presetId = 0); @@ -193,7 +270,6 @@ bool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient = 0); void setValuesFromSegment(uint8_t s); void setValuesFromMainSeg(); void setValuesFromFirstSelectedSeg(); -void resetTimebase(); void toggleOnOff(); void applyBri(); void applyFinalBri(); @@ -246,6 +322,7 @@ const char *getPresetsFileName(bool persistent = true); void initPresetsFile(); void handlePresets(); bool applyPreset(byte index, byte callMode = CALL_MODE_DIRECT_CHANGE); +bool applyPresetFromPlaylist(byte index); void applyPresetWithFallback(uint8_t presetID, uint8_t callMode, uint8_t effectID = 0, uint8_t paletteID = 0); inline bool applyTemporaryPreset() {return applyPreset(255);}; void savePreset(byte index, const char* pname = nullptr, JsonObject saveobj = JsonObject()); @@ -325,7 +402,7 @@ class Usermod { virtual bool handleButton(uint8_t b) { return false; } // button overrides are possible here virtual bool getUMData(um_data_t **data) { if (data) *data = nullptr; return false; }; // usermod data exchange [see examples for audio effects] virtual void connected() {} // called when WiFi is (re)connected - virtual void appendConfigData() {} // helper function called from usermod settings page to add metadata for entry fields + virtual void appendConfigData(Print& settingsScript); // helper function called from usermod settings page to add metadata for entry fields virtual void addToJsonState(JsonObject& obj) {} // add JSON objects for WLED state virtual void addToJsonInfo(JsonObject& obj) {} // add JSON objects for UI Info page virtual void readFromJsonState(JsonObject& obj) {} // process JSON messages received from web server @@ -333,36 +410,53 @@ class Usermod { virtual bool readFromConfig(JsonObject& obj) { return true; } // Note as of 2021-06 readFromConfig() now needs to return a bool, see usermod_v2_example.h virtual void onMqttConnect(bool sessionPresent) {} // fired when MQTT connection is established (so usermod can subscribe) virtual bool onMqttMessage(char* topic, char* payload) { return false; } // fired upon MQTT message received (wled topic) + virtual bool onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len) { return false; } // fired upon ESP-NOW message received virtual void onUpdateBegin(bool) {} // fired prior to and after unsuccessful firmware update virtual void onStateChange(uint8_t mode) {} // fired upon WLED state change virtual uint16_t getId() {return USERMOD_ID_UNSPECIFIED;} + + // API shims + private: + static Print* oappend_shim; + // old form of appendConfigData; called by default appendConfigData(Print&) with oappend_shim set up + // private so it is not accidentally invoked except via Usermod::appendConfigData(Print&) + virtual void appendConfigData() {} + protected: + // Shim for oappend(), which used to exist in utils.cpp + template static inline void oappend(const T& t) { oappend_shim->print(t); }; +#ifdef ESP8266 + // Handle print(PSTR()) without crashing by detecting PROGMEM strings + static void oappend(const char* c) { if ((intptr_t) c >= 0x40000000) oappend_shim->print(FPSTR(c)); else oappend_shim->print(c); }; +#endif }; -class UsermodManager { - private: - Usermod* ums[WLED_MAX_USERMODS]; - byte numMods = 0; +namespace UsermodManager { + extern byte numMods; - public: - void loop(); - void handleOverlayDraw(); - bool handleButton(uint8_t b); - bool getUMData(um_data_t **um_data, uint8_t mod_id = USERMOD_ID_RESERVED); // USERMOD_ID_RESERVED will poll all usermods - void setup(); - void connected(); - void appendConfigData(); - void addToJsonState(JsonObject& obj); - void addToJsonInfo(JsonObject& obj); - void readFromJsonState(JsonObject& obj); - void addToConfig(JsonObject& obj); - bool readFromConfig(JsonObject& obj); - void onMqttConnect(bool sessionPresent); - bool onMqttMessage(char* topic, char* payload); - void onUpdateBegin(bool); - void onStateChange(uint8_t); - bool add(Usermod* um); - Usermod* lookup(uint16_t mod_id); - byte getModCount() {return numMods;}; + void loop(); + void handleOverlayDraw(); + bool handleButton(uint8_t b); + bool getUMData(um_data_t **um_data, uint8_t mod_id = USERMOD_ID_RESERVED); // USERMOD_ID_RESERVED will poll all usermods + void setup(); + void connected(); + void appendConfigData(Print&); + void addToJsonState(JsonObject& obj); + void addToJsonInfo(JsonObject& obj); + void readFromJsonState(JsonObject& obj); + void addToConfig(JsonObject& obj); + bool readFromConfig(JsonObject& obj); +#ifndef WLED_DISABLE_MQTT + void onMqttConnect(bool sessionPresent); + bool onMqttMessage(char* topic, char* payload); +#endif +#ifndef WLED_DISABLE_ESPNOW + bool onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len); +#endif + void onUpdateBegin(bool); + void onStateChange(uint8_t); + bool add(Usermod* um); + Usermod* lookup(uint16_t mod_id); + inline byte getModCount() {return numMods;}; }; //usermods_list.cpp @@ -374,15 +468,22 @@ void userConnected(); void userLoop(); //util.cpp +#ifdef ESP8266 +#define HW_RND_REGISTER RANDOM_REG32 +#else // ESP32 family +#include "soc/wdev_reg.h" +#define HW_RND_REGISTER REG_READ(WDEV_RND_REG) +#endif int getNumVal(const String* req, uint16_t pos); void parseNumber(const char* str, byte* val, byte minv=0, byte maxv=255); -bool getVal(JsonVariant elem, byte* val, byte minv=0, byte maxv=255); +bool getVal(JsonVariant elem, byte* val, byte minv=0, byte maxv=255); // getVal supports inc/decrementing and random ("X~Y(r|~[w][-][Z])" form) bool getBoolVal(JsonVariant elem, bool dflt); bool updateVal(const char* req, const char* key, byte* val, byte minv=0, byte maxv=255); -bool oappend(const char* txt); // append new c string to temp buffer efficiently -bool oappendi(int i); // append new number to temp buffer efficiently -void sappend(char stype, const char* key, int val); -void sappends(char stype, const char* key, char* val); +size_t printSetFormCheckbox(Print& settingsScript, const char* key, int val); +size_t printSetFormValue(Print& settingsScript, const char* key, int val); +size_t printSetFormValue(Print& settingsScript, const char* key, const char* val); +size_t printSetFormIndex(Print& settingsScript, const char* key, int index); +size_t printSetClassElementHTML(Print& settingsScript, const char* key, const int index, const char* val); void prepareHostname(char* hostname); bool isAsterisksOnly(const char* str, byte maxLen); bool requestJSONBufferLock(uint8_t module=255); @@ -392,9 +493,30 @@ uint8_t extractModeSlider(uint8_t mode, uint8_t slider, char *dest, uint8_t maxL int16_t extractModeDefaults(uint8_t mode, const char *segVar); void checkSettingsPIN(const char *pin); uint16_t crc16(const unsigned char* data_p, size_t length); +uint16_t beatsin88_t(accum88 beats_per_minute_88, uint16_t lowest = 0, uint16_t highest = 65535, uint32_t timebase = 0, uint16_t phase_offset = 0); +uint16_t beatsin16_t(accum88 beats_per_minute, uint16_t lowest = 0, uint16_t highest = 65535, uint32_t timebase = 0, uint16_t phase_offset = 0); +uint8_t beatsin8_t(accum88 beats_per_minute, uint8_t lowest = 0, uint8_t highest = 255, uint32_t timebase = 0, uint8_t phase_offset = 0); um_data_t* simulateSound(uint8_t simulationId); void enumerateLedmaps(); uint8_t get_random_wheel_index(uint8_t pos); +float mapf(float x, float in_min, float in_max, float out_min, float out_max); + +// fast (true) random numbers using hardware RNG, all functions return values in the range lowerlimit to upperlimit-1 +// note: for true random numbers with high entropy, do not call faster than every 200ns (5MHz) +// tests show it is still highly random reading it quickly in a loop (better than fastled PRNG) +// for 8bit and 16bit random functions: no limit check is done for best speed +// 32bit inputs are used for speed and code size, limits don't work if inverted or out of range +// inlining does save code size except for random(a,b) and 32bit random with limits +#define random hw_random // replace arduino random() +inline uint32_t hw_random() { return HW_RND_REGISTER; }; +uint32_t hw_random(uint32_t upperlimit); // not inlined for code size +int32_t hw_random(int32_t lowerlimit, int32_t upperlimit); +inline uint16_t hw_random16() { return HW_RND_REGISTER; }; +inline uint16_t hw_random16(uint32_t upperlimit) { return (hw_random16() * upperlimit) >> 16; }; // input range 0-65535 (uint16_t) +inline int16_t hw_random16(int32_t lowerlimit, int32_t upperlimit) { int32_t range = upperlimit - lowerlimit; return lowerlimit + hw_random16(range); }; // signed limits, use int16_t ranges +inline uint8_t hw_random8() { return HW_RND_REGISTER; }; +inline uint8_t hw_random8(uint32_t upperlimit) { return (hw_random8() * upperlimit) >> 8; }; // input range 0-255 +inline uint8_t hw_random8(uint32_t lowerlimit, uint32_t upperlimit) { uint32_t range = upperlimit - lowerlimit; return lowerlimit + hw_random8(range); }; // input range 0-255 // RAII guard class for the JSON Buffer lock // Modeled after std::lock_guard @@ -421,33 +543,42 @@ void clearEEPROM(); #endif //wled_math.cpp -#ifndef WLED_USE_REAL_MATH - template T atan_t(T x); - float cos_t(float phi); - float sin_t(float x); - float tan_t(float x); - float acos_t(float x); - float asin_t(float x); - float floor_t(float x); - float fmod_t(float num, float denom); -#else - #include - #define sin_t sin - #define cos_t cos - #define tan_t tan - #define asin_t asin - #define acos_t acos - #define atan_t atan - #define fmod_t fmod - #define floor_t floor -#endif +//float cos_t(float phi); // use float math +//float sin_t(float phi); +//float tan_t(float x); +int16_t sin16_t(uint16_t theta); +int16_t cos16_t(uint16_t theta); +uint8_t sin8_t(uint8_t theta); +uint8_t cos8_t(uint8_t theta); +float sin_approx(float theta); // uses integer math (converted to float), accuracy +/-0.0015 (compared to sinf()) +float cos_approx(float theta); +float tan_approx(float x); +float atan2_t(float y, float x); +float acos_t(float x); +float asin_t(float x); +template T atan_t(T x); +float floor_t(float x); +float fmod_t(float num, float denom); +#define sin_t sin_approx +#define cos_t cos_approx +#define tan_t tan_approx +/* +#include // standard math functions. use a lot of flash +#define sin_t sinf +#define cos_t cosf +#define tan_t tanf +#define asin_t asinf +#define acos_t acosf +#define atan_t atanf +#define fmod_t fmodf +#define floor_t floorf +*/ //wled_serial.cpp void handleSerial(); void updateBaudRate(uint32_t rate); //wled_server.cpp -String getFileContentType(String &filename); void createEditHandler(bool enable); void initServer(); void serveMessage(AsyncWebServerRequest* request, uint16_t code, const String& headl, const String& subl="", byte optionT=255); @@ -461,7 +592,7 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp void sendDataWs(AsyncWebSocketClient * client = nullptr); //xml.cpp -void XML_response(AsyncWebServerRequest *request, char* dest = nullptr); -void getSettingsJS(byte subPage, char* dest); +void XML_response(Print& dest); +void getSettingsJS(byte subPage, Print& dest); #endif diff --git a/wled00/file.cpp b/wled00/file.cpp index 37f794424..bc3467202 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -138,7 +138,7 @@ static bool bufferedFindObjectEnd() { if (!f || !f.size()) return false; - uint16_t objDepth = 0; //num of '{' minus num of '}'. return once 0 + unsigned objDepth = 0; //num of '{' minus num of '}'. return once 0 //size_t start = f.position(); byte buf[FS_BUFSIZE]; @@ -375,21 +375,21 @@ void updateFSInfo() { #endif } -#if defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) + +#ifdef ARDUINO_ARCH_ESP32 // caching presets in PSRAM may prevent occasional flashes seen when HomeAssitant polls WLED // original idea by @akaricchi (https://github.com/Akaricchi) -// returns a pointer to the PSRAM buffer updates size parameter +// returns a pointer to the PSRAM buffer, updates size parameter static const uint8_t *getPresetCache(size_t &size) { - static unsigned long presetsCachedTime; - static uint8_t *presetsCached; - static size_t presetsCachedSize; + static unsigned long presetsCachedTime = 0; + static uint8_t *presetsCached = nullptr; + static size_t presetsCachedSize = 0; + static byte presetsCachedValidate = 0; - if (!psramFound()) { - size = 0; - return nullptr; - } + //if (presetsModifiedTime != presetsCachedTime) DEBUG_PRINTLN(F("getPresetCache(): presetsModifiedTime changed.")); + //if (presetsCachedValidate != cacheInvalidate) DEBUG_PRINTLN(F("getPresetCache(): cacheInvalidate changed.")); - if (presetsModifiedTime != presetsCachedTime) { + if ((presetsModifiedTime != presetsCachedTime) || (presetsCachedValidate != cacheInvalidate)) { if (presetsCached) { free(presetsCached); presetsCached = nullptr; @@ -400,6 +400,7 @@ static const uint8_t *getPresetCache(size_t &size) { File file = WLED_FS.open(FPSTR(getPresetsFileName()), "r"); if (file) { presetsCachedTime = presetsModifiedTime; + presetsCachedValidate = cacheInvalidate; presetsCachedSize = 0; presetsCached = (uint8_t*)ps_malloc(file.size() + 1); if (presetsCached) { @@ -420,26 +421,19 @@ bool handleFileRead(AsyncWebServerRequest* request, String path){ DEBUG_PRINT(F("WS FileRead: ")); DEBUG_PRINTLN(path); if(path.endsWith("/")) path += "index.htm"; if(path.indexOf(F("sec")) > -1) return false; - String contentType = getFileContentType(path); - if(request->hasArg(F("download"))) contentType = F("application/octet-stream"); - /*String pathWithGz = path + ".gz"; - if(WLED_FS.exists(pathWithGz)){ - request->send(WLED_FS, pathWithGz, contentType); - return true; - }*/ - #if defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) - if (path.endsWith(FPSTR(getPresetsFileName()))) { + #ifdef ARDUINO_ARCH_ESP32 + if (psramSafe && psramFound() && path.endsWith(FPSTR(getPresetsFileName()))) { size_t psize; const uint8_t *presets = getPresetCache(psize); if (presets) { - AsyncWebServerResponse *response = request->beginResponse_P(200, contentType, presets, psize); + AsyncWebServerResponse *response = request->beginResponse_P(200, FPSTR(CONTENT_TYPE_JSON), presets, psize); request->send(response); return true; } } #endif - if(WLED_FS.exists(path)) { - request->send(WLED_FS, path, contentType); + if(WLED_FS.exists(path) || WLED_FS.exists(path + ".gz")) { + request->send(request->beginResponse(WLED_FS, path, {}, request->hasArg(F("download")), {})); return true; } return false; diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp deleted file mode 100644 index 9ad4662e6..000000000 --- a/wled00/image_loader.cpp +++ /dev/null @@ -1,142 +0,0 @@ -#ifndef WLED_DISABLE_GIF - -#include "GifDecoder.h" -#include "wled.h" - -/* - * Functions to render images from filesystem to segments, used by the "Image" effect - */ - -File file; -char lastFilename[34] = "/"; -GifDecoder<320,320,12,true> decoder; -bool gifDecodeFailed = false; -unsigned long lastFrameDisplayTime = 0, currentFrameDelay = 0; - -bool fileSeekCallback(unsigned long position) { - return file.seek(position); -} - -unsigned long filePositionCallback(void) { - return file.position(); -} - -int fileReadCallback(void) { - return file.read(); -} - -int fileReadBlockCallback(void * buffer, int numberOfBytes) { - return file.read((uint8_t*)buffer, numberOfBytes); -} - -int fileSizeCallback(void) { - return file.size(); -} - -bool openGif(const char *filename) { - file = WLED_FS.open(filename, "r"); - - if (!file) return false; - return true; -} - -Segment* activeSeg; -uint16_t gifWidth, gifHeight; - -void screenClearCallback(void) { - activeSeg->fill(0); -} - -void updateScreenCallback(void) {} - -void drawPixelCallback(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { - // simple nearest-neighbor scaling - int16_t outY = y * activeSeg->height() / gifHeight; - int16_t outX = x * activeSeg->width() / gifWidth; - // set multiple pixels if upscaling - for (int16_t i = 0; i < (activeSeg->width()+(gifWidth-1)) / gifWidth; i++) { - for (int16_t j = 0; j < (activeSeg->height()+(gifHeight-1)) / gifHeight; j++) { - activeSeg->setPixelColorXY(outX + i, outY + j, gamma8(red), gamma8(green), gamma8(blue)); - } - } -} - -#define IMAGE_ERROR_NONE 0 -#define IMAGE_ERROR_NO_NAME 1 -#define IMAGE_ERROR_SEG_LIMIT 2 -#define IMAGE_ERROR_UNSUPPORTED_FORMAT 3 -#define IMAGE_ERROR_FILE_MISSING 4 -#define IMAGE_ERROR_DECODER_ALLOC 5 -#define IMAGE_ERROR_GIF_DECODE 6 -#define IMAGE_ERROR_FRAME_DECODE 7 -#define IMAGE_ERROR_WAITING 254 -#define IMAGE_ERROR_PREV 255 - -// renders an image (.gif only; .bmp and .fseq to be added soon) from FS to a segment -byte renderImageToSegment(Segment &seg) { - if (!seg.name) return IMAGE_ERROR_NO_NAME; - // disable during effect transition, causes flickering, multiple allocations and depending on image, part of old FX remaining - if (seg.mode != seg.currentMode()) return IMAGE_ERROR_WAITING; - if (activeSeg && activeSeg != &seg) return IMAGE_ERROR_SEG_LIMIT; // only one segment at a time - activeSeg = &seg; - - if (strncmp(lastFilename +1, seg.name, 32) != 0) { // segment name changed, load new image - strncpy(lastFilename +1, seg.name, 32); - gifDecodeFailed = false; - if (strcmp(lastFilename + strlen(lastFilename) - 4, ".gif") != 0) { - gifDecodeFailed = true; - return IMAGE_ERROR_UNSUPPORTED_FORMAT; - } - if (file) file.close(); - openGif(lastFilename); - if (!file) { gifDecodeFailed = true; return IMAGE_ERROR_FILE_MISSING; } - decoder.setScreenClearCallback(screenClearCallback); - decoder.setUpdateScreenCallback(updateScreenCallback); - decoder.setDrawPixelCallback(drawPixelCallback); - decoder.setFileSeekCallback(fileSeekCallback); - decoder.setFilePositionCallback(filePositionCallback); - decoder.setFileReadCallback(fileReadCallback); - decoder.setFileReadBlockCallback(fileReadBlockCallback); - decoder.setFileSizeCallback(fileSizeCallback); - decoder.alloc(); - Serial.println("Starting decoding"); - if(decoder.startDecoding() < 0) { gifDecodeFailed = true; return IMAGE_ERROR_GIF_DECODE; } - Serial.println("Decoding started"); - } - - if (gifDecodeFailed) return IMAGE_ERROR_PREV; - if (!file) { gifDecodeFailed = true; return IMAGE_ERROR_FILE_MISSING; } - //if (!decoder) { gifDecodeFailed = true; return IMAGE_ERROR_DECODER_ALLOC; } - - // speed 0 = half speed, 128 = normal, 255 = full FX FPS - // TODO: 0 = 4x slow, 64 = 2x slow, 128 = normal, 192 = 2x fast, 255 = 4x fast - uint32_t wait = currentFrameDelay * 2 - seg.speed * currentFrameDelay / 128; - - // TODO consider handling this on FX level with a different frametime, but that would cause slow gifs to speed up during transitions - if (millis() - lastFrameDisplayTime < wait) return IMAGE_ERROR_WAITING; - - decoder.getSize(&gifWidth, &gifHeight); - - int result = decoder.decodeFrame(false); - if (result < 0) { gifDecodeFailed = true; return IMAGE_ERROR_FRAME_DECODE; } - - currentFrameDelay = decoder.getFrameDelay_ms(); - unsigned long tooSlowBy = (millis() - lastFrameDisplayTime) - wait; // if last frame was longer than intended, compensate - currentFrameDelay = tooSlowBy > currentFrameDelay ? 0 : currentFrameDelay - tooSlowBy; - lastFrameDisplayTime = millis(); - - return IMAGE_ERROR_NONE; -} - -void endImagePlayback(Segment *seg) { - Serial.println("Image playback end called"); - if (!activeSeg || activeSeg != seg) return; - if (file) file.close(); - decoder.dealloc(); - gifDecodeFailed = false; - activeSeg = nullptr; - lastFilename[1] = '\0'; - Serial.println("Image playback ended"); -} - -#endif \ No newline at end of file diff --git a/wled00/improv.cpp b/wled00/improv.cpp index 9c2fb20e7..197148b2b 100644 --- a/wled00/improv.cpp +++ b/wled00/improv.cpp @@ -42,12 +42,12 @@ void handleImprovPacket() { uint8_t header[6] = {'I','M','P','R','O','V'}; bool timeout = false; - uint8_t waitTime = 25; - uint16_t packetByte = 0; - uint8_t packetLen = 9; - uint8_t checksum = 0; + unsigned waitTime = 25; + unsigned packetByte = 0; + unsigned packetLen = 9; + unsigned checksum = 0; - uint8_t rpcCommandType = 0; + unsigned rpcCommandType = 0; char rpcData[128]; rpcData[0] = 0; @@ -92,7 +92,7 @@ void handleImprovPacket() { switch (rpcCommandType) { case ImprovRPCType::Command_Wifi: parseWiFiCommand(rpcData); break; case ImprovRPCType::Request_State: { - uint8_t improvState = 0x02; //authorized + unsigned improvState = 0x02; //authorized if (WLED_WIFI_CONFIGURED) improvState = 0x03; //provisioning if (Network.isConnected()) improvState = 0x04; //provisioned sendImprovStateResponse(improvState, false); @@ -123,6 +123,7 @@ void handleImprovPacket() { } checksum += next; + checksum &= 0xFF; packetByte++; } } @@ -136,8 +137,8 @@ void sendImprovStateResponse(uint8_t state, bool error) { out[8] = 1; out[9] = state; - uint8_t checksum = 0; - for (uint8_t i = 0; i < 10; i++) checksum += out[i]; + unsigned checksum = 0; + for (unsigned i = 0; i < 10; i++) checksum += out[i]; out[10] = checksum; Serial.write((uint8_t*)out, 11); Serial.write('\n'); @@ -146,16 +147,16 @@ void sendImprovStateResponse(uint8_t state, bool error) { // used by sendImprovIPRPCResult(), sendImprovInfoResponse(), and handleImprovWifiScan() void sendImprovRPCResult(ImprovRPCType type, uint8_t n_strings, const char **strings) { if (improvError > 0 && improvError < 3) sendImprovStateResponse(0x00, true); - uint8_t packetLen = 12; + unsigned packetLen = 12; char out[256] = {'I','M','P','R','O','V'}; out[6] = IMPROV_VERSION; out[7] = ImprovPacketType::RPC_Response; //out[8] = 2; //Length (set below) out[9] = type; //out[10] = 0; //Data len (set below) - uint16_t pos = 11; + unsigned pos = 11; - for (uint8_t s = 0; s < n_strings; s++) { + for (unsigned s = 0; s < n_strings; s++) { size_t len = strlen(strings[s]); if (pos + len > 254) continue; // simple buffer overflow guard out[pos++] = len; @@ -167,8 +168,8 @@ void sendImprovRPCResult(ImprovRPCType type, uint8_t n_strings, const char **str out[8] = pos -9; // Length of packet (excluding first 9 header bytes and final checksum byte) out[10] = pos -11; // Data len - uint8_t checksum = 0; - for (uint8_t i = 0; i < packetLen -1; i++) checksum += out[i]; + unsigned checksum = 0; + for (unsigned i = 0; i < packetLen -1; i++) checksum += out[i]; out[packetLen -1] = checksum; Serial.write((uint8_t*)out, packetLen); Serial.write('\n'); @@ -181,7 +182,7 @@ void sendImprovIPRPCResult(ImprovRPCType type) { { char urlStr[64]; IPAddress localIP = Network.localIP(); - uint8_t len = sprintf(urlStr, "http://%d.%d.%d.%d", localIP[0], localIP[1], localIP[2], localIP[3]); + unsigned len = sprintf(urlStr, "http://%d.%d.%d.%d", localIP[0], localIP[1], localIP[2], localIP[3]); if (len > 24) return; //sprintf fail? const char *str[1] = {urlStr}; sendImprovRPCResult(type, 1, str); @@ -193,24 +194,22 @@ void sendImprovIPRPCResult(ImprovRPCType type) { } void sendImprovInfoResponse() { - const char* bString = - #ifdef ESP8266 - "esp8266" - #elif CONFIG_IDF_TARGET_ESP32C3 - "esp32-c3" - #elif CONFIG_IDF_TARGET_ESP32S2 - "esp32-s2" - #elif CONFIG_IDF_TARGET_ESP32S3 - "esp32-s3"; - #else // ESP32 - "esp32"; - #endif - ; - + char bString[32]; + #ifdef ESP8266 + strcpy(bString, "esp8266"); + #else // ESP32 + strncpy(bString, ESP.getChipModel(), 31); + #if CONFIG_IDF_TARGET_ESP32 + bString[5] = '\0'; // disregard chip revision for classic ESP32 + #else + bString[31] = '\0'; // just in case + #endif + strlwr(bString); + #endif //Use serverDescription if it has been changed from the default "WLED", else mDNS name bool useMdnsName = (strcmp(serverDescription, "WLED") == 0 && strlen(cmDNS) > 0); - char vString[20]; - sprintf_P(vString, PSTR("0.15.0-b1/%i"), VERSION); + char vString[32]; + sprintf_P(vString, PSTR("%s/%i"), versionString, VERSION); const char *str[4] = {"WLED", vString, bString, useMdnsName ? cmDNS : serverDescription}; sendImprovRPCResult(ImprovRPCType::Request_Info, 4, str); @@ -254,17 +253,17 @@ void handleImprovWifiScan() {} #endif void parseWiFiCommand(char* rpcData) { - uint8_t len = rpcData[0]; + unsigned len = rpcData[0]; if (!len || len > 126) return; - uint8_t ssidLen = rpcData[1]; + unsigned ssidLen = rpcData[1]; if (ssidLen > len -1 || ssidLen > 32) return; memset(multiWiFi[0].clientSSID, 0, 32); memcpy(multiWiFi[0].clientSSID, rpcData+2, ssidLen); memset(multiWiFi[0].clientPass, 0, 64); if (len > ssidLen +1) { - uint8_t passLen = rpcData[2+ssidLen]; + unsigned passLen = rpcData[2+ssidLen]; memset(multiWiFi[0].clientPass, 0, 64); memcpy(multiWiFi[0].clientPass, rpcData+3+ssidLen, passLen); } diff --git a/wled00/ir.cpp b/wled00/ir.cpp index ba34aa526..f01e2c320 100644 --- a/wled00/ir.cpp +++ b/wled00/ir.cpp @@ -1,20 +1,14 @@ #include "wled.h" +#ifndef WLED_DISABLE_INFRARED #include "ir_codes.h" /* - * Infrared sensor support for generic 24/40/44 key RGB remotes + * Infrared sensor support for several generic RGB remotes and custom JSON remote */ -#if defined(WLED_DISABLE_INFRARED) -void handleIR(){} -#else - IRrecv* irrecv; -//change pin in NpbWrapper.h - decode_results results; - unsigned long irCheckedTime = 0; uint32_t lastValidCode = 0; byte lastRepeatableAction = ACTION_NONE; @@ -35,16 +29,16 @@ uint8_t lastIR6ColourIdx = 0; // print("%d values: %s" % (len(result), result)) // // It would be hard to maintain repeatable steps if calculating this on the fly. -const byte brightnessSteps[] = { +const uint8_t brightnessSteps[] = { 5, 7, 9, 12, 16, 20, 26, 34, 43, 56, 72, 93, 119, 154, 198, 255 }; const size_t numBrightnessSteps = sizeof(brightnessSteps) / sizeof(uint8_t); // increment `bri` to the next `brightnessSteps` value -void incBrightness() +static void incBrightness() { // dumb incremental search is efficient enough for so few items - for (uint8_t index = 0; index < numBrightnessSteps; ++index) + for (unsigned index = 0; index < numBrightnessSteps; ++index) { if (brightnessSteps[index] > bri) { @@ -56,7 +50,7 @@ void incBrightness() } // decrement `bri` to the next `brightnessSteps` value -void decBrightness() +static void decBrightness() { // dumb incremental search is efficient enough for so few items for (int index = numBrightnessSteps - 1; index >= 0; --index) @@ -70,12 +64,12 @@ void decBrightness() } } -void presetFallback(uint8_t presetID, uint8_t effectID, uint8_t paletteID) +static void presetFallback(uint8_t presetID, uint8_t effectID, uint8_t paletteID) { applyPresetWithFallback(presetID, CALL_MODE_BUTTON_PRESET, effectID, paletteID); } -byte relativeChange(byte property, int8_t amount, byte lowerBoundary, byte higherBoundary) +static byte relativeChange(byte property, int8_t amount, byte lowerBoundary = 0, byte higherBoundary = 0xFF) { int16_t new_val = (int16_t) property + amount; if (lowerBoundary >= higherBoundary) return property; @@ -84,26 +78,26 @@ byte relativeChange(byte property, int8_t amount, byte lowerBoundary, byte highe return (byte)constrain(new_val, 0, 255); } -void changeEffect(uint8_t fx) +static void changeEffect(uint8_t fx) { if (irApplyToAllSelected) { - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; - strip.setMode(i, fx); + seg.setMode(fx); } setValuesFromFirstSelectedSeg(); } else { - strip.setMode(strip.getMainSegmentId(), fx); + strip.getSegment(strip.getMainSegmentId()).setMode(fx); setValuesFromMainSeg(); } stateChanged = true; } -void changePalette(uint8_t pal) +static void changePalette(uint8_t pal) { if (irApplyToAllSelected) { - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; seg.setPalette(pal); @@ -116,13 +110,13 @@ void changePalette(uint8_t pal) stateChanged = true; } -void changeEffectSpeed(int8_t amount) +static void changeEffectSpeed(int8_t amount) { if (effectCurrent != 0) { int16_t new_val = (int16_t) effectSpeed + amount; effectSpeed = (byte)constrain(new_val,0,255); if (irApplyToAllSelected) { - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; seg.speed = effectSpeed; @@ -134,18 +128,15 @@ void changeEffectSpeed(int8_t amount) } } else { // if Effect == "solid Color", change the hue of the primary color Segment& sseg = irApplyToAllSelected ? strip.getFirstSelectedSeg() : strip.getMainSegment(); - CRGB fastled_col; - fastled_col.red = R(sseg.colors[0]); - fastled_col.green = G(sseg.colors[0]); - fastled_col.blue = B(sseg.colors[0]); - CHSV prim_hsv = rgb2hsv_approximate(fastled_col); + CRGB fastled_col = CRGB(sseg.colors[0]); + CHSV prim_hsv = rgb2hsv(fastled_col); int16_t new_val = (int16_t)prim_hsv.h + amount; if (new_val > 255) new_val -= 255; // roll-over if bigger than 255 if (new_val < 0) new_val += 255; // roll-over if smaller than 0 prim_hsv.h = (byte)new_val; hsv2rgb_rainbow(prim_hsv, fastled_col); if (irApplyToAllSelected) { - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; seg.colors[0] = RGBW32(fastled_col.red, fastled_col.green, fastled_col.blue, W(sseg.colors[0])); @@ -163,13 +154,13 @@ void changeEffectSpeed(int8_t amount) lastRepeatableValue = amount; } -void changeEffectIntensity(int8_t amount) +static void changeEffectIntensity(int8_t amount) { if (effectCurrent != 0) { int16_t new_val = (int16_t) effectIntensity + amount; effectIntensity = (byte)constrain(new_val,0,255); if (irApplyToAllSelected) { - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; seg.intensity = effectIntensity; @@ -181,16 +172,13 @@ void changeEffectIntensity(int8_t amount) } } else { // if Effect == "solid Color", change the saturation of the primary color Segment& sseg = irApplyToAllSelected ? strip.getFirstSelectedSeg() : strip.getMainSegment(); - CRGB fastled_col; - fastled_col.red = R(sseg.colors[0]); - fastled_col.green = G(sseg.colors[0]); - fastled_col.blue = B(sseg.colors[0]); - CHSV prim_hsv = rgb2hsv_approximate(fastled_col); + CRGB fastled_col = CRGB(sseg.colors[0]); + CHSV prim_hsv = rgb2hsv(fastled_col); int16_t new_val = (int16_t) prim_hsv.s + amount; prim_hsv.s = (byte)constrain(new_val,0,255); // constrain to 0-255 hsv2rgb_rainbow(prim_hsv, fastled_col); if (irApplyToAllSelected) { - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; seg.colors[0] = RGBW32(fastled_col.red, fastled_col.green, fastled_col.blue, W(sseg.colors[0])); @@ -208,11 +196,11 @@ void changeEffectIntensity(int8_t amount) lastRepeatableValue = amount; } -void changeColor(uint32_t c, int16_t cct=-1) +static void changeColor(uint32_t c, int16_t cct=-1) { if (irApplyToAllSelected) { // main segment may not be selected! - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; byte capabilities = seg.getLightCapabilities(); @@ -249,7 +237,7 @@ void changeColor(uint32_t c, int16_t cct=-1) stateChanged = true; } -void changeWhite(int8_t amount, int16_t cct=-1) +static void changeWhite(int8_t amount, int16_t cct=-1) { Segment& seg = irApplyToAllSelected ? strip.getFirstSelectedSeg() : strip.getMainSegment(); byte r = R(seg.colors[0]); @@ -259,72 +247,7 @@ void changeWhite(int8_t amount, int16_t cct=-1) changeColor(RGBW32(r, g, b, w), cct); } -void decodeIR(uint32_t code) -{ - if (code == 0xFFFFFFFF) { - //repeated code, continue brightness up/down - irTimesRepeated++; - applyRepeatActions(); - return; - } - lastValidCode = 0; irTimesRepeated = 0; - lastRepeatableAction = ACTION_NONE; - - if (irEnabled == 8) { // any remote configurable with ir.json file - decodeIRJson(code); - stateUpdated(CALL_MODE_BUTTON); - return; - } - if (code > 0xFFFFFF) return; //invalid code - - switch (irEnabled) { - case 1: - if (code > 0xF80000) decodeIR24OLD(code); // white 24-key remote (old) - it sends 0xFF0000 values - else decodeIR24(code); // 24-key remote - 0xF70000 to 0xF80000 - break; - case 2: decodeIR24CT(code); break; // white 24-key remote with CW, WW, CT+ and CT- keys - case 3: decodeIR40(code); break; // blue 40-key remote with 25%, 50%, 75% and 100% keys - case 4: decodeIR44(code); break; // white 44-key remote with color-up/down keys and DIY1 to 6 keys - case 5: decodeIR21(code); break; // white 21-key remote - case 6: decodeIR6(code); break; // black 6-key learning remote defaults: "CH" controls brightness, - // "VOL +" controls effect, "VOL -" controls colour/palette, "MUTE" - // sets bright plain white - case 7: decodeIR9(code); break; - //case 8: return; // ir.json file, handled above switch statement - } - - if (nightlightActive && bri == 0) nightlightActive = false; - stateUpdated(CALL_MODE_BUTTON); //for notifier, IR is considered a button input -} - -void applyRepeatActions() -{ - if (irEnabled == 8) { - decodeIRJson(lastValidCode); - return; - } else switch (lastRepeatableAction) { - case ACTION_BRIGHT_UP : incBrightness(); stateUpdated(CALL_MODE_BUTTON); return; - case ACTION_BRIGHT_DOWN : decBrightness(); stateUpdated(CALL_MODE_BUTTON); return; - case ACTION_SPEED_UP : changeEffectSpeed(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; - case ACTION_SPEED_DOWN : changeEffectSpeed(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; - case ACTION_INTENSITY_UP : changeEffectIntensity(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; - case ACTION_INTENSITY_DOWN : changeEffectIntensity(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; - default: break; - } - if (lastValidCode == IR40_WPLUS) { - changeWhite(10); - stateUpdated(CALL_MODE_BUTTON); - } else if (lastValidCode == IR40_WMINUS) { - changeWhite(-10); - stateUpdated(CALL_MODE_BUTTON); - } else if ((lastValidCode == IR24_ON || lastValidCode == IR40_ON) && irTimesRepeated > 7 ) { - nightlightActive = true; - nightlightStartTime = millis(); - stateUpdated(CALL_MODE_BUTTON); - } -} - -void decodeIR24(uint32_t code) +static void decodeIR24(uint32_t code) { switch (code) { case IR24_BRIGHTER : incBrightness(); break; @@ -356,7 +279,7 @@ void decodeIR24(uint32_t code) lastValidCode = code; } -void decodeIR24OLD(uint32_t code) +static void decodeIR24OLD(uint32_t code) { switch (code) { case IR24_OLD_BRIGHTER : incBrightness(); break; @@ -388,7 +311,7 @@ void decodeIR24OLD(uint32_t code) lastValidCode = code; } -void decodeIR24CT(uint32_t code) +static void decodeIR24CT(uint32_t code) { switch (code) { case IR24_CT_BRIGHTER : incBrightness(); break; @@ -420,7 +343,7 @@ void decodeIR24CT(uint32_t code) lastValidCode = code; } -void decodeIR40(uint32_t code) +static void decodeIR40(uint32_t code) { Segment& seg = irApplyToAllSelected ? strip.getFirstSelectedSeg() : strip.getMainSegment(); byte r = R(seg.colors[0]); @@ -473,7 +396,7 @@ void decodeIR40(uint32_t code) lastValidCode = code; } -void decodeIR44(uint32_t code) +static void decodeIR44(uint32_t code) { switch (code) { case IR44_BPLUS : incBrightness(); break; @@ -512,7 +435,7 @@ void decodeIR44(uint32_t code) case IR44_DIY2 : presetFallback(2, FX_MODE_BREATH, 0); break; case IR44_DIY3 : presetFallback(3, FX_MODE_FIRE_FLICKER, 0); break; case IR44_DIY4 : presetFallback(4, FX_MODE_RAINBOW, 0); break; - case IR44_DIY5 : presetFallback(5, FX_MODE_METEOR_SMOOTH, 0); break; + case IR44_DIY5 : presetFallback(5, FX_MODE_METEOR, 0); break; case IR44_DIY6 : presetFallback(6, FX_MODE_RAIN, 0); break; case IR44_AUTO : changeEffect(FX_MODE_STATIC); break; case IR44_FLASH : changeEffect(FX_MODE_PALETTE); break; @@ -525,7 +448,7 @@ void decodeIR44(uint32_t code) lastValidCode = code; } -void decodeIR21(uint32_t code) +static void decodeIR21(uint32_t code) { switch (code) { case IR21_BRIGHTER: incBrightness(); break; @@ -554,7 +477,7 @@ void decodeIR21(uint32_t code) lastValidCode = code; } -void decodeIR6(uint32_t code) +static void decodeIR6(uint32_t code) { switch (code) { case IR6_POWER: toggleOnOff(); break; @@ -587,7 +510,7 @@ void decodeIR6(uint32_t code) lastValidCode = code; } -void decodeIR9(uint32_t code) +static void decodeIR9(uint32_t code) { switch (code) { case IR9_POWER : toggleOnOff(); break; @@ -628,7 +551,7 @@ Sample: "label": "Preset 1, fallback to Saw - Party if not found"}, } */ -void decodeIRJson(uint32_t code) +static void decodeIRJson(uint32_t code) { char objKey[10]; char fileName[16]; @@ -670,7 +593,7 @@ void decodeIRJson(uint32_t code) decBrightness(); } else if (cmdStr.startsWith(F("!presetF"))) { //!presetFallback uint8_t p1 = fdo["PL"] | 1; - uint8_t p2 = fdo["FX"] | random8(strip.getModeCount() -1); + uint8_t p2 = fdo["FX"] | hw_random8(strip.getModeCount() -1); uint8_t p3 = fdo["FP"] | 0; presetFallback(p1, p2, p3); } @@ -701,41 +624,101 @@ void decodeIRJson(uint32_t code) releaseJSONBufferLock(); } +static void applyRepeatActions() +{ + if (irEnabled == 8) { + decodeIRJson(lastValidCode); + return; + } else switch (lastRepeatableAction) { + case ACTION_BRIGHT_UP : incBrightness(); stateUpdated(CALL_MODE_BUTTON); return; + case ACTION_BRIGHT_DOWN : decBrightness(); stateUpdated(CALL_MODE_BUTTON); return; + case ACTION_SPEED_UP : changeEffectSpeed(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; + case ACTION_SPEED_DOWN : changeEffectSpeed(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; + case ACTION_INTENSITY_UP : changeEffectIntensity(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; + case ACTION_INTENSITY_DOWN : changeEffectIntensity(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; + default: break; + } + if (lastValidCode == IR40_WPLUS) { + changeWhite(10); + stateUpdated(CALL_MODE_BUTTON); + } else if (lastValidCode == IR40_WMINUS) { + changeWhite(-10); + stateUpdated(CALL_MODE_BUTTON); + } else if ((lastValidCode == IR24_ON || lastValidCode == IR40_ON) && irTimesRepeated > 7 ) { + nightlightActive = true; + nightlightStartTime = millis(); + stateUpdated(CALL_MODE_BUTTON); + } +} + +static void decodeIR(uint32_t code) +{ + if (code == 0xFFFFFFFF) { + //repeated code, continue brightness up/down + irTimesRepeated++; + applyRepeatActions(); + return; + } + lastValidCode = 0; irTimesRepeated = 0; + lastRepeatableAction = ACTION_NONE; + + if (irEnabled == 8) { // any remote configurable with ir.json file + decodeIRJson(code); + stateUpdated(CALL_MODE_BUTTON); + return; + } + if (code > 0xFFFFFF) return; //invalid code + + switch (irEnabled) { + case 1: + if (code > 0xF80000) decodeIR24OLD(code); // white 24-key remote (old) - it sends 0xFF0000 values + else decodeIR24(code); // 24-key remote - 0xF70000 to 0xF80000 + break; + case 2: decodeIR24CT(code); break; // white 24-key remote with CW, WW, CT+ and CT- keys + case 3: decodeIR40(code); break; // blue 40-key remote with 25%, 50%, 75% and 100% keys + case 4: decodeIR44(code); break; // white 44-key remote with color-up/down keys and DIY1 to 6 keys + case 5: decodeIR21(code); break; // white 21-key remote + case 6: decodeIR6(code); break; // black 6-key learning remote defaults: "CH" controls brightness, + // "VOL +" controls effect, "VOL -" controls colour/palette, "MUTE" + // sets bright plain white + case 7: decodeIR9(code); break; + //case 8: return; // ir.json file, handled above switch statement + } + + if (nightlightActive && bri == 0) nightlightActive = false; + stateUpdated(CALL_MODE_BUTTON); //for notifier, IR is considered a button input +} + void initIR() { - if (irEnabled > 0) - { + if (irEnabled > 0) { irrecv = new IRrecv(irPin); - irrecv->enableIRIn(); + if (irrecv) irrecv->enableIRIn(); + } else irrecv = nullptr; +} + +void deInitIR() +{ + if (irrecv) { + irrecv->disableIRIn(); + delete irrecv; } + irrecv = nullptr; } void handleIR() { - if (irEnabled > 0 && millis() - irCheckedTime > 120 && !strip.isUpdating()) - { - irCheckedTime = millis(); - if (irEnabled > 0) - { - if (irrecv == NULL) - { - initIR(); return; + unsigned long currentTime = millis(); + unsigned timeDiff = currentTime - irCheckedTime; + if (timeDiff > 120 && irEnabled > 0 && irrecv) { + if (strip.isUpdating() && timeDiff < 240) return; // be nice, but not too nice + irCheckedTime = currentTime; + if (irrecv->decode(&results)) { + if (results.value != 0 && serialCanTX) { // only print results if anything is received ( != 0 ) + Serial.printf_P(PSTR("IR recv: 0x%lX\n"), (unsigned long)results.value); } - - if (irrecv->decode(&results)) - { - if (results.value != 0) // only print results if anything is received ( != 0 ) - { - if (!pinManager.isPinAllocated(hardwareTX) || pinManager.getPinOwner(hardwareTX) == PinOwner::DebugOut) // Serial TX pin (GPIO 1 on ESP32 and ESP8266) - Serial.printf_P(PSTR("IR recv: 0x%lX\n"), (unsigned long)results.value); - } - decodeIR(results.value); - irrecv->resume(); - } - } else if (irrecv != NULL) - { - irrecv->disableIRIn(); - delete irrecv; irrecv = NULL; + decodeIR(results.value); + irrecv->resume(); } } } diff --git a/wled00/json.cpp b/wled00/json.cpp index b79da8d90..cc25d5f89 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -34,17 +34,17 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) //DEBUG_PRINTLN(F("-- JSON deserialize segment.")); Segment& seg = strip.getSegment(id); //DEBUG_PRINTF_P(PSTR("-- Original segment: %p (%p)\n"), &seg, seg.data); - Segment prev = seg; //make a backup so we can tell if something changed (calling copy constructor) + const Segment prev = seg; //make a backup so we can tell if something changed (calling copy constructor) //DEBUG_PRINTF_P(PSTR("-- Duplicate segment: %p (%p)\n"), &prev, prev.data); - uint16_t start = elem["start"] | seg.start; + int start = elem["start"] | seg.start; if (stop < 0) { int len = elem["len"]; stop = (len > 0) ? start + len : seg.stop; } // 2D segments - uint16_t startY = elem["startY"] | seg.startY; - uint16_t stopY = elem["stopY"] | seg.stopY; + int startY = elem["startY"] | seg.startY; + int stopY = elem["stopY"] | seg.stopY; //repeat, multiplies segment until all LEDs are used, or max segments reached bool repeat = elem["rpt"] | false; @@ -52,7 +52,7 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) elem.remove("id"); // remove for recursive call elem.remove("rpt"); // remove for recursive call elem.remove("n"); // remove for recursive call - uint16_t len = stop - start; + unsigned len = stop - start; for (size_t i=id+1; i= strip.getLengthTotal()) break; @@ -96,17 +96,11 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) uint16_t of = seg.offset; uint8_t soundSim = elem["si"] | seg.soundSim; uint8_t map1D2D = elem["m12"] | seg.map1D2D; - - if ((spc>0 && spc!=seg.spacing) || seg.map1D2D!=map1D2D) seg.fill(BLACK); // clear spacing gaps - - seg.map1D2D = constrain(map1D2D, 0, 7); + uint8_t set = elem[F("set")] | seg.set; + seg.set = constrain(set, 0, 3); seg.soundSim = constrain(soundSim, 0, 3); - uint8_t set = elem[F("set")] | seg.set; - seg.set = constrain(set, 0, 3); - - uint16_t len = 1; - if (stop > start) len = stop - start; + int len = (stop > start) ? stop - start : 1; int offset = elem[F("of")] | INT32_MAX; if (offset != INT32_MAX) { int offsetAbs = abs(offset); @@ -117,7 +111,7 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) if (stop > start && of > len -1) of = len -1; // update segment (delete if necessary) - seg.setUp(start, stop, grp, spc, of, startY, stopY); // strip needs to be suspended for this to work without issues + seg.setGeometry(start, stop, grp, spc, of, startY, stopY, map1D2D); // strip needs to be suspended for this to work without issues if (newSeg) seg.refreshLightCapabilities(); // fix for #3403 @@ -142,28 +136,42 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) { if (seg.getLightCapabilities() & 3) { // segment has RGB or White - for (size_t i = 0; i < 3; i++) - { + for (size_t i = 0; i < NUM_COLORS; i++) { + // JSON "col" array can contain the following values for each of segment's colors (primary, background, custom): + // "col":[int|string|object|array, int|string|object|array, int|string|object|array] + // int = Kelvin temperature or 0 for black + // string = hex representation of [WW]RRGGBB + // object = individual channel control {"r":0,"g":127,"b":255,"w":255}, each being optional (valid to send {}) + // array = direct channel values [r,g,b,w] (w element being optional) int rgbw[] = {0,0,0,0}; bool colValid = false; JsonArray colX = colarr[i]; if (colX.isNull()) { - byte brgbw[] = {0,0,0,0}; - const char* hexCol = colarr[i]; - if (hexCol == nullptr) { //Kelvin color temperature (or invalid), e.g 2400 - int kelvin = colarr[i] | -1; - if (kelvin < 0) continue; - if (kelvin == 0) seg.setColor(i, 0); - if (kelvin > 0) colorKtoRGB(kelvin, brgbw); + JsonObject oCol = colarr[i]; + if (!oCol.isNull()) { + // we have a JSON object for color {"w":123,"r":123,...}; allows individual channel control + rgbw[0] = oCol["r"] | R(seg.colors[i]); + rgbw[1] = oCol["g"] | G(seg.colors[i]); + rgbw[2] = oCol["b"] | B(seg.colors[i]); + rgbw[3] = oCol["w"] | W(seg.colors[i]); colValid = true; - } else { //HEX string, e.g. "FFAA00" - colValid = colorFromHexString(brgbw, hexCol); + } else { + byte brgbw[] = {0,0,0,0}; + const char* hexCol = colarr[i]; + if (hexCol == nullptr) { //Kelvin color temperature (or invalid), e.g 2400 + int kelvin = colarr[i] | -1; + if (kelvin < 0) continue; + if (kelvin == 0) seg.setColor(i, 0); + if (kelvin > 0) colorKtoRGB(kelvin, brgbw); + colValid = true; + } else { //HEX string, e.g. "FFAA00" + colValid = colorFromHexString(brgbw, hexCol); + } + for (size_t c = 0; c < 4; c++) rgbw[c] = brgbw[c]; } - for (size_t c = 0; c < 4; c++) rgbw[c] = brgbw[c]; } else { //Array of ints (RGB or RGBW color), e.g. [255,160,0] byte sz = colX.size(); if (sz == 0) continue; //do nothing on empty array - copyArray(colX, rgbw, 4); colValid = true; } @@ -183,11 +191,11 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) // lx parser #ifdef WLED_ENABLE_LOXONE int lx = elem[F("lx")] | -1; - if (lx > 0) { + if (lx >= 0) { parseLxJson(lx, id, false); } int ly = elem[F("ly")] | -1; - if (ly > 0) { + if (ly >= 0) { parseLxJson(ly, id, true); } #endif @@ -209,31 +217,23 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) #endif byte fx = seg.mode; - byte last = strip.getModeCount(); - // partial fix for #3605 - if (!elem["fx"].isNull() && elem["fx"].is()) { - const char *tmp = elem["fx"].as(); - if (strlen(tmp) > 3 && (strchr(tmp,'r') || strchr(tmp,'~') != strrchr(tmp,'~'))) last = 0; // we have "X~Y(r|[w]~[-])" form - } - // end fix - if (getVal(elem["fx"], &fx, 0, last)) { //load effect ('r' random, '~' inc/dec, 0-255 exact value, 5~10r pick random between 5 & 10) + if (getVal(elem["fx"], &fx, 0, strip.getModeCount())) { if (!presetId && currentPlaylist>=0) unloadPlaylist(); if (fx != seg.mode) seg.setMode(fx, elem[F("fxdef")]); } - //getVal also supports inc/decrementing and random getVal(elem["sx"], &seg.speed); getVal(elem["ix"], &seg.intensity); uint8_t pal = seg.palette; if (seg.getLightCapabilities() & 1) { // ignore palette for White and On/Off segments - if (getVal(elem["pal"], &pal)) seg.setPalette(pal); + if (getVal(elem["pal"], &pal, 0, strip.getPaletteCount())) seg.setPalette(pal); } getVal(elem["c1"], &seg.custom1); getVal(elem["c2"], &seg.custom2); uint8_t cust3 = seg.custom3; - getVal(elem["c3"], &cust3); // we can't pass reference to bitfield + getVal(elem["c3"], &cust3, 0, 31); // we can't pass reference to bitfield seg.custom3 = constrain(cust3, 0, 31); seg.check1 = getBoolVal(elem["o1"], seg.check1); @@ -257,8 +257,8 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) seg.fill(BLACK); } - uint16_t start = 0, stop = 0; - byte set = 0; //0 nothing set, 1 start set, 2 range set + start = 0, stop = 0; + set = 0; //0 nothing set, 1 start set, 2 range set for (size_t i = 0; i < iarr.size(); i++) { if(iarr[i].is()) { @@ -298,7 +298,7 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) return true; } -// deserializes WLED state (fileDoc points to doc object if called from web server) +// deserializes WLED state // presetId is non-0 if called from handlePreset() bool deserializeState(JsonObject root, byte callMode, byte presetId) { @@ -327,7 +327,7 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) } } - int tr = -1; + long tr = -1; if (!presetId || currentPlaylist < 0) { //do not apply transition time from preset if playlist active, as it would override playlist transition times tr = root[F("transition")] | -1; if (tr >= 0) { @@ -344,7 +344,7 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) } tr = root[F("tb")] | -1; - if (tr >= 0) strip.timebase = ((uint32_t)tr) - millis(); + if (tr >= 0) strip.timebase = (unsigned long)tr - millis(); JsonObject nl = root["nl"]; nightlightActive = getBoolVal(nl["on"], nightlightActive); @@ -397,7 +397,7 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) //bool didSet = false; for (size_t s = 0; s < strip.getSegmentsNum(); s++) { Segment &sg = strip.getSegment(s); - if (sg.isSelected()) { + if (sg.isActive() && sg.isSelected()) { deserializeSegment(segVar, s, presetId); //didSet = true; } @@ -417,7 +417,7 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) } strip.resume(); - usermods.readFromJsonState(root); + UsermodManager::readFromJsonState(root); loadLedmap = root[F("ledmap")] | loadLedmap; @@ -435,23 +435,25 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) handleSet(nullptr, apireq, false); // may set stateChanged } - // applying preset (2 cases: a) API call includes all preset values ("pd"), b) API only specifies preset ID ("ps")) + // Applying preset from JSON API has 2 cases: a) "pd" AKA "preset direct" and b) "ps" AKA "preset select" + // a) "preset direct" can only be an integer value representing preset ID. "preset direct" assumes JSON API contains the rest of preset content (i.e. from UI call) + // "preset direct" JSON can contain "ps" API (i.e. call from UI to cycle presets) in such case stateChanged has to be false (i.e. no "win" or "seg" API) + // b) "preset select" can be cycling ("1~5~""), random ("r" or "1~5r"), ID, etc. value allowed from JSON API. This type of call assumes no state changing content in API call byte presetToRestore = 0; - // a) already applied preset content (requires "seg" or "win" but will ignore the rest) if (!root[F("pd")].isNull() && stateChanged) { + // a) already applied preset content (requires "seg" or "win" but will ignore the rest) currentPreset = root[F("pd")] | currentPreset; - if (root["win"].isNull()) presetCycCurr = currentPreset; // otherwise it was set in handleSet() [set.cpp] + if (root["win"].isNull()) presetCycCurr = currentPreset; // otherwise presetCycCurr was set in handleSet() [set.cpp] presetToRestore = currentPreset; // stateUpdated() will clear the preset, so we need to restore it after - //unloadPlaylist(); // applying a preset unloads the playlist, may be needed here too? + DEBUG_PRINTF_P(PSTR("Preset direct: %d\n"), currentPreset); } else if (!root["ps"].isNull()) { - ps = presetCycCurr; - if (root["win"].isNull() && getVal(root["ps"], &ps, 0, 0) && ps > 0 && ps < 251 && ps != currentPreset) { + // we have "ps" call (i.e. from button or external API call) or "pd" that includes "ps" (i.e. from UI call) + if (root["win"].isNull() && getVal(root["ps"], &presetCycCurr, 1, 250) && presetCycCurr > 0 && presetCycCurr < 251 && presetCycCurr != currentPreset) { + DEBUG_PRINTF_P(PSTR("Preset select: %d\n"), presetCycCurr); // b) preset ID only or preset that does not change state (use embedded cycling limits if they exist in getVal()) - presetCycCurr = ps; - unloadPlaylist(); // applying a preset unloads the playlist - applyPreset(ps, callMode); // async load from file system (only preset ID was specified) + applyPreset(presetCycCurr, callMode); // async load from file system (only preset ID was specified) return stateResponse; - } + } else presetCycCurr = currentPreset; // restore presetCycCurr } JsonObject playlist = root[F("playlist")]; @@ -470,6 +472,8 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) } } + doAdvancePlaylist = root[F("np")] | doAdvancePlaylist; //advances to next preset in playlist when true + JsonObject wifi = root[F("wifi")]; if (!wifi.isNull()) { bool apMode = getBoolVal(wifi[F("ap")], apActive); @@ -571,15 +575,16 @@ void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segme root["ps"] = (currentPreset > 0) ? currentPreset : -1; root[F("pl")] = currentPlaylist; + root[F("ledmap")] = currentLedmap; - usermods.addToJsonState(root); + UsermodManager::addToJsonState(root); JsonObject nl = root.createNestedObject("nl"); nl["on"] = nightlightActive; nl["dur"] = nightlightDelayMins; nl["mode"] = nightlightMode; nl[F("tbri")] = nightlightTargetBri; - nl[F("rem")] = nightlightActive ? (nightlightDelayMs - (millis() - nightlightStartTime)) / 1000 : -1; // seconds remaining + nl[F("rem")] = nightlightActive ? (int)(nightlightDelayMs - (millis() - nightlightStartTime)) / 1000 : -1; // seconds remaining JsonObject udpn = root.createNestedObject("udpn"); udpn[F("send")] = sendNotificationsRT; @@ -619,6 +624,7 @@ void serializeInfo(JsonObject root) root[F("ver")] = versionString; root[F("vid")] = VERSION; root[F("cn")] = F(WLED_CODENAME); + root[F("release")] = releaseString; JsonObject leds = root.createNestedObject(F("leds")); leds[F("count")] = strip.getLengthTotal(); @@ -628,6 +634,7 @@ void serializeInfo(JsonObject root) leds[F("maxseg")] = strip.getMaxSegments(); //leds[F("actseg")] = strip.getActiveSegmentsNum(); //leds[F("seglock")] = false; //might be used in the future to prevent modifications to segment config + leds[F("bootps")] = bootPreset; #ifndef WLED_DISABLE_2D if (strip.isMatrix) { @@ -637,12 +644,12 @@ void serializeInfo(JsonObject root) } #endif - uint8_t totalLC = 0; + unsigned totalLC = 0; JsonArray lcarr = leds.createNestedArray(F("seglc")); size_t nSegs = strip.getSegmentsNum(); for (size_t s = 0; s < nSegs; s++) { if (!strip.getSegment(s).isActive()) continue; - uint8_t lc = strip.getSegment(s).getLightCapabilities(); + unsigned lc = strip.getSegment(s).getLightCapabilities(); totalLC |= lc; lcarr.add(lc); } @@ -712,6 +719,7 @@ void serializeInfo(JsonObject root) wifi_info[F("rssi")] = qrssi; wifi_info[F("signal")] = getSignalQuality(qrssi); wifi_info[F("channel")] = WiFi.channel(); + wifi_info[F("ap")] = apActive; JsonObject fs_info = root.createNestedObject("fs"); fs_info["u"] = fsBytesUsed / 1000; @@ -731,6 +739,8 @@ void serializeInfo(JsonObject root) root[F("arch")] = ESP.getChipModel(); #endif root[F("core")] = ESP.getSdkVersion(); + root[F("clock")] = ESP.getCpuFreqMHz(); + root[F("flash")] = (ESP.getFlashChipSize()/1024)/1024; #ifdef WLED_DEBUG root[F("maxalloc")] = ESP.getMaxAllocHeap(); root[F("resetReason0")] = (int)rtc_get_reset_reason(0); @@ -740,6 +750,8 @@ void serializeInfo(JsonObject root) #else root[F("arch")] = "esp8266"; root[F("core")] = ESP.getCoreVersion(); + root[F("clock")] = ESP.getCpuFreqMHz(); + root[F("flash")] = (ESP.getFlashChipSize()/1024)/1024; #ifdef WLED_DEBUG root[F("maxalloc")] = ESP.getMaxFreeBlockSize(); root[F("resetReason")] = (int)ESP.getResetInfoPtr()->reason; @@ -748,8 +760,8 @@ void serializeInfo(JsonObject root) #endif root[F("freeheap")] = ESP.getFreeHeap(); - #if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) - if (psramFound()) root[F("psram")] = ESP.getFreePsram(); + #if defined(ARDUINO_ARCH_ESP32) + if (psramSafe && psramFound()) root[F("psram")] = ESP.getFreePsram(); #endif root[F("uptime")] = millis()/1000 + rolloverMillis*4294967; @@ -757,7 +769,7 @@ void serializeInfo(JsonObject root) getTimeString(time); root[F("time")] = time; - usermods.addToJsonInfo(root); + UsermodManager::addToJsonInfo(root); uint16_t os = 0; #ifdef WLED_DEBUG @@ -820,7 +832,7 @@ void setPaletteColors(JsonArray json, byte* tcp) TRGBGradientPaletteEntryUnion u; // Count entries - uint16_t count = 0; + unsigned count = 0; do { u = *(ent + count); count++; @@ -851,8 +863,8 @@ void serializePalettes(JsonObject root, int page) int itemPerPage = 8; #endif - int palettesCount = strip.getPaletteCount(); int customPalettes = strip.customPalettes.size(); + int palettesCount = strip.getPaletteCount() - customPalettes; int maxPage = (palettesCount + customPalettes -1) / itemPerPage; if (page > maxPage) page = maxPage; @@ -871,10 +883,7 @@ void serializePalettes(JsonObject root, int page) setPaletteColors(curPalette, PartyColors_p); break; case 1: //random - curPalette.add("r"); - curPalette.add("r"); - curPalette.add("r"); - curPalette.add("r"); + for (int j = 0; j < 4; j++) curPalette.add("r"); break; case 2: //primary color only curPalette.add("c1"); @@ -891,53 +900,20 @@ void serializePalettes(JsonObject root, int page) curPalette.add("c1"); break; case 5: //primary + secondary (+tertiary if not off), more distinct + for (int j = 0; j < 5; j++) curPalette.add("c1"); + for (int j = 0; j < 5; j++) curPalette.add("c2"); + for (int j = 0; j < 5; j++) curPalette.add("c3"); curPalette.add("c1"); - curPalette.add("c1"); - curPalette.add("c1"); - curPalette.add("c1"); - curPalette.add("c1"); - curPalette.add("c2"); - curPalette.add("c2"); - curPalette.add("c2"); - curPalette.add("c2"); - curPalette.add("c2"); - curPalette.add("c3"); - curPalette.add("c3"); - curPalette.add("c3"); - curPalette.add("c3"); - curPalette.add("c3"); - curPalette.add("c1"); - break; - case 6: //Party colors - setPaletteColors(curPalette, PartyColors_p); - break; - case 7: //Cloud colors - setPaletteColors(curPalette, CloudColors_p); - break; - case 8: //Lava colors - setPaletteColors(curPalette, LavaColors_p); - break; - case 9: //Ocean colors - setPaletteColors(curPalette, OceanColors_p); - break; - case 10: //Forest colors - setPaletteColors(curPalette, ForestColors_p); - break; - case 11: //Rainbow colors - setPaletteColors(curPalette, RainbowColors_p); - break; - case 12: //Rainbow stripe colors - setPaletteColors(curPalette, RainbowStripeColors_p); break; default: - { - if (i>=palettesCount) { + if (i >= palettesCount) setPaletteColors(curPalette, strip.customPalettes[i - palettesCount]); - } else { + else if (i < 13) // palette 6 - 12, fastled palettes + setPaletteColors(curPalette, *fastledPalettes[i-6]); + else { memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[i - 13])), 72); setPaletteColors(curPalette, tcp); } - } break; } } @@ -1064,7 +1040,7 @@ void serveJson(AsyncWebServerRequest* request) } #endif else if (url.indexOf("pal") > 0) { - request->send_P(200, "application/json", JSON_palette_names); // contentType defined in AsyncJson-v6.h + request->send_P(200, FPSTR(CONTENT_TYPE_JSON), JSON_palette_names); return; } else if (url.indexOf(F("cfg")) > 0 && handleFileRead(request, F("/cfg.json"))) { @@ -1117,11 +1093,8 @@ void serveJson(AsyncWebServerRequest* request) DEBUG_PRINTF_P(PSTR("JSON buffer size: %u for request: %d\n"), lDoc.memoryUsage(), subJson); - #ifdef WLED_DEBUG - size_t len = - #endif - response->setLength(); - DEBUG_PRINT(F("JSON content length: ")); DEBUG_PRINTLN(len); + [[maybe_unused]] size_t len = response->setLength(); + DEBUG_PRINTF_P(PSTR("JSON content length: %u\n"), len); request->send(response); } @@ -1139,8 +1112,8 @@ bool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient) } #endif - uint16_t used = strip.getLengthTotal(); - uint16_t n = (used -1) /MAX_LIVE_LEDS +1; //only serve every n'th LED if count over MAX_LIVE_LEDS + unsigned used = strip.getLengthTotal(); + unsigned n = (used -1) /MAX_LIVE_LEDS +1; //only serve every n'th LED if count over MAX_LIVE_LEDS #ifndef WLED_DISABLE_2D if (strip.isMatrix) { // ignore anything behid matrix (i.e. extra strip) @@ -1151,10 +1124,10 @@ bool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient) } #endif - char buffer[2048]; // shoud be enough for 256 LEDs [RRGGBB] + all other text (9+25) - strcpy_P(buffer, PSTR("{\"leds\":[")); - obuf = buffer; // assign buffer for oappnd() functions - olen = 9; + DynamicBuffer buffer(9 + (9*(1+(used/n))) + 7 + 5 + 6 + 5 + 6 + 5 + 2); + char* buf = buffer.data(); // assign buffer for oappnd() functions + strncpy_P(buffer.data(), PSTR("{\"leds\":["), buffer.size()); + buf += 9; // sizeof(PSTR()) from last line for (size_t i = 0; i < used; i += n) { @@ -1169,29 +1142,27 @@ bool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient) r = scale8(qadd8(w, r), strip.getBrightness()); //R, add white channel to RGB channels as a simple RGBW -> RGB map g = scale8(qadd8(w, g), strip.getBrightness()); //G b = scale8(qadd8(w, b), strip.getBrightness()); //B - olen += sprintf_P(obuf + olen, PSTR("\"%06X\","), RGBW32(r,g,b,0)); + buf += sprintf_P(buf, PSTR("\"%06X\","), RGBW32(r,g,b,0)); } - olen -= 1; - oappend((const char*)F("],\"n\":")); - oappendi(n); + buf--; // remove last comma + buf += sprintf_P(buf, PSTR("],\"n\":%d"), n); #ifndef WLED_DISABLE_2D if (strip.isMatrix) { - oappend((const char*)F(",\"w\":")); - oappendi(Segment::maxWidth/n); - oappend((const char*)F(",\"h\":")); - oappendi(Segment::maxHeight/n); + buf += sprintf_P(buf, PSTR(",\"w\":%d"), Segment::maxWidth/n); + buf += sprintf_P(buf, PSTR(",\"h\":%d"), Segment::maxHeight/n); } #endif - oappend("}"); + (*buf++) = '}'; + (*buf++) = 0; + if (request) { - request->send(200, "application/json", buffer); // contentType defined in AsyncJson-v6.h + request->send(200, FPSTR(CONTENT_TYPE_JSON), toString(std::move(buffer))); } #ifdef WLED_ENABLE_WEBSOCKETS else { - wsc->text(obuf, olen); + wsc->text(toString(std::move(buffer))); } - #endif - obuf = nullptr; + #endif return true; } #endif diff --git a/wled00/led.cpp b/wled00/led.cpp index ba772df9b..68169509d 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -29,9 +29,9 @@ void setValuesFromSegment(uint8_t s) void applyValuesToSelectedSegs() { // copy of first selected segment to tell if value was updated - uint8_t firstSel = strip.getFirstSelectedSegId(); + unsigned firstSel = strip.getFirstSelectedSegId(); Segment selsegPrev = strip.getSegment(firstSel); - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (i != firstSel && (!seg.isActive() || !seg.isSelected())) continue; @@ -47,17 +47,12 @@ void applyValuesToSelectedSegs() } -void resetTimebase() -{ - strip.timebase = 0 - millis(); -} - - void toggleOnOff() { if (bri == 0) { bri = briLast; + strip.restartRuntime(); } else { briLast = bri; @@ -70,7 +65,7 @@ void toggleOnOff() //scales the brightness with the briMultiplier factor byte scaledBri(byte in) { - uint16_t val = ((uint16_t)in*briMultiplier)/100; + unsigned val = ((uint16_t)in*briMultiplier)/100; if (val > 255) val = 255; return (byte)val; } @@ -80,6 +75,7 @@ byte scaledBri(byte in) void applyBri() { if (!realtimeMode || !arlsForceMaxBri) { + //DEBUG_PRINTF_P(PSTR("Applying strip brightness: %d (%d,%d)\n"), (int)briT, (int)bri, (int)briOld); strip.setBrightness(scaledBri(briT)); } } @@ -122,7 +118,7 @@ void stateUpdated(byte callMode) { nightlightStartTime = millis(); } if (briT == 0) { - if (callMode != CALL_MODE_NOTIFICATION) resetTimebase(); //effect start from beginning + if (callMode != CALL_MODE_NOTIFICATION) strip.resetTimebase(); //effect start from beginning } if (bri > 0) briLast = bri; @@ -131,7 +127,7 @@ void stateUpdated(byte callMode) { if (bri == nightlightTargetBri && callMode != CALL_MODE_NO_NOTIFY && nightlightMode != NL_MODE_SUN) nightlightActive = false; // notify usermods of state change - usermods.onStateChange(callMode); + UsermodManager::onStateChange(callMode); if (fadeTransition) { if (strip.getTransition() == 0) { @@ -144,7 +140,6 @@ void stateUpdated(byte callMode) { if (transitionActive) { briOld = briT; - tperLast = 0; } else strip.setTransitionMode(true); // force all segments to transition mode transitionActive = true; @@ -172,7 +167,9 @@ void updateInterfaces(uint8_t callMode) espalexaDevice->setColor(col[0], col[1], col[2]); } #endif - doPublishMqtt = true; + #ifndef WLED_DISABLE_MQTT + publishMqtt(); + #endif } @@ -180,27 +177,23 @@ void handleTransitions() { //handle still pending interface update updateInterfaces(interfaceUpdateCallMode); -#ifndef WLED_DISABLE_MQTT - if (doPublishMqtt) publishMqtt(); -#endif if (transitionActive && strip.getTransition() > 0) { - float tper = (millis() - transitionStartTime)/(float)strip.getTransition(); - if (tper >= 1.0f) { + int ti = millis() - transitionStartTime; + int tr = strip.getTransition(); + if (ti/tr) { strip.setTransitionMode(false); // stop all transitions // restore (global) transition time if not called from UDP notifier or single/temporary transition from JSON (also playlist) if (jsonTransitionOnce) strip.setTransition(transitionDelay); transitionActive = false; jsonTransitionOnce = false; - tperLast = 0; applyFinalBri(); return; } - if (tper - tperLast < 0.004f) return; - tperLast = tper; - briT = briOld + ((bri - briOld) * tper); - - applyBri(); + byte briTO = briT; + int deltaBri = (int)bri - (int)briOld; + briT = briOld + (deltaBri * ti / tr); + if (briTO != briT) applyBri(); } } @@ -227,7 +220,7 @@ void handleNightlight() nightlightDelayMs = (unsigned)(nightlightDelayMins*60000); nightlightActiveOld = true; briNlT = bri; - for (byte i=0; i<4; i++) colNlT[i] = col[i]; // remember starting color + for (unsigned i=0; i<4; i++) colNlT[i] = col[i]; // remember starting color if (nightlightMode == NL_MODE_SUN) { //save current @@ -235,8 +228,8 @@ void handleNightlight() colNlT[1] = effectSpeed; colNlT[2] = effectPalette; - strip.setMode(strip.getFirstSelectedSegId(), FX_MODE_STATIC); // make sure seg runtime is reset if it was in sunrise mode - effectCurrent = FX_MODE_SUNRISE; + strip.getFirstSelectedSeg().setMode(FX_MODE_STATIC); // make sure seg runtime is reset if it was in sunrise mode + effectCurrent = FX_MODE_SUNRISE; // colorUpdated() will take care of assigning that to all selected segments effectSpeed = nightlightDelayMins; effectPalette = 0; if (effectSpeed > 60) effectSpeed = 60; //currently limited to 60 minutes @@ -252,7 +245,7 @@ void handleNightlight() bri = briNlT + ((nightlightTargetBri - briNlT)*nper); if (nightlightMode == NL_MODE_COLORFADE) // color fading only is enabled with "NF=2" { - for (byte i=0; i<4; i++) col[i] = colNlT[i]+ ((colSec[i] - colNlT[i])*nper); // fading from actual color to secondary color + for (unsigned i=0; i<4; i++) col[i] = colNlT[i]+ ((colSec[i] - colNlT[i])*nper); // fading from actual color to secondary color } colorUpdated(CALL_MODE_NO_NOTIFY); } diff --git a/wled00/mqtt.cpp b/wled00/mqtt.cpp index 3c753a9a9..38afeffe3 100644 --- a/wled00/mqtt.cpp +++ b/wled00/mqtt.cpp @@ -4,10 +4,14 @@ * MQTT communication protocol for home automation */ -#ifdef WLED_ENABLE_MQTT +#ifndef WLED_DISABLE_MQTT #define MQTT_KEEP_ALIVE_TIME 60 // contact the MQTT broker every 60 seconds -void parseMQTTBriPayload(char* payload) +#if MQTT_MAX_TOPIC_LEN > 32 +#warning "MQTT topics length > 32 is not recommended for compatibility with usermods!" +#endif + +static void parseMQTTBriPayload(char* payload) { if (strstr(payload, "ON") || strstr(payload, "on") || strstr(payload, "true")) {bri = briLast; stateUpdated(CALL_MODE_DIRECT_CHANGE);} else if (strstr(payload, "T" ) || strstr(payload, "t" )) {toggleOnOff(); stateUpdated(CALL_MODE_DIRECT_CHANGE);} @@ -20,43 +24,42 @@ void parseMQTTBriPayload(char* payload) } -void onMqttConnect(bool sessionPresent) +static void onMqttConnect(bool sessionPresent) { //(re)subscribe to required topics - char subuf[38]; + char subuf[MQTT_MAX_TOPIC_LEN + 6]; if (mqttDeviceTopic[0] != 0) { - strlcpy(subuf, mqttDeviceTopic, 33); + strlcpy(subuf, mqttDeviceTopic, MQTT_MAX_TOPIC_LEN + 1); mqtt->subscribe(subuf, 0); strcat_P(subuf, PSTR("/col")); mqtt->subscribe(subuf, 0); - strlcpy(subuf, mqttDeviceTopic, 33); + strlcpy(subuf, mqttDeviceTopic, MQTT_MAX_TOPIC_LEN + 1); strcat_P(subuf, PSTR("/api")); mqtt->subscribe(subuf, 0); } if (mqttGroupTopic[0] != 0) { - strlcpy(subuf, mqttGroupTopic, 33); + strlcpy(subuf, mqttGroupTopic, MQTT_MAX_TOPIC_LEN + 1); mqtt->subscribe(subuf, 0); strcat_P(subuf, PSTR("/col")); mqtt->subscribe(subuf, 0); - strlcpy(subuf, mqttGroupTopic, 33); + strlcpy(subuf, mqttGroupTopic, MQTT_MAX_TOPIC_LEN + 1); strcat_P(subuf, PSTR("/api")); mqtt->subscribe(subuf, 0); } - usermods.onMqttConnect(sessionPresent); + UsermodManager::onMqttConnect(sessionPresent); - doPublishMqtt = true; DEBUG_PRINTLN(F("MQTT ready")); + publishMqtt(); } -void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { +static void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { static char *payloadStr; - DEBUG_PRINT(F("MQTT msg: ")); - DEBUG_PRINTLN(topic); + DEBUG_PRINTF_P(PSTR("MQTT msg: %s\n"), topic); // paranoia check to avoid npe if no payload if (payload==nullptr) { @@ -76,7 +79,7 @@ void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties if (index + len >= total) { // at end payloadStr[total] = '\0'; // terminate c style string } else { - DEBUG_PRINTLN(F("Partial packet received.")); + DEBUG_PRINTLN(F("MQTT partial packet received.")); return; // process next packet } DEBUG_PRINTLN(payloadStr); @@ -90,7 +93,7 @@ void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties topic += topicPrefixLen; } else { // Non-Wled Topic used here. Probably a usermod subscribed to this topic. - usermods.onMqttMessage(topic, payloadStr); + UsermodManager::onMqttMessage(topic, payloadStr); delete[] payloadStr; payloadStr = nullptr; return; @@ -103,23 +106,20 @@ void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties colorFromDecOrHexString(col, payloadStr); colorUpdated(CALL_MODE_DIRECT_CHANGE); } else if (strcmp_P(topic, PSTR("/api")) == 0) { - if (!requestJSONBufferLock(15)) { - delete[] payloadStr; - payloadStr = nullptr; - return; + if (requestJSONBufferLock(15)) { + if (payloadStr[0] == '{') { //JSON API + deserializeJson(*pDoc, payloadStr); + deserializeState(pDoc->as()); + } else { //HTTP API + String apireq = "win"; apireq += '&'; // reduce flash string usage + apireq += payloadStr; + handleSet(nullptr, apireq); + } + releaseJSONBufferLock(); } - if (payloadStr[0] == '{') { //JSON API - deserializeJson(*pDoc, payloadStr); - deserializeState(pDoc->as()); - } else { //HTTP API - String apireq = "win"; apireq += '&'; // reduce flash string usage - apireq += payloadStr; - handleSet(nullptr, apireq); - } - releaseJSONBufferLock(); } else if (strlen(topic) != 0) { // non standard topic, check with usermods - usermods.onMqttMessage(topic, payloadStr); + UsermodManager::onMqttMessage(topic, payloadStr); } else { // topmost topic (just wled/MAC) parseMQTTBriPayload(payloadStr); @@ -128,36 +128,63 @@ void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties payloadStr = nullptr; } +// Print adapter for flat buffers +namespace { +class bufferPrint : public Print { + char* _buf; + size_t _size, _offset; + public: + + bufferPrint(char* buf, size_t size) : _buf(buf), _size(size), _offset(0) {}; + + size_t write(const uint8_t *buffer, size_t size) { + size = std::min(size, _size - _offset); + memcpy(_buf + _offset, buffer, size); + _offset += size; + return size; + } + + size_t write(uint8_t c) { + return this->write(&c, 1); + } + + char* data() const { return _buf; } + size_t size() const { return _offset; } + size_t capacity() const { return _size; } +}; +}; // anonymous namespace + void publishMqtt() { - doPublishMqtt = false; if (!WLED_MQTT_CONNECTED) return; DEBUG_PRINTLN(F("Publish MQTT")); #ifndef USERMOD_SMARTNEST char s[10]; - char subuf[38]; + char subuf[MQTT_MAX_TOPIC_LEN + 16]; sprintf_P(s, PSTR("%u"), bri); - strlcpy(subuf, mqttDeviceTopic, 33); + strlcpy(subuf, mqttDeviceTopic, MQTT_MAX_TOPIC_LEN + 1); strcat_P(subuf, PSTR("/g")); mqtt->publish(subuf, 0, retainMqttMsg, s); // optionally retain message (#2263) sprintf_P(s, PSTR("#%06X"), (col[3] << 24) | (col[0] << 16) | (col[1] << 8) | (col[2])); - strlcpy(subuf, mqttDeviceTopic, 33); + strlcpy(subuf, mqttDeviceTopic, MQTT_MAX_TOPIC_LEN + 1); strcat_P(subuf, PSTR("/c")); mqtt->publish(subuf, 0, retainMqttMsg, s); // optionally retain message (#2263) - strlcpy(subuf, mqttDeviceTopic, 33); + strlcpy(subuf, mqttDeviceTopic, MQTT_MAX_TOPIC_LEN + 1); strcat_P(subuf, PSTR("/status")); mqtt->publish(subuf, 0, true, "online"); // retain message for a LWT - char apires[1024]; // allocating 1024 bytes from stack can be risky - XML_response(nullptr, apires); - strlcpy(subuf, mqttDeviceTopic, 33); + // TODO: use a DynamicBufferList. Requires a list-read-capable MQTT client API. + DynamicBuffer buf(1024); + bufferPrint pbuf(buf.data(), buf.size()); + XML_response(pbuf); + strlcpy(subuf, mqttDeviceTopic, MQTT_MAX_TOPIC_LEN + 1); strcat_P(subuf, PSTR("/v")); - mqtt->publish(subuf, 0, retainMqttMsg, apires); // optionally retain message (#2263) + mqtt->publish(subuf, 0, retainMqttMsg, buf.data(), pbuf.size()); // optionally retain message (#2263) #endif } @@ -170,6 +197,7 @@ bool initMqtt() if (mqtt == nullptr) { mqtt = new AsyncMqttClient(); + if (!mqtt) return false; mqtt->onMessage(onMqttMessage); mqtt->onConnect(onMqttConnect); } @@ -187,7 +215,7 @@ bool initMqtt() if (mqttUser[0] && mqttPass[0]) mqtt->setCredentials(mqttUser, mqttPass); #ifndef USERMOD_SMARTNEST - strlcpy(mqttStatusTopic, mqttDeviceTopic, 33); + strlcpy(mqttStatusTopic, mqttDeviceTopic, MQTT_MAX_TOPIC_LEN + 1); strcat_P(mqttStatusTopic, PSTR("/status")); mqtt->setWill(mqttStatusTopic, 0, true, "offline"); // LWT message #endif diff --git a/wled00/network.cpp b/wled00/network.cpp index 2ae38f799..3bd589f94 100644 --- a/wled00/network.cpp +++ b/wled00/network.cpp @@ -133,6 +133,17 @@ const ethernet_settings ethernetBoards[] = { 18, // eth_mdio, ETH_PHY_LAN8720, // eth_type, ETH_CLOCK_GPIO0_OUT // eth_clk_mode + }, + + // LILYGO T-POE Pro + // https://github.com/Xinyuan-LilyGO/LilyGO-T-ETH-Series/blob/master/schematic/T-POE-PRO.pdf + { + 0, // eth_address, + 5, // eth_power, + 23, // eth_mdc, + 18, // eth_mdio, + ETH_PHY_LAN8720, // eth_type, + ETH_CLOCK_GPIO0_OUT // eth_clk_mode } }; #endif @@ -196,6 +207,7 @@ void WiFiEvent(WiFiEvent_t event) break; #endif default: + DEBUG_PRINTF_P(PSTR("Network event: %d\n"), (int)event); break; } } diff --git a/wled00/ntp.cpp b/wled00/ntp.cpp index 0b2cf3665..8d44e634e 100644 --- a/wled00/ntp.cpp +++ b/wled00/ntp.cpp @@ -2,20 +2,8 @@ #include "wled.h" #include "fcn_declare.h" -// on esp8266, building with `-D WLED_USE_UNREAL_MATH` saves around 7Kb flash and 1KB RAM -// warning: causes errors in sunset calculations, see #3400 -#if defined(WLED_USE_UNREAL_MATH) -#define sinf sin_t -#define asinf asin_t -#define cosf cos_t -#define acosf acos_t -#define tanf tan_t -#define atanf atan_t -#define fmodf fmod_t -#define floorf floor_t -#else -#include -#endif +// WARNING: may cause errors in sunset calculations on ESP8266, see #3400 +// building with `-D WLED_USE_REAL_MATH` will prevent those errors at the expense of flash and RAM /* * Acquires time from NTP server @@ -48,8 +36,9 @@ Timezone* tz; #define TZ_ANCHORAGE 20 #define TZ_MX_CENTRAL 21 #define TZ_PAKISTAN 22 +#define TZ_BRASILIA 23 -#define TZ_COUNT 23 +#define TZ_COUNT 24 #define TZ_INIT 255 byte tzCurrent = TZ_INIT; //uninitialized @@ -147,6 +136,10 @@ static const std::pair TZ_TABLE[] PROGMEM = { /* TZ_PAKISTAN */ { {Last, Sun, Mar, 1, 300}, //Pakistan Standard Time = UTC + 5 hours {Last, Sun, Mar, 1, 300} + }, + /* TZ_BRASILIA */ { + {Last, Sun, Mar, 1, -180}, //Brasília Standard Time = UTC - 3 hours + {Last, Sun, Mar, 1, -180} } }; @@ -258,8 +251,7 @@ bool checkNTPResponse() } uint32_t ntpPacketReceivedTime = millis(); - DEBUG_PRINT(F("NTP recv, l=")); - DEBUG_PRINTLN(cb); + DEBUG_PRINTF_P(PSTR("NTP recv, l=%d\n"), cb); byte pbuf[NTP_PACKET_SIZE]; ntpUdp.read(pbuf, NTP_PACKET_SIZE); // read the packet into the buffer if (!isValidNtpResponse(pbuf)) return false; // verify we have a valid response to client @@ -389,7 +381,7 @@ void checkTimers() if (!hour(localTime) && minute(localTime)==1) calculateSunriseAndSunset(); DEBUG_PRINTF_P(PSTR("Local time: %02d:%02d\n"), hour(localTime), minute(localTime)); - for (uint8_t i = 0; i < 8; i++) + for (unsigned i = 0; i < 8; i++) { if (timerMacro[i] != 0 && (timerWeekday[i] & 0x01) //timer is enabled @@ -399,7 +391,6 @@ void checkTimers() && isTodayInDateRange(((timerMonth[i] >> 4) & 0x0F), timerDay[i], timerMonth[i] & 0x0F, timerDayEnd[i]) ) { - unloadPlaylist(); applyPreset(timerMacro[i]); } } @@ -413,7 +404,6 @@ void checkTimers() && (timerWeekday[8] & 0x01) //timer is enabled && ((timerWeekday[8] >> weekdayMondayFirst()) & 0x01)) //timer should activate at current day of week { - unloadPlaylist(); applyPreset(timerMacro[8]); DEBUG_PRINTF_P(PSTR("Sunrise macro %d triggered."),timerMacro[8]); } @@ -428,7 +418,6 @@ void checkTimers() && (timerWeekday[9] & 0x01) //timer is enabled && ((timerWeekday[9] >> weekdayMondayFirst()) & 0x01)) //timer should activate at current day of week { - unloadPlaylist(); applyPreset(timerMacro[9]); DEBUG_PRINTF_P(PSTR("Sunset macro %d triggered."),timerMacro[9]); } @@ -442,7 +431,7 @@ static int getSunriseUTC(int year, int month, int day, float lat, float lon, boo //1. first calculate the day of the year float N1 = 275 * month / 9; float N2 = (month + 9) / 12; - float N3 = (1.0f + floorf((year - 4 * floorf(year / 4) + 2.0f) / 3.0f)); + float N3 = (1.0f + floor_t((year - 4 * floor_t(year / 4) + 2.0f) / 3.0f)); float N = N1 - (N2 * N3) + day - 30.0f; //2. convert the longitude to hour value and calculate an approximate time @@ -453,37 +442,37 @@ static int getSunriseUTC(int year, int month, int day, float lat, float lon, boo float M = (0.9856f * t) - 3.289f; //4. calculate the Sun's true longitude - float L = fmodf(M + (1.916f * sinf(DEG_TO_RAD*M)) + (0.02f * sinf(2*DEG_TO_RAD*M)) + 282.634f, 360.0f); + float L = fmod_t(M + (1.916f * sin_t(DEG_TO_RAD*M)) + (0.02f * sin_t(2*DEG_TO_RAD*M)) + 282.634f, 360.0f); //5a. calculate the Sun's right ascension - float RA = fmodf(RAD_TO_DEG*atanf(0.91764f * tanf(DEG_TO_RAD*L)), 360.0f); + float RA = fmod_t(RAD_TO_DEG*atan_t(0.91764f * tan_t(DEG_TO_RAD*L)), 360.0f); //5b. right ascension value needs to be in the same quadrant as L - float Lquadrant = floorf( L/90) * 90; - float RAquadrant = floorf(RA/90) * 90; + float Lquadrant = floor_t( L/90) * 90; + float RAquadrant = floor_t(RA/90) * 90; RA = RA + (Lquadrant - RAquadrant); //5c. right ascension value needs to be converted into hours RA /= 15.0f; //6. calculate the Sun's declination - float sinDec = 0.39782f * sinf(DEG_TO_RAD*L); - float cosDec = cosf(asinf(sinDec)); + float sinDec = 0.39782f * sin_t(DEG_TO_RAD*L); + float cosDec = cos_t(asin_t(sinDec)); //7a. calculate the Sun's local hour angle - float cosH = (sinf(DEG_TO_RAD*ZENITH) - (sinDec * sinf(DEG_TO_RAD*lat))) / (cosDec * cosf(DEG_TO_RAD*lat)); + float cosH = (sin_t(DEG_TO_RAD*ZENITH) - (sinDec * sin_t(DEG_TO_RAD*lat))) / (cosDec * cos_t(DEG_TO_RAD*lat)); if ((cosH > 1.0f) && !sunset) return INT16_MAX; // the sun never rises on this location (on the specified date) if ((cosH < -1.0f) && sunset) return INT16_MAX; // the sun never sets on this location (on the specified date) //7b. finish calculating H and convert into hours - float H = sunset ? RAD_TO_DEG*acosf(cosH) : 360 - RAD_TO_DEG*acosf(cosH); + float H = sunset ? RAD_TO_DEG*acos_t(cosH) : 360 - RAD_TO_DEG*acos_t(cosH); H /= 15.0f; //8. calculate local mean time of rising/setting float T = H + RA - (0.06571f * t) - 6.622f; //9. adjust back to UTC - float UT = fmodf(T - lngHour, 24.0f); + float UT = fmod_t(T - lngHour, 24.0f); // return in minutes from midnight return UT*60; @@ -508,7 +497,7 @@ void calculateSunriseAndSunset() { do { time_t theDay = localTime - retryCount * 86400; // one day back = 86400 seconds minUTC = getSunriseUTC(year(theDay), month(theDay), day(theDay), latitude, longitude, false); - DEBUG_PRINT(F("* sunrise (minutes from UTC) = ")); DEBUG_PRINTLN(minUTC); + DEBUG_PRINTF_P(PSTR("* sunrise (minutes from UTC) = %d\n"), minUTC); retryCount ++; } while ((abs(minUTC) > SUNSET_MAX) && (retryCount <= 3)); @@ -527,7 +516,7 @@ void calculateSunriseAndSunset() { do { time_t theDay = localTime - retryCount * 86400; // one day back = 86400 seconds minUTC = getSunriseUTC(year(theDay), month(theDay), day(theDay), latitude, longitude, true); - DEBUG_PRINT(F("* sunset (minutes from UTC) = ")); DEBUG_PRINTLN(minUTC); + DEBUG_PRINTF_P(PSTR("* sunset (minutes from UTC) = %d\n"), minUTC); retryCount ++; } while ((abs(minUTC) > SUNSET_MAX) && (retryCount <= 3)); diff --git a/wled00/overlay.cpp b/wled00/overlay.cpp index 19b26c224..fcd0a40c2 100644 --- a/wled00/overlay.cpp +++ b/wled00/overlay.cpp @@ -25,25 +25,25 @@ void _overlayAnalogClock() { if (secondPixel < analogClock12pixel) { - strip.setRange(analogClock12pixel, overlayMax, 0xFF0000); - strip.setRange(overlayMin, secondPixel, 0xFF0000); + strip.setRange(analogClock12pixel, overlayMax, color_fade(0xFF0000, bri)); + strip.setRange(overlayMin, secondPixel, color_fade(0xFF0000, bri)); } else { - strip.setRange(analogClock12pixel, secondPixel, 0xFF0000); + strip.setRange(analogClock12pixel, secondPixel, color_fade(0xFF0000, bri)); } } if (analogClock5MinuteMarks) { - for (byte i = 0; i <= 12; i++) + for (unsigned i = 0; i <= 12; i++) { unsigned pix = analogClock12pixel + roundf((overlaySize / 12.0f) *i); if (pix > overlayMax) pix -= overlaySize; - strip.setPixelColor(pix, 0x00FFAA); + strip.setPixelColor(pix, color_fade(0x00FFAA, bri)); } } - if (!analogClockSecondsTrail) strip.setPixelColor(secondPixel, 0xFF0000); - strip.setPixelColor(minutePixel, 0x00FF00); - strip.setPixelColor(hourPixel, 0x0000FF); + if (!analogClockSecondsTrail) strip.setPixelColor(secondPixel, color_fade(0xFF0000, bri)); + strip.setPixelColor(minutePixel, color_fade(0x00FF00, bri)); + strip.setPixelColor(hourPixel, color_fade(0x0000FF, bri)); } @@ -88,10 +88,10 @@ void _overlayAnalogCountdown() } void handleOverlayDraw() { - usermods.handleOverlayDraw(); + UsermodManager::handleOverlayDraw(); if (analogClockSolidBlack) { const Segment* segments = strip.getSegments(); - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { const Segment& segment = segments[i]; if (!segment.isActive()) continue; if (segment.mode > 0 || segment.colors[0] > 0) { diff --git a/wled00/palettes.h b/wled00/palettes.h index 41dfbbc16..c84c1fb97 100644 --- a/wled00/palettes.h +++ b/wled00/palettes.h @@ -1,5 +1,6 @@ /* * Color palettes for FastLED effects (65-73). + * 4 bytes per color: index, red, green, blue */ // From ColorWavesWithPalettes by Mark Kriegsman: https://gist.github.com/kriegsman/8281905786e8b2632aeb @@ -844,6 +845,23 @@ const byte candy2_gp[] PROGMEM = { 211, 39, 33, 34, 255, 1, 1, 1}; +const byte trafficlight_gp[] PROGMEM = { + 0, 0, 0, 0, //black + 85, 0, 255, 0, //green + 170, 255, 255, 0, //yellow + 255, 255, 0, 0}; //red + +// array of fastled palettes (palette 6 - 12) +const TProgmemRGBPalette16 *const fastledPalettes[] PROGMEM = { + &PartyColors_p, //06-00 Party + &CloudColors_p, //07-01 Cloud + &LavaColors_p, //08-02 Lava + &OceanColors_p, //09-03 Ocean + &ForestColors_p, //10-04 Forest + &RainbowColors_p, //11-05 Rainbow + &RainbowStripeColors_p //12-06 Rainbow Bands +}; + // Single array of defined cpt-city color palettes. // This will let us programmatically choose one based on // a number, rather than having to activate each explicitly @@ -906,7 +924,8 @@ const byte* const gGradientPalettes[] PROGMEM = { blink_red_gp, //67-54 Blink Red red_shift_gp, //68-55 Red Shift red_tide_gp, //69-56 Red Tide - candy2_gp //70-57 Candy2 + candy2_gp, //70-57 Candy2 + trafficlight_gp //71-58 Traffic Light }; #endif diff --git a/wled00/pin_manager.cpp b/wled00/pin_manager.cpp index 044dc6c92..37ebd41ec 100644 --- a/wled00/pin_manager.cpp +++ b/wled00/pin_manager.cpp @@ -1,46 +1,48 @@ #include "pin_manager.h" #include "wled.h" -#ifdef WLED_DEBUG -static void DebugPrintOwnerTag(PinOwner tag) -{ - uint32_t q = static_cast(tag); - if (q) { - DEBUG_PRINTF_P(PSTR("0x%02x (%d)"), q, q); - } else { - DEBUG_PRINT(F("(no owner)")); - } -} +#ifdef ARDUINO_ARCH_ESP32 + #ifdef bitRead + // Arduino variants assume 32 bit values + #undef bitRead + #undef bitSet + #undef bitClear + #define bitRead(var,bit) (((unsigned long long)(var)>>(bit))&0x1ULL) + #define bitSet(var,bit) ((var)|=(1ULL<<(bit))) + #define bitClear(var,bit) ((var)&=(~(1ULL<<(bit)))) + #endif #endif +// Pin management state variables +#ifdef ESP8266 +static uint32_t pinAlloc = 0UL; // 1 bit per pin, we use first 17bits +#else +static uint64_t pinAlloc = 0ULL; // 1 bit per pin, we use 50 bits on ESP32-S3 +static uint16_t ledcAlloc = 0; // up to 16 LEDC channels (WLED_MAX_ANALOG_CHANNELS) +#endif +static uint8_t i2cAllocCount = 0; // allow multiple allocation of I2C bus pins but keep track of allocations +static uint8_t spiAllocCount = 0; // allow multiple allocation of SPI bus pins but keep track of allocations +static PinOwner ownerTag[WLED_NUM_PINS] = { PinOwner::None }; + /// Actual allocation/deallocation routines -bool PinManagerClass::deallocatePin(byte gpio, PinOwner tag) +bool PinManager::deallocatePin(byte gpio, PinOwner tag) { if (gpio == 0xFF) return true; // explicitly allow clients to free -1 as a no-op if (!isPinOk(gpio, false)) return false; // but return false for any other invalid pin // if a non-zero ownerTag, only allow de-allocation if the owner's tag is provided if ((ownerTag[gpio] != PinOwner::None) && (ownerTag[gpio] != tag)) { - #ifdef WLED_DEBUG - DEBUG_PRINT(F("PIN DEALLOC: IO ")); - DEBUG_PRINT(gpio); - DEBUG_PRINT(F(" allocated by ")); - DebugPrintOwnerTag(ownerTag[gpio]); - DEBUG_PRINT(F(", but attempted de-allocation by ")); - DebugPrintOwnerTag(tag); - #endif + DEBUG_PRINTF_P(PSTR("PIN DEALLOC: FAIL GPIO %d allocated by 0x%02X, but attempted de-allocation by 0x%02X.\n"), gpio, static_cast(ownerTag[gpio]), static_cast(tag)); return false; } - byte by = gpio >> 3; - byte bi = gpio - 8*by; - bitWrite(pinAlloc[by], bi, false); + bitWrite(pinAlloc, gpio, false); ownerTag[gpio] = PinOwner::None; return true; } // support function for deallocating multiple pins -bool PinManagerClass::deallocateMultiplePins(const uint8_t *pinArray, byte arrayElementCount, PinOwner tag) +bool PinManager::deallocateMultiplePins(const uint8_t *pinArray, byte arrayElementCount, PinOwner tag) { bool shouldFail = false; DEBUG_PRINTLN(F("MULTIPIN DEALLOC")); @@ -56,14 +58,7 @@ bool PinManagerClass::deallocateMultiplePins(const uint8_t *pinArray, byte array // if the current pin is allocated by selected owner it is possible to release it continue; } - #ifdef WLED_DEBUG - DEBUG_PRINT(F("PIN DEALLOC: IO ")); - DEBUG_PRINT(gpio); - DEBUG_PRINT(F(" allocated by ")); - DebugPrintOwnerTag(ownerTag[gpio]); - DEBUG_PRINT(F(", but attempted de-allocation by ")); - DebugPrintOwnerTag(tag); - #endif + DEBUG_PRINTF_P(PSTR("PIN DEALLOC: FAIL GPIO %d allocated by 0x%02X, but attempted de-allocation by 0x%02X.\n"), gpio, static_cast(ownerTag[gpio]), static_cast(tag)); shouldFail = true; } if (shouldFail) { @@ -87,14 +82,14 @@ bool PinManagerClass::deallocateMultiplePins(const uint8_t *pinArray, byte array return true; } -bool PinManagerClass::deallocateMultiplePins(const managed_pin_type * mptArray, byte arrayElementCount, PinOwner tag) +bool PinManager::deallocateMultiplePins(const managed_pin_type * mptArray, byte arrayElementCount, PinOwner tag) { uint8_t pins[arrayElementCount]; for (int i=0; i(ownerTag[gpio])); shouldFail = true; } } @@ -146,77 +130,51 @@ bool PinManagerClass::allocateMultiplePins(const managed_pin_type * mptArray, by if (gpio >= WLED_NUM_PINS) continue; // other unexpected GPIO => avoid array bounds violation - byte by = gpio >> 3; - byte bi = gpio - 8*by; - bitWrite(pinAlloc[by], bi, true); + bitWrite(pinAlloc, gpio, true); ownerTag[gpio] = tag; - #ifdef WLED_DEBUG - DEBUG_PRINT(F("PIN ALLOC: Pin ")); - DEBUG_PRINT(gpio); - DEBUG_PRINT(F(" allocated by ")); - DebugPrintOwnerTag(tag); - DEBUG_PRINTLN(F("")); - #endif + DEBUG_PRINTF_P(PSTR("PIN ALLOC: Pin %d allocated by 0x%02X.\n"), gpio, static_cast(tag)); } + DEBUG_PRINTF_P(PSTR("PIN ALLOC: 0x%014llX.\n"), (unsigned long long)pinAlloc); return true; } -bool PinManagerClass::allocatePin(byte gpio, bool output, PinOwner tag) +bool PinManager::allocatePin(byte gpio, bool output, PinOwner tag) { // HW I2C & SPI pins have to be allocated using allocateMultiplePins variant since there is always SCL/SDA pair if (!isPinOk(gpio, output) || (gpio >= WLED_NUM_PINS) || tag==PinOwner::HW_I2C || tag==PinOwner::HW_SPI) { #ifdef WLED_DEBUG if (gpio < 255) { // 255 (-1) is the "not defined GPIO" if (!isPinOk(gpio, output)) { - DEBUG_PRINT(F("PIN ALLOC: FAIL for owner ")); - DebugPrintOwnerTag(tag); - DEBUG_PRINT(F(": GPIO ")); DEBUG_PRINT(gpio); + DEBUG_PRINTF_P(PSTR("PIN ALLOC: FAIL for owner 0x%02X: GPIO %d "), static_cast(tag), gpio); if (output) DEBUG_PRINTLN(F(" cannot be used for i/o on this MCU.")); else DEBUG_PRINTLN(F(" cannot be used as input on this MCU.")); } else { - DEBUG_PRINT(F("PIN ALLOC: FAIL: GPIO ")); DEBUG_PRINT(gpio); - DEBUG_PRINTLN(F(" - HW I2C & SPI pins have to be allocated using allocateMultiplePins()")); + DEBUG_PRINTF_P(PSTR("PIN ALLOC: FAIL GPIO %d - HW I2C & SPI pins have to be allocated using allocateMultiplePins.\n"), gpio); } } #endif return false; } if (isPinAllocated(gpio)) { - #ifdef WLED_DEBUG - DEBUG_PRINT(F("PIN ALLOC: Pin ")); - DEBUG_PRINT(gpio); - DEBUG_PRINT(F(" already allocated by ")); - DebugPrintOwnerTag(ownerTag[gpio]); - DEBUG_PRINTLN(F("")); - #endif + DEBUG_PRINTF_P(PSTR("PIN ALLOC: FAIL Pin %d already allocated by 0x%02X.\n"), gpio, static_cast(ownerTag[gpio])); return false; } - byte by = gpio >> 3; - byte bi = gpio - 8*by; - bitWrite(pinAlloc[by], bi, true); + bitWrite(pinAlloc, gpio, true); ownerTag[gpio] = tag; - #ifdef WLED_DEBUG - DEBUG_PRINT(F("PIN ALLOC: Pin ")); - DEBUG_PRINT(gpio); - DEBUG_PRINT(F(" successfully allocated by ")); - DebugPrintOwnerTag(tag); - DEBUG_PRINTLN(F("")); - #endif + DEBUG_PRINTF_P(PSTR("PIN ALLOC: Pin %d successfully allocated by 0x%02X.\n"), gpio, static_cast(ownerTag[gpio])); + DEBUG_PRINTF_P(PSTR("PIN ALLOC: 0x%014llX.\n"), (unsigned long long)pinAlloc); return true; } // if tag is set to PinOwner::None, checks for ANY owner of the pin. // if tag is set to any other value, checks if that tag is the current owner of the pin. -bool PinManagerClass::isPinAllocated(byte gpio, PinOwner tag) +bool PinManager::isPinAllocated(byte gpio, PinOwner tag) { if (!isPinOk(gpio, false)) return true; if ((tag != PinOwner::None) && (ownerTag[gpio] != tag)) return false; - if (gpio >= WLED_NUM_PINS) return false; // catch error case, to avoid array out-of-bounds access - byte by = gpio >> 3; - byte bi = gpio - (by<<3); - return bitRead(pinAlloc[by], bi); + return bitRead(pinAlloc, gpio); } /* see https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/gpio.html @@ -236,19 +194,29 @@ bool PinManagerClass::isPinAllocated(byte gpio, PinOwner tag) */ // Check if supplied GPIO is ok to use -bool PinManagerClass::isPinOk(byte gpio, bool output) +bool PinManager::isPinOk(byte gpio, bool output) { + if (gpio >= WLED_NUM_PINS) return false; // catch error case, to avoid array out-of-bounds access #ifdef ARDUINO_ARCH_ESP32 if (digitalPinIsValid(gpio)) { #if defined(CONFIG_IDF_TARGET_ESP32C3) // strapping pins: 2, 8, & 9 if (gpio > 11 && gpio < 18) return false; // 11-17 SPI FLASH + #if ARDUINO_USB_CDC_ON_BOOT == 1 || ARDUINO_USB_DFU_ON_BOOT == 1 if (gpio > 17 && gpio < 20) return false; // 18-19 USB-JTAG + #endif #elif defined(CONFIG_IDF_TARGET_ESP32S3) // 00 to 18 are for general use. Be careful about straping pins GPIO0 and GPIO3 - these may be pulled-up or pulled-down on your board. + #if ARDUINO_USB_CDC_ON_BOOT == 1 || ARDUINO_USB_DFU_ON_BOOT == 1 if (gpio > 18 && gpio < 21) return false; // 19 + 20 = USB-JTAG. Not recommended for other uses. + #endif if (gpio > 21 && gpio < 33) return false; // 22 to 32: not connected + SPI FLASH - //if (gpio > 32 && gpio < 38) return false; // 33 to 37: not available if using _octal_ SPI Flash or _octal_ PSRAM + #if CONFIG_ESPTOOLPY_FLASHMODE_OPI // 33-37: never available if using _octal_ Flash (opi_opi) + if (gpio > 32 && gpio < 38) return false; + #endif + #if CONFIG_SPIRAM_MODE_OCT // 33-37: not available if using _octal_ PSRAM (qio_opi), but free to use on _quad_ PSRAM (qio_qspi) + if (gpio > 32 && gpio < 38) return !psramFound(); + #endif // 38 to 48 are for general use. Be careful about straping pins GPIO45 and GPIO46 - these may be pull-up or pulled-down on your board. #elif defined(CONFIG_IDF_TARGET_ESP32S2) // strapping pins: 0, 45 & 46 @@ -256,10 +224,21 @@ bool PinManagerClass::isPinOk(byte gpio, bool output) // JTAG: GPIO39-42 are usually used for inline debugging // GPIO46 is input only and pulled down #else - if (gpio > 5 && gpio < 12) return false; //SPI flash pins - #ifdef BOARD_HAS_PSRAM - if (gpio == 16 || gpio == 17) return false; //PSRAM pins - #endif + + if ((strncmp_P(PSTR("ESP32-U4WDH"), ESP.getChipModel(), 11) == 0) || // this is the correct identifier, but.... + (strncmp_P(PSTR("ESP32-PICO-D2"), ESP.getChipModel(), 13) == 0)) { // https://github.com/espressif/arduino-esp32/issues/10683 + // this chip has 4 MB of internal Flash and different packaging, so available pins are different! + if (((gpio > 5) && (gpio < 9)) || (gpio == 11)) + return false; + } else { + // for classic ESP32 (non-mini) modules, these are the SPI flash pins + if (gpio > 5 && gpio < 12) return false; //SPI flash pins + } + + if (((strncmp_P(PSTR("ESP32-PICO"), ESP.getChipModel(), 10) == 0) || + (strncmp_P(PSTR("ESP32-U4WDH"), ESP.getChipModel(), 11) == 0)) + && (gpio == 16 || gpio == 17)) return false; // PICO-D4/U4WDH: gpio16+17 are in use for onboard SPI FLASH + if (gpio == 16 || gpio == 17) return !psramFound(); //PSRAM pins on ESP32 (these are IO) #endif if (output) return digitalPinCanOutput(gpio); else return true; @@ -272,41 +251,41 @@ bool PinManagerClass::isPinOk(byte gpio, bool output) return false; } -PinOwner PinManagerClass::getPinOwner(byte gpio) { - if (gpio >= WLED_NUM_PINS) return PinOwner::None; // catch error case, to avoid array out-of-bounds access +bool PinManager::isReadOnlyPin(byte gpio) +{ +#ifdef ARDUINO_ARCH_ESP32 + if (gpio < WLED_NUM_PINS) return (digitalPinIsValid(gpio) && !digitalPinCanOutput(gpio)); +#endif + return false; +} + +PinOwner PinManager::getPinOwner(byte gpio) +{ if (!isPinOk(gpio, false)) return PinOwner::None; return ownerTag[gpio]; } #ifdef ARDUINO_ARCH_ESP32 -#if defined(CONFIG_IDF_TARGET_ESP32C3) - #define MAX_LED_CHANNELS 6 -#else - #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) - #define MAX_LED_CHANNELS 8 - #else - #define MAX_LED_CHANNELS 16 - #endif -#endif -byte PinManagerClass::allocateLedc(byte channels) +byte PinManager::allocateLedc(byte channels) { - if (channels > MAX_LED_CHANNELS || channels == 0) return 255; - byte ca = 0; - for (byte i = 0; i < MAX_LED_CHANNELS; i++) { - byte by = i >> 3; - byte bi = i - 8*by; - if (bitRead(ledcAlloc[by], bi)) { //found occupied pin + if (channels > WLED_MAX_ANALOG_CHANNELS || channels == 0) return 255; + unsigned ca = 0; + for (unsigned i = 0; i < WLED_MAX_ANALOG_CHANNELS; i++) { + if (bitRead(ledcAlloc, i)) { //found occupied pin ca = 0; } else { - ca++; + // if we have PWM CCT bus allocation (2 channels) we need to make sure both channels share the same timer + // for phase shifting purposes (otherwise phase shifts may not be accurate) + if (channels == 2) { // will skip odd channel for first channel for phase shifting + if (ca == 0 && i % 2 == 0) ca++; // even LEDC channels is 1st PWM channel + if (ca == 1 && i % 2 == 1) ca++; // odd LEDC channel is 2nd PWM channel + } else + ca++; } if (ca >= channels) { //enough free channels - byte in = (i + 1) - ca; - for (byte j = 0; j < ca; j++) { - byte bChan = in + j; - byte byChan = bChan >> 3; - byte biChan = bChan - 8*byChan; - bitWrite(ledcAlloc[byChan], biChan, true); + unsigned in = (i + 1) - ca; + for (unsigned j = 0; j < ca; j++) { + bitWrite(ledcAlloc, in+j, true); } return in; } @@ -314,15 +293,10 @@ byte PinManagerClass::allocateLedc(byte channels) return 255; //not enough consecutive free LEDC channels } -void PinManagerClass::deallocateLedc(byte pos, byte channels) +void PinManager::deallocateLedc(byte pos, byte channels) { - for (byte j = pos; j < pos + channels; j++) { - if (j > MAX_LED_CHANNELS) return; - byte by = j >> 3; - byte bi = j - 8*by; - bitWrite(ledcAlloc[by], bi, false); + for (unsigned j = pos; j < pos + channels && j < WLED_MAX_ANALOG_CHANNELS; j++) { + bitWrite(ledcAlloc, j, false); } } #endif - -PinManagerClass pinManager = PinManagerClass(); diff --git a/wled00/pin_manager.h b/wled00/pin_manager.h index 6a50df588..c8fb165ce 100644 --- a/wled00/pin_manager.h +++ b/wled00/pin_manager.h @@ -4,8 +4,17 @@ * Registers pins so there is no attempt for two interfaces to use the same pin */ #include +#ifdef ARDUINO_ARCH_ESP32 +#include "driver/ledc.h" // needed for analog/LEDC channel counts +#endif #include "const.h" // for USERMOD_* values +#ifdef ESP8266 +#define WLED_NUM_PINS (GPIO_PIN_COUNT+1) // somehow they forgot GPIO 16 (0-16==17) +#else +#define WLED_NUM_PINS (GPIO_PIN_COUNT) +#endif + typedef struct PinManagerPinType { int8_t pin; bool isOutput; @@ -46,7 +55,6 @@ enum struct PinOwner : uint8_t { UM_RotaryEncoderUI = USERMOD_ID_ROTARY_ENC_UI, // 0x08 // Usermod "usermod_v2_rotary_encoder_ui.h" // #define USERMOD_ID_AUTO_SAVE // 0x09 // Usermod "usermod_v2_auto_save.h" -- Does not allocate pins // #define USERMOD_ID_DHT // 0x0A // Usermod "usermod_dht.h" -- Statically allocates pins, not compatible with pinManager? - // #define USERMOD_ID_MODE_SORT // 0x0B // Usermod "usermod_v2_mode_sort.h" -- Does not allocate pins // #define USERMOD_ID_VL53L0X // 0x0C // Usermod "usermod_vl53l0x_gestures.h" -- Uses "standard" HW_I2C pins UM_MultiRelay = USERMOD_ID_MULTI_RELAY, // 0x0D // Usermod "usermod_multi_relay.h" UM_AnimatedStaircase = USERMOD_ID_ANIMATED_STAIRCASE, // 0x0E // Usermod "Animated_Staircase.h" @@ -54,36 +62,21 @@ enum struct PinOwner : uint8_t { // #define USERMOD_ID_RTC // 0x0F // Usermod "usermod_rtc.h" -- Uses "standard" HW_I2C pins // #define USERMOD_ID_ELEKSTUBE_IPS // 0x10 // Usermod "usermod_elekstube_ips.h" -- Uses quite a few pins ... see Hardware.h and User_Setup.h // #define USERMOD_ID_SN_PHOTORESISTOR // 0x11 // Usermod "usermod_sn_photoresistor.h" -- Uses hard-coded pin (PHOTORESISTOR_PIN == A0), but could be easily updated to use pinManager - UM_BH1750 = USERMOD_ID_BH1750, // 0x14 // Usermod "usermod_bme280.h -- Uses "standard" HW_I2C pins + UM_BH1750 = USERMOD_ID_BH1750, // 0x14 // Usermod "bh1750.h -- Uses "standard" HW_I2C pins UM_RGBRotaryEncoder = USERMOD_RGB_ROTARY_ENCODER, // 0x16 // Usermod "rgb-rotary-encoder.h" UM_QuinLEDAnPenta = USERMOD_ID_QUINLED_AN_PENTA, // 0x17 // Usermod "quinled-an-penta.h" UM_BME280 = USERMOD_ID_BME280, // 0x1E // Usermod "usermod_bme280.h -- Uses "standard" HW_I2C pins UM_Audioreactive = USERMOD_ID_AUDIOREACTIVE, // 0x20 // Usermod "audio_reactive.h" UM_SdCard = USERMOD_ID_SD_CARD, // 0x25 // Usermod "usermod_sd_card.h" UM_PWM_OUTPUTS = USERMOD_ID_PWM_OUTPUTS, // 0x26 // Usermod "usermod_pwm_outputs.h" - UM_LDR_DUSK_DAWN = USERMOD_ID_LDR_DUSK_DAWN // 0x2B // Usermod "usermod_LDR_Dusk_Dawn_v2.h" + UM_LDR_DUSK_DAWN = USERMOD_ID_LDR_DUSK_DAWN, // 0x2B // Usermod "usermod_LDR_Dusk_Dawn_v2.h" + UM_MAX17048 = USERMOD_ID_MAX17048, // 0x2F // Usermod "usermod_max17048.h" + UM_BME68X = USERMOD_ID_BME68X, // 0x31 // Usermod "usermod_bme68x.h -- Uses "standard" HW_I2C pins + UM_PIXELS_DICE_TRAY = USERMOD_ID_PIXELS_DICE_TRAY // 0x35 // Usermod "pixels_dice_tray.h" -- Needs compile time specified 6 pins for display including SPI. }; static_assert(0u == static_cast(PinOwner::None), "PinOwner::None must be zero, so default array initialization works as expected"); -class PinManagerClass { - private: - #ifdef ESP8266 - #define WLED_NUM_PINS 17 - uint8_t pinAlloc[3] = {0x00, 0x00, 0x00}; //24bit, 1 bit per pin, we use first 17bits - PinOwner ownerTag[WLED_NUM_PINS] = { PinOwner::None }; - #else - #define WLED_NUM_PINS 50 - uint8_t pinAlloc[7] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // 56bit, 1 bit per pin, we use 50 bits on ESP32-S3 - uint8_t ledcAlloc[2] = {0x00, 0x00}; //16 LEDC channels - PinOwner ownerTag[WLED_NUM_PINS] = { PinOwner::None }; // new MCU's have up to 50 GPIO - #endif - struct { - uint8_t i2cAllocCount : 4; // allow multiple allocation of I2C bus pins but keep track of allocations - uint8_t spiAllocCount : 4; // allow multiple allocation of SPI bus pins but keep track of allocations - }; - - public: - PinManagerClass() : i2cAllocCount(0), spiAllocCount(0) {} +namespace PinManager { // De-allocates a single pin bool deallocatePin(byte gpio, PinOwner tag); // De-allocates multiple pins but only if all can be deallocated (PinOwner has to be specified) @@ -98,19 +91,17 @@ class PinManagerClass { // ethernet, etc.. bool allocateMultiplePins(const managed_pin_type * mptArray, byte arrayElementCount, PinOwner tag ); - #if !defined(ESP8266) // ESP8266 compiler doesn't understand deprecated attribute [[deprecated("Replaced by three-parameter allocatePin(gpio, output, ownerTag), for improved debugging")]] - #endif inline bool allocatePin(byte gpio, bool output = true) { return allocatePin(gpio, output, PinOwner::None); } - #if !defined(ESP8266) // ESP8266 compiler doesn't understand deprecated attribute [[deprecated("Replaced by two-parameter deallocatePin(gpio, ownerTag), for improved debugging")]] - #endif inline void deallocatePin(byte gpio) { deallocatePin(gpio, PinOwner::None); } // will return true for reserved pins bool isPinAllocated(byte gpio, PinOwner tag = PinOwner::None); // will return false for reserved pins bool isPinOk(byte gpio, bool output = true); + + bool isReadOnlyPin(byte gpio); PinOwner getPinOwner(byte gpio); @@ -120,5 +111,5 @@ class PinManagerClass { #endif }; -extern PinManagerClass pinManager; +//extern PinManager pinManager; #endif diff --git a/wled00/playlist.cpp b/wled00/playlist.cpp index bcbcb4516..36235ab9e 100644 --- a/wled00/playlist.cpp +++ b/wled00/playlist.cpp @@ -109,7 +109,10 @@ int16_t loadPlaylist(JsonObject playlistObj, byte presetId) { if (playlistRepeat > 0) playlistRepeat++; //add one extra repetition immediately since it will be deducted on first start playlistEndPreset = playlistObj["end"] | 0; // if end preset is 255 restore original preset (if any running) upon playlist end - if (playlistEndPreset == 255 && currentPreset > 0) playlistEndPreset = currentPreset; + if (playlistEndPreset == 255 && currentPreset > 0) { + playlistEndPreset = currentPreset; + playlistOptions |= PL_OPTION_RESTORE; // for async save operation + } if (playlistEndPreset > 250) playlistEndPreset = 0; shuffle = shuffle || playlistObj["r"]; if (shuffle) playlistOptions |= PL_OPTION_SHUFFLE; @@ -122,10 +125,9 @@ int16_t loadPlaylist(JsonObject playlistObj, byte presetId) { void handlePlaylist() { static unsigned long presetCycledTime = 0; - // if fileDoc is not null JSON buffer is in use so just quit - if (currentPlaylist < 0 || playlistEntries == nullptr || fileDoc != nullptr) return; + if (currentPlaylist < 0 || playlistEntries == nullptr) return; - if (millis() - presetCycledTime > (100*playlistEntryDur)) { +if (millis() - presetCycledTime > (100 * playlistEntryDur) || doAdvancePlaylist) { presetCycledTime = millis(); if (bri == 0 || nightlightActive) return; @@ -135,7 +137,7 @@ void handlePlaylist() { if (!playlistIndex) { if (playlistRepeat == 1) { //stop if all repetitions are done unloadPlaylist(); - if (playlistEndPreset) applyPreset(playlistEndPreset); + if (playlistEndPreset) applyPresetFromPlaylist(playlistEndPreset); return; } if (playlistRepeat > 1) playlistRepeat--; // decrease repeat count on each index reset if not an endless playlist @@ -146,7 +148,8 @@ void handlePlaylist() { jsonTransitionOnce = true; strip.setTransition(fadeTransition ? playlistEntries[playlistIndex].tr * 100 : 0); playlistEntryDur = playlistEntries[playlistIndex].dur; - applyPreset(playlistEntries[playlistIndex].preset); + applyPresetFromPlaylist(playlistEntries[playlistIndex].preset); + doAdvancePlaylist = false; } } @@ -157,7 +160,7 @@ void serializePlaylist(JsonObject sObj) { JsonArray dur = playlist.createNestedArray("dur"); JsonArray transition = playlist.createNestedArray(F("transition")); playlist[F("repeat")] = (playlistIndex < 0 && playlistRepeat > 0) ? playlistRepeat - 1 : playlistRepeat; // remove added repetition count (if not yet running) - playlist["end"] = playlistEndPreset; + playlist["end"] = playlistOptions & PL_OPTION_RESTORE ? 255 : playlistEndPreset; playlist["r"] = playlistOptions & PL_OPTION_SHUFFLE; for (int i=0; ito(); @@ -53,23 +53,21 @@ static void doSaveState() { #if defined(ARDUINO_ARCH_ESP32) if (!persist) { if (tmpRAMbuffer!=nullptr) free(tmpRAMbuffer); - size_t len = measureJson(*fileDoc) + 1; + size_t len = measureJson(*pDoc) + 1; DEBUG_PRINTLN(len); // if possible use SPI RAM on ESP32 - #if defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) - if (psramFound()) + if (psramSafe && psramFound()) tmpRAMbuffer = (char*) ps_malloc(len); else - #endif tmpRAMbuffer = (char*) malloc(len); if (tmpRAMbuffer!=nullptr) { - serializeJson(*fileDoc, tmpRAMbuffer, len); + serializeJson(*pDoc, tmpRAMbuffer, len); } else { - writeObjectToFileUsingId(getPresetsFileName(persist), presetToSave, fileDoc); + writeObjectToFileUsingId(getPresetsFileName(persist), presetToSave, pDoc); } } else #endif - writeObjectToFileUsingId(getPresetsFileName(persist), presetToSave, fileDoc); + writeObjectToFileUsingId(getPresetsFileName(persist), presetToSave, pDoc); if (persist) presetsModifiedTime = toki.second(); //unix time releaseJSONBufferLock(); @@ -117,10 +115,18 @@ void initPresetsFile() f.close(); } +bool applyPresetFromPlaylist(byte index) +{ + DEBUG_PRINTF_P(PSTR("Request to apply preset: %d\n"), index); + presetToApply = index; + callModeToApply = CALL_MODE_DIRECT_CHANGE; + return true; +} + bool applyPreset(byte index, byte callMode) { - DEBUG_PRINT(F("Request to apply preset: ")); - DEBUG_PRINTLN(index); + unloadPlaylist(); // applying a preset unloads the playlist (#3827) + DEBUG_PRINTF_P(PSTR("Request to apply preset: %u\n"), index); presetToApply = index; callModeToApply = callMode; return true; @@ -137,6 +143,7 @@ void applyPresetWithFallback(uint8_t index, uint8_t callMode, uint8_t effectID, void handlePresets() { + byte presetErrFlag = ERR_NONE; if (presetToSave) { strip.suspend(); doSaveState(); @@ -144,7 +151,7 @@ void handlePresets() return; } - if (presetToApply == 0 || fileDoc) return; // no preset waiting to apply, or JSON buffer is already allocated, return to loop until free + if (presetToApply == 0 || !requestJSONBufferLock(9)) return; // no preset waiting to apply, or JSON buffer is already allocated, return to loop until free bool changePreset = false; uint8_t tmpPreset = presetToApply; // store temporary since deserializeState() may call applyPreset() @@ -152,25 +159,28 @@ void handlePresets() JsonObject fdo; - // allocate buffer - if (!requestJSONBufferLock(9)) return; // will also assign fileDoc - presetToApply = 0; //clear request for preset callModeToApply = 0; - DEBUG_PRINT(F("Applying preset: ")); - DEBUG_PRINTLN(tmpPreset); + DEBUG_PRINTF_P(PSTR("Applying preset: %u\n"), (unsigned)tmpPreset); + + #if defined(ARDUINO_ARCH_ESP32S3) || defined(ARDUINO_ARCH_ESP32S2) || defined(ARDUINO_ARCH_ESP32C3) + unsigned long start = millis(); + while (strip.isUpdating() && millis() - start < FRAMETIME_FIXED) yield(); // wait for strip to finish updating, accessing FS during sendout causes glitches + #endif #ifdef ARDUINO_ARCH_ESP32 if (tmpPreset==255 && tmpRAMbuffer!=nullptr) { - deserializeJson(*fileDoc,tmpRAMbuffer); - errorFlag = ERR_NONE; + deserializeJson(*pDoc,tmpRAMbuffer); } else #endif { - errorFlag = readObjectFromFileUsingId(getPresetsFileName(tmpPreset < 255), tmpPreset, fileDoc) ? ERR_NONE : ERR_FS_PLOAD; + presetErrFlag = readObjectFromFileUsingId(getPresetsFileName(tmpPreset < 255), tmpPreset, pDoc) ? ERR_NONE : ERR_FS_PLOAD; } - fdo = fileDoc->as(); + fdo = pDoc->as(); + + // only reset errorflag if previous error was preset-related + if ((errorFlag == ERR_NONE) || (errorFlag == ERR_FS_PLOAD)) errorFlag = presetErrFlag; //HTTP API commands const char* httpwin = fdo["win"]; @@ -197,13 +207,13 @@ void handlePresets() } #endif - releaseJSONBufferLock(); // will also clear fileDoc + releaseJSONBufferLock(); if (changePreset) notify(tmpMode); // force UDP notification stateUpdated(tmpMode); // was colorUpdated() if anything breaks updateInterfaces(tmpMode); } -//called from handleSet(PS=) [network callback (fileDoc==nullptr), IR (irrational), deserializeState, UDP] and deserializeState() [network callback (filedoc!=nullptr)] +//called from handleSet(PS=) [network callback (sObj is empty), IR (irrational), deserializeState, UDP] and deserializeState() [network callback (filedoc!=nullptr)] void savePreset(byte index, const char* pname, JsonObject sObj) { if (!saveName) saveName = new char[33]; @@ -217,13 +227,20 @@ void savePreset(byte index, const char* pname, JsonObject sObj) else sprintf_P(saveName, PSTR("Preset %d"), index); } - DEBUG_PRINT(F("Saving preset (")); DEBUG_PRINT(index); DEBUG_PRINT(F(") ")); DEBUG_PRINTLN(saveName); + DEBUG_PRINTF_P(PSTR("Saving preset (%d) %s\n"), index, saveName); presetToSave = index; playlistSave = false; if (sObj[F("ql")].is()) strlcpy(quickLoad, sObj[F("ql")].as(), 9); // client limits QL to 2 chars, buffer for 8 bytes to allow unicode else quickLoad[0] = 0; + const char *bootPS = PSTR("bootps"); + if (!sObj[FPSTR(bootPS)].isNull()) { + bootPreset = sObj[FPSTR(bootPS)] | bootPreset; + sObj.remove(FPSTR(bootPS)); + doSerializeConfig = true; + } + if (sObj.size()==0 || sObj["o"].isNull()) { // no "o" means not a playlist or custom API call, saving of state is async (not immediately) includeBri = sObj["ib"].as() || sObj.size()==0 || index==255; // temporary preset needs brightness segBounds = sObj["sb"].as() || sObj.size()==0 || index==255; // temporary preset needs bounds @@ -234,7 +251,7 @@ void savePreset(byte index, const char* pname, JsonObject sObj) if (sObj[F("playlist")].isNull()) { // we will save API call immediately (often causes presets.json corruption) presetToSave = 0; - if (index <= 250 && fileDoc) { // cannot save API calls to temporary preset (255) + if (index <= 250) { // cannot save API calls to temporary preset (255) sObj.remove("o"); sObj.remove("v"); sObj.remove("time"); @@ -242,7 +259,7 @@ void savePreset(byte index, const char* pname, JsonObject sObj) sObj.remove(F("psave")); if (sObj["n"].isNull()) sObj["n"] = saveName; initPresetsFile(); // just in case if someone deleted presets.json using /edit - writeObjectToFileUsingId(getPresetsFileName(), index, fileDoc); + writeObjectToFileUsingId(getPresetsFileName(), index, pDoc); presetsModifiedTime = toki.second(); //unix time updateFSInfo(); } diff --git a/wled00/remote.cpp b/wled00/remote.cpp index 49fbc4b02..eb19cc1f3 100644 --- a/wled00/remote.cpp +++ b/wled00/remote.cpp @@ -25,11 +25,11 @@ typedef struct WizMoteMessageStructure { uint8_t program; // 0x91 for ON button, 0x81 for all others uint8_t seq[4]; // Incremetal sequence number 32 bit unsigned integer LSB first - uint8_t byte5; // Unknown (seen 0x20) + uint8_t dt1; // Button Data Type (0x32) uint8_t button; // Identifies which button is being pressed - uint8_t byte8; // Unknown, but always 0x01 - uint8_t byte9; // Unnkown, but always 0x64 - + uint8_t dt2; // Battery Level Data Type (0x01) + uint8_t batLevel; // Battery Level 0-100 + uint8_t byte10; // Unknown, maybe checksum uint8_t byte11; // Unknown, maybe checksum uint8_t byte12; // Unknown, maybe checksum @@ -68,7 +68,7 @@ static bool resetNightMode() { static void brightnessUp() { if (nightModeActive()) return; // dumb incremental search is efficient enough for so few items - for (uint8_t index = 0; index < numBrightnessSteps; ++index) { + for (unsigned index = 0; index < numBrightnessSteps; ++index) { if (brightnessSteps[index] > bri) { bri = brightnessSteps[index]; break; @@ -108,7 +108,6 @@ static void setOff() { void presetWithFallback(uint8_t presetID, uint8_t effectID, uint8_t paletteID) { resetNightMode(); - unloadPlaylist(); applyPresetWithFallback(presetID, CALL_MODE_BUTTON_PRESET, effectID, paletteID); } @@ -147,7 +146,7 @@ static bool remoteJson(int button) parsed = true; } else if (cmdStr.startsWith(F("!presetF"))) { //!presetFallback uint8_t p1 = fdo["PL"] | 1; - uint8_t p2 = fdo["FX"] | random8(strip.getModeCount() -1); + uint8_t p2 = fdo["FX"] | hw_random8(strip.getModeCount() -1); uint8_t p3 = fdo["FP"] | 0; presetWithFallback(p1, p2, p3); parsed = true; @@ -187,8 +186,7 @@ void handleRemote(uint8_t *incomingData, size_t len) { } if (len != sizeof(message_structure_t)) { - DEBUG_PRINT(F("Unknown incoming ESP Now message received of length ")); - DEBUG_PRINTLN(len); + DEBUG_PRINTF_P(PSTR("Unknown incoming ESP Now message received of length %u\n"), len); return; } @@ -226,4 +224,4 @@ void handleRemote(uint8_t *incomingData, size_t len) { #else void handleRemote(uint8_t *incomingData, size_t len) {} -#endif \ No newline at end of file +#endif diff --git a/wled00/set.cpp b/wled00/set.cpp old mode 100755 new mode 100644 index 4e2e60b3d..160eb48f0 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -77,6 +77,11 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) if (t != apChannel) forceReconnect = true; if (t > 0 && t < 14) apChannel = t; + #ifdef ARDUINO_ARCH_ESP32 + int tx = request->arg(F("TX")).toInt(); + txPower = min(max(tx, (int)WIFI_POWER_2dBm), (int)WIFI_POWER_19_5dBm); + #endif + force802_3g = request->hasArg(F("FG")); noWifiSleep = request->hasArg(F("WS")); @@ -99,30 +104,36 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) { int t = 0; - if (rlyPin>=0 && pinManager.isPinAllocated(rlyPin, PinOwner::Relay)) { - pinManager.deallocatePin(rlyPin, PinOwner::Relay); + if (rlyPin>=0 && PinManager::isPinAllocated(rlyPin, PinOwner::Relay)) { + PinManager::deallocatePin(rlyPin, PinOwner::Relay); } #ifndef WLED_DISABLE_INFRARED - if (irPin>=0 && pinManager.isPinAllocated(irPin, PinOwner::IR)) { - pinManager.deallocatePin(irPin, PinOwner::IR); + if (irPin>=0 && PinManager::isPinAllocated(irPin, PinOwner::IR)) { + deInitIR(); + PinManager::deallocatePin(irPin, PinOwner::IR); } #endif - for (uint8_t s=0; s=0 && pinManager.isPinAllocated(btnPin[s], PinOwner::Button)) { - pinManager.deallocatePin(btnPin[s], PinOwner::Button); + for (unsigned s=0; s=0 && PinManager::isPinAllocated(btnPin[s], PinOwner::Button)) { + PinManager::deallocatePin(btnPin[s], PinOwner::Button); + #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a function to check touch state, detach interrupt + if (digitalPinToTouchChannel(btnPin[s]) >= 0) // if touch capable pin + touchDetachInterrupt(btnPin[s]); // if not assigned previously, this will do nothing + #endif } } - uint8_t colorOrder, type, skip, awmode, channelSwap, maPerLed; - uint16_t length, start, maMax; + unsigned colorOrder, type, skip, awmode, channelSwap, maPerLed; + unsigned length, start, maMax; uint8_t pins[5] = {255, 255, 255, 255, 255}; - uint16_t ablMilliampsMax = request->arg(F("MA")).toInt(); + unsigned ablMilliampsMax = request->arg(F("MA")).toInt(); BusManager::setMilliampsMax(ablMilliampsMax); - autoSegments = request->hasArg(F("MS")); - correctWB = request->hasArg(F("CCT")); - cctFromRgb = request->hasArg(F("CR")); + strip.autoSegments = request->hasArg(F("MS")); + strip.correctWB = request->hasArg(F("CCT")); + strip.cctFromRgb = request->hasArg(F("CR")); + cctICused = request->hasArg(F("IC")); strip.cctBlending = request->arg(F("CB")).toInt(); Bus::setCCTBlend(strip.cctBlending); Bus::setGlobalAWMode(request->arg(F("AW")).toInt()); @@ -130,27 +141,27 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) useGlobalLedBuffer = request->hasArg(F("LD")); bool busesChanged = false; - for (uint8_t s = 0; s < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; s++) { - char lp[4] = "L0"; lp[2] = 48+s; lp[3] = 0; //ascii 0-9 //strip data pin - char lc[4] = "LC"; lc[2] = 48+s; lc[3] = 0; //strip length - char co[4] = "CO"; co[2] = 48+s; co[3] = 0; //strip color order - char lt[4] = "LT"; lt[2] = 48+s; lt[3] = 0; //strip type - char ls[4] = "LS"; ls[2] = 48+s; ls[3] = 0; //strip start LED - char cv[4] = "CV"; cv[2] = 48+s; cv[3] = 0; //strip reverse - char sl[4] = "SL"; sl[2] = 48+s; sl[3] = 0; //skip first N LEDs - char rf[4] = "RF"; rf[2] = 48+s; rf[3] = 0; //refresh required - char aw[4] = "AW"; aw[2] = 48+s; aw[3] = 0; //auto white mode - char wo[4] = "WO"; wo[2] = 48+s; wo[3] = 0; //channel swap - char sp[4] = "SP"; sp[2] = 48+s; sp[3] = 0; //bus clock speed (DotStar & PWM) - char la[4] = "LA"; la[2] = 48+s; la[3] = 0; //LED mA - char ma[4] = "MA"; ma[2] = 48+s; ma[3] = 0; //max mA + for (int s = 0; s < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; s++) { + int offset = s < 10 ? 48 : 55; + char lp[4] = "L0"; lp[2] = offset+s; lp[3] = 0; //ascii 0-9 //strip data pin + char lc[4] = "LC"; lc[2] = offset+s; lc[3] = 0; //strip length + char co[4] = "CO"; co[2] = offset+s; co[3] = 0; //strip color order + char lt[4] = "LT"; lt[2] = offset+s; lt[3] = 0; //strip type + char ls[4] = "LS"; ls[2] = offset+s; ls[3] = 0; //strip start LED + char cv[4] = "CV"; cv[2] = offset+s; cv[3] = 0; //strip reverse + char sl[4] = "SL"; sl[2] = offset+s; sl[3] = 0; //skip first N LEDs + char rf[4] = "RF"; rf[2] = offset+s; rf[3] = 0; //refresh required + char aw[4] = "AW"; aw[2] = offset+s; aw[3] = 0; //auto white mode + char wo[4] = "WO"; wo[2] = offset+s; wo[3] = 0; //channel swap + char sp[4] = "SP"; sp[2] = offset+s; sp[3] = 0; //bus clock speed (DotStar & PWM) + char la[4] = "LA"; la[2] = offset+s; la[3] = 0; //LED mA + char ma[4] = "MA"; ma[2] = offset+s; ma[3] = 0; //max mA if (!request->hasArg(lp)) { - DEBUG_PRINT(F("No data for ")); - DEBUG_PRINTLN(s); + DEBUG_PRINTF_P(PSTR("No data for %d\n"), s); break; } - for (uint8_t i = 0; i < 5; i++) { - lp[1] = 48+i; + for (int i = 0; i < 5; i++) { + lp[1] = offset+i; if (!request->hasArg(lp)) break; pins[i] = (request->arg(lp).length() > 0) ? request->arg(lp).toInt() : 255; } @@ -165,7 +176,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) } awmode = request->arg(aw).toInt(); uint16_t freq = request->arg(sp).toInt(); - if (IS_PWM(type)) { + if (Bus::isPWM(type)) { switch (freq) { case 0 : freq = WLED_PWM_FREQ/2; break; case 1 : freq = WLED_PWM_FREQ*2/3; break; @@ -174,7 +185,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) case 3 : freq = WLED_PWM_FREQ*2; break; case 4 : freq = WLED_PWM_FREQ*10/3; break; // uint16_t max (19531 * 3.333) } - } else if (IS_DIGITAL(type) && IS_2PIN(type)) { + } else if (Bus::is2Pin(type)) { switch (freq) { default: case 0 : freq = 1000; break; @@ -187,7 +198,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) freq = 0; } channelSwap = Bus::hasWhite(type) ? request->arg(wo).toInt() : 0; - if (type == TYPE_ONOFF || IS_PWM(type) || IS_VIRTUAL(type)) { // analog and virtual + if (Bus::isOnOff(type) || Bus::isPWM(type) || Bus::isVirtual(type)) { // analog and virtual maPerLed = 0; maMax = 0; } else { @@ -203,65 +214,83 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) } //doInitBusses = busesChanged; // we will do that below to ensure all input data is processed - ColorOrderMap com = {}; - for (uint8_t s = 0; s < WLED_MAX_COLOR_ORDER_MAPPINGS; s++) { - char xs[4] = "XS"; xs[2] = 48+s; xs[3] = 0; //start LED - char xc[4] = "XC"; xc[2] = 48+s; xc[3] = 0; //strip length - char xo[4] = "XO"; xo[2] = 48+s; xo[3] = 0; //color order - char xw[4] = "XW"; xw[2] = 48+s; xw[3] = 0; //W swap + // we will not bother with pre-allocating ColorOrderMappings vector + BusManager::getColorOrderMap().reset(); + for (int s = 0; s < WLED_MAX_COLOR_ORDER_MAPPINGS; s++) { + int offset = s < 10 ? 48 : 55; + char xs[4] = "XS"; xs[2] = offset+s; xs[3] = 0; //start LED + char xc[4] = "XC"; xc[2] = offset+s; xc[3] = 0; //strip length + char xo[4] = "XO"; xo[2] = offset+s; xo[3] = 0; //color order + char xw[4] = "XW"; xw[2] = offset+s; xw[3] = 0; //W swap if (request->hasArg(xs)) { start = request->arg(xs).toInt(); length = request->arg(xc).toInt(); colorOrder = request->arg(xo).toInt() & 0x0F; colorOrder |= (request->arg(xw).toInt() & 0x0F) << 4; // add W swap information - com.add(start, length, colorOrder); + if (!BusManager::getColorOrderMap().add(start, length, colorOrder)) break; } } - BusManager::updateColorOrderMap(com); // update other pins #ifndef WLED_DISABLE_INFRARED int hw_ir_pin = request->arg(F("IR")).toInt(); - if (pinManager.allocatePin(hw_ir_pin,false, PinOwner::IR)) { + if (PinManager::allocatePin(hw_ir_pin,false, PinOwner::IR)) { irPin = hw_ir_pin; } else { irPin = -1; } irEnabled = request->arg(F("IT")).toInt(); + initIR(); #endif irApplyToAllSelected = !request->hasArg(F("MSO")); int hw_rly_pin = request->arg(F("RL")).toInt(); - if (pinManager.allocatePin(hw_rly_pin,true, PinOwner::Relay)) { + if (PinManager::allocatePin(hw_rly_pin,true, PinOwner::Relay)) { rlyPin = hw_rly_pin; } else { rlyPin = -1; } rlyMde = (bool)request->hasArg(F("RM")); + rlyOpenDrain = (bool)request->hasArg(F("RO")); disablePullUp = (bool)request->hasArg(F("IP")); - for (uint8_t i=0; i10) - char be[4] = "BE"; be[2] = (i<10?48:55)+i; be[3] = 0; // button type (use A,B,C,... if WLED_MAX_BUTTONS>10) + touchThreshold = request->arg(F("TT")).toInt(); + for (int i = 0; i < WLED_MAX_BUTTONS; i++) { + int offset = i < 10 ? 48 : 55; + char bt[4] = "BT"; bt[2] = offset+i; bt[3] = 0; // button pin (use A,B,C,... if WLED_MAX_BUTTONS>10) + char be[4] = "BE"; be[2] = offset+i; be[3] = 0; // button type (use A,B,C,... if WLED_MAX_BUTTONS>10) int hw_btn_pin = request->arg(bt).toInt(); - if (hw_btn_pin >= 0 && pinManager.allocatePin(hw_btn_pin,false,PinOwner::Button)) { + if (hw_btn_pin >= 0 && PinManager::allocatePin(hw_btn_pin,false,PinOwner::Button)) { btnPin[i] = hw_btn_pin; buttonType[i] = request->arg(be).toInt(); #ifdef ARDUINO_ARCH_ESP32 // ESP32 only: check that button pin is a valid gpio - if (((buttonType[i] == BTN_TYPE_ANALOG) || (buttonType[i] == BTN_TYPE_ANALOG_INVERTED)) && (digitalPinToAnalogChannel(btnPin[i]) < 0)) + if ((buttonType[i] == BTN_TYPE_ANALOG) || (buttonType[i] == BTN_TYPE_ANALOG_INVERTED)) { - // not an ADC analog pin - DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), btnPin[i], i); - btnPin[i] = -1; - pinManager.deallocatePin(hw_btn_pin,PinOwner::Button); + if (digitalPinToAnalogChannel(btnPin[i]) < 0) { + // not an ADC analog pin + DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), btnPin[i], i); + btnPin[i] = -1; + PinManager::deallocatePin(hw_btn_pin,PinOwner::Button); + } else { + analogReadResolution(12); // see #4040 + } } - else if ((buttonType[i] == BTN_TYPE_TOUCH || buttonType[i] == BTN_TYPE_TOUCH_SWITCH) && digitalPinToTouchChannel(btnPin[i]) < 0) + else if ((buttonType[i] == BTN_TYPE_TOUCH || buttonType[i] == BTN_TYPE_TOUCH_SWITCH)) { - // not a touch pin - DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not an touch pin!\n"), btnPin[i], i); - btnPin[i] = -1; - pinManager.deallocatePin(hw_btn_pin,PinOwner::Button); + if (digitalPinToTouchChannel(btnPin[i]) < 0) + { + // not a touch pin + DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not an touch pin!\n"), btnPin[i], i); + btnPin[i] = -1; + PinManager::deallocatePin(hw_btn_pin,PinOwner::Button); + } + #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a fucntion to check touch state but need to attach an interrupt to do so + else + { + touchAttachInterrupt(btnPin[i], touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000) + } + #endif } else #endif @@ -281,7 +310,6 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) buttonType[i] = BTN_TYPE_NONE; } } - touchThreshold = request->arg(F("TT")).toInt(); briS = request->arg(F("CA")).toInt(); @@ -291,13 +319,12 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) gammaCorrectBri = request->hasArg(F("GB")); gammaCorrectCol = request->hasArg(F("GC")); gammaCorrectVal = request->arg(F("GV")).toFloat(); - if (gammaCorrectVal > 1.0f && gammaCorrectVal <= 3) - NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); - else { + if (gammaCorrectVal <= 1.0f || gammaCorrectVal > 3) { gammaCorrectVal = 1.0f; // no gamma correction gammaCorrectBri = false; gammaCorrectCol = false; } + NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); // fill look-up table fadeTransition = request->hasArg(F("TF")); modeBlending = request->hasArg(F("EB")); @@ -352,6 +379,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) receiveNotificationBrightness = request->hasArg(F("RB")); receiveNotificationColor = request->hasArg(F("RC")); receiveNotificationEffects = request->hasArg(F("RX")); + receiveNotificationPalette = request->hasArg(F("RP")); receiveSegmentOptions = request->hasArg(F("SO")); receiveSegmentBounds = request->hasArg(F("SG")); sendNotifications = request->hasArg(F("SS")); @@ -392,12 +420,14 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) t = request->arg(F("WO")).toInt(); if (t >= -255 && t <= 255) arlsOffset = t; + #ifndef WLED_DISABLE_ALEXA alexaEnabled = request->hasArg(F("AL")); strlcpy(alexaInvocationName, request->arg(F("AI")).c_str(), 33); t = request->arg(F("AP")).toInt(); if (t >= 0 && t <= 9) alexaNumPresets = t; + #endif - #ifdef WLED_ENABLE_MQTT + #ifndef WLED_DISABLE_MQTT mqttEnabled = request->hasArg(F("MQ")); strlcpy(mqttServer, request->arg(F("MS")).c_str(), MQTT_MAX_SERVER_LEN+1); t = request->arg(F("MQPORT")).toInt(); @@ -476,7 +506,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) macroAlexaOff = request->arg(F("A1")).toInt(); macroCountdown = request->arg(F("MC")).toInt(); macroNl = request->arg(F("MN")).toInt(); - for (uint8_t i=0; ihasArg(F("PIN"))) { const char *pin = request->arg(F("PIN")).c_str(); - uint8_t pinLen = strlen(pin); + unsigned pinLen = strlen(pin); if (pinLen == 4 || pinLen == 0) { - uint8_t numZeros = 0; - for (uint8_t i = 0; i < pinLen; i++) numZeros += (pin[i] == '0'); + unsigned numZeros = 0; + for (unsigned i = 0; i < pinLen; i++) numZeros += (pin[i] == '0'); if (numZeros < pinLen || pinLen == 0) { // ignore 0000 input (placeholder) strlcpy(settingsPIN, pin, 5); } @@ -601,10 +631,10 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) if (i2c_sda != hw_sda_pin || i2c_scl != hw_scl_pin) { // only if pins changed uint8_t old_i2c[2] = { static_cast(i2c_scl), static_cast(i2c_sda) }; - pinManager.deallocateMultiplePins(old_i2c, 2, PinOwner::HW_I2C); // just in case deallocation of old pins + PinManager::deallocateMultiplePins(old_i2c, 2, PinOwner::HW_I2C); // just in case deallocation of old pins PinManagerPinType i2c[2] = { { hw_sda_pin, true }, { hw_scl_pin, true } }; - if (hw_sda_pin >= 0 && hw_scl_pin >= 0 && pinManager.allocateMultiplePins(i2c, 2, PinOwner::HW_I2C)) { + if (hw_sda_pin >= 0 && hw_scl_pin >= 0 && PinManager::allocateMultiplePins(i2c, 2, PinOwner::HW_I2C)) { i2c_sda = hw_sda_pin; i2c_scl = hw_scl_pin; // no bus re-initialisation as usermods do not get any notification @@ -628,9 +658,9 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) if (spi_mosi != hw_mosi_pin || spi_miso != hw_miso_pin || spi_sclk != hw_sclk_pin) { // only if pins changed uint8_t old_spi[3] = { static_cast(spi_mosi), static_cast(spi_miso), static_cast(spi_sclk) }; - pinManager.deallocateMultiplePins(old_spi, 3, PinOwner::HW_SPI); // just in case deallocation of old pins + PinManager::deallocateMultiplePins(old_spi, 3, PinOwner::HW_SPI); // just in case deallocation of old pins PinManagerPinType spi[3] = { { hw_mosi_pin, true }, { hw_miso_pin, true }, { hw_sclk_pin, true } }; - if (hw_mosi_pin >= 0 && hw_sclk_pin >= 0 && pinManager.allocateMultiplePins(spi, 3, PinOwner::HW_SPI)) { + if (hw_mosi_pin >= 0 && hw_sclk_pin >= 0 && PinManager::allocateMultiplePins(spi, 3, PinOwner::HW_SPI)) { spi_mosi = hw_mosi_pin; spi_miso = hw_miso_pin; spi_sclk = hw_sclk_pin; @@ -653,7 +683,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) JsonObject um = pDoc->createNestedObject("um"); size_t args = request->args(); - uint16_t j=0; + unsigned j=0; for (size_t i=0; iargName(i); String value = request->arg(i); @@ -698,7 +728,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) else subObj[name].add(value.toInt()); // we may have an int j++; } - DEBUG_PRINT(F("[")); DEBUG_PRINT(j); DEBUG_PRINT(F("] = ")); DEBUG_PRINTLN(value); + DEBUG_PRINTF_P(PSTR("[%d] = %s\n"), j, value.c_str()); } else { // we are using a hidden field with the same name as our parameter (!before the actual parameter!) // to describe the type of parameter (text,float,int), for boolean parameters the first field contains "off" @@ -717,11 +747,11 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) } else if (type == "int") subObj[name] = value.toInt(); else subObj[name] = value; // text fields } - DEBUG_PRINT(F(" = ")); DEBUG_PRINTLN(value); + DEBUG_PRINTF_P(PSTR(" = %s\n"), value.c_str()); } } - usermods.readFromConfig(um); // force change of usermod parameters - DEBUG_PRINTLN(F("Done re-init usermods.")); + UsermodManager::readFromConfig(um); // force change of usermod parameters + DEBUG_PRINTLN(F("Done re-init UsermodManager::")); releaseJSONBufferLock(); } @@ -734,12 +764,12 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) if (strip.isMatrix) { strip.panels = MAX(1,MIN(WLED_MAX_PANELS,request->arg(F("MPC")).toInt())); strip.panel.reserve(strip.panels); // pre-allocate memory - for (uint8_t i=0; ihasArg(pO)) break; pO[l] = 'B'; p.bottomStart = request->arg(pO).toInt(); @@ -778,8 +808,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) if (!(req.indexOf("win") >= 0)) return false; int pos = 0; - DEBUG_PRINT(F("API req: ")); - DEBUG_PRINTLN(req); + DEBUG_PRINTF_P(PSTR("API req: %s\n"), req.c_str()); //segment select (sets main segment) pos = req.indexOf(F("SM=")); @@ -793,7 +822,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) pos = req.indexOf(F("SS=")); if (pos > 0) { - byte t = getNumVal(&req, pos); + unsigned t = getNumVal(&req, pos); if (t < strip.getSegmentsNum()) { selectedSeg = t; singleSegment = true; @@ -803,14 +832,15 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) Segment& selseg = strip.getSegment(selectedSeg); pos = req.indexOf(F("SV=")); //segment selected if (pos > 0) { - byte t = getNumVal(&req, pos); - if (t == 2) for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) strip.getSegment(i).selected = false; // unselect other segments + unsigned t = getNumVal(&req, pos); + if (t == 2) for (unsigned i = 0; i < strip.getSegmentsNum(); i++) strip.getSegment(i).selected = false; // unselect other segments selseg.selected = t; } // temporary values, write directly to segments, globals are updated by setValuesFromFirstSelectedSeg() - uint32_t col0 = selseg.colors[0]; - uint32_t col1 = selseg.colors[1]; + uint32_t col0 = selseg.colors[0]; + uint32_t col1 = selseg.colors[1]; + uint32_t col2 = selseg.colors[2]; byte colIn[4] = {R(col0), G(col0), B(col0), W(col0)}; byte colInSec[4] = {R(col1), G(col1), B(col1), W(col1)}; byte effectIn = selseg.mode; @@ -831,22 +861,23 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) uint16_t spcI = selseg.spacing; pos = req.indexOf(F("&S=")); //segment start if (pos > 0) { - startI = getNumVal(&req, pos); + startI = std::abs(getNumVal(&req, pos)); } pos = req.indexOf(F("S2=")); //segment stop if (pos > 0) { - stopI = getNumVal(&req, pos); + stopI = std::abs(getNumVal(&req, pos)); } pos = req.indexOf(F("GP=")); //segment grouping if (pos > 0) { - grpI = getNumVal(&req, pos); - if (grpI == 0) grpI = 1; + grpI = std::max(1,getNumVal(&req, pos)); } pos = req.indexOf(F("SP=")); //segment spacing if (pos > 0) { - spcI = getNumVal(&req, pos); + spcI = std::max(0,getNumVal(&req, pos)); } - strip.setSegment(selectedSeg, startI, stopI, grpI, spcI, UINT16_MAX, startY, stopY); + strip.suspend(); // must suspend strip operations before changing geometry + selseg.setGeometry(startI, stopI, grpI, spcI, UINT16_MAX, startY, stopY, selseg.map1D2D); + strip.resume(); pos = req.indexOf(F("RV=")); //Segment reverse if (pos > 0) selseg.reverse = req.charAt(pos+3) != '0'; @@ -883,14 +914,16 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) //apply preset if (updateVal(req.c_str(), "PL=", &presetCycCurr, presetCycMin, presetCycMax)) { - unloadPlaylist(); applyPreset(presetCycCurr); } + pos = req.indexOf(F("NP")); //advances to next preset in a playlist + if (pos > 0) doAdvancePlaylist = true; + //set brightness updateVal(req.c_str(), "&A=", &bri); - bool col0Changed = false, col1Changed = false; + bool col0Changed = false, col1Changed = false, col2Changed = false; //set colors col0Changed |= updateVal(req.c_str(), "&R=", &colIn[0]); col0Changed |= updateVal(req.c_str(), "&G=", &colIn[1]); @@ -947,7 +980,6 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) } //set color from HEX or 32bit DEC - byte tmpCol[4]; pos = req.indexOf(F("CL=")); if (pos > 0) { colorFromDecOrHexString(colIn, (char*)req.substring(pos + 3).c_str()); @@ -960,10 +992,11 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) } pos = req.indexOf(F("C3=")); if (pos > 0) { + byte tmpCol[4]; colorFromDecOrHexString(tmpCol, (char*)req.substring(pos + 3).c_str()); - uint32_t col2 = RGBW32(tmpCol[0], tmpCol[1], tmpCol[2], tmpCol[3]); + col2 = RGBW32(tmpCol[0], tmpCol[1], tmpCol[2], tmpCol[3]); selseg.setColor(2, col2); // defined above (SS= or main) - if (!singleSegment) strip.setColor(2, col2); // will set color to all active & selected segments + col2Changed = true; } //set to random hue SR=0->1st SR=1->2nd @@ -974,29 +1007,22 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) col0Changed |= (!sec); col1Changed |= sec; } - //swap 2nd & 1st - pos = req.indexOf(F("SC")); - if (pos > 0) { - byte temp; - for (uint8_t i=0; i<4; i++) { - temp = colIn[i]; - colIn[i] = colInSec[i]; - colInSec[i] = temp; - } - col0Changed = col1Changed = true; - } - // apply colors to selected segment, and all selected segments if applicable if (col0Changed) { - uint32_t colIn0 = RGBW32(colIn[0], colIn[1], colIn[2], colIn[3]); - selseg.setColor(0, colIn0); - if (!singleSegment) strip.setColor(0, colIn0); // will set color to all active & selected segments + col0 = RGBW32(colIn[0], colIn[1], colIn[2], colIn[3]); + selseg.setColor(0, col0); } if (col1Changed) { - uint32_t colIn1 = RGBW32(colInSec[0], colInSec[1], colInSec[2], colInSec[3]); - selseg.setColor(1, colIn1); - if (!singleSegment) strip.setColor(1, colIn1); // will set color to all active & selected segments + col1 = RGBW32(colInSec[0], colInSec[1], colInSec[2], colInSec[3]); + selseg.setColor(1, col1); + } + + //swap 2nd & 1st + pos = req.indexOf(F("SC")); + if (pos > 0) { + std::swap(col0,col1); + col0Changed = col1Changed = true; } bool fxModeChanged = false, speedChanged = false, intensityChanged = false, paletteChanged = false; @@ -1019,13 +1045,16 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) stateChanged |= (fxModeChanged || speedChanged || intensityChanged || paletteChanged || custom1Changed || custom2Changed || custom3Changed || check1Changed || check2Changed || check3Changed); // apply to main and all selected segments to prevent #1618. - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (i != selectedSeg && (singleSegment || !seg.isActive() || !seg.isSelected())) continue; // skip non main segments if not applying to all if (fxModeChanged) seg.setMode(effectIn, req.indexOf(F("FXD="))>0); // apply defaults if FXD= is specified if (speedChanged) seg.speed = speedIn; if (intensityChanged) seg.intensity = intensityIn; if (paletteChanged) seg.setPalette(paletteIn); + if (col0Changed) seg.setColor(0, col0); + if (col1Changed) seg.setColor(1, col1); + if (col2Changed) seg.setColor(2, col2); if (custom1Changed) seg.custom1 = custom1In; if (custom2Changed) seg.custom2 = custom2In; if (custom3Changed) seg.custom3 = custom3In; @@ -1161,7 +1190,11 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) // internal call, does not send XML response pos = req.indexOf(F("IN")); - if (pos < 1) XML_response(request); + if ((request != nullptr) && (pos < 1)) { + auto response = request->beginResponseStream("text/xml"); + XML_response(*response); + request->send(response); + } return true; } diff --git a/wled00/src/dependencies/dmx/SparkFunDMX.cpp b/wled00/src/dependencies/dmx/SparkFunDMX.cpp index dbc9b1590..064b9ff62 100644 --- a/wled00/src/dependencies/dmx/SparkFunDMX.cpp +++ b/wled00/src/dependencies/dmx/SparkFunDMX.cpp @@ -34,8 +34,8 @@ static const int enablePin = -1; // disable the enable pin because it is not ne static const int rxPin = -1; // disable the receiving pin because it is not needed - softhack007: Pin=-1 means "use default" not "disable" static const int txPin = 2; // transmit DMX data over this pin (default is pin 2) -//DMX value array and size. Entry 0 will hold startbyte -static uint8_t dmxData[dmxMaxChannel] = { 0 }; +//DMX value array and size. Entry 0 will hold startbyte, so we need 512+1 elements +static uint8_t dmxData[dmxMaxChannel+1] = { 0 }; static int chanSize = 0; #if !defined(DMX_SEND_ONLY) static int currentChannel = 0; diff --git a/wled00/src/dependencies/espalexa/Espalexa.h b/wled00/src/dependencies/espalexa/Espalexa.h index 5c780e248..ae761e9fa 100644 --- a/wled00/src/dependencies/espalexa/Espalexa.h +++ b/wled00/src/dependencies/espalexa/Espalexa.h @@ -120,10 +120,8 @@ private: void encodeLightId(uint8_t idx, char* out) { - uint8_t mac[6]; - WiFi.macAddress(mac); - - sprintf_P(out, PSTR("%02X:%02X:%02X:%02X:%02X:%02X:00:11-%02X"), mac[0],mac[1],mac[2],mac[3],mac[4],mac[5], idx); + String mymac = WiFi.macAddress(); + sprintf_P(out, PSTR("%02X:%s:AB-%02X"), idx, mymac.c_str(), idx); } // construct 'globally unique' Json dict key fitting into signed int diff --git a/wled00/src/dependencies/json/AsyncJson-v6.h b/wled00/src/dependencies/json/AsyncJson-v6.h index 32ac54607..4a127dedb 100644 --- a/wled00/src/dependencies/json/AsyncJson-v6.h +++ b/wled00/src/dependencies/json/AsyncJson-v6.h @@ -21,8 +21,6 @@ #define DYNAMIC_JSON_DOCUMENT_SIZE 16384 #endif -constexpr const char* JSON_MIMETYPE = "application/json"; - /* * Json Response * */ @@ -66,7 +64,7 @@ class AsyncJsonResponse: public AsyncAbstractResponse { AsyncJsonResponse(JsonDocument *ref, bool isArray=false) : _jsonBuffer(1), _isValid{false} { _code = 200; - _contentType = JSON_MIMETYPE; + _contentType = FPSTR(CONTENT_TYPE_JSON); if(isArray) _root = ref->to(); else @@ -75,7 +73,7 @@ class AsyncJsonResponse: public AsyncAbstractResponse { AsyncJsonResponse(size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE, bool isArray=false) : _jsonBuffer(maxJsonBufferSize), _isValid{false} { _code = 200; - _contentType = JSON_MIMETYPE; + _contentType = FPSTR(CONTENT_TYPE_JSON); if(isArray) _root = _jsonBuffer.createNestedArray(); else diff --git a/wled00/src/dependencies/toki/Toki.h b/wled00/src/dependencies/toki/Toki.h index e7f12a453..0e849d3c2 100644 --- a/wled00/src/dependencies/toki/Toki.h +++ b/wled00/src/dependencies/toki/Toki.h @@ -155,7 +155,7 @@ class Toki { return (tick == TickT::active); } - void printTime(const Time& t) { - Serial.printf_P(PSTR("%u,%03u\n"),t.sec,t.ms); + void printTime(const Time& t, Print &dest = Serial) { + dest.printf_P(PSTR("%u,%03u\n"),t.sec,t.ms); } }; \ No newline at end of file diff --git a/wled00/udp.cpp b/wled00/udp.cpp index 100ace166..47398bc8a 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -106,7 +106,7 @@ void notify(byte callMode, bool followUp) for (size_t i = 0; i < nsegs; i++) { Segment &selseg = strip.getSegment(i); if (!selseg.isActive()) continue; - uint16_t ofs = 41 + s*UDP_SEG_SIZE; //start of segment offset byte + unsigned ofs = 41 + s*UDP_SEG_SIZE; //start of segment offset byte udpOut[0 +ofs] = s; udpOut[1 +ofs] = selseg.start >> 8; udpOut[2 +ofs] = selseg.start & 0xFF; @@ -213,7 +213,7 @@ void parseNotifyPacket(uint8_t *udpIn) { //compatibilityVersionByte: byte version = udpIn[11]; - DEBUG_PRINT(F("UDP packet version: ")); DEBUG_PRINTLN(version); + DEBUG_PRINTF_P(PSTR("UDP packet version: %d\n"), (int)version); // if we are not part of any sync group ignore message if (version < 9) { @@ -221,7 +221,7 @@ void parseNotifyPacket(uint8_t *udpIn) { if (!(receiveGroups & 0x01)) return; } else if (!(receiveGroups & udpIn[36])) return; - bool someSel = (receiveNotificationBrightness || receiveNotificationColor || receiveNotificationEffects); + bool someSel = (receiveNotificationBrightness || receiveNotificationColor || receiveNotificationEffects || receiveNotificationPalette); // set transition time before making any segment changes if (version > 3) { @@ -234,14 +234,14 @@ void parseNotifyPacket(uint8_t *udpIn) { //apply colors from notification to main segment, only if not syncing full segments if ((receiveNotificationColor || !someSel) && (version < 11 || !receiveSegmentOptions)) { // primary color, only apply white if intented (version > 0) - strip.setColor(0, RGBW32(udpIn[3], udpIn[4], udpIn[5], (version > 0) ? udpIn[10] : 0)); + strip.getMainSegment().setColor(0, RGBW32(udpIn[3], udpIn[4], udpIn[5], (version > 0) ? udpIn[10] : 0)); if (version > 1) { - strip.setColor(1, RGBW32(udpIn[12], udpIn[13], udpIn[14], udpIn[15])); // secondary color + strip.getMainSegment().setColor(1, RGBW32(udpIn[12], udpIn[13], udpIn[14], udpIn[15])); // secondary color } if (version > 6) { - strip.setColor(2, RGBW32(udpIn[20], udpIn[21], udpIn[22], udpIn[23])); // tertiary color + strip.getMainSegment().setColor(2, RGBW32(udpIn[20], udpIn[21], udpIn[22], udpIn[23])); // tertiary color if (version > 9 && udpIn[37] < 255) { // valid CCT/Kelvin value - uint16_t cct = udpIn[38]; + unsigned cct = udpIn[38]; if (udpIn[37] > 0) { //Kelvin cct |= (udpIn[37] << 8); } @@ -255,28 +255,29 @@ void parseNotifyPacket(uint8_t *udpIn) { bool applyEffects = (receiveNotificationEffects || !someSel); if (applyEffects && currentPlaylist >= 0) unloadPlaylist(); if (version > 10 && (receiveSegmentOptions || receiveSegmentBounds)) { - uint8_t numSrcSegs = udpIn[39]; - DEBUG_PRINT(F("UDP segments: ")); DEBUG_PRINTLN(numSrcSegs); + unsigned numSrcSegs = udpIn[39]; + DEBUG_PRINTF_P(PSTR("UDP segments: %d\n"), numSrcSegs); // are we syncing bounds and slave has more active segments than master? if (receiveSegmentBounds && numSrcSegs < strip.getActiveSegmentsNum()) { DEBUG_PRINTLN(F("Removing excessive segments.")); - for (size_t i=strip.getSegmentsNum(); i>numSrcSegs; i--) { - if (strip.getSegment(i).isActive()) { - strip.setSegment(i-1,0,0); // delete segment - } + strip.suspend(); //should not be needed as UDP handling is not done in ISR callbacks but still added "just in case" + for (size_t i=strip.getSegmentsNum(); i>numSrcSegs && i>0; i--) { + Segment &seg = strip.getSegment(i-1); + if (seg.isActive()) seg.deactivate(); // delete segment } + strip.resume(); } size_t inactiveSegs = 0; for (size_t i = 0; i < numSrcSegs && i < strip.getMaxSegments(); i++) { - uint16_t ofs = 41 + i*udpIn[40]; //start of segment offset byte - uint8_t id = udpIn[0 +ofs]; - DEBUG_PRINT(F("UDP segment received: ")); DEBUG_PRINTLN(id); + unsigned ofs = 41 + i*udpIn[40]; //start of segment offset byte + unsigned id = udpIn[0 +ofs]; + DEBUG_PRINTF_P(PSTR("UDP segment received: %u\n"), id); if (id > strip.getSegmentsNum()) break; else if (id == strip.getSegmentsNum()) { if (receiveSegmentBounds && id < strip.getMaxSegments()) strip.appendSegment(); else break; } - DEBUG_PRINT(F("UDP segment check: ")); DEBUG_PRINTLN(id); + DEBUG_PRINTF_P(PSTR("UDP segment check: %u\n"), id); Segment& selseg = strip.getSegment(id); // if we are not syncing bounds skip unselected segments if (selseg.isActive() && !(selseg.isSelected() || receiveSegmentBounds)) continue; @@ -290,7 +291,7 @@ void parseNotifyPacket(uint8_t *udpIn) { id += inactiveSegs; // adjust id } } - DEBUG_PRINT(F("UDP segment processing: ")); DEBUG_PRINTLN(id); + DEBUG_PRINTF_P(PSTR("UDP segment processing: %u\n"), id); uint16_t start = (udpIn[1+ofs] << 8 | udpIn[2+ofs]); uint16_t stop = (udpIn[3+ofs] << 8 | udpIn[4+ofs]); @@ -300,21 +301,24 @@ void parseNotifyPacket(uint8_t *udpIn) { if (!receiveSegmentOptions) { DEBUG_PRINTF_P(PSTR("Set segment w/o options: %d [%d,%d;%d,%d]\n"), id, (int)start, (int)stop, (int)startY, (int)stopY); strip.suspend(); //should not be needed as UDP handling is not done in ISR callbacks but still added "just in case" - selseg.setUp(start, stop, selseg.grouping, selseg.spacing, offset, startY, stopY); + selseg.setGeometry(start, stop, selseg.grouping, selseg.spacing, offset, startY, stopY, selseg.map1D2D); strip.resume(); continue; // we do receive bounds, but not options } selseg.options = (selseg.options & 0x0071U) | (udpIn[9 +ofs] & 0x0E); // ignore selected, freeze, reset & transitional selseg.setOpacity(udpIn[10+ofs]); if (applyEffects) { - DEBUG_PRINT(F("Apply effect: ")); DEBUG_PRINTLN(id); + DEBUG_PRINTF_P(PSTR("Apply effect: %u\n"), id); selseg.setMode(udpIn[11+ofs]); selseg.speed = udpIn[12+ofs]; selseg.intensity = udpIn[13+ofs]; + } + if (receiveNotificationPalette || !someSel) { + DEBUG_PRINTF_P(PSTR("Apply palette: %u\n"), id); selseg.palette = udpIn[14+ofs]; } if (receiveNotificationColor || !someSel) { - DEBUG_PRINT(F("Apply color: ")); DEBUG_PRINTLN(id); + DEBUG_PRINTF_P(PSTR("Apply color: %u\n"), id); selseg.setColor(0, RGBW32(udpIn[15+ofs],udpIn[16+ofs],udpIn[17+ofs],udpIn[18+ofs])); selseg.setColor(1, RGBW32(udpIn[19+ofs],udpIn[20+ofs],udpIn[21+ofs],udpIn[22+ofs])); selseg.setColor(2, RGBW32(udpIn[23+ofs],udpIn[24+ofs],udpIn[25+ofs],udpIn[26+ofs])); @@ -324,10 +328,10 @@ void parseNotifyPacket(uint8_t *udpIn) { // when applying synced options ignore selected as it may be used as indicator of which segments to sync // freeze, reset should never be synced // LSB to MSB: select, reverse, on, mirror, freeze, reset, reverse_y, mirror_y, transpose, map1d2d (3), ssim (2), set (2) - DEBUG_PRINT(F("Apply options: ")); DEBUG_PRINTLN(id); + DEBUG_PRINTF_P(PSTR("Apply options: %u\n"), id); selseg.options = (selseg.options & 0b0000000000110001U) | (udpIn[28+ofs]<<8) | (udpIn[9 +ofs] & 0b11001110U); // ignore selected, freeze, reset if (applyEffects) { - DEBUG_PRINT(F("Apply sliders: ")); DEBUG_PRINTLN(id); + DEBUG_PRINTF_P(PSTR("Apply sliders: %u\n"), id); selseg.custom1 = udpIn[29+ofs]; selseg.custom2 = udpIn[30+ofs]; selseg.custom3 = udpIn[31+ofs] & 0x1F; @@ -339,12 +343,12 @@ void parseNotifyPacket(uint8_t *udpIn) { if (receiveSegmentBounds) { DEBUG_PRINTF_P(PSTR("Set segment w/ options: %d [%d,%d;%d,%d]\n"), id, (int)start, (int)stop, (int)startY, (int)stopY); strip.suspend(); //should not be needed as UDP handling is not done in ISR callbacks but still added "just in case" - selseg.setUp(start, stop, udpIn[5+ofs], udpIn[6+ofs], offset, startY, stopY); + selseg.setGeometry(start, stop, udpIn[5+ofs], udpIn[6+ofs], offset, startY, stopY, selseg.map1D2D); strip.resume(); } else { DEBUG_PRINTF_P(PSTR("Set segment grouping: %d [%d,%d]\n"), id, (int)udpIn[5+ofs], (int)udpIn[6+ofs]); strip.suspend(); //should not be needed as UDP handling is not done in ISR callbacks but still added "just in case" - selseg.setUp(selseg.start, selseg.stop, udpIn[5+ofs], udpIn[6+ofs], selseg.offset, selseg.startY, selseg.stopY); + selseg.setGeometry(selseg.start, selseg.stop, udpIn[5+ofs], udpIn[6+ofs], selseg.offset, selseg.startY, selseg.stopY, selseg.map1D2D); strip.resume(); } } @@ -352,14 +356,16 @@ void parseNotifyPacket(uint8_t *udpIn) { } // simple effect sync, applies to all selected segments - if (applyEffects && (version < 11 || !receiveSegmentOptions)) { + if ((applyEffects || receiveNotificationPalette) && (version < 11 || !receiveSegmentOptions)) { for (size_t i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; - seg.setMode(udpIn[8]); - seg.speed = udpIn[9]; - if (version > 2) seg.intensity = udpIn[16]; - if (version > 4) seg.setPalette(udpIn[19]); + if (applyEffects) { + seg.setMode(udpIn[8]); + seg.speed = udpIn[9]; + if (version > 2) seg.intensity = udpIn[16]; + } + if (version > 4 && receiveNotificationPalette) seg.setPalette(udpIn[19]); } stateChanged = true; } @@ -405,24 +411,24 @@ void parseNotifyPacket(uint8_t *udpIn) { void realtimeLock(uint32_t timeoutMs, byte md) { if (!realtimeMode && !realtimeOverride) { - uint16_t stop, start; + unsigned stop, start; if (useMainSegmentOnly) { Segment& mainseg = strip.getMainSegment(); start = mainseg.start; stop = mainseg.stop; mainseg.freeze = true; + // if WLED was off and using main segment only, freeze non-main segments so they stay off + if (bri == 0) { + for (size_t s = 0; s < strip.getSegmentsNum(); s++) { + strip.getSegment(s).freeze = true; + } + } } else { start = 0; stop = strip.getLengthTotal(); } // clear strip/segment for (size_t i = start; i < stop; i++) strip.setPixelColor(i,BLACK); - // if WLED was off and using main segment only, freeze non-main segments so they stay off - if (useMainSegmentOnly && bri == 0) { - for (size_t s=0; s < strip.getSegmentsNum(); s++) { - strip.getSegment(s).freeze = true; - } - } } // if strip is off (bri==0) and not already in RTM if (briT == 0 && !realtimeMode && !realtimeOverride) { @@ -505,12 +511,10 @@ void handleNotifications() rgbUdp.read(lbuf, packetSize); realtimeLock(realtimeTimeoutMs, REALTIME_MODE_HYPERION); if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; - uint16_t id = 0; - uint16_t totalLen = strip.getLengthTotal(); - for (size_t i = 0; i < packetSize -2; i += 3) - { + unsigned totalLen = strip.getLengthTotal(); + if (useMainSegmentOnly) strip.getMainSegment().beginDraw(); // set up parameters for get/setPixelColor() + for (size_t i = 0, id = 0; i < packetSize -2 && id < totalLen; i += 3, id++) { setRealtimePixel(id, lbuf[i], lbuf[i+1], lbuf[i+2], 0); - id++; if (id >= totalLen) break; } if (!(realtimeMode && useMainSegmentOnly)) strip.show(); return; @@ -523,7 +527,7 @@ void handleNotifications() if (!isSupp && notifierUdp.remoteIP() == localIP) return; //don't process broadcasts we send ourselves uint8_t udpIn[packetSize +1]; - uint16_t len; + unsigned len; if (isSupp) len = notifier2Udp.read(udpIn, packetSize); else len = notifierUdp.read(udpIn, packetSize); @@ -531,7 +535,7 @@ void handleNotifications() if (isSupp && udpIn[0] == 255 && udpIn[1] == 1 && len >= 40) { if (!nodeListEnabled || notifier2Udp.remoteIP() == localIP) return; - uint8_t unit = udpIn[39]; + unsigned unit = udpIn[39]; NodesMap::iterator it = Nodes.find(unit); if (it == Nodes.end() && Nodes.size() < WLED_MAX_NODES) { // Create a new element when not present Nodes[unit].age = 0; @@ -561,7 +565,7 @@ void handleNotifications() //wled notifier, ignore if realtime packets active if (udpIn[0] == 0 && !realtimeMode && receiveGroups) { - DEBUG_PRINT(F("UDP notification from: ")); DEBUG_PRINTLN(notifierUdp.remoteIP()); + DEBUG_PRINTF_P(PSTR("UDP notification from: %d.%d.%d.%d\n"), notifierUdp.remoteIP()[0], notifierUdp.remoteIP()[1], notifierUdp.remoteIP()[2], notifierUdp.remoteIP()[3]); parseNotifyPacket(udpIn); return; } @@ -588,19 +592,13 @@ void handleNotifications() byte packetNum = udpIn[4]; //starts with 1! byte numPackets = udpIn[5]; - uint16_t id = (tpmPayloadFrameSize/3)*(packetNum-1); //start LED - uint16_t totalLen = strip.getLengthTotal(); - for (size_t i = 6; i < tpmPayloadFrameSize + 4U; i += 3) - { - if (id < totalLen) - { - setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0); - id++; - } - else break; + unsigned id = (tpmPayloadFrameSize/3)*(packetNum-1); //start LED + unsigned totalLen = strip.getLengthTotal(); + if (useMainSegmentOnly) strip.getMainSegment().beginDraw(); // set up parameters for get/setPixelColor() + for (size_t i = 6; i < tpmPayloadFrameSize + 4U && id < totalLen; i += 3, id++) { + setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0); } - if (tpmPacketCount == numPackets) //reset packet count and show if all packets were received - { + if (tpmPacketCount == numPackets) { //reset packet count and show if all packets were received tpmPacketCount = 0; strip.show(); } @@ -623,7 +621,8 @@ void handleNotifications() } if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; - uint16_t totalLen = strip.getLengthTotal(); + unsigned totalLen = strip.getLengthTotal(); + if (useMainSegmentOnly) strip.getMainSegment().beginDraw(); // set up parameters for get/setPixelColor() if (udpIn[0] == 1 && packetSize > 5) //warls { for (size_t i = 2; i < packetSize -3; i += 4) @@ -632,39 +631,29 @@ void handleNotifications() } } else if (udpIn[0] == 2 && packetSize > 4) //drgb { - uint16_t id = 0; - for (size_t i = 2; i < packetSize -2; i += 3) + for (size_t i = 2, id = 0; i < packetSize -2 && id < totalLen; i += 3, id++) { setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0); - - id++; if (id >= totalLen) break; } } else if (udpIn[0] == 3 && packetSize > 6) //drgbw { - uint16_t id = 0; - for (size_t i = 2; i < packetSize -3; i += 4) + for (size_t i = 2, id = 0; i < packetSize -3 && id < totalLen; i += 4, id++) { setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3]); - - id++; if (id >= totalLen) break; } } else if (udpIn[0] == 4 && packetSize > 7) //dnrgb { - uint16_t id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00); - for (size_t i = 4; i < packetSize -2; i += 3) + unsigned id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00); + for (size_t i = 4; i < packetSize -2 && id < totalLen; i += 3, id++) { - if (id >= totalLen) break; setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0); - id++; } } else if (udpIn[0] == 5 && packetSize > 8) //dnrgbw { - uint16_t id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00); - for (size_t i = 4; i < packetSize -2; i += 4) + unsigned id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00); + for (size_t i = 4; i < packetSize -2 && id < totalLen; i += 4, id++) { - if (id >= totalLen) break; setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3]); - id++; } } strip.show(); @@ -691,7 +680,7 @@ void handleNotifications() void setRealtimePixel(uint16_t i, byte r, byte g, byte b, byte w) { - uint16_t pix = i + arlsOffset; + unsigned pix = i + arlsOffset; if (pix < strip.getLengthTotal()) { if (!arlsDisableGammaCorrection && gammaCorrectCol) { r = gamma8(r); @@ -699,11 +688,11 @@ void setRealtimePixel(uint16_t i, byte r, byte g, byte b, byte w) b = gamma8(b); w = gamma8(w); } + uint32_t col = RGBW32(r,g,b,w); if (useMainSegmentOnly) { - Segment &seg = strip.getMainSegment(); - if (pixsetup(); } -void UsermodManager::connected() { for (byte i = 0; i < numMods; i++) ums[i]->connected(); } -void UsermodManager::loop() { for (byte i = 0; i < numMods; i++) ums[i]->loop(); } -void UsermodManager::handleOverlayDraw() { for (byte i = 0; i < numMods; i++) ums[i]->handleOverlayDraw(); } -void UsermodManager::appendConfigData() { for (byte i = 0; i < numMods; i++) ums[i]->appendConfigData(); } +void UsermodManager::setup() { for (unsigned i = 0; i < numMods; i++) ums[i]->setup(); } +void UsermodManager::connected() { for (unsigned i = 0; i < numMods; i++) ums[i]->connected(); } +void UsermodManager::loop() { for (unsigned i = 0; i < numMods; i++) ums[i]->loop(); } +void UsermodManager::handleOverlayDraw() { for (unsigned i = 0; i < numMods; i++) ums[i]->handleOverlayDraw(); } +void UsermodManager::appendConfigData(Print& dest) { for (unsigned i = 0; i < numMods; i++) ums[i]->appendConfigData(dest); } bool UsermodManager::handleButton(uint8_t b) { bool overrideIO = false; - for (byte i = 0; i < numMods; i++) { + for (unsigned i = 0; i < numMods; i++) { if (ums[i]->handleButton(b)) overrideIO = true; } return overrideIO; } bool UsermodManager::getUMData(um_data_t **data, uint8_t mod_id) { - for (byte i = 0; i < numMods; i++) { + for (unsigned i = 0; i < numMods; i++) { if (mod_id > 0 && ums[i]->getId() != mod_id) continue; // only get data form requested usermod if provided if (ums[i]->getUMData(data)) return true; // if usermod does provide data return immediately (only one usermod can provide data at one time) } return false; } -void UsermodManager::addToJsonState(JsonObject& obj) { for (byte i = 0; i < numMods; i++) ums[i]->addToJsonState(obj); } -void UsermodManager::addToJsonInfo(JsonObject& obj) { for (byte i = 0; i < numMods; i++) ums[i]->addToJsonInfo(obj); } -void UsermodManager::readFromJsonState(JsonObject& obj) { for (byte i = 0; i < numMods; i++) ums[i]->readFromJsonState(obj); } -void UsermodManager::addToConfig(JsonObject& obj) { for (byte i = 0; i < numMods; i++) ums[i]->addToConfig(obj); } +void UsermodManager::addToJsonState(JsonObject& obj) { for (unsigned i = 0; i < numMods; i++) ums[i]->addToJsonState(obj); } +void UsermodManager::addToJsonInfo(JsonObject& obj) { for (unsigned i = 0; i < numMods; i++) ums[i]->addToJsonInfo(obj); } +void UsermodManager::readFromJsonState(JsonObject& obj) { for (unsigned i = 0; i < numMods; i++) ums[i]->readFromJsonState(obj); } +void UsermodManager::addToConfig(JsonObject& obj) { for (unsigned i = 0; i < numMods; i++) ums[i]->addToConfig(obj); } bool UsermodManager::readFromConfig(JsonObject& obj) { bool allComplete = true; - for (byte i = 0; i < numMods; i++) { + for (unsigned i = 0; i < numMods; i++) { if (!ums[i]->readFromConfig(obj)) allComplete = false; } return allComplete; } -void UsermodManager::onMqttConnect(bool sessionPresent) { for (byte i = 0; i < numMods; i++) ums[i]->onMqttConnect(sessionPresent); } +#ifndef WLED_DISABLE_MQTT +void UsermodManager::onMqttConnect(bool sessionPresent) { for (unsigned i = 0; i < numMods; i++) ums[i]->onMqttConnect(sessionPresent); } bool UsermodManager::onMqttMessage(char* topic, char* payload) { - for (byte i = 0; i < numMods; i++) if (ums[i]->onMqttMessage(topic, payload)) return true; + for (unsigned i = 0; i < numMods; i++) if (ums[i]->onMqttMessage(topic, payload)) return true; return false; } -void UsermodManager::onUpdateBegin(bool init) { for (byte i = 0; i < numMods; i++) ums[i]->onUpdateBegin(init); } // notify usermods that update is to begin -void UsermodManager::onStateChange(uint8_t mode) { for (byte i = 0; i < numMods; i++) ums[i]->onStateChange(mode); } // notify usermods that WLED state changed +#endif +#ifndef WLED_DISABLE_ESPNOW +bool UsermodManager::onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len) { + for (unsigned i = 0; i < numMods; i++) if (ums[i]->onEspNowMessage(sender, payload, len)) return true; + return false; +} +#endif +void UsermodManager::onUpdateBegin(bool init) { for (unsigned i = 0; i < numMods; i++) ums[i]->onUpdateBegin(init); } // notify usermods that update is to begin +void UsermodManager::onStateChange(uint8_t mode) { for (unsigned i = 0; i < numMods; i++) ums[i]->onStateChange(mode); } // notify usermods that WLED state changed /* * Enables usermods to lookup another Usermod. */ Usermod* UsermodManager::lookup(uint16_t mod_id) { - for (byte i = 0; i < numMods; i++) { + for (unsigned i = 0; i < numMods; i++) { if (ums[i]->getId() == mod_id) { return ums[i]; } @@ -60,3 +71,14 @@ bool UsermodManager::add(Usermod* um) ums[numMods++] = um; return true; } + + +/* Usermod v2 interface shim for oappend */ +Print* Usermod::oappend_shim = nullptr; + +void Usermod::appendConfigData(Print& settingsScript) { + assert(!oappend_shim); + oappend_shim = &settingsScript; + this->appendConfigData(); + oappend_shim = nullptr; +} diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index bcfb41fa3..627fa6308 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -45,7 +45,7 @@ #endif #ifdef USERMOD_BH1750 - #include "../usermods/BH1750_v2/usermod_BH1750.h" + #include "../usermods/BH1750_v2/usermod_bh1750.h" #endif // BME280 v2 usermod. Define "USERMOD_BME280" in my_config.h @@ -53,6 +53,11 @@ #include "../usermods/BME280_v2/usermod_bme280.h" #endif +#ifdef USERMOD_BME68X + #include "../usermods/BME68X_v2/usermod_bme68x.h" +#endif + + #ifdef USERMOD_FOUR_LINE_DISPLAY #include "../usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h" #endif @@ -101,6 +106,10 @@ #include "../usermods/ST7789_display/ST7789_Display.h" #endif +#ifdef USERMOD_PIXELS_DICE_TRAY + #include "../usermods/pixels_dice_tray/pixels_dice_tray.h" +#endif + #ifdef USERMOD_SEVEN_SEGMENT #include "../usermods/seven_segment_display/usermod_v2_seven_segment_display.h" #endif @@ -205,10 +214,39 @@ #include "../usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h" #endif +#ifdef USERMOD_POV_DISPLAY + #include "../usermods/pov_display/usermod_pov_display.h" +#endif + #ifdef USERMOD_STAIRCASE_WIPE #include "../usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h" #endif +#ifdef USERMOD_MAX17048 + #include "../usermods/MAX17048_v2/usermod_max17048.h" +#endif + +#ifdef USERMOD_TETRISAI + #include "../usermods/TetrisAI_v2/usermod_v2_tetrisai.h" +#endif + +#ifdef USERMOD_AHT10 + #include "../usermods/AHT10_v2/usermod_aht10.h" +#endif + +#ifdef USERMOD_INA226 + #include "../usermods/INA226_v2/usermod_ina226.h" +#endif + +#ifdef USERMOD_LD2410 +#include "../usermods/LD2410_v2/usermod_ld2410.h" +#endif + + +#ifdef USERMOD_DEEP_SLEEP + #include "../usermods/deep_sleep/usermod_deep_sleep.h" +#endif + void registerUsermods() { /* @@ -216,193 +254,229 @@ void registerUsermods() * || || || * \/ \/ \/ */ - //usermods.add(new MyExampleUsermod()); + //UsermodManager::add(new MyExampleUsermod()); #ifdef USERMOD_BATTERY - usermods.add(new UsermodBattery()); + UsermodManager::add(new UsermodBattery()); #endif #ifdef USERMOD_DALLASTEMPERATURE - usermods.add(new UsermodTemperature()); + UsermodManager::add(new UsermodTemperature()); #endif #ifdef USERMOD_SN_PHOTORESISTOR - usermods.add(new Usermod_SN_Photoresistor()); + UsermodManager::add(new Usermod_SN_Photoresistor()); #endif #ifdef USERMOD_PWM_FAN - usermods.add(new PWMFanUsermod()); + UsermodManager::add(new PWMFanUsermod()); #endif #ifdef USERMOD_BUZZER - usermods.add(new BuzzerUsermod()); + UsermodManager::add(new BuzzerUsermod()); #endif #ifdef USERMOD_BH1750 - usermods.add(new Usermod_BH1750()); + UsermodManager::add(new Usermod_BH1750()); #endif #ifdef USERMOD_BME280 - usermods.add(new UsermodBME280()); + UsermodManager::add(new UsermodBME280()); + #endif + + #ifdef USERMOD_BME68X + UsermodManager::add(new UsermodBME68X()); #endif #ifdef USERMOD_SENSORSTOMQTT - usermods.add(new UserMod_SensorsToMQTT()); + UsermodManager::add(new UserMod_SensorsToMQTT()); #endif #ifdef USERMOD_PIRSWITCH - usermods.add(new PIRsensorSwitch()); + UsermodManager::add(new PIRsensorSwitch()); #endif #ifdef USERMOD_FOUR_LINE_DISPLAY - usermods.add(new FourLineDisplayUsermod()); + UsermodManager::add(new FourLineDisplayUsermod()); #endif #ifdef USERMOD_ROTARY_ENCODER_UI - usermods.add(new RotaryEncoderUIUsermod()); // can use USERMOD_FOUR_LINE_DISPLAY + UsermodManager::add(new RotaryEncoderUIUsermod()); // can use USERMOD_FOUR_LINE_DISPLAY #endif #ifdef USERMOD_AUTO_SAVE - usermods.add(new AutoSaveUsermod()); // can use USERMOD_FOUR_LINE_DISPLAY + UsermodManager::add(new AutoSaveUsermod()); // can use USERMOD_FOUR_LINE_DISPLAY #endif #ifdef USERMOD_DHT - usermods.add(new UsermodDHT()); + UsermodManager::add(new UsermodDHT()); #endif #ifdef USERMOD_VL53L0X_GESTURES - usermods.add(new UsermodVL53L0XGestures()); + UsermodManager::add(new UsermodVL53L0XGestures()); #endif #ifdef USERMOD_ANIMATED_STAIRCASE - usermods.add(new Animated_Staircase()); + UsermodManager::add(new Animated_Staircase()); #endif #ifdef USERMOD_MULTI_RELAY - usermods.add(new MultiRelay()); + UsermodManager::add(new MultiRelay()); #endif #ifdef USERMOD_RTC - usermods.add(new RTCUsermod()); + UsermodManager::add(new RTCUsermod()); #endif #ifdef USERMOD_ELEKSTUBE_IPS - usermods.add(new ElekstubeIPSUsermod()); + UsermodManager::add(new ElekstubeIPSUsermod()); #endif #ifdef USERMOD_ROTARY_ENCODER_BRIGHTNESS_COLOR - usermods.add(new RotaryEncoderBrightnessColor()); + UsermodManager::add(new RotaryEncoderBrightnessColor()); #endif #ifdef RGB_ROTARY_ENCODER - usermods.add(new RgbRotaryEncoderUsermod()); + UsermodManager::add(new RgbRotaryEncoderUsermod()); #endif #ifdef USERMOD_ST7789_DISPLAY - usermods.add(new St7789DisplayUsermod()); + UsermodManager::add(new St7789DisplayUsermod()); + #endif + + #ifdef USERMOD_PIXELS_DICE_TRAY + UsermodManager::add(new PixelsDiceTrayUsermod()); #endif #ifdef USERMOD_SEVEN_SEGMENT - usermods.add(new SevenSegmentDisplay()); + UsermodManager::add(new SevenSegmentDisplay()); #endif #ifdef USERMOD_SSDR - usermods.add(new UsermodSSDR()); + UsermodManager::add(new UsermodSSDR()); #endif #ifdef USERMOD_CRONIXIE - usermods.add(new UsermodCronixie()); + UsermodManager::add(new UsermodCronixie()); #endif #ifdef QUINLED_AN_PENTA - usermods.add(new QuinLEDAnPentaUsermod()); + UsermodManager::add(new QuinLEDAnPentaUsermod()); #endif #ifdef USERMOD_WIZLIGHTS - usermods.add(new WizLightsUsermod()); + UsermodManager::add(new WizLightsUsermod()); #endif #ifdef USERMOD_WIREGUARD - usermods.add(new WireguardUsermod()); + UsermodManager::add(new WireguardUsermod()); #endif #ifdef USERMOD_WORDCLOCK - usermods.add(new WordClockUsermod()); + UsermodManager::add(new WordClockUsermod()); #endif #ifdef USERMOD_MY9291 - usermods.add(new MY9291Usermod()); + UsermodManager::add(new MY9291Usermod()); #endif #ifdef USERMOD_SI7021_MQTT_HA - usermods.add(new Si7021_MQTT_HA()); + UsermodManager::add(new Si7021_MQTT_HA()); #endif #ifdef USERMOD_SMARTNEST - usermods.add(new Smartnest()); + UsermodManager::add(new Smartnest()); #endif #ifdef USERMOD_AUDIOREACTIVE - usermods.add(new AudioReactive()); + UsermodManager::add(new AudioReactive()); #endif #ifdef USERMOD_ANALOG_CLOCK - usermods.add(new AnalogClockUsermod()); + UsermodManager::add(new AnalogClockUsermod()); #endif #ifdef USERMOD_PING_PONG_CLOCK - usermods.add(new PingPongClockUsermod()); + UsermodManager::add(new PingPongClockUsermod()); #endif #ifdef USERMOD_ADS1115 - usermods.add(new ADS1115Usermod()); + UsermodManager::add(new ADS1115Usermod()); #endif #ifdef USERMOD_KLIPPER_PERCENTAGE - usermods.add(new klipper_percentage()); + UsermodManager::add(new klipper_percentage()); #endif #ifdef USERMOD_BOBLIGHT - usermods.add(new BobLightUsermod()); + UsermodManager::add(new BobLightUsermod()); #endif #ifdef SD_ADAPTER - usermods.add(new UsermodSdCard()); + UsermodManager::add(new UsermodSdCard()); #endif #ifdef USERMOD_PWM_OUTPUTS - usermods.add(new PwmOutputsUsermod()); + UsermodManager::add(new PwmOutputsUsermod()); #endif #ifdef USERMOD_SHT - usermods.add(new ShtUsermod()); + UsermodManager::add(new ShtUsermod()); #endif #ifdef USERMOD_ANIMARTRIX - usermods.add(new AnimartrixUsermod("Animartrix", false)); + UsermodManager::add(new AnimartrixUsermod("Animartrix", false)); #endif #ifdef USERMOD_INTERNAL_TEMPERATURE - usermods.add(new InternalTemperatureUsermod()); + UsermodManager::add(new InternalTemperatureUsermod()); #endif #ifdef USERMOD_HTTP_PULL_LIGHT_CONTROL - usermods.add(new HttpPullLightControl()); + UsermodManager::add(new HttpPullLightControl()); #endif #ifdef USERMOD_MPU6050_IMU - static MPU6050Driver mpu6050; usermods.add(&mpu6050); + static MPU6050Driver mpu6050; UsermodManager::add(&mpu6050); #endif #ifdef USERMOD_GYRO_SURGE - static GyroSurge gyro_surge; usermods.add(&gyro_surge); + static GyroSurge gyro_surge; UsermodManager::add(&gyro_surge); #endif #ifdef USERMOD_LDR_DUSK_DAWN - usermods.add(new LDR_Dusk_Dawn_v2()); + UsermodManager::add(new LDR_Dusk_Dawn_v2()); #endif #ifdef USERMOD_STAIRCASE_WIPE - usermods.add(new StairwayWipeUsermod()); + UsermodManager::add(new StairwayWipeUsermod()); + #endif + + #ifdef USERMOD_MAX17048 + UsermodManager::add(new Usermod_MAX17048()); + #endif + + #ifdef USERMOD_TETRISAI + UsermodManager::add(new TetrisAIUsermod()); + #endif + + #ifdef USERMOD_AHT10 + UsermodManager::add(new UsermodAHT10()); + #endif + + #ifdef USERMOD_INA226 + UsermodManager::add(new UsermodINA226()); + #endif + + #ifdef USERMOD_LD2410 + UsermodManager::add(new LD2410Usermod()); + #endif + + #ifdef USERMOD_POV_DISPLAY + UsermodManager::add(new PovDisplayUsermod()); + #endif + + #ifdef USERMOD_DEEP_SLEEP + usermods.add(new DeepSleepUsermod()); #endif } diff --git a/wled00/util.cpp b/wled00/util.cpp index fa6c8faff..d9f1c00b9 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -14,7 +14,7 @@ int getNumVal(const String* req, uint16_t pos) void parseNumber(const char* str, byte* val, byte minv, byte maxv) { if (str == nullptr || str[0] == '\0') return; - if (str[0] == 'r') {*val = random8(minv,maxv?maxv:255); return;} // maxv for random cannot be 0 + if (str[0] == 'r') {*val = hw_random8(minv,maxv?maxv:255); return;} // maxv for random cannot be 0 bool wrap = false; if (str[0] == 'w' && strlen(str) > 1) {str++; wrap = true;} if (str[0] == '~') { @@ -52,7 +52,7 @@ void parseNumber(const char* str, byte* val, byte minv, byte maxv) *val = atoi(str); } - +//getVal supports inc/decrementing and random ("X~Y(r|~[w][-][Z])" form) bool getVal(JsonVariant elem, byte* val, byte vmin, byte vmax) { if (elem.is()) { if (elem < 0) return false; //ignore e.g. {"ps":-1} @@ -60,8 +60,12 @@ bool getVal(JsonVariant elem, byte* val, byte vmin, byte vmax) { return true; } else if (elem.is()) { const char* str = elem; - size_t len = strnlen(str, 12); - if (len == 0 || len > 10) return false; + size_t len = strnlen(str, 14); + if (len == 0 || len > 12) return false; + // fix for #3605 & #4346 + // ignore vmin and vmax and use as specified in API + if (len > 3 && (strchr(str,'r') || strchr(str,'~') != strrchr(str,'~'))) vmax = vmin = 0; // we have "X~Y(r|~[w][-][Z])" form + // end fix parseNumber(str, val, vmin, vmax); return true; } @@ -87,95 +91,35 @@ bool updateVal(const char* req, const char* key, byte* val, byte minv, byte maxv return true; } - -//append a numeric setting to string buffer -void sappend(char stype, const char* key, int val) -{ - char ds[] = "d.Sf."; - - switch(stype) - { - case 'c': //checkbox - oappend(ds); - oappend(key); - oappend(".checked="); - oappendi(val); - oappend(";"); - break; - case 'v': //numeric - oappend(ds); - oappend(key); - oappend(".value="); - oappendi(val); - oappend(";"); - break; - case 'i': //selectedIndex - oappend(ds); - oappend(key); - oappend(SET_F(".selectedIndex=")); - oappendi(val); - oappend(";"); - break; - } +static size_t printSetFormInput(Print& settingsScript, const char* key, const char* selector, int value) { + return settingsScript.printf_P(PSTR("d.Sf.%s.%s=%d;"), key, selector, value); } - -//append a string setting to buffer -void sappends(char stype, const char* key, char* val) -{ - switch(stype) - { - case 's': {//string (we can interpret val as char*) - String buf = val; - //convert "%" to "%%" to make EspAsyncWebServer happy - //buf.replace("%","%%"); - oappend("d.Sf."); - oappend(key); - oappend(".value=\""); - oappend(buf.c_str()); - oappend("\";"); - break;} - case 'm': //message - oappend(SET_F("d.getElementsByClassName")); - oappend(key); - oappend(SET_F(".innerHTML=\"")); - oappend(val); - oappend("\";"); - break; - } +size_t printSetFormCheckbox(Print& settingsScript, const char* key, int val) { + return printSetFormInput(settingsScript, key, PSTR("checked"), val); +} +size_t printSetFormValue(Print& settingsScript, const char* key, int val) { + return printSetFormInput(settingsScript, key, PSTR("value"), val); +} +size_t printSetFormIndex(Print& settingsScript, const char* key, int index) { + return printSetFormInput(settingsScript, key, PSTR("selectedIndex"), index); } - -bool oappendi(int i) -{ - char s[11]; - sprintf(s, "%d", i); - return oappend(s); +size_t printSetFormValue(Print& settingsScript, const char* key, const char* val) { + return settingsScript.printf_P(PSTR("d.Sf.%s.value=\"%s\";"),key,val); } - -bool oappend(const char* txt) -{ - uint16_t len = strlen(txt); - if ((obuf == nullptr) || (olen + len >= SETTINGS_STACK_BUF_SIZE)) { // sanity checks -#ifdef WLED_DEBUG - DEBUG_PRINT(F("oappend() buffer overflow. Cannot append ")); - DEBUG_PRINT(len); DEBUG_PRINT(F(" bytes \t\"")); - DEBUG_PRINT(txt); DEBUG_PRINTLN(F("\"")); -#endif - return false; // buffer full - } - strcpy(obuf + olen, txt); - olen += len; - return true; +size_t printSetClassElementHTML(Print& settingsScript, const char* key, const int index, const char* val) { + return settingsScript.printf_P(PSTR("d.getElementsByClassName(\"%s\")[%d].innerHTML=\"%s\";"), key, index, val); } + void prepareHostname(char* hostname) { sprintf_P(hostname, PSTR("wled-%*s"), 6, escapedMac.c_str() + 6); const char *pC = serverDescription; - uint8_t pos = 5; // keep "wled-" + unsigned pos = 5; // keep "wled-" while (*pC && pos < 24) { // while !null and not over length if (isalnum(*pC)) { // if the current char is alpha-numeric append it to the hostname hostname[pos] = *pC; @@ -197,7 +141,7 @@ void prepareHostname(char* hostname) bool isAsterisksOnly(const char* str, byte maxLen) { - for (byte i = 0; i < maxLen; i++) { + for (unsigned i = 0; i < maxLen; i++) { if (str[i] == 0) break; if (str[i] != '*') return false; } @@ -213,22 +157,33 @@ bool requestJSONBufferLock(uint8_t module) DEBUG_PRINTLN(F("ERROR: JSON buffer not allocated!")); return false; } - unsigned long now = millis(); - - while (jsonBufferLock && millis()-now < 100) delay(1); // wait for fraction for buffer lock +#if defined(ARDUINO_ARCH_ESP32) + // Use a recursive mutex type in case our task is the one holding the JSON buffer. + // This can happen during large JSON web transactions. In this case, we continue immediately + // and then will return out below if the lock is still held. + if (xSemaphoreTakeRecursive(jsonBufferLockMutex, 250) == pdFALSE) return false; // timed out waiting +#elif defined(ARDUINO_ARCH_ESP8266) + // If we're in system context, delay() won't return control to the user context, so there's + // no point in waiting. + if (can_yield()) { + unsigned long now = millis(); + while (jsonBufferLock && (millis()-now < 250)) delay(1); // wait for fraction for buffer lock + } +#else + #error Unsupported task framework - fix requestJSONBufferLock +#endif + // If the lock is still held - by us, or by another task if (jsonBufferLock) { - DEBUG_PRINT(F("ERROR: Locking JSON buffer failed! (still locked by ")); - DEBUG_PRINT(jsonBufferLock); - DEBUG_PRINTLN(")"); - return false; // waiting time-outed + DEBUG_PRINTF_P(PSTR("ERROR: Locking JSON buffer (%d) failed! (still locked by %d)\n"), module, jsonBufferLock); +#ifdef ARDUINO_ARCH_ESP32 + xSemaphoreGiveRecursive(jsonBufferLockMutex); +#endif + return false; } jsonBufferLock = module ? module : 255; - DEBUG_PRINT(F("JSON buffer locked. (")); - DEBUG_PRINT(jsonBufferLock); - DEBUG_PRINTLN(")"); - fileDoc = pDoc; // used for applying presets (presets.cpp) + DEBUG_PRINTF_P(PSTR("JSON buffer locked. (%d)\n"), jsonBufferLock); pDoc->clear(); return true; } @@ -236,11 +191,11 @@ bool requestJSONBufferLock(uint8_t module) void releaseJSONBufferLock() { - DEBUG_PRINT(F("JSON buffer released. (")); - DEBUG_PRINT(jsonBufferLock); - DEBUG_PRINTLN(")"); - fileDoc = nullptr; + DEBUG_PRINTF_P(PSTR("JSON buffer released. (%d)\n"), jsonBufferLock); jsonBufferLock = 0; +#ifdef ARDUINO_ARCH_ESP32 + xSemaphoreGiveRecursive(jsonBufferLockMutex); +#endif } @@ -265,15 +220,15 @@ uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLe } else return 0; } - if (src == JSON_palette_names && mode > GRADIENT_PALETTE_COUNT) { - snprintf_P(dest, maxLen, PSTR("~ Custom %d~"), 255-mode); + if (src == JSON_palette_names && mode > (GRADIENT_PALETTE_COUNT + 13)) { + snprintf_P(dest, maxLen, PSTR("~ Custom %d ~"), 255-mode); dest[maxLen-1] = '\0'; return strlen(dest); } - uint8_t qComma = 0; + unsigned qComma = 0; bool insideQuotes = false; - uint8_t printedChars = 0; + unsigned printedChars = 0; char singleJsonSymbol; size_t len = strlen_P(src); @@ -310,11 +265,11 @@ uint8_t extractModeSlider(uint8_t mode, uint8_t slider, char *dest, uint8_t maxL if (mode < strip.getModeCount()) { String lineBuffer = FPSTR(strip.getModeData(mode)); if (lineBuffer.length() > 0) { - int16_t start = lineBuffer.indexOf('@'); - int16_t stop = lineBuffer.indexOf(';', start); + unsigned start = lineBuffer.indexOf('@'); + unsigned stop = lineBuffer.indexOf(';', start); if (start>0 && stop>0) { String names = lineBuffer.substring(start, stop); // include @ - int16_t nameBegin = 1, nameEnd, nameDefault; + unsigned nameBegin = 1, nameEnd, nameDefault; if (slider < 10) { for (size_t i=0; i<=slider; i++) { const char *tmpstr; @@ -421,6 +376,39 @@ uint16_t crc16(const unsigned char* data_p, size_t length) { return crc; } +// fastled beatsin: 1:1 replacements to remove the use of fastled sin16() +// Generates a 16-bit sine wave at a given BPM that oscillates within a given range. see fastled for details. +uint16_t beatsin88_t(accum88 beats_per_minute_88, uint16_t lowest, uint16_t highest, uint32_t timebase, uint16_t phase_offset) +{ + uint16_t beat = beat88( beats_per_minute_88, timebase); + uint16_t beatsin (sin16_t( beat + phase_offset) + 32768); + uint16_t rangewidth = highest - lowest; + uint16_t scaledbeat = scale16( beatsin, rangewidth); + uint16_t result = lowest + scaledbeat; + return result; +} + +// Generates a 16-bit sine wave at a given BPM that oscillates within a given range. see fastled for details. +uint16_t beatsin16_t(accum88 beats_per_minute, uint16_t lowest, uint16_t highest, uint32_t timebase, uint16_t phase_offset) +{ + uint16_t beat = beat16( beats_per_minute, timebase); + uint16_t beatsin = (sin16_t( beat + phase_offset) + 32768); + uint16_t rangewidth = highest - lowest; + uint16_t scaledbeat = scale16( beatsin, rangewidth); + uint16_t result = lowest + scaledbeat; + return result; +} + +// Generates an 8-bit sine wave at a given BPM that oscillates within a given range. see fastled for details. +uint8_t beatsin8_t(accum88 beats_per_minute, uint8_t lowest, uint8_t highest, uint32_t timebase, uint8_t phase_offset) +{ + uint8_t beat = beat8( beats_per_minute, timebase); + uint8_t beatsin = sin8_t( beat + phase_offset); + uint8_t rangewidth = highest - lowest; + uint8_t scaledbeat = scale8( beatsin, rangewidth); + uint8_t result = lowest + scaledbeat; + return result; +} /////////////////////////////////////////////////////////////////////////////// // Begin simulateSound (to enable audio enhanced effects to display something) @@ -480,15 +468,15 @@ um_data_t* simulateSound(uint8_t simulationId) default: case UMS_BeatSin: for (int i = 0; i<16; i++) - fftResult[i] = beatsin8(120 / (i+1), 0, 255); - // fftResult[i] = (beatsin8(120, 0, 255) + (256/16 * i)) % 256; + fftResult[i] = beatsin8_t(120 / (i+1), 0, 255); + // fftResult[i] = (beatsin8_t(120, 0, 255) + (256/16 * i)) % 256; volumeSmth = fftResult[8]; break; case UMS_WeWillRockYou: if (ms%2000 < 200) { - volumeSmth = random8(255); + volumeSmth = hw_random8(); for (int i = 0; i<5; i++) - fftResult[i] = random8(255); + fftResult[i] = hw_random8(); } else if (ms%2000 < 400) { volumeSmth = 0; @@ -496,9 +484,9 @@ um_data_t* simulateSound(uint8_t simulationId) fftResult[i] = 0; } else if (ms%2000 < 600) { - volumeSmth = random8(255); + volumeSmth = hw_random8(); for (int i = 5; i<11; i++) - fftResult[i] = random8(255); + fftResult[i] = hw_random8(); } else if (ms%2000 < 800) { volumeSmth = 0; @@ -506,9 +494,9 @@ um_data_t* simulateSound(uint8_t simulationId) fftResult[i] = 0; } else if (ms%2000 < 1000) { - volumeSmth = random8(255); + volumeSmth = hw_random8(); for (int i = 11; i<16; i++) - fftResult[i] = random8(255); + fftResult[i] = hw_random8(); } else { volumeSmth = 0; @@ -518,17 +506,17 @@ um_data_t* simulateSound(uint8_t simulationId) break; case UMS_10_13: for (int i = 0; i<16; i++) - fftResult[i] = inoise8(beatsin8(90 / (i+1), 0, 200)*15 + (ms>>10), ms>>3); + fftResult[i] = inoise8(beatsin8_t(90 / (i+1), 0, 200)*15 + (ms>>10), ms>>3); volumeSmth = fftResult[8]; break; case UMS_14_3: for (int i = 0; i<16; i++) - fftResult[i] = inoise8(beatsin8(120 / (i+1), 10, 30)*10 + (ms>>14), ms>>3); + fftResult[i] = inoise8(beatsin8_t(120 / (i+1), 10, 30)*10 + (ms>>14), ms>>3); volumeSmth = fftResult[8]; break; } - samplePeak = random8() > 250; + samplePeak = hw_random8() > 250; FFT_MajorPeak = 21 + (volumeSmth*volumeSmth) / 8.0f; // walk thru full range of 21hz...8200hz maxVol = 31; // this gets feedback fro UI binNum = 8; // this gets feedback fro UI @@ -594,10 +582,30 @@ void enumerateLedmaps() { uint8_t get_random_wheel_index(uint8_t pos) { uint8_t r = 0, x = 0, y = 0, d = 0; while (d < 42) { - r = random8(); + r = hw_random8(); x = abs(pos - r); y = 255 - x; d = MIN(x, y); } return r; } + +// float version of map() +float mapf(float x, float in_min, float in_max, float out_min, float out_max) { + return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; +} + +// 32 bit random number generator, inlining uses more code, use hw_random16() if speed is critical (see fcn_declare.h) +uint32_t hw_random(uint32_t upperlimit) { + uint32_t rnd = hw_random(); + uint64_t scaled = uint64_t(rnd) * uint64_t(upperlimit); + return scaled >> 32; +} + +int32_t hw_random(int32_t lowerlimit, int32_t upperlimit) { + if(lowerlimit >= upperlimit) { + return lowerlimit; + } + uint32_t diff = upperlimit - lowerlimit; + return hw_random(diff) + lowerlimit; +} \ No newline at end of file diff --git a/wled00/wled.cpp b/wled00/wled.cpp index fc16dabcc..1f978a39b 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -8,6 +8,8 @@ #include "soc/rtc_cntl_reg.h" #endif +extern "C" void usePWMFixedNMI(); + /* * Main WLED class implementation. Mostly initialization and connection logic */ @@ -52,23 +54,25 @@ void WLED::loop() #endif handleTime(); -#ifndef WLED_DISABLE_INFRARED + #ifndef WLED_DISABLE_INFRARED handleIR(); // 2nd call to function needed for ESP32 to return valid results -- should be good for ESP8266, too -#endif + #endif handleConnection(); + #ifdef WLED_ENABLE_ADALIGHT handleSerial(); + #endif handleImprovWifiScan(); handleNotifications(); handleTransitions(); -#ifdef WLED_ENABLE_DMX + #ifdef WLED_ENABLE_DMX handleDMX(); -#endif + #endif #ifdef WLED_DEBUG unsigned long usermodMillis = millis(); #endif userLoop(); - usermods.loop(); + UsermodManager::loop(); #ifdef WLED_DEBUG usermodMillis = millis() - usermodMillis; avgUsermodMillis += usermodMillis; @@ -157,7 +161,7 @@ void WLED::loop() if (millis() - heapTime > 15000) { uint32_t heap = ESP.getFreeHeap(); if (heap < MIN_HEAP_SIZE && lastHeap < MIN_HEAP_SIZE) { - DEBUG_PRINT(F("Heap too low! ")); DEBUG_PRINTLN(heap); + DEBUG_PRINTF_P(PSTR("Heap too low! %u\n"), heap); forceReconnect = true; strip.resetSegments(); // remove all but one segments from memory } else if (heap < MIN_HEAP_SIZE) { @@ -175,23 +179,50 @@ void WLED::loop() DEBUG_PRINTLN(F("Re-init busses.")); bool aligned = strip.checkSegmentAlignment(); //see if old segments match old bus(ses) BusManager::removeAll(); - uint32_t mem = 0, globalBufMem = 0; - uint16_t maxlen = 0; - for (uint8_t i = 0; i < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; i++) { + unsigned mem = 0; + // determine if it is sensible to use parallel I2S outputs on ESP32 (i.e. more than 5 outputs = 1 I2S + 4 RMT) + bool useParallel = false; + #if defined(ARDUINO_ARCH_ESP32) && !defined(ARDUINO_ARCH_ESP32S2) && !defined(ARDUINO_ARCH_ESP32S3) && !defined(ARDUINO_ARCH_ESP32C3) + unsigned digitalCount = 0; + unsigned maxLedsOnBus = 0; + unsigned maxChannels = 0; + for (unsigned i = 0; i < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; i++) { if (busConfigs[i] == nullptr) break; - mem += BusManager::memUsage(*busConfigs[i]); - if (useGlobalLedBuffer && busConfigs[i]->start + busConfigs[i]->count > maxlen) { - maxlen = busConfigs[i]->start + busConfigs[i]->count; - globalBufMem = maxlen * 4; + if (!Bus::isDigital(busConfigs[i]->type)) continue; + if (!Bus::is2Pin(busConfigs[i]->type)) { + digitalCount++; + unsigned channels = Bus::getNumberOfChannels(busConfigs[i]->type); + if (busConfigs[i]->count > maxLedsOnBus) maxLedsOnBus = busConfigs[i]->count; + if (channels > maxChannels) maxChannels = channels; } - if (mem + globalBufMem <= MAX_LED_MEMORY) { - BusManager::add(*busConfigs[i]); - } - delete busConfigs[i]; busConfigs[i] = nullptr; + } + DEBUG_PRINTF_P(PSTR("Maximum LEDs on a bus: %u\nDigital buses: %u\n"), maxLedsOnBus, digitalCount); + // we may remove 300 LEDs per bus limit when NeoPixelBus is updated beyond 2.9.0 + if (maxLedsOnBus <= 300 && digitalCount > 5) { + DEBUG_PRINTF_P(PSTR("Switching to parallel I2S.")); + useParallel = true; + BusManager::useParallelOutput(); + mem = BusManager::memUsage(maxChannels, maxLedsOnBus, 8); // use alternate memory calculation (hse to be used *after* useParallelOutput()) + } + #endif + // create buses/outputs + for (unsigned i = 0; i < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; i++) { + if (busConfigs[i] == nullptr || (!useParallel && i > 10)) break; + if (useParallel && i < 8) { + // if for some unexplained reason the above pre-calculation was wrong, update + unsigned memT = BusManager::memUsage(*busConfigs[i]); // includes x8 memory allocation for parallel I2S + if (memT > mem) mem = memT; // if we have unequal LED count use the largest + } else + mem += BusManager::memUsage(*busConfigs[i]); // includes global buffer + if (mem <= MAX_LED_MEMORY) BusManager::add(*busConfigs[i]); + delete busConfigs[i]; + busConfigs[i] = nullptr; } strip.finalizeInit(); // also loads default ledmap if present + BusManager::setBrightness(bri); // fix re-initialised bus' brightness #4005 if (aligned) strip.makeAutoSegments(); else strip.fixInvalidSegments(); + BusManager::setBrightness(bri); // fix re-initialised bus' brightness doSerializeConfig = true; } if (loadLedmap >= 0) { @@ -237,38 +268,40 @@ void WLED::loop() if (loopMillis > maxLoopMillis) maxLoopMillis = loopMillis; if (millis() - debugTime > 29999) { DEBUG_PRINTLN(F("---DEBUG INFO---")); - DEBUG_PRINT(F("Runtime: ")); DEBUG_PRINTLN(millis()); - DEBUG_PRINT(F("Unix time: ")); toki.printTime(toki.getTime()); - DEBUG_PRINT(F("Free heap: ")); DEBUG_PRINTLN(ESP.getFreeHeap()); - #if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + DEBUG_PRINTF_P(PSTR("Runtime: %lu\n"), millis()); + DEBUG_PRINTF_P(PSTR("Unix time: %u,%03u\n"), toki.getTime().sec, toki.getTime().ms); + DEBUG_PRINTF_P(PSTR("Free heap: %u\n"), ESP.getFreeHeap()); + #if defined(ARDUINO_ARCH_ESP32) if (psramFound()) { - DEBUG_PRINT(F("Total PSRAM: ")); DEBUG_PRINT(ESP.getPsramSize()/1024); DEBUG_PRINTLN("kB"); - DEBUG_PRINT(F("Free PSRAM: ")); DEBUG_PRINT(ESP.getFreePsram()/1024); DEBUG_PRINTLN("kB"); + DEBUG_PRINTF_P(PSTR("PSRAM: %dkB/%dkB\n"), ESP.getFreePsram()/1024, ESP.getPsramSize()/1024); + if (!psramSafe) DEBUG_PRINTLN(F("Not using PSRAM.")); } + DEBUG_PRINTF_P(PSTR("TX power: %d/%d\n"), WiFi.getTxPower(), txPower); #endif - DEBUG_PRINT(F("Wifi state: ")); DEBUG_PRINTLN(WiFi.status()); + DEBUG_PRINTF_P(PSTR("Wifi state: %d\n"), WiFi.status()); #ifndef WLED_DISABLE_ESPNOW - DEBUG_PRINT(F("ESP-NOW state: ")); DEBUG_PRINTLN(statusESPNow); + DEBUG_PRINTF_P(PSTR("ESP-NOW state: %u\n"), statusESPNow); #endif if (WiFi.status() != lastWifiState) { wifiStateChangedTime = millis(); } lastWifiState = WiFi.status(); - DEBUG_PRINT(F("State time: ")); DEBUG_PRINTLN(wifiStateChangedTime); - DEBUG_PRINT(F("NTP last sync: ")); DEBUG_PRINTLN(ntpLastSyncTime); - DEBUG_PRINT(F("Client IP: ")); DEBUG_PRINTLN(Network.localIP()); + DEBUG_PRINTF_P(PSTR("State time: %lu\n"), wifiStateChangedTime); + DEBUG_PRINTF_P(PSTR("NTP last sync: %lu\n"), ntpLastSyncTime); + DEBUG_PRINTF_P(PSTR("Client IP: %u.%u.%u.%u\n"), Network.localIP()[0], Network.localIP()[1], Network.localIP()[2], Network.localIP()[3]); if (loops > 0) { // avoid division by zero - DEBUG_PRINT(F("Loops/sec: ")); DEBUG_PRINTLN(loops / 30); - DEBUG_PRINT(F("Loop time[ms]: ")); DEBUG_PRINT(avgLoopMillis/loops); DEBUG_PRINT("/");DEBUG_PRINTLN(maxLoopMillis); - DEBUG_PRINT(F("UM time[ms]: ")); DEBUG_PRINT(avgUsermodMillis/loops); DEBUG_PRINT("/");DEBUG_PRINTLN(maxUsermodMillis); - DEBUG_PRINT(F("Strip time[ms]: ")); DEBUG_PRINT(avgStripMillis/loops); DEBUG_PRINT("/"); DEBUG_PRINTLN(maxStripMillis); + DEBUG_PRINTF_P(PSTR("Loops/sec: %u\n"), loops / 30); + DEBUG_PRINTF_P(PSTR("Loop time[ms]: %u/%lu\n"), avgLoopMillis/loops, maxLoopMillis); + DEBUG_PRINTF_P(PSTR("UM time[ms]: %u/%lu\n"), avgUsermodMillis/loops, maxUsermodMillis); + DEBUG_PRINTF_P(PSTR("Strip time[ms]:%u/%lu\n"), avgStripMillis/loops, maxStripMillis); } strip.printSize(); loops = 0; maxLoopMillis = 0; maxUsermodMillis = 0; maxStripMillis = 0; + avgLoopMillis = 0; avgUsermodMillis = 0; avgStripMillis = 0; debugTime = millis(); @@ -314,6 +347,9 @@ void WLED::setup() #ifdef ARDUINO_ARCH_ESP32 pinMode(hardwareRX, INPUT_PULLDOWN); delay(1); // suppress noise in case RX pin is floating (at low noise energy) - see issue #3128 #endif + #ifdef WLED_BOOTUPDELAY + delay(WLED_BOOTUPDELAY); // delay to let voltage stabilize, helps with boot issues on some setups + #endif Serial.begin(115200); #if !ARDUINO_USB_CDC_ON_BOOT Serial.setTimeout(50); // this causes troubles on new MCUs that have a "virtual" USB Serial (HWCDC) @@ -326,102 +362,72 @@ void WLED::setup() Serial.setDebugOutput(false); // switch off kernel messages when using USBCDC #endif DEBUG_PRINTLN(); - DEBUG_PRINT(F("---WLED ")); - DEBUG_PRINT(versionString); - DEBUG_PRINT(F(" ")); - DEBUG_PRINT(VERSION); - DEBUG_PRINTLN(F(" INIT---")); + DEBUG_PRINTF_P(PSTR("---WLED %s %u INIT---\n"), versionString, VERSION); + DEBUG_PRINTLN(); #ifdef ARDUINO_ARCH_ESP32 - DEBUG_PRINT(F("esp32 ")); - DEBUG_PRINTLN(ESP.getSdkVersion()); + DEBUG_PRINTF_P(PSTR("esp32 %s\n"), ESP.getSdkVersion()); #if defined(ESP_ARDUINO_VERSION) - //DEBUG_PRINTF_P(PSTR("arduino-esp32 0x%06x\n"), ESP_ARDUINO_VERSION); - DEBUG_PRINTF_P(PSTR("arduino-esp32 v%d.%d.%d\n"), int(ESP_ARDUINO_VERSION_MAJOR), int(ESP_ARDUINO_VERSION_MINOR), int(ESP_ARDUINO_VERSION_PATCH)); // availeable since v2.0.0 + DEBUG_PRINTF_P(PSTR("arduino-esp32 v%d.%d.%d\n"), int(ESP_ARDUINO_VERSION_MAJOR), int(ESP_ARDUINO_VERSION_MINOR), int(ESP_ARDUINO_VERSION_PATCH)); // available since v2.0.0 #else DEBUG_PRINTLN(F("arduino-esp32 v1.0.x\n")); // we can't say in more detail. #endif - DEBUG_PRINT(F("CPU: ")); DEBUG_PRINT(ESP.getChipModel()); - DEBUG_PRINT(F(" rev.")); DEBUG_PRINT(ESP.getChipRevision()); - DEBUG_PRINT(F(", ")); DEBUG_PRINT(ESP.getChipCores()); DEBUG_PRINT(F(" core(s)")); - DEBUG_PRINT(F(", ")); DEBUG_PRINT(ESP.getCpuFreqMHz()); DEBUG_PRINTLN(F("MHz.")); - DEBUG_PRINT(F("FLASH: ")); DEBUG_PRINT((ESP.getFlashChipSize()/1024)/1024); - DEBUG_PRINT(F("MB, Mode ")); DEBUG_PRINT(ESP.getFlashChipMode()); + DEBUG_PRINTF_P(PSTR("CPU: %s rev.%d, %d core(s), %d MHz.\n"), ESP.getChipModel(), (int)ESP.getChipRevision(), ESP.getChipCores(), ESP.getCpuFreqMHz()); + DEBUG_PRINTF_P(PSTR("FLASH: %d MB, Mode %d "), (ESP.getFlashChipSize()/1024)/1024, (int)ESP.getFlashChipMode()); #ifdef WLED_DEBUG switch (ESP.getFlashChipMode()) { // missing: Octal modes - case FM_QIO: DEBUG_PRINT(F(" (QIO)")); break; - case FM_QOUT: DEBUG_PRINT(F(" (QOUT)"));break; - case FM_DIO: DEBUG_PRINT(F(" (DIO)")); break; - case FM_DOUT: DEBUG_PRINT(F(" (DOUT)"));break; + case FM_QIO: DEBUG_PRINT(F("(QIO)")); break; + case FM_QOUT: DEBUG_PRINT(F("(QOUT)"));break; + case FM_DIO: DEBUG_PRINT(F("(DIO)")); break; + case FM_DOUT: DEBUG_PRINT(F("(DOUT)"));break; + #if defined(CONFIG_IDF_TARGET_ESP32S3) && CONFIG_ESPTOOLPY_FLASHMODE_OPI + case FM_FAST_READ: DEBUG_PRINT(F("(OPI)")); break; + #else + case FM_FAST_READ: DEBUG_PRINT(F("(fast_read)")); break; + #endif + case FM_SLOW_READ: DEBUG_PRINT(F("(slow_read)")); break; default: break; } #endif - DEBUG_PRINT(F(", speed ")); DEBUG_PRINT(ESP.getFlashChipSpeed()/1000000);DEBUG_PRINTLN(F("MHz.")); + DEBUG_PRINTF_P(PSTR(", speed %u MHz.\n"), ESP.getFlashChipSpeed()/1000000); #else - DEBUG_PRINT(F("esp8266 ")); - DEBUG_PRINTLN(ESP.getCoreVersion()); + DEBUG_PRINTF_P(PSTR("esp8266 @ %u MHz.\nCore: %s\n"), ESP.getCpuFreqMHz(), ESP.getCoreVersion()); + DEBUG_PRINTF_P(PSTR("FLASH: %u MB\n"), (ESP.getFlashChipSize()/1024)/1024); #endif - DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(ESP.getFreeHeap()); + DEBUG_PRINTF_P(PSTR("heap %u\n"), ESP.getFreeHeap()); -#if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) -/* - * The following code is obsolete as PinManager::isPinOK() will return false for reserved GPIO. - * Additionally xml.cpp will inform UI about reserved GPIO. - * - - #if defined(CONFIG_IDF_TARGET_ESP32S3) - // S3: reserve GPIO 33-37 for "octal" PSRAM - managed_pin_type pins[] = { {33, true}, {34, true}, {35, true}, {36, true}, {37, true} }; - pinManager.allocateMultiplePins(pins, sizeof(pins)/sizeof(managed_pin_type), PinOwner::SPI_RAM); - #elif defined(CONFIG_IDF_TARGET_ESP32S2) - // S2: reserve GPIO 26-32 for PSRAM (may fail due to isPinOk() but that will also prevent other allocation) - managed_pin_type pins[] = { {26, true}, {27, true}, {28, true}, {29, true}, {30, true}, {31, true}, {32, true} }; - pinManager.allocateMultiplePins(pins, sizeof(pins)/sizeof(managed_pin_type), PinOwner::SPI_RAM); - #elif defined(CONFIG_IDF_TARGET_ESP32C3) - // C3: reserve GPIO 12-17 for PSRAM (may fail due to isPinOk() but that will also prevent other allocation) - managed_pin_type pins[] = { {12, true}, {13, true}, {14, true}, {15, true}, {16, true}, {17, true} }; - pinManager.allocateMultiplePins(pins, sizeof(pins)/sizeof(managed_pin_type), PinOwner::SPI_RAM); - #else - // GPIO16/GPIO17 reserved for SPI RAM - managed_pin_type pins[] = { {16, true}, {17, true} }; - pinManager.allocateMultiplePins(pins, sizeof(pins)/sizeof(managed_pin_type), PinOwner::SPI_RAM); +#if defined(ARDUINO_ARCH_ESP32) + // BOARD_HAS_PSRAM also means that a compiler flag "-mfix-esp32-psram-cache-issue" was used and so PSRAM is safe to use on rev.1 ESP32 + #if !defined(BOARD_HAS_PSRAM) && !(defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3)) + if (psramFound() && ESP.getChipRevision() < 3) psramSafe = false; + if (!psramSafe) DEBUG_PRINTLN(F("Not using PSRAM.")); #endif -*/ - #if defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) - pDoc = new PSRAMDynamicJsonDocument(2*JSON_BUFFER_SIZE); - if (!pDoc) pDoc = new PSRAMDynamicJsonDocument(JSON_BUFFER_SIZE); // falback if double sized buffer could not be allocated - // if the above still fails requestJsonBufferLock() will always return false preventing crashes + pDoc = new PSRAMDynamicJsonDocument((psramSafe && psramFound() ? 2 : 1)*JSON_BUFFER_SIZE); + DEBUG_PRINTF_P(PSTR("JSON buffer allocated: %u\n"), (psramSafe && psramFound() ? 2 : 1)*JSON_BUFFER_SIZE); + // if the above fails requestJsonBufferLock() will always return false preventing crashes if (psramFound()) { - DEBUG_PRINT(F("Total PSRAM: ")); DEBUG_PRINT(ESP.getPsramSize()/1024); DEBUG_PRINTLN("kB"); - DEBUG_PRINT(F("Free PSRAM : ")); DEBUG_PRINT(ESP.getFreePsram()/1024); DEBUG_PRINTLN("kB"); + DEBUG_PRINTF_P(PSTR("PSRAM: %dkB/%dkB\n"), ESP.getFreePsram()/1024, ESP.getPsramSize()/1024); } - #else - if (!pDoc) pDoc = &gDoc; // just in case ... (it should be globally assigned) - DEBUG_PRINTLN(F("PSRAM not used.")); - #endif -#endif -#if defined(ARDUINO_ESP32_PICO) - // special handling for PICO-D4: gpio16+17 are in use for onboard SPI FLASH (not PSRAM) - managed_pin_type pins[] = { {16, true}, {17, true} }; - pinManager.allocateMultiplePins(pins, sizeof(pins)/sizeof(managed_pin_type), PinOwner::SPI_RAM); + DEBUG_PRINTF_P(PSTR("TX power: %d/%d\n"), WiFi.getTxPower(), txPower); #endif - //DEBUG_PRINT(F("LEDs inited. heap usage ~")); - //DEBUG_PRINTLN(heapPreAlloc - ESP.getFreeHeap()); +#ifdef ESP8266 + usePWMFixedNMI(); // link the NMI fix +#endif #if defined(WLED_DEBUG) && !defined(WLED_DEBUG_HOST) - pinManager.allocatePin(hardwareTX, true, PinOwner::DebugOut); // TX (GPIO1 on ESP32) reserved for debug output + PinManager::allocatePin(hardwareTX, true, PinOwner::DebugOut); // TX (GPIO1 on ESP32) reserved for debug output #endif #ifdef WLED_ENABLE_DMX //reserve GPIO2 as hardcoded DMX pin - pinManager.allocatePin(2, true, PinOwner::DMX); + PinManager::allocatePin(2, true, PinOwner::DMX); #endif DEBUG_PRINTLN(F("Registering usermods ...")); registerUsermods(); - DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(ESP.getFreeHeap()); + DEBUG_PRINTF_P(PSTR("heap %u\n"), ESP.getFreeHeap()); bool fsinit = false; DEBUGFS_PRINTLN(F("Mount FS")); @@ -451,9 +457,10 @@ void WLED::setup() DEBUG_PRINTLN(F("Reading config")); deserializeConfigFromFS(); + DEBUG_PRINTF_P(PSTR("heap %u\n"), ESP.getFreeHeap()); #if defined(STATUSLED) && STATUSLED>=0 - if (!pinManager.isPinAllocated(STATUSLED)) { + if (!PinManager::isPinAllocated(STATUSLED)) { // NOTE: Special case: The status LED should *NOT* be allocated. // See comments in handleStatusLed(). pinMode(STATUSLED, OUTPUT); @@ -462,27 +469,28 @@ void WLED::setup() DEBUG_PRINTLN(F("Initializing strip")); beginStrip(); - DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(ESP.getFreeHeap()); + DEBUG_PRINTF_P(PSTR("heap %u\n"), ESP.getFreeHeap()); DEBUG_PRINTLN(F("Usermods setup")); userSetup(); - usermods.setup(); - DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(ESP.getFreeHeap()); + UsermodManager::setup(); + DEBUG_PRINTF_P(PSTR("heap %u\n"), ESP.getFreeHeap()); if (strcmp(multiWiFi[0].clientSSID, DEFAULT_CLIENT_SSID) == 0) showWelcomePage = true; WiFi.persistent(false); - #ifdef WLED_USE_ETHERNET WiFi.onEvent(WiFiEvent); - #endif - WiFi.mode(WIFI_STA); // enable scanning findWiFi(true); // start scanning for available WiFi-s + // all GPIOs are allocated at this point + serialCanRX = !PinManager::isPinAllocated(hardwareRX); // Serial RX pin (GPIO 3 on ESP32 and ESP8266) + serialCanTX = !PinManager::isPinAllocated(hardwareTX) || PinManager::getPinOwner(hardwareTX) == PinOwner::DebugOut; // Serial TX pin (GPIO 1 on ESP32 and ESP8266) + #ifdef WLED_ENABLE_ADALIGHT //Serial RX (Adalight, Improv, Serial JSON) only possible if GPIO3 unused //Serial TX (Debug, Improv, Serial JSON) only possible if GPIO1 unused - if (!pinManager.isPinAllocated(hardwareRX) && !pinManager.isPinAllocated(hardwareTX)) { + if (serialCanRX && serialCanTX) { Serial.println(F("Ada")); } #endif @@ -494,10 +502,6 @@ void WLED::setup() if (mqttClientID[0] == 0) sprintf_P(mqttClientID, PSTR("WLED-%*s"), 6, escapedMac.c_str() + 6); #endif -#ifdef WLED_ENABLE_ADALIGHT - if (Serial.available() > 0 && Serial.peek() == 'I') handleImprovPacket(); -#endif - #ifndef WLED_DISABLE_OTA if (aOtaEnabled) { ArduinoOTA.onStart([]() { @@ -524,23 +528,24 @@ void WLED::setup() #endif #ifdef WLED_ENABLE_ADALIGHT - if (Serial.available() > 0 && Serial.peek() == 'I') handleImprovPacket(); + if (serialCanRX && Serial.available() > 0 && Serial.peek() == 'I') handleImprovPacket(); #endif // HTTP server page init DEBUG_PRINTLN(F("initServer")); initServer(); - DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(ESP.getFreeHeap()); + DEBUG_PRINTF_P(PSTR("heap %u\n"), ESP.getFreeHeap()); + +#ifndef WLED_DISABLE_INFRARED + // init IR + DEBUG_PRINTLN(F("initIR")); + initIR(); + DEBUG_PRINTF_P(PSTR("heap %u\n"), ESP.getFreeHeap()); +#endif // Seed FastLED random functions with an esp random value, which already works properly at this point. -#if defined(ARDUINO_ARCH_ESP32) - const uint32_t seed32 = esp_random(); -#elif defined(ARDUINO_ARCH_ESP8266) - const uint32_t seed32 = RANDOM_REG32; -#else - const uint32_t seed32 = random(std::numeric_limits::max()); -#endif - random16_set_seed((uint16_t)((seed32 & 0xFFFF) ^ (seed32 >> 16))); + const uint32_t seed32 = hw_random(); + random16_set_seed((uint16_t)seed32); #if WLED_WATCHDOG_TIMEOUT > 0 enableWatchdog(); @@ -554,7 +559,7 @@ void WLED::setup() void WLED::beginStrip() { // Initialize NeoPixel Strip and button - strip.finalizeInit(); // busses created during deserializeConfig() + strip.finalizeInit(); // busses created during deserializeConfig() if config existed strip.makeAutoSegments(); strip.setBrightness(0); strip.setShowCallback(handleOverlayDraw); @@ -565,10 +570,11 @@ void WLED::beginStrip() } else { // fix for #3196 if (bootPreset > 0) { - bool oldTransition = fadeTransition; // workaround if transitions are enabled - fadeTransition = false; // ignore transitions temporarily - strip.setColor(0, BLACK); // set all segments black - fadeTransition = oldTransition; // restore transitions + // set all segments black (no transition) + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { + Segment &seg = strip.getSegment(i); + if (seg.isActive()) seg.colors[0] = BLACK; + } col[0] = col[1] = col[2] = col[3] = 0; // needed for colorUpdated() } briLast = briS; bri = 0; @@ -578,11 +584,13 @@ void WLED::beginStrip() if (bootPreset > 0) { applyPreset(bootPreset, CALL_MODE_INIT); } - colorUpdated(CALL_MODE_INIT); + colorUpdated(CALL_MODE_INIT); // will not send notification // init relay pin - if (rlyPin>=0) + if (rlyPin >= 0) { + pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT); digitalWrite(rlyPin, (rlyMde ? bri : !bri)); + } } void WLED::initAP(bool resetAP) @@ -598,8 +606,8 @@ void WLED::initAP(bool resetAP) DEBUG_PRINTLN(apSSID); WiFi.softAPConfig(IPAddress(4, 3, 2, 1), IPAddress(4, 3, 2, 1), IPAddress(255, 255, 255, 0)); WiFi.softAP(apSSID, apPass, apChannel, apHide); - #if defined(LOLIN_WIFI_FIX) && (defined(ARDUINO_ARCH_ESP32C3) || defined(ARDUINO_ARCH_ESP32S2) || defined(ARDUINO_ARCH_ESP32S3)) - WiFi.setTxPower(WIFI_POWER_8_5dBm); + #ifdef ARDUINO_ARCH_ESP32 + WiFi.setTxPower(wifi_power_t(txPower)); #endif if (!apActive) // start captive portal if AP active @@ -638,11 +646,11 @@ bool WLED::initEthernet() return false; } if (ethernetType >= WLED_NUM_ETH_TYPES) { - DEBUG_PRINT(F("initE: Ignoring attempt for invalid ethernetType ")); DEBUG_PRINTLN(ethernetType); + DEBUG_PRINTF_P(PSTR("initE: Ignoring attempt for invalid ethernetType (%d)\n"), ethernetType); return false; } - DEBUG_PRINT(F("initE: Attempting ETH config: ")); DEBUG_PRINTLN(ethernetType); + DEBUG_PRINTF_P(PSTR("initE: Attempting ETH config: %d\n"), ethernetType); // Ethernet initialization should only succeed once -- else reboot required ethernet_settings es = ethernetBoards[ethernetType]; @@ -673,13 +681,11 @@ bool WLED::initEthernet() pinsToAllocate[9].pin = 17; pinsToAllocate[9].isOutput = true; } else { - DEBUG_PRINT(F("initE: Failing due to invalid eth_clk_mode (")); - DEBUG_PRINT(es.eth_clk_mode); - DEBUG_PRINTLN(")"); + DEBUG_PRINTF_P(PSTR("initE: Failing due to invalid eth_clk_mode (%d)\n"), es.eth_clk_mode); return false; } - if (!pinManager.allocateMultiplePins(pinsToAllocate, 10, PinOwner::Ethernet)) { + if (!PinManager::allocateMultiplePins(pinsToAllocate, 10, PinOwner::Ethernet)) { DEBUG_PRINTLN(F("initE: Failed to allocate ethernet pins")); return false; } @@ -713,7 +719,7 @@ bool WLED::initEthernet() DEBUG_PRINTLN(F("initC: ETH.begin() failed")); // de-allocate the allocated pins for (managed_pin_type mpt : pinsToAllocate) { - pinManager.deallocatePin(mpt.pin, PinOwner::Ethernet); + PinManager::deallocatePin(mpt.pin, PinOwner::Ethernet); } return false; } @@ -768,7 +774,7 @@ int8_t WLED::findWiFi(bool doScan) { void WLED::initConnection() { - DEBUG_PRINTLN(F("initConnection() called.")); + DEBUG_PRINTF_P(PSTR("initConnection() called @ %lus.\n"), millis()/1000); #ifdef WLED_ENABLE_WEBSOCKETS ws.onEvent(wsEvent); @@ -812,9 +818,7 @@ void WLED::initConnection() if (WLED_WIFI_CONFIGURED) { showWelcomePage = false; - DEBUG_PRINT(F("Connecting to ")); - DEBUG_PRINT(multiWiFi[selectedWiFi].clientSSID); - DEBUG_PRINTLN(F("...")); + DEBUG_PRINTF_P(PSTR("Connecting to %s...\n"), multiWiFi[selectedWiFi].clientSSID); // convert the "serverDescription" into a valid DNS hostname (alphanumeric) char hostname[25]; @@ -822,9 +826,7 @@ void WLED::initConnection() WiFi.begin(multiWiFi[selectedWiFi].clientSSID, multiWiFi[selectedWiFi].clientPass); // no harm if called multiple times #ifdef ARDUINO_ARCH_ESP32 - #if defined(LOLIN_WIFI_FIX) && (defined(ARDUINO_ARCH_ESP32C3) || defined(ARDUINO_ARCH_ESP32S2) || defined(ARDUINO_ARCH_ESP32S3)) - WiFi.setTxPower(WIFI_POWER_8_5dBm); - #endif + WiFi.setTxPower(wifi_power_t(txPower)); WiFi.setSleep(!noWifiSleep); WiFi.setHostname(hostname); #else @@ -915,7 +917,8 @@ void WLED::handleConnection() { static bool scanDone = true; static byte stacO = 0; - unsigned long now = millis(); + const unsigned long now = millis(); + const unsigned long nowS = now/1000; const bool wifiConfigured = WLED_WIFI_CONFIGURED; // ignore connection handling if WiFi is configured and scan still running @@ -924,7 +927,7 @@ void WLED::handleConnection() return; if (lastReconnectAttempt == 0 || forceReconnect) { - DEBUG_PRINTLN(F("Initial connect or forced reconnect.")); + DEBUG_PRINTF_P(PSTR("Initial connect or forced reconnect (@ %lus).\n"), nowS); selectedWiFi = findWiFi(); // find strongest WiFi initConnection(); interfacesInited = false; @@ -944,8 +947,7 @@ void WLED::handleConnection() #endif if (stac != stacO) { stacO = stac; - DEBUG_PRINT(F("Connected AP clients: ")); - DEBUG_PRINTLN(stac); + DEBUG_PRINTF_P(PSTR("Connected AP clients: %d\n"), (int)stac); if (!WLED_CONNECTED && wifiConfigured) { // trying to connect, but not connected if (stac) WiFi.disconnect(); // disable search so that AP can work @@ -968,6 +970,7 @@ void WLED::handleConnection() initConnection(); interfacesInited = false; scanDone = true; + return; } //send improv failed 6 seconds after second init attempt (24 sec. after provisioning) if (improvActive > 2 && now - lastReconnectAttempt > 6000) { @@ -976,13 +979,13 @@ void WLED::handleConnection() } if (now - lastReconnectAttempt > ((stac) ? 300000 : 18000) && wifiConfigured) { if (improvActive == 2) improvActive = 3; - DEBUG_PRINTLN(F("Last reconnect too old.")); + DEBUG_PRINTF_P(PSTR("Last reconnect (%lus) too old (@ %lus).\n"), lastReconnectAttempt/1000, nowS); if (++selectedWiFi >= multiWiFi.size()) selectedWiFi = 0; // we couldn't connect, try with another network from the list initConnection(); } if (!apActive && now - lastReconnectAttempt > 12000 && (!wasConnected || apBehavior == AP_BEHAVIOR_NO_CONN)) { if (!(apBehavior == AP_BEHAVIOR_TEMPORARY && now > WLED_AP_TIMEOUT)) { - DEBUG_PRINTLN(F("Not connected AP.")); + DEBUG_PRINTF_P(PSTR("Not connected AP (@ %lus).\n"), nowS); initAP(); // start AP only within first 5min } } @@ -992,7 +995,7 @@ void WLED::handleConnection() dnsServer.stop(); WiFi.softAPdisconnect(true); apActive = false; - DEBUG_PRINTLN(F("Temporary AP disabled.")); + DEBUG_PRINTF_P(PSTR("Temporary AP disabled (@ %lus).\n"), nowS); } } } else if (!interfacesInited) { //newly connected @@ -1006,7 +1009,7 @@ void WLED::handleConnection() } initInterfaces(); userConnected(); - usermods.connected(); + UsermodManager::connected(); lastMqttReconnectAttempt = 0; // force immediate update // shut down AP @@ -1029,7 +1032,7 @@ void WLED::handleStatusLED() uint32_t c = 0; #if STATUSLED>=0 - if (pinManager.isPinAllocated(STATUSLED)) { + if (PinManager::isPinAllocated(STATUSLED)) { return; //lower priority if something else uses the same pin } #endif diff --git a/wled00/wled.h b/wled00/wled.h old mode 100755 new mode 100644 index e22603df3..533b12063 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -3,12 +3,11 @@ /* Main sketch, global variable declarations @title WLED project sketch - @version 0.15.0-b1 @author Christian Schwinne */ // version code in format yymmddb (b = daily build) -#define VERSION 2403070 +#define VERSION 2412040 //uncomment this if you have a "my_config.h" file you'd like to use //#define WLED_USE_MY_CONFIG @@ -35,13 +34,14 @@ #else #undef WLED_ENABLE_ADALIGHT // disable has priority over enable #endif -//#define WLED_ENABLE_DMX // uses 3.5kb (use LEDPIN other than 2) -#define WLED_ENABLE_JSONLIVE // peek LED output via /json/live (WS binary peek is always enabled) +//#define WLED_ENABLE_DMX // uses 3.5kb #ifndef WLED_DISABLE_LOXONE #define WLED_ENABLE_LOXONE // uses 1.2kb #endif #ifndef WLED_DISABLE_WEBSOCKETS #define WLED_ENABLE_WEBSOCKETS +#else + #define WLED_ENABLE_JSONLIVE // peek LED output via /json/live (WS binary peek is always enabled) #endif //#define WLED_DISABLE_ESPNOW // Removes dependence on esp now @@ -81,8 +81,6 @@ #define WIFI_MODE_AP WIFI_AP #include #endif - #undef WLED_DISABLE_GIF - #define WLED_DISABLE_GIF #else // ESP32 #include // ensure we have the correct "Serial" on new MCUs (depends on ARDUINO_USB_MODE and ARDUINO_USB_CDC_ON_BOOT) #include @@ -147,7 +145,7 @@ #endif #include "src/dependencies/e131/ESPAsyncE131.h" -#ifdef WLED_ENABLE_MQTT +#ifndef WLED_DISABLE_MQTT #include "src/dependencies/async-mqtt-client/AsyncMqttClient.h" #endif @@ -160,15 +158,16 @@ // The following is a construct to enable code to compile without it. // There is a code that will still not use PSRAM though: // AsyncJsonResponse is a derived class that implements DynamicJsonDocument (AsyncJson-v6.h) -#if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) +#if defined(ARDUINO_ARCH_ESP32) +extern bool psramSafe; struct PSRAM_Allocator { void* allocate(size_t size) { - if (psramFound()) return ps_malloc(size); // use PSRAM if it exists - else return malloc(size); // fallback + if (psramSafe && psramFound()) return ps_malloc(size); // use PSRAM if it exists + else return malloc(size); // fallback } void* reallocate(void* ptr, size_t new_size) { - if (psramFound()) return ps_realloc(ptr, new_size); // use PSRAM if it exists - else return realloc(ptr, new_size); // fallback + if (psramSafe && psramFound()) return ps_realloc(ptr, new_size); // use PSRAM if it exists + else return realloc(ptr, new_size); // fallback } void deallocate(void* pointer) { free(pointer); @@ -244,28 +243,33 @@ using PSRAMDynamicJsonDocument = BasicJsonDocument; // int arr[]{0,1,2} becomes WLED_GLOBAL int arr[] _INIT_N(({0,1,2})); #ifndef WLED_DEFINE_GLOBAL_VARS -# define WLED_GLOBAL extern -# define _INIT(x) -# define _INIT_N(x) + #define WLED_GLOBAL extern + #define _INIT(x) + #define _INIT_N(x) + #define _INIT_PROGMEM(x) #else -# define WLED_GLOBAL -# define _INIT(x) = x - -//needed to ignore commas in array definitions -#define UNPACK( ... ) __VA_ARGS__ -# define _INIT_N(x) UNPACK x + #define WLED_GLOBAL + #define _INIT(x) = x + //needed to ignore commas in array definitions + #define UNPACK( ... ) __VA_ARGS__ + #define _INIT_N(x) UNPACK x + #define _INIT_PROGMEM(x) PROGMEM = x #endif #define STRINGIFY(X) #X #define TOSTRING(X) STRINGIFY(X) #ifndef WLED_VERSION - #define WLED_VERSION "dev" + #define WLED_VERSION dev +#endif +#ifndef WLED_RELEASE_NAME + #define WLED_RELEASE_NAME "Custom" #endif // Global Variable definitions WLED_GLOBAL char versionString[] _INIT(TOSTRING(WLED_VERSION)); -#define WLED_CODENAME "Kōsen" +WLED_GLOBAL char releaseString[] _INIT(WLED_RELEASE_NAME); // must include the quotes when defining, e.g -D WLED_RELEASE_NAME=\"ESP32_MULTI_USREMODS\" +#define WLED_CODENAME "Niji" // AP and OTA default passwords (for maximum security change them!) WLED_GLOBAL char apPass[65] _INIT(WLED_AP_PASS); @@ -289,6 +293,12 @@ WLED_GLOBAL bool rlyMde _INIT(true); #else WLED_GLOBAL bool rlyMde _INIT(RLYMDE); #endif +//Use open drain (floating pin) when relay should be off +#ifndef RLYODRAIN +WLED_GLOBAL bool rlyOpenDrain _INIT(false); +#else +WLED_GLOBAL bool rlyOpenDrain _INIT(RLYODRAIN); +#endif #ifndef IRPIN #define IRPIN -1 #endif @@ -306,25 +316,63 @@ WLED_GLOBAL bool rlyMde _INIT(RLYMDE); constexpr uint8_t hardwareTX = 1; #endif -//WLED_GLOBAL byte presetToApply _INIT(0); - WLED_GLOBAL char ntpServerName[33] _INIT("0.wled.pool.ntp.org"); // NTP server to use // WiFi CONFIG (all these can be changed via web UI, no need to set them here) -WLED_GLOBAL uint8_t selectedWiFi _INIT(0); WLED_GLOBAL std::vector multiWiFi; WLED_GLOBAL IPAddress dnsAddress _INIT_N((( 8, 8, 8, 8))); // Google's DNS WLED_GLOBAL char cmDNS[33] _INIT(MDNS_NAME); // mDNS address (*.local, replaced by wledXXXXXX if default is used) WLED_GLOBAL char apSSID[33] _INIT(""); // AP off by default (unless setup) +#ifdef WLED_SAVE_RAM +typedef class WiFiOptions { + public: + struct { + uint8_t selectedWiFi : 4; // max 16 SSIDs + uint8_t apChannel : 4; + uint8_t apHide : 3; + uint8_t apBehavior : 3; + bool noWifiSleep : 1; + bool force802_3g : 1; + }; + WiFiOptions(uint8_t s, uint8_t c, bool h, uint8_t b, bool sl, bool g) { + selectedWiFi = s; + apChannel = c; + apHide = h; + apBehavior = b; + noWifiSleep = sl; + force802_3g = g; + } +} __attribute__ ((aligned(1), packed)) wifi_options_t; + #ifdef ARDUINO_ARCH_ESP32 +WLED_GLOBAL wifi_options_t wifiOpt _INIT_N(({0, 1, false, AP_BEHAVIOR_BOOT_NO_CONN, true, false})); + #else +WLED_GLOBAL wifi_options_t wifiOpt _INIT_N(({0, 1, false, AP_BEHAVIOR_BOOT_NO_CONN, false, false})); + #endif +#define selectedWiFi wifiOpt.selectedWiFi +#define apChannel wifiOpt.apChannel +#define apHide wifiOpt.apHide +#define apBehavior wifiOpt.apBehavior +#define noWifiSleep wifiOpt.noWifiSleep +#define force802_3g wifiOpt.force802_3g +#else +WLED_GLOBAL uint8_t selectedWiFi _INIT(0); WLED_GLOBAL byte apChannel _INIT(1); // 2.4GHz WiFi AP channel (1-13) WLED_GLOBAL byte apHide _INIT(0); // hidden AP SSID WLED_GLOBAL byte apBehavior _INIT(AP_BEHAVIOR_BOOT_NO_CONN); // access point opens when no connection after boot by default -#ifdef ARDUINO_ARCH_ESP32 + #ifdef ARDUINO_ARCH_ESP32 WLED_GLOBAL bool noWifiSleep _INIT(true); // disabling modem sleep modes will increase heat output and power usage, but may help with connection issues -#else + #else WLED_GLOBAL bool noWifiSleep _INIT(false); -#endif + #endif WLED_GLOBAL bool force802_3g _INIT(false); +#endif // WLED_SAVE_RAM +#ifdef ARDUINO_ARCH_ESP32 + #if defined(LOLIN_WIFI_FIX) && (defined(ARDUINO_ARCH_ESP32C3) || defined(ARDUINO_ARCH_ESP32S2) || defined(ARDUINO_ARCH_ESP32S3)) +WLED_GLOBAL uint8_t txPower _INIT(WIFI_POWER_8_5dBm); + #else +WLED_GLOBAL uint8_t txPower _INIT(WIFI_POWER_19_5dBm); + #endif +#endif #define WLED_WIFI_CONFIGURED (strlen(multiWiFi[0].clientSSID) >= 1 && strcmp(multiWiFi[0].clientSSID, DEFAULT_CLIENT_SSID) != 0) #ifdef WLED_USE_ETHERNET @@ -342,21 +390,22 @@ WLED_GLOBAL byte bootPreset _INIT(0); // save preset to load //if true, a segment per bus will be created on boot and LED settings save //if false, only one segment spanning the total LEDs is created, //but not on LED settings save if there is more than one segment currently -WLED_GLOBAL bool autoSegments _INIT(false); #ifdef ESP8266 WLED_GLOBAL bool useGlobalLedBuffer _INIT(false); // double buffering disabled on ESP8266 #else WLED_GLOBAL bool useGlobalLedBuffer _INIT(true); // double buffering enabled on ESP32 #endif -WLED_GLOBAL bool correctWB _INIT(false); // CCT color correction of RGB color -WLED_GLOBAL bool cctFromRgb _INIT(false); // CCT is calculated from RGB instead of using seg.cct +#ifdef WLED_USE_IC_CCT +WLED_GLOBAL bool cctICused _INIT(true); // CCT IC used (Athom 15W bulbs) +#else +WLED_GLOBAL bool cctICused _INIT(false); // CCT IC used (Athom 15W bulbs) +#endif WLED_GLOBAL bool gammaCorrectCol _INIT(true); // use gamma correction on colors WLED_GLOBAL bool gammaCorrectBri _INIT(false); // use gamma correction on brightness WLED_GLOBAL float gammaCorrectVal _INIT(2.8f); // gamma correction value WLED_GLOBAL byte col[] _INIT_N(({ 255, 160, 0, 0 })); // current RGB(W) primary color. col[] should be updated if you want to change the color. WLED_GLOBAL byte colSec[] _INIT_N(({ 0, 0, 0, 0 })); // current RGB(W) secondary color -WLED_GLOBAL byte briS _INIT(128); // default brightness WLED_GLOBAL byte nightlightTargetBri _INIT(0); // brightness after nightlight is over WLED_GLOBAL byte nightlightDelayMins _INIT(60); @@ -384,30 +433,14 @@ WLED_GLOBAL byte irEnabled _INIT(IRTYPE); // Infrared receiver #endif WLED_GLOBAL bool irApplyToAllSelected _INIT(true); //apply IR or ESP-NOW to all selected segments -WLED_GLOBAL uint16_t udpPort _INIT(21324); // WLED notifier default port -WLED_GLOBAL uint16_t udpPort2 _INIT(65506); // WLED notifier supplemental port -WLED_GLOBAL uint16_t udpRgbPort _INIT(19446); // Hyperion port - -WLED_GLOBAL uint8_t syncGroups _INIT(0x01); // sync groups this instance syncs (bit mapped) -WLED_GLOBAL uint8_t receiveGroups _INIT(0x01); // sync receive groups this instance belongs to (bit mapped) -WLED_GLOBAL bool receiveNotificationBrightness _INIT(true); // apply brightness from incoming notifications -WLED_GLOBAL bool receiveNotificationColor _INIT(true); // apply color -WLED_GLOBAL bool receiveNotificationEffects _INIT(true); // apply effects setup -WLED_GLOBAL bool receiveSegmentOptions _INIT(false); // apply segment options -WLED_GLOBAL bool receiveSegmentBounds _INIT(false); // apply segment bounds (start, stop, offset) -WLED_GLOBAL bool notifyDirect _INIT(false); // send notification if change via UI or HTTP API -WLED_GLOBAL bool notifyButton _INIT(false); // send if updated by button or infrared remote -WLED_GLOBAL bool notifyAlexa _INIT(false); // send notification if updated via Alexa -WLED_GLOBAL bool notifyHue _INIT(true); // send notification if Hue light changes -WLED_GLOBAL uint8_t udpNumRetries _INIT(0); // Number of times a UDP sync message is retransmitted. Increase to increase reliability - +#ifndef WLED_DISABLE_ALEXA WLED_GLOBAL bool alexaEnabled _INIT(false); // enable device discovery by Amazon Echo WLED_GLOBAL char alexaInvocationName[33] _INIT("Light"); // speech control name of device. Choose something voice-to-text can understand WLED_GLOBAL byte alexaNumPresets _INIT(0); // number of presets to expose to Alexa, starting from preset 1, up to 9 +#endif WLED_GLOBAL uint16_t realtimeTimeoutMs _INIT(2500); // ms timeout of realtime mode before returning to normal mode WLED_GLOBAL int arlsOffset _INIT(0); // realtime LED offset -WLED_GLOBAL bool receiveDirect _INIT(true); // receive UDP/Hyperion realtime WLED_GLOBAL bool arlsDisableGammaCorrection _INIT(true); // activate if gamma correction is handled by the source WLED_GLOBAL bool arlsForceMaxBri _INIT(false); // enable to force max brightness if source has very dark colors that would be black @@ -449,10 +482,10 @@ WLED_GLOBAL unsigned long lastMqttReconnectAttempt _INIT(0); // used for other #endif WLED_GLOBAL AsyncMqttClient *mqtt _INIT(NULL); WLED_GLOBAL bool mqttEnabled _INIT(false); -WLED_GLOBAL char mqttStatusTopic[40] _INIT(""); // this must be global because of async handlers -WLED_GLOBAL char mqttDeviceTopic[MQTT_MAX_TOPIC_LEN+1] _INIT(""); // main MQTT topic (individual per device, default is wled/mac) -WLED_GLOBAL char mqttGroupTopic[MQTT_MAX_TOPIC_LEN+1] _INIT("wled/all"); // second MQTT topic (for example to group devices) -WLED_GLOBAL char mqttServer[MQTT_MAX_SERVER_LEN+1] _INIT(""); // both domains and IPs should work (no SSL) +WLED_GLOBAL char mqttStatusTopic[MQTT_MAX_TOPIC_LEN + 8] _INIT(""); // this must be global because of async handlers +WLED_GLOBAL char mqttDeviceTopic[MQTT_MAX_TOPIC_LEN + 1] _INIT(""); // main MQTT topic (individual per device, default is wled/mac) +WLED_GLOBAL char mqttGroupTopic[MQTT_MAX_TOPIC_LEN + 1] _INIT("wled/all"); // second MQTT topic (for example to group devices) +WLED_GLOBAL char mqttServer[MQTT_MAX_SERVER_LEN + 1] _INIT(""); // both domains and IPs should work (no SSL) WLED_GLOBAL char mqttUser[41] _INIT(""); // optional: username for MQTT auth WLED_GLOBAL char mqttPass[65] _INIT(""); // optional: password for MQTT auth WLED_GLOBAL char mqttClientID[41] _INIT(""); // override the client ID @@ -475,6 +508,8 @@ WLED_GLOBAL bool hueApplyColor _INIT(true); #endif WLED_GLOBAL uint16_t serialBaud _INIT(1152); // serial baud rate, multiply by 100 +WLED_GLOBAL bool serialCanRX _INIT(false); +WLED_GLOBAL bool serialCanTX _INIT(false); #ifndef WLED_DISABLE_ESPNOW WLED_GLOBAL bool enableESPNow _INIT(false); // global on/off for ESP-NOW @@ -547,7 +582,6 @@ WLED_GLOBAL bool transitionActive _INIT(false); WLED_GLOBAL uint16_t transitionDelay _INIT(750); // global transition duration WLED_GLOBAL uint16_t transitionDelayDefault _INIT(750); // default transition time (stored in cfg.json) WLED_GLOBAL unsigned long transitionStartTime; -WLED_GLOBAL float tperLast _INIT(0.0f); // crossfade transition progress, 0.0f - 1.0f WLED_GLOBAL bool jsonTransitionOnce _INIT(false); // flag to override transitionDelay (playlist, JSON API: "live" & "seg":{"i"} & "tt") WLED_GLOBAL uint8_t randomPaletteChangeTime _INIT(5); // amount of time [s] between random palette changes (min: 1s, max: 255s) WLED_GLOBAL bool useHarmonicRandomPalette _INIT(true); // use *harmonic* random palette generation (nicer looking) or truly random @@ -565,6 +599,7 @@ WLED_GLOBAL byte colNlT[] _INIT_N(({ 0, 0, 0, 0 })); // current nightligh // brightness WLED_GLOBAL unsigned long lastOnTime _INIT(0); WLED_GLOBAL bool offMode _INIT(!turnOnAtBoot); +WLED_GLOBAL byte briS _INIT(128); // default brightness WLED_GLOBAL byte bri _INIT(briS); // global brightness (set) WLED_GLOBAL byte briOld _INIT(0); // global brightness while in transition loop (previous iteration) WLED_GLOBAL byte briT _INIT(0); // global brightness during transition @@ -588,6 +623,81 @@ WLED_GLOBAL bool sendNotificationsRT _INIT(false); // master notifica WLED_GLOBAL unsigned long notificationSentTime _INIT(0); WLED_GLOBAL byte notificationSentCallMode _INIT(CALL_MODE_INIT); WLED_GLOBAL uint8_t notificationCount _INIT(0); +WLED_GLOBAL uint8_t syncGroups _INIT(0x01); // sync send groups this instance syncs to (bit mapped) +WLED_GLOBAL uint8_t receiveGroups _INIT(0x01); // sync receive groups this instance belongs to (bit mapped) +#ifdef WLED_SAVE_RAM +// this will save us 8 bytes of RAM while increasing code by ~400 bytes +typedef class Receive { + public: + union { + uint8_t Options; + struct { + bool Brightness : 1; + bool Color : 1; + bool Effects : 1; + bool SegmentOptions : 1; + bool SegmentBounds : 1; + bool Direct : 1; + bool Palette : 1; + uint8_t reserved : 1; + }; + }; + Receive(int i) { Options = i; } + Receive(bool b, bool c, bool e, bool sO, bool sB, bool p) + : Brightness(b) + , Color(c) + , Effects(e) + , SegmentOptions(sO) + , SegmentBounds(sB) + , Palette(p) + {}; +} __attribute__ ((aligned(1), packed)) receive_notification_t; +typedef class Send { + public: + union { + uint8_t Options; + struct { + bool Direct : 1; + bool Button : 1; + bool Alexa : 1; + bool Hue : 1; + uint8_t reserved : 4; + }; + }; + Send(int o) { Options = o; } + Send(bool d, bool b, bool a, bool h) { + Direct = d; + Button = b; + Alexa = a; + Hue = h; + } +} __attribute__ ((aligned(1), packed)) send_notification_t; +WLED_GLOBAL receive_notification_t receiveN _INIT(0b01100111); +WLED_GLOBAL send_notification_t notifyG _INIT(0b00001111); +#define receiveNotificationBrightness receiveN.Brightness +#define receiveNotificationColor receiveN.Color +#define receiveNotificationEffects receiveN.Effects +#define receiveNotificationPalette receiveN.Palette +#define receiveSegmentOptions receiveN.SegmentOptions +#define receiveSegmentBounds receiveN.SegmentBounds +#define receiveDirect receiveN.Direct +#define notifyDirect notifyG.Direct +#define notifyButton notifyG.Button +#define notifyAlexa notifyG.Alexa +#define notifyHue notifyG.Hue +#else +WLED_GLOBAL bool receiveNotificationBrightness _INIT(true); // apply brightness from incoming notifications +WLED_GLOBAL bool receiveNotificationColor _INIT(true); // apply color +WLED_GLOBAL bool receiveNotificationEffects _INIT(true); // apply effects setup +WLED_GLOBAL bool receiveNotificationPalette _INIT(true); // apply palette +WLED_GLOBAL bool receiveSegmentOptions _INIT(false); // apply segment options +WLED_GLOBAL bool receiveSegmentBounds _INIT(false); // apply segment bounds (start, stop, offset) +WLED_GLOBAL bool receiveDirect _INIT(true); // receive UDP/Hyperion realtime +WLED_GLOBAL bool notifyDirect _INIT(false); // send notification if change via UI or HTTP API +WLED_GLOBAL bool notifyButton _INIT(false); // send if updated by button or infrared remote +WLED_GLOBAL bool notifyAlexa _INIT(false); // send notification if updated via Alexa +WLED_GLOBAL bool notifyHue _INIT(true); // send notification if Hue light changes +#endif // effects WLED_GLOBAL byte effectCurrent _INIT(0); @@ -597,7 +707,46 @@ WLED_GLOBAL byte effectPalette _INIT(0); WLED_GLOBAL bool stateChanged _INIT(false); // network -WLED_GLOBAL bool udpConnected _INIT(false), udp2Connected _INIT(false), udpRgbConnected _INIT(false); +#ifdef WLED_SAVE_RAM +// this will save us 2 bytes of RAM while increasing code by ~400 bytes +typedef class Udp { + public: + uint16_t Port; + uint16_t Port2; + uint16_t RgbPort; + struct { + uint8_t NumRetries : 5; + bool Connected : 1; + bool Connected2 : 1; + bool RgbConnected : 1; + }; + Udp(int p1, int p2, int p3, int r, bool c1, bool c2, bool c3) { + Port = p1; + Port2 = p2; + RgbPort = p3; + NumRetries = r; + Connected = c1; + Connected2 = c2; + RgbConnected = c3; + } +} __attribute__ ((aligned(1), packed)) udp_port_t; +WLED_GLOBAL udp_port_t udp _INIT_N(({21234, 65506, 19446, 0, false, false, false})); +#define udpPort udp.Port +#define udpPort2 udp.Port2 +#define udpRgbPort udp.RgbPort +#define udpNumRetries udp.NumRetries +#define udpConnected udp.Connected +#define udp2Connected udp.Connected2 +#define udpRgbConnected udp.RgbConnected +#else +WLED_GLOBAL uint16_t udpPort _INIT(21324); // WLED notifier default port +WLED_GLOBAL uint16_t udpPort2 _INIT(65506); // WLED notifier supplemental port +WLED_GLOBAL uint16_t udpRgbPort _INIT(19446); // Hyperion port +WLED_GLOBAL uint8_t udpNumRetries _INIT(0); // Number of times a UDP sync message is retransmitted. Increase to increase reliability +WLED_GLOBAL bool udpConnected _INIT(false); +WLED_GLOBAL bool udp2Connected _INIT(false); +WLED_GLOBAL bool udpRgbConnected _INIT(false); +#endif // ui style WLED_GLOBAL bool showWelcomePage _INIT(false); @@ -630,6 +779,7 @@ WLED_GLOBAL byte timerWeekday[] _INIT_N(({ 255, 255, 255, 255, 255, 255, 255, WLED_GLOBAL byte timerMonth[] _INIT_N(({28,28,28,28,28,28,28,28})); WLED_GLOBAL byte timerDay[] _INIT_N(({1,1,1,1,1,1,1,1})); WLED_GLOBAL byte timerDayEnd[] _INIT_N(({31,31,31,31,31,31,31,31})); +WLED_GLOBAL bool doAdvancePlaylist _INIT(false); //improv WLED_GLOBAL byte improvActive _INIT(0); //0: no improv packet received, 1: improv active, 2: provisioning @@ -686,15 +836,10 @@ WLED_GLOBAL time_t sunrise _INIT(0); WLED_GLOBAL time_t sunset _INIT(0); WLED_GLOBAL Toki toki _INIT(Toki()); -// Temp buffer -WLED_GLOBAL char* obuf; -WLED_GLOBAL uint16_t olen _INIT(0); - // General filesystem WLED_GLOBAL size_t fsBytesUsed _INIT(0); WLED_GLOBAL size_t fsBytesTotal _INIT(0); WLED_GLOBAL unsigned long presetsModifiedTime _INIT(0L); -WLED_GLOBAL JsonDocument* fileDoc; WLED_GLOBAL bool doCloseFile _INIT(false); // presets @@ -707,7 +852,8 @@ WLED_GLOBAL byte optionType; WLED_GLOBAL bool doSerializeConfig _INIT(false); // flag to initiate saving of config WLED_GLOBAL bool doReboot _INIT(false); // flag to initiate reboot from async handlers -WLED_GLOBAL bool doPublishMqtt _INIT(false); + +WLED_GLOBAL bool psramSafe _INIT(true); // is it safe to use PSRAM (on ESP32 rev.1; compiler fix used "-mfix-esp32-psram-cache-issue") // status led #if defined(STATUSLED) @@ -739,6 +885,7 @@ WLED_GLOBAL WS2812FX strip _INIT(WS2812FX()); WLED_GLOBAL BusConfig* busConfigs[WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES] _INIT({nullptr}); //temporary, to remember values from network callback until after WLED_GLOBAL bool doInitBusses _INIT(false); WLED_GLOBAL int8_t loadLedmap _INIT(-1); +WLED_GLOBAL uint8_t currentLedmap _INIT(0); #ifndef ESP8266 WLED_GLOBAL char *ledmapNames[WLED_MAX_LEDMAPS-1] _INIT_N(({nullptr})); #endif @@ -748,9 +895,6 @@ WLED_GLOBAL uint32_t ledMaps _INIT(0); // bitfield representation of available l WLED_GLOBAL uint16_t ledMaps _INIT(0); // bitfield representation of available ledmaps #endif -// Usermod manager -WLED_GLOBAL UsermodManager usermods _INIT(UsermodManager()); - // global I2C SDA pin (used for usermods) #ifndef I2CSDAPIN WLED_GLOBAL int8_t i2c_sda _INIT(-1); @@ -784,8 +928,9 @@ WLED_GLOBAL int8_t spi_sclk _INIT(SPISCLKPIN); #endif // global ArduinoJson buffer -#if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) +#if defined(ARDUINO_ARCH_ESP32) WLED_GLOBAL JsonDocument *pDoc _INIT(nullptr); +WLED_GLOBAL SemaphoreHandle_t jsonBufferLockMutex _INIT(xSemaphoreCreateRecursiveMutex()); #else WLED_GLOBAL StaticJsonDocument gDoc; WLED_GLOBAL JsonDocument *pDoc _INIT(&gDoc); diff --git a/wled00/wled_eeprom.cpp b/wled00/wled_eeprom.cpp old mode 100755 new mode 100644 index 4f2c14d47..8582b49df --- a/wled00/wled_eeprom.cpp +++ b/wled00/wled_eeprom.cpp @@ -2,6 +2,10 @@ #include #include "wled.h" +#if defined(WLED_ENABLE_MQTT) && MQTT_MAX_TOPIC_LEN < 32 +#error "MQTT topics length < 32 is not supported by the EEPROM module!" +#endif + /* * DEPRECATED, do not use for new settings * Only used to restore config from pre-0.11 installations using the deEEP() methods diff --git a/wled00/wled00.ino b/wled00/wled_main.cpp similarity index 76% rename from wled00/wled00.ino rename to wled00/wled_main.cpp index 866543ab9..f3f090715 100644 --- a/wled00/wled00.ino +++ b/wled00/wled_main.cpp @@ -1,9 +1,12 @@ +#include /* * WLED Arduino IDE compatibility file. + * (this is the former wled00.ino) * * Where has everything gone? * - * In April 2020, the project's structure underwent a major change. + * In April 2020, the project's structure underwent a major change. + * We now use the platformIO build system, and building WLED in Arduino IDE is not supported any more. * Global variables are now found in file "wled.h" * Global function declarations are found in "fcn_declare.h" * diff --git a/wled00/wled_math.cpp b/wled00/wled_math.cpp index a4c9fc123..a8ec55400 100644 --- a/wled00/wled_math.cpp +++ b/wled00/wled_math.cpp @@ -10,16 +10,25 @@ //#define WLED_DEBUG_MATH +// Note: cos_t, sin_t and tan_t are very accurate but slow +// the math.h functions use several kB of flash and are to be avoided if possible +// sin16_t / cos16_t are faster and much more accurate than the fastled variants +// sin_approx and cos_approx are float wrappers for sin16_t/cos16_t and have an accuracy better than +/-0.0015 compared to sinf() +// sin8_t / cos8_t are fastled replacements and use sin16_t / cos16_t. Slightly slower than fastled version but very accurate + + +// Taylor series approximations, replaced with Bhaskara I's approximation +/* #define modd(x, y) ((x) - (int)((x) / (y)) * (y)) float cos_t(float phi) { - float x = modd(phi, TWO_PI); + float x = modd(phi, M_TWOPI); if (x < 0) x = -1 * x; int8_t sign = 1; - if (x > PI) + if (x > M_PI) { - x -= PI; + x -= M_PI; sign = -1; } float xx = x * x; @@ -31,8 +40,8 @@ float cos_t(float phi) return res; } -float sin_t(float x) { - float res = cos_t(HALF_PI - x); +float sin_t(float phi) { + float res = cos_t(M_PI_2 - phi); #ifdef WLED_DEBUG_MATH Serial.printf("sin: %f,%f,%f,(%f)\n",x,res,sin(x),res-sin(x)); #endif @@ -48,6 +57,80 @@ float tan_t(float x) { #endif return res; } +*/ + +// 16-bit, integer based Bhaskara I's sine approximation: 16*x*(pi - x) / (5*pi^2 - 4*x*(pi - x)) +// input is 16bit unsigned (0-65535), output is 16bit signed (-32767 to +32767) +// optimized integer implementation by @dedehai +int16_t sin16_t(uint16_t theta) { + int scale = 1; + if (theta > 0x7FFF) { + theta = 0xFFFF - theta; + scale = -1; // second half of the sine function is negative (pi - 2*pi) + } + uint32_t precal = theta * (0x7FFF - theta); + uint64_t numerator = (uint64_t)precal * (4 * 0x7FFF); // 64bit required + int32_t denominator = 1342095361 - precal; // 1342095361 is 5 * 0x7FFF^2 / 4 + int16_t result = numerator / denominator; + return result * scale; +} + +int16_t cos16_t(uint16_t theta) { + return sin16_t(theta + 0x4000); //cos(x) = sin(x+pi/2) +} + +uint8_t sin8_t(uint8_t theta) { + int32_t sin16 = sin16_t((uint16_t)theta * 257); // 255 * 257 = 0xFFFF + sin16 += 0x7FFF + 128; //shift result to range 0-0xFFFF, +128 for rounding + return min(sin16, int32_t(0xFFFF)) >> 8; // min performs saturation, and prevents overflow +} + +uint8_t cos8_t(uint8_t theta) { + return sin8_t(theta + 64); //cos(x) = sin(x+pi/2) +} + +float sin_approx(float theta) { + uint16_t scaled_theta = (int)(theta * (float)(0xFFFF / M_TWOPI)); // note: do not cast negative float to uint! cast to int first (undefined on C3) + int32_t result = sin16_t(scaled_theta); + float sin = float(result) / 0x7FFF; + return sin; +} + +float cos_approx(float theta) { + uint16_t scaled_theta = (int)(theta * (float)(0xFFFF / M_TWOPI)); // note: do not cast negative float to uint! cast to int first (undefined on C3) + int32_t result = sin16_t(scaled_theta + 0x4000); + float cos = float(result) / 0x7FFF; + return cos; +} + +float tan_approx(float x) { + float c = cos_approx(x); + if (c==0.0f) return 0; + float res = sin_approx(x) / c; + return res; +} + +#define ATAN2_CONST_A 0.1963f +#define ATAN2_CONST_B 0.9817f + +// atan2_t approximation, with the idea from https://gist.github.com/volkansalma/2972237?permalink_comment_id=3872525#gistcomment-3872525 +float atan2_t(float y, float x) { + float abs_y = fabs(y); + float abs_x = fabs(x); + float r = (abs_x - abs_y) / (abs_y + abs_x + 1e-10f); // avoid division by zero by adding a small nubmer + float angle; + if(x < 0) { + r = -r; + angle = M_PI_2 + M_PI_4; + } + else + angle = M_PI_2 - M_PI_4; + + float add = (ATAN2_CONST_A * (r * r) - ATAN2_CONST_B) * r; + angle += add; + angle = y < 0 ? -angle : angle; + return angle; +} //https://stackoverflow.com/questions/3380628 // Absolute error <= 6.7e-5 @@ -60,10 +143,10 @@ float acos_t(float x) { ret = ret * xabs; ret = ret - 0.2121144f; ret = ret * xabs; - ret = ret + HALF_PI; + ret = ret + M_PI_2; ret = ret * sqrt(1.0f-xabs); ret = ret - 2 * negate * ret; - float res = negate * PI + ret; + float res = negate * M_PI + ret; #ifdef WLED_DEBUG_MATH Serial.printf("acos: %f,%f,%f,(%f)\n",x,res,acos(x),res-acos(x)); #endif @@ -71,7 +154,7 @@ float acos_t(float x) { } float asin_t(float x) { - float res = HALF_PI - acos_t(x); + float res = M_PI_2 - acos_t(x); #ifdef WLED_DEBUG_MATH Serial.printf("asin: %f,%f,%f,(%f)\n",x,res,asin(x),res-asin(x)); #endif @@ -87,7 +170,7 @@ float atan_t(float x) { //For A/B/C, see https://stackoverflow.com/a/42542593 static const double A { 0.0776509570923569 }; static const double B { -0.287434475393028 }; - static const double C { ((HALF_PI/2) - A - B) }; + static const double C { ((M_PI_4) - A - B) }; // polynominal factors for approximation between 1 and 5 static const float C0 { 0.089494f }; static const float C1 { 0.974207f }; @@ -102,7 +185,7 @@ float atan_t(float x) { x = std::abs(x); float res; if (x > 5.0f) { // atan(x) converges to pi/2 - (1/x) for large values - res = HALF_PI - (1.0f/x); + res = M_PI_2 - (1.0f/x); } else if (x > 1.0f) { //1 < x < 5 float xx = x * x; res = (C4*xx*xx)+(C3*xx*x)+(C2*xx)+(C1*x)+C0; diff --git a/wled00/wled_serial.cpp b/wled00/wled_serial.cpp index 9cca09db0..ad9bb1413 100644 --- a/wled00/wled_serial.cpp +++ b/wled00/wled_serial.cpp @@ -24,11 +24,11 @@ bool continuousSendLED = false; uint32_t lastUpdate = 0; void updateBaudRate(uint32_t rate){ - uint16_t rate100 = rate/100; + unsigned rate100 = rate/100; if (rate100 == currentBaud || rate100 < 96) return; currentBaud = rate100; - if (!pinManager.isPinAllocated(hardwareTX) || pinManager.getPinOwner(hardwareTX) == PinOwner::DebugOut){ + if (serialCanTX){ Serial.print(F("Baud is now ")); Serial.println(rate); } @@ -38,10 +38,10 @@ void updateBaudRate(uint32_t rate){ // RGB LED data return as JSON array. Slow, but easy to use on the other end. void sendJSON(){ - if (!pinManager.isPinAllocated(hardwareTX) || pinManager.getPinOwner(hardwareTX) == PinOwner::DebugOut) { - uint16_t used = strip.getLengthTotal(); + if (serialCanTX) { + unsigned used = strip.getLengthTotal(); Serial.write('['); - for (uint16_t i=0; i RGB map Serial.write(qadd8(W(c), G(c))); //G @@ -69,10 +69,8 @@ void sendBytes(){ void handleSerial() { - if (pinManager.isPinAllocated(hardwareRX)) return; - if (!Serial) return; // arduino docs: `if (Serial)` indicates whether or not the USB CDC serial connection is open. For all non-USB CDC ports, this will always return true + if (!(serialCanRX && Serial)) return; // arduino docs: `if (Serial)` indicates whether or not the USB CDC serial connection is open. For all non-USB CDC ports, this will always return true - #ifdef WLED_ENABLE_ADALIGHT static auto state = AdaState::Header_A; static uint16_t count = 0; static uint16_t pixel = 0; @@ -86,54 +84,43 @@ void handleSerial() byte next = Serial.peek(); switch (state) { case AdaState::Header_A: - if (next == 'A') state = AdaState::Header_d; - else if (next == 0xC9) { //TPM2 start byte - state = AdaState::TPM2_Header_Type; - } - else if (next == 'I') { - handleImprovPacket(); - return; - } else if (next == 'v') { - Serial.print("WLED"); Serial.write(' '); Serial.println(VERSION); - - } else if (next == 0xB0) {updateBaudRate( 115200); - } else if (next == 0xB1) {updateBaudRate( 230400); - } else if (next == 0xB2) {updateBaudRate( 460800); - } else if (next == 0xB3) {updateBaudRate( 500000); - } else if (next == 0xB4) {updateBaudRate( 576000); - } else if (next == 0xB5) {updateBaudRate( 921600); - } else if (next == 0xB6) {updateBaudRate(1000000); - } else if (next == 0xB7) {updateBaudRate(1500000); - - } else if (next == 'l') {sendJSON(); // Send LED data as JSON Array - } else if (next == 'L') {sendBytes(); // Send LED data as TPM2 Data Packet - - } else if (next == 'o') {continuousSendLED = false; // Disable Continuous Serial Streaming - } else if (next == 'O') {continuousSendLED = true; // Enable Continuous Serial Streaming - - } else if (next == '{') { //JSON API + if (next == 'A') { state = AdaState::Header_d; } + else if (next == 0xC9) { state = AdaState::TPM2_Header_Type; } //TPM2 start byte + else if (next == 'I') { handleImprovPacket(); return; } + else if (next == 'v') { Serial.print("WLED"); Serial.write(' '); Serial.println(VERSION); } + else if (next == 0xB0) { updateBaudRate( 115200); } + else if (next == 0xB1) { updateBaudRate( 230400); } + else if (next == 0xB2) { updateBaudRate( 460800); } + else if (next == 0xB3) { updateBaudRate( 500000); } + else if (next == 0xB4) { updateBaudRate( 576000); } + else if (next == 0xB5) { updateBaudRate( 921600); } + else if (next == 0xB6) { updateBaudRate(1000000); } + else if (next == 0xB7) { updateBaudRate(1500000); } + else if (next == 'l') { sendJSON(); } // Send LED data as JSON Array + else if (next == 'L') { sendBytes(); } // Send LED data as TPM2 Data Packet + else if (next == 'o') { continuousSendLED = false; } // Disable Continuous Serial Streaming + else if (next == 'O') { continuousSendLED = true; } // Enable Continuous Serial Streaming + else if (next == '{') { //JSON API bool verboseResponse = false; if (!requestJSONBufferLock(16)) { - Serial.println(F("{\"error\":3}")); // ERR_NOBUF + Serial.printf_P(PSTR("{\"error\":%d}\n"), ERR_NOBUF); return; } Serial.setTimeout(100); DeserializationError error = deserializeJson(*pDoc, Serial); - if (error) { - releaseJSONBufferLock(); - return; - } - verboseResponse = deserializeState(pDoc->as()); - //only send response if TX pin is unused for other purposes - if (verboseResponse && (!pinManager.isPinAllocated(hardwareTX) || pinManager.getPinOwner(hardwareTX) == PinOwner::DebugOut)) { - pDoc->clear(); - JsonObject state = pDoc->createNestedObject("state"); - serializeState(state); - JsonObject info = pDoc->createNestedObject("info"); - serializeInfo(info); + if (!error) { + verboseResponse = deserializeState(pDoc->as()); + //only send response if TX pin is unused for other purposes + if (verboseResponse && serialCanTX) { + pDoc->clear(); + JsonObject state = pDoc->createNestedObject("state"); + serializeState(state); + JsonObject info = pDoc->createNestedObject("info"); + serializeInfo(info); - serializeJson(*pDoc, Serial); - Serial.println(); + serializeJson(*pDoc, Serial); + Serial.println(); + } } releaseJSONBufferLock(); } @@ -199,11 +186,10 @@ void handleSerial() // All other received bytes will disable Continuous Serial Streaming if (continuousSendLED && next != 'O'){ continuousSendLED = false; - } + } Serial.read(); //discard the byte } - #endif // If Continuous Serial Streaming is enabled, send new LED data as bytes if (continuousSendLED && (lastUpdate != strip.getLastShow())){ diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 4ce113cb9..e8cbb41ae 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -18,36 +18,7 @@ static const char s_unlock_ota [] PROGMEM = "Please unlock OTA in security setti static const char s_unlock_cfg [] PROGMEM = "Please unlock settings using PIN code!"; static const char s_notimplemented[] PROGMEM = "Not implemented"; static const char s_accessdenied[] PROGMEM = "Access Denied"; -static const char s_javascript[] PROGMEM = "application/javascript"; -static const char s_json[] = "application/json"; // AsyncJson-v6.h -static const char s_html[] PROGMEM = "text/html"; -static const char s_plain[] = "text/plain"; // Espalexa.h -static const char s_css[] PROGMEM = "text/css"; -static const char s_png[] PROGMEM = "image/png"; -static const char s_gif[] PROGMEM = "image/gif"; -static const char s_jpg[] PROGMEM = "image/jpeg"; -static const char s_ico[] PROGMEM = "image/x-icon"; -//static const char s_xml[] PROGMEM = "text/xml"; -//static const char s_pdf[] PROGMEM = "application/x-pdf"; -//static const char s_zip[] PROGMEM = "application/x-zip"; -//static const char s_gz[] PROGMEM = "application/x-gzip"; - -String getFileContentType(String &filename) { - if (filename.endsWith(F(".htm"))) return FPSTR(s_html); - else if (filename.endsWith(F(".html"))) return FPSTR(s_html); - else if (filename.endsWith(F(".css"))) return FPSTR(s_css); - else if (filename.endsWith(F(".js"))) return FPSTR(s_javascript); - else if (filename.endsWith(F(".json"))) return s_json; - else if (filename.endsWith(F(".png"))) return FPSTR(s_png); - else if (filename.endsWith(F(".gif"))) return FPSTR(s_gif); - else if (filename.endsWith(F(".jpg"))) return FPSTR(s_jpg); - else if (filename.endsWith(F(".ico"))) return FPSTR(s_ico); -// else if (filename.endsWith(F(".xml"))) return FPSTR(s_xml); -// else if (filename.endsWith(F(".pdf"))) return FPSTR(s_pdf); -// else if (filename.endsWith(F(".zip"))) return FPSTR(s_zip); -// else if (filename.endsWith(F(".gz"))) return FPSTR(s_gz); - return s_plain; -} +static const char _common_js[] PROGMEM = "/common.js"; //Is this an IP? static bool isIp(String str) { @@ -183,7 +154,7 @@ static String msgProcessor(const String& var) static void handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) { if (!correctPIN) { - if (final) request->send(401, FPSTR(s_plain), FPSTR(s_unlock_cfg)); + if (final) request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_unlock_cfg)); return; } if (!index) { @@ -193,8 +164,7 @@ static void handleUpload(AsyncWebServerRequest *request, const String& filename, } request->_tempFile = WLED_FS.open(finalname, "w"); - DEBUG_PRINT(F("Uploading ")); - DEBUG_PRINTLN(finalname); + DEBUG_PRINTF_P(PSTR("Uploading %s\n"), finalname.c_str()); if (finalname.equals(FPSTR(getPresetsFileName()))) presetsModifiedTime = toki.second(); } if (len) { @@ -204,10 +174,10 @@ static void handleUpload(AsyncWebServerRequest *request, const String& filename, request->_tempFile.close(); if (filename.indexOf(F("cfg.json")) >= 0) { // check for filename with or without slash doReboot = true; - request->send(200, FPSTR(s_plain), F("Configuration restore successful.\nRebooting...")); + request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("Configuration restore successful.\nRebooting...")); } else { if (filename.indexOf(F("palette")) >= 0 && filename.indexOf(F(".json")) >= 0) strip.loadCustomPalettes(); - request->send(200, FPSTR(s_plain), F("File Uploaded!")); + request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("File Uploaded!")); } cacheInvalidate++; } @@ -223,12 +193,12 @@ void createEditHandler(bool enable) { editHandler = &server.addHandler(new SPIFFSEditor("","",WLED_FS));//http_username,http_password)); #endif #else - editHandler = &server.on(SET_F("/edit"), HTTP_GET, [](AsyncWebServerRequest *request){ + editHandler = &server.on(F("/edit"), HTTP_GET, [](AsyncWebServerRequest *request){ serveMessage(request, 501, FPSTR(s_notimplemented), F("The FS editor is disabled in this build."), 254); }); #endif } else { - editHandler = &server.on(SET_F("/edit"), HTTP_ANY, [](AsyncWebServerRequest *request){ + editHandler = &server.on(F("/edit"), HTTP_ANY, [](AsyncWebServerRequest *request){ serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254); }); } @@ -259,24 +229,28 @@ void initServer() #ifdef WLED_ENABLE_WEBSOCKETS #ifndef WLED_DISABLE_2D - server.on(SET_F("/liveview2D"), HTTP_GET, [](AsyncWebServerRequest *request) { - handleStaticContent(request, "", 200, FPSTR(s_html), PAGE_liveviewws2D, PAGE_liveviewws2D_length); + server.on(F("/liveview2D"), HTTP_GET, [](AsyncWebServerRequest *request) { + handleStaticContent(request, "", 200, FPSTR(CONTENT_TYPE_HTML), PAGE_liveviewws2D, PAGE_liveviewws2D_length); }); #endif #endif - server.on(SET_F("/liveview"), HTTP_GET, [](AsyncWebServerRequest *request) { - handleStaticContent(request, "", 200, FPSTR(s_html), PAGE_liveview, PAGE_liveview_length); + server.on(F("/liveview"), HTTP_GET, [](AsyncWebServerRequest *request) { + handleStaticContent(request, "", 200, FPSTR(CONTENT_TYPE_HTML), PAGE_liveview, PAGE_liveview_length); + }); + + server.on(_common_js, HTTP_GET, [](AsyncWebServerRequest *request) { + handleStaticContent(request, FPSTR(_common_js), 200, FPSTR(CONTENT_TYPE_JAVASCRIPT), JS_common, JS_common_length); }); //settings page - server.on(SET_F("/settings"), HTTP_GET, [](AsyncWebServerRequest *request){ + server.on(F("/settings"), HTTP_GET, [](AsyncWebServerRequest *request){ serveSettings(request); }); // "/settings/settings.js&p=x" request also handled by serveSettings() static const char _style_css[] PROGMEM = "/style.css"; server.on(_style_css, HTTP_GET, [](AsyncWebServerRequest *request) { - handleStaticContent(request, FPSTR(_style_css), 200, FPSTR(s_css), PAGE_settingsCss, PAGE_settingsCss_length); + handleStaticContent(request, FPSTR(_style_css), 200, FPSTR(CONTENT_TYPE_CSS), PAGE_settingsCss, PAGE_settingsCss_length); }); static const char _favicon_ico[] PROGMEM = "/favicon.ico"; @@ -287,28 +261,29 @@ void initServer() static const char _skin_css[] PROGMEM = "/skin.css"; server.on(_skin_css, HTTP_GET, [](AsyncWebServerRequest *request) { if (handleFileRead(request, FPSTR(_skin_css))) return; - AsyncWebServerResponse *response = request->beginResponse(200, FPSTR(s_css)); + AsyncWebServerResponse *response = request->beginResponse(200, FPSTR(CONTENT_TYPE_CSS)); request->send(response); }); - server.on(SET_F("/welcome"), HTTP_GET, [](AsyncWebServerRequest *request){ + server.on(F("/welcome"), HTTP_GET, [](AsyncWebServerRequest *request){ serveSettings(request); }); - server.on(SET_F("/reset"), HTTP_GET, [](AsyncWebServerRequest *request){ + server.on(F("/reset"), HTTP_GET, [](AsyncWebServerRequest *request){ serveMessage(request, 200,F("Rebooting now..."),F("Please wait ~10 seconds..."),129); doReboot = true; }); - server.on(SET_F("/settings"), HTTP_POST, [](AsyncWebServerRequest *request){ + server.on(F("/settings"), HTTP_POST, [](AsyncWebServerRequest *request){ serveSettings(request, true); }); - server.on(SET_F("/json"), HTTP_GET, [](AsyncWebServerRequest *request){ + const static char _json[] PROGMEM = "/json"; + server.on(FPSTR(_json), HTTP_GET, [](AsyncWebServerRequest *request){ serveJson(request); }); - AsyncCallbackJsonWebHandler* handler = new AsyncCallbackJsonWebHandler(F("/json"), [](AsyncWebServerRequest *request) { + AsyncCallbackJsonWebHandler* handler = new AsyncCallbackJsonWebHandler(FPSTR(_json), [](AsyncWebServerRequest *request) { bool verboseResponse = false; bool isConfig = false; @@ -356,33 +331,33 @@ void initServer() doSerializeConfig = true; //serializeConfig(); //Save new settings to FS } } - request->send(200, s_json, F("{\"success\":true}")); + request->send(200, CONTENT_TYPE_JSON, F("{\"success\":true}")); }, JSON_BUFFER_SIZE); server.addHandler(handler); - server.on(SET_F("/version"), HTTP_GET, [](AsyncWebServerRequest *request){ - request->send(200, FPSTR(s_plain), (String)VERSION); + server.on(F("/version"), HTTP_GET, [](AsyncWebServerRequest *request){ + request->send(200, FPSTR(CONTENT_TYPE_PLAIN), (String)VERSION); }); - server.on(SET_F("/uptime"), HTTP_GET, [](AsyncWebServerRequest *request){ - request->send(200, FPSTR(s_plain), (String)millis()); + server.on(F("/uptime"), HTTP_GET, [](AsyncWebServerRequest *request){ + request->send(200, FPSTR(CONTENT_TYPE_PLAIN), (String)millis()); }); - server.on(SET_F("/freeheap"), HTTP_GET, [](AsyncWebServerRequest *request){ - request->send(200, FPSTR(s_plain), (String)ESP.getFreeHeap()); + server.on(F("/freeheap"), HTTP_GET, [](AsyncWebServerRequest *request){ + request->send(200, FPSTR(CONTENT_TYPE_PLAIN), (String)ESP.getFreeHeap()); }); #ifdef WLED_ENABLE_USERMOD_PAGE server.on("/u", HTTP_GET, [](AsyncWebServerRequest *request) { - handleStaticContent(request, "", 200, FPSTR(s_html), PAGE_usermod, PAGE_usermod_length); + handleStaticContent(request, "", 200, FPSTR(CONTENT_TYPE_HTML), PAGE_usermod, PAGE_usermod_length); }); #endif - server.on(SET_F("/teapot"), HTTP_GET, [](AsyncWebServerRequest *request){ + server.on(F("/teapot"), HTTP_GET, [](AsyncWebServerRequest *request){ serveMessage(request, 418, F("418. I'm a teapot."), F("(Tangible Embedded Advanced Project Of Twinkling)"), 254); }); - server.on(SET_F("/upload"), HTTP_POST, [](AsyncWebServerRequest *request) {}, + server.on(F("/upload"), HTTP_POST, [](AsyncWebServerRequest *request) {}, [](AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) {handleUpload(request, filename, index, data, len, final);} ); @@ -421,7 +396,7 @@ void initServer() #if WLED_WATCHDOG_TIMEOUT > 0 WLED::instance().disableWatchdog(); #endif - usermods.onUpdateBegin(true); // notify usermods that update is about to begin (some may require task de-init) + UsermodManager::onUpdateBegin(true); // notify usermods that update is about to begin (some may require task de-init) lastEditTime = millis(); // make sure PIN does not lock during update strip.suspend(); #ifdef ESP8266 @@ -437,7 +412,7 @@ void initServer() } else { DEBUG_PRINTLN(F("Update Failed")); strip.resume(); - usermods.onUpdateBegin(false); // notify usermods that update has failed (some may require task init) + UsermodManager::onUpdateBegin(false); // notify usermods that update has failed (some may require task init) #if WLED_WATCHDOG_TIMEOUT > 0 WLED::instance().enableWatchdog(); #endif @@ -452,11 +427,11 @@ void initServer() #ifdef WLED_ENABLE_DMX - server.on(SET_F("/dmxmap"), HTTP_GET, [](AsyncWebServerRequest *request){ - request->send_P(200, FPSTR(s_html), PAGE_dmxmap , dmxProcessor); + server.on(F("/dmxmap"), HTTP_GET, [](AsyncWebServerRequest *request){ + request->send_P(200, FPSTR(CONTENT_TYPE_HTML), PAGE_dmxmap , dmxProcessor); }); #else - server.on(SET_F("/dmxmap"), HTTP_GET, [](AsyncWebServerRequest *request){ + server.on(F("/dmxmap"), HTTP_GET, [](AsyncWebServerRequest *request){ serveMessage(request, 501, FPSTR(s_notimplemented), F("DMX support is not enabled in this build."), 254); }); #endif @@ -464,7 +439,7 @@ void initServer() server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { if (captivePortal(request)) return; if (!showWelcomePage || request->hasArg(F("sliders"))) { - handleStaticContent(request, F("/index.htm"), 200, FPSTR(s_html), PAGE_index, PAGE_index_L); + handleStaticContent(request, F("/index.htm"), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_index, PAGE_index_L); } else { serveSettings(request); } @@ -473,20 +448,20 @@ void initServer() #ifdef WLED_ENABLE_PIXART static const char _pixart_htm[] PROGMEM = "/pixart.htm"; server.on(_pixart_htm, HTTP_GET, [](AsyncWebServerRequest *request) { - handleStaticContent(request, FPSTR(_pixart_htm), 200, FPSTR(s_html), PAGE_pixart, PAGE_pixart_L); + handleStaticContent(request, FPSTR(_pixart_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pixart, PAGE_pixart_L); }); #endif #ifndef WLED_DISABLE_PXMAGIC static const char _pxmagic_htm[] PROGMEM = "/pxmagic.htm"; server.on(_pxmagic_htm, HTTP_GET, [](AsyncWebServerRequest *request) { - handleStaticContent(request, FPSTR(_pxmagic_htm), 200, FPSTR(s_html), PAGE_pxmagic, PAGE_pxmagic_L); + handleStaticContent(request, FPSTR(_pxmagic_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pxmagic, PAGE_pxmagic_L); }); #endif static const char _cpal_htm[] PROGMEM = "/cpal.htm"; server.on(_cpal_htm, HTTP_GET, [](AsyncWebServerRequest *request) { - handleStaticContent(request, FPSTR(_cpal_htm), 200, FPSTR(s_html), PAGE_cpal, PAGE_cpal_L); + handleStaticContent(request, FPSTR(_cpal_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_cpal, PAGE_cpal_L); }); #ifdef WLED_ENABLE_WEBSOCKETS @@ -495,7 +470,7 @@ void initServer() //called when the url is not defined here, ajax-in; get-settings server.onNotFound([](AsyncWebServerRequest *request){ - DEBUG_PRINT(F("Not-Found HTTP call: ")); DEBUG_PRINTLN(request->url()); + DEBUG_PRINTF_P(PSTR("Not-Found HTTP call: %s\n"), request->url().c_str()); if (captivePortal(request)) return; //make API CORS compatible @@ -511,7 +486,7 @@ void initServer() #ifndef WLED_DISABLE_ALEXA if(espalexa.handleAlexaApiCall(request)) return; #endif - handleStaticContent(request, request->url(), 404, FPSTR(s_html), PAGE_404, PAGE_404_length); + handleStaticContent(request, request->url(), 404, FPSTR(CONTENT_TYPE_HTML), PAGE_404, PAGE_404_length); }); } @@ -522,7 +497,7 @@ void serveMessage(AsyncWebServerRequest* request, uint16_t code, const String& h messageSub = subl; optionType = optionT; - request->send_P(code, FPSTR(s_html), PAGE_msg, msgProcessor); + request->send_P(code, FPSTR(CONTENT_TYPE_HTML), PAGE_msg, msgProcessor); } @@ -530,7 +505,7 @@ void serveJsonError(AsyncWebServerRequest* request, uint16_t code, uint16_t erro { AsyncJsonResponse *response = new AsyncJsonResponse(64); if (error < ERR_NOT_IMPL) response->addHeader(F("Retry-After"), F("1")); - response->setContentType(s_json); + response->setContentType(CONTENT_TYPE_JSON); response->setCode(code); JsonObject obj = response->getRoot(); obj[F("error")] = error; @@ -541,27 +516,27 @@ void serveJsonError(AsyncWebServerRequest* request, uint16_t code, uint16_t erro void serveSettingsJS(AsyncWebServerRequest* request) { - char buf[SETTINGS_STACK_BUF_SIZE+37]; - buf[0] = 0; + if (request->url().indexOf(FPSTR(_common_js)) > 0) { + handleStaticContent(request, FPSTR(_common_js), 200, FPSTR(CONTENT_TYPE_JAVASCRIPT), JS_common, JS_common_length); + return; + } byte subPage = request->arg(F("p")).toInt(); if (subPage > 10) { - strcpy_P(buf, PSTR("alert('Settings for this request are not implemented.');")); - request->send(501, FPSTR(s_javascript), buf); + request->send_P(501, FPSTR(CONTENT_TYPE_JAVASCRIPT), PSTR("alert('Settings for this request are not implemented.');")); return; } if (subPage > 0 && !correctPIN && strlen(settingsPIN)>0) { - strcpy_P(buf, PSTR("alert('PIN incorrect.');")); - request->send(401, FPSTR(s_javascript), buf); + request->send_P(401, FPSTR(CONTENT_TYPE_JAVASCRIPT), PSTR("alert('PIN incorrect.');")); return; } - strcat_P(buf,PSTR("function GetV(){var d=document;")); - getSettingsJS(subPage, buf+strlen(buf)); // this may overflow by 35bytes!!! - strcat_P(buf,PSTR("}")); - AsyncWebServerResponse *response; - response = request->beginResponse(200, FPSTR(s_javascript), buf); + AsyncResponseStream *response = request->beginResponseStream(FPSTR(CONTENT_TYPE_JAVASCRIPT)); response->addHeader(F("Cache-Control"), F("no-store")); response->addHeader(F("Expires"), F("0")); + + response->print(F("function GetV(){var d=document;")); + getSettingsJS(subPage, *response); + response->print(F("}")); request->send(response); } @@ -640,7 +615,7 @@ void serveSettings(AsyncWebServerRequest* request, bool post) { } int code = 200; - String contentType = FPSTR(s_html); + String contentType = FPSTR(CONTENT_TYPE_HTML); const uint8_t* content; size_t len; @@ -666,7 +641,7 @@ void serveSettings(AsyncWebServerRequest* request, bool post) { return; } case SUBPAGE_PINREQ : content = PAGE_settings_pin; len = PAGE_settings_pin_length; code = 401; break; - case SUBPAGE_CSS : content = PAGE_settingsCss; len = PAGE_settingsCss_length; contentType = FPSTR(s_css); break; + case SUBPAGE_CSS : content = PAGE_settingsCss; len = PAGE_settingsCss_length; contentType = FPSTR(CONTENT_TYPE_CSS); break; case SUBPAGE_JS : serveSettingsJS(request); return; case SUBPAGE_WELCOME : content = PAGE_welcome; len = PAGE_welcome_length; break; default: content = PAGE_settings; len = PAGE_settings_length; break; diff --git a/wled00/ws.cpp b/wled00/ws.cpp index 1dd141a68..45640b68c 100644 --- a/wled00/ws.cpp +++ b/wled00/ws.cpp @@ -55,7 +55,7 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp } else { verboseResponse = deserializeState(root); } - releaseJSONBufferLock(); // will clean fileDoc + releaseJSONBufferLock(); if (!interfaceUpdateCallMode) { // individual client response only needed if no WS broadcast soon if (verboseResponse) { @@ -96,19 +96,21 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp //pong message was received (in response to a ping request maybe) DEBUG_PRINTLN(F("WS pong.")); + } else { + DEBUG_PRINTLN(F("WS unknown event.")); } } void sendDataWs(AsyncWebSocketClient * client) { if (!ws.count()) return; - AsyncWebSocketMessageBuffer * buffer; if (!requestJSONBufferLock(12)) { + const char* error = PSTR("{\"error\":3}"); if (client) { - client->text(F("{\"error\":3}")); // ERR_NOBUF + client->text(FPSTR(error)); // ERR_NOBUF } else { - ws.textAll(F("{\"error\":3}")); // ERR_NOBUF + ws.textAll(FPSTR(error)); // ERR_NOBUF } return; } @@ -121,18 +123,19 @@ void sendDataWs(AsyncWebSocketClient * client) size_t len = measureJson(*pDoc); DEBUG_PRINTF_P(PSTR("JSON buffer size: %u for WS request (%u).\n"), pDoc->memoryUsage(), len); + // the following may no longer be necessary as heap management has been fixed by @willmmiles in AWS size_t heap1 = ESP.getFreeHeap(); - DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(ESP.getFreeHeap()); + DEBUG_PRINTF_P(PSTR("heap %u\n"), ESP.getFreeHeap()); #ifdef ESP8266 if (len>heap1) { DEBUG_PRINTLN(F("Out of memory (WS)!")); return; } #endif - buffer = ws.makeBuffer(len); // will not allocate correct memory sometimes on ESP8266 + AsyncWebSocketBuffer buffer(len); #ifdef ESP8266 size_t heap2 = ESP.getFreeHeap(); - DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(ESP.getFreeHeap()); + DEBUG_PRINTF_P(PSTR("heap %u\n"), ESP.getFreeHeap()); #else size_t heap2 = 0; // ESP32 variants do not have the same issue and will work without checking heap allocation #endif @@ -141,23 +144,18 @@ void sendDataWs(AsyncWebSocketClient * client) DEBUG_PRINTLN(F("WS buffer allocation failed.")); ws.closeAll(1013); //code 1013 = temporary overload, try again later ws.cleanupClients(0); //disconnect all clients to release memory - ws._cleanBuffers(); return; //out of memory } - - buffer->lock(); - serializeJson(*pDoc, (char *)buffer->get(), len); + serializeJson(*pDoc, (char *)buffer.data(), len); DEBUG_PRINT(F("Sending WS data ")); if (client) { - client->text(buffer); DEBUG_PRINTLN(F("to a single client.")); + client->text(std::move(buffer)); } else { - ws.textAll(buffer); DEBUG_PRINTLN(F("to multiple clients.")); + ws.textAll(std::move(buffer)); } - buffer->unlock(); - ws._cleanBuffers(); releaseJSONBufferLock(); } @@ -187,11 +185,10 @@ bool sendLiveLedsWs(uint32_t wsClient) #endif size_t bufSize = pos + (used/n)*3; - AsyncWebSocketMessageBuffer * wsBuf = ws.makeBuffer(bufSize); + AsyncWebSocketBuffer wsBuf(bufSize); if (!wsBuf) return false; //out of memory - uint8_t* buffer = wsBuf->get(); + uint8_t* buffer = reinterpret_cast(wsBuf.data()); if (!buffer) return false; //out of memory - wsBuf->lock(); // protect buffer from being cleaned by another WS instance buffer[0] = 'L'; buffer[1] = 1; //version @@ -213,14 +210,12 @@ bool sendLiveLedsWs(uint32_t wsClient) uint8_t g = G(c); uint8_t b = B(c); uint8_t w = W(c); - buffer[pos++] = scale8(qadd8(w, r), strip.getBrightness()); //R, add white channel to RGB channels as a simple RGBW -> RGB map - buffer[pos++] = scale8(qadd8(w, g), strip.getBrightness()); //G - buffer[pos++] = scale8(qadd8(w, b), strip.getBrightness()); //B + buffer[pos++] = bri ? qadd8(w, r) : 0; //R, add white channel to RGB channels as a simple RGBW -> RGB map + buffer[pos++] = bri ? qadd8(w, g) : 0; //G + buffer[pos++] = bri ? qadd8(w, b) : 0; //B } - wsc->binary(wsBuf); - wsBuf->unlock(); // un-protect buffer - ws._cleanBuffers(); + wsc->binary(std::move(wsBuf)); return true; } diff --git a/wled00/xml.cpp b/wled00/xml.cpp old mode 100755 new mode 100644 index 04e0ebfdf..2a19cdfab --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -6,85 +6,39 @@ */ //build XML response to HTTP /win API request -void XML_response(AsyncWebServerRequest *request, char* dest) +void XML_response(Print& dest) { - char sbuf[(dest == nullptr)?1024:1]; //allocate local buffer if none passed - obuf = (dest == nullptr)? sbuf:dest; - - olen = 0; - oappend(SET_F("")); - oappendi((nightlightActive && nightlightMode > NL_MODE_SET) ? briT : bri); - oappend(SET_F("")); - + dest.printf_P(PSTR("%d"), (nightlightActive && nightlightMode > NL_MODE_SET) ? briT : bri); for (int i = 0; i < 3; i++) { - oappend(""); - oappendi(col[i]); - oappend(""); + dest.printf_P(PSTR("%d"), col[i]); } for (int i = 0; i < 3; i++) { - oappend(""); - oappendi(colSec[i]); - oappend(""); + dest.printf_P(PSTR("%d"), colSec[i]); } - oappend(SET_F("")); - oappendi(notifyDirect); - oappend(SET_F("")); - oappendi(receiveGroups!=0); - oappend(SET_F("")); - oappendi(nightlightActive); - oappend(SET_F("")); - oappendi(nightlightMode > NL_MODE_SET); - oappend(SET_F("")); - oappendi(nightlightDelayMins); - oappend(SET_F("")); - oappendi(nightlightTargetBri); - oappend(SET_F("")); - oappendi(effectCurrent); - oappend(SET_F("")); - oappendi(effectSpeed); - oappend(SET_F("")); - oappendi(effectIntensity); - oappend(SET_F("")); - oappendi(effectPalette); - oappend(SET_F("")); - if (strip.hasWhiteChannel()) { - oappendi(col[3]); - } else { - oappend("-1"); - } - oappend(SET_F("")); - oappendi(colSec[3]); - oappend(SET_F("")); - oappendi(currentPreset); - oappend(SET_F("")); - oappendi(currentPlaylist >= 0); - oappend(SET_F("")); - oappend(serverDescription); - if (realtimeMode) - { - oappend(SET_F(" (live)")); - } - oappend(SET_F("")); - oappendi(strip.getFirstSelectedSegId()); - oappend(SET_F("")); - if (request != nullptr) request->send(200, "text/xml", obuf); + dest.printf_P(PSTR("%d%d%d%d%d%d%d%d%d%d%d%d%d%d%s%s%d"), + notifyDirect, receiveGroups!=0, nightlightActive, nightlightMode > NL_MODE_SET, nightlightDelayMins, + nightlightTargetBri, effectCurrent, effectSpeed, effectIntensity, effectPalette, + strip.hasWhiteChannel() ? col[3] : -1, colSec[3], currentPreset, currentPlaylist >= 0, + serverDescription, realtimeMode ? PSTR(" (live)") : "", + strip.getFirstSelectedSegId() + ); } -void extractPin(JsonObject &obj, const char *key) { +static void extractPin(Print& settingsScript, JsonObject &obj, const char *key) { if (obj[key].is()) { JsonArray pins = obj[key].as(); for (JsonVariant pv : pins) { - if (pv.as() > -1) { oappend(","); oappendi(pv.as()); } + if (pv.as() > -1) { settingsScript.print(","); settingsScript.print(pv.as()); } } } else { - if (obj[key].as() > -1) { oappend(","); oappendi(obj[key].as()); } + if (obj[key].as() > -1) { settingsScript.print(","); settingsScript.print(obj[key].as()); } } } -// oappend used pins by scanning JsonObject (1 level deep) -void fillUMPins(JsonObject &mods) +// print used pins by scanning JsonObject (1 level deep) +static void fillUMPins(Print& settingsScript, JsonObject &mods) { for (JsonPair kv : mods) { // kv.key() is usermod name or subobject key @@ -93,7 +47,7 @@ void fillUMPins(JsonObject &mods) if (!obj.isNull()) { // element is an JsonObject if (!obj["pin"].isNull()) { - extractPin(obj, "pin"); + extractPin(settingsScript, obj, "pin"); } else { // scan keys (just one level deep as is possible with usermods) for (JsonPair so : obj) { @@ -102,7 +56,7 @@ void fillUMPins(JsonObject &mods) // we found a key containing "pin" substring if (strlen(strstr(key, "pin")) == 3) { // and it is at the end, we found another pin - extractPin(obj, key); + extractPin(settingsScript, obj, key); continue; } } @@ -110,7 +64,7 @@ void fillUMPins(JsonObject &mods) JsonObject subObj = obj[so.key()]; if (!subObj["pin"].isNull()) { // get pins from subobject - extractPin(subObj, "pin"); + extractPin(settingsScript, subObj, "pin"); } } } @@ -118,192 +72,158 @@ void fillUMPins(JsonObject &mods) } } -void appendGPIOinfo() { - char nS[8]; - - oappend(SET_F("d.um_p=[-1")); // has to have 1 element +void appendGPIOinfo(Print& settingsScript) { + settingsScript.print(F("d.um_p=[-1")); // has to have 1 element if (i2c_sda > -1 && i2c_scl > -1) { - oappend(","); oappend(itoa(i2c_sda,nS,10)); - oappend(","); oappend(itoa(i2c_scl,nS,10)); + settingsScript.printf_P(PSTR(",%d,%d"), i2c_sda, i2c_scl); } if (spi_mosi > -1 && spi_sclk > -1) { - oappend(","); oappend(itoa(spi_mosi,nS,10)); - oappend(","); oappend(itoa(spi_sclk,nS,10)); + settingsScript.printf_P(PSTR(",%d,%d"), spi_mosi, spi_sclk); } // usermod pin reservations will become unnecessary when settings pages will read cfg.json directly if (requestJSONBufferLock(6)) { // if we can't allocate JSON buffer ignore usermod pins - JsonObject mods = pDoc->createNestedObject(F("um")); - usermods.addToConfig(mods); - if (!mods.isNull()) fillUMPins(mods); + JsonObject mods = pDoc->createNestedObject("um"); + UsermodManager::addToConfig(mods); + if (!mods.isNull()) fillUMPins(settingsScript, mods); releaseJSONBufferLock(); } - oappend(SET_F("];")); - - // add reserved and usermod pins as d.um_p array - #if defined(CONFIG_IDF_TARGET_ESP32S2) - oappend(SET_F("d.rsvd=[22,23,24,25,26,27,28,29,30,31,32")); - #elif defined(CONFIG_IDF_TARGET_ESP32S3) - oappend(SET_F("d.rsvd=[19,20,22,23,24,25,26,27,28,29,30,31,32")); // includes 19+20 for USB OTG (JTAG) - #elif defined(CONFIG_IDF_TARGET_ESP32C3) - oappend(SET_F("d.rsvd=[11,12,13,14,15,16,17")); - #elif defined(ESP32) - oappend(SET_F("d.rsvd=[6,7,8,9,10,11,24,28,29,30,31,37,38")); - #else - oappend(SET_F("d.rsvd=[6,7,8,9,10,11")); - #endif + settingsScript.print(F("];")); + // add reserved (unusable) pins + bool firstPin = true; + settingsScript.print(F("d.rsvd=[")); + for (unsigned i = 0; i < WLED_NUM_PINS; i++) { + if (!PinManager::isPinOk(i, false)) { // include readonly pins + if (!firstPin) settingsScript.print(','); + settingsScript.print(i); + firstPin = false; + } + } #ifdef WLED_ENABLE_DMX - oappend(SET_F(",2")); // DMX hardcoded pin + if (!firstPin) settingsScript.print(','); + settingsScript.print(2); // DMX hardcoded pin + firstPin = false; #endif - #if defined(WLED_DEBUG) && !defined(WLED_DEBUG_HOST) - oappend(SET_F(",")); oappend(itoa(hardwareTX,nS,10)); // debug output (TX) pin + if (!firstPin) settingsScript.print(','); + settingsScript.print(hardwareTX); // debug output (TX) pin + firstPin = false; #endif - - //Note: Using pin 3 (RX) disables Adalight / Serial JSON - - #if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32C3) - if (psramFound()) oappend(SET_F(",16,17")); // GPIO16 & GPIO17 reserved for SPI RAM on ESP32 (not on S2, S3 or C3) - #elif defined(CONFIG_IDF_TARGET_ESP32S3) - if (psramFound()) oappend(SET_F(",33,34,35,36,37")); // in use for "octal" PSRAM or "octal" FLASH -seems that octal PSRAM is very common on S3. - #endif - #endif - #ifdef WLED_USE_ETHERNET if (ethernetType != WLED_ETH_NONE && ethernetType < WLED_NUM_ETH_TYPES) { - for (uint8_t p=0; p=0) { oappend(","); oappend(itoa(ethernetBoards[ethernetType].eth_power,nS,10)); } - if (ethernetBoards[ethernetType].eth_mdc>=0) { oappend(","); oappend(itoa(ethernetBoards[ethernetType].eth_mdc,nS,10)); } - if (ethernetBoards[ethernetType].eth_mdio>=0) { oappend(","); oappend(itoa(ethernetBoards[ethernetType].eth_mdio,nS,10)); } - switch (ethernetBoards[ethernetType].eth_clk_mode) { + if (!firstPin) settingsScript.print(','); + for (unsigned p=0; p= 0) { settingsScript.printf("%d,",ethernetBoards[ethernetType].eth_power); } + if (ethernetBoards[ethernetType].eth_mdc >= 0) { settingsScript.printf("%d,",ethernetBoards[ethernetType].eth_mdc); } + if (ethernetBoards[ethernetType].eth_mdio >= 0) { settingsScript.printf("%d,",ethernetBoards[ethernetType].eth_mdio); } + switch (ethernetBoards[ethernetType].eth_clk_mode) { case ETH_CLOCK_GPIO0_IN: case ETH_CLOCK_GPIO0_OUT: - oappend(SET_F(",0")); + settingsScript.print(0); break; case ETH_CLOCK_GPIO16_OUT: - oappend(SET_F(",16")); + settingsScript.print(16); break; case ETH_CLOCK_GPIO17_OUT: - oappend(SET_F(",17")); + settingsScript.print(17); break; } } #endif - - oappend(SET_F("];")); + settingsScript.print(F("];")); // rsvd // add info for read-only GPIO - oappend(SET_F("d.ro_gpio=[")); - #if defined(CONFIG_IDF_TARGET_ESP32S2) - oappendi(46); - #elif defined(CONFIG_IDF_TARGET_ESP32S3) - // none for S3 - #elif defined(CONFIG_IDF_TARGET_ESP32C3) - // none for C3 - #elif defined(ESP32) - oappend(SET_F("34,35,36,37,38,39")); - #else - // none for ESP8266 - #endif - oappend(SET_F("];")); + settingsScript.print(F("d.ro_gpio=[")); + firstPin = true; + for (unsigned i = 0; i < WLED_NUM_PINS; i++) { + if (PinManager::isReadOnlyPin(i)) { + // No comma before the first pin + if (!firstPin) settingsScript.print(','); + settingsScript.print(i); + firstPin = false; + } + } + settingsScript.print(F("];")); // add info about max. # of pins - oappend(SET_F("d.max_gpio=")); - #if defined(CONFIG_IDF_TARGET_ESP32S2) - oappendi(46); - #elif defined(CONFIG_IDF_TARGET_ESP32S3) - oappendi(48); - #elif defined(CONFIG_IDF_TARGET_ESP32C3) - oappendi(21); - #elif defined(ESP32) - oappendi(39); - #else - oappendi(16); - #endif - oappend(SET_F(";")); + settingsScript.printf_P(PSTR("d.max_gpio=%d;"),WLED_NUM_PINS); } //get values for settings form in javascript -void getSettingsJS(byte subPage, char* dest) +void getSettingsJS(byte subPage, Print& settingsScript) { //0: menu 1: wifi 2: leds 3: ui 4: sync 5: time 6: sec - DEBUG_PRINT(F("settings resp")); - DEBUG_PRINTLN(subPage); - obuf = dest; - olen = 0; + DEBUG_PRINTF_P(PSTR("settings resp %u\n"), (unsigned)subPage); if (subPage <0 || subPage >10) return; + char nS[32]; if (subPage == SUBPAGE_MENU) { #ifdef WLED_DISABLE_2D // include only if 2D is not compiled in - oappend(PSTR("gId('2dbtn').style.display='none';")); + settingsScript.print(F("gId('2dbtn').style.display='none';")); #endif #ifdef WLED_ENABLE_DMX // include only if DMX is enabled - oappend(PSTR("gId('dmxbtn').style.display='';")); + settingsScript.print(F("gId('dmxbtn').style.display='';")); #endif } if (subPage == SUBPAGE_WIFI) { - char nS[10]; size_t l; - oappend(SET_F("resetWiFi(")); - oappend(itoa(WLED_MAX_WIFI_COUNT,nS,10)); - oappend(SET_F(");")); + settingsScript.printf_P(PSTR("resetWiFi(%d);"), WLED_MAX_WIFI_COUNT); for (size_t n = 0; n < multiWiFi.size(); n++) { l = strlen(multiWiFi[n].clientPass); char fpass[l+1]; //fill password field with *** fpass[l] = 0; memset(fpass,'*',l); - oappend(SET_F("addWiFi(\"")); - oappend(multiWiFi[n].clientSSID); - oappend(SET_F("\",\"")); - oappend(fpass); - oappend(SET_F("\",0x")); - oappend(itoa(multiWiFi[n].staticIP,nS,16)); - oappend(SET_F(",0x")); - oappend(itoa(multiWiFi[n].staticGW,nS,16)); - oappend(SET_F(",0x")); - oappend(itoa(multiWiFi[n].staticSN,nS,16)); - oappend(SET_F(");")); + settingsScript.printf_P(PSTR("addWiFi(\"%s\",\"%s\",0x%X,0x%X,0x%X);"), + multiWiFi[n].clientSSID, + fpass, + (uint32_t) multiWiFi[n].staticIP, // explicit cast required as this is a struct + (uint32_t) multiWiFi[n].staticGW, + (uint32_t) multiWiFi[n].staticSN); } - sappend('v',SET_F("D0"),dnsAddress[0]); - sappend('v',SET_F("D1"),dnsAddress[1]); - sappend('v',SET_F("D2"),dnsAddress[2]); - sappend('v',SET_F("D3"),dnsAddress[3]); + printSetFormValue(settingsScript,PSTR("D0"),dnsAddress[0]); + printSetFormValue(settingsScript,PSTR("D1"),dnsAddress[1]); + printSetFormValue(settingsScript,PSTR("D2"),dnsAddress[2]); + printSetFormValue(settingsScript,PSTR("D3"),dnsAddress[3]); - sappends('s',SET_F("CM"),cmDNS); - sappend('i',SET_F("AB"),apBehavior); - sappends('s',SET_F("AS"),apSSID); - sappend('c',SET_F("AH"),apHide); + printSetFormValue(settingsScript,PSTR("CM"),cmDNS); + printSetFormIndex(settingsScript,PSTR("AB"),apBehavior); + printSetFormValue(settingsScript,PSTR("AS"),apSSID); + printSetFormCheckbox(settingsScript,PSTR("AH"),apHide); l = strlen(apPass); char fapass[l+1]; //fill password field with *** fapass[l] = 0; memset(fapass,'*',l); - sappends('s',SET_F("AP"),fapass); + printSetFormValue(settingsScript,PSTR("AP"),fapass); - sappend('v',SET_F("AC"),apChannel); - sappend('c',SET_F("FG"),force802_3g); - sappend('c',SET_F("WS"),noWifiSleep); + printSetFormValue(settingsScript,PSTR("AC"),apChannel); + #ifdef ARDUINO_ARCH_ESP32 + printSetFormValue(settingsScript,PSTR("TX"),txPower); + #else + settingsScript.print(F("gId('tx').style.display='none';")); + #endif + printSetFormCheckbox(settingsScript,PSTR("FG"),force802_3g); + printSetFormCheckbox(settingsScript,PSTR("WS"),noWifiSleep); #ifndef WLED_DISABLE_ESPNOW - sappend('c',SET_F("RE"),enableESPNow); - sappends('s',SET_F("RMAC"),linked_remote); + printSetFormCheckbox(settingsScript,PSTR("RE"),enableESPNow); + printSetFormValue(settingsScript,PSTR("RMAC"),linked_remote); #else //hide remote settings if not compiled - oappend(SET_F("toggle('ESPNOW');")); // hide ESP-NOW setting + settingsScript.print(F("toggle('ESPNOW');")); // hide ESP-NOW setting #endif #ifdef WLED_USE_ETHERNET - sappend('v',SET_F("ETH"),ethernetType); + printSetFormValue(settingsScript,PSTR("ETH"),ethernetType); #else //hide ethernet setting if not compiled in - oappend(SET_F("document.getElementById('ethd').style.display='none';")); + settingsScript.print(F("gId('ethd').style.display='none';")); #endif if (Network.isConnected()) //is connected @@ -313,12 +233,12 @@ void getSettingsJS(byte subPage, char* dest) sprintf(s, "%d.%d.%d.%d", localIP[0], localIP[1], localIP[2], localIP[3]); #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) - if (Network.isEthernet()) strcat_P(s ,SET_F(" (Ethernet)")); + if (Network.isEthernet()) strcat_P(s ,PSTR(" (Ethernet)")); #endif - sappends('m',SET_F("(\"sip\")[0]"),s); + printSetClassElementHTML(settingsScript,PSTR("sip"),0,s); } else { - sappends('m',SET_F("(\"sip\")[0]"),(char*)F("Not connected")); + printSetClassElementHTML(settingsScript,PSTR("sip"),0,(char*)F("Not connected")); } if (WiFi.softAPIP()[0] != 0) //is active @@ -326,81 +246,86 @@ void getSettingsJS(byte subPage, char* dest) char s[16]; IPAddress apIP = WiFi.softAPIP(); sprintf(s, "%d.%d.%d.%d", apIP[0], apIP[1], apIP[2], apIP[3]); - sappends('m',SET_F("(\"sip\")[1]"),s); + printSetClassElementHTML(settingsScript,PSTR("sip"),1,s); } else { - sappends('m',SET_F("(\"sip\")[1]"),(char*)F("Not active")); + printSetClassElementHTML(settingsScript,PSTR("sip"),1,(char*)F("Not active")); } #ifndef WLED_DISABLE_ESPNOW if (strlen(last_signal_src) > 0) { //Have seen an ESP-NOW Remote - sappends('m',SET_F("(\"rlid\")[0]"),last_signal_src); + printSetClassElementHTML(settingsScript,PSTR("rlid"),0,last_signal_src); } else if (!enableESPNow) { - sappends('m',SET_F("(\"rlid\")[0]"),(char*)F("(Enable ESP-NOW to listen)")); + printSetClassElementHTML(settingsScript,PSTR("rlid"),0,(char*)F("(Enable ESP-NOW to listen)")); } else { - sappends('m',SET_F("(\"rlid\")[0]"),(char*)F("None")); + printSetClassElementHTML(settingsScript,PSTR("rlid"),0,(char*)F("None")); } #endif } if (subPage == SUBPAGE_LEDS) { - char nS[32]; + appendGPIOinfo(settingsScript); - appendGPIOinfo(); + settingsScript.printf_P(PSTR("d.ledTypes=%s;"), BusManager::getLEDTypesJSONString().c_str()); // set limits - oappend(SET_F("bLimits(")); - oappend(itoa(WLED_MAX_BUSSES,nS,10)); oappend(","); - oappend(itoa(WLED_MIN_VIRTUAL_BUSSES,nS,10)); oappend(","); - oappend(itoa(MAX_LEDS_PER_BUS,nS,10)); oappend(","); - oappend(itoa(MAX_LED_MEMORY,nS,10)); oappend(","); - oappend(itoa(MAX_LEDS,nS,10)); - oappend(SET_F(");")); + settingsScript.printf_P(PSTR("bLimits(%d,%d,%d,%d,%d,%d,%d,%d);"), + WLED_MAX_BUSSES, + WLED_MIN_VIRTUAL_BUSSES, + MAX_LEDS_PER_BUS, + MAX_LED_MEMORY, + MAX_LEDS, + WLED_MAX_COLOR_ORDER_MAPPINGS, + WLED_MAX_DIGITAL_CHANNELS, + WLED_MAX_ANALOG_CHANNELS + ); - sappend('c',SET_F("MS"),autoSegments); - sappend('c',SET_F("CCT"),correctWB); - sappend('c',SET_F("CR"),cctFromRgb); - sappend('v',SET_F("CB"),strip.cctBlending); - sappend('v',SET_F("FR"),strip.getTargetFps()); - sappend('v',SET_F("AW"),Bus::getGlobalAWMode()); - sappend('c',SET_F("LD"),useGlobalLedBuffer); + printSetFormCheckbox(settingsScript,PSTR("MS"),strip.autoSegments); + printSetFormCheckbox(settingsScript,PSTR("CCT"),strip.correctWB); + printSetFormCheckbox(settingsScript,PSTR("IC"),cctICused); + printSetFormCheckbox(settingsScript,PSTR("CR"),strip.cctFromRgb); + printSetFormValue(settingsScript,PSTR("CB"),strip.cctBlending); + printSetFormValue(settingsScript,PSTR("FR"),strip.getTargetFps()); + printSetFormValue(settingsScript,PSTR("AW"),Bus::getGlobalAWMode()); + printSetFormCheckbox(settingsScript,PSTR("LD"),useGlobalLedBuffer); - uint16_t sumMa = 0; - for (uint8_t s=0; s < BusManager::getNumBusses(); s++) { + unsigned sumMa = 0; + for (int s = 0; s < BusManager::getNumBusses(); s++) { Bus* bus = BusManager::getBus(s); if (bus == nullptr) continue; - char lp[4] = "L0"; lp[2] = 48+s; lp[3] = 0; //ascii 0-9 //strip data pin - char lc[4] = "LC"; lc[2] = 48+s; lc[3] = 0; //strip length - char co[4] = "CO"; co[2] = 48+s; co[3] = 0; //strip color order - char lt[4] = "LT"; lt[2] = 48+s; lt[3] = 0; //strip type - char ls[4] = "LS"; ls[2] = 48+s; ls[3] = 0; //strip start LED - char cv[4] = "CV"; cv[2] = 48+s; cv[3] = 0; //strip reverse - char sl[4] = "SL"; sl[2] = 48+s; sl[3] = 0; //skip 1st LED - char rf[4] = "RF"; rf[2] = 48+s; rf[3] = 0; //off refresh - char aw[4] = "AW"; aw[2] = 48+s; aw[3] = 0; //auto white mode - char wo[4] = "WO"; wo[2] = 48+s; wo[3] = 0; //swap channels - char sp[4] = "SP"; sp[2] = 48+s; sp[3] = 0; //bus clock speed - char la[4] = "LA"; la[2] = 48+s; la[3] = 0; //LED current - char ma[4] = "MA"; ma[2] = 48+s; ma[3] = 0; //max per-port PSU current - oappend(SET_F("addLEDs(1);")); + int offset = s < 10 ? 48 : 55; + char lp[4] = "L0"; lp[2] = offset+s; lp[3] = 0; //ascii 0-9 //strip data pin + char lc[4] = "LC"; lc[2] = offset+s; lc[3] = 0; //strip length + char co[4] = "CO"; co[2] = offset+s; co[3] = 0; //strip color order + char lt[4] = "LT"; lt[2] = offset+s; lt[3] = 0; //strip type + char ls[4] = "LS"; ls[2] = offset+s; ls[3] = 0; //strip start LED + char cv[4] = "CV"; cv[2] = offset+s; cv[3] = 0; //strip reverse + char sl[4] = "SL"; sl[2] = offset+s; sl[3] = 0; //skip 1st LED + char rf[4] = "RF"; rf[2] = offset+s; rf[3] = 0; //off refresh + char aw[4] = "AW"; aw[2] = offset+s; aw[3] = 0; //auto white mode + char wo[4] = "WO"; wo[2] = offset+s; wo[3] = 0; //swap channels + char sp[4] = "SP"; sp[2] = offset+s; sp[3] = 0; //bus clock speed + char la[4] = "LA"; la[2] = offset+s; la[3] = 0; //LED current + char ma[4] = "MA"; ma[2] = offset+s; ma[3] = 0; //max per-port PSU current + settingsScript.print(F("addLEDs(1);")); uint8_t pins[5]; - uint8_t nPins = bus->getPins(pins); - for (uint8_t i = 0; i < nPins; i++) { - lp[1] = 48+i; - if (pinManager.isPinOk(pins[i]) || IS_VIRTUAL(bus->getType())) sappend('v',lp,pins[i]); + int nPins = bus->getPins(pins); + for (int i = 0; i < nPins; i++) { + lp[1] = offset+i; + if (PinManager::isPinOk(pins[i]) || bus->isVirtual()) printSetFormValue(settingsScript,lp,pins[i]); } - sappend('v',lc,bus->getLength()); - sappend('v',lt,bus->getType()); - sappend('v',co,bus->getColorOrder() & 0x0F); - sappend('v',ls,bus->getStart()); - sappend('c',cv,bus->isReversed()); - sappend('v',sl,bus->skippedLeds()); - sappend('c',rf,bus->isOffRefreshRequired()); - sappend('v',aw,bus->getAutoWhiteMode()); - sappend('v',wo,bus->getColorOrder() >> 4); - uint16_t speed = bus->getFrequency(); - if (IS_PWM(bus->getType())) { + printSetFormValue(settingsScript,lc,bus->getLength()); + printSetFormValue(settingsScript,lt,bus->getType()); + printSetFormValue(settingsScript,co,bus->getColorOrder() & 0x0F); + printSetFormValue(settingsScript,ls,bus->getStart()); + printSetFormCheckbox(settingsScript,cv,bus->isReversed()); + printSetFormValue(settingsScript,sl,bus->skippedLeds()); + printSetFormCheckbox(settingsScript,rf,bus->isOffRefreshRequired()); + printSetFormValue(settingsScript,aw,bus->getAutoWhiteMode()); + printSetFormValue(settingsScript,wo,bus->getColorOrder() >> 4); + unsigned speed = bus->getFrequency(); + if (bus->isPWM()) { switch (speed) { case WLED_PWM_FREQ/2 : speed = 0; break; case WLED_PWM_FREQ*2/3 : speed = 1; break; @@ -409,7 +334,7 @@ void getSettingsJS(byte subPage, char* dest) case WLED_PWM_FREQ*2 : speed = 3; break; case WLED_PWM_FREQ*10/3 : speed = 4; break; // uint16_t max (19531 * 3.333) } - } else if (IS_DIGITAL(bus->getType()) && IS_2PIN(bus->getType())) { + } else if (bus->is2Pin()) { switch (speed) { case 1000 : speed = 0; break; case 2000 : speed = 1; break; @@ -419,153 +344,147 @@ void getSettingsJS(byte subPage, char* dest) case 20000 : speed = 4; break; } } - sappend('v',sp,speed); - sappend('v',la,bus->getLEDCurrent()); - sappend('v',ma,bus->getMaxCurrent()); + printSetFormValue(settingsScript,sp,speed); + printSetFormValue(settingsScript,la,bus->getLEDCurrent()); + printSetFormValue(settingsScript,ma,bus->getMaxCurrent()); sumMa += bus->getMaxCurrent(); } - sappend('v',SET_F("MA"),BusManager::ablMilliampsMax() ? BusManager::ablMilliampsMax() : sumMa); - sappend('c',SET_F("ABL"),BusManager::ablMilliampsMax() || sumMa > 0); - sappend('c',SET_F("PPL"),!BusManager::ablMilliampsMax() && sumMa > 0); + printSetFormValue(settingsScript,PSTR("MA"),BusManager::ablMilliampsMax() ? BusManager::ablMilliampsMax() : sumMa); + printSetFormCheckbox(settingsScript,PSTR("ABL"),BusManager::ablMilliampsMax() || sumMa > 0); + printSetFormCheckbox(settingsScript,PSTR("PPL"),!BusManager::ablMilliampsMax() && sumMa > 0); - oappend(SET_F("resetCOM(")); - oappend(itoa(WLED_MAX_COLOR_ORDER_MAPPINGS,nS,10)); - oappend(SET_F(");")); + settingsScript.printf_P(PSTR("resetCOM(%d);"), WLED_MAX_COLOR_ORDER_MAPPINGS); const ColorOrderMap& com = BusManager::getColorOrderMap(); - for (uint8_t s=0; s < com.count(); s++) { + for (int s = 0; s < com.count(); s++) { const ColorOrderMapEntry* entry = com.get(s); if (entry == nullptr) break; - oappend(SET_F("addCOM(")); - oappend(itoa(entry->start,nS,10)); oappend(","); - oappend(itoa(entry->len,nS,10)); oappend(","); - oappend(itoa(entry->colorOrder,nS,10)); oappend(");"); + settingsScript.printf_P(PSTR("addCOM(%d,%d,%d);"), entry->start, entry->len, entry->colorOrder); } - sappend('v',SET_F("CA"),briS); + printSetFormValue(settingsScript,PSTR("CA"),briS); - sappend('c',SET_F("BO"),turnOnAtBoot); - sappend('v',SET_F("BP"),bootPreset); + printSetFormCheckbox(settingsScript,PSTR("BO"),turnOnAtBoot); + printSetFormValue(settingsScript,PSTR("BP"),bootPreset); - sappend('c',SET_F("GB"),gammaCorrectBri); - sappend('c',SET_F("GC"),gammaCorrectCol); - dtostrf(gammaCorrectVal,3,1,nS); sappends('s',SET_F("GV"),nS); - sappend('c',SET_F("TF"),fadeTransition); - sappend('c',SET_F("EB"),modeBlending); - sappend('v',SET_F("TD"),transitionDelayDefault); - sappend('c',SET_F("PF"),strip.paletteFade); - sappend('v',SET_F("TP"),randomPaletteChangeTime); - sappend('c',SET_F("TH"),useHarmonicRandomPalette); - sappend('v',SET_F("BF"),briMultiplier); - sappend('v',SET_F("TB"),nightlightTargetBri); - sappend('v',SET_F("TL"),nightlightDelayMinsDefault); - sappend('v',SET_F("TW"),nightlightMode); - sappend('i',SET_F("PB"),strip.paletteBlend); - sappend('v',SET_F("RL"),rlyPin); - sappend('c',SET_F("RM"),rlyMde); - for (uint8_t i=0; i> 4) & 0x0F); - k[0] = 'P'; sappend('v',k,timerMonth[i] & 0x0F); - k[0] = 'D'; sappend('v',k,timerDay[i]); - k[0] = 'E'; sappend('v',k,timerDayEnd[i]); + k[0] = 'M'; printSetFormValue(settingsScript,k,(timerMonth[i] >> 4) & 0x0F); + k[0] = 'P'; printSetFormValue(settingsScript,k,timerMonth[i] & 0x0F); + k[0] = 'D'; printSetFormValue(settingsScript,k,timerDay[i]); + k[0] = 'E'; printSetFormValue(settingsScript,k,timerDayEnd[i]); } } } @@ -657,119 +574,110 @@ void getSettingsJS(byte subPage, char* dest) char fpass[l+1]; //fill PIN field with 0000 fpass[l] = 0; memset(fpass,'0',l); - sappends('s',SET_F("PIN"),fpass); - sappend('c',SET_F("NO"),otaLock); - sappend('c',SET_F("OW"),wifiLock); - sappend('c',SET_F("AO"),aOtaEnabled); - sappends('m',SET_F("(\"sip\")[0]"),(char*)F("WLED ")); - olen -= 2; //delete "; - oappend(versionString); - oappend(SET_F(" (build ")); - oappendi(VERSION); - oappend(SET_F(")\";")); - oappend(SET_F("sd=\"")); - oappend(serverDescription); - oappend(SET_F("\";")); + printSetFormValue(settingsScript,PSTR("PIN"),fpass); + printSetFormCheckbox(settingsScript,PSTR("NO"),otaLock); + printSetFormCheckbox(settingsScript,PSTR("OW"),wifiLock); + printSetFormCheckbox(settingsScript,PSTR("AO"),aOtaEnabled); + char tmp_buf[128]; + snprintf_P(tmp_buf,sizeof(tmp_buf),PSTR("WLED %s (build %d)"),versionString,VERSION); + printSetClassElementHTML(settingsScript,PSTR("sip"),0,tmp_buf); + settingsScript.printf_P(PSTR("sd=\"%s\";"), serverDescription); } #ifdef WLED_ENABLE_DMX // include only if DMX is enabled if (subPage == SUBPAGE_DMX) { - sappend('v',SET_F("PU"),e131ProxyUniverse); + printSetFormValue(settingsScript,PSTR("PU"),e131ProxyUniverse); - sappend('v',SET_F("CN"),DMXChannels); - sappend('v',SET_F("CG"),DMXGap); - sappend('v',SET_F("CS"),DMXStart); - sappend('v',SET_F("SL"),DMXStartLED); + printSetFormValue(settingsScript,PSTR("CN"),DMXChannels); + printSetFormValue(settingsScript,PSTR("CG"),DMXGap); + printSetFormValue(settingsScript,PSTR("CS"),DMXStart); + printSetFormValue(settingsScript,PSTR("SL"),DMXStartLED); - sappend('i',SET_F("CH1"),DMXFixtureMap[0]); - sappend('i',SET_F("CH2"),DMXFixtureMap[1]); - sappend('i',SET_F("CH3"),DMXFixtureMap[2]); - sappend('i',SET_F("CH4"),DMXFixtureMap[3]); - sappend('i',SET_F("CH5"),DMXFixtureMap[4]); - sappend('i',SET_F("CH6"),DMXFixtureMap[5]); - sappend('i',SET_F("CH7"),DMXFixtureMap[6]); - sappend('i',SET_F("CH8"),DMXFixtureMap[7]); - sappend('i',SET_F("CH9"),DMXFixtureMap[8]); - sappend('i',SET_F("CH10"),DMXFixtureMap[9]); - sappend('i',SET_F("CH11"),DMXFixtureMap[10]); - sappend('i',SET_F("CH12"),DMXFixtureMap[11]); - sappend('i',SET_F("CH13"),DMXFixtureMap[12]); - sappend('i',SET_F("CH14"),DMXFixtureMap[13]); - sappend('i',SET_F("CH15"),DMXFixtureMap[14]); + printSetFormIndex(settingsScript,PSTR("CH1"),DMXFixtureMap[0]); + printSetFormIndex(settingsScript,PSTR("CH2"),DMXFixtureMap[1]); + printSetFormIndex(settingsScript,PSTR("CH3"),DMXFixtureMap[2]); + printSetFormIndex(settingsScript,PSTR("CH4"),DMXFixtureMap[3]); + printSetFormIndex(settingsScript,PSTR("CH5"),DMXFixtureMap[4]); + printSetFormIndex(settingsScript,PSTR("CH6"),DMXFixtureMap[5]); + printSetFormIndex(settingsScript,PSTR("CH7"),DMXFixtureMap[6]); + printSetFormIndex(settingsScript,PSTR("CH8"),DMXFixtureMap[7]); + printSetFormIndex(settingsScript,PSTR("CH9"),DMXFixtureMap[8]); + printSetFormIndex(settingsScript,PSTR("CH10"),DMXFixtureMap[9]); + printSetFormIndex(settingsScript,PSTR("CH11"),DMXFixtureMap[10]); + printSetFormIndex(settingsScript,PSTR("CH12"),DMXFixtureMap[11]); + printSetFormIndex(settingsScript,PSTR("CH13"),DMXFixtureMap[12]); + printSetFormIndex(settingsScript,PSTR("CH14"),DMXFixtureMap[13]); + printSetFormIndex(settingsScript,PSTR("CH15"),DMXFixtureMap[14]); } #endif if (subPage == SUBPAGE_UM) //usermods { - appendGPIOinfo(); - oappend(SET_F("numM=")); - oappendi(usermods.getModCount()); - oappend(";"); - sappend('v',SET_F("SDA"),i2c_sda); - sappend('v',SET_F("SCL"),i2c_scl); - sappend('v',SET_F("MOSI"),spi_mosi); - sappend('v',SET_F("MISO"),spi_miso); - sappend('v',SET_F("SCLK"),spi_sclk); - oappend(SET_F("addInfo('SDA','")); oappendi(HW_PIN_SDA); oappend(SET_F("');")); - oappend(SET_F("addInfo('SCL','")); oappendi(HW_PIN_SCL); oappend(SET_F("');")); - oappend(SET_F("addInfo('MOSI','")); oappendi(HW_PIN_DATASPI); oappend(SET_F("');")); - oappend(SET_F("addInfo('MISO','")); oappendi(HW_PIN_MISOSPI); oappend(SET_F("');")); - oappend(SET_F("addInfo('SCLK','")); oappendi(HW_PIN_CLOCKSPI); oappend(SET_F("');")); - usermods.appendConfigData(); + appendGPIOinfo(settingsScript); + settingsScript.printf_P(PSTR("numM=%d;"), UsermodManager::getModCount()); + printSetFormValue(settingsScript,PSTR("SDA"),i2c_sda); + printSetFormValue(settingsScript,PSTR("SCL"),i2c_scl); + printSetFormValue(settingsScript,PSTR("MOSI"),spi_mosi); + printSetFormValue(settingsScript,PSTR("MISO"),spi_miso); + printSetFormValue(settingsScript,PSTR("SCLK"),spi_sclk); + settingsScript.printf_P(PSTR("addInfo('SDA','%d');" + "addInfo('SCL','%d');" + "addInfo('MOSI','%d');" + "addInfo('MISO','%d');" + "addInfo('SCLK','%d');"), + HW_PIN_SDA, HW_PIN_SCL, HW_PIN_DATASPI, HW_PIN_MISOSPI, HW_PIN_CLOCKSPI + ); + UsermodManager::appendConfigData(settingsScript); } if (subPage == SUBPAGE_UPDATE) // update { - sappends('m',SET_F("(\"sip\")[0]"),(char*)F("WLED ")); - olen -= 2; //delete "; - oappend(versionString); - oappend(SET_F("
(")); + char tmp_buf[128]; + snprintf_P(tmp_buf,sizeof(tmp_buf),PSTR("WLED %s
%s
(%s build %d)"), + versionString, + releaseString, #if defined(ARDUINO_ARCH_ESP32) - oappend(ESP.getChipModel()); + ESP.getChipModel(), #else - oappend("esp8266"); + "esp8266", #endif - oappend(SET_F(" build ")); - oappendi(VERSION); - oappend(SET_F(")\";")); + VERSION); + + printSetClassElementHTML(settingsScript,PSTR("sip"),0,tmp_buf); } if (subPage == SUBPAGE_2D) // 2D matrices { - sappend('v',SET_F("SOMP"),strip.isMatrix); + printSetFormValue(settingsScript,PSTR("SOMP"),strip.isMatrix); #ifndef WLED_DISABLE_2D - oappend(SET_F("maxPanels=")); oappendi(WLED_MAX_PANELS); oappend(SET_F(";")); - oappend(SET_F("resetPanels();")); + settingsScript.printf_P(PSTR("maxPanels=%d;resetPanels();"),WLED_MAX_PANELS); if (strip.isMatrix) { if(strip.panels>0){ - sappend('v',SET_F("PW"),strip.panel[0].width); //Set generator Width and Height to first panel size for convenience - sappend('v',SET_F("PH"),strip.panel[0].height); + printSetFormValue(settingsScript,PSTR("PW"),strip.panel[0].width); //Set generator Width and Height to first panel size for convenience + printSetFormValue(settingsScript,PSTR("PH"),strip.panel[0].height); } - sappend('v',SET_F("MPC"),strip.panels); + printSetFormValue(settingsScript,PSTR("MPC"),strip.panels); // panels - for (uint8_t i=0; i
${isM?'Start X':'Start LED'}